@vmz/vmz 0.3.12 → 0.3.14

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.
Files changed (57) hide show
  1. package/bin/vmz.js +4 -0
  2. package/delivery-runtime-files.json +2 -0
  3. package/dist/cli/cli.js +46 -23
  4. package/dist/cli/format-cmd.d.ts +9 -17
  5. package/dist/cli/format-cmd.js +37 -49
  6. package/dist/cli/format-config.d.ts +6 -9
  7. package/dist/cli/format-config.js +5 -29
  8. package/dist/delivery/delivery-profile.js +1 -1
  9. package/dist/dev/dev-incremental.d.ts +0 -2
  10. package/dist/dev/dev-incremental.js +0 -1
  11. package/dist/dev/dev-session.d.ts +53 -1
  12. package/dist/dev/dev-session.js +368 -65
  13. package/dist/document/document-base-css.d.ts +15 -0
  14. package/dist/document/document-base-css.js +578 -0
  15. package/dist/document/document-build.d.ts +1 -0
  16. package/dist/document/document-build.js +94 -78
  17. package/dist/document/document-client.d.ts +13 -0
  18. package/dist/document/document-client.js +217 -0
  19. package/dist/document/document-cmd.js +3 -1
  20. package/dist/document/document-designs.d.ts +9 -3
  21. package/dist/document/document-designs.js +20 -34
  22. package/dist/document/document-enrich.d.ts +9 -0
  23. package/dist/document/document-enrich.js +55 -0
  24. package/dist/document/document-highlight.d.ts +26 -0
  25. package/dist/document/document-highlight.js +54 -0
  26. package/dist/document/document-integrate.d.ts +5 -0
  27. package/dist/document/document-integrate.js +1 -0
  28. package/dist/document/document-interactive.js +10 -2
  29. package/dist/document/document-layout-render.js +6 -2
  30. package/dist/document/document-schema.d.ts +5 -0
  31. package/dist/workspace/build-assemble.js +18 -6
  32. package/dist/workspace/content-addressed-assets.d.ts +6 -17
  33. package/dist/workspace/content-addressed-assets.js +10 -95
  34. package/dist/workspace/delivery-runtime-imports.js +9 -0
  35. package/dist/workspace/native-addon.d.ts +4 -0
  36. package/dist/workspace/pack-native.d.ts +151 -0
  37. package/dist/workspace/pack-native.js +280 -0
  38. package/dist/workspace/pack.d.ts +131 -32
  39. package/dist/workspace/pack.js +381 -65
  40. package/dist/workspace/plugin-host.d.ts +4 -4
  41. package/dist/workspace/plugin-host.js +6 -6
  42. package/dist/workspace/production-observability.d.ts +2 -0
  43. package/dist/workspace/production-observability.js +6 -2
  44. package/dist/workspace/project-build.js +26 -4
  45. package/dist/workspace/public-api.d.ts +7 -3
  46. package/dist/workspace/public-api.js +5 -3
  47. package/dist/workspace/static-emit.d.ts +30 -0
  48. package/dist/workspace/static-emit.js +211 -18
  49. package/dist/workspace/user-config.d.ts +112 -0
  50. package/dist/workspace/user-config.js +12 -0
  51. package/host-runtime-files.json +1 -0
  52. package/locales/en-US/cli.json +5 -4
  53. package/locales/en-US/diag.json +2 -0
  54. package/locales/zh-CN/diag.json +2 -0
  55. package/package.json +20 -20
  56. package/dist/workspace/pack-client-packages.d.ts +0 -34
  57. package/dist/workspace/pack-client-packages.js +0 -550
@@ -1,26 +1,109 @@
1
1
  /**
2
- * Pack stage: consume Deployment IR (VPG-owned units), emit pack manifest,
3
- * and lower browser-unreachable bare package imports to `dist/vendor/**`.
2
+ * Pack stage: run the browser delivery graph through the Rust `vmz-pack`
3
+ * binding (N-API `packPlan`/`packWrite`, frozen v0 contract) and record the
4
+ * resulting chunk/asset mapping in `_vmz/pack-manifest.json`.
4
5
  *
5
- * Current status: a thin module-moving slice — reachable module closure
6
- * materialization, specifier rewriting, and TypeScript `transpileModule` type
7
- * erasure. Linking, dead-code elimination, chunk splitting, and minification
8
- * are NOT implemented in this stage; the manifest reports that honestly.
9
- * (Any compaction of compile-emitted modules happens upstream in the compiler,
10
- * not here — pack never rewrites bytes beyond specifier lowering.)
6
+ * Browser graph roots are the bootstrap modules (`entry-client.js` /
7
+ * `entry-event.js`) emitted by the compiler / static delivery path; page and
8
+ * island modules enter the graph through the bootstrap's literal dynamic
9
+ * import tables, never as pack entries. Bootstrap entries declare
10
+ * `execution: 'full'` + `exports: { kind: 'all' }` (script-tag load,
11
+ * whole-module execution).
12
+ *
13
+ * SSR/Node is a separate graph: Node entries are the deployment units
14
+ * reachable from the page / application roots through `dependsOn` (logical =
15
+ * `chunkId`), packed under `_vmz/pack-node/` and indexed by
16
+ * `_vmz/pack-node-manifest.json`. Components have no Node consumer unless a
17
+ * page or layout reaches them, so orphan units are not packed. Node consumers
18
+ * never fall back to the unpacked root `${chunkId}.client.js`; browser
19
+ * bootstrap modules are not Node entries because they bind `dom.browser` /
20
+ * `document`.
21
+ *
22
+ * No vendor materialization, no specifier lowering, and no second hashing
23
+ * pass happen here: `packWrite` emits the final browser JS set under
24
+ * `_vmz/pack/` with content-hashed names, and consumers resolve logical
25
+ * entries through `packEntryHref()` against this manifest.
11
26
  */
12
27
  import crypto from 'node:crypto';
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
28
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
14
29
  import path from 'node:path';
15
- import { loadDeploymentIr, planBundleInputs } from './bundler-adapter.js';
16
- import { packClientBareImports } from './pack-client-packages.js';
30
+ import { loadDeploymentIr } from './bundler-adapter.js';
31
+ import { buildPackRequest, runPackWrite } from './pack-native.js';
17
32
  import { deliveryRuntimeFilePairs } from './delivery-runtime-files.js';
18
33
  import { rewriteDeliveryImports } from './delivery-runtime-imports.js';
19
34
  import { writePrettyJsonFile } from './pretty-json.js';
20
35
  export const PACK_MANIFEST_SCHEMA = 'vmz.pack.manifest.v0';
36
+ /** Directory (relative to outDir) where `packWrite` emits the final browser JS set. */
37
+ export const PACK_OUTPUT_DIR_REL = '_vmz/pack';
38
+ /** Site root base — VMZ static delivery has no configurable base (see delivery-profile). */
39
+ export const PACK_PUBLIC_BASE = '/';
40
+ /**
41
+ * Projection provenance for the pack graph: each deployment unit's emitted
42
+ * client entry maps back to its source `.vmz`. Bare-specifier package discovery
43
+ * then walks up from the source location — dependency components (e.g. a
44
+ * `@vmz/ui` component importing its own peer) keep their source package's
45
+ * node_modules visibility, matching Node realpath semantics. Relative
46
+ * specifiers keep resolving against the emitted tree.
47
+ */
48
+ export function deploymentUnitOrigins(absOut, ir) {
49
+ const origins = {};
50
+ for (const unit of ir.units || []) {
51
+ const source = String(unit.source || '');
52
+ const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
53
+ if (!source || !chunkId)
54
+ continue;
55
+ const rel = (unit.clientEntry ? String(unit.clientEntry) : `${chunkId}.client.js`).replace(/\\/g, '/');
56
+ const emitted = path.join(absOut, ...rel.split('/'));
57
+ if (!existsSync(emitted) || !existsSync(source))
58
+ continue;
59
+ origins[realpathSync(emitted)] = realpathSync(source);
60
+ }
61
+ return origins;
62
+ }
63
+ /** Bootstrap modules that are browser pack entries when present in outDir. */
64
+ const BOOTSTRAP_ENTRIES = ['entry-client.js', 'entry-event.js'];
65
+ /**
66
+ * 模块路径比较键:去掉 Windows verbatim 前缀(Rust `canonicalize` 给 `//?/E:/…`,
67
+ * Node `realpathSync` 给 `E:/…`)并统一分隔符,避免同一文件因前缀差异查不到。
68
+ */
69
+ function moduleKey(filePath) {
70
+ return filePath.replace(/^(?:\\\\\?\\|\/\/\?\/)/, '').replace(/\\/g, '/');
71
+ }
72
+ /**
73
+ * 逻辑 chunkId → 浏览器 chunk 的精确查表:按部署单元的产物客户端文件与
74
+ * chunk `modules` 的同路径匹配,不做后缀匹配、不猜命名规则。单元产物未进
75
+ * 客户端图(无消费者)时显式记 `null`,消费方可据此报错而不是静默取错 chunk。
76
+ */
77
+ function resolveUnitChunks(absOut, deploymentUnits, artifacts) {
78
+ const chunkOfModule = new Map();
79
+ for (const artifact of artifacts) {
80
+ for (const module of artifact.modules) {
81
+ chunkOfModule.set(moduleKey(module), { chunkId: artifact.chunkId, fileName: artifact.fileName });
82
+ }
83
+ }
84
+ const out = [];
85
+ for (const unit of deploymentUnits) {
86
+ const logical = String(unit.chunkId || '').replace(/\\/g, '/');
87
+ if (!logical)
88
+ continue;
89
+ const clientEntry = (unit.clientEntry ? String(unit.clientEntry) : `${logical}.client.js`).replace(/\\/g, '/');
90
+ const abs = path.join(absOut, ...clientEntry.split('/'));
91
+ const hit = chunkOfModule.get(moduleKey(existsSync(abs) ? realpathSync(abs) : abs)) ?? null;
92
+ out.push({
93
+ logical,
94
+ clientEntry,
95
+ chunkId: hit ? hit.chunkId : null,
96
+ fileName: hit ? hit.fileName : null,
97
+ });
98
+ }
99
+ out.sort((a, b) => (a.logical < b.logical ? -1 : a.logical > b.logical ? 1 : 0));
100
+ return out;
101
+ }
21
102
  /**
22
103
  * Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
23
104
  * Always refresh from `@vmz/core` when present so SSR/runtime fixes are not sticky in outDir.
105
+ * Document surfaces (Node-side document layout mount) consume this; browser delivery
106
+ * runtime files reach the pack graph through `@vmz/core` bare imports in the Rust graph.
24
107
  * @param {string} outDir
25
108
  * @param {string | null | undefined} coreDist `@vmz/core` dist root
26
109
  * @returns {string[]} copied relative names
@@ -41,37 +124,61 @@ export function ensureRuntimeCompanions(outDir, coreDist) {
41
124
  }
42
125
  return copied;
43
126
  }
127
+ /**
128
+ * Pack the browser delivery graph via the Rust `vmz-pack` binding and write
129
+ * `_vmz/pack-manifest.json` (v0). Entries are the bootstrap modules that
130
+ * already exist in `outDir` — an empty bootstrap set is an explicit request
131
+ * error, never a silently skipped stage.
132
+ */
44
133
  export function packFromDeploymentIr(outDir, opts = {}) {
45
- ensureRuntimeCompanions(outDir, opts.coreDist);
46
- // Browser ESM cannot resolve bare npm specs — materialize reachable packages first
47
- // so digests below reflect the rewritten graph.
48
- const clientPackages = packClientBareImports(outDir, { projectRoot: opts.projectRoot || null });
49
- const ir = loadDeploymentIr(outDir);
50
- const inputs = planBundleInputs(outDir, ir);
51
- const units = [];
52
- for (const entry of inputs) {
53
- const abs = entry.entry;
54
- let digest = null;
55
- let bytes = 0;
56
- let present = false;
57
- if (existsSync(abs)) {
58
- present = true;
59
- const buf = readFileSync(abs);
60
- bytes = buf.length;
61
- digest = crypto.createHash('sha256').update(buf).digest('hex');
62
- }
63
- units.push({
64
- chunkId: entry.chunkId,
65
- kind: entry.kind,
66
- entry: path.relative(outDir, abs).replace(/\\/g, '/'),
67
- programIr: path.relative(outDir, entry.programIr).replace(/\\/g, '/'),
68
- source: entry.source,
69
- present,
70
- bytes,
71
- digest,
72
- rebuilt: Boolean(entry.rebuilt),
73
- });
134
+ const absOut = path.resolve(outDir);
135
+ const root = opts.projectRoot ? path.resolve(opts.projectRoot) : path.dirname(absOut);
136
+ const entries = {};
137
+ for (const logical of BOOTSTRAP_ENTRIES) {
138
+ const abs = path.join(absOut, logical);
139
+ if (existsSync(abs))
140
+ entries[logical] = abs;
141
+ }
142
+ const uses = Object.keys(entries).map((logical) => ({
143
+ logical,
144
+ execution: 'full',
145
+ exports: { kind: 'all' },
146
+ }));
147
+ // Browser graph entries are the bootstrap modules; page/island units join
148
+ // through the bootstrap dynamic-import tables and need projection provenance.
149
+ const origins = deploymentUnitOrigins(absOut, loadDeploymentIr(absOut));
150
+ const requestJson = buildPackRequest({
151
+ entries,
152
+ target: 'browser',
153
+ root,
154
+ outRoot: absOut,
155
+ uses,
156
+ publicBase: PACK_PUBLIC_BASE,
157
+ origins,
158
+ });
159
+ const packOutDir = path.join(absOut, ...PACK_OUTPUT_DIR_REL.split('/'));
160
+ // 不再整体清空输出目录:`packWrite` 按内容摘要复用未变 chunk,并在写盘后
161
+ // 逐文件清理不再属于产物闭包的旧 chunk / map / css / assets(不留孤儿)。
162
+ const { plan } = runPackWrite(requestJson, packOutDir, opts.binding ? { binding: opts.binding } : {});
163
+ const ir = loadDeploymentIr(absOut);
164
+ let nativeManifest;
165
+ try {
166
+ nativeManifest = JSON.parse(plan.manifestJson);
74
167
  }
168
+ catch (error) {
169
+ throw new Error(`pack manifestJson from binding is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
170
+ }
171
+ const chunks = plan.artifacts.map((a) => {
172
+ const buf = Buffer.from(a.code, 'utf8');
173
+ return {
174
+ chunkId: a.chunkId,
175
+ fileName: a.fileName,
176
+ modules: a.modules,
177
+ bytes: buf.length,
178
+ digest: sha256Hex(buf),
179
+ sourceMap: a.sourceMapFileName,
180
+ };
181
+ });
75
182
  const body = {
76
183
  schema: PACK_MANIFEST_SCHEMA,
77
184
  profileId: opts.profileId || null,
@@ -79,33 +186,241 @@ export function packFromDeploymentIr(outDir, opts = {}) {
79
186
  release: Boolean(opts.release),
80
187
  preferredClientFace: opts.preferredClientFace || '@vmz/core/dom/client',
81
188
  deploymentSchema: ir.schema,
82
- unitCount: units.length,
83
- units,
84
- // Pack runs no compressor in either mode: it reports the bytes it was
85
- // given (dev output written verbatim). Release artifacts may already be
86
- // compacted by the compiler's release print path upstream — pack does not
87
- // claim, add, or verify that compaction.
88
- minify: opts.release ? 'none' : 'dev-identity',
89
- // Names how VPG deployment units are selected for this manifest. It does
90
- // NOT mean JavaScript dead-code elimination ran — that is not implemented.
91
- treeShakeBasis: 'vpg-deployment-ir',
92
- bundler: 'vmz-pack',
93
- clientPackageLowering: {
94
- status: 'thin',
95
- rewrittenFiles: clientPackages.rewrittenFiles,
96
- bareSpecs: clientPackages.bareSpecs,
97
- vendoredModules: clientPackages.vendoredModules,
98
- unresolvedBareSpecs: clientPackages.unresolvedBareSpecs || [],
99
- skippedVmzExports: clientPackages.skippedVmzExports || [],
100
- remainingBareSpecs: clientPackages.remainingBareSpecs || [],
101
- },
189
+ target: 'browser',
190
+ publicBase: PACK_PUBLIC_BASE,
191
+ entries: Object.keys(entries)
192
+ .sort()
193
+ .map((logical) => ({ logical, file: path.relative(absOut, entries[logical]).replace(/\\/g, '/') })),
194
+ entryFiles: plan.entryFiles,
195
+ dynamicFiles: plan.dynamicFiles,
196
+ // CSS artifacts are final bytes under `_vmz/pack/` (`packWrite` wrote
197
+ // them); the wire carries identity/provenance only, never content.
198
+ styles: plan.styles,
199
+ assets: plan.assets,
200
+ chunks,
201
+ unitChunks: resolveUnitChunks(absOut, ir.units || [], plan.artifacts),
202
+ chunkCount: chunks.length,
203
+ styleCount: plan.styles.length,
204
+ entryCount: plan.entryFiles.length,
205
+ // Chunk text is token-minified inside `vmz-pack` while preserving string,
206
+ // template and regex bodies plus ASI-significant line breaks. Identifiers
207
+ // are not renamed and dead code is not eliminated.
208
+ minify: 'tokens',
209
+ // `link` active fixed-point pruning really ran — bootstrap entries
210
+ // declare full execution, internal modules are pruned to reachable exports.
211
+ treeShake: 'vmz-pack-link-pruning',
212
+ // Chunk source maps are v3 token-level: every minified token maps back
213
+ // through the assembled line to its module line (`<packOut>/<fileName>.map`,
214
+ // `sourceMapFileName` on the wire). Generated columns are token columns,
215
+ // source columns stay at the assembled slice start; `names` is unused.
216
+ sourceMaps: 'chunk-v3-token-mappings',
217
+ // `packInputDigest` 是本次 pack 输入(图结构 + 各模块源字节 + emit 选项)
218
+ // 的内容摘要;`packGraphDigest` 只看图结构,任一边界/模块/边变化即整体
219
+ // 失效,不允许部分 chunk 复用。两者都来自 Rust 侧,不使用 mtime/时间戳。
220
+ packInputDigest: plan.inputDigest,
221
+ packGraphDigest: plan.graphDigest,
222
+ nativeManifest,
102
223
  };
103
224
  body.packDigest = sha256Hex(stableStringify({ ...body }));
104
- const vmzDir = path.join(outDir, '_vmz');
225
+ const vmzDir = path.join(absOut, '_vmz');
105
226
  mkdirSync(vmzDir, { recursive: true });
106
227
  const file = path.join(vmzDir, 'pack-manifest.json');
107
228
  writePrettyJsonFile(file, body);
108
- return { manifest: body, path: file, clientPackages };
229
+ return { manifest: body, path: file };
230
+ }
231
+ export const PACK_NODE_MANIFEST_SCHEMA = 'vmz.pack.node-manifest.v0';
232
+ /** Directory (relative to outDir) where `packWrite` emits the Node JS set. */
233
+ export const PACK_NODE_OUTPUT_DIR_REL = '_vmz/pack-node';
234
+ /** Node pack manifest (relative to outDir); `entryFiles` maps logical → chunk file. */
235
+ export const PACK_NODE_MANIFEST_REL = '_vmz/pack-node-manifest.json';
236
+ /**
237
+ * Node graph roots and their `dependsOn` closure — the only chunk ids a Node
238
+ * consumer can request. Pages and the application shell are roots; components
239
+ * enter through `dependsOn` (pages merge their `layoutChain` into it). Standalone
240
+ * layout units (`layouts/*`, emitted with kind `other`) are roots of their own
241
+ * because `loadCtor` asks for page / layout chunk ids directly and no page lists
242
+ * them in `dependsOn`. Units outside the closure have no Node consumer: the SSR
243
+ * registry imports a component entry only when some root reaches it.
244
+ */
245
+ function nodeReachableChunkIds(ir) {
246
+ const dependsOn = new Map();
247
+ const roots = [];
248
+ for (const unit of ir.units || []) {
249
+ const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
250
+ if (!chunkId)
251
+ continue;
252
+ const deps = dependsOn.get(chunkId) ?? [];
253
+ for (const dep of unit.dependsOn || []) {
254
+ const id = String(dep).replace(/\\/g, '/');
255
+ if (id)
256
+ deps.push(id);
257
+ }
258
+ dependsOn.set(chunkId, deps);
259
+ if (unit.kind === 'page' || unit.kind === 'application' || unit.kind === 'other')
260
+ roots.push(chunkId);
261
+ }
262
+ const reachable = new Set();
263
+ const queue = roots.slice();
264
+ while (queue.length) {
265
+ const id = queue.pop();
266
+ if (reachable.has(id))
267
+ continue;
268
+ reachable.add(id);
269
+ for (const dep of dependsOn.get(id) ?? [])
270
+ if (!reachable.has(dep))
271
+ queue.push(dep);
272
+ }
273
+ return reachable;
274
+ }
275
+ /**
276
+ * Pack the SSR/Node graph via the Rust `vmz-pack` binding and write
277
+ * `_vmz/pack-node-manifest.json` (v0). Entries are the node-reachable
278
+ * deployment units' compiled client modules (`clientEntry`, empty → mirrors
279
+ * `vmz-artifacts` `component_entries`: `${chunkId}.client.js`). Node consumers
280
+ * only read `default`, but the import itself is evaluated, hence
281
+ * `side-effects` + named default rather than `all` / `full` / `unconsumed`.
282
+ */
283
+ export function packSsrFromDeploymentIr(outDir, opts = {}) {
284
+ const absOut = path.resolve(outDir);
285
+ const root = opts.projectRoot ? path.resolve(opts.projectRoot) : path.dirname(absOut);
286
+ const ir = loadDeploymentIr(absOut);
287
+ if (!(ir.units || []).length) {
288
+ throw new Error(`packSsrFromDeploymentIr: deployment IR has no units in ${absOut}`);
289
+ }
290
+ const nodeReachable = nodeReachableChunkIds(ir);
291
+ const entries = {};
292
+ const uses = [];
293
+ // Deployment IR may list the same component unit more than once (one entry
294
+ // per consuming page). The node graph identity is `chunkId`, so duplicates
295
+ // collapse into one entry; a same-identity unit pointing at a different
296
+ // client file is a contract violation, not something to silently pick.
297
+ const seen = new Map();
298
+ for (const unit of ir.units || []) {
299
+ const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
300
+ if (!chunkId)
301
+ throw new Error(`packSsrFromDeploymentIr: deployment unit has an empty chunkId in ${absOut}`);
302
+ if (!nodeReachable.has(chunkId))
303
+ continue;
304
+ // `vmz-artifacts` derives the component entry as `${chunkId}.client.js`
305
+ // when `clientEntry` is empty. Decide it here so the pack request stays
306
+ // explicit, then require the file — a missing entry is never skipped.
307
+ const rel = (unit.clientEntry ? String(unit.clientEntry) : `${chunkId}.client.js`).replace(/\\/g, '/');
308
+ if (seen.has(chunkId)) {
309
+ if (seen.get(chunkId) !== rel) {
310
+ throw new Error(`packSsrFromDeploymentIr: deployment unit "${chunkId}" maps to conflicting client entries: ` +
311
+ `${seen.get(chunkId)} and ${rel} (in ${absOut})`);
312
+ }
313
+ continue;
314
+ }
315
+ const abs = path.join(absOut, ...rel.split('/'));
316
+ if (!existsSync(abs)) {
317
+ throw new Error(`packSsrFromDeploymentIr: unit "${chunkId}" client entry missing: ${rel} (in ${absOut})`);
318
+ }
319
+ seen.set(chunkId, rel);
320
+ entries[chunkId] = abs;
321
+ uses.push({ logical: chunkId, execution: 'side-effects', exports: { kind: 'named', names: ['default'] } });
322
+ }
323
+ if (!Object.keys(entries).length) {
324
+ throw new Error(`packSsrFromDeploymentIr: deployment IR has no node-reachable units in ${absOut}`);
325
+ }
326
+ const origins = deploymentUnitOrigins(absOut, ir);
327
+ const requestJson = buildPackRequest({
328
+ entries,
329
+ target: 'node',
330
+ root,
331
+ outRoot: absOut,
332
+ uses,
333
+ publicBase: PACK_PUBLIC_BASE,
334
+ origins,
335
+ });
336
+ const packOutDir = path.join(absOut, ...PACK_NODE_OUTPUT_DIR_REL.split('/'));
337
+ // 与浏览器 pack 同口径:不清空目录,由 `packWrite` 复用未变 chunk 并清理孤儿。
338
+ const { plan } = runPackWrite(requestJson, packOutDir, opts.binding ? { binding: opts.binding } : {});
339
+ let nativeManifest;
340
+ try {
341
+ nativeManifest = JSON.parse(plan.manifestJson);
342
+ }
343
+ catch (error) {
344
+ throw new Error(`node pack manifestJson from binding is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
345
+ }
346
+ const chunks = plan.artifacts.map((a) => {
347
+ const buf = Buffer.from(a.code, 'utf8');
348
+ return {
349
+ chunkId: a.chunkId,
350
+ fileName: a.fileName,
351
+ modules: a.modules,
352
+ bytes: buf.length,
353
+ digest: sha256Hex(buf),
354
+ sourceMap: a.sourceMapFileName,
355
+ };
356
+ });
357
+ // dist roots carry no package.json; Node needs one beside the chunk set to
358
+ // treat content-hashed `.js` files as ESM.
359
+ mkdirSync(packOutDir, { recursive: true });
360
+ writeFileSync(path.join(packOutDir, 'package.json'), `${JSON.stringify({ type: 'module' })}\n`, 'utf8');
361
+ const body = {
362
+ schema: PACK_NODE_MANIFEST_SCHEMA,
363
+ target: 'node',
364
+ deploymentSchema: ir.schema,
365
+ entries: Object.keys(entries)
366
+ .sort()
367
+ .map((logical) => ({ logical, file: path.relative(absOut, entries[logical]).replace(/\\/g, '/') })),
368
+ entryFiles: plan.entryFiles,
369
+ dynamicFiles: plan.dynamicFiles,
370
+ chunks,
371
+ chunkCount: chunks.length,
372
+ entryCount: plan.entryFiles.length,
373
+ // 同浏览器 pack:输入/图结构摘要来自 Rust 侧内容摘要判定。
374
+ packInputDigest: plan.inputDigest,
375
+ packGraphDigest: plan.graphDigest,
376
+ nativeManifest,
377
+ };
378
+ body.packDigest = sha256Hex(stableStringify({ ...body }));
379
+ const file = path.join(absOut, ...PACK_NODE_MANIFEST_REL.split('/'));
380
+ writePrettyJsonFile(file, body);
381
+ return { manifest: body, path: file };
382
+ }
383
+ /**
384
+ * Resolve a Node logical entry (`chunkId`) to its packed absolute file from
385
+ * `_vmz/pack-node-manifest.json`. Missing manifest, missing logical, or a
386
+ * missing chunk file is an explicit error — never a fallback to the unpacked
387
+ * root `${chunkId}.client.js`.
388
+ */
389
+ export function packNodeEntryFile(outDir, chunkId) {
390
+ const absOut = path.resolve(outDir);
391
+ const manifestPath = path.join(absOut, ...PACK_NODE_MANIFEST_REL.split('/'));
392
+ if (!existsSync(manifestPath)) {
393
+ throw new Error(`packNodeEntryFile: missing ${manifestPath} — run vmz build (node pack) first`);
394
+ }
395
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
396
+ const hit = (manifest.entryFiles || []).find((e) => e && e.logical === chunkId);
397
+ if (!hit || typeof hit.file !== 'string' || !hit.file) {
398
+ throw new Error(`packNodeEntryFile: node pack manifest has no entry file for "${chunkId}"`);
399
+ }
400
+ const abs = path.join(absOut, ...PACK_NODE_OUTPUT_DIR_REL.split('/'), ...hit.file.split('/'));
401
+ if (!existsSync(abs)) {
402
+ throw new Error(`packNodeEntryFile: node pack chunk missing on disk: ${path.relative(absOut, abs)}`);
403
+ }
404
+ return abs;
405
+ }
406
+ /**
407
+ * Resolve the final browser URL of a packed logical entry from
408
+ * `_vmz/pack-manifest.json` (`entryFiles` is the only logical→physical map).
409
+ * Missing manifest or missing logical entry is an explicit error — no
410
+ * fallback to unpacked root bytes.
411
+ */
412
+ export function packEntryHref(outDir, logical) {
413
+ const manifestPath = path.join(path.resolve(outDir), '_vmz', 'pack-manifest.json');
414
+ if (!existsSync(manifestPath)) {
415
+ throw new Error(`packEntryHref: missing ${manifestPath} — run vmz build (pack) first`);
416
+ }
417
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
418
+ const hit = (manifest.entryFiles || []).find((e) => e && e.logical === logical);
419
+ if (!hit || typeof hit.file !== 'string' || !hit.file) {
420
+ throw new Error(`packEntryHref: pack manifest has no entry file for "${logical}"`);
421
+ }
422
+ const base = String(manifest.publicBase || PACK_PUBLIC_BASE).replace(/\/$/, '');
423
+ return `${base}/${PACK_OUTPUT_DIR_REL}/${hit.file}`;
109
424
  }
110
425
  function stableStringify(value) {
111
426
  return JSON.stringify(sortKeys(value));
@@ -115,12 +430,13 @@ function sortKeys(value) {
115
430
  return value.map(sortKeys);
116
431
  if (value && typeof value === 'object') {
117
432
  const out = {};
118
- for (const k of Object.keys(value).sort())
433
+ for (const k of Object.keys(value).sort()) {
119
434
  out[k] = sortKeys(value[k]);
435
+ }
120
436
  return out;
121
437
  }
122
438
  return value;
123
439
  }
124
- function sha256Hex(text) {
125
- return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
440
+ function sha256Hex(data) {
441
+ return crypto.createHash('sha256').update(data).digest('hex');
126
442
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Plugin protocol v1 helpers + typed config loading.
3
3
  */
4
- import { contentHash, defineConfig, definePlugin } from '@vmz/plugin';
5
- export { contentHash, defineConfig, definePlugin };
4
+ import { contentHash, definePlugin } from '@vmz/plugin';
5
+ export { contentHash, definePlugin };
6
6
  export declare const PLUGIN_PROTOCOL = "0.1.0";
7
7
  export declare const CONFIG_NAMES: string[];
8
8
  export declare const CONFIG_LOAD_FAILED = "vmz::config::load_failed";
@@ -13,9 +13,9 @@ export declare const PLUGIN_ENTRY_INVALID = "vmz::plugin::invalid_entry";
13
13
  * @returns {Promise<{
14
14
  * plugins: import('@vmz/plugin').VmzPlugin[],
15
15
  * engines: import('@vmz/plugin').VmzEngines,
16
- * delivery: import('@vmz/plugin').DeliveryAuthoring | null,
16
+ * delivery: import('./user-config.js').DeliveryAuthoring | null,
17
17
  * application: { id?: string } | null,
18
- * format: import('@vmz/plugin').VmzFormatConfig | null,
18
+ * format: import('./user-config.js').VmzFormatConfig | null,
19
19
  * path: string | null,
20
20
  * pluginPath: string | null,
21
21
  * }>}
@@ -4,10 +4,10 @@
4
4
  import { existsSync } from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { pathToFileURL } from 'node:url';
7
- import { PLUGIN_PROTOCOL as PLUGIN_PROTOCOL_PKG, contentHash, defineConfig, definePlugin } from '@vmz/plugin';
7
+ import { PLUGIN_PROTOCOL as PLUGIN_PROTOCOL_PKG, contentHash, definePlugin } from '@vmz/plugin';
8
8
  import { log } from './log.js';
9
9
  import { resolveWorkspacePackages } from './packages.js';
10
- export { contentHash, defineConfig, definePlugin };
10
+ export { contentHash, definePlugin };
11
11
  export const PLUGIN_PROTOCOL = PLUGIN_PROTOCOL_PKG;
12
12
  export const CONFIG_NAMES = ['vmz.config.ts', 'vmz.config.mts', 'vmz.config.mjs', 'vmz.config.js'];
13
13
  const ROOT_PLUGIN_NAMES = ['vmz.plugin.ts', 'vmz.plugin.mts', 'vmz.plugin.mjs', 'vmz.plugin.js'];
@@ -61,9 +61,9 @@ function configDiagnosticError(code, sourcePath, err) {
61
61
  * @returns {Promise<{
62
62
  * plugins: import('@vmz/plugin').VmzPlugin[],
63
63
  * engines: import('@vmz/plugin').VmzEngines,
64
- * delivery: import('@vmz/plugin').DeliveryAuthoring | null,
64
+ * delivery: import('./user-config.js').DeliveryAuthoring | null,
65
65
  * application: { id?: string } | null,
66
- * format: import('@vmz/plugin').VmzFormatConfig | null,
66
+ * format: import('./user-config.js').VmzFormatConfig | null,
67
67
  * path: string | null,
68
68
  * pluginPath: string | null,
69
69
  * }>}
@@ -73,11 +73,11 @@ export async function loadVmzConfig(project) {
73
73
  const plugins = [];
74
74
  /** @type {import('@vmz/plugin').VmzEngines} */
75
75
  let engines = {};
76
- /** @type {import('@vmz/plugin').DeliveryAuthoring | null} */
76
+ /** @type {import('./user-config.js').DeliveryAuthoring | null} */
77
77
  let delivery = null;
78
78
  /** @type {{ id?: string } | null} */
79
79
  let application = null;
80
- /** @type {import('@vmz/plugin').VmzFormatConfig | null} */
80
+ /** @type {import('./user-config.js').VmzFormatConfig | null} */
81
81
  let format = null;
82
82
  /** @type {string | null} */
83
83
  let configPath = null;
@@ -95,6 +95,8 @@ export declare function checkCapabilityClosure(cap: any, policy: any): {
95
95
  export declare function applySecurityHeadersToCdnPolicy(cdnPolicy: any, security: any): any;
96
96
  /**
97
97
  * Measure dist sizes for budget gates.
98
+ * Browser delivery JS final set = packed chunks under `_vmz/pack/`
99
+ * (root `*.client.js` / bootstrap files are Node SSR / pack inputs, not delivery bytes).
98
100
  * @param {string} distDir
99
101
  */
100
102
  export declare function measureDistBudgets(distDir: any): {
@@ -398,13 +398,16 @@ export function applySecurityHeadersToCdnPolicy(cdnPolicy, security) {
398
398
  }
399
399
  /**
400
400
  * Measure dist sizes for budget gates.
401
+ * Browser delivery JS final set = packed chunks under `_vmz/pack/`
402
+ * (root `*.client.js` / bootstrap files are Node SSR / pack inputs, not delivery bytes).
401
403
  * @param {string} distDir
402
404
  */
403
405
  export function measureDistBudgets(distDir) {
404
406
  let htmlBytes = 0;
405
407
  let clientJsBytes = 0;
406
408
  let artifactBytes = 0;
407
- const stack = [distDir];
409
+ const abs = path.resolve(distDir);
410
+ const stack = [abs];
408
411
  while (stack.length) {
409
412
  const dir = stack.pop();
410
413
  if (!fs.existsSync(dir))
@@ -419,7 +422,8 @@ export function measureDistBudgets(distDir) {
419
422
  artifactBytes += st.size;
420
423
  if (name.endsWith('.html'))
421
424
  htmlBytes += st.size;
422
- if (name.endsWith('.client.js') || name === 'entry-client.js' || name === 'entry-event.js') {
425
+ const rel = path.relative(abs, full).replace(/\\/g, '/');
426
+ if (rel.startsWith('_vmz/pack/') && name.endsWith('.js')) {
423
427
  clientJsBytes += st.size;
424
428
  }
425
429
  }