@vmz/vmz 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -2
- package/bin/vmz.js +4 -0
- package/dist/application-cmd.d.ts +21 -0
- package/dist/application-cmd.js +347 -0
- package/dist/bundler-adapter.d.ts +63 -0
- package/dist/bundler-adapter.js +110 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +474 -0
- package/dist/dev-session.d.ts +35 -0
- package/dist/dev-session.js +290 -0
- package/dist/document-build.d.ts +99 -0
- package/dist/document-build.js +273 -0
- package/dist/document-check.d.ts +44 -0
- package/dist/document-check.js +246 -0
- package/dist/document-cmd.d.ts +8 -0
- package/dist/document-cmd.js +146 -0
- package/dist/document-designs.d.ts +9 -0
- package/dist/document-designs.js +126 -0
- package/dist/document-enrich.d.ts +23 -0
- package/dist/document-enrich.js +233 -0
- package/dist/document-evidence.d.ts +49 -0
- package/dist/document-evidence.js +509 -0
- package/dist/document-integrate.d.ts +34 -0
- package/dist/document-integrate.js +88 -0
- package/dist/document-interactive.d.ts +69 -0
- package/dist/document-interactive.js +254 -0
- package/dist/document-locale.d.ts +31 -0
- package/dist/document-locale.js +59 -0
- package/dist/document-markdown.d.ts +12 -0
- package/dist/document-markdown.js +45 -0
- package/dist/document-scan.d.ts +21 -0
- package/dist/document-scan.js +151 -0
- package/dist/document-schema.d.ts +86 -0
- package/dist/document-schema.js +87 -0
- package/dist/explain-cmd.d.ts +5 -0
- package/dist/explain-cmd.js +123 -0
- package/dist/index.d.ts +359 -0
- package/dist/index.js +580 -0
- package/dist/invocation.d.ts +91 -0
- package/dist/invocation.js +190 -0
- package/dist/locale-check.d.ts +106 -0
- package/dist/locale-check.js +736 -0
- package/dist/locale-cmd.d.ts +5 -0
- package/dist/locale-cmd.js +442 -0
- package/dist/locale-delivery.d.ts +298 -0
- package/dist/locale-delivery.js +443 -0
- package/dist/locale-router.d.ts +206 -0
- package/dist/locale-router.js +507 -0
- package/dist/locale-runtime.d.ts +406 -0
- package/dist/locale-runtime.js +541 -0
- package/dist/locale-schema.d.ts +8 -0
- package/dist/locale-schema.js +9 -0
- package/dist/locale-tooling.d.ts +118 -0
- package/dist/locale-tooling.js +357 -0
- package/dist/log.d.ts +19 -0
- package/dist/log.js +42 -0
- package/dist/packages.d.ts +26 -0
- package/dist/packages.js +146 -0
- package/dist/plugin-host.d.ts +29 -0
- package/dist/plugin-host.js +369 -0
- package/dist/refactor-cmd.d.ts +8 -0
- package/dist/refactor-cmd.js +156 -0
- package/dist/resolve-native-cli.d.ts +14 -0
- package/dist/resolve-native-cli.js +84 -0
- package/dist/resolve.d.ts +24 -0
- package/dist/resolve.js +55 -0
- package/dist/test-cmd.d.ts +9 -0
- package/dist/test-cmd.js +363 -0
- package/dist/test-compile.d.ts +2 -0
- package/dist/test-compile.js +3 -0
- package/dist/test-discover.d.ts +2 -0
- package/dist/test-discover.js +3 -0
- package/dist/test-logic.d.ts +2 -0
- package/dist/test-logic.js +3 -0
- package/dist/test-protocol.d.ts +2 -0
- package/dist/test-protocol.js +3 -0
- package/dist/watch-diff.d.ts +17 -0
- package/dist/watch-diff.js +56 -0
- package/package.json +96 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build DocumentManifest + run / --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 / --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
|
+
*: 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; strict mode 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
|
+
// 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,146 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* `vmz document` / `vmz docs` CLI .
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { buildDocuments } from './document-build.js';
|
|
8
|
+
import { checkDocuments, manifestHasErrors } from './document-check.js';
|
|
9
|
+
import { enrichDocumentContent } from './document-enrich.js';
|
|
10
|
+
import { enrichDocumentEvidence } from './document-evidence.js';
|
|
11
|
+
import { resolveMarkdownEngine } from './document-markdown.js';
|
|
12
|
+
import { createWorkspace } from './index.js';
|
|
13
|
+
import { log } from './log.js';
|
|
14
|
+
import { parseArgs } from './cli.js';
|
|
15
|
+
function printDocumentHelp() {
|
|
16
|
+
console.log(`vmz document — project /documents domain
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
vmz document check [project] Check locale tree + links/anchors + fence/API evidence
|
|
20
|
+
vmz document build [project] Static HTML + view + evidence + search/islands + /designs CSS
|
|
21
|
+
vmz docs check|build […] Alias of document
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--root <dir> Project root (default: . or positional path)
|
|
25
|
+
--out <dir> Build output (default: <project>/dist/documents)
|
|
26
|
+
--strict Fail on missing/orphan translations & require defaultLocale
|
|
27
|
+
--json [file] Emit DocumentManifest JSON to stdout or file
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @param {string[]} argv
|
|
32
|
+
* @returns {Promise<number>}
|
|
33
|
+
*/
|
|
34
|
+
export async function cmdDocument(argv) {
|
|
35
|
+
const [sub, ...rest] = argv;
|
|
36
|
+
if (!sub) {
|
|
37
|
+
if (process.stdout.isTTY) {
|
|
38
|
+
printDocumentHelp();
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
log.error('vmz document requires a subcommand in non-interactive/CI contexts');
|
|
42
|
+
printDocumentHelp();
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
if (sub === 'help' || sub === '-h' || sub === '--help') {
|
|
46
|
+
printDocumentHelp();
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
if (sub.startsWith('-')) {
|
|
50
|
+
log.error('vmz document requires a subcommand (check|build)');
|
|
51
|
+
printDocumentHelp();
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
const args = parseArgs(rest);
|
|
55
|
+
switch (sub) {
|
|
56
|
+
case 'check':
|
|
57
|
+
return cmdDocumentCheck(args);
|
|
58
|
+
case 'build':
|
|
59
|
+
return cmdDocumentBuild(args);
|
|
60
|
+
case 'dev':
|
|
61
|
+
case 'serve':
|
|
62
|
+
case 'test':
|
|
63
|
+
case 'clean':
|
|
64
|
+
log.error(`vmz document ${sub} is not implemented yet`);
|
|
65
|
+
return 1;
|
|
66
|
+
default:
|
|
67
|
+
log.error(`unknown document subcommand \`${sub}\``);
|
|
68
|
+
printDocumentHelp();
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** @param {Record<string, string | boolean> & { _: string[] }} args */
|
|
73
|
+
async function cmdDocumentCheck(args) {
|
|
74
|
+
const project = (typeof args.root === 'string' && args.root) || (typeof args._[0] === 'string' && args._[0]) || '.';
|
|
75
|
+
const projectRoot = path.resolve(project);
|
|
76
|
+
const strict = Boolean(args.strict);
|
|
77
|
+
const manifest = checkDocuments({ projectRoot, strict });
|
|
78
|
+
try {
|
|
79
|
+
const engine = await resolveMarkdownEngine({});
|
|
80
|
+
const enriched = enrichDocumentContent(manifest, {
|
|
81
|
+
analyzeMarkdown: engine.analyzeMarkdown,
|
|
82
|
+
projectRoot,
|
|
83
|
+
});
|
|
84
|
+
manifest.diagnostics = enriched.diagnostics;
|
|
85
|
+
const evidence = await enrichDocumentEvidence(manifest, {
|
|
86
|
+
analyzeMarkdown: engine.analyzeMarkdown,
|
|
87
|
+
projectRoot,
|
|
88
|
+
createWorkspace,
|
|
89
|
+
});
|
|
90
|
+
manifest.diagnostics = evidence.diagnostics;
|
|
91
|
+
manifest.evidence = evidence.evidence;
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
log.warn(`document check: markdown/evidence unavailable (${e.message}); skipping enrich`);
|
|
95
|
+
}
|
|
96
|
+
const jsonOut = args.json;
|
|
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
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
for (const d of manifest.diagnostics) {
|
|
109
|
+
const loc = d.path ? ` (${d.path})` : '';
|
|
110
|
+
if (d.severity === 'error')
|
|
111
|
+
log.error(`${d.code}: ${d.message}${loc}`);
|
|
112
|
+
else
|
|
113
|
+
console.warn(`vmz warn ${d.code}: ${d.message}${loc}`);
|
|
114
|
+
}
|
|
115
|
+
log.info(`document check: locales=${manifest.locales.join(',') || '(none)'} pages=${manifest.pages.length} defaultLocale=${manifest.defaultLocale ?? '(none)'}`);
|
|
116
|
+
}
|
|
117
|
+
return manifestHasErrors(manifest) ? 1 : 0;
|
|
118
|
+
}
|
|
119
|
+
/** @param {Record<string, string | boolean> & { _: string[] }} args */
|
|
120
|
+
async function cmdDocumentBuild(args) {
|
|
121
|
+
const project = (typeof args.root === 'string' && args.root) || (typeof args._[0] === 'string' && args._[0]) || '.';
|
|
122
|
+
const projectRoot = path.resolve(project);
|
|
123
|
+
const outDir = (typeof args.out === 'string' && args.out) || path.join(projectRoot, 'dist', 'documents');
|
|
124
|
+
const strict = Boolean(args.strict);
|
|
125
|
+
let result;
|
|
126
|
+
try {
|
|
127
|
+
result = await buildDocuments({ projectRoot, outDir, strict });
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
log.error(`document build failed: ${e.message || e}`);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
for (const d of result.manifest.diagnostics || []) {
|
|
134
|
+
const loc = d.path ? ` (${d.path})` : '';
|
|
135
|
+
if (d.severity === 'error')
|
|
136
|
+
log.error(`${d.code}: ${d.message}${loc}`);
|
|
137
|
+
else
|
|
138
|
+
console.warn(`vmz warn ${d.code}: ${d.message}${loc}`);
|
|
139
|
+
}
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
log.error('document build aborted due to diagnostics');
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
log.info(`document build: pages=${result.pages.length} out=${path.relative(process.cwd(), result.outDir) || '.'} designs=${result.manifest.build?.designsCss ?? '(none)'}`);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Document — wire project `/designs` into static document assets.
|
|
4
|
+
* Prefer compiled `vmz-designs.css` / `vmz.css`; else emit a minimal token CSS.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} projectRoot
|
|
10
|
+
* @returns {{ css: string, source: string | null, href: string | null }}
|
|
11
|
+
*/
|
|
12
|
+
export function resolveDocumentDesignsCss(projectRoot) {
|
|
13
|
+
const designsDir = path.join(projectRoot, 'designs');
|
|
14
|
+
if (!fs.existsSync(designsDir) || !fs.statSync(designsDir).isDirectory()) {
|
|
15
|
+
return { css: '', source: null, href: null };
|
|
16
|
+
}
|
|
17
|
+
/** @type {string[]} */
|
|
18
|
+
const parts = [];
|
|
19
|
+
/** @type {string[]} */
|
|
20
|
+
const sources = [];
|
|
21
|
+
// Layout first so @import (fonts) stays at stylesheet top; theme tokens follow.
|
|
22
|
+
const layoutCandidates = [path.join(designsDir, 'document', 'chrome.css'), path.join(designsDir, 'styles', 'document.css')];
|
|
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
|
+
}
|
|
30
|
+
const distCandidates = [path.join(projectRoot, 'dist', 'vmz-designs.css'), path.join(projectRoot, 'dist', 'vmz.css')];
|
|
31
|
+
for (const p of distCandidates) {
|
|
32
|
+
if (fs.existsSync(p)) {
|
|
33
|
+
parts.push(fs.readFileSync(p, 'utf8'));
|
|
34
|
+
sources.push(path.relative(projectRoot, p).replace(/\\/g, '/'));
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (parts.length) {
|
|
39
|
+
return {
|
|
40
|
+
css: parts.join('\n\n'),
|
|
41
|
+
source: sources.join('+'),
|
|
42
|
+
href: 'assets/vmz-designs.css',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const styleCandidates = [path.join(designsDir, 'styles', 'index.css')];
|
|
46
|
+
for (const p of styleCandidates) {
|
|
47
|
+
if (fs.existsSync(p)) {
|
|
48
|
+
return {
|
|
49
|
+
css: fs.readFileSync(p, 'utf8'),
|
|
50
|
+
source: path.relative(projectRoot, p).replace(/\\/g, '/'),
|
|
51
|
+
href: 'assets/vmz-designs.css',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const emitted = emitMinimalDesignsCss(designsDir);
|
|
56
|
+
if (emitted) {
|
|
57
|
+
return { css: emitted, source: 'designs/', href: 'assets/vmz-designs.css' };
|
|
58
|
+
}
|
|
59
|
+
return { css: '', source: 'designs/', href: null };
|
|
60
|
+
}
|
|
61
|
+
/** @param {string} designsDir */
|
|
62
|
+
function emitMinimalDesignsCss(designsDir) {
|
|
63
|
+
/** @type {Record<string, string>} */
|
|
64
|
+
const vars = {};
|
|
65
|
+
const tokenDir = path.join(designsDir, 'tokens');
|
|
66
|
+
if (fs.existsSync(tokenDir)) {
|
|
67
|
+
walkJson(tokenDir, (obj, prefix) => flattenTokens(obj, prefix, vars));
|
|
68
|
+
}
|
|
69
|
+
const themeJson = path.join(designsDir, 'theme.json');
|
|
70
|
+
if (fs.existsSync(themeJson)) {
|
|
71
|
+
try {
|
|
72
|
+
flattenTokens(JSON.parse(fs.readFileSync(themeJson, 'utf8')), '', vars);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* ignore */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const keys = Object.keys(vars).sort();
|
|
79
|
+
if (!keys.length) {
|
|
80
|
+
// Presence of /designs still warrants a readable baseline sheet.
|
|
81
|
+
return `/* vmz.document designs baseline */\n:root { color-scheme: light; }\nbody { font-family: system-ui, sans-serif; line-height: 1.5; margin: 0; }\nmain { max-width: 48rem; margin: 0 auto; padding: 1.5rem; }\nnav { padding: 1rem 1.5rem; border-bottom: 1px solid #ddd; }\n`;
|
|
82
|
+
}
|
|
83
|
+
const lines = keys.map((k) => ` ${cssVar(k)}: ${vars[k]};`);
|
|
84
|
+
return `/* vmz.document designs from /designs */\n:root {\n${lines.join('\n')}\n}\nbody { font-family: var(--font-sans, system-ui, sans-serif); line-height: 1.5; margin: 0; color: var(--color-fg, #111); background: var(--color-bg, #fff); }\nmain { max-width: 48rem; margin: 0 auto; padding: 1.5rem; }\nnav { padding: 1rem 1.5rem; border-bottom: 1px solid var(--color-border, #ddd); }\n`;
|
|
85
|
+
}
|
|
86
|
+
function cssVar(key) {
|
|
87
|
+
const name = String(key)
|
|
88
|
+
.replace(/[^a-zA-Z0-9_-]+/g, '-')
|
|
89
|
+
.replace(/^-|-$/g, '');
|
|
90
|
+
return `--${name}`;
|
|
91
|
+
}
|
|
92
|
+
function flattenTokens(obj, prefix, out) {
|
|
93
|
+
if (obj == null || typeof obj !== 'object' || Array.isArray(obj))
|
|
94
|
+
return;
|
|
95
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
96
|
+
const key = prefix ? `${prefix}-${k}` : k;
|
|
97
|
+
if (v != null && typeof v === 'object' && !Array.isArray(v)) {
|
|
98
|
+
if ('value' in v && (typeof v.value === 'string' || typeof v.value === 'number')) {
|
|
99
|
+
out[key] = String(v.value);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
flattenTokens(v, key, out);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (typeof v === 'string' || typeof v === 'number') {
|
|
106
|
+
out[key] = String(v);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function walkJson(dir, fn) {
|
|
111
|
+
if (!fs.existsSync(dir))
|
|
112
|
+
return;
|
|
113
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
114
|
+
const full = path.join(dir, ent.name);
|
|
115
|
+
if (ent.isDirectory())
|
|
116
|
+
walkJson(full, fn);
|
|
117
|
+
else if (ent.isFile() && ent.name.endsWith('.json')) {
|
|
118
|
+
try {
|
|
119
|
+
fn(JSON.parse(fs.readFileSync(full, 'utf8')), '');
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* ignore */
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} routeBase e.g. /docs
|
|
3
|
+
* @param {string} locale
|
|
4
|
+
* @param {string} pageKey
|
|
5
|
+
*/
|
|
6
|
+
export declare function pageRoute(routeBase: any, locale: any, pageKey: any): string;
|
|
7
|
+
/**
|
|
8
|
+
* Static file path relative to out dir (posix).
|
|
9
|
+
* @param {string} routeBase
|
|
10
|
+
* @param {string} locale
|
|
11
|
+
* @param {string} pageKey
|
|
12
|
+
*/
|
|
13
|
+
export declare function pageHtmlRel(routeBase: any, locale: any, pageKey: any): string;
|
|
14
|
+
/**
|
|
15
|
+
* @param {import('./document-schema.js').DocumentManifest} manifest
|
|
16
|
+
* @param {{ analyzeMarkdown: Function, projectRoot: string, designsCssHref?: string | null }} ctx
|
|
17
|
+
*/
|
|
18
|
+
export declare function enrichDocumentContent(manifest: any, ctx: any): {
|
|
19
|
+
byId: Map<any, any>;
|
|
20
|
+
navByLocale: {};
|
|
21
|
+
diagnostics: any[];
|
|
22
|
+
routeBase: any;
|
|
23
|
+
};
|