@markless/router 0.9.0 → 0.10.0

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/vite.js CHANGED
@@ -1,14 +1,14 @@
1
- import { d as PREFETCH_ATTRIBUTE, f as REPLACE_ATTRIBUTE, p as SCROLL_ATTRIBUTE, u as LINK_ATTRIBUTE } from "./request-locals-BPfJaoM_.js";
1
+ import { f as LINK_ATTRIBUTE, h as SCROLL_ATTRIBUTE, m as REPLACE_ATTRIBUTE, p as PREFETCH_ATTRIBUTE } from "./navigation-timing-CzH6VaBR.js";
2
2
  import { i as matchRouteManifest, l as transformRequestFileSource, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId, r as isPageModuleFile, t as assertNoEndpointConflicts } from "./route-manifest-DmoxXanW.js";
3
3
  import { t as __marklessRouteHref } from "./route-href-CGooO_g6.js";
4
4
  import { r as protocolIslandSegment } from "./protocol-constants-B4WxrZAS.js";
5
- import { a as runFragmentNavigation, c as listenForLinkIntent, i as FRAGMENT_PATH_PREFIX, n as routerLinkBridgeModules, o as startFragmentNavigation, s as documentHandlersMessage } from "./create-server-entry-CVhSjR45.js";
5
+ import { a as FRAGMENT_RESPONSE_HEADER, c as documentHandlersMessage, i as FRAGMENT_PATH_PREFIX, l as listenForLinkIntent, n as routerLinkBridgeModules, o as runFragmentNavigation, r as LINK_BRIDGE_DIR, s as startFragmentNavigation } from "./create-server-entry-BnaEfDHl.js";
6
6
  import { dirname, extname, isAbsolute, join, normalize, relative, resolve } from "pathe";
7
7
  import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withQuery, withoutBase, withoutLeadingSlash } from "ufo";
8
8
  import { existsSync, readdirSync } from "node:fs";
9
- import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
9
+ import { access, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
10
10
  import { nitro } from "nitro/vite";
11
- import { minifySync, sortUserPlugins } from "vite";
11
+ import { minifySync, parseAst, sortUserPlugins } from "vite";
12
12
  import { isClientPrimarySourceRequest, isExecutionLogChunk, isMarklessNavigationPack, isRenderDataSourceRequest, isResumeSourceRequest, isRouteNavigationSourceRequest, isSymbolOnlySourceRequest, lazyStartupPackName, marklessDeferredChunkFileNames, optimizedDepsToInclude, plannedLandingFiles, resolverVirtualModuleSourceFile, startupPackOfLazy, symbolVirtualModuleSourceFile, warmEvaluatedImports } from "@markless/bundler/preload";
13
13
  import { MARKLESS_BUILD_PREFIX, MARKLESS_CHUNK_SPECIFIER_PREFIX, MARKLESS_IMPORT_MAP_ASSET, MARKLESS_PUBLIC_BUILD_METADATA_FILES, importMapScript, marklessBuildManifestDir } from "@markless/bundler/rolldown";
14
14
  import { parse } from "@tsrx/yuku";
@@ -16,6 +16,9 @@ import { toHtml } from "hast-util-to-html";
16
16
  import { HastReader, createMdxHastHandle, defineHastPlugin, dropHandle, materializeHastTree, resolveHastSubscriptions, serializeHandle, visitHastHandle } from "satteri";
17
17
  import "@markless/web/render-to-string";
18
18
  import { isEventAttribute } from "@markless/compiler";
19
+ import { availableParallelism } from "node:os";
20
+ import { promisify } from "node:util";
21
+ import { brotliCompress, constants } from "node:zlib";
19
22
  //#region src/vite/tsrx-ast.ts
20
23
  /** A `.tsrx` module's tree; throws a `SyntaxError` naming the first error, as a module that does not parse has no tree to read. */
21
24
  function parseTsrxModule(source) {
@@ -195,6 +198,7 @@ function isNotFoundError(error) {
195
198
  //#region src/vite/anchor-transform.ts
196
199
  const ROUTE_HREF_HELPER_ID = "virtual:markless-router/route-href";
197
200
  const JSX_FILE_FILTER = /\.(?:[jt]sx|tsrx)(?:$|\?)/;
201
+ const VIRTUAL_MODULE_ID = /^\0/;
198
202
  const REWRITE_CACHE_LIMIT = 2048;
199
203
  const ROUTER_IMPORT_SOURCES = /* @__PURE__ */ new Set(["@markless/router", "@markless/core/router"]);
200
204
  function anchorTransformPlugin() {
@@ -214,7 +218,10 @@ function anchorTransformPlugin() {
214
218
  name: "markless-router:anchors",
215
219
  transform: {
216
220
  order: "pre",
217
- filter: { id: JSX_FILE_FILTER },
221
+ filter: { id: {
222
+ include: JSX_FILE_FILTER,
223
+ exclude: VIRTUAL_MODULE_ID
224
+ } },
218
225
  async handler(code, id) {
219
226
  if (!isTransformCandidateCode(code)) return;
220
227
  if (!routesLoaded) await refreshRoutes(this.fs);
@@ -622,6 +629,7 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
622
629
  };
623
630
  assertMatchingRouteKeys(routes);
624
631
  const importMap = input.importMap === void 0 ? void 0 : importMapImports(input.importMap);
632
+ const resumeModules = input.resumeModules === void 0 ? void 0 : routeResumeModules(input.resumeModules);
625
633
  const hrefs = /* @__PURE__ */ new Set([
626
634
  entries.resume,
627
635
  ...entries.prerenderWake !== void 0 ? [entries.prerenderWake] : [],
@@ -631,7 +639,8 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
631
639
  ...Object.values(routes.navigation).flat(),
632
640
  ...Object.values(routes.ssr).flat(),
633
641
  ...Object.values(routes.styles).flat(),
634
- ...Object.values(importMap?.imports ?? {})
642
+ ...Object.values(importMap?.imports ?? {}),
643
+ ...Object.values(resumeModules ?? {}).map(([href]) => href)
635
644
  ]);
636
645
  for (const href of hrefs) {
637
646
  const fileName = clientAssetFileName(href, base);
@@ -649,9 +658,15 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
649
658
  base,
650
659
  entries,
651
660
  routes,
661
+ ...resumeModules ? { resumeModules } : {},
652
662
  ...importMap ? { importMap } : {}
653
663
  };
654
664
  }
665
+ function routeResumeModules(input) {
666
+ const modules = routeMap(input, "resumeModules");
667
+ for (const [routeFile, [href, ...exports]] of Object.entries(modules)) if (!href || exports.length > 2 || !exports.every((name) => /^[\w$]+$/.test(name))) throw clientAssetsManifestError(`has an invalid resume module for route ${JSON.stringify(routeFile)}`);
668
+ return modules;
669
+ }
655
670
  function requiredStrings(input, label) {
656
671
  if (!Array.isArray(input)) throw clientAssetsManifestError(`has invalid ${label}`);
657
672
  return input.map((item, index) => requiredString(item, `${label}[${index}]`));
@@ -859,21 +874,21 @@ function emitQueuedResumeContainerEvent(source, { censusRepair = true } = {}) {
859
874
  " if (input.eventIndex >= 0) input = { ...input, eventRecord: (root.__mE ||= JSON.parse(root.querySelector('script[type=\"markless/view\"]').textContent).events)[input.eventIndex] };",
860
875
  censusRepair ? " if (root.__marklessShifted) return import('@markless/web/fns/served-census-repair').then((census) => (census.marklessRepairCensus(root), resumeContainerEvent(input)));" : "",
861
876
  " if (!root.__marklessDispatch) {",
862
- " let handler = marklessResumeContainerEvent, tail = Promise.resolve(), done = tail, lastEvent = null, lastEventTime = undefined, seen = new WeakMap();",
877
+ " let handler = marklessResumeContainerEvent, tail = Promise.resolve(), done = tail, lastEvent = null, lastEventTime = undefined, seen = new WeakMap(), life = root.__mL;",
863
878
  " root.__marklessRegisterDispatch = next => {",
864
879
  " const fallback = handler;",
865
880
  " handler = input => next(input, fallback);",
866
881
  " };",
867
882
  " root.__marklessDispatch = next => {",
868
883
  " const event = next.event, eventTime = event?.timeStamp;",
869
- " if (event && next.eventRecord && next.element) event.__marklessPressHost ??= next.element;",
884
+ " if (event && next.eventRecord && next.element) event.__marklessPressPath ??= [next.element];",
870
885
  " if ((event === lastEvent && eventTime === lastEventTime) || (seen.has(event) && seen.get(event) === eventTime)) return done;",
871
886
  " if (Object(event) === event) seen.set(event, eventTime);",
872
887
  " lastEvent = event;",
873
888
  " lastEventTime = eventTime;",
874
889
  " let release;",
875
890
  " const started = new Promise(resolve => release = resolve);",
876
- " const run = () => (root.__marklessReleaseDispatch = release, handler(next));",
891
+ " const run = () => root.__mL === life ? (root.__marklessReleaseDispatch = release, handler(next)) : release();",
877
892
  " done = tail.then(run, run);",
878
893
  " done.then(release, release);",
879
894
  " tail = started;",
@@ -885,6 +900,13 @@ function emitQueuedResumeContainerEvent(source, { censusRepair = true } = {}) {
885
900
  ].join("\n");
886
901
  }
887
902
  //#endregion
903
+ //#region ../bundler/src/served-render-data.ts
904
+ const MARKLESS_SERVED_RENDER_DATA_QUERY = "markless-served";
905
+ /** The served spelling of a render-data source request. */
906
+ function servedRenderDataSourceReference(source) {
907
+ return withQuery(source, { [MARKLESS_SERVED_RENDER_DATA_QUERY]: null });
908
+ }
909
+ //#endregion
888
910
  //#region src/vite/mdx.ts
889
911
  const MDX_ROUTE_RUNTIME_SPECIFIER = "@markless/router/vite/runtime/mdx-route";
890
912
  function mdxTransformPlugin() {
@@ -898,6 +920,7 @@ function mdxTransformPlugin() {
898
920
  },
899
921
  transform: {
900
922
  order: "pre",
923
+ filter: { id: /\.mdx(?:$|[?#])/ },
901
924
  async handler(code, id) {
902
925
  if (!isMdxFile(id)) return;
903
926
  return {
@@ -933,7 +956,7 @@ function emitMdxClientRoute(route, id, resume) {
933
956
  return [
934
957
  `import { createMdxRenderDataSurface, loadMdxSymbol${resume ? ", tryResumeMdxScalar" : ""} } from '${MDX_ROUTE_RUNTIME_SPECIFIER}';`,
935
958
  `const marklessMdxSymbolLoaders = ${renderSymbolLoaders(route.components)};`,
936
- `const marklessMdxRenderData = ${renderMdxRenderDataLoader(route.components, id, route.parts)};`,
959
+ `const marklessMdxRenderData = ${renderMdxRenderDataLoader(route.components, id, route.parts, resume)};`,
937
960
  renderOverlayLoader(),
938
961
  emitRosterResumeLoaderInstall(),
939
962
  "const marklessMdxPage = {",
@@ -1334,17 +1357,16 @@ function renderHeadInjections(components) {
1334
1357
  function renderSymbolLoaders(components) {
1335
1358
  return `[${components.map((component) => `(() => { let module; const loadModule = () => module ??= import(${JSON.stringify(scalarPlanSourceReference(component.specifier))}).catch(error => { module = undefined; throw error; }); return { prefix: ${JSON.stringify(component.prefix)}, loadSymbol(symbolId) { return loadModule().then((mod) => mod.loadSymbol(symbolId.slice(${component.prefix.length}))); }, loadScalarActionPlan(symbolId) { return loadModule().then(mod => mod.loadScalarActionPlan?.(symbolId.slice(${component.prefix.length}))).then(action => action && ({ ...action, scope: ${JSON.stringify(component.prefix)} + action.scope })); } }; })()`).join(", ")}]`;
1336
1359
  }
1337
- function renderMdxRenderDataLoader(components, id, parts) {
1360
+ function renderMdxRenderDataLoader(components, id, parts, served = false) {
1338
1361
  const reachedFrom = mdxMaterializedReachRoot(id);
1339
1362
  const imports = components.map((component) => {
1340
1363
  const source = withQuery(component.specifier, {
1341
1364
  "markless-render-data": null,
1342
1365
  ...reachedFrom ? { "markless-reached-from": reachedFrom } : {}
1343
1366
  });
1344
- return `import(${JSON.stringify(source)})`;
1367
+ return `import(${JSON.stringify(served ? servedRenderDataSourceReference(source) : source)})`;
1345
1368
  });
1346
- const exportName = reachedFrom ? "marklessPrerenderData" : "marklessRenderData";
1347
- const children = components.map((component, index) => `{ componentIndex: ${index}, hostPrefix: ${JSON.stringify(component.prefix)}, symbolPrefix: ${JSON.stringify(component.prefix)}, props: ${componentPropsExpression(component)}, surface: modules[${index}].${exportName} }`);
1369
+ const children = components.map((component, index) => `{ componentIndex: ${index}, hostPrefix: ${JSON.stringify(component.prefix)}, symbolPrefix: ${JSON.stringify(component.prefix)}, props: ${componentPropsExpression(component)}, surface: modules[${index}].marklessPrerenderData }`);
1348
1370
  const load = `Promise.all([${imports.join(", ")}]).then(modules => createMdxRenderDataSurface(marklessMdxParts, [${children.join(", ")}]))`;
1349
1371
  return parts ? `() => {\nconst marklessMdxParts = ${JSON.stringify(parts)};\nreturn ${load};\n}` : `() => ${load}`;
1350
1372
  }
@@ -1511,6 +1533,8 @@ function fragmentRequestModule() {
1511
1533
  await mkdir(dir, { recursive: true });
1512
1534
  await writeFile(plugin, [
1513
1535
  `const prefix = ${JSON.stringify(`${FRAGMENT_PATH_PREFIX}/`)};`,
1536
+ `const marker = ${JSON.stringify(FRAGMENT_RESPONSE_HEADER)};`,
1537
+ "const fragments = new WeakSet();",
1514
1538
  "export default function fragmentRequests(nitroApp) {",
1515
1539
  " nitroApp.hooks?.hook('request', (event) => {",
1516
1540
  " const { pathname } = event.url;",
@@ -1518,6 +1542,19 @@ function fragmentRequestModule() {
1518
1542
  " const page = new URL(event.url.href);",
1519
1543
  " page.pathname = pathname.slice(prefix.length - 1);",
1520
1544
  " event.url = page;",
1545
+ " fragments.add(event);",
1546
+ " event.res?.headers?.set(marker, '1');",
1547
+ " });",
1548
+ " nitroApp.hooks?.hook('response', (response, event) => {",
1549
+ " if (!fragments.has(event) || response.status < 300 || response.status > 399) return;",
1550
+ " const location = response.headers.get('location');",
1551
+ " if (!location) return;",
1552
+ " const target = new URL(location, event.url);",
1553
+ " if (target.origin !== event.url.origin || target.pathname.startsWith(prefix)) return;",
1554
+ " target.pathname = prefix.slice(0, -1) + target.pathname;",
1555
+ " try {",
1556
+ " response.headers.set('location', target.href);",
1557
+ " } catch {}",
1521
1558
  " });",
1522
1559
  "}",
1523
1560
  ""
@@ -1526,6 +1563,147 @@ function fragmentRequestModule() {
1526
1563
  }
1527
1564
  };
1528
1565
  }
1566
+ const compressRenderedSource = `import { constants, createBrotliCompress, createGzip } from 'node:zlib';
1567
+ const accepts = (header, name) =>
1568
+ header.split(',').some((part) => {
1569
+ const [coding, ...params] = part.trim().toLowerCase().split(';');
1570
+ const q = params.map((param) => param.trim()).find((param) => param.startsWith('q='));
1571
+ return coding === name && (!q || Number(q.slice(2)) > 0);
1572
+ });
1573
+ const compressed = (source, zip) =>
1574
+ new ReadableStream({
1575
+ start(controller) {
1576
+ zip.on('data', (chunk) => controller.enqueue(new Uint8Array(chunk)));
1577
+ zip.on('end', () => controller.close());
1578
+ zip.on('error', (error) => controller.error(error));
1579
+ void (async () => {
1580
+ const reader = source.getReader();
1581
+ try {
1582
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read())
1583
+ if (!zip.write(chunk.value)) await new Promise((done) => zip.once('drain', done));
1584
+ zip.end();
1585
+ } catch (error) {
1586
+ zip.destroy(error);
1587
+ }
1588
+ })();
1589
+ },
1590
+ cancel(reason) {
1591
+ zip.destroy();
1592
+ return source.cancel(reason);
1593
+ },
1594
+ });
1595
+ export default async function compressRendered(event, next) {
1596
+ const response = await next();
1597
+ if (!(response instanceof Response) || !response.body) return response;
1598
+ const headers = response.headers;
1599
+ if (headers.has('content-encoding') || !/^text\\/html\\b/i.test(headers.get('content-type') ?? '')) return response;
1600
+ if (/\\bno-transform\\b/i.test(headers.get('cache-control') ?? '')) return response;
1601
+ const accepted = event.req.headers.get('accept-encoding') ?? '';
1602
+ const encoding = accepts(accepted, 'br') ? 'br' : accepts(accepted, 'gzip') ? 'gzip' : undefined;
1603
+ const sent = new Headers(headers);
1604
+ sent.append('vary', 'accept-encoding');
1605
+ if (!encoding) return new Response(response.body, { status: response.status, statusText: response.statusText, headers: sent });
1606
+ sent.set('content-encoding', encoding);
1607
+ sent.delete('content-length');
1608
+ const zip =
1609
+ encoding === 'br'
1610
+ ? createBrotliCompress({
1611
+ flush: constants.BROTLI_OPERATION_FLUSH,
1612
+ params: { [constants.BROTLI_PARAM_QUALITY]: 5 },
1613
+ })
1614
+ : createGzip({ flush: constants.Z_SYNC_FLUSH });
1615
+ return new Response(compressed(response.body, zip), { status: response.status, statusText: response.statusText, headers: sent });
1616
+ }
1617
+ `;
1618
+ const EDGE_CONTENT_TYPES = {
1619
+ js: "text/javascript; charset=utf-8",
1620
+ css: "text/css; charset=utf-8"
1621
+ };
1622
+ function compressedAssetsState() {
1623
+ return { brotliDir: void 0 };
1624
+ }
1625
+ function compressedAssetsModule(configured, state = compressedAssetsState()) {
1626
+ return {
1627
+ name: "markless-router:compressed-assets",
1628
+ async setup(nitro) {
1629
+ const { preset, serveStatic } = nitro.options;
1630
+ if (preset === "nitro-prerender") return;
1631
+ if (preset?.startsWith("vercel")) {
1632
+ if (configured) return;
1633
+ const base = nitro.options.baseURL ?? "/";
1634
+ nitro.options.vercel ??= {};
1635
+ nitro.options.vercel.config ??= {};
1636
+ nitro.options.vercel.config.routes = [...Object.entries(EDGE_CONTENT_TYPES).map(([extension, type]) => ({
1637
+ src: `^${escapeRegExp$1(`${base}${MARKLESS_BUILD_PREFIX}`)}(.+\\.${extension})$`,
1638
+ has: [{
1639
+ type: "header",
1640
+ key: "accept-encoding",
1641
+ value: ".*\\bbr\\b.*"
1642
+ }],
1643
+ dest: `${base}${MARKLESS_BUILD_PREFIX}$1.br`,
1644
+ headers: {
1645
+ "content-encoding": "br",
1646
+ "content-type": type,
1647
+ vary: "Accept-Encoding",
1648
+ "cache-control": "public, max-age=31536000, immutable"
1649
+ }
1650
+ })), ...nitro.options.vercel.config.routes ?? []];
1651
+ state.edgeBrotliDir = join(nitro.options.output.publicDir, MARKLESS_BUILD_PREFIX);
1652
+ return;
1653
+ }
1654
+ if (!serveStatic || serveStatic === "inline") return;
1655
+ if (!nitro.options.dev && nitro.options.buildDir && nitro.options.handlers) {
1656
+ const dir = join(nitro.options.buildDir, "markless-router");
1657
+ const handler = join(dir, "compress-rendered.mjs");
1658
+ await mkdir(dir, { recursive: true });
1659
+ await writeFile(handler, compressRenderedSource);
1660
+ nitro.options.handlers.push({
1661
+ route: "/**",
1662
+ middleware: true,
1663
+ handler
1664
+ });
1665
+ }
1666
+ if (configured) return;
1667
+ nitro.options.compressPublicAssets = { gzip: true };
1668
+ state.brotliDir = nitro.options.output.publicDir;
1669
+ }
1670
+ };
1671
+ }
1672
+ const brotli = promisify(brotliCompress);
1673
+ const exists = (file) => access(file).then(() => true, () => false);
1674
+ function brotliCopiesPlugin(state) {
1675
+ return {
1676
+ name: "markless-router:brotli-copies",
1677
+ apply: "build",
1678
+ applyToEnvironment: (environment) => environment.name === "nitro",
1679
+ async buildStart() {
1680
+ if (state.brotliDir) {
1681
+ const dir = state.brotliDir;
1682
+ await writeBrotliCopies((await readdir(dir, { recursive: true })).filter((file) => file.endsWith(".gz")).map((file) => join(dir, file.slice(0, -3))), 9);
1683
+ }
1684
+ if (state.edgeBrotliDir) {
1685
+ const dir = state.edgeBrotliDir;
1686
+ const extensions = Object.keys(EDGE_CONTENT_TYPES);
1687
+ await writeBrotliCopies((await readdir(dir, { recursive: true }).catch(() => [])).filter((file) => extensions.includes(file.slice(file.lastIndexOf(".") + 1))).map((file) => join(dir, file)), 11);
1688
+ }
1689
+ }
1690
+ };
1691
+ }
1692
+ async function writeBrotliCopies(files, quality) {
1693
+ const next = async () => {
1694
+ const file = files.pop();
1695
+ if (file === void 0) return;
1696
+ if (!await exists(`${file}.br`)) {
1697
+ const source = await readFile(file).catch(() => void 0);
1698
+ if (source) await writeFile(`${file}.br`, await brotli(source, { params: { [constants.BROTLI_PARAM_QUALITY]: quality } }));
1699
+ }
1700
+ return next();
1701
+ };
1702
+ await Promise.all(Array.from({ length: availableParallelism() }, next));
1703
+ }
1704
+ function escapeRegExp$1(text) {
1705
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1706
+ }
1529
1707
  //#endregion
1530
1708
  //#region src/vite/nitro-dev-close.ts
1531
1709
  function devNitroState() {
@@ -1561,11 +1739,19 @@ function recordServerEntry(state, outDir, bundle, isServerEntry) {
1561
1739
  const entry = entries.find((chunk) => chunk.facadeModuleId && isServerEntry(chunk.facadeModuleId)) ?? (entries.length === 1 ? entries[0] : void 0);
1562
1740
  if (entry) state.entry = resolve(outDir, entry.fileName);
1563
1741
  }
1564
- function prerenderRendererModule(state) {
1742
+ function prerenderRendererModule(state, clientDirs) {
1565
1743
  return {
1566
1744
  name: "markless-router:prerender-renderer",
1567
1745
  setup(nitro) {
1746
+ nitro.hooks.hook("prerender:done", async ({ prerenderedRoutes = [] }) => {
1747
+ const missing = await missingPrerenderedFiles(nitro.options.output.publicDir, nitro.options.baseURL, clientDirs, prerenderedRoutes.flatMap((route) => route.fileName ? [route.fileName] : []));
1748
+ if (missing.length > 0) throw new Error(missingPrerenderedFilesMessage(missing));
1749
+ });
1568
1750
  nitro.hooks.hook("prerender:config", async (config) => {
1751
+ config.output = {
1752
+ ...config.output,
1753
+ publicDir: nitro.options.output.publicDir
1754
+ };
1569
1755
  if (!state.entry) return;
1570
1756
  const dir = join(nitro.options.buildDir, "markless-router");
1571
1757
  const handler = join(dir, "prerender-renderer.mjs");
@@ -1581,6 +1767,45 @@ function prerenderRendererModule(state) {
1581
1767
  }
1582
1768
  };
1583
1769
  }
1770
+ const IMPORT_MAP = /<script\b[^>]*\btype=["']importmap["'][^>]*>([\s\S]*?)<\/script>/gi;
1771
+ const LOADED_URL = /<(?:script|link)\b[^>]*?\b(?:src|href)=["']([^"']+)["']/gi;
1772
+ /** URLs a prerendered page loads from the client build folders: script and link targets plus import map targets. */
1773
+ function prerenderedClientUrls(html, baseURL, clientDirs) {
1774
+ const urls = [...html.matchAll(LOADED_URL)].map((match) => match[1]);
1775
+ for (const [, source] of html.matchAll(IMPORT_MAP)) {
1776
+ let map;
1777
+ try {
1778
+ map = JSON.parse(source);
1779
+ } catch {
1780
+ throw new Error(`Markless Router prerendered an import map that is not JSON: ${source}`);
1781
+ }
1782
+ for (const target of [...Object.values(map.imports ?? {}), ...Object.values(map.scopes ?? {}).flatMap((scope) => Object.values(scope))]) if (typeof target === "string") urls.push(target);
1783
+ }
1784
+ const prefixes = clientDirs.map((dir) => joinURL(baseURL, dir, "/"));
1785
+ return [...new Set(urls)].filter((url) => prefixes.some((prefix) => url.startsWith(prefix)));
1786
+ }
1787
+ async function missingPrerenderedFiles(publicDir, baseURL, clientDirs, pageFiles) {
1788
+ const missing = [];
1789
+ const exists = /* @__PURE__ */ new Map();
1790
+ for (const page of pageFiles) {
1791
+ const html = await readFile(join(publicDir, page), "utf8").catch(() => void 0);
1792
+ if (html === void 0) continue;
1793
+ for (const url of prerenderedClientUrls(html, baseURL, clientDirs)) {
1794
+ const file = join(publicDir, decodeURIComponent(withoutBase(parseURL(url).pathname, baseURL)));
1795
+ if (!exists.has(file)) exists.set(file, stat(file).then((entry) => entry.isFile(), () => false));
1796
+ if (!await exists.get(file)) missing.push({
1797
+ page,
1798
+ url
1799
+ });
1800
+ }
1801
+ }
1802
+ return missing;
1803
+ }
1804
+ function missingPrerenderedFilesMessage(missing) {
1805
+ const shown = missing.slice(0, 20).map(({ page, url }) => `- ${page} loads ${url}`);
1806
+ if (missing.length > shown.length) shown.push(`- and ${missing.length - shown.length} more`);
1807
+ return `Markless Router prerendered pages that load client files this build did not emit, so a browser would fail to load them:\n${shown.join("\n")}\nAn earlier build's output or a stale cache reached the prerendered pages; delete the build output folders and build again.`;
1808
+ }
1584
1809
  //#endregion
1585
1810
  //#region src/vite/index.ts
1586
1811
  const PAGE_SERVER_ONLY_PROPS = [{
@@ -1593,6 +1818,9 @@ const CLIENT_ENTRY_ID = "virtual:markless-router/client-entry";
1593
1818
  const RESUME_ENTRY_ID = "virtual:markless-router/resume-entry";
1594
1819
  const RESUME_ENTRY_ORIGIN = "/entries/resume-entry.ts";
1595
1820
  const RESUME_ENTRY_PATH_ID = "virtual:markless-router/resume-entry-path";
1821
+ const RESUME_MODULES_ID = "virtual:markless-router/resume-modules";
1822
+ const RESUME_MODULES_ORIGIN = "/entries/resume-modules.ts";
1823
+ const DEV_RESUME_MODULES_ID = "virtual:markless-router/dev-resume-modules";
1596
1824
  const PRERENDER_WAKE_ENTRY_ID = "virtual:markless-router/prerender-wake-entry";
1597
1825
  const PRERENDER_WAKE_ENTRY_ORIGIN = "/entries/prerender-wake-entry.ts";
1598
1826
  const PRERENDER_WAKE_ENTRY_PATH_ID = "virtual:markless-router/prerender-wake-entry-path";
@@ -1609,7 +1837,7 @@ const LINK_BRIDGE_PATHS_PLACEHOLDER = "__MARKLESS_ROUTER_LINK_BRIDGE_PATHS__";
1609
1837
  const SERVER_ENTRY_ID = "virtual:markless-router/server-entry";
1610
1838
  const ROUTE_HREF_ID = "virtual:markless-router/route-href";
1611
1839
  const ROUTER_OPTIONS_ID = "virtual:markless-router/options";
1612
- const PUBLIC_VIRTUAL_MODULE_ID_RE = /^virtual:markless-router\/(?:routes|client-entry|resume-entry|resume-entry-path|prerender-wake-entry|prerender-wake-entry-path|navigation-entry|navigation-entry-path|fragment-entry|fragment-entry-path|route-preloads|server-entry|route-href|options)(?:\?.*)?$/;
1840
+ const PUBLIC_VIRTUAL_MODULE_ID_RE = /^virtual:markless-router\/(?:routes|client-entry|resume-entry|resume-entry-path|resume-modules|dev-resume-modules|prerender-wake-entry|prerender-wake-entry-path|navigation-entry|navigation-entry-path|fragment-entry|fragment-entry-path|route-preloads|server-entry|route-href|options)(?:\?.*)?$/;
1613
1841
  const VITE_PLUGIN_FILE = decodePath(parseURL(import.meta.url).pathname);
1614
1842
  const VIRTUAL_ENTRY_DIR = VITE_PLUGIN_FILE.endsWith(".ts") ? join(dirname(VITE_PLUGIN_FILE), "entries") : join(dirname(dirname(VITE_PLUGIN_FILE)), "src/vite/entries");
1615
1843
  const DEFAULT_WATCH_IGNORES = [
@@ -1622,6 +1850,7 @@ const virtualEntryFiles = {
1622
1850
  [ROUTE_DISCOVERY_ID]: "route-discovery.ts",
1623
1851
  [CLIENT_ENTRY_ID]: "client-entry.ts",
1624
1852
  [RESUME_ENTRY_ID]: "resume-entry.ts",
1853
+ [RESUME_MODULES_ID]: "resume-modules.ts",
1625
1854
  [PRERENDER_WAKE_ENTRY_ID]: "prerender-wake-entry.ts",
1626
1855
  [NAVIGATION_ENTRY_ID]: "client-entry.ts",
1627
1856
  [FRAGMENT_ENTRY_ID]: "fragment-entry.ts",
@@ -1647,8 +1876,9 @@ function router(options = {}) {
1647
1876
  const routePreloads = routePreloadState();
1648
1877
  const serverBuild = serverBuildState();
1649
1878
  const devNitro = devNitroState();
1879
+ const compressedAssets = compressedAssetsState();
1650
1880
  return [
1651
- routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry, serverBuild, devNitro),
1881
+ routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry, serverBuild, devNitro, compressedAssets),
1652
1882
  devSourceModuleRequestPlugin(),
1653
1883
  devPageForUrlPlugin(options),
1654
1884
  pageServerOnlyPropsPlugin(),
@@ -1660,6 +1890,7 @@ function router(options = {}) {
1660
1890
  virtualModulesPlugin(resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, options, fragmentEntry),
1661
1891
  linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options),
1662
1892
  devNitroClosePlugin(devNitro),
1893
+ brotliCopiesPlugin(compressedAssets),
1663
1894
  nitroPlugins
1664
1895
  ];
1665
1896
  }
@@ -1724,7 +1955,7 @@ function devSourceModuleRequestPlugin() {
1724
1955
  }
1725
1956
  };
1726
1957
  }
1727
- function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState(), serverBuild = serverBuildState(), devNitro = devNitroState()) {
1958
+ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState(), serverBuild = serverBuildState(), devNitro = devNitroState(), compressedAssets = compressedAssetsState()) {
1728
1959
  let clientOutDir = "";
1729
1960
  let clientEnvironmentName = "";
1730
1961
  let clientEnvironmentConfig;
@@ -1757,12 +1988,10 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1757
1988
  ssrEnvironment.build.rolldownOptions ??= {};
1758
1989
  ssrEnvironment.build.rolldownOptions.input ??= scopedVirtualEntryId(SERVER_ENTRY_ID, config.root);
1759
1990
  const serverWatch = withWatchIgnores(config.server?.watch);
1760
- return {
1761
- nitro: createNitroConfig(config.nitro, config.root, serverWatch.ignored, env?.command === "build", serverBuild, devNitro),
1762
- server: {
1763
- ...config.server,
1764
- watch: serverWatch
1765
- }
1991
+ config.nitro = createNitroConfig(config.nitro, config.root, serverWatch.ignored, env?.command === "build", serverBuild, devNitro, compressedAssets);
1992
+ config.server = {
1993
+ ...config.server,
1994
+ watch: serverWatch
1766
1995
  };
1767
1996
  },
1768
1997
  configResolved(config) {
@@ -1808,6 +2037,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1808
2037
  ssr: {},
1809
2038
  styles: {}
1810
2039
  };
2040
+ routePreloads.resumeModules = {};
1811
2041
  routePreloads.persisted = false;
1812
2042
  routePreloads.importMap = void 0;
1813
2043
  await removeClientAssetsManifest(clientOutDir);
@@ -1821,6 +2051,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1821
2051
  fragmentEntry.fileName = manifest.entries.fragment ? clientAssetFileName(manifest.entries.fragment, manifest.base) : void 0;
1822
2052
  fragmentEntry.imports = manifest.entries.fragmentImports;
1823
2053
  routePreloads.routes = manifest.routes;
2054
+ routePreloads.resumeModules = manifest.resumeModules ?? {};
1824
2055
  routePreloads.persisted = true;
1825
2056
  routePreloads.importMap = manifest.importMap;
1826
2057
  },
@@ -1833,6 +2064,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1833
2064
  }
1834
2065
  const resumeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, RESUME_ENTRY_ORIGIN));
1835
2066
  if (resumeChunk) resumeEntry.fileName = resumeChunk.fileName;
2067
+ const resumeModulesChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, RESUME_MODULES_ORIGIN));
1836
2068
  const prerenderWakeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, PRERENDER_WAKE_ENTRY_ORIGIN));
1837
2069
  if (prerenderWakeChunk) prerenderWakeEntry.fileName = prerenderWakeChunk.fileName;
1838
2070
  const navigationChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, NAVIGATION_ENTRY_ORIGIN));
@@ -1856,6 +2088,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1856
2088
  navigationPolyfillIds,
1857
2089
  prerenderWakeChunk,
1858
2090
  resumeChunk,
2091
+ resumeModulesChunk,
1859
2092
  root: routePreloads.root
1860
2093
  });
1861
2094
  patchRoutePreloadsInBundle(bundle, routePreloads.routes);
@@ -1900,6 +2133,12 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1900
2133
  ...pendingClientAssets,
1901
2134
  importMap: JSON.parse(String(importMap.source))
1902
2135
  };
2136
+ const resumeModules = routeResumeModulesFromBundle(bundle ?? {}, routePreloads.base, pendingClientAssets.importMap?.imports ?? {}, pendingClientAssets.routes.ssr);
2137
+ routePreloads.resumeModules = resumeModules;
2138
+ pendingClientAssets = {
2139
+ ...pendingClientAssets,
2140
+ resumeModules
2141
+ };
1903
2142
  await writeClientAssetsManifest(clientOutDir, pendingClientAssets);
1904
2143
  }
1905
2144
  };
@@ -1920,17 +2159,21 @@ function requestFileTransformPlugin() {
1920
2159
  configResolved(config) {
1921
2160
  root = config.root;
1922
2161
  },
1923
- transform(code, id) {
1924
- const fileId = requestFileIdForTransform(root, id);
1925
- if (!fileId) return;
1926
- const transform = transformRequestFileSource(fileId, code);
1927
- return transform ? {
1928
- code: transform.code,
1929
- map: null
1930
- } : void 0;
2162
+ transform: {
2163
+ filter: { id: REQUEST_FILE_ID_FILTER },
2164
+ handler(code, id) {
2165
+ const fileId = requestFileIdForTransform(root, id);
2166
+ if (!fileId) return;
2167
+ const transform = transformRequestFileSource(fileId, code);
2168
+ return transform ? {
2169
+ code: transform.code,
2170
+ map: null
2171
+ } : void 0;
2172
+ }
1931
2173
  }
1932
2174
  };
1933
2175
  }
2176
+ const REQUEST_FILE_ID_FILTER = /[\\/](?:api|middleware)[\\/]/;
1934
2177
  function requestFileIdForTransform(root, id) {
1935
2178
  if (id.startsWith("\0")) return;
1936
2179
  const pathname = decodePath(parseURL(id).pathname);
@@ -1943,18 +2186,21 @@ function requestFileIdForTransform(root, id) {
1943
2186
  function requestFileBuildTransformPlugin(root) {
1944
2187
  return {
1945
2188
  name: "markless-router:nitro-request-files",
1946
- transform(code, id) {
1947
- const fileId = requestFileIdForTransform(root, id);
1948
- if (!fileId) return;
1949
- const transform = transformRequestFileSource(fileId, code);
1950
- return transform ? {
1951
- code: transform.code,
1952
- map: null
1953
- } : void 0;
2189
+ transform: {
2190
+ filter: { id: REQUEST_FILE_ID_FILTER },
2191
+ handler(code, id) {
2192
+ const fileId = requestFileIdForTransform(root, id);
2193
+ if (!fileId) return;
2194
+ const transform = transformRequestFileSource(fileId, code);
2195
+ return transform ? {
2196
+ code: transform.code,
2197
+ map: null
2198
+ } : void 0;
2199
+ }
1954
2200
  }
1955
2201
  };
1956
2202
  }
1957
- function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState(), devNitro = devNitroState()) {
2203
+ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState(), devNitro = devNitroState(), compressedAssets = compressedAssetsState()) {
1958
2204
  const scanDirs = Array.isArray(nitroConfig?.scanDirs) ? nitroConfig.scanDirs.filter((dir) => typeof dir === "string") : [];
1959
2205
  const watchOptions = withWatchIgnores(nitroConfig?.watchOptions, serverWatchIgnored);
1960
2206
  const publicAssets = Array.isArray(nitroConfig?.publicAssets) ? nitroConfig.publicAssets : [];
@@ -1977,7 +2223,8 @@ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isB
1977
2223
  modules: [
1978
2224
  ...nitroConfig?.modules ?? [],
1979
2225
  fragmentRequestModule(),
1980
- ...isBuild ? [prerenderRendererModule(serverBuild)] : [devNitroModule(devNitro)]
2226
+ compressedAssetsModule(nitroConfig?.compressPublicAssets !== void 0, compressedAssets),
2227
+ ...isBuild ? [prerenderRendererModule(serverBuild, [MARKLESS_BUILD_PREFIX, LINK_BRIDGE_DIR])] : [devNitroModule(devNitro)]
1981
2228
  ],
1982
2229
  ...isBuild && { routeRules: {
1983
2230
  [`/${MARKLESS_BUILD_PREFIX}**`]: { headers: { "cache-control": "public, max-age=31536000, immutable" } },
@@ -2041,7 +2288,8 @@ function routePreloadState() {
2041
2288
  navigation: {},
2042
2289
  ssr: {},
2043
2290
  styles: {}
2044
- }
2291
+ },
2292
+ resumeModules: {}
2045
2293
  };
2046
2294
  }
2047
2295
  let prerenderWakeChannelCaptured = false;
@@ -2050,18 +2298,21 @@ function prerenderWakeChannelEnabled() {
2050
2298
  }
2051
2299
  function routerClientInput(input, root) {
2052
2300
  const resumeEntryId = scopedVirtualEntryId(RESUME_ENTRY_ID, root);
2301
+ const resumeModulesId = scopedVirtualEntryId(RESUME_MODULES_ID, root);
2053
2302
  const prerenderWakeEntryId = scopedVirtualEntryId(PRERENDER_WAKE_ENTRY_ID, root);
2054
2303
  const navigationEntryId = scopedVirtualEntryId(NAVIGATION_ENTRY_ID, root);
2055
2304
  const fragmentEntryId = scopedVirtualEntryId(FRAGMENT_ENTRY_ID, root);
2056
2305
  const wakeEntries = prerenderWakeChannelEnabled() ? [prerenderWakeEntryId] : [];
2057
2306
  if (input === void 0) return [
2058
2307
  resumeEntryId,
2308
+ resumeModulesId,
2059
2309
  ...wakeEntries,
2060
2310
  navigationEntryId,
2061
2311
  fragmentEntryId
2062
2312
  ];
2063
2313
  if (typeof input === "string" || Array.isArray(input)) return [
2064
2314
  resumeEntryId,
2315
+ resumeModulesId,
2065
2316
  ...wakeEntries,
2066
2317
  navigationEntryId,
2067
2318
  fragmentEntryId,
@@ -2070,6 +2321,7 @@ function routerClientInput(input, root) {
2070
2321
  if (isRecord(input)) return {
2071
2322
  ...input,
2072
2323
  "markless-router-resume": resumeEntryId,
2324
+ "markless-router-resume-modules": resumeModulesId,
2073
2325
  ...prerenderWakeChannelEnabled() ? { "markless-router-prerender-wake": prerenderWakeEntryId } : {},
2074
2326
  "markless-router-navigation": navigationEntryId,
2075
2327
  "markless-router-fragment": fragmentEntryId
@@ -2081,6 +2333,7 @@ function isVirtualEntryChunk(chunk, origin) {
2081
2333
  }
2082
2334
  function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEntry = resumeEntryState(), navigationEntry = resumeEntryState(), routePreloads = routePreloadState(), options = {}, fragmentEntry = resumeEntryState()) {
2083
2335
  let root = "";
2336
+ let serving = false;
2084
2337
  return {
2085
2338
  name: "markless-router:routes",
2086
2339
  config() {
@@ -2089,6 +2342,7 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEnt
2089
2342
  sharedDuringBuild: true,
2090
2343
  configResolved(config) {
2091
2344
  root = config.root;
2345
+ serving = config.command === "serve";
2092
2346
  },
2093
2347
  resolveId: {
2094
2348
  filter: { id: PUBLIC_VIRTUAL_MODULE_ID_RE },
@@ -2098,27 +2352,31 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEnt
2098
2352
  id: `\0${baseId}${rootScopeQuery(root, id)}`,
2099
2353
  moduleSideEffects: false
2100
2354
  };
2101
- if (baseId === SERVER_ENTRY_ID || baseId === RESUME_ENTRY_PATH_ID || baseId === PRERENDER_WAKE_ENTRY_PATH_ID || baseId === NAVIGATION_ENTRY_PATH_ID || baseId === FRAGMENT_ENTRY_PATH_ID || baseId === ROUTE_PRELOADS_ID) return `\0${baseId}${rootScopeQuery(root, id)}`;
2355
+ if (baseId === SERVER_ENTRY_ID || baseId === RESUME_ENTRY_PATH_ID || baseId === PRERENDER_WAKE_ENTRY_PATH_ID || baseId === NAVIGATION_ENTRY_PATH_ID || baseId === FRAGMENT_ENTRY_PATH_ID || baseId === ROUTE_PRELOADS_ID || baseId === DEV_RESUME_MODULES_ID) return `\0${baseId}${rootScopeQuery(root, id)}`;
2102
2356
  const entryFile = virtualEntryFiles[baseId];
2103
2357
  if (!entryFile) return;
2104
2358
  if (baseId === ROUTE_HREF_ID) return join(VIRTUAL_ENTRY_DIR, entryFile);
2105
2359
  return `${join(VIRTUAL_ENTRY_DIR, entryFile)}${rootScopeQuery(root, id)}`;
2106
2360
  }
2107
2361
  },
2108
- load(id) {
2109
- if (id.startsWith(`\0${SERVER_ENTRY_ID}`)) return serverEntrySource(root, options);
2110
- if (id.startsWith(`\0${RESUME_ENTRY_PATH_ID}`)) return entryPathSource("resumeEntryPath", resumeEntry, RESUME_ENTRY_ID, root);
2111
- if (id.startsWith(`\0${PRERENDER_WAKE_ENTRY_PATH_ID}`)) {
2112
- if (!prerenderWakeChannelEnabled() || !prerenderWakeEntry.fileName) return "export const prerenderWakeEntryPath = undefined;";
2113
- return entryPathSource("prerenderWakeEntryPath", prerenderWakeEntry, PRERENDER_WAKE_ENTRY_ID, root);
2114
- }
2115
- if (id.startsWith(`\0${NAVIGATION_ENTRY_PATH_ID}`)) return entryPathSource("navigationEntryPath", navigationEntry, NAVIGATION_ENTRY_ID, root);
2116
- if (id.startsWith(`\0${FRAGMENT_ENTRY_PATH_ID}`)) return entryPathSource("fragmentEntryPath", fragmentEntry, FRAGMENT_ENTRY_ID, root);
2117
- if (id.startsWith(`\0${ROUTE_PRELOADS_ID}`)) return routePreloadsSource(routePreloads, this?.environment?.config.consumer === "client");
2118
- if (id.startsWith(`\0${ROUTER_OPTIONS_ID}`)) {
2119
- const documentFile = join(root, "document.tsrx");
2120
- this?.addWatchFile?.(documentFile);
2121
- return routerOptionsSource(options, documentFile);
2362
+ load: {
2363
+ filter: { id: /^\0/ },
2364
+ handler(id) {
2365
+ if (id.startsWith(`\0${SERVER_ENTRY_ID}`)) return serverEntrySource(root, options);
2366
+ if (id.startsWith(`\0${RESUME_ENTRY_PATH_ID}`)) return entryPathSource("resumeEntryPath", resumeEntry, RESUME_ENTRY_ID, root);
2367
+ if (id.startsWith(`\0${PRERENDER_WAKE_ENTRY_PATH_ID}`)) {
2368
+ if (!prerenderWakeChannelEnabled() || !prerenderWakeEntry.fileName) return "export const prerenderWakeEntryPath = undefined;";
2369
+ return entryPathSource("prerenderWakeEntryPath", prerenderWakeEntry, PRERENDER_WAKE_ENTRY_ID, root);
2370
+ }
2371
+ if (id.startsWith(`\0${NAVIGATION_ENTRY_PATH_ID}`)) return entryPathSource("navigationEntryPath", navigationEntry, NAVIGATION_ENTRY_ID, root);
2372
+ if (id.startsWith(`\0${FRAGMENT_ENTRY_PATH_ID}`)) return entryPathSource("fragmentEntryPath", fragmentEntry, FRAGMENT_ENTRY_ID, root);
2373
+ if (id.startsWith(`\0${DEV_RESUME_MODULES_ID}`)) return serving ? `export * from ${JSON.stringify(scopedVirtualEntryId(RESUME_MODULES_ID, root))};` : "export const mdxResumeModules = undefined, tsrxResumeModules = undefined;";
2374
+ if (id.startsWith(`\0${ROUTE_PRELOADS_ID}`)) return routePreloadsSource(routePreloads, this?.environment?.config.consumer === "client");
2375
+ if (id.startsWith(`\0${ROUTER_OPTIONS_ID}`)) {
2376
+ const documentFile = join(root, "document.tsrx");
2377
+ this?.addWatchFile?.(documentFile);
2378
+ return routerOptionsSource(options, documentFile);
2379
+ }
2122
2380
  }
2123
2381
  }
2124
2382
  };
@@ -2215,7 +2473,7 @@ function serverEntrySource(root, options) {
2215
2473
  `import { prerenderWakeEntryPath } from ${JSON.stringify(`${PRERENDER_WAKE_ENTRY_PATH_ID}${query}`)};`,
2216
2474
  `import { navigationEntryPath } from ${JSON.stringify(`${NAVIGATION_ENTRY_PATH_ID}${query}`)};`,
2217
2475
  `import { fragmentEntryPath } from ${JSON.stringify(`${FRAGMENT_ENTRY_PATH_ID}${query}`)};`,
2218
- `import { documentStylesheets, routeModulePreloads, routeSsrModulePreloads, routeStylesheets, documentImportMap } from ${JSON.stringify(`${ROUTE_PRELOADS_ID}${query}`)};`,
2476
+ `import { documentStylesheets, routeModulePreloads, routeSsrModulePreloads, routeStylesheets, documentImportMap, routeResumeModules } from ${JSON.stringify(`${ROUTE_PRELOADS_ID}${query}`)};`,
2219
2477
  `import { pageModuleLoaders, routeFileIds } from ${JSON.stringify(`${ROUTE_DISCOVERY_ID}${query}`)};`,
2220
2478
  `import { documentNavigation } from ${JSON.stringify(`${ROUTER_OPTIONS_ID}${query}`)};`,
2221
2479
  `const documentModuleLoaders = import.meta.glob(['/document.tsrx']);`,
@@ -2233,6 +2491,7 @@ function serverEntrySource(root, options) {
2233
2491
  ...options.prefetch === false ? [" prefetch: false,"] : [],
2234
2492
  ` routeModulePreloads,`,
2235
2493
  ` routeSsrModulePreloads,`,
2494
+ ` routeResumeModules,`,
2236
2495
  ` routeStylesheets,`,
2237
2496
  ` documentStylesheets,`,
2238
2497
  ` importMap: documentImportMap,`,
@@ -2273,7 +2532,8 @@ function routePreloadsSource(state, client) {
2273
2532
  `export const routeStylesheets = ${state.persisted ? "routePreloadData.styles ?? {}" : "undefined"};`,
2274
2533
  `const documentStylesheetsJson = globalThis.__marklessRouterDocumentStylesheetsJson ?? ${JSON.stringify(DOCUMENT_STYLESHEETS_PLACEHOLDER)};`,
2275
2534
  `export const documentStylesheets = documentStylesheetsJson === ${JSON.stringify(DOCUMENT_STYLESHEETS_PLACEHOLDER)} ? [] : JSON.parse(documentStylesheetsJson);`,
2276
- `export const documentImportMap = ${JSON.stringify(state.persisted && state.importMap ? importMapScript(state.importMap) : "")};`
2535
+ `export const documentImportMap = ${JSON.stringify(state.persisted && state.importMap ? importMapScript(state.importMap) : "")};`,
2536
+ `export const routeResumeModules = ${JSON.stringify(state.persisted ? state.resumeModules : {})};`
2277
2537
  ];
2278
2538
  return [
2279
2539
  `const routePreloadsJson = globalThis.__marklessRouterRoutePreloadsJson ?? ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)};`,
@@ -2403,7 +2663,7 @@ function routeModulePreloadsFromBundle(input) {
2403
2663
  const ssrFileNames = /* @__PURE__ */ new Set();
2404
2664
  if (input.resumeChunk) {
2405
2665
  includeChunk(ssrFileNames, landingScope, input.resumeChunk.fileName);
2406
- for (const fileName of routeScopedDynamicImports(input.resumeChunk, routeFile)) includeChunk(ssrFileNames, landingScope, fileName, true);
2666
+ for (const table of [input.resumeChunk, input.resumeModulesChunk]) for (const fileName of table ? routeScopedDynamicImports(table, routeFile) : []) includeChunk(ssrFileNames, landingScope, fileName, true);
2407
2667
  }
2408
2668
  if (input.prerenderWakeChunk) {
2409
2669
  includeChunk(ssrFileNames, landingScope, input.prerenderWakeChunk.fileName);
@@ -2433,7 +2693,7 @@ function routeModulePreloadsFromBundle(input) {
2433
2693
  for (const entry of lazySiblings.length ? [input.resumeChunk, input.prerenderWakeChunk] : []) {
2434
2694
  if (!entry) continue;
2435
2695
  includeChunk(firstEventPath, landingScope, entry.fileName);
2436
- for (const fileName of routeScopedDynamicImports(entry, routeFile)) includeChunk(firstEventPath, landingScope, fileName);
2696
+ for (const table of [entry, entry === input.resumeChunk && input.resumeModulesChunk]) for (const fileName of table ? routeScopedDynamicImports(table, routeFile) : []) includeChunk(firstEventPath, landingScope, fileName);
2437
2697
  }
2438
2698
  for (const fileName of lazySiblings) includeChunk(firstEventPath, landingScope, fileName);
2439
2699
  ssr[routeFile] = !routeHasSymbols && [...routeSourceFiles].some((file) => resolverSourceFiles.has(file)) ? [] : [...[...firstEventPath].filter((fileName) => ssrFileNames.has(fileName)), ...[...ssrFileNames].filter((fileName) => !firstEventPath.has(fileName))].map((fileName) => joinURL(input.base, fileName));
@@ -2509,6 +2769,68 @@ function narrowToPlanned(fileNames, planned, scope, entries) {
2509
2769
  fileNames.clear();
2510
2770
  for (const fileName of [...ordered, ...kept]) fileNames.add(fileName);
2511
2771
  }
2772
+ function routeResumeModulesFromBundle(bundle, base, importMap, landing) {
2773
+ const table = outputChunks(bundle).find((chunk) => isVirtualEntryChunk(chunk, RESUME_MODULES_ORIGIN));
2774
+ if (!table?.code) return {};
2775
+ const modules = {};
2776
+ const loaders = routeResumeLoaders(table.code);
2777
+ for (const { routeFile, specifier, evaluate } of loaders.found) {
2778
+ if (!evaluate) throw new Error(`Markless Router could not read how ${table.fileName} evaluates the resume module of ${routeFile}.`);
2779
+ const url = /^\.\.?\//.test(specifier) ? joinURL(base, normalize(join(dirname(table.fileName), specifier))) : importMap[specifier];
2780
+ if (!url) throw new Error(`Markless Router could not resolve the resume module of ${routeFile} (${specifier}).`);
2781
+ if (landing[routeFile]?.length && !landing[routeFile].includes(url)) throw new Error(`Markless Router's landing plan for ${routeFile} does not preload its resume module ${url}: the first event would fetch it.`);
2782
+ modules[routeFile] = [url, ...evaluate];
2783
+ }
2784
+ if (loaders.found.length !== loaders.imports) throw new Error(`Markless Router read ${loaders.found.length} of the ${loaders.imports} route resume loaders in ${table.fileName}.`);
2785
+ return modules;
2786
+ }
2787
+ function routeResumeLoaders(code) {
2788
+ const found = [];
2789
+ const evaluations = /* @__PURE__ */ new Map();
2790
+ let imports = 0;
2791
+ const visit = (node, route) => {
2792
+ if (Array.isArray(node)) return node.forEach((child) => visit(child, route));
2793
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return;
2794
+ const current = node;
2795
+ const key = current.type === "Property" ? current.key : void 0;
2796
+ const routeFile = typeof key?.value === "string" && /^\/pages\/.+\.(?:tsrx|mdx)$/.test(key.value) ? key.value.slice(1) : route;
2797
+ const then = current.type === "CallExpression" ? current.callee : void 0;
2798
+ if (then?.type === "MemberExpression" && then.object.type === "ImportExpression" && then.property.name === "then") evaluations.set(then.object, packEvaluation(current));
2799
+ if (current.type === "ImportExpression") {
2800
+ imports++;
2801
+ const specifier = staticString(current.source);
2802
+ if (routeFile && specifier) found.push({
2803
+ routeFile,
2804
+ specifier,
2805
+ evaluate: evaluations.has(current) ? evaluations.get(current) : []
2806
+ });
2807
+ }
2808
+ for (const [field, child] of Object.entries(current)) if (field !== "type") visit(child, routeFile);
2809
+ };
2810
+ visit(parseAst(code));
2811
+ return {
2812
+ found,
2813
+ imports
2814
+ };
2815
+ }
2816
+ function staticString(node) {
2817
+ if (node?.type === "Literal" && typeof node.value === "string") return node.value;
2818
+ const quasis = node?.type === "TemplateLiteral" ? node.quasis : [];
2819
+ return quasis.length === 1 ? quasis[0].value.cooked : void 0;
2820
+ }
2821
+ function packEvaluation(then) {
2822
+ const [callback] = then.arguments;
2823
+ const [param] = callback?.params ?? [];
2824
+ if (callback?.type !== "ArrowFunctionExpression" || param?.type !== "Identifier") return;
2825
+ const member = (node) => node?.type === "MemberExpression" && !node.computed && node.object.name === param.name ? node.property.name : void 0;
2826
+ const call = (node) => node?.type === "CallExpression" && node.arguments.length === 0 ? member(node.callee) : void 0;
2827
+ const body = callback.body;
2828
+ const [first, second, ...rest] = body.expressions ?? [];
2829
+ const init = body.type === "SequenceExpression" ? call(first) : call(body);
2830
+ const read = body.type === "SequenceExpression" && !rest.length ? member(second) : void 0;
2831
+ if (body.type === "SequenceExpression") return init && read ? [init, read] : void 0;
2832
+ return init ? [init] : void 0;
2833
+ }
2512
2834
  function routeScopedDynamicImports(chunk, routeFile) {
2513
2835
  if (!chunk.code) return [];
2514
2836
  const imports = /* @__PURE__ */ new Set();