@dp-bohrium/widget-cli 0.1.0 → 0.3.0
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/AGENTS.md +144 -8
- package/CHANGELOG.md +73 -0
- package/README.md +60 -97
- package/assets/licenses/ajv@8.20.0/LICENSE +22 -0
- package/assets/licenses/fast-deep-equal@3.1.3/LICENSE +21 -0
- package/assets/licenses/fast-uri@3.1.6/LICENSE +30 -0
- package/assets/licenses/json-schema-traverse@1.0.0/LICENSE +21 -0
- package/assets/licenses/require-from-string@2.0.2/license +21 -0
- package/assets/licenses/widget-contracts.LICENSE +21 -0
- package/assets/licenses/widget-sdk.LICENSE +21 -0
- package/assets/schemas/host-runtime-config.schema.json +305 -0
- package/assets/schemas/widget-manifest.schema.json +361 -0
- package/assets/schemas/widget-plugin-manifest.schema.json +162 -0
- package/assets/schemas/widget-runtime-contracts.schema.json +2870 -0
- package/assets/toolchain.json +24 -0
- package/dist/cli.mjs +1066 -143
- package/dist/core.mjs +1 -1
- package/dist/{dsh-DgkNH7L-.mjs → dsh-DbOeoKFJ.mjs} +554 -88
- package/llms.txt +55 -4
- package/package.json +13 -12
package/dist/cli.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as resolveWidgetProjects, i as
|
|
3
|
-
import { createRequire } from "node:module";
|
|
2
|
+
import { a as normalizeArchivePath, c as resolveWidgetProjects, i as collectWidgetPackageFiles, l as compileAuthorManifest, n as scaffoldDshWidget, o as storedZip, s as checkWidgetProject, t as auditDshPlugin } from "./dsh-DbOeoKFJ.mjs";
|
|
4
3
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
5
4
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
|
|
6
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
7
6
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
9
8
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -76,9 +75,9 @@ const RESOURCE_BUDGET = {
|
|
|
76
75
|
},
|
|
77
76
|
retry: {
|
|
78
77
|
maxAttempts: 1,
|
|
79
|
-
initialDelayMs:
|
|
80
|
-
maxDelayMs:
|
|
81
|
-
totalMs:
|
|
78
|
+
initialDelayMs: 250,
|
|
79
|
+
maxDelayMs: 1e3,
|
|
80
|
+
totalMs: 5e3
|
|
82
81
|
},
|
|
83
82
|
failure: {
|
|
84
83
|
consecutiveFailures: 3,
|
|
@@ -172,11 +171,15 @@ function parseArgs(argv) {
|
|
|
172
171
|
};
|
|
173
172
|
}
|
|
174
173
|
function createWidgetDevError(error, context = {}) {
|
|
174
|
+
const code = error?.cliCode ?? error?.code;
|
|
175
175
|
return {
|
|
176
|
-
code: typeof
|
|
176
|
+
code: typeof code === "string" && /^(?:WIDGET_DEV_|WIDGET_CLI_)/u.test(code) ? code : "WIDGET_DEV_FAILED",
|
|
177
177
|
message: error instanceof Error ? error.message : String(error),
|
|
178
|
-
context
|
|
179
|
-
|
|
178
|
+
context: {
|
|
179
|
+
...context,
|
|
180
|
+
...error?.context
|
|
181
|
+
},
|
|
182
|
+
recovery: error?.recovery ?? (context.phase === "watch-build" ? "读取 status.lastBuildError,修复工程后重新保存源码或重新执行 build。" : "先读取 error.context 和 build 输出,修复工程或宿主连接后重试。")
|
|
180
183
|
};
|
|
181
184
|
}
|
|
182
185
|
function secureHeaders(contentType) {
|
|
@@ -187,7 +190,7 @@ function secureHeaders(contentType) {
|
|
|
187
190
|
"x-content-type-options": "nosniff"
|
|
188
191
|
};
|
|
189
192
|
}
|
|
190
|
-
function json(response, status, value, extraHeaders = {}) {
|
|
193
|
+
function json$1(response, status, value, extraHeaders = {}) {
|
|
191
194
|
const body = JSON.stringify(value);
|
|
192
195
|
response.writeHead(status, {
|
|
193
196
|
...secureHeaders("application/json; charset=utf-8"),
|
|
@@ -406,9 +409,9 @@ function panelDevelopmentDescriptor(generation, profileUrl, statusUrl) {
|
|
|
406
409
|
presentationRequests
|
|
407
410
|
};
|
|
408
411
|
}
|
|
409
|
-
function readProject(projectRoot) {
|
|
412
|
+
function readProject(projectRoot, manifestOptions = {}) {
|
|
410
413
|
const sourceManifest = JSON.parse(readFileSync(join(projectRoot, "manifest.json"), "utf8"));
|
|
411
|
-
const manifest = compileAuthorManifest(sourceManifest);
|
|
414
|
+
const manifest = compileAuthorManifest(sourceManifest, manifestOptions);
|
|
412
415
|
const entryPath = manifest.widgetConfig.entry;
|
|
413
416
|
const entryFile = join(projectRoot, entryPath);
|
|
414
417
|
if (!existsSync(entryFile) || statSync(entryFile).size === 0) throw new Error(`entry 不存在或为空:${entryPath};先运行 build,或移除 --no-build`);
|
|
@@ -458,8 +461,8 @@ function collectGenerationAssets(projectRoot, entryPath, limits) {
|
|
|
458
461
|
};
|
|
459
462
|
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
460
463
|
}
|
|
461
|
-
function createDevelopmentGeneration(projectRoot, number = 0, limits = GENERATION_LIMITS) {
|
|
462
|
-
const { manifest, entryPath } = readProject(projectRoot);
|
|
464
|
+
function createDevelopmentGeneration(projectRoot, number = 0, limits = GENERATION_LIMITS, manifestOptions = {}) {
|
|
465
|
+
const { manifest, entryPath } = readProject(projectRoot, manifestOptions);
|
|
463
466
|
const collected = collectGenerationAssets(projectRoot, entryPath, limits);
|
|
464
467
|
if (!collected.some(({ path }) => path === entryPath)) throw new Error(`entry 未进入 generation:${entryPath}`);
|
|
465
468
|
const hash = createHash("sha256");
|
|
@@ -720,9 +723,28 @@ function runBuild(projectRoot, { capture = false } = {}) {
|
|
|
720
723
|
});
|
|
721
724
|
}
|
|
722
725
|
async function startWidgetDevelopment(projectRoot, options) {
|
|
726
|
+
const buildProject = options.buildProject ?? runBuild;
|
|
723
727
|
const emit = options.emit ?? ((line, error = false) => error ? console.error(line) : console.log(line));
|
|
724
|
-
|
|
725
|
-
|
|
728
|
+
const validateProject = async () => {
|
|
729
|
+
if (!options.checkProject) return;
|
|
730
|
+
const check = await options.checkProject(projectRoot, {
|
|
731
|
+
...options.manifestOptions,
|
|
732
|
+
requireBuiltEntry: true
|
|
733
|
+
});
|
|
734
|
+
if (!check.ok) {
|
|
735
|
+
const error = /* @__PURE__ */ new Error(`Widget Dev L0 check 失败:${check.errors.join("; ")}`);
|
|
736
|
+
error.code = "WIDGET_DEV_PROJECT_INVALID";
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
await options.beforeBuild?.({
|
|
741
|
+
projectRoot,
|
|
742
|
+
initial: true,
|
|
743
|
+
build: options.build
|
|
744
|
+
});
|
|
745
|
+
if (options.build) await buildProject(projectRoot, { capture: options.json });
|
|
746
|
+
await validateProject();
|
|
747
|
+
let successfulGeneration = createDevelopmentGeneration(projectRoot, 0, GENERATION_LIMITS, options.manifestOptions);
|
|
726
748
|
const retainedGenerations = /* @__PURE__ */ new Map([[successfulGeneration.guestCapability, successfulGeneration]]);
|
|
727
749
|
const initial = {
|
|
728
750
|
manifest: successfulGeneration.manifest,
|
|
@@ -754,7 +776,7 @@ async function startWidgetDevelopment(projectRoot, options) {
|
|
|
754
776
|
for (const response of eventClients.keys()) response.write(frame);
|
|
755
777
|
if (options.json) emit(formatDevelopmentEvent(type, data), type === "generation-failed");
|
|
756
778
|
};
|
|
757
|
-
const unauthorized = (response) => json(response, 401, {
|
|
779
|
+
const unauthorized = (response) => json$1(response, 401, {
|
|
758
780
|
ok: false,
|
|
759
781
|
error: {
|
|
760
782
|
code: "WIDGET_DEV_UNAUTHORIZED",
|
|
@@ -798,14 +820,14 @@ async function startWidgetDevelopment(projectRoot, options) {
|
|
|
798
820
|
return;
|
|
799
821
|
}
|
|
800
822
|
if (url.pathname === DESCRIPTOR_PATH && request.method === "GET") {
|
|
801
|
-
json(response, 200, session.descriptor);
|
|
823
|
+
json$1(response, 200, session.descriptor);
|
|
802
824
|
return;
|
|
803
825
|
}
|
|
804
826
|
if (url.pathname === CLAIM_PATH && request.method === "POST") {
|
|
805
827
|
try {
|
|
806
|
-
json(response, 200, session.claim(bearer(request.headers.authorization)));
|
|
828
|
+
json$1(response, 200, session.claim(bearer(request.headers.authorization)));
|
|
807
829
|
} catch (error) {
|
|
808
|
-
json(response, 401, {
|
|
830
|
+
json$1(response, 401, {
|
|
809
831
|
ok: false,
|
|
810
832
|
error: {
|
|
811
833
|
code: "WIDGET_DEV_CLAIM_REJECTED",
|
|
@@ -820,7 +842,7 @@ async function startWidgetDevelopment(projectRoot, options) {
|
|
|
820
842
|
return;
|
|
821
843
|
}
|
|
822
844
|
if (url.pathname === V1_STATUS_PATH && request.method === "GET") {
|
|
823
|
-
json(response, 200, developmentSnapshot());
|
|
845
|
+
json$1(response, 200, developmentSnapshot());
|
|
824
846
|
return;
|
|
825
847
|
}
|
|
826
848
|
if (url.pathname === EVENTS_PATH && request.method === "GET") {
|
|
@@ -861,13 +883,13 @@ async function startWidgetDevelopment(projectRoot, options) {
|
|
|
861
883
|
sendEvent("disconnected", { reason: "host-request" });
|
|
862
884
|
session.disconnect();
|
|
863
885
|
} else sendEvent("reload-requested", { generation: state.generation });
|
|
864
|
-
json(response, 200, {
|
|
886
|
+
json$1(response, 200, {
|
|
865
887
|
ok: true,
|
|
866
888
|
protocolVersion: 1,
|
|
867
889
|
action: control.action
|
|
868
890
|
});
|
|
869
891
|
} catch (error) {
|
|
870
|
-
json(response, 400, {
|
|
892
|
+
json$1(response, 400, {
|
|
871
893
|
ok: false,
|
|
872
894
|
error: { message: String(error) }
|
|
873
895
|
});
|
|
@@ -881,7 +903,7 @@ async function startWidgetDevelopment(projectRoot, options) {
|
|
|
881
903
|
const panelId = url.searchParams.get("id");
|
|
882
904
|
const manifestContribution = successfulGeneration.manifest.widgetConfig.contribution;
|
|
883
905
|
if (!(!contribution || contribution === manifestContribution.type && (contribution !== "panel" || !panelId || panelId === manifestContribution.id))) {
|
|
884
|
-
json(response, 404, {
|
|
906
|
+
json$1(response, 404, {
|
|
885
907
|
ok: false,
|
|
886
908
|
error: {
|
|
887
909
|
code: "WIDGET_DEV_PROFILE_NOT_FOUND",
|
|
@@ -896,7 +918,7 @@ async function startWidgetDevelopment(projectRoot, options) {
|
|
|
896
918
|
}
|
|
897
919
|
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
898
920
|
const launchUrl = `${origin}${guestGenerationRoot(successfulGeneration)}/launch.html`;
|
|
899
|
-
json(response, 200, createDevelopmentConfig({
|
|
921
|
+
json$1(response, 200, createDevelopmentConfig({
|
|
900
922
|
manifest: successfulGeneration.manifest,
|
|
901
923
|
runId,
|
|
902
924
|
origin,
|
|
@@ -908,7 +930,7 @@ async function startWidgetDevelopment(projectRoot, options) {
|
|
|
908
930
|
if (url.pathname === PROFILE_PATH) {
|
|
909
931
|
const runId = url.searchParams.get("runId") || "widget-dev-run";
|
|
910
932
|
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
911
|
-
json(response, 200, createDevelopmentConfig({
|
|
933
|
+
json$1(response, 200, createDevelopmentConfig({
|
|
912
934
|
manifest: successfulGeneration.manifest,
|
|
913
935
|
runId,
|
|
914
936
|
origin,
|
|
@@ -918,7 +940,7 @@ async function startWidgetDevelopment(projectRoot, options) {
|
|
|
918
940
|
return;
|
|
919
941
|
}
|
|
920
942
|
if (url.pathname === STATUS_PATH) {
|
|
921
|
-
json(response, 200, {
|
|
943
|
+
json$1(response, 200, {
|
|
922
944
|
ok: state.phase === "ready",
|
|
923
945
|
command: "dev",
|
|
924
946
|
profile: PROFILE_PATH,
|
|
@@ -1021,8 +1043,14 @@ async function startWidgetDevelopment(projectRoot, options) {
|
|
|
1021
1043
|
}
|
|
1022
1044
|
building = true;
|
|
1023
1045
|
try {
|
|
1024
|
-
|
|
1025
|
-
|
|
1046
|
+
await options.beforeBuild?.({
|
|
1047
|
+
projectRoot,
|
|
1048
|
+
initial: false,
|
|
1049
|
+
build: true
|
|
1050
|
+
});
|
|
1051
|
+
const build = await buildProject(projectRoot, { capture: options.json });
|
|
1052
|
+
await validateProject();
|
|
1053
|
+
const candidate = createDevelopmentGeneration(projectRoot, successfulGeneration.number + 1, GENERATION_LIMITS, options.manifestOptions);
|
|
1026
1054
|
successfulGeneration = candidate;
|
|
1027
1055
|
retainedGenerations.set(candidate.guestCapability, candidate);
|
|
1028
1056
|
while (retainedGenerations.size > RETAINED_GENERATIONS) retainedGenerations.delete(retainedGenerations.keys().next().value);
|
|
@@ -1103,15 +1131,22 @@ function emitCliEnvelope(type, data, error = false) {
|
|
|
1103
1131
|
if (error) console.error(line);
|
|
1104
1132
|
else console.log(line);
|
|
1105
1133
|
}
|
|
1106
|
-
async function runWidgetDevelopmentCli(argv = process.argv.slice(2)) {
|
|
1134
|
+
async function runWidgetDevelopmentCli(argv = process.argv.slice(2), overrides = {}) {
|
|
1107
1135
|
try {
|
|
1108
|
-
const options =
|
|
1136
|
+
const options = {
|
|
1137
|
+
...parseArgs(argv),
|
|
1138
|
+
...overrides
|
|
1139
|
+
};
|
|
1109
1140
|
if (options.help) {
|
|
1110
1141
|
console.log(usage$1());
|
|
1111
1142
|
return 0;
|
|
1112
1143
|
}
|
|
1113
1144
|
const [projectRoot] = resolveWidgetProjects(options.project);
|
|
1114
|
-
const
|
|
1145
|
+
const checkProject = options.checkProject ?? checkWidgetProject;
|
|
1146
|
+
const check = await checkProject(projectRoot, {
|
|
1147
|
+
...options.manifestOptions,
|
|
1148
|
+
requireBuiltEntry: false
|
|
1149
|
+
});
|
|
1115
1150
|
if (!check.ok) {
|
|
1116
1151
|
if (options.json) emitCliEnvelope("error", {
|
|
1117
1152
|
ok: false,
|
|
@@ -1130,7 +1165,10 @@ async function runWidgetDevelopmentCli(argv = process.argv.slice(2)) {
|
|
|
1130
1165
|
}
|
|
1131
1166
|
return 1;
|
|
1132
1167
|
}
|
|
1133
|
-
await startWidgetDevelopment(projectRoot,
|
|
1168
|
+
await startWidgetDevelopment(projectRoot, {
|
|
1169
|
+
...options,
|
|
1170
|
+
checkProject
|
|
1171
|
+
});
|
|
1134
1172
|
return 0;
|
|
1135
1173
|
} catch (error) {
|
|
1136
1174
|
if (argv.includes("--json")) emitCliEnvelope("error", {
|
|
@@ -1143,6 +1181,334 @@ async function runWidgetDevelopmentCli(argv = process.argv.slice(2)) {
|
|
|
1143
1181
|
}
|
|
1144
1182
|
}
|
|
1145
1183
|
//#endregion
|
|
1184
|
+
//#region src/help.mjs
|
|
1185
|
+
const COMMANDS = [
|
|
1186
|
+
"create",
|
|
1187
|
+
"check",
|
|
1188
|
+
"validate",
|
|
1189
|
+
"build",
|
|
1190
|
+
"dev",
|
|
1191
|
+
"pack",
|
|
1192
|
+
"doctor",
|
|
1193
|
+
"schema",
|
|
1194
|
+
"migrate",
|
|
1195
|
+
"version",
|
|
1196
|
+
"integration",
|
|
1197
|
+
"adopt",
|
|
1198
|
+
"help"
|
|
1199
|
+
];
|
|
1200
|
+
const HELP_OPTION = "-h, --help 显示当前命令帮助并退出";
|
|
1201
|
+
const JSON_OPTION = "--json 输出单个 JSON 文档(默认输出文本)";
|
|
1202
|
+
const HOST_OPTION = "--host-compat <声明文件> 核对目标宿主的声明;默认不提供,兼容状态为 unknown";
|
|
1203
|
+
function commandTopics(context) {
|
|
1204
|
+
const check = (name) => ({
|
|
1205
|
+
description: name === "validate" ? "check 的别名;校验工程的清单、已构建入口、samples 和预览夹具。" : "校验作者清单、编译后的宿主清单、已构建入口、samples 和预览夹具。",
|
|
1206
|
+
usage: [`bohr-widget ${name} <目录|manifest.json|all> [选项]`],
|
|
1207
|
+
arguments: ["目录|manifest.json|all 必填;检查单个工程,或从当前目录发现示例工程"],
|
|
1208
|
+
options: [HOST_OPTION, "--json 单工程输出检查结果;all 输出单个汇总 JSON 文档"],
|
|
1209
|
+
examples: [`bohr-widget ${name} ./my-widget --json`, `bohr-widget ${name} all --json`],
|
|
1210
|
+
notes: [
|
|
1211
|
+
"不执行构建;入口缺失时先运行 bohr-widget build <目录>。",
|
|
1212
|
+
"all 从当前目录查找 examples/sdk 和 examples/integration;未发现时尝试当前目录的 manifest.json。",
|
|
1213
|
+
"没有找到工程或任一工程校验失败时,退出码为 1。",
|
|
1214
|
+
"校验通过只证明本地结构和声明符合要求;实际渲染与能力授权仍需在 Host 中验证。"
|
|
1215
|
+
]
|
|
1216
|
+
});
|
|
1217
|
+
const migrationGroup = {
|
|
1218
|
+
description: "审计 DeepSeek Harness(DSH)插件,并按已审查的来源配方生成 Widget 工程。",
|
|
1219
|
+
usage: ["bohr-widget migrate dsh audit <plugin-dir> [选项]", "bohr-widget migrate dsh scaffold <plugin-dir> --out <widget-dir> [选项]"],
|
|
1220
|
+
arguments: [
|
|
1221
|
+
"dsh 当前唯一支持的迁移来源",
|
|
1222
|
+
"audit 只读审计来源、指纹和能力缺口",
|
|
1223
|
+
"scaffold 使用匹配的已审查配方创建新工程"
|
|
1224
|
+
],
|
|
1225
|
+
options: [],
|
|
1226
|
+
examples: ["bohr-widget migrate dsh audit ./dsh-plugin --json", "bohr-widget migrate dsh scaffold ./dsh-plugin --out ./migrated-widget --json"],
|
|
1227
|
+
notes: [
|
|
1228
|
+
"先 audit,再读取报告决定是否 scaffold;源指纹变化或无匹配配方时不能直接生成。",
|
|
1229
|
+
"不执行源插件安装脚本或 Cordis patch;Host adapter 工作仍需单独完成。",
|
|
1230
|
+
"使用 bohr-widget migrate dsh <audit|scaffold> --help 查看详细参数。"
|
|
1231
|
+
]
|
|
1232
|
+
};
|
|
1233
|
+
return {
|
|
1234
|
+
create: {
|
|
1235
|
+
description: "创建可独立安装、构建和打包的 Widget 工程,生成源码、样例和工具链锁定文件。",
|
|
1236
|
+
usage: ["bohr-widget create <目录> [选项]"],
|
|
1237
|
+
arguments: ["目录 必填;目标目录必须不存在"],
|
|
1238
|
+
options: [
|
|
1239
|
+
"--template <模板> panel 或 file-preview(默认 panel)",
|
|
1240
|
+
"--ui <模式> vanilla 或 react(默认 vanilla)",
|
|
1241
|
+
"--panel-id <id> 仅用于 panel;默认取规范化后的工程名",
|
|
1242
|
+
HOST_OPTION,
|
|
1243
|
+
JSON_OPTION
|
|
1244
|
+
],
|
|
1245
|
+
examples: ["bohr-widget create workspace-browser --ui react --panel-id workspace-files", "bohr-widget create text-preview --template file-preview --ui vanilla --json"],
|
|
1246
|
+
notes: [
|
|
1247
|
+
`默认组合为 panel + vanilla;生成工程的构建环境要求 Node.js ${context.generatedProjectNode}。`,
|
|
1248
|
+
"不会自动安装依赖;创建后进入目录,运行 pnpm install 或 npm install。",
|
|
1249
|
+
"workspace-files 会补齐 files.list、files.metadata、files.read 和目录引用样例;文件树逻辑与夹具需自行实现。",
|
|
1250
|
+
"其他 Panel id 需要 Host 预先注册和绑定;更改 id 不会自动接管宿主界面。",
|
|
1251
|
+
"将 widget-toolchain.json 与包管理器 lockfile 一起保存。"
|
|
1252
|
+
],
|
|
1253
|
+
quickStart: true
|
|
1254
|
+
},
|
|
1255
|
+
check: check("check"),
|
|
1256
|
+
validate: check("validate"),
|
|
1257
|
+
build: {
|
|
1258
|
+
description: "执行工程自己的 build 脚本,核对依赖与构建结果,并记录可复用的构建 receipt。",
|
|
1259
|
+
usage: ["bohr-widget build <目录> [选项]"],
|
|
1260
|
+
arguments: ["目录 必填;已安装依赖的 Widget 工程目录"],
|
|
1261
|
+
options: [HOST_OPTION, JSON_OPTION],
|
|
1262
|
+
examples: ["bohr-widget build ./my-widget", "bohr-widget build . --host-compat ./host.json --json"],
|
|
1263
|
+
notes: [
|
|
1264
|
+
"先安装工程依赖;使用 packageManager 指定的包管理器,其次按 lockfile 检测,最后回退 npm。",
|
|
1265
|
+
"成功后写入 .widget-build-receipt.json;pack --no-build 会检查这份记录。",
|
|
1266
|
+
"本命令不代替完整的 Manifest、samples 和 fixtures 校验;构建后继续运行 check。",
|
|
1267
|
+
"失败时读取 error、stdout 和 stderr;构建器退出 0 但仍报告未解析依赖也会失败。"
|
|
1268
|
+
]
|
|
1269
|
+
},
|
|
1270
|
+
dev: {
|
|
1271
|
+
description: "启动本地 Profile 与资源服务,供支持 Widget 开发会话的 Host 加载和调试。",
|
|
1272
|
+
usage: ["bohr-widget dev <目录|manifest.json> [选项]"],
|
|
1273
|
+
arguments: ["目录|manifest.json 必填;要运行的 Widget 工程"],
|
|
1274
|
+
options: [
|
|
1275
|
+
"--watch 监听源码与清单变化并重建(默认关闭)",
|
|
1276
|
+
"--open 连接或唤起 Widget 开发宿主(默认关闭)",
|
|
1277
|
+
"--no-build 跳过首次构建,使用现有入口(默认先构建)",
|
|
1278
|
+
"--port <端口> 本地服务端口,1024–65535(默认 4300)",
|
|
1279
|
+
"--scheme <scheme> 宿主 deep link scheme(默认 widget-host)",
|
|
1280
|
+
"--bridge <url> 指定 loopback HTTP(S) bridge;默认探测标准端口",
|
|
1281
|
+
HOST_OPTION,
|
|
1282
|
+
"--json 持续输出 NDJSON 事件流(默认输出文本)"
|
|
1283
|
+
],
|
|
1284
|
+
examples: ["bohr-widget dev . --watch --open", "bohr-widget dev ./my-widget --no-build --port 4301 --json"],
|
|
1285
|
+
notes: [
|
|
1286
|
+
"--open 打开的是目标 Host,不是浏览器预览;先连接 bridge,必要时尝试宿主 scheme。",
|
|
1287
|
+
"不传 --open 时可使用输出的 Host URL;服务常驻,按 Ctrl+C 结束。",
|
|
1288
|
+
"--no-build 不要求构建 receipt,但仍校验工具链与现有产物;搭配 --watch 时后续变化仍会构建。",
|
|
1289
|
+
"--bridge 须带 1024–65535 的显式端口,且不能包含认证信息、query 或 hash。",
|
|
1290
|
+
"--bridge 仅在使用 --open 时参与宿主连接。",
|
|
1291
|
+
"dev --json 是逐行事件流;session-ready 含 profileUrl、statusUrl、hostUrl、digest 和连接结果。",
|
|
1292
|
+
"当前本地开发文件能力只读;Manifest 申请或样例 grant 不代表目标 Host 已开放写入。",
|
|
1293
|
+
"watch 构建失败时保留上一轮可用产物;从 statusUrl 查看 lastBuildError。"
|
|
1294
|
+
],
|
|
1295
|
+
quickStart: true
|
|
1296
|
+
},
|
|
1297
|
+
pack: {
|
|
1298
|
+
description: "默认先构建和校验工程,再生成宿主可加载的 Widget ZIP 与 SHA-256 摘要。",
|
|
1299
|
+
usage: ["bohr-widget pack <目录> [选项]"],
|
|
1300
|
+
arguments: ["目录 必填;要打包的 Widget 工程目录"],
|
|
1301
|
+
options: [
|
|
1302
|
+
"--out <zip> 输出路径;默认 <工程目录>/<manifest.name>.zip",
|
|
1303
|
+
"--no-build 复用匹配的构建 receipt(默认重新构建)",
|
|
1304
|
+
"--include-preview-fixtures 包含 preview/fixtures.json(默认不包含)",
|
|
1305
|
+
HOST_OPTION,
|
|
1306
|
+
JSON_OPTION
|
|
1307
|
+
],
|
|
1308
|
+
examples: ["bohr-widget pack . --out ./artifacts/widget.zip --json", "bohr-widget pack ./my-widget --no-build --include-preview-fixtures --out ./repro.zip"],
|
|
1309
|
+
notes: [
|
|
1310
|
+
"--out 相对调用时的当前目录解析,自动创建父目录;目标 ZIP 已存在时会覆盖。",
|
|
1311
|
+
"--no-build 要求 .widget-build-receipt.json 与当前 CLI/profile、工具链摘要、实际依赖、输入和产物一致。",
|
|
1312
|
+
"单独执行工程 build 脚本不生成 CLI receipt;需先运行 bohr-widget build <目录>。",
|
|
1313
|
+
"携带预览夹具的 ZIP 用于本地复现,不作为上传 ZIP;上传包使用默认不含夹具的形式。",
|
|
1314
|
+
"入口构建目录中的 chunk、CSS、WASM 等部署资源一并保留;不要将源码或凭据放入该目录。",
|
|
1315
|
+
"--json 返回 path、bytes、sha256、toolchain、manifest 和 fixtures.included;后续交付使用同一份 ZIP 与摘要。",
|
|
1316
|
+
"本命令不上传、不发布、不绑定 Agent;成功打包也不代表 Host 已实际渲染。"
|
|
1317
|
+
]
|
|
1318
|
+
},
|
|
1319
|
+
doctor: {
|
|
1320
|
+
description: "诊断 Node、清单、精确依赖、工具链锁和构建入口,解释工程尚未就绪的原因。",
|
|
1321
|
+
usage: ["bohr-widget doctor [目录] [选项]"],
|
|
1322
|
+
arguments: ["目录 可选;默认当前目录(.)"],
|
|
1323
|
+
options: [HOST_OPTION, JSON_OPTION],
|
|
1324
|
+
examples: ["bohr-widget doctor --json", "bohr-widget doctor ./existing-widget --host-compat ./host.json --json"],
|
|
1325
|
+
notes: [
|
|
1326
|
+
"只检查状态,不安装依赖、不构建,也不写入工具链锁。",
|
|
1327
|
+
"ok 表示环境、清单与候选 profile 检查通过;ready 还要求工具链锁、已安装依赖和构建入口。",
|
|
1328
|
+
"尚未安装或构建时可能 ok=true、ready=false;先按 warnings 修复再继续。",
|
|
1329
|
+
"ready 不保证 pack --no-build 的 receipt 可复用;doctor 只检查 CLI 的 Node 要求,不验证构建器环境。",
|
|
1330
|
+
"旧工程缺少 widget-toolchain.json 时,先核对精确依赖并安装,再运行 adopt。"
|
|
1331
|
+
]
|
|
1332
|
+
},
|
|
1333
|
+
integration: {
|
|
1334
|
+
description: "发现固定工具链、执行器位置、Node 要求与文档入口,供 Agent 或其他 CLI 集成。",
|
|
1335
|
+
usage: ["bohr-widget integration [选项]"],
|
|
1336
|
+
options: [
|
|
1337
|
+
"--project <目录> 可选;同时核对指定工程的锁定 profile 与依赖声明",
|
|
1338
|
+
HOST_OPTION,
|
|
1339
|
+
JSON_OPTION
|
|
1340
|
+
],
|
|
1341
|
+
examples: ["bohr-widget integration --json", "bohr-widget integration --project ./my-widget --host-compat ./host.json --json"],
|
|
1342
|
+
notes: [
|
|
1343
|
+
"不传 --project 时只发现 CLI 自带工具链,不检查当前目录是否为 Widget 工程。",
|
|
1344
|
+
"传入工程时要求工具链锁;尚未安装依赖时先核对声明,已安装时同时核对实际版本。",
|
|
1345
|
+
"JSON 使用 interfaceVersion=2、toolchain.formatVersion=2;返回 execution、profile、requirements 和 documentation。",
|
|
1346
|
+
"调用方应使用 execution.executable 与 execution.entry 启动同一份 CLI,避免 PATH 指向另一版本。",
|
|
1347
|
+
"宿主声明由调用方提供;unknown、snapshot、confirmed 不代表上传或发布许可。"
|
|
1348
|
+
],
|
|
1349
|
+
agentDocs: true
|
|
1350
|
+
},
|
|
1351
|
+
adopt: {
|
|
1352
|
+
description: "核验旧工程与当前固定工具链匹配后,显式写入 widget-toolchain.json。",
|
|
1353
|
+
usage: ["bohr-widget adopt <目录> [选项]"],
|
|
1354
|
+
arguments: ["目录 必填;待锁定的已有 Widget 工程"],
|
|
1355
|
+
options: [HOST_OPTION, JSON_OPTION],
|
|
1356
|
+
examples: ["bohr-widget adopt ./existing-widget --json", "bohr-widget adopt . --host-compat ./host.json --json"],
|
|
1357
|
+
notes: [
|
|
1358
|
+
"先运行 doctor,恢复当前 profile 的精确 SDK/contracts 依赖并安装。",
|
|
1359
|
+
"核对实际依赖、Manifest 和 samples 后才写锁;不要求已构建入口。",
|
|
1360
|
+
"不会自动升级依赖、迁移源码或修正协议字段;检查不通过时先修复工程。"
|
|
1361
|
+
]
|
|
1362
|
+
},
|
|
1363
|
+
schema: {
|
|
1364
|
+
description: "打印本 CLI 自带的作者插件 Manifest Schema 的本地绝对路径。",
|
|
1365
|
+
usage: ["bohr-widget schema"],
|
|
1366
|
+
options: [],
|
|
1367
|
+
examples: ["bohr-widget schema", "bohr-widget schema > schema-path.txt"],
|
|
1368
|
+
notes: [
|
|
1369
|
+
"输出 widget-plugin-manifest.schema.json 的路径,不打印 Schema 正文。",
|
|
1370
|
+
"不读取工程;即使传 --json,仍输出纯文本路径。",
|
|
1371
|
+
"使用配套 contracts Schema 进行结构检查;通过 Schema 不代表 Host 授权。"
|
|
1372
|
+
]
|
|
1373
|
+
},
|
|
1374
|
+
version: {
|
|
1375
|
+
description: "查看当前 CLI 及其固定的 SDK/contracts 版本。",
|
|
1376
|
+
usage: ["bohr-widget version [--json]", "bohr-widget --version [--json]"],
|
|
1377
|
+
options: [JSON_OPTION],
|
|
1378
|
+
examples: ["bohr-widget version --json", "bohr-widget -v"],
|
|
1379
|
+
notes: [
|
|
1380
|
+
`当前 CLI ${context.cli},SDK ${context.sdk},contracts ${context.contracts}。`,
|
|
1381
|
+
"文本输出 CLI 与 SDK 版本;--json 同时包含 contracts,不访问 npm registry。",
|
|
1382
|
+
"全局 -v / --version 等同版本查询,不执行工程操作。"
|
|
1383
|
+
]
|
|
1384
|
+
},
|
|
1385
|
+
migrate: migrationGroup,
|
|
1386
|
+
"migrate dsh": {
|
|
1387
|
+
...migrationGroup,
|
|
1388
|
+
arguments: migrationGroup.arguments.slice(1)
|
|
1389
|
+
},
|
|
1390
|
+
"migrate dsh audit": {
|
|
1391
|
+
description: "只读审计 DSH 插件的来源指纹、已知配方与能力迁移缺口。",
|
|
1392
|
+
usage: ["bohr-widget migrate dsh audit <plugin-dir> [选项]"],
|
|
1393
|
+
arguments: ["plugin-dir 必填;DSH 插件源目录"],
|
|
1394
|
+
options: [JSON_OPTION],
|
|
1395
|
+
examples: ["bohr-widget migrate dsh audit ./dsh-plugin", "bohr-widget migrate dsh audit ./dsh-plugin --json"],
|
|
1396
|
+
notes: [
|
|
1397
|
+
"不创建目标工程,不执行 Cordis patch 或源插件安装脚本。",
|
|
1398
|
+
"JSON 的 report 包含来源指纹、配方兼容状态和能力分类;读取结论后再决定是否 scaffold。",
|
|
1399
|
+
"审计命令执行成功不等于插件可自动迁移;未知来源或变化后的指纹需要人工审查。"
|
|
1400
|
+
]
|
|
1401
|
+
},
|
|
1402
|
+
"migrate dsh scaffold": {
|
|
1403
|
+
description: "按已审查且来源指纹一致的 DSH 配方创建 Widget 工程及迁移报告。",
|
|
1404
|
+
usage: ["bohr-widget migrate dsh scaffold <plugin-dir> --out <widget-dir> [选项]"],
|
|
1405
|
+
arguments: ["plugin-dir 必填;已完成审计的 DSH 插件源目录"],
|
|
1406
|
+
options: [
|
|
1407
|
+
"--out <widget-dir> 必填;要创建的新工程目录,不能已存在",
|
|
1408
|
+
HOST_OPTION,
|
|
1409
|
+
JSON_OPTION
|
|
1410
|
+
],
|
|
1411
|
+
examples: ["bohr-widget migrate dsh scaffold ./dsh-plugin --out ./migrated-widget", "bohr-widget migrate dsh scaffold ./dsh-plugin --out ./migrated-widget --host-compat ./host.json --json"],
|
|
1412
|
+
notes: [
|
|
1413
|
+
"先运行 audit;无已审查配方或迁移期间指纹变化时停止,不覆盖已有工程;目标的父目录须已存在。",
|
|
1414
|
+
"输出 widget-toolchain.json 与 migration-report.json;随后安装依赖、build、check 和 pack。",
|
|
1415
|
+
"工具、Skill、system prompt、文件和网络等 Host adapter 工作不会自动迁入 Widget。",
|
|
1416
|
+
"生成成功只表示创建了工程,仍需目标 Host 验收。"
|
|
1417
|
+
]
|
|
1418
|
+
},
|
|
1419
|
+
help: {
|
|
1420
|
+
description: "查看全局命令概览,或查看某个命令及迁移子命令的详细帮助。",
|
|
1421
|
+
usage: ["bohr-widget help [命令 [子命令...]]", "bohr-widget <命令> --help"],
|
|
1422
|
+
arguments: ["命令 [子命令...] 可选;省略时显示全局帮助"],
|
|
1423
|
+
options: [],
|
|
1424
|
+
examples: ["bohr-widget help create", "bohr-widget migrate dsh scaffold --help"],
|
|
1425
|
+
notes: ["不传参数、-h 和 --help 都可查看全局帮助;子命令后的 -h / --help 显示对应主题。", "帮助始终输出文本,不执行创建、构建或迁移操作。"]
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function documentationLines(documentation, topic = {}) {
|
|
1430
|
+
return [
|
|
1431
|
+
...topic.quickStart ? [`快速开始:${documentation.quickStart}`] : [],
|
|
1432
|
+
`命令参考:${documentation.cli}`,
|
|
1433
|
+
...topic.agentDocs ? [
|
|
1434
|
+
`文档站:${documentation.url}`,
|
|
1435
|
+
`Agent 索引:${documentation.llms}`,
|
|
1436
|
+
`完整文档:${documentation.llmsFull}`
|
|
1437
|
+
] : []
|
|
1438
|
+
];
|
|
1439
|
+
}
|
|
1440
|
+
function section(title, lines) {
|
|
1441
|
+
return lines.length ? `\n${title}:\n${lines.map((line) => ` ${line}`).join("\n")}\n` : "";
|
|
1442
|
+
}
|
|
1443
|
+
function globalHelp(context) {
|
|
1444
|
+
const { cli, sdk, contracts, generatedProjectNode, documentation } = context;
|
|
1445
|
+
return `bohr-widget ${cli} — Bohrium Widget 插件工程工具
|
|
1446
|
+
SDK ${sdk} / contracts ${contracts}
|
|
1447
|
+
创建、校验、构建、开发调试与打包;上传、发布和 Agent 绑定由 Bohr CLI 或平台执行。
|
|
1448
|
+
|
|
1449
|
+
用法:
|
|
1450
|
+
bohr-widget <命令> [参数] [选项]
|
|
1451
|
+
bohr-widget help [命令 [子命令...]]
|
|
1452
|
+
|
|
1453
|
+
命令:
|
|
1454
|
+
create <目录> 创建 Widget 工程(默认 panel + vanilla)
|
|
1455
|
+
check <目录|manifest.json|all> 校验清单、已构建入口、samples 和 fixtures
|
|
1456
|
+
validate <目录|manifest.json|all> check 的别名
|
|
1457
|
+
build <目录> 执行工程构建并记录构建 receipt
|
|
1458
|
+
dev <目录|manifest.json> 启动本地开发服务,供 Widget Host 加载
|
|
1459
|
+
pack <目录> 默认先构建、校验,再生成 Widget ZIP
|
|
1460
|
+
doctor [目录] 诊断环境和工程状态(默认当前目录)
|
|
1461
|
+
integration 发现工具链、执行器和文档入口
|
|
1462
|
+
adopt <目录> 核验旧工程并写入工具链锁
|
|
1463
|
+
schema 打印作者 Manifest Schema 的本地路径
|
|
1464
|
+
migrate dsh audit|scaffold 审计 DSH 来源或按已审查配方生成工程
|
|
1465
|
+
version 查看 CLI 与固定的 SDK/contracts 版本
|
|
1466
|
+
help [命令...] 查看全局或指定主题帮助
|
|
1467
|
+
|
|
1468
|
+
全局选项:
|
|
1469
|
+
-h, --help 显示全局或指定命令帮助并退出
|
|
1470
|
+
-v, --version 显示版本并退出;可搭配 --json
|
|
1471
|
+
--json 多数命令输出单个 JSON;dev 输出 NDJSON 事件流
|
|
1472
|
+
check all 输出汇总;help 与 schema 始终输出文本
|
|
1473
|
+
|
|
1474
|
+
调用方式:
|
|
1475
|
+
工程内:pnpm exec bohr-widget ... 或 npx --no-install bohr-widget ...
|
|
1476
|
+
未安装:pnpm dlx @dp-bohrium/widget-cli <命令> ...
|
|
1477
|
+
下方示例中的 bohr-widget 可使用上述工程内调用前缀。
|
|
1478
|
+
CLI 要求 Node.js >=20;默认生成工程要求 Node.js ${generatedProjectNode}。
|
|
1479
|
+
|
|
1480
|
+
典型开发流程:
|
|
1481
|
+
pnpm dlx @dp-bohrium/widget-cli create my-widget --ui react --panel-id workspace-files
|
|
1482
|
+
cd my-widget
|
|
1483
|
+
pnpm install
|
|
1484
|
+
pnpm exec bohr-widget build .
|
|
1485
|
+
pnpm exec bohr-widget check .
|
|
1486
|
+
pnpm exec bohr-widget dev . --watch --open
|
|
1487
|
+
pnpm run widget:pack
|
|
1488
|
+
|
|
1489
|
+
check 不执行构建;dev 按 Ctrl+C 结束后可继续打包。
|
|
1490
|
+
Widget ZIP 使用 widget:pack 脚本;pnpm pack 是制作 npm .tgz 包的内置命令。
|
|
1491
|
+
工程命令可用 --host-compat <声明文件> 核对宿主;未提供时兼容状态为 unknown。
|
|
1492
|
+
|
|
1493
|
+
文档:
|
|
1494
|
+
文档站:${documentation.url}
|
|
1495
|
+
快速开始:${documentation.quickStart}
|
|
1496
|
+
命令参考:${documentation.cli}
|
|
1497
|
+
Agent 索引:${documentation.llms}
|
|
1498
|
+
完整文档:${documentation.llmsFull}
|
|
1499
|
+
|
|
1500
|
+
查看具体命令的参数、默认值和示例:bohr-widget <命令> --help
|
|
1501
|
+
`;
|
|
1502
|
+
}
|
|
1503
|
+
function renderHelp(path = [], context) {
|
|
1504
|
+
if (path.length === 0) return globalHelp(context);
|
|
1505
|
+
const key = path.join(" ");
|
|
1506
|
+
const topics = commandTopics(context);
|
|
1507
|
+
if (!Object.hasOwn(topics, key)) throw new Error(`未知帮助主题:${key}`);
|
|
1508
|
+
const topic = topics[key];
|
|
1509
|
+
return `bohr-widget ${key} — ${topic.description}\n` + section("用法", topic.usage) + section("参数", topic.arguments ?? []) + section("选项", [...topic.options, HELP_OPTION]) + section("示例", topic.examples) + section("说明", topic.notes) + section("文档", documentationLines(context.documentation, topic));
|
|
1510
|
+
}
|
|
1511
|
+
//#endregion
|
|
1146
1512
|
//#region src/manifest-lint.mjs
|
|
1147
1513
|
/**
|
|
1148
1514
|
* 作者清单(VS Code 风格 `contributes` 清单)的本地静态检查。
|
|
@@ -1233,8 +1599,371 @@ function lintAuthorManifest(manifest, options = {}) {
|
|
|
1233
1599
|
};
|
|
1234
1600
|
}
|
|
1235
1601
|
//#endregion
|
|
1602
|
+
//#region src/project-executor.mjs
|
|
1603
|
+
const PROJECT_TOOLCHAIN_FILE = "widget-toolchain.json";
|
|
1604
|
+
const BUILD_RECEIPT_FILE = ".widget-build-receipt.json";
|
|
1605
|
+
const PACKAGES = Object.freeze({
|
|
1606
|
+
sdk: "@dp-bohrium/widget-sdk",
|
|
1607
|
+
contracts: "@dp-bohrium/widget-contracts"
|
|
1608
|
+
});
|
|
1609
|
+
const EXACT_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
|
|
1610
|
+
function failure(code, message, context, recovery) {
|
|
1611
|
+
return Object.assign(new Error(message), {
|
|
1612
|
+
cliCode: `WIDGET_CLI_${code}`,
|
|
1613
|
+
context,
|
|
1614
|
+
recovery
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
function record(value) {
|
|
1618
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1619
|
+
}
|
|
1620
|
+
function text(value) {
|
|
1621
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1622
|
+
}
|
|
1623
|
+
async function json(path, code, recovery) {
|
|
1624
|
+
try {
|
|
1625
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
throw failure(code, `无法读取有效 JSON:${path}`, {
|
|
1628
|
+
path,
|
|
1629
|
+
cause: error.message
|
|
1630
|
+
}, recovery);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
function date(value) {
|
|
1634
|
+
return typeof value === "string" && /T.*(?:Z|[+-]\d{2}:\d{2})$/u.test(value) ? Date.parse(value) : NaN;
|
|
1635
|
+
}
|
|
1636
|
+
/** A caller-provided observation is evidence, not a platform publication permit. */
|
|
1637
|
+
async function assertHostCompatibility(hostCompatPath, toolchain, { now = Date.now(), expectedHost, expectedEnvironment } = {}) {
|
|
1638
|
+
const profileId = toolchain.profile.id;
|
|
1639
|
+
if (!hostCompatPath) return {
|
|
1640
|
+
status: "unknown",
|
|
1641
|
+
profileId
|
|
1642
|
+
};
|
|
1643
|
+
const invalid = (reason) => {
|
|
1644
|
+
throw failure("HOST_COMPAT_INVALID", `宿主兼容声明无效:${reason}`, {
|
|
1645
|
+
path: resolve(hostCompatPath),
|
|
1646
|
+
expectedHost,
|
|
1647
|
+
expectedEnvironment
|
|
1648
|
+
}, ["重新获取本次目标环境的宿主兼容声明,再通过 --host-compat 传入。"]);
|
|
1649
|
+
};
|
|
1650
|
+
const value = await json(hostCompatPath, "HOST_COMPAT_INVALID", ["重新获取本次目标环境的宿主兼容声明。"]);
|
|
1651
|
+
if (!record(value) || !record(value.statement)) invalid("缺少 statement");
|
|
1652
|
+
const { statement, observation } = value;
|
|
1653
|
+
if (statement.formatVersion !== 1 || !text(statement.hostId) || !text(statement.environment) || !text(statement.revision) || !Array.isArray(statement.acceptedProfiles) || !statement.acceptedProfiles.every(text) || new Set(statement.acceptedProfiles).size !== statement.acceptedProfiles.length) invalid("statement 字段不符合 formatVersion=1");
|
|
1654
|
+
if (expectedHost && statement.hostId !== expectedHost || expectedEnvironment && statement.environment !== expectedEnvironment) invalid("hostId/environment 与目标环境不符");
|
|
1655
|
+
const issuedAt = date(statement.issuedAt);
|
|
1656
|
+
const expiresAt = date(statement.expiresAt);
|
|
1657
|
+
const observedNow = now instanceof Date ? now.getTime() : Number(now);
|
|
1658
|
+
if (!Number.isFinite(observedNow) || !Number.isFinite(issuedAt) || !Number.isFinite(expiresAt) || expiresAt <= issuedAt || issuedAt > observedNow) invalid("声明时间无效或尚未生效");
|
|
1659
|
+
if (!record(observation) || !["online", "snapshot"].includes(observation.source) || !Number.isFinite(date(observation.fetchedAt)) || date(observation.fetchedAt) < issuedAt || date(observation.fetchedAt) > observedNow) invalid("observation 必须包含有效 source 和 fetchedAt");
|
|
1660
|
+
if (!statement.acceptedProfiles.includes(profileId)) throw failure("HOST_PROFILE_UNSUPPORTED", `宿主 ${statement.hostId}/${statement.environment} 未接受当前工具链 ${profileId}`, {
|
|
1661
|
+
profileId,
|
|
1662
|
+
hostId: statement.hostId,
|
|
1663
|
+
environment: statement.environment,
|
|
1664
|
+
revision: statement.revision,
|
|
1665
|
+
acceptedProfiles: statement.acceptedProfiles
|
|
1666
|
+
}, ["使用经该宿主验收的 Widget CLI,或完成宿主平台升级;不要修改 SDK 版本字符串绕过校验。"]);
|
|
1667
|
+
const fresh = expiresAt > observedNow;
|
|
1668
|
+
return {
|
|
1669
|
+
status: observation.source === "online" && fresh ? "confirmed" : "snapshot",
|
|
1670
|
+
profileId,
|
|
1671
|
+
hostId: statement.hostId,
|
|
1672
|
+
environment: statement.environment,
|
|
1673
|
+
revision: statement.revision,
|
|
1674
|
+
issuedAt: statement.issuedAt,
|
|
1675
|
+
expiresAt: statement.expiresAt,
|
|
1676
|
+
observation,
|
|
1677
|
+
...!fresh ? { reason: "expired" } : {}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function assertMatchingLock(lock, toolchain, root) {
|
|
1681
|
+
const { profile } = toolchain;
|
|
1682
|
+
if (!record(lock) || lock.formatVersion !== 1 || lock.profileId !== profile.id) throw failure("PROJECT_PROFILE_MISMATCH", "工程锁定的 profile 与当前 Widget CLI 不一致", {
|
|
1683
|
+
projectRoot: root,
|
|
1684
|
+
expectedProfile: profile.id,
|
|
1685
|
+
actualProfile: lock?.profileId
|
|
1686
|
+
}, ["使用携带工程 profile 的 Widget CLI;跨 profile 迁移需显式修改和验收工程。"]);
|
|
1687
|
+
if (lock.sdk !== profile.sdk || lock.contracts !== profile.contracts) throw failure("PROJECT_SDK_MISMATCH", "工程锁定的 SDK/contracts 与当前 profile 不一致", {
|
|
1688
|
+
projectRoot: root,
|
|
1689
|
+
expected: profile,
|
|
1690
|
+
actual: lock
|
|
1691
|
+
}, ["恢复正确的 widget-toolchain.json 和精确依赖,再重新安装工程依赖。"]);
|
|
1692
|
+
return lock;
|
|
1693
|
+
}
|
|
1694
|
+
async function readLock(root, toolchain, required) {
|
|
1695
|
+
const path = join(root, PROJECT_TOOLCHAIN_FILE);
|
|
1696
|
+
let value;
|
|
1697
|
+
try {
|
|
1698
|
+
value = await readFile(path, "utf8");
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
if (error.code !== "ENOENT") throw error;
|
|
1701
|
+
if (!required) return null;
|
|
1702
|
+
throw failure("PROJECT_TOOLCHAIN_REQUIRED", "工程缺少 widget-toolchain.json,需先显式确认工具链", {
|
|
1703
|
+
projectRoot: root,
|
|
1704
|
+
path
|
|
1705
|
+
}, ["先执行 bohr-widget doctor,再执行 bohr-widget adopt <工程目录>。"]);
|
|
1706
|
+
}
|
|
1707
|
+
let lock;
|
|
1708
|
+
try {
|
|
1709
|
+
lock = JSON.parse(value);
|
|
1710
|
+
} catch {
|
|
1711
|
+
throw failure("PROJECT_PROFILE_MISMATCH", "widget-toolchain.json 不是有效 JSON", {
|
|
1712
|
+
projectRoot: root,
|
|
1713
|
+
path
|
|
1714
|
+
}, ["从版本库恢复有效的工程锁定信息,不要直接改 profile 绕过校验。"]);
|
|
1715
|
+
}
|
|
1716
|
+
return assertMatchingLock(lock, toolchain, root);
|
|
1717
|
+
}
|
|
1718
|
+
/** Resolve from this project's node_modules, never from an ancestor or the CLI. */
|
|
1719
|
+
async function validateProjectToolchain(projectRoot, toolchain, { requireInstalled = true, requireLock = true } = {}) {
|
|
1720
|
+
const root = resolve(projectRoot);
|
|
1721
|
+
const lock = await readLock(root, toolchain, requireLock);
|
|
1722
|
+
const pkg = await json(join(root, "package.json"), "PROJECT_SDK_MISMATCH", ["恢复工程 package.json 中的精确 SDK/contracts 依赖。"]);
|
|
1723
|
+
if (!record(pkg)) throw failure("PROJECT_SDK_MISMATCH", "工程 package.json 必须是对象", { projectRoot: root }, ["恢复工程 package.json 中的精确 SDK/contracts 依赖。"]);
|
|
1724
|
+
const declared = {};
|
|
1725
|
+
const installed = {};
|
|
1726
|
+
for (const [key, name] of Object.entries(PACKAGES)) {
|
|
1727
|
+
const declarations = [
|
|
1728
|
+
"dependencies",
|
|
1729
|
+
"devDependencies",
|
|
1730
|
+
"optionalDependencies"
|
|
1731
|
+
].map((section) => pkg[section]?.[name]).filter((version) => version !== void 0);
|
|
1732
|
+
const expected = toolchain.profile[key];
|
|
1733
|
+
if (declarations.length === 0 || declarations.some((version) => typeof version !== "string" || !EXACT_VERSION.test(version) || version !== expected)) throw failure("PROJECT_SDK_MISMATCH", `${name} 必须精确声明为 ${expected}`, {
|
|
1734
|
+
projectRoot: root,
|
|
1735
|
+
package: name,
|
|
1736
|
+
expected,
|
|
1737
|
+
declared: declarations
|
|
1738
|
+
}, ["按 widget-toolchain.json 恢复精确版本,再重新安装工程依赖;不要使用 ^、~、workspace 或 latest。"]);
|
|
1739
|
+
declared[key] = expected;
|
|
1740
|
+
if (!requireInstalled) continue;
|
|
1741
|
+
const actual = await json(join(root, "node_modules", name, "package.json"), "PROJECT_SDK_MISMATCH", ["在该 Widget 工程目录安装依赖,再重试;父目录或 CLI 的安装不能替代工程安装。"]);
|
|
1742
|
+
if (!record(actual) || actual.name !== name || actual.version !== expected) throw failure("PROJECT_SDK_MISMATCH", `${name} 实际安装版本与工程工具链不一致`, {
|
|
1743
|
+
projectRoot: root,
|
|
1744
|
+
package: name,
|
|
1745
|
+
expected,
|
|
1746
|
+
installed: actual?.version
|
|
1747
|
+
}, ["按工程锁定信息恢复依赖并重新安装,再重新构建。"]);
|
|
1748
|
+
installed[key] = actual.version;
|
|
1749
|
+
}
|
|
1750
|
+
return {
|
|
1751
|
+
projectRoot: root,
|
|
1752
|
+
profile: { ...toolchain.profile },
|
|
1753
|
+
declared,
|
|
1754
|
+
installed,
|
|
1755
|
+
dependenciesVerified: requireInstalled,
|
|
1756
|
+
lock
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
async function writeProjectToolchain(projectRoot, toolchain) {
|
|
1760
|
+
const root = resolve(projectRoot);
|
|
1761
|
+
const existing = await readLock(root, toolchain, false);
|
|
1762
|
+
if (existing) return existing;
|
|
1763
|
+
const value = {
|
|
1764
|
+
formatVersion: 1,
|
|
1765
|
+
profileId: toolchain.profile.id,
|
|
1766
|
+
sdk: toolchain.profile.sdk,
|
|
1767
|
+
contracts: toolchain.profile.contracts,
|
|
1768
|
+
createdWithCli: toolchain.cli
|
|
1769
|
+
};
|
|
1770
|
+
await mkdir(root, { recursive: true });
|
|
1771
|
+
try {
|
|
1772
|
+
await writeFile(join(root, PROJECT_TOOLCHAIN_FILE), `${JSON.stringify(value, null, 2)}\n`, { flag: "wx" });
|
|
1773
|
+
} catch (error) {
|
|
1774
|
+
if (error.code !== "EEXIST") throw error;
|
|
1775
|
+
return readLock(root, toolchain, true);
|
|
1776
|
+
}
|
|
1777
|
+
return value;
|
|
1778
|
+
}
|
|
1779
|
+
async function adoptProjectToolchain(projectRoot, toolchain) {
|
|
1780
|
+
const result = await validateProjectToolchain(projectRoot, toolchain, { requireLock: false });
|
|
1781
|
+
const check = await checkWidgetProject(result.projectRoot, {
|
|
1782
|
+
requireBuiltEntry: false,
|
|
1783
|
+
toolchain: { sdkVersion: toolchain.profile.sdk }
|
|
1784
|
+
});
|
|
1785
|
+
const lint = looksLikeAuthorManifest(check.sourceManifest) ? lintAuthorManifest(check.sourceManifest) : {
|
|
1786
|
+
ok: true,
|
|
1787
|
+
errors: []
|
|
1788
|
+
};
|
|
1789
|
+
if (!lint.ok || !check.ok || check.manifest?.widgetConfig?.widgetSdkVersion !== toolchain.profile.sdk) throw failure("PROJECT_PROFILE_MISMATCH", "工程 Manifest 或 samples 无法通过当前 profile 校验,不能直接 adopt", {
|
|
1790
|
+
projectRoot: result.projectRoot,
|
|
1791
|
+
profileId: toolchain.profile.id,
|
|
1792
|
+
errors: [...lint.errors, ...check.errors],
|
|
1793
|
+
manifestSdkVersion: check.manifest?.widgetConfig?.widgetSdkVersion
|
|
1794
|
+
}, ["先按当前工具链修复 Manifest 和 samples;adopt 不会迁移 SDK、源码或协议字段。"]);
|
|
1795
|
+
const lock = await writeProjectToolchain(result.projectRoot, toolchain);
|
|
1796
|
+
return {
|
|
1797
|
+
...result,
|
|
1798
|
+
lock
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
function digest(value) {
|
|
1802
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
1803
|
+
}
|
|
1804
|
+
function receiptToolchain(toolchain) {
|
|
1805
|
+
return {
|
|
1806
|
+
cli: toolchain.cli,
|
|
1807
|
+
profile: toolchain.profile,
|
|
1808
|
+
digests: toolchain.digests
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1812
|
+
"node_modules",
|
|
1813
|
+
".git",
|
|
1814
|
+
".widget-build",
|
|
1815
|
+
".widget-dev"
|
|
1816
|
+
]);
|
|
1817
|
+
async function fingerprint(root, { outputs = false } = {}) {
|
|
1818
|
+
const files = [];
|
|
1819
|
+
const walk = async (directory, prefix, ancestors) => {
|
|
1820
|
+
const actualDirectory = await realpath(directory);
|
|
1821
|
+
if (ancestors.has(actualDirectory)) throw new Error(`文件目录存在符号链接循环:${prefix}`);
|
|
1822
|
+
const nextAncestors = /* @__PURE__ */ new Set([...ancestors, actualDirectory]);
|
|
1823
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
1824
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name, "en"))) {
|
|
1825
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1826
|
+
if (IGNORED_DIRECTORIES.has(entry.name) || !outputs && !prefix && entry.name === "dist" || entry.name === "widget-toolchain.json" || entry.name === ".widget-build-receipt.json" || /\.zip$/iu.test(entry.name)) continue;
|
|
1827
|
+
const path = join(directory, entry.name);
|
|
1828
|
+
const metadata = await stat(path);
|
|
1829
|
+
if (metadata.isDirectory()) await walk(path, relativePath, nextAncestors);
|
|
1830
|
+
else if (metadata.isFile()) files.push({
|
|
1831
|
+
path: relativePath,
|
|
1832
|
+
sha256: digest(await readFile(path))
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
await walk(outputs ? join(root, "dist") : root, "", /* @__PURE__ */ new Set());
|
|
1837
|
+
return {
|
|
1838
|
+
digest: digest(JSON.stringify(files)),
|
|
1839
|
+
files
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
async function captureBuildInputs(projectRoot) {
|
|
1843
|
+
return fingerprint(resolve(projectRoot));
|
|
1844
|
+
}
|
|
1845
|
+
async function writeBuildReceipt(projectRoot, toolchain, { inputsBefore } = {}) {
|
|
1846
|
+
const result = await validateProjectToolchain(projectRoot, toolchain);
|
|
1847
|
+
const inputs = await fingerprint(result.projectRoot);
|
|
1848
|
+
if (inputsBefore && inputsBefore.digest !== inputs.digest) throw failure("BUILD_RECEIPT_MISMATCH", "构建期间源输入发生变化,不能确认产物对应当前工程", { projectRoot: result.projectRoot }, ["保存工程文件后重新执行 bohr-widget build;构建结束前不要继续修改输入文件。"]);
|
|
1849
|
+
const outputs = await fingerprint(result.projectRoot, { outputs: true });
|
|
1850
|
+
if (outputs.files.length === 0) throw failure("BUILD_RECEIPT_MISMATCH", "构建目录 dist 没有可记录的产物", { projectRoot: result.projectRoot }, ["执行标准 build,确认 dist 产物存在后再打包。"]);
|
|
1851
|
+
const receipt = {
|
|
1852
|
+
formatVersion: 1,
|
|
1853
|
+
toolchain: receiptToolchain(toolchain),
|
|
1854
|
+
dependencies: result.installed,
|
|
1855
|
+
inputs,
|
|
1856
|
+
outputs
|
|
1857
|
+
};
|
|
1858
|
+
await writeFile(join(result.projectRoot, BUILD_RECEIPT_FILE), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1859
|
+
return receipt;
|
|
1860
|
+
}
|
|
1861
|
+
async function assertBuildReceipt(projectRoot, toolchain) {
|
|
1862
|
+
const root = resolve(projectRoot);
|
|
1863
|
+
const mismatch = (reason, context = {}) => {
|
|
1864
|
+
throw failure("BUILD_RECEIPT_MISMATCH", `不能复用构建产物:${reason}`, {
|
|
1865
|
+
projectRoot: root,
|
|
1866
|
+
...context
|
|
1867
|
+
}, ["重新执行 bohr-widget build 或去掉 pack 的 --no-build。"]);
|
|
1868
|
+
};
|
|
1869
|
+
const receipt = await json(join(root, BUILD_RECEIPT_FILE), "BUILD_RECEIPT_MISMATCH", ["先执行 bohr-widget build,或去掉 pack 的 --no-build。"]);
|
|
1870
|
+
const project = await validateProjectToolchain(root, toolchain);
|
|
1871
|
+
if (!record(receipt) || receipt.formatVersion !== 1 || JSON.stringify(receipt.toolchain) !== JSON.stringify(receiptToolchain(toolchain)) || JSON.stringify(receipt.dependencies) !== JSON.stringify(project.installed)) mismatch("receipt 工具链或实际依赖与本次执行不一致");
|
|
1872
|
+
let inputs;
|
|
1873
|
+
let outputs;
|
|
1874
|
+
try {
|
|
1875
|
+
inputs = await fingerprint(root);
|
|
1876
|
+
outputs = await fingerprint(root, { outputs: true });
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
mismatch("无法读取构建输入或 dist", { cause: error.message });
|
|
1879
|
+
}
|
|
1880
|
+
if (receipt.inputs?.digest !== inputs.digest || receipt.outputs?.digest !== outputs.digest || outputs.files.length === 0) mismatch("源文件或 dist 内容已变化");
|
|
1881
|
+
return receipt;
|
|
1882
|
+
}
|
|
1883
|
+
//#endregion
|
|
1236
1884
|
//#region src/templates.mjs
|
|
1885
|
+
const GENERATED_PROJECT_NODE = "^22.18.0 || >=24.11.0";
|
|
1886
|
+
function projectPackage({ name, ui, cliVersion, sdkVersion, contractsVersion }) {
|
|
1887
|
+
return {
|
|
1888
|
+
name: `@dp-bohrium/widget-example-${name}`,
|
|
1889
|
+
private: true,
|
|
1890
|
+
type: "module",
|
|
1891
|
+
engines: { node: GENERATED_PROJECT_NODE },
|
|
1892
|
+
scripts: {
|
|
1893
|
+
build: "tsc --noEmit && tsdown && node scripts/rename-iife.mjs",
|
|
1894
|
+
check: "bohr-widget check .",
|
|
1895
|
+
"widget:pack": "bohr-widget pack ."
|
|
1896
|
+
},
|
|
1897
|
+
dependencies: {
|
|
1898
|
+
"@dp-bohrium/widget-sdk": sdkVersion,
|
|
1899
|
+
...ui === "react" ? {
|
|
1900
|
+
react: "^19.0.0",
|
|
1901
|
+
"react-dom": "^19.0.0"
|
|
1902
|
+
} : {}
|
|
1903
|
+
},
|
|
1904
|
+
devDependencies: {
|
|
1905
|
+
"@dp-bohrium/widget-cli": cliVersion,
|
|
1906
|
+
"@dp-bohrium/widget-contracts": contractsVersion,
|
|
1907
|
+
tsdown: "0.22.14",
|
|
1908
|
+
typescript: "5.9.3",
|
|
1909
|
+
...ui === "react" ? {
|
|
1910
|
+
"@types/react": "^19.0.0",
|
|
1911
|
+
"@types/react-dom": "^19.0.0"
|
|
1912
|
+
} : {}
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
function projectReadme({ name, template, ui, sourceFile, hostFile }) {
|
|
1917
|
+
return `# ${name}
|
|
1918
|
+
|
|
1919
|
+
Bohrium Widget ${template} 插件工程(${ui})。
|
|
1920
|
+
|
|
1921
|
+
默认构建器要求 Node.js \`${GENERATED_PROJECT_NODE}\`,高于 CLI 自身的 Node.js 20 要求。
|
|
1922
|
+
工程已经精确依赖创建它的 CLI 版本;在本目录安装后使用本地命令,无需全局安装或源码仓库。
|
|
1923
|
+
|
|
1924
|
+
\`\`\`bash
|
|
1925
|
+
pnpm install
|
|
1926
|
+
pnpm exec bohr-widget build . --json
|
|
1927
|
+
pnpm exec bohr-widget check . --json
|
|
1928
|
+
pnpm run widget:pack --json
|
|
1929
|
+
\`\`\`
|
|
1930
|
+
|
|
1931
|
+
使用 npm 时,安装后可执行 \`npm run build\`、\`npm run check\`、\`npm run widget:pack\`;需要构建 receipt 时执行 \`npx --no-install bohr-widget build .\`。
|
|
1932
|
+
Widget ZIP 使用 \`pnpm run widget:pack\` 或 \`npm run widget:pack\`;\`pnpm pack\` 是制作 npm \`.tgz\` 包的内置命令。
|
|
1933
|
+
|
|
1934
|
+
- 源码入口:${sourceFile},由 ${hostFile} 调用 SDK 的 runWidgetHost 启动。
|
|
1935
|
+
- 高度链、布局与滚动策略直接写在 ${sourceFile} 中,样式随 activate 安装并在释放时清理;JSON 展示元素单独处理换行。
|
|
1936
|
+
- manifest.json 是作者清单,打包时由 CLI 编译为宿主清单。
|
|
1937
|
+
- 提交 widget-toolchain.json 与依赖 lockfile;dist/widget.js 与生成的 ZIP 不提交。
|
|
1938
|
+
- check 只验证结构、声明和样例,不执行 JavaScript;pack 成功不代表首帧渲染、真实授权或业务验收成功。
|
|
1939
|
+
- 上传前至少运行首帧、一次事件后再 render、空数据、缺授权和尺寸/主题更新,并检查滚动区域实际可见。
|
|
1940
|
+
- 本地开发连接使用 \`pnpm exec bohr-widget dev . --open --watch\`,需要已安装且支持开发接入的目标 Host。
|
|
1941
|
+
|
|
1942
|
+
通过 \`pnpm exec bohr-widget integration --project . --json\` 查看工具链和公开文档地址。
|
|
1943
|
+
本地创建、构建、校验与打包无需平台登录;平台上传和绑定由目标平台提供。
|
|
1944
|
+
`;
|
|
1945
|
+
}
|
|
1946
|
+
function layoutSource() {
|
|
1947
|
+
return ` // 在作者源码中维护高度链与滚动策略,随本次 activate 一起释放。
|
|
1948
|
+
const style = document.createElement("style");
|
|
1949
|
+
style.textContent = \`
|
|
1950
|
+
* { box-sizing: border-box; }
|
|
1951
|
+
html, body { height: 100%; min-height: 0; margin: 0; }
|
|
1952
|
+
body { font-family: system-ui, sans-serif; overflow: hidden; }
|
|
1953
|
+
.widget-root { height: 100%; min-height: 0; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
1954
|
+
.widget-json { flex: 1; min-height: 0; min-width: 0; margin: 0; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1955
|
+
\`;
|
|
1956
|
+
document.head.append(style);
|
|
1957
|
+
context.subscriptions.add(() => style.remove());
|
|
1958
|
+
`;
|
|
1959
|
+
}
|
|
1237
1960
|
function reactSource({ reportSize = false } = {}) {
|
|
1961
|
+
const resizeState = reportSize ? "\n let resized = false;" : "";
|
|
1962
|
+
const resizeCall = reportSize ? `
|
|
1963
|
+
if (!resized) {
|
|
1964
|
+
context.resize(640, 360);
|
|
1965
|
+
resized = true;
|
|
1966
|
+
}` : "";
|
|
1238
1967
|
return `import { createRoot } from "react-dom/client";
|
|
1239
1968
|
import type { WidgetContext } from "@dp-bohrium/widget-sdk";
|
|
1240
1969
|
import type { RenderRequest } from "@dp-bohrium/widget-contracts";
|
|
@@ -1242,9 +1971,9 @@ import type { RenderRequest } from "@dp-bohrium/widget-contracts";
|
|
|
1242
1971
|
function WidgetView({ request }: { request?: RenderRequest }) {
|
|
1243
1972
|
const data = request?.content ?? { state: "等待 Host 提供数据" };
|
|
1244
1973
|
return (
|
|
1245
|
-
<main style={{
|
|
1246
|
-
<h1>Widget 面板</h1>
|
|
1247
|
-
<pre
|
|
1974
|
+
<main style={{ flex: 1, minHeight: 0, minWidth: 0, display: "flex", flexDirection: "column", padding: 20 }}>
|
|
1975
|
+
<h1 style={{ flexShrink: 0 }}>Widget 面板</h1>
|
|
1976
|
+
<pre className="widget-json">
|
|
1248
1977
|
{JSON.stringify(data, null, 2)}
|
|
1249
1978
|
</pre>
|
|
1250
1979
|
</main>
|
|
@@ -1252,21 +1981,23 @@ function WidgetView({ request }: { request?: RenderRequest }) {
|
|
|
1252
1981
|
}
|
|
1253
1982
|
|
|
1254
1983
|
function activate(context: WidgetContext) {
|
|
1255
|
-
const host = document.createElement("
|
|
1984
|
+
${layoutSource()} const host = document.createElement("div");
|
|
1985
|
+
host.className = "widget-root";
|
|
1256
1986
|
document.body.replaceChildren(host);
|
|
1257
1987
|
const root = createRoot(host);
|
|
1258
|
-
let renderRequest: RenderRequest | undefined;${
|
|
1988
|
+
let renderRequest: RenderRequest | undefined;${resizeState}
|
|
1259
1989
|
const render = () => root.render(<WidgetView request={renderRequest} />);
|
|
1260
1990
|
render();
|
|
1261
1991
|
context.render((request) => {
|
|
1992
|
+
document.body.style.background = request.context.theme.tokens.background;
|
|
1993
|
+
document.body.style.color = request.context.theme.tokens.foreground;
|
|
1262
1994
|
renderRequest = request;
|
|
1263
|
-
render();${
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1995
|
+
render();${resizeCall}
|
|
1996
|
+
});
|
|
1997
|
+
context.subscriptions.add(() => {
|
|
1998
|
+
root.unmount();
|
|
1999
|
+
host.remove();
|
|
1268
2000
|
});
|
|
1269
|
-
context.subscriptions.add(() => root.unmount());
|
|
1270
2001
|
}
|
|
1271
2002
|
|
|
1272
2003
|
export { activate };
|
|
@@ -1276,7 +2007,13 @@ function hostSource({ sourceFile }) {
|
|
|
1276
2007
|
return `import { runWidgetHost } from "@dp-bohrium/widget-sdk";
|
|
1277
2008
|
import * as widget from "./${sourceFile.split("/").at(-1)}";
|
|
1278
2009
|
|
|
1279
|
-
runWidgetHost(widget
|
|
2010
|
+
runWidgetHost(widget, {
|
|
2011
|
+
events: { onError: (error) => console.error("Widget 协议错误", error) },
|
|
2012
|
+
onStartupError: (error) => {
|
|
2013
|
+
document.body.textContent = "Widget 初始化失败:" + error.message;
|
|
2014
|
+
console.error("Widget 初始化失败", error);
|
|
2015
|
+
},
|
|
2016
|
+
});
|
|
1280
2017
|
`;
|
|
1281
2018
|
}
|
|
1282
2019
|
const TEMPLATES = Object.freeze({
|
|
@@ -1307,12 +2044,20 @@ const TEMPLATES = Object.freeze({
|
|
|
1307
2044
|
source: `import type { WidgetContext } from "@dp-bohrium/widget-sdk";
|
|
1308
2045
|
|
|
1309
2046
|
function activate(context: WidgetContext) {
|
|
1310
|
-
const root = document.createElement("main");
|
|
1311
|
-
root.
|
|
2047
|
+
${layoutSource()} const root = document.createElement("main");
|
|
2048
|
+
root.className = "widget-root";
|
|
2049
|
+
root.style.padding = "20px";
|
|
2050
|
+
const display = document.createElement("pre");
|
|
2051
|
+
display.className = "widget-json";
|
|
2052
|
+
display.textContent = "等待 Host 提供面板数据";
|
|
2053
|
+
root.append(display);
|
|
1312
2054
|
document.body.replaceChildren(root);
|
|
2055
|
+
context.subscriptions.add(() => root.remove());
|
|
1313
2056
|
context.render((request) => {
|
|
2057
|
+
document.body.style.background = request.context.theme.tokens.background;
|
|
2058
|
+
document.body.style.color = request.context.theme.tokens.foreground;
|
|
1314
2059
|
const content = request.content;
|
|
1315
|
-
|
|
2060
|
+
display.textContent = JSON.stringify(
|
|
1316
2061
|
"data" in content ? content.data : content,
|
|
1317
2062
|
null,
|
|
1318
2063
|
2,
|
|
@@ -1405,11 +2150,19 @@ export { activate };
|
|
|
1405
2150
|
source: `import type { WidgetContext } from "@dp-bohrium/widget-sdk";
|
|
1406
2151
|
|
|
1407
2152
|
function activate(context: WidgetContext) {
|
|
1408
|
-
const root = document.createElement("main");
|
|
2153
|
+
${layoutSource()} const root = document.createElement("main");
|
|
2154
|
+
root.className = "widget-root";
|
|
2155
|
+
root.style.padding = "20px";
|
|
2156
|
+
const display = document.createElement("pre");
|
|
2157
|
+
display.className = "widget-json";
|
|
2158
|
+
root.append(display);
|
|
1409
2159
|
document.body.replaceChildren(root);
|
|
2160
|
+
context.subscriptions.add(() => root.remove());
|
|
1410
2161
|
let resized = false;
|
|
1411
2162
|
context.render((request) => {
|
|
1412
|
-
|
|
2163
|
+
document.body.style.background = request.context.theme.tokens.background;
|
|
2164
|
+
document.body.style.color = request.context.theme.tokens.foreground;
|
|
2165
|
+
display.textContent = JSON.stringify(request.content ?? {}, null, 2);
|
|
1413
2166
|
if (!resized) {
|
|
1414
2167
|
context.resize(640, 360);
|
|
1415
2168
|
resized = true;
|
|
@@ -1466,43 +2219,58 @@ export { activate };
|
|
|
1466
2219
|
}
|
|
1467
2220
|
});
|
|
1468
2221
|
//#endregion
|
|
2222
|
+
//#region src/toolchain.mjs
|
|
2223
|
+
/** Both src/ and the published dist/ locate resources in this package. */
|
|
2224
|
+
function getAssetRoot() {
|
|
2225
|
+
return fileURLToPath(new URL("../assets/", import.meta.url));
|
|
2226
|
+
}
|
|
2227
|
+
function getToolchain() {
|
|
2228
|
+
const path = join(getAssetRoot(), "toolchain.json");
|
|
2229
|
+
if (!existsSync(path)) throw new Error("缺少 Widget CLI 工具链资产;源码运行前请执行 pnpm --filter @dp-bohrium/widget-cli build:assets,发布包请重新安装。");
|
|
2230
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
2231
|
+
}
|
|
2232
|
+
function getDocumentation() {
|
|
2233
|
+
return getToolchain().documentation;
|
|
2234
|
+
}
|
|
2235
|
+
function getSchemaPath(name = "widget-manifest.schema.json") {
|
|
2236
|
+
if (name !== basename(name) || !name.endsWith(".schema.json")) throw new Error(`不支持的 Schema 名称:${name}`);
|
|
2237
|
+
const path = join(getAssetRoot(), "schemas", name);
|
|
2238
|
+
if (!existsSync(path)) throw new Error(`Widget CLI Schema 不存在:${name}`);
|
|
2239
|
+
return path;
|
|
2240
|
+
}
|
|
2241
|
+
//#endregion
|
|
1469
2242
|
//#region src/cli.mjs
|
|
1470
|
-
const
|
|
1471
|
-
const packageJson =
|
|
1472
|
-
const sdkPackage =
|
|
1473
|
-
const contractsPackage =
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
"
|
|
1485
|
-
"help"
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
bohr-widget version [--json]
|
|
1502
|
-
bohr-widget help | -h | --help
|
|
1503
|
-
|
|
1504
|
-
--json 只输出一个 JSON 文档;check all --json 输出 checked/passed/failed/results[] 汇总。
|
|
1505
|
-
`;
|
|
2243
|
+
const toolchain = getToolchain();
|
|
2244
|
+
const packageJson = { version: toolchain.cli };
|
|
2245
|
+
const sdkPackage = { version: toolchain.profile.sdk };
|
|
2246
|
+
const contractsPackage = { version: toolchain.profile.contracts };
|
|
2247
|
+
function usage(path = []) {
|
|
2248
|
+
return renderHelp(path, {
|
|
2249
|
+
cli: packageJson.version,
|
|
2250
|
+
sdk: sdkPackage.version,
|
|
2251
|
+
contracts: contractsPackage.version,
|
|
2252
|
+
documentation: getDocumentation(),
|
|
2253
|
+
generatedProjectNode: GENERATED_PROJECT_NODE
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
function requestedHelpPath(argv) {
|
|
2257
|
+
const hasHelpFlag = argv.includes("--help") || argv.includes("-h");
|
|
2258
|
+
if (argv.length > 0 && argv[0] !== "help" && !hasHelpFlag) return null;
|
|
2259
|
+
const args = argv.filter((arg) => arg !== "--help" && arg !== "-h");
|
|
2260
|
+
if (args[0] === "help") {
|
|
2261
|
+
if (args.length === 1 && hasHelpFlag) return ["help"];
|
|
2262
|
+
args.shift();
|
|
2263
|
+
}
|
|
2264
|
+
const command = args[0];
|
|
2265
|
+
if (!command || command.startsWith("-")) return [];
|
|
2266
|
+
if (command !== "migrate") return [command];
|
|
2267
|
+
let positional;
|
|
2268
|
+
try {
|
|
2269
|
+
positional = parseOptions(args.slice(1)).positional;
|
|
2270
|
+
} catch {
|
|
2271
|
+
positional = args.slice(1).filter((arg) => !arg.startsWith("-"));
|
|
2272
|
+
}
|
|
2273
|
+
return [command, ...positional.slice(0, 2)];
|
|
1506
2274
|
}
|
|
1507
2275
|
function parseOptions(argv) {
|
|
1508
2276
|
const positional = [];
|
|
@@ -1518,7 +2286,9 @@ function parseOptions(argv) {
|
|
|
1518
2286
|
port: 4300,
|
|
1519
2287
|
scheme: "widget-host",
|
|
1520
2288
|
bridge: null,
|
|
1521
|
-
panelId: void 0
|
|
2289
|
+
panelId: void 0,
|
|
2290
|
+
hostCompat: void 0,
|
|
2291
|
+
project: void 0
|
|
1522
2292
|
};
|
|
1523
2293
|
for (let index = 0; index < argv.length; index += 1) {
|
|
1524
2294
|
const argument = argv[index];
|
|
@@ -1537,7 +2307,12 @@ function parseOptions(argv) {
|
|
|
1537
2307
|
options.output = argv[++index];
|
|
1538
2308
|
if (!options.output) throw new Error("--out 需要输出路径");
|
|
1539
2309
|
} else if (argument === "--open") options.open = true;
|
|
1540
|
-
else if (argument === "--
|
|
2310
|
+
else if (argument === "--host-compat" || argument === "--project") {
|
|
2311
|
+
const value = argv[++index];
|
|
2312
|
+
if (!value || value.startsWith("--")) throw new Error(`${argument} 需要路径`);
|
|
2313
|
+
if (argument === "--host-compat") options.hostCompat = value;
|
|
2314
|
+
else options.project = value;
|
|
2315
|
+
} else if (argument === "--watch") options.watch = true;
|
|
1541
2316
|
else if (argument === "--no-build") options.build = false;
|
|
1542
2317
|
else if (argument === "--port") options.port = Number(argv[++index]);
|
|
1543
2318
|
else if (argument === "--scheme") options.scheme = argv[++index];
|
|
@@ -1585,10 +2360,65 @@ function commandError(command, error, context = {}) {
|
|
|
1585
2360
|
return structuredError({
|
|
1586
2361
|
code: typeof error?.cliCode === "string" ? error.cliCode : typeof error?.code === "string" && error.code.startsWith("WIDGET_") ? error.code : `WIDGET_CLI_${commandCode(command)}_FAILED`,
|
|
1587
2362
|
message: error instanceof Error ? error.message : String(error),
|
|
1588
|
-
context
|
|
1589
|
-
|
|
2363
|
+
context: {
|
|
2364
|
+
...context,
|
|
2365
|
+
...error?.context
|
|
2366
|
+
},
|
|
2367
|
+
recovery: error?.recovery ?? commandRecovery(command, error)
|
|
1590
2368
|
});
|
|
1591
2369
|
}
|
|
2370
|
+
async function projectPreflight(projectRoot, options = {}) {
|
|
2371
|
+
const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
|
|
2372
|
+
const project = await validateProjectToolchain(projectRoot, toolchain);
|
|
2373
|
+
return {
|
|
2374
|
+
profile: toolchain.profile,
|
|
2375
|
+
hostCompatibility,
|
|
2376
|
+
project
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
async function commandIntegration(options) {
|
|
2380
|
+
const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
|
|
2381
|
+
const project = options.project ? await validateProjectToolchain(resolve(options.project), toolchain, { requireInstalled: existsSync(join(resolve(options.project), "node_modules")) }) : void 0;
|
|
2382
|
+
const result = {
|
|
2383
|
+
ok: true,
|
|
2384
|
+
command: "integration",
|
|
2385
|
+
interfaceVersion: 2,
|
|
2386
|
+
requirements: {
|
|
2387
|
+
node: ">=20",
|
|
2388
|
+
generatedProjectNode: GENERATED_PROJECT_NODE
|
|
2389
|
+
},
|
|
2390
|
+
execution: {
|
|
2391
|
+
executable: process.execPath,
|
|
2392
|
+
entry: fileURLToPath(import.meta.url)
|
|
2393
|
+
},
|
|
2394
|
+
profile: toolchain.profile,
|
|
2395
|
+
toolchain,
|
|
2396
|
+
assetRoot: getAssetRoot(),
|
|
2397
|
+
documentation: getDocumentation(),
|
|
2398
|
+
hostCompatibility,
|
|
2399
|
+
...project ? { project } : {}
|
|
2400
|
+
};
|
|
2401
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
2402
|
+
else console.log(`${toolchain.profile.id}\n${result.documentation.url}`);
|
|
2403
|
+
return result;
|
|
2404
|
+
}
|
|
2405
|
+
async function commandAdopt(directory, options) {
|
|
2406
|
+
if (!directory) throw new Error("adopt 需要工程目录");
|
|
2407
|
+
const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
|
|
2408
|
+
const projectRoot = resolve(directory);
|
|
2409
|
+
const project = await adoptProjectToolchain(projectRoot, toolchain);
|
|
2410
|
+
const result = {
|
|
2411
|
+
ok: true,
|
|
2412
|
+
command: "adopt",
|
|
2413
|
+
projectRoot,
|
|
2414
|
+
profile: toolchain.profile,
|
|
2415
|
+
project,
|
|
2416
|
+
hostCompatibility
|
|
2417
|
+
};
|
|
2418
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
2419
|
+
else console.log(`已锁定工程工具链:${toolchain.profile.id}`);
|
|
2420
|
+
return result;
|
|
2421
|
+
}
|
|
1592
2422
|
/**
|
|
1593
2423
|
* 目录名 → Widget name(同时用作 panel id)。中文或纯符号目录名必须回退成合法标识符,
|
|
1594
2424
|
* 不能净化成 "----" 这类空壳,否则 manifest 的 name/id 会直接违反作者清单契约。
|
|
@@ -1599,6 +2429,7 @@ function deriveWidgetName(directoryName) {
|
|
|
1599
2429
|
return slug.slice(0, 64).replace(/-+$/gu, "");
|
|
1600
2430
|
}
|
|
1601
2431
|
async function commandCreate(directory, options) {
|
|
2432
|
+
const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
|
|
1602
2433
|
if (!directory) throw new Error("create 需要目标目录");
|
|
1603
2434
|
const template = TEMPLATES[options.template];
|
|
1604
2435
|
if (!template) throw new Error(`未知模板:${options.template}`);
|
|
@@ -1632,22 +2463,14 @@ async function commandCreate(directory, options) {
|
|
|
1632
2463
|
await writeFile(join(root, sourceFile), source);
|
|
1633
2464
|
await writeFile(join(root, hostFile), template.hostSource({ sourceFile }));
|
|
1634
2465
|
await writeFile(join(root, "scripts/rename-iife.mjs"), `import { rename } from "node:fs/promises";\n\nawait rename("dist/widget.iife.js", "dist/widget.js");\n`);
|
|
1635
|
-
await writeFile(join(root, ".gitignore"), "node_modules/\ndist/\n.artifacts/\n*.zip\n");
|
|
1636
|
-
await writeFile(join(root, "README.md"),
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
bohr-widget check .
|
|
1644
|
-
bohr-widget pack .
|
|
1645
|
-
\`\`\`
|
|
1646
|
-
|
|
1647
|
-
- 源码入口:${sourceFile},由 ${hostFile} 调用 SDK 的 runWidgetHost 启动
|
|
1648
|
-
- manifest.json 是作者清单,打包时由 CLI 编译为宿主清单
|
|
1649
|
-
- 构建产物 dist/widget.js 不提交到版本库
|
|
1650
|
-
`);
|
|
2466
|
+
await writeFile(join(root, ".gitignore"), "node_modules/\ndist/\n.artifacts/\n.widget-build-receipt.json\n*.zip\n");
|
|
2467
|
+
await writeFile(join(root, "README.md"), projectReadme({
|
|
2468
|
+
name,
|
|
2469
|
+
template: options.template,
|
|
2470
|
+
ui: options.ui,
|
|
2471
|
+
sourceFile,
|
|
2472
|
+
hostFile
|
|
2473
|
+
}));
|
|
1651
2474
|
await writeFile(join(root, "tsconfig.json"), `${JSON.stringify({
|
|
1652
2475
|
compilerOptions: {
|
|
1653
2476
|
target: "ES2022",
|
|
@@ -1663,29 +2486,15 @@ bohr-widget pack .
|
|
|
1663
2486
|
include: ["src"]
|
|
1664
2487
|
}, null, 2)}\n`);
|
|
1665
2488
|
await writeFile(join(root, "tsdown.config.ts"), `import { defineConfig } from "tsdown";\n\nexport default defineConfig({\n entry: { widget: "${hostFile}" },\n format: ["iife"],\n platform: "browser",\n target: "es2022",\n ${options.ui === "react" ? "jsx: \"automatic\"," : ""}\n dts: false,\n outDir: "dist",\n outExtensions: () => ({ js: ".js" }),\n clean: true,\n sourcemap: false,\n minify: true,\n deps: {\n alwaysBundle: ["@dp-bohrium/widget-sdk"${options.ui === "react" ? ", \"react\", \"react-dom\", \"react-dom/client\", \"react/jsx-runtime\"" : ""}],\n onlyBundle: false,\n },\n});\n`);
|
|
1666
|
-
await writeFile(join(root, "package.json"), `${JSON.stringify({
|
|
1667
|
-
name
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
...options.ui === "react" ? {
|
|
1674
|
-
react: "^19.0.0",
|
|
1675
|
-
"react-dom": "^19.0.0"
|
|
1676
|
-
} : {}
|
|
1677
|
-
},
|
|
1678
|
-
devDependencies: {
|
|
1679
|
-
"@dp-bohrium/widget-contracts": contractsPackage.version,
|
|
1680
|
-
tsdown: "0.22.14",
|
|
1681
|
-
typescript: "5.9.3",
|
|
1682
|
-
...options.ui === "react" ? {
|
|
1683
|
-
"@types/react": "^19.0.0",
|
|
1684
|
-
"@types/react-dom": "^19.0.0"
|
|
1685
|
-
} : {}
|
|
1686
|
-
}
|
|
1687
|
-
}, null, 2)}\n`);
|
|
2489
|
+
await writeFile(join(root, "package.json"), `${JSON.stringify(projectPackage({
|
|
2490
|
+
name,
|
|
2491
|
+
ui: options.ui,
|
|
2492
|
+
cliVersion: packageJson.version,
|
|
2493
|
+
sdkVersion: sdkPackage.version,
|
|
2494
|
+
contractsVersion: contractsPackage.version
|
|
2495
|
+
}), null, 2)}\n`);
|
|
1688
2496
|
const warnings = [];
|
|
2497
|
+
await writeProjectToolchain(root, toolchain);
|
|
1689
2498
|
if (name !== directoryName) warnings.push(`目录名 "${directoryName}" 不是合法标识符,manifest 的 name 已取 "${name}";需要其他取值请直接编辑 manifest.json。`);
|
|
1690
2499
|
if (options.template === "panel" && panelId !== "workspace-files") warnings.push(`Panel id "${panelId}" 需要 Host 预先注册并绑定;工作区文件面板请使用 --panel-id workspace-files。`);
|
|
1691
2500
|
const result = {
|
|
@@ -1699,13 +2508,15 @@ bohr-widget pack .
|
|
|
1699
2508
|
...panelId ? { panelId } : {},
|
|
1700
2509
|
manifestPath: join(root, "manifest.json"),
|
|
1701
2510
|
packagePath: join(root, "package.json"),
|
|
2511
|
+
profile: toolchain.profile,
|
|
2512
|
+
hostCompatibility,
|
|
1702
2513
|
...warnings.length === 0 ? {} : { warnings }
|
|
1703
2514
|
};
|
|
1704
2515
|
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1705
2516
|
else {
|
|
1706
2517
|
console.log(`已创建 Widget 工程:${root}(manifest name: ${name})`);
|
|
1707
2518
|
for (const warning of warnings) console.warn(`提示:${warning}`);
|
|
1708
|
-
console.log(
|
|
2519
|
+
console.log(`下一步:cd ${JSON.stringify(root)},执行 pnpm install,再执行 pnpm exec bohr-widget build . --json 和 pnpm exec bohr-widget check . --json。`);
|
|
1709
2520
|
}
|
|
1710
2521
|
return result;
|
|
1711
2522
|
}
|
|
@@ -1841,7 +2652,28 @@ async function commandCheck(input, options, commandName = "check") {
|
|
|
1841
2652
|
projectRoot: project
|
|
1842
2653
|
};
|
|
1843
2654
|
else {
|
|
1844
|
-
|
|
2655
|
+
try {
|
|
2656
|
+
const compatibility = await projectPreflight(project, options);
|
|
2657
|
+
result = {
|
|
2658
|
+
...applyCliProjectPolicies(await checkWidgetProject(project, {
|
|
2659
|
+
label,
|
|
2660
|
+
toolchain: { sdkVersion: toolchain.profile.sdk }
|
|
2661
|
+
})),
|
|
2662
|
+
...compatibility
|
|
2663
|
+
};
|
|
2664
|
+
} catch (error) {
|
|
2665
|
+
result = {
|
|
2666
|
+
ok: false,
|
|
2667
|
+
label,
|
|
2668
|
+
projectRoot: project,
|
|
2669
|
+
checks: [{
|
|
2670
|
+
name: "工程工具链与宿主兼容检查",
|
|
2671
|
+
ok: false
|
|
2672
|
+
}],
|
|
2673
|
+
errors: [error.message],
|
|
2674
|
+
compatibilityError: commandError(commandName, error, { projectRoot: project })
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
1845
2677
|
if (lint?.warnings.length) result.warnings = [...lint.warnings, ...result.warnings ?? []];
|
|
1846
2678
|
if (!result.ok && result.manifestKind === "host" && looksLikeAuthorManifest(result.sourceManifest)) result.errors = [...result.errors, "manifest.json 含作者字段 contributes,却被按宿主清单校验:请补齐 version,并确认 contributes.widget.type 为 panel 或 file-preview。"];
|
|
1847
2679
|
}
|
|
@@ -1850,7 +2682,7 @@ async function commandCheck(input, options, commandName = "check") {
|
|
|
1850
2682
|
if (input !== "all") printResult(options.json ? {
|
|
1851
2683
|
command: commandName,
|
|
1852
2684
|
...result,
|
|
1853
|
-
...result.ok ? {} : { error: structuredError({
|
|
2685
|
+
...result.ok ? {} : { error: result.compatibilityError ?? structuredError({
|
|
1854
2686
|
code: result.lintErrors ? "WIDGET_CLI_MANIFEST_INVALID" : "WIDGET_CLI_VALIDATION_FAILED",
|
|
1855
2687
|
message: "Widget 工程校验失败。",
|
|
1856
2688
|
context: {
|
|
@@ -1914,9 +2746,9 @@ function detectPackageManager(root, projectPackage) {
|
|
|
1914
2746
|
for (const [lockfile, manager] of LOCKFILE_MANAGERS) if (existsSync(join(root, lockfile))) return manager;
|
|
1915
2747
|
return "npm";
|
|
1916
2748
|
}
|
|
1917
|
-
function firstFailureLine(
|
|
1918
|
-
const lines = String(output ?? "").split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
1919
|
-
const line = lines.find((item) => !/^(?:npm|pnpm|yarn)\s+(?:warn|warning)\b/iu.test(item)) ?? lines[0];
|
|
2749
|
+
function firstFailureLine(stderr, stdout) {
|
|
2750
|
+
const lines = [stderr, stdout].flatMap((output) => String(output ?? "").split("\n")).map((item) => item.trim()).filter((item) => item.length > 0);
|
|
2751
|
+
const line = lines.find((item) => !/^(?:(?:npm|pnpm|yarn)\s+(?:warn|warning)\b|>)/iu.test(item)) ?? lines[0];
|
|
1920
2752
|
return line ? line.slice(0, 300) : void 0;
|
|
1921
2753
|
}
|
|
1922
2754
|
const UNRESOLVED_BUILD_PATTERNS = [
|
|
@@ -1969,7 +2801,7 @@ async function runProjectBuild(root, { capture = false } = {}) {
|
|
|
1969
2801
|
Object.assign(error, result, { cliCode: "WIDGET_CLI_BUILD_UNRESOLVED_IMPORT" });
|
|
1970
2802
|
reject(error);
|
|
1971
2803
|
} else {
|
|
1972
|
-
const detail = firstFailureLine(stderr
|
|
2804
|
+
const detail = firstFailureLine(stderr, stdout);
|
|
1973
2805
|
const error = /* @__PURE__ */ new Error(`构建失败(exit ${code ?? signal ?? "unknown"})${detail ? `:${detail}` : ""}`);
|
|
1974
2806
|
Object.assign(error, result);
|
|
1975
2807
|
reject(error);
|
|
@@ -1981,13 +2813,21 @@ async function commandBuild(directory, options = {}) {
|
|
|
1981
2813
|
if (!directory) throw new Error("build 需要工程目录");
|
|
1982
2814
|
const root = resolve(directory);
|
|
1983
2815
|
try {
|
|
2816
|
+
const compatibility = await projectPreflight(root, options);
|
|
2817
|
+
await rm(join(root, BUILD_RECEIPT_FILE), { force: true });
|
|
2818
|
+
const inputsBefore = await captureBuildInputs(root);
|
|
1984
2819
|
const build = await runProjectBuild(root, { capture: true });
|
|
2820
|
+
await validateProjectToolchain(root, toolchain);
|
|
2821
|
+
const receipt = await writeBuildReceipt(root, toolchain, { inputsBefore });
|
|
1985
2822
|
const result = {
|
|
1986
2823
|
ok: true,
|
|
1987
2824
|
command: "build",
|
|
1988
2825
|
projectRoot: root,
|
|
1989
2826
|
stdout: build.stdout,
|
|
1990
|
-
stderr: build.stderr
|
|
2827
|
+
stderr: build.stderr,
|
|
2828
|
+
...compatibility,
|
|
2829
|
+
hostCompatibility: await assertHostCompatibility(options.hostCompat, toolchain),
|
|
2830
|
+
receipt
|
|
1991
2831
|
};
|
|
1992
2832
|
if (options.json && options.emit !== false) console.log(JSON.stringify(result, null, 2));
|
|
1993
2833
|
else if (options.emit !== false) {
|
|
@@ -2017,8 +2857,23 @@ async function commandBuild(directory, options = {}) {
|
|
|
2017
2857
|
return result;
|
|
2018
2858
|
}
|
|
2019
2859
|
}
|
|
2860
|
+
const RUNTIME_MAX_ASSET_BYTES = 1048576;
|
|
2861
|
+
function oversizedAssetWarnings(files) {
|
|
2862
|
+
const oversized = /* @__PURE__ */ new Map();
|
|
2863
|
+
for (const [path, bytes] of files) if (bytes.byteLength > RUNTIME_MAX_ASSET_BYTES) oversized.set(normalizeArchivePath(path), bytes.byteLength);
|
|
2864
|
+
return [...oversized].map(([path, size]) => `${path} 为 ${(size / 1048576).toFixed(2)} MiB(${size} 字节),超过 Widget Runtime 单资产 1 MiB 上限:上传 Shrimp 会被拒绝,云端也无法加载。请确认 tsdown.config.ts 已开启 minify: true,或拆分、移除体积较大的依赖。`);
|
|
2865
|
+
}
|
|
2020
2866
|
async function commandPack(directory, options) {
|
|
2021
2867
|
if (!directory) throw new Error("pack 需要工程目录");
|
|
2868
|
+
const root = resolve(directory);
|
|
2869
|
+
const compatibility = await projectPreflight(root, options);
|
|
2870
|
+
const lint = lintProjectManifest(root);
|
|
2871
|
+
if (lint && !lint.ok) {
|
|
2872
|
+
const error = new Error(lint.errors.join("\n"));
|
|
2873
|
+
error.cliCode = "WIDGET_CLI_MANIFEST_INVALID";
|
|
2874
|
+
throw error;
|
|
2875
|
+
}
|
|
2876
|
+
if (!options.build) await assertBuildReceipt(root, toolchain);
|
|
2022
2877
|
if (options.build) {
|
|
2023
2878
|
const build = await commandBuild(directory, {
|
|
2024
2879
|
...options,
|
|
@@ -2038,7 +2893,10 @@ async function commandPack(directory, options) {
|
|
|
2038
2893
|
return build;
|
|
2039
2894
|
}
|
|
2040
2895
|
}
|
|
2041
|
-
const result = applyCliProjectPolicies(await checkWidgetProject(resolve(directory), {
|
|
2896
|
+
const result = applyCliProjectPolicies(await checkWidgetProject(resolve(directory), {
|
|
2897
|
+
requireBuiltEntry: true,
|
|
2898
|
+
toolchain: { sdkVersion: toolchain.profile.sdk }
|
|
2899
|
+
}));
|
|
2042
2900
|
if (!options.json) printResult(result, false);
|
|
2043
2901
|
if (!result.ok) {
|
|
2044
2902
|
if (options.json) console.log(JSON.stringify({
|
|
@@ -2071,7 +2929,9 @@ async function commandPack(directory, options) {
|
|
|
2071
2929
|
const warnings = [];
|
|
2072
2930
|
if (options.includePreviewFixtures && !hasFixtures) warnings.push("使用了 --include-preview-fixtures,但 preview/fixtures.json 不存在,ZIP 未包含任何 fixtures。");
|
|
2073
2931
|
else if (hasFixtures && !options.includePreviewFixtures) warnings.push("工程包含 preview/fixtures.json,未使用 --include-preview-fixtures,ZIP 未包含 fixtures。");
|
|
2074
|
-
const
|
|
2932
|
+
const files = collectWidgetPackageFiles(result, { includePreviewFixtures: fixturesIncluded });
|
|
2933
|
+
warnings.push(...oversizedAssetWarnings(files));
|
|
2934
|
+
const bundle = storedZip(files);
|
|
2075
2935
|
await mkdir(dirname(destination), { recursive: true });
|
|
2076
2936
|
await writeFile(destination, bundle);
|
|
2077
2937
|
const artifact = {
|
|
@@ -2086,9 +2946,11 @@ async function commandPack(directory, options) {
|
|
|
2086
2946
|
sdk: sdkPackage.version,
|
|
2087
2947
|
contracts: contractsPackage.version
|
|
2088
2948
|
},
|
|
2949
|
+
...compatibility,
|
|
2950
|
+
hostCompatibility: await assertHostCompatibility(options.hostCompat, toolchain),
|
|
2089
2951
|
manifest: {
|
|
2090
2952
|
name: result.manifest.name,
|
|
2091
|
-
entry: result.manifest.widgetConfig.entry,
|
|
2953
|
+
entry: normalizeArchivePath(result.manifest.widgetConfig.entry),
|
|
2092
2954
|
contribution: result.manifest.widgetConfig.contribution,
|
|
2093
2955
|
sampleIds: (result.manifest.widgetConfig.samples ?? []).map((sample) => sample.id)
|
|
2094
2956
|
},
|
|
@@ -2124,10 +2986,14 @@ async function commandMigrate(positional, options) {
|
|
|
2124
2986
|
}
|
|
2125
2987
|
if (action === "scaffold") {
|
|
2126
2988
|
if (!options.output) throw new Error("migrate dsh scaffold 需要 --out <widget-dir>");
|
|
2989
|
+
const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
|
|
2127
2990
|
const result = await scaffoldDshWidget(input, options.output, {
|
|
2128
2991
|
sdkVersion: sdkPackage.version,
|
|
2129
2992
|
contractsVersion: contractsPackage.version
|
|
2130
2993
|
});
|
|
2994
|
+
await writeProjectToolchain(result.projectRoot, toolchain);
|
|
2995
|
+
result.profile = toolchain.profile;
|
|
2996
|
+
result.hostCompatibility = hostCompatibility;
|
|
2131
2997
|
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
2132
2998
|
else console.log(`已生成迁移 Widget:${result.projectRoot}`);
|
|
2133
2999
|
return result;
|
|
@@ -2139,6 +3005,7 @@ async function commandDoctor(directory, options) {
|
|
|
2139
3005
|
const packagePath = join(root, "package.json");
|
|
2140
3006
|
const manifestPath = join(root, "manifest.json");
|
|
2141
3007
|
const report = {
|
|
3008
|
+
command: "doctor",
|
|
2142
3009
|
cli: packageJson.version,
|
|
2143
3010
|
node: process.versions.node,
|
|
2144
3011
|
sdk: sdkPackage.version,
|
|
@@ -2164,6 +3031,36 @@ async function commandDoctor(directory, options) {
|
|
|
2164
3031
|
report.entry = existsSync(join(root, report.entryPath));
|
|
2165
3032
|
report.ok = report.nodeSupported && report.packageJson && report.manifest && report.manifestValid;
|
|
2166
3033
|
report.ready = report.ok && report.dependenciesInstalled && report.entry;
|
|
3034
|
+
report.profile = toolchain.profile;
|
|
3035
|
+
report.toolchainLocked = existsSync(join(root, "widget-toolchain.json"));
|
|
3036
|
+
report.hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
|
|
3037
|
+
if (!report.toolchainLocked) {
|
|
3038
|
+
report.ready = false;
|
|
3039
|
+
report.warnings.push("工程尚未锁定工具链:安装依赖后执行 bohr-widget adopt .。");
|
|
3040
|
+
}
|
|
3041
|
+
if (report.packageJson) try {
|
|
3042
|
+
report.projectToolchain = await validateProjectToolchain(root, toolchain, {
|
|
3043
|
+
requireInstalled: report.dependenciesInstalled,
|
|
3044
|
+
requireLock: false
|
|
3045
|
+
});
|
|
3046
|
+
const lint = lintProjectManifest(root);
|
|
3047
|
+
const candidate = await checkWidgetProject(root, {
|
|
3048
|
+
requireBuiltEntry: false,
|
|
3049
|
+
toolchain: { sdkVersion: toolchain.profile.sdk }
|
|
3050
|
+
});
|
|
3051
|
+
if (lint && !lint.ok) candidate.errors.push(...lint.errors);
|
|
3052
|
+
report.candidateProfile = candidate.ok && (!lint || lint.ok) ? toolchain.profile.id : null;
|
|
3053
|
+
if (!report.candidateProfile) {
|
|
3054
|
+
report.ok = false;
|
|
3055
|
+
report.ready = false;
|
|
3056
|
+
report.warnings.push(...candidate.errors);
|
|
3057
|
+
}
|
|
3058
|
+
} catch (error) {
|
|
3059
|
+
report.ok = false;
|
|
3060
|
+
report.ready = false;
|
|
3061
|
+
report.error = commandError("doctor", error, { projectRoot: root });
|
|
3062
|
+
report.warnings.push(error.message);
|
|
3063
|
+
}
|
|
2167
3064
|
if (report.manifest && !report.manifestValid) report.warnings.push("manifest.json 不是有效 JSON:修复语法后再执行 doctor、check 或 pack。");
|
|
2168
3065
|
if (!report.dependenciesInstalled) report.warnings.push("未找到 node_modules:先执行包管理器 install,build/pack 需要工程依赖。");
|
|
2169
3066
|
if (!report.entry) report.warnings.push(`未找到构建入口 ${report.entryPath}:先执行 bohr-widget build . 或工程 build 脚本。`);
|
|
@@ -2176,7 +3073,7 @@ async function commandDoctor(directory, options) {
|
|
|
2176
3073
|
if (!report.ok) process.exitCode = 1;
|
|
2177
3074
|
}
|
|
2178
3075
|
function commandSchema() {
|
|
2179
|
-
console.log(
|
|
3076
|
+
console.log(getSchemaPath("widget-plugin-manifest.schema.json"));
|
|
2180
3077
|
}
|
|
2181
3078
|
function commandVersion(options) {
|
|
2182
3079
|
const result = {
|
|
@@ -2191,17 +3088,43 @@ function commandVersion(options) {
|
|
|
2191
3088
|
return result;
|
|
2192
3089
|
}
|
|
2193
3090
|
async function main(argv) {
|
|
2194
|
-
|
|
3091
|
+
const helpPath = requestedHelpPath(argv);
|
|
3092
|
+
if (helpPath !== null) return console.log(usage(helpPath));
|
|
3093
|
+
if (argv.includes("--version") || argv.includes("-v")) return commandVersion({ json: argv.includes("--json") });
|
|
2195
3094
|
const command = argv[0] ?? "help";
|
|
2196
3095
|
if (command === "dev") {
|
|
2197
|
-
const
|
|
3096
|
+
const devArgs = [];
|
|
3097
|
+
let hostCompat;
|
|
3098
|
+
for (let index = 1; index < argv.length; index += 1) if (argv[index] === "--host-compat") {
|
|
3099
|
+
hostCompat = argv[++index];
|
|
3100
|
+
if (!hostCompat || hostCompat.startsWith("--")) throw new Error("--host-compat 需要声明文件路径");
|
|
3101
|
+
} else devArgs.push(argv[index]);
|
|
3102
|
+
const exitCode = await runWidgetDevelopmentCli(devArgs, {
|
|
3103
|
+
beforeBuild: ({ projectRoot }) => projectPreflight(projectRoot, { hostCompat }),
|
|
3104
|
+
buildProject: runProjectBuild,
|
|
3105
|
+
checkProject: async (projectRoot, checkOptions) => {
|
|
3106
|
+
await projectPreflight(projectRoot, { hostCompat });
|
|
3107
|
+
const lint = lintProjectManifest(projectRoot);
|
|
3108
|
+
if (lint && !lint.ok) {
|
|
3109
|
+
const error = new Error(lint.errors.join("\n"));
|
|
3110
|
+
error.cliCode = "WIDGET_CLI_MANIFEST_INVALID";
|
|
3111
|
+
throw error;
|
|
3112
|
+
}
|
|
3113
|
+
return applyCliProjectPolicies(await checkWidgetProject(projectRoot, {
|
|
3114
|
+
...checkOptions,
|
|
3115
|
+
toolchain: { sdkVersion: toolchain.profile.sdk }
|
|
3116
|
+
}));
|
|
3117
|
+
},
|
|
3118
|
+
manifestOptions: { sdkVersion: toolchain.profile.sdk }
|
|
3119
|
+
});
|
|
2198
3120
|
if (exitCode !== 0) process.exitCode = exitCode;
|
|
2199
3121
|
return exitCode;
|
|
2200
3122
|
}
|
|
2201
3123
|
const { positional, options } = parseOptions(argv.slice(1));
|
|
2202
3124
|
if (!COMMANDS.includes(command)) throw new Error(`未知命令:${command}`);
|
|
2203
|
-
if (command === "help") return console.log(usage());
|
|
2204
3125
|
if (command === "version") return commandVersion(options);
|
|
3126
|
+
if (command === "integration") return commandIntegration(options);
|
|
3127
|
+
if (command === "adopt") return commandAdopt(positional[0], options);
|
|
2205
3128
|
if (command === "create") return commandCreate(positional[0], options);
|
|
2206
3129
|
if (command === "check" || command === "validate") return commandCheck(positional[0], options, command);
|
|
2207
3130
|
if (command === "build") return commandBuild(positional[0], options);
|