@vmz/vmz 0.1.0 → 0.1.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/dist/build-assemble.js +4 -3
- package/dist/cdn-policy.js +5 -8
- package/dist/cli.js +20 -7
- package/dist/content-addressed-assets.js +2 -1
- package/dist/delivery-profile.d.ts +10 -0
- package/dist/delivery-profile.js +69 -0
- package/dist/dev-session.d.ts +6 -0
- package/dist/dev-session.js +166 -40
- package/dist/document-build.js +24 -28
- package/dist/document-cmd.js +2 -9
- package/dist/document-integrate.js +10 -17
- package/dist/embedded-packaging.js +3 -7
- package/dist/index.d.ts +19 -1
- package/dist/index.js +62 -2
- package/dist/locale-check.js +28 -58
- package/dist/locale-cmd.js +10 -42
- package/dist/locale-route-emit.d.ts +4 -1
- package/dist/locale-route-emit.js +7 -32
- package/dist/mini-host.d.ts +47 -0
- package/dist/mini-host.js +202 -0
- package/dist/native-addon.d.ts +9 -0
- package/dist/native-addon.js +84 -0
- package/dist/pack-client-packages.d.ts +25 -0
- package/dist/pack-client-packages.js +399 -0
- package/dist/pack.d.ts +21 -3
- package/dist/pack.js +21 -6
- package/dist/pretty-json.d.ts +19 -0
- package/dist/pretty-json.js +43 -0
- package/dist/production-observability.js +3 -2
- package/dist/production-test-pack.js +4 -3
- package/dist/release-pack.js +5 -18
- package/dist/route-path.d.ts +35 -0
- package/dist/route-path.js +77 -0
- package/dist/server-artifact.js +5 -6
- package/dist/site-delivery.js +3 -2
- package/dist/static-emit.js +67 -86
- package/dist/test-cmd.js +2 -1
- package/dist/wechat-packaging.d.ts +22 -0
- package/dist/wechat-packaging.js +59 -0
- package/package.json +12 -12
package/dist/build-assemble.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* B5 Assemble dispatch + B6 build-proof (per-build semantic id slots).
|
|
3
3
|
*/
|
|
4
4
|
// @ts-nocheck
|
|
5
|
-
import { mkdirSync
|
|
5
|
+
import { mkdirSync } from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { semanticIdsForAssembly, sha256Hex, canonicalJson } from './delivery-profile.js';
|
|
8
8
|
import { emitServerArtifact } from './server-artifact.js';
|
|
9
9
|
import { emitEmbeddedPackaging } from './embedded-packaging.js';
|
|
10
10
|
import { emitSiteDelivery } from './site-delivery.js';
|
|
11
11
|
import { emitWebStatic } from './static-emit.js';
|
|
12
|
+
import { writePrettyJsonFile } from './pretty-json.js';
|
|
12
13
|
export const BUILD_PROOF_SCHEMA = 'vmz.build.proof.v0';
|
|
13
14
|
export const ASSEMBLE_MANIFEST_SCHEMA = 'vmz.assemble.manifest.v0';
|
|
14
15
|
/**
|
|
@@ -106,7 +107,7 @@ export async function assembleDelivery(outDir, ctx) {
|
|
|
106
107
|
const vmzDir = path.join(outDir, '_vmz');
|
|
107
108
|
mkdirSync(vmzDir, { recursive: true });
|
|
108
109
|
const file = path.join(vmzDir, 'assemble-manifest.json');
|
|
109
|
-
|
|
110
|
+
writePrettyJsonFile(file, result);
|
|
110
111
|
return { manifest: result, path: file };
|
|
111
112
|
}
|
|
112
113
|
/**
|
|
@@ -186,6 +187,6 @@ export function emitBuildProof(outDir, ctx) {
|
|
|
186
187
|
const vmzDir = path.join(outDir, '_vmz');
|
|
187
188
|
mkdirSync(vmzDir, { recursive: true });
|
|
188
189
|
const file = path.join(vmzDir, 'build-proof.json');
|
|
189
|
-
|
|
190
|
+
writePrettyJsonFile(file, body);
|
|
190
191
|
return { proof: body, path: file };
|
|
191
192
|
}
|
package/dist/cdn-policy.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { assertLocaleCacheKey, localeAwareCacheKey } from './locale-router.js';
|
|
11
|
+
import { writePrettyJsonFile } from './pretty-json.js';
|
|
11
12
|
export const CDN_POLICY_MANIFEST_SCHEMA = 'vmz.cdn.policy_manifest.v0';
|
|
12
13
|
export const CDN_ADAPTER_PROJECTION_SCHEMA = 'vmz.cdn.adapter_projection.v0';
|
|
13
14
|
/** HTML: revalidate. Hashed/static assets: long immutable. */
|
|
@@ -27,11 +28,7 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
|
27
28
|
const origin = String(staticManifest.origin || '');
|
|
28
29
|
const localeArt = opts.localeArtifact || null;
|
|
29
30
|
const localeRedirects = buildOmitPrefixRedirects(staticManifest, localeArt);
|
|
30
|
-
const redirects = [
|
|
31
|
-
{ from: '/home', to: '/', status: 301, reason: 'canonical-alias' },
|
|
32
|
-
...localeRedirects,
|
|
33
|
-
...(opts.redirects || []),
|
|
34
|
-
];
|
|
31
|
+
const redirects = [{ from: '/home', to: '/', status: 301, reason: 'canonical-alias' }, ...localeRedirects, ...(opts.redirects || [])];
|
|
35
32
|
const headers = [
|
|
36
33
|
{ match: '**/*.html', headers: { 'cache-control': CACHE_HTML } },
|
|
37
34
|
{
|
|
@@ -149,14 +146,14 @@ export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
|
|
|
149
146
|
const policy = buildCdnPolicyManifest(staticManifest, { ...opts, localeArtifact });
|
|
150
147
|
const vmzDir = path.join(distDir, '_vmz');
|
|
151
148
|
fs.mkdirSync(vmzDir, { recursive: true });
|
|
152
|
-
|
|
149
|
+
writePrettyJsonFile(path.join(vmzDir, 'cdn-policy-manifest.json'), policy);
|
|
153
150
|
const local = projectCdnAdapter(policy, 'local-static');
|
|
154
151
|
const netlify = projectCdnAdapter(policy, 'netlify');
|
|
155
152
|
const adaptersDir = path.join(vmzDir, 'adapters');
|
|
156
153
|
fs.mkdirSync(path.join(adaptersDir, 'local-static'), { recursive: true });
|
|
157
154
|
fs.mkdirSync(path.join(adaptersDir, 'netlify'), { recursive: true });
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
writePrettyJsonFile(path.join(adaptersDir, 'local-static', 'projection.json'), local);
|
|
156
|
+
writePrettyJsonFile(path.join(adaptersDir, 'netlify', 'projection.json'), netlify);
|
|
160
157
|
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_headers'), String(netlify.files['_headers'] || ''), 'utf8');
|
|
161
158
|
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_redirects'), String(netlify.files['_redirects'] || ''), 'utf8');
|
|
162
159
|
return { policy, adapters: { 'local-static': local, netlify } };
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { copyFileSync, existsSync } from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntimeDist } from './index.js';
|
|
8
|
+
import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntimeDist, resolveNativePath } from './index.js';
|
|
9
9
|
import { createDevSession } from './dev-session.js';
|
|
10
10
|
import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from './invocation.js';
|
|
11
11
|
import { log } from './log.js';
|
|
@@ -106,8 +106,8 @@ Usage:
|
|
|
106
106
|
vmz check [path] Check project via Workspace
|
|
107
107
|
vmz build [path] [options] Build project via Workspace
|
|
108
108
|
vmz serve [path] [options] Serve dist (optional --build)
|
|
109
|
-
vmz dev [path] [options]
|
|
110
|
-
vmz format [path] [--check] Format .vmz via N-API (oxc
|
|
109
|
+
vmz dev [path] [options] Rebuild session; --target mini-program-wechat packs dist/wechat
|
|
110
|
+
vmz format [path] [--check] Format .vmz via N-API (oxc formatter + EditorConfig)
|
|
111
111
|
vmz lint [path] [--deny-warnings] Lint (= check) via N-API
|
|
112
112
|
vmz test [path] [options] Native test discover / report
|
|
113
113
|
vmz document|docs <cmd> Project /documents domain
|
|
@@ -124,6 +124,7 @@ Options:
|
|
|
124
124
|
--out-dir, -o <dir> Output directory (default: dist)
|
|
125
125
|
--release Release build (omit serve-host; pack minify slot; proof)
|
|
126
126
|
--profile <name> Delivery profile (default from config; builtins: web-ssr|web-static|web-client|web-hybrid)
|
|
127
|
+
--target <id> Dev preview: browser (default) | mini-program-wechat (pack dist/wechat; WeChat DevTools)
|
|
127
128
|
--origin <url> Site origin for static-cdn canonical/sitemap
|
|
128
129
|
--host <host> Listen host (default: 127.0.0.1)
|
|
129
130
|
--port <port> Listen port (dev: omit = auto from 5173; set = lock)
|
|
@@ -373,10 +374,7 @@ async function cmdBuild(args) {
|
|
|
373
374
|
});
|
|
374
375
|
// Always surface locale diagnostics (warnings included) — missing /locales must not be silent.
|
|
375
376
|
// Dedupe: runtime emit + route realization both report the same missing-manifest warning.
|
|
376
|
-
const localeDiags = dedupeDiagnostics([
|
|
377
|
-
...(localeEmit.diagnostics ?? []),
|
|
378
|
-
...(localeRoutes.diagnostics ?? []),
|
|
379
|
-
]);
|
|
377
|
+
const localeDiags = dedupeDiagnostics([...(localeEmit.diagnostics ?? []), ...(localeRoutes.diagnostics ?? [])]);
|
|
380
378
|
log.diagnostics(localeDiags);
|
|
381
379
|
if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
|
|
382
380
|
log.error('locale runtime emit failed');
|
|
@@ -404,7 +402,15 @@ async function cmdBuild(args) {
|
|
|
404
402
|
profileId: selected.selection.profileId,
|
|
405
403
|
assembly: selected.selection.assembly,
|
|
406
404
|
coreDist: resolveCoreRuntimeDist(),
|
|
405
|
+
projectRoot: project,
|
|
407
406
|
});
|
|
407
|
+
const lower = pack.manifest?.clientPackageLowering;
|
|
408
|
+
if (lower?.bareSpecs?.length) {
|
|
409
|
+
log.info(`pack client packages (${lower.bareSpecs.length} bare → vendor; rewritten=${lower.rewrittenFiles})`);
|
|
410
|
+
}
|
|
411
|
+
if (lower?.remainingBareSpecs?.length) {
|
|
412
|
+
log.warn(`pack client packages: ${lower.remainingBareSpecs.length} bare specifier(s) still unresolved for browser: ${lower.remainingBareSpecs.slice(0, 8).join(', ')}${lower.remainingBareSpecs.length > 8 ? '…' : ''}`);
|
|
413
|
+
}
|
|
408
414
|
log.info(`pack ok (units=${pack.manifest.unitCount}, digest=${String(pack.manifest.packDigest).slice(0, 12)}…)`);
|
|
409
415
|
}
|
|
410
416
|
catch (err) {
|
|
@@ -488,6 +494,7 @@ async function cmdServe(args) {
|
|
|
488
494
|
VMZ_DIST: outDir,
|
|
489
495
|
VMZ_PORT: String(port),
|
|
490
496
|
VMZ_HOST: host,
|
|
497
|
+
VMZ_NATIVE_NODE: resolveNativePath(),
|
|
491
498
|
},
|
|
492
499
|
stdio: 'inherit',
|
|
493
500
|
});
|
|
@@ -534,6 +541,11 @@ async function cmdDev(args) {
|
|
|
534
541
|
}
|
|
535
542
|
}
|
|
536
543
|
const pollMs = Number(args['poll-ms'] ?? 300);
|
|
544
|
+
const targetRaw = typeof args.target === 'string' ? args.target : 'browser';
|
|
545
|
+
if (targetRaw !== 'browser' && targetRaw !== 'mini-program-wechat') {
|
|
546
|
+
log.error(`unknown --target ${targetRaw} (browser | mini-program-wechat)`);
|
|
547
|
+
return 1;
|
|
548
|
+
}
|
|
537
549
|
const ac = new AbortController();
|
|
538
550
|
const onSig = () => {
|
|
539
551
|
log.info('shutting down…');
|
|
@@ -547,6 +559,7 @@ async function cmdDev(args) {
|
|
|
547
559
|
host,
|
|
548
560
|
port,
|
|
549
561
|
pollMs,
|
|
562
|
+
target: targetRaw,
|
|
550
563
|
signal: ac.signal,
|
|
551
564
|
});
|
|
552
565
|
try {
|
|
@@ -8,6 +8,7 @@ import crypto from 'node:crypto';
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { canonicalJson, sha256Hex } from './release-pack.js';
|
|
11
|
+
import { writePrettyJsonFile } from './pretty-json.js';
|
|
11
12
|
export const CONTENT_ADDRESSED_ASSETS_SCHEMA = 'vmz.content_addressed_assets.v0';
|
|
12
13
|
/** Immutable delivery candidates (client-facing bytes). */
|
|
13
14
|
const DEFAULT_CANDIDATES = [
|
|
@@ -85,7 +86,7 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
|
|
|
85
86
|
const vmzDir = path.join(abs, '_vmz');
|
|
86
87
|
fs.mkdirSync(vmzDir, { recursive: true });
|
|
87
88
|
const outPath = path.join(vmzDir, 'content-addressed-assets.json');
|
|
88
|
-
|
|
89
|
+
writePrettyJsonFile(outPath, manifest);
|
|
89
90
|
return { manifest, assetsDir, rewrites, manifestPath: outPath };
|
|
90
91
|
}
|
|
91
92
|
/**
|
|
@@ -32,6 +32,16 @@ export declare function pickSiteAuthoring(raw: any): {
|
|
|
32
32
|
artifact: string;
|
|
33
33
|
sources: any;
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* `delivery.packaging.wechat` — vendor identity, not WeChat JSON / wx APIs.
|
|
37
|
+
* @param {unknown} raw
|
|
38
|
+
* @param {Array<{ code: string, message: string }>} diagnostics
|
|
39
|
+
*/
|
|
40
|
+
export declare function pickDeliveryPackaging(raw: any, diagnostics: any): {
|
|
41
|
+
wechat?: undefined;
|
|
42
|
+
} | {
|
|
43
|
+
wechat: {};
|
|
44
|
+
};
|
|
35
45
|
export declare function normalizeDeliveryAuthoring(raw: any): {
|
|
36
46
|
ok: boolean;
|
|
37
47
|
table: {
|
package/dist/delivery-profile.js
CHANGED
|
@@ -49,6 +49,67 @@ export function pickSiteAuthoring(raw) {
|
|
|
49
49
|
}
|
|
50
50
|
return site;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* `delivery.packaging.wechat` — vendor identity, not WeChat JSON / wx APIs.
|
|
54
|
+
* @param {unknown} raw
|
|
55
|
+
* @param {Array<{ code: string, message: string }>} diagnostics
|
|
56
|
+
*/
|
|
57
|
+
export function pickDeliveryPackaging(raw, diagnostics) {
|
|
58
|
+
if (!isPlainObject(raw) || raw.packaging == null)
|
|
59
|
+
return null;
|
|
60
|
+
if (!isPlainObject(raw.packaging)) {
|
|
61
|
+
diagnostics.push({ code: 'delivery.packaging', message: 'delivery.packaging must be an object' });
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
for (const key of Object.keys(raw.packaging)) {
|
|
65
|
+
if (key !== 'wechat') {
|
|
66
|
+
diagnostics.push({
|
|
67
|
+
code: 'delivery.packaging.vendor',
|
|
68
|
+
message: `delivery.packaging.${key} is not a known vendor (wechat)`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const wechat = raw.packaging.wechat;
|
|
73
|
+
if (wechat == null)
|
|
74
|
+
return {};
|
|
75
|
+
if (!isPlainObject(wechat)) {
|
|
76
|
+
diagnostics.push({
|
|
77
|
+
code: 'delivery.packaging.wechat',
|
|
78
|
+
message: 'delivery.packaging.wechat must be an object',
|
|
79
|
+
});
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
for (const [k, v] of Object.entries(wechat)) {
|
|
83
|
+
if (typeof v === 'function') {
|
|
84
|
+
diagnostics.push({
|
|
85
|
+
code: 'delivery.packaging.executable',
|
|
86
|
+
message: `delivery.packaging.wechat.${k} must be pure data (no functions)`,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (k !== 'appId' && k !== 'projectName' && k !== 'title') {
|
|
91
|
+
diagnostics.push({
|
|
92
|
+
code: 'delivery.packaging.wechat.field',
|
|
93
|
+
message: `delivery.packaging.wechat.${k} is not a known field (appId|projectName|title)`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else if (v != null && typeof v !== 'string') {
|
|
97
|
+
diagnostics.push({
|
|
98
|
+
code: 'delivery.packaging.wechat.type',
|
|
99
|
+
message: `delivery.packaging.wechat.${k} must be a string`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const out = {};
|
|
104
|
+
if (typeof wechat.appId === 'string' && wechat.appId.trim())
|
|
105
|
+
out.appId = wechat.appId.trim();
|
|
106
|
+
if (typeof wechat.projectName === 'string' && wechat.projectName.trim()) {
|
|
107
|
+
out.projectName = wechat.projectName.trim();
|
|
108
|
+
}
|
|
109
|
+
if (typeof wechat.title === 'string' && wechat.title.trim())
|
|
110
|
+
out.title = wechat.title.trim();
|
|
111
|
+
return { wechat: out };
|
|
112
|
+
}
|
|
52
113
|
function normalizeProfileEntry(entry, id, diagnostics) {
|
|
53
114
|
if (!isPlainObject(entry)) {
|
|
54
115
|
diagnostics.push({ code: 'delivery.profile.invalid', message: `profiles.${id} must be an object` });
|
|
@@ -183,6 +244,10 @@ export function normalizeDeliveryAuthoring(raw) {
|
|
|
183
244
|
},
|
|
184
245
|
};
|
|
185
246
|
}
|
|
247
|
+
else if (isPlainObject(raw.packaging)) {
|
|
248
|
+
defaultId = String(raw.default || 'web-ssr').trim() || 'web-ssr';
|
|
249
|
+
profileInputs = { ...BUILTIN_PROFILES };
|
|
250
|
+
}
|
|
186
251
|
else {
|
|
187
252
|
return {
|
|
188
253
|
ok: false,
|
|
@@ -206,6 +271,9 @@ export function normalizeDeliveryAuthoring(raw) {
|
|
|
206
271
|
message: `delivery.default '${defaultId}' is not a known profile`,
|
|
207
272
|
});
|
|
208
273
|
}
|
|
274
|
+
if (diagnostics.length)
|
|
275
|
+
return { ok: false, diagnostics };
|
|
276
|
+
const packaging = pickDeliveryPackaging(raw, diagnostics);
|
|
209
277
|
if (diagnostics.length)
|
|
210
278
|
return { ok: false, diagnostics };
|
|
211
279
|
const table = {
|
|
@@ -213,6 +281,7 @@ export function normalizeDeliveryAuthoring(raw) {
|
|
|
213
281
|
default: defaultId,
|
|
214
282
|
profiles,
|
|
215
283
|
sugar,
|
|
284
|
+
...(packaging ? { packaging } : {}),
|
|
216
285
|
};
|
|
217
286
|
table.digest = sha256Hex(canonicalJson(table));
|
|
218
287
|
return { ok: true, table };
|
package/dist/dev-session.d.ts
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Rebuilds go through the N-API `Workspace` — never spawn `cargo` / `vmz-tools`.
|
|
5
5
|
* session: only dirty leaves are marked; Workspace emits affected deployment units.
|
|
6
|
+
*
|
|
7
|
+
* Reload policy (Vite-like — author never hand-restarts):
|
|
8
|
+
* 1. Prefer in-process soft reload (`POST /__vmz/reload`) with transitive `?t=` via serve-host hook.
|
|
9
|
+
* 2. Soft reload only re-imports affected pages unless shared `lib/` / full rebuild.
|
|
10
|
+
* 3. Soft reload failure → **auto-respawn** serve-host (fresh ESM graph), not "keep broken host".
|
|
6
11
|
*/
|
|
7
12
|
/**
|
|
8
13
|
* @typedef {object} DevSessionOptions
|
|
@@ -11,6 +16,7 @@
|
|
|
11
16
|
* @property {string} [host]
|
|
12
17
|
* @property {number} [port]
|
|
13
18
|
* @property {number} [pollMs]
|
|
19
|
+
* @property {'browser' | 'mini-program-wechat'} [target]
|
|
14
20
|
* @property {AbortSignal} [signal]
|
|
15
21
|
* @property {typeof createWorkspace} [createWorkspaceFn]
|
|
16
22
|
* @property {(opts: { project: string, outDir: string, host: string, port: number }) => import('node:child_process').ChildProcess} [spawnHostFn]
|
package/dist/dev-session.js
CHANGED
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Rebuilds go through the N-API `Workspace` — never spawn `cargo` / `vmz-tools`.
|
|
6
6
|
* session: only dirty leaves are marked; Workspace emits affected deployment units.
|
|
7
|
+
*
|
|
8
|
+
* Reload policy (Vite-like — author never hand-restarts):
|
|
9
|
+
* 1. Prefer in-process soft reload (`POST /__vmz/reload`) with transitive `?t=` via serve-host hook.
|
|
10
|
+
* 2. Soft reload only re-imports affected pages unless shared `lib/` / full rebuild.
|
|
11
|
+
* 3. Soft reload failure → **auto-respawn** serve-host (fresh ESM graph), not "keep broken host".
|
|
7
12
|
*/
|
|
8
13
|
import { spawn } from 'node:child_process';
|
|
9
14
|
import { existsSync } from 'node:fs';
|
|
10
15
|
import path from 'node:path';
|
|
11
16
|
import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
|
|
12
|
-
import { createWorkspace } from './index.js';
|
|
17
|
+
import { createWorkspace, resolveNativePath } from './index.js';
|
|
13
18
|
import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
|
|
14
19
|
import { log } from './log.js';
|
|
15
20
|
import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
|
|
@@ -20,6 +25,7 @@ import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
|
|
|
20
25
|
* @property {string} [host]
|
|
21
26
|
* @property {number} [port]
|
|
22
27
|
* @property {number} [pollMs]
|
|
28
|
+
* @property {'browser' | 'mini-program-wechat'} [target]
|
|
23
29
|
* @property {AbortSignal} [signal]
|
|
24
30
|
* @property {typeof createWorkspace} [createWorkspaceFn]
|
|
25
31
|
* @property {(opts: { project: string, outDir: string, host: string, port: number }) => import('node:child_process').ChildProcess} [spawnHostFn]
|
|
@@ -34,6 +40,7 @@ export function createDevSession(options) {
|
|
|
34
40
|
const host = options.host ?? '127.0.0.1';
|
|
35
41
|
const port = options.port ?? 5173;
|
|
36
42
|
const pollMs = Math.max(50, options.pollMs ?? 300);
|
|
43
|
+
const wechatPreview = options.target === 'mini-program-wechat';
|
|
37
44
|
const createWs = options.createWorkspaceFn ?? createWorkspace;
|
|
38
45
|
const spawnHost = options.spawnHostFn ?? defaultSpawnHost;
|
|
39
46
|
const softReload = options.softReloadFn ?? defaultSoftReload;
|
|
@@ -51,7 +58,6 @@ export function createDevSession(options) {
|
|
|
51
58
|
}
|
|
52
59
|
function emitLocales() {
|
|
53
60
|
const localeEmit = emitLocaleRuntimeModules(project, outDir);
|
|
54
|
-
// Always surface locale diagnostics (warnings included) — missing /locales must not be silent.
|
|
55
61
|
log.diagnostics(localeEmit.diagnostics ?? []);
|
|
56
62
|
if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
|
|
57
63
|
log.error('locale runtime emit failed');
|
|
@@ -72,13 +78,90 @@ export function createDevSession(options) {
|
|
|
72
78
|
log.info(`${label} ok (${mode}; chunks=[${chunks}]; ${(report.emitted ?? []).length} emitted)`);
|
|
73
79
|
return true;
|
|
74
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Rewrite `dist/wechat` so WeChat DevTools (already open) recompiles WXSS.
|
|
83
|
+
* Preview is vendor compile, not the browser serve-host.
|
|
84
|
+
*/
|
|
85
|
+
function packWechatPreview() {
|
|
86
|
+
if (typeof ws.lowerMiniprogramWechatPackaging !== 'function') {
|
|
87
|
+
log.error('wechat pack: workspace missing lowerMiniprogramWechatPackaging');
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
let report;
|
|
91
|
+
try {
|
|
92
|
+
const raw = ws.lowerMiniprogramWechatPackaging();
|
|
93
|
+
report = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
log.error(`wechat pack failed: ${err}`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
log.diagnostics(report.diagnostics ?? []);
|
|
100
|
+
if (report.status !== 'ready') {
|
|
101
|
+
log.error(`wechat pack ${report.status || 'failed'}`);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const packRoot = report.packRoot || 'dist/wechat';
|
|
105
|
+
log.info(`wechat pack ok → ${path.join(project, packRoot)} (WeChat DevTools compiles WXSS here)`);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
async function waitHostReady(timeoutMs = 8000) {
|
|
109
|
+
const start = Date.now();
|
|
110
|
+
while (Date.now() - start < timeoutMs) {
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetch(`http://${host}:${port}/__vmz/ready`);
|
|
113
|
+
if (res.ok)
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* not up yet */
|
|
118
|
+
}
|
|
119
|
+
await sleep(50);
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`serve-host not ready on ${host}:${port}`);
|
|
122
|
+
}
|
|
123
|
+
async function waitHostGone(timeoutMs = 3000) {
|
|
124
|
+
const start = Date.now();
|
|
125
|
+
while (Date.now() - start < timeoutMs) {
|
|
126
|
+
try {
|
|
127
|
+
await fetch(`http://${host}:${port}/__vmz/health`);
|
|
128
|
+
await sleep(40);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function respawnHost(reason) {
|
|
136
|
+
log.warn(`respawning serve-host (${reason})…`);
|
|
137
|
+
killChild(child);
|
|
138
|
+
child = null;
|
|
139
|
+
await waitHostGone();
|
|
140
|
+
child = spawnHost({ project, outDir, host, port });
|
|
141
|
+
await waitHostReady();
|
|
142
|
+
log.info('serve-host respawned — browser SSE will reconnect and reload');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Soft reload first; on failure auto-respawn so authors never hand-restart.
|
|
146
|
+
* @param {object} payload
|
|
147
|
+
*/
|
|
148
|
+
async function reloadAfterBuild(payload) {
|
|
149
|
+
try {
|
|
150
|
+
await softReload(host, port, payload);
|
|
151
|
+
return 'soft';
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
log.warn(`soft reload failed (${err}) — auto-respawning serve-host`);
|
|
155
|
+
await respawnHost('soft-reload-failed');
|
|
156
|
+
return 'respawn';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
75
159
|
async function start() {
|
|
76
160
|
const src = path.join(project, 'src');
|
|
77
161
|
if (!existsSync(src)) {
|
|
78
162
|
throw new Error(`vmz dev: missing src/ under ${project}`);
|
|
79
163
|
}
|
|
80
164
|
log.info('initial build (N-API workspace, full)…');
|
|
81
|
-
// Empty dirty → full project build (session).
|
|
82
165
|
const initial = rebuild();
|
|
83
166
|
if (!printReport(initial, 'build')) {
|
|
84
167
|
throw new Error('vmz dev: initial build failed');
|
|
@@ -89,17 +172,32 @@ export function createDevSession(options) {
|
|
|
89
172
|
throw new Error('vmz dev: integrated document build failed');
|
|
90
173
|
}
|
|
91
174
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
175
|
+
if (wechatPreview) {
|
|
176
|
+
if (!packWechatPreview()) {
|
|
177
|
+
throw new Error('vmz dev: wechat pack failed');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
|
|
182
|
+
if (!existsSync(hostJs)) {
|
|
183
|
+
throw new Error(`vmz dev: missing ${hostJs}`);
|
|
184
|
+
}
|
|
185
|
+
child = spawnHost({ project, outDir, host, port });
|
|
186
|
+
await waitHostReady().catch((err) => {
|
|
187
|
+
log.warn(String(err));
|
|
188
|
+
});
|
|
95
189
|
}
|
|
96
|
-
child = spawnHost({ project, outDir, host, port });
|
|
97
190
|
const docsRoot = path.join(project, 'documents');
|
|
98
191
|
const localesRoot = path.join(project, 'locales');
|
|
99
192
|
const watchRoots = [src].concat(existsSync(docsRoot) ? [docsRoot] : []).concat(existsSync(localesRoot) ? [localesRoot] : []);
|
|
100
|
-
|
|
193
|
+
if (wechatPreview) {
|
|
194
|
+
log.info(`dev → WeChat DevTools (watching ${watchRoots.join(', ')}; keep dist/wechat open)`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
log.info(`dev → http://${host}:${port} (watching ${watchRoots.join(', ')})`);
|
|
198
|
+
}
|
|
101
199
|
/** @type {Map<string, Map<string, string>>} */
|
|
102
|
-
|
|
200
|
+
const fingerprints = new Map();
|
|
103
201
|
for (const root of watchRoots) {
|
|
104
202
|
fingerprints.set(root, fileFingerprintMap(root));
|
|
105
203
|
}
|
|
@@ -108,21 +206,35 @@ export function createDevSession(options) {
|
|
|
108
206
|
void stop();
|
|
109
207
|
};
|
|
110
208
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
209
|
+
/**
|
|
210
|
+
* Wait until src/ stops changing (multi-file agent edits).
|
|
211
|
+
*/
|
|
212
|
+
async function coalesceSrcBurst() {
|
|
213
|
+
let guard = 0;
|
|
214
|
+
while (guard++ < 20) {
|
|
215
|
+
await sleep(220);
|
|
216
|
+
const prev = fingerprints.get(src) || new Map();
|
|
217
|
+
const next = fileFingerprintMap(src);
|
|
218
|
+
const diff = diffFingerprints(prev, next);
|
|
219
|
+
if (!diff.changed.length && !diff.deleted.length)
|
|
220
|
+
break;
|
|
221
|
+
fingerprints.set(src, next);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
111
224
|
try {
|
|
112
225
|
while (!stopped && !signal?.aborted) {
|
|
113
226
|
await sleep(pollMs);
|
|
114
227
|
if (stopped || signal?.aborted)
|
|
115
228
|
break;
|
|
116
|
-
if (child && child.exitCode != null) {
|
|
229
|
+
if (!wechatPreview && child && child.exitCode != null) {
|
|
117
230
|
log.warn(`serve-host exited (${child.exitCode}) — respawning…`);
|
|
118
231
|
child = spawnHost({ project, outDir, host, port });
|
|
232
|
+
await waitHostReady().catch(() => { });
|
|
119
233
|
continue;
|
|
120
234
|
}
|
|
121
235
|
/** @type {{ srcChanged: string[], srcDeleted: string[], docsDirty: boolean, localesDirty: boolean }} */
|
|
122
236
|
let batch = { srcChanged: [], srcDeleted: [], docsDirty: false, localesDirty: false };
|
|
123
237
|
try {
|
|
124
|
-
// Probe only — keep prior fingerprints until debounce resample
|
|
125
|
-
// (same contract as pre-docs watcher: empty second pass would miss soft reload).
|
|
126
238
|
for (const root of watchRoots) {
|
|
127
239
|
const prev = fingerprints.get(root) || new Map();
|
|
128
240
|
const next = fileFingerprintMap(root);
|
|
@@ -146,8 +258,12 @@ export function createDevSession(options) {
|
|
|
146
258
|
}
|
|
147
259
|
if (!batch.srcChanged.length && !batch.srcDeleted.length && !batch.docsDirty && !batch.localesDirty)
|
|
148
260
|
continue;
|
|
149
|
-
|
|
150
|
-
|
|
261
|
+
if (batch.srcChanged.length + batch.srcDeleted.length > 1) {
|
|
262
|
+
await coalesceSrcBurst();
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
await sleep(200);
|
|
266
|
+
}
|
|
151
267
|
batch = { srcChanged: [], srcDeleted: [], docsDirty: false, localesDirty: false };
|
|
152
268
|
for (const root of watchRoots) {
|
|
153
269
|
const prev = fingerprints.get(root) || new Map();
|
|
@@ -181,7 +297,6 @@ export function createDevSession(options) {
|
|
|
181
297
|
continue;
|
|
182
298
|
}
|
|
183
299
|
if (batch.docsDirty || projectHasDocuments(project)) {
|
|
184
|
-
// App rebuild may refresh designs CSS consumed by documents.
|
|
185
300
|
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
186
301
|
if (!docs.ok) {
|
|
187
302
|
log.warn('document mount rebuild failed — keeping previous docs');
|
|
@@ -190,22 +305,26 @@ export function createDevSession(options) {
|
|
|
190
305
|
needFullReload = true;
|
|
191
306
|
}
|
|
192
307
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
308
|
+
if (wechatPreview) {
|
|
309
|
+
if (!packWechatPreview()) {
|
|
310
|
+
log.warn('wechat pack failed — keeping previous dist/wechat');
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const kind = await reloadAfterBuild({
|
|
315
|
+
affectedChunks: report.affectedChunks ?? [],
|
|
316
|
+
seedChunks: report.seedChunks ?? [],
|
|
317
|
+
emitted: report.emitted ?? [],
|
|
318
|
+
full: Boolean(report.full) || needFullReload,
|
|
319
|
+
islandHmr: Boolean(report.islandHmr) && !needFullReload,
|
|
320
|
+
});
|
|
321
|
+
log.info(kind === 'respawn'
|
|
322
|
+
? 'reload ok (respawned serve-host)'
|
|
323
|
+
: needFullReload
|
|
201
324
|
? 'soft reload ok (full page; docs)'
|
|
202
325
|
: report.islandHmr
|
|
203
326
|
? 'soft reload ok (island HMR)'
|
|
204
|
-
: 'soft reload ok
|
|
205
|
-
}
|
|
206
|
-
catch (err) {
|
|
207
|
-
log.warn(`soft reload failed (${err}) — keeping previous serve-host (fix and save)`);
|
|
208
|
-
}
|
|
327
|
+
: 'soft reload ok');
|
|
209
328
|
continue;
|
|
210
329
|
}
|
|
211
330
|
if (batch.localesDirty) {
|
|
@@ -214,15 +333,19 @@ export function createDevSession(options) {
|
|
|
214
333
|
log.warn('locale runtime emit failed — keeping previous modules');
|
|
215
334
|
continue;
|
|
216
335
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
336
|
+
if (wechatPreview) {
|
|
337
|
+
if (!packWechatPreview()) {
|
|
338
|
+
log.warn('wechat pack failed — keeping previous dist/wechat');
|
|
339
|
+
}
|
|
340
|
+
if (!batch.docsDirty)
|
|
341
|
+
continue;
|
|
220
342
|
}
|
|
221
|
-
|
|
222
|
-
|
|
343
|
+
else {
|
|
344
|
+
const kind = await reloadAfterBuild({ full: true, islandHmr: false, emitted: [] });
|
|
345
|
+
log.info(kind === 'respawn' ? 'reload ok (respawned; locales)' : 'soft reload ok (full page; locales)');
|
|
346
|
+
if (!batch.docsDirty)
|
|
347
|
+
continue;
|
|
223
348
|
}
|
|
224
|
-
if (!batch.docsDirty)
|
|
225
|
-
continue;
|
|
226
349
|
}
|
|
227
350
|
if (batch.docsDirty) {
|
|
228
351
|
log.info('documents change detected — rebuilding document mount…');
|
|
@@ -231,12 +354,14 @@ export function createDevSession(options) {
|
|
|
231
354
|
log.warn('document mount rebuild failed — keeping previous docs');
|
|
232
355
|
continue;
|
|
233
356
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
357
|
+
if (wechatPreview) {
|
|
358
|
+
if (!packWechatPreview()) {
|
|
359
|
+
log.warn('wechat pack failed — keeping previous dist/wechat');
|
|
360
|
+
}
|
|
237
361
|
}
|
|
238
|
-
|
|
239
|
-
|
|
362
|
+
else {
|
|
363
|
+
const kind = await reloadAfterBuild({ full: true, islandHmr: false, emitted: [] });
|
|
364
|
+
log.info(kind === 'respawn' ? 'reload ok (respawned; docs)' : 'soft reload ok (full page; docs)');
|
|
240
365
|
}
|
|
241
366
|
}
|
|
242
367
|
}
|
|
@@ -288,6 +413,7 @@ function defaultSpawnHost(opts) {
|
|
|
288
413
|
VMZ_PORT: String(opts.port),
|
|
289
414
|
VMZ_HOST: opts.host,
|
|
290
415
|
VMZ_DEV: '1',
|
|
416
|
+
VMZ_NATIVE_NODE: resolveNativePath(),
|
|
291
417
|
},
|
|
292
418
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
293
419
|
});
|