@dp-bohrium/widget-cli 0.0.0-stage → 0.2.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 +217 -0
- package/CHANGELOG.md +97 -0
- package/LICENSE +21 -0
- package/README.md +80 -2
- 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 +360 -0
- package/assets/schemas/widget-plugin-manifest.schema.json +162 -0
- package/assets/schemas/widget-runtime-contracts.schema.json +2725 -0
- package/assets/toolchain.json +24 -0
- package/dist/cli.mjs +3174 -0
- package/dist/core.mjs +39 -0
- package/dist/dsh-BzxDpEcZ.mjs +11688 -0
- package/llms.txt +73 -0
- package/package.json +67 -3
- package/src/core.d.mts +58 -0
- package/src/development-host.d.mts +119 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,3174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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-BzxDpEcZ.mjs";
|
|
3
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
|
|
5
|
+
import { mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
//#region ../../scripts/widget/widget-dev.mjs
|
|
11
|
+
const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
12
|
+
const DEFAULT_PORT = 4300;
|
|
13
|
+
const PROFILE_PATH = "/__widget/profile";
|
|
14
|
+
const STATUS_PATH = "/__widget/status";
|
|
15
|
+
const LAUNCH_PATH = "/__widget/launch.html";
|
|
16
|
+
const V1_ROOT = "/__widget/v1";
|
|
17
|
+
const DESCRIPTOR_PATH = `${V1_ROOT}/descriptor`;
|
|
18
|
+
const CLAIM_PATH = `${V1_ROOT}/claim`;
|
|
19
|
+
const V1_STATUS_PATH = `${V1_ROOT}/status`;
|
|
20
|
+
const V1_PROFILE_PATH = `${V1_ROOT}/profile`;
|
|
21
|
+
const EVENTS_PATH = `${V1_ROOT}/events`;
|
|
22
|
+
const CONTROL_PATH = `${V1_ROOT}/control`;
|
|
23
|
+
const GUEST_ROOT = "/__widget/guest";
|
|
24
|
+
const DEVELOPMENT_PROTOCOL = "bohrium.widget-development-host";
|
|
25
|
+
const CLI_PROTOCOL = "bohrium.widget-development-cli";
|
|
26
|
+
const SESSION_HARD_TTL_MS = 864e5;
|
|
27
|
+
const SSE_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
28
|
+
const RETAINED_GENERATIONS = 2;
|
|
29
|
+
const GENERATION_LIMITS = Object.freeze({
|
|
30
|
+
fileCount: 256,
|
|
31
|
+
singleFileBytes: 25e6,
|
|
32
|
+
totalBytes: 1e8
|
|
33
|
+
});
|
|
34
|
+
const LOCAL_BRIDGE_HOST = "127.0.0.1";
|
|
35
|
+
const LOCAL_BRIDGE_PORTS = [
|
|
36
|
+
47850,
|
|
37
|
+
47851,
|
|
38
|
+
47852,
|
|
39
|
+
47853,
|
|
40
|
+
47854
|
|
41
|
+
];
|
|
42
|
+
const OPEN_WIDGET_DEVELOPMENT_PATH = "/widget-host/desktop/open-widget-development";
|
|
43
|
+
const LOCAL_BRIDGE_TIMEOUT_MS = 350;
|
|
44
|
+
const RESOURCE_BUDGET = {
|
|
45
|
+
version: 1,
|
|
46
|
+
bundle: {
|
|
47
|
+
compressedBytes: 5e7,
|
|
48
|
+
expandedBytes: 1e8,
|
|
49
|
+
fileCount: 256,
|
|
50
|
+
singleFileBytes: 25e6
|
|
51
|
+
},
|
|
52
|
+
bridge: {
|
|
53
|
+
messageBytes: 256e3,
|
|
54
|
+
messagesPerSecond: 60,
|
|
55
|
+
pendingRequests: 32
|
|
56
|
+
},
|
|
57
|
+
files: {
|
|
58
|
+
readBytesPerRequest: 4194304,
|
|
59
|
+
readBytesPerRun: 16777216,
|
|
60
|
+
callsPerMinute: 120,
|
|
61
|
+
listPageSize: 100
|
|
62
|
+
},
|
|
63
|
+
timeouts: {
|
|
64
|
+
bundleLoadMs: 1e4,
|
|
65
|
+
handshakeMs: 1e4,
|
|
66
|
+
firstRenderMs: 1e4,
|
|
67
|
+
updateMs: 1e4,
|
|
68
|
+
capabilityMs: 15e3,
|
|
69
|
+
disposeGraceMs: 1e3
|
|
70
|
+
},
|
|
71
|
+
concurrency: {
|
|
72
|
+
activeIframesPerPage: 4,
|
|
73
|
+
activeIframesPerRun: 2,
|
|
74
|
+
hiddenRecycleMs: 6e4
|
|
75
|
+
},
|
|
76
|
+
retry: {
|
|
77
|
+
maxAttempts: 1,
|
|
78
|
+
initialDelayMs: 250,
|
|
79
|
+
maxDelayMs: 1e3,
|
|
80
|
+
totalMs: 5e3
|
|
81
|
+
},
|
|
82
|
+
failure: {
|
|
83
|
+
consecutiveFailures: 3,
|
|
84
|
+
windowMs: 6e4
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const NETWORK_LIMITS = {
|
|
88
|
+
timeoutMs: 15e3,
|
|
89
|
+
maxRedirects: 4,
|
|
90
|
+
maxConcurrentRequests: 4,
|
|
91
|
+
callsPerMinute: 60,
|
|
92
|
+
requestHeaderBytes: 16384,
|
|
93
|
+
responseHeaderBytes: 32768,
|
|
94
|
+
requestBodyBytes: 1048576,
|
|
95
|
+
responseBodyBytes: 8388608
|
|
96
|
+
};
|
|
97
|
+
function usage$1() {
|
|
98
|
+
return `用法:pnpm widget:dev <project-dir|manifest.json> [选项]
|
|
99
|
+
|
|
100
|
+
选项:
|
|
101
|
+
--open 启动后唤起宿主
|
|
102
|
+
--scheme <scheme> deep link scheme(默认 widget-host;可按宿主配置覆盖)
|
|
103
|
+
--bridge <url> 指定宿主 loopback bridge;未指定时探测标准 bridge 端口
|
|
104
|
+
--json 输出机器可读启动结果和错误
|
|
105
|
+
--watch 监听 src、manifest.json 和 package.json,变化后重新 build
|
|
106
|
+
--no-build 使用现有的 manifest.widgetConfig.entry,不执行 build
|
|
107
|
+
--port <port> 固定 Profile 服务端口(默认 ${DEFAULT_PORT})
|
|
108
|
+
--help 显示帮助
|
|
109
|
+
|
|
110
|
+
开发流程:
|
|
111
|
+
1. 在 Widget 工程目录执行此命令。
|
|
112
|
+
2. 使用 --open(优先连接已运行的 host bridge),或复制命令输出的 <scheme>://widget-dev URL。
|
|
113
|
+
3. 在生产宿主中触发对应 Tool Result / File Preview / Panel。
|
|
114
|
+
4. 修改源码;--watch 会重建 Bundle。需要重新加载时,手动打开上方 Host URL。
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
function parseArgs(argv) {
|
|
118
|
+
const positional = [];
|
|
119
|
+
const options = {
|
|
120
|
+
open: false,
|
|
121
|
+
watch: false,
|
|
122
|
+
build: true,
|
|
123
|
+
port: DEFAULT_PORT,
|
|
124
|
+
scheme: "widget-host",
|
|
125
|
+
bridge: null,
|
|
126
|
+
json: false
|
|
127
|
+
};
|
|
128
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
129
|
+
const argument = argv[index];
|
|
130
|
+
if (argument === "--help" || argument === "-h") return { help: true };
|
|
131
|
+
if (argument === "--json") options.json = true;
|
|
132
|
+
else if (argument === "--open") options.open = true;
|
|
133
|
+
else if (argument === "--watch") options.watch = true;
|
|
134
|
+
else if (argument === "--no-build") options.build = false;
|
|
135
|
+
else if (argument === "--scheme") {
|
|
136
|
+
const value = argv[++index];
|
|
137
|
+
if (!value || !/^[a-z][a-z0-9+.-]*$/u.test(value)) throw new Error("--scheme 必须是合法的 URI scheme");
|
|
138
|
+
options.scheme = value;
|
|
139
|
+
} else if (argument === "--bridge") {
|
|
140
|
+
const value = argv[++index];
|
|
141
|
+
if (!value) throw new Error("--bridge 需要一个 loopback bridge URL");
|
|
142
|
+
let bridge;
|
|
143
|
+
try {
|
|
144
|
+
bridge = new URL(value);
|
|
145
|
+
} catch {
|
|
146
|
+
throw new Error("--bridge 必须是合法的 HTTP(S) URL");
|
|
147
|
+
}
|
|
148
|
+
if (bridge.protocol !== "http:" && bridge.protocol !== "https:") throw new Error("--bridge 必须使用 HTTP(S) 协议");
|
|
149
|
+
if (![
|
|
150
|
+
"localhost",
|
|
151
|
+
"127.0.0.1",
|
|
152
|
+
"[::1]",
|
|
153
|
+
"::1"
|
|
154
|
+
].includes(bridge.hostname)) throw new Error("--bridge 必须指向 loopback 主机");
|
|
155
|
+
if (bridge.username || bridge.password || bridge.hash || bridge.search) throw new Error("--bridge 不得包含认证信息、query 或 hash");
|
|
156
|
+
const port = Number(bridge.port);
|
|
157
|
+
if (!bridge.port || !Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("--bridge 必须包含 1024 到 65535 之间的显式端口");
|
|
158
|
+
options.bridge = bridge.href;
|
|
159
|
+
} else if (argument === "--port") {
|
|
160
|
+
const value = Number(argv[++index]);
|
|
161
|
+
if (!Number.isInteger(value) || value < 1024 || value > 65535) throw new Error("--port 必须是 1024 到 65535 之间的整数");
|
|
162
|
+
options.port = value;
|
|
163
|
+
} else if (argument.startsWith("-")) throw new Error(`未知选项:${argument}`);
|
|
164
|
+
else positional.push(argument);
|
|
165
|
+
}
|
|
166
|
+
if (positional.length !== 1) throw new Error("需要一个 Widget 工程目录或 manifest.json");
|
|
167
|
+
return {
|
|
168
|
+
help: false,
|
|
169
|
+
project: positional[0],
|
|
170
|
+
...options
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function createWidgetDevError(error, context = {}) {
|
|
174
|
+
const code = error?.cliCode ?? error?.code;
|
|
175
|
+
return {
|
|
176
|
+
code: typeof code === "string" && /^(?:WIDGET_DEV_|WIDGET_CLI_)/u.test(code) ? code : "WIDGET_DEV_FAILED",
|
|
177
|
+
message: error instanceof Error ? error.message : String(error),
|
|
178
|
+
context: {
|
|
179
|
+
...context,
|
|
180
|
+
...error?.context
|
|
181
|
+
},
|
|
182
|
+
recovery: error?.recovery ?? (context.phase === "watch-build" ? "读取 status.lastBuildError,修复工程后重新保存源码或重新执行 build。" : "先读取 error.context 和 build 输出,修复工程或宿主连接后重试。")
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function secureHeaders(contentType) {
|
|
186
|
+
return {
|
|
187
|
+
"cache-control": "no-store",
|
|
188
|
+
"content-type": contentType,
|
|
189
|
+
"referrer-policy": "no-referrer",
|
|
190
|
+
"x-content-type-options": "nosniff"
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function json$1(response, status, value, extraHeaders = {}) {
|
|
194
|
+
const body = JSON.stringify(value);
|
|
195
|
+
response.writeHead(status, {
|
|
196
|
+
...secureHeaders("application/json; charset=utf-8"),
|
|
197
|
+
"content-length": Buffer.byteLength(body),
|
|
198
|
+
...extraHeaders
|
|
199
|
+
});
|
|
200
|
+
response.end(body);
|
|
201
|
+
}
|
|
202
|
+
function token() {
|
|
203
|
+
return randomBytes(32).toString("base64url");
|
|
204
|
+
}
|
|
205
|
+
function equalToken(actual, expected) {
|
|
206
|
+
if (typeof actual !== "string") return false;
|
|
207
|
+
const actualBytes = Buffer.from(actual);
|
|
208
|
+
const expectedBytes = Buffer.from(expected);
|
|
209
|
+
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
|
|
210
|
+
}
|
|
211
|
+
function bearer(header) {
|
|
212
|
+
return /^Bearer ([A-Za-z0-9_-]+)$/u.exec(String(header ?? ""))?.[1] ?? null;
|
|
213
|
+
}
|
|
214
|
+
function formatDevelopmentEvent(type, data) {
|
|
215
|
+
return JSON.stringify({
|
|
216
|
+
protocol: CLI_PROTOCOL,
|
|
217
|
+
version: 1,
|
|
218
|
+
type,
|
|
219
|
+
data
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function createWidgetDevelopmentSession({ origin, sessionId = token(), now = /* @__PURE__ */ new Date() }) {
|
|
223
|
+
const claimToken = token();
|
|
224
|
+
const accessToken = token();
|
|
225
|
+
const issuedAt = now.toISOString();
|
|
226
|
+
const expiresAt = new Date(now.getTime() + SESSION_HARD_TTL_MS).toISOString();
|
|
227
|
+
let claimed = false;
|
|
228
|
+
let disconnected = false;
|
|
229
|
+
const descriptor = Object.freeze({
|
|
230
|
+
protocol: DEVELOPMENT_PROTOCOL,
|
|
231
|
+
protocolVersion: 1,
|
|
232
|
+
sessionId,
|
|
233
|
+
issuedAt,
|
|
234
|
+
expiresAt,
|
|
235
|
+
origin,
|
|
236
|
+
endpoints: Object.freeze({
|
|
237
|
+
claim: CLAIM_PATH,
|
|
238
|
+
status: V1_STATUS_PATH,
|
|
239
|
+
events: EVENTS_PATH,
|
|
240
|
+
control: CONTROL_PATH,
|
|
241
|
+
profile: V1_PROFILE_PATH
|
|
242
|
+
})
|
|
243
|
+
});
|
|
244
|
+
return Object.freeze({
|
|
245
|
+
claimToken,
|
|
246
|
+
accessToken,
|
|
247
|
+
descriptor,
|
|
248
|
+
claim(candidate) {
|
|
249
|
+
if (claimed) throw new Error("Widget development session already claimed(已领取)");
|
|
250
|
+
if (Date.now() >= Date.parse(expiresAt)) throw new Error("claim token expired");
|
|
251
|
+
if (!equalToken(candidate, claimToken)) throw new Error("invalid claim token");
|
|
252
|
+
claimed = true;
|
|
253
|
+
return {
|
|
254
|
+
tokenType: "Bearer",
|
|
255
|
+
accessToken,
|
|
256
|
+
expiresAt
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
authorize(header) {
|
|
260
|
+
return !disconnected && Date.now() < Date.parse(expiresAt) && claimed && equalToken(bearer(header), accessToken);
|
|
261
|
+
},
|
|
262
|
+
disconnect() {
|
|
263
|
+
disconnected = true;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function slug(value) {
|
|
268
|
+
return String(value).toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "") || "widget";
|
|
269
|
+
}
|
|
270
|
+
function requestedGrants(requested = {}) {
|
|
271
|
+
const grants = [];
|
|
272
|
+
const files = requested.files ?? {};
|
|
273
|
+
if (files.list) grants.push({
|
|
274
|
+
name: "files.list",
|
|
275
|
+
limits: {
|
|
276
|
+
pageSize: 100,
|
|
277
|
+
callsPerMinute: 120
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
if (files.metadata) grants.push({
|
|
281
|
+
name: "files.metadata",
|
|
282
|
+
limits: { callsPerMinute: 120 }
|
|
283
|
+
});
|
|
284
|
+
if (files.read) grants.push({
|
|
285
|
+
name: "files.read",
|
|
286
|
+
limits: {
|
|
287
|
+
bytesPerRequest: 4194304,
|
|
288
|
+
bytesPerRun: 16777216,
|
|
289
|
+
callsPerMinute: 120
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
if (requested.events?.topics?.length) grants.push({
|
|
293
|
+
name: "events.read",
|
|
294
|
+
topics: [...requested.events.topics],
|
|
295
|
+
limits: {
|
|
296
|
+
callsPerMinute: 120,
|
|
297
|
+
pageSize: 100,
|
|
298
|
+
minPollMs: 1e3
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
if (requested.actions?.names?.length) grants.push({
|
|
302
|
+
name: "actions.invoke",
|
|
303
|
+
actions: [...requested.actions.names],
|
|
304
|
+
limits: { callsPerMinute: 120 }
|
|
305
|
+
});
|
|
306
|
+
if (requested.network?.fetch) grants.push({
|
|
307
|
+
name: "network.fetch",
|
|
308
|
+
limits: NETWORK_LIMITS
|
|
309
|
+
});
|
|
310
|
+
if (requested.presentation) grants.push({
|
|
311
|
+
name: "ui.presentation",
|
|
312
|
+
surfaces: [...requested.presentation.surfaces],
|
|
313
|
+
requiresHostConfirmation: true
|
|
314
|
+
});
|
|
315
|
+
return grants;
|
|
316
|
+
}
|
|
317
|
+
function createDevelopmentConfig({ manifest, runId, origin, launchUrl, digest, now = /* @__PURE__ */ new Date() }) {
|
|
318
|
+
const issuedAt = now.toISOString();
|
|
319
|
+
const expiresAt = new Date(now.getTime() + 6e5).toISOString();
|
|
320
|
+
const widgetId = slug(manifest.name);
|
|
321
|
+
return {
|
|
322
|
+
schemaVersion: 1,
|
|
323
|
+
hostApiVersion: 1,
|
|
324
|
+
configId: `widget-dev-${digest.slice(-12)}`,
|
|
325
|
+
runId,
|
|
326
|
+
mode: "preview",
|
|
327
|
+
issuedAt,
|
|
328
|
+
expiresAt,
|
|
329
|
+
recheckAt: new Date(now.getTime() + 3e5).toISOString(),
|
|
330
|
+
securityProfile: "runtime-security-profile-v1",
|
|
331
|
+
resourceBudget: RESOURCE_BUDGET,
|
|
332
|
+
runtime: {
|
|
333
|
+
origin,
|
|
334
|
+
catalogRevision: "widget-dev-local"
|
|
335
|
+
},
|
|
336
|
+
network: {
|
|
337
|
+
mode: "unavailable",
|
|
338
|
+
policyVersion: "widget-dev-local",
|
|
339
|
+
limits: NETWORK_LIMITS
|
|
340
|
+
},
|
|
341
|
+
bindings: [{
|
|
342
|
+
status: "loadable",
|
|
343
|
+
bindingId: `widget-dev-binding-${widgetId}`,
|
|
344
|
+
widgetId: `widget-dev-widget-${widgetId}`,
|
|
345
|
+
widgetVersionId: `widget-dev-version-${digest.slice(-12)}`,
|
|
346
|
+
digest,
|
|
347
|
+
manifest,
|
|
348
|
+
launch: {
|
|
349
|
+
url: launchUrl,
|
|
350
|
+
expiresAt
|
|
351
|
+
},
|
|
352
|
+
grantedCapabilities: requestedGrants(manifest.widgetConfig.requestedCapabilities)
|
|
353
|
+
}]
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function createLaunchHtml(entryPath) {
|
|
357
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>Widget Development Host</title></head><body><script src="/${entryPath.split("/").map(encodeURIComponent).join("/")}"><\/script></body></html>`;
|
|
358
|
+
}
|
|
359
|
+
function guestGenerationRoot(generation) {
|
|
360
|
+
return `${GUEST_ROOT}/${generation.guestCapability}`;
|
|
361
|
+
}
|
|
362
|
+
function panelDevelopmentDescriptor(generation, profileUrl, statusUrl) {
|
|
363
|
+
const contribution = generation.manifest.widgetConfig.contribution;
|
|
364
|
+
const descriptor = {
|
|
365
|
+
profileUrl,
|
|
366
|
+
statusUrl
|
|
367
|
+
};
|
|
368
|
+
if (contribution.type !== "panel") return {
|
|
369
|
+
descriptor,
|
|
370
|
+
presentationRequests: []
|
|
371
|
+
};
|
|
372
|
+
const id = contribution.id;
|
|
373
|
+
const key = `widget-dev-panel-${slug(id)}`;
|
|
374
|
+
const runId = "widget-dev-run";
|
|
375
|
+
const widgetId = `widget-dev-widget-${slug(generation.manifest.name)}`;
|
|
376
|
+
const widgetVersionId = `widget-dev-version-${generation.digest.slice(-12)}`;
|
|
377
|
+
const requestedSurfaces = generation.manifest.widgetConfig.requestedCapabilities?.presentation?.surfaces;
|
|
378
|
+
const allowedSurfaces = ["workspace", ...Array.isArray(requestedSurfaces) ? requestedSurfaces : []].filter((surface, index, surfaces) => surfaces.indexOf(surface) === index);
|
|
379
|
+
const defaultSurface = allowedSurfaces[0];
|
|
380
|
+
const panel = {
|
|
381
|
+
key,
|
|
382
|
+
runId,
|
|
383
|
+
widgetId,
|
|
384
|
+
widgetVersionId,
|
|
385
|
+
snapshot: {
|
|
386
|
+
id,
|
|
387
|
+
title: generation.manifest.name,
|
|
388
|
+
state: "complete",
|
|
389
|
+
data: {}
|
|
390
|
+
},
|
|
391
|
+
presentation: {
|
|
392
|
+
allowedSurfaces,
|
|
393
|
+
defaultSurface,
|
|
394
|
+
movable: true,
|
|
395
|
+
resizable: true
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const presentationRequests = [{
|
|
399
|
+
requestId: `widget-dev-present-${slug(id)}`,
|
|
400
|
+
key,
|
|
401
|
+
surface: defaultSurface
|
|
402
|
+
}];
|
|
403
|
+
return {
|
|
404
|
+
descriptor: {
|
|
405
|
+
...descriptor,
|
|
406
|
+
panel,
|
|
407
|
+
presentationRequests
|
|
408
|
+
},
|
|
409
|
+
presentationRequests
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function readProject(projectRoot, manifestOptions = {}) {
|
|
413
|
+
const sourceManifest = JSON.parse(readFileSync(join(projectRoot, "manifest.json"), "utf8"));
|
|
414
|
+
const manifest = compileAuthorManifest(sourceManifest, manifestOptions);
|
|
415
|
+
const entryPath = manifest.widgetConfig.entry;
|
|
416
|
+
const entryFile = join(projectRoot, entryPath);
|
|
417
|
+
if (!existsSync(entryFile) || statSync(entryFile).size === 0) throw new Error(`entry 不存在或为空:${entryPath};先运行 build,或移除 --no-build`);
|
|
418
|
+
return {
|
|
419
|
+
manifest,
|
|
420
|
+
entryPath,
|
|
421
|
+
digest: `sha256:${createHash("sha256").update(readFileSync(entryFile)).digest("hex")}`
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function contentType(path) {
|
|
425
|
+
if (path.endsWith(".js") || path.endsWith(".mjs")) return "text/javascript; charset=utf-8";
|
|
426
|
+
if (path.endsWith(".css")) return "text/css; charset=utf-8";
|
|
427
|
+
if (path.endsWith(".json")) return "application/json; charset=utf-8";
|
|
428
|
+
if (path.endsWith(".svg")) return "image/svg+xml";
|
|
429
|
+
if (path.endsWith(".png")) return "image/png";
|
|
430
|
+
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
|
|
431
|
+
if (path.endsWith(".woff2")) return "font/woff2";
|
|
432
|
+
return "application/octet-stream";
|
|
433
|
+
}
|
|
434
|
+
function collectGenerationAssets(projectRoot, entryPath, limits) {
|
|
435
|
+
const rootName = entryPath.split("/")[0];
|
|
436
|
+
const assetRoot = resolve(projectRoot, rootName);
|
|
437
|
+
if (assetRoot !== projectRoot && !assetRoot.startsWith(`${projectRoot}${sep}`)) throw new Error(`entry 路径越界:${entryPath}`);
|
|
438
|
+
if (!existsSync(assetRoot) || !statSync(assetRoot).isDirectory()) throw new Error(`entry 资源目录不存在:${rootName}`);
|
|
439
|
+
const files = [];
|
|
440
|
+
const visit = (directory) => {
|
|
441
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
442
|
+
const file = join(directory, entry.name);
|
|
443
|
+
if (entry.isSymbolicLink() || lstatSync(file).isSymbolicLink()) throw new Error(`generation 不允许 symbolic link(符号链接):${relative(projectRoot, file)}`);
|
|
444
|
+
if (entry.isDirectory()) visit(file);
|
|
445
|
+
else if (entry.isFile()) files.push(file);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
visit(assetRoot);
|
|
449
|
+
if (files.length > limits.fileCount) throw new Error(`generation 资源数量超限:${files.length}/${limits.fileCount}`);
|
|
450
|
+
let totalBytes = 0;
|
|
451
|
+
return files.map((file) => {
|
|
452
|
+
const path = relative(projectRoot, file).replaceAll("\\", "/");
|
|
453
|
+
if (path.startsWith("../") || path.split("/").includes("..")) throw new Error(`generation 资源路径越界:${path}`);
|
|
454
|
+
const bytes = readFileSync(file);
|
|
455
|
+
if (bytes.byteLength > limits.singleFileBytes) throw new Error(`generation 单文件超限:${path}`);
|
|
456
|
+
totalBytes += bytes.byteLength;
|
|
457
|
+
if (totalBytes > limits.totalBytes) throw new Error(`generation 总字节数超限:${totalBytes}/${limits.totalBytes}`);
|
|
458
|
+
return {
|
|
459
|
+
path,
|
|
460
|
+
bytes
|
|
461
|
+
};
|
|
462
|
+
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
463
|
+
}
|
|
464
|
+
function createDevelopmentGeneration(projectRoot, number = 0, limits = GENERATION_LIMITS, manifestOptions = {}) {
|
|
465
|
+
const { manifest, entryPath } = readProject(projectRoot, manifestOptions);
|
|
466
|
+
const collected = collectGenerationAssets(projectRoot, entryPath, limits);
|
|
467
|
+
if (!collected.some(({ path }) => path === entryPath)) throw new Error(`entry 未进入 generation:${entryPath}`);
|
|
468
|
+
const hash = createHash("sha256");
|
|
469
|
+
const assets = [];
|
|
470
|
+
const assetBytes = /* @__PURE__ */ new Map();
|
|
471
|
+
for (const { path, bytes } of collected) {
|
|
472
|
+
const assetDigest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
473
|
+
assets.push(Object.freeze({
|
|
474
|
+
path,
|
|
475
|
+
bytes: bytes.byteLength,
|
|
476
|
+
digest: assetDigest
|
|
477
|
+
}));
|
|
478
|
+
assetBytes.set(path, Buffer.from(bytes));
|
|
479
|
+
hash.update(path).update("\0").update(assetDigest).update("\0");
|
|
480
|
+
}
|
|
481
|
+
const digest = `sha256:${hash.digest("hex")}`;
|
|
482
|
+
return Object.freeze({
|
|
483
|
+
number,
|
|
484
|
+
digest,
|
|
485
|
+
guestCapability: token(),
|
|
486
|
+
manifest: Object.freeze(manifest),
|
|
487
|
+
entryPath,
|
|
488
|
+
assets: Object.freeze(assets),
|
|
489
|
+
getAsset(path) {
|
|
490
|
+
const bytes = assetBytes.get(path);
|
|
491
|
+
return bytes ? Buffer.from(bytes) : null;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function serveGenerationAsset(generation, path, response) {
|
|
496
|
+
const bytes = generation.getAsset(path);
|
|
497
|
+
if (!bytes) {
|
|
498
|
+
response.writeHead(404, secureHeaders("text/plain; charset=utf-8")).end("not found");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
response.writeHead(200, {
|
|
502
|
+
...secureHeaders(contentType(path)),
|
|
503
|
+
"content-length": bytes.byteLength,
|
|
504
|
+
etag: `"${generation.assets.find((asset) => asset.path === path).digest}"`
|
|
505
|
+
});
|
|
506
|
+
response.end(bytes);
|
|
507
|
+
}
|
|
508
|
+
function createWidgetDevelopmentBridgeUrl(profileUrl, bridge) {
|
|
509
|
+
const base = typeof bridge === "number" ? `http://${LOCAL_BRIDGE_HOST}:${bridge}${OPEN_WIDGET_DEVELOPMENT_PATH}` : bridge;
|
|
510
|
+
const url = new URL(base);
|
|
511
|
+
url.searchParams.set("profile", profileUrl);
|
|
512
|
+
return url.href;
|
|
513
|
+
}
|
|
514
|
+
function createWidgetDevelopmentV1BridgeUrl(bridge) {
|
|
515
|
+
return typeof bridge === "number" ? `http://${LOCAL_BRIDGE_HOST}:${bridge}/widget-host/desktop/v1/development-sessions` : bridge;
|
|
516
|
+
}
|
|
517
|
+
function createWidgetDevelopmentBridgeRequest({ descriptorUrl, session, expiresAt }) {
|
|
518
|
+
return {
|
|
519
|
+
protocol: DEVELOPMENT_PROTOCOL,
|
|
520
|
+
protocolVersion: 1,
|
|
521
|
+
sessionId: session.descriptor.sessionId,
|
|
522
|
+
descriptorUrl,
|
|
523
|
+
expiresAt
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
async function openViaLocalBridge(profileUrl, bridgeUrl, developmentSession = null, descriptorUrl = null, expiresAt = null) {
|
|
527
|
+
let origin;
|
|
528
|
+
try {
|
|
529
|
+
origin = new URL(profileUrl).origin;
|
|
530
|
+
} catch {
|
|
531
|
+
return {
|
|
532
|
+
ok: false,
|
|
533
|
+
mode: "bridge",
|
|
534
|
+
error: createWidgetDevError(/* @__PURE__ */ new Error("Profile URL 无效"), {
|
|
535
|
+
phase: "bridge",
|
|
536
|
+
profileUrl
|
|
537
|
+
})
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const bridgeBases = bridgeUrl ? [bridgeUrl] : LOCAL_BRIDGE_PORTS.map((port) => `http://${LOCAL_BRIDGE_HOST}:${port}${OPEN_WIDGET_DEVELOPMENT_PATH}`);
|
|
541
|
+
for (const bridgeBase of bridgeBases) {
|
|
542
|
+
if (developmentSession && descriptorUrl) {
|
|
543
|
+
const v1Endpoint = createWidgetDevelopmentV1BridgeUrl(bridgeUrl ?? Number(new URL(bridgeBase).port));
|
|
544
|
+
const controller = new AbortController();
|
|
545
|
+
const timeout = setTimeout(() => controller.abort(), LOCAL_BRIDGE_TIMEOUT_MS);
|
|
546
|
+
try {
|
|
547
|
+
const response = await fetch(v1Endpoint, {
|
|
548
|
+
method: "POST",
|
|
549
|
+
headers: {
|
|
550
|
+
Authorization: `Bearer ${developmentSession.claimToken}`,
|
|
551
|
+
"Content-Type": "application/json",
|
|
552
|
+
Origin: origin
|
|
553
|
+
},
|
|
554
|
+
body: JSON.stringify(createWidgetDevelopmentBridgeRequest({
|
|
555
|
+
descriptorUrl,
|
|
556
|
+
session: developmentSession,
|
|
557
|
+
expiresAt
|
|
558
|
+
})),
|
|
559
|
+
signal: controller.signal
|
|
560
|
+
});
|
|
561
|
+
const body = response.ok ? await response.json() : null;
|
|
562
|
+
if (body?.accepted === true && body.protocolVersion === 1 && body.sessionId === developmentSession.descriptor.sessionId) return {
|
|
563
|
+
ok: true,
|
|
564
|
+
mode: "bridge-v1",
|
|
565
|
+
endpoint: v1Endpoint
|
|
566
|
+
};
|
|
567
|
+
} catch {} finally {
|
|
568
|
+
clearTimeout(timeout);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const bridge = createWidgetDevelopmentBridgeUrl(profileUrl, bridgeBase);
|
|
572
|
+
const controller = new AbortController();
|
|
573
|
+
const timeout = setTimeout(() => controller.abort(), LOCAL_BRIDGE_TIMEOUT_MS);
|
|
574
|
+
try {
|
|
575
|
+
const response = await fetch(bridge, {
|
|
576
|
+
headers: { Origin: origin },
|
|
577
|
+
signal: controller.signal
|
|
578
|
+
});
|
|
579
|
+
if (!response.ok) continue;
|
|
580
|
+
if ((await response.json())?.opened === true) return {
|
|
581
|
+
ok: true,
|
|
582
|
+
mode: "bridge-legacy",
|
|
583
|
+
endpoint: bridge,
|
|
584
|
+
warning: "宿主不支持 Widget Development Host v1,已回落 legacy profile。"
|
|
585
|
+
};
|
|
586
|
+
} catch {} finally {
|
|
587
|
+
clearTimeout(timeout);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
ok: false,
|
|
592
|
+
mode: "bridge",
|
|
593
|
+
error: createWidgetDevError(/* @__PURE__ */ new Error("未发现可用的 loopback host bridge"), {
|
|
594
|
+
phase: "bridge",
|
|
595
|
+
profileUrl,
|
|
596
|
+
endpoints: bridgeBases
|
|
597
|
+
})
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
async function openHost(url, profileUrl, bridgeUrl, { jsonOutput = false, developmentSession = null, descriptorUrl = null, expiresAt = null } = {}) {
|
|
601
|
+
const bridgeResult = await openViaLocalBridge(profileUrl, bridgeUrl, developmentSession, descriptorUrl, expiresAt);
|
|
602
|
+
if (bridgeResult.ok) {
|
|
603
|
+
if (!jsonOutput) {
|
|
604
|
+
console.log("已通过本地 host bridge 打开 Widget Development Host。\n");
|
|
605
|
+
if (bridgeResult.warning) console.warn(bridgeResult.warning);
|
|
606
|
+
}
|
|
607
|
+
return bridgeResult;
|
|
608
|
+
}
|
|
609
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
610
|
+
const args = process.platform === "win32" ? ["", url] : [url];
|
|
611
|
+
const result = spawnSync(command, args, {
|
|
612
|
+
encoding: "utf8",
|
|
613
|
+
shell: process.platform === "win32"
|
|
614
|
+
});
|
|
615
|
+
if (result.error || result.status !== 0) {
|
|
616
|
+
const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status}`;
|
|
617
|
+
const failure = {
|
|
618
|
+
ok: false,
|
|
619
|
+
mode: "deep-link",
|
|
620
|
+
url,
|
|
621
|
+
error: createWidgetDevError(new Error(detail), {
|
|
622
|
+
phase: "open-host",
|
|
623
|
+
scheme: new URL(url).protocol.slice(0, -1),
|
|
624
|
+
bridge: bridgeResult.error
|
|
625
|
+
})
|
|
626
|
+
};
|
|
627
|
+
if (!jsonOutput) {
|
|
628
|
+
console.error(`无法唤起宿主(scheme=${failure.error.context.scheme}):${detail}`);
|
|
629
|
+
console.error("请确认生产宿主已安装并注册该 scheme,或复制上方 URL 手动打开。");
|
|
630
|
+
}
|
|
631
|
+
return failure;
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
ok: true,
|
|
635
|
+
mode: "deep-link",
|
|
636
|
+
url,
|
|
637
|
+
bridge: bridgeResult.error
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function projectFingerprint(projectRoot) {
|
|
641
|
+
const files = [];
|
|
642
|
+
const visit = (directory) => {
|
|
643
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
644
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) continue;
|
|
645
|
+
const path = join(directory, entry.name);
|
|
646
|
+
if (entry.isDirectory()) visit(path);
|
|
647
|
+
else if (entry.isFile()) {
|
|
648
|
+
const stat = statSync(path);
|
|
649
|
+
files.push(`${path}:${stat.mtimeMs}:${stat.size}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
visit(projectRoot);
|
|
654
|
+
return files.sort().join("\n");
|
|
655
|
+
}
|
|
656
|
+
function shouldIgnoreProjectChange(filename, projectRoot = "") {
|
|
657
|
+
const normalized = String(filename).replaceAll("\\", "/");
|
|
658
|
+
const rootName = projectRoot.replaceAll("\\", "/").split("/").at(-1);
|
|
659
|
+
return normalized === "dist" || normalized.startsWith("dist/") || normalized.includes("/dist/") || normalized.includes("node_modules") || normalized.startsWith("_tmp_") || Boolean(rootName && normalized === rootName);
|
|
660
|
+
}
|
|
661
|
+
function createProjectWatcher(projectRoot, onChange) {
|
|
662
|
+
try {
|
|
663
|
+
const watcher = watch(projectRoot, { recursive: true }, (_event, filename) => {
|
|
664
|
+
if (!filename || shouldIgnoreProjectChange(filename, projectRoot)) return;
|
|
665
|
+
onChange();
|
|
666
|
+
});
|
|
667
|
+
return { close: () => watcher.close() };
|
|
668
|
+
} catch (error) {
|
|
669
|
+
console.warn(`递归文件监听不可用,改用轮询:${String(error)}`);
|
|
670
|
+
let previous = projectFingerprint(projectRoot);
|
|
671
|
+
const timer = setInterval(() => {
|
|
672
|
+
const current = projectFingerprint(projectRoot);
|
|
673
|
+
if (current === previous) return;
|
|
674
|
+
previous = current;
|
|
675
|
+
onChange();
|
|
676
|
+
}, 500);
|
|
677
|
+
return { close: () => clearInterval(timer) };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function runBuild(projectRoot, { capture = false } = {}) {
|
|
681
|
+
return new Promise((resolveBuild, rejectBuild) => {
|
|
682
|
+
const command = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
|
683
|
+
const child = spawn(command, [
|
|
684
|
+
"--dir",
|
|
685
|
+
projectRoot,
|
|
686
|
+
"run",
|
|
687
|
+
"build"
|
|
688
|
+
], { stdio: capture ? [
|
|
689
|
+
"ignore",
|
|
690
|
+
"pipe",
|
|
691
|
+
"pipe"
|
|
692
|
+
] : "inherit" });
|
|
693
|
+
let stdout = "";
|
|
694
|
+
let stderr = "";
|
|
695
|
+
if (capture) {
|
|
696
|
+
child.stdout.on("data", (chunk) => {
|
|
697
|
+
stdout += String(chunk);
|
|
698
|
+
});
|
|
699
|
+
child.stderr.on("data", (chunk) => {
|
|
700
|
+
stderr += String(chunk);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
child.once("error", (error) => {
|
|
704
|
+
error.code = "WIDGET_DEV_BUILD_FAILED";
|
|
705
|
+
error.stdout = stdout;
|
|
706
|
+
error.stderr = stderr;
|
|
707
|
+
rejectBuild(error);
|
|
708
|
+
});
|
|
709
|
+
child.once("exit", (code, signal) => {
|
|
710
|
+
if (code === 0) {
|
|
711
|
+
resolveBuild({
|
|
712
|
+
stdout,
|
|
713
|
+
stderr
|
|
714
|
+
});
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const error = /* @__PURE__ */ new Error(`Widget build 失败(exit ${code ?? signal ?? "unknown"})`);
|
|
718
|
+
error.code = "WIDGET_DEV_BUILD_FAILED";
|
|
719
|
+
error.stdout = stdout;
|
|
720
|
+
error.stderr = stderr;
|
|
721
|
+
rejectBuild(error);
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
async function startWidgetDevelopment(projectRoot, options) {
|
|
726
|
+
const buildProject = options.buildProject ?? runBuild;
|
|
727
|
+
const emit = options.emit ?? ((line, error = false) => error ? console.error(line) : console.log(line));
|
|
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);
|
|
748
|
+
const retainedGenerations = /* @__PURE__ */ new Map([[successfulGeneration.guestCapability, successfulGeneration]]);
|
|
749
|
+
const initial = {
|
|
750
|
+
manifest: successfulGeneration.manifest,
|
|
751
|
+
entryPath: successfulGeneration.entryPath,
|
|
752
|
+
digest: successfulGeneration.digest
|
|
753
|
+
};
|
|
754
|
+
const state = {
|
|
755
|
+
phase: "ready",
|
|
756
|
+
generation: successfulGeneration.number,
|
|
757
|
+
watch: options.watch,
|
|
758
|
+
lastBuild: {
|
|
759
|
+
ok: true,
|
|
760
|
+
digest: successfulGeneration.digest,
|
|
761
|
+
assets: successfulGeneration.assets
|
|
762
|
+
},
|
|
763
|
+
lastBuildError: null
|
|
764
|
+
};
|
|
765
|
+
const eventClients = /* @__PURE__ */ new Map();
|
|
766
|
+
let eventId = 0;
|
|
767
|
+
let session;
|
|
768
|
+
const sendEvent = (type, data) => {
|
|
769
|
+
const event = {
|
|
770
|
+
protocolVersion: 1,
|
|
771
|
+
eventId: String(++eventId),
|
|
772
|
+
type,
|
|
773
|
+
data
|
|
774
|
+
};
|
|
775
|
+
const frame = `id: ${event.eventId}\nevent: ${type}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
776
|
+
for (const response of eventClients.keys()) response.write(frame);
|
|
777
|
+
if (options.json) emit(formatDevelopmentEvent(type, data), type === "generation-failed");
|
|
778
|
+
};
|
|
779
|
+
const unauthorized = (response) => json$1(response, 401, {
|
|
780
|
+
ok: false,
|
|
781
|
+
error: {
|
|
782
|
+
code: "WIDGET_DEV_UNAUTHORIZED",
|
|
783
|
+
message: "Bearer token required"
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
const developmentSnapshot = () => {
|
|
787
|
+
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
788
|
+
const development = panelDevelopmentDescriptor(successfulGeneration, `${origin}${V1_PROFILE_PATH}`, `${origin}${V1_STATUS_PATH}`);
|
|
789
|
+
return {
|
|
790
|
+
protocolVersion: 1,
|
|
791
|
+
state: state.phase,
|
|
792
|
+
sessionId: session.descriptor.sessionId,
|
|
793
|
+
revision: state.generation + 1,
|
|
794
|
+
generation: state.generation,
|
|
795
|
+
updateAvailable: false,
|
|
796
|
+
descriptor: development.descriptor,
|
|
797
|
+
presentationRequests: development.presentationRequests,
|
|
798
|
+
digest: successfulGeneration.digest,
|
|
799
|
+
assets: successfulGeneration.assets,
|
|
800
|
+
watch: state.watch,
|
|
801
|
+
lastBuild: state.lastBuild,
|
|
802
|
+
lastBuildError: state.lastBuildError
|
|
803
|
+
};
|
|
804
|
+
};
|
|
805
|
+
const generationReadyData = () => {
|
|
806
|
+
const snapshot = developmentSnapshot();
|
|
807
|
+
return {
|
|
808
|
+
generation: snapshot.generation,
|
|
809
|
+
revision: snapshot.revision,
|
|
810
|
+
digest: snapshot.digest,
|
|
811
|
+
assets: snapshot.assets,
|
|
812
|
+
descriptor: snapshot.descriptor,
|
|
813
|
+
presentationRequests: snapshot.presentationRequests
|
|
814
|
+
};
|
|
815
|
+
};
|
|
816
|
+
const server = createServer((request, response) => {
|
|
817
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
818
|
+
if (request.method === "OPTIONS") {
|
|
819
|
+
response.writeHead(405, secureHeaders("text/plain; charset=utf-8")).end();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (url.pathname === DESCRIPTOR_PATH && request.method === "GET") {
|
|
823
|
+
json$1(response, 200, session.descriptor);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (url.pathname === CLAIM_PATH && request.method === "POST") {
|
|
827
|
+
try {
|
|
828
|
+
json$1(response, 200, session.claim(bearer(request.headers.authorization)));
|
|
829
|
+
} catch (error) {
|
|
830
|
+
json$1(response, 401, {
|
|
831
|
+
ok: false,
|
|
832
|
+
error: {
|
|
833
|
+
code: "WIDGET_DEV_CLAIM_REJECTED",
|
|
834
|
+
message: String(error)
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (url.pathname.startsWith(V1_ROOT) && !session.authorize(request.headers.authorization)) {
|
|
841
|
+
unauthorized(response);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (url.pathname === V1_STATUS_PATH && request.method === "GET") {
|
|
845
|
+
json$1(response, 200, developmentSnapshot());
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (url.pathname === EVENTS_PATH && request.method === "GET") {
|
|
849
|
+
response.writeHead(200, {
|
|
850
|
+
...secureHeaders("text/event-stream; charset=utf-8"),
|
|
851
|
+
connection: "keep-alive"
|
|
852
|
+
});
|
|
853
|
+
const initialEvent = {
|
|
854
|
+
protocolVersion: 1,
|
|
855
|
+
eventId: String(++eventId),
|
|
856
|
+
type: "generation-ready",
|
|
857
|
+
data: generationReadyData()
|
|
858
|
+
};
|
|
859
|
+
response.write(`id: ${initialEvent.eventId}\nevent: generation-ready\ndata: ${JSON.stringify(initialEvent)}\n\n`);
|
|
860
|
+
const heartbeat = setInterval(() => {
|
|
861
|
+
if (!response.writableEnded) response.write(": heartbeat\n\n");
|
|
862
|
+
}, options.heartbeatIntervalMs ?? SSE_HEARTBEAT_INTERVAL_MS);
|
|
863
|
+
eventClients.set(response, heartbeat);
|
|
864
|
+
const removeEventClient = () => {
|
|
865
|
+
clearInterval(heartbeat);
|
|
866
|
+
eventClients.delete(response);
|
|
867
|
+
};
|
|
868
|
+
request.once("close", removeEventClient);
|
|
869
|
+
response.once("close", removeEventClient);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (url.pathname === CONTROL_PATH && request.method === "POST") {
|
|
873
|
+
let body = "";
|
|
874
|
+
request.on("data", (chunk) => {
|
|
875
|
+
body += String(chunk);
|
|
876
|
+
if (body.length > 16384) request.destroy();
|
|
877
|
+
});
|
|
878
|
+
request.on("end", () => {
|
|
879
|
+
try {
|
|
880
|
+
const control = JSON.parse(body || "{}");
|
|
881
|
+
if (control.protocolVersion !== 1 || !["reload", "disconnect"].includes(control.action)) throw new Error("invalid control action");
|
|
882
|
+
if (control.action === "disconnect") {
|
|
883
|
+
sendEvent("disconnected", { reason: "host-request" });
|
|
884
|
+
session.disconnect();
|
|
885
|
+
} else sendEvent("reload-requested", { generation: state.generation });
|
|
886
|
+
json$1(response, 200, {
|
|
887
|
+
ok: true,
|
|
888
|
+
protocolVersion: 1,
|
|
889
|
+
action: control.action
|
|
890
|
+
});
|
|
891
|
+
} catch (error) {
|
|
892
|
+
json$1(response, 400, {
|
|
893
|
+
ok: false,
|
|
894
|
+
error: { message: String(error) }
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (url.pathname === V1_PROFILE_PATH && request.method === "GET") {
|
|
901
|
+
const runId = url.searchParams.get("runId") || "widget-dev-run";
|
|
902
|
+
const contribution = url.searchParams.get("contribution");
|
|
903
|
+
const panelId = url.searchParams.get("id");
|
|
904
|
+
const manifestContribution = successfulGeneration.manifest.widgetConfig.contribution;
|
|
905
|
+
if (!(!contribution || contribution === manifestContribution.type && (contribution !== "panel" || !panelId || panelId === manifestContribution.id))) {
|
|
906
|
+
json$1(response, 404, {
|
|
907
|
+
ok: false,
|
|
908
|
+
error: {
|
|
909
|
+
code: "WIDGET_DEV_PROFILE_NOT_FOUND",
|
|
910
|
+
message: "No development binding matches the requested contribution",
|
|
911
|
+
attempted: {
|
|
912
|
+
contribution,
|
|
913
|
+
id: panelId
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
920
|
+
const launchUrl = `${origin}${guestGenerationRoot(successfulGeneration)}/launch.html`;
|
|
921
|
+
json$1(response, 200, createDevelopmentConfig({
|
|
922
|
+
manifest: successfulGeneration.manifest,
|
|
923
|
+
runId,
|
|
924
|
+
origin,
|
|
925
|
+
launchUrl,
|
|
926
|
+
digest: successfulGeneration.digest
|
|
927
|
+
}));
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (url.pathname === PROFILE_PATH) {
|
|
931
|
+
const runId = url.searchParams.get("runId") || "widget-dev-run";
|
|
932
|
+
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
933
|
+
json$1(response, 200, createDevelopmentConfig({
|
|
934
|
+
manifest: successfulGeneration.manifest,
|
|
935
|
+
runId,
|
|
936
|
+
origin,
|
|
937
|
+
launchUrl: `${origin}${LAUNCH_PATH}`,
|
|
938
|
+
digest: successfulGeneration.digest
|
|
939
|
+
}));
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (url.pathname === STATUS_PATH) {
|
|
943
|
+
json$1(response, 200, {
|
|
944
|
+
ok: state.phase === "ready",
|
|
945
|
+
command: "dev",
|
|
946
|
+
profile: PROFILE_PATH,
|
|
947
|
+
launch: LAUNCH_PATH,
|
|
948
|
+
...state
|
|
949
|
+
});
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (url.pathname === LAUNCH_PATH) {
|
|
953
|
+
response.writeHead(200, secureHeaders("text/html; charset=utf-8"));
|
|
954
|
+
response.end(createLaunchHtml(successfulGeneration.entryPath));
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (url.pathname.startsWith(`${GUEST_ROOT}/`)) {
|
|
958
|
+
const parts = url.pathname.slice(`${GUEST_ROOT}/`.length).split("/");
|
|
959
|
+
const capability = parts.shift();
|
|
960
|
+
const generation = retainedGenerations.get(capability);
|
|
961
|
+
if (!generation || parts.length === 0) {
|
|
962
|
+
response.writeHead(404, secureHeaders("text/plain; charset=utf-8")).end("not found");
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const guestPath = parts.join("/");
|
|
966
|
+
if (guestPath === "launch.html") {
|
|
967
|
+
response.writeHead(200, secureHeaders("text/html; charset=utf-8"));
|
|
968
|
+
response.end(createLaunchHtml(`${GUEST_ROOT.slice(1)}/${capability}/${generation.entryPath}`));
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (guestPath.includes("\\") || guestPath.split("/").includes("..")) {
|
|
972
|
+
response.writeHead(403, secureHeaders("text/plain; charset=utf-8")).end("forbidden");
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
serveGenerationAsset(generation, guestPath, response);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
let assetPath;
|
|
979
|
+
try {
|
|
980
|
+
assetPath = decodeURIComponent(url.pathname.replace(/^\/+/, ""));
|
|
981
|
+
} catch {
|
|
982
|
+
response.writeHead(400, secureHeaders("text/plain; charset=utf-8")).end("bad path");
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (assetPath.includes("\\") || assetPath.split("/").includes("..")) {
|
|
986
|
+
response.writeHead(403, secureHeaders("text/plain; charset=utf-8")).end("forbidden");
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
serveGenerationAsset(successfulGeneration, assetPath, response);
|
|
990
|
+
});
|
|
991
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
992
|
+
server.once("error", rejectListen);
|
|
993
|
+
server.listen(options.port, "127.0.0.1", resolveListen);
|
|
994
|
+
});
|
|
995
|
+
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
996
|
+
session = createWidgetDevelopmentSession({ origin });
|
|
997
|
+
const profileUrl = `${origin}${PROFILE_PATH}`;
|
|
998
|
+
const descriptorUrl = `${origin}${DESCRIPTOR_PATH}`;
|
|
999
|
+
const deepLink = `${options.scheme}://widget-dev?profile=${encodeURIComponent(profileUrl)}`;
|
|
1000
|
+
const openResult = options.open ? await openHost(deepLink, profileUrl, options.bridge, {
|
|
1001
|
+
jsonOutput: options.json,
|
|
1002
|
+
developmentSession: session,
|
|
1003
|
+
descriptorUrl,
|
|
1004
|
+
expiresAt: session.descriptor.expiresAt
|
|
1005
|
+
}) : {
|
|
1006
|
+
ok: false,
|
|
1007
|
+
mode: "manual",
|
|
1008
|
+
reason: "not_requested",
|
|
1009
|
+
url: deepLink
|
|
1010
|
+
};
|
|
1011
|
+
const startup = {
|
|
1012
|
+
ok: true,
|
|
1013
|
+
command: "dev",
|
|
1014
|
+
protocolVersion: 1,
|
|
1015
|
+
sessionId: session.descriptor.sessionId,
|
|
1016
|
+
projectRoot,
|
|
1017
|
+
origin,
|
|
1018
|
+
descriptorUrl,
|
|
1019
|
+
profileUrl,
|
|
1020
|
+
statusUrl: `${origin}${STATUS_PATH}`,
|
|
1021
|
+
hostUrl: deepLink,
|
|
1022
|
+
digest: successfulGeneration.digest,
|
|
1023
|
+
assets: successfulGeneration.assets,
|
|
1024
|
+
watch: options.watch,
|
|
1025
|
+
open: openResult
|
|
1026
|
+
};
|
|
1027
|
+
if (options.json) emit(formatDevelopmentEvent("session-ready", startup));
|
|
1028
|
+
else {
|
|
1029
|
+
emit(`\nWidget Development Host 已启动:${relative(repoRoot, projectRoot) || projectRoot}`);
|
|
1030
|
+
emit(`Descriptor: ${descriptorUrl}`);
|
|
1031
|
+
emit(`Profile: ${profileUrl}`);
|
|
1032
|
+
emit(`Status: ${startup.statusUrl}`);
|
|
1033
|
+
emit(`Host: ${deepLink}`);
|
|
1034
|
+
emit("\n将生产宿主用于 Widget 开发:复制 Host URL,或加 --open 自动唤起。\n");
|
|
1035
|
+
}
|
|
1036
|
+
let timer;
|
|
1037
|
+
let building = false;
|
|
1038
|
+
let pending = false;
|
|
1039
|
+
const rebuild = async () => {
|
|
1040
|
+
if (building) {
|
|
1041
|
+
pending = true;
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
building = true;
|
|
1045
|
+
try {
|
|
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);
|
|
1054
|
+
successfulGeneration = candidate;
|
|
1055
|
+
retainedGenerations.set(candidate.guestCapability, candidate);
|
|
1056
|
+
while (retainedGenerations.size > RETAINED_GENERATIONS) retainedGenerations.delete(retainedGenerations.keys().next().value);
|
|
1057
|
+
state.generation = candidate.number;
|
|
1058
|
+
state.lastBuild = {
|
|
1059
|
+
ok: true,
|
|
1060
|
+
digest: candidate.digest,
|
|
1061
|
+
assets: candidate.assets,
|
|
1062
|
+
stdout: build.stdout,
|
|
1063
|
+
stderr: build.stderr
|
|
1064
|
+
};
|
|
1065
|
+
state.lastBuildError = null;
|
|
1066
|
+
sendEvent("generation-ready", generationReadyData());
|
|
1067
|
+
if (!options.json) emit("Widget 已重新构建;宿主可通过事件或 reload 获取新 generation。");
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
const redactProjectRoot = (value) => String(value ?? "").replaceAll(projectRoot, "<project>");
|
|
1070
|
+
state.lastBuildError = createWidgetDevError(error, {
|
|
1071
|
+
phase: "watch-build",
|
|
1072
|
+
stdout: redactProjectRoot(error?.stdout),
|
|
1073
|
+
stderr: redactProjectRoot(error?.stderr)
|
|
1074
|
+
});
|
|
1075
|
+
sendEvent("generation-failed", {
|
|
1076
|
+
generation: successfulGeneration.number,
|
|
1077
|
+
retainedDigest: successfulGeneration.digest,
|
|
1078
|
+
error: state.lastBuildError
|
|
1079
|
+
});
|
|
1080
|
+
if (!options.json) emit(String(error), true);
|
|
1081
|
+
} finally {
|
|
1082
|
+
building = false;
|
|
1083
|
+
if (pending) {
|
|
1084
|
+
pending = false;
|
|
1085
|
+
rebuild();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
const watcher = options.watch ? createProjectWatcher(projectRoot, () => {
|
|
1090
|
+
clearTimeout(timer);
|
|
1091
|
+
timer = setTimeout(() => void rebuild(), 150);
|
|
1092
|
+
}) : null;
|
|
1093
|
+
let closed = false;
|
|
1094
|
+
const close = (reason = "cli-stop") => {
|
|
1095
|
+
if (closed) return;
|
|
1096
|
+
closed = true;
|
|
1097
|
+
state.phase = "stopping";
|
|
1098
|
+
sendEvent("session-stopping", { reason });
|
|
1099
|
+
session.disconnect();
|
|
1100
|
+
clearTimeout(timer);
|
|
1101
|
+
watcher?.close();
|
|
1102
|
+
for (const [response, heartbeat] of eventClients) {
|
|
1103
|
+
clearInterval(heartbeat);
|
|
1104
|
+
response.end();
|
|
1105
|
+
}
|
|
1106
|
+
eventClients.clear();
|
|
1107
|
+
server.close();
|
|
1108
|
+
};
|
|
1109
|
+
if (options.registerSignals !== false) {
|
|
1110
|
+
process.once("SIGINT", () => {
|
|
1111
|
+
close("signal");
|
|
1112
|
+
process.exit(0);
|
|
1113
|
+
});
|
|
1114
|
+
process.once("SIGTERM", () => {
|
|
1115
|
+
close("signal");
|
|
1116
|
+
process.exit(0);
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
server,
|
|
1121
|
+
close,
|
|
1122
|
+
rebuild,
|
|
1123
|
+
initial,
|
|
1124
|
+
startup,
|
|
1125
|
+
state,
|
|
1126
|
+
session
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function emitCliEnvelope(type, data, error = false) {
|
|
1130
|
+
const line = formatDevelopmentEvent(type, data);
|
|
1131
|
+
if (error) console.error(line);
|
|
1132
|
+
else console.log(line);
|
|
1133
|
+
}
|
|
1134
|
+
async function runWidgetDevelopmentCli(argv = process.argv.slice(2), overrides = {}) {
|
|
1135
|
+
try {
|
|
1136
|
+
const options = {
|
|
1137
|
+
...parseArgs(argv),
|
|
1138
|
+
...overrides
|
|
1139
|
+
};
|
|
1140
|
+
if (options.help) {
|
|
1141
|
+
console.log(usage$1());
|
|
1142
|
+
return 0;
|
|
1143
|
+
}
|
|
1144
|
+
const [projectRoot] = resolveWidgetProjects(options.project);
|
|
1145
|
+
const checkProject = options.checkProject ?? checkWidgetProject;
|
|
1146
|
+
const check = await checkProject(projectRoot, {
|
|
1147
|
+
...options.manifestOptions,
|
|
1148
|
+
requireBuiltEntry: false
|
|
1149
|
+
});
|
|
1150
|
+
if (!check.ok) {
|
|
1151
|
+
if (options.json) emitCliEnvelope("error", {
|
|
1152
|
+
ok: false,
|
|
1153
|
+
command: "dev",
|
|
1154
|
+
error: createWidgetDevError(/* @__PURE__ */ new Error("Widget Dev 启动前 L0 check 失败"), {
|
|
1155
|
+
phase: "validate",
|
|
1156
|
+
projectRoot,
|
|
1157
|
+
errors: check.errors
|
|
1158
|
+
})
|
|
1159
|
+
});
|
|
1160
|
+
else {
|
|
1161
|
+
console.error("Widget Dev 启动前 L0 check 失败:");
|
|
1162
|
+
check.errors.forEach((error) => {
|
|
1163
|
+
console.error(` - ${error}`);
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
return 1;
|
|
1167
|
+
}
|
|
1168
|
+
await startWidgetDevelopment(projectRoot, {
|
|
1169
|
+
...options,
|
|
1170
|
+
checkProject
|
|
1171
|
+
});
|
|
1172
|
+
return 0;
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
if (argv.includes("--json")) emitCliEnvelope("error", {
|
|
1175
|
+
ok: false,
|
|
1176
|
+
command: "dev",
|
|
1177
|
+
error: createWidgetDevError(error, { input: argv[0] ?? null })
|
|
1178
|
+
});
|
|
1179
|
+
else console.error(`${String(error)}\n\n${usage$1()}`);
|
|
1180
|
+
return 1;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
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
|
|
1512
|
+
//#region src/manifest-lint.mjs
|
|
1513
|
+
/**
|
|
1514
|
+
* 作者清单(VS Code 风格 `contributes` 清单)的本地静态检查。
|
|
1515
|
+
*
|
|
1516
|
+
* `@dp-bohrium/widget-contracts` 的 host schema 只在 `compileAuthorManifest` 之后生效,
|
|
1517
|
+
* 而编译只保留宿主运行时需要的字段:作者的 `version`、`displayName` 和未知顶层键会被
|
|
1518
|
+
* 静默丢弃,于是"清单写错但 check 全绿"。这里只补下游永远看不到的作者侧约束,报错文案
|
|
1519
|
+
* 直接指向作者填的字段名。
|
|
1520
|
+
*
|
|
1521
|
+
* 边界:`contributes` / `capabilities` / `runtime` / `samples` / `description` 的结构
|
|
1522
|
+
* 在编译后仍由 host schema 校验,这里不重复实现,避免与
|
|
1523
|
+
* `widget-plugin-manifest.schema.json` 产生第二份真相。
|
|
1524
|
+
*/
|
|
1525
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
1526
|
+
const AUTHOR_MANIFEST_KEYS = Object.freeze([
|
|
1527
|
+
"$schema",
|
|
1528
|
+
"name",
|
|
1529
|
+
"version",
|
|
1530
|
+
"displayName",
|
|
1531
|
+
"description",
|
|
1532
|
+
"entry",
|
|
1533
|
+
"activationEvents",
|
|
1534
|
+
"contributes",
|
|
1535
|
+
"capabilities",
|
|
1536
|
+
"runtime",
|
|
1537
|
+
"samples"
|
|
1538
|
+
]);
|
|
1539
|
+
const AUTHOR_MANIFEST_SCHEMA_ID = "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json";
|
|
1540
|
+
function isPlainObject(value) {
|
|
1541
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1542
|
+
}
|
|
1543
|
+
/** 有 `contributes` 就按作者清单处理;这也是 `compileAuthorManifest` 的输入前提。 */
|
|
1544
|
+
function looksLikeAuthorManifest(value) {
|
|
1545
|
+
return isPlainObject(value) && "contributes" in value;
|
|
1546
|
+
}
|
|
1547
|
+
function suggestKey(key) {
|
|
1548
|
+
const lower = key.toLowerCase();
|
|
1549
|
+
const candidate = AUTHOR_MANIFEST_KEYS.find((allowed) => {
|
|
1550
|
+
const normalized = allowed.toLowerCase();
|
|
1551
|
+
return normalized === lower || normalized.startsWith(lower) || lower.startsWith(normalized);
|
|
1552
|
+
});
|
|
1553
|
+
return candidate && candidate !== key ? candidate : void 0;
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[] }}
|
|
1557
|
+
* `errors` 是作者必须修的清单错误;`warnings` 是可继续执行但值得提示的问题。
|
|
1558
|
+
*/
|
|
1559
|
+
function lintAuthorManifest(manifest, options = {}) {
|
|
1560
|
+
const fileExists = options.fileExists;
|
|
1561
|
+
const errors = [];
|
|
1562
|
+
const warnings = [];
|
|
1563
|
+
if (!isPlainObject(manifest)) return {
|
|
1564
|
+
ok: false,
|
|
1565
|
+
errors: ["manifest.json 必须是 JSON 对象"],
|
|
1566
|
+
warnings
|
|
1567
|
+
};
|
|
1568
|
+
for (const key of Object.keys(manifest)) {
|
|
1569
|
+
if (AUTHOR_MANIFEST_KEYS.includes(key)) continue;
|
|
1570
|
+
const suggestion = suggestKey(key);
|
|
1571
|
+
errors.push(`manifest.json 不允许多余字段 "${key}"${suggestion ? `,是否想写 "${suggestion}"?` : ""}(additionalProperties=false)`);
|
|
1572
|
+
}
|
|
1573
|
+
if (typeof manifest.name !== "string" || manifest.name.length === 0) errors.push("manifest.json 缺少必填字段 \"name\"(非空字符串)");
|
|
1574
|
+
else if (manifest.name.length > 128) errors.push("manifest.json 的 \"name\" 超过 128 个字符");
|
|
1575
|
+
if (manifest.version === void 0) errors.push("manifest.json 缺少必填字段 \"version\"(例如 \"0.1.0\")");
|
|
1576
|
+
else if (typeof manifest.version !== "string" || !VERSION_PATTERN.test(manifest.version)) errors.push(`manifest.json 的 "version" 必须是 semver,当前是 ${JSON.stringify(manifest.version)}`);
|
|
1577
|
+
if (manifest.displayName !== void 0 && typeof manifest.displayName !== "string") errors.push(`manifest.json 的 "displayName" 必须是字符串,当前是 ${JSON.stringify(manifest.displayName)}`);
|
|
1578
|
+
else if (typeof manifest.displayName === "string" && manifest.displayName.length > 128) errors.push("manifest.json 的 \"displayName\" 超过 128 个字符");
|
|
1579
|
+
if (manifest.entry !== void 0) {
|
|
1580
|
+
if (typeof manifest.entry !== "string" || manifest.entry.length === 0) errors.push("manifest.json 的 \"entry\" 必须是非空字符串");
|
|
1581
|
+
else if (fileExists && !fileExists(manifest.entry)) warnings.push(`manifest.json 的 "entry" 指向的源文件不存在:${manifest.entry}(CLI 打包使用构建后的入口,此处仅提示)`);
|
|
1582
|
+
}
|
|
1583
|
+
if (isPlainObject(manifest.widgetConfig)) warnings.push("manifest.json 同时包含作者字段(contributes)和宿主字段(widgetConfig);打包时以编译结果为准,建议只保留作者清单");
|
|
1584
|
+
const contribution = manifest.contributes?.widget;
|
|
1585
|
+
if (contribution?.type === "panel" && contribution.id === "workspace-files") {
|
|
1586
|
+
const files = manifest.capabilities?.files;
|
|
1587
|
+
const missing = [
|
|
1588
|
+
"list",
|
|
1589
|
+
"metadata",
|
|
1590
|
+
"read"
|
|
1591
|
+
].filter((capability) => files?.[capability] !== true);
|
|
1592
|
+
if (missing.length > 0) errors.push(`contributes.widget.id 为 "workspace-files" 时必须申请 capabilities.files.${missing.join("、capabilities.files.")};否则 Host 文件面板无法加载`);
|
|
1593
|
+
}
|
|
1594
|
+
if (errors.length > 0) errors.push(`作者清单结构以 ${AUTHOR_MANIFEST_SCHEMA_ID} 为准;执行 bohr-widget schema 可查看本地 Schema 路径。`);
|
|
1595
|
+
return {
|
|
1596
|
+
ok: errors.length === 0,
|
|
1597
|
+
errors,
|
|
1598
|
+
warnings
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
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
|
|
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
|
+
}
|
|
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
|
+
}` : "";
|
|
1967
|
+
return `import { createRoot } from "react-dom/client";
|
|
1968
|
+
import type { WidgetContext } from "@dp-bohrium/widget-sdk";
|
|
1969
|
+
import type { RenderRequest } from "@dp-bohrium/widget-contracts";
|
|
1970
|
+
|
|
1971
|
+
function WidgetView({ request }: { request?: RenderRequest }) {
|
|
1972
|
+
const data = request?.content ?? { state: "等待 Host 提供数据" };
|
|
1973
|
+
return (
|
|
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">
|
|
1977
|
+
{JSON.stringify(data, null, 2)}
|
|
1978
|
+
</pre>
|
|
1979
|
+
</main>
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
function activate(context: WidgetContext) {
|
|
1984
|
+
${layoutSource()} const host = document.createElement("div");
|
|
1985
|
+
host.className = "widget-root";
|
|
1986
|
+
document.body.replaceChildren(host);
|
|
1987
|
+
const root = createRoot(host);
|
|
1988
|
+
let renderRequest: RenderRequest | undefined;${resizeState}
|
|
1989
|
+
const render = () => root.render(<WidgetView request={renderRequest} />);
|
|
1990
|
+
render();
|
|
1991
|
+
context.render((request) => {
|
|
1992
|
+
document.body.style.background = request.context.theme.tokens.background;
|
|
1993
|
+
document.body.style.color = request.context.theme.tokens.foreground;
|
|
1994
|
+
renderRequest = request;
|
|
1995
|
+
render();${resizeCall}
|
|
1996
|
+
});
|
|
1997
|
+
context.subscriptions.add(() => {
|
|
1998
|
+
root.unmount();
|
|
1999
|
+
host.remove();
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
export { activate };
|
|
2004
|
+
`;
|
|
2005
|
+
}
|
|
2006
|
+
function hostSource({ sourceFile }) {
|
|
2007
|
+
return `import { runWidgetHost } from "@dp-bohrium/widget-sdk";
|
|
2008
|
+
import * as widget from "./${sourceFile.split("/").at(-1)}";
|
|
2009
|
+
|
|
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
|
+
});
|
|
2017
|
+
`;
|
|
2018
|
+
}
|
|
2019
|
+
const TEMPLATES = Object.freeze({
|
|
2020
|
+
panel: {
|
|
2021
|
+
manifest: ({ name, panelId = name, ui = "vanilla" }) => ({
|
|
2022
|
+
$schema: "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json",
|
|
2023
|
+
name,
|
|
2024
|
+
version: "0.1.0",
|
|
2025
|
+
displayName: name,
|
|
2026
|
+
entry: ui === "react" ? "src/widget.tsx" : "src/widget.ts",
|
|
2027
|
+
activationEvents: ["session.start"],
|
|
2028
|
+
contributes: { widget: {
|
|
2029
|
+
type: "panel",
|
|
2030
|
+
id: panelId,
|
|
2031
|
+
surfaces: ["workspace"]
|
|
2032
|
+
} },
|
|
2033
|
+
...panelId === "workspace-files" ? { capabilities: { files: {
|
|
2034
|
+
list: true,
|
|
2035
|
+
metadata: true,
|
|
2036
|
+
read: true
|
|
2037
|
+
} } } : {},
|
|
2038
|
+
samples: [{
|
|
2039
|
+
id: "default",
|
|
2040
|
+
title: "默认样例",
|
|
2041
|
+
request: "samples/default.json"
|
|
2042
|
+
}]
|
|
2043
|
+
}),
|
|
2044
|
+
source: `import type { WidgetContext } from "@dp-bohrium/widget-sdk";
|
|
2045
|
+
|
|
2046
|
+
function activate(context: WidgetContext) {
|
|
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);
|
|
2054
|
+
document.body.replaceChildren(root);
|
|
2055
|
+
context.subscriptions.add(() => root.remove());
|
|
2056
|
+
context.render((request) => {
|
|
2057
|
+
document.body.style.background = request.context.theme.tokens.background;
|
|
2058
|
+
document.body.style.color = request.context.theme.tokens.foreground;
|
|
2059
|
+
const content = request.content;
|
|
2060
|
+
display.textContent = JSON.stringify(
|
|
2061
|
+
"data" in content ? content.data : content,
|
|
2062
|
+
null,
|
|
2063
|
+
2,
|
|
2064
|
+
);
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
export { activate };
|
|
2069
|
+
`,
|
|
2070
|
+
reactSource,
|
|
2071
|
+
hostSource,
|
|
2072
|
+
sample: ({ name, panelId = name }) => ({
|
|
2073
|
+
schemaVersion: 1,
|
|
2074
|
+
requestId: `${name}-default`,
|
|
2075
|
+
instanceId: name,
|
|
2076
|
+
revision: 1,
|
|
2077
|
+
contribution: "panel",
|
|
2078
|
+
state: "complete",
|
|
2079
|
+
content: {
|
|
2080
|
+
contribution: "panel",
|
|
2081
|
+
id: panelId,
|
|
2082
|
+
title: name,
|
|
2083
|
+
data: panelId === "workspace-files" ? { root: {
|
|
2084
|
+
id: "workspace-root",
|
|
2085
|
+
kind: "directory",
|
|
2086
|
+
name: "workspace"
|
|
2087
|
+
} } : { status: "ready" }
|
|
2088
|
+
},
|
|
2089
|
+
context: {
|
|
2090
|
+
locale: "zh-CN",
|
|
2091
|
+
theme: {
|
|
2092
|
+
mode: "light",
|
|
2093
|
+
tokens: {
|
|
2094
|
+
background: "#fff",
|
|
2095
|
+
foreground: "#111",
|
|
2096
|
+
muted: "#eee",
|
|
2097
|
+
mutedForeground: "#666",
|
|
2098
|
+
border: "#ddd",
|
|
2099
|
+
primary: "#06c",
|
|
2100
|
+
danger: "#c00"
|
|
2101
|
+
}
|
|
2102
|
+
},
|
|
2103
|
+
viewport: {
|
|
2104
|
+
width: 640,
|
|
2105
|
+
height: 360
|
|
2106
|
+
},
|
|
2107
|
+
capabilities: panelId === "workspace-files" ? [
|
|
2108
|
+
{
|
|
2109
|
+
name: "files.list",
|
|
2110
|
+
limits: {
|
|
2111
|
+
pageSize: 100,
|
|
2112
|
+
callsPerMinute: 60
|
|
2113
|
+
}
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
name: "files.metadata",
|
|
2117
|
+
limits: { callsPerMinute: 60 }
|
|
2118
|
+
},
|
|
2119
|
+
{
|
|
2120
|
+
name: "files.read",
|
|
2121
|
+
limits: {
|
|
2122
|
+
bytesPerRequest: 65536,
|
|
2123
|
+
bytesPerRun: 262144,
|
|
2124
|
+
callsPerMinute: 60
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
] : [],
|
|
2128
|
+
correlationId: `${name}-default`
|
|
2129
|
+
}
|
|
2130
|
+
})
|
|
2131
|
+
},
|
|
2132
|
+
"file-preview": {
|
|
2133
|
+
manifest: ({ name, ui = "vanilla" }) => ({
|
|
2134
|
+
$schema: "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json",
|
|
2135
|
+
name,
|
|
2136
|
+
version: "0.1.0",
|
|
2137
|
+
displayName: name,
|
|
2138
|
+
entry: ui === "react" ? "src/widget.tsx" : "src/widget.ts",
|
|
2139
|
+
activationEvents: ["workspace-file-opened"],
|
|
2140
|
+
contributes: { widget: {
|
|
2141
|
+
type: "file-preview",
|
|
2142
|
+
extensions: ["txt"]
|
|
2143
|
+
} },
|
|
2144
|
+
samples: [{
|
|
2145
|
+
id: "default",
|
|
2146
|
+
title: "默认样例",
|
|
2147
|
+
request: "samples/default.json"
|
|
2148
|
+
}]
|
|
2149
|
+
}),
|
|
2150
|
+
source: `import type { WidgetContext } from "@dp-bohrium/widget-sdk";
|
|
2151
|
+
|
|
2152
|
+
function activate(context: WidgetContext) {
|
|
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);
|
|
2159
|
+
document.body.replaceChildren(root);
|
|
2160
|
+
context.subscriptions.add(() => root.remove());
|
|
2161
|
+
let resized = false;
|
|
2162
|
+
context.render((request) => {
|
|
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);
|
|
2166
|
+
if (!resized) {
|
|
2167
|
+
context.resize(640, 360);
|
|
2168
|
+
resized = true;
|
|
2169
|
+
}
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
export { activate };
|
|
2174
|
+
`,
|
|
2175
|
+
reactSource: (options) => reactSource({
|
|
2176
|
+
...options,
|
|
2177
|
+
reportSize: true
|
|
2178
|
+
}),
|
|
2179
|
+
hostSource,
|
|
2180
|
+
sample: () => ({
|
|
2181
|
+
schemaVersion: 1,
|
|
2182
|
+
requestId: "sample-file-preview",
|
|
2183
|
+
instanceId: "sample-file-preview",
|
|
2184
|
+
revision: 1,
|
|
2185
|
+
contribution: "file-preview",
|
|
2186
|
+
state: "complete",
|
|
2187
|
+
content: {
|
|
2188
|
+
contribution: "file-preview",
|
|
2189
|
+
file: {
|
|
2190
|
+
id: "sample",
|
|
2191
|
+
name: "README.txt",
|
|
2192
|
+
extension: "txt",
|
|
2193
|
+
kind: "file"
|
|
2194
|
+
},
|
|
2195
|
+
preview: { format: "text" }
|
|
2196
|
+
},
|
|
2197
|
+
context: {
|
|
2198
|
+
locale: "en-US",
|
|
2199
|
+
theme: {
|
|
2200
|
+
mode: "light",
|
|
2201
|
+
tokens: {
|
|
2202
|
+
background: "#fff",
|
|
2203
|
+
foreground: "#111",
|
|
2204
|
+
muted: "#eee",
|
|
2205
|
+
mutedForeground: "#666",
|
|
2206
|
+
border: "#ddd",
|
|
2207
|
+
primary: "#06c",
|
|
2208
|
+
danger: "#c00"
|
|
2209
|
+
}
|
|
2210
|
+
},
|
|
2211
|
+
viewport: {
|
|
2212
|
+
width: 640,
|
|
2213
|
+
height: 360
|
|
2214
|
+
},
|
|
2215
|
+
capabilities: [],
|
|
2216
|
+
correlationId: "sample-file-preview"
|
|
2217
|
+
}
|
|
2218
|
+
})
|
|
2219
|
+
}
|
|
2220
|
+
});
|
|
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
|
|
2242
|
+
//#region src/cli.mjs
|
|
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)];
|
|
2274
|
+
}
|
|
2275
|
+
function parseOptions(argv) {
|
|
2276
|
+
const positional = [];
|
|
2277
|
+
const options = {
|
|
2278
|
+
json: false,
|
|
2279
|
+
template: "panel",
|
|
2280
|
+
ui: "vanilla",
|
|
2281
|
+
includePreviewFixtures: false,
|
|
2282
|
+
output: void 0,
|
|
2283
|
+
open: false,
|
|
2284
|
+
watch: false,
|
|
2285
|
+
build: true,
|
|
2286
|
+
port: 4300,
|
|
2287
|
+
scheme: "widget-host",
|
|
2288
|
+
bridge: null,
|
|
2289
|
+
panelId: void 0,
|
|
2290
|
+
hostCompat: void 0,
|
|
2291
|
+
project: void 0
|
|
2292
|
+
};
|
|
2293
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
2294
|
+
const argument = argv[index];
|
|
2295
|
+
if (argument === "--json") options.json = true;
|
|
2296
|
+
else if (argument === "--include-preview-fixtures") options.includePreviewFixtures = true;
|
|
2297
|
+
else if (argument === "--template") {
|
|
2298
|
+
options.template = argv[++index];
|
|
2299
|
+
if (!options.template) throw new Error("--template 需要模板名");
|
|
2300
|
+
} else if (argument === "--ui") {
|
|
2301
|
+
options.ui = argv[++index];
|
|
2302
|
+
if (!options.ui) throw new Error("--ui 需要 vanilla 或 react");
|
|
2303
|
+
} else if (argument === "--panel-id") {
|
|
2304
|
+
options.panelId = argv[++index];
|
|
2305
|
+
if (!options.panelId) throw new Error("--panel-id 需要稳定的 Panel id");
|
|
2306
|
+
} else if (argument === "--out") {
|
|
2307
|
+
options.output = argv[++index];
|
|
2308
|
+
if (!options.output) throw new Error("--out 需要输出路径");
|
|
2309
|
+
} else if (argument === "--open") options.open = true;
|
|
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;
|
|
2316
|
+
else if (argument === "--no-build") options.build = false;
|
|
2317
|
+
else if (argument === "--port") options.port = Number(argv[++index]);
|
|
2318
|
+
else if (argument === "--scheme") options.scheme = argv[++index];
|
|
2319
|
+
else if (argument === "--bridge") options.bridge = argv[++index];
|
|
2320
|
+
else if (argument.startsWith("-")) throw new Error(`未知选项:${argument}`);
|
|
2321
|
+
else positional.push(argument);
|
|
2322
|
+
}
|
|
2323
|
+
return {
|
|
2324
|
+
positional,
|
|
2325
|
+
options
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
function printResult(result, json) {
|
|
2329
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
2330
|
+
else {
|
|
2331
|
+
for (const check of result.checks ?? []) console.log(`${check.ok ? "✓" : "✗"} ${check.name}`);
|
|
2332
|
+
for (const error of result.errors ?? []) console.error(`错误:${error}`);
|
|
2333
|
+
console.log(result.ok ? "校验通过" : "校验失败");
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
function structuredError({ code, message, context = {}, recovery }) {
|
|
2337
|
+
return {
|
|
2338
|
+
code,
|
|
2339
|
+
message,
|
|
2340
|
+
context,
|
|
2341
|
+
recovery
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
function commandRecovery(command, error) {
|
|
2345
|
+
if (!COMMANDS.includes(command)) return "命令不存在或拼写有误:执行 bohr-widget help 查看支持的命令与参数。";
|
|
2346
|
+
if (command === "create") return "确认目标目录不存在,且 --template 与 --ui 取值合法后重新执行 create。";
|
|
2347
|
+
if (command === "build") {
|
|
2348
|
+
if (error?.cliCode === "WIDGET_CLI_BUILD_UNRESOLVED_IMPORT") return "安装或修正 error.message 中的依赖,并确认 bundler 已把浏览器运行时依赖打进 dist/widget.js 后重新执行 build。";
|
|
2349
|
+
return error?.code === 127 ? "工程缺少构建依赖:先在工程目录执行包管理器 install,再重新执行 build。" : "读取 error.stdout / error.stderr 定位构建错误,修复工程后重新执行 build。";
|
|
2350
|
+
}
|
|
2351
|
+
if (command === "pack") return "先执行 bohr-widget check <工程目录>,修复 manifest、entry、sample 或 fixtures 后重新打包。";
|
|
2352
|
+
if (command === "check" || command === "validate") return "根据 errors 修复 Manifest、entry、sample 或 fixtures 后重新校验。";
|
|
2353
|
+
return "根据错误上下文修复后重试;不确定时保留 command、error 和上下文交给上层 CLI。";
|
|
2354
|
+
}
|
|
2355
|
+
/** 命令名要进 error.code,必须净化成标识符片段(unknown-cmd → UNKNOWN_CMD)。 */
|
|
2356
|
+
function commandCode(command) {
|
|
2357
|
+
return String(command ?? "").replace(/[^A-Za-z0-9]+/gu, "_").replace(/^_+|_+$/gu, "").toUpperCase() || "UNKNOWN";
|
|
2358
|
+
}
|
|
2359
|
+
function commandError(command, error, context = {}) {
|
|
2360
|
+
return structuredError({
|
|
2361
|
+
code: typeof error?.cliCode === "string" ? error.cliCode : typeof error?.code === "string" && error.code.startsWith("WIDGET_") ? error.code : `WIDGET_CLI_${commandCode(command)}_FAILED`,
|
|
2362
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2363
|
+
context: {
|
|
2364
|
+
...context,
|
|
2365
|
+
...error?.context
|
|
2366
|
+
},
|
|
2367
|
+
recovery: error?.recovery ?? commandRecovery(command, error)
|
|
2368
|
+
});
|
|
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
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* 目录名 → Widget name(同时用作 panel id)。中文或纯符号目录名必须回退成合法标识符,
|
|
2424
|
+
* 不能净化成 "----" 这类空壳,否则 manifest 的 name/id 会直接违反作者清单契约。
|
|
2425
|
+
*/
|
|
2426
|
+
function deriveWidgetName(directoryName) {
|
|
2427
|
+
const slug = String(directoryName ?? "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
|
|
2428
|
+
if (slug.length === 0) return `widget-${createHash("sha256").update(String(directoryName ?? "")).digest("hex").slice(0, 8)}`;
|
|
2429
|
+
return slug.slice(0, 64).replace(/-+$/gu, "");
|
|
2430
|
+
}
|
|
2431
|
+
async function commandCreate(directory, options) {
|
|
2432
|
+
const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
|
|
2433
|
+
if (!directory) throw new Error("create 需要目标目录");
|
|
2434
|
+
const template = TEMPLATES[options.template];
|
|
2435
|
+
if (!template) throw new Error(`未知模板:${options.template}`);
|
|
2436
|
+
if (!["vanilla", "react"].includes(options.ui)) throw new Error(`未知 UI 模式:${options.ui},可选 vanilla 或 react`);
|
|
2437
|
+
if (options.panelId && options.template !== "panel") throw new Error("--panel-id 只适用于 --template panel");
|
|
2438
|
+
if (options.panelId && !/^[A-Za-z0-9_.:-]{1,128}$/u.test(options.panelId)) throw new Error("--panel-id 必须为 1-128 位,只能包含字母、数字、下划线、点、冒号和连字符");
|
|
2439
|
+
const root = resolve(directory);
|
|
2440
|
+
if (existsSync(root)) throw new Error(`目标目录已存在:${root}`);
|
|
2441
|
+
const directoryName = basename(root);
|
|
2442
|
+
const name = deriveWidgetName(directoryName);
|
|
2443
|
+
const panelId = options.template === "panel" ? options.panelId ?? name : void 0;
|
|
2444
|
+
const templateContext = {
|
|
2445
|
+
name,
|
|
2446
|
+
panelId,
|
|
2447
|
+
sdkVersion: sdkPackage.version,
|
|
2448
|
+
ui: options.ui
|
|
2449
|
+
};
|
|
2450
|
+
const manifest = template.manifest(templateContext);
|
|
2451
|
+
const sample = template.sample(templateContext);
|
|
2452
|
+
await mkdir(join(root, "src"), { recursive: true });
|
|
2453
|
+
await mkdir(join(root, "samples"), { recursive: true });
|
|
2454
|
+
await mkdir(join(root, "scripts"), { recursive: true });
|
|
2455
|
+
await writeFile(join(root, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
2456
|
+
await writeFile(join(root, "samples/default.json"), `${JSON.stringify(sample, null, 2)}\n`);
|
|
2457
|
+
const sourceFile = options.ui === "react" ? "src/widget.tsx" : "src/widget.ts";
|
|
2458
|
+
const hostFile = "src/widget-host.ts";
|
|
2459
|
+
const source = options.ui === "react" && template.reactSource ? template.reactSource({ name }) : typeof template.source === "function" ? template.source({
|
|
2460
|
+
ui: options.ui,
|
|
2461
|
+
name
|
|
2462
|
+
}) : template.source;
|
|
2463
|
+
await writeFile(join(root, sourceFile), source);
|
|
2464
|
+
await writeFile(join(root, hostFile), template.hostSource({ sourceFile }));
|
|
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`);
|
|
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
|
+
}));
|
|
2474
|
+
await writeFile(join(root, "tsconfig.json"), `${JSON.stringify({
|
|
2475
|
+
compilerOptions: {
|
|
2476
|
+
target: "ES2022",
|
|
2477
|
+
module: "ESNext",
|
|
2478
|
+
moduleResolution: "bundler",
|
|
2479
|
+
lib: ["ES2022", "DOM"],
|
|
2480
|
+
strict: true,
|
|
2481
|
+
noEmit: true,
|
|
2482
|
+
allowImportingTsExtensions: true,
|
|
2483
|
+
skipLibCheck: true,
|
|
2484
|
+
...options.ui === "react" ? { jsx: "react-jsx" } : {}
|
|
2485
|
+
},
|
|
2486
|
+
include: ["src"]
|
|
2487
|
+
}, null, 2)}\n`);
|
|
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`);
|
|
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`);
|
|
2496
|
+
const warnings = [];
|
|
2497
|
+
await writeProjectToolchain(root, toolchain);
|
|
2498
|
+
if (name !== directoryName) warnings.push(`目录名 "${directoryName}" 不是合法标识符,manifest 的 name 已取 "${name}";需要其他取值请直接编辑 manifest.json。`);
|
|
2499
|
+
if (options.template === "panel" && panelId !== "workspace-files") warnings.push(`Panel id "${panelId}" 需要 Host 预先注册并绑定;工作区文件面板请使用 --panel-id workspace-files。`);
|
|
2500
|
+
const result = {
|
|
2501
|
+
ok: true,
|
|
2502
|
+
command: "create",
|
|
2503
|
+
projectRoot: root,
|
|
2504
|
+
name,
|
|
2505
|
+
template: options.template,
|
|
2506
|
+
ui: options.ui,
|
|
2507
|
+
entry: sourceFile,
|
|
2508
|
+
...panelId ? { panelId } : {},
|
|
2509
|
+
manifestPath: join(root, "manifest.json"),
|
|
2510
|
+
packagePath: join(root, "package.json"),
|
|
2511
|
+
profile: toolchain.profile,
|
|
2512
|
+
hostCompatibility,
|
|
2513
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
2514
|
+
};
|
|
2515
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
2516
|
+
else {
|
|
2517
|
+
console.log(`已创建 Widget 工程:${root}(manifest name: ${name})`);
|
|
2518
|
+
for (const warning of warnings) console.warn(`提示:${warning}`);
|
|
2519
|
+
console.log(`下一步:cd ${JSON.stringify(root)},执行 pnpm install,再执行 pnpm exec bohr-widget build . --json 和 pnpm exec bohr-widget check . --json。`);
|
|
2520
|
+
}
|
|
2521
|
+
return result;
|
|
2522
|
+
}
|
|
2523
|
+
function projectLabel(projectRoot) {
|
|
2524
|
+
const relativePath = relative(process.cwd(), projectRoot);
|
|
2525
|
+
return !relativePath || relativePath.startsWith("..") ? projectRoot : relativePath;
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* `check all` 以当前工作目录为根。发布形态下 CLI 位于消费者的 node_modules 内,
|
|
2529
|
+
* 用包自身位置反推仓库根只会得到空集合(旧行为:静默 exit 0,零覆盖)。
|
|
2530
|
+
*/
|
|
2531
|
+
function resolveProjectSet(input) {
|
|
2532
|
+
if (input !== "all") return resolveWidgetProjects(input);
|
|
2533
|
+
const base = resolve(process.cwd());
|
|
2534
|
+
const catalogued = resolveWidgetProjects("all", base);
|
|
2535
|
+
if (catalogued.length > 0) return catalogued;
|
|
2536
|
+
return existsSync(join(base, "manifest.json")) ? [base] : [];
|
|
2537
|
+
}
|
|
2538
|
+
/** 作者清单里下游校验看不到的字段,先在这里给出精确错误。 */
|
|
2539
|
+
function lintProjectManifest(projectRoot) {
|
|
2540
|
+
const manifestPath = join(projectRoot, "manifest.json");
|
|
2541
|
+
if (!existsSync(manifestPath)) return null;
|
|
2542
|
+
let manifest;
|
|
2543
|
+
try {
|
|
2544
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
2545
|
+
} catch {
|
|
2546
|
+
return null;
|
|
2547
|
+
}
|
|
2548
|
+
if (!looksLikeAuthorManifest(manifest)) return null;
|
|
2549
|
+
return lintAuthorManifest(manifest, { fileExists: (declared) => existsSync(join(projectRoot, declared)) });
|
|
2550
|
+
}
|
|
2551
|
+
function fixtureContentBytes(value) {
|
|
2552
|
+
if (!value || typeof value.content !== "string") return null;
|
|
2553
|
+
try {
|
|
2554
|
+
return value.encoding === "base64" ? Buffer.from(value.content, "base64").byteLength : Buffer.byteLength(value.content, "utf8");
|
|
2555
|
+
} catch {
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
/** CLI 在共享 L0 契约之外补充当前 Host 与预览 fixture 的可运行性检查。 */
|
|
2560
|
+
function applyCliProjectPolicies(result) {
|
|
2561
|
+
const widgetConfig = result.manifest?.widgetConfig;
|
|
2562
|
+
if (!widgetConfig?.contribution) return result;
|
|
2563
|
+
const contribution = widgetConfig.contribution;
|
|
2564
|
+
const files = widgetConfig.requestedCapabilities?.files;
|
|
2565
|
+
if (contribution.type === "panel" && contribution.id === "workspace-files") {
|
|
2566
|
+
const missing = [
|
|
2567
|
+
"list",
|
|
2568
|
+
"metadata",
|
|
2569
|
+
"read"
|
|
2570
|
+
].filter((capability) => files?.[capability] !== true);
|
|
2571
|
+
const ok = missing.length === 0;
|
|
2572
|
+
result.checks.push({
|
|
2573
|
+
name: "workspace-files 申请完整只读文件能力",
|
|
2574
|
+
ok
|
|
2575
|
+
});
|
|
2576
|
+
if (!ok) result.errors.push(`workspace-files 缺少 capabilities.files.${missing.join("、capabilities.files.")};Host 无法加载文件面板`);
|
|
2577
|
+
}
|
|
2578
|
+
if (result.fixtures && files?.read) {
|
|
2579
|
+
const listedFiles = /* @__PURE__ */ new Map();
|
|
2580
|
+
for (const listing of Object.values(result.fixtures.files?.list ?? {})) for (const item of listing?.items ?? []) if (item?.kind === "file" && typeof item.id === "string") listedFiles.set(item.id, item);
|
|
2581
|
+
const readable = result.fixtures.files?.read ?? {};
|
|
2582
|
+
const metadata = result.fixtures.files?.metadata ?? {};
|
|
2583
|
+
const missing = [...listedFiles.keys()].filter((id) => readable[id] === void 0);
|
|
2584
|
+
const sizeMismatches = [];
|
|
2585
|
+
for (const [id, item] of listedFiles) {
|
|
2586
|
+
const bytes = fixtureContentBytes(readable[id]);
|
|
2587
|
+
if (bytes === null) continue;
|
|
2588
|
+
for (const [source, size] of [["files.list", item.size], ["files.metadata", metadata[id]?.size]]) if (typeof size === "number" && size !== bytes) sizeMismatches.push(`${id} (${source}=${size}, read=${bytes})`);
|
|
2589
|
+
}
|
|
2590
|
+
const ok = missing.length === 0 && sizeMismatches.length === 0;
|
|
2591
|
+
result.checks.push({
|
|
2592
|
+
name: "fixtures 覆盖目录清单中的可点击文件",
|
|
2593
|
+
ok,
|
|
2594
|
+
detail: `${listedFiles.size} listed files`
|
|
2595
|
+
});
|
|
2596
|
+
if (missing.length > 0) result.errors.push(`preview/fixtures.json: files.read 缺少 files.list 中的文件:${missing.join(", ")}`);
|
|
2597
|
+
if (sizeMismatches.length > 0) result.errors.push(`preview/fixtures.json: 文件大小与 read 内容字节数不一致:${sizeMismatches.join(", ")}`);
|
|
2598
|
+
}
|
|
2599
|
+
result.ok = result.errors.length === 0 && result.checks.every((check) => check.ok);
|
|
2600
|
+
return result;
|
|
2601
|
+
}
|
|
2602
|
+
async function commandCheck(input, options, commandName = "check") {
|
|
2603
|
+
if (!input) throw new Error("check 需要工程目录、manifest.json 或 all");
|
|
2604
|
+
const base = resolve(process.cwd());
|
|
2605
|
+
const projects = resolveProjectSet(input);
|
|
2606
|
+
if (projects.length === 0) {
|
|
2607
|
+
const result = {
|
|
2608
|
+
ok: false,
|
|
2609
|
+
command: commandName,
|
|
2610
|
+
checked: 0,
|
|
2611
|
+
base,
|
|
2612
|
+
checks: [],
|
|
2613
|
+
errors: [`${base} 下没有找到 Widget 工程(已查找 examples/sdk、examples/integration 和当前目录的 manifest.json)`],
|
|
2614
|
+
error: structuredError({
|
|
2615
|
+
code: "WIDGET_CLI_NO_WIDGET_PROJECT",
|
|
2616
|
+
message: "check all 没有匹配到任何 Widget 工程,未校验任何文件。",
|
|
2617
|
+
context: {
|
|
2618
|
+
command: commandName,
|
|
2619
|
+
input,
|
|
2620
|
+
base
|
|
2621
|
+
},
|
|
2622
|
+
recovery: "在 Widget 工程目录内执行 bohr-widget check .;或在包含 examples/sdk|examples/integration 的工作区根目录执行 check all。"
|
|
2623
|
+
})
|
|
2624
|
+
};
|
|
2625
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
2626
|
+
else {
|
|
2627
|
+
console.error(`错误:${result.errors[0]}`);
|
|
2628
|
+
console.error(`恢复建议:${result.error.recovery}`);
|
|
2629
|
+
}
|
|
2630
|
+
process.exitCode = 1;
|
|
2631
|
+
return result;
|
|
2632
|
+
}
|
|
2633
|
+
const results = [];
|
|
2634
|
+
let passed = true;
|
|
2635
|
+
for (const project of projects) {
|
|
2636
|
+
const label = projectLabel(project);
|
|
2637
|
+
const lint = lintProjectManifest(project);
|
|
2638
|
+
let result;
|
|
2639
|
+
if (lint && !lint.ok) result = {
|
|
2640
|
+
ok: false,
|
|
2641
|
+
label,
|
|
2642
|
+
lintErrors: lint.errors,
|
|
2643
|
+
checks: [{
|
|
2644
|
+
name: "作者清单静态检查",
|
|
2645
|
+
ok: false
|
|
2646
|
+
}],
|
|
2647
|
+
errors: lint.errors,
|
|
2648
|
+
warnings: lint.warnings,
|
|
2649
|
+
manifest: null,
|
|
2650
|
+
sourceManifest: null,
|
|
2651
|
+
manifestKind: "author",
|
|
2652
|
+
projectRoot: project
|
|
2653
|
+
};
|
|
2654
|
+
else {
|
|
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
|
+
}
|
|
2677
|
+
if (lint?.warnings.length) result.warnings = [...lint.warnings, ...result.warnings ?? []];
|
|
2678
|
+
if (!result.ok && result.manifestKind === "host" && looksLikeAuthorManifest(result.sourceManifest)) result.errors = [...result.errors, "manifest.json 含作者字段 contributes,却被按宿主清单校验:请补齐 version,并确认 contributes.widget.type 为 panel 或 file-preview。"];
|
|
2679
|
+
}
|
|
2680
|
+
results.push(result);
|
|
2681
|
+
passed &&= result.ok;
|
|
2682
|
+
if (input !== "all") printResult(options.json ? {
|
|
2683
|
+
command: commandName,
|
|
2684
|
+
...result,
|
|
2685
|
+
...result.ok ? {} : { error: result.compatibilityError ?? structuredError({
|
|
2686
|
+
code: result.lintErrors ? "WIDGET_CLI_MANIFEST_INVALID" : "WIDGET_CLI_VALIDATION_FAILED",
|
|
2687
|
+
message: "Widget 工程校验失败。",
|
|
2688
|
+
context: {
|
|
2689
|
+
projectRoot: result.projectRoot,
|
|
2690
|
+
label
|
|
2691
|
+
},
|
|
2692
|
+
recovery: commandRecovery(commandName)
|
|
2693
|
+
}) }
|
|
2694
|
+
} : result, options.json);
|
|
2695
|
+
}
|
|
2696
|
+
if (input === "all") {
|
|
2697
|
+
const failed = results.filter((result) => !result.ok);
|
|
2698
|
+
const summary = {
|
|
2699
|
+
ok: passed,
|
|
2700
|
+
command: commandName,
|
|
2701
|
+
base,
|
|
2702
|
+
checked: results.length,
|
|
2703
|
+
passed: results.length - failed.length,
|
|
2704
|
+
failed: failed.length,
|
|
2705
|
+
results: results.map((result) => ({
|
|
2706
|
+
label: result.label,
|
|
2707
|
+
projectRoot: result.projectRoot,
|
|
2708
|
+
ok: result.ok,
|
|
2709
|
+
checks: result.checks,
|
|
2710
|
+
errors: result.errors,
|
|
2711
|
+
...result.warnings?.length ? { warnings: result.warnings } : {}
|
|
2712
|
+
})),
|
|
2713
|
+
...passed ? {} : { error: structuredError({
|
|
2714
|
+
code: "WIDGET_CLI_VALIDATION_FAILED",
|
|
2715
|
+
message: "部分 Widget 工程校验失败。",
|
|
2716
|
+
context: {
|
|
2717
|
+
base,
|
|
2718
|
+
failedProjects: failed.map((result) => result.projectRoot)
|
|
2719
|
+
},
|
|
2720
|
+
recovery: commandRecovery(commandName)
|
|
2721
|
+
}) }
|
|
2722
|
+
};
|
|
2723
|
+
if (options.json) console.log(JSON.stringify(summary, null, 2));
|
|
2724
|
+
else {
|
|
2725
|
+
for (const result of results) printResult(result, false);
|
|
2726
|
+
console.log(`\n已校验 ${summary.checked} 个工程:${summary.passed} 通过 / ${summary.failed} 失败`);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
if (!passed) process.exitCode = 1;
|
|
2730
|
+
return input === "all" ? void 0 : results[0];
|
|
2731
|
+
}
|
|
2732
|
+
const LOCKFILE_MANAGERS = [
|
|
2733
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
2734
|
+
["yarn.lock", "yarn"],
|
|
2735
|
+
["package-lock.json", "npm"],
|
|
2736
|
+
["npm-shrinkwrap.json", "npm"]
|
|
2737
|
+
];
|
|
2738
|
+
/** 用哪个包管理器跑工程构建:packageManager 优先,其次 lockfile,最后回退 npm。 */
|
|
2739
|
+
function detectPackageManager(root, projectPackage) {
|
|
2740
|
+
const declaredManager = String(projectPackage.packageManager ?? "").split("@")[0];
|
|
2741
|
+
if ([
|
|
2742
|
+
"pnpm",
|
|
2743
|
+
"yarn",
|
|
2744
|
+
"npm"
|
|
2745
|
+
].includes(declaredManager)) return declaredManager;
|
|
2746
|
+
for (const [lockfile, manager] of LOCKFILE_MANAGERS) if (existsSync(join(root, lockfile))) return manager;
|
|
2747
|
+
return "npm";
|
|
2748
|
+
}
|
|
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];
|
|
2752
|
+
return line ? line.slice(0, 300) : void 0;
|
|
2753
|
+
}
|
|
2754
|
+
const UNRESOLVED_BUILD_PATTERNS = [
|
|
2755
|
+
/\[UNRESOLVED_IMPORT\]/u,
|
|
2756
|
+
/Could not resolve ['"][^'"]+['"]/u,
|
|
2757
|
+
/Module not found, treating it as an external dependency/u,
|
|
2758
|
+
/No name was provided for external module/u
|
|
2759
|
+
];
|
|
2760
|
+
function unresolvedBuildDiagnostic(stdout, stderr) {
|
|
2761
|
+
return `${stderr ?? ""}\n${stdout ?? ""}`.split("\n").map((line) => line.trim()).filter(Boolean).find((line) => UNRESOLVED_BUILD_PATTERNS.some((pattern) => pattern.test(line)))?.slice(0, 300);
|
|
2762
|
+
}
|
|
2763
|
+
async function runProjectBuild(root, { capture = false } = {}) {
|
|
2764
|
+
const manager = detectPackageManager(root, JSON.parse(readFileSync(join(root, "package.json"), "utf8")));
|
|
2765
|
+
const { spawn } = await import("node:child_process");
|
|
2766
|
+
const command = process.platform === "win32" ? `${manager}.cmd` : manager;
|
|
2767
|
+
return await new Promise((resolvePromise, reject) => {
|
|
2768
|
+
const child = spawn(command, ["run", "build"], {
|
|
2769
|
+
cwd: root,
|
|
2770
|
+
stdio: capture ? [
|
|
2771
|
+
"ignore",
|
|
2772
|
+
"pipe",
|
|
2773
|
+
"pipe"
|
|
2774
|
+
] : "inherit"
|
|
2775
|
+
});
|
|
2776
|
+
let stdout = "";
|
|
2777
|
+
let stderr = "";
|
|
2778
|
+
if (capture) {
|
|
2779
|
+
child.stdout.on("data", (chunk) => {
|
|
2780
|
+
stdout += String(chunk);
|
|
2781
|
+
});
|
|
2782
|
+
child.stderr.on("data", (chunk) => {
|
|
2783
|
+
stderr += String(chunk);
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
child.once("error", (error) => reject(error));
|
|
2787
|
+
child.once("exit", (code, signal) => {
|
|
2788
|
+
const result = {
|
|
2789
|
+
code,
|
|
2790
|
+
signal,
|
|
2791
|
+
stdout,
|
|
2792
|
+
stderr
|
|
2793
|
+
};
|
|
2794
|
+
if (code === 0) {
|
|
2795
|
+
const unresolved = unresolvedBuildDiagnostic(stdout, stderr);
|
|
2796
|
+
if (!unresolved) {
|
|
2797
|
+
resolvePromise(result);
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
const error = /* @__PURE__ */ new Error(`构建器退出码为 0,但存在未解析依赖:${unresolved}`);
|
|
2801
|
+
Object.assign(error, result, { cliCode: "WIDGET_CLI_BUILD_UNRESOLVED_IMPORT" });
|
|
2802
|
+
reject(error);
|
|
2803
|
+
} else {
|
|
2804
|
+
const detail = firstFailureLine(stderr, stdout);
|
|
2805
|
+
const error = /* @__PURE__ */ new Error(`构建失败(exit ${code ?? signal ?? "unknown"})${detail ? `:${detail}` : ""}`);
|
|
2806
|
+
Object.assign(error, result);
|
|
2807
|
+
reject(error);
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
async function commandBuild(directory, options = {}) {
|
|
2813
|
+
if (!directory) throw new Error("build 需要工程目录");
|
|
2814
|
+
const root = resolve(directory);
|
|
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);
|
|
2819
|
+
const build = await runProjectBuild(root, { capture: true });
|
|
2820
|
+
await validateProjectToolchain(root, toolchain);
|
|
2821
|
+
const receipt = await writeBuildReceipt(root, toolchain, { inputsBefore });
|
|
2822
|
+
const result = {
|
|
2823
|
+
ok: true,
|
|
2824
|
+
command: "build",
|
|
2825
|
+
projectRoot: root,
|
|
2826
|
+
stdout: build.stdout,
|
|
2827
|
+
stderr: build.stderr,
|
|
2828
|
+
...compatibility,
|
|
2829
|
+
hostCompatibility: await assertHostCompatibility(options.hostCompat, toolchain),
|
|
2830
|
+
receipt
|
|
2831
|
+
};
|
|
2832
|
+
if (options.json && options.emit !== false) console.log(JSON.stringify(result, null, 2));
|
|
2833
|
+
else if (options.emit !== false) {
|
|
2834
|
+
if (build.stdout) process.stdout.write(build.stdout);
|
|
2835
|
+
if (build.stderr) process.stderr.write(build.stderr);
|
|
2836
|
+
}
|
|
2837
|
+
return result;
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
const result = {
|
|
2840
|
+
ok: false,
|
|
2841
|
+
command: "build",
|
|
2842
|
+
projectRoot: root,
|
|
2843
|
+
error: commandError("build", error, {
|
|
2844
|
+
projectRoot: root,
|
|
2845
|
+
phase: "build"
|
|
2846
|
+
}),
|
|
2847
|
+
stdout: error?.stdout ?? "",
|
|
2848
|
+
stderr: error?.stderr ?? ""
|
|
2849
|
+
};
|
|
2850
|
+
if (options.json && options.emit !== false) console.log(JSON.stringify(result, null, 2));
|
|
2851
|
+
if (!options.json && options.emit !== false) {
|
|
2852
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
2853
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
2854
|
+
}
|
|
2855
|
+
process.exitCode = 1;
|
|
2856
|
+
if (!options.json && options.emit !== false) throw error;
|
|
2857
|
+
return result;
|
|
2858
|
+
}
|
|
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
|
+
}
|
|
2866
|
+
async function commandPack(directory, options) {
|
|
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);
|
|
2877
|
+
if (options.build) {
|
|
2878
|
+
const build = await commandBuild(directory, {
|
|
2879
|
+
...options,
|
|
2880
|
+
emit: false
|
|
2881
|
+
});
|
|
2882
|
+
if (!options.json) {
|
|
2883
|
+
if (build.stdout) process.stdout.write(build.stdout);
|
|
2884
|
+
if (build.stderr) process.stderr.write(build.stderr);
|
|
2885
|
+
}
|
|
2886
|
+
if (!build.ok) {
|
|
2887
|
+
if (options.json) console.log(JSON.stringify({
|
|
2888
|
+
...build,
|
|
2889
|
+
command: "pack",
|
|
2890
|
+
phase: "build"
|
|
2891
|
+
}, null, 2));
|
|
2892
|
+
process.exitCode = 1;
|
|
2893
|
+
return build;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
const result = applyCliProjectPolicies(await checkWidgetProject(resolve(directory), {
|
|
2897
|
+
requireBuiltEntry: true,
|
|
2898
|
+
toolchain: { sdkVersion: toolchain.profile.sdk }
|
|
2899
|
+
}));
|
|
2900
|
+
if (!options.json) printResult(result, false);
|
|
2901
|
+
if (!result.ok) {
|
|
2902
|
+
if (options.json) console.log(JSON.stringify({
|
|
2903
|
+
ok: false,
|
|
2904
|
+
command: "pack",
|
|
2905
|
+
projectRoot: result.projectRoot,
|
|
2906
|
+
error: structuredError({
|
|
2907
|
+
code: "WIDGET_CLI_PACK_VALIDATION_FAILED",
|
|
2908
|
+
message: "Widget 工程校验失败,未生成可交付 Bundle。",
|
|
2909
|
+
context: {
|
|
2910
|
+
projectRoot: result.projectRoot,
|
|
2911
|
+
phase: "validate"
|
|
2912
|
+
},
|
|
2913
|
+
recovery: commandRecovery("pack")
|
|
2914
|
+
}),
|
|
2915
|
+
checks: result.checks,
|
|
2916
|
+
errors: result.errors
|
|
2917
|
+
}, null, 2));
|
|
2918
|
+
process.exitCode = 1;
|
|
2919
|
+
return {
|
|
2920
|
+
ok: false,
|
|
2921
|
+
command: "pack",
|
|
2922
|
+
projectRoot: result.projectRoot
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
const destination = resolve(options.output ?? join(result.projectRoot, `${result.manifest.name}.zip`));
|
|
2926
|
+
const fixturesPath = join(result.projectRoot, "preview", "fixtures.json");
|
|
2927
|
+
const hasFixtures = existsSync(fixturesPath);
|
|
2928
|
+
const fixturesIncluded = hasFixtures && Boolean(options.includePreviewFixtures);
|
|
2929
|
+
const warnings = [];
|
|
2930
|
+
if (options.includePreviewFixtures && !hasFixtures) warnings.push("使用了 --include-preview-fixtures,但 preview/fixtures.json 不存在,ZIP 未包含任何 fixtures。");
|
|
2931
|
+
else if (hasFixtures && !options.includePreviewFixtures) warnings.push("工程包含 preview/fixtures.json,未使用 --include-preview-fixtures,ZIP 未包含 fixtures。");
|
|
2932
|
+
const files = collectWidgetPackageFiles(result, { includePreviewFixtures: fixturesIncluded });
|
|
2933
|
+
warnings.push(...oversizedAssetWarnings(files));
|
|
2934
|
+
const bundle = storedZip(files);
|
|
2935
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
2936
|
+
await writeFile(destination, bundle);
|
|
2937
|
+
const artifact = {
|
|
2938
|
+
ok: true,
|
|
2939
|
+
command: "pack",
|
|
2940
|
+
projectRoot: result.projectRoot,
|
|
2941
|
+
path: destination,
|
|
2942
|
+
bytes: bundle.byteLength,
|
|
2943
|
+
sha256: `sha256:${createHash("sha256").update(bundle).digest("hex")}`,
|
|
2944
|
+
toolchain: {
|
|
2945
|
+
cli: packageJson.version,
|
|
2946
|
+
sdk: sdkPackage.version,
|
|
2947
|
+
contracts: contractsPackage.version
|
|
2948
|
+
},
|
|
2949
|
+
...compatibility,
|
|
2950
|
+
hostCompatibility: await assertHostCompatibility(options.hostCompat, toolchain),
|
|
2951
|
+
manifest: {
|
|
2952
|
+
name: result.manifest.name,
|
|
2953
|
+
entry: normalizeArchivePath(result.manifest.widgetConfig.entry),
|
|
2954
|
+
contribution: result.manifest.widgetConfig.contribution,
|
|
2955
|
+
sampleIds: (result.manifest.widgetConfig.samples ?? []).map((sample) => sample.id)
|
|
2956
|
+
},
|
|
2957
|
+
checks: result.checks,
|
|
2958
|
+
fixtures: {
|
|
2959
|
+
path: fixturesPath,
|
|
2960
|
+
requested: Boolean(options.includePreviewFixtures),
|
|
2961
|
+
included: fixturesIncluded
|
|
2962
|
+
},
|
|
2963
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
2964
|
+
};
|
|
2965
|
+
if (options.json) console.log(JSON.stringify(artifact, null, 2));
|
|
2966
|
+
else {
|
|
2967
|
+
console.log(`已生成可加载插件包:${destination}`);
|
|
2968
|
+
for (const warning of warnings) console.warn(`提示:${warning}`);
|
|
2969
|
+
}
|
|
2970
|
+
return artifact;
|
|
2971
|
+
}
|
|
2972
|
+
async function commandMigrate(positional, options) {
|
|
2973
|
+
if (positional[0] !== "dsh") throw new Error("migrate 目前只支持 dsh");
|
|
2974
|
+
const action = positional[1];
|
|
2975
|
+
const input = positional[2];
|
|
2976
|
+
if (!input) throw new Error(`migrate dsh ${action ?? ""} 需要源插件目录`);
|
|
2977
|
+
if (action === "audit") {
|
|
2978
|
+
const report = await auditDshPlugin(input);
|
|
2979
|
+
if (options.json) console.log(JSON.stringify({
|
|
2980
|
+
ok: true,
|
|
2981
|
+
command: "migrate dsh audit",
|
|
2982
|
+
report
|
|
2983
|
+
}, null, 2));
|
|
2984
|
+
else console.log(`${report.migrationStatus}: ${report.source.name ?? "unknown"} (${report.compatibility.status})`);
|
|
2985
|
+
return report;
|
|
2986
|
+
}
|
|
2987
|
+
if (action === "scaffold") {
|
|
2988
|
+
if (!options.output) throw new Error("migrate dsh scaffold 需要 --out <widget-dir>");
|
|
2989
|
+
const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
|
|
2990
|
+
const result = await scaffoldDshWidget(input, options.output, {
|
|
2991
|
+
sdkVersion: sdkPackage.version,
|
|
2992
|
+
contractsVersion: contractsPackage.version
|
|
2993
|
+
});
|
|
2994
|
+
await writeProjectToolchain(result.projectRoot, toolchain);
|
|
2995
|
+
result.profile = toolchain.profile;
|
|
2996
|
+
result.hostCompatibility = hostCompatibility;
|
|
2997
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
2998
|
+
else console.log(`已生成迁移 Widget:${result.projectRoot}`);
|
|
2999
|
+
return result;
|
|
3000
|
+
}
|
|
3001
|
+
throw new Error(`未知 migrate dsh 操作:${action ?? ""}`);
|
|
3002
|
+
}
|
|
3003
|
+
async function commandDoctor(directory, options) {
|
|
3004
|
+
const root = resolve(directory ?? ".");
|
|
3005
|
+
const packagePath = join(root, "package.json");
|
|
3006
|
+
const manifestPath = join(root, "manifest.json");
|
|
3007
|
+
const report = {
|
|
3008
|
+
command: "doctor",
|
|
3009
|
+
cli: packageJson.version,
|
|
3010
|
+
node: process.versions.node,
|
|
3011
|
+
sdk: sdkPackage.version,
|
|
3012
|
+
contracts: contractsPackage.version,
|
|
3013
|
+
project: root,
|
|
3014
|
+
packageJson: existsSync(packagePath),
|
|
3015
|
+
manifest: existsSync(manifestPath),
|
|
3016
|
+
nodeSupported: Number(process.versions.node.split(".")[0]) >= 20,
|
|
3017
|
+
dependenciesInstalled: existsSync(join(root, "node_modules")),
|
|
3018
|
+
manifestValid: false,
|
|
3019
|
+
warnings: []
|
|
3020
|
+
};
|
|
3021
|
+
if (report.manifest) try {
|
|
3022
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
3023
|
+
report.manifestValid = true;
|
|
3024
|
+
report.manifestKind = manifest.contributes ? "plugin" : "host";
|
|
3025
|
+
report.pluginName = manifest.name;
|
|
3026
|
+
report.entryPath = manifest.widgetConfig?.entry;
|
|
3027
|
+
} catch {
|
|
3028
|
+
report.manifestKind = "invalid";
|
|
3029
|
+
}
|
|
3030
|
+
report.entryPath ??= "dist/widget.js";
|
|
3031
|
+
report.entry = existsSync(join(root, report.entryPath));
|
|
3032
|
+
report.ok = report.nodeSupported && report.packageJson && report.manifest && report.manifestValid;
|
|
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
|
+
}
|
|
3064
|
+
if (report.manifest && !report.manifestValid) report.warnings.push("manifest.json 不是有效 JSON:修复语法后再执行 doctor、check 或 pack。");
|
|
3065
|
+
if (!report.dependenciesInstalled) report.warnings.push("未找到 node_modules:先执行包管理器 install,build/pack 需要工程依赖。");
|
|
3066
|
+
if (!report.entry) report.warnings.push(`未找到构建入口 ${report.entryPath}:先执行 bohr-widget build . 或工程 build 脚本。`);
|
|
3067
|
+
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
3068
|
+
else {
|
|
3069
|
+
console.log(`${report.ok ? "✓" : "✗"} 环境检查${report.ok ? "通过" : "失败"}:Node ${report.node},SDK ${report.sdk},Contracts ${report.contracts}`);
|
|
3070
|
+
console.log(`${report.ready ? "✓" : "✗"} 工程可直接 check/pack:${report.ready ? "是" : "否"}`);
|
|
3071
|
+
for (const warning of report.warnings) console.warn(`提示:${warning}`);
|
|
3072
|
+
}
|
|
3073
|
+
if (!report.ok) process.exitCode = 1;
|
|
3074
|
+
}
|
|
3075
|
+
function commandSchema() {
|
|
3076
|
+
console.log(getSchemaPath("widget-plugin-manifest.schema.json"));
|
|
3077
|
+
}
|
|
3078
|
+
function commandVersion(options) {
|
|
3079
|
+
const result = {
|
|
3080
|
+
ok: true,
|
|
3081
|
+
command: "version",
|
|
3082
|
+
cli: packageJson.version,
|
|
3083
|
+
sdk: sdkPackage.version,
|
|
3084
|
+
contracts: contractsPackage.version
|
|
3085
|
+
};
|
|
3086
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
3087
|
+
else console.log(`${result.cli} (SDK ${result.sdk})`);
|
|
3088
|
+
return result;
|
|
3089
|
+
}
|
|
3090
|
+
async function main(argv) {
|
|
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") });
|
|
3094
|
+
const command = argv[0] ?? "help";
|
|
3095
|
+
if (command === "dev") {
|
|
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
|
+
});
|
|
3120
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
3121
|
+
return exitCode;
|
|
3122
|
+
}
|
|
3123
|
+
const { positional, options } = parseOptions(argv.slice(1));
|
|
3124
|
+
if (!COMMANDS.includes(command)) throw new Error(`未知命令:${command}`);
|
|
3125
|
+
if (command === "version") return commandVersion(options);
|
|
3126
|
+
if (command === "integration") return commandIntegration(options);
|
|
3127
|
+
if (command === "adopt") return commandAdopt(positional[0], options);
|
|
3128
|
+
if (command === "create") return commandCreate(positional[0], options);
|
|
3129
|
+
if (command === "check" || command === "validate") return commandCheck(positional[0], options, command);
|
|
3130
|
+
if (command === "build") return commandBuild(positional[0], options);
|
|
3131
|
+
if (command === "pack") return commandPack(positional[0], options);
|
|
3132
|
+
if (command === "doctor") return commandDoctor(positional[0], options);
|
|
3133
|
+
if (command === "migrate") return commandMigrate(positional, options);
|
|
3134
|
+
if (command === "schema") return commandSchema();
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* 是否直接执行本文件。npm/pnpm 的 node_modules/.bin/bohr-widget 是符号链接,
|
|
3138
|
+
* import.meta.url 已被 Node 解析为 realpath,而 process.argv[1] 保留调用时的路径;
|
|
3139
|
+
* 只做 resolve() 比较会让 `bohr-widget ...`、`npx bohr-widget ...` 静默空转(exit 0)。
|
|
3140
|
+
*/
|
|
3141
|
+
function isDirectInvocation() {
|
|
3142
|
+
const entry = process.argv[1];
|
|
3143
|
+
if (!entry) return false;
|
|
3144
|
+
const normalize = (value) => {
|
|
3145
|
+
let resolved;
|
|
3146
|
+
try {
|
|
3147
|
+
resolved = realpathSync(value);
|
|
3148
|
+
} catch {
|
|
3149
|
+
resolved = resolve(value);
|
|
3150
|
+
}
|
|
3151
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
3152
|
+
};
|
|
3153
|
+
return normalize(entry) === normalize(fileURLToPath(import.meta.url));
|
|
3154
|
+
}
|
|
3155
|
+
if (isDirectInvocation()) try {
|
|
3156
|
+
await main(process.argv.slice(2));
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
const argv = process.argv.slice(2);
|
|
3159
|
+
const operands = argv.filter((argument) => !argument.startsWith("-"));
|
|
3160
|
+
const command = operands[0] ?? "help";
|
|
3161
|
+
if (argv.includes("--json")) console.log(JSON.stringify({
|
|
3162
|
+
ok: false,
|
|
3163
|
+
command,
|
|
3164
|
+
error: commandError(command, error, {
|
|
3165
|
+
input: operands[1] ?? null,
|
|
3166
|
+
stdout: error?.stdout ?? "",
|
|
3167
|
+
stderr: error?.stderr ?? ""
|
|
3168
|
+
})
|
|
3169
|
+
}, null, 2));
|
|
3170
|
+
else console.error(`${error instanceof Error ? error.message : String(error)}\n\n${usage()}`);
|
|
3171
|
+
process.exitCode = 1;
|
|
3172
|
+
}
|
|
3173
|
+
//#endregion
|
|
3174
|
+
export {};
|