@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.
- package/README.md +48 -2
- package/bin/vmz.js +4 -0
- package/dist/application-cmd.d.ts +22 -0
- package/dist/application-cmd.js +348 -0
- package/dist/bundler-adapter.d.ts +64 -0
- package/dist/bundler-adapter.js +111 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +370 -0
- package/dist/dev-session.d.ts +35 -0
- package/dist/dev-session.js +290 -0
- package/dist/document-build.d.ts +99 -0
- package/dist/document-build.js +274 -0
- package/dist/document-check.d.ts +44 -0
- package/dist/document-check.js +246 -0
- package/dist/document-cmd.d.ts +9 -0
- package/dist/document-cmd.js +147 -0
- package/dist/document-designs.d.ts +9 -0
- package/dist/document-designs.js +126 -0
- package/dist/document-enrich.d.ts +23 -0
- package/dist/document-enrich.js +234 -0
- package/dist/document-evidence.d.ts +49 -0
- package/dist/document-evidence.js +501 -0
- package/dist/document-integrate.d.ts +35 -0
- package/dist/document-integrate.js +89 -0
- package/dist/document-interactive.d.ts +69 -0
- package/dist/document-interactive.js +255 -0
- package/dist/document-locale.d.ts +31 -0
- package/dist/document-locale.js +59 -0
- package/dist/document-markdown.d.ts +13 -0
- package/dist/document-markdown.js +39 -0
- package/dist/document-scan.d.ts +21 -0
- package/dist/document-scan.js +151 -0
- package/dist/document-schema.d.ts +87 -0
- package/dist/document-schema.js +88 -0
- package/dist/explain-cmd.d.ts +5 -0
- package/dist/explain-cmd.js +123 -0
- package/dist/index.d.ts +808 -0
- package/dist/index.js +569 -0
- package/dist/locale-check.d.ts +106 -0
- package/dist/locale-check.js +737 -0
- package/dist/locale-cmd.d.ts +5 -0
- package/dist/locale-cmd.js +443 -0
- package/dist/locale-delivery.d.ts +298 -0
- package/dist/locale-delivery.js +444 -0
- package/dist/locale-router.d.ts +207 -0
- package/dist/locale-router.js +508 -0
- package/dist/locale-runtime.d.ts +406 -0
- package/dist/locale-runtime.js +542 -0
- package/dist/locale-schema.d.ts +9 -0
- package/dist/locale-schema.js +10 -0
- package/dist/locale-tooling.d.ts +118 -0
- package/dist/locale-tooling.js +358 -0
- package/dist/log.d.ts +19 -0
- package/dist/log.js +42 -0
- package/dist/packages.d.ts +27 -0
- package/dist/packages.js +147 -0
- package/dist/plugin-host.d.ts +30 -0
- package/dist/plugin-host.js +370 -0
- package/dist/refactor-cmd.d.ts +8 -0
- package/dist/refactor-cmd.js +156 -0
- package/dist/resolve.d.ts +25 -0
- package/dist/resolve.js +56 -0
- package/dist/test-cmd.d.ts +9 -0
- package/dist/test-cmd.js +343 -0
- package/dist/test-compile.d.ts +2 -0
- package/dist/test-compile.js +3 -0
- package/dist/test-discover.d.ts +2 -0
- package/dist/test-discover.js +3 -0
- package/dist/test-logic.d.ts +2 -0
- package/dist/test-logic.js +3 -0
- package/dist/test-protocol.d.ts +2 -0
- package/dist/test-protocol.js +3 -0
- package/dist/watch-diff.d.ts +17 -0
- package/dist/watch-diff.js +56 -0
- 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
|
+
}
|