@markless/router 0.8.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 REPLACE_ATTRIBUTE, f as SCROLL_ATTRIBUTE, l as LINK_ATTRIBUTE, u as PREFETCH_ATTRIBUTE } from "./request-locals-BDAhNpEa.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-BsYTWdxi.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,171 @@ 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
+ }
1707
+ //#endregion
1708
+ //#region src/vite/nitro-dev-close.ts
1709
+ function devNitroState() {
1710
+ return { nitro: void 0 };
1711
+ }
1712
+ function devNitroModule(state) {
1713
+ return {
1714
+ name: "markless-router:dev-nitro",
1715
+ setup(nitro) {
1716
+ state.nitro = nitro;
1717
+ }
1718
+ };
1719
+ }
1720
+ function devNitroClosePlugin(state) {
1721
+ return {
1722
+ name: "markless-router:dev-nitro-close",
1723
+ apply: "serve",
1724
+ async closeBundle() {
1725
+ const nitro = state.nitro;
1726
+ state.nitro = void 0;
1727
+ await nitro?.close();
1728
+ }
1729
+ };
1730
+ }
1529
1731
  //#endregion
1530
1732
  //#region src/vite/nitro-prerender.ts
1531
1733
  function serverBuildState() {
@@ -1537,11 +1739,19 @@ function recordServerEntry(state, outDir, bundle, isServerEntry) {
1537
1739
  const entry = entries.find((chunk) => chunk.facadeModuleId && isServerEntry(chunk.facadeModuleId)) ?? (entries.length === 1 ? entries[0] : void 0);
1538
1740
  if (entry) state.entry = resolve(outDir, entry.fileName);
1539
1741
  }
1540
- function prerenderRendererModule(state) {
1742
+ function prerenderRendererModule(state, clientDirs) {
1541
1743
  return {
1542
1744
  name: "markless-router:prerender-renderer",
1543
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
+ });
1544
1750
  nitro.hooks.hook("prerender:config", async (config) => {
1751
+ config.output = {
1752
+ ...config.output,
1753
+ publicDir: nitro.options.output.publicDir
1754
+ };
1545
1755
  if (!state.entry) return;
1546
1756
  const dir = join(nitro.options.buildDir, "markless-router");
1547
1757
  const handler = join(dir, "prerender-renderer.mjs");
@@ -1557,6 +1767,45 @@ function prerenderRendererModule(state) {
1557
1767
  }
1558
1768
  };
1559
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
+ }
1560
1809
  //#endregion
1561
1810
  //#region src/vite/index.ts
1562
1811
  const PAGE_SERVER_ONLY_PROPS = [{
@@ -1569,6 +1818,9 @@ const CLIENT_ENTRY_ID = "virtual:markless-router/client-entry";
1569
1818
  const RESUME_ENTRY_ID = "virtual:markless-router/resume-entry";
1570
1819
  const RESUME_ENTRY_ORIGIN = "/entries/resume-entry.ts";
1571
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";
1572
1824
  const PRERENDER_WAKE_ENTRY_ID = "virtual:markless-router/prerender-wake-entry";
1573
1825
  const PRERENDER_WAKE_ENTRY_ORIGIN = "/entries/prerender-wake-entry.ts";
1574
1826
  const PRERENDER_WAKE_ENTRY_PATH_ID = "virtual:markless-router/prerender-wake-entry-path";
@@ -1585,7 +1837,7 @@ const LINK_BRIDGE_PATHS_PLACEHOLDER = "__MARKLESS_ROUTER_LINK_BRIDGE_PATHS__";
1585
1837
  const SERVER_ENTRY_ID = "virtual:markless-router/server-entry";
1586
1838
  const ROUTE_HREF_ID = "virtual:markless-router/route-href";
1587
1839
  const ROUTER_OPTIONS_ID = "virtual:markless-router/options";
1588
- 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)(?:\?.*)?$/;
1589
1841
  const VITE_PLUGIN_FILE = decodePath(parseURL(import.meta.url).pathname);
1590
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");
1591
1843
  const DEFAULT_WATCH_IGNORES = [
@@ -1598,6 +1850,7 @@ const virtualEntryFiles = {
1598
1850
  [ROUTE_DISCOVERY_ID]: "route-discovery.ts",
1599
1851
  [CLIENT_ENTRY_ID]: "client-entry.ts",
1600
1852
  [RESUME_ENTRY_ID]: "resume-entry.ts",
1853
+ [RESUME_MODULES_ID]: "resume-modules.ts",
1601
1854
  [PRERENDER_WAKE_ENTRY_ID]: "prerender-wake-entry.ts",
1602
1855
  [NAVIGATION_ENTRY_ID]: "client-entry.ts",
1603
1856
  [FRAGMENT_ENTRY_ID]: "fragment-entry.ts",
@@ -1621,8 +1874,11 @@ function router(options = {}) {
1621
1874
  const navigationEntry = resumeEntryState();
1622
1875
  const fragmentEntry = resumeEntryState();
1623
1876
  const routePreloads = routePreloadState();
1877
+ const serverBuild = serverBuildState();
1878
+ const devNitro = devNitroState();
1879
+ const compressedAssets = compressedAssetsState();
1624
1880
  return [
1625
- routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry, serverBuildState()),
1881
+ routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry, serverBuild, devNitro, compressedAssets),
1626
1882
  devSourceModuleRequestPlugin(),
1627
1883
  devPageForUrlPlugin(options),
1628
1884
  pageServerOnlyPropsPlugin(),
@@ -1633,6 +1889,8 @@ function router(options = {}) {
1633
1889
  htmlTransformPlugin(),
1634
1890
  virtualModulesPlugin(resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, options, fragmentEntry),
1635
1891
  linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options),
1892
+ devNitroClosePlugin(devNitro),
1893
+ brotliCopiesPlugin(compressedAssets),
1636
1894
  nitroPlugins
1637
1895
  ];
1638
1896
  }
@@ -1697,7 +1955,7 @@ function devSourceModuleRequestPlugin() {
1697
1955
  }
1698
1956
  };
1699
1957
  }
1700
- function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState(), serverBuild = serverBuildState()) {
1958
+ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState(), serverBuild = serverBuildState(), devNitro = devNitroState(), compressedAssets = compressedAssetsState()) {
1701
1959
  let clientOutDir = "";
1702
1960
  let clientEnvironmentName = "";
1703
1961
  let clientEnvironmentConfig;
@@ -1730,12 +1988,10 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1730
1988
  ssrEnvironment.build.rolldownOptions ??= {};
1731
1989
  ssrEnvironment.build.rolldownOptions.input ??= scopedVirtualEntryId(SERVER_ENTRY_ID, config.root);
1732
1990
  const serverWatch = withWatchIgnores(config.server?.watch);
1733
- return {
1734
- nitro: createNitroConfig(config.nitro, config.root, serverWatch.ignored, env?.command === "build", serverBuild),
1735
- server: {
1736
- ...config.server,
1737
- watch: serverWatch
1738
- }
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
1739
1995
  };
1740
1996
  },
1741
1997
  configResolved(config) {
@@ -1781,6 +2037,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1781
2037
  ssr: {},
1782
2038
  styles: {}
1783
2039
  };
2040
+ routePreloads.resumeModules = {};
1784
2041
  routePreloads.persisted = false;
1785
2042
  routePreloads.importMap = void 0;
1786
2043
  await removeClientAssetsManifest(clientOutDir);
@@ -1794,6 +2051,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1794
2051
  fragmentEntry.fileName = manifest.entries.fragment ? clientAssetFileName(manifest.entries.fragment, manifest.base) : void 0;
1795
2052
  fragmentEntry.imports = manifest.entries.fragmentImports;
1796
2053
  routePreloads.routes = manifest.routes;
2054
+ routePreloads.resumeModules = manifest.resumeModules ?? {};
1797
2055
  routePreloads.persisted = true;
1798
2056
  routePreloads.importMap = manifest.importMap;
1799
2057
  },
@@ -1806,6 +2064,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1806
2064
  }
1807
2065
  const resumeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, RESUME_ENTRY_ORIGIN));
1808
2066
  if (resumeChunk) resumeEntry.fileName = resumeChunk.fileName;
2067
+ const resumeModulesChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, RESUME_MODULES_ORIGIN));
1809
2068
  const prerenderWakeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, PRERENDER_WAKE_ENTRY_ORIGIN));
1810
2069
  if (prerenderWakeChunk) prerenderWakeEntry.fileName = prerenderWakeChunk.fileName;
1811
2070
  const navigationChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, NAVIGATION_ENTRY_ORIGIN));
@@ -1829,6 +2088,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1829
2088
  navigationPolyfillIds,
1830
2089
  prerenderWakeChunk,
1831
2090
  resumeChunk,
2091
+ resumeModulesChunk,
1832
2092
  root: routePreloads.root
1833
2093
  });
1834
2094
  patchRoutePreloadsInBundle(bundle, routePreloads.routes);
@@ -1873,6 +2133,12 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1873
2133
  ...pendingClientAssets,
1874
2134
  importMap: JSON.parse(String(importMap.source))
1875
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
+ };
1876
2142
  await writeClientAssetsManifest(clientOutDir, pendingClientAssets);
1877
2143
  }
1878
2144
  };
@@ -1893,17 +2159,21 @@ function requestFileTransformPlugin() {
1893
2159
  configResolved(config) {
1894
2160
  root = config.root;
1895
2161
  },
1896
- transform(code, id) {
1897
- const fileId = requestFileIdForTransform(root, id);
1898
- if (!fileId) return;
1899
- const transform = transformRequestFileSource(fileId, code);
1900
- return transform ? {
1901
- code: transform.code,
1902
- map: null
1903
- } : 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
+ }
1904
2173
  }
1905
2174
  };
1906
2175
  }
2176
+ const REQUEST_FILE_ID_FILTER = /[\\/](?:api|middleware)[\\/]/;
1907
2177
  function requestFileIdForTransform(root, id) {
1908
2178
  if (id.startsWith("\0")) return;
1909
2179
  const pathname = decodePath(parseURL(id).pathname);
@@ -1916,18 +2186,21 @@ function requestFileIdForTransform(root, id) {
1916
2186
  function requestFileBuildTransformPlugin(root) {
1917
2187
  return {
1918
2188
  name: "markless-router:nitro-request-files",
1919
- transform(code, id) {
1920
- const fileId = requestFileIdForTransform(root, id);
1921
- if (!fileId) return;
1922
- const transform = transformRequestFileSource(fileId, code);
1923
- return transform ? {
1924
- code: transform.code,
1925
- map: null
1926
- } : 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
+ }
1927
2200
  }
1928
2201
  };
1929
2202
  }
1930
- function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState()) {
2203
+ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState(), devNitro = devNitroState(), compressedAssets = compressedAssetsState()) {
1931
2204
  const scanDirs = Array.isArray(nitroConfig?.scanDirs) ? nitroConfig.scanDirs.filter((dir) => typeof dir === "string") : [];
1932
2205
  const watchOptions = withWatchIgnores(nitroConfig?.watchOptions, serverWatchIgnored);
1933
2206
  const publicAssets = Array.isArray(nitroConfig?.publicAssets) ? nitroConfig.publicAssets : [];
@@ -1950,7 +2223,8 @@ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isB
1950
2223
  modules: [
1951
2224
  ...nitroConfig?.modules ?? [],
1952
2225
  fragmentRequestModule(),
1953
- ...isBuild ? [prerenderRendererModule(serverBuild)] : []
2226
+ compressedAssetsModule(nitroConfig?.compressPublicAssets !== void 0, compressedAssets),
2227
+ ...isBuild ? [prerenderRendererModule(serverBuild, [MARKLESS_BUILD_PREFIX, LINK_BRIDGE_DIR])] : [devNitroModule(devNitro)]
1954
2228
  ],
1955
2229
  ...isBuild && { routeRules: {
1956
2230
  [`/${MARKLESS_BUILD_PREFIX}**`]: { headers: { "cache-control": "public, max-age=31536000, immutable" } },
@@ -2014,7 +2288,8 @@ function routePreloadState() {
2014
2288
  navigation: {},
2015
2289
  ssr: {},
2016
2290
  styles: {}
2017
- }
2291
+ },
2292
+ resumeModules: {}
2018
2293
  };
2019
2294
  }
2020
2295
  let prerenderWakeChannelCaptured = false;
@@ -2023,18 +2298,21 @@ function prerenderWakeChannelEnabled() {
2023
2298
  }
2024
2299
  function routerClientInput(input, root) {
2025
2300
  const resumeEntryId = scopedVirtualEntryId(RESUME_ENTRY_ID, root);
2301
+ const resumeModulesId = scopedVirtualEntryId(RESUME_MODULES_ID, root);
2026
2302
  const prerenderWakeEntryId = scopedVirtualEntryId(PRERENDER_WAKE_ENTRY_ID, root);
2027
2303
  const navigationEntryId = scopedVirtualEntryId(NAVIGATION_ENTRY_ID, root);
2028
2304
  const fragmentEntryId = scopedVirtualEntryId(FRAGMENT_ENTRY_ID, root);
2029
2305
  const wakeEntries = prerenderWakeChannelEnabled() ? [prerenderWakeEntryId] : [];
2030
2306
  if (input === void 0) return [
2031
2307
  resumeEntryId,
2308
+ resumeModulesId,
2032
2309
  ...wakeEntries,
2033
2310
  navigationEntryId,
2034
2311
  fragmentEntryId
2035
2312
  ];
2036
2313
  if (typeof input === "string" || Array.isArray(input)) return [
2037
2314
  resumeEntryId,
2315
+ resumeModulesId,
2038
2316
  ...wakeEntries,
2039
2317
  navigationEntryId,
2040
2318
  fragmentEntryId,
@@ -2043,6 +2321,7 @@ function routerClientInput(input, root) {
2043
2321
  if (isRecord(input)) return {
2044
2322
  ...input,
2045
2323
  "markless-router-resume": resumeEntryId,
2324
+ "markless-router-resume-modules": resumeModulesId,
2046
2325
  ...prerenderWakeChannelEnabled() ? { "markless-router-prerender-wake": prerenderWakeEntryId } : {},
2047
2326
  "markless-router-navigation": navigationEntryId,
2048
2327
  "markless-router-fragment": fragmentEntryId
@@ -2054,6 +2333,7 @@ function isVirtualEntryChunk(chunk, origin) {
2054
2333
  }
2055
2334
  function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEntry = resumeEntryState(), navigationEntry = resumeEntryState(), routePreloads = routePreloadState(), options = {}, fragmentEntry = resumeEntryState()) {
2056
2335
  let root = "";
2336
+ let serving = false;
2057
2337
  return {
2058
2338
  name: "markless-router:routes",
2059
2339
  config() {
@@ -2062,6 +2342,7 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEnt
2062
2342
  sharedDuringBuild: true,
2063
2343
  configResolved(config) {
2064
2344
  root = config.root;
2345
+ serving = config.command === "serve";
2065
2346
  },
2066
2347
  resolveId: {
2067
2348
  filter: { id: PUBLIC_VIRTUAL_MODULE_ID_RE },
@@ -2071,27 +2352,31 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEnt
2071
2352
  id: `\0${baseId}${rootScopeQuery(root, id)}`,
2072
2353
  moduleSideEffects: false
2073
2354
  };
2074
- 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)}`;
2075
2356
  const entryFile = virtualEntryFiles[baseId];
2076
2357
  if (!entryFile) return;
2077
2358
  if (baseId === ROUTE_HREF_ID) return join(VIRTUAL_ENTRY_DIR, entryFile);
2078
2359
  return `${join(VIRTUAL_ENTRY_DIR, entryFile)}${rootScopeQuery(root, id)}`;
2079
2360
  }
2080
2361
  },
2081
- load(id) {
2082
- if (id.startsWith(`\0${SERVER_ENTRY_ID}`)) return serverEntrySource(root, options);
2083
- if (id.startsWith(`\0${RESUME_ENTRY_PATH_ID}`)) return entryPathSource("resumeEntryPath", resumeEntry, RESUME_ENTRY_ID, root);
2084
- if (id.startsWith(`\0${PRERENDER_WAKE_ENTRY_PATH_ID}`)) {
2085
- if (!prerenderWakeChannelEnabled() || !prerenderWakeEntry.fileName) return "export const prerenderWakeEntryPath = undefined;";
2086
- return entryPathSource("prerenderWakeEntryPath", prerenderWakeEntry, PRERENDER_WAKE_ENTRY_ID, root);
2087
- }
2088
- if (id.startsWith(`\0${NAVIGATION_ENTRY_PATH_ID}`)) return entryPathSource("navigationEntryPath", navigationEntry, NAVIGATION_ENTRY_ID, root);
2089
- if (id.startsWith(`\0${FRAGMENT_ENTRY_PATH_ID}`)) return entryPathSource("fragmentEntryPath", fragmentEntry, FRAGMENT_ENTRY_ID, root);
2090
- if (id.startsWith(`\0${ROUTE_PRELOADS_ID}`)) return routePreloadsSource(routePreloads, this?.environment?.config.consumer === "client");
2091
- if (id.startsWith(`\0${ROUTER_OPTIONS_ID}`)) {
2092
- const documentFile = join(root, "document.tsrx");
2093
- this?.addWatchFile?.(documentFile);
2094
- 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
+ }
2095
2380
  }
2096
2381
  }
2097
2382
  };
@@ -2188,7 +2473,7 @@ function serverEntrySource(root, options) {
2188
2473
  `import { prerenderWakeEntryPath } from ${JSON.stringify(`${PRERENDER_WAKE_ENTRY_PATH_ID}${query}`)};`,
2189
2474
  `import { navigationEntryPath } from ${JSON.stringify(`${NAVIGATION_ENTRY_PATH_ID}${query}`)};`,
2190
2475
  `import { fragmentEntryPath } from ${JSON.stringify(`${FRAGMENT_ENTRY_PATH_ID}${query}`)};`,
2191
- `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}`)};`,
2192
2477
  `import { pageModuleLoaders, routeFileIds } from ${JSON.stringify(`${ROUTE_DISCOVERY_ID}${query}`)};`,
2193
2478
  `import { documentNavigation } from ${JSON.stringify(`${ROUTER_OPTIONS_ID}${query}`)};`,
2194
2479
  `const documentModuleLoaders = import.meta.glob(['/document.tsrx']);`,
@@ -2206,6 +2491,7 @@ function serverEntrySource(root, options) {
2206
2491
  ...options.prefetch === false ? [" prefetch: false,"] : [],
2207
2492
  ` routeModulePreloads,`,
2208
2493
  ` routeSsrModulePreloads,`,
2494
+ ` routeResumeModules,`,
2209
2495
  ` routeStylesheets,`,
2210
2496
  ` documentStylesheets,`,
2211
2497
  ` importMap: documentImportMap,`,
@@ -2246,7 +2532,8 @@ function routePreloadsSource(state, client) {
2246
2532
  `export const routeStylesheets = ${state.persisted ? "routePreloadData.styles ?? {}" : "undefined"};`,
2247
2533
  `const documentStylesheetsJson = globalThis.__marklessRouterDocumentStylesheetsJson ?? ${JSON.stringify(DOCUMENT_STYLESHEETS_PLACEHOLDER)};`,
2248
2534
  `export const documentStylesheets = documentStylesheetsJson === ${JSON.stringify(DOCUMENT_STYLESHEETS_PLACEHOLDER)} ? [] : JSON.parse(documentStylesheetsJson);`,
2249
- `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 : {})};`
2250
2537
  ];
2251
2538
  return [
2252
2539
  `const routePreloadsJson = globalThis.__marklessRouterRoutePreloadsJson ?? ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)};`,
@@ -2376,7 +2663,7 @@ function routeModulePreloadsFromBundle(input) {
2376
2663
  const ssrFileNames = /* @__PURE__ */ new Set();
2377
2664
  if (input.resumeChunk) {
2378
2665
  includeChunk(ssrFileNames, landingScope, input.resumeChunk.fileName);
2379
- 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);
2380
2667
  }
2381
2668
  if (input.prerenderWakeChunk) {
2382
2669
  includeChunk(ssrFileNames, landingScope, input.prerenderWakeChunk.fileName);
@@ -2406,7 +2693,7 @@ function routeModulePreloadsFromBundle(input) {
2406
2693
  for (const entry of lazySiblings.length ? [input.resumeChunk, input.prerenderWakeChunk] : []) {
2407
2694
  if (!entry) continue;
2408
2695
  includeChunk(firstEventPath, landingScope, entry.fileName);
2409
- 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);
2410
2697
  }
2411
2698
  for (const fileName of lazySiblings) includeChunk(firstEventPath, landingScope, fileName);
2412
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));
@@ -2482,6 +2769,68 @@ function narrowToPlanned(fileNames, planned, scope, entries) {
2482
2769
  fileNames.clear();
2483
2770
  for (const fileName of [...ordered, ...kept]) fileNames.add(fileName);
2484
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
+ }
2485
2834
  function routeScopedDynamicImports(chunk, routeFile) {
2486
2835
  if (!chunk.code) return [];
2487
2836
  const imports = /* @__PURE__ */ new Set();