@vmz/vmz 0.0.2 → 0.0.4

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 (83) hide show
  1. package/README.md +19 -13
  2. package/dist/application-cmd.d.ts +1 -2
  3. package/dist/application-cmd.js +9 -10
  4. package/dist/bundler-adapter.d.ts +2 -3
  5. package/dist/bundler-adapter.js +2 -3
  6. package/dist/cdn-policy.d.ts +178 -0
  7. package/dist/cdn-policy.js +344 -0
  8. package/dist/cli.d.ts +10 -2
  9. package/dist/cli.js +179 -16
  10. package/dist/content-addressed-assets.d.ts +69 -0
  11. package/dist/content-addressed-assets.js +206 -0
  12. package/dist/dev-session.d.ts +2 -2
  13. package/dist/dev-session.js +51 -16
  14. package/dist/document-build.js +1 -2
  15. package/dist/document-check.d.ts +1 -1
  16. package/dist/document-check.js +4 -4
  17. package/dist/document-cmd.d.ts +1 -2
  18. package/dist/document-cmd.js +2 -3
  19. package/dist/document-designs.js +31 -3
  20. package/dist/document-enrich.js +2 -3
  21. package/dist/document-evidence.d.ts +4 -4
  22. package/dist/document-evidence.js +20 -12
  23. package/dist/document-integrate.d.ts +0 -1
  24. package/dist/document-integrate.js +0 -1
  25. package/dist/document-interactive.d.ts +11 -11
  26. package/dist/document-interactive.js +12 -13
  27. package/dist/document-locale.d.ts +1 -1
  28. package/dist/document-locale.js +1 -1
  29. package/dist/document-markdown.d.ts +1 -2
  30. package/dist/document-markdown.js +13 -7
  31. package/dist/document-scan.d.ts +4 -4
  32. package/dist/document-scan.js +4 -4
  33. package/dist/document-schema.d.ts +28 -29
  34. package/dist/document-schema.js +28 -29
  35. package/dist/explain-cmd.js +3 -3
  36. package/dist/index.d.ts +349 -789
  37. package/dist/index.js +109 -85
  38. package/dist/invocation.d.ts +68 -0
  39. package/dist/invocation.js +169 -0
  40. package/dist/locale-check.d.ts +19 -3
  41. package/dist/locale-check.js +130 -6
  42. package/dist/locale-cmd.js +6 -7
  43. package/dist/locale-delivery.d.ts +38 -38
  44. package/dist/locale-delivery.js +39 -40
  45. package/dist/locale-router.d.ts +39 -40
  46. package/dist/locale-router.js +39 -40
  47. package/dist/locale-runtime.d.ts +39 -39
  48. package/dist/locale-runtime.js +44 -45
  49. package/dist/locale-schema.d.ts +1 -2
  50. package/dist/locale-schema.js +1 -2
  51. package/dist/locale-tooling.d.ts +13 -13
  52. package/dist/locale-tooling.js +14 -15
  53. package/dist/log.d.ts +1 -1
  54. package/dist/log.js +1 -1
  55. package/dist/packages.d.ts +1 -2
  56. package/dist/packages.js +1 -2
  57. package/dist/plugin-host.d.ts +11 -3
  58. package/dist/plugin-host.js +21 -5
  59. package/dist/port.d.ts +10 -0
  60. package/dist/port.js +46 -0
  61. package/dist/production-observability.d.ts +286 -0
  62. package/dist/production-observability.js +469 -0
  63. package/dist/production-test-pack.d.ts +158 -0
  64. package/dist/production-test-pack.js +452 -0
  65. package/dist/refactor-cmd.d.ts +1 -1
  66. package/dist/refactor-cmd.js +6 -6
  67. package/dist/release-cmd.d.ts +8 -0
  68. package/dist/release-cmd.js +126 -0
  69. package/dist/release-pack.d.ts +96 -0
  70. package/dist/release-pack.js +337 -0
  71. package/dist/resolve-native-cli.d.ts +14 -0
  72. package/dist/resolve-native-cli.js +84 -0
  73. package/dist/resolve.d.ts +1 -2
  74. package/dist/resolve.js +1 -2
  75. package/dist/site-delivery.d.ts +134 -0
  76. package/dist/site-delivery.js +345 -0
  77. package/dist/static-emit.d.ts +136 -0
  78. package/dist/static-emit.js +463 -0
  79. package/dist/test-cmd.d.ts +2 -2
  80. package/dist/test-cmd.js +37 -17
  81. package/dist/watch-diff.d.ts +1 -1
  82. package/dist/watch-diff.js +1 -1
  83. package/package.json +32 -17
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Node CLI command implementations (N2).
2
+ * Node CLI command implementations .
3
3
  */
4
4
  /**
5
5
  * @param {string[]} argv
@@ -7,9 +7,17 @@
7
7
  export declare function parseArgs(argv: any): {
8
8
  _: any[];
9
9
  };
10
+ export declare function printGlobalHelp(): void;
11
+ export declare function printProjectHelp(): void;
12
+ /** @deprecated use printProjectHelp / printGlobalHelp */
10
13
  export declare function printHelp(): void;
11
14
  /**
12
15
  * @param {string[]} argv
16
+ * @param {{
17
+ * cwd?: string,
18
+ * thisPackageRoot?: string,
19
+ * reexec?: (bin: string, argv: string[]) => Promise<number>,
20
+ * }} [opts]
13
21
  * @returns {Promise<number>}
14
22
  */
15
- export declare function runCli(argv: any): Promise<any>;
23
+ export declare function runCli(argv: any, opts?: {}): Promise<any>;
package/dist/cli.js CHANGED
@@ -1,21 +1,29 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * Node CLI command implementations (N2).
3
+ * Node CLI command implementations .
4
4
  */
5
5
  import { spawn } from 'node:child_process';
6
- import { existsSync } from 'node:fs';
6
+ import { copyFileSync, existsSync } from 'node:fs';
7
7
  import path from 'node:path';
8
- import { HOST_PROTOCOL, createWorkspace, getProtocolVersions } from './index.js';
8
+ import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntimeDist } from './index.js';
9
9
  import { createDevSession } from './dev-session.js';
10
+ import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from './invocation.js';
10
11
  import { log } from './log.js';
12
+ import { findAvailablePort } from './port.js';
11
13
  import { readPackageMeta, resolveWorkspaceDirs } from './resolve.js';
12
14
  import { cmdTest } from './test-cmd.js';
13
15
  import { cmdDocument } from './document-cmd.js';
14
16
  import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
15
17
  import { cmdLocale } from './locale-cmd.js';
18
+ import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
16
19
  import { cmdApplication } from './application-cmd.js';
20
+ import { cmdArtifact } from './release-cmd.js';
17
21
  import { cmdRefactor } from './refactor-cmd.js';
18
22
  import { cmdExplain } from './explain-cmd.js';
23
+ import { resolveNativeVmzCli } from './resolve-native-cli.js';
24
+ import { emitWebStatic } from './static-emit.js';
25
+ import { emitSiteDelivery } from './site-delivery.js';
26
+ import { loadVmzConfig } from './plugin-host.js';
19
27
  /**
20
28
  * @param {string[]} argv
21
29
  */
@@ -61,29 +69,62 @@ export function parseArgs(argv) {
61
69
  }
62
70
  return out;
63
71
  }
64
- export function printHelp() {
65
- console.log(`vmz — Node toolchain host (N-API workspace)
72
+ export function printGlobalHelp() {
73
+ console.log(`vmz — global mode (scaffold only)
74
+
75
+ Install faces: @vmz/core (runtime) · @vmz/vmz (this CLI) · optional @vmz/ui / @vmz/plugin-*
76
+
77
+ Three install modes:
78
+ developer monorepo source (packages/runtimes/vmz) — full CLI
79
+ project app node_modules/@vmz/vmz — full CLI
80
+ global npm/pnpm -g — only new/init/help/version
81
+
82
+ You are in global mode. Pin \`@vmz/vmz\` in the app so check/build/lsp
83
+ use a traceable project install.
84
+
85
+ Usage:
86
+ vmz new|init <dir> Scaffold (native CLI; Node only gates + forwards)
87
+ vmz version Show host + native protocol versions
88
+ vmz help Show this help
89
+
90
+ Project commands:
91
+ pnpm add @vmz/core && pnpm add -D @vmz/vmz
92
+ pnpm exec vmz check
93
+ # or: vmz new my-app && cd my-app && pnpm install
94
+
95
+ If a project \`node_modules/@vmz/vmz\` exists, a global
96
+ \`vmz <cmd>\` re-execs that bin.
97
+ `);
98
+ }
99
+ export function printProjectHelp() {
100
+ console.log(`vmz — Node toolchain host (project / developer mode)
66
101
 
67
102
  Usage:
103
+ vmz new|init <dir> Scaffold a minimal app (native CLI)
68
104
  vmz check [path] Check project via Workspace
69
105
  vmz build [path] [options] Build project via Workspace
70
106
  vmz serve [path] [options] Serve dist (optional --build)
71
107
  vmz dev [path] [options] Long-lived rebuild session (no CLI spawn)
72
108
  vmz format [path] [--check] Format .vmz via N-API (oxc codegen)
73
109
  vmz lint [path] [--deny-warnings] Lint (= check) via N-API
74
- vmz test [path] [options] Native test discover / report (T0+)
75
- vmz document|docs <cmd> Project /documents domain (D0: check)
76
- vmz application <cmd> Application Collection / Mount (M0–M5)
77
- vmz refactor <cmd> DX rename plans / apply (X1)
110
+ vmz test [path] [options] Native test discover / report
111
+ vmz document|docs <cmd> Project /documents domain
112
+ vmz application <cmd> Application Collection / Mount
113
+ vmz artifact <cmd> Release pack / publish / rollback / diff (A3)
114
+ vmz refactor <cmd> DX rename plans / apply
78
115
  vmz explain [style] <target> DX causal explain (style Theme chain)
116
+ vmz lsp [root] [--out-dir] Language server (stdio; native CLI)
117
+ vmz mcp [root] [--out-dir] MCP server (stdio; native CLI)
79
118
  vmz version Show host + native protocol versions
80
119
  vmz help Show this help
81
120
 
82
121
  Options:
83
122
  --out-dir, -o <dir> Output directory (default: dist)
84
123
  --release Release build (build only)
124
+ --profile <name> Delivery profile after build (web-static)
125
+ --origin <url> Site origin for web-static canonical/sitemap
85
126
  --host <host> Listen host (default: 127.0.0.1)
86
- --port <port> Listen port (default: 5173)
127
+ --port <port> Listen port (dev: omit = auto from 5173; set = lock)
87
128
  --poll-ms <ms> Dev watch poll interval (default: 300)
88
129
  --build Build before serve
89
130
  --check Format check-only (format)
@@ -99,21 +140,51 @@ Options:
99
140
  --strict Strict document locale/PageKey coverage (document check)
100
141
  `);
101
142
  }
143
+ /** @deprecated use printProjectHelp / printGlobalHelp */
144
+ export function printHelp() {
145
+ printProjectHelp();
146
+ }
102
147
  /**
103
148
  * @param {string[]} argv
149
+ * @param {{
150
+ * cwd?: string,
151
+ * thisPackageRoot?: string,
152
+ * reexec?: (bin: string, argv: string[]) => Promise<number>,
153
+ * }} [opts]
104
154
  * @returns {Promise<number>}
105
155
  */
106
- export async function runCli(argv) {
156
+ export async function runCli(argv, opts = {}) {
107
157
  const [cmd, ...rest] = argv;
158
+ const inv = getInvocationContext({
159
+ cwd: opts.cwd,
160
+ thisPackageRoot: opts.thisPackageRoot,
161
+ });
108
162
  if (!cmd || cmd === 'help' || cmd === '-h' || cmd === '--help') {
109
- printHelp();
163
+ if (inv.mode === 'global')
164
+ printGlobalHelp();
165
+ else
166
+ printProjectHelp();
110
167
  return 0;
111
168
  }
112
169
  if (cmd === 'version' || cmd === '-V' || cmd === '--version') {
113
170
  return cmdVersion();
114
171
  }
172
+ if (!isGlobalAllowedCommand(cmd)) {
173
+ const gated = await gateGlobalProjectCommand({
174
+ argv,
175
+ cwd: opts.cwd,
176
+ thisPackageRoot: opts.thisPackageRoot,
177
+ reexec: opts.reexec,
178
+ logError: (msg) => log.error(msg),
179
+ });
180
+ if (gated.action === 'exit')
181
+ return gated.code;
182
+ }
115
183
  const args = parseArgs(rest);
116
184
  switch (cmd) {
185
+ case 'new':
186
+ case 'init':
187
+ return cmdNativeForward(cmd, rest);
117
188
  case 'check':
118
189
  return cmdCheck(args);
119
190
  case 'build':
@@ -138,16 +209,57 @@ export async function runCli(argv) {
138
209
  case 'applications':
139
210
  case 'app':
140
211
  return cmdApplication(rest);
212
+ case 'artifact':
213
+ case 'artifacts':
214
+ case 'release':
215
+ return cmdArtifact(rest);
141
216
  case 'refactor':
142
217
  return cmdRefactor(rest);
143
218
  case 'explain':
144
219
  return cmdExplain(rest);
220
+ case 'lsp':
221
+ return cmdNativeForward('lsp', rest);
222
+ case 'mcp':
223
+ return cmdNativeForward('mcp', rest);
145
224
  default:
146
225
  log.error(`unknown command \`${cmd}\``);
147
- printHelp();
226
+ if (inv.mode === 'global')
227
+ printGlobalHelp();
228
+ else
229
+ printProjectHelp();
148
230
  return 1;
149
231
  }
150
232
  }
233
+ /**
234
+ * Forward to the single native `vmz` binary (vmz-tools).
235
+ * Scaffold / stdio servers live in Rust — Node only gates + re-execs.
236
+ *
237
+ * @param {'new' | 'init' | 'lsp' | 'mcp'} sub
238
+ * @param {string[]} argv
239
+ * @returns {Promise<number>}
240
+ */
241
+ function cmdNativeForward(sub, argv) {
242
+ const bin = resolveNativeVmzCli();
243
+ if (!bin) {
244
+ log.error('native `vmz` CLI not found (vmz-tools).');
245
+ log.error('Build: cargo build -p vmz-tools');
246
+ log.error('Or set VMZ_NATIVE to the absolute path of that binary.');
247
+ return Promise.resolve(1);
248
+ }
249
+ return new Promise((resolve) => {
250
+ const child = spawn(bin, [sub, ...argv], { stdio: 'inherit' });
251
+ child.on('error', (err) => {
252
+ log.error(`failed to spawn ${bin}: ${err.message}`);
253
+ resolve(1);
254
+ });
255
+ child.on('exit', (code, signal) => {
256
+ if (signal)
257
+ resolve(1);
258
+ else
259
+ resolve(code ?? 1);
260
+ });
261
+ });
262
+ }
151
263
  function cmdVersion() {
152
264
  const native = getProtocolVersions();
153
265
  console.log(`vmz host ${HOST_PROTOCOL}`);
@@ -217,11 +329,39 @@ async function cmdBuild(args) {
217
329
  });
218
330
  if (code !== 0)
219
331
  return code;
332
+ const localeEmit = emitLocaleRuntimeModules(project, outDir);
333
+ if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
334
+ log.diagnostics(localeEmit.diagnostics ?? []);
335
+ log.error('locale runtime emit failed');
336
+ return 1;
337
+ }
338
+ if (localeEmit.written.length) {
339
+ log.info(`locale runtime emit (${localeEmit.written.length} module(s))`);
340
+ }
220
341
  if (projectHasDocuments(project)) {
221
342
  const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
222
343
  if (!docs.ok)
223
344
  return 1;
224
345
  }
346
+ const cfg = await loadVmzConfig(project);
347
+ if (cfg.delivery) {
348
+ log.info(`site-delivery emit ${outDir}`);
349
+ const site = emitSiteDelivery(outDir, cfg.delivery, {
350
+ siteId: cfg.application?.id || undefined,
351
+ });
352
+ log.info(`site-delivery ok (digest=${String(site.contract.contractDigest).slice(0, 12)}…)`);
353
+ }
354
+ const profile = typeof args.profile === 'string' ? args.profile : '';
355
+ if (profile === 'web-static') {
356
+ const origin = typeof args.origin === 'string' ? args.origin : undefined;
357
+ log.info(`web-static emit ${outDir}`);
358
+ const result = await emitWebStatic(outDir, { origin });
359
+ log.info(`web-static ok (${result.htmlFiles.length} html, ${result.skipped.length} skipped, digest=${result.digest.slice(0, 12)}…)`);
360
+ }
361
+ else if (profile) {
362
+ log.error(`unknown build --profile ${profile} (supported: web-static)`);
363
+ return 1;
364
+ }
225
365
  return 0;
226
366
  }
227
367
  finally {
@@ -244,8 +384,16 @@ async function cmdServe(args) {
244
384
  });
245
385
  const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
246
386
  if (!existsSync(hostJs)) {
247
- log.error(`missing ${hostJs} run \`vmz build\` first (or pass --build)`);
248
- return 1;
387
+ const coreDist = resolveCoreRuntimeDist();
388
+ const src = coreDist ? path.join(coreDist, 'serve-host.mjs') : null;
389
+ if (src && existsSync(src)) {
390
+ copyFileSync(src, hostJs);
391
+ log.info(`materialized ${hostJs} from @vmz/core (release builds omit it)`);
392
+ }
393
+ else {
394
+ log.error(`missing ${hostJs} — run \`vmz build\` (without --release) or ensure @vmz/core is installed`);
395
+ return 1;
396
+ }
249
397
  }
250
398
  const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
251
399
  const port = Number(args.port ?? 5173);
@@ -287,7 +435,22 @@ async function cmdDev(args) {
287
435
  outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
288
436
  });
289
437
  const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
290
- const port = Number(args.port ?? 5173);
438
+ const portLocked = Object.prototype.hasOwnProperty.call(args, 'port');
439
+ let port;
440
+ if (portLocked) {
441
+ port = Number(args.port);
442
+ if (!Number.isFinite(port) || port <= 0) {
443
+ log.error(`invalid --port ${String(args.port)}`);
444
+ return 1;
445
+ }
446
+ }
447
+ else {
448
+ const preferred = 5173;
449
+ port = await findAvailablePort(host, preferred);
450
+ if (port !== preferred) {
451
+ log.info(`port ${preferred} busy → using ${port}`);
452
+ }
453
+ }
291
454
  const pollMs = Number(args['poll-ms'] ?? 300);
292
455
  const ac = new AbortController();
293
456
  const onSig = () => {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * A3: content-addressed assets/<hash> layout for immutable CDN objects.
3
+ * Logical paths stay available for serve/dev; web-static HTML rewrites to hashed URLs.
4
+ * Identical bytes → identical asset path (cross-release / cross-source reuse by digest).
5
+ */
6
+ export declare const CONTENT_ADDRESSED_ASSETS_SCHEMA = "vmz.content_addressed_assets.v0";
7
+ /**
8
+ * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
9
+ * @param {string} distDir
10
+ * @param {{ candidates?: string[], rewriteHtml?: boolean }} [opts]
11
+ */
12
+ export declare function emitContentAddressedAssets(distDir: any, opts?: {}): {
13
+ manifest: {
14
+ schema: string;
15
+ layout: string;
16
+ immutable: boolean;
17
+ objectCount: number;
18
+ objects: any[];
19
+ rewrittenHtml: number;
20
+ };
21
+ assetsDir: string;
22
+ rewrites: {};
23
+ manifestPath: string;
24
+ };
25
+ /**
26
+ * Resolve an immutable object by digest under dist/assets (cross-source reuse).
27
+ * @param {string} distDir
28
+ * @param {string} digest
29
+ * @param {string} [ext]
30
+ */
31
+ export declare function resolveAssetByDigest(distDir: any, digest: any, ext?: string): {
32
+ assetPath: string;
33
+ digest: string;
34
+ bytes: number;
35
+ };
36
+ /**
37
+ * Prove two buffers share one asset path (content-address stability).
38
+ * @param {string} distDir
39
+ * @param {Buffer|string} a
40
+ * @param {Buffer|string} b
41
+ * @param {string} [ext]
42
+ */
43
+ export declare function assertSharedAssetPath(distDir: any, a: any, b: any, ext?: string): {
44
+ ok: boolean;
45
+ reason: string;
46
+ digestA: string;
47
+ digestB: string;
48
+ rel?: undefined;
49
+ assetPath?: undefined;
50
+ digest?: undefined;
51
+ } | {
52
+ ok: boolean;
53
+ reason: string;
54
+ rel: string;
55
+ digestA?: undefined;
56
+ digestB?: undefined;
57
+ assetPath?: undefined;
58
+ digest?: undefined;
59
+ } | {
60
+ ok: boolean;
61
+ assetPath: string;
62
+ digest: string;
63
+ reason?: undefined;
64
+ digestA?: undefined;
65
+ digestB?: undefined;
66
+ rel?: undefined;
67
+ };
68
+ export declare function contentAddressedAssetsDigest(manifest: any): any;
69
+ export declare function sha256Buffer(buf: any): string;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * A3: content-addressed assets/<hash> layout for immutable CDN objects.
3
+ * Logical paths stay available for serve/dev; web-static HTML rewrites to hashed URLs.
4
+ * Identical bytes → identical asset path (cross-release / cross-source reuse by digest).
5
+ */
6
+ // @ts-nocheck
7
+ import crypto from 'node:crypto';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { canonicalJson, sha256Hex } from './release-pack.js';
11
+ export const CONTENT_ADDRESSED_ASSETS_SCHEMA = 'vmz.content_addressed_assets.v0';
12
+ /** Immutable delivery candidates (client-facing bytes). */
13
+ const DEFAULT_CANDIDATES = [
14
+ 'entry-client.js',
15
+ 'entry-event.js',
16
+ 'vmz.css',
17
+ 'vmz-designs.css',
18
+ 'vmz-dom.js',
19
+ 'vmz-runtime.js',
20
+ 'vmz-http.js',
21
+ 'vmz-client-nav.js',
22
+ ];
23
+ /**
24
+ * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
25
+ * @param {string} distDir
26
+ * @param {{ candidates?: string[], rewriteHtml?: boolean }} [opts]
27
+ */
28
+ export function emitContentAddressedAssets(distDir, opts = {}) {
29
+ const abs = path.resolve(distDir);
30
+ if (!fs.existsSync(abs)) {
31
+ throw new Error(`emitContentAddressedAssets: missing dist ${abs}`);
32
+ }
33
+ const assetsDir = path.join(abs, 'assets');
34
+ fs.mkdirSync(assetsDir, { recursive: true });
35
+ const candidates = Array.isArray(opts.candidates) ? opts.candidates : collectCandidates(abs);
36
+ /** @type {Array<Record<string, any>>} */
37
+ const objects = [];
38
+ /** @type {Record<string, string>} */
39
+ const rewrites = {};
40
+ for (const rel of candidates) {
41
+ const logical = String(rel).replace(/\\/g, '/').replace(/^\//, '');
42
+ const src = path.join(abs, ...logical.split('/'));
43
+ if (!fs.existsSync(src) || !fs.statSync(src).isFile())
44
+ continue;
45
+ const buf = fs.readFileSync(src);
46
+ const digest = sha256Hex(buf);
47
+ const ext = path.extname(logical) || '';
48
+ const assetRel = `assets/${digest}${ext}`;
49
+ const dest = path.join(abs, ...assetRel.split('/'));
50
+ if (!fs.existsSync(dest)) {
51
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
52
+ fs.writeFileSync(dest, buf);
53
+ }
54
+ else {
55
+ // Cross-release reuse: identical digest must not be rewritten.
56
+ const existing = sha256Hex(fs.readFileSync(dest));
57
+ if (existing !== digest) {
58
+ throw new Error(`content-address collision at ${assetRel}`);
59
+ }
60
+ }
61
+ objects.push({
62
+ logicalPath: logical,
63
+ assetPath: assetRel,
64
+ digest,
65
+ bytes: buf.length,
66
+ immutable: true,
67
+ });
68
+ rewrites[`/${logical}`] = `/${assetRel}`;
69
+ rewrites[logical] = assetRel;
70
+ }
71
+ objects.sort((a, b) => (a.logicalPath < b.logicalPath ? -1 : a.logicalPath > b.logicalPath ? 1 : 0));
72
+ let rewrittenHtml = 0;
73
+ if (opts.rewriteHtml !== false) {
74
+ rewrittenHtml = rewriteHtmlReferences(abs, rewrites);
75
+ }
76
+ const manifest = {
77
+ schema: CONTENT_ADDRESSED_ASSETS_SCHEMA,
78
+ layout: 'assets/<sha256>.<ext>',
79
+ immutable: true,
80
+ objectCount: objects.length,
81
+ objects,
82
+ rewrittenHtml,
83
+ };
84
+ manifest.manifestDigest = sha256Hex(canonicalJson({ ...manifest, manifestDigest: undefined }));
85
+ const vmzDir = path.join(abs, '_vmz');
86
+ fs.mkdirSync(vmzDir, { recursive: true });
87
+ const outPath = path.join(vmzDir, 'content-addressed-assets.json');
88
+ fs.writeFileSync(outPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
89
+ return { manifest, assetsDir, rewrites, manifestPath: outPath };
90
+ }
91
+ /**
92
+ * Resolve an immutable object by digest under dist/assets (cross-source reuse).
93
+ * @param {string} distDir
94
+ * @param {string} digest
95
+ * @param {string} [ext]
96
+ */
97
+ export function resolveAssetByDigest(distDir, digest, ext = '') {
98
+ const d = String(digest || '').trim();
99
+ if (!/^[a-f0-9]{64}$/i.test(d))
100
+ return null;
101
+ const suffix = ext && !ext.startsWith('.') ? `.${ext}` : ext;
102
+ const rel = `assets/${d}${suffix}`;
103
+ const abs = path.join(distDir, ...rel.split('/'));
104
+ if (!fs.existsSync(abs))
105
+ return null;
106
+ return { assetPath: rel, digest: d.toLowerCase(), bytes: fs.statSync(abs).size };
107
+ }
108
+ /**
109
+ * Prove two buffers share one asset path (content-address stability).
110
+ * @param {string} distDir
111
+ * @param {Buffer|string} a
112
+ * @param {Buffer|string} b
113
+ * @param {string} [ext]
114
+ */
115
+ export function assertSharedAssetPath(distDir, a, b, ext = '.js') {
116
+ const da = sha256Hex(a);
117
+ const db = sha256Hex(b);
118
+ if (da !== db) {
119
+ return { ok: false, reason: 'digests differ', digestA: da, digestB: db };
120
+ }
121
+ const assetsDir = path.join(distDir, 'assets');
122
+ fs.mkdirSync(assetsDir, { recursive: true });
123
+ const rel = `assets/${da}${ext}`;
124
+ const dest = path.join(distDir, ...rel.split('/'));
125
+ fs.writeFileSync(dest, typeof a === 'string' ? Buffer.from(a) : a);
126
+ // Second write of identical bytes must be reuse, not fork.
127
+ fs.writeFileSync(dest, typeof b === 'string' ? Buffer.from(b) : b);
128
+ const again = resolveAssetByDigest(distDir, da, ext);
129
+ if (!again || again.assetPath !== rel) {
130
+ return { ok: false, reason: 'resolve missed shared path', rel };
131
+ }
132
+ return { ok: true, assetPath: rel, digest: da };
133
+ }
134
+ function collectCandidates(distDir) {
135
+ /** @type {string[]} */
136
+ const out = [];
137
+ for (const name of DEFAULT_CANDIDATES) {
138
+ if (fs.existsSync(path.join(distDir, name)))
139
+ out.push(name);
140
+ }
141
+ // Include top-level *.client.js and pages/**/*.client.js referenced by resume.
142
+ walk(distDir, distDir, (rel) => {
143
+ if (/\.client\.js$/i.test(rel))
144
+ out.push(rel);
145
+ });
146
+ return [...new Set(out)].sort();
147
+ }
148
+ function walk(root, dir, onFile) {
149
+ for (const name of fs.readdirSync(dir)) {
150
+ if (name === 'assets' || name === '_vmz' || name === 'node_modules')
151
+ continue;
152
+ const full = path.join(dir, name);
153
+ const st = fs.statSync(full);
154
+ if (st.isDirectory())
155
+ walk(root, full, onFile);
156
+ else
157
+ onFile(path.relative(root, full).replace(/\\/g, '/'));
158
+ }
159
+ }
160
+ /**
161
+ * @param {string} distDir
162
+ * @param {Record<string, string>} rewrites map `/logical` → `/assets/hash.ext`
163
+ */
164
+ function rewriteHtmlReferences(distDir, rewrites) {
165
+ const pairs = Object.entries(rewrites)
166
+ .filter(([from]) => from.startsWith('/'))
167
+ .sort((a, b) => b[0].length - a[0].length);
168
+ if (!pairs.length)
169
+ return 0;
170
+ let count = 0;
171
+ walkHtml(distDir, (file) => {
172
+ let text = fs.readFileSync(file, 'utf8');
173
+ let next = text;
174
+ for (const [from, to] of pairs) {
175
+ // href="/x" src="/x" and unquoted variants in attributes
176
+ next = next.split(from).join(to);
177
+ }
178
+ if (next !== text) {
179
+ fs.writeFileSync(file, next, 'utf8');
180
+ count += 1;
181
+ }
182
+ });
183
+ return count;
184
+ }
185
+ function walkHtml(dir, onFile) {
186
+ const stack = [dir];
187
+ while (stack.length) {
188
+ const cur = stack.pop();
189
+ for (const name of fs.readdirSync(cur)) {
190
+ if (name === 'assets' || name === '_vmz' || name === 'node_modules')
191
+ continue;
192
+ const full = path.join(cur, name);
193
+ const st = fs.statSync(full);
194
+ if (st.isDirectory())
195
+ stack.push(full);
196
+ else if (name.endsWith('.html'))
197
+ onFile(full);
198
+ }
199
+ }
200
+ }
201
+ export function contentAddressedAssetsDigest(manifest) {
202
+ return manifest.manifestDigest || sha256Hex(canonicalJson({ ...manifest, manifestDigest: undefined }));
203
+ }
204
+ export function sha256Buffer(buf) {
205
+ return crypto.createHash('sha256').update(buf).digest('hex');
206
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Long-lived Node dev session (N2/N4).
2
+ * Long-lived Node dev session (/session).
3
3
  *
4
4
  * Rebuilds go through the N-API `Workspace` — never spawn `cargo` / `vmz-tools`.
5
- * N4: only dirty leaves are marked; Workspace emits affected deployment units.
5
+ * session: only dirty leaves are marked; Workspace emits affected deployment units.
6
6
  */
7
7
  /**
8
8
  * @typedef {object} DevSessionOptions