@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.
Files changed (40) hide show
  1. package/dist/build-assemble.js +4 -3
  2. package/dist/cdn-policy.js +5 -8
  3. package/dist/cli.js +20 -7
  4. package/dist/content-addressed-assets.js +2 -1
  5. package/dist/delivery-profile.d.ts +10 -0
  6. package/dist/delivery-profile.js +69 -0
  7. package/dist/dev-session.d.ts +6 -0
  8. package/dist/dev-session.js +166 -40
  9. package/dist/document-build.js +24 -28
  10. package/dist/document-cmd.js +2 -9
  11. package/dist/document-integrate.js +10 -17
  12. package/dist/embedded-packaging.js +3 -7
  13. package/dist/index.d.ts +19 -1
  14. package/dist/index.js +62 -2
  15. package/dist/locale-check.js +28 -58
  16. package/dist/locale-cmd.js +10 -42
  17. package/dist/locale-route-emit.d.ts +4 -1
  18. package/dist/locale-route-emit.js +7 -32
  19. package/dist/mini-host.d.ts +47 -0
  20. package/dist/mini-host.js +202 -0
  21. package/dist/native-addon.d.ts +9 -0
  22. package/dist/native-addon.js +84 -0
  23. package/dist/pack-client-packages.d.ts +25 -0
  24. package/dist/pack-client-packages.js +399 -0
  25. package/dist/pack.d.ts +21 -3
  26. package/dist/pack.js +21 -6
  27. package/dist/pretty-json.d.ts +19 -0
  28. package/dist/pretty-json.js +43 -0
  29. package/dist/production-observability.js +3 -2
  30. package/dist/production-test-pack.js +4 -3
  31. package/dist/release-pack.js +5 -18
  32. package/dist/route-path.d.ts +35 -0
  33. package/dist/route-path.js +77 -0
  34. package/dist/server-artifact.js +5 -6
  35. package/dist/site-delivery.js +3 -2
  36. package/dist/static-emit.js +67 -86
  37. package/dist/test-cmd.js +2 -1
  38. package/dist/wechat-packaging.d.ts +22 -0
  39. package/dist/wechat-packaging.js +59 -0
  40. package/package.json +12 -12
@@ -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, writeFileSync } from 'node:fs';
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
- writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
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
- writeFileSync(file, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
190
+ writePrettyJsonFile(file, body);
190
191
  return { proof: body, path: file };
191
192
  }
@@ -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
- fs.writeFileSync(path.join(vmzDir, 'cdn-policy-manifest.json'), `${JSON.stringify(policy, null, 2)}\n`, 'utf8');
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
- fs.writeFileSync(path.join(adaptersDir, 'local-static', 'projection.json'), `${JSON.stringify(local, null, 2)}\n`, 'utf8');
159
- fs.writeFileSync(path.join(adaptersDir, 'netlify', 'projection.json'), `${JSON.stringify(netlify, null, 2)}\n`, 'utf8');
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] Long-lived rebuild session (no CLI spawn)
110
- vmz format [path] [--check] Format .vmz via N-API (oxc codegen)
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
- fs.writeFileSync(outPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
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: {
@@ -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 };
@@ -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]
@@ -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
- const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
93
- if (!existsSync(hostJs)) {
94
- throw new Error(`vmz dev: missing ${hostJs}`);
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
- log.info(`dev → http://${host}:${port} (watching ${watchRoots.join(', ')})`);
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
- let fingerprints = new Map();
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
- await sleep(200);
150
- // Resample against the same prior fingerprints, then commit.
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
- try {
194
- await softReload(host, port, {
195
- affectedChunks: report.affectedChunks ?? [],
196
- seedChunks: report.seedChunks ?? [],
197
- full: Boolean(report.full) || needFullReload,
198
- islandHmr: Boolean(report.islandHmr) && !needFullReload,
199
- });
200
- log.info(needFullReload
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 (full page)');
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
- try {
218
- await softReload(host, port, { full: true, islandHmr: false });
219
- log.info('soft reload ok (full page; locales)');
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
- catch (err) {
222
- log.warn(`soft reload failed (${err}) keeping previous serve-host (fix and save)`);
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
- try {
235
- await softReload(host, port, { full: true, islandHmr: false });
236
- log.info('soft reload ok (full page; docs)');
357
+ if (wechatPreview) {
358
+ if (!packWechatPreview()) {
359
+ log.warn('wechat pack failed keeping previous dist/wechat');
360
+ }
237
361
  }
238
- catch (err) {
239
- log.warn(`soft reload failed (${err}) keeping previous serve-host (fix and save)`);
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
  });