@vmz/vmz 0.1.21 → 0.1.23

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.
package/dist/cli.js CHANGED
@@ -21,6 +21,7 @@ import { registerArtifactCommands } from './release-cmd.js';
21
21
  import { registerRefactorCommands } from './refactor-cmd.js';
22
22
  import { registerExplainCommand } from './explain-cmd.js';
23
23
  import { registerPlanCommands } from './plan-cmd.js';
24
+ import { registerGithubActionsCommand } from './github-actions-cmd.js';
24
25
  import { loadVmzConfig } from './plugin-host.js';
25
26
  import { normalizeDeliveryAuthoring, resolveProfileArtifactDir, selectBuildProfile } from './delivery-profile.js';
26
27
  import { packFromDeploymentIr } from './pack.js';
@@ -76,6 +77,7 @@ function buildProductCli(opts = {}) {
76
77
  registerArtifactCommands(cli.command('artifact|artifacts|release', 'cli.cmd.artifact'));
77
78
  registerRefactorCommands(cli.command('refactor', 'cli.cmd.refactor'));
78
79
  registerExplainCommand(cli);
80
+ registerGithubActionsCommand(cli);
79
81
  cli.command('version', 'cli.cmd.version').action(() => cmdVersion());
80
82
  return cli;
81
83
  }
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * A3: content-addressed assets/<hash> layout for immutable CDN objects.
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).
3
+ * Logical paths stay available for serve/dev. Static HTML must receive hashed
4
+ * `cssEntry` / `moduleScriptSrc` at shell emit time — this module does **not**
5
+ * post-rewrite HTML. CSS aggregators rewrite `@import` before hash. JS under
6
+ * `assets/` rewrites ESM `./` → `../` via oxc (plus dynamic `import("./"+)` form)
7
+ * so barrels resolve at dist root.
7
8
  */
8
9
  export declare const CONTENT_ADDRESSED_ASSETS_SCHEMA = "vmz.content_addressed_assets.v0";
9
10
  interface ContentAddressedAssetObject {
@@ -24,7 +25,6 @@ export interface ContentAddressedAssetsManifest {
24
25
  }
25
26
  interface EmitContentAddressedAssetsOpts {
26
27
  candidates?: string[];
27
- rewriteHtml?: boolean;
28
28
  }
29
29
  /**
30
30
  * Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
@@ -46,9 +46,10 @@ export declare function rewriteCssImports(cssText: any, rewrites: any): any;
46
46
  */
47
47
  export declare function rewriteJsEntryRelativeImports(jsText: any, _rewrites?: {}): string;
48
48
  /**
49
- * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
49
+ * Emit `assets/<sha256>.<ext>` copies and return logical→hashed rewrites.
50
+ * Callers must pass hashed URLs into `generatePageShell` — HTML is never rewritten here.
50
51
  * @param {string} distDir
51
- * @param {{ candidates?: string[], rewriteHtml?: boolean }} [opts]
52
+ * @param {{ candidates?: string[] }} [opts]
52
53
  */
53
54
  export declare function emitContentAddressedAssets(distDir: string, opts?: EmitContentAddressedAssetsOpts): {
54
55
  manifest: ContentAddressedAssetsManifest;
@@ -99,6 +100,8 @@ export declare function assertSharedAssetPath(distDir: any, a: any, b: any, ext?
99
100
  digestB?: undefined;
100
101
  rel?: undefined;
101
102
  };
103
+ /** Resolve `/logical` → hashed href from emit rewrites (`/assets/<sha>.ext`). */
104
+ export declare function hashedAssetHref(rewrites: Record<string, string>, logical: string | null | undefined): string | null;
102
105
  export declare function contentAddressedAssetsDigest(manifest: any): any;
103
106
  export declare function sha256Buffer(buf: any): string;
104
107
  export {};
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * A3: content-addressed assets/<hash> layout for immutable CDN objects.
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).
3
+ * Logical paths stay available for serve/dev. Static HTML must receive hashed
4
+ * `cssEntry` / `moduleScriptSrc` at shell emit time — this module does **not**
5
+ * post-rewrite HTML. CSS aggregators rewrite `@import` before hash. JS under
6
+ * `assets/` rewrites ESM `./` → `../` via oxc (plus dynamic `import("./"+)` form)
7
+ * so barrels resolve at dist root.
7
8
  */
8
9
  import crypto from 'node:crypto';
9
10
  import fs from 'node:fs';
10
11
  import path from 'node:path';
12
+ import { requireNativeAddon } from './native-addon.js';
11
13
  import { canonicalJson, sha256Hex } from './release-pack.js';
12
14
  import { writePrettyJsonFile } from './pretty-json.js';
13
15
  export const CONTENT_ADDRESSED_ASSETS_SCHEMA = 'vmz.content_addressed_assets.v0';
@@ -59,26 +61,25 @@ export function rewriteCssImports(cssText, rewrites) {
59
61
  * @param {Record<string, string>} [_rewrites]
60
62
  */
61
63
  export function rewriteJsEntryRelativeImports(jsText, _rewrites = {}) {
62
- let out = String(jsText || '');
63
- // Dynamic: import("./" + id) → import("../" + id)
64
+ const native = requireNativeAddon();
65
+ if (typeof native.rewriteModuleSpecifiers !== 'function') {
66
+ throw new Error('native missing rewriteModuleSpecifiers — run `pnpm napi:build`');
67
+ }
68
+ // Static import/export specs via oxc AST (`./x` → `../x`). Dynamic
69
+ // `import("./"+id)` is not a literal specifier — rewrite that codegen form only.
70
+ let out = String(native.rewriteModuleSpecifiers(String(jsText ?? ''), JSON.stringify({
71
+ tsExtToJs: false,
72
+ dotSlashToParent: true,
73
+ })));
64
74
  out = out.replace(/import\(\s*"\.\/"\s*\+/g, 'import("../"+');
65
75
  out = out.replace(/import\(\s*'\.\/'\s*\+/g, "import('../'+");
66
- const rewriteSpec = (spec) => {
67
- const logical = String(spec || '').replace(/^\.\//, '');
68
- if (!logical || logical.startsWith('../') || logical.startsWith('/'))
69
- return spec;
70
- return `../${logical}`;
71
- };
72
- // import/export … from "./x" | import("./x")
73
- 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}`);
74
- // side-effect: import "./x.js"
75
- out = out.replace(/(^|[;\s])(import\s*)(['"])(\.\/[^'"]+)\3/gm, (match, lead, kw, quote, spec) => `${lead}${kw}${quote}${rewriteSpec(spec)}${quote}`);
76
76
  return out;
77
77
  }
78
78
  /**
79
- * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
79
+ * Emit `assets/<sha256>.<ext>` copies and return logical→hashed rewrites.
80
+ * Callers must pass hashed URLs into `generatePageShell` — HTML is never rewritten here.
80
81
  * @param {string} distDir
81
- * @param {{ candidates?: string[], rewriteHtml?: boolean }} [opts]
82
+ * @param {{ candidates?: string[] }} [opts]
82
83
  */
83
84
  export function emitContentAddressedAssets(distDir, opts = {}) {
84
85
  const abs = path.resolve(distDir);
@@ -141,17 +142,13 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
141
142
  });
142
143
  }
143
144
  objects.sort((a, b) => (a.logicalPath < b.logicalPath ? -1 : a.logicalPath > b.logicalPath ? 1 : 0));
144
- let rewrittenHtml = 0;
145
- if (opts.rewriteHtml !== false) {
146
- rewrittenHtml = rewriteHtmlReferences(abs, rewrites);
147
- }
148
145
  const manifest = {
149
146
  schema: CONTENT_ADDRESSED_ASSETS_SCHEMA,
150
147
  layout: 'assets/<sha256>.<ext>',
151
148
  immutable: true,
152
149
  objectCount: objects.length,
153
150
  objects,
154
- rewrittenHtml,
151
+ rewrittenHtml: 0,
155
152
  };
156
153
  manifest.manifestDigest = sha256Hex(canonicalJson({ ...manifest, manifestDigest: undefined }));
157
154
  const vmzDir = path.join(abs, '_vmz');
@@ -299,45 +296,16 @@ function walk(root, dir, onFile) {
299
296
  onFile(path.relative(root, full).replace(/\\/g, '/'));
300
297
  }
301
298
  }
302
- /**
303
- * @param {string} distDir
304
- * @param {Record<string, string>} rewrites map `/logical` → `/assets/hash.ext`
305
- */
306
- function rewriteHtmlReferences(distDir, rewrites) {
307
- const pairs = Object.entries(rewrites)
308
- .filter(([from]) => from.startsWith('/'))
309
- .sort((a, b) => b[0].length - a[0].length);
310
- if (!pairs.length)
311
- return 0;
312
- let count = 0;
313
- walkHtml(distDir, (file) => {
314
- let text = fs.readFileSync(file, 'utf8');
315
- let next = text;
316
- for (const [from, to] of pairs) {
317
- next = next.split(from).join(to);
318
- }
319
- if (next !== text) {
320
- fs.writeFileSync(file, next, 'utf8');
321
- count += 1;
322
- }
323
- });
324
- return count;
325
- }
326
- function walkHtml(dir, onFile) {
327
- const stack = [dir];
328
- while (stack.length) {
329
- const cur = stack.pop();
330
- for (const name of fs.readdirSync(cur)) {
331
- if (name === 'assets' || name === '_vmz' || name === 'node_modules')
332
- continue;
333
- const full = path.join(cur, name);
334
- const st = fs.statSync(full);
335
- if (st.isDirectory())
336
- stack.push(full);
337
- else if (name.endsWith('.html'))
338
- onFile(full);
339
- }
340
- }
299
+ /** Resolve `/logical` → hashed href from emit rewrites (`/assets/<sha>.ext`). */
300
+ export function hashedAssetHref(rewrites, logical) {
301
+ if (!logical)
302
+ return null;
303
+ const raw = String(logical).replace(/\\/g, '/');
304
+ const key = raw.startsWith('/') ? raw : `/${raw.replace(/^\.\//, '')}`;
305
+ const hit = rewrites[key] || rewrites[key.slice(1)];
306
+ if (!hit)
307
+ return key.startsWith('/') ? key : `/${key}`;
308
+ return hit.startsWith('/') ? hit : `/${hit}`;
341
309
  }
342
310
  export function contentAddressedAssetsDigest(manifest) {
343
311
  return manifest.manifestDigest || sha256Hex(canonicalJson({ ...manifest, manifestDigest: undefined }));
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `vmz github-actions` — monitor workflow runs via N-API → Rust `vmz-github` (octocrab).
3
+ */
4
+ import type { Cli, ParsedOptions } from '@vmz/commander';
5
+ export declare function registerGithubActionsCommand(cli: Cli): void;
6
+ export declare function cmdGithubActions(args: ParsedOptions): number;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `vmz github-actions` — monitor workflow runs via N-API → Rust `vmz-github` (octocrab).
3
+ */
4
+ import { loadNative } from './index.js';
5
+ import { log } from './log.js';
6
+ import { emitPrettyJson } from './pretty-json.js';
7
+ export function registerGithubActionsCommand(cli) {
8
+ cli.command('github-actions|gha', 'cli.cmd.github-actions')
9
+ .option('--mode <mode>', 'cli.opt.gha.mode')
10
+ .option('--workflow <id>', 'cli.opt.gha.workflow')
11
+ .option('--run-id <id>', 'cli.opt.gha.run-id')
12
+ .option('--ref <sha>', 'cli.opt.gha.ref')
13
+ .option('--branch <name>', 'cli.opt.gha.branch')
14
+ .option('--owner <owner>', 'cli.opt.gha.owner')
15
+ .option('--repo <repo>', 'cli.opt.gha.repo')
16
+ .option('--token <token>', 'cli.opt.gha.token')
17
+ .option('--timeout-minutes <n>', 'cli.opt.gha.timeout-minutes')
18
+ .option('--interval-seconds <n>', 'cli.opt.gha.interval-seconds')
19
+ .option('--required-conclusion <c>', 'cli.opt.gha.required-conclusion')
20
+ .option('--json [file]', 'cli.opt.json')
21
+ .action((options) => cmdGithubActions(options));
22
+ }
23
+ export function cmdGithubActions(args) {
24
+ const native = loadNative();
25
+ if (typeof native.githubActionsMonitorJson !== 'function') {
26
+ log.error('githubActionsMonitorJson missing — rebuild native (`pnpm napi:build`)');
27
+ return 1;
28
+ }
29
+ const modeRaw = String(args.mode ?? 'status')
30
+ .trim()
31
+ .toLowerCase();
32
+ if (modeRaw !== 'status' && modeRaw !== 'wait') {
33
+ log.errorId('cli.err.gha.mode', { mode: modeRaw });
34
+ return 1;
35
+ }
36
+ const { owner, repo } = resolveOwnerRepo(args);
37
+ if (!owner || !repo) {
38
+ log.errorId('cli.err.gha.repo');
39
+ return 1;
40
+ }
41
+ const token = (typeof args.token === 'string' && args.token.trim()) || process.env.GITHUB_TOKEN || process.env.GH_TOKEN || undefined;
42
+ const timeoutMinutes = positiveNumber(args['timeout-minutes'], 45);
43
+ const intervalSeconds = positiveNumber(args['interval-seconds'], 15);
44
+ const request = {
45
+ owner,
46
+ repo,
47
+ token: token || null,
48
+ mode: modeRaw,
49
+ workflow: strOrNull(args.workflow),
50
+ runId: parseRunId(args['run-id']),
51
+ headSha: strOrNull(args.ref),
52
+ branch: strOrNull(args.branch),
53
+ timeoutSecs: Math.round(timeoutMinutes * 60),
54
+ intervalSecs: Math.round(intervalSeconds),
55
+ requiredConclusion: String(args['required-conclusion'] ?? 'success'),
56
+ };
57
+ let resultJson;
58
+ try {
59
+ resultJson = native.githubActionsMonitorJson(JSON.stringify(request));
60
+ }
61
+ catch (err) {
62
+ const msg = err instanceof Error ? err.message : String(err);
63
+ log.error(msg);
64
+ return 1;
65
+ }
66
+ let result;
67
+ try {
68
+ result = JSON.parse(resultJson);
69
+ }
70
+ catch {
71
+ log.error('githubActionsMonitorJson returned invalid JSON');
72
+ return 1;
73
+ }
74
+ log.info(`github-actions: run_id=${result.runId} status=${result.status} conclusion=${result.conclusion ?? 'null'} html_url=${result.htmlUrl}`);
75
+ if (args.json !== undefined && args.json !== false) {
76
+ emitPrettyJson(typeof args.json === 'string' ? args.json : true, result);
77
+ }
78
+ return 0;
79
+ }
80
+ function resolveOwnerRepo(args) {
81
+ let owner = typeof args.owner === 'string' ? args.owner.trim() : '';
82
+ let repo = typeof args.repo === 'string' ? args.repo.trim() : '';
83
+ if (!owner || !repo) {
84
+ const fromEnv = (process.env.GITHUB_REPOSITORY || '').trim();
85
+ const slash = fromEnv.indexOf('/');
86
+ if (slash > 0) {
87
+ if (!owner)
88
+ owner = fromEnv.slice(0, slash);
89
+ if (!repo)
90
+ repo = fromEnv.slice(slash + 1);
91
+ }
92
+ }
93
+ if (!owner || !repo) {
94
+ const fallback = (process.env.VMZ_GITHUB_REPO || 'doki-land/vmz-framework').trim();
95
+ const slash = fallback.indexOf('/');
96
+ if (slash > 0) {
97
+ if (!owner)
98
+ owner = fallback.slice(0, slash);
99
+ if (!repo)
100
+ repo = fallback.slice(slash + 1);
101
+ }
102
+ }
103
+ return { owner, repo };
104
+ }
105
+ function strOrNull(v) {
106
+ if (typeof v !== 'string')
107
+ return null;
108
+ const t = v.trim();
109
+ return t ? t : null;
110
+ }
111
+ function parseRunId(v) {
112
+ if (v === undefined || v === null || v === true || v === false)
113
+ return null;
114
+ const n = Number(v);
115
+ return Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
116
+ }
117
+ function positiveNumber(v, fallback) {
118
+ const n = Number(v);
119
+ return Number.isFinite(n) && n > 0 ? n : fallback;
120
+ }
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()) {
@@ -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 = {
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Bare npm/workspace imports are legal on the author surface (01/04).
5
5
  * Browser ESM cannot resolve them. Pack materializes reachable package
6
- * modules under `dist/vendor/<pkg>/…` and rewrites importers to relative paths.
6
+ * modules under `dist/vendor/<pkg>/…` and rewrites importers to relative paths
7
+ * via oxc AST (N-API `rewriteModuleSpecifiers`) — no import-text regex.
7
8
  *
8
9
  * Not full oxc chunk-split/minify (`oxc-pending` remains for release minify).
9
10
  */
@@ -25,5 +26,6 @@ export declare function packClientBareImports(outDir: string, opts?: PackClientB
25
26
  };
26
27
  /** @param {string} js */
27
28
  export declare function collectBareSpecs(js: any): unknown[];
28
- export declare function rewriteRelativeTsSpecs(js: any): any;
29
+ /** oxc AST module specifier rewrite (`.ts`→`.js` + optional exact map). */
30
+ export declare function rewriteRelativeTsSpecs(js: any, exactMap?: any): any;
29
31
  export {};
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Bare npm/workspace imports are legal on the author surface (01/04).
5
5
  * Browser ESM cannot resolve them. Pack materializes reachable package
6
- * modules under `dist/vendor/<pkg>/…` and rewrites importers to relative paths.
6
+ * modules under `dist/vendor/<pkg>/…` and rewrites importers to relative paths
7
+ * via oxc AST (N-API `rewriteModuleSpecifiers`) — no import-text regex.
7
8
  *
8
9
  * Not full oxc chunk-split/minify (`oxc-pending` remains for release minify).
9
10
  */
@@ -11,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
11
12
  import path from 'node:path';
12
13
  import { createRequire } from 'node:module';
13
14
  import { resolvePackageRoot } from './packages.js';
15
+ import { requireNativeAddon } from './native-addon.js';
14
16
  const VENDOR_DIR = 'vendor';
15
17
  const SKIP_PREFIXES = ['node:', 'nodejs:', 'cloudflare:', 'data:', 'http:', 'https:', 'vmz:', '#'];
16
18
  export function packClientBareImports(outDir, opts = {}) {
@@ -327,16 +329,22 @@ function transpileTs(source, filename) {
327
329
  }
328
330
  }
329
331
  function rewriteTsExt(p) {
332
+ // Path basename only (output layout) — not ESM source rewrite.
330
333
  return String(p)
331
334
  .replace(/\.tsx$/i, '.js')
332
335
  .replace(/\.ts$/i, '.js');
333
336
  }
334
- export function rewriteRelativeTsSpecs(js) {
335
- return js
336
- .replace(/(from\s+['"])(\.[^'"]+)\.tsx(['"])/g, '$1$2.js$3')
337
- .replace(/(from\s+['"])(\.[^'"]+)\.ts(['"])/g, '$1$2.js$3')
338
- .replace(/(import\s*\(\s*['"])(\.[^'"]+)\.tsx(['"]\s*\))/g, '$1$2.js$3')
339
- .replace(/(import\s*\(\s*['"])(\.[^'"]+)\.ts(['"]\s*\))/g, '$1$2.js$3');
337
+ /** oxc AST module specifier rewrite (`.ts`→`.js` + optional exact map). */
338
+ export function rewriteRelativeTsSpecs(js, exactMap = null) {
339
+ const native = requireNativeAddon();
340
+ if (typeof native.rewriteModuleSpecifiers !== 'function') {
341
+ throw new Error('native missing rewriteModuleSpecifiers — run `pnpm napi:build`');
342
+ }
343
+ const rules = {
344
+ tsExtToJs: true,
345
+ exact: exactMap && typeof exactMap === 'object' ? exactMap : {},
346
+ };
347
+ return native.rewriteModuleSpecifiers(String(js ?? ''), JSON.stringify(rules));
340
348
  }
341
349
  function resolveRelativeSource(fromDir, rel) {
342
350
  const cleaned = rel.replace(/\?.*$/, '').replace(/#.*$/, '');
@@ -361,33 +369,15 @@ function resolveRelativeSource(fromDir, rel) {
361
369
  return null;
362
370
  }
363
371
  function rewriteBareImports(js, fromFile, bareToVendor) {
364
- const toRel = (spec) => {
365
- const dest = bareToVendor.get(spec);
366
- if (!dest)
367
- return null;
372
+ /** @type {Record<string, string>} */
373
+ const exact = {};
374
+ for (const [spec, dest] of bareToVendor.entries()) {
375
+ if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
376
+ continue;
368
377
  let rel = path.relative(path.dirname(fromFile), dest).replace(/\\/g, '/');
369
378
  if (!rel.startsWith('.'))
370
379
  rel = `./${rel}`;
371
- return rel;
372
- };
373
- let out = js;
374
- out = out.replace(/\bimport\s*\(\s*(['"])([^'"]+)\1\s*\)/g, (full, quote, spec) => {
375
- if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
376
- return full;
377
- const rel = toRel(spec);
378
- return rel ? `import(${quote}${rel}${quote})` : full;
379
- });
380
- out = out.replace(/\bfrom\s+(['"])([^'"]+)\1/g, (full, quote, spec) => {
381
- if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
382
- return full;
383
- const rel = toRel(spec);
384
- return rel ? `from ${quote}${rel}${quote}` : full;
385
- });
386
- out = out.replace(/\bimport\s+(['"])([^'"]+)\1/g, (full, quote, spec) => {
387
- if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
388
- return full;
389
- const rel = toRel(spec);
390
- return rel ? `import ${quote}${rel}${quote}` : full;
391
- });
392
- return out;
380
+ exact[spec] = rel;
381
+ }
382
+ return rewriteRelativeTsSpecs(js, exact);
393
383
  }
@@ -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 {};
@@ -1,109 +1,30 @@
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
- import crypto from 'node:crypto';
6
5
  import { existsSync, mkdirSync, readFileSync } from 'node:fs';
7
6
  import path from 'node:path';
8
- import { SERVER_RUNTIMES } from './delivery-profile.js';
7
+ import { requireNativeAddon } from './native-addon.js';
9
8
  import { writePrettyJsonFile } from './pretty-json.js';
10
9
  export const SERVER_ARTIFACT_SCHEMA = 'vmz.server.artifact.v0';
11
10
  export const HTTP_CONTRACT_SCHEMA = 'vmz.http.contract.v0';
12
11
  export const SERVER_RUNTIME_ADAPTER_SCHEMA = 'vmz.server.runtime_adapter.v0';
13
- const DEFAULT_RPC_PATH = '/__vmz/rpc';
14
12
  export function emitServerArtifact(outDir, opts = {}) {
15
- const deployment = readJson(path.join(outDir, 'vmz-deployment.json')) || { schema: null, units: [] };
16
- const routes = readJson(path.join(outDir, 'vmz-routes.json'));
17
- const routeRows = Array.isArray(routes) ? routes : [];
18
- const selectedRuntime = normalizeRuntime(opts.serverRuntime);
19
- const units = Array.isArray(deployment.units) ? deployment.units : [];
20
- const publicRoutes = routeRows.map((r) => ({
21
- verb: String(r.verb || 'GET').toUpperCase(),
22
- path: String(r.path || ''),
23
- moduleId: String(r.moduleId || ''),
24
- method: String(r.method || ''),
25
- className: r.className != null ? String(r.className) : null,
26
- visibility: 'public',
27
- kind: 'server-route',
28
- }));
29
- const publicKeys = new Set(publicRoutes.map((r) => `${r.moduleId}::${r.method}`));
30
- /** @type {Array<Record<string, unknown>>} */
31
- const internalCapabilities = [];
32
- for (const u of units) {
33
- const moduleId = u.serverModuleId != null ? String(u.serverModuleId) : '';
34
- if (!moduleId)
35
- continue;
36
- const caps = Array.isArray(u.capabilities) ? u.capabilities.map(String) : [];
37
- for (const method of caps) {
38
- const key = `${moduleId}::${method}`;
39
- if (publicKeys.has(key))
40
- continue;
41
- internalCapabilities.push({
42
- chunkId: String(u.chunkId || ''),
43
- moduleId,
44
- method,
45
- visibility: 'internal',
46
- kind: 'capability',
47
- });
48
- }
49
- }
50
- const routeDecisionTree = [
51
- {
52
- id: 'rpc',
53
- match: { method: 'POST', path: DEFAULT_RPC_PATH },
54
- action: 'invoke-rpc',
55
- visibility: 'internal-transport',
56
- },
57
- ...publicRoutes.map((r, i) => ({
58
- id: `public-route-${i}`,
59
- match: { method: r.verb, path: r.path },
60
- action: 'invoke-server-route',
61
- target: { moduleId: r.moduleId, method: r.method },
62
- visibility: 'public',
63
- })),
64
- ];
65
- const httpContractBody = {
66
- schema: HTTP_CONTRACT_SCHEMA,
67
- rpcPath: DEFAULT_RPC_PATH,
68
- publicRoutes: publicRoutes.map((r) => ({
69
- verb: r.verb,
70
- path: r.path,
71
- moduleId: r.moduleId,
72
- method: r.method,
73
- })),
74
- internalCapabilityCount: internalCapabilities.length,
75
- entry: 'fetch',
76
- };
77
- const httpContractDigest = sha256Hex(canonicalJson(httpContractBody));
78
- const artifact = {
79
- schema: SERVER_ARTIFACT_SCHEMA,
80
- profileId: opts.profileId || null,
81
- assembly: opts.assembly || null,
82
- selectedRuntime,
83
- entry: {
84
- kind: 'fetch',
85
- standards: ['Request', 'Response', 'Streams', 'AbortSignal'],
86
- rpcPath: DEFAULT_RPC_PATH,
87
- },
88
- httpContract: {
89
- schema: HTTP_CONTRACT_SCHEMA,
90
- digest: httpContractDigest,
91
- },
92
- publicRoutes,
93
- internalCapabilities,
94
- middlewareUnits: [],
95
- routeDecisionTree,
96
- deploymentSchema: deployment.schema || null,
97
- packDigest: opts.packDigest || null,
98
- adapters: {
99
- node: { kind: 'node-http', status: 'runtime', entry: 'handleNodeRequest' },
100
- worker: { kind: 'fetch', status: 'runtime', entry: 'handleFetchRequest' },
101
- deno: { kind: 'fetch', status: 'projected', entry: 'handleFetchRequest' },
102
- bun: { kind: 'fetch', status: 'projected', entry: 'handleFetchRequest' },
103
- 'rust-host': { kind: 'contract-projection', status: 'projected', entry: 'fetch' },
104
- },
105
- };
106
- artifact.artifactDigest = sha256Hex(canonicalJson({ ...artifact, artifactDigest: undefined }));
13
+ const deploymentPath = path.join(outDir, 'vmz-deployment.json');
14
+ const routesPath = path.join(outDir, 'vmz-routes.json');
15
+ const deploymentJson = existsSync(deploymentPath) ? readFileSync(deploymentPath, 'utf8') : '{}';
16
+ const routesJson = existsSync(routesPath) ? readFileSync(routesPath, 'utf8') : '[]';
17
+ const native = requireNativeAddon();
18
+ if (typeof native.normalizeServerArtifactJson !== 'function') {
19
+ throw new Error('vmz native addon missing normalizeServerArtifactJson — rebuild with `pnpm napi:build`');
20
+ }
21
+ const artifactJson = native.normalizeServerArtifactJson(deploymentJson, routesJson, {
22
+ profileId: opts.profileId ?? null,
23
+ assembly: opts.assembly ?? null,
24
+ serverRuntime: opts.serverRuntime ?? null,
25
+ packDigest: opts.packDigest ?? null,
26
+ });
27
+ const artifact = JSON.parse(artifactJson);
107
28
  const vmzDir = path.join(outDir, '_vmz');
108
29
  mkdirSync(vmzDir, { recursive: true });
109
30
  const file = path.join(vmzDir, 'server-artifact.json');
@@ -116,79 +37,19 @@ export function emitServerArtifact(outDir, opts = {}) {
116
37
  mkdirSync(dir, { recursive: true });
117
38
  writePrettyJsonFile(path.join(dir, 'adapter.json'), projection);
118
39
  }
119
- return { artifact, path: file, httpContractDigest };
40
+ return {
41
+ artifact,
42
+ path: file,
43
+ httpContractDigest: artifact.httpContract?.digest || null,
44
+ };
120
45
  }
121
46
  /**
122
- * @param {Record<string, any>} artifact
123
- * @param {string} adapterId
47
+ * Project a runtime adapter document from a normalized artifact (N-API).
124
48
  */
125
49
  export function projectServerRuntimeAdapter(artifact, adapterId) {
126
- const id = String(adapterId || '').trim();
127
- if (!SERVER_RUNTIMES.includes(id) && id !== 'worker') {
128
- throw new Error(`projectServerRuntimeAdapter: unknown adapter ${id}`);
129
- }
130
- const base = {
131
- schema: SERVER_RUNTIME_ADAPTER_SCHEMA,
132
- adapterId: id,
133
- artifactDigest: artifact.artifactDigest,
134
- httpContractDigest: artifact.httpContract?.digest || null,
135
- spaFallback: false,
136
- entry: artifact.entry,
137
- publicRouteCount: Array.isArray(artifact.publicRoutes) ? artifact.publicRoutes.length : 0,
138
- internalCapabilityCount: Array.isArray(artifact.internalCapabilities) ? artifact.internalCapabilities.length : 0,
139
- };
140
- if (id === 'node') {
141
- return { ...base, host: 'node:http', invoke: 'handleNodeRequest', status: 'runtime' };
142
- }
143
- if (id === 'worker' || id === 'deno' || id === 'bun') {
144
- return {
145
- ...base,
146
- host: 'fetch',
147
- invoke: 'handleFetchRequest',
148
- status: id === 'worker' ? 'runtime' : 'projected',
149
- note: id === 'worker'
150
- ? 'Fetch entry; live thin gated via worker-shaped subprocess host'
151
- : 'Fetch contract projection; live runtime not gated',
152
- };
50
+ const native = requireNativeAddon();
51
+ if (typeof native.projectServerRuntimeAdapterJson !== 'function') {
52
+ throw new Error('vmz native addon missing projectServerRuntimeAdapterJson — rebuild with `pnpm napi:build`');
153
53
  }
154
- // rust-host
155
- return {
156
- ...base,
157
- host: 'rust-fetch-consumer',
158
- invoke: 'fetch',
159
- status: 'projected',
160
- note: 'contract projection only — live Rust host binary parity not gated',
161
- consumes: ['server-artifact.json', 'vmz-routes.json', 'vmz-deployment.json'],
162
- };
163
- }
164
- function normalizeRuntime(raw) {
165
- const v = String(raw || 'node').trim();
166
- return SERVER_RUNTIMES.includes(v) ? v : 'node';
167
- }
168
- function readJson(file) {
169
- if (!existsSync(file))
170
- return null;
171
- try {
172
- return JSON.parse(readFileSync(file, 'utf8'));
173
- }
174
- catch {
175
- return null;
176
- }
177
- }
178
- function canonicalJson(value) {
179
- return JSON.stringify(sortKeys(value));
180
- }
181
- function sortKeys(value) {
182
- if (Array.isArray(value))
183
- return value.map(sortKeys);
184
- if (value && typeof value === 'object') {
185
- const out = {};
186
- for (const k of Object.keys(value).sort())
187
- out[k] = sortKeys(value[k]);
188
- return out;
189
- }
190
- return value;
191
- }
192
- function sha256Hex(text) {
193
- return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
54
+ return JSON.parse(native.projectServerRuntimeAdapterJson(JSON.stringify(artifact), String(adapterId || '').trim()));
194
55
  }
@@ -10,12 +10,12 @@ import { createRenderHost } from '@vmz/core/render-host';
10
10
  import { resolveRouteLayoutChain } from '@vmz/core/route-layout-chain';
11
11
  import { listClientComponentsSync } from '@vmz/core/component-registry';
12
12
  import { emitCdnPolicy } from './cdn-policy.js';
13
- import { emitContentAddressedAssets } from './content-addressed-assets.js';
13
+ import { emitContentAddressedAssets, hashedAssetHref } from './content-addressed-assets.js';
14
14
  import { absoluteUrl, buildLocalePageMeta, localizeBodyLinks } from './locale-router.js';
15
15
  import { requireNativeAddon } from './native-addon.js';
16
16
  import { writePrettyJsonFile } from './pretty-json.js';
17
17
  import { emitPublicStaticAssets } from './public-static-assets.js';
18
- import { filePathPatternFromChunk, isRouteBoundaryStem, listPublicPageUnits, parsePathPattern, unitBrowserPathPattern } from './route-path.js';
18
+ import { listPublicPageUnits, parsePathPattern, unitBrowserPathPattern } from './route-path.js';
19
19
  import { emitSiteFavicon, readSiteFaviconHeadHtml } from './site-favicon.js';
20
20
  export const STATIC_DELIVERY_MANIFEST_SCHEMA = 'vmz.static.delivery_manifest.v0';
21
21
  export async function emitWebStatic(distDir, opts = {}) {
@@ -33,6 +33,13 @@ export async function emitWebStatic(distDir, opts = {}) {
33
33
  const pageCatalog = listPageClientFiles(distDir);
34
34
  const generations = [];
35
35
  const skipped = [];
36
+ // Hash immutable assets before any HTML write so shell slots carry final URLs
37
+ // (no post-emit HTML split/join rewrite).
38
+ emitStaticClientEntries(distDir, pageCatalog);
39
+ const assets = emitContentAddressedAssets(distDir);
40
+ const cssLogical = readCssEntry(distDir);
41
+ const cssEntry = hashedAssetHref(assets.rewrites, cssLogical) || hashedAssetHref(assets.rewrites, 'vmz.css');
42
+ const moduleScriptSrc = hashedAssetHref(assets.rewrites, 'entry-client.js') || '/entry-client.js';
36
43
  for (const page of pageCatalog) {
37
44
  const pattern = page.pathPattern || patternFromSegs(page.segs);
38
45
  const routeId = guessRouteId(distDir, page.chunkId);
@@ -118,7 +125,8 @@ export async function emitWebStatic(distDir, opts = {}) {
118
125
  layoutChain,
119
126
  props,
120
127
  meta: gen.meta,
121
- cssEntry: readCssEntry(distDir),
128
+ cssEntry,
129
+ moduleScriptSrc,
122
130
  headExtraHtml: faviconHead,
123
131
  });
124
132
  fs.writeFileSync(absHtml, html, 'utf8');
@@ -149,7 +157,8 @@ export async function emitWebStatic(distDir, opts = {}) {
149
157
  robots: 'noindex,nofollow',
150
158
  lang: 'en',
151
159
  },
152
- cssEntry: readCssEntry(distDir),
160
+ cssEntry,
161
+ moduleScriptSrc: '',
153
162
  isErrorDocument: true,
154
163
  headExtraHtml: faviconHead,
155
164
  });
@@ -208,22 +217,16 @@ export async function emitWebStatic(distDir, opts = {}) {
208
217
  fileCount: publicAssets.fileCount ?? 0,
209
218
  source: 'source' in publicAssets && typeof publicAssets.source === 'string' ? publicAssets.source : null,
210
219
  },
220
+ contentAddressedAssets: {
221
+ schema: assets.manifest.schema,
222
+ manifestDigest: assets.manifest.manifestDigest,
223
+ objectCount: assets.manifest.objectCount,
224
+ layout: assets.manifest.layout,
225
+ },
211
226
  };
212
227
  const digest = sha256Hex(canonicalJson(manifest));
213
228
  manifest.manifestDigest = digest;
214
229
  writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
215
- emitStaticClientEntries(distDir, pageCatalog);
216
- const assets = emitContentAddressedAssets(distDir);
217
- manifest.contentAddressedAssets = {
218
- schema: assets.manifest.schema,
219
- manifestDigest: assets.manifest.manifestDigest,
220
- objectCount: assets.manifest.objectCount,
221
- layout: assets.manifest.layout,
222
- };
223
- // Re-stamp static manifest after linking asset digest (HTML already rewritten on disk).
224
- delete manifest.manifestDigest;
225
- manifest.manifestDigest = sha256Hex(canonicalJson(manifest));
226
- writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
227
230
  const cdn = emitCdnPolicy(distDir, manifest);
228
231
  return {
229
232
  manifest,
@@ -265,53 +268,25 @@ function sortKeys(value) {
265
268
  */
266
269
  function listPageClientFiles(distDir) {
267
270
  const fromDep = listPagesFromDeployment(distDir);
268
- if (fromDep.length)
269
- return fromDep;
270
- const root = path.join(distDir, 'pages');
271
- /** @type {Array<{ chunkId: string, segs: ReturnType<typeof parsePathPattern>, pathPattern: string }>} */
272
- const out = [];
273
- function walk(abs, relParts) {
274
- let ents;
275
- try {
276
- ents = fs.readdirSync(abs, { withFileTypes: true });
277
- }
278
- catch {
279
- return;
280
- }
281
- for (const e of ents) {
282
- if (e.isDirectory())
283
- walk(path.join(abs, e.name), [...relParts, e.name]);
284
- else if (e.isFile() && e.name.endsWith('.client.js')) {
285
- const stem = e.name.replace(/\.client\.js$/, '');
286
- if (isRouteBoundaryStem(stem))
287
- continue;
288
- const chunkId = ['pages', ...relParts, stem].join('/');
289
- const pathPattern = filePathPatternFromChunk(chunkId);
290
- out.push({ chunkId, pathPattern, segs: parsePathPattern(pathPattern) });
291
- }
292
- }
271
+ if (!fromDep.length) {
272
+ throw new Error(`emitWebStatic: no page units with pathPattern in ${path.join(distDir, 'vmz-deployment.json')} (plan-only host)`);
293
273
  }
294
- walk(root, []);
295
- return out;
274
+ return fromDep;
296
275
  }
297
276
  /**
298
277
  * @param {string} distDir
299
278
  */
300
279
  function listPagesFromDeployment(distDir) {
301
280
  const deploymentPath = path.join(distDir, 'vmz-deployment.json');
302
- if (!fs.existsSync(deploymentPath))
303
- return [];
304
- try {
305
- const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
306
- return listPublicPageUnits(deployment).map((u) => {
307
- const chunkId = String(u.chunkId || '').replace(/\\/g, '/');
308
- const pathPattern = unitBrowserPathPattern(u);
309
- return { chunkId, pathPattern, segs: parsePathPattern(pathPattern) };
310
- });
311
- }
312
- catch {
313
- return [];
281
+ if (!fs.existsSync(deploymentPath)) {
282
+ throw new Error(`emitWebStatic: missing ${deploymentPath} (plan-only host)`);
314
283
  }
284
+ const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
285
+ return listPublicPageUnits(deployment).map((u) => {
286
+ const chunkId = String(u.chunkId || '').replace(/\\/g, '/');
287
+ const pathPattern = unitBrowserPathPattern(u);
288
+ return { chunkId, pathPattern, segs: parsePathPattern(pathPattern) };
289
+ });
315
290
  }
316
291
  /**
317
292
  * @param {ReturnType<typeof parsePathPattern>} segs
@@ -491,6 +466,7 @@ function readCssEntry(distDir) {
491
466
  * props: Record<string, unknown>,
492
467
  * meta: { title: string, description: string, canonical: string, robots: string, lang: string, dir?: string, alternates?: Array<{ hreflang: string, href: string }> },
493
468
  * cssEntry: string | null,
469
+ * moduleScriptSrc?: string,
494
470
  * isErrorDocument?: boolean,
495
471
  * headExtraHtml?: string,
496
472
  * }} input
@@ -521,6 +497,7 @@ function wrapDocument(input) {
521
497
  ...(input.cssEntry ? { cssEntry: String(input.cssEntry) } : {}),
522
498
  isErrorDocument: !!input.isErrorDocument,
523
499
  ...(input.headExtraHtml ? { headExtraHtml: String(input.headExtraHtml) } : {}),
500
+ ...(input.moduleScriptSrc !== undefined ? { moduleScriptSrc: String(input.moduleScriptSrc) } : {}),
524
501
  });
525
502
  }
526
503
  /**
@@ -43,6 +43,7 @@
43
43
  "cli.cmd.refactor": "DX rename plans / apply",
44
44
  "cli.cmd.refactor.rename": "Plan or apply a workspace rename",
45
45
  "cli.cmd.explain": "DX causal explain",
46
+ "cli.cmd.github-actions": "Monitor GitHub Actions via Rust octocrab (status|wait)",
46
47
  "cli.cmd.plan": "Dump frozen Rust plans via N-API",
47
48
  "cli.cmd.plan.locale": "LocalePlan from locales/",
48
49
  "cli.cmd.plan.document-route": "DocumentRoutePlan from documents/",
@@ -83,7 +84,20 @@
83
84
  "cli.opt.apply": "Apply workspace edit plan",
84
85
  "cli.opt.explain": "Include rename explain chain",
85
86
  "cli.opt.explain.target": "Generic Workspace.explain target",
87
+ "cli.opt.gha.mode": "status (one-shot) or wait (poll until completed)",
88
+ "cli.opt.gha.workflow": "Workflow file name (e.g. ci.yml) or numeric id",
89
+ "cli.opt.gha.run-id": "Watch a specific workflow run id",
90
+ "cli.opt.gha.ref": "Commit SHA filter (head_sha)",
91
+ "cli.opt.gha.branch": "Branch name filter",
92
+ "cli.opt.gha.owner": "Repository owner (default GITHUB_REPOSITORY)",
93
+ "cli.opt.gha.repo": "Repository name (default GITHUB_REPOSITORY)",
94
+ "cli.opt.gha.token": "GitHub token (default GITHUB_TOKEN / GH_TOKEN)",
95
+ "cli.opt.gha.timeout-minutes": "Max wait minutes for mode=wait",
96
+ "cli.opt.gha.interval-seconds": "Poll interval seconds for mode=wait",
97
+ "cli.opt.gha.required-conclusion": "Required conclusion when mode=wait (default success)",
86
98
  "cli.err.unknown_plan_kind": "unknown plan kind `{kind}` (locale | document-route)",
99
+ "cli.err.gha.mode": "unknown --mode {mode} (status | wait)",
100
+ "cli.err.gha.repo": "set --owner/--repo or GITHUB_REPOSITORY (owner/repo)",
87
101
  "cli.err.unknown_target": "unknown --target {target} (browser | mini-program-wechat)",
88
102
  "cli.err.document_build_aborted": "document build aborted due to diagnostics",
89
103
  "cli.err.locale_rename_usage": "usage: vmz locale rename <from-message-id> <to-message-id>",
@@ -2,7 +2,6 @@
2
2
  "diag.message": "{message}",
3
3
  "diag.detail": "{detail}",
4
4
 
5
- "vmz::template::jsx_rejected": "JSX / single-brace expressions are rejected in `<template>`: {detail}",
6
5
  "vmz::template::illegal_directive": "Illegal or unsupported directive in `<template>`: {detail}",
7
6
  "vmz::template::parse_failed": "Failed to parse `<template>`: {detail}",
8
7
  "vmz::template::invalid_expr": "Invalid template expression: {detail}",
@@ -2,7 +2,6 @@
2
2
  "diag.message": "{message}",
3
3
  "diag.detail": "{detail}",
4
4
 
5
- "vmz::template::jsx_rejected": "`<template>` 拒绝 JSX / 单花括号表达式:{detail}",
6
5
  "vmz::template::illegal_directive": "`<template>` 中存在非法或不支持的指令:{detail}",
7
6
  "vmz::template::parse_failed": "解析 `<template>` 失败:{detail}",
8
7
  "vmz::template::invalid_expr": "模板表达式无效:{detail}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/vmz",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
6
6
  "license": "MIT",
@@ -48,16 +48,16 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@vmz/commander": "0.1.21",
52
- "@vmz/core": "0.1.21",
53
- "@vmz/diagnostic": "0.1.21",
54
- "@vmz/plugin": "0.1.21",
55
- "@vmz/protocol": "0.1.21",
51
+ "@vmz/commander": "0.1.23",
52
+ "@vmz/core": "0.1.23",
53
+ "@vmz/diagnostic": "0.1.23",
54
+ "@vmz/plugin": "0.1.23",
55
+ "@vmz/protocol": "0.1.23",
56
56
  "jiti": "^2.6.1"
57
57
  },
58
58
  "peerDependencies": {
59
- "@vmz/plugin-markdown-it": "0.1.21",
60
- "@vmz/test": "0.1.21",
59
+ "@vmz/plugin-markdown-it": "0.1.23",
60
+ "@vmz/test": "0.1.23",
61
61
  "typescript": "^5.8.3"
62
62
  },
63
63
  "peerDependenciesMeta": {
@@ -92,12 +92,12 @@
92
92
  "cli"
93
93
  ],
94
94
  "optionalDependencies": {
95
- "@vmz/vmz-win32-x64": "0.1.21",
96
- "@vmz/vmz-win32-arm64": "0.1.21",
97
- "@vmz/vmz-darwin-x64": "0.1.21",
98
- "@vmz/vmz-darwin-arm64": "0.1.21",
99
- "@vmz/vmz-linux-x64": "0.1.21",
100
- "@vmz/vmz-linux-arm64": "0.1.21"
95
+ "@vmz/vmz-win32-x64": "0.1.23",
96
+ "@vmz/vmz-win32-arm64": "0.1.23",
97
+ "@vmz/vmz-darwin-x64": "0.1.23",
98
+ "@vmz/vmz-darwin-arm64": "0.1.23",
99
+ "@vmz/vmz-linux-x64": "0.1.23",
100
+ "@vmz/vmz-linux-arm64": "0.1.23"
101
101
  },
102
102
  "publishConfig": {
103
103
  "access": "public"