@vmz/vmz 0.3.11 → 0.3.13

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.
Files changed (52) hide show
  1. package/delivery-runtime-files.json +1 -0
  2. package/dist/cli/cli.js +46 -23
  3. package/dist/cli/format-cmd.d.ts +9 -17
  4. package/dist/cli/format-cmd.js +37 -49
  5. package/dist/cli/format-config.d.ts +6 -9
  6. package/dist/cli/format-config.js +5 -29
  7. package/dist/delivery/delivery-profile.js +1 -1
  8. package/dist/dev/dev-session.js +16 -5
  9. package/dist/document/document-base-css.d.ts +15 -0
  10. package/dist/document/document-base-css.js +578 -0
  11. package/dist/document/document-build.d.ts +1 -0
  12. package/dist/document/document-build.js +94 -78
  13. package/dist/document/document-client.d.ts +13 -0
  14. package/dist/document/document-client.js +217 -0
  15. package/dist/document/document-cmd.js +3 -1
  16. package/dist/document/document-designs.d.ts +9 -3
  17. package/dist/document/document-designs.js +20 -34
  18. package/dist/document/document-enrich.d.ts +9 -0
  19. package/dist/document/document-enrich.js +55 -0
  20. package/dist/document/document-highlight.d.ts +26 -0
  21. package/dist/document/document-highlight.js +54 -0
  22. package/dist/document/document-integrate.d.ts +5 -0
  23. package/dist/document/document-integrate.js +1 -0
  24. package/dist/document/document-interactive.js +10 -2
  25. package/dist/document/document-layout-render.js +6 -2
  26. package/dist/document/document-schema.d.ts +5 -0
  27. package/dist/workspace/build-assemble.js +18 -6
  28. package/dist/workspace/cdn-policy.js +12 -0
  29. package/dist/workspace/content-addressed-assets.d.ts +6 -17
  30. package/dist/workspace/content-addressed-assets.js +10 -95
  31. package/dist/workspace/delivery-runtime-imports.js +3 -0
  32. package/dist/workspace/native-addon.d.ts +4 -0
  33. package/dist/workspace/pack-native.d.ts +139 -0
  34. package/dist/workspace/pack-native.js +273 -0
  35. package/dist/workspace/pack.d.ts +113 -27
  36. package/dist/workspace/pack.js +271 -54
  37. package/dist/workspace/plugin-host.d.ts +4 -4
  38. package/dist/workspace/plugin-host.js +9 -12
  39. package/dist/workspace/production-observability.d.ts +2 -0
  40. package/dist/workspace/production-observability.js +6 -2
  41. package/dist/workspace/project-build.js +26 -4
  42. package/dist/workspace/public-api.d.ts +7 -3
  43. package/dist/workspace/public-api.js +5 -3
  44. package/dist/workspace/static-emit.d.ts +30 -0
  45. package/dist/workspace/static-emit.js +211 -18
  46. package/dist/workspace/user-config.d.ts +112 -0
  47. package/dist/workspace/user-config.js +12 -0
  48. package/host-runtime-files.json +1 -0
  49. package/locales/en-US/cli.json +5 -4
  50. package/package.json +20 -20
  51. package/dist/workspace/pack-client-packages.d.ts +0 -32
  52. package/dist/workspace/pack-client-packages.js +0 -437
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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="Document search"
197
+ aria-label="${esc(label)}"
192
198
  >
193
- <p class="doc-search-fallback">Search requires Island resume; use navigation without JavaScript.</p>
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
- const href = pathToFileURL(path.join(distDir, `${chunkId}.client.js`)).href;
50
- const mod = await import(`${href}?t=${Date.now()}`);
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' || assembly === 'local-static') {
45
- // SSR / local packs: favicon + opaque public/ for serve-host.
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: 'client modules packed; no ServerArtifact',
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)} units=${packCtx.unitCount ?? '?'}`
189
+ ? `pack=${String(packCtx.packDigest).slice(0, 12)} chunks=${packCtx.chunkCount ?? '?'}`
178
190
  : 'pack missing',
179
191
  };
180
192
  }
@@ -396,6 +396,18 @@ function contentType(file) {
396
396
  return 'text/javascript; charset=utf-8';
397
397
  if (file.endsWith('.css'))
398
398
  return 'text/css; charset=utf-8';
399
+ if (file.endsWith('.wasm'))
400
+ return 'application/wasm';
401
+ if (file.endsWith('.woff2'))
402
+ return 'font/woff2';
403
+ if (file.endsWith('.woff'))
404
+ return 'font/woff';
405
+ if (file.endsWith('.otf'))
406
+ return 'font/otf';
407
+ if (file.endsWith('.ttf'))
408
+ return 'font/ttf';
409
+ if (file.endsWith('.svg'))
410
+ return 'image/svg+xml';
399
411
  if (file.endsWith('.xml'))
400
412
  return 'application/xml';
401
413
  if (file.endsWith('.txt'))
@@ -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` / `moduleScriptSrc` at shell emit time — this module does **not**
5
- * post-rewrite HTML. CSS aggregators rewrite `@import` before hash. JS under
6
- * `assets/` rewrites ESM `./` → `../` via oxc (plus dynamic `import("./"+)` form)
7
- * so barrels resolve at dist root.
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` / `moduleScriptSrc` at shell emit time — this module does **not**
5
- * post-rewrite HTML. CSS aggregators rewrite `@import` before hash. JS under
6
- * `assets/` rewrites ESM `./` → `../` via oxc (plus dynamic `import("./"+)` form)
7
- * so barrels resolve at dist root.
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 (client-facing bytes). */
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) && !JS_ENTRY_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) && !JS_ENTRY_AGGREGATORS.has(name) && !out.includes(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)
@@ -45,6 +45,9 @@ function rewriteNestedSsrDelivery(text) {
45
45
  }
46
46
  function rewriteNestedBrowserDelivery(text) {
47
47
  let out = replaceImportPath(text, '../../shared/dom-attr-normalize.js', '../../dom-attr-normalize.js');
48
+ // `dom-ssr.js` 只交付在根(flat barrel),nested `ssr/` 下没有它:
49
+ // island resume 的动态边必须指向根级产物。
50
+ out = replaceImportPath(out, '../../ssr/dom-ssr.js', '../../dom-ssr.js');
48
51
  for (const [from, to] of [
49
52
  ['../direct-host-box.js', '../../direct-host-box.js'],
50
53
  ['../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). */