@vmz/vmz 0.0.4 → 0.1.1

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 (55) hide show
  1. package/dist/build-assemble.d.ts +52 -0
  2. package/dist/build-assemble.js +192 -0
  3. package/dist/cdn-policy.d.ts +22 -4
  4. package/dist/cdn-policy.js +109 -13
  5. package/dist/cli.js +122 -27
  6. package/dist/content-addressed-assets.js +2 -1
  7. package/dist/delivery-profile.d.ts +84 -0
  8. package/dist/delivery-profile.js +348 -0
  9. package/dist/dev-session.d.ts +6 -0
  10. package/dist/dev-session.js +167 -40
  11. package/dist/document-build.js +33 -32
  12. package/dist/document-cmd.js +2 -9
  13. package/dist/document-enrich.js +8 -0
  14. package/dist/document-integrate.js +10 -17
  15. package/dist/embedded-packaging.d.ts +22 -0
  16. package/dist/embedded-packaging.js +109 -0
  17. package/dist/index.d.ts +26 -1
  18. package/dist/index.js +69 -2
  19. package/dist/locale-check.js +39 -66
  20. package/dist/locale-cmd.js +12 -44
  21. package/dist/locale-route-emit.d.ts +37 -0
  22. package/dist/locale-route-emit.js +109 -0
  23. package/dist/locale-router.d.ts +20 -0
  24. package/dist/locale-router.js +68 -0
  25. package/dist/log.d.ts +2 -2
  26. package/dist/log.js +11 -3
  27. package/dist/mini-host.d.ts +47 -0
  28. package/dist/mini-host.js +202 -0
  29. package/dist/native-addon.d.ts +9 -0
  30. package/dist/native-addon.js +84 -0
  31. package/dist/pack-client-packages.d.ts +25 -0
  32. package/dist/pack-client-packages.js +399 -0
  33. package/dist/pack.d.ts +58 -0
  34. package/dist/pack.js +123 -0
  35. package/dist/plugin-host.d.ts +1 -1
  36. package/dist/plugin-host.js +2 -2
  37. package/dist/pretty-json.d.ts +19 -0
  38. package/dist/pretty-json.js +43 -0
  39. package/dist/production-observability.js +3 -2
  40. package/dist/production-test-pack.d.ts +0 -14
  41. package/dist/production-test-pack.js +27 -31
  42. package/dist/release-pack.js +17 -21
  43. package/dist/route-path.d.ts +35 -0
  44. package/dist/route-path.js +77 -0
  45. package/dist/server-artifact.d.ts +140 -0
  46. package/dist/server-artifact.js +204 -0
  47. package/dist/server-language-backend.d.ts +89 -0
  48. package/dist/server-language-backend.js +121 -0
  49. package/dist/site-delivery.js +3 -2
  50. package/dist/static-emit.d.ts +10 -1
  51. package/dist/static-emit.js +192 -97
  52. package/dist/test-cmd.js +2 -1
  53. package/dist/wechat-packaging.d.ts +22 -0
  54. package/dist/wechat-packaging.js +59 -0
  55. package/package.json +12 -12
@@ -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,8 +58,8 @@ export function createDevSession(options) {
51
58
  }
52
59
  function emitLocales() {
53
60
  const localeEmit = emitLocaleRuntimeModules(project, outDir);
61
+ log.diagnostics(localeEmit.diagnostics ?? []);
54
62
  if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
55
- log.diagnostics(localeEmit.diagnostics ?? []);
56
63
  log.error('locale runtime emit failed');
57
64
  return false;
58
65
  }
@@ -71,13 +78,90 @@ export function createDevSession(options) {
71
78
  log.info(`${label} ok (${mode}; chunks=[${chunks}]; ${(report.emitted ?? []).length} emitted)`);
72
79
  return true;
73
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
+ }
74
159
  async function start() {
75
160
  const src = path.join(project, 'src');
76
161
  if (!existsSync(src)) {
77
162
  throw new Error(`vmz dev: missing src/ under ${project}`);
78
163
  }
79
164
  log.info('initial build (N-API workspace, full)…');
80
- // Empty dirty → full project build (session).
81
165
  const initial = rebuild();
82
166
  if (!printReport(initial, 'build')) {
83
167
  throw new Error('vmz dev: initial build failed');
@@ -88,17 +172,32 @@ export function createDevSession(options) {
88
172
  throw new Error('vmz dev: integrated document build failed');
89
173
  }
90
174
  }
91
- const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
92
- if (!existsSync(hostJs)) {
93
- 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
+ });
94
189
  }
95
- child = spawnHost({ project, outDir, host, port });
96
190
  const docsRoot = path.join(project, 'documents');
97
191
  const localesRoot = path.join(project, 'locales');
98
192
  const watchRoots = [src].concat(existsSync(docsRoot) ? [docsRoot] : []).concat(existsSync(localesRoot) ? [localesRoot] : []);
99
- 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
+ }
100
199
  /** @type {Map<string, Map<string, string>>} */
101
- let fingerprints = new Map();
200
+ const fingerprints = new Map();
102
201
  for (const root of watchRoots) {
103
202
  fingerprints.set(root, fileFingerprintMap(root));
104
203
  }
@@ -107,21 +206,35 @@ export function createDevSession(options) {
107
206
  void stop();
108
207
  };
109
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
+ }
110
224
  try {
111
225
  while (!stopped && !signal?.aborted) {
112
226
  await sleep(pollMs);
113
227
  if (stopped || signal?.aborted)
114
228
  break;
115
- if (child && child.exitCode != null) {
229
+ if (!wechatPreview && child && child.exitCode != null) {
116
230
  log.warn(`serve-host exited (${child.exitCode}) — respawning…`);
117
231
  child = spawnHost({ project, outDir, host, port });
232
+ await waitHostReady().catch(() => { });
118
233
  continue;
119
234
  }
120
235
  /** @type {{ srcChanged: string[], srcDeleted: string[], docsDirty: boolean, localesDirty: boolean }} */
121
236
  let batch = { srcChanged: [], srcDeleted: [], docsDirty: false, localesDirty: false };
122
237
  try {
123
- // Probe only — keep prior fingerprints until debounce resample
124
- // (same contract as pre-docs watcher: empty second pass would miss soft reload).
125
238
  for (const root of watchRoots) {
126
239
  const prev = fingerprints.get(root) || new Map();
127
240
  const next = fileFingerprintMap(root);
@@ -145,8 +258,12 @@ export function createDevSession(options) {
145
258
  }
146
259
  if (!batch.srcChanged.length && !batch.srcDeleted.length && !batch.docsDirty && !batch.localesDirty)
147
260
  continue;
148
- await sleep(200);
149
- // 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
+ }
150
267
  batch = { srcChanged: [], srcDeleted: [], docsDirty: false, localesDirty: false };
151
268
  for (const root of watchRoots) {
152
269
  const prev = fingerprints.get(root) || new Map();
@@ -180,7 +297,6 @@ export function createDevSession(options) {
180
297
  continue;
181
298
  }
182
299
  if (batch.docsDirty || projectHasDocuments(project)) {
183
- // App rebuild may refresh designs CSS consumed by documents.
184
300
  const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
185
301
  if (!docs.ok) {
186
302
  log.warn('document mount rebuild failed — keeping previous docs');
@@ -189,22 +305,26 @@ export function createDevSession(options) {
189
305
  needFullReload = true;
190
306
  }
191
307
  }
192
- try {
193
- await softReload(host, port, {
194
- affectedChunks: report.affectedChunks ?? [],
195
- seedChunks: report.seedChunks ?? [],
196
- full: Boolean(report.full) || needFullReload,
197
- islandHmr: Boolean(report.islandHmr) && !needFullReload,
198
- });
199
- 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
200
324
  ? 'soft reload ok (full page; docs)'
201
325
  : report.islandHmr
202
326
  ? 'soft reload ok (island HMR)'
203
- : 'soft reload ok (full page)');
204
- }
205
- catch (err) {
206
- log.warn(`soft reload failed (${err}) — keeping previous serve-host (fix and save)`);
207
- }
327
+ : 'soft reload ok');
208
328
  continue;
209
329
  }
210
330
  if (batch.localesDirty) {
@@ -213,15 +333,19 @@ export function createDevSession(options) {
213
333
  log.warn('locale runtime emit failed — keeping previous modules');
214
334
  continue;
215
335
  }
216
- try {
217
- await softReload(host, port, { full: true, islandHmr: false });
218
- 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;
219
342
  }
220
- catch (err) {
221
- 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;
222
348
  }
223
- if (!batch.docsDirty)
224
- continue;
225
349
  }
226
350
  if (batch.docsDirty) {
227
351
  log.info('documents change detected — rebuilding document mount…');
@@ -230,12 +354,14 @@ export function createDevSession(options) {
230
354
  log.warn('document mount rebuild failed — keeping previous docs');
231
355
  continue;
232
356
  }
233
- try {
234
- await softReload(host, port, { full: true, islandHmr: false });
235
- 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
+ }
236
361
  }
237
- catch (err) {
238
- 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)');
239
365
  }
240
366
  }
241
367
  }
@@ -287,6 +413,7 @@ function defaultSpawnHost(opts) {
287
413
  VMZ_PORT: String(opts.port),
288
414
  VMZ_HOST: opts.host,
289
415
  VMZ_DEV: '1',
416
+ VMZ_NATIVE_NODE: resolveNativePath(),
290
417
  },
291
418
  stdio: ['ignore', 'inherit', 'inherit'],
292
419
  });
@@ -12,6 +12,8 @@ import { artifactHrefFromHtml, buildDocumentIslands, buildDocumentSearch, collec
12
12
  import { resolveMarkdownEngine } from './document-markdown.js';
13
13
  import { DOCUMENT_VIEW_SCHEMA } from './document-schema.js';
14
14
  import { createWorkspace } from './index.js';
15
+ import { requireNativeAddon } from './native-addon.js';
16
+ import { writePrettyJsonFile } from './pretty-json.js';
15
17
  /**
16
18
  * @param {{ projectRoot: string, outDir?: string, strict?: boolean, engines?: { markdown?: string } }} opts
17
19
  */
@@ -113,7 +115,7 @@ export async function buildDocuments(opts) {
113
115
  const viewRel = path.posix.join('views', page.identity.locale, `${page.identity.pageKey === 'index' ? 'index' : page.identity.pageKey}.view.json`);
114
116
  const viewAbs = path.join(outDir, viewRel);
115
117
  fs.mkdirSync(path.dirname(viewAbs), { recursive: true });
116
- fs.writeFileSync(viewAbs, JSON.stringify(view, null, 2) + '\n', 'utf8');
118
+ writePrettyJsonFile(viewAbs, view);
117
119
  const html = renderStaticHtml({
118
120
  title: info.title,
119
121
  locale: page.identity.locale,
@@ -147,10 +149,10 @@ export async function buildDocuments(opts) {
147
149
  islands: 'document.islands.json',
148
150
  },
149
151
  };
150
- fs.writeFileSync(path.join(outDir, 'document.manifest.json'), JSON.stringify(manifestOut, null, 2) + '\n', 'utf8');
151
- fs.writeFileSync(path.join(outDir, 'document.evidence.json'), JSON.stringify(evidence.evidence, null, 2) + '\n', 'utf8');
152
- fs.writeFileSync(path.join(outDir, 'document.search.json'), JSON.stringify(search, null, 2) + '\n', 'utf8');
153
- fs.writeFileSync(path.join(outDir, 'document.islands.json'), JSON.stringify(islands, null, 2) + '\n', 'utf8');
152
+ writePrettyJsonFile(path.join(outDir, 'document.manifest.json'), manifestOut);
153
+ writePrettyJsonFile(path.join(outDir, 'document.evidence.json'), evidence.evidence);
154
+ writePrettyJsonFile(path.join(outDir, 'document.search.json'), search);
155
+ writePrettyJsonFile(path.join(outDir, 'document.islands.json'), islands);
154
156
  return { ok: true, manifest: manifestOut, outDir, pages: written, search, islands };
155
157
  }
156
158
  /**
@@ -164,12 +166,12 @@ function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, desig
164
166
  /** @type {string[]} */
165
167
  const cssHrefs = [];
166
168
  if (hostChrome) {
167
- // Same application stylesheet as landing pages (header/footer chrome).
168
- cssHrefs.push(`${prefix}vmz.css`);
169
+ // Integrated documents are served with pretty directory URLs. Root
170
+ // absolute assets remain correct for both emitted files and rewrites.
171
+ cssHrefs.push('/vmz.css');
169
172
  }
170
173
  if (designsHref)
171
- cssHrefs.push(prefix + designsHref);
172
- const cssLink = cssHrefs.map((href) => ` <link rel="stylesheet" href="${esc(href)}" />`).join('\n') + (cssHrefs.length ? '\n' : '');
174
+ cssHrefs.push(hostChrome ? `/${designsHref}` : prefix + designsHref);
173
175
  const navItems = nav
174
176
  .map((n) => {
175
177
  const href = relativeHref(htmlRel, n.href, route);
@@ -187,51 +189,50 @@ function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, desig
187
189
  ${navItems}
188
190
  </ul>
189
191
  </nav>`;
192
+ /** @type {string} */
193
+ let bodyInner;
190
194
  if (hostChrome) {
191
195
  const header = hostChrome.header.replace(/(<a\s+href="\/d\/?")([^>]*>文档<\/a>)/, '$1 aria-current="page"$2');
192
- return `<!DOCTYPE html>
193
- <html lang="${esc(locale)}">
194
- <head>
195
- <meta charset="utf-8" />
196
- <meta name="viewport" content="width=device-width, initial-scale=1" />
197
- <title>${esc(title)}</title>
198
- ${cssLink}</head>
199
- <body data-vmz-hydrate="island-only">
200
- <div class="site site--docs">
196
+ bodyInner = ` <div class="site site--docs">
201
197
  <a class="skip-link" href="#main">Skip to content</a>
202
198
  ${header}
199
+ <div class="doc-body">
200
+ <aside class="doc-sidebar">
203
201
  ${docsNav}
204
202
  ${searchShellHtml}
205
- <div class="doc-body">
203
+ </aside>
204
+ <div class="doc-content">
206
205
  ${toc}<main id="main">
207
206
  ${bodyHtml}
208
207
  ${playgroundShellHtml}
209
208
  </main>
209
+ </div>
210
210
  </div>
211
211
  ${hostChrome.footer}
212
212
  </div>
213
- </body>
214
- </html>
215
213
  `;
216
214
  }
217
- return `<!DOCTYPE html>
218
- <html lang="${esc(locale)}">
219
- <head>
220
- <meta charset="utf-8" />
221
- <meta name="viewport" content="width=device-width, initial-scale=1" />
222
- <title>${esc(title)}</title>
223
- ${cssLink}</head>
224
- <body data-vmz-hydrate="island-only">
225
- <a class="skip-link" href="#main">Skip to content</a>
215
+ else {
216
+ bodyInner = ` <a class="skip-link" href="#main">Skip to content</a>
226
217
  ${docsNav}
227
218
  ${searchShellHtml}
228
219
  ${toc}<main id="main">
229
220
  ${bodyHtml}
230
221
  ${playgroundShellHtml}
231
222
  </main>
232
- </body>
233
- </html>
234
223
  `;
224
+ }
225
+ const native = requireNativeAddon();
226
+ if (typeof native.generateHtmlShell !== 'function') {
227
+ throw new Error('vmz native addon missing generateHtmlShell — rebuild with `pnpm napi:build`');
228
+ }
229
+ return native.generateHtmlShell({
230
+ title,
231
+ lang: locale,
232
+ cssHrefs,
233
+ bodyHtml: bodyInner,
234
+ bodyAttrs: ['data-vmz-hydrate', 'island-only'],
235
+ });
235
236
  }
236
237
  /**
237
238
  * Integrated DocumentMount: reuse host SiteHeader / SiteFooter .vmz templates.
@@ -2,7 +2,6 @@
2
2
  /**
3
3
  * `vmz document` / `vmz docs` CLI .
4
4
  */
5
- import fs from 'node:fs';
6
5
  import path from 'node:path';
7
6
  import { buildDocuments } from './document-build.js';
8
7
  import { checkDocuments, manifestHasErrors } from './document-check.js';
@@ -12,6 +11,7 @@ import { resolveMarkdownEngine } from './document-markdown.js';
12
11
  import { createWorkspace } from './index.js';
13
12
  import { log } from './log.js';
14
13
  import { parseArgs } from './cli.js';
14
+ import { emitPrettyJson } from './pretty-json.js';
15
15
  function printDocumentHelp() {
16
16
  console.log(`vmz document — project /documents domain
17
17
 
@@ -95,14 +95,7 @@ async function cmdDocumentCheck(args) {
95
95
  }
96
96
  const jsonOut = args.json;
97
97
  if (jsonOut) {
98
- const text = JSON.stringify(manifest, null, 2);
99
- if (typeof jsonOut === 'string') {
100
- fs.writeFileSync(jsonOut, text + '\n', 'utf8');
101
- log.info(`wrote ${jsonOut}`);
102
- }
103
- else {
104
- console.log(text);
105
- }
98
+ emitPrettyJson(jsonOut, manifest, { logWrote: (p) => log.info(`wrote ${p}`) });
106
99
  }
107
100
  else {
108
101
  for (const d of manifest.diagnostics) {
@@ -199,6 +199,14 @@ function resolveDocHref(href, fromPageKey, locale, routeBase, pageKeySet) {
199
199
  pk = normalizePageKey(pk);
200
200
  const keys = pageKeySet.get(locale) || new Set();
201
201
  if (!keys.has(pk)) {
202
+ // A directory index and a leaf page share the same normalized PageKey shape.
203
+ // Prefer the regular sibling resolution above, then retry relative to the
204
+ // PageKey itself so `guide/optimizations/index.md` keeps its directory.
205
+ const indexJoined = path.posix.normalize(path.posix.join(fromPageKey || '.', pathPart));
206
+ const indexPk = normalizePageKey(indexJoined.replace(/^\.\//, ''));
207
+ if (keys.has(indexPk)) {
208
+ return { ok: true, locale, pageKey: indexPk, anchor, anchors: [] };
209
+ }
202
210
  return { ok: false, reason: `no PageKey ${pk} in ${locale}` };
203
211
  }
204
212
  return { ok: true, locale, pageKey: pk, anchor, anchors: [] };
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  import { buildDocuments } from './document-build.js';
9
9
  import { resolveDocumentsRoot } from './document-check.js';
10
10
  import { log } from './log.js';
11
+ import { requireNativeAddon } from './native-addon.js';
11
12
  /**
12
13
  * @param {string} projectRoot
13
14
  */
@@ -66,23 +67,15 @@ function writeMountRootRedirects(manifest, outDir) {
66
67
  const relDir = base.replace(/^\//, '');
67
68
  const abs = path.join(outDir, relDir, 'index.html');
68
69
  fs.mkdirSync(path.dirname(abs), { recursive: true });
69
- const html = `<!DOCTYPE html>
70
- <html lang="${escapeAttr(defaultLocale)}">
71
- <head>
72
- <meta charset="utf-8" />
73
- <meta http-equiv="refresh" content="0;url=${escapeAttr(target)}" />
74
- <link rel="canonical" href="${escapeAttr(target)}" />
75
- <title>Documents</title>
76
- </head>
77
- <body>
78
- <p><a href="${escapeAttr(target)}">Continue to ${escapeAttr(defaultLocale)} docs</a></p>
79
- </body>
80
- </html>
81
- `;
70
+ const native = requireNativeAddon();
71
+ if (typeof native.generateRedirectHtml !== 'function') {
72
+ throw new Error('vmz native addon missing generateRedirectHtml — rebuild with `pnpm napi:build`');
73
+ }
74
+ const html = native.generateRedirectHtml({
75
+ lang: defaultLocale,
76
+ target,
77
+ title: 'Documents',
78
+ });
82
79
  fs.writeFileSync(abs, html, 'utf8');
83
80
  }
84
81
  }
85
- /** @param {string} s */
86
- function escapeAttr(s) {
87
- return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
88
- }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * rust-embedded packaging adapter: resource index + baseline closure.
3
+ * Packaging only — does not invent route / MIME / fallback semantics.
4
+ */
5
+ export declare const EMBEDDED_RESOURCE_INDEX_SCHEMA = "vmz.embedded.resource_index.v0";
6
+ /**
7
+ * Walk outDir and build path → digest → relative blob path map.
8
+ * Copies files into `dist/_vmz/embedded-baseline/` (whole release, no file-level mix).
9
+ * @param {string} outDir
10
+ * @param {{ siteId?: string, contractDigest?: string | null }} [opts]
11
+ */
12
+ export declare function emitEmbeddedPackaging(outDir: any, opts?: {}): {
13
+ index: {
14
+ schema: string;
15
+ siteId: any;
16
+ contractDigest: any;
17
+ objectCount: number;
18
+ objects: any[];
19
+ };
20
+ indexPath: string;
21
+ baselineDir: string;
22
+ };