@vmz/vmz 0.3.11 → 0.3.13

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 (52) hide show
  1. package/delivery-runtime-files.json +1 -0
  2. package/dist/cli/cli.js +46 -23
  3. package/dist/cli/format-cmd.d.ts +9 -17
  4. package/dist/cli/format-cmd.js +37 -49
  5. package/dist/cli/format-config.d.ts +6 -9
  6. package/dist/cli/format-config.js +5 -29
  7. package/dist/delivery/delivery-profile.js +1 -1
  8. package/dist/dev/dev-session.js +16 -5
  9. package/dist/document/document-base-css.d.ts +15 -0
  10. package/dist/document/document-base-css.js +578 -0
  11. package/dist/document/document-build.d.ts +1 -0
  12. package/dist/document/document-build.js +94 -78
  13. package/dist/document/document-client.d.ts +13 -0
  14. package/dist/document/document-client.js +217 -0
  15. package/dist/document/document-cmd.js +3 -1
  16. package/dist/document/document-designs.d.ts +9 -3
  17. package/dist/document/document-designs.js +20 -34
  18. package/dist/document/document-enrich.d.ts +9 -0
  19. package/dist/document/document-enrich.js +55 -0
  20. package/dist/document/document-highlight.d.ts +26 -0
  21. package/dist/document/document-highlight.js +54 -0
  22. package/dist/document/document-integrate.d.ts +5 -0
  23. package/dist/document/document-integrate.js +1 -0
  24. package/dist/document/document-interactive.js +10 -2
  25. package/dist/document/document-layout-render.js +6 -2
  26. package/dist/document/document-schema.d.ts +5 -0
  27. package/dist/workspace/build-assemble.js +18 -6
  28. package/dist/workspace/cdn-policy.js +12 -0
  29. package/dist/workspace/content-addressed-assets.d.ts +6 -17
  30. package/dist/workspace/content-addressed-assets.js +10 -95
  31. package/dist/workspace/delivery-runtime-imports.js +3 -0
  32. package/dist/workspace/native-addon.d.ts +4 -0
  33. package/dist/workspace/pack-native.d.ts +139 -0
  34. package/dist/workspace/pack-native.js +273 -0
  35. package/dist/workspace/pack.d.ts +113 -27
  36. package/dist/workspace/pack.js +271 -54
  37. package/dist/workspace/plugin-host.d.ts +4 -4
  38. package/dist/workspace/plugin-host.js +9 -12
  39. package/dist/workspace/production-observability.d.ts +2 -0
  40. package/dist/workspace/production-observability.js +6 -2
  41. package/dist/workspace/project-build.js +26 -4
  42. package/dist/workspace/public-api.d.ts +7 -3
  43. package/dist/workspace/public-api.js +5 -3
  44. package/dist/workspace/static-emit.d.ts +30 -0
  45. package/dist/workspace/static-emit.js +211 -18
  46. package/dist/workspace/user-config.d.ts +112 -0
  47. package/dist/workspace/user-config.js +12 -0
  48. package/host-runtime-files.json +1 -0
  49. package/locales/en-US/cli.json +5 -4
  50. package/package.json +20 -20
  51. package/dist/workspace/pack-client-packages.d.ts +0 -32
  52. package/dist/workspace/pack-client-packages.js +0 -437
@@ -4,9 +4,12 @@
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { checkDocuments, manifestHasErrors } from './document-check.js';
7
+ import { DOCUMENT_BASE_CSS } from './document-base-css.js';
8
+ import { DOCUMENT_CLIENT_HREF, DOCUMENT_CLIENT_JS } from './document-client.js';
7
9
  import { resolveDocumentDesignsCss } from './document-designs.js';
8
- import { enrichDocumentContent, pageHtmlRel } from './document-enrich.js';
10
+ import { enrichDocumentContent, pageHtmlRel, pageRoute, routeHrefForDocLink } from './document-enrich.js';
9
11
  import { enrichDocumentEvidence } from './document-evidence.js';
12
+ import { resolveDocumentCodeHighlighter } from './document-highlight.js';
10
13
  import { artifactHrefFromHtml, buildDocumentIslands, buildDocumentSearch, collectFenceBodies, renderIslandShellsHtml, } from './document-interactive.js';
11
14
  import { assertIntegratedDistReady, renderCompiledDocumentLayout } from './document-layout-render.js';
12
15
  import { resolveMarkdownEngine } from './document-markdown.js';
@@ -39,11 +42,19 @@ export async function buildDocuments(opts) {
39
42
  return { ok: false, manifest, outDir, pages: [] };
40
43
  }
41
44
  const analyzedByPageId = new Map();
45
+ const highlighter = await resolveDocumentCodeHighlighter({ code: opts.engines?.code }, manifest.diagnostics);
46
+ const pageKeySet = new Map();
47
+ for (const p of manifest.pages) {
48
+ const set = pageKeySet.get(p.identity.locale) || new Set();
49
+ set.add(p.identity.pageKey);
50
+ pageKeySet.set(p.identity.locale, set);
51
+ }
42
52
  for (const page of manifest.pages) {
43
53
  const abs = path.isAbsolute(page.sourcePath) ? page.sourcePath : path.join(manifest.root, page.sourcePath);
44
54
  const source = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : '';
45
55
  const id = `${page.identity.locale}:${page.identity.pageKey}`;
46
- analyzedByPageId.set(id, engine.analyzeMarkdown(source));
56
+ const hooks = documentRenderHooks(page, enriched.routeBase, pageKeySet, routing, highlighter);
57
+ analyzedByPageId.set(id, engine.analyzeMarkdown(source, hooks));
47
58
  }
48
59
  const fenceBodies = collectFenceBodies(analyzedByPageId, manifest.pages);
49
60
  const search = buildDocumentSearch({
@@ -67,12 +78,20 @@ export async function buildDocuments(opts) {
67
78
  }
68
79
  fs.mkdirSync(outDir, { recursive: true });
69
80
  const designs = resolveDocumentDesignsCss(projectRoot, appDistDir);
70
- let designsHref = null;
71
- if (designs.css && designs.href) {
72
- const cssPath = path.join(outDir, designs.href);
81
+ // Framework document base always emits; site token bundle emits when the
82
+ // project has compiled tokens (integrated) or a `designs/styles/index.css`
83
+ // entry (standalone).
84
+ const baseCssPath = path.join(outDir, designs.baseHref);
85
+ fs.mkdirSync(path.dirname(baseCssPath), { recursive: true });
86
+ fs.writeFileSync(baseCssPath, DOCUMENT_BASE_CSS, 'utf8');
87
+ // Framework client enhancement (search + copy) ships with every build.
88
+ const clientPath = path.join(outDir, DOCUMENT_CLIENT_HREF);
89
+ fs.mkdirSync(path.dirname(clientPath), { recursive: true });
90
+ fs.writeFileSync(clientPath, DOCUMENT_CLIENT_JS, 'utf8');
91
+ if (designs.tokensCss && designs.tokensHref) {
92
+ const cssPath = path.join(outDir, designs.tokensHref);
73
93
  fs.mkdirSync(path.dirname(cssPath), { recursive: true });
74
- fs.writeFileSync(cssPath, designs.css, 'utf8');
75
- designsHref = designs.href;
94
+ fs.writeFileSync(cssPath, designs.tokensCss, 'utf8');
76
95
  }
77
96
  const viewsDir = path.join(outDir, 'views');
78
97
  fs.mkdirSync(viewsDir, { recursive: true });
@@ -82,6 +101,10 @@ export async function buildDocuments(opts) {
82
101
  const info = enriched.byId.get(id);
83
102
  if (!info)
84
103
  continue;
104
+ // Render-pass HTML: fences highlighted and author links rewritten to
105
+ // route form (validation already ran on the author form in enrich).
106
+ const rendered = analyzedByPageId.get(id);
107
+ const bodyHtml = rendered?.html ?? info.html;
85
108
  const nav = enriched.navByLocale[page.identity.locale] || [];
86
109
  const htmlRel = pageHtmlRel(enriched.routeBase, page.identity.locale, page.identity.pageKey);
87
110
  const htmlAbs = path.join(outDir, htmlRel);
@@ -95,10 +118,12 @@ export async function buildDocuments(opts) {
95
118
  });
96
119
  const slotHtml = buildDocumentSlotHtml({
97
120
  nav,
98
- bodyHtml: info.html,
121
+ bodyHtml,
99
122
  headings: info.headings,
100
- htmlRel,
123
+ title: info.title,
101
124
  route: info.route,
125
+ routeBase: enriched.routeBase,
126
+ locale: page.identity.locale,
102
127
  routing,
103
128
  searchShellHtml: shells.searchHtml,
104
129
  playgroundShellHtml: shells.playgroundHtml,
@@ -116,8 +141,10 @@ export async function buildDocuments(opts) {
116
141
  headings: info.headings,
117
142
  nav,
118
143
  bodyKind: 'html',
119
- html: info.html,
120
- designsCss: designsHref,
144
+ html: bodyHtml,
145
+ designsCss: designs.tokensHref,
146
+ documentCss: designs.baseHref,
147
+ clientJs: DOCUMENT_CLIENT_HREF,
121
148
  noJsReadable: true,
122
149
  hydrate: 'island-only',
123
150
  hostShell: useCompiledShell ? 'compiled-layout' : false,
@@ -135,14 +162,9 @@ export async function buildDocuments(opts) {
135
162
  title: info.title,
136
163
  locale: page.identity.locale,
137
164
  route: info.route,
138
- nav,
139
- bodyHtml: info.html,
140
- headings: info.headings,
141
- designsHref,
142
- htmlRel,
143
- searchShellHtml: shells.searchHtml,
144
- playgroundShellHtml: shells.playgroundHtml,
145
- routing,
165
+ slotHtml,
166
+ tokensHref: designs.tokensHref,
167
+ baseHref: designs.baseHref,
146
168
  compiledLayoutHtml,
147
169
  useCompiledShell,
148
170
  });
@@ -159,7 +181,9 @@ export async function buildDocuments(opts) {
159
181
  engine: engine.engine,
160
182
  outDir: path.relative(projectRoot, outDir).replace(/\\/g, '/') || '.',
161
183
  designs: designs.source,
162
- designsCss: designsHref,
184
+ designsCss: designs.tokensHref,
185
+ documentCss: designs.baseHref,
186
+ clientJs: DOCUMENT_CLIENT_HREF,
163
187
  hostShell: useCompiledShell ? 'compiled-layout' : 'standalone',
164
188
  pages: written,
165
189
  evidence: 'document.evidence.json',
@@ -180,22 +204,36 @@ function resolveAppDistDir(opts, outDir) {
180
204
  }
181
205
  return fs.existsSync(path.join(outDir, 'vmz-dom.js')) ? outDir : null;
182
206
  }
183
- /** Document main column + sidebar (injected into DocumentLayout slot). */
184
- function buildDocumentSlotHtml({ nav, bodyHtml, headings, htmlRel, route, routing, searchShellHtml = '', playgroundShellHtml = '', }) {
207
+ /** Document-layer chrome copy, localized by page locale (normal case, no decorative micro labels). */
208
+ function docUiCopy(locale) {
209
+ return locale.startsWith('zh')
210
+ ? { home: '首页', docs: '文档', toc: '本页内容' }
211
+ : { home: 'Home', docs: 'Documentation', toc: 'On this page' };
212
+ }
213
+ /**
214
+ * Document main column + sidebar (injected into the DocumentLayout slot;
215
+ * standalone mounts wrap the same markup — one DOM shape, one stylesheet).
216
+ * In-site links are route-form (`/d/<locale>/<pageKey>`), identical to
217
+ * application routes — no `.html` file paths leak into the page.
218
+ */
219
+ function buildDocumentSlotHtml({ nav, bodyHtml, headings, title, route, routeBase, locale, routing, searchShellHtml = '', playgroundShellHtml = '', }) {
185
220
  const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
221
+ const copy = docUiCopy(locale);
186
222
  const navItems = nav
187
223
  .map((n) => {
188
- const href = routing.strategy === 'none' || routing.strategy === 'domain' ? n.href : relativeHref(htmlRel, n.href, route);
189
224
  const current = n.href === route ? ' aria-current="page"' : '';
190
- return ` <li><a href="${esc(href)}"${current}>${esc(n.title)}</a></li>`;
225
+ return ` <li><a href="${esc(n.href)}"${current}>${esc(n.title)}</a></li>`;
191
226
  })
192
227
  .join('\n');
193
228
  const toc = headings.length > 1
194
- ? `<nav aria-label="On this page" class="toc">\n <ol>\n${headings
229
+ ? `<nav aria-label="${esc(copy.toc)}" class="toc">\n <p class="toc__label">${esc(copy.toc)}</p>\n <ol>\n${headings
195
230
  .map((h) => ` <li class="h${h.level}"><a href="#${esc(h.id)}">${esc(h.text)}</a></li>`)
196
231
  .join('\n')}\n </ol>\n </nav>\n`
197
232
  : '';
198
- const docsNav = ` <nav aria-label="Documents" class="doc-subnav">
233
+ const docsRoot = pageRoute(routeBase, locale, 'index', routing);
234
+ const breadcrumb = `<nav class="doc-breadcrumb" aria-label="Breadcrumb"><a href="/">${esc(copy.home)}</a><span aria-hidden="true"> / </span><a href="${esc(docsRoot)}">${esc(copy.docs)}</a><span aria-hidden="true"> / </span><span aria-current="page">${esc(title)}</span></nav>`;
235
+ const docsNav = ` <p class="doc-sidebar__label">${esc(copy.docs)}</p>
236
+ <nav aria-label="Documents" class="doc-subnav">
199
237
  <ul>
200
238
  ${navItems}
201
239
  </ul>
@@ -206,55 +244,40 @@ ${searchShellHtml}
206
244
  </aside>
207
245
  <div class="doc-content">
208
246
  ${toc}<main id="main">
247
+ ${breadcrumb}
209
248
  ${bodyHtml}
210
249
  ${playgroundShellHtml}
211
250
  </main>
212
251
  </div>`;
213
252
  }
214
253
  /**
215
- * No-JS readable static HTML: nav + main landmarks, Island shells without scripts.
216
- * Integrated mounts wrap content in compiled DocumentLayout (SiteHeader/SiteFooter SSR).
254
+ * No-JS readable static HTML: nav + main landmarks, Island shells, and exactly
255
+ * one deferred framework enhancement module (`assets/vmz-document.js` — search
256
+ * filter + copy buttons). Integrated mounts wrap content in compiled
257
+ * DocumentLayout (SiteHeader/SiteFooter SSR); standalone mounts emit the same
258
+ * `.site.site--docs > .doc-body` skeleton themselves.
217
259
  */
218
- function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, designsHref, htmlRel, searchShellHtml = '', playgroundShellHtml = '', routing = { strategy: 'prefix' }, compiledLayoutHtml, useCompiledShell = false, }) {
219
- const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
220
- const depth = htmlRel.split('/').length - 1;
221
- const prefix = depth > 0 ? '../'.repeat(depth) : './';
260
+ function renderStaticHtml({ title, locale, slotHtml, tokensHref, baseHref, compiledLayoutHtml, useCompiledShell = false, }) {
222
261
  const cssHrefs = [];
223
262
  if (useCompiledShell) {
224
263
  cssHrefs.push('/vmz.css');
225
264
  }
226
- if (designsHref)
227
- cssHrefs.push(useCompiledShell ? `/${designsHref}` : prefix + designsHref);
228
- let bodyInner;
229
- if (useCompiledShell) {
230
- bodyInner = compiledLayoutHtml || '';
231
- }
232
- else {
233
- const navItems = nav
234
- .map((n) => {
235
- const href = routing.strategy === 'none' || routing.strategy === 'domain' ? n.href : relativeHref(htmlRel, n.href, route);
236
- const current = n.href === route ? ' aria-current="page"' : '';
237
- return ` <li><a href="${esc(href)}"${current}>${esc(n.title)}</a></li>`;
238
- })
239
- .join('\n');
240
- const toc = headings.length > 1
241
- ? `<nav aria-label="On this page" class="toc">\n <ol>\n${headings
242
- .map((h) => ` <li class="h${h.level}"><a href="#${esc(h.id)}">${esc(h.text)}</a></li>`)
243
- .join('\n')}\n </ol>\n </nav>\n`
244
- : '';
245
- bodyInner = ` <a class="skip-link" href="#main">Skip to content</a>
246
- <nav aria-label="Documents" class="doc-subnav">
247
- <ul>
248
- ${navItems}
249
- </ul>
250
- </nav>
251
- ${searchShellHtml}
252
- ${toc}<main id="main">
253
- ${bodyHtml}
254
- ${playgroundShellHtml}
255
- </main>
265
+ // Absolute hrefs: document pages are served under the mount root by the
266
+ // same host as the app (integrated) or as the site root (standalone).
267
+ if (tokensHref)
268
+ cssHrefs.push(`/${tokensHref}`);
269
+ cssHrefs.push(`/${baseHref}`);
270
+ const enhancement = `<script type="module" src="/${DOCUMENT_CLIENT_HREF}"></script>`;
271
+ const bodyInner = useCompiledShell
272
+ ? `${compiledLayoutHtml || ''}\n ${enhancement}`
273
+ : ` <a class="skip-link" href="#main">Skip to content</a>
274
+ <div class="site site--docs">
275
+ <div class="doc-body">
276
+ ${slotHtml}
277
+ </div>
278
+ </div>
279
+ ${enhancement}
256
280
  `;
257
- }
258
281
  const native = requireNativeAddon();
259
282
  if (typeof native.generateHtmlShell !== 'function') {
260
283
  throw new Error('vmz native addon missing generateHtmlShell — rebuild with `pnpm napi:build`');
@@ -267,19 +290,12 @@ ${playgroundShellHtml}
267
290
  bodyAttrs: ['data-vmz-hydrate', 'island-only'],
268
291
  });
269
292
  }
270
- function relativeHref(fromHtmlRel, toRoute, _fromRoute) {
271
- const toParts = String(toRoute).replace(/^\//, '').split('/').filter(Boolean);
272
- let toRel;
273
- if (toParts.length <= 2) {
274
- toRel = [...toParts, 'index.html'].join('/');
275
- }
276
- else {
277
- const file = toParts.slice(2).join('/') + '.html';
278
- toRel = [...toParts.slice(0, 2), file].join('/');
279
- }
280
- const fromDir = path.posix.dirname(fromHtmlRel.replace(/\\/g, '/'));
281
- let rel = path.posix.relative(fromDir, toRel);
282
- if (!rel.startsWith('.') && !rel.startsWith('/'))
283
- rel = './' + rel;
284
- return rel || './index.html';
293
+ /** Render-pass hooks: fence highlight + author link → route-form rewrite + heading anchors. */
294
+ function documentRenderHooks(page, routeBase, pageKeySet, routing, highlighter) {
295
+ return {
296
+ resolveHref: (href) => routeHrefForDocLink(href, page.identity.pageKey, page.identity.locale, routeBase, pageKeySet, routing),
297
+ // Self-links on h2+ (the h1 is the page title — anchoring it is noise).
298
+ headingAnchor: (h) => (h.level >= 2 ? `#${h.id}` : null),
299
+ ...(highlighter ? { highlight: (code, lang) => highlighter.highlightFenceInline(code, lang) } : {}),
300
+ };
285
301
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Framework-owned document client enhancement (`assets/vmz-document.js`).
3
+ *
4
+ * Progressive enhancement only: pages stay no-JS readable. The module wires
5
+ * the static DocumentSearch shell to `document.search.json` (locale-scoped
6
+ * substring filter, full keyboard loop) and injects copy buttons on article
7
+ * code blocks. No framework runtime, no full-page hydration.
8
+ *
9
+ * Emitted verbatim by `buildDocuments`; keep it dependency-free plain JS —
10
+ * this file is served as-authored, never bundled.
11
+ */
12
+ export declare const DOCUMENT_CLIENT_HREF = "assets/vmz-document.js";
13
+ export declare const DOCUMENT_CLIENT_JS = "/* vmz document layer \u2014 progressive enhancement (no framework runtime). */\n(function () {\n 'use strict';\n\n var doc = document.documentElement;\n var zh = String(doc.lang || '').toLowerCase().indexOf('zh') === 0;\n var copy = zh\n ? { search: '\u641C\u7D22\u6587\u6863', noResults: '\u6CA1\u6709\u5339\u914D\u7ED3\u679C', hintJs: '\u8F93\u5165\u5173\u952E\u8BCD\u8FC7\u6EE4\u6587\u6863', copyLabel: '\u590D\u5236', copied: '\u5DF2\u590D\u5236', results: '\u641C\u7D22\u7ED3\u679C' }\n : { search: 'Search docs', noResults: 'No matching results', hintJs: 'Type to filter the docs', copyLabel: 'Copy', copied: 'Copied', results: 'Search results' };\n\n function ready(fn) {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', fn, { once: true });\n } else {\n fn();\n }\n }\n\n /* Search ------------------------------------------------------------ */\n\n function enhanceSearch(host) {\n var input = host.querySelector('.doc-search__input');\n var list = host.querySelector('.doc-search__results');\n var hint = host.querySelector('.doc-search__hint');\n if (!input || !list) return;\n var indexHref = host.getAttribute('data-vmz-search-index');\n if (!indexHref) return;\n\n var locale = String(doc.lang || '');\n var records = null;\n var loading = null;\n var active = -1;\n var current = [];\n\n function loadIndex() {\n if (records) return Promise.resolve(records);\n if (loading) return loading;\n loading = fetch(indexHref, { credentials: 'same-origin' })\n .then(function (res) {\n if (!res.ok) throw new Error('search index ' + res.status);\n return res.json();\n })\n .then(function (json) {\n records = (json && json.records ? json.records : []).filter(function (r) {\n return r && r.locale === locale && (r.kind === 'page' || r.kind === 'heading') && r.route;\n });\n return records;\n })\n .catch(function () {\n records = [];\n return records;\n });\n return loading;\n }\n\n function close() {\n list.hidden = true;\n input.setAttribute('aria-expanded', 'false');\n current = [];\n active = -1;\n }\n\n function open() {\n list.hidden = false;\n input.setAttribute('aria-expanded', 'true');\n }\n\n function render(query) {\n var q = String(query || '').trim().toLowerCase();\n current = [];\n active = -1;\n list.innerHTML = '';\n if (!q) {\n close();\n return;\n }\n loadIndex().then(function (all) {\n // Re-read the live query: a slow fetch must not paint stale hits.\n var live = String(input.value || '').trim().toLowerCase();\n if (live !== q) return;\n var seen = {};\n for (var i = 0; i < all.length && current.length < 8; i++) {\n var r = all[i];\n var hay = String(r.title || '') + ' ' + String(r.text || '');\n if (hay.toLowerCase().indexOf(live) < 0) continue;\n if (seen[r.route]) continue;\n seen[r.route] = true;\n current.push(r);\n }\n list.innerHTML = '';\n if (!current.length) {\n var empty = document.createElement('li');\n empty.className = 'doc-search__empty';\n empty.setAttribute('role', 'option');\n empty.setAttribute('aria-disabled', 'true');\n empty.textContent = copy.noResults;\n list.appendChild(empty);\n } else {\n current.forEach(function (r, i) {\n var li = document.createElement('li');\n li.setAttribute('role', 'option');\n li.id = 'vmz-doc-search-hit-' + i;\n li.setAttribute('aria-selected', 'false');\n var a = document.createElement('a');\n a.href = r.route;\n a.textContent = r.title || r.route;\n if (r.kind === 'heading') {\n var ctx = document.createElement('span');\n ctx.className = 'doc-search__ctx';\n ctx.textContent = '\u00A0\u00B7\u00A0' + (r.pageKey || '');\n a.appendChild(ctx);\n }\n li.appendChild(a);\n list.appendChild(li);\n });\n }\n open();\n });\n }\n\n function move(delta) {\n if (!current.length) return;\n active = (active + delta + current.length) % current.length;\n var rows = list.querySelectorAll('li[role=\"option\"]');\n rows.forEach(function (row, i) {\n row.setAttribute('aria-selected', i === active ? 'true' : 'false');\n });\n input.setAttribute('aria-activedescendant', 'vmz-doc-search-hit-' + active);\n }\n\n input.setAttribute('role', 'combobox');\n input.setAttribute('aria-controls', list.id || 'vmz-doc-search-results');\n list.id = list.id || 'vmz-doc-search-results';\n input.setAttribute('aria-expanded', 'false');\n input.setAttribute('placeholder', copy.search);\n list.setAttribute('aria-label', copy.results);\n if (hint) hint.textContent = copy.hintJs;\n\n input.addEventListener('input', function () {\n render(input.value);\n });\n input.addEventListener('focus', function () {\n loadIndex();\n });\n input.addEventListener('keydown', function (ev) {\n if (ev.key === 'ArrowDown') {\n ev.preventDefault();\n move(1);\n } else if (ev.key === 'ArrowUp') {\n ev.preventDefault();\n move(-1);\n } else if (ev.key === 'Enter') {\n if (active >= 0 && current[active]) {\n ev.preventDefault();\n window.location.assign(current[active].route);\n }\n } else if (ev.key === 'Escape') {\n input.value = '';\n close();\n }\n });\n document.addEventListener('click', function (ev) {\n if (!host.contains(ev.target)) close();\n });\n }\n\n /* Code copy ----------------------------------------------------------- */\n\n function enhanceCopy() {\n if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return;\n var blocks = document.querySelectorAll('.doc-content main pre');\n blocks.forEach(function (pre) {\n if (pre.querySelector('.doc-copy')) return;\n var btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'doc-copy';\n btn.textContent = copy.copyLabel;\n btn.setAttribute('aria-label', copy.copyLabel);\n btn.addEventListener('click', function () {\n var code = pre.querySelector('code');\n var text = code ? code.innerText : pre.innerText;\n navigator.clipboard.writeText(text).then(function () {\n btn.textContent = copy.copied;\n btn.classList.add('doc-copy--done');\n setTimeout(function () {\n btn.textContent = copy.copyLabel;\n btn.classList.remove('doc-copy--done');\n }, 1200);\n });\n });\n pre.appendChild(btn);\n });\n }\n\n ready(function () {\n try {\n var host = document.querySelector('[data-vmz-search-index]');\n if (host) enhanceSearch(host);\n enhanceCopy();\n } catch (err) {\n /* Enhancement must never take the readable page down. */\n }\n });\n})();\n";
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Framework-owned document client enhancement (`assets/vmz-document.js`).
3
+ *
4
+ * Progressive enhancement only: pages stay no-JS readable. The module wires
5
+ * the static DocumentSearch shell to `document.search.json` (locale-scoped
6
+ * substring filter, full keyboard loop) and injects copy buttons on article
7
+ * code blocks. No framework runtime, no full-page hydration.
8
+ *
9
+ * Emitted verbatim by `buildDocuments`; keep it dependency-free plain JS —
10
+ * this file is served as-authored, never bundled.
11
+ */
12
+ export const DOCUMENT_CLIENT_HREF = 'assets/vmz-document.js';
13
+ export const DOCUMENT_CLIENT_JS = `/* vmz document layer — progressive enhancement (no framework runtime). */
14
+ (function () {
15
+ 'use strict';
16
+
17
+ var doc = document.documentElement;
18
+ var zh = String(doc.lang || '').toLowerCase().indexOf('zh') === 0;
19
+ var copy = zh
20
+ ? { search: '搜索文档', noResults: '没有匹配结果', hintJs: '输入关键词过滤文档', copyLabel: '复制', copied: '已复制', results: '搜索结果' }
21
+ : { search: 'Search docs', noResults: 'No matching results', hintJs: 'Type to filter the docs', copyLabel: 'Copy', copied: 'Copied', results: 'Search results' };
22
+
23
+ function ready(fn) {
24
+ if (document.readyState === 'loading') {
25
+ document.addEventListener('DOMContentLoaded', fn, { once: true });
26
+ } else {
27
+ fn();
28
+ }
29
+ }
30
+
31
+ /* Search ------------------------------------------------------------ */
32
+
33
+ function enhanceSearch(host) {
34
+ var input = host.querySelector('.doc-search__input');
35
+ var list = host.querySelector('.doc-search__results');
36
+ var hint = host.querySelector('.doc-search__hint');
37
+ if (!input || !list) return;
38
+ var indexHref = host.getAttribute('data-vmz-search-index');
39
+ if (!indexHref) return;
40
+
41
+ var locale = String(doc.lang || '');
42
+ var records = null;
43
+ var loading = null;
44
+ var active = -1;
45
+ var current = [];
46
+
47
+ function loadIndex() {
48
+ if (records) return Promise.resolve(records);
49
+ if (loading) return loading;
50
+ loading = fetch(indexHref, { credentials: 'same-origin' })
51
+ .then(function (res) {
52
+ if (!res.ok) throw new Error('search index ' + res.status);
53
+ return res.json();
54
+ })
55
+ .then(function (json) {
56
+ records = (json && json.records ? json.records : []).filter(function (r) {
57
+ return r && r.locale === locale && (r.kind === 'page' || r.kind === 'heading') && r.route;
58
+ });
59
+ return records;
60
+ })
61
+ .catch(function () {
62
+ records = [];
63
+ return records;
64
+ });
65
+ return loading;
66
+ }
67
+
68
+ function close() {
69
+ list.hidden = true;
70
+ input.setAttribute('aria-expanded', 'false');
71
+ current = [];
72
+ active = -1;
73
+ }
74
+
75
+ function open() {
76
+ list.hidden = false;
77
+ input.setAttribute('aria-expanded', 'true');
78
+ }
79
+
80
+ function render(query) {
81
+ var q = String(query || '').trim().toLowerCase();
82
+ current = [];
83
+ active = -1;
84
+ list.innerHTML = '';
85
+ if (!q) {
86
+ close();
87
+ return;
88
+ }
89
+ loadIndex().then(function (all) {
90
+ // Re-read the live query: a slow fetch must not paint stale hits.
91
+ var live = String(input.value || '').trim().toLowerCase();
92
+ if (live !== q) return;
93
+ var seen = {};
94
+ for (var i = 0; i < all.length && current.length < 8; i++) {
95
+ var r = all[i];
96
+ var hay = String(r.title || '') + ' ' + String(r.text || '');
97
+ if (hay.toLowerCase().indexOf(live) < 0) continue;
98
+ if (seen[r.route]) continue;
99
+ seen[r.route] = true;
100
+ current.push(r);
101
+ }
102
+ list.innerHTML = '';
103
+ if (!current.length) {
104
+ var empty = document.createElement('li');
105
+ empty.className = 'doc-search__empty';
106
+ empty.setAttribute('role', 'option');
107
+ empty.setAttribute('aria-disabled', 'true');
108
+ empty.textContent = copy.noResults;
109
+ list.appendChild(empty);
110
+ } else {
111
+ current.forEach(function (r, i) {
112
+ var li = document.createElement('li');
113
+ li.setAttribute('role', 'option');
114
+ li.id = 'vmz-doc-search-hit-' + i;
115
+ li.setAttribute('aria-selected', 'false');
116
+ var a = document.createElement('a');
117
+ a.href = r.route;
118
+ a.textContent = r.title || r.route;
119
+ if (r.kind === 'heading') {
120
+ var ctx = document.createElement('span');
121
+ ctx.className = 'doc-search__ctx';
122
+ ctx.textContent = ' · ' + (r.pageKey || '');
123
+ a.appendChild(ctx);
124
+ }
125
+ li.appendChild(a);
126
+ list.appendChild(li);
127
+ });
128
+ }
129
+ open();
130
+ });
131
+ }
132
+
133
+ function move(delta) {
134
+ if (!current.length) return;
135
+ active = (active + delta + current.length) % current.length;
136
+ var rows = list.querySelectorAll('li[role="option"]');
137
+ rows.forEach(function (row, i) {
138
+ row.setAttribute('aria-selected', i === active ? 'true' : 'false');
139
+ });
140
+ input.setAttribute('aria-activedescendant', 'vmz-doc-search-hit-' + active);
141
+ }
142
+
143
+ input.setAttribute('role', 'combobox');
144
+ input.setAttribute('aria-controls', list.id || 'vmz-doc-search-results');
145
+ list.id = list.id || 'vmz-doc-search-results';
146
+ input.setAttribute('aria-expanded', 'false');
147
+ input.setAttribute('placeholder', copy.search);
148
+ list.setAttribute('aria-label', copy.results);
149
+ if (hint) hint.textContent = copy.hintJs;
150
+
151
+ input.addEventListener('input', function () {
152
+ render(input.value);
153
+ });
154
+ input.addEventListener('focus', function () {
155
+ loadIndex();
156
+ });
157
+ input.addEventListener('keydown', function (ev) {
158
+ if (ev.key === 'ArrowDown') {
159
+ ev.preventDefault();
160
+ move(1);
161
+ } else if (ev.key === 'ArrowUp') {
162
+ ev.preventDefault();
163
+ move(-1);
164
+ } else if (ev.key === 'Enter') {
165
+ if (active >= 0 && current[active]) {
166
+ ev.preventDefault();
167
+ window.location.assign(current[active].route);
168
+ }
169
+ } else if (ev.key === 'Escape') {
170
+ input.value = '';
171
+ close();
172
+ }
173
+ });
174
+ document.addEventListener('click', function (ev) {
175
+ if (!host.contains(ev.target)) close();
176
+ });
177
+ }
178
+
179
+ /* Code copy ----------------------------------------------------------- */
180
+
181
+ function enhanceCopy() {
182
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return;
183
+ var blocks = document.querySelectorAll('.doc-content main pre');
184
+ blocks.forEach(function (pre) {
185
+ if (pre.querySelector('.doc-copy')) return;
186
+ var btn = document.createElement('button');
187
+ btn.type = 'button';
188
+ btn.className = 'doc-copy';
189
+ btn.textContent = copy.copyLabel;
190
+ btn.setAttribute('aria-label', copy.copyLabel);
191
+ btn.addEventListener('click', function () {
192
+ var code = pre.querySelector('code');
193
+ var text = code ? code.innerText : pre.innerText;
194
+ navigator.clipboard.writeText(text).then(function () {
195
+ btn.textContent = copy.copied;
196
+ btn.classList.add('doc-copy--done');
197
+ setTimeout(function () {
198
+ btn.textContent = copy.copyLabel;
199
+ btn.classList.remove('doc-copy--done');
200
+ }, 1200);
201
+ });
202
+ });
203
+ pre.appendChild(btn);
204
+ });
205
+ }
206
+
207
+ ready(function () {
208
+ try {
209
+ var host = document.querySelector('[data-vmz-search-index]');
210
+ if (host) enhanceSearch(host);
211
+ enhanceCopy();
212
+ } catch (err) {
213
+ /* Enhancement must never take the readable page down. */
214
+ }
215
+ });
216
+ })();
217
+ `;
@@ -8,6 +8,7 @@ import { enrichDocumentContent } from './document-enrich.js';
8
8
  import { enrichDocumentEvidence } from './document-evidence.js';
9
9
  import { resolveMarkdownEngine } from './document-markdown.js';
10
10
  import { createWorkspace } from '../workspace/public-api.js';
11
+ import { loadVmzConfig } from '../workspace/plugin-host.js';
11
12
  import { log } from '../workspace/log.js';
12
13
  import { emitPrettyJson } from '../workspace/pretty-json.js';
13
14
  export function registerDocumentCommands(parent) {
@@ -62,7 +63,8 @@ async function cmdDocumentBuild(args) {
62
63
  const strict = Boolean(args.strict);
63
64
  let result;
64
65
  try {
65
- result = await buildDocuments({ projectRoot, outDir, strict });
66
+ const cfg = await loadVmzConfig(projectRoot).catch(() => null);
67
+ result = await buildDocuments({ projectRoot, outDir, strict, engines: cfg?.engines });
66
68
  }
67
69
  catch (e) {
68
70
  const msg = e instanceof Error ? e.message : String(e);
@@ -1,6 +1,12 @@
1
1
  export interface DocumentDesignsCssResult {
2
- css: string;
3
- source: string | null;
4
- href: string | null;
2
+ /** Site token stylesheet body (empty when the project has no compiled tokens). */
3
+ tokensCss: string;
4
+ tokensSource: string | null;
5
+ /** Site token stylesheet href (emitted name), null when no tokens exist. */
6
+ tokensHref: string | null;
7
+ /** Framework document base stylesheet href (always emitted). */
8
+ baseHref: string;
9
+ /** Combined source description for the manifest. */
10
+ source: string;
5
11
  }
6
12
  export declare function resolveDocumentDesignsCss(projectRoot: string, appDistDir?: string | null): DocumentDesignsCssResult;
@@ -1,53 +1,39 @@
1
1
  /**
2
- * Document — wire project `/designs` and the compiled app stylesheet into
3
- * static document assets.
2
+ * Document — wire the compiled app stylesheet and the framework-owned document
3
+ * base layer into static document assets.
4
4
  *
5
- * Layout chrome comes from `designs/document/chrome.css`. Theme tokens are not
6
- * re-derived here: they come from the app build output (`vmz-designs.css` /
7
- * `vmz.css` under `appDistDir`), which is the single compiled artifact.
5
+ * Layering (see 规划设计/vmz 04 文档层):
6
+ * - `assets/vmz-designs.css`: the site's compiled design tokens (`vmz-designs.css`
7
+ * / `vmz.css` from the app build, or `designs/styles/index.css` standalone).
8
+ * - `assets/vmz-document.css`: the framework document base (`DOCUMENT_BASE_CSS`),
9
+ * the single canonical layout/typography surface. Site `designs/document/*`
10
+ * is no longer an input — brand customization goes through `designs/tokens`.
8
11
  */
9
12
  import fs from 'node:fs';
10
13
  import path from 'node:path';
11
14
  export function resolveDocumentDesignsCss(projectRoot, appDistDir) {
12
- const designsDir = path.join(projectRoot, 'designs');
13
- if (!fs.existsSync(designsDir) || !fs.statSync(designsDir).isDirectory()) {
14
- return { css: '', source: null, href: null };
15
- }
16
- const parts = [];
17
- const sources = [];
18
- // Layout first so @import stays at stylesheet top; compiled tokens follow.
19
- const layoutCandidates = [
20
- path.join(designsDir, 'document', 'chrome.css'),
21
- path.join(designsDir, 'styles', 'document.css'),
22
- ];
23
- for (const p of layoutCandidates) {
24
- if (!fs.existsSync(p))
25
- continue;
26
- parts.push(fs.readFileSync(p, 'utf8'));
27
- sources.push(path.relative(projectRoot, p).replace(/\\/g, '/'));
28
- break;
29
- }
15
+ let tokensCss = '';
16
+ let tokensSource = null;
30
17
  if (appDistDir) {
31
18
  const compiled = [path.join(appDistDir, 'vmz-designs.css'), path.join(appDistDir, 'vmz.css')].find((p) => fs.existsSync(p));
32
19
  if (!compiled) {
33
20
  throw new Error(`vmz.document: app dist ${appDistDir} is missing vmz-designs.css / vmz.css; build the app before the documents`);
34
21
  }
35
- parts.push(fs.readFileSync(compiled, 'utf8'));
36
- sources.push(path.relative(projectRoot, compiled).replace(/\\/g, '/'));
22
+ tokensCss = fs.readFileSync(compiled, 'utf8');
23
+ tokensSource = path.relative(projectRoot, compiled).replace(/\\/g, '/');
37
24
  }
38
25
  else {
39
- const entry = path.join(designsDir, 'styles', 'index.css');
26
+ const entry = path.join(projectRoot, 'designs', 'styles', 'index.css');
40
27
  if (fs.existsSync(entry)) {
41
- parts.push(fs.readFileSync(entry, 'utf8'));
42
- sources.push(path.relative(projectRoot, entry).replace(/\\/g, '/'));
28
+ tokensCss = fs.readFileSync(entry, 'utf8');
29
+ tokensSource = 'designs/styles/index.css';
43
30
  }
44
31
  }
45
- if (!parts.length) {
46
- return { css: '', source: 'designs/', href: null };
47
- }
48
32
  return {
49
- css: parts.join('\n\n'),
50
- source: sources.join('+'),
51
- href: 'assets/vmz-designs.css',
33
+ tokensCss,
34
+ tokensSource,
35
+ tokensHref: tokensCss ? 'assets/vmz-designs.css' : null,
36
+ baseHref: 'assets/vmz-document.css',
37
+ source: ['vmz/document-base', tokensSource].filter(Boolean).join('+'),
52
38
  };
53
39
  }
@@ -50,3 +50,12 @@ export declare function pageRoute(routeBase: string, locale: string, pageKey: st
50
50
  /** Static file path relative to out dir (posix). */
51
51
  export declare function pageHtmlRel(routeBase: string, locale: string, pageKey: string): string;
52
52
  export declare function enrichDocumentContent(manifest: DocumentManifest, ctx: EnrichDocumentContentCtx): EnrichDocumentContentResult;
53
+ /**
54
+ * Author link → route-form href for emitted pages (document layer link form:
55
+ * `/d/<locale>/<pageKey>` without `.html`, matching application routes).
56
+ *
57
+ * Returns `null` for links that must stay as-authored: external/mailto, API
58
+ * refs, pure anchors, absolute application routes outside the document mount,
59
+ * and unresolvable relatives (the check pass reports those as broken).
60
+ */
61
+ export declare function routeHrefForDocLink(href: string, fromPageKey: string, locale: string, routeBase: string, pageKeySet: Map<string, Set<string>>, routing?: DocumentRoutingStrategy): string | null;