@vmz/vmz 0.3.12 → 0.3.14
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/bin/vmz.js +4 -0
- package/delivery-runtime-files.json +2 -0
- package/dist/cli/cli.js +46 -23
- package/dist/cli/format-cmd.d.ts +9 -17
- package/dist/cli/format-cmd.js +37 -49
- package/dist/cli/format-config.d.ts +6 -9
- package/dist/cli/format-config.js +5 -29
- package/dist/delivery/delivery-profile.js +1 -1
- package/dist/dev/dev-incremental.d.ts +0 -2
- package/dist/dev/dev-incremental.js +0 -1
- package/dist/dev/dev-session.d.ts +53 -1
- package/dist/dev/dev-session.js +368 -65
- package/dist/document/document-base-css.d.ts +15 -0
- package/dist/document/document-base-css.js +578 -0
- package/dist/document/document-build.d.ts +1 -0
- package/dist/document/document-build.js +94 -78
- package/dist/document/document-client.d.ts +13 -0
- package/dist/document/document-client.js +217 -0
- package/dist/document/document-cmd.js +3 -1
- package/dist/document/document-designs.d.ts +9 -3
- package/dist/document/document-designs.js +20 -34
- package/dist/document/document-enrich.d.ts +9 -0
- package/dist/document/document-enrich.js +55 -0
- package/dist/document/document-highlight.d.ts +26 -0
- package/dist/document/document-highlight.js +54 -0
- package/dist/document/document-integrate.d.ts +5 -0
- package/dist/document/document-integrate.js +1 -0
- package/dist/document/document-interactive.js +10 -2
- package/dist/document/document-layout-render.js +6 -2
- package/dist/document/document-schema.d.ts +5 -0
- package/dist/workspace/build-assemble.js +18 -6
- package/dist/workspace/content-addressed-assets.d.ts +6 -17
- package/dist/workspace/content-addressed-assets.js +10 -95
- package/dist/workspace/delivery-runtime-imports.js +9 -0
- package/dist/workspace/native-addon.d.ts +4 -0
- package/dist/workspace/pack-native.d.ts +151 -0
- package/dist/workspace/pack-native.js +280 -0
- package/dist/workspace/pack.d.ts +131 -32
- package/dist/workspace/pack.js +381 -65
- package/dist/workspace/plugin-host.d.ts +4 -4
- package/dist/workspace/plugin-host.js +6 -6
- package/dist/workspace/production-observability.d.ts +2 -0
- package/dist/workspace/production-observability.js +6 -2
- package/dist/workspace/project-build.js +26 -4
- package/dist/workspace/public-api.d.ts +7 -3
- package/dist/workspace/public-api.js +5 -3
- package/dist/workspace/static-emit.d.ts +30 -0
- package/dist/workspace/static-emit.js +211 -18
- package/dist/workspace/user-config.d.ts +112 -0
- package/dist/workspace/user-config.js +12 -0
- package/host-runtime-files.json +1 -0
- package/locales/en-US/cli.json +5 -4
- package/locales/en-US/diag.json +2 -0
- package/locales/zh-CN/diag.json +2 -0
- package/package.json +20 -20
- package/dist/workspace/pack-client-packages.d.ts +0 -34
- package/dist/workspace/pack-client-packages.js +0 -550
|
@@ -230,3 +230,58 @@ function normalizePageKey(pageKey) {
|
|
|
230
230
|
function escapeRe(s) {
|
|
231
231
|
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
232
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Author link → route-form href for emitted pages (document layer link form:
|
|
235
|
+
* `/d/<locale>/<pageKey>` without `.html`, matching application routes).
|
|
236
|
+
*
|
|
237
|
+
* Returns `null` for links that must stay as-authored: external/mailto, API
|
|
238
|
+
* refs, pure anchors, absolute application routes outside the document mount,
|
|
239
|
+
* and unresolvable relatives (the check pass reports those as broken).
|
|
240
|
+
*/
|
|
241
|
+
export function routeHrefForDocLink(href, fromPageKey, locale, routeBase, pageKeySet, routing = {}) {
|
|
242
|
+
const raw = String(href || '').trim();
|
|
243
|
+
if (!raw)
|
|
244
|
+
return null;
|
|
245
|
+
if (raw.startsWith('#'))
|
|
246
|
+
return null;
|
|
247
|
+
if (/^(mailto:|https?:\/\/)/i.test(raw))
|
|
248
|
+
return null;
|
|
249
|
+
if (raw.startsWith('vmz-api:') || raw.startsWith('api:'))
|
|
250
|
+
return null;
|
|
251
|
+
// Split anchor first; both branches reattach it.
|
|
252
|
+
let pathPart = raw;
|
|
253
|
+
let anchor = '';
|
|
254
|
+
const hash = raw.indexOf('#');
|
|
255
|
+
if (hash >= 0) {
|
|
256
|
+
pathPart = raw.slice(0, hash);
|
|
257
|
+
anchor = raw.slice(hash);
|
|
258
|
+
}
|
|
259
|
+
if (pathPart.startsWith('/')) {
|
|
260
|
+
// Absolute: normalize only when it lands on a known document page.
|
|
261
|
+
const want = pathPart.replace(/\/$/, '') || '/';
|
|
262
|
+
for (const [loc, keys] of pageKeySet) {
|
|
263
|
+
for (const pk of keys) {
|
|
264
|
+
const r = pageRoute(routeBase, loc, pk, routing).replace(/\/$/, '');
|
|
265
|
+
if (r === want)
|
|
266
|
+
return r + anchor;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
// Relative author link (usually `./x.md` / `../y/z.md`): resolve to PageKey.
|
|
272
|
+
const cleaned = pathPart.replace(/\.md$/i, '').replace(/\\/g, '/');
|
|
273
|
+
const fromDir = pageKeyDir(fromPageKey);
|
|
274
|
+
const joined = path.posix.normalize(path.posix.join(fromDir || '.', cleaned));
|
|
275
|
+
let pk = joined === '.' || joined === '' ? 'index' : joined.replace(/^\.\//, '');
|
|
276
|
+
pk = normalizePageKey(pk);
|
|
277
|
+
const keys = pageKeySet.get(locale) || new Set();
|
|
278
|
+
if (!keys.has(pk)) {
|
|
279
|
+
const indexJoined = path.posix.normalize(path.posix.join(fromPageKey || '.', cleaned));
|
|
280
|
+
const indexPk = normalizePageKey(indexJoined.replace(/^\.\//, ''));
|
|
281
|
+
if (keys.has(indexPk))
|
|
282
|
+
pk = indexPk;
|
|
283
|
+
else
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return pageRoute(routeBase, locale, pk, routing) + anchor;
|
|
287
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document code highlight — wire `engines.code` (author surface) into the
|
|
3
|
+
* markdown pipeline. Shiki is prewarmed once per build; fences render static
|
|
4
|
+
* highlighted HTML through markdown-it's `highlight` callback (inline spans,
|
|
5
|
+
* the document chrome owns the `<pre>` surface).
|
|
6
|
+
*
|
|
7
|
+
* No `engines.code` (or the plugin missing) means honest plain `<pre><code>` —
|
|
8
|
+
* never an error, never a fake colored block.
|
|
9
|
+
*/
|
|
10
|
+
import { type DocumentDiagnostic } from './document-schema.js';
|
|
11
|
+
export interface DocumentCodeHighlighter {
|
|
12
|
+
/** Sync inline highlight for one fence body. '' → renderer falls back to escaped code. */
|
|
13
|
+
highlightFenceInline(code: string, lang: string): string;
|
|
14
|
+
}
|
|
15
|
+
export interface ResolveDocumentHighlighterOpts {
|
|
16
|
+
/** `engines.code` from the project plugin host (`'shiki'` supported). */
|
|
17
|
+
code?: string | null;
|
|
18
|
+
/** Themes to prewarm (defaults to the plugin default theme). */
|
|
19
|
+
themes?: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the build-time code highlighter. `null` engine → `null` highlighter
|
|
23
|
+
* (plain fences). Unknown engine → an explicit diagnostic (no silent fallback
|
|
24
|
+
* to a different engine).
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveDocumentCodeHighlighter(opts: ResolveDocumentHighlighterOpts, diagnostics: DocumentDiagnostic[]): Promise<DocumentCodeHighlighter | null>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document code highlight — wire `engines.code` (author surface) into the
|
|
3
|
+
* markdown pipeline. Shiki is prewarmed once per build; fences render static
|
|
4
|
+
* highlighted HTML through markdown-it's `highlight` callback (inline spans,
|
|
5
|
+
* the document chrome owns the `<pre>` surface).
|
|
6
|
+
*
|
|
7
|
+
* No `engines.code` (or the plugin missing) means honest plain `<pre><code>` —
|
|
8
|
+
* never an error, never a fake colored block.
|
|
9
|
+
*/
|
|
10
|
+
import { DIAG } from './document-schema.js';
|
|
11
|
+
const SUPPORTED_CODE_ENGINES = new Set(['shiki']);
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the build-time code highlighter. `null` engine → `null` highlighter
|
|
14
|
+
* (plain fences). Unknown engine → an explicit diagnostic (no silent fallback
|
|
15
|
+
* to a different engine).
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveDocumentCodeHighlighter(opts, diagnostics) {
|
|
18
|
+
const engine = typeof opts.code === 'string' ? opts.code.trim() : '';
|
|
19
|
+
if (!engine)
|
|
20
|
+
return null;
|
|
21
|
+
if (!SUPPORTED_CODE_ENGINES.has(engine)) {
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
code: DIAG.FENCE_UNSUPPORTED,
|
|
24
|
+
severity: 'error',
|
|
25
|
+
message: `unsupported engines.code ${JSON.stringify(engine)} (supported: ${[...SUPPORTED_CODE_ENGINES].join(', ')})`,
|
|
26
|
+
});
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
let runtime;
|
|
30
|
+
try {
|
|
31
|
+
runtime = await import('@vmz/plugin-shiki/runtime');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
diagnostics.push({
|
|
35
|
+
code: DIAG.FENCE_UNSUPPORTED,
|
|
36
|
+
severity: 'error',
|
|
37
|
+
message: 'engines.code "shiki" needs `@vmz/plugin-shiki` (optional peer of `@vmz/vmz`).\n' +
|
|
38
|
+
' Install: pnpm add -D @vmz/plugin-shiki',
|
|
39
|
+
});
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await runtime.prewarmShiki(opts.themes?.length ? { themes: opts.themes } : {});
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Bootstrap failure must not fail the build: fences degrade to plain code.
|
|
47
|
+
return {
|
|
48
|
+
highlightFenceInline: () => '',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
highlightFenceInline: (code, lang) => runtime.highlightInline(code, lang),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -7,6 +7,11 @@ export interface BuildIntegratedDocumentsOpts {
|
|
|
7
7
|
projectRoot: string;
|
|
8
8
|
outDir: string;
|
|
9
9
|
strict?: boolean;
|
|
10
|
+
/** Project plugin host engines (`engines.code` wires build-time fence highlight). */
|
|
11
|
+
engines?: {
|
|
12
|
+
markdown?: string;
|
|
13
|
+
code?: string;
|
|
14
|
+
};
|
|
10
15
|
}
|
|
11
16
|
export interface BuildIntegratedDocumentsResult {
|
|
12
17
|
ok: boolean;
|
|
@@ -41,6 +41,7 @@ export async function buildIntegratedDocuments(opts) {
|
|
|
41
41
|
outDir,
|
|
42
42
|
appDistDir: outDir,
|
|
43
43
|
strict: Boolean(opts.strict),
|
|
44
|
+
engines: opts.engines,
|
|
44
45
|
});
|
|
45
46
|
if (!result.ok) {
|
|
46
47
|
const errs = (result.manifest?.diagnostics || []).filter((d) => d.severity === 'error');
|
|
@@ -182,15 +182,23 @@ export function collectFenceBodies(analyzedByPageId, pages) {
|
|
|
182
182
|
/** Render SSR island shells (no script — resume later). */
|
|
183
183
|
export function renderIslandShellsHtml(opts) {
|
|
184
184
|
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
185
|
+
const zh = opts.locale.toLowerCase().startsWith('zh');
|
|
186
|
+
const label = zh ? '搜索文档' : 'Search docs';
|
|
187
|
+
const hint = zh ? '搜索需要 JavaScript;无脚本时请使用导航。' : 'Search needs JavaScript; use the navigation without it.';
|
|
188
|
+
// Static form skeleton: readable without JS; `assets/vmz-document.js`
|
|
189
|
+
// wires it to the search index (framework-owned enhancement module).
|
|
185
190
|
const search = `
|
|
186
191
|
<div
|
|
187
192
|
data-vmz-island="DocumentSearch"
|
|
188
193
|
data-vmz-resume="island"
|
|
189
194
|
data-vmz-search-index="${esc(opts.searchIndexHref)}"
|
|
195
|
+
class="doc-search"
|
|
190
196
|
role="search"
|
|
191
|
-
aria-label="
|
|
197
|
+
aria-label="${esc(label)}"
|
|
192
198
|
>
|
|
193
|
-
<
|
|
199
|
+
<input type="search" class="doc-search__input" placeholder="${esc(label)}" aria-label="${esc(label)}" autocomplete="off" aria-expanded="false" />
|
|
200
|
+
<ol class="doc-search__results" role="listbox" hidden></ol>
|
|
201
|
+
<p class="doc-search__hint">${esc(hint)}</p>
|
|
194
202
|
</div>`;
|
|
195
203
|
const playgrounds = (opts.islands?.islands || [])
|
|
196
204
|
.filter((isl) => isl.kind === 'playground' && isl.fence?.locale === opts.locale && isl.fence?.pageKey === opts.pageKey)
|
|
@@ -6,6 +6,7 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
8
|
import { createRenderHost } from '@vmz/core/render-host';
|
|
9
|
+
import { packNodeEntryFile } from '../workspace/pack.js';
|
|
9
10
|
export function resolveDocumentLayoutChunkId(distDir) {
|
|
10
11
|
for (const chunkId of ['layouts/DocumentLayout', 'components/DocumentLayout']) {
|
|
11
12
|
if (fs.existsSync(path.join(distDir, `${chunkId}.client.js`)))
|
|
@@ -46,8 +47,11 @@ export function assertCompiledShellHtml(html) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
async function loadCtor(distDir, chunkId) {
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// Node chunk identity comes from `_vmz/pack-node-manifest.json`; the
|
|
51
|
+
// unpacked root `${chunkId}.client.js` is not a fallback. No `?t=` cache
|
|
52
|
+
// bust: packed chunks are shared instance URLs.
|
|
53
|
+
const href = pathToFileURL(packNodeEntryFile(distDir, chunkId)).href;
|
|
54
|
+
const mod = await import(href);
|
|
51
55
|
return mod.default;
|
|
52
56
|
}
|
|
53
57
|
export async function renderCompiledDocumentLayout(distDir, localeId, slotHtml) {
|
|
@@ -139,7 +139,12 @@ export interface DocumentBuildMeta {
|
|
|
139
139
|
engine?: string;
|
|
140
140
|
outDir?: string;
|
|
141
141
|
designs?: string | null;
|
|
142
|
+
/** Site token stylesheet href (`assets/vmz-designs.css`), null when none. */
|
|
142
143
|
designsCss?: string | null;
|
|
144
|
+
/** Framework document base stylesheet href (`assets/vmz-document.css`). */
|
|
145
|
+
documentCss?: string | null;
|
|
146
|
+
/** Framework client enhancement href (`assets/vmz-document.js`). */
|
|
147
|
+
clientJs?: string | null;
|
|
143
148
|
hostShell?: string;
|
|
144
149
|
pages?: Array<{
|
|
145
150
|
route: string;
|
|
@@ -9,7 +9,7 @@ import { emitEmbeddedPackaging } from './embedded-packaging.js';
|
|
|
9
9
|
import { emitSiteDelivery } from './site-delivery.js';
|
|
10
10
|
import { emitSiteFavicon } from './site-favicon.js';
|
|
11
11
|
import { emitPublicStaticAssets } from './public-static-assets.js';
|
|
12
|
-
import { emitWebStatic } from './static-emit.js';
|
|
12
|
+
import { emitWebStatic, pruneWebStaticDelivery } from './static-emit.js';
|
|
13
13
|
import { writePrettyJsonFile } from './pretty-json.js';
|
|
14
14
|
export const BUILD_PROOF_SCHEMA = 'vmz.build.proof.v0';
|
|
15
15
|
export const ASSEMBLE_MANIFEST_SCHEMA = 'vmz.assemble.manifest.v0';
|
|
@@ -24,10 +24,11 @@ export async function assembleDelivery(outDir, ctx) {
|
|
|
24
24
|
serverRuntime: selection.serverRuntime || null,
|
|
25
25
|
steps: [],
|
|
26
26
|
};
|
|
27
|
-
if (assembly === 'web-static' || assembly === 'cdn+server') {
|
|
27
|
+
if (assembly === 'web-static' || assembly === 'cdn+server' || assembly === 'local-static') {
|
|
28
28
|
const staticResult = await emitWebStatic(outDir, {
|
|
29
29
|
origin: ctx.origin,
|
|
30
30
|
projectRoot: ctx.projectRoot,
|
|
31
|
+
deliveryProfile: assembly === 'local-static' ? 'local-static' : undefined,
|
|
31
32
|
});
|
|
32
33
|
result.steps.push({
|
|
33
34
|
kind: 'web-static',
|
|
@@ -40,9 +41,20 @@ export async function assembleDelivery(outDir, ctx) {
|
|
|
40
41
|
htmlFiles: staticResult.htmlFiles,
|
|
41
42
|
skipped: staticResult.skipped,
|
|
42
43
|
};
|
|
44
|
+
if (assembly === 'web-static') {
|
|
45
|
+
// Final browser set only: Node-only host material and unpacked module JS
|
|
46
|
+
// superseded by `_vmz/pack/` chunks are stripped after HTML emit.
|
|
47
|
+
const pruned = pruneWebStaticDelivery(outDir);
|
|
48
|
+
result.steps.push({
|
|
49
|
+
kind: 'web-static-prune',
|
|
50
|
+
status: 'pruned',
|
|
51
|
+
note: `removed=${pruned.removed} faces=${pruned.keptFaces.length}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
43
54
|
}
|
|
44
|
-
else if (assembly === 'server-host'
|
|
45
|
-
// SSR
|
|
55
|
+
else if (assembly === 'server-host') {
|
|
56
|
+
// SSR host: favicon + opaque public/ (local-static shares the static
|
|
57
|
+
// document emit above, so it no longer joins this branch).
|
|
46
58
|
emitSiteFavicon(outDir, { projectRoot: ctx.projectRoot });
|
|
47
59
|
emitPublicStaticAssets(outDir, { projectRoot: ctx.projectRoot });
|
|
48
60
|
}
|
|
@@ -50,7 +62,7 @@ export async function assembleDelivery(outDir, ctx) {
|
|
|
50
62
|
result.steps.push({
|
|
51
63
|
kind: 'local-static',
|
|
52
64
|
status: 'modules-ready',
|
|
53
|
-
note: '
|
|
65
|
+
note: 'entry HTML emitted; full module tree kept for local embed (no prune, no ServerArtifact)',
|
|
54
66
|
});
|
|
55
67
|
}
|
|
56
68
|
if (assembly === 'server-host' || assembly === 'cdn+server') {
|
|
@@ -174,7 +186,7 @@ export function emitBuildProof(outDir, ctx) {
|
|
|
174
186
|
slots[id] = {
|
|
175
187
|
status: packCtx?.packDigest ? 'pack-digest' : 'pending',
|
|
176
188
|
detail: packCtx?.packDigest
|
|
177
|
-
? `pack=${String(packCtx.packDigest).slice(0, 12)}
|
|
189
|
+
? `pack=${String(packCtx.packDigest).slice(0, 12)} chunks=${packCtx.chunkCount ?? '?'}`
|
|
178
190
|
: 'pack missing',
|
|
179
191
|
};
|
|
180
192
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
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 must receive hashed
|
|
4
|
-
* `cssEntry`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `cssEntry` at shell emit time — this module does **not** post-rewrite HTML.
|
|
5
|
+
* CSS aggregators rewrite `@import` before hash.
|
|
6
|
+
*
|
|
7
|
+
* Browser JS is NOT handled here: `vmz-pack` emits the final content-hashed
|
|
8
|
+
* chunk set under `_vmz/pack/` and the pack manifest `entryFiles` is the only
|
|
9
|
+
* logical→physical mapping. This layer only covers non-packed static assets.
|
|
8
10
|
*/
|
|
9
11
|
export declare const CONTENT_ADDRESSED_ASSETS_SCHEMA = "vmz.content_addressed_assets.v0";
|
|
10
12
|
export declare const ASSET_PLAN_SCHEMA = "vmz.asset.plan.v0";
|
|
@@ -33,19 +35,6 @@ interface EmitContentAddressedAssetsOpts {
|
|
|
33
35
|
* @param {Record<string, string>} rewrites logical (no leading slash) or `/logical` → `assets/hash.ext`
|
|
34
36
|
*/
|
|
35
37
|
export declare function rewriteCssImports(cssText: any, rewrites: any): any;
|
|
36
|
-
/**
|
|
37
|
-
* Rewrite relative ESM so a file served from `/assets/<hash>.js` resolves against
|
|
38
|
-
* dist root (static `from "./x"` / `export * from "./x"` + dynamic `import("./"+…)`).
|
|
39
|
-
*
|
|
40
|
-
* Always use `../…` (Fix A). Do **not** prefer hashed siblings under `assets/`:
|
|
41
|
-
* barrels like `vmz-dom.js` (`export * from './dom-core.js'`) would then resolve
|
|
42
|
-
* as `/assets/dom-core.js` and 404. `rewrites` is accepted for API parity with CSS
|
|
43
|
-
* but intentionally ignored for JS path choice.
|
|
44
|
-
*
|
|
45
|
-
* @param {string} jsText
|
|
46
|
-
* @param {Record<string, string>} [_rewrites]
|
|
47
|
-
*/
|
|
48
|
-
export declare function rewriteJsEntryRelativeImports(jsText: any, _rewrites?: {}): string;
|
|
49
38
|
/**
|
|
50
39
|
* Emit `assets/<sha256>.<ext>` copies and return logical→hashed rewrites.
|
|
51
40
|
* Callers must pass hashed URLs into `generatePageShell` — HTML is never rewritten here.
|
|
@@ -1,10 +1,12 @@
|
|
|
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 must receive hashed
|
|
4
|
-
* `cssEntry`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `cssEntry` at shell emit time — this module does **not** post-rewrite HTML.
|
|
5
|
+
* CSS aggregators rewrite `@import` before hash.
|
|
6
|
+
*
|
|
7
|
+
* Browser JS is NOT handled here: `vmz-pack` emits the final content-hashed
|
|
8
|
+
* chunk set under `_vmz/pack/` and the pack manifest `entryFiles` is the only
|
|
9
|
+
* logical→physical mapping. This layer only covers non-packed static assets.
|
|
8
10
|
*/
|
|
9
11
|
import crypto from 'node:crypto';
|
|
10
12
|
import fs from 'node:fs';
|
|
@@ -13,22 +15,10 @@ import { canonicalJson, sha256Hex } from './release-pack.js';
|
|
|
13
15
|
import { writePrettyJsonFile } from './pretty-json.js';
|
|
14
16
|
export const CONTENT_ADDRESSED_ASSETS_SCHEMA = 'vmz.content_addressed_assets.v0';
|
|
15
17
|
export const ASSET_PLAN_SCHEMA = 'vmz.asset.plan.v0';
|
|
16
|
-
/** Immutable delivery candidates (
|
|
17
|
-
const DEFAULT_CANDIDATES = [
|
|
18
|
-
'entry-client.js',
|
|
19
|
-
'entry-event.js',
|
|
20
|
-
'vmz.css',
|
|
21
|
-
'vmz-designs.css',
|
|
22
|
-
'vmz-style.css',
|
|
23
|
-
'vmz-dom.js',
|
|
24
|
-
'vmz-runtime.js',
|
|
25
|
-
'vmz-http.js',
|
|
26
|
-
'vmz-client-nav.js',
|
|
27
|
-
];
|
|
18
|
+
/** Immutable delivery candidates (non-packed static bytes; browser JS is final-named by vmz-pack). */
|
|
19
|
+
const DEFAULT_CANDIDATES = ['vmz.css', 'vmz-designs.css', 'vmz-style.css'];
|
|
28
20
|
/** CSS files that may @import other logical CSS; processed after leaf CSS is hashed. */
|
|
29
21
|
const CSS_AGGREGATORS = new Set(['vmz.css']);
|
|
30
|
-
/** JS entry shells hashed under assets/; relative ESM must be rewritten first. */
|
|
31
|
-
const JS_ENTRY_AGGREGATORS = new Set(['entry-client.js', 'entry-event.js']);
|
|
32
22
|
const CSS_IMPORT_RE = /@import\s*(?:url\()?['"]?(\.\/)?([^'")\s;]+)['"]?\)?/gi;
|
|
33
23
|
/**
|
|
34
24
|
* Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
|
|
@@ -48,29 +38,6 @@ export function rewriteCssImports(cssText, rewrites) {
|
|
|
48
38
|
return `@import"${sibling}"`;
|
|
49
39
|
});
|
|
50
40
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Rewrite relative ESM so a file served from `/assets/<hash>.js` resolves against
|
|
53
|
-
* dist root (static `from "./x"` / `export * from "./x"` + dynamic `import("./"+…)`).
|
|
54
|
-
*
|
|
55
|
-
* Always use `../…` (Fix A). Do **not** prefer hashed siblings under `assets/`:
|
|
56
|
-
* barrels like `vmz-dom.js` (`export * from './dom-core.js'`) would then resolve
|
|
57
|
-
* as `/assets/dom-core.js` and 404. `rewrites` is accepted for API parity with CSS
|
|
58
|
-
* but intentionally ignored for JS path choice.
|
|
59
|
-
*
|
|
60
|
-
* @param {string} jsText
|
|
61
|
-
* @param {Record<string, string>} [_rewrites]
|
|
62
|
-
*/
|
|
63
|
-
export function rewriteJsEntryRelativeImports(jsText, _rewrites = {}) {
|
|
64
|
-
// These files are already generated JavaScript. Re-parsing them through
|
|
65
|
-
// the native module parser here creates a second parser boundary during
|
|
66
|
-
// static assembly and can re-enter the generated graph indefinitely.
|
|
67
|
-
// Restrict the rewrite to import/export module-specifier positions and
|
|
68
|
-
// keep it bounded to the source text.
|
|
69
|
-
let out = String(jsText ?? '').replace(/(\b(?:from|import)\s*)(["'])\.\/([^"'\\]+)\2/g, (_match, prefix, quote, specifier) => `${prefix}${quote}../${specifier}${quote}`);
|
|
70
|
-
out = out.replace(/import\(\s*(["'])\.\/([^"'\\]+)\1\s*\)/g, (_match, quote, specifier) => `import(${quote}../${specifier}${quote})`);
|
|
71
|
-
out = out.replace(/import\(\s*(["'])\.\/\1\s*\+/g, (_match, quote) => `import(${quote}../${quote}+`);
|
|
72
|
-
return out;
|
|
73
|
-
}
|
|
74
41
|
/**
|
|
75
42
|
* Emit `assets/<sha256>.<ext>` copies and return logical→hashed rewrites.
|
|
76
43
|
* Callers must pass hashed URLs into `generatePageShell` — HTML is never rewritten here.
|
|
@@ -97,22 +64,6 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
|
|
|
97
64
|
candidates: ordered,
|
|
98
65
|
});
|
|
99
66
|
for (const rel of ordered) {
|
|
100
|
-
// Aggregators are replaced in later passes; leaf JS must rewrite ./ → ../ so a
|
|
101
|
-
// hashed barrel under assets/ never 404s on second-hop relative re-exports.
|
|
102
|
-
if (CSS_AGGREGATORS.has(rel) || JS_ENTRY_AGGREGATORS.has(rel)) {
|
|
103
|
-
ingestCandidate(abs, rel, rewrites, objects, { transform: null });
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (/\.m?js$/i.test(rel)) {
|
|
107
|
-
const src = path.join(abs, rel);
|
|
108
|
-
if (!fs.existsSync(src))
|
|
109
|
-
continue;
|
|
110
|
-
const rewritten = rewriteJsEntryRelativeImports(fs.readFileSync(src, 'utf8'), {});
|
|
111
|
-
ingestCandidate(abs, rel, rewrites, objects, {
|
|
112
|
-
transform: () => Buffer.from(rewritten, 'utf8'),
|
|
113
|
-
});
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
67
|
ingestCandidate(abs, rel, rewrites, objects, { transform: null });
|
|
117
68
|
}
|
|
118
69
|
// Aggregator CSS (vmz.css) must import hashed leaf files — rewrite then hash.
|
|
@@ -130,21 +81,6 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
|
|
|
130
81
|
transform: () => Buffer.from(rewritten, 'utf8'),
|
|
131
82
|
});
|
|
132
83
|
}
|
|
133
|
-
// JS entry shells: rewrite relative ESM (static + dynamic) then re-hash.
|
|
134
|
-
for (const rel of ordered) {
|
|
135
|
-
if (!JS_ENTRY_AGGREGATORS.has(rel))
|
|
136
|
-
continue;
|
|
137
|
-
const src = path.join(abs, rel);
|
|
138
|
-
if (!fs.existsSync(src))
|
|
139
|
-
continue;
|
|
140
|
-
const rewritten = rewriteJsEntryRelativeImports(fs.readFileSync(src, 'utf8'), rewrites);
|
|
141
|
-
removeLogicalObject(objects, rel);
|
|
142
|
-
delete rewrites[`/${rel}`];
|
|
143
|
-
delete rewrites[rel];
|
|
144
|
-
ingestCandidate(abs, rel, rewrites, objects, {
|
|
145
|
-
transform: () => Buffer.from(rewritten, 'utf8'),
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
84
|
objects.sort((a, b) => (a.logicalPath < b.logicalPath ? -1 : a.logicalPath > b.logicalPath ? 1 : 0));
|
|
149
85
|
const manifest = {
|
|
150
86
|
schema: CONTENT_ADDRESSED_ASSETS_SCHEMA,
|
|
@@ -168,20 +104,15 @@ function orderCandidates(candidates) {
|
|
|
168
104
|
const set = new Set(candidates.map((c) => String(c).replace(/\\/g, '/').replace(/^\//, '')));
|
|
169
105
|
const out = [];
|
|
170
106
|
for (const name of DEFAULT_CANDIDATES) {
|
|
171
|
-
if (set.has(name) && !CSS_AGGREGATORS.has(name)
|
|
107
|
+
if (set.has(name) && !CSS_AGGREGATORS.has(name))
|
|
172
108
|
out.push(name);
|
|
173
109
|
}
|
|
174
110
|
for (const name of [...set].sort()) {
|
|
175
|
-
if (!CSS_AGGREGATORS.has(name) && !
|
|
111
|
+
if (!CSS_AGGREGATORS.has(name) && !out.includes(name))
|
|
176
112
|
out.push(name);
|
|
177
|
-
}
|
|
178
113
|
}
|
|
179
114
|
if (set.has('vmz.css'))
|
|
180
115
|
out.push('vmz.css');
|
|
181
|
-
for (const name of ['entry-client.js', 'entry-event.js']) {
|
|
182
|
-
if (set.has(name))
|
|
183
|
-
out.push(name);
|
|
184
|
-
}
|
|
185
116
|
return out;
|
|
186
117
|
}
|
|
187
118
|
/**
|
|
@@ -282,24 +213,8 @@ function collectCandidates(distDir) {
|
|
|
282
213
|
if (fs.existsSync(path.join(distDir, name)))
|
|
283
214
|
out.push(name);
|
|
284
215
|
}
|
|
285
|
-
walk(distDir, distDir, (rel) => {
|
|
286
|
-
if (/\.client\.js$/i.test(rel))
|
|
287
|
-
out.push(rel);
|
|
288
|
-
});
|
|
289
216
|
return [...new Set(out)].sort();
|
|
290
217
|
}
|
|
291
|
-
function walk(root, dir, onFile) {
|
|
292
|
-
for (const name of fs.readdirSync(dir)) {
|
|
293
|
-
if (name === 'assets' || name === '_vmz' || name === 'node_modules')
|
|
294
|
-
continue;
|
|
295
|
-
const full = path.join(dir, name);
|
|
296
|
-
const st = fs.statSync(full);
|
|
297
|
-
if (st.isDirectory())
|
|
298
|
-
walk(root, full, onFile);
|
|
299
|
-
else
|
|
300
|
-
onFile(path.relative(root, full).replace(/\\/g, '/'));
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
218
|
/** Resolve `/logical` → hashed href from emit rewrites (`/assets/<sha>.ext`). */
|
|
304
219
|
export function hashedAssetHref(rewrites, logical) {
|
|
305
220
|
if (!logical)
|
|
@@ -21,6 +21,7 @@ function rewriteFlatDeliveryImports(text) {
|
|
|
21
21
|
['../browser/direct-host-box.js', './direct-host-box.js'],
|
|
22
22
|
['../browser/unknown-component.js', './unknown-component.js'],
|
|
23
23
|
['../shared/dom-attr-normalize.js', './dom-attr-normalize.js'],
|
|
24
|
+
['../shared/html-digest.js', './html-digest.js'],
|
|
24
25
|
['../browser/client-nav.js', './vmz-client-nav.js'],
|
|
25
26
|
['../ssr/dom-ssr.js', './dom-ssr.js'],
|
|
26
27
|
['../faces/server.js', './vmz-runtime.js'],
|
|
@@ -34,6 +35,10 @@ function rewriteFlatDeliveryImports(text) {
|
|
|
34
35
|
}
|
|
35
36
|
function rewriteNestedSsrDelivery(text) {
|
|
36
37
|
let out = text;
|
|
38
|
+
// `shared/` is flattened to the delivery root, so nested SSR files must
|
|
39
|
+
// reach the root copy one level up.
|
|
40
|
+
out = replaceImportPath(out, '../shared/dom-attr-normalize.js', '../dom-attr-normalize.js');
|
|
41
|
+
out = replaceImportPath(out, '../shared/html-digest.js', '../html-digest.js');
|
|
37
42
|
for (const [from, to] of [
|
|
38
43
|
['../browser/dom-core.js', '../dom-core.js'],
|
|
39
44
|
['../browser/direct-host-box.js', '../direct-host-box.js'],
|
|
@@ -45,6 +50,10 @@ function rewriteNestedSsrDelivery(text) {
|
|
|
45
50
|
}
|
|
46
51
|
function rewriteNestedBrowserDelivery(text) {
|
|
47
52
|
let out = replaceImportPath(text, '../../shared/dom-attr-normalize.js', '../../dom-attr-normalize.js');
|
|
53
|
+
out = replaceImportPath(out, '../../shared/html-digest.js', '../../html-digest.js');
|
|
54
|
+
// `dom-ssr.js` 只交付在根(flat barrel),nested `ssr/` 下没有它:
|
|
55
|
+
// island resume 的动态边必须指向根级产物。
|
|
56
|
+
out = replaceImportPath(out, '../../ssr/dom-ssr.js', '../../dom-ssr.js');
|
|
48
57
|
for (const [from, to] of [
|
|
49
58
|
['../direct-host-box.js', '../../direct-host-box.js'],
|
|
50
59
|
['../unknown-component.js', '../../unknown-component.js'],
|
|
@@ -3,6 +3,10 @@ export interface NativeAddon {
|
|
|
3
3
|
authorJson5ToCanonicalJson?: (source: string) => string;
|
|
4
4
|
loadLocalePlan?: (projectRoot: string) => string;
|
|
5
5
|
loadDocumentRoutePlan?: (projectRoot: string) => string;
|
|
6
|
+
/** vmz-pack v0 contract — synchronous plan request/response over JSON strings. */
|
|
7
|
+
packPlan?: (requestJson: string) => string;
|
|
8
|
+
/** vmz-pack v0 contract — plan plus artifact write under `outDir`. */
|
|
9
|
+
packWrite?: (requestJson: string, outDir: string) => string;
|
|
6
10
|
[key: string]: unknown;
|
|
7
11
|
}
|
|
8
12
|
/** Load the N-API addon or throw (production printers must not fall back to TS). */
|