@vmz/vmz 0.3.13 → 0.3.15

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.
@@ -21,6 +21,8 @@ function rewriteFlatDeliveryImports(text) {
21
21
  ['../browser/direct-host-box.js', './direct-host-box.js'],
22
22
  ['../browser/unknown-component.js', './unknown-component.js'],
23
23
  ['../shared/dom-attr-normalize.js', './dom-attr-normalize.js'],
24
+ ['../shared/html-digest.js', './html-digest.js'],
25
+ ['../shared/row-key.js', './row-key.js'],
24
26
  ['../browser/client-nav.js', './vmz-client-nav.js'],
25
27
  ['../ssr/dom-ssr.js', './dom-ssr.js'],
26
28
  ['../faces/server.js', './vmz-runtime.js'],
@@ -34,6 +36,11 @@ function rewriteFlatDeliveryImports(text) {
34
36
  }
35
37
  function rewriteNestedSsrDelivery(text) {
36
38
  let out = text;
39
+ // `shared/` is flattened to the delivery root, so nested SSR files must
40
+ // reach the root copy one level up.
41
+ out = replaceImportPath(out, '../shared/dom-attr-normalize.js', '../dom-attr-normalize.js');
42
+ out = replaceImportPath(out, '../shared/html-digest.js', '../html-digest.js');
43
+ out = replaceImportPath(out, '../shared/row-key.js', '../row-key.js');
37
44
  for (const [from, to] of [
38
45
  ['../browser/dom-core.js', '../dom-core.js'],
39
46
  ['../browser/direct-host-box.js', '../direct-host-box.js'],
@@ -45,6 +52,8 @@ function rewriteNestedSsrDelivery(text) {
45
52
  }
46
53
  function rewriteNestedBrowserDelivery(text) {
47
54
  let out = replaceImportPath(text, '../../shared/dom-attr-normalize.js', '../../dom-attr-normalize.js');
55
+ out = replaceImportPath(out, '../../shared/html-digest.js', '../../html-digest.js');
56
+ out = replaceImportPath(out, '../../shared/row-key.js', '../../row-key.js');
48
57
  // `dom-ssr.js` 只交付在根(flat barrel),nested `ssr/` 下没有它:
49
58
  // island resume 的动态边必须指向根级产物。
50
59
  out = replaceImportPath(out, '../../ssr/dom-ssr.js', '../../dom-ssr.js');
@@ -39,6 +39,12 @@ export interface PackRequest {
39
39
  entries: PackRequestEntry[];
40
40
  target: PackTarget;
41
41
  root: string;
42
+ /**
43
+ * Output tree root (absolute). Modules under it are labelled relative to it,
44
+ * so chunk ids stay identical when `--out-dir` moves or the project is built
45
+ * on another machine. Source-tree modules stay relative to `root`.
46
+ */
47
+ outRoot: string;
42
48
  uses: PackRequestUse[];
43
49
  publicBase: string;
44
50
  /**
@@ -89,6 +95,10 @@ export interface PackPlan {
89
95
  entryFiles: PackEntryFile[];
90
96
  dynamicFiles: PackDynamicFile[];
91
97
  manifestJson: string;
98
+ /** Content digest of this pack input (graph structure + source bytes + emit options). */
99
+ inputDigest: string;
100
+ /** Graph-structure digest; any change invalidates every cached chunk. */
101
+ graphDigest: string;
92
102
  }
93
103
  export interface PackDiagnostic {
94
104
  code: string;
@@ -126,6 +136,8 @@ export interface BuildPackRequestInput {
126
136
  entries: Record<string, string>;
127
137
  target: PackTarget;
128
138
  root: string;
139
+ /** Output tree root: chunk id label base for emitted modules. */
140
+ outRoot: string;
129
141
  uses: PackRequestUse[];
130
142
  publicBase: string;
131
143
  /** Emitted absolute path → source absolute path (projection provenance). */
@@ -122,7 +122,7 @@ function parseDynamicFile(value, where) {
122
122
  function parsePackPlan(value) {
123
123
  if (!isPlainObject(value))
124
124
  throw responseShape('response.plan must be an object');
125
- rejectUnknownKeys(value, ['artifacts', 'styles', 'assets', 'entryFiles', 'dynamicFiles', 'manifestJson'], 'response.plan');
125
+ rejectUnknownKeys(value, ['artifacts', 'styles', 'assets', 'entryFiles', 'dynamicFiles', 'manifestJson', 'inputDigest', 'graphDigest'], 'response.plan');
126
126
  if (!Array.isArray(value.artifacts))
127
127
  throw responseShape('response.plan.artifacts must be an array');
128
128
  if (!Array.isArray(value.styles))
@@ -140,6 +140,8 @@ function parsePackPlan(value) {
140
140
  entryFiles: value.entryFiles.map((e, i) => parseEntryFile(e, `response.plan.entryFiles[${i}]`)),
141
141
  dynamicFiles: value.dynamicFiles.map((d, i) => parseDynamicFile(d, `response.plan.dynamicFiles[${i}]`)),
142
142
  manifestJson: requireString(value.manifestJson, 'response.plan.manifestJson'),
143
+ inputDigest: requireString(value.inputDigest, 'response.plan.inputDigest'),
144
+ graphDigest: requireString(value.graphDigest, 'response.plan.graphDigest'),
143
145
  };
144
146
  }
145
147
  /**
@@ -199,6 +201,11 @@ export function buildPackRequest(input) {
199
201
  throw requestInvalid('root must be a non-empty string');
200
202
  if (!path.isAbsolute(input.root))
201
203
  throw requestInvalid(`root must be an absolute path: ${input.root}`);
204
+ if (typeof input.outRoot !== 'string' || !input.outRoot) {
205
+ throw requestInvalid('outRoot must be a non-empty string');
206
+ }
207
+ if (!path.isAbsolute(input.outRoot))
208
+ throw requestInvalid(`outRoot must be an absolute path: ${input.outRoot}`);
202
209
  if (typeof input.publicBase !== 'string' || !input.publicBase) {
203
210
  throw requestInvalid('publicBase must be a non-empty string');
204
211
  }
@@ -249,7 +256,7 @@ export function buildPackRequest(input) {
249
256
  if (!Object.keys(origins).length)
250
257
  origins = undefined;
251
258
  }
252
- const request = { entries, target: input.target, root: input.root, uses, publicBase: input.publicBase, ...(origins ? { origins } : {}) };
259
+ const request = { entries, target: input.target, root: input.root, outRoot: input.outRoot, uses, publicBase: input.publicBase, ...(origins ? { origins } : {}) };
253
260
  return JSON.stringify(request);
254
261
  }
255
262
  function resolveBinding(opts, fn) {
@@ -10,11 +10,14 @@
10
10
  * `execution: 'full'` + `exports: { kind: 'all' }` (script-tag load,
11
11
  * whole-module execution).
12
12
  *
13
- * SSR/Node is a separate graph: every deployment unit's `clientEntry` is a
14
- * Node entry (logical = `chunkId`), packed under `_vmz/pack-node/` and indexed
15
- * by `_vmz/pack-node-manifest.json`. Node consumers never fall back to the
16
- * unpacked root `${chunkId}.client.js`; browser bootstrap modules are not Node
17
- * entries because they bind `dom.browser` / `document`.
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`.
18
21
  *
19
22
  * No vendor materialization, no specifier lowering, and no second hashing
20
23
  * pass happen here: `packWrite` emits the final browser JS set under
@@ -37,6 +40,17 @@ export declare const PACK_PUBLIC_BASE = "/";
37
40
  * specifiers keep resolving against the emitted tree.
38
41
  */
39
42
  export declare function deploymentUnitOrigins(absOut: string, ir: DeploymentIr): Record<string, string>;
43
+ /** 一个 deployment 逻辑 chunkId 在浏览器图里的落点(未进图时字段为 `null`)。 */
44
+ export interface PackUnitChunk {
45
+ /** 运行时逻辑 chunk id(`vmz-deployment.json` 的 `units[].chunkId`)。 */
46
+ logical: string;
47
+ /** 该单元的产物客户端文件(输出树相对路径)。 */
48
+ clientEntry: string;
49
+ /** 承载该文件的 chunk id,未进客户端图时为 `null`。 */
50
+ chunkId: string | null;
51
+ /** 承载该文件的 chunk 文件名,未进客户端图时为 `null`。 */
52
+ fileName: string | null;
53
+ }
40
54
  /**
41
55
  * Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
42
56
  * Always refresh from `@vmz/core` when present so SSR/runtime fixes are not sticky in outDir.
@@ -81,12 +95,15 @@ export declare function packFromDeploymentIr(outDir: string, opts?: PackFromDepl
81
95
  styles: unknown[];
82
96
  assets: unknown[];
83
97
  chunks: unknown[];
98
+ unitChunks: PackUnitChunk[];
84
99
  chunkCount: number;
85
100
  styleCount: number;
86
101
  entryCount: number;
87
102
  minify: string;
88
103
  treeShake: string;
89
104
  sourceMaps: string;
105
+ packInputDigest: string;
106
+ packGraphDigest: string;
90
107
  nativeManifest: unknown;
91
108
  packDigest?: string;
92
109
  };
@@ -104,11 +121,11 @@ interface PackSsrFromDeploymentIrOpts {
104
121
  }
105
122
  /**
106
123
  * Pack the SSR/Node graph via the Rust `vmz-pack` binding and write
107
- * `_vmz/pack-node-manifest.json` (v0). Entries are the deployment units'
108
- * compiled client modules (`clientEntry`, empty → mirrors `vmz-artifacts`
109
- * `component_entries`: `${chunkId}.client.js`). Node consumers only read
110
- * `default`, but the import itself is evaluated, hence `side-effects` +
111
- * named default rather than `all` / `full` / `unconsumed`.
124
+ * `_vmz/pack-node-manifest.json` (v0). Entries are the node-reachable
125
+ * deployment units' compiled client modules (`clientEntry`, empty → mirrors
126
+ * `vmz-artifacts` `component_entries`: `${chunkId}.client.js`). Node consumers
127
+ * only read `default`, but the import itself is evaluated, hence
128
+ * `side-effects` + named default rather than `all` / `full` / `unconsumed`.
112
129
  */
113
130
  export declare function packSsrFromDeploymentIr(outDir: string, opts?: PackSsrFromDeploymentIrOpts): {
114
131
  manifest: {
@@ -124,6 +141,8 @@ export declare function packSsrFromDeploymentIr(outDir: string, opts?: PackSsrFr
124
141
  chunks: unknown[];
125
142
  chunkCount: number;
126
143
  entryCount: number;
144
+ packInputDigest: string;
145
+ packGraphDigest: string;
127
146
  nativeManifest: unknown;
128
147
  packDigest?: string;
129
148
  };
@@ -10,11 +10,14 @@
10
10
  * `execution: 'full'` + `exports: { kind: 'all' }` (script-tag load,
11
11
  * whole-module execution).
12
12
  *
13
- * SSR/Node is a separate graph: every deployment unit's `clientEntry` is a
14
- * Node entry (logical = `chunkId`), packed under `_vmz/pack-node/` and indexed
15
- * by `_vmz/pack-node-manifest.json`. Node consumers never fall back to the
16
- * unpacked root `${chunkId}.client.js`; browser bootstrap modules are not Node
17
- * entries because they bind `dom.browser` / `document`.
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`.
18
21
  *
19
22
  * No vendor materialization, no specifier lowering, and no second hashing
20
23
  * pass happen here: `packWrite` emits the final browser JS set under
@@ -22,7 +25,7 @@
22
25
  * entries through `packEntryHref()` against this manifest.
23
26
  */
24
27
  import crypto from 'node:crypto';
25
- import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
28
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
26
29
  import path from 'node:path';
27
30
  import { loadDeploymentIr } from './bundler-adapter.js';
28
31
  import { buildPackRequest, runPackWrite } from './pack-native.js';
@@ -59,6 +62,43 @@ export function deploymentUnitOrigins(absOut, ir) {
59
62
  }
60
63
  /** Bootstrap modules that are browser pack entries when present in outDir. */
61
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
+ }
62
102
  /**
63
103
  * Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
64
104
  * Always refresh from `@vmz/core` when present so SSR/runtime fixes are not sticky in outDir.
@@ -107,11 +147,18 @@ export function packFromDeploymentIr(outDir, opts = {}) {
107
147
  // Browser graph entries are the bootstrap modules; page/island units join
108
148
  // through the bootstrap dynamic-import tables and need projection provenance.
109
149
  const origins = deploymentUnitOrigins(absOut, loadDeploymentIr(absOut));
110
- const requestJson = buildPackRequest({ entries, target: 'browser', root, uses, publicBase: PACK_PUBLIC_BASE, origins });
150
+ const requestJson = buildPackRequest({
151
+ entries,
152
+ target: 'browser',
153
+ root,
154
+ outRoot: absOut,
155
+ uses,
156
+ publicBase: PACK_PUBLIC_BASE,
157
+ origins,
158
+ });
111
159
  const packOutDir = path.join(absOut, ...PACK_OUTPUT_DIR_REL.split('/'));
112
- // 重建不得残留上一轮 pack 的过期 chunk:manifest/磁盘闭包是交付合同,
113
- // 写盘前整体清空旧输出目录(`packWrite` 只增不删)。
114
- rmSync(packOutDir, { recursive: true, force: true });
160
+ // 不再整体清空输出目录:`packWrite` 按内容摘要复用未变 chunk,并在写盘后
161
+ // 逐文件清理不再属于产物闭包的旧 chunk / map / css / assets(不留孤儿)。
115
162
  const { plan } = runPackWrite(requestJson, packOutDir, opts.binding ? { binding: opts.binding } : {});
116
163
  const ir = loadDeploymentIr(absOut);
117
164
  let nativeManifest;
@@ -151,18 +198,27 @@ export function packFromDeploymentIr(outDir, opts = {}) {
151
198
  styles: plan.styles,
152
199
  assets: plan.assets,
153
200
  chunks,
201
+ unitChunks: resolveUnitChunks(absOut, ir.units || [], plan.artifacts),
154
202
  chunkCount: chunks.length,
155
203
  styleCount: plan.styles.length,
156
204
  entryCount: plan.entryFiles.length,
157
- // `vmz-pack` runs no compressor: chunk bytes are the linked output verbatim.
158
- minify: 'none',
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',
159
209
  // `link` active fixed-point pruning really ran — bootstrap entries
160
210
  // declare full execution, internal modules are pruned to reachable exports.
161
211
  treeShake: 'vmz-pack-link-pruning',
162
- // Chunk source maps are line-level v3: every assembled line maps back
163
- // to its module line (`<packOut>/<fileName>.map`, `sourceMapFileName`
164
- // on the wire); token-level mappings and `names` are not implemented.
165
- sourceMaps: 'chunk-v3-line-mappings',
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,
166
222
  nativeManifest,
167
223
  };
168
224
  body.packDigest = sha256Hex(stableStringify({ ...body }));
@@ -177,18 +233,61 @@ export const PACK_NODE_MANIFEST_SCHEMA = 'vmz.pack.node-manifest.v0';
177
233
  export const PACK_NODE_OUTPUT_DIR_REL = '_vmz/pack-node';
178
234
  /** Node pack manifest (relative to outDir); `entryFiles` maps logical → chunk file. */
179
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
+ }
180
275
  /**
181
276
  * Pack the SSR/Node graph via the Rust `vmz-pack` binding and write
182
- * `_vmz/pack-node-manifest.json` (v0). Entries are the deployment units'
183
- * compiled client modules (`clientEntry`, empty → mirrors `vmz-artifacts`
184
- * `component_entries`: `${chunkId}.client.js`). Node consumers only read
185
- * `default`, but the import itself is evaluated, hence `side-effects` +
186
- * named default rather than `all` / `full` / `unconsumed`.
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`.
187
282
  */
188
283
  export function packSsrFromDeploymentIr(outDir, opts = {}) {
189
284
  const absOut = path.resolve(outDir);
190
285
  const root = opts.projectRoot ? path.resolve(opts.projectRoot) : path.dirname(absOut);
191
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);
192
291
  const entries = {};
193
292
  const uses = [];
194
293
  // Deployment IR may list the same component unit more than once (one entry
@@ -200,6 +299,8 @@ export function packSsrFromDeploymentIr(outDir, opts = {}) {
200
299
  const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
201
300
  if (!chunkId)
202
301
  throw new Error(`packSsrFromDeploymentIr: deployment unit has an empty chunkId in ${absOut}`);
302
+ if (!nodeReachable.has(chunkId))
303
+ continue;
203
304
  // `vmz-artifacts` derives the component entry as `${chunkId}.client.js`
204
305
  // when `clientEntry` is empty. Decide it here so the pack request stays
205
306
  // explicit, then require the file — a missing entry is never skipped.
@@ -220,13 +321,20 @@ export function packSsrFromDeploymentIr(outDir, opts = {}) {
220
321
  uses.push({ logical: chunkId, execution: 'side-effects', exports: { kind: 'named', names: ['default'] } });
221
322
  }
222
323
  if (!Object.keys(entries).length) {
223
- throw new Error(`packSsrFromDeploymentIr: deployment IR has no units in ${absOut}`);
324
+ throw new Error(`packSsrFromDeploymentIr: deployment IR has no node-reachable units in ${absOut}`);
224
325
  }
225
326
  const origins = deploymentUnitOrigins(absOut, ir);
226
- const requestJson = buildPackRequest({ entries, target: 'node', root, uses, publicBase: PACK_PUBLIC_BASE, origins });
327
+ const requestJson = buildPackRequest({
328
+ entries,
329
+ target: 'node',
330
+ root,
331
+ outRoot: absOut,
332
+ uses,
333
+ publicBase: PACK_PUBLIC_BASE,
334
+ origins,
335
+ });
227
336
  const packOutDir = path.join(absOut, ...PACK_NODE_OUTPUT_DIR_REL.split('/'));
228
- // 与浏览器 pack 同口径:写盘前清空旧 Node chunk 目录,闭包合同不接受残留。
229
- rmSync(packOutDir, { recursive: true, force: true });
337
+ // 与浏览器 pack 同口径:不清空目录,由 `packWrite` 复用未变 chunk 并清理孤儿。
230
338
  const { plan } = runPackWrite(requestJson, packOutDir, opts.binding ? { binding: opts.binding } : {});
231
339
  let nativeManifest;
232
340
  try {
@@ -262,6 +370,9 @@ export function packSsrFromDeploymentIr(outDir, opts = {}) {
262
370
  chunks,
263
371
  chunkCount: chunks.length,
264
372
  entryCount: plan.entryFiles.length,
373
+ // 同浏览器 pack:输入/图结构摘要来自 Rust 侧内容摘要判定。
374
+ packInputDigest: plan.inputDigest,
375
+ packGraphDigest: plan.graphDigest,
265
376
  nativeManifest,
266
377
  };
267
378
  body.packDigest = sha256Hex(stableStringify({ ...body }));
@@ -10,6 +10,9 @@
10
10
  "vmz::template::illegal_directive": "Illegal or unsupported directive in `<template>`: {detail}",
11
11
  "vmz::template::parse_failed": "Failed to parse `<template>`: {detail}",
12
12
  "vmz::template::invalid_expr": "Invalid template expression: {detail}",
13
+
14
+ "vmz::template::v_model_invalid_target": "`v-model` target must be an assignable identifier or member expression: `{expr}`",
15
+ "vmz::template::v_model_on_file_input": "`v-model` cannot bind an `<input type=\"file\">`",
13
16
  "vmz::template::each_missing_key": "`v-for` on `<{tag}>` should declare a `:key`",
14
17
  "vmz::template::each_static_key": "`v-for` on `<{tag}>` must not use a static key `{key}`",
15
18
  "vmz::template::each_literal_key": "`v-for` on `<{tag}>` must not use a literal key `{key}`",
@@ -6,6 +6,9 @@
6
6
  "vmz::template::illegal_directive": "`<template>` 中存在非法或不支持的指令:{detail}",
7
7
  "vmz::template::parse_failed": "解析 `<template>` 失败:{detail}",
8
8
  "vmz::template::invalid_expr": "模板表达式无效:{detail}",
9
+
10
+ "vmz::template::v_model_invalid_target": "`v-model` 的目标必须是可赋值的标识符或成员表达式:`{expr}`",
11
+ "vmz::template::v_model_on_file_input": "`v-model` 不能绑定到 `<input type=\"file\">`",
9
12
  "vmz::template::each_missing_key": "`<{tag}>` 上的 `v-for` 应声明 `:key`",
10
13
  "vmz::template::each_static_key": "`<{tag}>` 上的 `v-for` 不得使用静态 key `{key}`",
11
14
  "vmz::template::each_literal_key": "`<{tag}>` 上的 `v-for` 不得使用字面量 key `{key}`",
package/package.json CHANGED
@@ -3,23 +3,23 @@
3
3
  "vmz": "./bin/vmz.js"
4
4
  },
5
5
  "dependencies": {
6
- "@vmz/commander": "0.3.13",
7
- "@vmz/core": "0.3.13",
8
- "@vmz/diagnostic": "0.3.13",
9
- "@vmz/plugin": "0.3.13",
10
- "@vmz/protocol": "0.3.13"
6
+ "@vmz/commander": "0.3.15",
7
+ "@vmz/core": "0.3.15",
8
+ "@vmz/diagnostic": "0.3.15",
9
+ "@vmz/plugin": "0.3.15",
10
+ "@vmz/protocol": "0.3.15"
11
11
  },
12
12
  "description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
13
13
  "devDependencies": {
14
14
  "@types/node": "^22.15.0",
15
- "@vmz/plugin": "0.3.13",
16
- "@vmz/plugin-katex": "0.3.13",
17
- "@vmz/plugin-markdown-it": "0.3.13",
18
- "@vmz/plugin-shiki": "0.3.13",
19
- "@vmz/test": "0.3.13",
15
+ "@vmz/plugin": "0.3.15",
16
+ "@vmz/plugin-katex": "0.3.15",
17
+ "@vmz/plugin-markdown-it": "0.3.15",
18
+ "@vmz/plugin-shiki": "0.3.15",
19
+ "@vmz/test": "0.3.15",
20
20
  "linkedom": "^0.18.13",
21
21
  "typescript": "^5.8.3",
22
- "vmz-plugin-conformance": "0.3.13"
22
+ "vmz-plugin-conformance": "0.3.15"
23
23
  },
24
24
  "engines": {
25
25
  "node": ">=24.12.0"
@@ -80,16 +80,16 @@
80
80
  "main": "./dist/index.js",
81
81
  "name": "@vmz/vmz",
82
82
  "optionalDependencies": {
83
- "@vmz/vmz-darwin-arm64": "0.3.13",
84
- "@vmz/vmz-darwin-x64": "0.3.13",
85
- "@vmz/vmz-linux-arm64": "0.3.13",
86
- "@vmz/vmz-linux-x64": "0.3.13",
87
- "@vmz/vmz-win32-arm64": "0.3.13",
88
- "@vmz/vmz-win32-x64": "0.3.13"
83
+ "@vmz/vmz-darwin-arm64": "0.3.15",
84
+ "@vmz/vmz-darwin-x64": "0.3.15",
85
+ "@vmz/vmz-linux-arm64": "0.3.15",
86
+ "@vmz/vmz-linux-x64": "0.3.15",
87
+ "@vmz/vmz-win32-arm64": "0.3.15",
88
+ "@vmz/vmz-win32-x64": "0.3.15"
89
89
  },
90
90
  "peerDependencies": {
91
- "@vmz/plugin-markdown-it": "0.3.13",
92
- "@vmz/test": "0.3.13",
91
+ "@vmz/plugin-markdown-it": "0.3.15",
92
+ "@vmz/test": "0.3.15",
93
93
  "typescript": "^5.8.3"
94
94
  },
95
95
  "peerDependenciesMeta": {
@@ -122,5 +122,5 @@
122
122
  },
123
123
  "type": "module",
124
124
  "types": "./dist/index.d.ts",
125
- "version": "0.3.13"
125
+ "version": "0.3.15"
126
126
  }