@vmz/vmz 0.1.20 → 0.1.22

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.
@@ -10,7 +10,7 @@
10
10
  * 3. Soft reload failure → **auto-respawn** serve-host (fresh ESM graph), not "keep broken host".
11
11
  */
12
12
  import { spawn } from 'node:child_process';
13
- import { existsSync } from 'node:fs';
13
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
16
16
  import { createWorkspace, resolveNativePath } from './index.js';
@@ -18,6 +18,8 @@ import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
18
18
  import { log } from './log.js';
19
19
  import { coalesceRootBurst, collectDevWatchRoots, classifyWatchRoot, isDependencyPath, mergeDirtySets } from './dev-watch-roots.js';
20
20
  import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
21
+ /** Track serve-host so a failed/killed launcher cannot leave an orphan port. */
22
+ const DEV_HOST_PID = '.vmz-dev-host.pid';
21
23
  export function createDevSession(options) {
22
24
  const project = options.project;
23
25
  const outDir = options.outDir;
@@ -141,11 +143,29 @@ export function createDevSession(options) {
141
143
  if (!existsSync(src)) {
142
144
  throw new Error(`vmz dev: missing src/ under ${project}`);
143
145
  }
146
+ // Same as `vmz build`: materialize plugin sources (e.g. `<Shiki>`) before compile.
147
+ // Soft rebuilds reuse the workspace; config/plugin changes still need a process restart
148
+ // until `vmz.config.*` is watched.
149
+ {
150
+ const { loadVmzConfig, applyPlugins } = await import('./plugin-host.js');
151
+ const { plugins, engines } = await loadVmzConfig(project);
152
+ if (plugins.length) {
153
+ await applyPlugins(ws, plugins, { project, outDir, engines });
154
+ }
155
+ }
144
156
  log.info('initial build (N-API workspace, full)…');
145
157
  const initial = rebuild();
146
158
  if (!printReport(initial, 'build')) {
147
159
  throw new Error('vmz dev: initial build failed');
148
160
  }
161
+ // `vmz build` assemble copies `public/**` → outDir; soft `ws.build()` does not.
162
+ // Without this, marketing images and other opaque assets 404 under `vmz dev`.
163
+ {
164
+ const { emitPublicStaticAssets } = await import('./public-static-assets.js');
165
+ emitPublicStaticAssets(outDir, { projectRoot: project });
166
+ }
167
+ // Reclaim orphan serve-host from a previous crashed launcher (same outDir).
168
+ reclaimStaleDevHost(outDir);
149
169
  if (projectHasDocuments(project)) {
150
170
  const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
151
171
  if (!docs.ok) {
@@ -495,6 +515,7 @@ export function createDevSession(options) {
495
515
  stopped = true;
496
516
  killChild(child);
497
517
  child = null;
518
+ clearDevHostPid(outDir, null);
498
519
  try {
499
520
  ws.dispose();
500
521
  }
@@ -523,7 +544,7 @@ export function listWatchedFiles(srcDir) {
523
544
  function defaultSpawnHost(opts) {
524
545
  const hostJs = path.join(opts.outDir, 'vmz-serve-host.mjs');
525
546
  const node = process.env.VMZ_NODE || process.execPath;
526
- return spawn(node, [hostJs], {
547
+ const child = spawn(node, [hostJs], {
527
548
  cwd: opts.project,
528
549
  env: {
529
550
  ...process.env,
@@ -535,7 +556,13 @@ function defaultSpawnHost(opts) {
535
556
  VMZ_NATIVE_NODE: resolveNativePath(),
536
557
  },
537
558
  stdio: ['ignore', 'inherit', 'inherit'],
559
+ // Keep child in the same process group so launcher exit can reclaim it.
560
+ detached: false,
561
+ windowsHide: true,
538
562
  });
563
+ writeDevHostPid(opts.outDir, child.pid);
564
+ child.once('exit', () => clearDevHostPid(opts.outDir, child.pid));
565
+ return child;
539
566
  }
540
567
  /**
541
568
  * @param {string} host
@@ -559,12 +586,103 @@ async function defaultSoftReload(host, port, payload = {}) {
559
586
  function killChild(child) {
560
587
  if (!child || child.killed)
561
588
  return;
589
+ const pid = child.pid;
590
+ try {
591
+ if (process.platform === 'win32' && pid) {
592
+ // Tree-kill so orphaned grandchildren (if any) do not keep the port.
593
+ spawn('taskkill', ['/pid', String(pid), '/T', '/F'], {
594
+ stdio: 'ignore',
595
+ windowsHide: true,
596
+ });
597
+ }
598
+ else {
599
+ child.kill('SIGTERM');
600
+ }
601
+ }
602
+ catch {
603
+ try {
604
+ child.kill();
605
+ }
606
+ catch {
607
+ /* ignore */
608
+ }
609
+ }
610
+ }
611
+ function devHostPidPath(outDir) {
612
+ return path.join(outDir, DEV_HOST_PID);
613
+ }
614
+ function writeDevHostPid(outDir, pid) {
615
+ if (!outDir || !pid)
616
+ return;
617
+ try {
618
+ writeFileSync(devHostPidPath(outDir), `${pid}\n`, 'utf8');
619
+ }
620
+ catch {
621
+ /* ignore */
622
+ }
623
+ }
624
+ function clearDevHostPid(outDir, pid) {
625
+ if (!outDir)
626
+ return;
627
+ try {
628
+ const p = devHostPidPath(outDir);
629
+ if (!existsSync(p))
630
+ return;
631
+ if (pid != null) {
632
+ const cur = Number(readFileSync(p, 'utf8').trim());
633
+ if (Number.isFinite(cur) && cur !== pid)
634
+ return;
635
+ }
636
+ unlinkSync(p);
637
+ }
638
+ catch {
639
+ /* ignore */
640
+ }
641
+ }
642
+ /** Kill leftover serve-host recorded for this outDir (previous crashed `vmz dev`). */
643
+ function reclaimStaleDevHost(outDir) {
644
+ const p = devHostPidPath(outDir);
645
+ if (!existsSync(p))
646
+ return;
647
+ let pid = 0;
562
648
  try {
563
- child.kill();
649
+ pid = Number(readFileSync(p, 'utf8').trim());
650
+ }
651
+ catch {
652
+ clearDevHostPid(outDir, null);
653
+ return;
654
+ }
655
+ if (!Number.isFinite(pid) || pid <= 0) {
656
+ clearDevHostPid(outDir, null);
657
+ return;
658
+ }
659
+ if (pid === process.pid) {
660
+ clearDevHostPid(outDir, null);
661
+ return;
662
+ }
663
+ try {
664
+ process.kill(pid, 0);
665
+ }
666
+ catch {
667
+ clearDevHostPid(outDir, null);
668
+ return;
669
+ }
670
+ log.warn(`reclaiming stale serve-host pid=${pid} (prior vmz dev left an orphan)`);
671
+ try {
672
+ if (process.platform === 'win32') {
673
+ spawn('taskkill', ['/pid', String(pid), '/T', '/F'], {
674
+ stdio: 'ignore',
675
+ windowsHide: true,
676
+ });
677
+ }
678
+ else {
679
+ process.kill(pid, 'SIGTERM');
680
+ }
564
681
  }
565
682
  catch {
566
683
  /* ignore */
567
684
  }
685
+ clearDevHostPid(outDir, null);
568
686
  }
569
687
  function sleep(ms) {
570
688
  return new Promise((r) => setTimeout(r, ms));
@@ -8,6 +8,7 @@ import { buildDocuments } from './document-build.js';
8
8
  import { resolveDocumentsRoot } from './document-check.js';
9
9
  import { pageHtmlRel } from './document-enrich.js';
10
10
  import { loadLocalesRouting } from './document-routing-config.js';
11
+ import { ensureLocaleImportsRewritten } from './locale-check.js';
11
12
  import { log } from './log.js';
12
13
  import { requireNativeAddon } from './native-addon.js';
13
14
  export function projectHasDocuments(projectRoot) {
@@ -22,6 +23,13 @@ export async function buildIntegratedDocuments(opts) {
22
23
  return { ok: true, skipped: true };
23
24
  }
24
25
  try {
26
+ // Node dynamic import of compiled chrome forbids bare `#locales/*`
27
+ // (no package.json imports). Rewrite must be complete before mount.
28
+ const locales = ensureLocaleImportsRewritten(outDir);
29
+ if (!locales.ok) {
30
+ log.error(locales.error || 'locale import rewrite incomplete');
31
+ return { ok: false, error: locales.error || 'locale rewrite', pages: 0 };
32
+ }
25
33
  const result = await buildDocuments({
26
34
  projectRoot,
27
35
  outDir,
package/dist/index.js CHANGED
@@ -127,6 +127,7 @@ export const SERVE_HOST_RUNTIME_FILES = [
127
127
  ['deployment-registry.js', 'deployment-registry.js'],
128
128
  ['render-host.js', 'render-host.js'],
129
129
  ['route-layout-chain.js', 'route-layout-chain.js'],
130
+ ['localize-body-links.js', 'localize-body-links.js'],
130
131
  ];
131
132
  /** Copy serve-host + registry bootstrap modules from `@vmz/core` into app outDir. */
132
133
  export function materializeServeHostRuntime(outDir, coreDist = resolveCoreRuntimeDist()) {
@@ -80,12 +80,25 @@ export declare function emitLocaleTypedModules(report: ReturnType<typeof checkLo
80
80
  *
81
81
  * Variant pick reads `html[data-locale]` (else defaultLocale). Thin bridge until
82
82
  * host LocaleTransition reloads locale-scoped chunks (I2/I4).
83
+ *
84
+ * Contract: after this returns `ok`, `distDir` must not contain bare `#locales/`
85
+ * import specifiers (document SSR / Node import would otherwise require a fake
86
+ * `package.json#imports` map).
83
87
  */
84
88
  export declare function emitLocaleRuntimeModules(projectRoot: string, distDir: string): {
85
89
  ok: boolean;
86
90
  written: string[];
87
91
  diagnostics: LocaleDiagnostic[];
88
92
  };
93
+ /**
94
+ * Rewrite `#locales/*` → relative `locales/*.js` and assert none remain.
95
+ * Call before document SSR (`createRenderHost`) or any Node import of app dist.
96
+ */
97
+ export declare function ensureLocaleImportsRewritten(distDir: string): {
98
+ ok: boolean;
99
+ rewritten: number;
100
+ error?: string;
101
+ };
89
102
  /** MessageId rename plan — WorkspaceEdit-shaped, no parallel rename IR. */
90
103
  export declare function planLocaleRename(report: ReturnType<typeof checkLocales>, fromId: string, toId: string): {
91
104
  schema: string;
@@ -589,6 +589,10 @@ export function emitLocaleTypedModules(report, outDir) {
589
589
  *
590
590
  * Variant pick reads `html[data-locale]` (else defaultLocale). Thin bridge until
591
591
  * host LocaleTransition reloads locale-scoped chunks (I2/I4).
592
+ *
593
+ * Contract: after this returns `ok`, `distDir` must not contain bare `#locales/`
594
+ * import specifiers (document SSR / Node import would otherwise require a fake
595
+ * `package.json#imports` map).
592
596
  */
593
597
  export function emitLocaleRuntimeModules(projectRoot, distDir) {
594
598
  const report = checkLocales({ projectRoot, checkUnused: false });
@@ -597,6 +601,21 @@ export function emitLocaleRuntimeModules(projectRoot, distDir) {
597
601
  }
598
602
  // Missing locales.json5 is warning-only for now — surface diagnostics, skip emit.
599
603
  if (!report.manifest) {
604
+ const leftover = findBareLocaleImports(distDir);
605
+ if (leftover.length) {
606
+ return {
607
+ ok: false,
608
+ written: [],
609
+ diagnostics: [
610
+ ...(report.diagnostics || []),
611
+ {
612
+ severity: 'error',
613
+ code: 'locale::dist_bare_import',
614
+ message: `dist still has #locales/* imports but locales manifest is missing: ${leftover.slice(0, 4).join(', ')}`,
615
+ },
616
+ ],
617
+ };
618
+ }
600
619
  return { ok: true, written: [], diagnostics: report.diagnostics || [] };
601
620
  }
602
621
  const defaultLocale = report.manifest.defaultLocale || 'zh-hans';
@@ -630,12 +649,55 @@ export function emitLocaleRuntimeModules(projectRoot, distDir) {
630
649
  fs.writeFileSync(file, code, 'utf8');
631
650
  written.push(file);
632
651
  }
633
- rewriteLocaleImportsInDist(distDir);
652
+ const ensured = ensureLocaleImportsRewritten(distDir);
653
+ if (!ensured.ok) {
654
+ return {
655
+ ok: false,
656
+ written,
657
+ diagnostics: [
658
+ ...(report.diagnostics || []),
659
+ {
660
+ severity: 'error',
661
+ code: 'locale::dist_bare_import',
662
+ message: ensured.error || 'bare #locales/* imports remain in dist',
663
+ },
664
+ ],
665
+ };
666
+ }
634
667
  return { ok: true, written, diagnostics: report.diagnostics || [] };
635
668
  }
669
+ /**
670
+ * Rewrite `#locales/*` → relative `locales/*.js` and assert none remain.
671
+ * Call before document SSR (`createRenderHost`) or any Node import of app dist.
672
+ */
673
+ export function ensureLocaleImportsRewritten(distDir) {
674
+ const rewritten = rewriteLocaleImportsInDist(distDir);
675
+ const leftover = findBareLocaleImports(distDir);
676
+ if (leftover.length) {
677
+ return {
678
+ ok: false,
679
+ rewritten,
680
+ error: `bare #locales/* remain after rewrite: ${leftover.slice(0, 6).join(', ')}`,
681
+ };
682
+ }
683
+ return { ok: true, rewritten };
684
+ }
685
+ /** True import / dynamic-import of `#locales/…` (not comments mentioning the prefix). */
686
+ const BARE_LOCALE_IMPORT_RE = /(?:from\s*|import\s*\()\s*['"`]#locales\//;
687
+ function findBareLocaleImports(distDir) {
688
+ const hits = [];
689
+ walkDistJs(distDir, (file) => {
690
+ const text = fs.readFileSync(file, 'utf8');
691
+ if (!BARE_LOCALE_IMPORT_RE.test(text))
692
+ return;
693
+ hits.push(path.relative(distDir, file).replace(/\\/g, '/'));
694
+ });
695
+ return hits;
696
+ }
636
697
  function rewriteLocaleImportsInDist(distDir) {
637
698
  const files = [];
638
699
  walkDistJs(distDir, (file) => files.push(file));
700
+ let rewritten = 0;
639
701
  for (const file of files) {
640
702
  const text = fs.readFileSync(file, 'utf8');
641
703
  if (!text.includes('#locales/'))
@@ -649,9 +711,12 @@ function rewriteLocaleImportsInDist(distDir) {
649
711
  rel = `./${rel}`;
650
712
  return `from ${quote}${rel}${quote}`;
651
713
  });
652
- if (next !== text)
714
+ if (next !== text) {
653
715
  fs.writeFileSync(file, next, 'utf8');
716
+ rewritten += 1;
717
+ }
654
718
  }
719
+ return rewritten;
655
720
  }
656
721
  function walkDistJs(dir, fn) {
657
722
  if (!fs.existsSync(dir))
@@ -659,7 +724,8 @@ function walkDistJs(dir, fn) {
659
724
  for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
660
725
  const full = path.join(dir, ent.name);
661
726
  if (ent.isDirectory()) {
662
- if (ent.name === 'node_modules')
727
+ // Skip package trees and pack vendor — only app emit must clear `#locales/*`.
728
+ if (ent.name === 'node_modules' || ent.name === 'vendor')
663
729
  continue;
664
730
  walkDistJs(full, fn);
665
731
  }
@@ -130,17 +130,9 @@ export declare function parseLocaleFromPath(pathname: string, supportedLocales:
130
130
  localeId: string;
131
131
  restPath: string;
132
132
  };
133
- export interface LocaleHrefArtifact {
134
- locales?: Array<{
135
- id: string;
136
- } | string>;
137
- defaultLocale?: string;
138
- routing?: LocaleRoutingOpts;
139
- }
140
- /** Rewrite a same-app href to the given LocaleId via route realization. */
141
- export declare function localizeSameAppHref(href: string, localeId: string, artifact: LocaleHrefArtifact): string;
142
- /** Rewrite `<a data-vmz-route href>` in HTML body to retain `localeId`. */
143
- export declare function localizeBodyLinks(html: string, localeId: string, artifact: LocaleHrefArtifact, escapeAttr?: (s: string) => string): string;
133
+ export type { LocaleHrefArtifact } from '@vmz/core/localize-body-links';
134
+ /** Rewrite a same-app href — single implementation in `@vmz/core/localize-body-links`. */
135
+ export { localizeBodyLinks, localizeSameAppHref } from '@vmz/core/localize-body-links';
144
136
  /** Plan redirect / negotiation for an incoming URL (omit-prefix aware). */
145
137
  export declare function planLocalePathNavigation(input: {
146
138
  pathname: string;
@@ -228,58 +228,8 @@ export function parseLocaleFromPath(pathname, supportedLocales) {
228
228
  }
229
229
  return { localeId: null, restPath: normalizePath(pathname) };
230
230
  }
231
- /** Rewrite a same-app href to the given LocaleId via route realization. */
232
- export function localizeSameAppHref(href, localeId, artifact) {
233
- if (!href || !localeId || !artifact)
234
- return href;
235
- if (href.startsWith('#') || /^(mailto|tel|javascript):/i.test(href))
236
- return href;
237
- if (/^[a-z][a-z0-9+.-]*:/i.test(href) && !href.startsWith('/'))
238
- return href;
239
- let pathname = String(href);
240
- let search = '';
241
- let hash = '';
242
- const hashIdx = pathname.indexOf('#');
243
- if (hashIdx >= 0) {
244
- hash = pathname.slice(hashIdx);
245
- pathname = pathname.slice(0, hashIdx);
246
- }
247
- const qIdx = pathname.indexOf('?');
248
- if (qIdx >= 0) {
249
- search = pathname.slice(qIdx);
250
- pathname = pathname.slice(0, qIdx);
251
- }
252
- if (!pathname)
253
- pathname = '/';
254
- const supported = (artifact.locales || []).map((l) => (typeof l === 'string' ? l : l.id)).filter(Boolean);
255
- const defaultLocale = artifact.defaultLocale || artifact.routing?.defaultLocale;
256
- const routing = {
257
- strategy: artifact.routing?.strategy || 'prefix',
258
- defaultPrefix: artifact.routing?.defaultPrefix || 'include',
259
- defaultLocale,
260
- };
261
- const parsed = parseLocaleFromPath(pathname, supported);
262
- const rest = parsed.restPath || '/';
263
- const realized = realizeRoutePath(localeId, rest, routing);
264
- return `${realized.path}${search}${hash}`;
265
- }
266
- /** Rewrite `<a data-vmz-route href>` in HTML body to retain `localeId`. */
267
- export function localizeBodyLinks(html, localeId, artifact, escapeAttr = (s) => String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;')) {
268
- if (!html || !localeId || !artifact)
269
- return html;
270
- return String(html).replace(/<a\b([^>]*)>/gi, (full, attrs) => {
271
- if (!/\bdata-vmz-route\s*=/.test(attrs))
272
- return full;
273
- const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
274
- if (!hm)
275
- return full;
276
- const next = localizeSameAppHref(hm[1], localeId, artifact);
277
- if (next === hm[1])
278
- return full;
279
- const newAttrs = attrs.replace(/\bhref\s*=\s*"[^"]*"/i, `href="${escapeAttr(next)}"`);
280
- return `<a${newAttrs}>`;
281
- });
282
- }
231
+ /** Rewrite a same-app href — single implementation in `@vmz/core/localize-body-links`. */
232
+ export { localizeBodyLinks, localizeSameAppHref } from '@vmz/core/localize-body-links';
283
233
  /** Plan redirect / negotiation for an incoming URL (omit-prefix aware). */
284
234
  export function planLocalePathNavigation(input) {
285
235
  const routing = {
package/dist/log.js CHANGED
@@ -22,14 +22,16 @@ export const log = {
22
22
  },
23
23
  diagnostic(d) {
24
24
  const severity = d.severity === 'warning' || d.severity === 'advice' || d.severity === 'error' ? d.severity : 'error';
25
- const code = d.code && String(d.code).length ? String(d.code) : 'diag.message';
25
+ // Only invent transitional `diag.message` when wire has no code; never remap a real code.
26
+ const hasCode = Boolean(d.code && String(d.code).length);
27
+ const code = hasCode ? String(d.code) : 'diag.message';
26
28
  const locale = resolveVmzLocale();
27
29
  const catalog = loadCliCatalog(locale);
28
30
  const line = formatDiagnostic({
29
31
  path: d.path || '',
30
32
  severity,
31
33
  code,
32
- args: d.args || (d.message ? { message: String(d.message) } : undefined),
34
+ args: d.args ?? (!hasCode && d.message ? { message: String(d.message) } : undefined),
33
35
  message: d.message,
34
36
  span: d.span,
35
37
  }, {
package/dist/pack.js CHANGED
@@ -20,7 +20,7 @@ export const PACK_MANIFEST_SCHEMA = 'vmz.pack.manifest.v0';
20
20
  export function ensureRuntimeCompanions(outDir, coreDist) {
21
21
  if (!coreDist)
22
22
  return [];
23
- const names = ['dom-core.js', 'dom-ssr.js', 'dom.client.js'];
23
+ const names = ['dom-core.js', 'dom-ssr.js', 'dom.client.js', 'direct-host-box.js', 'unknown-component.js'];
24
24
  const copied = [];
25
25
  for (const name of names) {
26
26
  const src = path.join(coreDist, name);
@@ -274,13 +274,13 @@ function sourceItem(id, path, content) {
274
274
  * @param {string[]} engines
275
275
  */
276
276
  function buildMathFacade(defaultEngine, engines) {
277
- const defaultLit = JSON.stringify(defaultEngine);
277
+ const defaultLit = jsSingleQuoted(defaultEngine);
278
278
  const branches = engines
279
279
  .map((eng, i) => {
280
280
  const tag = engineToTag(eng);
281
- const kw = i === 0 ? 'if' : 'else-if';
282
- const engLit = JSON.stringify(eng);
283
- return ` <${tag} ${kw}={(engine || ${defaultLit}) === ${engLit}} tex={tex} display={display} />`;
281
+ const kw = i === 0 ? 'v-if' : 'v-else-if';
282
+ const engLit = jsSingleQuoted(eng);
283
+ return ` <${tag} ${kw}="(engine || ${defaultLit}) === ${engLit}" :tex="tex" :display="display" />`;
284
284
  })
285
285
  .join('\n');
286
286
  return `<template>
@@ -301,13 +301,13 @@ export default class Math {
301
301
  * @param {string[]} engines
302
302
  */
303
303
  function buildCodeFacade(defaultEngine, engines) {
304
- const defaultLit = JSON.stringify(defaultEngine);
304
+ const defaultLit = jsSingleQuoted(defaultEngine);
305
305
  const branches = engines
306
306
  .map((eng, i) => {
307
307
  const tag = engineToTag(eng);
308
- const kw = i === 0 ? 'if' : 'else-if';
309
- const engLit = JSON.stringify(eng);
310
- return ` <${tag} ${kw}={(engine || ${defaultLit}) === ${engLit}} code={code} lang={lang} theme={theme} />`;
308
+ const kw = i === 0 ? 'v-if' : 'v-else-if';
309
+ const engLit = jsSingleQuoted(eng);
310
+ return ` <${tag} ${kw}="(engine || ${defaultLit}) === ${engLit}" :code="code" :lang="lang" :theme="theme" />`;
311
311
  })
312
312
  .join('\n');
313
313
  return `<template>
@@ -329,13 +329,13 @@ export default class Code {
329
329
  * @param {string[]} engines
330
330
  */
331
331
  function buildMarkdownFacade(defaultEngine, engines) {
332
- const defaultLit = JSON.stringify(defaultEngine);
332
+ const defaultLit = jsSingleQuoted(defaultEngine);
333
333
  const branches = engines
334
334
  .map((eng, i) => {
335
335
  const tag = engineToTag(eng);
336
- const kw = i === 0 ? 'if' : 'else-if';
337
- const engLit = JSON.stringify(eng);
338
- return ` <${tag} ${kw}={(engine || ${defaultLit}) === ${engLit}} source={source} />`;
336
+ const kw = i === 0 ? 'v-if' : 'v-else-if';
337
+ const engLit = jsSingleQuoted(eng);
338
+ return ` <${tag} ${kw}="(engine || ${defaultLit}) === ${engLit}" :source="source" />`;
339
339
  })
340
340
  .join('\n');
341
341
  return `<template>
@@ -350,6 +350,10 @@ export default class Markdown {
350
350
  </script>
351
351
  `;
352
352
  }
353
+ /** Embed a JS string literal inside a double-quoted Vue attribute. */
354
+ function jsSingleQuoted(value) {
355
+ return `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
356
+ }
353
357
  /** @param {string} engine */
354
358
  function engineToTag(engine) {
355
359
  const map = {
@@ -6,7 +6,7 @@ export declare function isRouteBoundaryStem(stem: string): boolean;
6
6
  export declare function isRouteGroupDir(seg: string): boolean;
7
7
  /**
8
8
  * File-route fallback (`pages/home` → `/home`, `pages/index` → `/`).
9
- * Used only when a deployment unit has no `pathPattern`.
9
+ * Kept for tooling diagnostics only — production hosts must use Plan `pathPattern`.
10
10
  */
11
11
  export declare function filePathPatternFromChunk(chunkId: string): string;
12
12
  export type DeploymentPageUnit = {
@@ -17,7 +17,7 @@ export type DeploymentPageUnit = {
17
17
  pathPattern?: string;
18
18
  routeId?: string;
19
19
  };
20
- /** Canonical Browser HTTP pattern for a page unit. Mini must not read this. */
20
+ /** Canonical Browser HTTP pattern for a page unit. Mini must not read this. Requires Plan `pathPattern`. */
21
21
  export declare function unitBrowserPathPattern(unit: DeploymentPageUnit | null | undefined): string;
22
22
  export type PathSeg = {
23
23
  kind: 'static';
@@ -10,7 +10,7 @@ export function isRouteGroupDir(seg) {
10
10
  }
11
11
  /**
12
12
  * File-route fallback (`pages/home` → `/home`, `pages/index` → `/`).
13
- * Used only when a deployment unit has no `pathPattern`.
13
+ * Kept for tooling diagnostics only — production hosts must use Plan `pathPattern`.
14
14
  */
15
15
  export function filePathPatternFromChunk(chunkId) {
16
16
  const rel = String(chunkId || '').replace(/^pages\//, '');
@@ -28,12 +28,14 @@ export function filePathPatternFromChunk(chunkId) {
28
28
  }
29
29
  return segs.length ? `/${segs.join('/')}` : '/';
30
30
  }
31
- /** Canonical Browser HTTP pattern for a page unit. Mini must not read this. */
31
+ /** Canonical Browser HTTP pattern for a page unit. Mini must not read this. Requires Plan `pathPattern`. */
32
32
  export function unitBrowserPathPattern(unit) {
33
33
  const explicit = String(unit?.pathPattern || '').trim();
34
- if (explicit)
35
- return explicit.startsWith('/') ? explicit : `/${explicit}`;
36
- return filePathPatternFromChunk(String(unit?.chunkId || ''));
34
+ if (!explicit) {
35
+ const chunkId = String(unit?.chunkId || '');
36
+ throw new Error(`unitBrowserPathPattern: page unit ${chunkId || '(unknown)'} missing pathPattern (plan-only host)`);
37
+ }
38
+ return explicit.startsWith('/') ? explicit : `/${explicit}`;
37
39
  }
38
40
  export function parsePathPattern(pattern) {
39
41
  const raw = String(pattern || '').trim();
@@ -1,19 +1,10 @@
1
1
  /**
2
- * P4 ServerArtifact — compiled route decision tree + public ServerRoute contracts
3
- * + internal capability units + selected runtime adapter. Web Standards Fetch entry.
2
+ * P4 ServerArtifact — thin host write of Rust/N-API normalized Plan.
3
+ * Assembly / digests / adapters live in vmz-artifacts via N-API.
4
4
  */
5
5
  export declare const SERVER_ARTIFACT_SCHEMA = "vmz.server.artifact.v0";
6
6
  export declare const HTTP_CONTRACT_SCHEMA = "vmz.http.contract.v0";
7
7
  export declare const SERVER_RUNTIME_ADAPTER_SCHEMA = "vmz.server.runtime_adapter.v0";
8
- /**
9
- * @param {string} outDir
10
- * @param {{
11
- * profileId?: string | null,
12
- * assembly?: string | null,
13
- * serverRuntime?: string | null,
14
- * packDigest?: string | null,
15
- * }} [opts]
16
- */
17
8
  interface ServerArtifactOpts {
18
9
  profileId?: string | null;
19
10
  assembly?: string | null;
@@ -42,6 +33,7 @@ interface ServerArtifactBody {
42
33
  packDigest: string | null;
43
34
  adapters: Record<string, unknown>;
44
35
  artifactDigest?: string;
36
+ [key: string]: unknown;
45
37
  }
46
38
  export declare function emitServerArtifact(outDir: string, opts?: ServerArtifactOpts): {
47
39
  artifact: ServerArtifactBody;
@@ -49,47 +41,7 @@ export declare function emitServerArtifact(outDir: string, opts?: ServerArtifact
49
41
  httpContractDigest: string;
50
42
  };
51
43
  /**
52
- * @param {Record<string, any>} artifact
53
- * @param {string} adapterId
44
+ * Project a runtime adapter document from a normalized artifact (N-API).
54
45
  */
55
- export declare function projectServerRuntimeAdapter(artifact: any, adapterId: any): {
56
- host: string;
57
- invoke: string;
58
- status: string;
59
- schema: string;
60
- adapterId: string;
61
- artifactDigest: any;
62
- httpContractDigest: any;
63
- spaFallback: boolean;
64
- entry: any;
65
- publicRouteCount: any;
66
- internalCapabilityCount: any;
67
- } | {
68
- host: string;
69
- invoke: string;
70
- status: string;
71
- note: string;
72
- schema: string;
73
- adapterId: string;
74
- artifactDigest: any;
75
- httpContractDigest: any;
76
- spaFallback: boolean;
77
- entry: any;
78
- publicRouteCount: any;
79
- internalCapabilityCount: any;
80
- } | {
81
- host: string;
82
- invoke: string;
83
- status: string;
84
- note: string;
85
- consumes: string[];
86
- schema: string;
87
- adapterId: string;
88
- artifactDigest: any;
89
- httpContractDigest: any;
90
- spaFallback: boolean;
91
- entry: any;
92
- publicRouteCount: any;
93
- internalCapabilityCount: any;
94
- };
46
+ export declare function projectServerRuntimeAdapter(artifact: Record<string, unknown>, adapterId: string): any;
95
47
  export {};