Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

17 changed files with 156 additions and 626 deletions

6
.gitignore vendored
View File

@ -1,6 +0,0 @@
node_modules/
dist/
test-results/
playwright-report/
.DS_Store
*.log

1
.npmrc
View File

@ -1 +0,0 @@
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}

View File

@ -1,51 +1,58 @@
# @apigo.cc/loader API 手册 # @web/loader 模块调度中心
全自动依赖调度器。通过动态注入脚本标签,实现对 Apigo 全线组件的一键加载、版本管理及环境自动适配 `@web/loader` 是整个 `@web` 体系的入口调度中心。它负责根据当前环境自动生成 `importmap`,并管理各模块的版本
--- ---
## 1. 引入方式 (UMD 优先) ## 一、 集成方式
您可以通过 URL 参数或 **Hash (#)** 直接定义要加载的模块。 ### 方式一:内联引导 (推荐 - 无额外请求)
在 HTML 的 `<head>` 顶部加入以下代码。这种方式不需要额外请求 `loader.min.js` 文件。
### A. 极简带版本模式 (防缓存)
```html ```html
<!-- 使用 ?v= 指定全局版本,所有依赖包都会自动带上该版本后缀 --> <script>
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/loader@1.0.5/dist/loader.min.js?v=1.0.12#state,base,datatable"></script> (function __load(tpl, ...pkgs) {
const DEFAULT_VERSIONS = { state: 'v1.0.11', bootstrap: 'v1.0.1', base: 'v1.0.7' };
const importMap = { imports: {} };
pkgs.forEach(pkg => {
const [name, version] = pkg.split(':');
const v = version || DEFAULT_VERSIONS[name] || 'main';
importMap.imports[`@web/${name}`] = tpl.replace(/{project}/g, name).replace(/{tag}/g, v);
});
const s = document.createElement('script');
s.type = 'importmap';
s.textContent = JSON.stringify(importMap);
document.currentScript.parentNode.insertBefore(s, document.currentScript);
})('https://apigo.cc/web/{project}/raw/tag/{tag}/dist/{project}.js', 'state', 'bootstrap', 'base');
</script>
``` ```
### B. 简写 Hash 模式 ### 方式二:引用外部 Loader
直接引用打包好的 `loader.min.js`。它会自动根据当前的 URL 模板进行调度。
```html ```html
<!-- 直接在 # 后面列出模块名,多个模块用逗号分隔 --> <script src="https://apigo.cc/web/loader/raw/tag/v1.0.0/dist/loader.min.js"></script>
<script src="/libs/loader.js#state,bootstrap,base"></script> <script>
// 无需版本号,自动使用内置的默认版本
Loader.load('state', 'bootstrap', 'base');
</script>
``` ```
--- ---
## 2. 核心特性 ## 二、 核心特性
### 🚀 智能定位与版本传播 (Smart Propagation) 1. **智能模板 (Smart Templating)**:自动根据 `loader.js` 的加载路径推断其他组件的下载路径。
- **同域跟随**: 如果 `loader.js` 加载自 `/static/js/`,则所有子包默认也从 `/static/js/` 获取。 2. **默认版本 (Default Versioning)**:构建时内置了 `@web` 家族各成员的推荐稳定版本,解决“版本选择困难症”。
- **版本锁步**: 只要 Loader URL 带有 `?v=X.Y.Z`,加载的所有依赖包(如 `base.js`)都会自动变为 `base.js?v=X.Y.Z` 3. **按需加载 (On-demand)**:在页面头部一次性声明所需模块,避免加载无关资源。
- **压缩同步**: 只要引入的是 `loader.min.js`,其加载的所有依赖也会自动请求 `.min.js` 版本。
### 🔗 自动依赖分析
Loader 内部维护了完整的依赖链。例如:
- 请求 `datatable` -> 自动按序注入 `state` -> `bootstrap` -> `base` -> `datatable`
--- ---
## 3. API 接口:`__apigo_load()` ## 三、 支持模块清单
* `state` (原子状态机)
脚本引入后,全局会挂载该函数。**注意:该函数在运行一次后会自动自毁(从全局删除)以保持环境纯净。** * `bootstrap` (UI 引擎)
* `base` (常用控件库)
### `__apigo_load(...pkgs)` * `dataTable` (高性能表格)
- **`__apigo_load('base')`**: 加载默认稳定版。 * `kanban` (看板)
- **`__apigo_load('state:1.0.11')`**: 指定特定包的版本。 * `mindmap` (思维导图)
* `chart` (图表)
---
## 开发者提示 (AI 必读)
1. **参数优先级**: URL 查询参数 `?load=` 优先级高于 Hash 参数 `#`
2. **同步执行**: Loader 利用浏览器对动态插入脚本的顺序解析机制,保障了依赖项在业务代码运行前就绪。
3. **自清理**: `__apigo_load` 仅在 `<script type="module">` 运行前可用。

128
dist/loader.js vendored
View File

@ -1,94 +1,54 @@
(function(factory) { (function(exports) {
typeof define === "function" && define.amd ? define(factory) : factory();
})(function() {
"use strict"; "use strict";
const DEFAULT_VERSIONS = { const DEFAULT_VERSIONS = {
"state": "1.0.20", "state": "v1.0.11",
"bootstrap": "1.0.7", "bootstrap": "v1.0.1",
"base": "1.0.19", "base": "v1.0.7",
"datatable": "1.0.6", "dataTable": "v1.0.6",
"kanban": "1.0.2", "kanban": "v1.0.0",
"mindmap": "1.0.2", "mindmap": "v1.0.0",
"chart": "1.0.2", "chart": "v1.0.0"
"editor": "1.0.2",
"loader": "1.0.5"
};
const DEPENDENCY_GRAPH = {
"bootstrap": ["state"],
"base": ["state", "bootstrap"],
"datatable": ["state", "bootstrap", "base"],
"editor": ["state", "bootstrap", "base"]
}; };
const Loader = { const Loader = {
_loaded: /* @__PURE__ */ new Set(), /**
_v: null, * 加载指定模块并注入 importmap
* @param {...string} pkgs 模块名格式: 'name' 'name:version'
*/
load: (...pkgs) => { load: (...pkgs) => {
if (typeof document === "undefined") return; if (typeof document === "undefined") return;
const currentScript = document.currentScript; const currentScript = document.currentScript;
const currentUrl = currentScript && currentScript.src ? currentScript.src : ""; const currentUrl = currentScript ? currentScript.src : "";
let tpl = ""; let tpl = "https://apigo.cc/web/{project}/raw/tag/{tag}/dist/{project}.js";
const isMin = currentUrl.includes(".min.js"); if (currentUrl.includes("/loader/")) {
const ext = isMin ? ".min.js" : ".js"; const match = currentUrl.match(/(.*\/web\/)loader\/raw\/tag\/([^\/]+)\//);
if (currentUrl && (currentUrl.includes("jsdelivr.net") || currentUrl.includes("unpkg.com") || currentUrl.includes("npm.elemecdn.com"))) { if (match) {
let cdnBase = "https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}"; const baseUrl = match[1];
if (currentUrl.includes("unpkg.com")) cdnBase = "https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}"; const loaderTag = match[2];
else if (currentUrl.includes("npm.elemecdn.com")) cdnBase = "https://npm.elemecdn.com/@apigo.cc/{project}@{tag}/dist/{project}"; tpl = `${baseUrl}{project}/raw/tag/{tag}/dist/{project}.js`;
tpl = cdnBase + ext; Object.keys(DEFAULT_VERSIONS).forEach((k) => {
} else if (currentUrl) { if (DEFAULT_VERSIONS[k] === "v1.0.0") DEFAULT_VERSIONS[k] = loaderTag;
const baseDir = currentUrl.substring(0, currentUrl.lastIndexOf("/") + 1);
tpl = baseDir + "{project}" + ext;
} else {
tpl = "https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}" + ext;
}
const requested = pkgs.map((p) => {
const s = String(p).trim();
const colonIdx = s.indexOf(":");
return {
name: colonIdx === -1 ? s : s.slice(0, colonIdx),
version: colonIdx === -1 ? null : s.slice(colonIdx + 1)
};
}).filter((item) => item.name);
const finalQueue = [];
const nameToVersion = {};
const addWithDeps = (item) => {
const name = item.name.toLowerCase();
if (item.version) nameToVersion[name] = item.version;
const deps = DEPENDENCY_GRAPH[name] || [];
deps.forEach((depName) => addWithDeps({ name: depName }));
if (!finalQueue.includes(name)) finalQueue.push(name);
};
requested.forEach((item) => addWithDeps(item));
const isInitialLoad = document.readyState === "loading";
finalQueue.forEach((name) => {
if (Loader._loaded.has(name)) return;
const version = nameToVersion[name] || DEFAULT_VERSIONS[name] || "latest";
let url = tpl.replace(/{project}/g, name).replace(/{tag}/g, version);
if (Loader._v) {
const joinChar = url.includes("?") ? "&" : "?";
url += `${joinChar}v=${Loader._v}`;
}
if (isInitialLoad) {
document.write(`<script src="${url}"><\/script>`);
} else {
const script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
}
Loader._loaded.add(name);
}); });
} }
}
const importMap = { imports: {} };
pkgs.forEach((pkg) => {
let [name, version] = pkg.split(":");
version = version || DEFAULT_VERSIONS[name] || "main";
const url = tpl.replace(/{project}/g, name).replace(/{tag}/g, version);
importMap.imports[`@web/${name}`] = url;
});
const script = document.createElement("script");
script.type = "importmap";
script.textContent = JSON.stringify(importMap);
if (currentScript) {
currentScript.parentNode.insertBefore(script, currentScript);
} else {
document.head.appendChild(script);
}
}
}; };
if (typeof document !== "undefined" && document.currentScript) { globalThis.Loader = Loader;
const loaderUrl = new URL(document.currentScript.src, location.href); exports.Loader = Loader;
Loader._v = loaderUrl.searchParams.get("v"); exports.default = Loader;
let toLoad = loaderUrl.searchParams.get("load"); Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
const hash = loaderUrl.hash.substring(1); })(this.Loader = this.Loader || {});
if (!toLoad && hash) {
toLoad = hash.startsWith("load=") ? hash.substring(5) : hash;
}
if (toLoad) {
Loader.load(...toLoad.split(","));
}
}
globalThis.__apigo_load = Loader.load;
});

2
dist/loader.min.js vendored
View File

@ -1 +1 @@
!function(t){"function"==typeof define&&define.amd?define(t):t()}(function(){"use strict";const t={state:"1.0.20",bootstrap:"1.0.7",base:"1.0.19",datatable:"1.0.6",kanban:"1.0.2",mindmap:"1.0.2",chart:"1.0.2",editor:"1.0.2",loader:"1.0.5"},e={bootstrap:["state"],base:["state","bootstrap"],datatable:["state","bootstrap","base"],editor:["state","bootstrap","base"]},n={_loaded:new Set,_v:null,load:(...c)=>{if("undefined"==typeof document)return;const s=document.currentScript,o=s&&s.src?s.src:"";let a="";const r=o.includes(".min.js")?".min.js":".js";if(o&&(o.includes("jsdelivr.net")||o.includes("unpkg.com")||o.includes("npm.elemecdn.com"))){let t="https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}";o.includes("unpkg.com")?t="https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}":o.includes("npm.elemecdn.com")&&(t="https://npm.elemecdn.com/@apigo.cc/{project}@{tag}/dist/{project}"),a=t+r}else if(o){const t=o.substring(0,o.lastIndexOf("/")+1);a=t+"{project}"+r}else a="https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}"+r;const i=c.map(t=>{const e=String(t).trim(),n=e.indexOf(":");return{name:-1===n?e:e.slice(0,n),version:-1===n?null:e.slice(n+1)}}).filter(t=>t.name),d=[],l={},p=t=>{const n=t.name.toLowerCase();t.version&&(l[n]=t.version);(e[n]||[]).forEach(t=>p({name:t})),d.includes(n)||d.push(n)};i.forEach(t=>p(t));const u="loading"===document.readyState;d.forEach(e=>{if(n._loaded.has(e))return;const c=l[e]||t[e]||"latest";let s=a.replace(/{project}/g,e).replace(/{tag}/g,c);if(n._v){const t=s.includes("?")?"&":"?";s+=`${t}v=${n._v}`}if(u)document.write(`<script src="${s}"><\/script>`);else{const t=document.createElement("script");t.src=s,document.head.appendChild(t)}n._loaded.add(e)})}};if("undefined"!=typeof document&&document.currentScript){const t=new URL(document.currentScript.src,location.href);n._v=t.searchParams.get("v");let e=t.searchParams.get("load");const c=t.hash.substring(1);!e&&c&&(e=c.startsWith("load=")?c.substring(5):c),e&&n.load(...e.split(","))}globalThis.__apigo_load=n.load}); !function(t){"use strict";const e={state:"v1.0.11",bootstrap:"v1.0.1",base:"v1.0.7",dataTable:"v1.0.6",kanban:"v1.0.0",mindmap:"v1.0.0",chart:"v1.0.0"},a={load:(...t)=>{if("undefined"==typeof document)return;const a=document.currentScript,o=a?a.src:"";let r="https://apigo.cc/web/{project}/raw/tag/{tag}/dist/{project}.js";if(o.includes("/loader/")){const t=o.match(/(.*\/web\/)loader\/raw\/tag\/([^\/]+)\//);if(t){const a=t[1],o=t[2];r=`${a}{project}/raw/tag/{tag}/dist/{project}.js`,Object.keys(e).forEach(t=>{"v1.0.0"===e[t]&&(e[t]=o)})}}const c={imports:{}};t.forEach(t=>{let[a,o]=t.split(":");o=o||e[a]||"main";const n=r.replace(/{project}/g,a).replace(/{tag}/g,o);c.imports[`@web/${a}`]=n});const n=document.createElement("script");n.type="importmap",n.textContent=JSON.stringify(c),a?a.parentNode.insertBefore(n,a):document.head.appendChild(n)}};globalThis.Loader=a,t.Loader=a,t.default=a,Object.defineProperties(t,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}(this.Loader=this.Loader||{});

49
node_modules/.package-lock.json generated vendored
View File

@ -1,6 +1,6 @@
{ {
"name": "@apigo.cc/loader", "name": "loader",
"version": "1.0.2", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@ -65,21 +65,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@playwright/test": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"dev": true,
"dependencies": {
"playwright": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rollup/plugin-terser": { "node_modules/@rollup/plugin-terser": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz",
@ -284,36 +269,6 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true "dev": true
}, },
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true,
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.15", "version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",

68
package-lock.json generated
View File

@ -1,15 +1,14 @@
{ {
"name": "@apigo.cc/loader", "name": "loader",
"version": "1.0.4", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@apigo.cc/loader", "name": "loader",
"version": "1.0.4", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.60.0",
"@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-terser": "^1.0.0",
"terser": "^5.48.0", "terser": "^5.48.0",
"vite": "^5.4.21" "vite": "^5.4.21"
@ -428,21 +427,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@playwright/test": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"dev": true,
"dependencies": {
"playwright": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rollup/plugin-terser": { "node_modules/@rollup/plugin-terser": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz",
@ -1169,50 +1153,6 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true "dev": true
}, },
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true,
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.15", "version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",

View File

@ -1,25 +1,16 @@
{ {
"name": "@apigo.cc/loader", "name": "loader",
"version": "1.0.5", "version": "1.0.0",
"type": "module", "type": "module",
"main": "dist/loader.js",
"module": "dist/loader.js",
"files": [
"dist"
],
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"prebuild": "node scripts/update-versions.js", "build": "vite build"
"build": "vite build",
"test": "playwright test",
"pub": "node scripts/publish.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.60.0",
"@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-terser": "^1.0.0",
"terser": "^5.48.0", "terser": "^5.48.0",
"vite": "^5.4.21" "vite": "^5.4.21"

View File

@ -1,14 +0,0 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './test',
use: {
baseURL: 'http://127.0.0.1:8084',
},
webServer: {
command: 'npx -y http-server -p 8084',
url: 'http://127.0.0.1:8084',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});

View File

@ -1,48 +0,0 @@
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
try {
// 1. 获取最新 tag
let tag;
try {
tag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim();
} catch (err) {
throw new Error('Failed to find git tags. Please make sure the repository has tags (e.g., v1.0.0) before publishing.');
}
// 去掉 v 前缀
const version = tag.startsWith('v') ? tag.slice(1) : tag;
console.log(`Latest git tag: ${tag}, Version to publish: ${version}`);
// 2. 读取并更新 package.json
const pkgPath = path.join(__dirname, '../package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
// 保持原有名称(如果已经带有 @apigo.cc/ 前缀)或替换前缀
if (!pkg.name.startsWith('@apigo.cc/')) {
const baseName = pkg.name.includes('/') ? pkg.name.split('/')[1] : pkg.name;
pkg.name = `@apigo.cc/${baseName}`;
}
pkg.version = version;
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
console.log(`Updated package.json: name=${pkg.name}, version=${pkg.version}`);
// 3. 构建
console.log('Running build...');
execSync('npm run build', { stdio: 'inherit', cwd: path.join(__dirname, '..') });
// 4. 发布
console.log('Publishing to npm...');
const args = process.argv.slice(2).join(' ');
execSync(`npm publish --access public ${args}`, { stdio: 'inherit', cwd: path.join(__dirname, '..') });
console.log('Publish successful!');
} catch (error) {
console.error('Publish failed:', error.message);
process.exit(1);
}

View File

@ -1,131 +0,0 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const parentDir = path.join(__dirname, '../..');
// 默认版本,如果没扫到就用这个
const versions = {
'state': '1.0.11',
'bootstrap': '1.0.0',
'base': '1.0.7',
'datatable': '1.0.6',
'kanban': '1.0.0',
'mindmap': '1.0.0',
'chart': '1.0.0',
'editor': '1.0.0'
};
// 1. 扫描同级目录获取最新版本号
try {
const dirs = fs.readdirSync(parentDir);
dirs.forEach(dir => {
const pkgPath = path.join(parentDir, dir, 'package.json');
if (fs.existsSync(pkgPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (pkg.name && pkg.name.startsWith('@apigo.cc/')) {
const key = pkg.name.replace('@apigo.cc/', '').toLowerCase();
if (pkg.version) {
versions[key] = pkg.version;
}
}
} catch (e) {
console.warn(`Failed to parse ${pkgPath}:`, e.message);
}
}
});
} catch (err) {
console.error('Failed to read parent directory:', err.message);
}
console.log('Gathered versions:', versions);
// 2. 获取 loader 自己的版本号
const loaderPkgPath = path.join(__dirname, '../package.json');
let loaderVersion = '1.0.0';
try {
const loaderPkg = JSON.parse(fs.readFileSync(loaderPkgPath, 'utf8'));
loaderVersion = loaderPkg.version || '1.0.0';
} catch (e) {
console.warn('Failed to read loader package.json:', e.message);
}
// 3. 更新 loader/src/index.js
const indexPath = path.join(__dirname, '../src/index.js');
if (fs.existsSync(indexPath)) {
let indexContent = fs.readFileSync(indexPath, 'utf8');
const versionsString = Object.entries(versions)
.map(([k, v]) => ` '${k}': '${v}'`)
.join(',\n');
const newDefaultVersions = `const DEFAULT_VERSIONS = {\n${versionsString}\n};`;
indexContent = indexContent.replace(/const DEFAULT_VERSIONS = \{[\s\S]*?\};/, newDefaultVersions);
fs.writeFileSync(indexPath, indexContent, 'utf8');
console.log('Updated loader/src/index.js DEFAULT_VERSIONS.');
}
// 4. 更新 loader/README.md
const readmePath = path.join(__dirname, '../README.md');
if (fs.existsSync(readmePath)) {
let readmeContent = fs.readFileSync(readmePath, 'utf8');
// 替换 Loader.load('state', 'bootstrap', 'base') 里的版本号
// 支持形如 Loader.load('state:x.y.z', 'bootstrap:x.y.z', 'base:x.y.z')
// 或者最原始的 Loader.load('state', 'bootstrap', 'base')
readmeContent = readmeContent.replace(
/Loader\.load\(([^)]*)\)/g,
(match, p1) => {
// 按照逗号拆分,但保留各行原样(包含可能的换行和注释)
const items = p1.split(',');
const newItems = items.map(item => {
const trimmed = item.trim();
if (trimmed.startsWith('//') || !trimmed) {
return item;
}
// 提取清除引号后的内容进行路径和版本判断
const cleanItem = trimmed.replace(/['"]/g, '');
const colonIdx = cleanItem.indexOf(':');
const name = colonIdx === -1 ? cleanItem : cleanItem.slice(0, colonIdx);
const val = colonIdx === -1 ? '' : cleanItem.slice(colonIdx + 1);
// 如果值里包含 http://, https://, ./, ../, / 说明是调试重定向路径,原样保留
if (val && (/^(https?:|\.|\/)/.test(val))) {
return item;
}
const key = name.toLowerCase();
if (versions[key]) {
// 尽量保留原有的包裹引号格式
const quote = item.includes('"') ? '"' : "'";
return `${quote}${key}:${versions[key]}${quote}`;
}
return item;
});
return `Loader.load(${newItems.join(',')})`;
}
);
// 替换 cdn 链接及所有地方的 loader 版本
readmeContent = readmeContent.replace(
/(@apigo\.cc\/loader@)[0-9.]+(\/dist\/loader)/g,
`$1${loaderVersion}$2`
);
// 替换 manual import 里的依赖版本,如:
// "@apigo.cc/state": "https://cdn.jsdelivr.net/npm/@apigo.cc/state@1.0.11/dist/state.min.js",
readmeContent = readmeContent.replace(
/(\/@apigo\.cc\/([a-zA-Z0-9_-]+)@)[0-9.]+(\/dist\/)/g,
(match, prefix, name, suffix) => {
const key = name.toLowerCase();
const ver = key === 'loader' ? loaderVersion : (versions[key] || 'latest');
return `${prefix}${ver}${suffix}`;
}
);
fs.writeFileSync(readmePath, readmeContent, 'utf8');
console.log('Updated loader/README.md versions.');
}

View File

@ -1,121 +1,77 @@
/** /**
* @apigo.cc/loader * @web/loader
* 全自动同步依赖调度器 (UMD 优先) * 极简模块调度中心
*/ */
const DEFAULT_VERSIONS = { const DEFAULT_VERSIONS = {
'state': '1.0.20', 'state': 'v1.0.11',
'bootstrap': '1.0.7', 'bootstrap': 'v1.0.1',
'base': '1.0.19', 'base': 'v1.0.7',
'datatable': '1.0.6', 'dataTable': 'v1.0.6',
'kanban': '1.0.2', 'kanban': 'v1.0.0',
'mindmap': '1.0.2', 'mindmap': 'v1.0.0',
'chart': '1.0.2', 'chart': 'v1.0.0'
'editor': '1.0.2',
'loader': '1.0.5'
};
const DEPENDENCY_GRAPH = {
'bootstrap': ['state'],
'base': ['state', 'bootstrap'],
'datatable': ['state', 'bootstrap', 'base'],
'editor': ['state', 'bootstrap', 'base']
}; };
const Loader = { const Loader = {
_loaded: new Set(), /**
_v: null, * 加载指定模块并注入 importmap
* @param {...string} pkgs 模块名格式: 'name' 'name:version'
*/
load: (...pkgs) => { load: (...pkgs) => {
if (typeof document === 'undefined') return; if (typeof document === 'undefined') return;
const currentScript = document.currentScript; const currentScript = document.currentScript;
const currentUrl = (currentScript && currentScript.src) ? currentScript.src : ''; const currentUrl = currentScript ? currentScript.src : '';
// 1. 确定加载源与格式 // 解析 URL 模板
let tpl = ''; // 假设当前 URL 是 https://apigo.cc/web/loader/raw/tag/v1.0.0/dist/loader.min.js
const isMin = currentUrl.includes('.min.js'); // 目标模板 https://apigo.cc/web/{project}/raw/tag/{tag}/dist/{project}.js
const ext = isMin ? '.min.js' : '.js'; let tpl = 'https://apigo.cc/web/{project}/raw/tag/{tag}/dist/{project}.js';
if (currentUrl && (currentUrl.includes('jsdelivr.net') || currentUrl.includes('unpkg.com') || currentUrl.includes('npm.elemecdn.com'))) { if (currentUrl.includes('/loader/')) {
let cdnBase = 'https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}'; // 自动推断基础路径和 tag
if (currentUrl.includes('unpkg.com')) cdnBase = 'https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}'; const match = currentUrl.match(/(.*\/web\/)loader\/raw\/tag\/([^\/]+)\//);
else if (currentUrl.includes('npm.elemecdn.com')) cdnBase = 'https://npm.elemecdn.com/@apigo.cc/{project}@{tag}/dist/{project}'; if (match) {
tpl = cdnBase + ext; const baseUrl = match[1];
} else if (currentUrl) { const loaderTag = match[2];
const baseDir = currentUrl.substring(0, currentUrl.lastIndexOf('/') + 1); tpl = `${baseUrl}{project}/raw/tag/{tag}/dist/{project}.js`;
tpl = baseDir + '{project}' + ext; // 默认情况下其他库也使用和 loader 相同的 tag除非另有定义
} else { Object.keys(DEFAULT_VERSIONS).forEach(k => {
tpl = 'https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}' + ext; if (DEFAULT_VERSIONS[k] === 'v1.0.0') DEFAULT_VERSIONS[k] = loaderTag;
});
}
} }
// 2. 解析包名与显式指定的版本号 const importMap = { imports: {} };
const requested = pkgs.map(p => {
const s = String(p).trim();
const colonIdx = s.indexOf(':');
return {
name: colonIdx === -1 ? s : s.slice(0, colonIdx),
version: colonIdx === -1 ? null : s.slice(colonIdx + 1)
};
}).filter(item => item.name);
// 3. 分析依赖并排序 pkgs.forEach(pkg => {
const finalQueue = []; let [name, version] = pkg.split(':');
const nameToVersion = {}; version = version || DEFAULT_VERSIONS[name] || 'main';
const addWithDeps = (item) => { const url = tpl.replace(/{project}/g, name).replace(/{tag}/g, version);
const name = item.name.toLowerCase(); importMap.imports[`@web/${name}`] = url;
if (item.version) nameToVersion[name] = item.version;
const deps = DEPENDENCY_GRAPH[name] || []; // 特殊处理:如果是 bootstrap可能需要额外处理或依赖
deps.forEach(depName => addWithDeps({ name: depName })); if (name === 'bootstrap') {
// bootstrap 内部现在是自包含的,直接映射即可
if (!finalQueue.includes(name)) finalQueue.push(name);
};
requested.forEach(item => addWithDeps(item));
// 4. 注入脚本
const isInitialLoad = document.readyState === 'loading';
finalQueue.forEach(name => {
if (Loader._loaded.has(name)) return;
const version = nameToVersion[name] || DEFAULT_VERSIONS[name] || 'latest';
let url = tpl.replace(/{project}/g, name).replace(/{tag}/g, version);
if (Loader._v) {
const joinChar = url.includes('?') ? '&' : '?';
url += `${joinChar}v=${Loader._v}`;
} }
});
// 核心改进:如果是初始化阶段,使用 document.write 实现同步阻塞加载
// 这能保证下一行内联脚本执行时,全局变量已经就绪。
if (isInitialLoad) {
document.write(`<script src="${url}"><\/script>`);
} else {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = url; script.type = 'importmap';
script.textContent = JSON.stringify(importMap);
if (currentScript) {
currentScript.parentNode.insertBefore(script, currentScript);
} else {
document.head.appendChild(script); document.head.appendChild(script);
} }
Loader._loaded.add(name);
});
} }
}; };
// 立即解析参数 // 挂载到全局
if (typeof document !== 'undefined' && document.currentScript) { globalThis.Loader = Loader;
const loaderUrl = new URL(document.currentScript.src, location.href);
Loader._v = loaderUrl.searchParams.get('v');
let toLoad = loaderUrl.searchParams.get('load'); export { Loader };
const hash = loaderUrl.hash.substring(1); export default Loader;
if (!toLoad && hash) {
toLoad = hash.startsWith('load=') ? hash.substring(5) : hash;
}
if (toLoad) {
Loader.load(...toLoad.split(','));
}
}
globalThis.__apigo_load = Loader.load;

View File

@ -1,34 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Loader Integration Test</title>
<!-- 引入本地构建的 loader.js -->
<script src="../dist/loader.js"></script>
<script>
// 声明加载:
// state 采用默认版本号(验证常规版本拼接)
// bootstrap 采用相对路径(验证本地重定向)
// base 采用绝对 HTTP 路径(验证自定义域名重定向)
Loader.load(
'state',
'bootstrap:./mock-bootstrap.js',
'base:http://127.0.0.1:8084/test/mock-base.js'
);
</script>
</head>
<body>
<div id="status">loading</div>
<script type="module">
import { Bootstrap } from '@apigo.cc/bootstrap';
import { HTTP } from '@apigo.cc/base';
// 验证导入的重定向模块是否正确加载
if (Bootstrap && Bootstrap.isMock && HTTP && HTTP.isMock) {
document.getElementById('status').textContent = 'success';
} else {
document.getElementById('status').textContent = 'failed';
}
</script>
</body>
</html>

View File

@ -1,33 +0,0 @@
import { test, expect } from '@playwright/test';
test('loader dynamic importmap generation and custom path redirect', async ({ page }) => {
page.on('console', msg => console.log('BROWSER LOG:', msg.text()));
// 打开测试页面
await page.goto('http://127.0.0.1:8084/test/index.html');
// 1. 验证生成的 importmap 标签结构
const importMapJson = await page.evaluate(() => {
const script = document.querySelector('script[type="importmap"]');
return script ? JSON.parse(script.textContent) : null;
});
expect(importMapJson).not.toBeNull();
expect(importMapJson.imports).toBeDefined();
// 2. 检查 state (常规 CDN 映射) 是否生成正确
const stateUrl = importMapJson.imports['@apigo.cc/state'];
expect(stateUrl).toContain('cdn.jsdelivr.net/npm/@apigo.cc/state');
// 3. 检查 bootstrap (本地相对路径重定向) 是否保留
const bootstrapUrl = importMapJson.imports['@apigo.cc/bootstrap'];
expect(bootstrapUrl).toBe('./mock-bootstrap.js');
// 4. 检查 base (本地绝对 HTTP 重定向) 是否保留
const baseUrl = importMapJson.imports['@apigo.cc/base'];
expect(baseUrl).toBe('http://127.0.0.1:8084/test/mock-base.js');
// 5. 验证重定向的 mock 模块确实可以被 ESM 成功导入并运行 (通过页面 status 的变化)
const status = page.locator('#status');
await expect(status).toHaveText('success');
});

View File

@ -1,3 +0,0 @@
export const HTTP = { isMock: true, name: 'base-mock' };
export const UI = { toast: () => {} };
export default { HTTP, UI };

View File

@ -1,2 +0,0 @@
export const Bootstrap = { isMock: true, name: 'bootstrap-mock' };
export default Bootstrap;

View File

@ -6,29 +6,22 @@ export default defineConfig({
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'src/index.js'), entry: resolve(__dirname, 'src/index.js'),
name: 'ApigoLoader', name: 'Loader',
formats: ['umd', 'es'] formats: ['iife'] // 使用 IIFE 格式,方便直接在 script 标签中使用
}, },
rollupOptions: { rollupOptions: {
output: [ output: [
{ {
format: 'umd', format: 'iife',
name: 'ApigoLoader', name: 'Loader',
entryFileNames: 'loader.js' entryFileNames: 'loader.js',
extend: true
}, },
{ {
format: 'umd', format: 'iife',
name: 'ApigoLoader', name: 'Loader',
entryFileNames: 'loader.min.js', entryFileNames: 'loader.min.js',
plugins: [terser()] extend: true,
},
{
format: 'es',
entryFileNames: 'loader.mjs'
},
{
format: 'es',
entryFileNames: 'loader.min.mjs',
plugins: [terser()] plugins: [terser()]
} }
] ]