@vmz/vmz 0.1.9 → 0.1.11
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/cli.js +6 -8
- package/dist/content-addressed-assets.d.ts +7 -1
- package/dist/content-addressed-assets.js +107 -32
- package/dist/document-build.js +37 -6
- package/dist/document-enrich.d.ts +2 -1
- package/dist/document-enrich.js +15 -4
- package/dist/document-host-chrome.d.ts +28 -0
- package/dist/document-host-chrome.js +128 -0
- package/dist/document-integrate.js +18 -4
- package/dist/index.d.ts +10 -0
- package/dist/index.js +28 -1
- package/dist/static-emit.js +53 -3
- package/package.json +12 -12
package/dist/cli.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Node CLI command implementations .
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
|
-
import {
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntimeDist, resolveNativePath } from './index.js';
|
|
8
|
+
import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, materializeServeHostRuntime, resolveCoreRuntimeDist, resolveNativePath, } from './index.js';
|
|
9
9
|
import { createDevSession } from './dev-session.js';
|
|
10
10
|
import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from './invocation.js';
|
|
11
11
|
import { log } from './log.js';
|
|
@@ -501,14 +501,12 @@ async function cmdServe(args) {
|
|
|
501
501
|
});
|
|
502
502
|
const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
|
|
503
503
|
if (!existsSync(hostJs)) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if (src && existsSync(src)) {
|
|
507
|
-
copyFileSync(src, hostJs);
|
|
504
|
+
try {
|
|
505
|
+
materializeServeHostRuntime(outDir);
|
|
508
506
|
log.info(`materialized ${hostJs} from @vmz/core (release builds omit it)`);
|
|
509
507
|
}
|
|
510
|
-
|
|
511
|
-
log.error(`missing ${hostJs} — run \`vmz build\` (without --release) or ensure @vmz/core is installed`);
|
|
508
|
+
catch (err) {
|
|
509
|
+
log.error(`missing ${hostJs} — run \`vmz build\` (without --release) or ensure @vmz/core is installed (${err instanceof Error ? err.message : err})`);
|
|
512
510
|
return 1;
|
|
513
511
|
}
|
|
514
512
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A3: content-addressed assets/<hash> layout for immutable CDN objects.
|
|
3
3
|
* Logical paths stay available for serve/dev; static HTML rewrites to hashed URLs.
|
|
4
|
-
*
|
|
4
|
+
* CSS aggregators (vmz.css) rewrite `@import` to hashed sibling paths under assets/.
|
|
5
5
|
*/
|
|
6
6
|
export declare const CONTENT_ADDRESSED_ASSETS_SCHEMA = "vmz.content_addressed_assets.v0";
|
|
7
|
+
/**
|
|
8
|
+
* Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
|
|
9
|
+
* @param {string} cssText
|
|
10
|
+
* @param {Record<string, string>} rewrites logical (no leading slash) or `/logical` → `assets/hash.ext`
|
|
11
|
+
*/
|
|
12
|
+
export declare function rewriteCssImports(cssText: any, rewrites: any): any;
|
|
7
13
|
/**
|
|
8
14
|
* Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
|
|
9
15
|
* @param {string} distDir
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A3: content-addressed assets/<hash> layout for immutable CDN objects.
|
|
3
3
|
* Logical paths stay available for serve/dev; static HTML rewrites to hashed URLs.
|
|
4
|
-
*
|
|
4
|
+
* CSS aggregators (vmz.css) rewrite `@import` to hashed sibling paths under assets/.
|
|
5
5
|
*/
|
|
6
6
|
// @ts-nocheck
|
|
7
7
|
import crypto from 'node:crypto';
|
|
@@ -16,11 +16,33 @@ const DEFAULT_CANDIDATES = [
|
|
|
16
16
|
'entry-event.js',
|
|
17
17
|
'vmz.css',
|
|
18
18
|
'vmz-designs.css',
|
|
19
|
+
'vmz-style.css',
|
|
19
20
|
'vmz-dom.js',
|
|
20
21
|
'vmz-runtime.js',
|
|
21
22
|
'vmz-http.js',
|
|
22
23
|
'vmz-client-nav.js',
|
|
23
24
|
];
|
|
25
|
+
/** CSS files that may @import other logical CSS; processed after leaf CSS is hashed. */
|
|
26
|
+
const CSS_AGGREGATORS = new Set(['vmz.css']);
|
|
27
|
+
const CSS_IMPORT_RE = /@import\s*(?:url\()?['"]?(\.\/)?([^'")\s;]+)['"]?\)?/gi;
|
|
28
|
+
/**
|
|
29
|
+
* Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
|
|
30
|
+
* @param {string} cssText
|
|
31
|
+
* @param {Record<string, string>} rewrites logical (no leading slash) or `/logical` → `assets/hash.ext`
|
|
32
|
+
*/
|
|
33
|
+
export function rewriteCssImports(cssText, rewrites) {
|
|
34
|
+
return cssText.replace(CSS_IMPORT_RE, (match, _dot, target) => {
|
|
35
|
+
const logical = String(target || '').replace(/^\.\//, '');
|
|
36
|
+
if (!logical)
|
|
37
|
+
return match;
|
|
38
|
+
const hashed = rewrites[logical] || rewrites[`/${logical}`] || rewrites[`assets/${logical}`];
|
|
39
|
+
if (!hashed)
|
|
40
|
+
return match;
|
|
41
|
+
const rel = hashed.startsWith('/') ? hashed.slice(1) : hashed;
|
|
42
|
+
const sibling = rel.startsWith('assets/') ? `./${path.basename(rel)}` : `./${rel}`;
|
|
43
|
+
return `@import"${sibling}"`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
24
46
|
/**
|
|
25
47
|
* Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
|
|
26
48
|
* @param {string} distDir
|
|
@@ -38,36 +60,24 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
|
|
|
38
60
|
const objects = [];
|
|
39
61
|
/** @type {Record<string, string>} */
|
|
40
62
|
const rewrites = {};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
const ordered = orderCandidates(candidates);
|
|
64
|
+
for (const rel of ordered) {
|
|
65
|
+
ingestCandidate(abs, rel, rewrites, objects, { transform: null });
|
|
66
|
+
}
|
|
67
|
+
// Aggregator CSS (vmz.css) must import hashed leaf files — rewrite then hash.
|
|
68
|
+
for (const rel of ordered) {
|
|
69
|
+
if (!CSS_AGGREGATORS.has(rel))
|
|
45
70
|
continue;
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
else {
|
|
56
|
-
// Cross-release reuse: identical digest must not be rewritten.
|
|
57
|
-
const existing = sha256Hex(fs.readFileSync(dest));
|
|
58
|
-
if (existing !== digest) {
|
|
59
|
-
throw new Error(`content-address collision at ${assetRel}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
objects.push({
|
|
63
|
-
logicalPath: logical,
|
|
64
|
-
assetPath: assetRel,
|
|
65
|
-
digest,
|
|
66
|
-
bytes: buf.length,
|
|
67
|
-
immutable: true,
|
|
71
|
+
const src = path.join(abs, rel);
|
|
72
|
+
if (!fs.existsSync(src))
|
|
73
|
+
continue;
|
|
74
|
+
const rewritten = rewriteCssImports(fs.readFileSync(src, 'utf8'), rewrites);
|
|
75
|
+
removeLogicalObject(objects, rel);
|
|
76
|
+
delete rewrites[`/${rel}`];
|
|
77
|
+
delete rewrites[rel];
|
|
78
|
+
ingestCandidate(abs, rel, rewrites, objects, {
|
|
79
|
+
transform: () => Buffer.from(rewritten, 'utf8'),
|
|
68
80
|
});
|
|
69
|
-
rewrites[`/${logical}`] = `/${assetRel}`;
|
|
70
|
-
rewrites[logical] = assetRel;
|
|
71
81
|
}
|
|
72
82
|
objects.sort((a, b) => (a.logicalPath < b.logicalPath ? -1 : a.logicalPath > b.logicalPath ? 1 : 0));
|
|
73
83
|
let rewrittenHtml = 0;
|
|
@@ -89,6 +99,74 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
|
|
|
89
99
|
writePrettyJsonFile(outPath, manifest);
|
|
90
100
|
return { manifest, assetsDir, rewrites, manifestPath: outPath };
|
|
91
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* @param {string[]} candidates
|
|
104
|
+
*/
|
|
105
|
+
function orderCandidates(candidates) {
|
|
106
|
+
const set = new Set(candidates.map((c) => String(c).replace(/\\/g, '/').replace(/^\//, '')));
|
|
107
|
+
/** @type {string[]} */
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const name of DEFAULT_CANDIDATES) {
|
|
110
|
+
if (set.has(name) && !CSS_AGGREGATORS.has(name))
|
|
111
|
+
out.push(name);
|
|
112
|
+
}
|
|
113
|
+
for (const name of [...set].sort()) {
|
|
114
|
+
if (!CSS_AGGREGATORS.has(name) && !out.includes(name))
|
|
115
|
+
out.push(name);
|
|
116
|
+
}
|
|
117
|
+
if (set.has('vmz.css'))
|
|
118
|
+
out.push('vmz.css');
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* @param {Array<Record<string, any>>} objects
|
|
123
|
+
* @param {string} logical
|
|
124
|
+
*/
|
|
125
|
+
function removeLogicalObject(objects, logical) {
|
|
126
|
+
const idx = objects.findIndex((o) => o.logicalPath === logical);
|
|
127
|
+
if (idx >= 0)
|
|
128
|
+
objects.splice(idx, 1);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} absDist
|
|
132
|
+
* @param {string} rel
|
|
133
|
+
* @param {Record<string, string>} rewrites
|
|
134
|
+
* @param {Array<Record<string, any>>} objects
|
|
135
|
+
* @param {{ transform?: ((buf: Buffer) => Buffer) | null }} opts
|
|
136
|
+
*/
|
|
137
|
+
function ingestCandidate(absDist, rel, rewrites, objects, opts) {
|
|
138
|
+
const logical = String(rel).replace(/\\/g, '/').replace(/^\//, '');
|
|
139
|
+
const src = path.join(absDist, ...logical.split('/'));
|
|
140
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isFile())
|
|
141
|
+
return;
|
|
142
|
+
let buf = fs.readFileSync(src);
|
|
143
|
+
if (typeof opts.transform === 'function') {
|
|
144
|
+
buf = opts.transform(buf);
|
|
145
|
+
}
|
|
146
|
+
const digest = sha256Hex(buf);
|
|
147
|
+
const ext = path.extname(logical) || '';
|
|
148
|
+
const assetRel = `assets/${digest}${ext}`;
|
|
149
|
+
const dest = path.join(absDist, ...assetRel.split('/'));
|
|
150
|
+
if (!fs.existsSync(dest)) {
|
|
151
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
152
|
+
fs.writeFileSync(dest, buf);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const existing = sha256Hex(fs.readFileSync(dest));
|
|
156
|
+
if (existing !== digest) {
|
|
157
|
+
throw new Error(`content-address collision at ${assetRel}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
objects.push({
|
|
161
|
+
logicalPath: logical,
|
|
162
|
+
assetPath: assetRel,
|
|
163
|
+
digest,
|
|
164
|
+
bytes: buf.length,
|
|
165
|
+
immutable: true,
|
|
166
|
+
});
|
|
167
|
+
rewrites[`/${logical}`] = `/${assetRel}`;
|
|
168
|
+
rewrites[logical] = assetRel;
|
|
169
|
+
}
|
|
92
170
|
/**
|
|
93
171
|
* Resolve an immutable object by digest under dist/assets (cross-source reuse).
|
|
94
172
|
* @param {string} distDir
|
|
@@ -124,7 +202,6 @@ export function assertSharedAssetPath(distDir, a, b, ext = '.js') {
|
|
|
124
202
|
const rel = `assets/${da}${ext}`;
|
|
125
203
|
const dest = path.join(distDir, ...rel.split('/'));
|
|
126
204
|
fs.writeFileSync(dest, typeof a === 'string' ? Buffer.from(a) : a);
|
|
127
|
-
// Second write of identical bytes must be reuse, not fork.
|
|
128
205
|
fs.writeFileSync(dest, typeof b === 'string' ? Buffer.from(b) : b);
|
|
129
206
|
const again = resolveAssetByDigest(distDir, da, ext);
|
|
130
207
|
if (!again || again.assetPath !== rel) {
|
|
@@ -139,7 +216,6 @@ function collectCandidates(distDir) {
|
|
|
139
216
|
if (fs.existsSync(path.join(distDir, name)))
|
|
140
217
|
out.push(name);
|
|
141
218
|
}
|
|
142
|
-
// Include top-level *.client.js and pages/**/*.client.js referenced by resume.
|
|
143
219
|
walk(distDir, distDir, (rel) => {
|
|
144
220
|
if (/\.client\.js$/i.test(rel))
|
|
145
221
|
out.push(rel);
|
|
@@ -173,7 +249,6 @@ function rewriteHtmlReferences(distDir, rewrites) {
|
|
|
173
249
|
let text = fs.readFileSync(file, 'utf8');
|
|
174
250
|
let next = text;
|
|
175
251
|
for (const [from, to] of pairs) {
|
|
176
|
-
// href="/x" src="/x" and unquoted variants in attributes
|
|
177
252
|
next = next.split(from).join(to);
|
|
178
253
|
}
|
|
179
254
|
if (next !== text) {
|
package/dist/document-build.js
CHANGED
|
@@ -8,6 +8,7 @@ import { checkDocuments, manifestHasErrors } from './document-check.js';
|
|
|
8
8
|
import { resolveDocumentDesignsCss } from './document-designs.js';
|
|
9
9
|
import { enrichDocumentContent, pageHtmlRel } from './document-enrich.js';
|
|
10
10
|
import { enrichDocumentEvidence } from './document-evidence.js';
|
|
11
|
+
import { docsRouteNone, loadLocaleCommonMessages, loadLocalesRouting, localeNonePickerScript, readSiteGithubUrl, renderHostChromeTemplate, } from './document-host-chrome.js';
|
|
11
12
|
import { artifactHrefFromHtml, buildDocumentIslands, buildDocumentSearch, collectFenceBodies, renderIslandShellsHtml, } from './document-interactive.js';
|
|
12
13
|
import { resolveMarkdownEngine } from './document-markdown.js';
|
|
13
14
|
import { DOCUMENT_VIEW_SCHEMA } from './document-schema.js';
|
|
@@ -22,10 +23,12 @@ export async function buildDocuments(opts) {
|
|
|
22
23
|
const outDir = path.resolve(opts.outDir || path.join(projectRoot, 'dist', 'documents'));
|
|
23
24
|
const strict = Boolean(opts.strict);
|
|
24
25
|
const manifest = checkDocuments({ projectRoot, strict });
|
|
26
|
+
const routing = loadLocalesRouting(projectRoot) || { strategy: 'prefix' };
|
|
25
27
|
const engine = await resolveMarkdownEngine({ engines: opts.engines, projectRoot });
|
|
26
28
|
const enriched = enrichDocumentContent(manifest, {
|
|
27
29
|
analyzeMarkdown: engine.analyzeMarkdown,
|
|
28
30
|
projectRoot,
|
|
31
|
+
routing,
|
|
29
32
|
});
|
|
30
33
|
manifest.diagnostics = enriched.diagnostics;
|
|
31
34
|
const evidence = await enrichDocumentEvidence(manifest, {
|
|
@@ -60,8 +63,8 @@ export async function buildDocuments(opts) {
|
|
|
60
63
|
});
|
|
61
64
|
manifest.search = search;
|
|
62
65
|
manifest.islands = islands;
|
|
63
|
-
const
|
|
64
|
-
const useHostShell = Boolean(
|
|
66
|
+
const hostChromeRaw = resolveHostSiteChromeRaw(projectRoot);
|
|
67
|
+
const useHostShell = Boolean(hostChromeRaw) && (manifest.mounts || []).some((m) => m.mode === 'integrated');
|
|
65
68
|
fs.mkdirSync(outDir, { recursive: true });
|
|
66
69
|
const designs = resolveDocumentDesignsCss(projectRoot);
|
|
67
70
|
/** @type {string | null} */
|
|
@@ -127,7 +130,12 @@ export async function buildDocuments(opts) {
|
|
|
127
130
|
htmlRel,
|
|
128
131
|
searchShellHtml: shells.searchHtml,
|
|
129
132
|
playgroundShellHtml: shells.playgroundHtml,
|
|
130
|
-
hostChrome: useHostShell
|
|
133
|
+
hostChrome: useHostShell
|
|
134
|
+
? renderHostChromeForLocale(projectRoot, page.identity.locale, routing, enriched.routeBase, hostChromeRaw)
|
|
135
|
+
: null,
|
|
136
|
+
routing,
|
|
137
|
+
routeBase: enriched.routeBase,
|
|
138
|
+
pageKey: page.identity.pageKey,
|
|
131
139
|
});
|
|
132
140
|
fs.writeFileSync(htmlAbs, html, 'utf8');
|
|
133
141
|
written.push({ route: info.route, htmlPath: htmlRel, viewPath: viewRel });
|
|
@@ -159,7 +167,7 @@ export async function buildDocuments(opts) {
|
|
|
159
167
|
* No-JS readable static HTML: nav + main landmarks, Island shells without scripts.
|
|
160
168
|
* Integrated mounts reuse host SiteHeader/SiteFooter templates when present.
|
|
161
169
|
*/
|
|
162
|
-
function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, designsHref, htmlRel, searchShellHtml = '', playgroundShellHtml = '', hostChrome = null, }) {
|
|
170
|
+
function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, designsHref, htmlRel, searchShellHtml = '', playgroundShellHtml = '', hostChrome = null, routing = { strategy: 'prefix' }, routeBase = '/docs', pageKey = 'index', }) {
|
|
163
171
|
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
164
172
|
const depth = htmlRel.split('/').length - 1;
|
|
165
173
|
const prefix = depth > 0 ? '../'.repeat(depth) : './';
|
|
@@ -174,7 +182,7 @@ function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, desig
|
|
|
174
182
|
cssHrefs.push(hostChrome ? `/${designsHref}` : prefix + designsHref);
|
|
175
183
|
const navItems = nav
|
|
176
184
|
.map((n) => {
|
|
177
|
-
const href = relativeHref(htmlRel, n.href, route);
|
|
185
|
+
const href = routing.strategy === 'none' || routing.strategy === 'domain' ? n.href : relativeHref(htmlRel, n.href, route);
|
|
178
186
|
const current = n.href === route ? ' aria-current="page"' : '';
|
|
179
187
|
return ` <li><a href="${esc(href)}"${current}>${esc(n.title)}</a></li>`;
|
|
180
188
|
})
|
|
@@ -210,6 +218,7 @@ ${playgroundShellHtml}
|
|
|
210
218
|
</div>
|
|
211
219
|
${hostChrome.footer}
|
|
212
220
|
</div>
|
|
221
|
+
${routing.strategy === 'none' && hostChrome ? localeNonePickerScript() : ''}
|
|
213
222
|
`;
|
|
214
223
|
}
|
|
215
224
|
else {
|
|
@@ -239,7 +248,7 @@ ${playgroundShellHtml}
|
|
|
239
248
|
* @param {string} projectRoot
|
|
240
249
|
* @returns {{ header: string, footer: string } | null}
|
|
241
250
|
*/
|
|
242
|
-
function
|
|
251
|
+
function resolveHostSiteChromeRaw(projectRoot) {
|
|
243
252
|
const headerPath = path.join(projectRoot, 'src', 'components', 'SiteHeader.vmz');
|
|
244
253
|
const footerPath = path.join(projectRoot, 'src', 'components', 'SiteFooter.vmz');
|
|
245
254
|
if (!fs.existsSync(headerPath) || !fs.existsSync(footerPath))
|
|
@@ -250,6 +259,28 @@ function resolveHostSiteChrome(projectRoot) {
|
|
|
250
259
|
return null;
|
|
251
260
|
return { header, footer };
|
|
252
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* @param {string} projectRoot
|
|
264
|
+
* @param {string} localeId
|
|
265
|
+
* @param {{ strategy?: string }} routing
|
|
266
|
+
* @param {string} routeBase
|
|
267
|
+
* @param {{ header: string, footer: string }} raw
|
|
268
|
+
*/
|
|
269
|
+
function renderHostChromeForLocale(projectRoot, localeId, routing, routeBase, raw) {
|
|
270
|
+
const messages = loadLocaleCommonMessages(projectRoot, localeId);
|
|
271
|
+
const githubUrl = readSiteGithubUrl(projectRoot);
|
|
272
|
+
const docsRootHref = routing.strategy === 'none' || routing.strategy === 'domain'
|
|
273
|
+
? `${routeBase.replace(/\/$/, '')}/`
|
|
274
|
+
: `${routeBase.replace(/\/$/, '')}/${localeId}/`;
|
|
275
|
+
const guideHref = routing.strategy === 'none' || routing.strategy === 'domain'
|
|
276
|
+
? docsRouteNone(routeBase, 'guide/getting-started')
|
|
277
|
+
: `${docsRootHref}guide/getting-started`;
|
|
278
|
+
const opts = { docsRootHref, guideHref, githubUrl, routing };
|
|
279
|
+
return {
|
|
280
|
+
header: renderHostChromeTemplate(raw.header, localeId, messages, opts),
|
|
281
|
+
footer: renderHostChromeTemplate(raw.footer, localeId, messages, opts),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
253
284
|
/** @param {string} filePath */
|
|
254
285
|
function extractVmzTemplateHtml(filePath) {
|
|
255
286
|
const src = fs.readFileSync(filePath, 'utf8');
|
|
@@ -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;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} projectRoot
|
|
3
|
+
* @returns {{ strategy?: string, defaultLocale?: string } | null}
|
|
4
|
+
*/
|
|
5
|
+
export declare function loadLocalesRouting(projectRoot: any): any;
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} projectRoot
|
|
8
|
+
* @param {string} localeId
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadLocaleCommonMessages(projectRoot: any, localeId: any): any;
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} projectRoot
|
|
13
|
+
*/
|
|
14
|
+
export declare function readSiteGithubUrl(projectRoot: any): string;
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} template
|
|
17
|
+
* @param {string} localeId
|
|
18
|
+
* @param {Record<string, string>} messages
|
|
19
|
+
* @param {{ docsRootHref: string, guideHref: string, githubUrl: string, routing?: { strategy?: string } }} opts
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderHostChromeTemplate(template: any, localeId: any, messages: any, opts: any): any;
|
|
22
|
+
/** Inline locale preference picker for strategy `none` (Host state, not URL). */
|
|
23
|
+
export declare function localeNonePickerScript(): string;
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} routeBase
|
|
26
|
+
* @param {string} pageKey
|
|
27
|
+
*/
|
|
28
|
+
export declare function docsRouteNone(routeBase: any, pageKey: any): string;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Integrated DocumentMount — lower host SiteHeader/SiteFooter .vmz templates to
|
|
4
|
+
* static HTML with locale-resolved copy (LocaleId is Host preference, not URL).
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} projectRoot
|
|
10
|
+
* @returns {{ strategy?: string, defaultLocale?: string } | null}
|
|
11
|
+
*/
|
|
12
|
+
export function loadLocalesRouting(projectRoot) {
|
|
13
|
+
const p = path.join(projectRoot, 'locales', 'locales.json5');
|
|
14
|
+
if (!fs.existsSync(p))
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
18
|
+
let s = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
19
|
+
const m = s.match(/routing\s*:\s*\{([\s\S]*?)\}/);
|
|
20
|
+
if (!m)
|
|
21
|
+
return null;
|
|
22
|
+
let block = `{${m[1]}}`;
|
|
23
|
+
block = block.replace(/([,{]\s*)([A-Za-z_][A-Za-z0-9_]*)\s*:/g, '$1"$2":');
|
|
24
|
+
block = block.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, (_, inner) => JSON.stringify(inner));
|
|
25
|
+
block = block.replace(/,\s*([}\]])/g, '$1');
|
|
26
|
+
return JSON.parse(block);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} projectRoot
|
|
34
|
+
* @param {string} localeId
|
|
35
|
+
*/
|
|
36
|
+
export function loadLocaleCommonMessages(projectRoot, localeId) {
|
|
37
|
+
const p = path.join(projectRoot, 'locales', localeId, 'common.json5');
|
|
38
|
+
if (!fs.existsSync(p))
|
|
39
|
+
return {};
|
|
40
|
+
try {
|
|
41
|
+
let s = fs.readFileSync(p, 'utf8');
|
|
42
|
+
s = s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
43
|
+
s = s.replace(/([,{]\s*)([A-Za-z_][A-Za-z0-9_]*)\s*:/g, '$1"$2":');
|
|
44
|
+
s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, (_, inner) => JSON.stringify(inner));
|
|
45
|
+
s = s.replace(/,\s*([}\]])/g, '$1');
|
|
46
|
+
return JSON.parse(s);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} projectRoot
|
|
54
|
+
*/
|
|
55
|
+
export function readSiteGithubUrl(projectRoot) {
|
|
56
|
+
const p = path.join(projectRoot, 'src', 'lib', 'site.ts');
|
|
57
|
+
if (!fs.existsSync(p))
|
|
58
|
+
return 'https://github.com/voml/iris-orm';
|
|
59
|
+
const m = fs.readFileSync(p, 'utf8').match(/githubUrl\s*=\s*["']([^"']+)["']/);
|
|
60
|
+
return m?.[1] || 'https://github.com/voml/iris-orm';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} template
|
|
64
|
+
* @param {string} localeId
|
|
65
|
+
* @param {Record<string, string>} messages
|
|
66
|
+
* @param {{ docsRootHref: string, guideHref: string, githubUrl: string, routing?: { strategy?: string } }} opts
|
|
67
|
+
*/
|
|
68
|
+
export function renderHostChromeTemplate(template, localeId, messages, opts) {
|
|
69
|
+
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
70
|
+
const bindings = {
|
|
71
|
+
brandLabel: messages.brand || 'Iris',
|
|
72
|
+
brandFullLabel: `${messages.brand || 'Iris'} ORM`,
|
|
73
|
+
navHomeLabel: messages.navHome || 'Home',
|
|
74
|
+
navDocsLabel: messages.navDocs || 'Docs',
|
|
75
|
+
navPlaygroundLabel: messages.navPlayground || 'Playground',
|
|
76
|
+
navGithubLabel: messages.navGithub || 'GitHub',
|
|
77
|
+
selectLanguageLabel: messages.selectLanguage || 'Language',
|
|
78
|
+
langZhLabel: messages.langZh || '中文',
|
|
79
|
+
langEnLabel: messages.langEn || 'English',
|
|
80
|
+
footerTagLabel: messages.footerTag || messages.brand || 'Iris',
|
|
81
|
+
footerScopeLabel: messages.footerScopeNote || '',
|
|
82
|
+
footerCopyrightLabel: messages.footerCopyright || '',
|
|
83
|
+
footerGuideLabel: messages.footerGuide || messages.ctaInstall || 'Getting started',
|
|
84
|
+
footerColProductLabel: messages.footerColProduct || 'Product',
|
|
85
|
+
footerColProjectLabel: messages.footerColProject || 'Project',
|
|
86
|
+
ctaInstallLabel: messages.ctaInstall || messages.ctaDocs || 'Docs',
|
|
87
|
+
docsRootHref: opts.docsRootHref,
|
|
88
|
+
guideHref: opts.guideHref,
|
|
89
|
+
githubUrl: opts.githubUrl,
|
|
90
|
+
};
|
|
91
|
+
let html = template;
|
|
92
|
+
html = html.replace(/data-home=\{home \? 'true' : 'false'\}/g, 'data-home="false"');
|
|
93
|
+
html = html.replace(/href=\{docsRootHref\}/g, `href="${esc(bindings.docsRootHref)}"`);
|
|
94
|
+
html = html.replace(/href=\{guideHref\}/g, `href="${esc(bindings.guideHref)}"`);
|
|
95
|
+
html = html.replace(/href=\{githubUrl\}/g, `href="${esc(bindings.githubUrl)}"`);
|
|
96
|
+
for (const [key, val] of Object.entries(bindings)) {
|
|
97
|
+
html = html.replace(new RegExp(`\\{${key}\\}`, 'g'), esc(String(val)));
|
|
98
|
+
}
|
|
99
|
+
html = html.replace(/<Link(\s)/g, '<a class="vmz-ui-link"$1');
|
|
100
|
+
html = html.replace(/<\/Link>/g, '</a>');
|
|
101
|
+
html = html.replace(/<Icon[^>]*\/>/g, '');
|
|
102
|
+
html = html.replace(/<div class="locale-switch"[\s\S]*?<\/div>/g, () => {
|
|
103
|
+
const zh = `<button type="button" class="locale-switch__btn${localeId === 'zh-hans' ? ' is-active' : ''}" data-vmz-locale-pick="zh-hans"${localeId === 'zh-hans' ? ' aria-current="true"' : ''}>${esc(bindings.langZhLabel)}</button>`;
|
|
104
|
+
const en = `<button type="button" class="locale-switch__btn${localeId === 'en-us' ? ' is-active' : ''}" data-vmz-locale-pick="en-us"${localeId === 'en-us' ? ' aria-current="true"' : ''}>${esc(bindings.langEnLabel)}</button>`;
|
|
105
|
+
return `<div class="locale-switch" role="group" aria-label="${esc(bindings.selectLanguageLabel)}">${zh}${en}</div>`;
|
|
106
|
+
});
|
|
107
|
+
html = html.replace(/<Button[\s\S]*?onClick=\{\(\) => this\.switchLocale\('zh-hans'\)\}[\s\S]*?>\s*[\s\S]*?<\/Button>/g, `<button type="button" class="locale-switch__btn${localeId === 'zh-hans' ? ' is-active' : ''}" data-vmz-locale-pick="zh-hans"${localeId === 'zh-hans' ? ' aria-current="true"' : ''}>${esc(bindings.langZhLabel)}</button>`);
|
|
108
|
+
html = html.replace(/<Button[\s\S]*?onClick=\{\(\) => this\.switchLocale\('en-us'\)\}[\s\S]*?>\s*[\s\S]*?<\/Button>/g, `<button type="button" class="locale-switch__btn${localeId === 'en-us' ? ' is-active' : ''}" data-vmz-locale-pick="en-us"${localeId === 'en-us' ? ' aria-current="true"' : ''}>${esc(bindings.langEnLabel)}</button>`);
|
|
109
|
+
html = html.replace(/<Button[\s\S]*?<\/Button>/g, '');
|
|
110
|
+
html = html.replace(/aria-current=\{localeId === '[^']+' \? 'true' : null\}/g, '');
|
|
111
|
+
html = html.replace(/aria-label=\{selectLanguageLabel\}/g, `aria-label="${esc(bindings.selectLanguageLabel)}"`);
|
|
112
|
+
html = html.replace(/label=\{[^}]+\}/g, '');
|
|
113
|
+
return html;
|
|
114
|
+
}
|
|
115
|
+
/** Inline locale preference picker for strategy `none` (Host state, not URL). */
|
|
116
|
+
export function localeNonePickerScript() {
|
|
117
|
+
return `<script>(function(){try{document.querySelectorAll("[data-vmz-locale-pick]").forEach(function(btn){btn.addEventListener("click",function(){var id=btn.getAttribute("data-vmz-locale-pick");if(!id)return;try{localStorage.setItem("vmz.locale",id);}catch(e){}try{document.cookie="vmz.locale="+encodeURIComponent(id)+"; path=/; max-age=31536000; SameSite=Lax";}catch(e){}location.reload();});});}catch(e){}})();</script>`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} routeBase
|
|
121
|
+
* @param {string} pageKey
|
|
122
|
+
*/
|
|
123
|
+
export function docsRouteNone(routeBase, pageKey) {
|
|
124
|
+
const base = String(routeBase || '/').replace(/\/$/, '') || '';
|
|
125
|
+
const key = pageKey === 'index' ? '' : pageKey.replace(/\\/g, '/');
|
|
126
|
+
const parts = [base.replace(/^\//, ''), key].filter((p) => p !== '');
|
|
127
|
+
return '/' + (parts.length ? parts.join('/') : '');
|
|
128
|
+
}
|
|
@@ -7,6 +7,8 @@ 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-host-chrome.js';
|
|
11
|
+
import { pageHtmlRel } from './document-enrich.js';
|
|
10
12
|
import { log } from './log.js';
|
|
11
13
|
import { requireNativeAddon } from './native-addon.js';
|
|
12
14
|
/**
|
|
@@ -40,7 +42,7 @@ export async function buildIntegratedDocuments(opts) {
|
|
|
40
42
|
}
|
|
41
43
|
return { ok: false, error: 'document diagnostics', pages: 0 };
|
|
42
44
|
}
|
|
43
|
-
writeMountRootRedirects(result.manifest, outDir);
|
|
45
|
+
writeMountRootRedirects(result.manifest, outDir, projectRoot);
|
|
44
46
|
log.info(`document mount: pages=${result.pages.length} → ${path.relative(process.cwd(), outDir) || '.'}`);
|
|
45
47
|
return { ok: true, pages: result.pages.length };
|
|
46
48
|
}
|
|
@@ -51,22 +53,34 @@ export async function buildIntegratedDocuments(opts) {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
|
-
* Emit `{routeBase}/index.html`
|
|
56
|
+
* Emit `{routeBase}/index.html` for integrated mounts.
|
|
57
|
+
* `routing.strategy: none` → copy default-locale docs index (LocaleId is Host state).
|
|
58
|
+
* prefix strategy → redirect HTML to `{routeBase}/{defaultLocale}/`.
|
|
55
59
|
* @param {import('./document-schema.js').DocumentManifest} manifest
|
|
56
60
|
* @param {string} outDir
|
|
61
|
+
* @param {string} projectRoot
|
|
57
62
|
*/
|
|
58
|
-
function writeMountRootRedirects(manifest, outDir) {
|
|
63
|
+
function writeMountRootRedirects(manifest, outDir, projectRoot) {
|
|
59
64
|
const defaultLocale = manifest.defaultLocale || manifest.locales?.[0];
|
|
60
65
|
if (!defaultLocale)
|
|
61
66
|
return;
|
|
67
|
+
const routing = loadLocalesRouting(projectRoot) || { strategy: 'prefix' };
|
|
62
68
|
for (const mount of manifest.mounts || []) {
|
|
63
69
|
if (!mount?.routeBase || mount.routeBase === '/')
|
|
64
70
|
continue;
|
|
65
71
|
const base = String(mount.routeBase).replace(/\/$/, '');
|
|
66
|
-
const target = `${base}/${defaultLocale}/`;
|
|
67
72
|
const relDir = base.replace(/^\//, '');
|
|
68
73
|
const abs = path.join(outDir, relDir, 'index.html');
|
|
69
74
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
75
|
+
if (routing.strategy === 'none' || routing.strategy === 'domain') {
|
|
76
|
+
const srcRel = pageHtmlRel(base, defaultLocale, 'index');
|
|
77
|
+
const srcAbs = path.join(outDir, srcRel);
|
|
78
|
+
if (fs.existsSync(srcAbs)) {
|
|
79
|
+
fs.copyFileSync(srcAbs, abs);
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const target = `${base}/${defaultLocale}/`;
|
|
70
84
|
const native = requireNativeAddon();
|
|
71
85
|
if (typeof native.generateRedirectHtml !== 'function') {
|
|
72
86
|
throw new Error('vmz native addon missing generateRedirectHtml — rebuild with `pnpm napi:build`');
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,14 @@ export declare function handshake(host?: {
|
|
|
53
53
|
* @returns {string | null}
|
|
54
54
|
*/
|
|
55
55
|
export declare function resolveCoreRuntimeDist(): string;
|
|
56
|
+
/** Runtime companions required by dist/vmz-serve-host.mjs relative imports. */
|
|
57
|
+
export declare const SERVE_HOST_RUNTIME_FILES: string[][];
|
|
58
|
+
/**
|
|
59
|
+
* Copy serve-host + registry bootstrap modules from `@vmz/core` into app outDir.
|
|
60
|
+
* @param {string} outDir
|
|
61
|
+
* @param {string} [coreDist]
|
|
62
|
+
*/
|
|
63
|
+
export declare function materializeServeHostRuntime(outDir: any, coreDist?: string): void;
|
|
56
64
|
/**
|
|
57
65
|
* @typedef {object} WorkspaceOptions
|
|
58
66
|
* @property {string} root
|
|
@@ -234,6 +242,8 @@ declare const _default: {
|
|
|
234
242
|
expectedProtocol: typeof expectedProtocol;
|
|
235
243
|
resolveNativePath: typeof resolveNativePath;
|
|
236
244
|
resolveCoreRuntimeDist: typeof resolveCoreRuntimeDist;
|
|
245
|
+
materializeServeHostRuntime: typeof materializeServeHostRuntime;
|
|
246
|
+
SERVE_HOST_RUNTIME_FILES: string[][];
|
|
237
247
|
loadNative: typeof loadNative;
|
|
238
248
|
getProtocolVersions: typeof getProtocolVersions;
|
|
239
249
|
handshake: typeof handshake;
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Coarse-grained only — no transform hooks / per-AST callbacks.
|
|
5
5
|
*/
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
|
-
import { existsSync } from 'node:fs';
|
|
7
|
+
import { copyFileSync, existsSync } from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { materializeWechatPackaging } from './wechat-packaging.js';
|
|
@@ -144,6 +144,31 @@ export function resolveCoreRuntimeDist() {
|
|
|
144
144
|
return nested;
|
|
145
145
|
return null;
|
|
146
146
|
}
|
|
147
|
+
/** Runtime companions required by dist/vmz-serve-host.mjs relative imports. */
|
|
148
|
+
export const SERVE_HOST_RUNTIME_FILES = [
|
|
149
|
+
['serve-host.mjs', 'vmz-serve-host.mjs'],
|
|
150
|
+
['list-client-components.js', 'list-client-components.js'],
|
|
151
|
+
['deployment-registry.js', 'deployment-registry.js'],
|
|
152
|
+
['render-host.js', 'render-host.js'],
|
|
153
|
+
];
|
|
154
|
+
/**
|
|
155
|
+
* Copy serve-host + registry bootstrap modules from `@vmz/core` into app outDir.
|
|
156
|
+
* @param {string} outDir
|
|
157
|
+
* @param {string} [coreDist]
|
|
158
|
+
*/
|
|
159
|
+
export function materializeServeHostRuntime(outDir, coreDist = resolveCoreRuntimeDist()) {
|
|
160
|
+
if (!coreDist) {
|
|
161
|
+
throw new Error('materializeServeHostRuntime: @vmz/core dist not found');
|
|
162
|
+
}
|
|
163
|
+
for (const [srcName, outName] of SERVE_HOST_RUNTIME_FILES) {
|
|
164
|
+
const src = path.join(coreDist, srcName);
|
|
165
|
+
const dst = path.join(outDir, outName);
|
|
166
|
+
if (!existsSync(src)) {
|
|
167
|
+
throw new Error(`materializeServeHostRuntime: missing ${src}`);
|
|
168
|
+
}
|
|
169
|
+
copyFileSync(src, dst);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
147
172
|
/**
|
|
148
173
|
* @typedef {object} WorkspaceOptions
|
|
149
174
|
* @property {string} root
|
|
@@ -458,6 +483,8 @@ export default {
|
|
|
458
483
|
expectedProtocol,
|
|
459
484
|
resolveNativePath,
|
|
460
485
|
resolveCoreRuntimeDist,
|
|
486
|
+
materializeServeHostRuntime,
|
|
487
|
+
SERVE_HOST_RUNTIME_FILES,
|
|
461
488
|
loadNative,
|
|
462
489
|
getProtocolVersions,
|
|
463
490
|
handshake,
|
package/dist/static-emit.js
CHANGED
|
@@ -7,7 +7,8 @@ import crypto from 'node:crypto';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
|
-
import {
|
|
10
|
+
import { createRenderHost } from '@vmz/core/render-host';
|
|
11
|
+
import { listClientComponentsSync } from '@vmz/core/component-registry';
|
|
11
12
|
import { emitCdnPolicy } from './cdn-policy.js';
|
|
12
13
|
import { emitContentAddressedAssets } from './content-addressed-assets.js';
|
|
13
14
|
import { absoluteUrl, buildLocalePageMeta, localizeBodyLinks } from './locale-router.js';
|
|
@@ -30,8 +31,8 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
30
31
|
if (!fs.existsSync(domPath)) {
|
|
31
32
|
throw new Error(`emitWebStatic: missing ${domPath} — run vmz build first`);
|
|
32
33
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
34
|
+
const host = await createRenderHost(distDir, { strictDeployment: true, preload: 'none' });
|
|
35
|
+
const { renderToString, renderToStream } = host;
|
|
35
36
|
const pageCatalog = listPageClientFiles(distDir);
|
|
36
37
|
/** @type {Array<{
|
|
37
38
|
* routeId: string,
|
|
@@ -100,6 +101,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
100
101
|
}
|
|
101
102
|
const meta = await resolvePageMeta(Page, { params, props, pathname: pattern, origin });
|
|
102
103
|
const layoutChain = resolveLayoutChain(distDir, page.chunkId);
|
|
104
|
+
await host.ensureComponents([page.chunkId, ...layoutChain]);
|
|
103
105
|
let bodyHtml = '';
|
|
104
106
|
for await (const chunk of renderToStream(Page, props, {})) {
|
|
105
107
|
bodyHtml += chunk;
|
|
@@ -217,6 +219,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
217
219
|
const digest = sha256Hex(canonicalJson(manifest));
|
|
218
220
|
manifest.manifestDigest = digest;
|
|
219
221
|
writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
|
|
222
|
+
emitStaticClientEntries(distDir, pageCatalog);
|
|
220
223
|
const assets = emitContentAddressedAssets(distDir);
|
|
221
224
|
manifest.contentAddressedAssets = {
|
|
222
225
|
schema: assets.manifest.schema,
|
|
@@ -558,3 +561,50 @@ function buildSitemap(_origin, generations) {
|
|
|
558
561
|
}
|
|
559
562
|
return native.generateSitemapXml(urls);
|
|
560
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* Static CDN must ship entry-client/event like serve-host (content-addressed + HTML rewrite).
|
|
566
|
+
* @param {string} distDir
|
|
567
|
+
* @param {Array<{ chunkId: string }>} pageCatalog
|
|
568
|
+
*/
|
|
569
|
+
function emitStaticClientEntries(distDir, pageCatalog) {
|
|
570
|
+
const componentEntries = listClientComponentsSync(distDir, { strict: true });
|
|
571
|
+
const indexChunk = pageCatalog.find((p) => p.chunkId === 'pages/index')?.chunkId || pageCatalog[0]?.chunkId || 'pages/index';
|
|
572
|
+
const resumeEntries = loadPageResumeEntriesSync(distDir, indexChunk);
|
|
573
|
+
const lazySet = new Set(resumeEntries
|
|
574
|
+
.filter((e) => isEventResumeStrategy(e.strategy))
|
|
575
|
+
.map((e) => e.component)
|
|
576
|
+
.filter(Boolean));
|
|
577
|
+
const eager = componentEntries.filter((e) => !lazySet.has(e.name));
|
|
578
|
+
const lazy = componentEntries.filter((e) => lazySet.has(e.name));
|
|
579
|
+
const native = requireNativeAddon();
|
|
580
|
+
if (typeof native.generateServeEntryClient !== 'function') {
|
|
581
|
+
throw new Error('vmz native addon missing generateServeEntryClient — rebuild with `pnpm napi:build`');
|
|
582
|
+
}
|
|
583
|
+
fs.writeFileSync(path.join(distDir, 'entry-client.js'), native.generateServeEntryClient(eager, lazy, ''), 'utf8');
|
|
584
|
+
if (typeof native.generateServeEntryEvent === 'function') {
|
|
585
|
+
fs.writeFileSync(path.join(distDir, 'entry-event.js'), native.generateServeEntryEvent(''), 'utf8');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* @param {string} distDir
|
|
590
|
+
* @param {string} chunkId
|
|
591
|
+
*/
|
|
592
|
+
function loadPageResumeEntriesSync(distDir, chunkId) {
|
|
593
|
+
try {
|
|
594
|
+
const dep = JSON.parse(fs.readFileSync(path.join(distDir, 'vmz-deployment.json'), 'utf8'));
|
|
595
|
+
const units = Array.isArray(dep.units) ? dep.units : [];
|
|
596
|
+
const page = units.find((u) => u.chunkId === chunkId) || units.find((u) => u.chunkId === 'pages/index') || units.find((u) => u.kind === 'page');
|
|
597
|
+
const entries = Array.isArray(page?.resumeEntries) ? page.resumeEntries : [];
|
|
598
|
+
return entries.map((e) => ({
|
|
599
|
+
component: String(e.component || ''),
|
|
600
|
+
strategy: String(e.strategy || ''),
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/** @param {string} strategy */
|
|
608
|
+
function isEventResumeStrategy(strategy) {
|
|
609
|
+
return strategy === 'event' || strategy === 'click' || String(strategy).startsWith('event:');
|
|
610
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/vmz",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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,15 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@vmz/core": "0.1.
|
|
52
|
-
"@vmz/plugin": "0.1.
|
|
53
|
-
"@vmz/protocol": "0.1.
|
|
51
|
+
"@vmz/core": "0.1.11",
|
|
52
|
+
"@vmz/plugin": "0.1.11",
|
|
53
|
+
"@vmz/protocol": "0.1.11",
|
|
54
54
|
"jiti": "^2.6.1",
|
|
55
55
|
"json5": "^2.2.3"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@vmz/plugin-markdown-it": "0.1.
|
|
59
|
-
"@vmz/test": "0.1.
|
|
58
|
+
"@vmz/plugin-markdown-it": "0.1.11",
|
|
59
|
+
"@vmz/test": "0.1.11",
|
|
60
60
|
"typescript": "^5.8.3"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
@@ -90,12 +90,12 @@
|
|
|
90
90
|
"cli"
|
|
91
91
|
],
|
|
92
92
|
"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.
|
|
93
|
+
"@vmz/vmz-win32-x64": "0.1.11",
|
|
94
|
+
"@vmz/vmz-win32-arm64": "0.1.11",
|
|
95
|
+
"@vmz/vmz-darwin-x64": "0.1.11",
|
|
96
|
+
"@vmz/vmz-darwin-arm64": "0.1.11",
|
|
97
|
+
"@vmz/vmz-linux-x64": "0.1.11",
|
|
98
|
+
"@vmz/vmz-linux-arm64": "0.1.11"
|
|
99
99
|
},
|
|
100
100
|
"publishConfig": {
|
|
101
101
|
"access": "public"
|