@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
@@ -1,11 +1,14 @@
1
1
  // @ts-nocheck
2
2
  /**
3
3
  * Build DocumentManifest + run / --strict checks.
4
+ *
5
+ * Author documents config is parsed by Rust DocumentRoutePlan; TS only
6
+ * merges filesystem scan (pageKeys) and coverage diagnostics.
4
7
  */
5
- import fs from 'node:fs';
6
8
  import path from 'node:path';
7
- import { DIAG, DOCUMENT_MANIFEST_SCHEMA } from './document-schema.js';
9
+ import { loadDocumentRoutePlan, mapPlanDiagnostics } from './author-input.js';
8
10
  import { scanDocumentsTree } from './document-scan.js';
11
+ import { DIAG, DOCUMENT_MANIFEST_SCHEMA } from './document-schema.js';
9
12
  /**
10
13
  * Resolve project documents root.
11
14
  * @param {string} projectRoot
@@ -14,59 +17,36 @@ export function resolveDocumentsRoot(projectRoot) {
14
17
  return path.resolve(projectRoot, 'documents');
15
18
  }
16
19
  /**
17
- * Parse documents.config.json or a JSON-compatible export-default .ts/.js.
18
- * @param {string} documentsRoot
19
- * @returns {{ config: Record<string, any> | null, diagnostics: any[], configPath: string | null }}
20
+ * Load normalized DocumentRoutePlan from Rust (author JSON5/JSON/declaration).
21
+ * @param {string} projectRoot
20
22
  */
21
- export function loadDocumentsConfig(documentsRoot) {
22
- /** @type {any[]} */
23
- const diagnostics = [];
24
- const candidates = ['documents.config.json', 'documents.config.ts', 'documents.config.js'];
25
- for (const name of candidates) {
26
- const p = path.join(documentsRoot, name);
27
- if (!fs.existsSync(p))
28
- continue;
29
- try {
30
- const raw = fs.readFileSync(p, 'utf8');
31
- const config = parseConfigSource(raw, name);
32
- return { config, diagnostics, configPath: p };
33
- }
34
- catch (e) {
35
- diagnostics.push({
36
- code: DIAG.CONFIG_INVALID,
37
- severity: 'error',
38
- message: `failed to parse ${name}: ${e instanceof Error ? e.message : String(e)}`,
39
- path: p,
40
- });
41
- return { config: null, diagnostics, configPath: p };
42
- }
43
- }
44
- return { config: null, diagnostics, configPath: null };
23
+ export function loadDocumentsRoutePlan(projectRoot) {
24
+ return loadDocumentRoutePlan(projectRoot);
45
25
  }
46
26
  /**
47
- *: only declaration objects no arbitrary hooks.
48
- * @param {string} raw
49
- * @param {string} filename
27
+ * @deprecated Prefer loadDocumentsRoutePlan(projectRoot). Kept for call sites that
28
+ * only need the plan-shaped config fields.
29
+ * @param {string} documentsRoot
50
30
  */
51
- function parseConfigSource(raw, filename) {
52
- if (filename.endsWith('.json')) {
53
- return JSON.parse(raw);
31
+ export function loadDocumentsConfig(documentsRoot) {
32
+ const projectRoot = path.dirname(documentsRoot);
33
+ const plan = loadDocumentRoutePlan(projectRoot);
34
+ const diagnostics = mapPlanDiagnostics(plan.diagnostics);
35
+ const configPath = plan.sourcePath ? path.join(projectRoot, plan.sourcePath) : null;
36
+ const missing = diagnostics.some((d) => d.code === DIAG.CONFIG_MISSING);
37
+ if (missing) {
38
+ return { config: null, diagnostics, configPath, plan };
54
39
  }
55
- // Strip line/block comments, then require `export default { ... }`
56
- let s = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
57
- s = s.trim();
58
- const m = s.match(/^export\s+default\s+([\s\S]*?);?\s*$/);
59
- if (!m) {
60
- throw new Error('expected `export default { ... }` (JSON-compatible declaration only)');
40
+ /** @type {Record<string, any>} */
41
+ const config = {
42
+ defaultLocale: plan.defaultLocale ?? undefined,
43
+ locales: Object.fromEntries(Object.entries(plan.localeLabels || {}).map(([id, label]) => [id, { label }])),
44
+ collections: Object.fromEntries((plan.collections || []).map((c) => [c.id, { source: c.sourceRoot, mount: c.routeBase }])),
45
+ };
46
+ if (plan.silentFallbackRequested) {
47
+ config.fallback = true;
61
48
  }
62
- let body = m[1].trim();
63
- // Quote bare keys: { defaultLocale: "x" } → { "defaultLocale": "x" }
64
- body = body.replace(/([,{]\s*)([A-Za-z_][A-Za-z0-9_]*)\s*:/g, '$1"$2":');
65
- // Single-quoted strings → JSON double-quoted
66
- body = body.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, (_, inner) => JSON.stringify(inner.replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\\\/g, '\\')));
67
- // Trailing commas
68
- body = body.replace(/,\s*([}\]])/g, '$1');
69
- return JSON.parse(body);
49
+ return { config, diagnostics, configPath, plan };
70
50
  }
71
51
  /**
72
52
  * @param {object} opts
@@ -82,61 +62,53 @@ export function checkDocuments(opts) {
82
62
  const diagnostics = [];
83
63
  const scanned = scanDocumentsTree(documentsRoot);
84
64
  diagnostics.push(...scanned.diagnostics);
85
- const { config, diagnostics: cfgDiags, configPath } = loadDocumentsConfig(documentsRoot);
86
- diagnostics.push(...cfgDiags);
87
- if (!config) {
88
- diagnostics.push({
89
- code: DIAG.CONFIG_MISSING,
90
- severity: strict ? 'error' : 'warning',
91
- message: 'documents.config.json|ts missing; strict mode requires defaultLocale + locales for strict coverage checks',
92
- path: documentsRoot,
93
- });
65
+ const plan = loadDocumentRoutePlan(projectRoot);
66
+ diagnostics.push(...mapPlanDiagnostics(plan.diagnostics));
67
+ const configMissing = (plan.diagnostics || []).some((d) => d.code === DIAG.CONFIG_MISSING);
68
+ const configPath = plan.sourcePath ? path.join(projectRoot, plan.sourcePath) : null;
69
+ if (configMissing && strict) {
70
+ // Plan already emitted warning; elevate message under --strict if needed.
71
+ if (!diagnostics.some((d) => d.code === DIAG.CONFIG_MISSING && d.severity === 'error')) {
72
+ diagnostics.push({
73
+ code: DIAG.CONFIG_MISSING,
74
+ severity: 'error',
75
+ message: 'documents.config.json|json5|ts|js missing; strict mode requires defaultLocale + locales for strict coverage checks',
76
+ path: documentsRoot,
77
+ });
78
+ }
94
79
  }
95
80
  /** @type {Record<string, string>} */
96
- const localeLabels = {};
97
- let defaultLocale = null;
81
+ const localeLabels = { ...(plan.localeLabels || {}) };
82
+ const defaultLocale = plan.defaultLocale || null;
98
83
  /** @type {import('./document-schema.js').DocumentCollection[]} */
99
84
  const collections = [];
100
85
  /** @type {import('./document-schema.js').DocumentMount[]} */
101
86
  const mounts = [];
102
- if (config && typeof config === 'object') {
103
- if (typeof config.defaultLocale === 'string') {
104
- defaultLocale = config.defaultLocale;
105
- }
106
- if (config.locales && typeof config.locales === 'object') {
107
- for (const [k, v] of Object.entries(config.locales)) {
108
- localeLabels[k] = v && typeof v === 'object' && typeof v.label === 'string' ? v.label : k;
109
- }
110
- }
111
- if (config.collections && typeof config.collections === 'object') {
112
- for (const [id, c] of Object.entries(config.collections)) {
113
- const sourceRoot = c && typeof c === 'object' && typeof c.source === 'string' ? c.source : '.';
114
- const routeBase = c && typeof c === 'object' && typeof c.mount === 'string' ? c.mount : '/docs';
115
- const pageKeys = [
116
- ...new Set(scanned.pages
117
- .filter((p) => {
118
- if (sourceRoot === '.' || sourceRoot === './')
119
- return true;
120
- const prefix = sourceRoot.replace(/^\.\//, '').replace(/\/$/, '');
121
- return p.identity.pageKey === prefix || p.identity.pageKey.startsWith(prefix + '/');
122
- })
123
- .map((p) => p.identity.pageKey)),
124
- ].sort();
125
- collections.push({ id, sourceRoot, pageKeys });
126
- mounts.push({
127
- collectionId: id,
128
- routeBase,
129
- mode: routeBase === '/' ? 'standalone' : 'integrated',
130
- });
131
- }
132
- }
87
+ for (const c of plan.collections || []) {
88
+ const sourceRoot = c.sourceRoot || '.';
89
+ const routeBase = c.routeBase || '/docs';
90
+ const pageKeys = [
91
+ ...new Set(scanned.pages
92
+ .filter((p) => {
93
+ if (sourceRoot === '.' || sourceRoot === './')
94
+ return true;
95
+ const prefix = sourceRoot.replace(/^\.\//, '').replace(/\/$/, '');
96
+ return p.identity.pageKey === prefix || p.identity.pageKey.startsWith(`${prefix}/`);
97
+ })
98
+ .map((p) => p.identity.pageKey)),
99
+ ].sort();
100
+ collections.push({ id: c.id, sourceRoot, pageKeys });
101
+ mounts.push({
102
+ collectionId: c.id,
103
+ routeBase,
104
+ mode: routeBase === '/' ? 'standalone' : 'integrated',
105
+ });
133
106
  }
134
107
  if (collections.length === 0) {
135
108
  const pageKeys = [...new Set(scanned.pages.map((p) => p.identity.pageKey))].sort();
136
109
  collections.push({ id: 'default', sourceRoot: '.', pageKeys });
137
110
  mounts.push({ collectionId: 'default', routeBase: '/docs', mode: 'integrated' });
138
111
  }
139
- // Config locales vs disk
140
112
  const diskLocales = new Set(scanned.locales);
141
113
  const configLocales = Object.keys(localeLabels);
142
114
  if (defaultLocale) {
@@ -175,15 +147,6 @@ export function checkDocuments(opts) {
175
147
  });
176
148
  }
177
149
  }
178
- // no silent whole-page fallback by default
179
- if (config && config.fallback === true) {
180
- diagnostics.push({
181
- code: DIAG.FALLBACK_SILENT,
182
- severity: 'error',
183
- message: 'silent whole-page fallback is forbidden; allow only explicit nav/metadata or per-page fallback',
184
- path: configPath || documentsRoot,
185
- });
186
- }
187
150
  // Coverage: default locale PageKeys are baseline; other locales missing/orphan under --strict
188
151
  if (defaultLocale && diskLocales.has(defaultLocale)) {
189
152
  const byLocale = new Map();
@@ -2,7 +2,6 @@
2
2
  /**
3
3
  * `vmz document` / `vmz docs` CLI .
4
4
  */
5
- import fs from 'node:fs';
6
5
  import path from 'node:path';
7
6
  import { buildDocuments } from './document-build.js';
8
7
  import { checkDocuments, manifestHasErrors } from './document-check.js';
@@ -12,6 +11,7 @@ import { resolveMarkdownEngine } from './document-markdown.js';
12
11
  import { createWorkspace } from './index.js';
13
12
  import { log } from './log.js';
14
13
  import { parseArgs } from './cli.js';
14
+ import { emitPrettyJson } from './pretty-json.js';
15
15
  function printDocumentHelp() {
16
16
  console.log(`vmz document — project /documents domain
17
17
 
@@ -95,14 +95,7 @@ async function cmdDocumentCheck(args) {
95
95
  }
96
96
  const jsonOut = args.json;
97
97
  if (jsonOut) {
98
- const text = JSON.stringify(manifest, null, 2);
99
- if (typeof jsonOut === 'string') {
100
- fs.writeFileSync(jsonOut, text + '\n', 'utf8');
101
- log.info(`wrote ${jsonOut}`);
102
- }
103
- else {
104
- console.log(text);
105
- }
98
+ emitPrettyJson(jsonOut, manifest, { logWrote: (p) => log.info(`wrote ${p}`) });
106
99
  }
107
100
  else {
108
101
  for (const d of manifest.diagnostics) {
@@ -2,8 +2,9 @@
2
2
  * @param {string} routeBase e.g. /docs
3
3
  * @param {string} locale
4
4
  * @param {string} pageKey
5
+ * @param {{ strategy?: string }} [routing]
5
6
  */
6
- export declare function pageRoute(routeBase: any, locale: any, pageKey: any): string;
7
+ export declare function pageRoute(routeBase: any, locale: any, pageKey: any, routing?: {}): string;
7
8
  /**
8
9
  * Static file path relative to out dir (posix).
9
10
  * @param {string} routeBase
@@ -9,10 +9,16 @@ import { DIAG } from './document-schema.js';
9
9
  * @param {string} routeBase e.g. /docs
10
10
  * @param {string} locale
11
11
  * @param {string} pageKey
12
+ * @param {{ strategy?: string }} [routing]
12
13
  */
13
- export function pageRoute(routeBase, locale, pageKey) {
14
+ export function pageRoute(routeBase, locale, pageKey, routing = {}) {
14
15
  const base = String(routeBase || '/').replace(/\/$/, '') || '';
15
16
  const key = pageKey === 'index' ? '' : pageKey.replace(/\\/g, '/');
17
+ const strategy = routing.strategy || 'prefix';
18
+ if (strategy === 'none' || strategy === 'domain') {
19
+ const parts = [base.replace(/^\//, ''), key].filter((p) => p !== '');
20
+ return '/' + (parts.length ? parts.join('/') : '');
21
+ }
16
22
  const parts = [base.replace(/^\//, ''), locale, key].filter((p) => p !== '');
17
23
  return '/' + parts.join('/');
18
24
  }
@@ -35,6 +41,7 @@ export function pageHtmlRel(routeBase, locale, pageKey) {
35
41
  */
36
42
  export function enrichDocumentContent(manifest, ctx) {
37
43
  const routeBase = manifest.mounts?.[0]?.routeBase || '/docs';
44
+ const routing = ctx.routing || { strategy: 'prefix' };
38
45
  /** @type {Map<string, { html: string, headings: any[], links: any[], title: string, route: string, anchors: string[] }>} */
39
46
  const byId = new Map();
40
47
  /** @type {import('./document-schema.js').DocumentDiagnostic[]} */
@@ -45,7 +52,7 @@ export function enrichDocumentContent(manifest, ctx) {
45
52
  const abs = path.isAbsolute(page.sourcePath) ? page.sourcePath : path.join(manifest.root, page.sourcePath);
46
53
  const source = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : '';
47
54
  const analyzed = ctx.analyzeMarkdown(source);
48
- const route = pageRoute(routeBase, page.identity.locale, page.identity.pageKey);
55
+ const route = pageRoute(routeBase, page.identity.locale, page.identity.pageKey, routing);
49
56
  const anchors = analyzed.headings.map((h) => h.id);
50
57
  const title = analyzed.headings.find((h) => h.level === 1)?.text || analyzed.headings[0]?.text || page.identity.pageKey;
51
58
  // Duplicate anchors on page
@@ -61,7 +68,11 @@ export function enrichDocumentContent(manifest, ctx) {
61
68
  }
62
69
  seen.add(id);
63
70
  }
64
- if (routeOwners.has(route)) {
71
+ const owner = `${page.identity.locale}:${page.identity.pageKey}`;
72
+ if (routing.strategy === 'none' || routing.strategy === 'domain') {
73
+ routeOwners.set(route, owner);
74
+ }
75
+ else if (routeOwners.has(route)) {
65
76
  diagnostics.push({
66
77
  code: DIAG.ROUTE_DUPLICATE,
67
78
  severity: 'error',
@@ -70,7 +81,7 @@ export function enrichDocumentContent(manifest, ctx) {
70
81
  });
71
82
  }
72
83
  else {
73
- routeOwners.set(route, `${page.identity.locale}:${page.identity.pageKey}`);
84
+ routeOwners.set(route, owner);
74
85
  }
75
86
  page.route = route;
76
87
  page.anchors = anchors;
@@ -7,7 +7,10 @@ import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { buildDocuments } from './document-build.js';
9
9
  import { resolveDocumentsRoot } from './document-check.js';
10
+ import { loadLocalesRouting } from './document-routing-config.js';
11
+ import { pageHtmlRel } from './document-enrich.js';
10
12
  import { log } from './log.js';
13
+ import { requireNativeAddon } from './native-addon.js';
11
14
  /**
12
15
  * @param {string} projectRoot
13
16
  */
@@ -30,6 +33,7 @@ export async function buildIntegratedDocuments(opts) {
30
33
  const result = await buildDocuments({
31
34
  projectRoot,
32
35
  outDir,
36
+ appDistDir: outDir,
33
37
  strict: Boolean(opts.strict),
34
38
  });
35
39
  if (!result.ok) {
@@ -39,7 +43,7 @@ export async function buildIntegratedDocuments(opts) {
39
43
  }
40
44
  return { ok: false, error: 'document diagnostics', pages: 0 };
41
45
  }
42
- writeMountRootRedirects(result.manifest, outDir);
46
+ writeMountRootRedirects(result.manifest, outDir, projectRoot);
43
47
  log.info(`document mount: pages=${result.pages.length} → ${path.relative(process.cwd(), outDir) || '.'}`);
44
48
  return { ok: true, pages: result.pages.length };
45
49
  }
@@ -50,39 +54,43 @@ export async function buildIntegratedDocuments(opts) {
50
54
  }
51
55
  }
52
56
  /**
53
- * Emit `{routeBase}/index.html` → defaultLocale landing (for /d/ and /docs/).
57
+ * Emit `{routeBase}/index.html` for integrated mounts.
58
+ * `routing.strategy: none` → copy default-locale docs index (LocaleId is Host state).
59
+ * prefix strategy → redirect HTML to `{routeBase}/{defaultLocale}/`.
54
60
  * @param {import('./document-schema.js').DocumentManifest} manifest
55
61
  * @param {string} outDir
62
+ * @param {string} projectRoot
56
63
  */
57
- function writeMountRootRedirects(manifest, outDir) {
64
+ function writeMountRootRedirects(manifest, outDir, projectRoot) {
58
65
  const defaultLocale = manifest.defaultLocale || manifest.locales?.[0];
59
66
  if (!defaultLocale)
60
67
  return;
68
+ const routing = loadLocalesRouting(projectRoot) || { strategy: 'prefix' };
61
69
  for (const mount of manifest.mounts || []) {
62
70
  if (!mount?.routeBase || mount.routeBase === '/')
63
71
  continue;
64
72
  const base = String(mount.routeBase).replace(/\/$/, '');
65
- const target = `${base}/${defaultLocale}/`;
66
73
  const relDir = base.replace(/^\//, '');
67
74
  const abs = path.join(outDir, relDir, 'index.html');
68
75
  fs.mkdirSync(path.dirname(abs), { recursive: true });
69
- const html = `<!DOCTYPE html>
70
- <html lang="${escapeAttr(defaultLocale)}">
71
- <head>
72
- <meta charset="utf-8" />
73
- <meta http-equiv="refresh" content="0;url=${escapeAttr(target)}" />
74
- <link rel="canonical" href="${escapeAttr(target)}" />
75
- <title>Documents</title>
76
- </head>
77
- <body>
78
- <p><a href="${escapeAttr(target)}">Continue to ${escapeAttr(defaultLocale)} docs</a></p>
79
- </body>
80
- </html>
81
- `;
76
+ if (routing.strategy === 'none' || routing.strategy === 'domain') {
77
+ const srcRel = pageHtmlRel(base, defaultLocale, 'index');
78
+ const srcAbs = path.join(outDir, srcRel);
79
+ if (fs.existsSync(srcAbs)) {
80
+ fs.copyFileSync(srcAbs, abs);
81
+ }
82
+ continue;
83
+ }
84
+ const target = `${base}/${defaultLocale}/`;
85
+ const native = requireNativeAddon();
86
+ if (typeof native.generateRedirectHtml !== 'function') {
87
+ throw new Error('vmz native addon missing generateRedirectHtml — rebuild with `pnpm napi:build`');
88
+ }
89
+ const html = native.generateRedirectHtml({
90
+ lang: defaultLocale,
91
+ target,
92
+ title: 'Documents',
93
+ });
82
94
  fs.writeFileSync(abs, html, 'utf8');
83
95
  }
84
96
  }
85
- /** @param {string} s */
86
- function escapeAttr(s) {
87
- return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
88
- }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Integrated DocumentMount — compile host chrome via DocumentLayout + createRenderHost.
3
+ * Replaces the removed regex template lowering in document-host-chrome.ts.
4
+ */
5
+ /** @param {string} distDir */
6
+ export declare function resolveDocumentLayoutChunkId(distDir: any): string;
7
+ /**
8
+ * @param {string} distDir
9
+ */
10
+ export declare function assertIntegratedDistReady(distDir: any): string;
11
+ /**
12
+ * @param {string} html
13
+ */
14
+ export declare function assertCompiledShellHtml(html: any): void;
15
+ /**
16
+ * @param {string} distDir
17
+ * @param {string} localeId
18
+ * @param {string} slotHtml
19
+ */
20
+ export declare function renderCompiledDocumentLayout(distDir: any, localeId: any, slotHtml: any): Promise<any>;
@@ -0,0 +1,87 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Integrated DocumentMount — compile host chrome via DocumentLayout + createRenderHost.
4
+ * Replaces the removed regex template lowering in document-host-chrome.ts.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
9
+ import { createRenderHost } from '@vmz/core/render-host';
10
+ /** @param {string} distDir */
11
+ export function resolveDocumentLayoutChunkId(distDir) {
12
+ for (const chunkId of ['layouts/DocumentLayout', 'components/DocumentLayout']) {
13
+ if (fs.existsSync(path.join(distDir, `${chunkId}.client.js`)))
14
+ return chunkId;
15
+ }
16
+ return null;
17
+ }
18
+ /**
19
+ * @param {string} distDir
20
+ */
21
+ export function assertIntegratedDistReady(distDir) {
22
+ const dom = path.join(distDir, 'vmz-dom.js');
23
+ if (!fs.existsSync(dom)) {
24
+ throw new Error('integrated document mount requires vmz build output (vmz-dom.js in app dist). Run `vmz build` before document emit.');
25
+ }
26
+ const chunkId = resolveDocumentLayoutChunkId(distDir);
27
+ if (!chunkId) {
28
+ throw new Error('integrated document mount requires compiled DocumentLayout (add src/layouts/DocumentLayout.vmz and rebuild)');
29
+ }
30
+ return chunkId;
31
+ }
32
+ /**
33
+ * @param {string} html
34
+ */
35
+ export function assertCompiledShellHtml(html) {
36
+ const header = html.match(/<header[^>]*data-vmz-fixture="site-header"[\s\S]*?<\/header>/i)?.[0] ?? '';
37
+ const footer = html.match(/<footer[^>]*data-vmz-fixture="site-footer"[\s\S]*?<\/footer>/i)?.[0] ?? '';
38
+ for (const part of [header, footer]) {
39
+ const leak = part.match(/\{[A-Za-z][A-Za-z0-9]*\}/);
40
+ if (leak) {
41
+ throw new Error(`document layout SSR leaked binding placeholder ${leak[0]}`);
42
+ }
43
+ if (/<(?:Link|Button|Icon)\b/.test(part)) {
44
+ throw new Error('document layout SSR leaked uncompiled VMZ component tag in chrome');
45
+ }
46
+ }
47
+ if (!header || !footer) {
48
+ throw new Error('document layout SSR missing compiled SiteHeader/SiteFooter fixtures');
49
+ }
50
+ }
51
+ /**
52
+ * @param {string} distDir
53
+ * @param {string} chunkId
54
+ */
55
+ async function loadCtor(distDir, chunkId) {
56
+ const href = pathToFileURL(path.join(distDir, `${chunkId}.client.js`)).href;
57
+ const mod = await import(`${href}?t=${Date.now()}`);
58
+ return mod.default;
59
+ }
60
+ /**
61
+ * @param {string} distDir
62
+ * @param {string} localeId
63
+ * @param {string} slotHtml
64
+ */
65
+ export async function renderCompiledDocumentLayout(distDir, localeId, slotHtml) {
66
+ const chunkId = assertIntegratedDistReady(distDir);
67
+ const prevHint = globalThis.__vmzLocaleIdHint;
68
+ globalThis.__vmzLocaleIdHint = localeId;
69
+ try {
70
+ if (typeof globalThis.document !== 'undefined' && globalThis.document?.documentElement) {
71
+ globalThis.document.documentElement.setAttribute('data-locale', localeId);
72
+ globalThis.document.documentElement.setAttribute('lang', localeId);
73
+ }
74
+ const host = await createRenderHost(distDir, { strictDeployment: true, preload: 'none' });
75
+ await host.ensureComponents([chunkId]);
76
+ const Layout = await loadCtor(distDir, chunkId);
77
+ const html = await host.renderToString(Layout, {}, { slotHtml });
78
+ assertCompiledShellHtml(html);
79
+ return html;
80
+ }
81
+ finally {
82
+ if (prevHint === undefined)
83
+ delete globalThis.__vmzLocaleIdHint;
84
+ else
85
+ globalThis.__vmzLocaleIdHint = prevHint;
86
+ }
87
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Project locale routing — consume Rust LocalePlan (no author JSON5 in TS).
3
+ */
4
+ /**
5
+ * @param {string} projectRoot
6
+ * @returns {{ strategy?: string, defaultPrefix?: string, defaultLocale?: string } | null}
7
+ */
8
+ export declare function loadLocalesRouting(projectRoot: any): {
9
+ strategy: any;
10
+ defaultPrefix: any;
11
+ defaultLocale: any;
12
+ };
13
+ /**
14
+ * @param {string} routeBase
15
+ * @param {string} pageKey
16
+ */
17
+ export declare function docsRouteNone(routeBase: any, pageKey: any): string;
@@ -0,0 +1,31 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Project locale routing — consume Rust LocalePlan (no author JSON5 in TS).
4
+ */
5
+ import { loadLocalePlan } from './author-input.js';
6
+ /**
7
+ * @param {string} projectRoot
8
+ * @returns {{ strategy?: string, defaultPrefix?: string, defaultLocale?: string } | null}
9
+ */
10
+ export function loadLocalesRouting(projectRoot) {
11
+ const plan = loadLocalePlan(projectRoot);
12
+ if (!plan || plan.diagnostics?.some((d) => d.code === 'vmz::locale::manifest_missing')) {
13
+ return null;
14
+ }
15
+ const routing = plan.routing || {};
16
+ return {
17
+ strategy: routing.strategy || 'prefix',
18
+ defaultPrefix: routing.defaultPrefix || 'include',
19
+ defaultLocale: plan.defaultLocale || undefined,
20
+ };
21
+ }
22
+ /**
23
+ * @param {string} routeBase
24
+ * @param {string} pageKey
25
+ */
26
+ export function docsRouteNone(routeBase, pageKey) {
27
+ const base = String(routeBase || '/').replace(/\/$/, '') || '';
28
+ const key = pageKey === 'index' ? '' : pageKey.replace(/\\/g, '/');
29
+ const parts = [base.replace(/^\//, ''), key].filter((p) => p !== '');
30
+ return `/${parts.length ? parts.join('/') : ''}`;
31
+ }
@@ -48,7 +48,14 @@ export const DOCUMENT_SEARCH_SCHEMA = 'vmz.document.search.v0';
48
48
  /** Island-only resume plan for search/playground — not a Doc IR. */
49
49
  export const DOCUMENT_ISLANDS_SCHEMA = 'vmz.document.islands.v0';
50
50
  /** Top-level non-locale reserved names under /documents */
51
- export const DOCUMENT_RESERVED_TOP = new Set(['package.json', 'documents.config.ts', 'documents.config.json', 'documents.config.js', 'public']);
51
+ export const DOCUMENT_RESERVED_TOP = new Set([
52
+ 'package.json',
53
+ 'documents.config.ts',
54
+ 'documents.config.json',
55
+ 'documents.config.json5',
56
+ 'documents.config.js',
57
+ 'public',
58
+ ]);
52
59
  /** Known locale aliases → canonical key (lowercase, hyphen). */
53
60
  export const LOCALE_ALIASES = {
54
61
  'zh-cn': 'zh-hans',
@@ -6,6 +6,7 @@
6
6
  import crypto from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
+ import { writePrettyJsonFile } from './pretty-json.js';
9
10
  export const EMBEDDED_RESOURCE_INDEX_SCHEMA = 'vmz.embedded.resource_index.v0';
10
11
  /** Paths that must never enter the embedded baseline closure. */
11
12
  const SKIP_NAMES = new Set(['vmz-serve-host.mjs', 'vmz-serve-host.js', 'node_modules', '.git']);
@@ -55,7 +56,7 @@ export function emitEmbeddedPackaging(outDir, opts = {}) {
55
56
  };
56
57
  index.indexDigest = sha256Hex(canonicalJson({ ...index, indexDigest: undefined }));
57
58
  const indexPath = path.join(vmzDir, 'embedded-resource-index.json');
58
- fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`, 'utf8');
59
+ writePrettyJsonFile(indexPath, index);
59
60
  // Optional include_bytes entry point for Rust packaging adapters
60
61
  const rsPath = path.join(vmzDir, 'embedded_site.rs');
61
62
  fs.writeFileSync(rsPath, `// @generated by vmz — rust-embedded packaging adapter (do not edit)