@vmz/vmz 0.0.0 → 0.0.2

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 (75) hide show
  1. package/README.md +48 -2
  2. package/bin/vmz.js +4 -0
  3. package/dist/application-cmd.d.ts +22 -0
  4. package/dist/application-cmd.js +348 -0
  5. package/dist/bundler-adapter.d.ts +64 -0
  6. package/dist/bundler-adapter.js +111 -0
  7. package/dist/cli.d.ts +15 -0
  8. package/dist/cli.js +370 -0
  9. package/dist/dev-session.d.ts +35 -0
  10. package/dist/dev-session.js +290 -0
  11. package/dist/document-build.d.ts +99 -0
  12. package/dist/document-build.js +274 -0
  13. package/dist/document-check.d.ts +44 -0
  14. package/dist/document-check.js +246 -0
  15. package/dist/document-cmd.d.ts +9 -0
  16. package/dist/document-cmd.js +147 -0
  17. package/dist/document-designs.d.ts +9 -0
  18. package/dist/document-designs.js +126 -0
  19. package/dist/document-enrich.d.ts +23 -0
  20. package/dist/document-enrich.js +234 -0
  21. package/dist/document-evidence.d.ts +49 -0
  22. package/dist/document-evidence.js +501 -0
  23. package/dist/document-integrate.d.ts +35 -0
  24. package/dist/document-integrate.js +89 -0
  25. package/dist/document-interactive.d.ts +69 -0
  26. package/dist/document-interactive.js +255 -0
  27. package/dist/document-locale.d.ts +31 -0
  28. package/dist/document-locale.js +59 -0
  29. package/dist/document-markdown.d.ts +13 -0
  30. package/dist/document-markdown.js +39 -0
  31. package/dist/document-scan.d.ts +21 -0
  32. package/dist/document-scan.js +151 -0
  33. package/dist/document-schema.d.ts +87 -0
  34. package/dist/document-schema.js +88 -0
  35. package/dist/explain-cmd.d.ts +5 -0
  36. package/dist/explain-cmd.js +123 -0
  37. package/dist/index.d.ts +808 -0
  38. package/dist/index.js +569 -0
  39. package/dist/locale-check.d.ts +106 -0
  40. package/dist/locale-check.js +737 -0
  41. package/dist/locale-cmd.d.ts +5 -0
  42. package/dist/locale-cmd.js +443 -0
  43. package/dist/locale-delivery.d.ts +298 -0
  44. package/dist/locale-delivery.js +444 -0
  45. package/dist/locale-router.d.ts +207 -0
  46. package/dist/locale-router.js +508 -0
  47. package/dist/locale-runtime.d.ts +406 -0
  48. package/dist/locale-runtime.js +542 -0
  49. package/dist/locale-schema.d.ts +9 -0
  50. package/dist/locale-schema.js +10 -0
  51. package/dist/locale-tooling.d.ts +118 -0
  52. package/dist/locale-tooling.js +358 -0
  53. package/dist/log.d.ts +19 -0
  54. package/dist/log.js +42 -0
  55. package/dist/packages.d.ts +27 -0
  56. package/dist/packages.js +147 -0
  57. package/dist/plugin-host.d.ts +30 -0
  58. package/dist/plugin-host.js +370 -0
  59. package/dist/refactor-cmd.d.ts +8 -0
  60. package/dist/refactor-cmd.js +156 -0
  61. package/dist/resolve.d.ts +25 -0
  62. package/dist/resolve.js +56 -0
  63. package/dist/test-cmd.d.ts +9 -0
  64. package/dist/test-cmd.js +343 -0
  65. package/dist/test-compile.d.ts +2 -0
  66. package/dist/test-compile.js +3 -0
  67. package/dist/test-discover.d.ts +2 -0
  68. package/dist/test-discover.js +3 -0
  69. package/dist/test-logic.d.ts +2 -0
  70. package/dist/test-logic.js +3 -0
  71. package/dist/test-protocol.d.ts +2 -0
  72. package/dist/test-protocol.js +3 -0
  73. package/dist/watch-diff.d.ts +17 -0
  74. package/dist/watch-diff.js +56 -0
  75. package/package.json +86 -4
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @param {{ projectRoot: string, outDir?: string, strict?: boolean, engines?: { markdown?: string } }} opts
3
+ */
4
+ export declare function buildDocuments(opts: any): Promise<{
5
+ ok: boolean;
6
+ manifest: {
7
+ schema: string;
8
+ root: string;
9
+ defaultLocale: any;
10
+ locales: any[];
11
+ localeLabels: {};
12
+ collections: any[];
13
+ mounts: any[];
14
+ pages: {
15
+ identity: any;
16
+ sourcePath: any;
17
+ route: any;
18
+ anchors: any[];
19
+ }[];
20
+ diagnostics: any[];
21
+ };
22
+ outDir: string;
23
+ pages: any[];
24
+ search?: undefined;
25
+ islands?: undefined;
26
+ } | {
27
+ ok: boolean;
28
+ manifest: {
29
+ schema: string;
30
+ evidence: {
31
+ schema: string;
32
+ fences: any[];
33
+ apiRefs: any[];
34
+ testSelections: any[];
35
+ status: string;
36
+ };
37
+ search: {
38
+ schema: string;
39
+ status: string;
40
+ version: any;
41
+ records: any[];
42
+ };
43
+ islands: {
44
+ schema: string;
45
+ hydrate: string;
46
+ fullPageHydrate: boolean;
47
+ islands: {
48
+ name: string;
49
+ kind: string;
50
+ resume: string;
51
+ index: any;
52
+ }[];
53
+ status: string;
54
+ };
55
+ build: {
56
+ engine: any;
57
+ outDir: string;
58
+ designs: string;
59
+ designsCss: any;
60
+ pages: any[];
61
+ evidence: string;
62
+ search: string;
63
+ islands: string;
64
+ };
65
+ root: string;
66
+ defaultLocale: any;
67
+ locales: any[];
68
+ localeLabels: {};
69
+ collections: any[];
70
+ mounts: any[];
71
+ pages: {
72
+ identity: any;
73
+ sourcePath: any;
74
+ route: any;
75
+ anchors: any[];
76
+ }[];
77
+ diagnostics: any[];
78
+ };
79
+ outDir: string;
80
+ pages: any[];
81
+ search: {
82
+ schema: string;
83
+ status: string;
84
+ version: any;
85
+ records: any[];
86
+ };
87
+ islands: {
88
+ schema: string;
89
+ hydrate: string;
90
+ fullPageHydrate: boolean;
91
+ islands: {
92
+ name: string;
93
+ kind: string;
94
+ resume: string;
95
+ index: any;
96
+ }[];
97
+ status: string;
98
+ };
99
+ }>;
@@ -0,0 +1,274 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Document D1 Static + D3 Interactive artifacts.
4
+ * Design: 规划设计/vmz/19 §5–6 · D1/D3
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { checkDocuments, manifestHasErrors } from './document-check.js';
9
+ import { resolveDocumentDesignsCss } from './document-designs.js';
10
+ import { enrichDocumentContent, pageHtmlRel } from './document-enrich.js';
11
+ import { enrichDocumentEvidence } from './document-evidence.js';
12
+ import { artifactHrefFromHtml, buildDocumentIslands, buildDocumentSearch, collectFenceBodies, renderIslandShellsHtml, } from './document-interactive.js';
13
+ import { resolveMarkdownEngine } from './document-markdown.js';
14
+ import { DOCUMENT_VIEW_SCHEMA } from './document-schema.js';
15
+ import { createWorkspace } from './index.js';
16
+ /**
17
+ * @param {{ projectRoot: string, outDir?: string, strict?: boolean, engines?: { markdown?: string } }} opts
18
+ */
19
+ export async function buildDocuments(opts) {
20
+ const projectRoot = path.resolve(opts.projectRoot);
21
+ const outDir = path.resolve(opts.outDir || path.join(projectRoot, 'dist', 'documents'));
22
+ const strict = Boolean(opts.strict);
23
+ const manifest = checkDocuments({ projectRoot, strict });
24
+ const engine = await resolveMarkdownEngine({ engines: opts.engines, projectRoot });
25
+ const enriched = enrichDocumentContent(manifest, {
26
+ analyzeMarkdown: engine.analyzeMarkdown,
27
+ projectRoot,
28
+ });
29
+ manifest.diagnostics = enriched.diagnostics;
30
+ const evidence = await enrichDocumentEvidence(manifest, {
31
+ analyzeMarkdown: engine.analyzeMarkdown,
32
+ projectRoot,
33
+ createWorkspace,
34
+ });
35
+ manifest.diagnostics = evidence.diagnostics;
36
+ manifest.evidence = evidence.evidence;
37
+ if (manifestHasErrors(manifest)) {
38
+ return { ok: false, manifest, outDir, pages: [] };
39
+ }
40
+ /** @type {Map<string, any>} */
41
+ const analyzedByPageId = new Map();
42
+ for (const page of manifest.pages) {
43
+ const abs = path.isAbsolute(page.sourcePath) ? page.sourcePath : path.join(manifest.root, page.sourcePath);
44
+ const source = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : '';
45
+ const id = `${page.identity.locale}:${page.identity.pageKey}`;
46
+ analyzedByPageId.set(id, engine.analyzeMarkdown(source));
47
+ }
48
+ const fenceBodies = collectFenceBodies(analyzedByPageId, manifest.pages);
49
+ const search = buildDocumentSearch({
50
+ manifest,
51
+ enriched,
52
+ evidence: evidence.evidence,
53
+ version: null,
54
+ });
55
+ const islands = buildDocumentIslands({
56
+ evidence: evidence.evidence,
57
+ searchHref: 'document.search.json',
58
+ fenceBodies,
59
+ });
60
+ manifest.search = search;
61
+ manifest.islands = islands;
62
+ const hostChrome = resolveHostSiteChrome(projectRoot);
63
+ const useHostShell = Boolean(hostChrome) && (manifest.mounts || []).some((m) => m.mode === 'integrated');
64
+ fs.mkdirSync(outDir, { recursive: true });
65
+ const designs = resolveDocumentDesignsCss(projectRoot);
66
+ /** @type {string | null} */
67
+ let designsHref = null;
68
+ if (designs.css && designs.href) {
69
+ const cssPath = path.join(outDir, designs.href);
70
+ fs.mkdirSync(path.dirname(cssPath), { recursive: true });
71
+ fs.writeFileSync(cssPath, designs.css, 'utf8');
72
+ designsHref = designs.href;
73
+ }
74
+ const viewsDir = path.join(outDir, 'views');
75
+ fs.mkdirSync(viewsDir, { recursive: true });
76
+ /** @type {Array<{ route: string, htmlPath: string, viewPath: string }>} */
77
+ const written = [];
78
+ for (const page of manifest.pages) {
79
+ const id = `${page.identity.locale}:${page.identity.pageKey}`;
80
+ const info = enriched.byId.get(id);
81
+ if (!info)
82
+ continue;
83
+ const nav = enriched.navByLocale[page.identity.locale] || [];
84
+ const htmlRel = pageHtmlRel(enriched.routeBase, page.identity.locale, page.identity.pageKey);
85
+ const htmlAbs = path.join(outDir, htmlRel);
86
+ fs.mkdirSync(path.dirname(htmlAbs), { recursive: true });
87
+ const searchIndexHref = artifactHrefFromHtml(htmlRel, 'document.search.json');
88
+ const shells = renderIslandShellsHtml({
89
+ islands,
90
+ searchIndexHref,
91
+ pageKey: page.identity.pageKey,
92
+ locale: page.identity.locale,
93
+ });
94
+ const view = {
95
+ schema: DOCUMENT_VIEW_SCHEMA,
96
+ pageKey: page.identity.pageKey,
97
+ locale: page.identity.locale,
98
+ route: info.route,
99
+ title: info.title,
100
+ headings: info.headings,
101
+ nav,
102
+ bodyKind: 'html',
103
+ html: info.html,
104
+ designsCss: designsHref,
105
+ noJsReadable: true,
106
+ hydrate: 'island-only',
107
+ hostShell: useHostShell,
108
+ islands: ['DocumentSearch'].concat((islands.islands || [])
109
+ .filter((isl) => isl.kind === 'playground' &&
110
+ isl.fence?.locale === page.identity.locale &&
111
+ isl.fence?.pageKey === page.identity.pageKey)
112
+ .map((isl) => isl.name)),
113
+ };
114
+ const viewRel = path.posix.join('views', page.identity.locale, `${page.identity.pageKey === 'index' ? 'index' : page.identity.pageKey}.view.json`);
115
+ const viewAbs = path.join(outDir, viewRel);
116
+ fs.mkdirSync(path.dirname(viewAbs), { recursive: true });
117
+ fs.writeFileSync(viewAbs, JSON.stringify(view, null, 2) + '\n', 'utf8');
118
+ const html = renderStaticHtml({
119
+ title: info.title,
120
+ locale: page.identity.locale,
121
+ route: info.route,
122
+ nav,
123
+ bodyHtml: info.html,
124
+ headings: info.headings,
125
+ designsHref,
126
+ htmlRel,
127
+ searchShellHtml: shells.searchHtml,
128
+ playgroundShellHtml: shells.playgroundHtml,
129
+ hostChrome: useHostShell ? hostChrome : null,
130
+ });
131
+ fs.writeFileSync(htmlAbs, html, 'utf8');
132
+ written.push({ route: info.route, htmlPath: htmlRel, viewPath: viewRel });
133
+ }
134
+ const manifestOut = {
135
+ ...manifest,
136
+ schema: manifest.schema,
137
+ evidence: evidence.evidence,
138
+ search,
139
+ islands,
140
+ build: {
141
+ engine: engine.engine,
142
+ outDir: path.relative(projectRoot, outDir).replace(/\\/g, '/') || '.',
143
+ designs: designs.source,
144
+ designsCss: designsHref,
145
+ pages: written,
146
+ evidence: 'document.evidence.json',
147
+ search: 'document.search.json',
148
+ islands: 'document.islands.json',
149
+ },
150
+ };
151
+ fs.writeFileSync(path.join(outDir, 'document.manifest.json'), JSON.stringify(manifestOut, null, 2) + '\n', 'utf8');
152
+ fs.writeFileSync(path.join(outDir, 'document.evidence.json'), JSON.stringify(evidence.evidence, null, 2) + '\n', 'utf8');
153
+ fs.writeFileSync(path.join(outDir, 'document.search.json'), JSON.stringify(search, null, 2) + '\n', 'utf8');
154
+ fs.writeFileSync(path.join(outDir, 'document.islands.json'), JSON.stringify(islands, null, 2) + '\n', 'utf8');
155
+ return { ok: true, manifest: manifestOut, outDir, pages: written, search, islands };
156
+ }
157
+ /**
158
+ * No-JS readable static HTML: nav + main landmarks, Island shells without scripts.
159
+ * Integrated mounts reuse host SiteHeader/SiteFooter templates when present.
160
+ */
161
+ function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, designsHref, htmlRel, searchShellHtml = '', playgroundShellHtml = '', hostChrome = null, }) {
162
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
163
+ const depth = htmlRel.split('/').length - 1;
164
+ const prefix = depth > 0 ? '../'.repeat(depth) : './';
165
+ /** @type {string[]} */
166
+ const cssHrefs = [];
167
+ if (hostChrome) {
168
+ // Same application stylesheet as landing pages (header/footer chrome).
169
+ cssHrefs.push(`${prefix}vmz.css`);
170
+ }
171
+ if (designsHref)
172
+ cssHrefs.push(prefix + designsHref);
173
+ const cssLink = cssHrefs.map((href) => ` <link rel="stylesheet" href="${esc(href)}" />`).join('\n') + (cssHrefs.length ? '\n' : '');
174
+ const navItems = nav
175
+ .map((n) => {
176
+ const href = relativeHref(htmlRel, n.href, route);
177
+ const current = n.href === route ? ' aria-current="page"' : '';
178
+ return ` <li><a href="${esc(href)}"${current}>${esc(n.title)}</a></li>`;
179
+ })
180
+ .join('\n');
181
+ const toc = headings.length > 1
182
+ ? `<nav aria-label="On this page" class="toc">\n <ol>\n${headings
183
+ .map((h) => ` <li class="h${h.level}"><a href="#${esc(h.id)}">${esc(h.text)}</a></li>`)
184
+ .join('\n')}\n </ol>\n </nav>\n`
185
+ : '';
186
+ const docsNav = ` <nav aria-label="Documents" class="doc-subnav">
187
+ <ul>
188
+ ${navItems}
189
+ </ul>
190
+ </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
+ ${docsNav}
205
+ ${searchShellHtml}
206
+ <div class="doc-body">
207
+ ${toc}<main id="main">
208
+ ${bodyHtml}
209
+ ${playgroundShellHtml}
210
+ </main>
211
+ </div>
212
+ ${hostChrome.footer}
213
+ </div>
214
+ </body>
215
+ </html>
216
+ `;
217
+ }
218
+ return `<!DOCTYPE html>
219
+ <html lang="${esc(locale)}">
220
+ <head>
221
+ <meta charset="utf-8" />
222
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
223
+ <title>${esc(title)}</title>
224
+ ${cssLink}</head>
225
+ <body data-vmz-hydrate="island-only">
226
+ <a class="skip-link" href="#main">Skip to content</a>
227
+ ${docsNav}
228
+ ${searchShellHtml}
229
+ ${toc}<main id="main">
230
+ ${bodyHtml}
231
+ ${playgroundShellHtml}
232
+ </main>
233
+ </body>
234
+ </html>
235
+ `;
236
+ }
237
+ /**
238
+ * Integrated DocumentMount: reuse host SiteHeader / SiteFooter .vmz templates.
239
+ * @param {string} projectRoot
240
+ * @returns {{ header: string, footer: string } | null}
241
+ */
242
+ function resolveHostSiteChrome(projectRoot) {
243
+ const headerPath = path.join(projectRoot, 'src', 'components', 'SiteHeader.vmz');
244
+ const footerPath = path.join(projectRoot, 'src', 'components', 'SiteFooter.vmz');
245
+ if (!fs.existsSync(headerPath) || !fs.existsSync(footerPath))
246
+ return null;
247
+ const header = extractVmzTemplateHtml(headerPath);
248
+ const footer = extractVmzTemplateHtml(footerPath);
249
+ if (!header || !footer)
250
+ return null;
251
+ return { header, footer };
252
+ }
253
+ /** @param {string} filePath */
254
+ function extractVmzTemplateHtml(filePath) {
255
+ const src = fs.readFileSync(filePath, 'utf8');
256
+ const m = src.match(/<template>([\s\S]*?)<\/template>/);
257
+ return m ? m[1].trim() : '';
258
+ }
259
+ function relativeHref(fromHtmlRel, toRoute, _fromRoute) {
260
+ const toParts = String(toRoute).replace(/^\//, '').split('/').filter(Boolean);
261
+ let toRel;
262
+ if (toParts.length <= 2) {
263
+ toRel = [...toParts, 'index.html'].join('/');
264
+ }
265
+ else {
266
+ const file = toParts.slice(2).join('/') + '.html';
267
+ toRel = [...toParts.slice(0, 2), file].join('/');
268
+ }
269
+ const fromDir = path.posix.dirname(fromHtmlRel.replace(/\\/g, '/'));
270
+ let rel = path.posix.relative(fromDir, toRel);
271
+ if (!rel.startsWith('.') && !rel.startsWith('/'))
272
+ rel = './' + rel;
273
+ return rel || './index.html';
274
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Build DocumentManifest + run D0 / --strict checks.
3
+ */
4
+ /**
5
+ * Resolve project documents root.
6
+ * @param {string} projectRoot
7
+ */
8
+ export declare function resolveDocumentsRoot(projectRoot: any): string;
9
+ /**
10
+ * Parse documents.config.json or a JSON-compatible export-default .ts/.js.
11
+ * @param {string} documentsRoot
12
+ * @returns {{ config: Record<string, any> | null, diagnostics: any[], configPath: string | null }}
13
+ */
14
+ export declare function loadDocumentsConfig(documentsRoot: any): {
15
+ config: any;
16
+ diagnostics: any[];
17
+ configPath: string;
18
+ };
19
+ /**
20
+ * @param {object} opts
21
+ * @param {string} opts.projectRoot
22
+ * @param {boolean} [opts.strict]
23
+ * @returns {import('./document-schema.js').DocumentManifest}
24
+ */
25
+ export declare function checkDocuments(opts: any): {
26
+ schema: string;
27
+ root: string;
28
+ defaultLocale: any;
29
+ locales: any[];
30
+ localeLabels: {};
31
+ collections: any[];
32
+ mounts: any[];
33
+ pages: {
34
+ identity: any;
35
+ sourcePath: any;
36
+ route: any;
37
+ anchors: any[];
38
+ }[];
39
+ diagnostics: any[];
40
+ };
41
+ /**
42
+ * @param {import('./document-schema.js').DocumentManifest} manifest
43
+ */
44
+ export declare function manifestHasErrors(manifest: any): any;
@@ -0,0 +1,246 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Build DocumentManifest + run D0 / --strict checks.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { DIAG, DOCUMENT_MANIFEST_SCHEMA } from './document-schema.js';
8
+ import { scanDocumentsTree } from './document-scan.js';
9
+ /**
10
+ * Resolve project documents root.
11
+ * @param {string} projectRoot
12
+ */
13
+ export function resolveDocumentsRoot(projectRoot) {
14
+ return path.resolve(projectRoot, 'documents');
15
+ }
16
+ /**
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
+ */
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 };
45
+ }
46
+ /**
47
+ * D0: only declaration objects — no arbitrary hooks.
48
+ * @param {string} raw
49
+ * @param {string} filename
50
+ */
51
+ function parseConfigSource(raw, filename) {
52
+ if (filename.endsWith('.json')) {
53
+ return JSON.parse(raw);
54
+ }
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)');
61
+ }
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);
70
+ }
71
+ /**
72
+ * @param {object} opts
73
+ * @param {string} opts.projectRoot
74
+ * @param {boolean} [opts.strict]
75
+ * @returns {import('./document-schema.js').DocumentManifest}
76
+ */
77
+ export function checkDocuments(opts) {
78
+ const projectRoot = path.resolve(opts.projectRoot);
79
+ const documentsRoot = resolveDocumentsRoot(projectRoot);
80
+ const strict = Boolean(opts.strict);
81
+ /** @type {import('./document-schema.js').DocumentDiagnostic[]} */
82
+ const diagnostics = [];
83
+ const scanned = scanDocumentsTree(documentsRoot);
84
+ 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; D0 requires defaultLocale + locales for strict coverage checks',
92
+ path: documentsRoot,
93
+ });
94
+ }
95
+ /** @type {Record<string, string>} */
96
+ const localeLabels = {};
97
+ let defaultLocale = null;
98
+ /** @type {import('./document-schema.js').DocumentCollection[]} */
99
+ const collections = [];
100
+ /** @type {import('./document-schema.js').DocumentMount[]} */
101
+ 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
+ }
133
+ }
134
+ if (collections.length === 0) {
135
+ const pageKeys = [...new Set(scanned.pages.map((p) => p.identity.pageKey))].sort();
136
+ collections.push({ id: 'default', sourceRoot: '.', pageKeys });
137
+ mounts.push({ collectionId: 'default', routeBase: '/docs', mode: 'integrated' });
138
+ }
139
+ // Config locales vs disk
140
+ const diskLocales = new Set(scanned.locales);
141
+ const configLocales = Object.keys(localeLabels);
142
+ if (defaultLocale) {
143
+ if (!diskLocales.has(defaultLocale)) {
144
+ diagnostics.push({
145
+ code: DIAG.LOCALE_MISSING_DEFAULT,
146
+ severity: 'error',
147
+ message: `defaultLocale ${JSON.stringify(defaultLocale)} has no directory under documents/`,
148
+ path: configPath || documentsRoot,
149
+ });
150
+ }
151
+ if (configLocales.length && !configLocales.includes(defaultLocale)) {
152
+ diagnostics.push({
153
+ code: DIAG.CONFIG_DEFAULT_LOCALE,
154
+ severity: 'error',
155
+ message: `defaultLocale ${JSON.stringify(defaultLocale)} not listed in config.locales`,
156
+ path: configPath || documentsRoot,
157
+ });
158
+ }
159
+ }
160
+ else if (strict) {
161
+ diagnostics.push({
162
+ code: DIAG.CONFIG_DEFAULT_LOCALE,
163
+ severity: 'error',
164
+ message: 'defaultLocale is required under --strict',
165
+ path: configPath || documentsRoot,
166
+ });
167
+ }
168
+ for (const loc of configLocales) {
169
+ if (!diskLocales.has(loc)) {
170
+ diagnostics.push({
171
+ code: DIAG.LOCALE_MISSING_DEFAULT,
172
+ severity: 'error',
173
+ message: `config.locales entry ${JSON.stringify(loc)} has no documents/${loc}/ directory`,
174
+ path: configPath || documentsRoot,
175
+ });
176
+ }
177
+ }
178
+ // D0: 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
+ // Coverage: default locale PageKeys are baseline; other locales missing/orphan under --strict
188
+ if (defaultLocale && diskLocales.has(defaultLocale)) {
189
+ const byLocale = new Map();
190
+ for (const p of scanned.pages) {
191
+ const set = byLocale.get(p.identity.locale) || new Set();
192
+ set.add(p.identity.pageKey);
193
+ byLocale.set(p.identity.locale, set);
194
+ }
195
+ const baseline = byLocale.get(defaultLocale) || new Set();
196
+ for (const loc of scanned.locales) {
197
+ if (loc === defaultLocale)
198
+ continue;
199
+ const keys = byLocale.get(loc) || new Set();
200
+ for (const pk of baseline) {
201
+ if (!keys.has(pk)) {
202
+ diagnostics.push({
203
+ code: DIAG.LOCALE_MISSING_PAGE,
204
+ severity: strict ? 'error' : 'warning',
205
+ message: `missing translation: locale ${loc} lacks PageKey ${JSON.stringify(pk)} (present in ${defaultLocale})`,
206
+ path: `documents/${loc}`,
207
+ });
208
+ }
209
+ }
210
+ for (const pk of keys) {
211
+ if (!baseline.has(pk)) {
212
+ diagnostics.push({
213
+ code: DIAG.LOCALE_ORPHAN_PAGE,
214
+ severity: strict ? 'error' : 'warning',
215
+ message: `orphan page: locale ${loc} has PageKey ${JSON.stringify(pk)} missing from defaultLocale ${defaultLocale}`,
216
+ path: `documents/${loc}/${pk}.md`,
217
+ });
218
+ }
219
+ }
220
+ }
221
+ }
222
+ /** @type {import('./document-schema.js').PageRecord[]} */
223
+ const pages = scanned.pages.map((p) => ({
224
+ identity: p.identity,
225
+ sourcePath: p.sourcePath,
226
+ route: null,
227
+ anchors: [],
228
+ }));
229
+ return {
230
+ schema: DOCUMENT_MANIFEST_SCHEMA,
231
+ root: documentsRoot,
232
+ defaultLocale,
233
+ locales: scanned.locales,
234
+ localeLabels,
235
+ collections,
236
+ mounts,
237
+ pages,
238
+ diagnostics,
239
+ };
240
+ }
241
+ /**
242
+ * @param {import('./document-schema.js').DocumentManifest} manifest
243
+ */
244
+ export function manifestHasErrors(manifest) {
245
+ return (manifest.diagnostics || []).some((d) => d.severity === 'error');
246
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `vmz document` / `vmz docs` CLI (D0 check · D1 build).
3
+ * Design: 规划设计/vmz/19 §1 · §8
4
+ */
5
+ /**
6
+ * @param {string[]} argv
7
+ * @returns {Promise<number>}
8
+ */
9
+ export declare function cmdDocument(argv: any): Promise<0 | 1>;