@vmz/vmz 0.1.14 → 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.
- package/dist/author-input.d.ts +19 -0
- package/dist/author-input.js +52 -0
- package/dist/document-build.d.ts +2 -2
- package/dist/document-check.d.ts +24 -5
- package/dist/document-check.js +64 -101
- package/dist/document-routing-config.d.ts +7 -3
- package/dist/document-routing-config.js +12 -21
- package/dist/document-schema.js +8 -1
- package/dist/locale-check.js +53 -122
- package/dist/locale-cmd.js +3 -3
- package/package.json +13 -14
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Degrade author JSON5/JSON text to a plain object via Rust (not a semantic plan API).
|
|
3
|
+
* @param {string} source
|
|
4
|
+
* @returns {any}
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseAuthorInput(source: any): any;
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} projectRoot
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadLocalePlan(projectRoot: any): any;
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} projectRoot
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadDocumentRoutePlan(projectRoot: any): any;
|
|
15
|
+
/**
|
|
16
|
+
* Map Rust ReportedDiagnostic rows into host `{ code, severity, message, path }` rows.
|
|
17
|
+
* @param {Array<{ code?: string, severity?: string, message?: string, path?: string }>} rows
|
|
18
|
+
*/
|
|
19
|
+
export declare function mapPlanDiagnostics(rows: any): any;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Author declaration input via Rust N-API (no JS JSON5 package).
|
|
4
|
+
*
|
|
5
|
+
* Locale/document policy: loadLocalePlan / loadDocumentRoutePlan.
|
|
6
|
+
* Catalogs / transitional tables: parseAuthorInput → Rust degrade → JSON.parse.
|
|
7
|
+
*/
|
|
8
|
+
import { requireNativeAddon } from './native-addon.js';
|
|
9
|
+
/**
|
|
10
|
+
* Degrade author JSON5/JSON text to a plain object via Rust (not a semantic plan API).
|
|
11
|
+
* @param {string} source
|
|
12
|
+
* @returns {any}
|
|
13
|
+
*/
|
|
14
|
+
export function parseAuthorInput(source) {
|
|
15
|
+
const native = requireNativeAddon();
|
|
16
|
+
if (typeof native.authorJson5ToCanonicalJson !== 'function') {
|
|
17
|
+
throw new Error('native missing authorJson5ToCanonicalJson — run `pnpm napi:build`');
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(native.authorJson5ToCanonicalJson(String(source)));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} projectRoot
|
|
23
|
+
*/
|
|
24
|
+
export function loadLocalePlan(projectRoot) {
|
|
25
|
+
const native = requireNativeAddon();
|
|
26
|
+
if (typeof native.loadLocalePlan !== 'function') {
|
|
27
|
+
throw new Error('native missing loadLocalePlan — run `pnpm napi:build`');
|
|
28
|
+
}
|
|
29
|
+
return JSON.parse(native.loadLocalePlan(String(projectRoot)));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} projectRoot
|
|
33
|
+
*/
|
|
34
|
+
export function loadDocumentRoutePlan(projectRoot) {
|
|
35
|
+
const native = requireNativeAddon();
|
|
36
|
+
if (typeof native.loadDocumentRoutePlan !== 'function') {
|
|
37
|
+
throw new Error('native missing loadDocumentRoutePlan — run `pnpm napi:build`');
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(native.loadDocumentRoutePlan(String(projectRoot)));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Map Rust ReportedDiagnostic rows into host `{ code, severity, message, path }` rows.
|
|
43
|
+
* @param {Array<{ code?: string, severity?: string, message?: string, path?: string }>} rows
|
|
44
|
+
*/
|
|
45
|
+
export function mapPlanDiagnostics(rows) {
|
|
46
|
+
return (rows || []).map((d) => ({
|
|
47
|
+
code: d.code || 'vmz::unknown',
|
|
48
|
+
severity: d.severity === 'advice' ? 'warning' : d.severity || 'error',
|
|
49
|
+
message: d.message || '',
|
|
50
|
+
path: d.path || undefined,
|
|
51
|
+
}));
|
|
52
|
+
}
|
package/dist/document-build.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare function buildDocuments(opts: any): Promise<{
|
|
|
8
8
|
root: string;
|
|
9
9
|
defaultLocale: any;
|
|
10
10
|
locales: any[];
|
|
11
|
-
localeLabels:
|
|
11
|
+
localeLabels: any;
|
|
12
12
|
collections: any[];
|
|
13
13
|
mounts: any[];
|
|
14
14
|
pages: {
|
|
@@ -66,7 +66,7 @@ export declare function buildDocuments(opts: any): Promise<{
|
|
|
66
66
|
root: string;
|
|
67
67
|
defaultLocale: any;
|
|
68
68
|
locales: any[];
|
|
69
|
-
localeLabels:
|
|
69
|
+
localeLabels: any;
|
|
70
70
|
collections: any[];
|
|
71
71
|
mounts: any[];
|
|
72
72
|
pages: {
|
package/dist/document-check.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build DocumentManifest + run / --strict checks.
|
|
3
|
+
*
|
|
4
|
+
* Author documents config is parsed by Rust DocumentRoutePlan; TS only
|
|
5
|
+
* merges filesystem scan (pageKeys) and coverage diagnostics.
|
|
3
6
|
*/
|
|
4
7
|
/**
|
|
5
8
|
* Resolve project documents root.
|
|
@@ -7,14 +10,30 @@
|
|
|
7
10
|
*/
|
|
8
11
|
export declare function resolveDocumentsRoot(projectRoot: any): string;
|
|
9
12
|
/**
|
|
10
|
-
*
|
|
13
|
+
* Load normalized DocumentRoutePlan from Rust (author JSON5/JSON/declaration).
|
|
14
|
+
* @param {string} projectRoot
|
|
15
|
+
*/
|
|
16
|
+
export declare function loadDocumentsRoutePlan(projectRoot: any): any;
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Prefer loadDocumentsRoutePlan(projectRoot). Kept for call sites that
|
|
19
|
+
* only need the plan-shaped config fields.
|
|
11
20
|
* @param {string} documentsRoot
|
|
12
|
-
* @returns {{ config: Record<string, any> | null, diagnostics: any[], configPath: string | null }}
|
|
13
21
|
*/
|
|
14
22
|
export declare function loadDocumentsConfig(documentsRoot: any): {
|
|
15
|
-
config:
|
|
16
|
-
|
|
23
|
+
config: {
|
|
24
|
+
defaultLocale: any;
|
|
25
|
+
locales: {
|
|
26
|
+
[k: string]: {
|
|
27
|
+
label: unknown;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
collections: {
|
|
31
|
+
[k: string]: any;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
diagnostics: any;
|
|
17
35
|
configPath: string;
|
|
36
|
+
plan: any;
|
|
18
37
|
};
|
|
19
38
|
/**
|
|
20
39
|
* @param {object} opts
|
|
@@ -27,7 +46,7 @@ export declare function checkDocuments(opts: any): {
|
|
|
27
46
|
root: string;
|
|
28
47
|
defaultLocale: any;
|
|
29
48
|
locales: any[];
|
|
30
|
-
localeLabels:
|
|
49
|
+
localeLabels: any;
|
|
31
50
|
collections: any[];
|
|
32
51
|
mounts: any[];
|
|
33
52
|
pages: {
|
package/dist/document-check.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
18
|
-
* @param {string}
|
|
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
|
|
22
|
-
|
|
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
|
-
|
|
48
|
-
*
|
|
49
|
-
* @param {string}
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
86
|
-
diagnostics.push(...
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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();
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Project locale routing
|
|
2
|
+
* Project locale routing — consume Rust LocalePlan (no author JSON5 in TS).
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
5
|
* @param {string} projectRoot
|
|
6
|
-
* @returns {{ strategy?: string, defaultLocale?: string } | null}
|
|
6
|
+
* @returns {{ strategy?: string, defaultPrefix?: string, defaultLocale?: string } | null}
|
|
7
7
|
*/
|
|
8
|
-
export declare function loadLocalesRouting(projectRoot: any):
|
|
8
|
+
export declare function loadLocalesRouting(projectRoot: any): {
|
|
9
|
+
strategy: any;
|
|
10
|
+
defaultPrefix: any;
|
|
11
|
+
defaultLocale: any;
|
|
12
|
+
};
|
|
9
13
|
/**
|
|
10
14
|
* @param {string} routeBase
|
|
11
15
|
* @param {string} pageKey
|
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* Project locale routing
|
|
3
|
+
* Project locale routing — consume Rust LocalePlan (no author JSON5 in TS).
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
6
|
-
import path from 'node:path';
|
|
5
|
+
import { loadLocalePlan } from './author-input.js';
|
|
7
6
|
/**
|
|
8
7
|
* @param {string} projectRoot
|
|
9
|
-
* @returns {{ strategy?: string, defaultLocale?: string } | null}
|
|
8
|
+
* @returns {{ strategy?: string, defaultPrefix?: string, defaultLocale?: string } | null}
|
|
10
9
|
*/
|
|
11
10
|
export function loadLocalesRouting(projectRoot) {
|
|
12
|
-
const
|
|
13
|
-
if (!
|
|
14
|
-
return null;
|
|
15
|
-
try {
|
|
16
|
-
const raw = fs.readFileSync(p, 'utf8');
|
|
17
|
-
let s = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
18
|
-
const m = s.match(/routing\s*:\s*\{([\s\S]*?)\}/);
|
|
19
|
-
if (!m)
|
|
20
|
-
return null;
|
|
21
|
-
let block = `{${m[1]}}`;
|
|
22
|
-
block = block.replace(/([,{]\s*)([A-Za-z_][A-Za-z0-9_]*)\s*:/g, '$1"$2":');
|
|
23
|
-
block = block.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, (_, inner) => JSON.stringify(inner));
|
|
24
|
-
block = block.replace(/,\s*([}\]])/g, '$1');
|
|
25
|
-
return JSON.parse(block);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
11
|
+
const plan = loadLocalePlan(projectRoot);
|
|
12
|
+
if (!plan || plan.diagnostics?.some((d) => d.code === 'vmz::locale::manifest_missing')) {
|
|
28
13
|
return null;
|
|
29
14
|
}
|
|
15
|
+
const routing = plan.routing || {};
|
|
16
|
+
return {
|
|
17
|
+
strategy: routing.strategy || 'prefix',
|
|
18
|
+
defaultPrefix: routing.defaultPrefix || 'include',
|
|
19
|
+
defaultLocale: plan.defaultLocale || undefined,
|
|
20
|
+
};
|
|
30
21
|
}
|
|
31
22
|
/**
|
|
32
23
|
* @param {string} routeBase
|
|
@@ -36,5 +27,5 @@ export function docsRouteNone(routeBase, pageKey) {
|
|
|
36
27
|
const base = String(routeBase || '/').replace(/\/$/, '') || '';
|
|
37
28
|
const key = pageKey === 'index' ? '' : pageKey.replace(/\\/g, '/');
|
|
38
29
|
const parts = [base.replace(/^\//, ''), key].filter((p) => p !== '');
|
|
39
|
-
return
|
|
30
|
+
return `/${parts.length ? parts.join('/') : ''}`;
|
|
40
31
|
}
|
package/dist/document-schema.js
CHANGED
|
@@ -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([
|
|
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',
|
package/dist/locale-check.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import
|
|
9
|
+
import { loadLocalePlan, mapPlanDiagnostics, parseAuthorInput } from './author-input.js';
|
|
10
|
+
import { DIAG_CATALOG_CONFLICT, DIAG_CATALOG_PARSE, DIAG_DIR_MISSING, DIAG_DIR_ORPHAN, DIAG_ID_INVALID, DIAG_LAYOUT_ILLEGAL, DIAG_MANIFEST_MISSING, DIAG_MESSAGE_ARRAY_FORBIDDEN, DIAG_MESSAGE_HTML_FORBIDDEN, DIAG_MESSAGE_MISSING_DEFAULT, DIAG_MESSAGE_MISSING_VARIANT, DIAG_MESSAGE_PARAMETER_MISMATCH, DIAG_MESSAGE_SYNTAX_INVALID, DIAG_MESSAGE_UNUSED, LOCALE_CHECK_SCHEMA, LOCALE_ID_RE, LOCALE_MANIFEST_SCHEMA, LOCALE_RESERVED_TOP, LOCALE_TYPED_MODULE_SCHEMA, LOCALE_VIRTUAL_MODULE_PREFIX, MESSAGE_CATALOG_SCHEMA, MESSAGE_NODE_SCHEMA, } from './locale-schema.js';
|
|
10
11
|
import { requireNativeAddon } from './native-addon.js';
|
|
11
12
|
import { writePrettyJsonFile } from './pretty-json.js';
|
|
12
|
-
import { DIAG_CATALOG_CONFLICT, DIAG_CATALOG_PARSE, DIAG_DEFAULT_MISSING, DIAG_DIR_MISSING, DIAG_DIR_ORPHAN, DIAG_FALLBACK_CYCLE, DIAG_FALLBACK_UNKNOWN, DIAG_ID_COLLISION, DIAG_ID_INVALID, DIAG_LAYOUT_ILLEGAL, DIAG_MANIFEST_MISSING, DIAG_MESSAGE_ARRAY_FORBIDDEN, DIAG_MESSAGE_HTML_FORBIDDEN, DIAG_MESSAGE_MISSING_DEFAULT, DIAG_MESSAGE_PARAMETER_MISMATCH, DIAG_MESSAGE_SYNTAX_INVALID, DIAG_MESSAGE_UNUSED, LOCALE_CHECK_SCHEMA, LOCALE_ID_RE, LOCALE_MANIFEST_SCHEMA, LOCALE_RESERVED_TOP, LOCALE_TYPED_MODULE_SCHEMA, LOCALE_VIRTUAL_MODULE_PREFIX, MESSAGE_CATALOG_SCHEMA, MESSAGE_NODE_SCHEMA, } from './locale-schema.js';
|
|
13
13
|
/**
|
|
14
14
|
* @param {string} literal
|
|
15
15
|
*/
|
|
@@ -248,124 +248,57 @@ export function checkLocales(opts) {
|
|
|
248
248
|
const localesRoot = path.join(projectRoot, 'locales');
|
|
249
249
|
/** @type {Array<{ code: string, severity: string, message: string, path?: string }>} */
|
|
250
250
|
const diagnostics = [];
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
255
|
-
// Discipline nudge: i18n is first-class — missing policy is never silent.
|
|
256
|
-
// Soft for now (warning); production profiles may elevate to error later.
|
|
257
|
-
diagnostics.push({
|
|
258
|
-
code: DIAG_MANIFEST_MISSING,
|
|
259
|
-
severity: 'warning',
|
|
260
|
-
message: 'locales/locales.json5 missing — declare LocaleId policy under /locales (native i18n, not an afterthought)',
|
|
261
|
-
path: 'locales/locales.json5',
|
|
262
|
-
});
|
|
263
|
-
return emptyReport(projectRoot, diagnostics);
|
|
264
|
-
}
|
|
265
|
-
/** @type {any} */
|
|
266
|
-
let rawManifest = null;
|
|
267
|
-
try {
|
|
268
|
-
rawManifest = JSON5.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
269
|
-
}
|
|
270
|
-
catch (e) {
|
|
271
|
-
diagnostics.push({
|
|
272
|
-
code: DIAG_CATALOG_PARSE,
|
|
273
|
-
severity: 'error',
|
|
274
|
-
message: `locales.json5 parse failed: ${e.message || e}`,
|
|
275
|
-
path: path.relative(projectRoot, manifestPath).replace(/\\/g, '/'),
|
|
276
|
-
});
|
|
251
|
+
const plan = loadLocalePlan(projectRoot);
|
|
252
|
+
diagnostics.push(...mapPlanDiagnostics(plan.diagnostics));
|
|
253
|
+
const missingManifest = (plan.diagnostics || []).some((d) => d.code === DIAG_MANIFEST_MISSING);
|
|
254
|
+
if (missingManifest || !plan.locales?.length) {
|
|
277
255
|
return emptyReport(projectRoot, diagnostics);
|
|
278
256
|
}
|
|
279
|
-
const localeEntries =
|
|
257
|
+
const localeEntries = plan.locales || [];
|
|
280
258
|
/** @type {string[]} */
|
|
281
|
-
const orderedIds =
|
|
259
|
+
const orderedIds = localeEntries.map((e) => e.id);
|
|
282
260
|
/** @type {Set<string>} */
|
|
283
|
-
const seen = new Set();
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
diagnostics.push({
|
|
289
|
-
code: DIAG_ID_INVALID,
|
|
290
|
-
severity: 'error',
|
|
291
|
-
message: v.message,
|
|
292
|
-
path: path.relative(projectRoot, manifestPath).replace(/\\/g, '/'),
|
|
293
|
-
});
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
if (seen.has(id)) {
|
|
297
|
-
diagnostics.push({
|
|
298
|
-
code: DIAG_ID_COLLISION,
|
|
299
|
-
severity: 'error',
|
|
300
|
-
message: `duplicate LocaleId ${id} in locales[]`,
|
|
301
|
-
path: path.relative(projectRoot, manifestPath).replace(/\\/g, '/'),
|
|
302
|
-
});
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
seen.add(id);
|
|
306
|
-
orderedIds.push(id);
|
|
307
|
-
}
|
|
308
|
-
const defaultLocale = String(rawManifest.defaultLocale || '');
|
|
309
|
-
if (!defaultLocale || !seen.has(defaultLocale)) {
|
|
310
|
-
diagnostics.push({
|
|
311
|
-
code: DIAG_DEFAULT_MISSING,
|
|
312
|
-
severity: 'error',
|
|
313
|
-
message: `defaultLocale ${JSON.stringify(defaultLocale)} missing from locales[]`,
|
|
314
|
-
path: path.relative(projectRoot, manifestPath).replace(/\\/g, '/'),
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
const fallback = rawManifest.fallback && typeof rawManifest.fallback === 'object' ? rawManifest.fallback : {};
|
|
318
|
-
const fb = findFallbackCycles(fallback, seen);
|
|
319
|
-
for (const u of fb.unknown) {
|
|
320
|
-
diagnostics.push({
|
|
321
|
-
code: DIAG_FALLBACK_UNKNOWN,
|
|
322
|
-
severity: 'error',
|
|
323
|
-
message: `fallback references unknown LocaleId: ${u}`,
|
|
324
|
-
path: path.relative(projectRoot, manifestPath).replace(/\\/g, '/'),
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
for (const c of fb.cycles) {
|
|
328
|
-
diagnostics.push({
|
|
329
|
-
code: DIAG_FALLBACK_CYCLE,
|
|
330
|
-
severity: 'error',
|
|
331
|
-
message: `fallback cycle: ${c}`,
|
|
332
|
-
path: path.relative(projectRoot, manifestPath).replace(/\\/g, '/'),
|
|
333
|
-
});
|
|
334
|
-
}
|
|
261
|
+
const seen = new Set(orderedIds);
|
|
262
|
+
const defaultLocale = String(plan.defaultLocale || '');
|
|
263
|
+
const fallback = plan.fallback && typeof plan.fallback === 'object' ? plan.fallback : {};
|
|
264
|
+
const missingPolicy = plan.missing || 'error';
|
|
265
|
+
const routing = plan.routing || { strategy: 'prefix', defaultPrefix: 'include' };
|
|
335
266
|
/** @type {string[]} */
|
|
336
267
|
const diskLocales = [];
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
268
|
+
if (fs.existsSync(localesRoot)) {
|
|
269
|
+
for (const ent of fs.readdirSync(localesRoot, { withFileTypes: true })) {
|
|
270
|
+
if (LOCALE_RESERVED_TOP.has(ent.name))
|
|
271
|
+
continue;
|
|
272
|
+
if (ent.isFile()) {
|
|
273
|
+
diagnostics.push({
|
|
274
|
+
code: DIAG_LAYOUT_ILLEGAL,
|
|
275
|
+
severity: 'error',
|
|
276
|
+
message: `illegal top-level file under /locales: ${ent.name} (only locales.json5 + LocaleId dirs)`,
|
|
277
|
+
path: `locales/${ent.name}`,
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (!ent.isDirectory())
|
|
282
|
+
continue;
|
|
283
|
+
const v = validateLocaleId(ent.name);
|
|
284
|
+
if (!v.ok) {
|
|
285
|
+
diagnostics.push({
|
|
286
|
+
code: DIAG_ID_INVALID,
|
|
287
|
+
severity: 'error',
|
|
288
|
+
message: `directory ${ent.name}: ${v.message}`,
|
|
289
|
+
path: `locales/${ent.name}`,
|
|
290
|
+
});
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
diskLocales.push(ent.name);
|
|
294
|
+
if (!seen.has(ent.name)) {
|
|
295
|
+
diagnostics.push({
|
|
296
|
+
code: DIAG_DIR_ORPHAN,
|
|
297
|
+
severity: 'error',
|
|
298
|
+
message: `locale directory ${ent.name} not listed in locales.json5 locales[]`,
|
|
299
|
+
path: `locales/${ent.name}`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
369
302
|
}
|
|
370
303
|
}
|
|
371
304
|
for (const id of orderedIds) {
|
|
@@ -404,7 +337,7 @@ export function checkLocales(opts) {
|
|
|
404
337
|
});
|
|
405
338
|
return;
|
|
406
339
|
}
|
|
407
|
-
parsed =
|
|
340
|
+
parsed = parseAuthorInput(text);
|
|
408
341
|
}
|
|
409
342
|
catch (e) {
|
|
410
343
|
diagnostics.push({
|
|
@@ -448,7 +381,6 @@ export function checkLocales(opts) {
|
|
|
448
381
|
}
|
|
449
382
|
});
|
|
450
383
|
}
|
|
451
|
-
// Default locale must define every MessageId; param contracts must match across variants.
|
|
452
384
|
for (const node of messages.values()) {
|
|
453
385
|
if (defaultLocale && !node.variants[defaultLocale]) {
|
|
454
386
|
diagnostics.push({
|
|
@@ -477,7 +409,7 @@ export function checkLocales(opts) {
|
|
|
477
409
|
if (!node.variants[loc] && loc !== defaultLocale) {
|
|
478
410
|
const edges = fallback[loc] || [];
|
|
479
411
|
const canFallback = edges.some((e) => node.variants[e]);
|
|
480
|
-
if (!canFallback &&
|
|
412
|
+
if (!canFallback && missingPolicy !== 'warn') {
|
|
481
413
|
diagnostics.push({
|
|
482
414
|
code: DIAG_MESSAGE_MISSING_VARIANT,
|
|
483
415
|
severity: 'error',
|
|
@@ -488,7 +420,6 @@ export function checkLocales(opts) {
|
|
|
488
420
|
}
|
|
489
421
|
}
|
|
490
422
|
}
|
|
491
|
-
// scan source for #locales/* imports / message references.
|
|
492
423
|
const used = scanLocaleUsages(projectRoot);
|
|
493
424
|
/** @type {any[]} */
|
|
494
425
|
const typedModules = [];
|
|
@@ -559,12 +490,12 @@ export function checkLocales(opts) {
|
|
|
559
490
|
localesRoot: path.relative(projectRoot, localesRoot).replace(/\\/g, '/') || 'locales',
|
|
560
491
|
manifest: {
|
|
561
492
|
schema: LOCALE_MANIFEST_SCHEMA,
|
|
562
|
-
schemaVersion:
|
|
493
|
+
schemaVersion: 1,
|
|
563
494
|
defaultLocale,
|
|
564
495
|
locales: localeEntries,
|
|
565
496
|
fallback,
|
|
566
|
-
routing
|
|
567
|
-
missing:
|
|
497
|
+
routing,
|
|
498
|
+
missing: missingPolicy,
|
|
568
499
|
},
|
|
569
500
|
catalogIds: catalogIds.sort(),
|
|
570
501
|
messageCatalog: {
|
|
@@ -757,7 +688,7 @@ function rewriteLocaleImportsInDist(distDir) {
|
|
|
757
688
|
const files = [];
|
|
758
689
|
walkDistJs(distDir, (file) => files.push(file));
|
|
759
690
|
for (const file of files) {
|
|
760
|
-
|
|
691
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
761
692
|
if (!text.includes('#locales/'))
|
|
762
693
|
continue;
|
|
763
694
|
const fromDir = path.dirname(file);
|
package/dist/locale-cmd.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import
|
|
7
|
+
import { parseAuthorInput } from './author-input.js';
|
|
8
8
|
import { parseArgs } from './cli.js';
|
|
9
9
|
import { checkLocales, emitLocaleTypedModules, localeHasErrors, planLocaleRename } from './locale-check.js';
|
|
10
10
|
import { checkLocaleDelivery } from './locale-delivery.js';
|
|
@@ -192,7 +192,7 @@ function loadRoutesFixture(projectRoot) {
|
|
|
192
192
|
const candidates = [path.join(projectRoot, 'locale-routes.json5'), path.join(projectRoot, 'locale-routes.json')];
|
|
193
193
|
for (const p of candidates) {
|
|
194
194
|
if (fs.existsSync(p)) {
|
|
195
|
-
return
|
|
195
|
+
return parseAuthorInput(fs.readFileSync(p, 'utf8'));
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
return {
|
|
@@ -383,7 +383,7 @@ function cmdLocaleConformance(args) {
|
|
|
383
383
|
const routesPath = path.join(projectRoot, 'locale-routes.json5');
|
|
384
384
|
if (fs.existsSync(routesPath)) {
|
|
385
385
|
try {
|
|
386
|
-
const routesFile =
|
|
386
|
+
const routesFile = parseAuthorInput(fs.readFileSync(routesPath, 'utf8'));
|
|
387
387
|
routeIds = (routesFile.routes || []).map((r) => r.routeId);
|
|
388
388
|
}
|
|
389
389
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/vmz",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,15 +48,14 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@vmz/core": "0.1.
|
|
52
|
-
"@vmz/plugin": "0.1.
|
|
53
|
-
"@vmz/protocol": "0.1.
|
|
54
|
-
"jiti": "^2.6.1"
|
|
55
|
-
"json5": "^2.2.3"
|
|
51
|
+
"@vmz/core": "0.1.15",
|
|
52
|
+
"@vmz/plugin": "0.1.15",
|
|
53
|
+
"@vmz/protocol": "0.1.15",
|
|
54
|
+
"jiti": "^2.6.1"
|
|
56
55
|
},
|
|
57
56
|
"peerDependencies": {
|
|
58
|
-
"@vmz/plugin-markdown-it": "0.1.
|
|
59
|
-
"@vmz/test": "0.1.
|
|
57
|
+
"@vmz/plugin-markdown-it": "0.1.15",
|
|
58
|
+
"@vmz/test": "0.1.15",
|
|
60
59
|
"typescript": "^5.8.3"
|
|
61
60
|
},
|
|
62
61
|
"peerDependenciesMeta": {
|
|
@@ -90,12 +89,12 @@
|
|
|
90
89
|
"cli"
|
|
91
90
|
],
|
|
92
91
|
"optionalDependencies": {
|
|
93
|
-
"@vmz/vmz-win32-x64": "0.1.
|
|
94
|
-
"@vmz/vmz-win32-arm64": "0.1.
|
|
95
|
-
"@vmz/vmz-darwin-x64": "0.1.
|
|
96
|
-
"@vmz/vmz-darwin-arm64": "0.1.
|
|
97
|
-
"@vmz/vmz-linux-x64": "0.1.
|
|
98
|
-
"@vmz/vmz-linux-arm64": "0.1.
|
|
92
|
+
"@vmz/vmz-win32-x64": "0.1.15",
|
|
93
|
+
"@vmz/vmz-win32-arm64": "0.1.15",
|
|
94
|
+
"@vmz/vmz-darwin-x64": "0.1.15",
|
|
95
|
+
"@vmz/vmz-darwin-arm64": "0.1.15",
|
|
96
|
+
"@vmz/vmz-linux-x64": "0.1.15",
|
|
97
|
+
"@vmz/vmz-linux-arm64": "0.1.15"
|
|
99
98
|
},
|
|
100
99
|
"publishConfig": {
|
|
101
100
|
"access": "public"
|