@vmz/vmz 0.1.22 → 0.1.24

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,60 @@
1
+ /**
2
+ * Dev incremental state helpers (0.1.24): HMR plan parsing, reload decisions, static revision.
3
+ */
4
+ export interface HmrPlanWire {
5
+ mode?: string;
6
+ islandOnly?: boolean;
7
+ island_only?: boolean;
8
+ rerunLoaders?: string[];
9
+ rerun_loaders?: string[];
10
+ affectedChunks?: string[];
11
+ affected_chunks?: string[];
12
+ seedChunks?: string[];
13
+ seed_chunks?: string[];
14
+ }
15
+ export interface BuildReportWire {
16
+ full?: boolean;
17
+ islandHmr?: boolean;
18
+ island_hmr?: boolean;
19
+ affectedChunks?: string[];
20
+ affected_chunks?: string[];
21
+ seedChunks?: string[];
22
+ seed_chunks?: string[];
23
+ emitted?: string[];
24
+ outputRevision?: string;
25
+ output_revision?: string;
26
+ writtenOutputs?: string[];
27
+ written_outputs?: string[];
28
+ reloadRequired?: boolean;
29
+ reload_required?: boolean;
30
+ }
31
+ export interface DevReloadPayload {
32
+ affectedChunks?: string[];
33
+ seedChunks?: string[];
34
+ emitted?: string[];
35
+ full?: boolean;
36
+ islandHmr?: boolean;
37
+ rerunLoaders?: string[];
38
+ staticRevision?: string;
39
+ skipEntryRewrite?: boolean;
40
+ outputRevision?: string;
41
+ }
42
+ export declare function parseHmrPlan(raw: string | object | null | undefined): HmrPlanWire | null;
43
+ export declare function hmrRerunLoaders(plan: HmrPlanWire | null | undefined): string[];
44
+ export declare function buildReloadPayload(report: BuildReportWire, hmrPlan: HmrPlanWire | null, opts?: {
45
+ staticRevision?: string;
46
+ skipEntryRewrite?: boolean;
47
+ }): DevReloadPayload;
48
+ export declare function shouldSoftReload(report: BuildReportWire, lastOutputRevision: string | null, opts?: {
49
+ force?: boolean;
50
+ }): boolean;
51
+ export declare function staticRevisionFromArtifact(artifact: {
52
+ files?: Array<{
53
+ path: string;
54
+ bytes?: number;
55
+ }>;
56
+ fileCount?: number;
57
+ } | null): string;
58
+ export declare function filterGenerationIgnore(changed: string[], ignore: Set<string>): string[];
59
+ export declare function devPathNormalize(p: string): string;
60
+ export declare function registerWrittenOutputsIgnore(writtenOutputs: string[] | undefined, outDir: string, projectRoot: string, ignore: Set<string>): void;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Dev incremental state helpers (0.1.24): HMR plan parsing, reload decisions, static revision.
3
+ */
4
+ import path from 'node:path';
5
+ export function parseHmrPlan(raw) {
6
+ if (!raw)
7
+ return null;
8
+ try {
9
+ const doc = typeof raw === 'string' ? JSON.parse(raw) : raw;
10
+ return doc && typeof doc === 'object' ? doc : null;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function hmrRerunLoaders(plan) {
17
+ if (!plan)
18
+ return [];
19
+ const list = plan.rerunLoaders ?? plan.rerun_loaders ?? [];
20
+ return Array.isArray(list) ? list.map(String) : [];
21
+ }
22
+ export function buildReloadPayload(report, hmrPlan, opts = {}) {
23
+ const rerunLoaders = hmrRerunLoaders(hmrPlan);
24
+ const affectedChunks = report.affectedChunks ?? report.affected_chunks ?? [];
25
+ const islandHmr = Boolean(report.islandHmr ?? report.island_hmr);
26
+ const mode = String(hmrPlan?.mode ?? '').toLowerCase();
27
+ let full = Boolean(report.full);
28
+ if (mode === 'full')
29
+ full = true;
30
+ if (islandHmr && rerunLoaders.length === 0 && affectedChunks.length > 0) {
31
+ full = true;
32
+ }
33
+ return {
34
+ affectedChunks,
35
+ seedChunks: report.seedChunks ?? report.seed_chunks ?? [],
36
+ emitted: report.emitted ?? [],
37
+ full,
38
+ islandHmr: islandHmr && !full,
39
+ rerunLoaders,
40
+ staticRevision: opts.staticRevision,
41
+ skipEntryRewrite: opts.skipEntryRewrite,
42
+ outputRevision: report.outputRevision ?? report.output_revision,
43
+ };
44
+ }
45
+ export function shouldSoftReload(report, lastOutputRevision, opts = {}) {
46
+ if (opts.force)
47
+ return true;
48
+ const reloadRequired = report.reloadRequired ?? report.reload_required;
49
+ if (reloadRequired === false)
50
+ return false;
51
+ const rev = report.outputRevision ?? report.output_revision ?? '';
52
+ if (lastOutputRevision && rev && rev === lastOutputRevision)
53
+ return false;
54
+ return true;
55
+ }
56
+ export function staticRevisionFromArtifact(artifact) {
57
+ if (!artifact || artifact.fileCount === 0)
58
+ return 'empty';
59
+ const parts = (artifact.files ?? []).map((f) => `${f.path}:${f.bytes ?? 0}`);
60
+ parts.sort();
61
+ let h = 0xcbf29ce484222325n;
62
+ for (const p of parts) {
63
+ for (const b of Buffer.from(p)) {
64
+ h = (h * 0x100000001b3n + BigInt(b)) & 0xffffffffffffffffn;
65
+ }
66
+ }
67
+ return h.toString(16);
68
+ }
69
+ export function filterGenerationIgnore(changed, ignore) {
70
+ if (!ignore.size)
71
+ return changed;
72
+ return changed.filter((p) => !ignore.has(devPathNormalize(p)));
73
+ }
74
+ export function devPathNormalize(p) {
75
+ return String(p).replace(/\\/g, '/');
76
+ }
77
+ export function registerWrittenOutputsIgnore(writtenOutputs, outDir, projectRoot, ignore) {
78
+ for (const rel of writtenOutputs ?? []) {
79
+ const abs = path.isAbsolute(rel) ? rel : path.join(outDir, rel);
80
+ ignore.add(devPathNormalize(abs));
81
+ const underProject = devPathNormalize(abs);
82
+ if (underProject.startsWith(devPathNormalize(projectRoot))) {
83
+ ignore.add(underProject);
84
+ }
85
+ }
86
+ }
@@ -16,7 +16,8 @@ import { buildIntegratedDocuments, projectHasDocuments } from './document-integr
16
16
  import { createWorkspace, resolveNativePath } from './index.js';
17
17
  import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
18
18
  import { log } from './log.js';
19
- import { coalesceRootBurst, collectDevWatchRoots, classifyWatchRoot, isDependencyPath, mergeDirtySets } from './dev-watch-roots.js';
19
+ import { coalesceRootBurst, collectDevWatchRoots, classifyWatchRoot, isDependencyPath, mergeDirtySets, collectDevConfigPaths, configFingerprintMap, } from './dev-watch-roots.js';
20
+ import { buildReloadPayload, filterGenerationIgnore, parseHmrPlan, registerWrittenOutputsIgnore, shouldSoftReload, staticRevisionFromArtifact, } from './dev-incremental.js';
20
21
  import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
21
22
  /** Track serve-host so a failed/killed launcher cannot leave an orphan port. */
22
23
  const DEV_HOST_PID = '.vmz-dev-host.pid';
@@ -33,10 +34,57 @@ export function createDevSession(options) {
33
34
  const ws = createWs({ root: project, outDir });
34
35
  let child = null;
35
36
  let stopped = false;
36
- function rebuild(changes) {
37
+ let lastOutputRevision = null;
38
+ let lastStaticRevision = null;
39
+ const generationIgnore = new Set();
40
+ function planAndRebuild(changes) {
37
41
  if (changes?.length)
38
42
  ws.updateFiles(changes);
39
- return ws.build();
43
+ let hmrPlan = null;
44
+ try {
45
+ if (typeof ws.queryHmrPlan === 'function') {
46
+ hmrPlan = parseHmrPlan(ws.queryHmrPlan());
47
+ }
48
+ }
49
+ catch {
50
+ hmrPlan = null;
51
+ }
52
+ const report = ws.build();
53
+ return { report, hmrPlan };
54
+ }
55
+ function rebuild(changes) {
56
+ return planAndRebuild(changes).report;
57
+ }
58
+ async function emitPublicAndTrack() {
59
+ const { emitPublicStaticAssets } = await import('./public-static-assets.js');
60
+ const artifact = emitPublicStaticAssets(outDir, { projectRoot: project });
61
+ lastStaticRevision = staticRevisionFromArtifact(artifact);
62
+ return artifact;
63
+ }
64
+ async function maybeReloadAfterBuild(report, hmrPlan, opts = {}) {
65
+ const rev = report.outputRevision ?? report.output_revision ?? null;
66
+ const skipEntry = Boolean(rev && rev === lastOutputRevision);
67
+ if (!opts.force && !opts.staticOnly && !shouldSoftReload(report, lastOutputRevision)) {
68
+ log.info('rebuild ok — artifact unchanged, skip soft reload');
69
+ if (rev)
70
+ lastOutputRevision = rev;
71
+ registerWrittenOutputsIgnore(report.writtenOutputs ?? report.written_outputs, outDir, project, generationIgnore);
72
+ return 'skip';
73
+ }
74
+ const payload = buildReloadPayload(report, hmrPlan, {
75
+ staticRevision: lastStaticRevision ?? undefined,
76
+ skipEntryRewrite: skipEntry && !opts.force,
77
+ });
78
+ if (opts.staticOnly) {
79
+ payload.full = false;
80
+ payload.islandHmr = false;
81
+ payload.skipEntryRewrite = true;
82
+ }
83
+ const kind = await reloadAfterBuild(payload);
84
+ if (rev)
85
+ lastOutputRevision = rev;
86
+ registerWrittenOutputsIgnore(report.writtenOutputs ?? report.written_outputs, outDir, project, generationIgnore);
87
+ return kind;
40
88
  }
41
89
  function emitLocales() {
42
90
  const localeEmit = emitLocaleRuntimeModules(project, outDir);
@@ -154,16 +202,14 @@ export function createDevSession(options) {
154
202
  }
155
203
  }
156
204
  log.info('initial build (N-API workspace, full)…');
157
- const initial = rebuild();
205
+ const initialPair = planAndRebuild();
206
+ const initial = initialPair.report;
158
207
  if (!printReport(initial, 'build')) {
159
208
  throw new Error('vmz dev: initial build failed');
160
209
  }
210
+ lastOutputRevision = initial.outputRevision ?? initial.output_revision ?? null;
161
211
  // `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
- }
212
+ await emitPublicAndTrack();
167
213
  // Reclaim orphan serve-host from a previous crashed launcher (same outDir).
168
214
  reclaimStaleDevHost(outDir);
169
215
  if (projectHasDocuments(project)) {
@@ -191,6 +237,8 @@ export function createDevSession(options) {
191
237
  const localesRoot = path.join(project, 'locales');
192
238
  const designsRoot = path.join(project, 'designs');
193
239
  const watched = collectDevWatchRoots({ project, outDir });
240
+ const publicRoot = watched.publicRoot;
241
+ const configPaths = watched.configPaths.length ? watched.configPaths : collectDevConfigPaths(project);
194
242
  /** @type {string[]} */
195
243
  const watchRoots = [...watched.roots];
196
244
  /** @type {string[]} */
@@ -209,6 +257,7 @@ export function createDevSession(options) {
209
257
  for (const root of watchRoots) {
210
258
  fingerprints.set(root, fileFingerprintMap(root));
211
259
  }
260
+ let configFingerprints = configFingerprintMap(configPaths);
212
261
  const signal = options.signal;
213
262
  const onAbort = () => {
214
263
  void stop();
@@ -218,7 +267,7 @@ export function createDevSession(options) {
218
267
  * Scan all watch roots into a batch. Does not update fingerprints.
219
268
  */
220
269
  function scanBatch() {
221
- /** @type {{ srcChanged: string[], srcDeleted: string[], depChanged: string[], depDeleted: string[], designsChanged: string[], designsDeleted: string[], docsDirty: boolean, localesDirty: boolean, designsDirty: boolean }} */
270
+ /** @type {{ srcChanged: string[], srcDeleted: string[], depChanged: string[], depDeleted: string[], designsChanged: string[], designsDeleted: string[], publicChanged: string[], docsDirty: boolean, localesDirty: boolean, designsDirty: boolean, configDirty: boolean }} */
222
271
  const batch = {
223
272
  srcChanged: [],
224
273
  srcDeleted: [],
@@ -226,11 +275,13 @@ export function createDevSession(options) {
226
275
  depDeleted: [],
227
276
  designsChanged: [],
228
277
  designsDeleted: [],
278
+ publicChanged: [],
229
279
  docsDirty: false,
230
280
  localesDirty: false,
231
281
  designsDirty: false,
282
+ configDirty: false,
232
283
  };
233
- const watchCtx = { src, docsRoot, localesRoot, designsRoot, dependencyRoots };
284
+ const watchCtx = { src, docsRoot, localesRoot, designsRoot, publicRoot, dependencyRoots };
234
285
  for (const root of watchRoots) {
235
286
  const prev = fingerprints.get(root) || new Map();
236
287
  const next = fileFingerprintMap(root);
@@ -242,6 +293,9 @@ export function createDevSession(options) {
242
293
  batch.srcChanged.push(...diff.changed);
243
294
  batch.srcDeleted.push(...diff.deleted);
244
295
  }
296
+ else if (kind === 'public') {
297
+ batch.publicChanged.push(...diff.changed);
298
+ }
245
299
  else if (kind === 'locales') {
246
300
  batch.localesDirty = true;
247
301
  }
@@ -282,16 +336,51 @@ export function createDevSession(options) {
282
336
  let batch;
283
337
  try {
284
338
  batch = scanBatch();
339
+ const nextConfigFp = configFingerprintMap(configPaths);
340
+ const configDiff = diffFingerprints(configFingerprints, nextConfigFp);
341
+ if (configDiff.changed.length || configDiff.deleted.length) {
342
+ batch.configDirty = true;
343
+ }
285
344
  }
286
345
  catch (err) {
287
346
  log.warn('watch error:', err);
288
347
  continue;
289
348
  }
349
+ if (generationIgnore.size) {
350
+ batch.srcChanged = filterGenerationIgnore(batch.srcChanged, generationIgnore);
351
+ batch.srcDeleted = filterGenerationIgnore(batch.srcDeleted, generationIgnore);
352
+ generationIgnore.clear();
353
+ }
290
354
  const srcDirty = batch.srcChanged.length + batch.srcDeleted.length;
291
355
  const depDirty = batch.depChanged.length + batch.depDeleted.length;
292
356
  const designsDirty = batch.designsDirty || batch.designsChanged.length + batch.designsDeleted.length;
293
- if (!srcDirty && !depDirty && !batch.docsDirty && !batch.localesDirty && !designsDirty)
357
+ const publicDirty = batch.publicChanged.length;
358
+ if (!srcDirty && !depDirty && !batch.docsDirty && !batch.localesDirty && !designsDirty && !publicDirty && !batch.configDirty) {
294
359
  continue;
360
+ }
361
+ if (batch.configDirty) {
362
+ log.info('vmz.config change detected — respawn required (devControl)');
363
+ configFingerprints = configFingerprintMap(configPaths);
364
+ commitFingerprints();
365
+ await respawnHost('config-changed');
366
+ const report = rebuild();
367
+ if (printReport(report, 'rebuild')) {
368
+ lastOutputRevision = report.outputRevision ?? report.output_revision ?? null;
369
+ await emitPublicAndTrack();
370
+ }
371
+ continue;
372
+ }
373
+ if (publicDirty && !srcDirty && !depDirty && !batch.docsDirty && !batch.localesDirty && !designsDirty) {
374
+ log.info(`public change detected (${publicDirty} file(s)) — re-emit static assets…`);
375
+ await emitPublicAndTrack();
376
+ commitFingerprints();
377
+ if (!wechatPreview) {
378
+ const kind = await maybeReloadAfterBuild({ reloadRequired: true }, null, { force: true, staticOnly: true });
379
+ if (kind !== 'skip')
380
+ log.info('soft reload ok (public assets)');
381
+ }
382
+ continue;
383
+ }
295
384
  // Coalesce multi-file bursts without dropping the initial dirty set.
296
385
  if (srcDirty > 1) {
297
386
  const coalesced = await coalesceRootBurst(src, fingerprints, {
@@ -344,7 +433,6 @@ export function createDevSession(options) {
344
433
  batch.designsChanged = designsMerged.changed;
345
434
  batch.designsDeleted = designsMerged.deleted;
346
435
  }
347
- commitFingerprints();
348
436
  if (!batch.srcChanged.length &&
349
437
  !batch.srcDeleted.length &&
350
438
  !batch.depChanged.length &&
@@ -357,11 +445,12 @@ export function createDevSession(options) {
357
445
  // Dependency changes: conservative full rebuild + full reload (v0.1.5).
358
446
  if (batch.depChanged.length || batch.depDeleted.length) {
359
447
  log.info(`dependency change detected (${batch.depChanged.length} update, ${batch.depDeleted.length} delete) — full rebuild…`);
360
- const report = rebuild();
448
+ const { report, hmrPlan } = planAndRebuild();
361
449
  if (!printReport(report, 'rebuild')) {
362
450
  log.warn('rebuild failed — keeping previous server');
363
451
  continue;
364
452
  }
453
+ await emitPublicAndTrack();
365
454
  // Refresh watch roots in case the graph gained new packages.
366
455
  const refreshed = collectDevWatchRoots({ project, outDir });
367
456
  for (const r of refreshed.roots) {
@@ -381,14 +470,11 @@ export function createDevSession(options) {
381
470
  log.warn('wechat pack failed — keeping previous dist/wechat');
382
471
  continue;
383
472
  }
384
- const kind = await reloadAfterBuild({
385
- affectedChunks: report.affectedChunks ?? [],
386
- seedChunks: report.seedChunks ?? [],
387
- emitted: report.emitted ?? [],
388
- full: true,
389
- islandHmr: false,
390
- });
391
- log.info(kind === 'respawn' ? 'reload ok (respawned; deps)' : 'soft reload ok (full page; deps)');
473
+ const kind = await maybeReloadAfterBuild(report, hmrPlan, { force: true });
474
+ commitFingerprints();
475
+ if (kind !== 'skip') {
476
+ log.info(kind === 'respawn' ? 'reload ok (respawned; deps)' : 'soft reload ok (full page; deps)');
477
+ }
392
478
  continue;
393
479
  }
394
480
  let needFullReload = batch.docsDirty || batch.designsDirty;
@@ -398,11 +484,12 @@ export function createDevSession(options) {
398
484
  ...batch.srcChanged.map((p) => ({ path: p, kind: /** @type {'update'} */ ('update') })),
399
485
  ...batch.srcDeleted.map((p) => ({ path: p, kind: /** @type {'delete'} */ ('delete') })),
400
486
  ];
401
- const report = rebuild(changes);
487
+ const { report, hmrPlan } = planAndRebuild(changes);
402
488
  if (!printReport(report, 'rebuild')) {
403
489
  log.warn('rebuild failed — keeping previous server');
404
490
  continue;
405
491
  }
492
+ await emitPublicAndTrack();
406
493
  if (batch.docsDirty || projectHasDocuments(project)) {
407
494
  const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
408
495
  if (!docs.ok) {
@@ -418,20 +505,19 @@ export function createDevSession(options) {
418
505
  }
419
506
  continue;
420
507
  }
421
- const kind = await reloadAfterBuild({
422
- affectedChunks: report.affectedChunks ?? [],
423
- seedChunks: report.seedChunks ?? [],
424
- emitted: report.emitted ?? [],
425
- full: Boolean(report.full) || needFullReload,
426
- islandHmr: Boolean(report.islandHmr) && !needFullReload,
508
+ const kind = await maybeReloadAfterBuild(report, hmrPlan, {
509
+ force: needFullReload,
427
510
  });
428
- log.info(kind === 'respawn'
429
- ? 'reload ok (respawned serve-host)'
430
- : needFullReload
431
- ? 'soft reload ok (full page; docs)'
432
- : report.islandHmr
433
- ? 'soft reload ok (island HMR)'
434
- : 'soft reload ok');
511
+ commitFingerprints();
512
+ if (kind !== 'skip') {
513
+ log.info(kind === 'respawn'
514
+ ? 'reload ok (respawned serve-host)'
515
+ : needFullReload
516
+ ? 'soft reload ok (full page; docs)'
517
+ : report.islandHmr
518
+ ? 'soft reload ok (island HMR)'
519
+ : 'soft reload ok');
520
+ }
435
521
  continue;
436
522
  }
437
523
  if (batch.designsChanged.length || batch.designsDeleted.length || batch.designsDirty) {
@@ -440,11 +526,12 @@ export function createDevSession(options) {
440
526
  ...batch.designsChanged.map((p) => ({ path: p, kind: /** @type {'update'} */ ('update') })),
441
527
  ...batch.designsDeleted.map((p) => ({ path: p, kind: /** @type {'delete'} */ ('delete') })),
442
528
  ];
443
- const report = rebuild(changes.length ? changes : undefined);
529
+ const { report, hmrPlan } = planAndRebuild(changes.length ? changes : undefined);
444
530
  if (!printReport(report, 'rebuild')) {
445
531
  log.warn('designs rebuild failed — keeping previous styles');
446
532
  continue;
447
533
  }
534
+ await emitPublicAndTrack();
448
535
  if (batch.docsDirty || projectHasDocuments(project)) {
449
536
  const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
450
537
  if (!docs.ok)
@@ -455,14 +542,11 @@ export function createDevSession(options) {
455
542
  log.warn('wechat pack failed — keeping previous dist/wechat');
456
543
  continue;
457
544
  }
458
- const kind = await reloadAfterBuild({
459
- affectedChunks: report.affectedChunks ?? [],
460
- seedChunks: report.seedChunks ?? [],
461
- emitted: report.emitted ?? [],
462
- full: true,
463
- islandHmr: false,
464
- });
465
- log.info(kind === 'respawn' ? 'reload ok (respawned; designs)' : 'soft reload ok (full page; designs)');
545
+ const kind = await maybeReloadAfterBuild(report, hmrPlan, { force: true });
546
+ commitFingerprints();
547
+ if (kind !== 'skip') {
548
+ log.info(kind === 'respawn' ? 'reload ok (respawned; designs)' : 'soft reload ok (full page; designs)');
549
+ }
466
550
  continue;
467
551
  }
468
552
  if (batch.localesDirty) {
@@ -479,8 +563,11 @@ export function createDevSession(options) {
479
563
  continue;
480
564
  }
481
565
  else {
482
- const kind = await reloadAfterBuild({ full: true, islandHmr: false, emitted: [] });
483
- log.info(kind === 'respawn' ? 'reload ok (respawned; locales)' : 'soft reload ok (full page; locales)');
566
+ const kind = await maybeReloadAfterBuild({ reloadRequired: true, full: true }, null, { force: true });
567
+ commitFingerprints();
568
+ if (kind !== 'skip') {
569
+ log.info(kind === 'respawn' ? 'reload ok (respawned; locales)' : 'soft reload ok (full page; locales)');
570
+ }
484
571
  if (!batch.docsDirty)
485
572
  continue;
486
573
  }
@@ -498,8 +585,11 @@ export function createDevSession(options) {
498
585
  }
499
586
  }
500
587
  else {
501
- const kind = await reloadAfterBuild({ full: true, islandHmr: false, emitted: [] });
502
- log.info(kind === 'respawn' ? 'reload ok (respawned; docs)' : 'soft reload ok (full page; docs)');
588
+ const kind = await maybeReloadAfterBuild({ reloadRequired: true, full: true }, null, { force: true });
589
+ commitFingerprints();
590
+ if (kind !== 'skip') {
591
+ log.info(kind === 'respawn' ? 'reload ok (respawned; docs)' : 'soft reload ok (full page; docs)');
592
+ }
503
593
  }
504
594
  }
505
595
  }
@@ -1,7 +1,16 @@
1
1
  /**
2
2
  * Dev watch helpers: coalesce multi-file bursts without dropping dirty set,
3
3
  * and derive extra watch roots from the compile graph (deployment unit sources).
4
+ *
5
+ * DevInput layers (0.1.24): author · dependency · public · devControl.
6
+ * Never watch `dist/`, generated indexes, or generation write-set paths.
4
7
  */
8
+ /** Author-facing vs generated dev input bucket. */
9
+ export type DevInputKind = 'author' | 'dependency' | 'public' | 'devControl' | 'locales' | 'docs' | 'designs' | 'other';
10
+ /** `vmz.config.*` at project root (devControl). */
11
+ export declare function collectDevConfigPaths(project: string): string[];
12
+ /** Fingerprint map for explicit config files (not directory walks). */
13
+ export declare function configFingerprintMap(configPaths: string[]): Map<string, string>;
5
14
  export interface DirtySet {
6
15
  changed: string[];
7
16
  deleted: string[];
@@ -59,7 +68,11 @@ export declare function collectDevWatchRoots(opts: CollectDevWatchRootsOpts): {
59
68
  roots: string[];
60
69
  dependencyRoots: string[];
61
70
  applicationRoots: string[];
71
+ publicRoot: string | null;
72
+ configPaths: string[];
62
73
  };
74
+ /** Map watch bucket to DevInput layer. */
75
+ export declare function devInputKindForWatchBucket(bucket: ReturnType<typeof classifyWatchRoot>): DevInputKind;
63
76
  /**
64
77
  * Classify which watch bucket a root belongs to.
65
78
  * @param {string} root
@@ -77,9 +90,10 @@ export interface ClassifyWatchRootCtx {
77
90
  docsRoot: string;
78
91
  localesRoot: string;
79
92
  designsRoot: string;
93
+ publicRoot?: string | null;
80
94
  dependencyRoots: string[];
81
95
  }
82
- export declare function classifyWatchRoot(root: string, ctx: ClassifyWatchRootCtx): 'src' | 'locales' | 'docs' | 'designs' | 'dep' | 'other';
96
+ export declare function classifyWatchRoot(root: string, ctx: ClassifyWatchRootCtx): 'src' | 'locales' | 'docs' | 'designs' | 'dep' | 'public' | 'other';
83
97
  /**
84
98
  * Classify whether a changed file lives under a dependency watch root (not app src).
85
99
  * @param {string} file
@@ -1,10 +1,33 @@
1
1
  /**
2
2
  * Dev watch helpers: coalesce multi-file bursts without dropping dirty set,
3
3
  * and derive extra watch roots from the compile graph (deployment unit sources).
4
+ *
5
+ * DevInput layers (0.1.24): author · dependency · public · devControl.
6
+ * Never watch `dist/`, generated indexes, or generation write-set paths.
4
7
  */
5
- import { existsSync, readFileSync, realpathSync } from 'node:fs';
8
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
6
9
  import path from 'node:path';
7
10
  import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
11
+ const CONFIG_BASENAMES = ['vmz.config.ts', 'vmz.config.js', 'vmz.config.mjs', 'vmz.config.cjs'];
12
+ /** `vmz.config.*` at project root (devControl). */
13
+ export function collectDevConfigPaths(project) {
14
+ const root = path.resolve(project);
15
+ return CONFIG_BASENAMES.map((name) => path.join(root, name)).filter((p) => existsSync(p));
16
+ }
17
+ /** Fingerprint map for explicit config files (not directory walks). */
18
+ export function configFingerprintMap(configPaths) {
19
+ const map = new Map();
20
+ for (const p of configPaths) {
21
+ try {
22
+ const st = statSync(p);
23
+ map.set(p, `${st.mtimeMs}:${st.size}`);
24
+ }
25
+ catch {
26
+ /* ignore */
27
+ }
28
+ }
29
+ return map;
30
+ }
8
31
  export function mergeDirtySets(a, b) {
9
32
  const changed = new Set(a?.changed || []);
10
33
  const deleted = new Set(a?.deleted || []);
@@ -135,10 +158,14 @@ export function localLinkDependencyRoots(project) {
135
158
  export function collectDevWatchRoots(opts) {
136
159
  const project = path.resolve(opts.project);
137
160
  const outDir = path.resolve(opts.outDir);
161
+ void outDir;
138
162
  const src = path.join(project, 'src');
139
163
  const docsRoot = path.join(project, 'documents');
140
164
  const localesRoot = path.join(project, 'locales');
141
165
  const designsRoot = path.join(project, 'designs');
166
+ const publicRootPath = path.join(project, 'public');
167
+ const publicRoot = existsSync(publicRootPath) && statSync(publicRootPath).isDirectory() ? publicRootPath : null;
168
+ const configPaths = collectDevConfigPaths(project);
142
169
  const applicationRoots = [];
143
170
  if (existsSync(src))
144
171
  applicationRoots.push(src);
@@ -148,6 +175,8 @@ export function collectDevWatchRoots(opts) {
148
175
  applicationRoots.push(localesRoot);
149
176
  if (existsSync(designsRoot))
150
177
  applicationRoots.push(designsRoot);
178
+ if (publicRoot)
179
+ applicationRoots.push(publicRoot);
151
180
  const depSet = new Set();
152
181
  const depJson = path.join(outDir, 'vmz-deployment.json');
153
182
  if (existsSync(depJson)) {
@@ -188,11 +217,29 @@ export function collectDevWatchRoots(opts) {
188
217
  if (!roots.includes(r))
189
218
  roots.push(r);
190
219
  }
191
- return { roots, dependencyRoots, applicationRoots };
220
+ return { roots, dependencyRoots, applicationRoots, publicRoot, configPaths };
221
+ }
222
+ /** Map watch bucket to DevInput layer. */
223
+ export function devInputKindForWatchBucket(bucket) {
224
+ if (bucket === 'src')
225
+ return 'author';
226
+ if (bucket === 'dep')
227
+ return 'dependency';
228
+ if (bucket === 'public')
229
+ return 'public';
230
+ if (bucket === 'locales')
231
+ return 'locales';
232
+ if (bucket === 'docs')
233
+ return 'docs';
234
+ if (bucket === 'designs')
235
+ return 'designs';
236
+ return 'other';
192
237
  }
193
238
  export function classifyWatchRoot(root, ctx) {
194
239
  if (root === ctx.src)
195
240
  return 'src';
241
+ if (ctx.publicRoot && root === ctx.publicRoot)
242
+ return 'public';
196
243
  if (root === ctx.localesRoot)
197
244
  return 'locales';
198
245
  if (root === ctx.docsRoot)
@@ -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.24",
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.24",
52
+ "@vmz/core": "0.1.24",
53
+ "@vmz/diagnostic": "0.1.24",
54
+ "@vmz/plugin": "0.1.24",
55
+ "@vmz/protocol": "0.1.24",
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.24",
60
+ "@vmz/test": "0.1.24",
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.24",
96
+ "@vmz/vmz-win32-arm64": "0.1.24",
97
+ "@vmz/vmz-darwin-x64": "0.1.24",
98
+ "@vmz/vmz-darwin-arm64": "0.1.24",
99
+ "@vmz/vmz-linux-x64": "0.1.24",
100
+ "@vmz/vmz-linux-arm64": "0.1.24"
101
101
  },
102
102
  "publishConfig": {
103
103
  "access": "public"