@vmz/vmz 0.0.3 → 0.1.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.
Files changed (52) hide show
  1. package/README.md +6 -4
  2. package/dist/build-assemble.d.ts +52 -0
  3. package/dist/build-assemble.js +191 -0
  4. package/dist/cdn-policy.d.ts +196 -0
  5. package/dist/cdn-policy.js +443 -0
  6. package/dist/cli.js +152 -11
  7. package/dist/content-addressed-assets.d.ts +69 -0
  8. package/dist/content-addressed-assets.js +206 -0
  9. package/dist/delivery-profile.d.ts +74 -0
  10. package/dist/delivery-profile.js +279 -0
  11. package/dist/dev-session.js +49 -13
  12. package/dist/document-build.js +9 -4
  13. package/dist/document-designs.js +30 -2
  14. package/dist/document-enrich.js +8 -0
  15. package/dist/embedded-packaging.d.ts +22 -0
  16. package/dist/embedded-packaging.js +113 -0
  17. package/dist/index.d.ts +19 -3
  18. package/dist/index.js +35 -15
  19. package/dist/invocation.d.ts +8 -31
  20. package/dist/invocation.js +12 -33
  21. package/dist/locale-check.d.ts +16 -0
  22. package/dist/locale-check.js +131 -3
  23. package/dist/locale-cmd.js +2 -2
  24. package/dist/locale-route-emit.d.ts +34 -0
  25. package/dist/locale-route-emit.js +134 -0
  26. package/dist/locale-router.d.ts +20 -0
  27. package/dist/locale-router.js +68 -0
  28. package/dist/log.d.ts +2 -2
  29. package/dist/log.js +11 -3
  30. package/dist/pack.d.ts +40 -0
  31. package/dist/pack.js +108 -0
  32. package/dist/plugin-host.d.ts +10 -1
  33. package/dist/plugin-host.js +19 -2
  34. package/dist/port.d.ts +10 -0
  35. package/dist/port.js +46 -0
  36. package/dist/production-observability.d.ts +286 -0
  37. package/dist/production-observability.js +469 -0
  38. package/dist/production-test-pack.d.ts +144 -0
  39. package/dist/production-test-pack.js +447 -0
  40. package/dist/release-cmd.d.ts +8 -0
  41. package/dist/release-cmd.js +126 -0
  42. package/dist/release-pack.d.ts +96 -0
  43. package/dist/release-pack.js +346 -0
  44. package/dist/server-artifact.d.ts +140 -0
  45. package/dist/server-artifact.js +205 -0
  46. package/dist/server-language-backend.d.ts +89 -0
  47. package/dist/server-language-backend.js +121 -0
  48. package/dist/site-delivery.d.ts +134 -0
  49. package/dist/site-delivery.js +345 -0
  50. package/dist/static-emit.d.ts +145 -0
  51. package/dist/static-emit.js +577 -0
  52. package/package.json +13 -13
package/dist/cli.js CHANGED
@@ -9,15 +9,23 @@ import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntime
9
9
  import { createDevSession } from './dev-session.js';
10
10
  import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from './invocation.js';
11
11
  import { log } from './log.js';
12
+ import { findAvailablePort } from './port.js';
12
13
  import { readPackageMeta, resolveWorkspaceDirs } from './resolve.js';
13
14
  import { cmdTest } from './test-cmd.js';
14
15
  import { cmdDocument } from './document-cmd.js';
15
16
  import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
16
17
  import { cmdLocale } from './locale-cmd.js';
18
+ import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
19
+ import { emitLocaleRouteRealization } from './locale-route-emit.js';
17
20
  import { cmdApplication } from './application-cmd.js';
21
+ import { cmdArtifact } from './release-cmd.js';
18
22
  import { cmdRefactor } from './refactor-cmd.js';
19
23
  import { cmdExplain } from './explain-cmd.js';
20
24
  import { resolveNativeVmzCli } from './resolve-native-cli.js';
25
+ import { loadVmzConfig } from './plugin-host.js';
26
+ import { normalizeDeliveryAuthoring, selectBuildProfile } from './delivery-profile.js';
27
+ import { packFromDeploymentIr } from './pack.js';
28
+ import { assembleDelivery, emitBuildProof } from './build-assemble.js';
21
29
  /**
22
30
  * @param {string[]} argv
23
31
  */
@@ -66,8 +74,11 @@ export function parseArgs(argv) {
66
74
  export function printGlobalHelp() {
67
75
  console.log(`vmz — global mode (scaffold only)
68
76
 
77
+ Install faces: @vmz/core (runtime) · @vmz/vmz (this CLI) · optional @vmz/ui / @vmz/plugin-*
78
+
79
+ Three install modes:
69
80
  developer monorepo source (packages/runtimes/vmz) — full CLI
70
- project app node_modules/@vmz/vmz (or vmz) — full CLI
81
+ project app node_modules/@vmz/vmz — full CLI
71
82
  global npm/pnpm -g — only new/init/help/version
72
83
 
73
84
  You are in global mode. Pin \`@vmz/vmz\` in the app so check/build/lsp
@@ -79,11 +90,11 @@ Usage:
79
90
  vmz help Show this help
80
91
 
81
92
  Project commands:
82
- pnpm add -D @vmz/vmz
93
+ pnpm add @vmz/core && pnpm add -D @vmz/vmz
83
94
  pnpm exec vmz check
84
95
  # or: vmz new my-app && cd my-app && pnpm install
85
96
 
86
- If a project \`node_modules/@vmz/vmz\` (or \`vmz\`) exists, a global
97
+ If a project \`node_modules/@vmz/vmz\` exists, a global
87
98
  \`vmz <cmd>\` re-execs that bin.
88
99
  `);
89
100
  }
@@ -101,6 +112,7 @@ Usage:
101
112
  vmz test [path] [options] Native test discover / report
102
113
  vmz document|docs <cmd> Project /documents domain
103
114
  vmz application <cmd> Application Collection / Mount
115
+ vmz artifact <cmd> Release pack / publish / rollback / diff (A3)
104
116
  vmz refactor <cmd> DX rename plans / apply
105
117
  vmz explain [style] <target> DX causal explain (style Theme chain)
106
118
  vmz lsp [root] [--out-dir] Language server (stdio; native CLI)
@@ -110,9 +122,11 @@ Usage:
110
122
 
111
123
  Options:
112
124
  --out-dir, -o <dir> Output directory (default: dist)
113
- --release Release build (build only)
125
+ --release Release build (omit serve-host; pack minify slot; proof)
126
+ --profile <name> Delivery profile (default from config; builtins: web-ssr|web-static|web-client|web-hybrid)
127
+ --origin <url> Site origin for static-cdn canonical/sitemap
114
128
  --host <host> Listen host (default: 127.0.0.1)
115
- --port <port> Listen port (default: 5173)
129
+ --port <port> Listen port (dev: omit = auto from 5173; set = lock)
116
130
  --poll-ms <ms> Dev watch poll interval (default: 300)
117
131
  --build Build before serve
118
132
  --check Format check-only (format)
@@ -197,6 +211,10 @@ export async function runCli(argv, opts = {}) {
197
211
  case 'applications':
198
212
  case 'app':
199
213
  return cmdApplication(rest);
214
+ case 'artifact':
215
+ case 'artifacts':
216
+ case 'release':
217
+ return cmdArtifact(rest);
200
218
  case 'refactor':
201
219
  return cmdRefactor(rest);
202
220
  case 'explain':
@@ -265,7 +283,11 @@ function cmdCheck(args) {
265
283
  try {
266
284
  return runWithPlugins(ws, project, outDir, async () => {
267
285
  const report = ws.check();
268
- const errors = log.diagnostics(report.diagnostics ?? []);
286
+ const { checkLocales, localeHasErrors } = await import('./locale-check.js');
287
+ const localeReport = checkLocales({ projectRoot: project, checkUnused: false });
288
+ // Locale policy is first-class: missing /locales is warning (not silent), hard errors still fail.
289
+ const errors = log.diagnostics([...(report.diagnostics ?? []), ...(localeReport.diagnostics ?? [])]) ||
290
+ (localeHasErrors(localeReport) ? 1 : 0);
269
291
  log.info(`checked ${report.filesChecked} file(s)`);
270
292
  return errors ? 1 : 0;
271
293
  });
@@ -274,11 +296,28 @@ function cmdCheck(args) {
274
296
  ws.dispose();
275
297
  }
276
298
  }
299
+ /**
300
+ * Dedupe locale/build diagnostics by code+path+message (runtime + route emit both report missing manifest).
301
+ * @param {Array<{ code?: string, path?: string, message?: string, severity?: string }>} list
302
+ */
303
+ function dedupeDiagnostics(list) {
304
+ const seen = new Set();
305
+ /** @type {typeof list} */
306
+ const out = [];
307
+ for (const d of list || []) {
308
+ const key = `${d.code || ''}\0${d.path || ''}\0${d.message || ''}\0${d.severity || ''}`;
309
+ if (seen.has(key))
310
+ continue;
311
+ seen.add(key);
312
+ out.push(d);
313
+ }
314
+ return out;
315
+ }
277
316
  /**
278
317
  * @param {import('./index.js').Workspace} ws
279
318
  * @param {string} project
280
319
  * @param {string} outDir
281
- * @param { => Promise<number> | number} fn
320
+ * @param {() => Promise<number> | number} fn
282
321
  */
283
322
  async function runWithPlugins(ws, project, outDir, fn) {
284
323
  const { loadVmzConfig, applyPlugins } = await import('./plugin-host.js');
@@ -300,6 +339,21 @@ async function cmdBuild(args) {
300
339
  log.info(`build ${project} → ${outDir}`);
301
340
  const ws = createWorkspace({ root: project, outDir });
302
341
  try {
342
+ const cfg = await loadVmzConfig(project);
343
+ const cliProfile = typeof args.profile === 'string' ? args.profile : '';
344
+ const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
345
+ if (!norm.ok) {
346
+ log.diagnostics(norm.diagnostics ?? []);
347
+ log.error('delivery authoring invalid');
348
+ return 1;
349
+ }
350
+ const selected = selectBuildProfile(norm.table, cliProfile);
351
+ if (!selected.ok) {
352
+ log.diagnostics(selected.diagnostics ?? []);
353
+ log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
354
+ return 1;
355
+ }
356
+ log.info(`delivery profile ${selected.selection.profileId} (assembly=${selected.selection.assembly})`);
303
357
  const code = await runWithPlugins(ws, project, outDir, () => {
304
358
  const report = ws.build(Boolean(args.release));
305
359
  const errors = log.diagnostics(report.diagnostics ?? []);
@@ -313,20 +367,92 @@ async function cmdBuild(args) {
313
367
  });
314
368
  if (code !== 0)
315
369
  return code;
370
+ const localeEmit = emitLocaleRuntimeModules(project, outDir);
371
+ const localeRoutes = emitLocaleRouteRealization(project, outDir, {
372
+ origin: typeof args.origin === 'string' ? args.origin : undefined,
373
+ });
374
+ // Always surface locale diagnostics (warnings included) — missing /locales must not be silent.
375
+ // Dedupe: runtime emit + route realization both report the same missing-manifest warning.
376
+ const localeDiags = dedupeDiagnostics([
377
+ ...(localeEmit.diagnostics ?? []),
378
+ ...(localeRoutes.diagnostics ?? []),
379
+ ]);
380
+ log.diagnostics(localeDiags);
381
+ if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
382
+ log.error('locale runtime emit failed');
383
+ return 1;
384
+ }
385
+ if (localeEmit.written.length) {
386
+ log.info(`locale runtime emit (${localeEmit.written.length} module(s))`);
387
+ }
388
+ if (!localeRoutes.ok) {
389
+ log.error('locale route realization emit failed');
390
+ return 1;
391
+ }
392
+ if (localeRoutes.written.length) {
393
+ log.info(`locale route realization (${localeRoutes.written.length} artifact(s))`);
394
+ }
316
395
  if (projectHasDocuments(project)) {
317
396
  const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
318
397
  if (!docs.ok)
319
398
  return 1;
320
399
  }
400
+ let pack = null;
401
+ try {
402
+ pack = packFromDeploymentIr(outDir, {
403
+ release: Boolean(args.release),
404
+ profileId: selected.selection.profileId,
405
+ assembly: selected.selection.assembly,
406
+ coreDist: resolveCoreRuntimeDist(),
407
+ });
408
+ log.info(`pack ok (units=${pack.manifest.unitCount}, digest=${String(pack.manifest.packDigest).slice(0, 12)}…)`);
409
+ }
410
+ catch (err) {
411
+ log.error(`pack failed: ${err instanceof Error ? err.message : String(err)}`);
412
+ return 1;
413
+ }
414
+ const origin = typeof args.origin === 'string' ? args.origin : undefined;
415
+ let assemble = null;
416
+ try {
417
+ if (selected.selection.assembly === 'static-cdn') {
418
+ log.info(`web-static emit ${outDir}`);
419
+ }
420
+ assemble = await assembleDelivery(outDir, {
421
+ selection: selected.selection,
422
+ profile: {
423
+ ...selected.profile,
424
+ sources: selected.profile.sources || (norm.table.sugar ? norm.table.profiles[norm.table.default]?.sources : null),
425
+ },
426
+ siteId: cfg.application?.id || undefined,
427
+ origin,
428
+ pack: pack.manifest,
429
+ });
430
+ for (const step of assemble.manifest.steps || []) {
431
+ if (step.kind === 'static-cdn') {
432
+ log.info(`web-static ok (${step.htmlFiles} html, ${step.skipped} skipped, digest=${String(step.digest).slice(0, 12)}…)`);
433
+ }
434
+ else if (step.kind === 'site-delivery' && step.digest) {
435
+ log.info(`site-delivery ok (digest=${String(step.digest).slice(0, 12)}…)`);
436
+ }
437
+ }
438
+ }
439
+ catch (err) {
440
+ log.error(`assemble failed: ${err instanceof Error ? err.message : String(err)}`);
441
+ return 1;
442
+ }
443
+ const proof = emitBuildProof(outDir, {
444
+ selection: selected.selection,
445
+ pack: pack.manifest,
446
+ assemble: assemble.manifest,
447
+ release: Boolean(args.release),
448
+ });
449
+ log.info(`build-proof ok (profile=${proof.proof.profileId}, slots=${proof.proof.semanticIds.join(',')})`);
321
450
  return 0;
322
451
  }
323
452
  finally {
324
453
  ws.dispose();
325
454
  }
326
455
  }
327
- /**
328
- * @param {Record<string, string | boolean> & { _: string[] }} args
329
- */
330
456
  async function cmdServe(args) {
331
457
  const pathArg = args._[0] ?? '.';
332
458
  if (args.build) {
@@ -391,7 +517,22 @@ async function cmdDev(args) {
391
517
  outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
392
518
  });
393
519
  const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
394
- const port = Number(args.port ?? 5173);
520
+ const portLocked = Object.prototype.hasOwnProperty.call(args, 'port');
521
+ let port;
522
+ if (portLocked) {
523
+ port = Number(args.port);
524
+ if (!Number.isFinite(port) || port <= 0) {
525
+ log.error(`invalid --port ${String(args.port)}`);
526
+ return 1;
527
+ }
528
+ }
529
+ else {
530
+ const preferred = 5173;
531
+ port = await findAvailablePort(host, preferred);
532
+ if (port !== preferred) {
533
+ log.info(`port ${preferred} busy → using ${port}`);
534
+ }
535
+ }
395
536
  const pollMs = Number(args['poll-ms'] ?? 300);
396
537
  const ac = new AbortController();
397
538
  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
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * B0 — Delivery profile authoring normalize + CLI --profile resolve.
3
+ * Pure data only; expands legacy site-delivery sugar into profiles[default].
4
+ */
5
+ export declare const DELIVERY_PROFILE_AUTHORING_SCHEMA = "vmz.delivery.authoring.v0";
6
+ export declare const BUILD_PROFILE_SELECTION_SCHEMA = "vmz.build.profile_selection.v0";
7
+ /** Browser-era assembly kinds (04 B5). */
8
+ export declare const ASSEMBLIES: readonly string[];
9
+ export declare const SERVER_RUNTIMES: readonly string[];
10
+ /** Official built-in aliases when not overridden in config. */
11
+ export declare const BUILTIN_PROFILES: Readonly<{
12
+ 'web-client': {
13
+ host: string;
14
+ assembly: string;
15
+ };
16
+ 'web-static': {
17
+ host: string;
18
+ assembly: string;
19
+ };
20
+ 'web-ssr': {
21
+ host: string;
22
+ assembly: string;
23
+ serverRuntime: string;
24
+ };
25
+ 'web-hybrid': {
26
+ host: string;
27
+ assembly: string;
28
+ serverRuntime: string;
29
+ };
30
+ }>;
31
+ export declare function pickSiteAuthoring(raw: any): {
32
+ artifact: string;
33
+ sources: any;
34
+ };
35
+ export declare function normalizeDeliveryAuthoring(raw: any): {
36
+ ok: boolean;
37
+ table: {
38
+ schema: string;
39
+ default: string;
40
+ profiles: {};
41
+ sugar: boolean;
42
+ };
43
+ diagnostics?: undefined;
44
+ } | {
45
+ ok: boolean;
46
+ diagnostics: any[];
47
+ table?: undefined;
48
+ };
49
+ export declare function selectBuildProfile(table: any, cliProfile?: string): {
50
+ ok: boolean;
51
+ diagnostics: {
52
+ code: string;
53
+ message: string;
54
+ }[];
55
+ selection?: undefined;
56
+ profile?: undefined;
57
+ } | {
58
+ ok: boolean;
59
+ selection: {
60
+ schema: string;
61
+ profileId: any;
62
+ host: any;
63
+ assembly: any;
64
+ serverRuntime: any;
65
+ hasSiteSources: boolean;
66
+ authoringDigest: any;
67
+ fromCli: boolean;
68
+ };
69
+ profile: any;
70
+ diagnostics?: undefined;
71
+ };
72
+ export declare function semanticIdsForAssembly(assembly: any): string[];
73
+ export declare function canonicalJson(value: any): string;
74
+ export declare function sha256Hex(text: any): string;