Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79eeec8423 | ||
|
|
042ebe61e3 | ||
|
|
70ebb40e4f |
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
106
README.md
106
README.md
@ -1,101 +1,51 @@
|
|||||||
# @apigo.cc/loader 模块调度中心
|
# @apigo.cc/loader API 手册
|
||||||
|
|
||||||
`@apigo.cc/loader` 是整个 `@apigo.cc` 体系的入口调度中心。它负责根据当前环境自动生成 `importmap`,并管理各模块的版本。
|
全自动依赖调度器。通过动态注入脚本标签,实现对 Apigo 全线组件的一键加载、版本管理及环境自动适配。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 一、 集成方式
|
## 1. 引入方式 (UMD 优先)
|
||||||
|
|
||||||
直接引用打包好的 `loader.min.js`,它会自动根据当前的加载源适配 CDN 镜像。
|
您可以通过 URL 参数或 **Hash (#)** 直接定义要加载的模块。
|
||||||
|
|
||||||
### 1. 自动注入 Importmap
|
|
||||||
在 HTML 中引入 loader 并声明需要加载的模块:
|
|
||||||
|
|
||||||
|
### A. 极简带版本模式 (防缓存)
|
||||||
```html
|
```html
|
||||||
<!-- 引入 loader -->
|
<!-- 使用 ?v= 指定全局版本,所有依赖包都会自动带上该版本后缀 -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/loader@1.0.2/dist/loader.min.js"></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>
|
||||||
|
|
||||||
<script>
|
|
||||||
// 定义需要使用的模块,自动生成 importmap
|
|
||||||
// 支持格式: 'name' 或 'name:version'
|
|
||||||
// 也可以使用标签模式: 'name:latest', 'name:beta' 等
|
|
||||||
Loader.load('state:1.0.11', 'bootstrap:1.0.3', 'base:1.0.7');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- 随后即可使用原生 import -->
|
|
||||||
<script type="module">
|
|
||||||
import { $ } from '@apigo.cc/state';
|
|
||||||
import { Bootstrap } from '@apigo.cc/bootstrap';
|
|
||||||
|
|
||||||
console.log('Ready');
|
|
||||||
</script>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 原生朴素写法 (手动定义)
|
### B. 简写 Hash 模式
|
||||||
如果您不想使用 `Loader.load` 的自动注入,也可以手动定义 `importmap`:
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="importmap">
|
<!-- 直接在 # 后面列出模块名,多个模块用逗号分隔 -->
|
||||||
{
|
<script src="/libs/loader.js#state,bootstrap,base"></script>
|
||||||
"imports": {
|
|
||||||
"@apigo.cc/state": "https://cdn.jsdelivr.net/npm/@apigo.cc/state@1.0.11/dist/state.min.js",
|
|
||||||
"@apigo.cc/bootstrap": "https://cdn.jsdelivr.net/npm/@apigo.cc/bootstrap@1.0.3/dist/bootstrap.min.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
import { $ } from '@apigo.cc/state';
|
|
||||||
// ... 业务代码
|
|
||||||
</script>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 二、 核心特性
|
## 2. 核心特性
|
||||||
|
|
||||||
1. **多镜像自适应**:自动识别 `loader.min.js` 的来源 URL,并自动将其他模块指向相同的 CDN 镜像。支持以下镜像:
|
### 🚀 智能定位与版本传播 (Smart Propagation)
|
||||||
* `cdn.jsdelivr.net` (默认)
|
- **同域跟随**: 如果 `loader.js` 加载自 `/static/js/`,则所有子包默认也从 `/static/js/` 获取。
|
||||||
* `esm.sh`
|
- **版本锁步**: 只要 Loader URL 带有 `?v=X.Y.Z`,加载的所有依赖包(如 `base.js`)都会自动变为 `base.js?v=X.Y.Z`。
|
||||||
* `unpkg.com`
|
- **压缩同步**: 只要引入的是 `loader.min.js`,其加载的所有依赖也会自动请求 `.min.js` 版本。
|
||||||
* `unpkg.zhimg.com` (中国加速)
|
|
||||||
* `npm.elemecdn.com` (中国加速)
|
### 🔗 自动依赖分析
|
||||||
2. **默认版本管理**:内置了各组件的稳定版本号,无需手动记忆。
|
Loader 内部维护了完整的依赖链。例如:
|
||||||
3. **零配置**:只需一行代码即可完成整个生态链的模块映射。
|
- 请求 `datatable` -> 自动按序注入 `state` -> `bootstrap` -> `base` -> `datatable`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 三、 本地开发与调试重定向
|
## 3. API 接口:`__apigo_load()`
|
||||||
|
|
||||||
在本地开发时,如果需要将某些模块重定向到本地开发服务器进行调试,可以通过在浏览器控制台设置 `localStorage` 来实现,**无需修改任何 HTML 业务代码**。
|
脚本引入后,全局会挂载该函数。**注意:该函数在运行一次后会自动自毁(从全局删除)以保持环境纯净。**
|
||||||
|
|
||||||
### 1. 启用本地调试重定向
|
### `__apigo_load(...pkgs)`
|
||||||
比如将 `bootstrap` 和 `state` 模块重定向到您本地运行的 Vite 开发服务器:
|
- **`__apigo_load('base')`**: 加载默认稳定版。
|
||||||
```javascript
|
- **`__apigo_load('state:1.0.11')`**: 指定特定包的版本。
|
||||||
// 重定向 bootstrap 模块
|
|
||||||
localStorage.setItem('dev:@apigo.cc/bootstrap', 'http://localhost:5173/src/index.js');
|
|
||||||
|
|
||||||
// 重定向 state 模块
|
|
||||||
localStorage.setItem('dev:@apigo.cc/state', 'http://localhost:5174/src/index.js');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 取消本地调试重定向
|
|
||||||
调试完成后,清除 `localStorage` 对应的键即可恢复使用 CDN 版本:
|
|
||||||
```javascript
|
|
||||||
// 恢复为 CDN 版本
|
|
||||||
localStorage.removeItem('dev:@apigo.cc/bootstrap');
|
|
||||||
localStorage.removeItem('dev:@apigo.cc/state');
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 四、 支持模块清单
|
## 开发者提示 (AI 必读)
|
||||||
所有模块均发布在 `@apigo.cc` 组织下:
|
1. **参数优先级**: URL 查询参数 `?load=` 优先级高于 Hash 参数 `#`。
|
||||||
* `state` (原子状态机)
|
2. **同步执行**: Loader 利用浏览器对动态插入脚本的顺序解析机制,保障了依赖项在业务代码运行前就绪。
|
||||||
* `bootstrap` (UI 引擎)
|
3. **自清理**: `__apigo_load` 仅在 `<script type="module">` 运行前可用。
|
||||||
* `base` (常用控件库)
|
|
||||||
* `datatable` (高性能表格)
|
|
||||||
* `kanban` (看板)
|
|
||||||
* `mindmap` (思维导图)
|
|
||||||
* `chart` (图表)
|
|
||||||
* `editor` (代码编辑器)
|
|
||||||
|
|||||||
133
dist/loader.js
vendored
133
dist/loader.js
vendored
@ -1,65 +1,94 @@
|
|||||||
(function(exports) {
|
(function(factory) {
|
||||||
|
typeof define === "function" && define.amd ? define(factory) : factory();
|
||||||
|
})(function() {
|
||||||
"use strict";
|
"use strict";
|
||||||
const DEFAULT_VERSIONS = {
|
const DEFAULT_VERSIONS = {
|
||||||
"state": "1.0.11",
|
"state": "1.0.20",
|
||||||
"bootstrap": "1.0.3",
|
"bootstrap": "1.0.7",
|
||||||
"base": "1.0.7",
|
"base": "1.0.19",
|
||||||
"datatable": "1.0.6",
|
"datatable": "1.0.6",
|
||||||
"kanban": "1.0.0",
|
"kanban": "1.0.2",
|
||||||
"mindmap": "1.0.0",
|
"mindmap": "1.0.2",
|
||||||
"chart": "1.0.0",
|
"chart": "1.0.2",
|
||||||
"editor": "1.0.0",
|
"editor": "1.0.2",
|
||||||
"loader": "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,
|
||||||
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 : "";
|
const currentUrl = currentScript && currentScript.src ? currentScript.src : "";
|
||||||
let tpl = "https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}.min.js";
|
let tpl = "";
|
||||||
if (currentUrl) {
|
const isMin = currentUrl.includes(".min.js");
|
||||||
if (currentUrl.includes("esm.sh")) {
|
const ext = isMin ? ".min.js" : ".js";
|
||||||
tpl = "https://esm.sh/@apigo.cc/{project}@{tag}/dist/{project}.min.js";
|
if (currentUrl && (currentUrl.includes("jsdelivr.net") || currentUrl.includes("unpkg.com") || currentUrl.includes("npm.elemecdn.com"))) {
|
||||||
} else if (currentUrl.includes("unpkg.com")) {
|
let cdnBase = "https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}";
|
||||||
tpl = "https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js";
|
if (currentUrl.includes("unpkg.com")) cdnBase = "https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}";
|
||||||
} else if (currentUrl.includes("unpkg.zhimg.com")) {
|
else if (currentUrl.includes("npm.elemecdn.com")) cdnBase = "https://npm.elemecdn.com/@apigo.cc/{project}@{tag}/dist/{project}";
|
||||||
tpl = "https://unpkg.zhimg.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js";
|
tpl = cdnBase + ext;
|
||||||
} else if (currentUrl.includes("npm.elemecdn.com")) {
|
} else if (currentUrl) {
|
||||||
tpl = "https://npm.elemecdn.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js";
|
const baseDir = currentUrl.substring(0, currentUrl.lastIndexOf("/") + 1);
|
||||||
} else if (currentUrl.includes("jsdelivr.net")) {
|
tpl = baseDir + "{project}" + ext;
|
||||||
tpl = "https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}.min.js";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const importMap = { imports: {} };
|
|
||||||
pkgs.forEach((pkg) => {
|
|
||||||
let [name, version] = pkg.split(":");
|
|
||||||
const key = name.toLowerCase();
|
|
||||||
const fullKey = `@apigo.cc/${key}`;
|
|
||||||
let url = "";
|
|
||||||
try {
|
|
||||||
if (typeof localStorage !== "undefined") {
|
|
||||||
url = localStorage.getItem(`dev:${fullKey}`) || "";
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
if (!url) {
|
|
||||||
version = version || DEFAULT_VERSIONS[key] || "latest";
|
|
||||||
url = tpl.replace(/{project}/g, key).replace(/{tag}/g, version);
|
|
||||||
}
|
|
||||||
importMap.imports[fullKey] = url;
|
|
||||||
});
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.type = "importmap";
|
|
||||||
script.textContent = JSON.stringify(importMap);
|
|
||||||
if (currentScript) {
|
|
||||||
currentScript.parentNode.insertBefore(script, currentScript);
|
|
||||||
} else {
|
} 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);
|
document.head.appendChild(script);
|
||||||
}
|
}
|
||||||
|
Loader._loaded.add(name);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
globalThis.Loader = Loader;
|
if (typeof document !== "undefined" && document.currentScript) {
|
||||||
exports.Loader = Loader;
|
const loaderUrl = new URL(document.currentScript.src, location.href);
|
||||||
exports.default = Loader;
|
Loader._v = loaderUrl.searchParams.get("v");
|
||||||
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
let toLoad = loaderUrl.searchParams.get("load");
|
||||||
})(this.Loader = this.Loader || {});
|
const hash = loaderUrl.hash.substring(1);
|
||||||
|
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
2
dist/loader.min.js
vendored
@ -1 +1 @@
|
|||||||
!function(t){"use strict";const e={state:"1.0.11",bootstrap:"1.0.3",base:"1.0.7",datatable:"1.0.6",kanban:"1.0.0",mindmap:"1.0.0",chart:"1.0.0",editor:"1.0.0",loader:"1.0.2"},c={load:(...t)=>{if("undefined"==typeof document)return;const c=document.currentScript,o=c?c.src:"";let n="https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}.min.js";o&&(o.includes("esm.sh")?n="https://esm.sh/@apigo.cc/{project}@{tag}/dist/{project}.min.js":o.includes("unpkg.com")?n="https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js":o.includes("unpkg.zhimg.com")?n="https://unpkg.zhimg.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js":o.includes("npm.elemecdn.com")?n="https://npm.elemecdn.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js":o.includes("jsdelivr.net")&&(n="https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}.min.js"));const s={imports:{}};t.forEach(t=>{let[c,o]=t.split(":");const i=c.toLowerCase(),p=`@apigo.cc/${i}`;let a="";try{"undefined"!=typeof localStorage&&(a=localStorage.getItem(`dev:${p}`)||"")}catch(t){}a||(o=o||e[i]||"latest",a=n.replace(/{project}/g,i).replace(/{tag}/g,o)),s.imports[p]=a});const i=document.createElement("script");i.type="importmap",i.textContent=JSON.stringify(s),c?c.parentNode.insertBefore(i,c):document.head.appendChild(i)}};globalThis.Loader=c,t.Loader=c,t.default=c,Object.defineProperties(t,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}(this.Loader=this.Loader||{});
|
!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});
|
||||||
|
|||||||
47
node_modules/.package-lock.json
generated
vendored
47
node_modules/.package-lock.json
generated
vendored
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@apigo.cc/loader",
|
"name": "@apigo.cc/loader",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
@ -65,6 +65,21 @@
|
|||||||
"@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",
|
||||||
@ -269,6 +284,36 @@
|
|||||||
"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",
|
||||||
|
|||||||
64
package-lock.json
generated
64
package-lock.json
generated
@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@apigo.cc/loader",
|
"name": "@apigo.cc/loader",
|
||||||
"version": "1.0.0",
|
"version": "1.0.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@apigo.cc/loader",
|
"name": "@apigo.cc/loader",
|
||||||
"version": "1.0.0",
|
"version": "1.0.4",
|
||||||
"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"
|
||||||
@ -427,6 +428,21 @@
|
|||||||
"@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",
|
||||||
@ -1153,6 +1169,50 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@apigo.cc/loader",
|
"name": "@apigo.cc/loader",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/loader.js",
|
"main": "dist/loader.js",
|
||||||
"module": "dist/loader.js",
|
"module": "dist/loader.js",
|
||||||
@ -11,6 +11,7 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"prebuild": "node scripts/update-versions.js",
|
"prebuild": "node scripts/update-versions.js",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"test": "playwright test",
|
||||||
"pub": "node scripts/publish.js"
|
"pub": "node scripts/publish.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@ -18,6 +19,7 @@
|
|||||||
"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"
|
||||||
|
|||||||
14
playwright.config.js
Normal file
14
playwright.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -78,24 +78,40 @@ if (fs.existsSync(readmePath)) {
|
|||||||
readmeContent = readmeContent.replace(
|
readmeContent = readmeContent.replace(
|
||||||
/Loader\.load\(([^)]*)\)/g,
|
/Loader\.load\(([^)]*)\)/g,
|
||||||
(match, p1) => {
|
(match, p1) => {
|
||||||
// 提取被单引号/双引号包裹的各个 package 项
|
// 按照逗号拆分,但保留各行原样(包含可能的换行和注释)
|
||||||
const items = p1.split(',').map(item => item.trim().replace(/['"]/g, ''));
|
const items = p1.split(',');
|
||||||
const newItems = items.map(item => {
|
const newItems = items.map(item => {
|
||||||
const [name] = item.split(':');
|
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();
|
const key = name.toLowerCase();
|
||||||
if (versions[key]) {
|
if (versions[key]) {
|
||||||
return `'${key}:${versions[key]}'`;
|
// 尽量保留原有的包裹引号格式
|
||||||
|
const quote = item.includes('"') ? '"' : "'";
|
||||||
|
return `${quote}${key}:${versions[key]}${quote}`;
|
||||||
}
|
}
|
||||||
return `'${item}'`;
|
return item;
|
||||||
});
|
});
|
||||||
return `Loader.load(${newItems.join(',')})`;
|
return `Loader.load(${newItems.join(',')})`;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 替换 cdn 链接里的 loader 版本
|
// 替换 cdn 链接及所有地方的 loader 版本
|
||||||
// 如 <script src="https://cdn.jsdelivr.net/npm/@apigo.cc/loader@1.0.0/dist/loader.min.js"></script>
|
|
||||||
readmeContent = readmeContent.replace(
|
readmeContent = readmeContent.replace(
|
||||||
/(\/\/cdn\.jsdelivr\.net\/npm\/@apigo\.cc\/loader@)[0-9.]+(\/dist\/loader\.min\.js)/g,
|
/(@apigo\.cc\/loader@)[0-9.]+(\/dist\/loader)/g,
|
||||||
`$1${loaderVersion}$2`
|
`$1${loaderVersion}$2`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
160
src/index.js
160
src/index.js
@ -1,81 +1,121 @@
|
|||||||
/**
|
/**
|
||||||
* @web/loader
|
* @apigo.cc/loader
|
||||||
* 极简模块调度中心
|
* 全自动同步依赖调度器 (UMD 优先)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DEFAULT_VERSIONS = {
|
const DEFAULT_VERSIONS = {
|
||||||
'state': '1.0.11',
|
'state': '1.0.20',
|
||||||
'bootstrap': '1.0.3',
|
'bootstrap': '1.0.7',
|
||||||
'base': '1.0.7',
|
'base': '1.0.19',
|
||||||
'datatable': '1.0.6',
|
'datatable': '1.0.6',
|
||||||
'kanban': '1.0.0',
|
'kanban': '1.0.2',
|
||||||
'mindmap': '1.0.0',
|
'mindmap': '1.0.2',
|
||||||
'chart': '1.0.0',
|
'chart': '1.0.2',
|
||||||
'editor': '1.0.0',
|
'editor': '1.0.2',
|
||||||
'loader': '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,
|
||||||
|
|
||||||
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 : '';
|
const currentUrl = (currentScript && currentScript.src) ? currentScript.src : '';
|
||||||
|
|
||||||
// 1. 默认模板 (jsDelivr)
|
// 1. 确定加载源与格式
|
||||||
let tpl = 'https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}.min.js';
|
let tpl = '';
|
||||||
|
const isMin = currentUrl.includes('.min.js');
|
||||||
|
const ext = isMin ? '.min.js' : '.js';
|
||||||
|
|
||||||
// 2. 根据当前 loader.js 来源自动适配镜像
|
if (currentUrl && (currentUrl.includes('jsdelivr.net') || currentUrl.includes('unpkg.com') || currentUrl.includes('npm.elemecdn.com'))) {
|
||||||
if (currentUrl) {
|
let cdnBase = 'https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}';
|
||||||
if (currentUrl.includes('esm.sh')) {
|
if (currentUrl.includes('unpkg.com')) cdnBase = 'https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}';
|
||||||
tpl = 'https://esm.sh/@apigo.cc/{project}@{tag}/dist/{project}.min.js';
|
else if (currentUrl.includes('npm.elemecdn.com')) cdnBase = 'https://npm.elemecdn.com/@apigo.cc/{project}@{tag}/dist/{project}';
|
||||||
} else if (currentUrl.includes('unpkg.com')) {
|
tpl = cdnBase + ext;
|
||||||
tpl = 'https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js';
|
} else if (currentUrl) {
|
||||||
} else if (currentUrl.includes('unpkg.zhimg.com')) {
|
const baseDir = currentUrl.substring(0, currentUrl.lastIndexOf('/') + 1);
|
||||||
tpl = 'https://unpkg.zhimg.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js';
|
tpl = baseDir + '{project}' + ext;
|
||||||
} else if (currentUrl.includes('npm.elemecdn.com')) {
|
|
||||||
tpl = 'https://npm.elemecdn.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js';
|
|
||||||
} else if (currentUrl.includes('jsdelivr.net')) {
|
|
||||||
tpl = 'https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}.min.js';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const importMap = { imports: {} };
|
|
||||||
|
|
||||||
pkgs.forEach(pkg => {
|
|
||||||
let [name, version] = pkg.split(':');
|
|
||||||
const key = name.toLowerCase();
|
|
||||||
const fullKey = `@apigo.cc/${key}`;
|
|
||||||
|
|
||||||
let url = '';
|
|
||||||
// 优先检查本地 localStorage 重定向配置,方便开发调试
|
|
||||||
try {
|
|
||||||
if (typeof localStorage !== 'undefined') {
|
|
||||||
url = localStorage.getItem(`dev:${fullKey}`) || '';
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
version = version || DEFAULT_VERSIONS[key] || 'latest';
|
|
||||||
url = tpl.replace(/{project}/g, key).replace(/{tag}/g, version);
|
|
||||||
}
|
|
||||||
importMap.imports[fullKey] = url;
|
|
||||||
});
|
|
||||||
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.type = 'importmap';
|
|
||||||
script.textContent = JSON.stringify(importMap);
|
|
||||||
|
|
||||||
if (currentScript) {
|
|
||||||
currentScript.parentNode.insertBefore(script, currentScript);
|
|
||||||
} else {
|
} else {
|
||||||
|
tpl = 'https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}' + ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解析包名与显式指定的版本号
|
||||||
|
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. 分析依赖并排序
|
||||||
|
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));
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
script.src = url;
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
}
|
}
|
||||||
|
Loader._loaded.add(name);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 挂载到全局
|
// 立即解析参数
|
||||||
globalThis.Loader = Loader;
|
if (typeof document !== 'undefined' && document.currentScript) {
|
||||||
|
const loaderUrl = new URL(document.currentScript.src, location.href);
|
||||||
|
Loader._v = loaderUrl.searchParams.get('v');
|
||||||
|
|
||||||
export { Loader };
|
let toLoad = loaderUrl.searchParams.get('load');
|
||||||
export default Loader;
|
const hash = loaderUrl.hash.substring(1);
|
||||||
|
if (!toLoad && hash) {
|
||||||
|
toLoad = hash.startsWith('load=') ? hash.substring(5) : hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toLoad) {
|
||||||
|
Loader.load(...toLoad.split(','));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.__apigo_load = Loader.load;
|
||||||
|
|||||||
34
test/index.html
Normal file
34
test/index.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<!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>
|
||||||
33
test/loader.spec.js
Normal file
33
test/loader.spec.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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');
|
||||||
|
});
|
||||||
3
test/mock-base.js
Normal file
3
test/mock-base.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const HTTP = { isMock: true, name: 'base-mock' };
|
||||||
|
export const UI = { toast: () => {} };
|
||||||
|
export default { HTTP, UI };
|
||||||
2
test/mock-bootstrap.js
Normal file
2
test/mock-bootstrap.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const Bootstrap = { isMock: true, name: 'bootstrap-mock' };
|
||||||
|
export default Bootstrap;
|
||||||
@ -6,22 +6,29 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/index.js'),
|
entry: resolve(__dirname, 'src/index.js'),
|
||||||
name: 'Loader',
|
name: 'ApigoLoader',
|
||||||
formats: ['iife'] // 使用 IIFE 格式,方便直接在 script 标签中使用
|
formats: ['umd', 'es']
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
format: 'iife',
|
format: 'umd',
|
||||||
name: 'Loader',
|
name: 'ApigoLoader',
|
||||||
entryFileNames: 'loader.js',
|
entryFileNames: 'loader.js'
|
||||||
extend: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: 'iife',
|
format: 'umd',
|
||||||
name: 'Loader',
|
name: 'ApigoLoader',
|
||||||
entryFileNames: 'loader.min.js',
|
entryFileNames: 'loader.min.js',
|
||||||
extend: true,
|
plugins: [terser()]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: 'es',
|
||||||
|
entryFileNames: 'loader.mjs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: 'es',
|
||||||
|
entryFileNames: 'loader.min.mjs',
|
||||||
plugins: [terser()]
|
plugins: [terser()]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user