@vmz/vmz 0.1.14 → 0.1.16

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.
@@ -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
+ }
@@ -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: {
@@ -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
- * Parse documents.config.json or a JSON-compatible export-default .ts/.js.
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: any;
16
- diagnostics: any[];
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: {
@@ -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();
@@ -1,11 +1,15 @@
1
1
  /**
2
- * Project locale routing config for document mount (locales/locales.json5).
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): 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 config for document mount (locales/locales.json5).
3
+ * Project locale routing consume Rust LocalePlan (no author JSON5 in TS).
4
4
  */
5
- import fs from 'node:fs';
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 p = path.join(projectRoot, 'locales', 'locales.json5');
13
- if (!fs.existsSync(p))
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 '/' + (parts.length ? parts.join('/') : '');
30
+ return `/${parts.length ? parts.join('/') : ''}`;
40
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,10 +6,10 @@
6
6
  */
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
- import JSON5 from 'json5';
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 manifestPathJson5 = path.join(localesRoot, 'locales.json5');
252
- const manifestPathJson = path.join(localesRoot, 'locales.json');
253
- const manifestPath = fs.existsSync(manifestPathJson5) ? manifestPathJson5 : fs.existsSync(manifestPathJson) ? manifestPathJson : null;
254
- if (!fs.existsSync(localesRoot) || !manifestPath) {
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 = Array.isArray(rawManifest.locales) ? rawManifest.locales : [];
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
- for (const entry of localeEntries) {
285
- const id = String(entry?.id || '');
286
- const v = validateLocaleId(id);
287
- if (!v.ok) {
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
- for (const ent of fs.readdirSync(localesRoot, { withFileTypes: true })) {
338
- if (LOCALE_RESERVED_TOP.has(ent.name))
339
- continue;
340
- if (ent.isFile()) {
341
- diagnostics.push({
342
- code: DIAG_LAYOUT_ILLEGAL,
343
- severity: 'error',
344
- message: `illegal top-level file under /locales: ${ent.name} (only locales.json5 + LocaleId dirs)`,
345
- path: `locales/${ent.name}`,
346
- });
347
- continue;
348
- }
349
- if (!ent.isDirectory())
350
- continue;
351
- const v = validateLocaleId(ent.name);
352
- if (!v.ok) {
353
- diagnostics.push({
354
- code: DIAG_ID_INVALID,
355
- severity: 'error',
356
- message: `directory ${ent.name}: ${v.message}`,
357
- path: `locales/${ent.name}`,
358
- });
359
- continue;
360
- }
361
- diskLocales.push(ent.name);
362
- if (!seen.has(ent.name)) {
363
- diagnostics.push({
364
- code: DIAG_DIR_ORPHAN,
365
- severity: 'error',
366
- message: `locale directory ${ent.name} not listed in locales.json5 locales[]`,
367
- path: `locales/${ent.name}`,
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 = JSON5.parse(text);
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 && rawManifest.missing !== 'warn') {
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: rawManifest.schemaVersion ?? 1,
493
+ schemaVersion: 1,
563
494
  defaultLocale,
564
495
  locales: localeEntries,
565
496
  fallback,
566
- routing: rawManifest.routing || { strategy: 'prefix', defaultPrefix: 'include' },
567
- missing: rawManifest.missing || 'error',
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
- let text = fs.readFileSync(file, 'utf8');
691
+ const text = fs.readFileSync(file, 'utf8');
761
692
  if (!text.includes('#locales/'))
762
693
  continue;
763
694
  const fromDir = path.dirname(file);
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import JSON5 from 'json5';
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 JSON5.parse(fs.readFileSync(p, 'utf8'));
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 = JSON5.parse(fs.readFileSync(routesPath, 'utf8'));
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.14",
3
+ "version": "0.1.16",
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.14",
52
- "@vmz/plugin": "0.1.14",
53
- "@vmz/protocol": "0.1.14",
54
- "jiti": "^2.6.1",
55
- "json5": "^2.2.3"
51
+ "@vmz/core": "0.1.16",
52
+ "@vmz/plugin": "0.1.16",
53
+ "@vmz/protocol": "0.1.16",
54
+ "jiti": "^2.6.1"
56
55
  },
57
56
  "peerDependencies": {
58
- "@vmz/plugin-markdown-it": "0.1.14",
59
- "@vmz/test": "0.1.14",
57
+ "@vmz/plugin-markdown-it": "0.1.16",
58
+ "@vmz/test": "0.1.16",
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.14",
94
- "@vmz/vmz-win32-arm64": "0.1.14",
95
- "@vmz/vmz-darwin-x64": "0.1.14",
96
- "@vmz/vmz-darwin-arm64": "0.1.14",
97
- "@vmz/vmz-linux-x64": "0.1.14",
98
- "@vmz/vmz-linux-arm64": "0.1.14"
92
+ "@vmz/vmz-win32-x64": "0.1.16",
93
+ "@vmz/vmz-win32-arm64": "0.1.16",
94
+ "@vmz/vmz-darwin-x64": "0.1.16",
95
+ "@vmz/vmz-darwin-arm64": "0.1.16",
96
+ "@vmz/vmz-linux-x64": "0.1.16",
97
+ "@vmz/vmz-linux-arm64": "0.1.16"
99
98
  },
100
99
  "publishConfig": {
101
100
  "access": "public"