@vmz/vmz 0.1.13 → 0.1.15

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 (56) hide show
  1. package/dist/author-input.d.ts +19 -0
  2. package/dist/author-input.js +52 -0
  3. package/dist/build-assemble.js +18 -7
  4. package/dist/cdn-policy.js +5 -4
  5. package/dist/cli.js +106 -38
  6. package/dist/content-addressed-assets.d.ts +23 -2
  7. package/dist/content-addressed-assets.js +178 -32
  8. package/dist/delivery-profile.d.ts +30 -2
  9. package/dist/delivery-profile.js +139 -5
  10. package/dist/dev-session.d.ts +1 -0
  11. package/dist/dev-session.js +269 -65
  12. package/dist/dev-watch-roots.d.ts +80 -0
  13. package/dist/dev-watch-roots.js +245 -0
  14. package/dist/document-build.d.ts +4 -3
  15. package/dist/document-build.js +104 -82
  16. package/dist/document-check.d.ts +24 -5
  17. package/dist/document-check.js +64 -101
  18. package/dist/document-cmd.js +2 -9
  19. package/dist/document-enrich.d.ts +2 -1
  20. package/dist/document-enrich.js +15 -4
  21. package/dist/document-integrate.js +29 -21
  22. package/dist/document-layout-render.d.ts +20 -0
  23. package/dist/document-layout-render.js +87 -0
  24. package/dist/document-routing-config.d.ts +17 -0
  25. package/dist/document-routing-config.js +31 -0
  26. package/dist/document-schema.js +8 -1
  27. package/dist/embedded-packaging.js +2 -1
  28. package/dist/index.d.ts +18 -2
  29. package/dist/index.js +63 -4
  30. package/dist/locale-check.js +81 -183
  31. package/dist/locale-cmd.js +13 -45
  32. package/dist/locale-route-emit.d.ts +4 -1
  33. package/dist/locale-route-emit.js +7 -32
  34. package/dist/locale-router.js +26 -8
  35. package/dist/native-addon.d.ts +9 -0
  36. package/dist/native-addon.js +84 -0
  37. package/dist/pack.js +3 -2
  38. package/dist/pretty-json.d.ts +19 -0
  39. package/dist/pretty-json.js +43 -0
  40. package/dist/production-observability.js +3 -2
  41. package/dist/production-test-pack.js +4 -3
  42. package/dist/public-static-assets.d.ts +24 -0
  43. package/dist/public-static-assets.js +140 -0
  44. package/dist/release-pack.js +5 -18
  45. package/dist/route-path.d.ts +35 -0
  46. package/dist/route-path.js +77 -0
  47. package/dist/server-artifact.js +4 -3
  48. package/dist/site-delivery.js +4 -3
  49. package/dist/site-favicon.d.ts +31 -0
  50. package/dist/site-favicon.js +140 -0
  51. package/dist/static-emit.d.ts +21 -0
  52. package/dist/static-emit.js +134 -97
  53. package/dist/test-cmd.js +2 -1
  54. package/dist/wechat-packaging.d.ts +22 -0
  55. package/dist/wechat-packaging.js +59 -0
  56. package/package.json +13 -14
@@ -0,0 +1,245 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Dev watch helpers: coalesce multi-file bursts without dropping dirty set,
4
+ * and derive extra watch roots from the compile graph (deployment unit sources).
5
+ */
6
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
9
+ /**
10
+ * @typedef {{ changed: string[], deleted: string[] }} DirtySet
11
+ */
12
+ /**
13
+ * Merge dirty sets: later change cancels delete and vice versa.
14
+ * @param {DirtySet} a
15
+ * @param {DirtySet} b
16
+ * @returns {DirtySet}
17
+ */
18
+ export function mergeDirtySets(a, b) {
19
+ const changed = new Set(a?.changed || []);
20
+ const deleted = new Set(a?.deleted || []);
21
+ for (const f of b?.changed || []) {
22
+ changed.add(f);
23
+ deleted.delete(f);
24
+ }
25
+ for (const f of b?.deleted || []) {
26
+ deleted.add(f);
27
+ changed.delete(f);
28
+ }
29
+ return { changed: [...changed], deleted: [...deleted] };
30
+ }
31
+ /**
32
+ * Wait until `root` stops changing; return the **accumulated** dirty set since `initial`.
33
+ * Updates `fingerprints` for `root` as it polls. Does **not** discard `initial`.
34
+ *
35
+ * @param {string} root
36
+ * @param {Map<string, Map<string, string>>} fingerprints
37
+ * @param {DirtySet} initial
38
+ * @param {{ sleep?: (ms: number) => Promise<void>, maxRounds?: number, settleMs?: number }} [opts]
39
+ * @returns {Promise<DirtySet>}
40
+ */
41
+ export async function coalesceRootBurst(root, fingerprints, initial, opts = {}) {
42
+ const sleepFn = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
43
+ const maxRounds = opts.maxRounds ?? 20;
44
+ const settleMs = opts.settleMs ?? 220;
45
+ let accumulated = {
46
+ changed: [...(initial?.changed || [])],
47
+ deleted: [...(initial?.deleted || [])],
48
+ };
49
+ let guard = 0;
50
+ while (guard++ < maxRounds) {
51
+ await sleepFn(settleMs);
52
+ const prev = fingerprints.get(root) || new Map();
53
+ const next = fileFingerprintMap(root);
54
+ const diff = diffFingerprints(prev, next);
55
+ fingerprints.set(root, next);
56
+ if (!diff.changed.length && !diff.deleted.length)
57
+ break;
58
+ accumulated = mergeDirtySets(accumulated, diff);
59
+ }
60
+ return accumulated;
61
+ }
62
+ /**
63
+ * Walk up from a file to find a package.json directory.
64
+ * @param {string} file
65
+ * @returns {string | null}
66
+ */
67
+ export function findPackageRoot(file) {
68
+ let dir = path.dirname(path.resolve(file));
69
+ for (let i = 0; i < 24; i++) {
70
+ if (existsSync(path.join(dir, 'package.json')))
71
+ return dir;
72
+ const parent = path.dirname(dir);
73
+ if (parent === dir)
74
+ break;
75
+ dir = parent;
76
+ }
77
+ return null;
78
+ }
79
+ /**
80
+ * Prefer package/src when present; otherwise the directory containing the source file.
81
+ * @param {string} sourceFile
82
+ * @returns {string | null}
83
+ */
84
+ export function watchRootForSourceFile(sourceFile) {
85
+ const abs = path.resolve(sourceFile);
86
+ if (!existsSync(abs))
87
+ return null;
88
+ const pkg = findPackageRoot(abs);
89
+ if (pkg) {
90
+ const src = path.join(pkg, 'src');
91
+ if (existsSync(src))
92
+ return src;
93
+ return pkg;
94
+ }
95
+ return path.dirname(abs);
96
+ }
97
+ /**
98
+ * Absolute roots for workspace / file / link deps that have a src tree.
99
+ * @param {string} project
100
+ * @returns {string[]}
101
+ */
102
+ export function localLinkDependencyRoots(project) {
103
+ const pkgPath = path.join(path.resolve(project), 'package.json');
104
+ if (!existsSync(pkgPath))
105
+ return [];
106
+ /** @type {string[]} */
107
+ const roots = [];
108
+ try {
109
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
110
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
111
+ for (const [name, spec] of Object.entries(deps)) {
112
+ if (typeof spec !== 'string')
113
+ continue;
114
+ let target = null;
115
+ if (spec.startsWith('workspace:')) {
116
+ // Resolve via node_modules (pnpm links workspace packages there).
117
+ const nm = path.join(path.resolve(project), 'node_modules', ...name.split('/'));
118
+ if (existsSync(path.join(nm, 'package.json')))
119
+ target = nm;
120
+ }
121
+ else if (spec.startsWith('file:') || spec.startsWith('link:')) {
122
+ const rel = spec.replace(/^(file|link):/, '');
123
+ target = path.resolve(project, rel);
124
+ }
125
+ if (!target)
126
+ continue;
127
+ try {
128
+ target = realpathSync(target);
129
+ }
130
+ catch {
131
+ /* keep as-is */
132
+ }
133
+ const src = path.join(target, 'src');
134
+ if (existsSync(src))
135
+ roots.push(src);
136
+ else if (existsSync(target))
137
+ roots.push(target);
138
+ }
139
+ }
140
+ catch {
141
+ /* ignore */
142
+ }
143
+ return roots;
144
+ }
145
+ /**
146
+ * Collect watch roots: project src/locales/documents + compile-graph external sources
147
+ * + local link/workspace package roots. Never adds a bare registry node_modules tree.
148
+ *
149
+ * @param {{ project: string, outDir: string }} opts
150
+ * @returns {{ roots: string[], dependencyRoots: string[], applicationRoots: string[] }}
151
+ */
152
+ export function collectDevWatchRoots(opts) {
153
+ const project = path.resolve(opts.project);
154
+ const outDir = path.resolve(opts.outDir);
155
+ const src = path.join(project, 'src');
156
+ const docsRoot = path.join(project, 'documents');
157
+ const localesRoot = path.join(project, 'locales');
158
+ const designsRoot = path.join(project, 'designs');
159
+ /** @type {string[]} */
160
+ const applicationRoots = [];
161
+ if (existsSync(src))
162
+ applicationRoots.push(src);
163
+ if (existsSync(docsRoot))
164
+ applicationRoots.push(docsRoot);
165
+ if (existsSync(localesRoot))
166
+ applicationRoots.push(localesRoot);
167
+ if (existsSync(designsRoot))
168
+ applicationRoots.push(designsRoot);
169
+ /** @type {Set<string>} */
170
+ const depSet = new Set();
171
+ const depJson = path.join(outDir, 'vmz-deployment.json');
172
+ if (existsSync(depJson)) {
173
+ try {
174
+ const dep = JSON.parse(readFileSync(depJson, 'utf8'));
175
+ for (const unit of dep.units || []) {
176
+ const source = unit?.source;
177
+ if (typeof source !== 'string' || !source)
178
+ continue;
179
+ let abs = path.resolve(source);
180
+ try {
181
+ abs = realpathSync(abs);
182
+ }
183
+ catch {
184
+ /* keep */
185
+ }
186
+ const underProject = abs === project || abs.startsWith(project + path.sep) || abs.startsWith(project + '/');
187
+ if (underProject)
188
+ continue;
189
+ const root = watchRootForSourceFile(abs);
190
+ if (root)
191
+ depSet.add(path.resolve(root));
192
+ }
193
+ }
194
+ catch {
195
+ /* ignore corrupt deployment */
196
+ }
197
+ }
198
+ for (const r of localLinkDependencyRoots(project)) {
199
+ depSet.add(path.resolve(r));
200
+ }
201
+ // Drop dependency roots that are already under an application root.
202
+ const dependencyRoots = [...depSet].filter((r) => {
203
+ return !applicationRoots.some((app) => r === app || r.startsWith(app + path.sep) || r.startsWith(app + '/'));
204
+ });
205
+ const roots = [...applicationRoots];
206
+ for (const r of dependencyRoots) {
207
+ if (!roots.includes(r))
208
+ roots.push(r);
209
+ }
210
+ return { roots, dependencyRoots, applicationRoots };
211
+ }
212
+ /**
213
+ * Classify which watch bucket a root belongs to.
214
+ * @param {string} root
215
+ * @param {{
216
+ * src: string,
217
+ * docsRoot: string,
218
+ * localesRoot: string,
219
+ * designsRoot: string,
220
+ * dependencyRoots: string[],
221
+ * }} ctx
222
+ * @returns {'src' | 'locales' | 'docs' | 'designs' | 'dep' | 'other'}
223
+ */
224
+ export function classifyWatchRoot(root, ctx) {
225
+ if (root === ctx.src)
226
+ return 'src';
227
+ if (root === ctx.localesRoot)
228
+ return 'locales';
229
+ if (root === ctx.docsRoot)
230
+ return 'docs';
231
+ if (root === ctx.designsRoot)
232
+ return 'designs';
233
+ if ((ctx.dependencyRoots || []).includes(root))
234
+ return 'dep';
235
+ return 'other';
236
+ }
237
+ /**
238
+ * Classify whether a changed file lives under a dependency watch root (not app src).
239
+ * @param {string} file
240
+ * @param {string[]} dependencyRoots
241
+ */
242
+ export function isDependencyPath(file, dependencyRoots) {
243
+ const abs = path.resolve(file);
244
+ return (dependencyRoots || []).some((r) => abs === r || abs.startsWith(r + path.sep) || abs.startsWith(r + '/'));
245
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @param {{ projectRoot: string, outDir?: string, strict?: boolean, engines?: { markdown?: string } }} opts
2
+ * @param {{ projectRoot: string, outDir?: string, appDistDir?: string, strict?: boolean, engines?: { markdown?: string } }} opts
3
3
  */
4
4
  export declare function buildDocuments(opts: any): Promise<{
5
5
  ok: boolean;
@@ -8,7 +8,7 @@ export declare function buildDocuments(opts: any): Promise<{
8
8
  root: string;
9
9
  defaultLocale: any;
10
10
  locales: any[];
11
- localeLabels: {};
11
+ localeLabels: any;
12
12
  collections: any[];
13
13
  mounts: any[];
14
14
  pages: {
@@ -57,6 +57,7 @@ export declare function buildDocuments(opts: any): Promise<{
57
57
  outDir: string;
58
58
  designs: string;
59
59
  designsCss: any;
60
+ hostShell: string;
60
61
  pages: any[];
61
62
  evidence: string;
62
63
  search: string;
@@ -65,7 +66,7 @@ export declare function buildDocuments(opts: any): Promise<{
65
66
  root: string;
66
67
  defaultLocale: any;
67
68
  locales: any[];
68
- localeLabels: {};
69
+ localeLabels: any;
69
70
  collections: any[];
70
71
  mounts: any[];
71
72
  pages: {
@@ -9,21 +9,27 @@ import { resolveDocumentDesignsCss } from './document-designs.js';
9
9
  import { enrichDocumentContent, pageHtmlRel } from './document-enrich.js';
10
10
  import { enrichDocumentEvidence } from './document-evidence.js';
11
11
  import { artifactHrefFromHtml, buildDocumentIslands, buildDocumentSearch, collectFenceBodies, renderIslandShellsHtml, } from './document-interactive.js';
12
+ import { assertIntegratedDistReady, renderCompiledDocumentLayout } from './document-layout-render.js';
12
13
  import { resolveMarkdownEngine } from './document-markdown.js';
14
+ import { loadLocalesRouting } from './document-routing-config.js';
13
15
  import { DOCUMENT_VIEW_SCHEMA } from './document-schema.js';
14
16
  import { createWorkspace } from './index.js';
17
+ import { requireNativeAddon } from './native-addon.js';
18
+ import { writePrettyJsonFile } from './pretty-json.js';
15
19
  /**
16
- * @param {{ projectRoot: string, outDir?: string, strict?: boolean, engines?: { markdown?: string } }} opts
20
+ * @param {{ projectRoot: string, outDir?: string, appDistDir?: string, strict?: boolean, engines?: { markdown?: string } }} opts
17
21
  */
18
22
  export async function buildDocuments(opts) {
19
23
  const projectRoot = path.resolve(opts.projectRoot);
20
24
  const outDir = path.resolve(opts.outDir || path.join(projectRoot, 'dist', 'documents'));
21
25
  const strict = Boolean(opts.strict);
22
26
  const manifest = checkDocuments({ projectRoot, strict });
27
+ const routing = loadLocalesRouting(projectRoot) || { strategy: 'prefix' };
23
28
  const engine = await resolveMarkdownEngine({ engines: opts.engines, projectRoot });
24
29
  const enriched = enrichDocumentContent(manifest, {
25
30
  analyzeMarkdown: engine.analyzeMarkdown,
26
31
  projectRoot,
32
+ routing,
27
33
  });
28
34
  manifest.diagnostics = enriched.diagnostics;
29
35
  const evidence = await enrichDocumentEvidence(manifest, {
@@ -58,8 +64,12 @@ export async function buildDocuments(opts) {
58
64
  });
59
65
  manifest.search = search;
60
66
  manifest.islands = islands;
61
- const hostChrome = resolveHostSiteChrome(projectRoot);
62
- const useHostShell = Boolean(hostChrome) && (manifest.mounts || []).some((m) => m.mode === 'integrated');
67
+ const integratedMount = (manifest.mounts || []).some((m) => m.mode === 'integrated');
68
+ const appDistDir = resolveAppDistDir(opts, outDir);
69
+ const useCompiledShell = Boolean(integratedMount && appDistDir);
70
+ if (integratedMount && appDistDir) {
71
+ assertIntegratedDistReady(appDistDir);
72
+ }
63
73
  fs.mkdirSync(outDir, { recursive: true });
64
74
  const designs = resolveDocumentDesignsCss(projectRoot);
65
75
  /** @type {string | null} */
@@ -90,6 +100,20 @@ export async function buildDocuments(opts) {
90
100
  pageKey: page.identity.pageKey,
91
101
  locale: page.identity.locale,
92
102
  });
103
+ const slotHtml = buildDocumentSlotHtml({
104
+ nav,
105
+ bodyHtml: info.html,
106
+ headings: info.headings,
107
+ htmlRel,
108
+ route: info.route,
109
+ routing,
110
+ searchShellHtml: shells.searchHtml,
111
+ playgroundShellHtml: shells.playgroundHtml,
112
+ });
113
+ let compiledLayoutHtml = null;
114
+ if (useCompiledShell) {
115
+ compiledLayoutHtml = await renderCompiledDocumentLayout(appDistDir, page.identity.locale, slotHtml);
116
+ }
93
117
  const view = {
94
118
  schema: DOCUMENT_VIEW_SCHEMA,
95
119
  pageKey: page.identity.pageKey,
@@ -103,7 +127,7 @@ export async function buildDocuments(opts) {
103
127
  designsCss: designsHref,
104
128
  noJsReadable: true,
105
129
  hydrate: 'island-only',
106
- hostShell: useHostShell,
130
+ hostShell: useCompiledShell ? 'compiled-layout' : false,
107
131
  islands: ['DocumentSearch'].concat((islands.islands || [])
108
132
  .filter((isl) => isl.kind === 'playground' &&
109
133
  isl.fence?.locale === page.identity.locale &&
@@ -113,7 +137,7 @@ export async function buildDocuments(opts) {
113
137
  const viewRel = path.posix.join('views', page.identity.locale, `${page.identity.pageKey === 'index' ? 'index' : page.identity.pageKey}.view.json`);
114
138
  const viewAbs = path.join(outDir, viewRel);
115
139
  fs.mkdirSync(path.dirname(viewAbs), { recursive: true });
116
- fs.writeFileSync(viewAbs, JSON.stringify(view, null, 2) + '\n', 'utf8');
140
+ writePrettyJsonFile(viewAbs, view);
117
141
  const html = renderStaticHtml({
118
142
  title: info.title,
119
143
  locale: page.identity.locale,
@@ -125,7 +149,9 @@ export async function buildDocuments(opts) {
125
149
  htmlRel,
126
150
  searchShellHtml: shells.searchHtml,
127
151
  playgroundShellHtml: shells.playgroundHtml,
128
- hostChrome: useHostShell ? hostChrome : null,
152
+ routing,
153
+ compiledLayoutHtml,
154
+ useCompiledShell,
129
155
  });
130
156
  fs.writeFileSync(htmlAbs, html, 'utf8');
131
157
  written.push({ route: info.route, htmlPath: htmlRel, viewPath: viewRel });
@@ -141,39 +167,38 @@ export async function buildDocuments(opts) {
141
167
  outDir: path.relative(projectRoot, outDir).replace(/\\/g, '/') || '.',
142
168
  designs: designs.source,
143
169
  designsCss: designsHref,
170
+ hostShell: useCompiledShell ? 'compiled-layout' : 'standalone',
144
171
  pages: written,
145
172
  evidence: 'document.evidence.json',
146
173
  search: 'document.search.json',
147
174
  islands: 'document.islands.json',
148
175
  },
149
176
  };
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');
177
+ writePrettyJsonFile(path.join(outDir, 'document.manifest.json'), manifestOut);
178
+ writePrettyJsonFile(path.join(outDir, 'document.evidence.json'), evidence.evidence);
179
+ writePrettyJsonFile(path.join(outDir, 'document.search.json'), search);
180
+ writePrettyJsonFile(path.join(outDir, 'document.islands.json'), islands);
154
181
  return { ok: true, manifest: manifestOut, outDir, pages: written, search, islands };
155
182
  }
156
183
  /**
157
- * No-JS readable static HTML: nav + main landmarks, Island shells without scripts.
158
- * Integrated mounts reuse host SiteHeader/SiteFooter templates when present.
184
+ * @param {{ appDistDir?: string }} opts
185
+ * @param {string} outDir
159
186
  */
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
- // Integrated documents are served with pretty directory URLs. Root
168
- // absolute assets remain correct for both emitted files and rewrites.
169
- cssHrefs.push('/vmz.css');
187
+ function resolveAppDistDir(opts, outDir) {
188
+ if (opts.appDistDir) {
189
+ const p = path.resolve(opts.appDistDir);
190
+ return fs.existsSync(path.join(p, 'vmz-dom.js')) ? p : null;
170
191
  }
171
- if (designsHref)
172
- cssHrefs.push(hostChrome ? `/${designsHref}` : prefix + designsHref);
173
- const cssLink = cssHrefs.map((href) => ` <link rel="stylesheet" href="${esc(href)}" />`).join('\n') + (cssHrefs.length ? '\n' : '');
192
+ return fs.existsSync(path.join(outDir, 'vmz-dom.js')) ? outDir : null;
193
+ }
194
+ /**
195
+ * Document main column + sidebar (injected into DocumentLayout slot).
196
+ */
197
+ function buildDocumentSlotHtml({ nav, bodyHtml, headings, htmlRel, route, routing, searchShellHtml = '', playgroundShellHtml = '' }) {
198
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
174
199
  const navItems = nav
175
200
  .map((n) => {
176
- const href = relativeHref(htmlRel, n.href, route);
201
+ const href = routing.strategy === 'none' || routing.strategy === 'domain' ? n.href : relativeHref(htmlRel, n.href, route);
177
202
  const current = n.href === route ? ' aria-current="page"' : '';
178
203
  return ` <li><a href="${esc(href)}"${current}>${esc(n.title)}</a></li>`;
179
204
  })
@@ -188,21 +213,7 @@ function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, desig
188
213
  ${navItems}
189
214
  </ul>
190
215
  </nav>`;
191
- if (hostChrome) {
192
- const header = hostChrome.header.replace(/(<a\s+href="\/d\/?")([^>]*>文档<\/a>)/, '$1 aria-current="page"$2');
193
- return `<!DOCTYPE html>
194
- <html lang="${esc(locale)}">
195
- <head>
196
- <meta charset="utf-8" />
197
- <meta name="viewport" content="width=device-width, initial-scale=1" />
198
- <title>${esc(title)}</title>
199
- ${cssLink}</head>
200
- <body data-vmz-hydrate="island-only">
201
- <div class="site site--docs">
202
- <a class="skip-link" href="#main">Skip to content</a>
203
- ${header}
204
- <div class="doc-body">
205
- <aside class="doc-sidebar">
216
+ return ` <aside class="doc-sidebar">
206
217
  ${docsNav}
207
218
  ${searchShellHtml}
208
219
  </aside>
@@ -211,54 +222,65 @@ ${searchShellHtml}
211
222
  ${bodyHtml}
212
223
  ${playgroundShellHtml}
213
224
  </main>
214
- </div>
215
- </div>
216
- ${hostChrome.footer}
217
- </div>
218
- </body>
219
- </html>
220
- `;
225
+ </div>`;
226
+ }
227
+ /**
228
+ * No-JS readable static HTML: nav + main landmarks, Island shells without scripts.
229
+ * Integrated mounts wrap content in compiled DocumentLayout (SiteHeader/SiteFooter SSR).
230
+ */
231
+ function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, designsHref, htmlRel, searchShellHtml = '', playgroundShellHtml = '', routing = { strategy: 'prefix' }, compiledLayoutHtml, useCompiledShell = false, }) {
232
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
233
+ const depth = htmlRel.split('/').length - 1;
234
+ const prefix = depth > 0 ? '../'.repeat(depth) : './';
235
+ /** @type {string[]} */
236
+ const cssHrefs = [];
237
+ if (useCompiledShell) {
238
+ cssHrefs.push('/vmz.css');
221
239
  }
222
- return `<!DOCTYPE html>
223
- <html lang="${esc(locale)}">
224
- <head>
225
- <meta charset="utf-8" />
226
- <meta name="viewport" content="width=device-width, initial-scale=1" />
227
- <title>${esc(title)}</title>
228
- ${cssLink}</head>
229
- <body data-vmz-hydrate="island-only">
230
- <a class="skip-link" href="#main">Skip to content</a>
231
- ${docsNav}
240
+ if (designsHref)
241
+ cssHrefs.push(useCompiledShell ? `/${designsHref}` : prefix + designsHref);
242
+ /** @type {string} */
243
+ let bodyInner;
244
+ if (useCompiledShell) {
245
+ bodyInner = compiledLayoutHtml;
246
+ }
247
+ else {
248
+ const navItems = nav
249
+ .map((n) => {
250
+ const href = routing.strategy === 'none' || routing.strategy === 'domain' ? n.href : relativeHref(htmlRel, n.href, route);
251
+ const current = n.href === route ? ' aria-current="page"' : '';
252
+ return ` <li><a href="${esc(href)}"${current}>${esc(n.title)}</a></li>`;
253
+ })
254
+ .join('\n');
255
+ const toc = headings.length > 1
256
+ ? `<nav aria-label="On this page" class="toc">\n <ol>\n${headings
257
+ .map((h) => ` <li class="h${h.level}"><a href="#${esc(h.id)}">${esc(h.text)}</a></li>`)
258
+ .join('\n')}\n </ol>\n </nav>\n`
259
+ : '';
260
+ bodyInner = ` <a class="skip-link" href="#main">Skip to content</a>
261
+ <nav aria-label="Documents" class="doc-subnav">
262
+ <ul>
263
+ ${navItems}
264
+ </ul>
265
+ </nav>
232
266
  ${searchShellHtml}
233
267
  ${toc}<main id="main">
234
268
  ${bodyHtml}
235
269
  ${playgroundShellHtml}
236
270
  </main>
237
- </body>
238
- </html>
239
271
  `;
240
- }
241
- /**
242
- * Integrated DocumentMount: reuse host SiteHeader / SiteFooter .vmz templates.
243
- * @param {string} projectRoot
244
- * @returns {{ header: string, footer: string } | null}
245
- */
246
- function resolveHostSiteChrome(projectRoot) {
247
- const headerPath = path.join(projectRoot, 'src', 'components', 'SiteHeader.vmz');
248
- const footerPath = path.join(projectRoot, 'src', 'components', 'SiteFooter.vmz');
249
- if (!fs.existsSync(headerPath) || !fs.existsSync(footerPath))
250
- return null;
251
- const header = extractVmzTemplateHtml(headerPath);
252
- const footer = extractVmzTemplateHtml(footerPath);
253
- if (!header || !footer)
254
- return null;
255
- return { header, footer };
256
- }
257
- /** @param {string} filePath */
258
- function extractVmzTemplateHtml(filePath) {
259
- const src = fs.readFileSync(filePath, 'utf8');
260
- const m = src.match(/<template>([\s\S]*?)<\/template>/);
261
- return m ? m[1].trim() : '';
272
+ }
273
+ const native = requireNativeAddon();
274
+ if (typeof native.generateHtmlShell !== 'function') {
275
+ throw new Error('vmz native addon missing generateHtmlShell — rebuild with `pnpm napi:build`');
276
+ }
277
+ return native.generateHtmlShell({
278
+ title,
279
+ lang: locale,
280
+ cssHrefs,
281
+ bodyHtml: bodyInner,
282
+ bodyAttrs: ['data-vmz-hydrate', 'island-only'],
283
+ });
262
284
  }
263
285
  function relativeHref(fromHtmlRel, toRoute, _fromRoute) {
264
286
  const toParts = String(toRoute).replace(/^\//, '').split('/').filter(Boolean);
@@ -1,5 +1,8 @@
1
1
  /**
2
2
  * Build DocumentManifest + run / --strict checks.
3
+ *
4
+ * Author documents config is parsed by Rust DocumentRoutePlan; TS only
5
+ * merges filesystem scan (pageKeys) and coverage diagnostics.
3
6
  */
4
7
  /**
5
8
  * Resolve project documents root.
@@ -7,14 +10,30 @@
7
10
  */
8
11
  export declare function resolveDocumentsRoot(projectRoot: any): string;
9
12
  /**
10
- * Parse documents.config.json or a JSON-compatible export-default .ts/.js.
13
+ * Load normalized DocumentRoutePlan from Rust (author JSON5/JSON/declaration).
14
+ * @param {string} projectRoot
15
+ */
16
+ export declare function loadDocumentsRoutePlan(projectRoot: any): any;
17
+ /**
18
+ * @deprecated Prefer loadDocumentsRoutePlan(projectRoot). Kept for call sites that
19
+ * only need the plan-shaped config fields.
11
20
  * @param {string} documentsRoot
12
- * @returns {{ config: Record<string, any> | null, diagnostics: any[], configPath: string | null }}
13
21
  */
14
22
  export declare function loadDocumentsConfig(documentsRoot: any): {
15
- config: any;
16
- diagnostics: any[];
23
+ config: {
24
+ defaultLocale: any;
25
+ locales: {
26
+ [k: string]: {
27
+ label: unknown;
28
+ };
29
+ };
30
+ collections: {
31
+ [k: string]: any;
32
+ };
33
+ };
34
+ diagnostics: any;
17
35
  configPath: string;
36
+ plan: any;
18
37
  };
19
38
  /**
20
39
  * @param {object} opts
@@ -27,7 +46,7 @@ export declare function checkDocuments(opts: any): {
27
46
  root: string;
28
47
  defaultLocale: any;
29
48
  locales: any[];
30
- localeLabels: {};
49
+ localeLabels: any;
31
50
  collections: any[];
32
51
  mounts: any[];
33
52
  pages: {