@vmz/vmz 0.1.22 → 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
+ }
@@ -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
  }
@@ -10,7 +10,7 @@ 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';
@@ -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,
@@ -463,6 +466,7 @@ function readCssEntry(distDir) {
463
466
  * props: Record<string, unknown>,
464
467
  * meta: { title: string, description: string, canonical: string, robots: string, lang: string, dir?: string, alternates?: Array<{ hreflang: string, href: string }> },
465
468
  * cssEntry: string | null,
469
+ * moduleScriptSrc?: string,
466
470
  * isErrorDocument?: boolean,
467
471
  * headExtraHtml?: string,
468
472
  * }} input
@@ -493,6 +497,7 @@ function wrapDocument(input) {
493
497
  ...(input.cssEntry ? { cssEntry: String(input.cssEntry) } : {}),
494
498
  isErrorDocument: !!input.isErrorDocument,
495
499
  ...(input.headExtraHtml ? { headExtraHtml: String(input.headExtraHtml) } : {}),
500
+ ...(input.moduleScriptSrc !== undefined ? { moduleScriptSrc: String(input.moduleScriptSrc) } : {}),
496
501
  });
497
502
  }
498
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.22",
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.22",
52
- "@vmz/core": "0.1.22",
53
- "@vmz/diagnostic": "0.1.22",
54
- "@vmz/plugin": "0.1.22",
55
- "@vmz/protocol": "0.1.22",
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.22",
60
- "@vmz/test": "0.1.22",
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.22",
96
- "@vmz/vmz-win32-arm64": "0.1.22",
97
- "@vmz/vmz-darwin-x64": "0.1.22",
98
- "@vmz/vmz-darwin-arm64": "0.1.22",
99
- "@vmz/vmz-linux-x64": "0.1.22",
100
- "@vmz/vmz-linux-arm64": "0.1.22"
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"