@vmz/vmz 0.0.1 → 0.0.3

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 (79) hide show
  1. package/README.md +52 -2
  2. package/bin/vmz.js +4 -0
  3. package/dist/application-cmd.d.ts +21 -0
  4. package/dist/application-cmd.js +347 -0
  5. package/dist/bundler-adapter.d.ts +63 -0
  6. package/dist/bundler-adapter.js +110 -0
  7. package/dist/cli.d.ts +23 -0
  8. package/dist/cli.js +474 -0
  9. package/dist/dev-session.d.ts +35 -0
  10. package/dist/dev-session.js +290 -0
  11. package/dist/document-build.d.ts +99 -0
  12. package/dist/document-build.js +273 -0
  13. package/dist/document-check.d.ts +44 -0
  14. package/dist/document-check.js +246 -0
  15. package/dist/document-cmd.d.ts +8 -0
  16. package/dist/document-cmd.js +146 -0
  17. package/dist/document-designs.d.ts +9 -0
  18. package/dist/document-designs.js +126 -0
  19. package/dist/document-enrich.d.ts +23 -0
  20. package/dist/document-enrich.js +233 -0
  21. package/dist/document-evidence.d.ts +49 -0
  22. package/dist/document-evidence.js +509 -0
  23. package/dist/document-integrate.d.ts +34 -0
  24. package/dist/document-integrate.js +88 -0
  25. package/dist/document-interactive.d.ts +69 -0
  26. package/dist/document-interactive.js +254 -0
  27. package/dist/document-locale.d.ts +31 -0
  28. package/dist/document-locale.js +59 -0
  29. package/dist/document-markdown.d.ts +12 -0
  30. package/dist/document-markdown.js +45 -0
  31. package/dist/document-scan.d.ts +21 -0
  32. package/dist/document-scan.js +151 -0
  33. package/dist/document-schema.d.ts +86 -0
  34. package/dist/document-schema.js +87 -0
  35. package/dist/explain-cmd.d.ts +5 -0
  36. package/dist/explain-cmd.js +123 -0
  37. package/dist/index.d.ts +359 -0
  38. package/dist/index.js +580 -0
  39. package/dist/invocation.d.ts +91 -0
  40. package/dist/invocation.js +190 -0
  41. package/dist/locale-check.d.ts +106 -0
  42. package/dist/locale-check.js +736 -0
  43. package/dist/locale-cmd.d.ts +5 -0
  44. package/dist/locale-cmd.js +442 -0
  45. package/dist/locale-delivery.d.ts +298 -0
  46. package/dist/locale-delivery.js +443 -0
  47. package/dist/locale-router.d.ts +206 -0
  48. package/dist/locale-router.js +507 -0
  49. package/dist/locale-runtime.d.ts +406 -0
  50. package/dist/locale-runtime.js +541 -0
  51. package/dist/locale-schema.d.ts +8 -0
  52. package/dist/locale-schema.js +9 -0
  53. package/dist/locale-tooling.d.ts +118 -0
  54. package/dist/locale-tooling.js +357 -0
  55. package/dist/log.d.ts +19 -0
  56. package/dist/log.js +42 -0
  57. package/dist/packages.d.ts +26 -0
  58. package/dist/packages.js +146 -0
  59. package/dist/plugin-host.d.ts +29 -0
  60. package/dist/plugin-host.js +369 -0
  61. package/dist/refactor-cmd.d.ts +8 -0
  62. package/dist/refactor-cmd.js +156 -0
  63. package/dist/resolve-native-cli.d.ts +14 -0
  64. package/dist/resolve-native-cli.js +84 -0
  65. package/dist/resolve.d.ts +24 -0
  66. package/dist/resolve.js +55 -0
  67. package/dist/test-cmd.d.ts +9 -0
  68. package/dist/test-cmd.js +363 -0
  69. package/dist/test-compile.d.ts +2 -0
  70. package/dist/test-compile.js +3 -0
  71. package/dist/test-discover.d.ts +2 -0
  72. package/dist/test-discover.js +3 -0
  73. package/dist/test-logic.d.ts +2 -0
  74. package/dist/test-logic.js +3 -0
  75. package/dist/test-protocol.d.ts +2 -0
  76. package/dist/test-protocol.js +3 -0
  77. package/dist/watch-diff.d.ts +17 -0
  78. package/dist/watch-diff.js +56 -0
  79. package/package.json +96 -3
@@ -0,0 +1,290 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Long-lived Node dev session (/session).
4
+ *
5
+ * Rebuilds go through the N-API `Workspace` — never spawn `cargo` / `vmz-tools`.
6
+ * session: only dirty leaves are marked; Workspace emits affected deployment units.
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ import { existsSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
12
+ import { createWorkspace } from './index.js';
13
+ import { log } from './log.js';
14
+ import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
15
+ /**
16
+ * @typedef {object} DevSessionOptions
17
+ * @property {string} project
18
+ * @property {string} outDir
19
+ * @property {string} [host]
20
+ * @property {number} [port]
21
+ * @property {number} [pollMs]
22
+ * @property {AbortSignal} [signal]
23
+ * @property {typeof createWorkspace} [createWorkspaceFn]
24
+ * @property {(opts: { project: string, outDir: string, host: string, port: number }) => import('node:child_process').ChildProcess} [spawnHostFn]
25
+ * @property {(host: string, port: number, payload?: object) => Promise<void>} [softReloadFn]
26
+ */
27
+ /**
28
+ * @param {DevSessionOptions} options
29
+ */
30
+ export function createDevSession(options) {
31
+ const project = options.project;
32
+ const outDir = options.outDir;
33
+ const host = options.host ?? '127.0.0.1';
34
+ const port = options.port ?? 5173;
35
+ const pollMs = Math.max(50, options.pollMs ?? 300);
36
+ const createWs = options.createWorkspaceFn ?? createWorkspace;
37
+ const spawnHost = options.spawnHostFn ?? defaultSpawnHost;
38
+ const softReload = options.softReloadFn ?? defaultSoftReload;
39
+ const ws = createWs({ root: project, outDir });
40
+ /** @type {import('node:child_process').ChildProcess | null} */
41
+ let child = null;
42
+ let stopped = false;
43
+ /**
44
+ * @param {Array<{ path: string, kind: 'update' | 'delete' }>} [changes]
45
+ */
46
+ function rebuild(changes) {
47
+ if (changes?.length)
48
+ ws.updateFiles(changes);
49
+ return ws.build();
50
+ }
51
+ function printReport(report, label) {
52
+ const errors = log.diagnostics(report.diagnostics ?? []);
53
+ if (errors) {
54
+ log.error(`${label} failed (${errors} error(s))`);
55
+ return false;
56
+ }
57
+ const mode = report.full ? 'full' : 'affected';
58
+ const chunks = (report.affectedChunks || []).join(', ') || '(none)';
59
+ log.info(`${label} ok (${mode}; chunks=[${chunks}]; ${(report.emitted ?? []).length} emitted)`);
60
+ return true;
61
+ }
62
+ async function start() {
63
+ const src = path.join(project, 'src');
64
+ if (!existsSync(src)) {
65
+ throw new Error(`vmz dev: missing src/ under ${project}`);
66
+ }
67
+ log.info('initial build (N-API workspace, full)…');
68
+ // Empty dirty → full project build (session).
69
+ const initial = rebuild();
70
+ if (!printReport(initial, 'build')) {
71
+ throw new Error('vmz dev: initial build failed');
72
+ }
73
+ if (projectHasDocuments(project)) {
74
+ const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
75
+ if (!docs.ok) {
76
+ throw new Error('vmz dev: integrated document build failed');
77
+ }
78
+ }
79
+ const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
80
+ if (!existsSync(hostJs)) {
81
+ throw new Error(`vmz dev: missing ${hostJs}`);
82
+ }
83
+ child = spawnHost({ project, outDir, host, port });
84
+ const docsRoot = path.join(project, 'documents');
85
+ const watchRoots = [src].concat(existsSync(docsRoot) ? [docsRoot] : []);
86
+ log.info(`dev → http://${host}:${port} (watching ${watchRoots.join(', ')})`);
87
+ /** @type {Map<string, Map<string, string>>} */
88
+ let fingerprints = new Map();
89
+ for (const root of watchRoots) {
90
+ fingerprints.set(root, fileFingerprintMap(root));
91
+ }
92
+ const signal = options.signal;
93
+ const onAbort = () => {
94
+ void stop();
95
+ };
96
+ signal?.addEventListener('abort', onAbort, { once: true });
97
+ try {
98
+ while (!stopped && !signal?.aborted) {
99
+ await sleep(pollMs);
100
+ if (stopped || signal?.aborted)
101
+ break;
102
+ if (child && child.exitCode != null) {
103
+ throw new Error(`vmz serve-host exited: ${child.exitCode}`);
104
+ }
105
+ /** @type {{ srcChanged: string[], srcDeleted: string[], docsDirty: boolean }} */
106
+ let batch = { srcChanged: [], srcDeleted: [], docsDirty: false };
107
+ try {
108
+ // Probe only — keep prior fingerprints until debounce resample
109
+ // (same contract as pre-docs watcher: empty second pass would miss soft reload).
110
+ for (const root of watchRoots) {
111
+ const prev = fingerprints.get(root) || new Map();
112
+ const next = fileFingerprintMap(root);
113
+ const diff = diffFingerprints(prev, next);
114
+ if (root === src) {
115
+ batch.srcChanged = diff.changed;
116
+ batch.srcDeleted = diff.deleted;
117
+ }
118
+ else if (diff.changed.length || diff.deleted.length) {
119
+ batch.docsDirty = true;
120
+ }
121
+ }
122
+ }
123
+ catch (err) {
124
+ log.warn('watch error:', err);
125
+ continue;
126
+ }
127
+ if (!batch.srcChanged.length && !batch.srcDeleted.length && !batch.docsDirty)
128
+ continue;
129
+ await sleep(200);
130
+ // Resample against the same prior fingerprints, then commit.
131
+ batch = { srcChanged: [], srcDeleted: [], docsDirty: false };
132
+ for (const root of watchRoots) {
133
+ const prev = fingerprints.get(root) || new Map();
134
+ const next = fileFingerprintMap(root);
135
+ const diff = diffFingerprints(prev, next);
136
+ fingerprints.set(root, next);
137
+ if (root === src) {
138
+ batch.srcChanged = diff.changed;
139
+ batch.srcDeleted = diff.deleted;
140
+ }
141
+ else if (diff.changed.length || diff.deleted.length) {
142
+ batch.docsDirty = true;
143
+ }
144
+ }
145
+ if (!batch.srcChanged.length && !batch.srcDeleted.length && !batch.docsDirty)
146
+ continue;
147
+ let needFullReload = batch.docsDirty;
148
+ if (batch.srcChanged.length || batch.srcDeleted.length) {
149
+ log.info(`change detected (${batch.srcChanged.length} update, ${batch.srcDeleted.length} delete) — affected rebuild…`);
150
+ const changes = [
151
+ ...batch.srcChanged.map((p) => ({ path: p, kind: /** @type {'update'} */ ('update') })),
152
+ ...batch.srcDeleted.map((p) => ({ path: p, kind: /** @type {'delete'} */ ('delete') })),
153
+ ];
154
+ const report = rebuild(changes);
155
+ if (!printReport(report, 'rebuild')) {
156
+ log.warn('rebuild failed — keeping previous server');
157
+ continue;
158
+ }
159
+ if (batch.docsDirty || projectHasDocuments(project)) {
160
+ // App rebuild may refresh designs CSS consumed by documents.
161
+ const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
162
+ if (!docs.ok) {
163
+ log.warn('document mount rebuild failed — keeping previous docs');
164
+ }
165
+ else {
166
+ needFullReload = true;
167
+ }
168
+ }
169
+ try {
170
+ await softReload(host, port, {
171
+ affectedChunks: report.affectedChunks ?? [],
172
+ seedChunks: report.seedChunks ?? [],
173
+ full: Boolean(report.full) || needFullReload,
174
+ islandHmr: Boolean(report.islandHmr) && !needFullReload,
175
+ });
176
+ log.info(needFullReload
177
+ ? 'soft reload ok (full page; docs)'
178
+ : report.islandHmr
179
+ ? 'soft reload ok (island HMR)'
180
+ : 'soft reload ok (full page)');
181
+ }
182
+ catch (err) {
183
+ log.warn(`soft reload failed (${err}) — restarting serve-host…`);
184
+ killChild(child);
185
+ child = spawnHost({ project, outDir, host, port });
186
+ }
187
+ continue;
188
+ }
189
+ if (batch.docsDirty) {
190
+ log.info('documents change detected — rebuilding document mount…');
191
+ const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
192
+ if (!docs.ok) {
193
+ log.warn('document mount rebuild failed — keeping previous docs');
194
+ continue;
195
+ }
196
+ try {
197
+ await softReload(host, port, { full: true, islandHmr: false });
198
+ log.info('soft reload ok (full page; docs)');
199
+ }
200
+ catch (err) {
201
+ log.warn(`soft reload failed (${err}) — restarting serve-host…`);
202
+ killChild(child);
203
+ child = spawnHost({ project, outDir, host, port });
204
+ }
205
+ }
206
+ }
207
+ }
208
+ finally {
209
+ signal?.removeEventListener('abort', onAbort);
210
+ await stop();
211
+ }
212
+ }
213
+ async function stop() {
214
+ if (stopped)
215
+ return;
216
+ stopped = true;
217
+ killChild(child);
218
+ child = null;
219
+ try {
220
+ ws.dispose();
221
+ }
222
+ catch {
223
+ /* ignore */
224
+ }
225
+ }
226
+ return { ws, rebuild, start, stop, project, outDir, host, port };
227
+ }
228
+ /** @deprecated use fileFingerprintMap */
229
+ export function srcFingerprint(srcDir) {
230
+ const map = fileFingerprintMap(srcDir);
231
+ let h = 0xcbf29ce484222325n;
232
+ const keys = [...map.keys()].sort();
233
+ for (const k of keys) {
234
+ for (const b of Buffer.from(`${k}|${map.get(k)}`)) {
235
+ h = (h * 0x100000001b3n + BigInt(b)) & 0xffffffffffffffffn;
236
+ }
237
+ }
238
+ return Number(h & 0xffffffffffffffffn);
239
+ }
240
+ /** @deprecated */
241
+ export function listWatchedFiles(srcDir) {
242
+ return [...fileFingerprintMap(srcDir).keys()];
243
+ }
244
+ function defaultSpawnHost(opts) {
245
+ const hostJs = path.join(opts.outDir, 'vmz-serve-host.mjs');
246
+ const node = process.env.VMZ_NODE || process.execPath;
247
+ return spawn(node, [hostJs], {
248
+ cwd: opts.project,
249
+ env: {
250
+ ...process.env,
251
+ VMZ_DIST: opts.outDir,
252
+ VMZ_PORT: String(opts.port),
253
+ VMZ_HOST: opts.host,
254
+ VMZ_DEV: '1',
255
+ },
256
+ stdio: ['ignore', 'inherit', 'inherit'],
257
+ });
258
+ }
259
+ /**
260
+ * @param {string} host
261
+ * @param {number} port
262
+ * @param {object} [payload]
263
+ */
264
+ async function defaultSoftReload(host, port, payload = {}) {
265
+ const url = `http://${host}:${port}/__vmz/reload`;
266
+ const body = JSON.stringify(payload);
267
+ const res = await fetch(url, {
268
+ method: 'POST',
269
+ headers: { 'content-type': 'application/json' },
270
+ body,
271
+ });
272
+ if (!res.ok)
273
+ throw new Error(`HTTP ${res.status}`);
274
+ const json = await res.json().catch(() => ({}));
275
+ if (!json?.ok)
276
+ throw new Error(JSON.stringify(json));
277
+ }
278
+ function killChild(child) {
279
+ if (!child || child.killed)
280
+ return;
281
+ try {
282
+ child.kill();
283
+ }
284
+ catch {
285
+ /* ignore */
286
+ }
287
+ }
288
+ function sleep(ms) {
289
+ return new Promise((r) => setTimeout(r, ms));
290
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @param {{ projectRoot: string, outDir?: string, strict?: boolean, engines?: { markdown?: string } }} opts
3
+ */
4
+ export declare function buildDocuments(opts: any): Promise<{
5
+ ok: boolean;
6
+ manifest: {
7
+ schema: string;
8
+ root: string;
9
+ defaultLocale: any;
10
+ locales: any[];
11
+ localeLabels: {};
12
+ collections: any[];
13
+ mounts: any[];
14
+ pages: {
15
+ identity: any;
16
+ sourcePath: any;
17
+ route: any;
18
+ anchors: any[];
19
+ }[];
20
+ diagnostics: any[];
21
+ };
22
+ outDir: string;
23
+ pages: any[];
24
+ search?: undefined;
25
+ islands?: undefined;
26
+ } | {
27
+ ok: boolean;
28
+ manifest: {
29
+ schema: string;
30
+ evidence: {
31
+ schema: string;
32
+ fences: any[];
33
+ apiRefs: any[];
34
+ testSelections: any[];
35
+ status: string;
36
+ };
37
+ search: {
38
+ schema: string;
39
+ status: string;
40
+ version: any;
41
+ records: any[];
42
+ };
43
+ islands: {
44
+ schema: string;
45
+ hydrate: string;
46
+ fullPageHydrate: boolean;
47
+ islands: {
48
+ name: string;
49
+ kind: string;
50
+ resume: string;
51
+ index: any;
52
+ }[];
53
+ status: string;
54
+ };
55
+ build: {
56
+ engine: any;
57
+ outDir: string;
58
+ designs: string;
59
+ designsCss: any;
60
+ pages: any[];
61
+ evidence: string;
62
+ search: string;
63
+ islands: string;
64
+ };
65
+ root: string;
66
+ defaultLocale: any;
67
+ locales: any[];
68
+ localeLabels: {};
69
+ collections: any[];
70
+ mounts: any[];
71
+ pages: {
72
+ identity: any;
73
+ sourcePath: any;
74
+ route: any;
75
+ anchors: any[];
76
+ }[];
77
+ diagnostics: any[];
78
+ };
79
+ outDir: string;
80
+ pages: any[];
81
+ search: {
82
+ schema: string;
83
+ status: string;
84
+ version: any;
85
+ records: any[];
86
+ };
87
+ islands: {
88
+ schema: string;
89
+ hydrate: string;
90
+ fullPageHydrate: boolean;
91
+ islands: {
92
+ name: string;
93
+ kind: string;
94
+ resume: string;
95
+ index: any;
96
+ }[];
97
+ status: string;
98
+ };
99
+ }>;
@@ -0,0 +1,273 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Document Static + Interactive artifacts.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { checkDocuments, manifestHasErrors } from './document-check.js';
8
+ import { resolveDocumentDesignsCss } from './document-designs.js';
9
+ import { enrichDocumentContent, pageHtmlRel } from './document-enrich.js';
10
+ import { enrichDocumentEvidence } from './document-evidence.js';
11
+ import { artifactHrefFromHtml, buildDocumentIslands, buildDocumentSearch, collectFenceBodies, renderIslandShellsHtml, } from './document-interactive.js';
12
+ import { resolveMarkdownEngine } from './document-markdown.js';
13
+ import { DOCUMENT_VIEW_SCHEMA } from './document-schema.js';
14
+ import { createWorkspace } from './index.js';
15
+ /**
16
+ * @param {{ projectRoot: string, outDir?: string, strict?: boolean, engines?: { markdown?: string } }} opts
17
+ */
18
+ export async function buildDocuments(opts) {
19
+ const projectRoot = path.resolve(opts.projectRoot);
20
+ const outDir = path.resolve(opts.outDir || path.join(projectRoot, 'dist', 'documents'));
21
+ const strict = Boolean(opts.strict);
22
+ const manifest = checkDocuments({ projectRoot, strict });
23
+ const engine = await resolveMarkdownEngine({ engines: opts.engines, projectRoot });
24
+ const enriched = enrichDocumentContent(manifest, {
25
+ analyzeMarkdown: engine.analyzeMarkdown,
26
+ projectRoot,
27
+ });
28
+ manifest.diagnostics = enriched.diagnostics;
29
+ const evidence = await enrichDocumentEvidence(manifest, {
30
+ analyzeMarkdown: engine.analyzeMarkdown,
31
+ projectRoot,
32
+ createWorkspace,
33
+ });
34
+ manifest.diagnostics = evidence.diagnostics;
35
+ manifest.evidence = evidence.evidence;
36
+ if (manifestHasErrors(manifest)) {
37
+ return { ok: false, manifest, outDir, pages: [] };
38
+ }
39
+ /** @type {Map<string, any>} */
40
+ const analyzedByPageId = new Map();
41
+ for (const page of manifest.pages) {
42
+ const abs = path.isAbsolute(page.sourcePath) ? page.sourcePath : path.join(manifest.root, page.sourcePath);
43
+ const source = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : '';
44
+ const id = `${page.identity.locale}:${page.identity.pageKey}`;
45
+ analyzedByPageId.set(id, engine.analyzeMarkdown(source));
46
+ }
47
+ const fenceBodies = collectFenceBodies(analyzedByPageId, manifest.pages);
48
+ const search = buildDocumentSearch({
49
+ manifest,
50
+ enriched,
51
+ evidence: evidence.evidence,
52
+ version: null,
53
+ });
54
+ const islands = buildDocumentIslands({
55
+ evidence: evidence.evidence,
56
+ searchHref: 'document.search.json',
57
+ fenceBodies,
58
+ });
59
+ manifest.search = search;
60
+ manifest.islands = islands;
61
+ const hostChrome = resolveHostSiteChrome(projectRoot);
62
+ const useHostShell = Boolean(hostChrome) && (manifest.mounts || []).some((m) => m.mode === 'integrated');
63
+ fs.mkdirSync(outDir, { recursive: true });
64
+ const designs = resolveDocumentDesignsCss(projectRoot);
65
+ /** @type {string | null} */
66
+ let designsHref = null;
67
+ if (designs.css && designs.href) {
68
+ const cssPath = path.join(outDir, designs.href);
69
+ fs.mkdirSync(path.dirname(cssPath), { recursive: true });
70
+ fs.writeFileSync(cssPath, designs.css, 'utf8');
71
+ designsHref = designs.href;
72
+ }
73
+ const viewsDir = path.join(outDir, 'views');
74
+ fs.mkdirSync(viewsDir, { recursive: true });
75
+ /** @type {Array<{ route: string, htmlPath: string, viewPath: string }>} */
76
+ const written = [];
77
+ for (const page of manifest.pages) {
78
+ const id = `${page.identity.locale}:${page.identity.pageKey}`;
79
+ const info = enriched.byId.get(id);
80
+ if (!info)
81
+ continue;
82
+ const nav = enriched.navByLocale[page.identity.locale] || [];
83
+ const htmlRel = pageHtmlRel(enriched.routeBase, page.identity.locale, page.identity.pageKey);
84
+ const htmlAbs = path.join(outDir, htmlRel);
85
+ fs.mkdirSync(path.dirname(htmlAbs), { recursive: true });
86
+ const searchIndexHref = artifactHrefFromHtml(htmlRel, 'document.search.json');
87
+ const shells = renderIslandShellsHtml({
88
+ islands,
89
+ searchIndexHref,
90
+ pageKey: page.identity.pageKey,
91
+ locale: page.identity.locale,
92
+ });
93
+ const view = {
94
+ schema: DOCUMENT_VIEW_SCHEMA,
95
+ pageKey: page.identity.pageKey,
96
+ locale: page.identity.locale,
97
+ route: info.route,
98
+ title: info.title,
99
+ headings: info.headings,
100
+ nav,
101
+ bodyKind: 'html',
102
+ html: info.html,
103
+ designsCss: designsHref,
104
+ noJsReadable: true,
105
+ hydrate: 'island-only',
106
+ hostShell: useHostShell,
107
+ islands: ['DocumentSearch'].concat((islands.islands || [])
108
+ .filter((isl) => isl.kind === 'playground' &&
109
+ isl.fence?.locale === page.identity.locale &&
110
+ isl.fence?.pageKey === page.identity.pageKey)
111
+ .map((isl) => isl.name)),
112
+ };
113
+ const viewRel = path.posix.join('views', page.identity.locale, `${page.identity.pageKey === 'index' ? 'index' : page.identity.pageKey}.view.json`);
114
+ const viewAbs = path.join(outDir, viewRel);
115
+ fs.mkdirSync(path.dirname(viewAbs), { recursive: true });
116
+ fs.writeFileSync(viewAbs, JSON.stringify(view, null, 2) + '\n', 'utf8');
117
+ const html = renderStaticHtml({
118
+ title: info.title,
119
+ locale: page.identity.locale,
120
+ route: info.route,
121
+ nav,
122
+ bodyHtml: info.html,
123
+ headings: info.headings,
124
+ designsHref,
125
+ htmlRel,
126
+ searchShellHtml: shells.searchHtml,
127
+ playgroundShellHtml: shells.playgroundHtml,
128
+ hostChrome: useHostShell ? hostChrome : null,
129
+ });
130
+ fs.writeFileSync(htmlAbs, html, 'utf8');
131
+ written.push({ route: info.route, htmlPath: htmlRel, viewPath: viewRel });
132
+ }
133
+ const manifestOut = {
134
+ ...manifest,
135
+ schema: manifest.schema,
136
+ evidence: evidence.evidence,
137
+ search,
138
+ islands,
139
+ build: {
140
+ engine: engine.engine,
141
+ outDir: path.relative(projectRoot, outDir).replace(/\\/g, '/') || '.',
142
+ designs: designs.source,
143
+ designsCss: designsHref,
144
+ pages: written,
145
+ evidence: 'document.evidence.json',
146
+ search: 'document.search.json',
147
+ islands: 'document.islands.json',
148
+ },
149
+ };
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');
154
+ return { ok: true, manifest: manifestOut, outDir, pages: written, search, islands };
155
+ }
156
+ /**
157
+ * No-JS readable static HTML: nav + main landmarks, Island shells without scripts.
158
+ * Integrated mounts reuse host SiteHeader/SiteFooter templates when present.
159
+ */
160
+ function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, designsHref, htmlRel, searchShellHtml = '', playgroundShellHtml = '', hostChrome = null, }) {
161
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
162
+ const depth = htmlRel.split('/').length - 1;
163
+ const prefix = depth > 0 ? '../'.repeat(depth) : './';
164
+ /** @type {string[]} */
165
+ const cssHrefs = [];
166
+ if (hostChrome) {
167
+ // Same application stylesheet as landing pages (header/footer chrome).
168
+ cssHrefs.push(`${prefix}vmz.css`);
169
+ }
170
+ if (designsHref)
171
+ cssHrefs.push(prefix + designsHref);
172
+ const cssLink = cssHrefs.map((href) => ` <link rel="stylesheet" href="${esc(href)}" />`).join('\n') + (cssHrefs.length ? '\n' : '');
173
+ const navItems = nav
174
+ .map((n) => {
175
+ const href = relativeHref(htmlRel, n.href, route);
176
+ const current = n.href === route ? ' aria-current="page"' : '';
177
+ return ` <li><a href="${esc(href)}"${current}>${esc(n.title)}</a></li>`;
178
+ })
179
+ .join('\n');
180
+ const toc = headings.length > 1
181
+ ? `<nav aria-label="On this page" class="toc">\n <ol>\n${headings
182
+ .map((h) => ` <li class="h${h.level}"><a href="#${esc(h.id)}">${esc(h.text)}</a></li>`)
183
+ .join('\n')}\n </ol>\n </nav>\n`
184
+ : '';
185
+ const docsNav = ` <nav aria-label="Documents" class="doc-subnav">
186
+ <ul>
187
+ ${navItems}
188
+ </ul>
189
+ </nav>`;
190
+ if (hostChrome) {
191
+ 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">
201
+ <a class="skip-link" href="#main">Skip to content</a>
202
+ ${header}
203
+ ${docsNav}
204
+ ${searchShellHtml}
205
+ <div class="doc-body">
206
+ ${toc}<main id="main">
207
+ ${bodyHtml}
208
+ ${playgroundShellHtml}
209
+ </main>
210
+ </div>
211
+ ${hostChrome.footer}
212
+ </div>
213
+ </body>
214
+ </html>
215
+ `;
216
+ }
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>
226
+ ${docsNav}
227
+ ${searchShellHtml}
228
+ ${toc}<main id="main">
229
+ ${bodyHtml}
230
+ ${playgroundShellHtml}
231
+ </main>
232
+ </body>
233
+ </html>
234
+ `;
235
+ }
236
+ /**
237
+ * Integrated DocumentMount: reuse host SiteHeader / SiteFooter .vmz templates.
238
+ * @param {string} projectRoot
239
+ * @returns {{ header: string, footer: string } | null}
240
+ */
241
+ function resolveHostSiteChrome(projectRoot) {
242
+ const headerPath = path.join(projectRoot, 'src', 'components', 'SiteHeader.vmz');
243
+ const footerPath = path.join(projectRoot, 'src', 'components', 'SiteFooter.vmz');
244
+ if (!fs.existsSync(headerPath) || !fs.existsSync(footerPath))
245
+ return null;
246
+ const header = extractVmzTemplateHtml(headerPath);
247
+ const footer = extractVmzTemplateHtml(footerPath);
248
+ if (!header || !footer)
249
+ return null;
250
+ return { header, footer };
251
+ }
252
+ /** @param {string} filePath */
253
+ function extractVmzTemplateHtml(filePath) {
254
+ const src = fs.readFileSync(filePath, 'utf8');
255
+ const m = src.match(/<template>([\s\S]*?)<\/template>/);
256
+ return m ? m[1].trim() : '';
257
+ }
258
+ function relativeHref(fromHtmlRel, toRoute, _fromRoute) {
259
+ const toParts = String(toRoute).replace(/^\//, '').split('/').filter(Boolean);
260
+ let toRel;
261
+ if (toParts.length <= 2) {
262
+ toRel = [...toParts, 'index.html'].join('/');
263
+ }
264
+ else {
265
+ const file = toParts.slice(2).join('/') + '.html';
266
+ toRel = [...toParts.slice(0, 2), file].join('/');
267
+ }
268
+ const fromDir = path.posix.dirname(fromHtmlRel.replace(/\\/g, '/'));
269
+ let rel = path.posix.relative(fromDir, toRel);
270
+ if (!rel.startsWith('.') && !rel.startsWith('/'))
271
+ rel = './' + rel;
272
+ return rel || './index.html';
273
+ }