@vmz/vmz 0.1.12 → 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.
@@ -8,6 +8,8 @@ import { semanticIdsForAssembly, sha256Hex, canonicalJson } from './delivery-pro
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';
12
14
  import { writePrettyJsonFile } from './pretty-json.js';
13
15
  export const BUILD_PROOF_SCHEMA = 'vmz.build.proof.v0';
@@ -26,10 +28,13 @@ export async function assembleDelivery(outDir, ctx) {
26
28
  serverRuntime: selection.serverRuntime || null,
27
29
  steps: [],
28
30
  };
29
- if (assembly === 'static-cdn' || assembly === 'cdn+server') {
30
- 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
+ });
31
36
  result.steps.push({
32
- kind: 'static-cdn',
37
+ kind: 'web-static',
33
38
  digest: staticResult.digest,
34
39
  htmlFiles: staticResult.htmlFiles?.length ?? 0,
35
40
  skipped: staticResult.skipped?.length ?? 0,
@@ -40,6 +45,11 @@ export async function assembleDelivery(outDir, ctx) {
40
45
  skipped: staticResult.skipped,
41
46
  };
42
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
+ }
43
53
  if (assembly === 'local-static') {
44
54
  result.steps.push({
45
55
  kind: 'local-static',
@@ -124,7 +134,7 @@ export function emitBuildProof(outDir, ctx) {
124
134
  };
125
135
  for (const id of semanticIds) {
126
136
  if (id === 'static-delivery') {
127
- const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'static-cdn');
137
+ const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'web-static');
128
138
  slots[id] = step
129
139
  ? { status: 'emitted', detail: `digest=${String(step.digest).slice(0, 12)}` }
130
140
  : { status: 'pending', detail: 'assembly requires static emit' };
package/dist/cli.js CHANGED
@@ -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
  /**
@@ -121,11 +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
126
  --profile <name> Delivery profile (default from config; builtins: web-ssr|static|web-client|web-hybrid)
127
127
  --target <id> browser (default) | mini-program-wechat (pack dist/wechat for WeChat DevTools; build+dev)
128
- --origin <url> Site origin for static-cdn canonical/sitemap
128
+ --origin <url> Site origin for web-static canonical/sitemap
129
129
  --host <host> Listen host (default: 127.0.0.1)
130
130
  --port <port> Listen port (dev: omit = auto from 5173; set = lock)
131
131
  --poll-ms <ms> Dev watch poll interval (default: 300)
@@ -339,28 +339,29 @@ async function cmdBuild(args) {
339
339
  return 1;
340
340
  }
341
341
  const wechatPack = targetRaw === 'mini-program-wechat';
342
- const { project, outDir } = resolveWorkspaceDirs({
342
+ const { project, outDir: outDirRoot } = resolveWorkspaceDirs({
343
343
  path: pathArg,
344
344
  outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
345
345
  });
346
- log.info(`build ${project} ${outDir}${wechatPack ? ' (target=mini-program-wechat)' : ''}`);
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})`);
347
362
  const ws = createWorkspace({ root: project, outDir });
348
363
  try {
349
- const cfg = await loadVmzConfig(project);
350
- const cliProfile = typeof args.profile === 'string' ? args.profile : '';
351
- const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
352
- if (!norm.ok) {
353
- log.diagnostics(norm.diagnostics ?? []);
354
- log.error('delivery authoring invalid');
355
- return 1;
356
- }
357
- const selected = selectBuildProfile(norm.table, cliProfile);
358
- if (!selected.ok) {
359
- log.diagnostics(selected.diagnostics ?? []);
360
- log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
361
- return 1;
362
- }
363
- 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})`);
364
365
  const code = await runWithPlugins(ws, project, outDir, () => {
365
366
  const report = ws.build(Boolean(args.release));
366
367
  const errors = log.diagnostics(report.diagnostics ?? []);
@@ -449,7 +450,7 @@ async function cmdBuild(args) {
449
450
  const origin = typeof args.origin === 'string' ? args.origin : undefined;
450
451
  let assemble = null;
451
452
  try {
452
- if (selected.selection.assembly === 'static-cdn') {
453
+ if (selected.selection.assembly === 'web-static') {
453
454
  log.info(`static emit ${outDir}`);
454
455
  }
455
456
  assemble = await assembleDelivery(outDir, {
@@ -461,9 +462,10 @@ async function cmdBuild(args) {
461
462
  siteId: cfg.application?.id || undefined,
462
463
  origin,
463
464
  pack: pack.manifest,
465
+ projectRoot: project,
464
466
  });
465
467
  for (const step of assemble.manifest.steps || []) {
466
- if (step.kind === 'static-cdn') {
468
+ if (step.kind === 'web-static') {
467
469
  log.info(`static ok (${step.htmlFiles} html, ${step.skipped} skipped, digest=${String(step.digest).slice(0, 12)}…)`);
468
470
  }
469
471
  else if (step.kind === 'site-delivery' && step.digest) {
@@ -495,10 +497,25 @@ async function cmdServe(args) {
495
497
  if (code !== 0)
496
498
  return code;
497
499
  }
498
- const { project, outDir } = resolveWorkspaceDirs({
500
+ const { project, outDir: outDirRoot } = resolveWorkspaceDirs({
499
501
  path: pathArg,
500
502
  outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
501
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);
502
519
  const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
503
520
  if (!existsSync(hostJs)) {
504
521
  try {
@@ -521,6 +538,7 @@ async function cmdServe(args) {
521
538
  VMZ_DIST: outDir,
522
539
  VMZ_PORT: String(port),
523
540
  VMZ_HOST: host,
541
+ VMZ_PROJECT_ROOT: project,
524
542
  VMZ_NATIVE_NODE: resolveNativePath(),
525
543
  },
526
544
  stdio: 'inherit',
@@ -546,10 +564,25 @@ async function cmdServe(args) {
546
564
  */
547
565
  async function cmdDev(args) {
548
566
  const pathArg = args._[0] ?? '.';
549
- const { project, outDir } = resolveWorkspaceDirs({
567
+ const { project, outDir: outDirRoot } = resolveWorkspaceDirs({
550
568
  path: pathArg,
551
569
  outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
552
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);
553
586
  const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
554
587
  const portLocked = Object.prototype.hasOwnProperty.call(args, 'port');
555
588
  let port;
@@ -2,6 +2,8 @@
2
2
  * A3: content-addressed assets/<hash> layout for immutable CDN objects.
3
3
  * Logical paths stay available for serve/dev; static HTML rewrites to hashed URLs.
4
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";
7
9
  /**
@@ -10,6 +12,19 @@ export declare const CONTENT_ADDRESSED_ASSETS_SCHEMA = "vmz.content_addressed_as
10
12
  * @param {Record<string, string>} rewrites logical (no leading slash) or `/logical` → `assets/hash.ext`
11
13
  */
12
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;
13
28
  /**
14
29
  * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
15
30
  * @param {string} distDir
@@ -2,6 +2,8 @@
2
2
  * A3: content-addressed assets/<hash> layout for immutable CDN objects.
3
3
  * Logical paths stay available for serve/dev; static HTML rewrites to hashed URLs.
4
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';
@@ -24,6 +26,8 @@ const DEFAULT_CANDIDATES = [
24
26
  ];
25
27
  /** CSS files that may @import other logical CSS; processed after leaf CSS is hashed. */
26
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']);
27
31
  const CSS_IMPORT_RE = /@import\s*(?:url\()?['"]?(\.\/)?([^'")\s;]+)['"]?\)?/gi;
28
32
  /**
29
33
  * Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
@@ -43,6 +47,35 @@ export function rewriteCssImports(cssText, rewrites) {
43
47
  return `@import"${sibling}"`;
44
48
  });
45
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
+ }
46
79
  /**
47
80
  * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
48
81
  * @param {string} distDir
@@ -62,6 +95,22 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
62
95
  const rewrites = {};
63
96
  const ordered = orderCandidates(candidates);
64
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 });
102
+ continue;
103
+ }
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;
113
+ }
65
114
  ingestCandidate(abs, rel, rewrites, objects, { transform: null });
66
115
  }
67
116
  // Aggregator CSS (vmz.css) must import hashed leaf files — rewrite then hash.
@@ -79,6 +128,21 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
79
128
  transform: () => Buffer.from(rewritten, 'utf8'),
80
129
  });
81
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'),
144
+ });
145
+ }
82
146
  objects.sort((a, b) => (a.logicalPath < b.logicalPath ? -1 : a.logicalPath > b.logicalPath ? 1 : 0));
83
147
  let rewrittenHtml = 0;
84
148
  if (opts.rewriteHtml !== false) {
@@ -107,15 +171,20 @@ function orderCandidates(candidates) {
107
171
  /** @type {string[]} */
108
172
  const out = [];
109
173
  for (const name of DEFAULT_CANDIDATES) {
110
- if (set.has(name) && !CSS_AGGREGATORS.has(name))
174
+ if (set.has(name) && !CSS_AGGREGATORS.has(name) && !JS_ENTRY_AGGREGATORS.has(name))
111
175
  out.push(name);
112
176
  }
113
177
  for (const name of [...set].sort()) {
114
- if (!CSS_AGGREGATORS.has(name) && !out.includes(name))
178
+ if (!CSS_AGGREGATORS.has(name) && !JS_ENTRY_AGGREGATORS.has(name) && !out.includes(name)) {
115
179
  out.push(name);
180
+ }
116
181
  }
117
182
  if (set.has('vmz.css'))
118
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
+ }
119
188
  return out;
120
189
  }
121
190
  /**
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export declare const DELIVERY_PROFILE_AUTHORING_SCHEMA = "vmz.delivery.authoring.v0";
6
6
  export declare const BUILD_PROFILE_SELECTION_SCHEMA = "vmz.build.profile_selection.v0";
7
- /** Browser-era assembly kinds (04 B5). */
7
+ /** Browser-era assembly kinds (04 B5). `static-cdn` was renamed to `web-static`. */
8
8
  export declare const ASSEMBLIES: readonly string[];
9
9
  export declare const SERVER_RUNTIMES: readonly string[];
10
10
  /** Official built-in aliases when not overridden in config. */
@@ -28,6 +28,22 @@ export declare const BUILTIN_PROFILES: Readonly<{
28
28
  serverRuntime: string;
29
29
  };
30
30
  }>;
31
+ /**
32
+ * Profile artifact directory name under CLI `--out-dir` (default = profile id).
33
+ * @param {string} id
34
+ * @param {unknown} rawName
35
+ * @param {Array<{ code: string, message: string }>} diagnostics
36
+ * @returns {string | null}
37
+ */
38
+ export declare function normalizeProfileArtifactName(id: any, rawName: any, diagnostics: any): string;
39
+ /**
40
+ * Workspace `--out-dir` + profile `name` → artifact root.
41
+ * Always nests: `path.join(outDir, name)` where `name` defaults to profile id
42
+ * (`name: 'cdn'` → `dist/cdn`; omit → `dist/static` for profile `static`).
43
+ * @param {string} outDir
44
+ * @param {{ name?: string, id?: string } | null | undefined} profile
45
+ */
46
+ export declare function resolveProfileArtifactDir(outDir: any, profile: any): any;
31
47
  export declare function pickSiteAuthoring(raw: any): {
32
48
  artifact: string;
33
49
  sources: any;
@@ -69,6 +85,8 @@ export declare function selectBuildProfile(table: any, cliProfile?: string): {
69
85
  selection: {
70
86
  schema: string;
71
87
  profileId: any;
88
+ name: any;
89
+ nameExplicit: boolean;
72
90
  host: any;
73
91
  assembly: any;
74
92
  serverRuntime: any;
@@ -4,18 +4,67 @@
4
4
  */
5
5
  // @ts-nocheck
6
6
  import crypto from 'node:crypto';
7
+ import path from 'node:path';
7
8
  export const DELIVERY_PROFILE_AUTHORING_SCHEMA = 'vmz.delivery.authoring.v0';
8
9
  export const BUILD_PROFILE_SELECTION_SCHEMA = 'vmz.build.profile_selection.v0';
9
- /** Browser-era assembly kinds (04 B5). */
10
- export const ASSEMBLIES = Object.freeze(['local-static', 'static-cdn', 'server-host', 'cdn+server', 'rust-embedded']);
10
+ /** Browser-era assembly kinds (04 B5). `static-cdn` was renamed to `web-static`. */
11
+ export const ASSEMBLIES = Object.freeze(['local-static', 'web-static', 'server-host', 'cdn+server', 'rust-embedded']);
11
12
  export const SERVER_RUNTIMES = Object.freeze(['node', 'worker', 'deno', 'bun', 'rust-host']);
12
13
  /** Official built-in aliases when not overridden in config. */
13
14
  export const BUILTIN_PROFILES = Object.freeze({
14
15
  'web-client': { host: 'browser', assembly: 'local-static' },
15
- static: { host: 'browser', assembly: 'static-cdn' },
16
+ static: { host: 'browser', assembly: 'web-static' },
16
17
  'web-ssr': { host: 'browser', assembly: 'server-host', serverRuntime: 'node' },
17
18
  'web-hybrid': { host: 'browser', assembly: 'cdn+server', serverRuntime: 'node' },
18
19
  });
20
+ /**
21
+ * Profile artifact directory name under CLI `--out-dir` (default = profile id).
22
+ * @param {string} id
23
+ * @param {unknown} rawName
24
+ * @param {Array<{ code: string, message: string }>} diagnostics
25
+ * @returns {string | null}
26
+ */
27
+ export function normalizeProfileArtifactName(id, rawName, diagnostics) {
28
+ const fallback = String(id || '').trim();
29
+ if (rawName == null || rawName === '')
30
+ return fallback || null;
31
+ if (typeof rawName !== 'string') {
32
+ diagnostics.push({
33
+ code: 'delivery.profile.name',
34
+ message: `profiles.${id}.name must be a string (got ${typeof rawName})`,
35
+ });
36
+ return null;
37
+ }
38
+ const name = rawName.trim();
39
+ if (!name) {
40
+ diagnostics.push({
41
+ code: 'delivery.profile.name',
42
+ message: `profiles.${id}.name must be a non-empty string`,
43
+ });
44
+ return null;
45
+ }
46
+ if (name === '.' || name === '..' || name.includes('/') || name.includes('\\') || name.includes('\0')) {
47
+ diagnostics.push({
48
+ code: 'delivery.profile.name',
49
+ message: `profiles.${id}.name must be a single path segment under --out-dir (got '${name}')`,
50
+ });
51
+ return null;
52
+ }
53
+ return name;
54
+ }
55
+ /**
56
+ * Workspace `--out-dir` + profile `name` → artifact root.
57
+ * Always nests: `path.join(outDir, name)` where `name` defaults to profile id
58
+ * (`name: 'cdn'` → `dist/cdn`; omit → `dist/static` for profile `static`).
59
+ * @param {string} outDir
60
+ * @param {{ name?: string, id?: string } | null | undefined} profile
61
+ */
62
+ export function resolveProfileArtifactDir(outDir, profile) {
63
+ const name = String(profile?.name || profile?.id || '').trim();
64
+ if (!name)
65
+ return outDir;
66
+ return path.join(outDir, name);
67
+ }
19
68
  function isPlainObject(v) {
20
69
  return v != null && typeof v === 'object' && !Array.isArray(v);
21
70
  }
@@ -122,7 +171,14 @@ function normalizeProfileEntry(entry, id, diagnostics) {
122
171
  message: `profiles.${id}.host: only 'browser' is supported before Browser Production (got ${host})`,
123
172
  });
124
173
  }
125
- const assembly = String(entry.assembly || '').trim();
174
+ let assembly = String(entry.assembly || '').trim();
175
+ if (assembly === 'static-cdn') {
176
+ diagnostics.push({
177
+ code: 'delivery.profile.assembly.renamed',
178
+ message: `profiles.${id}.assembly 'static-cdn' was renamed to 'web-static'`,
179
+ });
180
+ return null;
181
+ }
126
182
  if (!ASSEMBLIES.includes(assembly)) {
127
183
  diagnostics.push({
128
184
  code: 'delivery.profile.assembly',
@@ -130,6 +186,10 @@ function normalizeProfileEntry(entry, id, diagnostics) {
130
186
  });
131
187
  return null;
132
188
  }
189
+ const nameExplicit = entry.name != null && String(entry.name).trim() !== '';
190
+ const name = normalizeProfileArtifactName(id, nameExplicit ? entry.name : id, diagnostics);
191
+ if (!name)
192
+ return null;
133
193
  let serverRuntime = null;
134
194
  if (assembly === 'server-host' || assembly === 'cdn+server') {
135
195
  serverRuntime = String(entry.serverRuntime || 'node');
@@ -171,6 +231,9 @@ function normalizeProfileEntry(entry, id, diagnostics) {
171
231
  }
172
232
  return {
173
233
  id,
234
+ name,
235
+ /** True when author set `profiles.<id>.name`; false → `name` defaulted to profile id. */
236
+ nameExplicit,
174
237
  host: 'browser',
175
238
  assembly,
176
239
  serverRuntime,
@@ -303,6 +366,8 @@ export function selectBuildProfile(table, cliProfile = '') {
303
366
  const selection = {
304
367
  schema: BUILD_PROFILE_SELECTION_SCHEMA,
305
368
  profileId: id,
369
+ name: profile.name,
370
+ nameExplicit: Boolean(profile.nameExplicit),
306
371
  host: profile.host,
307
372
  assembly: profile.assembly,
308
373
  serverRuntime: profile.serverRuntime,
@@ -315,7 +380,7 @@ export function selectBuildProfile(table, cliProfile = '') {
315
380
  }
316
381
  export function semanticIdsForAssembly(assembly) {
317
382
  switch (assembly) {
318
- case 'static-cdn':
383
+ case 'web-static':
319
384
  return ['static-delivery', 'asset-graph'];
320
385
  case 'server-host':
321
386
  return ['server-host', 'asset-graph'];
@@ -552,6 +552,7 @@ function defaultSpawnHost(opts) {
552
552
  VMZ_PORT: String(opts.port),
553
553
  VMZ_HOST: opts.host,
554
554
  VMZ_DEV: '1',
555
+ VMZ_PROJECT_ROOT: opts.project,
555
556
  VMZ_NATIVE_NODE: resolveNativePath(),
556
557
  },
557
558
  stdio: ['ignore', 'inherit', 'inherit'],
package/dist/index.d.ts CHANGED
@@ -166,13 +166,15 @@ export { cmdApplication, runCheck as runApplicationCheck } from './application-c
166
166
  export { cmdArtifact } from './release-cmd.js';
167
167
  export { ARTIFACT_DIFF_SCHEMA, DELIVERY_ARTIFACT_MANIFEST_SCHEMA, RELEASE_ENVELOPE_SCHEMA, ROUTE_REALIZATION_TABLE_SCHEMA, atomicWritePointer, canonicalJson, diffArtifacts, loadReleaseEnvelope, packRelease, publishRelease, readPointer, rollbackRelease, sha256File, sha256Hex, } from './release-pack.js';
168
168
  export { STATIC_DELIVERY_MANIFEST_SCHEMA, emitWebStatic, } from './static-emit.js';
169
- export { CONTENT_ADDRESSED_ASSETS_SCHEMA, emitContentAddressedAssets, resolveAssetByDigest, assertSharedAssetPath, contentAddressedAssetsDigest, } from './content-addressed-assets.js';
169
+ export { CONTENT_ADDRESSED_ASSETS_SCHEMA, emitContentAddressedAssets, resolveAssetByDigest, assertSharedAssetPath, contentAddressedAssetsDigest, rewriteCssImports, rewriteJsEntryRelativeImports, } from './content-addressed-assets.js';
170
+ export { SITE_FAVICON_SCHEMA, emitSiteFavicon, readSiteFaviconHeadHtml, packPngsIntoIco, } from './site-favicon.js';
171
+ export { PUBLIC_STATIC_ASSETS_SCHEMA, emitPublicStaticAssets, } from './public-static-assets.js';
170
172
  export { CDN_POLICY_MANIFEST_SCHEMA, CDN_ADAPTER_PROJECTION_SCHEMA, CACHE_HTML, CACHE_ASSET_IMMUTABLE, CACHE_META, buildCdnPolicyManifest, emitCdnPolicy, projectCdnAdapter, createLocalStaticHandler, listenLocalStaticHost, matchGlob, } from './cdn-policy.js';
171
173
  export { SITE_DELIVERY_CONTRACT_SCHEMA, SITE_DELIVERY_RESOLUTION_SCHEMA, defineSite, normalizeSiteDelivery, normalizeSourceProbe, resolveSiteRelease, probeReleaseDirectory, emitSiteDelivery, } from './site-delivery.js';
172
174
  export { EMBEDDED_RESOURCE_INDEX_SCHEMA, emitEmbeddedPackaging, } from './embedded-packaging.js';
173
175
  export { SERVER_LANG_IDS, SERVER_LANG_ALIASES, SERVER_LANGUAGE_BACKENDS, resolveServerLanguage, assertLangRuntimePair, } from './server-language-backend.js';
174
176
  export { WECHAT_PACKAGING_SCHEMA, WECHAT_PACKAGING_REL, wechatPackagingFromDelivery, materializeWechatPackaging, } from './wechat-packaging.js';
175
- export { DELIVERY_PROFILE_AUTHORING_SCHEMA, BUILD_PROFILE_SELECTION_SCHEMA, ASSEMBLIES, SERVER_RUNTIMES, BUILTIN_PROFILES, pickSiteAuthoring, pickDeliveryPackaging, normalizeDeliveryAuthoring, selectBuildProfile, semanticIdsForAssembly, } from './delivery-profile.js';
177
+ export { DELIVERY_PROFILE_AUTHORING_SCHEMA, BUILD_PROFILE_SELECTION_SCHEMA, ASSEMBLIES, SERVER_RUNTIMES, BUILTIN_PROFILES, pickSiteAuthoring, pickDeliveryPackaging, normalizeProfileArtifactName, resolveProfileArtifactDir, normalizeDeliveryAuthoring, selectBuildProfile, semanticIdsForAssembly, } from './delivery-profile.js';
176
178
  export { PACK_MANIFEST_SCHEMA, packFromDeploymentIr, ensureRuntimeCompanions } from './pack.js';
177
179
  export { SERVER_ARTIFACT_SCHEMA, HTTP_CONTRACT_SCHEMA, SERVER_RUNTIME_ADAPTER_SCHEMA, emitServerArtifact, projectServerRuntimeAdapter, } from './server-artifact.js';
178
180
  export { BUILD_PROOF_SCHEMA, ASSEMBLE_MANIFEST_SCHEMA, assembleDelivery, emitBuildProof, } from './build-assemble.js';
package/dist/index.js CHANGED
@@ -147,6 +147,7 @@ export function resolveCoreRuntimeDist() {
147
147
  /** Runtime companions required by dist/vmz-serve-host.mjs relative imports. */
148
148
  export const SERVE_HOST_RUNTIME_FILES = [
149
149
  ['serve-host.mjs', 'vmz-serve-host.mjs'],
150
+ ['native-addon.js', 'native-addon.js'],
150
151
  ['list-client-components.js', 'list-client-components.js'],
151
152
  ['deployment-registry.js', 'deployment-registry.js'],
152
153
  ['render-host.js', 'render-host.js'],
@@ -408,13 +409,15 @@ export { ARTIFACT_DIFF_SCHEMA, DELIVERY_ARTIFACT_MANIFEST_SCHEMA, RELEASE_ENVELO
408
409
  // APPLICATION_ARTIFACT_SCHEMA lives in @vmz/protocol (already re-exported above);
409
410
  // release-pack keeps a local constant for envelope writes — do not dual-export the name.
410
411
  export { STATIC_DELIVERY_MANIFEST_SCHEMA, emitWebStatic, } from './static-emit.js';
411
- export { CONTENT_ADDRESSED_ASSETS_SCHEMA, emitContentAddressedAssets, resolveAssetByDigest, assertSharedAssetPath, contentAddressedAssetsDigest, } from './content-addressed-assets.js';
412
+ export { CONTENT_ADDRESSED_ASSETS_SCHEMA, emitContentAddressedAssets, resolveAssetByDigest, assertSharedAssetPath, contentAddressedAssetsDigest, rewriteCssImports, rewriteJsEntryRelativeImports, } from './content-addressed-assets.js';
413
+ export { SITE_FAVICON_SCHEMA, emitSiteFavicon, readSiteFaviconHeadHtml, packPngsIntoIco, } from './site-favicon.js';
414
+ export { PUBLIC_STATIC_ASSETS_SCHEMA, emitPublicStaticAssets, } from './public-static-assets.js';
412
415
  export { CDN_POLICY_MANIFEST_SCHEMA, CDN_ADAPTER_PROJECTION_SCHEMA, CACHE_HTML, CACHE_ASSET_IMMUTABLE, CACHE_META, buildCdnPolicyManifest, emitCdnPolicy, projectCdnAdapter, createLocalStaticHandler, listenLocalStaticHost, matchGlob, } from './cdn-policy.js';
413
416
  export { SITE_DELIVERY_CONTRACT_SCHEMA, SITE_DELIVERY_RESOLUTION_SCHEMA, defineSite, normalizeSiteDelivery, normalizeSourceProbe, resolveSiteRelease, probeReleaseDirectory, emitSiteDelivery, } from './site-delivery.js';
414
417
  export { EMBEDDED_RESOURCE_INDEX_SCHEMA, emitEmbeddedPackaging, } from './embedded-packaging.js';
415
418
  export { SERVER_LANG_IDS, SERVER_LANG_ALIASES, SERVER_LANGUAGE_BACKENDS, resolveServerLanguage, assertLangRuntimePair, } from './server-language-backend.js';
416
419
  export { WECHAT_PACKAGING_SCHEMA, WECHAT_PACKAGING_REL, wechatPackagingFromDelivery, materializeWechatPackaging, } from './wechat-packaging.js';
417
- export { DELIVERY_PROFILE_AUTHORING_SCHEMA, BUILD_PROFILE_SELECTION_SCHEMA, ASSEMBLIES, SERVER_RUNTIMES, BUILTIN_PROFILES, pickSiteAuthoring, pickDeliveryPackaging, normalizeDeliveryAuthoring, selectBuildProfile, semanticIdsForAssembly, } from './delivery-profile.js';
420
+ export { DELIVERY_PROFILE_AUTHORING_SCHEMA, BUILD_PROFILE_SELECTION_SCHEMA, ASSEMBLIES, SERVER_RUNTIMES, BUILTIN_PROFILES, pickSiteAuthoring, pickDeliveryPackaging, normalizeProfileArtifactName, resolveProfileArtifactDir, normalizeDeliveryAuthoring, selectBuildProfile, semanticIdsForAssembly, } from './delivery-profile.js';
418
421
  export { PACK_MANIFEST_SCHEMA, packFromDeploymentIr, ensureRuntimeCompanions } from './pack.js';
419
422
  export { SERVER_ARTIFACT_SCHEMA, HTTP_CONTRACT_SCHEMA, SERVER_RUNTIME_ADAPTER_SCHEMA, emitServerArtifact, projectServerRuntimeAdapter, } from './server-artifact.js';
420
423
  export { BUILD_PROOF_SCHEMA, ASSEMBLE_MANIFEST_SCHEMA, assembleDelivery, emitBuildProof, } from './build-assemble.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Opaque site-root static files: project `public/**` → deploy root (static / web-static).
3
+ * Not dependency packaging (no node_modules / @dxo/* scanning).
4
+ */
5
+ export declare const PUBLIC_STATIC_ASSETS_SCHEMA = "vmz.public_static_assets.v0";
6
+ /**
7
+ * @param {string} distDir
8
+ * @param {{ projectRoot?: string, publicDir?: string }} [opts]
9
+ */
10
+ export declare function emitPublicStaticAssets(distDir: any, opts?: {}): {
11
+ schema: string;
12
+ status: string;
13
+ reason: string;
14
+ fileCount: number;
15
+ files: any[];
16
+ skippedConflicts: any[];
17
+ } | {
18
+ schema: string;
19
+ status: string;
20
+ source: string;
21
+ fileCount: number;
22
+ files: any[];
23
+ skippedConflicts: any[];
24
+ };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Opaque site-root static files: project `public/**` → deploy root (static / web-static).
3
+ * Not dependency packaging (no node_modules / @dxo/* scanning).
4
+ */
5
+ // @ts-nocheck
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { writePrettyJsonFile } from './pretty-json.js';
9
+ export const PUBLIC_STATIC_ASSETS_SCHEMA = 'vmz.public_static_assets.v0';
10
+ /** Relative paths under dist that VMZ owns; public/ must not clobber. */
11
+ const RESERVED_EXACT = new Set([
12
+ 'entry-client.js',
13
+ 'entry-event.js',
14
+ 'vmz-dom.js',
15
+ 'vmz-runtime.js',
16
+ 'vmz-http.js',
17
+ 'vmz-client-nav.js',
18
+ 'vmz.css',
19
+ 'vmz-designs.css',
20
+ 'vmz-style.css',
21
+ 'vmz-deployment.json',
22
+ 'vmz-routes.json',
23
+ 'vmz-plugin-targets.json',
24
+ 'sitemap.xml',
25
+ 'robots.txt',
26
+ '404.html',
27
+ ]);
28
+ /**
29
+ * @param {string} distDir
30
+ * @param {{ projectRoot?: string, publicDir?: string }} [opts]
31
+ */
32
+ export function emitPublicStaticAssets(distDir, opts = {}) {
33
+ const absDist = path.resolve(distDir);
34
+ const projectRoot = opts.projectRoot ? path.resolve(opts.projectRoot) : null;
35
+ const publicDir = opts.publicDir ? path.resolve(opts.publicDir) : projectRoot ? path.join(projectRoot, 'public') : null;
36
+ if (!publicDir || !fs.existsSync(publicDir) || !fs.statSync(publicDir).isDirectory()) {
37
+ const skipped = {
38
+ schema: PUBLIC_STATIC_ASSETS_SCHEMA,
39
+ status: 'skipped',
40
+ reason: publicDir ? 'public/ missing' : 'no projectRoot',
41
+ fileCount: 0,
42
+ files: [],
43
+ skippedConflicts: [],
44
+ };
45
+ writeArtifact(absDist, skipped);
46
+ return skipped;
47
+ }
48
+ /** @type {Array<{ path: string, bytes: number }>} */
49
+ const files = [];
50
+ /** @type {Array<{ path: string, reason: string }>} */
51
+ const skippedConflicts = [];
52
+ walkFiles(publicDir, (absFile) => {
53
+ const rel = path.relative(publicDir, absFile).replace(/\\/g, '/');
54
+ if (!rel || rel.startsWith('..'))
55
+ return;
56
+ if (shouldSkipReserved(rel)) {
57
+ skippedConflicts.push({ path: rel, reason: 'reserved-vmz-path' });
58
+ return;
59
+ }
60
+ const dest = path.join(absDist, ...rel.split('/'));
61
+ if (fs.existsSync(dest) && isReservedExisting(rel, dest, absDist)) {
62
+ skippedConflicts.push({ path: rel, reason: 'would-clobber-generated' });
63
+ return;
64
+ }
65
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
66
+ fs.copyFileSync(absFile, dest);
67
+ files.push({ path: rel, bytes: fs.statSync(dest).size });
68
+ });
69
+ files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
70
+ const artifact = {
71
+ schema: PUBLIC_STATIC_ASSETS_SCHEMA,
72
+ status: 'ready',
73
+ source: 'public/',
74
+ fileCount: files.length,
75
+ files,
76
+ skippedConflicts,
77
+ };
78
+ writeArtifact(absDist, artifact);
79
+ return artifact;
80
+ }
81
+ /**
82
+ * @param {string} rel
83
+ */
84
+ function shouldSkipReserved(rel) {
85
+ if (rel === '_vmz' || rel.startsWith('_vmz/'))
86
+ return true;
87
+ if (RESERVED_EXACT.has(rel))
88
+ return true;
89
+ if (rel.startsWith('pages/') || rel.startsWith('components/'))
90
+ return true;
91
+ if (/\.client\.js$/i.test(rel))
92
+ return true;
93
+ if (rel.startsWith('assets/') && /^assets\/[a-f0-9]{64}\./i.test(rel))
94
+ return true;
95
+ return false;
96
+ }
97
+ /**
98
+ * After compile, many generated files already exist; do not let public overwrite them.
99
+ * Allow overwrite of non-reserved paths (e.g. re-copy same wasm).
100
+ * @param {string} rel
101
+ * @param {string} dest
102
+ * @param {string} absDist
103
+ */
104
+ function isReservedExisting(rel, dest, absDist) {
105
+ if (shouldSkipReserved(rel))
106
+ return true;
107
+ // Never overwrite generated HTML shells that static-emit will (or did) write.
108
+ if (/\.html$/i.test(rel) && fs.existsSync(dest)) {
109
+ const text = fs.readFileSync(dest, 'utf8');
110
+ if (text.includes('data-vmz-page') || text.includes('entry-client'))
111
+ return true;
112
+ }
113
+ void absDist;
114
+ return false;
115
+ }
116
+ /**
117
+ * @param {string} dir
118
+ * @param {(absFile: string) => void} onFile
119
+ */
120
+ function walkFiles(dir, onFile) {
121
+ for (const name of fs.readdirSync(dir)) {
122
+ if (name === '.' || name === '..')
123
+ continue;
124
+ const full = path.join(dir, name);
125
+ const st = fs.statSync(full);
126
+ if (st.isDirectory())
127
+ walkFiles(full, onFile);
128
+ else if (st.isFile())
129
+ onFile(full);
130
+ }
131
+ }
132
+ /**
133
+ * @param {string} absDist
134
+ * @param {Record<string, unknown>} artifact
135
+ */
136
+ function writeArtifact(absDist, artifact) {
137
+ const vmzDir = path.join(absDist, '_vmz');
138
+ fs.mkdirSync(vmzDir, { recursive: true });
139
+ writePrettyJsonFile(path.join(vmzDir, 'public-static-assets.json'), artifact);
140
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Site favicon: author SVG → PNG + ICO + `_vmz/site-favicon.json` head links.
3
+ * Convention: `assets/favicon.svg` (or `favicon.svg`) under project / already in dist.
4
+ */
5
+ export declare const SITE_FAVICON_SCHEMA = "vmz.site_favicon.v0";
6
+ /**
7
+ * @param {string} distDir
8
+ * @param {{ projectRoot?: string, skipNative?: boolean }} [opts]
9
+ */
10
+ export declare function emitSiteFavicon(distDir: any, opts?: {}): {
11
+ schema: string;
12
+ status: string;
13
+ source: string;
14
+ svg: string;
15
+ ico: string;
16
+ png: string[];
17
+ headHtml: string;
18
+ } | {
19
+ status: string;
20
+ reason: string;
21
+ };
22
+ /**
23
+ * Read ready head HTML for static/SSR shells (empty when missing).
24
+ * @param {string} distDir
25
+ */
26
+ export declare function readSiteFaviconHeadHtml(distDir: any): any;
27
+ /**
28
+ * PNG-in-ICO (Vista+); widely accepted by modern browsers.
29
+ * @param {Array<{ png: Buffer, size: number }>} images
30
+ */
31
+ export declare function packPngsIntoIco(images: any): Buffer<ArrayBuffer>;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Site favicon: author SVG → PNG + ICO + `_vmz/site-favicon.json` head links.
3
+ * Convention: `assets/favicon.svg` (or `favicon.svg`) under project / already in dist.
4
+ */
5
+ // @ts-nocheck
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { requireNativeAddon } from './native-addon.js';
9
+ import { writePrettyJsonFile } from './pretty-json.js';
10
+ export const SITE_FAVICON_SCHEMA = 'vmz.site_favicon.v0';
11
+ const ICO_SIZES = [16, 32, 48];
12
+ /**
13
+ * @param {string} distDir
14
+ * @param {{ projectRoot?: string, skipNative?: boolean }} [opts]
15
+ */
16
+ export function emitSiteFavicon(distDir, opts = {}) {
17
+ const absDist = path.resolve(distDir);
18
+ const projectRoot = opts.projectRoot ? path.resolve(opts.projectRoot) : null;
19
+ const src = discoverFaviconSvg(absDist, projectRoot);
20
+ if (!src) {
21
+ return { status: 'skipped', reason: 'no assets/favicon.svg' };
22
+ }
23
+ const svgText = fs.readFileSync(src.path, 'utf8');
24
+ const assetsDir = path.join(absDist, 'assets');
25
+ fs.mkdirSync(assetsDir, { recursive: true });
26
+ const svgDest = path.join(assetsDir, 'favicon.svg');
27
+ if (path.resolve(src.path) !== path.resolve(svgDest)) {
28
+ fs.copyFileSync(src.path, svgDest);
29
+ }
30
+ /** @type {Buffer[]} */
31
+ const pngs = [];
32
+ if (!opts.skipNative) {
33
+ const native = requireNativeAddon();
34
+ if (typeof native.rasterizeSvgPng !== 'function') {
35
+ throw new Error('vmz native addon missing rasterizeSvgPng — rebuild with `pnpm napi:build`');
36
+ }
37
+ for (const px of ICO_SIZES) {
38
+ const buf = Buffer.from(native.rasterizeSvgPng(svgText, px));
39
+ pngs.push(buf);
40
+ fs.writeFileSync(path.join(assetsDir, `favicon-${px}.png`), buf);
41
+ }
42
+ }
43
+ let icoPath = null;
44
+ if (pngs.length) {
45
+ const ico = packPngsIntoIco(pngs.map((png, i) => ({ png, size: ICO_SIZES[i] })));
46
+ icoPath = path.join(absDist, 'favicon.ico');
47
+ fs.writeFileSync(icoPath, ico);
48
+ }
49
+ const headParts = [];
50
+ if (icoPath) {
51
+ headParts.push('<link rel="icon" href="/favicon.ico" sizes="any">');
52
+ }
53
+ headParts.push('<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">');
54
+ for (const px of ICO_SIZES) {
55
+ if (fs.existsSync(path.join(assetsDir, `favicon-${px}.png`))) {
56
+ headParts.push(`<link rel="icon" type="image/png" sizes="${px}x${px}" href="/assets/favicon-${px}.png">`);
57
+ }
58
+ }
59
+ const headHtml = `${headParts.join('\n')}\n`;
60
+ const artifact = {
61
+ schema: SITE_FAVICON_SCHEMA,
62
+ status: 'ready',
63
+ source: src.rel,
64
+ svg: 'assets/favicon.svg',
65
+ ico: icoPath ? 'favicon.ico' : null,
66
+ png: ICO_SIZES.filter((px) => fs.existsSync(path.join(assetsDir, `favicon-${px}.png`))).map((px) => `assets/favicon-${px}.png`),
67
+ headHtml,
68
+ };
69
+ const vmzDir = path.join(absDist, '_vmz');
70
+ fs.mkdirSync(vmzDir, { recursive: true });
71
+ writePrettyJsonFile(path.join(vmzDir, 'site-favicon.json'), artifact);
72
+ return artifact;
73
+ }
74
+ /**
75
+ * Read ready head HTML for static/SSR shells (empty when missing).
76
+ * @param {string} distDir
77
+ */
78
+ export function readSiteFaviconHeadHtml(distDir) {
79
+ try {
80
+ const p = path.join(distDir, '_vmz', 'site-favicon.json');
81
+ if (!fs.existsSync(p))
82
+ return '';
83
+ const j = JSON.parse(fs.readFileSync(p, 'utf8'));
84
+ if (j?.status !== 'ready' || typeof j.headHtml !== 'string')
85
+ return '';
86
+ return j.headHtml;
87
+ }
88
+ catch {
89
+ return '';
90
+ }
91
+ }
92
+ /**
93
+ * @param {string} distDir
94
+ * @param {string|null} projectRoot
95
+ */
96
+ function discoverFaviconSvg(distDir, projectRoot) {
97
+ const candidates = [path.join(distDir, 'assets', 'favicon.svg'), path.join(distDir, 'favicon.svg')];
98
+ if (projectRoot) {
99
+ candidates.push(path.join(projectRoot, 'assets', 'favicon.svg'), path.join(projectRoot, 'favicon.svg'), path.join(projectRoot, 'public', 'assets', 'favicon.svg'), path.join(projectRoot, 'public', 'favicon.svg'));
100
+ }
101
+ for (const abs of candidates) {
102
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
103
+ return {
104
+ path: abs,
105
+ rel: path.relative(projectRoot || distDir, abs).replace(/\\/g, '/') || 'assets/favicon.svg',
106
+ };
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+ /**
112
+ * PNG-in-ICO (Vista+); widely accepted by modern browsers.
113
+ * @param {Array<{ png: Buffer, size: number }>} images
114
+ */
115
+ export function packPngsIntoIco(images) {
116
+ const count = images.length;
117
+ const header = Buffer.alloc(6);
118
+ header.writeUInt16LE(0, 0);
119
+ header.writeUInt16LE(1, 2);
120
+ header.writeUInt16LE(count, 4);
121
+ const entries = Buffer.alloc(16 * count);
122
+ /** @type {Buffer[]} */
123
+ const blobs = [];
124
+ let offset = 6 + 16 * count;
125
+ for (let i = 0; i < count; i += 1) {
126
+ const { png, size } = images[i];
127
+ const o = i * 16;
128
+ entries.writeUInt8(size >= 256 ? 0 : size, o);
129
+ entries.writeUInt8(size >= 256 ? 0 : size, o + 1);
130
+ entries.writeUInt8(0, o + 2);
131
+ entries.writeUInt8(0, o + 3);
132
+ entries.writeUInt16LE(1, o + 4);
133
+ entries.writeUInt16LE(32, o + 6);
134
+ entries.writeUInt32LE(png.length, o + 8);
135
+ entries.writeUInt32LE(offset, o + 12);
136
+ blobs.push(png);
137
+ offset += png.length;
138
+ }
139
+ return Buffer.concat([header, entries, ...blobs]);
140
+ }
@@ -43,6 +43,12 @@ export declare function emitWebStatic(distDir: any, opts?: {}): Promise<{
43
43
  sitemap: string;
44
44
  robots: string;
45
45
  };
46
+ publicAssets: {
47
+ schema: string;
48
+ status: string;
49
+ fileCount: number;
50
+ source: any;
51
+ };
46
52
  };
47
53
  htmlFiles: any[];
48
54
  skipped: any[];
@@ -55,6 +61,21 @@ export declare function emitWebStatic(distDir: any, opts?: {}): Promise<{
55
61
  objects: any[];
56
62
  rewrittenHtml: number;
57
63
  };
64
+ publicAssets: {
65
+ schema: string;
66
+ status: string;
67
+ reason: string;
68
+ fileCount: number;
69
+ files: any[];
70
+ skippedConflicts: any[];
71
+ } | {
72
+ schema: string;
73
+ status: string;
74
+ source: string;
75
+ fileCount: number;
76
+ files: any[];
77
+ skippedConflicts: any[];
78
+ };
58
79
  cdnPolicy: {
59
80
  schema: string;
60
81
  applicationId: any;
@@ -15,7 +15,9 @@ import { emitContentAddressedAssets } from './content-addressed-assets.js';
15
15
  import { absoluteUrl, buildLocalePageMeta, localizeBodyLinks } from './locale-router.js';
16
16
  import { requireNativeAddon } from './native-addon.js';
17
17
  import { writePrettyJsonFile } from './pretty-json.js';
18
+ import { emitPublicStaticAssets } from './public-static-assets.js';
18
19
  import { filePathPatternFromChunk, isRouteBoundaryStem, listPublicPageUnits, parsePathPattern, unitBrowserPathPattern } from './route-path.js';
20
+ import { emitSiteFavicon, readSiteFaviconHeadHtml } from './site-favicon.js';
19
21
  export const STATIC_DELIVERY_MANIFEST_SCHEMA = 'vmz.static.delivery_manifest.v0';
20
22
  /**
21
23
  * @param {string} distDir
@@ -32,6 +34,9 @@ export async function emitWebStatic(distDir, opts = {}) {
32
34
  if (!fs.existsSync(domPath)) {
33
35
  throw new Error(`emitWebStatic: missing ${domPath} — run vmz build first`);
34
36
  }
37
+ emitSiteFavicon(distDir, { projectRoot: opts.projectRoot });
38
+ const faviconHead = readSiteFaviconHeadHtml(distDir);
39
+ const publicAssets = emitPublicStaticAssets(distDir, { projectRoot: opts.projectRoot });
35
40
  const host = await createRenderHost(distDir, { strictDeployment: true, preload: 'none' });
36
41
  const { renderToString, renderToStream } = host;
37
42
  const pageCatalog = listPageClientFiles(distDir);
@@ -135,6 +140,7 @@ export async function emitWebStatic(distDir, opts = {}) {
135
140
  props,
136
141
  meta: gen.meta,
137
142
  cssEntry: readCssEntry(distDir),
143
+ headExtraHtml: faviconHead,
138
144
  });
139
145
  fs.writeFileSync(absHtml, html, 'utf8');
140
146
  generations.push({
@@ -166,6 +172,7 @@ export async function emitWebStatic(distDir, opts = {}) {
166
172
  },
167
173
  cssEntry: readCssEntry(distDir),
168
174
  isErrorDocument: true,
175
+ headExtraHtml: faviconHead,
169
176
  });
170
177
  fs.writeFileSync(path.join(distDir, '404.html'), notFoundHtml, 'utf8');
171
178
  const sitemap = buildSitemap(origin, generations);
@@ -216,6 +223,12 @@ export async function emitWebStatic(distDir, opts = {}) {
216
223
  sitemap: 'sitemap.xml',
217
224
  robots: 'robots.txt',
218
225
  },
226
+ publicAssets: {
227
+ schema: publicAssets.schema,
228
+ status: publicAssets.status,
229
+ fileCount: publicAssets.fileCount ?? 0,
230
+ source: publicAssets.source || null,
231
+ },
219
232
  };
220
233
  const digest = sha256Hex(canonicalJson(manifest));
221
234
  manifest.manifestDigest = digest;
@@ -239,6 +252,7 @@ export async function emitWebStatic(distDir, opts = {}) {
239
252
  skipped,
240
253
  digest: manifest.manifestDigest,
241
254
  assets: assets.manifest,
255
+ publicAssets,
242
256
  cdnPolicy: cdn.policy,
243
257
  cdnAdapters: cdn.adapters,
244
258
  };
@@ -503,6 +517,7 @@ function readCssEntry(distDir) {
503
517
  * meta: { title: string, description: string, canonical: string, robots: string, lang: string, dir?: string, alternates?: Array<{ hreflang: string, href: string }> },
504
518
  * cssEntry: string | null,
505
519
  * isErrorDocument?: boolean,
520
+ * headExtraHtml?: string,
506
521
  * }} input
507
522
  */
508
523
  function wrapDocument(input) {
@@ -530,6 +545,7 @@ function wrapDocument(input) {
530
545
  // napi Option<String>: omit/undefined = None; null is rejected as String
531
546
  ...(input.cssEntry ? { cssEntry: String(input.cssEntry) } : {}),
532
547
  isErrorDocument: !!input.isErrorDocument,
548
+ ...(input.headExtraHtml ? { headExtraHtml: String(input.headExtraHtml) } : {}),
533
549
  });
534
550
  }
535
551
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/vmz",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
6
6
  "license": "MIT",
@@ -48,15 +48,15 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@vmz/core": "0.1.12",
52
- "@vmz/plugin": "0.1.12",
53
- "@vmz/protocol": "0.1.12",
51
+ "@vmz/core": "0.1.14",
52
+ "@vmz/plugin": "0.1.14",
53
+ "@vmz/protocol": "0.1.14",
54
54
  "jiti": "^2.6.1",
55
55
  "json5": "^2.2.3"
56
56
  },
57
57
  "peerDependencies": {
58
- "@vmz/plugin-markdown-it": "0.1.12",
59
- "@vmz/test": "0.1.12",
58
+ "@vmz/plugin-markdown-it": "0.1.14",
59
+ "@vmz/test": "0.1.14",
60
60
  "typescript": "^5.8.3"
61
61
  },
62
62
  "peerDependenciesMeta": {
@@ -90,12 +90,12 @@
90
90
  "cli"
91
91
  ],
92
92
  "optionalDependencies": {
93
- "@vmz/vmz-win32-x64": "0.1.12",
94
- "@vmz/vmz-win32-arm64": "0.1.12",
95
- "@vmz/vmz-darwin-x64": "0.1.12",
96
- "@vmz/vmz-darwin-arm64": "0.1.12",
97
- "@vmz/vmz-linux-x64": "0.1.12",
98
- "@vmz/vmz-linux-arm64": "0.1.12"
93
+ "@vmz/vmz-win32-x64": "0.1.14",
94
+ "@vmz/vmz-win32-arm64": "0.1.14",
95
+ "@vmz/vmz-darwin-x64": "0.1.14",
96
+ "@vmz/vmz-darwin-arm64": "0.1.14",
97
+ "@vmz/vmz-linux-x64": "0.1.14",
98
+ "@vmz/vmz-linux-arm64": "0.1.14"
99
99
  },
100
100
  "publishConfig": {
101
101
  "access": "public"