@vmz/vmz 0.3.6 → 0.3.8

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 CHANGED
@@ -1,55 +1,58 @@
1
- # `@vmz/vmz` (bin: `vmz`)
2
-
3
- CLI / N-API toolchain host. Runtime mental entry is **`@vmz/core`**; optional official UI is **`@vmz/ui`**; plugins are
4
- **`@vmz/plugin-*`**. Shelved umbrella name: `@vmz/vmz`.
5
-
6
- When a project reaches the point where it has a browser surface, SSR, server work, documents, tests, and several
7
- deployment concerns, the usual workflow becomes a chain of tools that each see a different version of the application. A
8
- build may pass while a route boundary is wrong; a browser test may pass while SSR work is replayed; a plugin may
9
- transform code without anyone being able to explain the delivery result.
10
-
11
- `vmz` is the command-line home for a different workflow. Development, checking, building, serving, testing, document
12
- generation, and deployment output all begin from VMZ's understanding of the same application. The useful unit is not
13
- only a module graph. It is a program with state reads and writes, control regions, routes, server capabilities,
14
- ownership, and delivery boundaries.
15
-
16
- That means the CLI can grow into something more useful than a collection of commands: it can say why a change affects a
17
- region, why code was placed on the client, why a conservative boundary was used, or why a route and its server work
18
- belong together. ⚡
19
-
20
- With VMZ, a normal workflow is expected to answer all of these from the same source program:
21
-
22
- - **Develop:** which application regions are affected by a change?
23
- - **Check:** which state, route, server, or lifecycle boundary is unsafe?
24
- - **Build:** what belongs in browser, SSR, resume, and server output?
25
- - **Test:** what did a user interaction actually cause?
26
- - **Explain:** why did the compiler make that decision?
27
-
28
- Use it when you are adopting VMZ as the application model. The CLI targets VMZ-native source and semantics; Vue, React,
29
- and legacy VDOM ecosystems keep their own toolchains.
30
-
31
- ## One tool, several views of the same program
32
-
33
- | Workflow | The question it should answer |
34
- |-------------|--------------------------------------------------------------------|
35
- | Development | What changed, and which application regions are affected? |
36
- | Checking | Which state, route, server, or lifetime boundary cannot be proven? |
37
- | Building | What belongs in browser, SSR, resume, and server output? |
38
- | Testing | Did the application behave correctly and avoid unrelated work? |
39
- | Documents | Are project documents connected, localized, and deployable? |
40
-
41
- ### Designed for the npm world
42
-
43
- VMZ does not ask users to abandon JavaScript packaging. Node remains the npm, plugin, development-server, and
44
- orchestration host. A long-lived N-API bridge connects that ecosystem to Rust and oxc without reducing semantic analysis
45
- to a sequence of tiny file transforms.
46
-
47
- ### More than pass or fail
48
-
49
- The interesting future of the CLI is explanation. A useful compiler should expose the source span, graph edge, owner,
50
- deployment boundary, and fallback reason behind a decision. That is a better developer experience than adding more
51
- colored output to an opaque build. 🧭
52
-
53
- ## License
54
-
55
- MIT
1
+ # `@vmz/vmz` (bin: `vmz`)
2
+
3
+ Requires Node `>=24.12.0`. Project `vmz.config.ts|mts` loads through native ESM (erasable TypeScript only, no runtime
4
+ transpiler).
5
+
6
+ CLI / N-API toolchain host. Runtime mental entry is **`@vmz/core`**; optional official UI is **`@vmz/ui`**; plugins are
7
+ **`@vmz/plugin-*`**. Shelved umbrella name: `@vmz/vmz`.
8
+
9
+ When a project reaches the point where it has a browser surface, SSR, server work, documents, tests, and several
10
+ deployment concerns, the usual workflow becomes a chain of tools that each see a different version of the application. A
11
+ build may pass while a route boundary is wrong; a browser test may pass while SSR work is replayed; a plugin may
12
+ transform code without anyone being able to explain the delivery result.
13
+
14
+ `vmz` is the command-line home for a different workflow. Development, checking, building, serving, testing, document
15
+ generation, and deployment output all begin from VMZ's understanding of the same application. The useful unit is not
16
+ only a module graph. It is a program with state reads and writes, control regions, routes, server capabilities,
17
+ ownership, and delivery boundaries.
18
+
19
+ That means the CLI can grow into something more useful than a collection of commands: it can say why a change affects a
20
+ region, why code was placed on the client, why a conservative boundary was used, or why a route and its server work
21
+ belong together. ⚡
22
+
23
+ With VMZ, a normal workflow is expected to answer all of these from the same source program:
24
+
25
+ - **Develop:** which application regions are affected by a change?
26
+ - **Check:** which state, route, server, or lifecycle boundary is unsafe?
27
+ - **Build:** what belongs in browser, SSR, resume, and server output?
28
+ - **Test:** what did a user interaction actually cause?
29
+ - **Explain:** why did the compiler make that decision?
30
+
31
+ Use it when you are adopting VMZ as the application model. The CLI targets VMZ-native source and semantics; Vue, React,
32
+ and legacy VDOM ecosystems keep their own toolchains.
33
+
34
+ ## One tool, several views of the same program
35
+
36
+ | Workflow | The question it should answer |
37
+ |-------------|--------------------------------------------------------------------|
38
+ | Development | What changed, and which application regions are affected? |
39
+ | Checking | Which state, route, server, or lifetime boundary cannot be proven? |
40
+ | Building | What belongs in browser, SSR, resume, and server output? |
41
+ | Testing | Did the application behave correctly and avoid unrelated work? |
42
+ | Documents | Are project documents connected, localized, and deployable? |
43
+
44
+ ### Designed for the npm world
45
+
46
+ VMZ does not ask users to abandon JavaScript packaging. Node remains the npm, plugin, development-server, and
47
+ orchestration host. A long-lived N-API bridge connects that ecosystem to Rust and oxc without reducing semantic analysis
48
+ to a sequence of tiny file transforms.
49
+
50
+ ### More than pass or fail
51
+
52
+ The interesting future of the CLI is explanation. A useful compiler should expose the source span, graph edge, owner,
53
+ deployment boundary, and fallback reason behind a decision. That is a better developer experience than adding more
54
+ colored output to an opaque build. 🧭
55
+
56
+ ## License
57
+
58
+ MIT
package/bin/vmz.js CHANGED
@@ -1,4 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from '../dist/cli/cli.js';
2
+ const [major = 0, minor = 0] = process.versions.node.split('.').map((part) => Number.parseInt(part, 10));
3
+ if (major < 24 || (major === 24 && minor < 12)) {
4
+ process.stderr.write(
5
+ `vmz: Node >=24.12.0 is required (found ${process.versions.node}).\n` +
6
+ ' Why: project `vmz.config.*` and plugin entries load through native ESM type stripping.\n',
7
+ );
8
+ process.exit(1);
9
+ }
10
+ const { runCli } = await import('../dist/cli/cli.js');
3
11
  const code = await runCli(process.argv.slice(2));
4
12
  process.exit(code ?? 0);
@@ -1,38 +1,38 @@
1
- {
2
- "schema": "vmz.delivery-runtime-files.v0",
3
- "note": "Browser/SSR runtime companions copied into app delivery dist. Consumed by vmz-compiler emit_runtime_js and pack ensureRuntimeCompanions.",
4
- "flatBarrels": [
5
- { "src": "faces/server.js", "out": "vmz-runtime.js" },
6
- { "src": "faces/dom.js", "out": "vmz-dom.js" },
7
- { "src": "browser/dom-core.js", "out": "dom-core.js" },
8
- { "src": "ssr/dom-ssr.js", "out": "dom-ssr.js" },
9
- { "src": "faces/dom.client.js", "out": "dom.client.js" },
10
- { "src": "faces/dom.browser.js", "out": "dom.browser.js" },
11
- { "src": "browser/direct-host-box.js", "out": "direct-host-box.js" },
12
- { "src": "browser/unknown-component.js", "out": "unknown-component.js" },
13
- { "src": "shared/dom-attr-normalize.js", "out": "dom-attr-normalize.js" },
14
- { "src": "faces/http.js", "out": "vmz-http.js" },
15
- { "src": "browser/client-nav.js", "out": "vmz-client-nav.js" }
16
- ],
17
- "nested": [
18
- { "src": "browser/diagnostics/precision-trace.js", "out": "browser/diagnostics/precision-trace.js" },
19
- { "src": "browser/dom/attributes.js", "out": "browser/dom/attributes.js" },
20
- { "src": "browser/dom/component.js", "out": "browser/dom/component.js" },
21
- { "src": "browser/dom/direct-api.js", "out": "browser/dom/direct-api.js" },
22
- { "src": "browser/dom/lifecycle.js", "out": "browser/dom/lifecycle.js" },
23
- { "src": "browser/dom/registry.js", "out": "browser/dom/registry.js" },
24
- { "src": "browser/reactivity/patch-scheduler.js", "out": "browser/reactivity/patch-scheduler.js" },
25
- { "src": "browser/reactivity/write-barrier.js", "out": "browser/reactivity/write-barrier.js" },
26
- { "src": "browser/navigation/client-navigation.js", "out": "browser/navigation/client-navigation.js" },
27
- { "src": "browser/navigation/extract-html.js", "out": "browser/navigation/extract-html.js" },
28
- { "src": "browser/navigation/layout-retain.js", "out": "browser/navigation/layout-retain.js" },
29
- { "src": "browser/navigation/locale-transition.js", "out": "browser/navigation/locale-transition.js" },
30
- { "src": "browser/navigation/scroll-focus.js", "out": "browser/navigation/scroll-focus.js" },
31
- { "src": "browser/resume/adopt.js", "out": "browser/resume/adopt.js" },
32
- { "src": "browser/resume/hydrate.js", "out": "browser/resume/hydrate.js" },
33
- { "src": "browser/resume/resume.js", "out": "browser/resume/resume.js" },
34
- { "src": "ssr/render.js", "out": "ssr/render.js" },
35
- { "src": "ssr/serialize.js", "out": "ssr/serialize.js" },
36
- { "src": "ssr/document.js", "out": "ssr/document.js" }
37
- ]
38
- }
1
+ {
2
+ "schema": "vmz.delivery-runtime-files.v0",
3
+ "note": "Browser/SSR runtime companions copied into app delivery dist. Consumed by vmz-compiler emit_runtime_js and pack ensureRuntimeCompanions.",
4
+ "flatBarrels": [
5
+ { "src": "faces/server.js", "out": "vmz-runtime.js" },
6
+ { "src": "faces/dom.js", "out": "vmz-dom.js" },
7
+ { "src": "browser/dom-core.js", "out": "dom-core.js" },
8
+ { "src": "ssr/dom-ssr.js", "out": "dom-ssr.js" },
9
+ { "src": "faces/dom.client.js", "out": "dom.client.js" },
10
+ { "src": "faces/dom.browser.js", "out": "dom.browser.js" },
11
+ { "src": "browser/direct-host-box.js", "out": "direct-host-box.js" },
12
+ { "src": "browser/unknown-component.js", "out": "unknown-component.js" },
13
+ { "src": "shared/dom-attr-normalize.js", "out": "dom-attr-normalize.js" },
14
+ { "src": "faces/http.js", "out": "vmz-http.js" },
15
+ { "src": "browser/client-nav.js", "out": "vmz-client-nav.js" }
16
+ ],
17
+ "nested": [
18
+ { "src": "browser/diagnostics/precision-trace.js", "out": "browser/diagnostics/precision-trace.js" },
19
+ { "src": "browser/dom/attributes.js", "out": "browser/dom/attributes.js" },
20
+ { "src": "browser/dom/component.js", "out": "browser/dom/component.js" },
21
+ { "src": "browser/dom/direct-api.js", "out": "browser/dom/direct-api.js" },
22
+ { "src": "browser/dom/lifecycle.js", "out": "browser/dom/lifecycle.js" },
23
+ { "src": "browser/dom/registry.js", "out": "browser/dom/registry.js" },
24
+ { "src": "browser/reactivity/patch-scheduler.js", "out": "browser/reactivity/patch-scheduler.js" },
25
+ { "src": "browser/reactivity/write-barrier.js", "out": "browser/reactivity/write-barrier.js" },
26
+ { "src": "browser/navigation/client-navigation.js", "out": "browser/navigation/client-navigation.js" },
27
+ { "src": "browser/navigation/extract-html.js", "out": "browser/navigation/extract-html.js" },
28
+ { "src": "browser/navigation/layout-retain.js", "out": "browser/navigation/layout-retain.js" },
29
+ { "src": "browser/navigation/locale-transition.js", "out": "browser/navigation/locale-transition.js" },
30
+ { "src": "browser/navigation/scroll-focus.js", "out": "browser/navigation/scroll-focus.js" },
31
+ { "src": "browser/resume/adopt.js", "out": "browser/resume/adopt.js" },
32
+ { "src": "browser/resume/hydrate.js", "out": "browser/resume/hydrate.js" },
33
+ { "src": "browser/resume/resume.js", "out": "browser/resume/resume.js" },
34
+ { "src": "ssr/render.js", "out": "ssr/render.js" },
35
+ { "src": "ssr/serialize.js", "out": "ssr/serialize.js" },
36
+ { "src": "ssr/document.js", "out": "ssr/document.js" }
37
+ ]
38
+ }
package/dist/cli/cli.js CHANGED
@@ -13,7 +13,7 @@ import { registerDocumentCommands } from '../document/document-cmd.js';
13
13
  import { buildIntegratedDocuments, projectHasDocuments } from '../document/document-integrate.js';
14
14
  import { registerExplainCommand } from './explain-cmd.js';
15
15
  import { registerGithubActionsCommand } from './github-actions-cmd.js';
16
- import { createWorkspace, getProtocolVersions, HOST_PROTOCOL, materializeServeHostRuntime, resolveCoreRuntimeDist, resolveNativePath, } from '../workspace/public-api.js';
16
+ import { createWorkspace, getProtocolVersions, HOST_PROTOCOL, materializeServeHostRuntime, materializeWechatPackaging, resolveCoreRuntimeDist, resolveNativePath, } from '../workspace/public-api.js';
17
17
  import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from '../workspace/invocation.js';
18
18
  import { emitLocaleRuntimeModules, localeHasErrors } from '../locale/locale-check.js';
19
19
  import { registerLocaleCommands } from '../locale/locale-cmd.js';
@@ -56,32 +56,29 @@ function buildProductCli(opts = {}) {
56
56
  cli.command('version', 'cli.cmd.version').action(() => cmdVersion());
57
57
  return cli;
58
58
  }
59
- const withWorkspaceOpts = (cmd) => cmd
59
+ const withCoreOpts = (cmd) => cmd
60
60
  .option('--out-dir, -o <dir>', 'cli.opt.out-dir')
61
61
  .option('--release', 'cli.opt.release')
62
62
  .option('--profile <name>', 'cli.opt.profile')
63
63
  .option('--target <id>', 'cli.opt.target')
64
64
  .option('--origin <url>', 'cli.opt.origin')
65
- .option('--host <host>', 'cli.opt.host')
66
- .option('--port <port>', 'cli.opt.port')
67
- .option('--poll-ms <ms>', 'cli.opt.poll-ms')
68
- .option('--build', 'cli.opt.build')
69
65
  .option('--check', 'cli.opt.check')
70
66
  .option('--deny-warnings', 'cli.opt.deny-warnings');
71
- withWorkspaceOpts(cli.command('check', 'cli.cmd.check'))
67
+ const withServeExtras = (cmd) => cmd.option('--host <host>', 'cli.opt.host').option('--port <port>', 'cli.opt.port').option('--poll-ms <ms>', 'cli.opt.poll-ms').option('--build', 'cli.opt.build');
68
+ withCoreOpts(cli.command('check', 'cli.cmd.check'))
72
69
  .option('--level <level>', 'cli.opt.level')
73
70
  .option('--info', 'cli.opt.info-level')
74
71
  .option('--style <style>', 'cli.opt.diag-style')
75
72
  .action((options) => cmdCheck(options));
76
- withWorkspaceOpts(cli.command('build', 'cli.cmd.build')).action((options) => cmdBuild(options));
77
- withWorkspaceOpts(cli.command('serve', 'cli.cmd.serve')).action((options) => cmdServe(options));
78
- withWorkspaceOpts(cli.command('dev', 'cli.cmd.dev')).action((options) => cmdDev(options));
79
- withWorkspaceOpts(cli.command('format', 'cli.cmd.format'))
73
+ withCoreOpts(cli.command('build', 'cli.cmd.build')).action((options) => cmdBuild(options));
74
+ withServeExtras(withCoreOpts(cli.command('serve', 'cli.cmd.serve'))).action((options) => cmdServe(options));
75
+ withServeExtras(withCoreOpts(cli.command('dev', 'cli.cmd.dev'))).action((options) => cmdDev(options));
76
+ withCoreOpts(cli.command('format', 'cli.cmd.format'))
80
77
  .option('--no-javascript', 'cli.opt.format-no-javascript')
81
78
  .option('--no-rust', 'cli.opt.format-no-rust')
82
79
  .option('--no-vmz', 'cli.opt.format-no-vmz')
83
80
  .action((options) => cmdFormat(options));
84
- withWorkspaceOpts(cli.command('lint', 'cli.cmd.lint'))
81
+ withCoreOpts(cli.command('lint', 'cli.cmd.lint'))
85
82
  .option('--level <level>', 'cli.opt.level')
86
83
  .option('--info', 'cli.opt.info-level')
87
84
  .option('--style <style>', 'cli.opt.diag-style')
@@ -155,9 +152,10 @@ async function cmdCheck(args) {
155
152
  });
156
153
  const meta = readPackageMeta(project);
157
154
  log.info(`check ${project}${meta?.name ? ` (${meta.name})` : ''}`);
155
+ const cfg = await loadVmzConfig(project);
158
156
  const ws = createWorkspace({ root: project, outDir });
159
157
  try {
160
- return runWithPlugins(ws, project, outDir, async () => {
158
+ return runWithPlugins(ws, cfg, project, outDir, async () => {
161
159
  const report = ws.check();
162
160
  const { checkLocales, localeHasErrors } = await import('../locale/locale-check.js');
163
161
  const localeReport = checkLocales({ projectRoot: project, checkUnused: false });
@@ -185,9 +183,9 @@ function dedupeDiagnostics(list) {
185
183
  }
186
184
  return out;
187
185
  }
188
- async function runWithPlugins(ws, project, outDir, fn) {
189
- const { loadVmzConfig, applyPlugins } = await import('../workspace/plugin-host.js');
190
- const { plugins, engines } = await loadVmzConfig(project);
186
+ async function runWithPlugins(ws, cfg, project, outDir, fn) {
187
+ const { applyPlugins } = await import('../workspace/plugin-host.js');
188
+ const { plugins, engines } = cfg;
191
189
  if (plugins.length) {
192
190
  await applyPlugins(ws, plugins, { project, outDir, engines });
193
191
  }
@@ -221,10 +219,12 @@ async function cmdBuild(args) {
221
219
  }
222
220
  const outDir = resolveProfileArtifactDir(outDirRoot, selected.profile);
223
221
  log.info(`build ${project} → ${outDir}${wechatPack ? ' (target=mini-program-wechat)' : ''} (out-dir=${outDirRoot}, name=${selected.profile.name})`);
222
+ if (wechatPack)
223
+ materializeWechatPackaging(project, cfg.delivery);
224
224
  const ws = createWorkspace({ root: project, outDir });
225
225
  try {
226
226
  log.info(`delivery profile ${selected.selection.profileId} (assembly=${selected.selection.assembly}, name=${selected.profile.name})`);
227
- const code = await runWithPlugins(ws, project, outDir, () => {
227
+ const code = await runWithPlugins(ws, cfg, project, outDir, () => {
228
228
  const report = ws.build(Boolean(args.release));
229
229
  const errors = log.diagnostics(report.diagnostics ?? []);
230
230
  if (errors)
@@ -488,6 +488,7 @@ async function cmdDev(args) {
488
488
  port,
489
489
  pollMs,
490
490
  target: targetRaw,
491
+ delivery: cfg.delivery,
491
492
  signal: ac.signal,
492
493
  });
493
494
  try {
@@ -497,6 +498,9 @@ async function cmdDev(args) {
497
498
  catch (err) {
498
499
  if (ac.signal.aborted)
499
500
  return 0;
501
+ // Config/plugin failures carry a product diagnostic: let the CLI boundary print once.
502
+ if (err && typeof err === 'object' && 'diagnostic' in err)
503
+ throw err;
500
504
  log.error(String(err));
501
505
  return 1;
502
506
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Child process for one dev config build cycle.
3
+ *
4
+ * Evaluates `vmz.config.*`, applies plugin contributions into the workspace,
5
+ * then writes a data-only report (no functions across the process boundary).
6
+ */
7
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Child process for one dev config build cycle.
3
+ *
4
+ * Evaluates `vmz.config.*`, applies plugin contributions into the workspace,
5
+ * then writes a data-only report (no functions across the process boundary).
6
+ */
7
+ import { readFileSync, writeFileSync } from 'node:fs';
8
+ import path from 'node:path';
9
+ import { applyPlugins, loadVmzConfig } from '../workspace/plugin-host.js';
10
+ import { createWorkspace } from '../workspace/public-api.js';
11
+ function errorDetail(err) {
12
+ const message = err instanceof Error ? err.message : String(err);
13
+ const cause = err instanceof Error && err.cause ? `: ${errorDetail(err.cause)}` : '';
14
+ return `${message}${cause}`;
15
+ }
16
+ function writeReport(reportPath, report) {
17
+ let text;
18
+ try {
19
+ text = JSON.stringify(report, (_key, value) => (typeof value === 'function' ? undefined : value));
20
+ }
21
+ catch (err) {
22
+ text = JSON.stringify({
23
+ ok: false,
24
+ configPath: null,
25
+ pluginPath: null,
26
+ plugins: [],
27
+ engines: {},
28
+ delivery: null,
29
+ error: {
30
+ code: 'vmz::config::load_failed',
31
+ severity: 'error',
32
+ args: { detail: `config report is not serializable: ${errorDetail(err)}` },
33
+ },
34
+ });
35
+ }
36
+ writeFileSync(reportPath, text);
37
+ }
38
+ const [inputPath, reportPath] = process.argv.slice(2);
39
+ if (!inputPath || !reportPath) {
40
+ process.stderr.write('config-cycle-worker: <input.json> <report.json> required\n');
41
+ process.exit(2);
42
+ }
43
+ const input = JSON.parse(readFileSync(inputPath, 'utf8'));
44
+ let ws = null;
45
+ try {
46
+ const cfg = await loadVmzConfig(input.project);
47
+ ws = createWorkspace({ root: input.project, outDir: input.outDir });
48
+ if (cfg.plugins.length) {
49
+ await applyPlugins(ws, cfg.plugins, { project: input.project, outDir: input.outDir, engines: cfg.engines });
50
+ }
51
+ writeReport(reportPath, {
52
+ ok: true,
53
+ configPath: cfg.path,
54
+ pluginPath: cfg.pluginPath,
55
+ plugins: cfg.plugins.map((p) => ({
56
+ name: p.manifest.name,
57
+ version: p.manifest.version,
58
+ stages: p.manifest.stages ?? [],
59
+ })),
60
+ engines: cfg.engines,
61
+ delivery: cfg.delivery ?? null,
62
+ });
63
+ }
64
+ catch (err) {
65
+ const diagnostic = err?.diagnostic ?? {
66
+ code: 'vmz::config::load_failed',
67
+ severity: 'error',
68
+ path: path.join(input.project, 'vmz.config.ts'),
69
+ args: { detail: errorDetail(err) },
70
+ };
71
+ writeReport(reportPath, {
72
+ ok: false,
73
+ configPath: null,
74
+ pluginPath: null,
75
+ plugins: [],
76
+ engines: {},
77
+ delivery: null,
78
+ error: diagnostic,
79
+ });
80
+ }
81
+ finally {
82
+ ws?.dispose();
83
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Dev config build cycle.
3
+ *
4
+ * Each cycle runs config evaluation + plugin contribution in its own Node process.
5
+ * Functions and plugin objects stay in that process; the parent receives data only.
6
+ * Config/IPS changes restart the cycle: the old process is cancelled and awaited
7
+ * before a new one may write plugin artifacts.
8
+ */
9
+ export interface ConfigCycleDiagnostic {
10
+ code: string;
11
+ severity?: 'error';
12
+ path?: string;
13
+ args?: Record<string, string>;
14
+ }
15
+ export interface ConfigCycleReport {
16
+ ok: boolean;
17
+ configPath: string | null;
18
+ pluginPath: string | null;
19
+ plugins: Array<{
20
+ name: string;
21
+ version: string;
22
+ stages: string[];
23
+ }>;
24
+ engines: Record<string, string>;
25
+ delivery: unknown;
26
+ error?: ConfigCycleDiagnostic;
27
+ }
28
+ export interface ConfigCycleOptions {
29
+ project: string;
30
+ outDir: string;
31
+ /** Built worker (`dist/dev/config-cycle-worker.js`); defaults next to this module. */
32
+ workerPath?: string;
33
+ nodePath?: string;
34
+ timeoutMs?: number;
35
+ }
36
+ export interface ConfigCycle {
37
+ start(): Promise<ConfigCycleReport>;
38
+ restart(): Promise<ConfigCycleReport>;
39
+ cancel(): Promise<void>;
40
+ running(): boolean;
41
+ }
42
+ export declare class ConfigCycleCancelledError extends Error {
43
+ constructor();
44
+ }
45
+ export declare function createConfigCycle(options: ConfigCycleOptions): ConfigCycle;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Dev config build cycle.
3
+ *
4
+ * Each cycle runs config evaluation + plugin contribution in its own Node process.
5
+ * Functions and plugin objects stay in that process; the parent receives data only.
6
+ * Config/IPS changes restart the cycle: the old process is cancelled and awaited
7
+ * before a new one may write plugin artifacts.
8
+ */
9
+ import { spawn } from 'node:child_process';
10
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ export class ConfigCycleCancelledError extends Error {
15
+ constructor() {
16
+ super('vmz dev: config build process cancelled');
17
+ this.name = 'ConfigCycleCancelledError';
18
+ }
19
+ }
20
+ function defaultWorkerPath() {
21
+ return fileURLToPath(new URL('./config-cycle-worker.js', import.meta.url));
22
+ }
23
+ function emptyReport(project, error) {
24
+ return {
25
+ ok: !error,
26
+ configPath: null,
27
+ pluginPath: null,
28
+ plugins: [],
29
+ engines: {},
30
+ delivery: null,
31
+ ...(error ? { error } : {}),
32
+ };
33
+ }
34
+ function syntheticFailure(project, detail, configPath) {
35
+ return emptyReport(project, {
36
+ code: 'vmz::config::load_failed',
37
+ severity: 'error',
38
+ path: configPath ?? path.join(project, 'vmz.config.ts'),
39
+ args: { detail },
40
+ });
41
+ }
42
+ export function createConfigCycle(options) {
43
+ const project = path.resolve(options.project);
44
+ const outDir = path.resolve(options.outDir);
45
+ const workerPath = options.workerPath ?? defaultWorkerPath();
46
+ const nodePath = options.nodePath ?? process.env.VMZ_NODE ?? process.execPath;
47
+ const timeoutMs = options.timeoutMs ?? 120_000;
48
+ let child = null;
49
+ let exited = null;
50
+ let cancelled = false;
51
+ async function cancel() {
52
+ if (!child)
53
+ return;
54
+ cancelled = true;
55
+ const proc = child;
56
+ const done = exited;
57
+ try {
58
+ proc.kill();
59
+ }
60
+ catch {
61
+ /* already gone */
62
+ }
63
+ if (done)
64
+ await Promise.race([done, new Promise((r) => setTimeout(r, 5000))]);
65
+ if (proc.exitCode == null && proc.signalCode == null) {
66
+ try {
67
+ proc.kill('SIGKILL');
68
+ }
69
+ catch {
70
+ /* already gone */
71
+ }
72
+ if (done)
73
+ await Promise.race([done, new Promise((r) => setTimeout(r, 2000))]);
74
+ }
75
+ }
76
+ async function start() {
77
+ if (child)
78
+ throw new Error('vmz dev: config build process already running');
79
+ if (!existsSync(workerPath)) {
80
+ throw new Error(`vmz dev: missing config cycle worker ${workerPath} — run build:js`);
81
+ }
82
+ cancelled = false;
83
+ const tmp = mkdtempSync(path.join(os.tmpdir(), 'vmz-config-cycle-'));
84
+ const inputPath = path.join(tmp, 'input.json');
85
+ const reportPath = path.join(tmp, 'report.json');
86
+ writeFileSync(inputPath, JSON.stringify({ project, outDir }));
87
+ let stderr = '';
88
+ let timedOut = false;
89
+ const proc = spawn(nodePath, [workerPath, inputPath, reportPath], {
90
+ cwd: project,
91
+ env: { ...process.env, VMZ_PROJECT_ROOT: project, VMZ_DIST: outDir },
92
+ stdio: ['ignore', 'pipe', 'pipe'],
93
+ windowsHide: true,
94
+ });
95
+ child = proc;
96
+ proc.stderr?.on('data', (chunk) => {
97
+ if (stderr.length < 16_384)
98
+ stderr += chunk.toString();
99
+ });
100
+ exited = new Promise((resolve) => {
101
+ proc.once('exit', (code, signal) => resolve({ code, signal }));
102
+ });
103
+ const timer = setTimeout(() => {
104
+ timedOut = true;
105
+ try {
106
+ proc.kill();
107
+ }
108
+ catch {
109
+ /* already gone */
110
+ }
111
+ }, timeoutMs);
112
+ const result = await exited;
113
+ clearTimeout(timer);
114
+ child = null;
115
+ exited = null;
116
+ let report = null;
117
+ if (!cancelled) {
118
+ try {
119
+ report = JSON.parse(readFileSync(reportPath, 'utf8'));
120
+ }
121
+ catch {
122
+ report = null;
123
+ }
124
+ }
125
+ rmSync(tmp, { recursive: true, force: true });
126
+ if (cancelled)
127
+ throw new ConfigCycleCancelledError();
128
+ if (!report) {
129
+ const detail = timedOut
130
+ ? `config build process timed out after ${timeoutMs}ms`
131
+ : `config build process exited with code ${String(result.code)}${result.signal ? ` (${result.signal})` : ''}${stderr.trim() ? `: ${stderr.trim().split('\n').slice(0, 4).join(' | ')}` : ''}`;
132
+ return syntheticFailure(project, detail);
133
+ }
134
+ if (!report.ok && report.error) {
135
+ if (!report.error.args?.detail && stderr.trim()) {
136
+ report.error.args = { ...(report.error.args ?? {}), detail: stderr.trim().split('\n').slice(0, 4).join(' | ') };
137
+ }
138
+ return report;
139
+ }
140
+ return report;
141
+ }
142
+ async function restart() {
143
+ await cancel();
144
+ return start();
145
+ }
146
+ return {
147
+ start,
148
+ restart,
149
+ cancel,
150
+ running: () => child != null,
151
+ };
152
+ }
@@ -10,6 +10,7 @@
10
10
  * 3. Soft reload failure → **auto-respawn** serve-host (fresh ESM graph), not "keep broken host".
11
11
  */
12
12
  import { createWorkspace } from '../workspace/public-api.js';
13
+ import { type ConfigCycle } from './config-cycle.js';
13
14
  interface DevSessionOptions {
14
15
  project: string;
15
16
  outDir: string;
@@ -17,6 +18,8 @@ interface DevSessionOptions {
17
18
  port?: number;
18
19
  pollMs?: number;
19
20
  target?: 'browser' | 'mini-program-wechat';
21
+ /** Delivery snapshot evaluated by the config host; the package contract is prepared from it. */
22
+ delivery?: unknown;
20
23
  signal?: AbortSignal;
21
24
  createWorkspaceFn?: typeof createWorkspace;
22
25
  spawnHostFn?: (opts: {
@@ -26,6 +29,14 @@ interface DevSessionOptions {
26
29
  port: number;
27
30
  }) => import('node:child_process').ChildProcess;
28
31
  softReloadFn?: (host: string, port: number, payload?: object) => Promise<void>;
32
+ /** Override the detached config build process (tests). */
33
+ configCycleFn?: (opts: {
34
+ project: string;
35
+ outDir: string;
36
+ workerPath?: string;
37
+ }) => ConfigCycle;
38
+ /** Built config cycle worker path (defaults to `dist/dev/config-cycle-worker.js`). */
39
+ configCycleWorkerPath?: string;
29
40
  }
30
41
  export declare function createDevSession(options: DevSessionOptions): {
31
42
  ws: any;