@vmz/vmz 0.3.6 → 0.3.7
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 +58 -55
- package/bin/vmz.js +9 -1
- package/delivery-runtime-files.json +38 -38
- package/dist/cli/cli.js +21 -17
- package/dist/dev/config-cycle-worker.d.ts +7 -0
- package/dist/dev/config-cycle-worker.js +83 -0
- package/dist/dev/config-cycle.d.ts +45 -0
- package/dist/dev/config-cycle.js +152 -0
- package/dist/dev/dev-session.d.ts +11 -0
- package/dist/dev/dev-session.js +89 -15
- package/dist/dev/dev-watch-roots.d.ts +7 -0
- package/dist/dev/dev-watch-roots.js +65 -2
- package/dist/document/document-build.js +24 -24
- package/dist/document/document-markdown.d.ts +1 -1
- package/dist/document/document-markdown.js +7 -21
- package/dist/workspace/plugin-host.d.ts +3 -5
- package/dist/workspace/plugin-host.js +85 -49
- package/dist/workspace/public-api.d.ts +5 -1
- package/dist/workspace/public-api.js +6 -22
- package/dist/workspace/wechat-packaging.d.ts +9 -2
- package/dist/workspace/wechat-packaging.js +15 -20
- package/locales/en-US/cli.json +126 -126
- package/locales/en-US/diag.json +7 -1
- package/locales/zh-CN/diag.json +3 -1
- package/package.json +23 -21
package/README.md
CHANGED
|
@@ -1,55 +1,58 @@
|
|
|
1
|
-
# `@vmz/vmz` (bin: `vmz`)
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
|
37
|
-
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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 {
|
|
190
|
-
const { plugins, engines } =
|
|
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,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;
|