diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..f3b1a9f --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} diff --git a/README.md b/README.md index b03c4a5..26dd8fc 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,52 @@ -# @web/loader 模块调度中心 +# @apigo.cc/loader 模块调度中心 -`@web/loader` 是整个 `@web` 体系的入口调度中心。它负责根据当前环境自动生成 `importmap`,并管理各模块的版本。 +`@apigo.cc/loader` 是整个 `@apigo.cc` 体系的入口调度中心。它负责根据当前环境自动生成 `importmap`,并管理各模块的版本。 --- ## 一、 集成方式 -### 方式一:内联引导 (推荐 - 无额外请求) -在 HTML 的 `
` 顶部加入以下代码。这种方式不需要额外请求 `loader.min.js` 文件。 +直接引用打包好的 `loader.min.js`,它会自动根据当前的加载源适配 CDN 镜像。 + +### 1. 自动注入 Importmap +在 HTML 中引入 loader 并声明需要加载的模块: ```html + + + + + + ``` -### 方式二:引用外部 Loader -直接引用打包好的 `loader.min.js`。它会自动根据当前的 URL 模板进行调度。 +### 2. 原生朴素写法 (手动定义) +如果您不想使用 `Loader.load` 的自动注入,也可以手动定义 `importmap`: ```html - - + + ``` @@ -42,17 +54,48 @@ ## 二、 核心特性 -1. **智能模板 (Smart Templating)**:自动根据 `loader.js` 的加载路径推断其他组件的下载路径。 -2. **默认版本 (Default Versioning)**:构建时内置了 `@web` 家族各成员的推荐稳定版本,解决“版本选择困难症”。 -3. **按需加载 (On-demand)**:在页面头部一次性声明所需模块,避免加载无关资源。 +1. **多镜像自适应**:自动识别 `loader.min.js` 的来源 URL,并自动将其他模块指向相同的 CDN 镜像。支持以下镜像: + * `cdn.jsdelivr.net` (默认) + * `esm.sh` + * `unpkg.com` + * `unpkg.zhimg.com` (中国加速) + * `npm.elemecdn.com` (中国加速) +2. **默认版本管理**:内置了各组件的稳定版本号,无需手动记忆。 +3. **零配置**:只需一行代码即可完成整个生态链的模块映射。 --- -## 三、 支持模块清单 +## 三、 本地开发与调试重定向 + +在本地开发时,如果需要将某些模块重定向到本地开发服务器进行调试,可以通过在浏览器控制台设置 `localStorage` 来实现,**无需修改任何 HTML 业务代码**。 + +### 1. 启用本地调试重定向 +比如将 `bootstrap` 和 `state` 模块重定向到您本地运行的 Vite 开发服务器: +```javascript +// 重定向 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'); +``` + +--- + +## 四、 支持模块清单 +所有模块均发布在 `@apigo.cc` 组织下: * `state` (原子状态机) * `bootstrap` (UI 引擎) * `base` (常用控件库) -* `dataTable` (高性能表格) +* `datatable` (高性能表格) * `kanban` (看板) * `mindmap` (思维导图) * `chart` (图表) +* `editor` (代码编辑器) diff --git a/dist/loader.js b/dist/loader.js index df6409b..acb58f7 100644 --- a/dist/loader.js +++ b/dist/loader.js @@ -1,41 +1,52 @@ (function(exports) { "use strict"; const DEFAULT_VERSIONS = { - "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" + "state": "1.0.11", + "bootstrap": "1.0.2", + "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.0" }; const Loader = { - /** - * 加载指定模块并注入 importmap - * @param {...string} pkgs 模块名格式: 'name' 或 'name:version' - */ load: (...pkgs) => { if (typeof document === "undefined") return; const currentScript = document.currentScript; const currentUrl = currentScript ? currentScript.src : ""; - let tpl = "https://apigo.cc/web/{project}/raw/tag/{tag}/dist/{project}.js"; - if (currentUrl.includes("/loader/")) { - const match = currentUrl.match(/(.*\/web\/)loader\/raw\/tag\/([^\/]+)\//); - if (match) { - const baseUrl = match[1]; - const loaderTag = match[2]; - tpl = `${baseUrl}{project}/raw/tag/{tag}/dist/{project}.js`; - Object.keys(DEFAULT_VERSIONS).forEach((k) => { - if (DEFAULT_VERSIONS[k] === "v1.0.0") DEFAULT_VERSIONS[k] = loaderTag; - }); + let tpl = "https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}.min.js"; + if (currentUrl) { + if (currentUrl.includes("esm.sh")) { + tpl = "https://esm.sh/@apigo.cc/{project}@{tag}/dist/{project}.min.js"; + } else if (currentUrl.includes("unpkg.com")) { + tpl = "https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js"; + } else if (currentUrl.includes("unpkg.zhimg.com")) { + tpl = "https://unpkg.zhimg.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js"; + } 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(":"); - version = version || DEFAULT_VERSIONS[name] || "main"; - const url = tpl.replace(/{project}/g, name).replace(/{tag}/g, version); - importMap.imports[`@web/${name}`] = url; + 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"; diff --git a/dist/loader.min.js b/dist/loader.min.js index 2ad1c15..c796f1c 100644 --- a/dist/loader.min.js +++ b/dist/loader.min.js @@ -1 +1 @@ -!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||{}); +!function(t){"use strict";const e={state:"1.0.11",bootstrap:"1.0.2",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.0"},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||{}); diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index a51502b..dbf0e92 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1,5 +1,5 @@ { - "name": "loader", + "name": "@apigo.cc/loader", "version": "1.0.0", "lockfileVersion": 3, "requires": true, diff --git a/package-lock.json b/package-lock.json index ebb36af..21f2c6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "loader", + "name": "@apigo.cc/loader", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "loader", + "name": "@apigo.cc/loader", "version": "1.0.0", "license": "ISC", "devDependencies": { diff --git a/package.json b/package.json index 93a8f3e..dd90cf7 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,17 @@ { - "name": "loader", + "name": "@apigo.cc/loader", "version": "1.0.0", "type": "module", + "main": "dist/loader.js", + "module": "dist/loader.js", + "files": [ + "dist" + ], "scripts": { "dev": "vite", - "build": "vite build" + "prebuild": "node scripts/update-versions.js", + "build": "vite build", + "pub": "node scripts/publish.js" }, "keywords": [], "author": "", diff --git a/scripts/publish.js b/scripts/publish.js new file mode 100644 index 0000000..b562aa9 --- /dev/null +++ b/scripts/publish.js @@ -0,0 +1,48 @@ +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); +} diff --git a/scripts/update-versions.js b/scripts/update-versions.js new file mode 100644 index 0000000..e3dfc95 --- /dev/null +++ b/scripts/update-versions.js @@ -0,0 +1,115 @@ +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) => { + // 提取被单引号/双引号包裹的各个 package 项 + const items = p1.split(',').map(item => item.trim().replace(/['"]/g, '')); + const newItems = items.map(item => { + const [name] = item.split(':'); + const key = name.toLowerCase(); + if (versions[key]) { + return `'${key}:${versions[key]}'`; + } + return `'${item}'`; + }); + return `Loader.load(${newItems.join(', ')})`; + } + ); + + // 替换 cdn 链接里的 loader 版本 + // 如 + readmeContent = readmeContent.replace( + /(\/\/cdn\.jsdelivr\.net\/npm\/@apigo\.cc\/loader@)[0-9.]+(\/dist\/loader\.min\.js)/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.'); +} diff --git a/src/index.js b/src/index.js index ed6dcc1..ab2f494 100644 --- a/src/index.js +++ b/src/index.js @@ -4,42 +4,39 @@ */ const DEFAULT_VERSIONS = { - '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' + 'state': '1.0.11', + 'bootstrap': '1.0.2', + '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.0' }; const Loader = { - /** - * 加载指定模块并注入 importmap - * @param {...string} pkgs 模块名格式: 'name' 或 'name:version' - */ load: (...pkgs) => { if (typeof document === 'undefined') return; const currentScript = document.currentScript; const currentUrl = currentScript ? currentScript.src : ''; - // 解析 URL 模板 - // 假设当前 URL 是 https://apigo.cc/web/loader/raw/tag/v1.0.0/dist/loader.min.js - // 目标模板 https://apigo.cc/web/{project}/raw/tag/{tag}/dist/{project}.js - let tpl = 'https://apigo.cc/web/{project}/raw/tag/{tag}/dist/{project}.js'; - - if (currentUrl.includes('/loader/')) { - // 自动推断基础路径和 tag - const match = currentUrl.match(/(.*\/web\/)loader\/raw\/tag\/([^\/]+)\//); - if (match) { - const baseUrl = match[1]; - const loaderTag = match[2]; - tpl = `${baseUrl}{project}/raw/tag/{tag}/dist/{project}.js`; - // 默认情况下其他库也使用和 loader 相同的 tag,除非另有定义 - Object.keys(DEFAULT_VERSIONS).forEach(k => { - if (DEFAULT_VERSIONS[k] === 'v1.0.0') DEFAULT_VERSIONS[k] = loaderTag; - }); + // 1. 默认模板 (jsDelivr) + let tpl = 'https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}.min.js'; + + // 2. 根据当前 loader.js 来源自动适配镜像 + if (currentUrl) { + if (currentUrl.includes('esm.sh')) { + tpl = 'https://esm.sh/@apigo.cc/{project}@{tag}/dist/{project}.min.js'; + } else if (currentUrl.includes('unpkg.com')) { + tpl = 'https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js'; + } else if (currentUrl.includes('unpkg.zhimg.com')) { + tpl = 'https://unpkg.zhimg.com/@apigo.cc/{project}@{tag}/dist/{project}.min.js'; + } 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'; } } @@ -47,15 +44,22 @@ const Loader = { 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; - - // 特殊处理:如果是 bootstrap,可能需要额外处理或依赖 - if (name === 'bootstrap') { - // bootstrap 内部现在是自包含的,直接映射即可 + 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');