@vmz/vmz 0.0.1 → 0.0.2

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.
Files changed (75) hide show
  1. package/README.md +48 -2
  2. package/bin/vmz.js +4 -0
  3. package/dist/application-cmd.d.ts +22 -0
  4. package/dist/application-cmd.js +348 -0
  5. package/dist/bundler-adapter.d.ts +64 -0
  6. package/dist/bundler-adapter.js +111 -0
  7. package/dist/cli.d.ts +15 -0
  8. package/dist/cli.js +370 -0
  9. package/dist/dev-session.d.ts +35 -0
  10. package/dist/dev-session.js +290 -0
  11. package/dist/document-build.d.ts +99 -0
  12. package/dist/document-build.js +274 -0
  13. package/dist/document-check.d.ts +44 -0
  14. package/dist/document-check.js +246 -0
  15. package/dist/document-cmd.d.ts +9 -0
  16. package/dist/document-cmd.js +147 -0
  17. package/dist/document-designs.d.ts +9 -0
  18. package/dist/document-designs.js +126 -0
  19. package/dist/document-enrich.d.ts +23 -0
  20. package/dist/document-enrich.js +234 -0
  21. package/dist/document-evidence.d.ts +49 -0
  22. package/dist/document-evidence.js +501 -0
  23. package/dist/document-integrate.d.ts +35 -0
  24. package/dist/document-integrate.js +89 -0
  25. package/dist/document-interactive.d.ts +69 -0
  26. package/dist/document-interactive.js +255 -0
  27. package/dist/document-locale.d.ts +31 -0
  28. package/dist/document-locale.js +59 -0
  29. package/dist/document-markdown.d.ts +13 -0
  30. package/dist/document-markdown.js +39 -0
  31. package/dist/document-scan.d.ts +21 -0
  32. package/dist/document-scan.js +151 -0
  33. package/dist/document-schema.d.ts +87 -0
  34. package/dist/document-schema.js +88 -0
  35. package/dist/explain-cmd.d.ts +5 -0
  36. package/dist/explain-cmd.js +123 -0
  37. package/dist/index.d.ts +808 -0
  38. package/dist/index.js +569 -0
  39. package/dist/locale-check.d.ts +106 -0
  40. package/dist/locale-check.js +737 -0
  41. package/dist/locale-cmd.d.ts +5 -0
  42. package/dist/locale-cmd.js +443 -0
  43. package/dist/locale-delivery.d.ts +298 -0
  44. package/dist/locale-delivery.js +444 -0
  45. package/dist/locale-router.d.ts +207 -0
  46. package/dist/locale-router.js +508 -0
  47. package/dist/locale-runtime.d.ts +406 -0
  48. package/dist/locale-runtime.js +542 -0
  49. package/dist/locale-schema.d.ts +9 -0
  50. package/dist/locale-schema.js +10 -0
  51. package/dist/locale-tooling.d.ts +118 -0
  52. package/dist/locale-tooling.js +358 -0
  53. package/dist/log.d.ts +19 -0
  54. package/dist/log.js +42 -0
  55. package/dist/packages.d.ts +27 -0
  56. package/dist/packages.js +147 -0
  57. package/dist/plugin-host.d.ts +30 -0
  58. package/dist/plugin-host.js +370 -0
  59. package/dist/refactor-cmd.d.ts +8 -0
  60. package/dist/refactor-cmd.js +156 -0
  61. package/dist/resolve.d.ts +25 -0
  62. package/dist/resolve.js +56 -0
  63. package/dist/test-cmd.d.ts +9 -0
  64. package/dist/test-cmd.js +343 -0
  65. package/dist/test-compile.d.ts +2 -0
  66. package/dist/test-compile.js +3 -0
  67. package/dist/test-discover.d.ts +2 -0
  68. package/dist/test-discover.js +3 -0
  69. package/dist/test-logic.d.ts +2 -0
  70. package/dist/test-logic.js +3 -0
  71. package/dist/test-protocol.d.ts +2 -0
  72. package/dist/test-protocol.js +3 -0
  73. package/dist/watch-diff.d.ts +17 -0
  74. package/dist/watch-diff.js +56 -0
  75. package/package.json +81 -3
package/dist/cli.js ADDED
@@ -0,0 +1,370 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Node CLI command implementations (N2).
4
+ */
5
+ import { spawn } from 'node:child_process';
6
+ import { existsSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { HOST_PROTOCOL, createWorkspace, getProtocolVersions } from './index.js';
9
+ import { createDevSession } from './dev-session.js';
10
+ import { log } from './log.js';
11
+ import { readPackageMeta, resolveWorkspaceDirs } from './resolve.js';
12
+ import { cmdTest } from './test-cmd.js';
13
+ import { cmdDocument } from './document-cmd.js';
14
+ import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
15
+ import { cmdLocale } from './locale-cmd.js';
16
+ import { cmdApplication } from './application-cmd.js';
17
+ import { cmdRefactor } from './refactor-cmd.js';
18
+ import { cmdExplain } from './explain-cmd.js';
19
+ /**
20
+ * @param {string[]} argv
21
+ */
22
+ export function parseArgs(argv) {
23
+ /** @type {Record<string, string | boolean> & { _: string[] }} */
24
+ const out = { _: [] };
25
+ for (let i = 0; i < argv.length; i++) {
26
+ const a = argv[i];
27
+ if (a === '--') {
28
+ out._.push(...argv.slice(i + 1));
29
+ break;
30
+ }
31
+ if (a.startsWith('--')) {
32
+ const eq = a.indexOf('=');
33
+ if (eq !== -1) {
34
+ out[a.slice(2, eq)] = a.slice(eq + 1);
35
+ continue;
36
+ }
37
+ const key = a.slice(2);
38
+ const next = argv[i + 1];
39
+ if (next && !next.startsWith('-')) {
40
+ out[key] = next;
41
+ i += 1;
42
+ }
43
+ else {
44
+ out[key] = true;
45
+ }
46
+ continue;
47
+ }
48
+ if (a.startsWith('-') && a.length === 2) {
49
+ const key = a === '-o' ? 'out-dir' : a.slice(1);
50
+ const next = argv[i + 1];
51
+ if (next && !next.startsWith('-')) {
52
+ out[key] = next;
53
+ i += 1;
54
+ }
55
+ else {
56
+ out[key] = true;
57
+ }
58
+ continue;
59
+ }
60
+ out._.push(a);
61
+ }
62
+ return out;
63
+ }
64
+ export function printHelp() {
65
+ console.log(`vmz — Node toolchain host (N-API workspace)
66
+
67
+ Usage:
68
+ vmz check [path] Check project via Workspace
69
+ vmz build [path] [options] Build project via Workspace
70
+ vmz serve [path] [options] Serve dist (optional --build)
71
+ vmz dev [path] [options] Long-lived rebuild session (no CLI spawn)
72
+ vmz format [path] [--check] Format .vmz via N-API (oxc codegen)
73
+ vmz lint [path] [--deny-warnings] Lint (= check) via N-API
74
+ vmz test [path] [options] Native test discover / report (T0+)
75
+ vmz document|docs <cmd> Project /documents domain (D0: check)
76
+ vmz application <cmd> Application Collection / Mount (M0–M5)
77
+ vmz refactor <cmd> DX rename plans / apply (X1)
78
+ vmz explain [style] <target> DX causal explain (style Theme chain)
79
+ vmz version Show host + native protocol versions
80
+ vmz help Show this help
81
+
82
+ Options:
83
+ --out-dir, -o <dir> Output directory (default: dist)
84
+ --release Release build (build only)
85
+ --host <host> Listen host (default: 127.0.0.1)
86
+ --port <port> Listen port (default: 5173)
87
+ --poll-ms <ms> Dev watch poll interval (default: 300)
88
+ --build Build before serve
89
+ --check Format check-only (format)
90
+ --deny-warnings Treat warnings as errors (lint)
91
+ --list List discovered tests (test)
92
+ --json [file] Emit TestReport / DocumentManifest / ApplicationCheckReport JSON
93
+ --mode <modes> compile|logic|browser|ssr|resume|deployment|all (test)
94
+ --filter <pattern> Filter by test id or file (test)
95
+ --application <id> Run only tests for ApplicationId (standalone scope)
96
+ --mounted <id> Run relocation + host-boundary tests for ApplicationId
97
+ --affected Select tests from dirty VPG units (test; DX)
98
+ --root <dir> Project root (document check)
99
+ --strict Strict document locale/PageKey coverage (document check)
100
+ `);
101
+ }
102
+ /**
103
+ * @param {string[]} argv
104
+ * @returns {Promise<number>}
105
+ */
106
+ export async function runCli(argv) {
107
+ const [cmd, ...rest] = argv;
108
+ if (!cmd || cmd === 'help' || cmd === '-h' || cmd === '--help') {
109
+ printHelp();
110
+ return 0;
111
+ }
112
+ if (cmd === 'version' || cmd === '-V' || cmd === '--version') {
113
+ return cmdVersion();
114
+ }
115
+ const args = parseArgs(rest);
116
+ switch (cmd) {
117
+ case 'check':
118
+ return cmdCheck(args);
119
+ case 'build':
120
+ return cmdBuild(args);
121
+ case 'serve':
122
+ return cmdServe(args);
123
+ case 'dev':
124
+ return cmdDev(args);
125
+ case 'format':
126
+ return cmdFormat(args);
127
+ case 'lint':
128
+ return cmdLint(args);
129
+ case 'test':
130
+ return cmdTest(args);
131
+ case 'document':
132
+ case 'docs':
133
+ return cmdDocument(rest);
134
+ case 'locale':
135
+ case 'locales':
136
+ return cmdLocale(rest);
137
+ case 'application':
138
+ case 'applications':
139
+ case 'app':
140
+ return cmdApplication(rest);
141
+ case 'refactor':
142
+ return cmdRefactor(rest);
143
+ case 'explain':
144
+ return cmdExplain(rest);
145
+ default:
146
+ log.error(`unknown command \`${cmd}\``);
147
+ printHelp();
148
+ return 1;
149
+ }
150
+ }
151
+ function cmdVersion() {
152
+ const native = getProtocolVersions();
153
+ console.log(`vmz host ${HOST_PROTOCOL}`);
154
+ console.log(`native host=${native.hostProtocol} compiler=${native.compilerProtocol} program_ir=${native.programIrSchema} plugin=${native.pluginProtocol}`);
155
+ return 0;
156
+ }
157
+ /**
158
+ * @param {Record<string, string | boolean> & { _: string[] }} args
159
+ */
160
+ function cmdCheck(args) {
161
+ const pathArg = args._[0] ?? '.';
162
+ const { project, outDir } = resolveWorkspaceDirs({
163
+ path: pathArg,
164
+ outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
165
+ });
166
+ const meta = readPackageMeta(project);
167
+ log.info(`check ${project}${meta?.name ? ` (${meta.name})` : ''}`);
168
+ const ws = createWorkspace({ root: project, outDir });
169
+ try {
170
+ return runWithPlugins(ws, project, outDir, async () => {
171
+ const report = ws.check();
172
+ const errors = log.diagnostics(report.diagnostics ?? []);
173
+ log.info(`checked ${report.filesChecked} file(s)`);
174
+ return errors ? 1 : 0;
175
+ });
176
+ }
177
+ finally {
178
+ ws.dispose();
179
+ }
180
+ }
181
+ /**
182
+ * @param {import('./index.js').Workspace} ws
183
+ * @param {string} project
184
+ * @param {string} outDir
185
+ * @param {() => Promise<number> | number} fn
186
+ */
187
+ async function runWithPlugins(ws, project, outDir, fn) {
188
+ const { loadVmzConfig, applyPlugins } = await import('./plugin-host.js');
189
+ const { plugins, engines } = await loadVmzConfig(project);
190
+ if (plugins.length) {
191
+ await applyPlugins(ws, plugins, { project, outDir, engines });
192
+ }
193
+ return await fn();
194
+ }
195
+ /**
196
+ * @param {Record<string, string | boolean> & { _: string[] }} args
197
+ */
198
+ async function cmdBuild(args) {
199
+ const pathArg = args._[0] ?? '.';
200
+ const { project, outDir } = resolveWorkspaceDirs({
201
+ path: pathArg,
202
+ outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
203
+ });
204
+ log.info(`build ${project} → ${outDir}`);
205
+ const ws = createWorkspace({ root: project, outDir });
206
+ try {
207
+ const code = await runWithPlugins(ws, project, outDir, () => {
208
+ const report = ws.build(Boolean(args.release));
209
+ const errors = log.diagnostics(report.diagnostics ?? []);
210
+ if (errors)
211
+ return 1;
212
+ for (const p of report.emitted ?? []) {
213
+ console.log(`emitted ${p}`);
214
+ }
215
+ log.info(`build ok (${(report.emitted ?? []).length} file(s))`);
216
+ return 0;
217
+ });
218
+ if (code !== 0)
219
+ return code;
220
+ if (projectHasDocuments(project)) {
221
+ const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
222
+ if (!docs.ok)
223
+ return 1;
224
+ }
225
+ return 0;
226
+ }
227
+ finally {
228
+ ws.dispose();
229
+ }
230
+ }
231
+ /**
232
+ * @param {Record<string, string | boolean> & { _: string[] }} args
233
+ */
234
+ async function cmdServe(args) {
235
+ const pathArg = args._[0] ?? '.';
236
+ if (args.build) {
237
+ const code = await cmdBuild(args);
238
+ if (code !== 0)
239
+ return code;
240
+ }
241
+ const { project, outDir } = resolveWorkspaceDirs({
242
+ path: pathArg,
243
+ outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
244
+ });
245
+ const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
246
+ if (!existsSync(hostJs)) {
247
+ log.error(`missing ${hostJs} — run \`vmz build\` first (or pass --build)`);
248
+ return 1;
249
+ }
250
+ const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
251
+ const port = Number(args.port ?? 5173);
252
+ log.info(`serve http://${host}:${port}`);
253
+ const node = process.env.VMZ_NODE || process.execPath;
254
+ const child = spawn(node, [hostJs], {
255
+ cwd: project,
256
+ env: {
257
+ ...process.env,
258
+ VMZ_DIST: outDir,
259
+ VMZ_PORT: String(port),
260
+ VMZ_HOST: host,
261
+ },
262
+ stdio: 'inherit',
263
+ });
264
+ return await new Promise((resolve) => {
265
+ const shutdown = () => {
266
+ child.kill();
267
+ };
268
+ process.once('SIGINT', shutdown);
269
+ process.once('SIGTERM', shutdown);
270
+ child.on('exit', (code, signal) => {
271
+ process.off('SIGINT', shutdown);
272
+ process.off('SIGTERM', shutdown);
273
+ if (signal)
274
+ resolve(0);
275
+ else
276
+ resolve(code ?? 1);
277
+ });
278
+ });
279
+ }
280
+ /**
281
+ * @param {Record<string, string | boolean> & { _: string[] }} args
282
+ */
283
+ async function cmdDev(args) {
284
+ const pathArg = args._[0] ?? '.';
285
+ const { project, outDir } = resolveWorkspaceDirs({
286
+ path: pathArg,
287
+ outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
288
+ });
289
+ const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
290
+ const port = Number(args.port ?? 5173);
291
+ const pollMs = Number(args['poll-ms'] ?? 300);
292
+ const ac = new AbortController();
293
+ const onSig = () => {
294
+ log.info('shutting down…');
295
+ ac.abort();
296
+ };
297
+ process.on('SIGINT', onSig);
298
+ process.on('SIGTERM', onSig);
299
+ const session = createDevSession({
300
+ project,
301
+ outDir,
302
+ host,
303
+ port,
304
+ pollMs,
305
+ signal: ac.signal,
306
+ });
307
+ try {
308
+ await session.start();
309
+ return 0;
310
+ }
311
+ catch (err) {
312
+ if (ac.signal.aborted)
313
+ return 0;
314
+ log.error(String(err));
315
+ return 1;
316
+ }
317
+ finally {
318
+ process.off('SIGINT', onSig);
319
+ process.off('SIGTERM', onSig);
320
+ await session.stop();
321
+ }
322
+ }
323
+ /**
324
+ * @param {Record<string, string | boolean> & { _: string[] }} args
325
+ */
326
+ function cmdFormat(args) {
327
+ const pathArg = args._[0] ?? '.';
328
+ const { project, outDir } = resolveWorkspaceDirs({
329
+ path: pathArg,
330
+ outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
331
+ });
332
+ const checkOnly = Boolean(args.check);
333
+ log.info(`format ${project}${checkOnly ? ' --check' : ''}`);
334
+ const ws = createWorkspace({ root: project, outDir });
335
+ try {
336
+ const report = ws.format(checkOnly);
337
+ const errors = log.diagnostics(report.diagnostics ?? []);
338
+ if (checkOnly) {
339
+ log.info(`checked ${report.filesChecked} file(s); ${report.filesNeedWrite} need write`);
340
+ return errors || report.filesNeedWrite > 0 ? 1 : 0;
341
+ }
342
+ log.info(`formatted ${report.filesWritten}/${report.filesChecked} file(s)`);
343
+ return errors ? 1 : 0;
344
+ }
345
+ finally {
346
+ ws.dispose();
347
+ }
348
+ }
349
+ /**
350
+ * @param {Record<string, string | boolean> & { _: string[] }} args
351
+ */
352
+ function cmdLint(args) {
353
+ const pathArg = args._[0] ?? '.';
354
+ const { project, outDir } = resolveWorkspaceDirs({
355
+ path: pathArg,
356
+ outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
357
+ });
358
+ const denyWarnings = Boolean(args['deny-warnings']);
359
+ log.info(`lint ${project}`);
360
+ const ws = createWorkspace({ root: project, outDir });
361
+ try {
362
+ const report = ws.lint(denyWarnings);
363
+ const errors = log.diagnostics(report.diagnostics ?? [], { denyWarnings });
364
+ log.info(`linted ${report.filesChecked} file(s)`);
365
+ return errors ? 1 : 0;
366
+ }
367
+ finally {
368
+ ws.dispose();
369
+ }
370
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Long-lived Node dev session (N2/N4).
3
+ *
4
+ * Rebuilds go through the N-API `Workspace` — never spawn `cargo` / `vmz-tools`.
5
+ * N4: only dirty leaves are marked; Workspace emits affected deployment units.
6
+ */
7
+ /**
8
+ * @typedef {object} DevSessionOptions
9
+ * @property {string} project
10
+ * @property {string} outDir
11
+ * @property {string} [host]
12
+ * @property {number} [port]
13
+ * @property {number} [pollMs]
14
+ * @property {AbortSignal} [signal]
15
+ * @property {typeof createWorkspace} [createWorkspaceFn]
16
+ * @property {(opts: { project: string, outDir: string, host: string, port: number }) => import('node:child_process').ChildProcess} [spawnHostFn]
17
+ * @property {(host: string, port: number, payload?: object) => Promise<void>} [softReloadFn]
18
+ */
19
+ /**
20
+ * @param {DevSessionOptions} options
21
+ */
22
+ export declare function createDevSession(options: any): {
23
+ ws: any;
24
+ rebuild: (changes: any) => any;
25
+ start: () => Promise<void>;
26
+ stop: () => Promise<void>;
27
+ project: any;
28
+ outDir: any;
29
+ host: any;
30
+ port: any;
31
+ };
32
+ /** @deprecated use fileFingerprintMap */
33
+ export declare function srcFingerprint(srcDir: any): number;
34
+ /** @deprecated */
35
+ export declare function listWatchedFiles(srcDir: any): any[];
@@ -0,0 +1,290 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Long-lived Node dev session (N2/N4).
4
+ *
5
+ * Rebuilds go through the N-API `Workspace` — never spawn `cargo` / `vmz-tools`.
6
+ * N4: only dirty leaves are marked; Workspace emits affected deployment units.
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ import { existsSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
12
+ import { createWorkspace } from './index.js';
13
+ import { log } from './log.js';
14
+ import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
15
+ /**
16
+ * @typedef {object} DevSessionOptions
17
+ * @property {string} project
18
+ * @property {string} outDir
19
+ * @property {string} [host]
20
+ * @property {number} [port]
21
+ * @property {number} [pollMs]
22
+ * @property {AbortSignal} [signal]
23
+ * @property {typeof createWorkspace} [createWorkspaceFn]
24
+ * @property {(opts: { project: string, outDir: string, host: string, port: number }) => import('node:child_process').ChildProcess} [spawnHostFn]
25
+ * @property {(host: string, port: number, payload?: object) => Promise<void>} [softReloadFn]
26
+ */
27
+ /**
28
+ * @param {DevSessionOptions} options
29
+ */
30
+ export function createDevSession(options) {
31
+ const project = options.project;
32
+ const outDir = options.outDir;
33
+ const host = options.host ?? '127.0.0.1';
34
+ const port = options.port ?? 5173;
35
+ const pollMs = Math.max(50, options.pollMs ?? 300);
36
+ const createWs = options.createWorkspaceFn ?? createWorkspace;
37
+ const spawnHost = options.spawnHostFn ?? defaultSpawnHost;
38
+ const softReload = options.softReloadFn ?? defaultSoftReload;
39
+ const ws = createWs({ root: project, outDir });
40
+ /** @type {import('node:child_process').ChildProcess | null} */
41
+ let child = null;
42
+ let stopped = false;
43
+ /**
44
+ * @param {Array<{ path: string, kind: 'update' | 'delete' }>} [changes]
45
+ */
46
+ function rebuild(changes) {
47
+ if (changes?.length)
48
+ ws.updateFiles(changes);
49
+ return ws.build();
50
+ }
51
+ function printReport(report, label) {
52
+ const errors = log.diagnostics(report.diagnostics ?? []);
53
+ if (errors) {
54
+ log.error(`${label} failed (${errors} error(s))`);
55
+ return false;
56
+ }
57
+ const mode = report.full ? 'full' : 'affected';
58
+ const chunks = (report.affectedChunks || []).join(', ') || '(none)';
59
+ log.info(`${label} ok (${mode}; chunks=[${chunks}]; ${(report.emitted ?? []).length} emitted)`);
60
+ return true;
61
+ }
62
+ async function start() {
63
+ const src = path.join(project, 'src');
64
+ if (!existsSync(src)) {
65
+ throw new Error(`vmz dev: missing src/ under ${project}`);
66
+ }
67
+ log.info('initial build (N-API workspace, full)…');
68
+ // Empty dirty → full project build (N4).
69
+ const initial = rebuild();
70
+ if (!printReport(initial, 'build')) {
71
+ throw new Error('vmz dev: initial build failed');
72
+ }
73
+ if (projectHasDocuments(project)) {
74
+ const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
75
+ if (!docs.ok) {
76
+ throw new Error('vmz dev: integrated document build failed');
77
+ }
78
+ }
79
+ const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
80
+ if (!existsSync(hostJs)) {
81
+ throw new Error(`vmz dev: missing ${hostJs}`);
82
+ }
83
+ child = spawnHost({ project, outDir, host, port });
84
+ const docsRoot = path.join(project, 'documents');
85
+ const watchRoots = [src].concat(existsSync(docsRoot) ? [docsRoot] : []);
86
+ log.info(`dev → http://${host}:${port} (watching ${watchRoots.join(', ')})`);
87
+ /** @type {Map<string, Map<string, string>>} */
88
+ let fingerprints = new Map();
89
+ for (const root of watchRoots) {
90
+ fingerprints.set(root, fileFingerprintMap(root));
91
+ }
92
+ const signal = options.signal;
93
+ const onAbort = () => {
94
+ void stop();
95
+ };
96
+ signal?.addEventListener('abort', onAbort, { once: true });
97
+ try {
98
+ while (!stopped && !signal?.aborted) {
99
+ await sleep(pollMs);
100
+ if (stopped || signal?.aborted)
101
+ break;
102
+ if (child && child.exitCode != null) {
103
+ throw new Error(`vmz serve-host exited: ${child.exitCode}`);
104
+ }
105
+ /** @type {{ srcChanged: string[], srcDeleted: string[], docsDirty: boolean }} */
106
+ let batch = { srcChanged: [], srcDeleted: [], docsDirty: false };
107
+ try {
108
+ // Probe only — keep prior fingerprints until debounce resample
109
+ // (same contract as pre-docs watcher: empty second pass would miss soft reload).
110
+ for (const root of watchRoots) {
111
+ const prev = fingerprints.get(root) || new Map();
112
+ const next = fileFingerprintMap(root);
113
+ const diff = diffFingerprints(prev, next);
114
+ if (root === src) {
115
+ batch.srcChanged = diff.changed;
116
+ batch.srcDeleted = diff.deleted;
117
+ }
118
+ else if (diff.changed.length || diff.deleted.length) {
119
+ batch.docsDirty = true;
120
+ }
121
+ }
122
+ }
123
+ catch (err) {
124
+ log.warn('watch error:', err);
125
+ continue;
126
+ }
127
+ if (!batch.srcChanged.length && !batch.srcDeleted.length && !batch.docsDirty)
128
+ continue;
129
+ await sleep(200);
130
+ // Resample against the same prior fingerprints, then commit.
131
+ batch = { srcChanged: [], srcDeleted: [], docsDirty: false };
132
+ for (const root of watchRoots) {
133
+ const prev = fingerprints.get(root) || new Map();
134
+ const next = fileFingerprintMap(root);
135
+ const diff = diffFingerprints(prev, next);
136
+ fingerprints.set(root, next);
137
+ if (root === src) {
138
+ batch.srcChanged = diff.changed;
139
+ batch.srcDeleted = diff.deleted;
140
+ }
141
+ else if (diff.changed.length || diff.deleted.length) {
142
+ batch.docsDirty = true;
143
+ }
144
+ }
145
+ if (!batch.srcChanged.length && !batch.srcDeleted.length && !batch.docsDirty)
146
+ continue;
147
+ let needFullReload = batch.docsDirty;
148
+ if (batch.srcChanged.length || batch.srcDeleted.length) {
149
+ log.info(`change detected (${batch.srcChanged.length} update, ${batch.srcDeleted.length} delete) — affected rebuild…`);
150
+ const changes = [
151
+ ...batch.srcChanged.map((p) => ({ path: p, kind: /** @type {'update'} */ ('update') })),
152
+ ...batch.srcDeleted.map((p) => ({ path: p, kind: /** @type {'delete'} */ ('delete') })),
153
+ ];
154
+ const report = rebuild(changes);
155
+ if (!printReport(report, 'rebuild')) {
156
+ log.warn('rebuild failed — keeping previous server');
157
+ continue;
158
+ }
159
+ if (batch.docsDirty || projectHasDocuments(project)) {
160
+ // App rebuild may refresh designs CSS consumed by documents.
161
+ const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
162
+ if (!docs.ok) {
163
+ log.warn('document mount rebuild failed — keeping previous docs');
164
+ }
165
+ else {
166
+ needFullReload = true;
167
+ }
168
+ }
169
+ try {
170
+ await softReload(host, port, {
171
+ affectedChunks: report.affectedChunks ?? [],
172
+ seedChunks: report.seedChunks ?? [],
173
+ full: Boolean(report.full) || needFullReload,
174
+ islandHmr: Boolean(report.islandHmr) && !needFullReload,
175
+ });
176
+ log.info(needFullReload
177
+ ? 'soft reload ok (full page; docs)'
178
+ : report.islandHmr
179
+ ? 'soft reload ok (island HMR)'
180
+ : 'soft reload ok (full page)');
181
+ }
182
+ catch (err) {
183
+ log.warn(`soft reload failed (${err}) — restarting serve-host…`);
184
+ killChild(child);
185
+ child = spawnHost({ project, outDir, host, port });
186
+ }
187
+ continue;
188
+ }
189
+ if (batch.docsDirty) {
190
+ log.info('documents change detected — rebuilding document mount…');
191
+ const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
192
+ if (!docs.ok) {
193
+ log.warn('document mount rebuild failed — keeping previous docs');
194
+ continue;
195
+ }
196
+ try {
197
+ await softReload(host, port, { full: true, islandHmr: false });
198
+ log.info('soft reload ok (full page; docs)');
199
+ }
200
+ catch (err) {
201
+ log.warn(`soft reload failed (${err}) — restarting serve-host…`);
202
+ killChild(child);
203
+ child = spawnHost({ project, outDir, host, port });
204
+ }
205
+ }
206
+ }
207
+ }
208
+ finally {
209
+ signal?.removeEventListener('abort', onAbort);
210
+ await stop();
211
+ }
212
+ }
213
+ async function stop() {
214
+ if (stopped)
215
+ return;
216
+ stopped = true;
217
+ killChild(child);
218
+ child = null;
219
+ try {
220
+ ws.dispose();
221
+ }
222
+ catch {
223
+ /* ignore */
224
+ }
225
+ }
226
+ return { ws, rebuild, start, stop, project, outDir, host, port };
227
+ }
228
+ /** @deprecated use fileFingerprintMap */
229
+ export function srcFingerprint(srcDir) {
230
+ const map = fileFingerprintMap(srcDir);
231
+ let h = 0xcbf29ce484222325n;
232
+ const keys = [...map.keys()].sort();
233
+ for (const k of keys) {
234
+ for (const b of Buffer.from(`${k}|${map.get(k)}`)) {
235
+ h = (h * 0x100000001b3n + BigInt(b)) & 0xffffffffffffffffn;
236
+ }
237
+ }
238
+ return Number(h & 0xffffffffffffffffn);
239
+ }
240
+ /** @deprecated */
241
+ export function listWatchedFiles(srcDir) {
242
+ return [...fileFingerprintMap(srcDir).keys()];
243
+ }
244
+ function defaultSpawnHost(opts) {
245
+ const hostJs = path.join(opts.outDir, 'vmz-serve-host.mjs');
246
+ const node = process.env.VMZ_NODE || process.execPath;
247
+ return spawn(node, [hostJs], {
248
+ cwd: opts.project,
249
+ env: {
250
+ ...process.env,
251
+ VMZ_DIST: opts.outDir,
252
+ VMZ_PORT: String(opts.port),
253
+ VMZ_HOST: opts.host,
254
+ VMZ_DEV: '1',
255
+ },
256
+ stdio: ['ignore', 'inherit', 'inherit'],
257
+ });
258
+ }
259
+ /**
260
+ * @param {string} host
261
+ * @param {number} port
262
+ * @param {object} [payload]
263
+ */
264
+ async function defaultSoftReload(host, port, payload = {}) {
265
+ const url = `http://${host}:${port}/__vmz/reload`;
266
+ const body = JSON.stringify(payload);
267
+ const res = await fetch(url, {
268
+ method: 'POST',
269
+ headers: { 'content-type': 'application/json' },
270
+ body,
271
+ });
272
+ if (!res.ok)
273
+ throw new Error(`HTTP ${res.status}`);
274
+ const json = await res.json().catch(() => ({}));
275
+ if (!json?.ok)
276
+ throw new Error(JSON.stringify(json));
277
+ }
278
+ function killChild(child) {
279
+ if (!child || child.killed)
280
+ return;
281
+ try {
282
+ child.kill();
283
+ }
284
+ catch {
285
+ /* ignore */
286
+ }
287
+ }
288
+ function sleep(ms) {
289
+ return new Promise((r) => setTimeout(r, ms));
290
+ }