@vmz/vmz 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/author-input.d.ts +19 -0
- package/dist/author-input.js +52 -0
- package/dist/build-assemble.js +18 -7
- package/dist/cdn-policy.js +5 -4
- package/dist/cli.js +106 -38
- package/dist/content-addressed-assets.d.ts +23 -2
- package/dist/content-addressed-assets.js +178 -32
- package/dist/delivery-profile.d.ts +30 -2
- package/dist/delivery-profile.js +139 -5
- package/dist/dev-session.d.ts +1 -0
- package/dist/dev-session.js +269 -65
- package/dist/dev-watch-roots.d.ts +80 -0
- package/dist/dev-watch-roots.js +245 -0
- package/dist/document-build.d.ts +4 -3
- package/dist/document-build.js +104 -82
- package/dist/document-check.d.ts +24 -5
- package/dist/document-check.js +64 -101
- package/dist/document-cmd.js +2 -9
- package/dist/document-enrich.d.ts +2 -1
- package/dist/document-enrich.js +15 -4
- package/dist/document-integrate.js +29 -21
- package/dist/document-layout-render.d.ts +20 -0
- package/dist/document-layout-render.js +87 -0
- package/dist/document-routing-config.d.ts +17 -0
- package/dist/document-routing-config.js +31 -0
- package/dist/document-schema.js +8 -1
- package/dist/embedded-packaging.js +2 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.js +63 -4
- package/dist/locale-check.js +81 -183
- package/dist/locale-cmd.js +13 -45
- package/dist/locale-route-emit.d.ts +4 -1
- package/dist/locale-route-emit.js +7 -32
- package/dist/locale-router.js +26 -8
- package/dist/native-addon.d.ts +9 -0
- package/dist/native-addon.js +84 -0
- package/dist/pack.js +3 -2
- package/dist/pretty-json.d.ts +19 -0
- package/dist/pretty-json.js +43 -0
- package/dist/production-observability.js +3 -2
- package/dist/production-test-pack.js +4 -3
- package/dist/public-static-assets.d.ts +24 -0
- package/dist/public-static-assets.js +140 -0
- package/dist/release-pack.js +5 -18
- package/dist/route-path.d.ts +35 -0
- package/dist/route-path.js +77 -0
- package/dist/server-artifact.js +4 -3
- package/dist/site-delivery.js +4 -3
- package/dist/site-favicon.d.ts +31 -0
- package/dist/site-favicon.js +140 -0
- package/dist/static-emit.d.ts +21 -0
- package/dist/static-emit.js +134 -97
- package/dist/test-cmd.js +2 -1
- package/dist/wechat-packaging.d.ts +22 -0
- package/dist/wechat-packaging.js +59 -0
- package/package.json +13 -14
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();
|
package/dist/document-cmd.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `vmz document` / `vmz docs` CLI .
|
|
4
4
|
*/
|
|
5
|
-
import fs from 'node:fs';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { buildDocuments } from './document-build.js';
|
|
8
7
|
import { checkDocuments, manifestHasErrors } from './document-check.js';
|
|
@@ -12,6 +11,7 @@ import { resolveMarkdownEngine } from './document-markdown.js';
|
|
|
12
11
|
import { createWorkspace } from './index.js';
|
|
13
12
|
import { log } from './log.js';
|
|
14
13
|
import { parseArgs } from './cli.js';
|
|
14
|
+
import { emitPrettyJson } from './pretty-json.js';
|
|
15
15
|
function printDocumentHelp() {
|
|
16
16
|
console.log(`vmz document — project /documents domain
|
|
17
17
|
|
|
@@ -95,14 +95,7 @@ async function cmdDocumentCheck(args) {
|
|
|
95
95
|
}
|
|
96
96
|
const jsonOut = args.json;
|
|
97
97
|
if (jsonOut) {
|
|
98
|
-
|
|
99
|
-
if (typeof jsonOut === 'string') {
|
|
100
|
-
fs.writeFileSync(jsonOut, text + '\n', 'utf8');
|
|
101
|
-
log.info(`wrote ${jsonOut}`);
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
console.log(text);
|
|
105
|
-
}
|
|
98
|
+
emitPrettyJson(jsonOut, manifest, { logWrote: (p) => log.info(`wrote ${p}`) });
|
|
106
99
|
}
|
|
107
100
|
else {
|
|
108
101
|
for (const d of manifest.diagnostics) {
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* @param {string} routeBase e.g. /docs
|
|
3
3
|
* @param {string} locale
|
|
4
4
|
* @param {string} pageKey
|
|
5
|
+
* @param {{ strategy?: string }} [routing]
|
|
5
6
|
*/
|
|
6
|
-
export declare function pageRoute(routeBase: any, locale: any, pageKey: any): string;
|
|
7
|
+
export declare function pageRoute(routeBase: any, locale: any, pageKey: any, routing?: {}): string;
|
|
7
8
|
/**
|
|
8
9
|
* Static file path relative to out dir (posix).
|
|
9
10
|
* @param {string} routeBase
|
package/dist/document-enrich.js
CHANGED
|
@@ -9,10 +9,16 @@ import { DIAG } from './document-schema.js';
|
|
|
9
9
|
* @param {string} routeBase e.g. /docs
|
|
10
10
|
* @param {string} locale
|
|
11
11
|
* @param {string} pageKey
|
|
12
|
+
* @param {{ strategy?: string }} [routing]
|
|
12
13
|
*/
|
|
13
|
-
export function pageRoute(routeBase, locale, pageKey) {
|
|
14
|
+
export function pageRoute(routeBase, locale, pageKey, routing = {}) {
|
|
14
15
|
const base = String(routeBase || '/').replace(/\/$/, '') || '';
|
|
15
16
|
const key = pageKey === 'index' ? '' : pageKey.replace(/\\/g, '/');
|
|
17
|
+
const strategy = routing.strategy || 'prefix';
|
|
18
|
+
if (strategy === 'none' || strategy === 'domain') {
|
|
19
|
+
const parts = [base.replace(/^\//, ''), key].filter((p) => p !== '');
|
|
20
|
+
return '/' + (parts.length ? parts.join('/') : '');
|
|
21
|
+
}
|
|
16
22
|
const parts = [base.replace(/^\//, ''), locale, key].filter((p) => p !== '');
|
|
17
23
|
return '/' + parts.join('/');
|
|
18
24
|
}
|
|
@@ -35,6 +41,7 @@ export function pageHtmlRel(routeBase, locale, pageKey) {
|
|
|
35
41
|
*/
|
|
36
42
|
export function enrichDocumentContent(manifest, ctx) {
|
|
37
43
|
const routeBase = manifest.mounts?.[0]?.routeBase || '/docs';
|
|
44
|
+
const routing = ctx.routing || { strategy: 'prefix' };
|
|
38
45
|
/** @type {Map<string, { html: string, headings: any[], links: any[], title: string, route: string, anchors: string[] }>} */
|
|
39
46
|
const byId = new Map();
|
|
40
47
|
/** @type {import('./document-schema.js').DocumentDiagnostic[]} */
|
|
@@ -45,7 +52,7 @@ export function enrichDocumentContent(manifest, ctx) {
|
|
|
45
52
|
const abs = path.isAbsolute(page.sourcePath) ? page.sourcePath : path.join(manifest.root, page.sourcePath);
|
|
46
53
|
const source = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : '';
|
|
47
54
|
const analyzed = ctx.analyzeMarkdown(source);
|
|
48
|
-
const route = pageRoute(routeBase, page.identity.locale, page.identity.pageKey);
|
|
55
|
+
const route = pageRoute(routeBase, page.identity.locale, page.identity.pageKey, routing);
|
|
49
56
|
const anchors = analyzed.headings.map((h) => h.id);
|
|
50
57
|
const title = analyzed.headings.find((h) => h.level === 1)?.text || analyzed.headings[0]?.text || page.identity.pageKey;
|
|
51
58
|
// Duplicate anchors on page
|
|
@@ -61,7 +68,11 @@ export function enrichDocumentContent(manifest, ctx) {
|
|
|
61
68
|
}
|
|
62
69
|
seen.add(id);
|
|
63
70
|
}
|
|
64
|
-
|
|
71
|
+
const owner = `${page.identity.locale}:${page.identity.pageKey}`;
|
|
72
|
+
if (routing.strategy === 'none' || routing.strategy === 'domain') {
|
|
73
|
+
routeOwners.set(route, owner);
|
|
74
|
+
}
|
|
75
|
+
else if (routeOwners.has(route)) {
|
|
65
76
|
diagnostics.push({
|
|
66
77
|
code: DIAG.ROUTE_DUPLICATE,
|
|
67
78
|
severity: 'error',
|
|
@@ -70,7 +81,7 @@ export function enrichDocumentContent(manifest, ctx) {
|
|
|
70
81
|
});
|
|
71
82
|
}
|
|
72
83
|
else {
|
|
73
|
-
routeOwners.set(route,
|
|
84
|
+
routeOwners.set(route, owner);
|
|
74
85
|
}
|
|
75
86
|
page.route = route;
|
|
76
87
|
page.anchors = anchors;
|
|
@@ -7,7 +7,10 @@ import fs from 'node:fs';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { buildDocuments } from './document-build.js';
|
|
9
9
|
import { resolveDocumentsRoot } from './document-check.js';
|
|
10
|
+
import { loadLocalesRouting } from './document-routing-config.js';
|
|
11
|
+
import { pageHtmlRel } from './document-enrich.js';
|
|
10
12
|
import { log } from './log.js';
|
|
13
|
+
import { requireNativeAddon } from './native-addon.js';
|
|
11
14
|
/**
|
|
12
15
|
* @param {string} projectRoot
|
|
13
16
|
*/
|
|
@@ -30,6 +33,7 @@ export async function buildIntegratedDocuments(opts) {
|
|
|
30
33
|
const result = await buildDocuments({
|
|
31
34
|
projectRoot,
|
|
32
35
|
outDir,
|
|
36
|
+
appDistDir: outDir,
|
|
33
37
|
strict: Boolean(opts.strict),
|
|
34
38
|
});
|
|
35
39
|
if (!result.ok) {
|
|
@@ -39,7 +43,7 @@ export async function buildIntegratedDocuments(opts) {
|
|
|
39
43
|
}
|
|
40
44
|
return { ok: false, error: 'document diagnostics', pages: 0 };
|
|
41
45
|
}
|
|
42
|
-
writeMountRootRedirects(result.manifest, outDir);
|
|
46
|
+
writeMountRootRedirects(result.manifest, outDir, projectRoot);
|
|
43
47
|
log.info(`document mount: pages=${result.pages.length} → ${path.relative(process.cwd(), outDir) || '.'}`);
|
|
44
48
|
return { ok: true, pages: result.pages.length };
|
|
45
49
|
}
|
|
@@ -50,39 +54,43 @@ export async function buildIntegratedDocuments(opts) {
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
/**
|
|
53
|
-
* Emit `{routeBase}/index.html`
|
|
57
|
+
* Emit `{routeBase}/index.html` for integrated mounts.
|
|
58
|
+
* `routing.strategy: none` → copy default-locale docs index (LocaleId is Host state).
|
|
59
|
+
* prefix strategy → redirect HTML to `{routeBase}/{defaultLocale}/`.
|
|
54
60
|
* @param {import('./document-schema.js').DocumentManifest} manifest
|
|
55
61
|
* @param {string} outDir
|
|
62
|
+
* @param {string} projectRoot
|
|
56
63
|
*/
|
|
57
|
-
function writeMountRootRedirects(manifest, outDir) {
|
|
64
|
+
function writeMountRootRedirects(manifest, outDir, projectRoot) {
|
|
58
65
|
const defaultLocale = manifest.defaultLocale || manifest.locales?.[0];
|
|
59
66
|
if (!defaultLocale)
|
|
60
67
|
return;
|
|
68
|
+
const routing = loadLocalesRouting(projectRoot) || { strategy: 'prefix' };
|
|
61
69
|
for (const mount of manifest.mounts || []) {
|
|
62
70
|
if (!mount?.routeBase || mount.routeBase === '/')
|
|
63
71
|
continue;
|
|
64
72
|
const base = String(mount.routeBase).replace(/\/$/, '');
|
|
65
|
-
const target = `${base}/${defaultLocale}/`;
|
|
66
73
|
const relDir = base.replace(/^\//, '');
|
|
67
74
|
const abs = path.join(outDir, relDir, 'index.html');
|
|
68
75
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
if (routing.strategy === 'none' || routing.strategy === 'domain') {
|
|
77
|
+
const srcRel = pageHtmlRel(base, defaultLocale, 'index');
|
|
78
|
+
const srcAbs = path.join(outDir, srcRel);
|
|
79
|
+
if (fs.existsSync(srcAbs)) {
|
|
80
|
+
fs.copyFileSync(srcAbs, abs);
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const target = `${base}/${defaultLocale}/`;
|
|
85
|
+
const native = requireNativeAddon();
|
|
86
|
+
if (typeof native.generateRedirectHtml !== 'function') {
|
|
87
|
+
throw new Error('vmz native addon missing generateRedirectHtml — rebuild with `pnpm napi:build`');
|
|
88
|
+
}
|
|
89
|
+
const html = native.generateRedirectHtml({
|
|
90
|
+
lang: defaultLocale,
|
|
91
|
+
target,
|
|
92
|
+
title: 'Documents',
|
|
93
|
+
});
|
|
82
94
|
fs.writeFileSync(abs, html, 'utf8');
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
|
-
/** @param {string} s */
|
|
86
|
-
function escapeAttr(s) {
|
|
87
|
-
return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
88
|
-
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integrated DocumentMount — compile host chrome via DocumentLayout + createRenderHost.
|
|
3
|
+
* Replaces the removed regex template lowering in document-host-chrome.ts.
|
|
4
|
+
*/
|
|
5
|
+
/** @param {string} distDir */
|
|
6
|
+
export declare function resolveDocumentLayoutChunkId(distDir: any): string;
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} distDir
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertIntegratedDistReady(distDir: any): string;
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} html
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertCompiledShellHtml(html: any): void;
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} distDir
|
|
17
|
+
* @param {string} localeId
|
|
18
|
+
* @param {string} slotHtml
|
|
19
|
+
*/
|
|
20
|
+
export declare function renderCompiledDocumentLayout(distDir: any, localeId: any, slotHtml: any): Promise<any>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Integrated DocumentMount — compile host chrome via DocumentLayout + createRenderHost.
|
|
4
|
+
* Replaces the removed regex template lowering in document-host-chrome.ts.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { createRenderHost } from '@vmz/core/render-host';
|
|
10
|
+
/** @param {string} distDir */
|
|
11
|
+
export function resolveDocumentLayoutChunkId(distDir) {
|
|
12
|
+
for (const chunkId of ['layouts/DocumentLayout', 'components/DocumentLayout']) {
|
|
13
|
+
if (fs.existsSync(path.join(distDir, `${chunkId}.client.js`)))
|
|
14
|
+
return chunkId;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} distDir
|
|
20
|
+
*/
|
|
21
|
+
export function assertIntegratedDistReady(distDir) {
|
|
22
|
+
const dom = path.join(distDir, 'vmz-dom.js');
|
|
23
|
+
if (!fs.existsSync(dom)) {
|
|
24
|
+
throw new Error('integrated document mount requires vmz build output (vmz-dom.js in app dist). Run `vmz build` before document emit.');
|
|
25
|
+
}
|
|
26
|
+
const chunkId = resolveDocumentLayoutChunkId(distDir);
|
|
27
|
+
if (!chunkId) {
|
|
28
|
+
throw new Error('integrated document mount requires compiled DocumentLayout (add src/layouts/DocumentLayout.vmz and rebuild)');
|
|
29
|
+
}
|
|
30
|
+
return chunkId;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} html
|
|
34
|
+
*/
|
|
35
|
+
export function assertCompiledShellHtml(html) {
|
|
36
|
+
const header = html.match(/<header[^>]*data-vmz-fixture="site-header"[\s\S]*?<\/header>/i)?.[0] ?? '';
|
|
37
|
+
const footer = html.match(/<footer[^>]*data-vmz-fixture="site-footer"[\s\S]*?<\/footer>/i)?.[0] ?? '';
|
|
38
|
+
for (const part of [header, footer]) {
|
|
39
|
+
const leak = part.match(/\{[A-Za-z][A-Za-z0-9]*\}/);
|
|
40
|
+
if (leak) {
|
|
41
|
+
throw new Error(`document layout SSR leaked binding placeholder ${leak[0]}`);
|
|
42
|
+
}
|
|
43
|
+
if (/<(?:Link|Button|Icon)\b/.test(part)) {
|
|
44
|
+
throw new Error('document layout SSR leaked uncompiled VMZ component tag in chrome');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!header || !footer) {
|
|
48
|
+
throw new Error('document layout SSR missing compiled SiteHeader/SiteFooter fixtures');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} distDir
|
|
53
|
+
* @param {string} chunkId
|
|
54
|
+
*/
|
|
55
|
+
async function loadCtor(distDir, chunkId) {
|
|
56
|
+
const href = pathToFileURL(path.join(distDir, `${chunkId}.client.js`)).href;
|
|
57
|
+
const mod = await import(`${href}?t=${Date.now()}`);
|
|
58
|
+
return mod.default;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} distDir
|
|
62
|
+
* @param {string} localeId
|
|
63
|
+
* @param {string} slotHtml
|
|
64
|
+
*/
|
|
65
|
+
export async function renderCompiledDocumentLayout(distDir, localeId, slotHtml) {
|
|
66
|
+
const chunkId = assertIntegratedDistReady(distDir);
|
|
67
|
+
const prevHint = globalThis.__vmzLocaleIdHint;
|
|
68
|
+
globalThis.__vmzLocaleIdHint = localeId;
|
|
69
|
+
try {
|
|
70
|
+
if (typeof globalThis.document !== 'undefined' && globalThis.document?.documentElement) {
|
|
71
|
+
globalThis.document.documentElement.setAttribute('data-locale', localeId);
|
|
72
|
+
globalThis.document.documentElement.setAttribute('lang', localeId);
|
|
73
|
+
}
|
|
74
|
+
const host = await createRenderHost(distDir, { strictDeployment: true, preload: 'none' });
|
|
75
|
+
await host.ensureComponents([chunkId]);
|
|
76
|
+
const Layout = await loadCtor(distDir, chunkId);
|
|
77
|
+
const html = await host.renderToString(Layout, {}, { slotHtml });
|
|
78
|
+
assertCompiledShellHtml(html);
|
|
79
|
+
return html;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
if (prevHint === undefined)
|
|
83
|
+
delete globalThis.__vmzLocaleIdHint;
|
|
84
|
+
else
|
|
85
|
+
globalThis.__vmzLocaleIdHint = prevHint;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project locale routing — consume Rust LocalePlan (no author JSON5 in TS).
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} projectRoot
|
|
6
|
+
* @returns {{ strategy?: string, defaultPrefix?: string, defaultLocale?: string } | null}
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadLocalesRouting(projectRoot: any): {
|
|
9
|
+
strategy: any;
|
|
10
|
+
defaultPrefix: any;
|
|
11
|
+
defaultLocale: any;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} routeBase
|
|
15
|
+
* @param {string} pageKey
|
|
16
|
+
*/
|
|
17
|
+
export declare function docsRouteNone(routeBase: any, pageKey: any): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Project locale routing — consume Rust LocalePlan (no author JSON5 in TS).
|
|
4
|
+
*/
|
|
5
|
+
import { loadLocalePlan } from './author-input.js';
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} projectRoot
|
|
8
|
+
* @returns {{ strategy?: string, defaultPrefix?: string, defaultLocale?: string } | null}
|
|
9
|
+
*/
|
|
10
|
+
export function loadLocalesRouting(projectRoot) {
|
|
11
|
+
const plan = loadLocalePlan(projectRoot);
|
|
12
|
+
if (!plan || plan.diagnostics?.some((d) => d.code === 'vmz::locale::manifest_missing')) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const routing = plan.routing || {};
|
|
16
|
+
return {
|
|
17
|
+
strategy: routing.strategy || 'prefix',
|
|
18
|
+
defaultPrefix: routing.defaultPrefix || 'include',
|
|
19
|
+
defaultLocale: plan.defaultLocale || undefined,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} routeBase
|
|
24
|
+
* @param {string} pageKey
|
|
25
|
+
*/
|
|
26
|
+
export function docsRouteNone(routeBase, pageKey) {
|
|
27
|
+
const base = String(routeBase || '/').replace(/\/$/, '') || '';
|
|
28
|
+
const key = pageKey === 'index' ? '' : pageKey.replace(/\\/g, '/');
|
|
29
|
+
const parts = [base.replace(/^\//, ''), key].filter((p) => p !== '');
|
|
30
|
+
return `/${parts.length ? parts.join('/') : ''}`;
|
|
31
|
+
}
|
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',
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
+
import { writePrettyJsonFile } from './pretty-json.js';
|
|
9
10
|
export const EMBEDDED_RESOURCE_INDEX_SCHEMA = 'vmz.embedded.resource_index.v0';
|
|
10
11
|
/** Paths that must never enter the embedded baseline closure. */
|
|
11
12
|
const SKIP_NAMES = new Set(['vmz-serve-host.mjs', 'vmz-serve-host.js', 'node_modules', '.git']);
|
|
@@ -55,7 +56,7 @@ export function emitEmbeddedPackaging(outDir, opts = {}) {
|
|
|
55
56
|
};
|
|
56
57
|
index.indexDigest = sha256Hex(canonicalJson({ ...index, indexDigest: undefined }));
|
|
57
58
|
const indexPath = path.join(vmzDir, 'embedded-resource-index.json');
|
|
58
|
-
|
|
59
|
+
writePrettyJsonFile(indexPath, index);
|
|
59
60
|
// Optional include_bytes entry point for Rust packaging adapters
|
|
60
61
|
const rsPath = path.join(vmzDir, 'embedded_site.rs');
|
|
61
62
|
fs.writeFileSync(rsPath, `// @generated by vmz — rust-embedded packaging adapter (do not edit)
|