create-ax-trusted-plugin 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,55 +1,56 @@
1
- # create-ax-trusted-plugin
2
-
3
- 可信(internal)插件**脚手架**:一条命令生成一个基于 `@ax-npm/host-trusted-sdk-v4` 的可信插件骨架
4
- (Module Federation remote + 共享 Vue + `activate(ctx)` + 自定义元素面板 + 调 `map.flyTo` + 遥测订阅)。
5
-
6
- > 脚手架本身已发**公网 npm**,生成这步**零配置**;生成出的工程才走私有源(`192.168.1.209` GitLab)拉
7
- > `@ax-npm/*` 依赖,届时只需一个 `GITLAB_TOKEN`(无需手建任何 `.npmrc`,工程自带)
8
-
9
- ## 用法
10
- ```bash
11
- # 交互式(缺参数会提示输入 id/标题/锚点)
12
- npm create ax-trusted-plugin@latest my-panel
13
-
14
- # 非交互(传参)
15
- npm create ax-trusted-plugin@latest my-panel -- --title "我的面板" --anchor top-right
16
- ```
17
- 等价于直接跑 bin:`node bin/index.mjs my-panel --title "我的面板" --anchor top-right`。
18
-
19
- 生成 `my-panel/` 后(把 `GITLAB_TOKEN` 换成你的只读 PAT,按所用 shell 选一行):
20
- ```bash
21
- cd my-panel
22
- export GITLAB_TOKEN=<读权限 PAT> # bash / git-bash
23
- # $env:GITLAB_TOKEN="<读权限 PAT>" # PowerShell
24
- # set GITLAB_TOKEN=<读权限 PAT> # cmd
25
- npm install
26
- npm run dev
27
- ```
28
-
29
- ## 参数
30
- | 参数 | 说明 | 默认 |
31
- |---|---|---|
32
- | `<id>`(位置参数) | 插件 id,小写短横线 | 必填(缺则提示) |
33
- | `--title` | 面板标题 | id 推导 |
34
- | `--anchor` | 浮层锚点 | `top-right` |
35
-
36
- 锚点可选:`top-left/top-right/top-center/bottom-left/bottom-right/left-center/right-center/center`。
37
-
38
- ## 生成内容
39
- `template/` 经占位替换(`__ID__`/`__TITLE__`/`__ANCHOR__`/`__FED_NAME__`)后落地:`src/{index.ts,Panel.vue,state.ts}`、
40
- `manifest.json`(trust:internal)、`vite.config.ts`(federation remote)、`serve.mjs`(dev 静态源)、`package.json`、
41
- `tsconfig.json`、`.npmrc`、`.gitignore`、`README.md`。
42
-
43
- ## 发布(维护者)
44
- 脚手架包发**公网 npm**(`publishConfig` 已指向 `registry.npmjs.org`、`access: public`):
45
- ```bash
46
- npm login --registry https://registry.npmjs.org/ # 首次
47
- npm publish --otp=<6位验证码> # npm 强制 2FA,验证码来自验证器 App
48
- ```
49
- - 若提示包名被占,改 `package.json` 的 `name`(及随之的 `npm create <name>` 用法)。
50
- - 自动化/CI 免输码:在 npm 生成「可绕过 2FA Granular Access Token」,
51
- `//registry.npmjs.org/:_authToken=<token>` 注入后 `npm publish`。
52
-
53
- > 注意:`template/` 内仍引用私网地址 `192.168.1.209`(RFC1918 不可路由,非机密),发公网后会公开可见。
54
-
55
- 源码仓:GitLab 项目 1244。
1
+ # create-ax-trusted-plugin
2
+
3
+ 可信(internal)插件**脚手架**:一条命令生成一个基于 `@ax-npm/host-trusted-sdk-v4` 的可信插件骨架
4
+ (默认「形态一」:ESM 直出 `activate` + 自带 Vue + 自定义元素面板 + 调 `map.flyTo` + 遥测订阅;
5
+ dev 走裸 `vite`,原生 sourcemap、无构建步)。
6
+
7
+ > 脚手架本身已发**公网 npm**,生成这步**零配置**;生成出的工程才走私有源(`192.168.1.209` GitLab)
8
+ > `@ax-npm/*` 依赖,届时只需一个 `GITLAB_TOKEN`(无需手建任何 `.npmrc`,工程自带)。
9
+
10
+ ## 用法
11
+ ```bash
12
+ # 交互式(缺参数会提示输入 id/标题/锚点)
13
+ npm create ax-trusted-plugin@latest my-panel
14
+
15
+ # 非交互(传参)
16
+ npm create ax-trusted-plugin@latest my-panel -- --title "我的面板" --anchor top-right
17
+ ```
18
+ 等价于直接跑 bin:`node bin/index.mjs my-panel --title "我的面板" --anchor top-right`。
19
+
20
+ 生成 `my-panel/` 后(把 `GITLAB_TOKEN` 换成你的只读 PAT,按所用 shell 选一行):
21
+ ```bash
22
+ cd my-panel
23
+ export GITLAB_TOKEN=<读权限 PAT> # bash / git-bash
24
+ # $env:GITLAB_TOKEN="<读权限 PAT>" # PowerShell
25
+ # set GITLAB_TOKEN=<读权限 PAT> # cmd
26
+ npm install
27
+ npm run dev
28
+ ```
29
+
30
+ ## 参数
31
+ | 参数 | 说明 | 默认 |
32
+ |---|---|---|
33
+ | `<id>`(位置参数) | 插件 id,小写短横线 | 必填(缺则提示) |
34
+ | `--title` | 面板标题 | id 推导 |
35
+ | `--anchor` | 浮层锚点 | `top-right` |
36
+
37
+ 锚点可选:`top-left/top-right/top-center/bottom-left/bottom-right/left-center/right-center/center`。
38
+
39
+ ## 生成内容
40
+ `template/` 经占位替换(`__ID__`/`__TITLE__`/`__ANCHOR__`)后落地:`src/{index.ts,Panel.vue,state.ts}`、
41
+ `manifest.json`(trust:internal、entry:main.js)、`vite.config.ts`(形态一 lib 构建 + dev 市场源桥)、
42
+ `package.json`、`tsconfig.json`、`.npmrc`、`.gitignore`、`README.md`。
43
+
44
+ ## 发布(维护者)
45
+ 脚手架包发**公网 npm**(`publishConfig` 已指向 `registry.npmjs.org`、`access: public`):
46
+ ```bash
47
+ npm login --registry https://registry.npmjs.org/ # 首次
48
+ npm publish --otp=<6位验证码> # npm 强制 2FA,验证码来自验证器 App
49
+ ```
50
+ - 若提示包名被占,改 `package.json` `name`(及随之的 `npm create <name>` 用法)。
51
+ - 自动化/CI 免输码:在 npm 生成「可绕过 2FA 的 Granular Access Token」,
52
+ 以 `//registry.npmjs.org/:_authToken=<token>` 注入后 `npm publish`。
53
+
54
+ > 注意:`template/` 内仍引用私网地址 `192.168.1.209`(RFC1918 不可路由,非机密),发公网后会公开可见。
55
+
56
+ 源码仓:GitLab 项目 1244。
package/bin/index.mjs CHANGED
@@ -1,112 +1,111 @@
1
- #!/usr/bin/env node
2
- // create-ax-trusted-plugin —— 可信(internal)插件脚手架(发布于公网 npm)。
3
- // 用法:
4
- // npm create ax-trusted-plugin <id> [-- --title "标题" --anchor top-right]
5
- // node bin/index.mjs <id> --title "标题" --anchor top-right
6
- // 把 template/ 复制到 <id>/,按 id/title/anchor 做占位替换(dotfile 以 _ 前缀存放,生成时改名)。
7
- import { readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises';
8
- import { existsSync } from 'node:fs';
9
- import { fileURLToPath } from 'node:url';
10
- import { dirname, join, resolve, relative } from 'node:path';
11
- import { createInterface } from 'node:readline/promises';
12
-
13
- const here = dirname(fileURLToPath(import.meta.url));
14
- const TEMPLATE = resolve(here, '..', 'template');
15
-
16
- const ANCHORS = [
17
- 'top-left', 'top-right', 'top-center',
18
- 'bottom-left', 'bottom-right',
19
- 'left-center', 'right-center', 'center',
20
- ];
21
-
22
- function parseArgs(argv) {
23
- const out = { _: [] };
24
- for (let i = 0; i < argv.length; i++) {
25
- const a = argv[i];
26
- if (a.startsWith('--')) out[a.slice(2)] = argv[++i] ?? '';
27
- else out._.push(a);
28
- }
29
- return out;
30
- }
31
-
32
- function toTitle(id) {
33
- return id.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
34
- }
35
-
36
- async function prompt(rl, q, def) {
37
- if (!process.stdin.isTTY) return def;
38
- const ans = (await rl.question(`${q}${def ? ` (${def})` : ''}: `)).trim();
39
- return ans || def;
40
- }
41
-
42
- // 复制 + 占位替换 + dotfile 改名(_gitignore→.gitignore、_npmrc→.npmrc)。
43
- async function scaffold(srcDir, destDir, vars) {
44
- await mkdir(destDir, { recursive: true });
45
- for (const name of await readdir(srcDir)) {
46
- const src = join(srcDir, name);
47
- const s = await stat(src);
48
- const outName = name === '_gitignore' ? '.gitignore' : name === '_npmrc' ? '.npmrc' : name;
49
- const dest = join(destDir, outName);
50
- if (s.isDirectory()) {
51
- await scaffold(src, dest, vars);
52
- } else {
53
- let body = await readFile(src, 'utf8');
54
- for (const [k, v] of Object.entries(vars)) body = body.split(k).join(v);
55
- await writeFile(dest, body);
56
- }
57
- }
58
- }
59
-
60
- async function main() {
61
- const args = parseArgs(process.argv.slice(2));
62
- const rl = createInterface({ input: process.stdin, output: process.stdout });
63
- try {
64
- let id = args._[0] || args.id;
65
- if (!id) id = await prompt(rl, '插件 id(小写短横线,如 my-panel)', '');
66
- id = String(id || '').trim().toLowerCase();
67
- if (!/^[a-z][a-z0-9-]*$/.test(id)) {
68
- console.error('✗ 非法 id:须小写字母开头,仅含小写字母/数字/短横线。');
69
- process.exit(1);
70
- }
71
-
72
- const title = args.title || (await prompt(rl, '面板标题', toTitle(id)));
73
- let anchor = args.anchor || (await prompt(rl, `锚点(${ANCHORS.join('/')})`, 'top-right'));
74
- anchor = String(anchor).trim();
75
- if (!ANCHORS.includes(anchor)) {
76
- console.error(`✗ 非法 anchor:${anchor}。可选:${ANCHORS.join(', ')}`);
77
- process.exit(1);
78
- }
79
-
80
- const destDir = resolve(process.cwd(), args.dir || id);
81
- if (existsSync(destDir)) {
82
- console.error(`✗ 目标已存在:${destDir}`);
83
- process.exit(1);
84
- }
85
-
86
- const vars = {
87
- __ID__: id,
88
- __TITLE__: title,
89
- __ANCHOR__: anchor,
90
- __FED_NAME__: id.replace(/[^a-zA-Z0-9]/g, '_'),
91
- };
92
- await scaffold(TEMPLATE, destDir, vars);
93
-
94
- const rel = relative(process.cwd(), destDir) || '.';
95
- console.log(`\n✓ 已生成可信插件:${rel}`);
96
- console.log('\n下一步:');
97
- console.log(` cd ${rel}`);
98
- console.log(' # 设只读 PAT(按所用 shell 选一行):');
99
- console.log(' export GITLAB_TOKEN=<读权限 PAT> # bash / git-bash');
100
- console.log(' $env:GITLAB_TOKEN="<读权限 PAT>" # PowerShell');
101
- console.log(' set GITLAB_TOKEN=<读权限 PAT> # cmd');
102
- console.log(' npm install');
103
- console.log(' npm run dev # build:watch + 静态托管 + host-dev,浏览器调试\n');
104
- } finally {
105
- rl.close();
106
- }
107
- }
108
-
109
- main().catch((e) => {
110
- console.error(e);
111
- process.exit(1);
112
- });
1
+ #!/usr/bin/env node
2
+ // create-ax-trusted-plugin —— 可信(internal)插件脚手架(发布于公网 npm)。
3
+ // 用法:
4
+ // npm create ax-trusted-plugin <id> [-- --title "标题" --anchor top-right]
5
+ // node bin/index.mjs <id> --title "标题" --anchor top-right
6
+ // 把 template/ 复制到 <id>/,按 id/title/anchor 做占位替换(dotfile 以 _ 前缀存放,生成时改名)。
7
+ import { readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises';
8
+ import { existsSync } from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { dirname, join, resolve, relative } from 'node:path';
11
+ import { createInterface } from 'node:readline/promises';
12
+
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ const TEMPLATE = resolve(here, '..', 'template');
15
+
16
+ const ANCHORS = [
17
+ 'top-left', 'top-right', 'top-center',
18
+ 'bottom-left', 'bottom-right',
19
+ 'left-center', 'right-center', 'center',
20
+ ];
21
+
22
+ function parseArgs(argv) {
23
+ const out = { _: [] };
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a.startsWith('--')) out[a.slice(2)] = argv[++i] ?? '';
27
+ else out._.push(a);
28
+ }
29
+ return out;
30
+ }
31
+
32
+ function toTitle(id) {
33
+ return id.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
34
+ }
35
+
36
+ async function prompt(rl, q, def) {
37
+ if (!process.stdin.isTTY) return def;
38
+ const ans = (await rl.question(`${q}${def ? ` (${def})` : ''}: `)).trim();
39
+ return ans || def;
40
+ }
41
+
42
+ // 复制 + 占位替换 + dotfile 改名(_gitignore→.gitignore、_npmrc→.npmrc)。
43
+ async function scaffold(srcDir, destDir, vars) {
44
+ await mkdir(destDir, { recursive: true });
45
+ for (const name of await readdir(srcDir)) {
46
+ const src = join(srcDir, name);
47
+ const s = await stat(src);
48
+ const outName = name === '_gitignore' ? '.gitignore' : name === '_npmrc' ? '.npmrc' : name;
49
+ const dest = join(destDir, outName);
50
+ if (s.isDirectory()) {
51
+ await scaffold(src, dest, vars);
52
+ } else {
53
+ let body = await readFile(src, 'utf8');
54
+ for (const [k, v] of Object.entries(vars)) body = body.split(k).join(v);
55
+ await writeFile(dest, body);
56
+ }
57
+ }
58
+ }
59
+
60
+ async function main() {
61
+ const args = parseArgs(process.argv.slice(2));
62
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
63
+ try {
64
+ let id = args._[0] || args.id;
65
+ if (!id) id = await prompt(rl, '插件 id(小写短横线,如 my-panel)', '');
66
+ id = String(id || '').trim().toLowerCase();
67
+ if (!/^[a-z][a-z0-9-]*$/.test(id)) {
68
+ console.error('✗ 非法 id:须小写字母开头,仅含小写字母/数字/短横线。');
69
+ process.exit(1);
70
+ }
71
+
72
+ const title = args.title || (await prompt(rl, '面板标题', toTitle(id)));
73
+ let anchor = args.anchor || (await prompt(rl, `锚点(${ANCHORS.join('/')})`, 'top-right'));
74
+ anchor = String(anchor).trim();
75
+ if (!ANCHORS.includes(anchor)) {
76
+ console.error(`✗ 非法 anchor:${anchor}。可选:${ANCHORS.join(', ')}`);
77
+ process.exit(1);
78
+ }
79
+
80
+ const destDir = resolve(process.cwd(), args.dir || id);
81
+ if (existsSync(destDir)) {
82
+ console.error(`✗ 目标已存在:${destDir}`);
83
+ process.exit(1);
84
+ }
85
+
86
+ const vars = {
87
+ __ID__: id,
88
+ __TITLE__: title,
89
+ __ANCHOR__: anchor,
90
+ };
91
+ await scaffold(TEMPLATE, destDir, vars);
92
+
93
+ const rel = relative(process.cwd(), destDir) || '.';
94
+ console.log(`\n✓ 已生成可信插件:${rel}`);
95
+ console.log('\n下一步:');
96
+ console.log(` cd ${rel}`);
97
+ console.log(' # 设只读 PAT(按所用 shell 选一行):');
98
+ console.log(' export GITLAB_TOKEN=<读权限 PAT> # bash / git-bash');
99
+ console.log(' $env:GITLAB_TOKEN="<读权限 PAT>" # PowerShell');
100
+ console.log(' set GITLAB_TOKEN=<读权限 PAT> # cmd');
101
+ console.log(' npm install');
102
+ console.log(' npm run dev # 裸 vite(市场源,原生 sourcemap) + host-dev 真宿主,浏览器调试\n');
103
+ } finally {
104
+ rl.close();
105
+ }
106
+ }
107
+
108
+ main().catch((e) => {
109
+ console.error(e);
110
+ process.exit(1);
111
+ });
package/package.json CHANGED
@@ -1,28 +1,27 @@
1
- {
2
- "name": "create-ax-trusted-plugin",
3
- "version": "1.0.4",
4
- "description": "可信(internal)插件脚手架:一条命令生成一个基于 @ax-npm/host-trusted-sdk-v4 的可信插件骨架(Module Federation remote + 共享 Vue + activate(ctx))。用法:npm create ax-trusted-plugin <id>。生成器本身无机密,公网可装;生成出的工程仍走私有源拉 @ax-npm/* 依赖。",
5
- "type": "module",
6
- "license": "MIT",
7
- "keywords": [
8
- "create-ax-trusted-plugin",
9
- "scaffold",
10
- "module-federation",
11
- "vue",
12
- "plugin"
13
- ],
14
- "bin": {
15
- "create-ax-trusted-plugin": "bin/index.mjs"
16
- },
17
- "files": [
18
- "bin",
19
- "template"
20
- ],
21
- "engines": {
22
- "node": ">=18"
23
- },
24
- "publishConfig": {
25
- "registry": "https://registry.npmjs.org/",
26
- "access": "public"
27
- }
28
- }
1
+ {
2
+ "name": "create-ax-trusted-plugin",
3
+ "version": "1.0.6",
4
+ "description": "可信(internal)插件脚手架:一条命令生成一个基于 @ax-npm/host-trusted-sdk-v4 的可信插件骨架(默认形态一:ESM 直出 activate + 自带 Vue,dev vite 原生 sourcemap)。用法:npm create ax-trusted-plugin <id>。生成器本身无机密,公网可装;生成出的工程仍走私有源拉 @ax-npm/* 依赖。",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "create-ax-trusted-plugin",
9
+ "scaffold",
10
+ "vue",
11
+ "plugin"
12
+ ],
13
+ "bin": {
14
+ "create-ax-trusted-plugin": "bin/index.mjs"
15
+ },
16
+ "files": [
17
+ "bin",
18
+ "template"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "publishConfig": {
24
+ "registry": "https://registry.npmjs.org/",
25
+ "access": "public"
26
+ }
27
+ }
@@ -1,30 +1,33 @@
1
- # __ID__
2
-
3
- 可信(internal)插件:经 Module Federation 融入宿主进程、共享宿主 Vue、拿完整 `ctx`。
4
- `create-ax-trusted-plugin` 脚手架生成。
5
-
6
- ## 开发
7
- ```bash
8
- export GITLAB_TOKEN=<读权限 PAT> # bash / git-bash
9
- # $env:GITLAB_TOKEN="<读权限 PAT>" # PowerShell(别用 set / %VAR%)
10
- # set GITLAB_TOKEN=<读权限 PAT> # cmd
11
- npm install
12
- npm run dev # build:watch + serve(:5174) + host-dev(:5173)
13
- ```
14
- 浏览器打开 `http://localhost:5173`,锚点出现「__TITLE__」面板。改代码自动重建 → 刷新浏览器。
15
-
16
- > ⚠️ 可信插件不能用 `vite dev`(federation dev 不产可加载 remoteEntry),故用 `build --watch` + 静态托管。
17
-
18
- ## 构建 / 上架
19
- ```bash
20
- npm run build # → dist/assets/remoteEntry.js
21
- ```
22
- 把 `dist/` + `manifest.json` 部署到静态源,在宿主市场 `plugins.json` 登记本插件 id 即可加载。
23
-
24
- ## 关键约定
25
- - federation:`exposes:{'./plugin':'./src/index.ts'}` + `shared:['vue','pinia']`(**别设 requiredVersion**)。
26
- - `manifest.entry` = `assets/remoteEntry.js`,`mfExpose` exposes key 一致(`./plugin`)
27
- - 提供能力须命名空间化(`__ID__/...`)且在 `contributes.capabilities` 声明;消费走 `uses.capabilities`。
28
- - 插件间不互相 import,只走 `ctx.commands` / `ctx.events`。
29
-
30
- 完整说明见 `@ax-npm/host-trusted-sdk-v4` 的 README。
1
+ # __ID__
2
+
3
+ 可信(internal)插件「形态一」:模块直接 `export activate`,壳 `import` 即用;进程内拿完整 `ctx`。
4
+ Vue 打进本插件 bundle(与宿主/其它插件隔离)。由 `create-ax-trusted-plugin` 脚手架生成。
5
+
6
+ ## 开发
7
+ ```bash
8
+ export GITLAB_TOKEN=<读权限 PAT> # bash / git-bash
9
+ # $env:GITLAB_TOKEN="<读权限 PAT>" # PowerShell(别用 set / %VAR%)
10
+ # set GITLAB_TOKEN=<读权限 PAT> # cmd
11
+ npm install
12
+ npm run dev # concurrently: vite(:5174,市场源) + host-dev 真宿主(:5173)
13
+ ```
14
+ 浏览器打开 `http://localhost:5173`,锚点出现「__TITLE__」面板。**改代码刷新浏览器即生效**(原生 sourcemap)。
15
+
16
+ > dev 走裸 `vite`:`vite.config.ts` `hostSourceBridge` 把单插件服务伪装成市场源,
17
+ > `/<id>/main.js` 实时转译 `src/index.ts`。多插件并行各自 `vite --port 5175/5176/…`。
18
+
19
+ ## 构建 / 上架
20
+ ```bash
21
+ npm run build # → dist/main.js(+ main.js.map)
22
+ ```
23
+ 把 `dist/main.js`(+ `.map`)+ `manifest.json` 部署到静态源,在宿主市场 `plugins.json` 登记本插件 id(及 `baseUrl`)即可加载。
24
+
25
+ ## 关键约定
26
+ - 入口 `src/index.ts` 必须 `export function activate(ctx)`(可选 `elementTag`/`deactivate`);`manifest.entry`=`main.js`。
27
+ - `vue({customElement:true})` 把样式注入 shadow root;Vue 自带打包,不与宿主共享。
28
+ - 提供能力须命名空间化(`__ID__/...`)且在 `contributes.capabilities` 声明;消费(含**调自己提供的能力**)须在 `uses.capabilities` 声明——contributes 与 uses 是两道独立门,`execute` 一律过 uses 闸。
29
+ - 插件间不互相 import,只走 `ctx.commands` / `ctx.events`。
30
+
31
+ > 需要多个可信插件**共享单例** Vue / 跨边界共享响应式 store,或要省 Vue 体积时,才回退「形态二」
32
+ > (Module Federation):`vite.config` 改用 `@originjs/vite-plugin-federation`、`manifest` 加 `mfExpose`、
33
+ > dev 走 `vite build --watch` + 静态托管。详见 `@ax-npm/host-trusted-sdk-v4` 的 README 与《三角色开发方法.md》。
@@ -1,18 +1,17 @@
1
- {
2
- "id": "__ID__",
3
- "version": "0.1.0",
4
- "trust": "internal",
5
- "host": "^1.0.0",
6
- "entry": "assets/remoteEntry.js",
7
- "mfExpose": "./plugin",
8
- "activationEvents": ["onStartup"],
9
- "contributes": { "capabilities": ["__ID__/ping"] },
10
- "uses": { "capabilities": ["map.flyTo"] },
11
- "component": {
12
- "anchor": "__ANCHOR__",
13
- "width": 320,
14
- "height": 220,
15
- "title": "__TITLE__",
16
- "visible": true
17
- }
18
- }
1
+ {
2
+ "id": "__ID__",
3
+ "version": "0.1.0",
4
+ "trust": "internal",
5
+ "host": "^1.0.0",
6
+ "entry": "main.js",
7
+ "activationEvents": ["onStartup"],
8
+ "contributes": { "capabilities": ["__ID__/ping"] },
9
+ "uses": { "capabilities": ["map.flyTo", "__ID__/ping"] },
10
+ "component": {
11
+ "anchor": "__ANCHOR__",
12
+ "width": 320,
13
+ "height": 220,
14
+ "title": "__TITLE__",
15
+ "visible": true
16
+ }
17
+ }
@@ -1,30 +1,27 @@
1
- {
2
- "name": "__ID__",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "description": "可信(internal)插件:经 Module Federation 融入宿主进程、共享 Vue、拿完整 ctx。只依赖 @ax-npm/host-trusted-sdk-v4。",
7
- "scripts": {
8
- "build": "vue-tsc --noEmit && vite build",
9
- "build:watch": "vite build --watch",
10
- "serve": "node serve.mjs",
11
- "dev": "vite build && concurrently -k -n build,serve,host -c blue,magenta,green \"vite build --watch\" \"node serve.mjs\" \"host-dev --plugin http://localhost:5174\"",
12
- "dev:electron": "vite build && concurrently -k -n build,serve,host -c blue,magenta,green \"vite build --watch\" \"node serve.mjs\" \"host-dev --electron --plugin http://localhost:5174\"",
13
- "typecheck": "vue-tsc --noEmit"
14
- },
15
- "dependencies": {
16
- "@ax-npm/host-trusted-sdk-v4": "^1.0.2",
17
- "vue": "^3.4.0"
18
- },
19
- "devDependencies": {
20
- "@ax-npm/host-dev-v4": "^1.0.7",
21
- "@originjs/vite-plugin-federation": "^1.3.5",
22
- "@vitejs/plugin-vue": "^5.0.0",
23
- "concurrently": "^9.0.0",
24
- "electron": "^39.0.0",
25
- "pinia": "^2.1.7",
26
- "typescript": "^5.4.0",
27
- "vite": "^5.2.0",
28
- "vue-tsc": "^2.0.0"
29
- }
30
- }
1
+ {
2
+ "name": "__ID__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "可信(internal)插件「形态一」:ESM 直出 activate,壳 import 即用、Vue 自带打包(隔离)、拿完整 ctx。dev 走裸 vite(原生 sourcemap、无构建步)。只依赖 @ax-npm/host-trusted-sdk-v4。",
7
+ "scripts": {
8
+ "build": "vue-tsc --noEmit && vite build",
9
+ "serve": "vite",
10
+ "dev": "concurrently -k -n plugin,host -c blue,green \"vite\" \"host-dev --plugin http://localhost:5174\"",
11
+ "dev:electron": "concurrently -k -n plugin,host -c blue,green \"vite\" \"host-dev --electron --plugin http://localhost:5174\"",
12
+ "typecheck": "vue-tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "@ax-npm/host-trusted-sdk-v4": "^1.0.2",
16
+ "vue": "^3.4.0"
17
+ },
18
+ "devDependencies": {
19
+ "@ax-npm/host-dev-v4": "^1.0.7",
20
+ "@vitejs/plugin-vue": "^5.0.0",
21
+ "concurrently": "^9.0.0",
22
+ "electron": "^39.0.0",
23
+ "typescript": "^5.4.0",
24
+ "vite": "^5.2.0",
25
+ "vue-tsc": "^2.0.0"
26
+ }
27
+ }
@@ -1,41 +1,44 @@
1
- // 可信插件入口(打成 MF remote 的 expose './plugin')。
2
- // 宿主经容器协议加载本模块后调 activate(ctx),据 elementTag + manifest.component 挂自定义元素。
3
- import { definePluginElement, type PluginCtx } from '@ax-npm/host-trusted-sdk-v4';
4
- import Panel from './Panel.vue';
5
- import { state, type FcsBasic } from './state';
6
-
7
- export const elementTag = definePluginElement(Panel, { tag: 'plugin-__ID__' });
8
-
9
- export function activate(ctx: PluginCtx) {
10
- // 提供能力:id 须命名空间化 + 在 manifest.contributes.capabilities 声明。
11
- ctx.commands.register('__ID__/ping', async () => 'pong @ ' + new Date().toLocaleTimeString());
12
-
13
- // 订阅宿主遥测(广播)。
14
- ctx.events.on('telemetry:fcs.basic', (p) => {
15
- state.telemetry = p as FcsBasic;
16
- });
17
-
18
- // Panel 是自定义元素,拿不到 ctx;经 window 事件把用户动作桥回 activate 作用域。
19
- window.addEventListener('__ID__:ping', async () => {
20
- state.lastPing = String(await ctx.commands.execute('__ID__/ping'));
21
- });
22
- window.addEventListener('__ID__:flyto', async () => {
23
- try {
24
- // 调宿主地图能力(需在 manifest.uses.capabilities 声明 map.flyTo)
25
- await ctx.commands.execute('map.flyTo', 116.397, 39.908);
26
- state.flyToNote = '已飞至北京';
27
- } catch (e) {
28
- state.flyToNote = '失败:' + (e instanceof Error ? e.message : String(e));
29
- }
30
- });
31
-
32
- return {
33
- elementTag,
34
- onVisible() {},
35
- onHidden() {},
36
- };
37
- }
38
-
39
- export function deactivate() {
40
- // 仅收自有资源;经 ctx 注册的命令/事件由运行时自动回收。
41
- }
1
+ // 可信插件入口「形态一」:本模块直接导出 activate/elementTag(manifest.entry main.js)。
2
+ // 宿主 loader `import(url)` 命中「直出 activate」分支,调 activate(ctx),据 elementTag +
3
+ // manifest.component 挂自定义元素。(业务代码与形态二一致,只差打包/加载方式。)
4
+ import { definePluginElement, type PluginCtx } from '@ax-npm/host-trusted-sdk-v4';
5
+ import Panel from './Panel.vue';
6
+ import { state, type FcsBasic } from './state';
7
+
8
+ export const elementTag = definePluginElement(Panel, { tag: 'plugin-__ID__' });
9
+
10
+ export function activate(ctx: PluginCtx) {
11
+ // 提供能力:id 须命名空间化 + manifest.contributes.capabilities 声明。
12
+ // 注意:contributes(提供)与 uses(调用)是两道独立门——execute 走 uses 闸,**调自己
13
+ // register 的能力也要在 manifest.uses.capabilities 声明**(见下方 ③ 的 __ID__/ping)。
14
+ ctx.commands.register('__ID__/ping', async () => 'pong @ ' + new Date().toLocaleTimeString());
15
+
16
+ // ② 订阅宿主遥测(广播)
17
+ ctx.events.on('telemetry:fcs.basic', (p) => {
18
+ state.telemetry = p as FcsBasic;
19
+ });
20
+
21
+ // ③ Panel 是自定义元素,拿不到 ctx;经 window 事件把用户动作桥回 activate 作用域。
22
+ window.addEventListener('__ID__:ping', async () => {
23
+ state.lastPing = String(await ctx.commands.execute('__ID__/ping'));
24
+ });
25
+ window.addEventListener('__ID__:flyto', async () => {
26
+ try {
27
+ // 调宿主地图能力(需在 manifest.uses.capabilities 声明 map.flyTo)
28
+ await ctx.commands.execute('map.flyTo', 116.397, 39.908);
29
+ state.flyToNote = '已飞至北京';
30
+ } catch (e) {
31
+ state.flyToNote = '失败:' + (e instanceof Error ? e.message : String(e));
32
+ }
33
+ });
34
+
35
+ return {
36
+ elementTag,
37
+ onVisible() {},
38
+ onHidden() {},
39
+ };
40
+ }
41
+
42
+ export function deactivate() {
43
+ // 仅收自有资源;经 ctx 注册的命令/事件由运行时自动回收。
44
+ }
@@ -1,33 +1,83 @@
1
- import { defineConfig } from 'vite';
2
- import vue from '@vitejs/plugin-vue';
3
- import federation from '@originjs/vite-plugin-federation';
4
-
5
- // 可信插件构建:打成 Module Federation remote。
6
- // - vue({ customElement: true }):SFC 以自定义元素模式编译,样式自动进 shadow root。
7
- // - federation exposes './plugin' → src/index.ts(导出 activate/elementTag);
8
- // shared ['vue','pinia'] 让本插件对 vue/pinia 的引用运行时复用宿主单例。
9
- // **不要给 shared requiredVersion**(宿主按首个注册版本提供,无需精确匹配)
10
- // 产物 dist/assets/remoteEntry.js manifest.entry 指向的容器。
11
- //
12
- // 可信插件**不能**用 vite dev 调试(federation dev 不产可加载 remoteEntry),
13
- // 故走 `vite build --watch` + 静态托管 dist(见 serve.mjs / package.json dev)。
14
- export default defineConfig({
15
- plugins: [
16
- vue({ customElement: true }),
17
- federation({
18
- name: '__FED_NAME__',
19
- filename: 'remoteEntry.js',
20
- exposes: { './plugin': './src/index.ts' },
21
- shared: ['vue', 'pinia'],
22
- }),
23
- ],
24
- build: {
25
- target: 'esnext',
26
- outDir: 'dist',
27
- // 不清空 outDir:`vite build --watch` 每次重建都会先清空再写,留下一个 remoteEntry.js
28
- // 缺失的窗口;若浏览器在该窗口请求 remoteEntry 就会 404、插件激活失败。保留旧产物、
29
- // 原地覆盖可消除该窗口(产物文件名带 hash,残留旧 chunk 无害)。
30
- emptyOutDir: false,
31
- minify: false,
32
- },
33
- });
1
+ import { defineConfig } from 'vite';
2
+ import vue from '@vitejs/plugin-vue';
3
+ import { fileURLToPath, URL } from 'node:url';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ // 可信插件「形态一」(ESM 直出 activate)构建。与「形态二」(Module Federation 容器)对比:
7
+ // - 形态一:本模块 import(url) 后**直接导出 activate/elementTag**,壳 loader 命中直出分支。
8
+ // Vue 打进 bundle(自带依赖,与宿主/其它插件隔离 —— 宿主升级 Vue 波及不到本插件)。
9
+ // - 形态二:打成 federation remoteEntry,运行时与宿主**共享单例** Vue/Pinia
10
+ // 只经 ctx 交互的内部插件用形态一**功能零损失**,且换来:dev 走裸 `vite`(实时转译、原生
11
+ // sourcemap、无构建步),build 产单文件 `main.js`。仅当需要跨边界共享响应式 store / 省体积
12
+ // 到单份 Vue 时才回退形态二( README 与《三角色开发方法.md》)
13
+ //
14
+ // vue({ customElement: true }):SFC 以自定义元素模式编译,样式自动注入 shadow root,
15
+ // 故无独立 CSS 产物,单个 main.js 即完整插件。
16
+
17
+ const MANIFEST = JSON.parse(
18
+ readFileSync(fileURLToPath(new URL('./manifest.json', import.meta.url)), 'utf-8'),
19
+ ) as { id: string };
20
+ const PLUGIN_ID = MANIFEST.id;
21
+
22
+ // —— dev 桥:让「单插件 vite 服务」对外长得像宿主的插件市场源 ——
23
+ // 宿主(LocalRegistry)会请求 /plugins.json、/<id>/manifest.json、/<id>/<entry>。
24
+ // 这里:① /plugins.json = [{ id }];② 把 /<id>/* 重写回 /*(插件本体在根目录);
25
+ // ③ manifest.entry 是 `main.js`(生产构建产物),dev 无此产物 → 把对它的请求
26
+ // 顶替为 `src/index.ts`,由 vite 实时转译(原生 sourcemap、无构建步)。
27
+ // 这样单跑 `vite` 即可被 harness/host-dev 当市场源加载,改代码刷新即生效。
28
+ function hostSourceBridge() {
29
+ return {
30
+ name: 'host-source-bridge',
31
+ configureServer(server: {
32
+ middlewares: { use: (fn: (req: any, res: any, next: () => void) => void) => void };
33
+ }) {
34
+ server.middlewares.use((req, res, next) => {
35
+ const url = (req.url || '').split('?')[0];
36
+ if (url === '/plugins.json') {
37
+ res.setHeader('content-type', 'application/json');
38
+ res.setHeader('access-control-allow-origin', '*');
39
+ res.end(JSON.stringify([{ id: PLUGIN_ID }]));
40
+ return;
41
+ }
42
+ // /<id>/xxx → /xxx(去掉宿主加的 id 前缀)
43
+ if (req.url === `/${PLUGIN_ID}` || req.url === `/${PLUGIN_ID}/`) {
44
+ req.url = '/';
45
+ } else if (req.url && req.url.startsWith(`/${PLUGIN_ID}/`)) {
46
+ req.url = req.url.slice(PLUGIN_ID.length + 1);
47
+ }
48
+ // 形态一 dev:把 main.js 入口请求顶替为可实时转译的源文件。
49
+ if ((req.url || '').split('?')[0] === '/main.js') req.url = '/src/index.ts';
50
+ next();
51
+ });
52
+ },
53
+ };
54
+ }
55
+
56
+ export default defineConfig({
57
+ plugins: [vue({ customElement: true }), hostSourceBridge()],
58
+ server: {
59
+ // 活跃集多插件并行时用 `vite --port 5175/5176/…` 覆盖,一份多源 plugins.json 列各端口。
60
+ port: 5174,
61
+ strictPort: true,
62
+ // 宿主(:5180 / app://)跨源拉清单/manifest/入口模块,需放开 CORS。
63
+ cors: { origin: '*' },
64
+ },
65
+ build: {
66
+ target: 'esnext',
67
+ outDir: 'dist',
68
+ emptyOutDir: true,
69
+ // 源码级调试可信插件产物:留 sourcemap、不混淆。
70
+ sourcemap: true,
71
+ minify: false,
72
+ // library 模式:入口导出(activate/elementTag/deactivate)即模块导出,壳 import(url) 直出。
73
+ lib: {
74
+ entry: fileURLToPath(new URL('./src/index.ts', import.meta.url)),
75
+ formats: ['es'],
76
+ fileName: () => 'main.js',
77
+ },
78
+ rollupOptions: {
79
+ // 保住入口导出签名,别被当可执行 chunk 摇掉 activate。
80
+ preserveEntrySignatures: 'strict',
81
+ },
82
+ },
83
+ });
@@ -1,6 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head><meta charset="utf-8" /><title>__ID__ (build entry)</title></head>
4
- <!-- 仅作 federation 构建入口;宿主实际加载的是 assets/remoteEntry.js 容器,不是本页。 -->
5
- <body></body>
6
- </html>
@@ -1,77 +0,0 @@
1
- #!/usr/bin/env node
2
- // 零依赖静态服务:把构建产物 dist/ 对外伪装成「宿主插件源」。
3
- // 宿主会请求 /plugins.json、/<id>/manifest.json、/<id>/assets/remoteEntry.js…
4
- // 配合 host-dev --plugin http://localhost:5174 调试。改代码 → vite build --watch 自动重建 → 刷新浏览器。
5
- import { createServer } from 'node:http';
6
- import { readFile, stat } from 'node:fs/promises';
7
- import { readFileSync, existsSync } from 'node:fs';
8
- import { fileURLToPath } from 'node:url';
9
- import { dirname, join, normalize, extname, resolve } from 'node:path';
10
-
11
- const here = dirname(fileURLToPath(import.meta.url));
12
- const DIST = resolve(here, 'dist');
13
- const MANIFEST = JSON.parse(readFileSync(resolve(here, 'manifest.json'), 'utf8'));
14
- const ID = MANIFEST.id;
15
- const PORT = Number(process.env.PORT || 5174);
16
- // 真正决定「插件就绪」的产物 = manifest.entry 指向的 remoteEntry。
17
- // /plugins.json 的就绪信号必须以它为准,否则 host-dev 会在 dist 尚未产出(或 watch 重建
18
- // 清空 dist 的窗口内)就判定就绪并开浏览器,导致 remoteEntry 404、插件激活失败。
19
- const REMOTE_ENTRY = resolve(DIST, MANIFEST.entry || 'assets/remoteEntry.js');
20
-
21
- const MIME = {
22
- '.js': 'text/javascript; charset=utf-8',
23
- '.mjs': 'text/javascript; charset=utf-8',
24
- '.json': 'application/json; charset=utf-8',
25
- '.css': 'text/css; charset=utf-8',
26
- '.html': 'text/html; charset=utf-8',
27
- '.svg': 'image/svg+xml',
28
- '.map': 'application/json; charset=utf-8',
29
- };
30
-
31
- createServer(async (req, res) => {
32
- res.setHeader('access-control-allow-origin', '*');
33
- const url = (req.url || '/').split('?')[0];
34
-
35
- if (url === '/plugins.json') {
36
- // 产物未就绪(首次构建未完成 / watch 重建窗口)时回 503,让 host-dev 的就绪轮询继续等。
37
- if (!existsSync(REMOTE_ENTRY)) {
38
- res.writeHead(503, { 'content-type': MIME['.json'], 'retry-after': '1' });
39
- return res.end(JSON.stringify({ error: 'building', detail: 'remoteEntry 未就绪,等 vite build 完成' }));
40
- }
41
- res.writeHead(200, { 'content-type': MIME['.json'] });
42
- return res.end(JSON.stringify([{ id: ID }]));
43
- }
44
-
45
- let rel = url;
46
- if (rel === `/${ID}` || rel === `/${ID}/`) rel = '/';
47
- else if (rel.startsWith(`/${ID}/`)) rel = rel.slice(ID.length + 1);
48
-
49
- if (rel === '/manifest.json') {
50
- res.writeHead(200, { 'content-type': MIME['.json'] });
51
- return res.end(JSON.stringify(MANIFEST));
52
- }
53
-
54
- let file = normalize(join(DIST, rel));
55
- if (file !== DIST && !file.startsWith(DIST + (process.platform === 'win32' ? '\\' : '/'))) {
56
- res.writeHead(403);
57
- return res.end('forbidden');
58
- }
59
- try {
60
- const s = await stat(file);
61
- if (s.isDirectory()) file = join(file, 'index.html');
62
- } catch {
63
- res.writeHead(404);
64
- return res.end('not found (dist 未就绪? 等 vite build --watch 首次完成)');
65
- }
66
- try {
67
- const body = await readFile(file);
68
- res.writeHead(200, { 'content-type': MIME[extname(file).toLowerCase()] || 'application/octet-stream' });
69
- res.end(body);
70
- } catch {
71
- res.writeHead(404);
72
- res.end('not found');
73
- }
74
- }).listen(PORT, () => {
75
- console.log(`[__ID__] 插件源 → http://localhost:${PORT} (id=${ID})`);
76
- console.log('[__ID__] 配合 host-dev --plugin http://localhost:' + PORT + ' 调试');
77
- });