@vmz/vmz 0.1.13 → 0.1.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 (51) hide show
  1. package/dist/build-assemble.js +18 -7
  2. package/dist/cdn-policy.js +5 -4
  3. package/dist/cli.js +106 -38
  4. package/dist/content-addressed-assets.d.ts +23 -2
  5. package/dist/content-addressed-assets.js +178 -32
  6. package/dist/delivery-profile.d.ts +30 -2
  7. package/dist/delivery-profile.js +139 -5
  8. package/dist/dev-session.d.ts +1 -0
  9. package/dist/dev-session.js +269 -65
  10. package/dist/dev-watch-roots.d.ts +80 -0
  11. package/dist/dev-watch-roots.js +245 -0
  12. package/dist/document-build.d.ts +2 -1
  13. package/dist/document-build.js +104 -82
  14. package/dist/document-cmd.js +2 -9
  15. package/dist/document-enrich.d.ts +2 -1
  16. package/dist/document-enrich.js +15 -4
  17. package/dist/document-integrate.js +29 -21
  18. package/dist/document-layout-render.d.ts +20 -0
  19. package/dist/document-layout-render.js +87 -0
  20. package/dist/document-routing-config.d.ts +13 -0
  21. package/dist/document-routing-config.js +40 -0
  22. package/dist/embedded-packaging.js +2 -1
  23. package/dist/index.d.ts +18 -2
  24. package/dist/index.js +63 -4
  25. package/dist/locale-check.js +28 -61
  26. package/dist/locale-cmd.js +10 -42
  27. package/dist/locale-route-emit.d.ts +4 -1
  28. package/dist/locale-route-emit.js +7 -32
  29. package/dist/locale-router.js +26 -8
  30. package/dist/native-addon.d.ts +9 -0
  31. package/dist/native-addon.js +84 -0
  32. package/dist/pack.js +3 -2
  33. package/dist/pretty-json.d.ts +19 -0
  34. package/dist/pretty-json.js +43 -0
  35. package/dist/production-observability.js +3 -2
  36. package/dist/production-test-pack.js +4 -3
  37. package/dist/public-static-assets.d.ts +24 -0
  38. package/dist/public-static-assets.js +140 -0
  39. package/dist/release-pack.js +5 -18
  40. package/dist/route-path.d.ts +35 -0
  41. package/dist/route-path.js +77 -0
  42. package/dist/server-artifact.js +4 -3
  43. package/dist/site-delivery.js +4 -3
  44. package/dist/site-favicon.d.ts +31 -0
  45. package/dist/site-favicon.js +140 -0
  46. package/dist/static-emit.d.ts +21 -0
  47. package/dist/static-emit.js +134 -97
  48. package/dist/test-cmd.js +2 -1
  49. package/dist/wechat-packaging.d.ts +22 -0
  50. package/dist/wechat-packaging.js +59 -0
  51. package/package.json +12 -12
@@ -2,13 +2,16 @@
2
2
  * B5 Assemble dispatch + B6 build-proof (per-build semantic id slots).
3
3
  */
4
4
  // @ts-nocheck
5
- import { mkdirSync, writeFileSync } from 'node:fs';
5
+ import { mkdirSync } from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { semanticIdsForAssembly, sha256Hex, canonicalJson } from './delivery-profile.js';
8
8
  import { emitServerArtifact } from './server-artifact.js';
9
9
  import { emitEmbeddedPackaging } from './embedded-packaging.js';
10
10
  import { emitSiteDelivery } from './site-delivery.js';
11
+ import { emitSiteFavicon } from './site-favicon.js';
12
+ import { emitPublicStaticAssets } from './public-static-assets.js';
11
13
  import { emitWebStatic } from './static-emit.js';
14
+ import { writePrettyJsonFile } from './pretty-json.js';
12
15
  export const BUILD_PROOF_SCHEMA = 'vmz.build.proof.v0';
13
16
  export const ASSEMBLE_MANIFEST_SCHEMA = 'vmz.assemble.manifest.v0';
14
17
  /**
@@ -25,10 +28,13 @@ export async function assembleDelivery(outDir, ctx) {
25
28
  serverRuntime: selection.serverRuntime || null,
26
29
  steps: [],
27
30
  };
28
- if (assembly === 'static-cdn' || assembly === 'cdn+server') {
29
- const staticResult = await emitWebStatic(outDir, { origin: ctx.origin });
31
+ if (assembly === 'web-static' || assembly === 'cdn+server') {
32
+ const staticResult = await emitWebStatic(outDir, {
33
+ origin: ctx.origin,
34
+ projectRoot: ctx.projectRoot,
35
+ });
30
36
  result.steps.push({
31
- kind: 'static-cdn',
37
+ kind: 'web-static',
32
38
  digest: staticResult.digest,
33
39
  htmlFiles: staticResult.htmlFiles?.length ?? 0,
34
40
  skipped: staticResult.skipped?.length ?? 0,
@@ -39,6 +45,11 @@ export async function assembleDelivery(outDir, ctx) {
39
45
  skipped: staticResult.skipped,
40
46
  };
41
47
  }
48
+ else if (assembly === 'server-host' || assembly === 'local-static') {
49
+ // SSR / local packs: favicon + opaque public/ for serve-host.
50
+ emitSiteFavicon(outDir, { projectRoot: ctx.projectRoot });
51
+ emitPublicStaticAssets(outDir, { projectRoot: ctx.projectRoot });
52
+ }
42
53
  if (assembly === 'local-static') {
43
54
  result.steps.push({
44
55
  kind: 'local-static',
@@ -106,7 +117,7 @@ export async function assembleDelivery(outDir, ctx) {
106
117
  const vmzDir = path.join(outDir, '_vmz');
107
118
  mkdirSync(vmzDir, { recursive: true });
108
119
  const file = path.join(vmzDir, 'assemble-manifest.json');
109
- writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
120
+ writePrettyJsonFile(file, result);
110
121
  return { manifest: result, path: file };
111
122
  }
112
123
  /**
@@ -123,7 +134,7 @@ export function emitBuildProof(outDir, ctx) {
123
134
  };
124
135
  for (const id of semanticIds) {
125
136
  if (id === 'static-delivery') {
126
- const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'static-cdn');
137
+ const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'web-static');
127
138
  slots[id] = step
128
139
  ? { status: 'emitted', detail: `digest=${String(step.digest).slice(0, 12)}` }
129
140
  : { status: 'pending', detail: 'assembly requires static emit' };
@@ -186,6 +197,6 @@ export function emitBuildProof(outDir, ctx) {
186
197
  const vmzDir = path.join(outDir, '_vmz');
187
198
  mkdirSync(vmzDir, { recursive: true });
188
199
  const file = path.join(vmzDir, 'build-proof.json');
189
- writeFileSync(file, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
200
+ writePrettyJsonFile(file, body);
190
201
  return { proof: body, path: file };
191
202
  }
@@ -8,6 +8,7 @@ import fs from 'node:fs';
8
8
  import http from 'node:http';
9
9
  import path from 'node:path';
10
10
  import { assertLocaleCacheKey, localeAwareCacheKey } from './locale-router.js';
11
+ import { writePrettyJsonFile } from './pretty-json.js';
11
12
  export const CDN_POLICY_MANIFEST_SCHEMA = 'vmz.cdn.policy_manifest.v0';
12
13
  export const CDN_ADAPTER_PROJECTION_SCHEMA = 'vmz.cdn.adapter_projection.v0';
13
14
  /** HTML: revalidate. Hashed/static assets: long immutable. */
@@ -85,7 +86,7 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
85
86
  const body = {
86
87
  schema: CDN_POLICY_MANIFEST_SCHEMA,
87
88
  applicationId: staticManifest.applicationId || null,
88
- deliveryProfile: 'web-static',
89
+ deliveryProfile: 'static',
89
90
  origin,
90
91
  spaFallback: false,
91
92
  staticManifestDigest: staticManifest.manifestDigest || null,
@@ -145,14 +146,14 @@ export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
145
146
  const policy = buildCdnPolicyManifest(staticManifest, { ...opts, localeArtifact });
146
147
  const vmzDir = path.join(distDir, '_vmz');
147
148
  fs.mkdirSync(vmzDir, { recursive: true });
148
- fs.writeFileSync(path.join(vmzDir, 'cdn-policy-manifest.json'), `${JSON.stringify(policy, null, 2)}\n`, 'utf8');
149
+ writePrettyJsonFile(path.join(vmzDir, 'cdn-policy-manifest.json'), policy);
149
150
  const local = projectCdnAdapter(policy, 'local-static');
150
151
  const netlify = projectCdnAdapter(policy, 'netlify');
151
152
  const adaptersDir = path.join(vmzDir, 'adapters');
152
153
  fs.mkdirSync(path.join(adaptersDir, 'local-static'), { recursive: true });
153
154
  fs.mkdirSync(path.join(adaptersDir, 'netlify'), { recursive: true });
154
- fs.writeFileSync(path.join(adaptersDir, 'local-static', 'projection.json'), `${JSON.stringify(local, null, 2)}\n`, 'utf8');
155
- fs.writeFileSync(path.join(adaptersDir, 'netlify', 'projection.json'), `${JSON.stringify(netlify, null, 2)}\n`, 'utf8');
155
+ writePrettyJsonFile(path.join(adaptersDir, 'local-static', 'projection.json'), local);
156
+ writePrettyJsonFile(path.join(adaptersDir, 'netlify', 'projection.json'), netlify);
156
157
  fs.writeFileSync(path.join(adaptersDir, 'netlify', '_headers'), String(netlify.files['_headers'] || ''), 'utf8');
157
158
  fs.writeFileSync(path.join(adaptersDir, 'netlify', '_redirects'), String(netlify.files['_redirects'] || ''), 'utf8');
158
159
  return { policy, adapters: { 'local-static': local, netlify } };
package/dist/cli.js CHANGED
@@ -3,9 +3,9 @@
3
3
  * Node CLI command implementations .
4
4
  */
5
5
  import { spawn } from 'node:child_process';
6
- import { copyFileSync, existsSync } from 'node:fs';
6
+ import { existsSync } from 'node:fs';
7
7
  import path from 'node:path';
8
- import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntimeDist } from './index.js';
8
+ import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, materializeServeHostRuntime, resolveCoreRuntimeDist, resolveNativePath, } from './index.js';
9
9
  import { createDevSession } from './dev-session.js';
10
10
  import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from './invocation.js';
11
11
  import { log } from './log.js';
@@ -23,7 +23,7 @@ import { cmdRefactor } from './refactor-cmd.js';
23
23
  import { cmdExplain } from './explain-cmd.js';
24
24
  import { resolveNativeVmzCli } from './resolve-native-cli.js';
25
25
  import { loadVmzConfig } from './plugin-host.js';
26
- import { normalizeDeliveryAuthoring, selectBuildProfile } from './delivery-profile.js';
26
+ import { normalizeDeliveryAuthoring, resolveProfileArtifactDir, selectBuildProfile } from './delivery-profile.js';
27
27
  import { packFromDeploymentIr } from './pack.js';
28
28
  import { assembleDelivery, emitBuildProof } from './build-assemble.js';
29
29
  /**
@@ -104,10 +104,10 @@ export function printProjectHelp() {
104
104
  Usage:
105
105
  vmz new|init <dir> Scaffold a minimal app (native CLI)
106
106
  vmz check [path] Check project via Workspace
107
- vmz build [path] [options] Build project via Workspace
107
+ vmz build [path] [options] Build project via Workspace; --target mini-program-wechat packs dist/wechat
108
108
  vmz serve [path] [options] Serve dist (optional --build)
109
- vmz dev [path] [options] Long-lived rebuild session (no CLI spawn)
110
- vmz format [path] [--check] Format .vmz via N-API (oxc codegen)
109
+ vmz dev [path] [options] Rebuild session; --target mini-program-wechat packs dist/wechat
110
+ vmz format [path] [--check] Format .vmz via N-API (oxc formatter + EditorConfig)
111
111
  vmz lint [path] [--deny-warnings] Lint (= check) via N-API
112
112
  vmz test [path] [options] Native test discover / report
113
113
  vmz document|docs <cmd> Project /documents domain
@@ -121,10 +121,11 @@ Usage:
121
121
  vmz help Show this help
122
122
 
123
123
  Options:
124
- --out-dir, -o <dir> Output directory (default: dist)
124
+ --out-dir, -o <dir> Workspace output root (default: dist). Profile artifacts land in <out-dir>/<name> (name defaults to profile id; CDN: name:'cdn' → dist/cdn)
125
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
126
+ --profile <name> Delivery profile (default from config; builtins: web-ssr|static|web-client|web-hybrid)
127
+ --target <id> browser (default) | mini-program-wechat (pack dist/wechat for WeChat DevTools; build+dev)
128
+ --origin <url> Site origin for web-static canonical/sitemap
128
129
  --host <host> Listen host (default: 127.0.0.1)
129
130
  --port <port> Listen port (dev: omit = auto from 5173; set = lock)
130
131
  --poll-ms <ms> Dev watch poll interval (default: 300)
@@ -332,28 +333,35 @@ async function runWithPlugins(ws, project, outDir, fn) {
332
333
  */
333
334
  async function cmdBuild(args) {
334
335
  const pathArg = args._[0] ?? '.';
335
- const { project, outDir } = resolveWorkspaceDirs({
336
+ const targetRaw = typeof args.target === 'string' ? args.target : 'browser';
337
+ if (targetRaw !== 'browser' && targetRaw !== 'mini-program-wechat') {
338
+ log.error(`unknown --target ${targetRaw} (browser | mini-program-wechat)`);
339
+ return 1;
340
+ }
341
+ const wechatPack = targetRaw === 'mini-program-wechat';
342
+ const { project, outDir: outDirRoot } = resolveWorkspaceDirs({
336
343
  path: pathArg,
337
344
  outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
338
345
  });
339
- log.info(`build ${project} ${outDir}`);
346
+ const cfg = await loadVmzConfig(project);
347
+ const cliProfile = typeof args.profile === 'string' ? args.profile : '';
348
+ const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
349
+ if (!norm.ok) {
350
+ log.diagnostics(norm.diagnostics ?? []);
351
+ log.error('delivery authoring invalid');
352
+ return 1;
353
+ }
354
+ const selected = selectBuildProfile(norm.table, cliProfile);
355
+ if (!selected.ok) {
356
+ log.diagnostics(selected.diagnostics ?? []);
357
+ log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
358
+ return 1;
359
+ }
360
+ const outDir = resolveProfileArtifactDir(outDirRoot, selected.profile);
361
+ log.info(`build ${project} → ${outDir}${wechatPack ? ' (target=mini-program-wechat)' : ''} (out-dir=${outDirRoot}, name=${selected.profile.name})`);
340
362
  const ws = createWorkspace({ root: project, outDir });
341
363
  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})`);
364
+ log.info(`delivery profile ${selected.selection.profileId} (assembly=${selected.selection.assembly}, name=${selected.profile.name})`);
357
365
  const code = await runWithPlugins(ws, project, outDir, () => {
358
366
  const report = ws.build(Boolean(args.release));
359
367
  const errors = log.diagnostics(report.diagnostics ?? []);
@@ -394,6 +402,29 @@ async function cmdBuild(args) {
394
402
  if (!docs.ok)
395
403
  return 1;
396
404
  }
405
+ if (wechatPack) {
406
+ if (typeof ws.lowerMiniprogramWechatPackaging !== 'function') {
407
+ log.error('wechat pack: workspace missing lowerMiniprogramWechatPackaging');
408
+ return 1;
409
+ }
410
+ let report;
411
+ try {
412
+ const raw = ws.lowerMiniprogramWechatPackaging();
413
+ report = typeof raw === 'string' ? JSON.parse(raw) : raw;
414
+ }
415
+ catch (err) {
416
+ log.error(`wechat pack failed: ${err instanceof Error ? err.message : String(err)}`);
417
+ return 1;
418
+ }
419
+ log.diagnostics(report.diagnostics ?? []);
420
+ if (report.status !== 'ready') {
421
+ log.error(`wechat pack ${report.status || 'failed'}`);
422
+ return 1;
423
+ }
424
+ const packRoot = report.packRoot || 'dist/wechat';
425
+ log.info(`wechat pack ok → ${path.join(project, packRoot)} (open in WeChat DevTools)`);
426
+ return 0;
427
+ }
397
428
  let pack = null;
398
429
  try {
399
430
  pack = packFromDeploymentIr(outDir, {
@@ -419,8 +450,8 @@ async function cmdBuild(args) {
419
450
  const origin = typeof args.origin === 'string' ? args.origin : undefined;
420
451
  let assemble = null;
421
452
  try {
422
- if (selected.selection.assembly === 'static-cdn') {
423
- log.info(`web-static emit ${outDir}`);
453
+ if (selected.selection.assembly === 'web-static') {
454
+ log.info(`static emit ${outDir}`);
424
455
  }
425
456
  assemble = await assembleDelivery(outDir, {
426
457
  selection: selected.selection,
@@ -431,10 +462,11 @@ async function cmdBuild(args) {
431
462
  siteId: cfg.application?.id || undefined,
432
463
  origin,
433
464
  pack: pack.manifest,
465
+ projectRoot: project,
434
466
  });
435
467
  for (const step of assemble.manifest.steps || []) {
436
- if (step.kind === 'static-cdn') {
437
- log.info(`web-static ok (${step.htmlFiles} html, ${step.skipped} skipped, digest=${String(step.digest).slice(0, 12)}…)`);
468
+ if (step.kind === 'web-static') {
469
+ log.info(`static ok (${step.htmlFiles} html, ${step.skipped} skipped, digest=${String(step.digest).slice(0, 12)}…)`);
438
470
  }
439
471
  else if (step.kind === 'site-delivery' && step.digest) {
440
472
  log.info(`site-delivery ok (digest=${String(step.digest).slice(0, 12)}…)`);
@@ -465,20 +497,33 @@ async function cmdServe(args) {
465
497
  if (code !== 0)
466
498
  return code;
467
499
  }
468
- const { project, outDir } = resolveWorkspaceDirs({
500
+ const { project, outDir: outDirRoot } = resolveWorkspaceDirs({
469
501
  path: pathArg,
470
502
  outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
471
503
  });
504
+ const cfg = await loadVmzConfig(project);
505
+ const cliProfile = typeof args.profile === 'string' ? args.profile : '';
506
+ const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
507
+ if (!norm.ok) {
508
+ log.diagnostics(norm.diagnostics ?? []);
509
+ log.error('delivery authoring invalid');
510
+ return 1;
511
+ }
512
+ const selected = selectBuildProfile(norm.table, cliProfile);
513
+ if (!selected.ok) {
514
+ log.diagnostics(selected.diagnostics ?? []);
515
+ log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
516
+ return 1;
517
+ }
518
+ const outDir = resolveProfileArtifactDir(outDirRoot, selected.profile);
472
519
  const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
473
520
  if (!existsSync(hostJs)) {
474
- const coreDist = resolveCoreRuntimeDist();
475
- const src = coreDist ? path.join(coreDist, 'serve-host.mjs') : null;
476
- if (src && existsSync(src)) {
477
- copyFileSync(src, hostJs);
521
+ try {
522
+ materializeServeHostRuntime(outDir);
478
523
  log.info(`materialized ${hostJs} from @vmz/core (release builds omit it)`);
479
524
  }
480
- else {
481
- log.error(`missing ${hostJs} — run \`vmz build\` (without --release) or ensure @vmz/core is installed`);
525
+ catch (err) {
526
+ log.error(`missing ${hostJs} — run \`vmz build\` (without --release) or ensure @vmz/core is installed (${err instanceof Error ? err.message : err})`);
482
527
  return 1;
483
528
  }
484
529
  }
@@ -493,6 +538,8 @@ async function cmdServe(args) {
493
538
  VMZ_DIST: outDir,
494
539
  VMZ_PORT: String(port),
495
540
  VMZ_HOST: host,
541
+ VMZ_PROJECT_ROOT: project,
542
+ VMZ_NATIVE_NODE: resolveNativePath(),
496
543
  },
497
544
  stdio: 'inherit',
498
545
  });
@@ -517,10 +564,25 @@ async function cmdServe(args) {
517
564
  */
518
565
  async function cmdDev(args) {
519
566
  const pathArg = args._[0] ?? '.';
520
- const { project, outDir } = resolveWorkspaceDirs({
567
+ const { project, outDir: outDirRoot } = resolveWorkspaceDirs({
521
568
  path: pathArg,
522
569
  outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
523
570
  });
571
+ const cfg = await loadVmzConfig(project);
572
+ const cliProfile = typeof args.profile === 'string' ? args.profile : '';
573
+ const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
574
+ if (!norm.ok) {
575
+ log.diagnostics(norm.diagnostics ?? []);
576
+ log.error('delivery authoring invalid');
577
+ return 1;
578
+ }
579
+ const selected = selectBuildProfile(norm.table, cliProfile);
580
+ if (!selected.ok) {
581
+ log.diagnostics(selected.diagnostics ?? []);
582
+ log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
583
+ return 1;
584
+ }
585
+ const outDir = resolveProfileArtifactDir(outDirRoot, selected.profile);
524
586
  const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
525
587
  const portLocked = Object.prototype.hasOwnProperty.call(args, 'port');
526
588
  let port;
@@ -539,6 +601,11 @@ async function cmdDev(args) {
539
601
  }
540
602
  }
541
603
  const pollMs = Number(args['poll-ms'] ?? 300);
604
+ const targetRaw = typeof args.target === 'string' ? args.target : 'browser';
605
+ if (targetRaw !== 'browser' && targetRaw !== 'mini-program-wechat') {
606
+ log.error(`unknown --target ${targetRaw} (browser | mini-program-wechat)`);
607
+ return 1;
608
+ }
542
609
  const ac = new AbortController();
543
610
  const onSig = () => {
544
611
  log.info('shutting down…');
@@ -552,6 +619,7 @@ async function cmdDev(args) {
552
619
  host,
553
620
  port,
554
621
  pollMs,
622
+ target: targetRaw,
555
623
  signal: ac.signal,
556
624
  });
557
625
  try {
@@ -1,9 +1,30 @@
1
1
  /**
2
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).
3
+ * Logical paths stay available for serve/dev; static HTML rewrites to hashed URLs.
4
+ * CSS aggregators (vmz.css) rewrite `@import` to hashed sibling paths under assets/.
5
+ * JS under assets/ always rewrites ESM `./` → `../` so barrels (vmz-dom → dom-core)
6
+ * resolve at dist root — never prefer hashed siblings for JS (second-hop 404).
5
7
  */
6
8
  export declare const CONTENT_ADDRESSED_ASSETS_SCHEMA = "vmz.content_addressed_assets.v0";
9
+ /**
10
+ * Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
11
+ * @param {string} cssText
12
+ * @param {Record<string, string>} rewrites logical (no leading slash) or `/logical` → `assets/hash.ext`
13
+ */
14
+ export declare function rewriteCssImports(cssText: any, rewrites: any): any;
15
+ /**
16
+ * Rewrite relative ESM so a file served from `/assets/<hash>.js` resolves against
17
+ * dist root (static `from "./x"` / `export * from "./x"` + dynamic `import("./"+…)`).
18
+ *
19
+ * Always use `../…` (Fix A). Do **not** prefer hashed siblings under `assets/`:
20
+ * barrels like `vmz-dom.js` (`export * from './dom-core.js'`) would then resolve
21
+ * as `/assets/dom-core.js` and 404. `rewrites` is accepted for API parity with CSS
22
+ * but intentionally ignored for JS path choice.
23
+ *
24
+ * @param {string} jsText
25
+ * @param {Record<string, string>} [_rewrites]
26
+ */
27
+ export declare function rewriteJsEntryRelativeImports(jsText: any, _rewrites?: {}): string;
7
28
  /**
8
29
  * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
9
30
  * @param {string} distDir
@@ -1,13 +1,16 @@
1
1
  /**
2
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).
3
+ * Logical paths stay available for serve/dev; static HTML rewrites to hashed URLs.
4
+ * CSS aggregators (vmz.css) rewrite `@import` to hashed sibling paths under assets/.
5
+ * JS under assets/ always rewrites ESM `./` → `../` so barrels (vmz-dom → dom-core)
6
+ * resolve at dist root — never prefer hashed siblings for JS (second-hop 404).
5
7
  */
6
8
  // @ts-nocheck
7
9
  import crypto from 'node:crypto';
8
10
  import fs from 'node:fs';
9
11
  import path from 'node:path';
10
12
  import { canonicalJson, sha256Hex } from './release-pack.js';
13
+ import { writePrettyJsonFile } from './pretty-json.js';
11
14
  export const CONTENT_ADDRESSED_ASSETS_SCHEMA = 'vmz.content_addressed_assets.v0';
12
15
  /** Immutable delivery candidates (client-facing bytes). */
13
16
  const DEFAULT_CANDIDATES = [
@@ -15,11 +18,64 @@ const DEFAULT_CANDIDATES = [
15
18
  'entry-event.js',
16
19
  'vmz.css',
17
20
  'vmz-designs.css',
21
+ 'vmz-style.css',
18
22
  'vmz-dom.js',
19
23
  'vmz-runtime.js',
20
24
  'vmz-http.js',
21
25
  'vmz-client-nav.js',
22
26
  ];
27
+ /** CSS files that may @import other logical CSS; processed after leaf CSS is hashed. */
28
+ const CSS_AGGREGATORS = new Set(['vmz.css']);
29
+ /** JS entry shells hashed under assets/; relative ESM must be rewritten first. */
30
+ const JS_ENTRY_AGGREGATORS = new Set(['entry-client.js', 'entry-event.js']);
31
+ const CSS_IMPORT_RE = /@import\s*(?:url\()?['"]?(\.\/)?([^'")\s;]+)['"]?\)?/gi;
32
+ /**
33
+ * Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
34
+ * @param {string} cssText
35
+ * @param {Record<string, string>} rewrites logical (no leading slash) or `/logical` → `assets/hash.ext`
36
+ */
37
+ export function rewriteCssImports(cssText, rewrites) {
38
+ return cssText.replace(CSS_IMPORT_RE, (match, _dot, target) => {
39
+ const logical = String(target || '').replace(/^\.\//, '');
40
+ if (!logical)
41
+ return match;
42
+ const hashed = rewrites[logical] || rewrites[`/${logical}`] || rewrites[`assets/${logical}`];
43
+ if (!hashed)
44
+ return match;
45
+ const rel = hashed.startsWith('/') ? hashed.slice(1) : hashed;
46
+ const sibling = rel.startsWith('assets/') ? `./${path.basename(rel)}` : `./${rel}`;
47
+ return `@import"${sibling}"`;
48
+ });
49
+ }
50
+ /**
51
+ * Rewrite relative ESM so a file served from `/assets/<hash>.js` resolves against
52
+ * dist root (static `from "./x"` / `export * from "./x"` + dynamic `import("./"+…)`).
53
+ *
54
+ * Always use `../…` (Fix A). Do **not** prefer hashed siblings under `assets/`:
55
+ * barrels like `vmz-dom.js` (`export * from './dom-core.js'`) would then resolve
56
+ * as `/assets/dom-core.js` and 404. `rewrites` is accepted for API parity with CSS
57
+ * but intentionally ignored for JS path choice.
58
+ *
59
+ * @param {string} jsText
60
+ * @param {Record<string, string>} [_rewrites]
61
+ */
62
+ export function rewriteJsEntryRelativeImports(jsText, _rewrites = {}) {
63
+ let out = String(jsText || '');
64
+ // Dynamic: import("./" + id) → import("../" + id)
65
+ out = out.replace(/import\(\s*"\.\/"\s*\+/g, 'import("../"+');
66
+ out = out.replace(/import\(\s*'\.\/'\s*\+/g, "import('../'+");
67
+ const rewriteSpec = (spec) => {
68
+ const logical = String(spec || '').replace(/^\.\//, '');
69
+ if (!logical || logical.startsWith('../') || logical.startsWith('/'))
70
+ return spec;
71
+ return `../${logical}`;
72
+ };
73
+ // import/export … from "./x" | import("./x")
74
+ out = out.replace(/\b((?:import|export)\s+[^'"\n]*?\s+from\s+|import\s*\(\s*)(['"])(\.\/[^'"]+)\2/g, (match, prefix, quote, spec) => `${prefix}${quote}${rewriteSpec(spec)}${quote}`);
75
+ // side-effect: import "./x.js"
76
+ out = out.replace(/(^|[;\s])(import\s*)(['"])(\.\/[^'"]+)\3/gm, (match, lead, kw, quote, spec) => `${lead}${kw}${quote}${rewriteSpec(spec)}${quote}`);
77
+ return out;
78
+ }
23
79
  /**
24
80
  * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
25
81
  * @param {string} distDir
@@ -37,36 +93,55 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
37
93
  const objects = [];
38
94
  /** @type {Record<string, string>} */
39
95
  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())
96
+ const ordered = orderCandidates(candidates);
97
+ for (const rel of ordered) {
98
+ // Aggregators are replaced in later passes; leaf JS must rewrite ./ → ../ so a
99
+ // hashed barrel under assets/ never 404s on second-hop relative re-exports.
100
+ if (CSS_AGGREGATORS.has(rel) || JS_ENTRY_AGGREGATORS.has(rel)) {
101
+ ingestCandidate(abs, rel, rewrites, objects, { transform: null });
44
102
  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
103
  }
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
- }
104
+ if (/\.m?js$/i.test(rel)) {
105
+ const src = path.join(abs, rel);
106
+ if (!fs.existsSync(src))
107
+ continue;
108
+ const rewritten = rewriteJsEntryRelativeImports(fs.readFileSync(src, 'utf8'), {});
109
+ ingestCandidate(abs, rel, rewrites, objects, {
110
+ transform: () => Buffer.from(rewritten, 'utf8'),
111
+ });
112
+ continue;
60
113
  }
61
- objects.push({
62
- logicalPath: logical,
63
- assetPath: assetRel,
64
- digest,
65
- bytes: buf.length,
66
- immutable: true,
114
+ ingestCandidate(abs, rel, rewrites, objects, { transform: null });
115
+ }
116
+ // Aggregator CSS (vmz.css) must import hashed leaf files — rewrite then hash.
117
+ for (const rel of ordered) {
118
+ if (!CSS_AGGREGATORS.has(rel))
119
+ continue;
120
+ const src = path.join(abs, rel);
121
+ if (!fs.existsSync(src))
122
+ continue;
123
+ const rewritten = rewriteCssImports(fs.readFileSync(src, 'utf8'), rewrites);
124
+ removeLogicalObject(objects, rel);
125
+ delete rewrites[`/${rel}`];
126
+ delete rewrites[rel];
127
+ ingestCandidate(abs, rel, rewrites, objects, {
128
+ transform: () => Buffer.from(rewritten, 'utf8'),
129
+ });
130
+ }
131
+ // JS entry shells: rewrite relative ESM (static + dynamic) then re-hash.
132
+ for (const rel of ordered) {
133
+ if (!JS_ENTRY_AGGREGATORS.has(rel))
134
+ continue;
135
+ const src = path.join(abs, rel);
136
+ if (!fs.existsSync(src))
137
+ continue;
138
+ const rewritten = rewriteJsEntryRelativeImports(fs.readFileSync(src, 'utf8'), rewrites);
139
+ removeLogicalObject(objects, rel);
140
+ delete rewrites[`/${rel}`];
141
+ delete rewrites[rel];
142
+ ingestCandidate(abs, rel, rewrites, objects, {
143
+ transform: () => Buffer.from(rewritten, 'utf8'),
67
144
  });
68
- rewrites[`/${logical}`] = `/${assetRel}`;
69
- rewrites[logical] = assetRel;
70
145
  }
71
146
  objects.sort((a, b) => (a.logicalPath < b.logicalPath ? -1 : a.logicalPath > b.logicalPath ? 1 : 0));
72
147
  let rewrittenHtml = 0;
@@ -85,9 +160,83 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
85
160
  const vmzDir = path.join(abs, '_vmz');
86
161
  fs.mkdirSync(vmzDir, { recursive: true });
87
162
  const outPath = path.join(vmzDir, 'content-addressed-assets.json');
88
- fs.writeFileSync(outPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
163
+ writePrettyJsonFile(outPath, manifest);
89
164
  return { manifest, assetsDir, rewrites, manifestPath: outPath };
90
165
  }
166
+ /**
167
+ * @param {string[]} candidates
168
+ */
169
+ function orderCandidates(candidates) {
170
+ const set = new Set(candidates.map((c) => String(c).replace(/\\/g, '/').replace(/^\//, '')));
171
+ /** @type {string[]} */
172
+ const out = [];
173
+ for (const name of DEFAULT_CANDIDATES) {
174
+ if (set.has(name) && !CSS_AGGREGATORS.has(name) && !JS_ENTRY_AGGREGATORS.has(name))
175
+ out.push(name);
176
+ }
177
+ for (const name of [...set].sort()) {
178
+ if (!CSS_AGGREGATORS.has(name) && !JS_ENTRY_AGGREGATORS.has(name) && !out.includes(name)) {
179
+ out.push(name);
180
+ }
181
+ }
182
+ if (set.has('vmz.css'))
183
+ out.push('vmz.css');
184
+ for (const name of ['entry-client.js', 'entry-event.js']) {
185
+ if (set.has(name))
186
+ out.push(name);
187
+ }
188
+ return out;
189
+ }
190
+ /**
191
+ * @param {Array<Record<string, any>>} objects
192
+ * @param {string} logical
193
+ */
194
+ function removeLogicalObject(objects, logical) {
195
+ const idx = objects.findIndex((o) => o.logicalPath === logical);
196
+ if (idx >= 0)
197
+ objects.splice(idx, 1);
198
+ }
199
+ /**
200
+ * @param {string} absDist
201
+ * @param {string} rel
202
+ * @param {Record<string, string>} rewrites
203
+ * @param {Array<Record<string, any>>} objects
204
+ * @param {{ transform?: ((buf: Buffer) => Buffer) | null }} opts
205
+ */
206
+ function ingestCandidate(absDist, rel, rewrites, objects, opts) {
207
+ const logical = String(rel).replace(/\\/g, '/').replace(/^\//, '');
208
+ const src = path.join(absDist, ...logical.split('/'));
209
+ if (!fs.existsSync(src) || !fs.statSync(src).isFile())
210
+ return;
211
+ let buf = fs.readFileSync(src);
212
+ if (typeof opts.transform === 'function') {
213
+ buf = opts.transform(buf);
214
+ }
215
+ const digest = sha256Hex(buf);
216
+ const ext = path.extname(logical) || '';
217
+ const assetRel = `assets/${digest}${ext}`;
218
+ const dest = path.join(absDist, ...assetRel.split('/'));
219
+ if (!fs.existsSync(dest)) {
220
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
221
+ fs.writeFileSync(dest, buf);
222
+ }
223
+ else {
224
+ const existing = sha256Hex(fs.readFileSync(dest));
225
+ if (existing !== digest) {
226
+ // Stale assets/ from a prior partial build can reuse hash filenames with different bytes.
227
+ fs.writeFileSync(dest, buf);
228
+ }
229
+ }
230
+ objects.push({
231
+ logicalPath: logical,
232
+ assetPath: assetRel,
233
+ digest,
234
+ bytes: buf.length,
235
+ immutable: true,
236
+ });
237
+ rewrites[`/${logical}`] = `/${assetRel}`;
238
+ rewrites[logical] = assetRel;
239
+ }
91
240
  /**
92
241
  * Resolve an immutable object by digest under dist/assets (cross-source reuse).
93
242
  * @param {string} distDir
@@ -123,7 +272,6 @@ export function assertSharedAssetPath(distDir, a, b, ext = '.js') {
123
272
  const rel = `assets/${da}${ext}`;
124
273
  const dest = path.join(distDir, ...rel.split('/'));
125
274
  fs.writeFileSync(dest, typeof a === 'string' ? Buffer.from(a) : a);
126
- // Second write of identical bytes must be reuse, not fork.
127
275
  fs.writeFileSync(dest, typeof b === 'string' ? Buffer.from(b) : b);
128
276
  const again = resolveAssetByDigest(distDir, da, ext);
129
277
  if (!again || again.assetPath !== rel) {
@@ -138,7 +286,6 @@ function collectCandidates(distDir) {
138
286
  if (fs.existsSync(path.join(distDir, name)))
139
287
  out.push(name);
140
288
  }
141
- // Include top-level *.client.js and pages/**/*.client.js referenced by resume.
142
289
  walk(distDir, distDir, (rel) => {
143
290
  if (/\.client\.js$/i.test(rel))
144
291
  out.push(rel);
@@ -172,7 +319,6 @@ function rewriteHtmlReferences(distDir, rewrites) {
172
319
  let text = fs.readFileSync(file, 'utf8');
173
320
  let next = text;
174
321
  for (const [from, to] of pairs) {
175
- // href="/x" src="/x" and unquoted variants in attributes
176
322
  next = next.split(from).join(to);
177
323
  }
178
324
  if (next !== text) {