@markless/router 0.5.0 → 0.6.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.
@@ -368,9 +368,23 @@ type ProtocolViewPayload = {
368
368
  * Element children of the parent that stand before the rows. Rows occupy
369
369
  * `[rowStartOffset, rowStartOffset + items.length)` among the parent's child
370
370
  * elements; a repeat whose rows begin at 0 omits the field. A repeat whose
371
- * prefix has no compile-time element count ships no record at all.
371
+ * prefix has no compile-time element count says `rowEndOffset` instead, or
372
+ * ships no record at all when its suffix has none either.
372
373
  */
373
374
  readonly rowStartOffset?: number;
375
+ /**
376
+ * Element children of the parent that stand after the rows, set only when
377
+ * the prefix has no compile-time element count. Rows then occupy the
378
+ * `items.length` element children ending `rowEndOffset` before the last.
379
+ */
380
+ readonly rowEndOffset?: number;
381
+ /**
382
+ * The browser can never change these rows: the collection and every value a
383
+ * row shows are fixed once the page is served, so the record carries no row
384
+ * markup and resume neither follows the collection nor loads row-building
385
+ * code. Pay-per-use: a repeat whose rows can change omits the field.
386
+ */
387
+ readonly fixed?: true;
374
388
  /**
375
389
  * The `@empty` arm's finished markup, for the mint that raises it when the
376
390
  * collection empties after resume and takes it back out when a row returns.
@@ -756,11 +770,13 @@ type RuntimeGraphDelete = {
756
770
  readonly graphNodeId: string;
757
771
  readonly path: ReadonlyArray<string>;
758
772
  };
773
+ /** DOM work a subscription holds back until its whole pass has run, so it lands in one task with that pass's journal. */
774
+ type RuntimeGraphCommit = () => void | Promise<void>;
759
775
  type RuntimeGraphSubscription = {
760
776
  readonly id: string;
761
777
  readonly graphNodeId: string;
762
778
  readonly path?: ReadonlyArray<string>;
763
- readonly run: (value: unknown) => DomJournalResult | void | Promise<DomJournalResult | void>;
779
+ readonly run: (value: unknown) => DomJournalResult | RuntimeGraphCommit | void | Promise<DomJournalResult | RuntimeGraphCommit | void>;
764
780
  };
765
781
  /**
766
782
  * Told about a write the moment it lands, before the flush that write schedules.
@@ -820,6 +836,8 @@ type RuntimeGraph = {
820
836
  readonly settleWriteObservers?: () => Promise<void> | undefined;
821
837
  /** Whether a write is waiting on a flush that has not yet reached the journal. */
822
838
  readonly hasPendingFlush?: () => boolean;
839
+ /** Keeps the scheduled flush back until each returned release has run once; `flush()` itself still runs. */
840
+ readonly holdFlush?: () => () => void;
823
841
  readonly subscribeJournal: (listener: DomJournalListener) => () => void;
824
842
  readonly flush: () => Promise<void>;
825
843
  readonly takeJournal: () => DomJournalEntry[];
@@ -897,6 +915,7 @@ type ResumeDomEvent = {
897
915
  readonly bubbles?: boolean;
898
916
  readonly preventDefault?: () => void;
899
917
  readonly stopPropagation?: () => void;
918
+ readonly composedPath?: () => ReadonlyArray<ResumeDomElement>;
900
919
  __marklessPressHost?: ResumeDomElement;
901
920
  };
902
921
  type ResumeSharedPatchEvent = {
@@ -1,4 +1,5 @@
1
1
  import { i as protocolStateVersion, n as protocolInstanceQualifies, t as protocolInstancePath } from "../../protocol-constants-B4WxrZAS.js";
2
+ //#endregion
2
3
  //#region ../serializer/src/storage-cookie.ts
3
4
  const STORAGE_RENDER_CONTEXT_READER = "storageValue";
4
5
  //#endregion
@@ -582,9 +583,11 @@ const marklessDecodeScalarCell = (c, g, s) => {
582
583
  const marklessFindElementAtDomOrderIndex = (r, i) => {
583
584
  let c = r.__marklessCensus;
584
585
  if (!c) {
585
- const w = document.createTreeWalker(r, 1);
586
+ const w = document.createTreeWalker(r, 129);
586
587
  c = r.__marklessCensus = r.nodeType === 1 ? [r] : [];
587
- for (let n; n = w.nextNode();) c.push(n);
588
+ for (let n; n = w.nextNode();) if (n.nodeType !== 8) c.push(n);
589
+ else if (n.data === "markless:page-start") c.length = 1;
590
+ else if (n.data === "markless:page-end") break;
588
591
  }
589
592
  return c[i];
590
593
  };
package/dist/vite.js CHANGED
@@ -1,17 +1,16 @@
1
- import { i as SCROLL_ATTRIBUTE, n as PREFETCH_ATTRIBUTE, r as REPLACE_ATTRIBUTE, t as LINK_ATTRIBUTE } from "./link-attributes-nY1Qkmx2.js";
2
- import { a as normalizeRouteFileId, n as isPageModuleFile, t as buildRouteManifestFromFileIds } from "./route-manifest-Bw_nFohL.js";
3
- import { r as transformRequestFileSource } from "./request-files-Ca8E0FOo.js";
1
+ import { c as PREFETCH_ATTRIBUTE, l as REPLACE_ATTRIBUTE, s as LINK_ATTRIBUTE, u as SCROLL_ATTRIBUTE } from "./request-locals-z3rJTizs.js";
2
+ import { l as transformRequestFileSource, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId, r as isPageModuleFile, t as assertNoEndpointConflicts } from "./route-manifest-DMizh42B.js";
4
3
  import { t as __marklessRouteHref } from "./route-href-CGooO_g6.js";
5
4
  import { r as protocolIslandSegment } from "./protocol-constants-B4WxrZAS.js";
6
- import { a as startFragmentNavigation, s as listenForLinkIntent } from "./fragment-navigation-CIiNnFFc.js";
5
+ import { a as runFragmentNavigation, i as FRAGMENT_PATH_PREFIX, n as routerLinkBridgeModules, o as startFragmentNavigation, s as listenForLinkIntent } from "./create-server-entry-DaYssO9x.js";
7
6
  import { dirname, extname, isAbsolute, join, normalize, relative, resolve } from "pathe";
8
7
  import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withQuery, withoutLeadingSlash } from "ufo";
9
8
  import { existsSync } from "node:fs";
10
9
  import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
11
10
  import { nitro } from "nitro/vite";
12
11
  import { minifySync, sortUserPlugins } from "vite";
13
- import { isClientPrimarySourceRequest, isExecutionLogChunk, isMarklessNavigationPack, isRenderDataSourceRequest, isResumeSourceRequest, isRouteNavigationSourceRequest, isSymbolOnlySourceRequest, lazyStartupPackName, marklessDeferredChunkFileNames, optimizedDepsToInclude, plannedLandingFiles, startupPackOfLazy, symbolVirtualModuleSourceFile } from "@markless/bundler/preload";
14
- import { MARKLESS_BUILD_METADATA_FILES, MARKLESS_BUILD_PREFIX, MARKLESS_CHUNK_SPECIFIER_PREFIX, MARKLESS_IMPORT_MAP_ASSET, importMapScript, marklessBuildManifestDir } from "@markless/bundler/rolldown";
12
+ import { isClientPrimarySourceRequest, isExecutionLogChunk, isMarklessNavigationPack, isRenderDataSourceRequest, isResumeSourceRequest, isRouteNavigationSourceRequest, isSymbolOnlySourceRequest, lazyStartupPackName, marklessDeferredChunkFileNames, optimizedDepsToInclude, plannedLandingFiles, resolverVirtualModuleSourceFile, startupPackOfLazy, symbolVirtualModuleSourceFile, warmEvaluatedImports } from "@markless/bundler/preload";
13
+ import { MARKLESS_BUILD_PREFIX, MARKLESS_CHUNK_SPECIFIER_PREFIX, MARKLESS_IMPORT_MAP_ASSET, MARKLESS_PUBLIC_BUILD_METADATA_FILES, importMapScript, marklessBuildManifestDir } from "@markless/bundler/rolldown";
15
14
  import { parseModule } from "@tsrx/yuku";
16
15
  import { toHtml } from "hast-util-to-html";
17
16
  import { HastReader, createMdxHastHandle, defineHastPlugin, dropHandle, materializeHastTree, resolveHastSubscriptions, serializeHandle, visitHastHandle } from "satteri";
@@ -119,6 +118,7 @@ function createLinkTypes() {
119
118
  //#endregion
120
119
  //#region src/vite/route-typegen.ts
121
120
  const PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".tsrx", ".mdx"]);
121
+ const ENDPOINT_EXTENSIONS = /* @__PURE__ */ new Set([".ts"]);
122
122
  function routeTypegenPlugin() {
123
123
  let root = "";
124
124
  return {
@@ -137,26 +137,25 @@ function routeTypegenPlugin() {
137
137
  async function writeRouteTypes(fs, root) {
138
138
  const pageFiles = await discoverPageFiles(fs, root);
139
139
  const manifest = buildRouteManifestFromFileIds(pageFiles);
140
+ assertNoEndpointConflicts(manifest, await discoverFiles(fs, root, "api", ENDPOINT_EXTENSIONS));
140
141
  await writeIfChanged(fs, join(root, routeTypesEnvPath), routeTypesEnvDeclaration);
141
142
  await writeIfChanged(fs, join(root, routeTypesOutputPath), createRouteTypesDeclaration(manifest));
142
143
  }
143
- async function discoverPageFiles(fs, root) {
144
- const pagesDir = join(root, "pages");
144
+ function discoverPageFiles(fs, root) {
145
+ return discoverFiles(fs, root, "pages", PAGE_EXTENSIONS);
146
+ }
147
+ async function discoverFiles(fs, root, dir, extensions) {
145
148
  const files = [];
146
- await collectPageFiles(fs, root, pagesDir, files);
149
+ await collectFiles(fs, root, join(root, dir), extensions, files);
147
150
  return files.toSorted((left, right) => left.localeCompare(right));
148
151
  }
149
- async function collectPageFiles(fs, root, dir, files) {
152
+ async function collectFiles(fs, root, dir, extensions, files) {
150
153
  const entries = await readdirIfExists(fs, dir);
151
- await Promise.all(entries.map((entry) => collectPageFile(fs, root, dir, files, entry)));
152
- }
153
- async function collectPageFile(fs, root, dir, files, entry) {
154
- const path = join(dir, entry.name);
155
- if (entry.isDirectory()) {
156
- await collectPageFiles(fs, root, path, files);
157
- return;
158
- }
159
- if (entry.isFile() && PAGE_EXTENSIONS.has(extname(path))) files.push(withLeadingSlash(relative(root, path)));
154
+ await Promise.all(entries.map(async (entry) => {
155
+ const path = join(dir, entry.name);
156
+ if (entry.isDirectory()) await collectFiles(fs, root, path, extensions, files);
157
+ else if (entry.isFile() && extensions.has(extname(path))) files.push(withLeadingSlash(relative(root, path)));
158
+ }));
160
159
  }
161
160
  async function readdirIfExists(fs, dir) {
162
161
  try {
@@ -513,7 +512,8 @@ function createClientAssetsManifest(input) {
513
512
  resume: input.resumeEntry,
514
513
  ...input.prerenderWakeEntry ? { prerenderWake: input.prerenderWakeEntry } : {},
515
514
  navigation: input.navigationEntry,
516
- ...input.fragmentEntry ? { fragment: input.fragmentEntry } : {}
515
+ ...input.fragmentEntry ? { fragment: input.fragmentEntry } : {},
516
+ ...input.fragmentImports?.length ? { fragmentImports: input.fragmentImports } : {}
517
517
  },
518
518
  routes: {
519
519
  navigation: sortRouteMap(input.routes.navigation),
@@ -602,7 +602,8 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
602
602
  resume: requiredString(input.entries.resume, "entries.resume"),
603
603
  ...input.entries.prerenderWake !== void 0 ? { prerenderWake: requiredString(input.entries.prerenderWake, "entries.prerenderWake") } : {},
604
604
  navigation: requiredString(input.entries.navigation, "entries.navigation"),
605
- ...input.entries.fragment !== void 0 ? { fragment: requiredString(input.entries.fragment, "entries.fragment") } : {}
605
+ ...input.entries.fragment !== void 0 ? { fragment: requiredString(input.entries.fragment, "entries.fragment") } : {},
606
+ ...input.entries.fragmentImports !== void 0 ? { fragmentImports: requiredStrings(input.entries.fragmentImports, "entries.fragmentImports") } : {}
606
607
  };
607
608
  const routes = {
608
609
  navigation: routeMap(input.routes.navigation, "routes.navigation"),
@@ -616,6 +617,7 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
616
617
  ...entries.prerenderWake !== void 0 ? [entries.prerenderWake] : [],
617
618
  entries.navigation,
618
619
  ...entries.fragment !== void 0 ? [entries.fragment] : [],
620
+ ...entries.fragmentImports ?? [],
619
621
  ...Object.values(routes.navigation).flat(),
620
622
  ...Object.values(routes.ssr).flat(),
621
623
  ...Object.values(routes.styles).flat(),
@@ -640,6 +642,10 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
640
642
  ...importMap ? { importMap } : {}
641
643
  };
642
644
  }
645
+ function requiredStrings(input, label) {
646
+ if (!Array.isArray(input)) throw clientAssetsManifestError(`has invalid ${label}`);
647
+ return input.map((item, index) => requiredString(item, `${label}[${index}]`));
648
+ }
643
649
  function routeMap(input, label) {
644
650
  if (!isRecord$1(input)) throw clientAssetsManifestError(`has invalid ${label}`);
645
651
  const routes = {};
@@ -840,7 +846,7 @@ function emitQueuedResumeContainerEvent(source, { censusRepair = true } = {}) {
840
846
  " const root = input.root;",
841
847
  censusRepair ? " if (root.__marklessShifted) return import('@markless/web/fns/served-census-repair').then((census) => (census.marklessRepairCensus(root), resumeContainerEvent(input)));" : "",
842
848
  " if (!root.__marklessDispatch) {",
843
- " let handler = marklessResumeContainerEvent, tail = Promise.resolve(), done = tail, lastEvent = null, lastEventTime = undefined;",
849
+ " let handler = marklessResumeContainerEvent, tail = Promise.resolve(), done = tail, lastEvent = null, lastEventTime = undefined, seen = new WeakMap();",
844
850
  " root.__marklessRegisterDispatch = next => {",
845
851
  " const fallback = handler;",
846
852
  " handler = input => next(input, fallback);",
@@ -848,7 +854,8 @@ function emitQueuedResumeContainerEvent(source, { censusRepair = true } = {}) {
848
854
  " root.__marklessDispatch = next => {",
849
855
  " const event = next.event, eventTime = event?.timeStamp;",
850
856
  " if (event && next.eventRecord && next.element) event.__marklessPressHost ??= next.element;",
851
- " if (event === lastEvent && eventTime === lastEventTime) return done;",
857
+ " if ((event === lastEvent && eventTime === lastEventTime) || (seen.has(event) && seen.get(event) === eventTime)) return done;",
858
+ " if (Object(event) === event) seen.set(event, eventTime);",
852
859
  " lastEvent = event;",
853
860
  " lastEventTime = eventTime;",
854
861
  " let release;",
@@ -1444,6 +1451,63 @@ function nodes(value) {
1444
1451
  return Array.isArray(value) ? value.filter((item) => node(item)) : [];
1445
1452
  }
1446
1453
  //#endregion
1454
+ //#region src/vite/nitro-fragment-requests.ts
1455
+ function fragmentRequestModule() {
1456
+ return {
1457
+ name: "markless-router:fragment-requests",
1458
+ async setup(nitro) {
1459
+ const dir = join(nitro.options.buildDir, "markless-router");
1460
+ const plugin = join(dir, "fragment-requests.mjs");
1461
+ await mkdir(dir, { recursive: true });
1462
+ await writeFile(plugin, [
1463
+ `const prefix = ${JSON.stringify(`${FRAGMENT_PATH_PREFIX}/`)};`,
1464
+ "export default function fragmentRequests(nitroApp) {",
1465
+ " nitroApp.hooks?.hook('request', (event) => {",
1466
+ " const { pathname } = event.url;",
1467
+ " if (!pathname.startsWith(prefix)) return;",
1468
+ " const page = new URL(event.url.href);",
1469
+ " page.pathname = pathname.slice(prefix.length - 1);",
1470
+ " event.url = page;",
1471
+ " });",
1472
+ "}",
1473
+ ""
1474
+ ].join("\n"));
1475
+ nitro.options.plugins.push(plugin);
1476
+ }
1477
+ };
1478
+ }
1479
+ //#endregion
1480
+ //#region src/vite/nitro-prerender.ts
1481
+ function serverBuildState() {
1482
+ return { entry: void 0 };
1483
+ }
1484
+ function recordServerEntry(state, outDir, bundle, isServerEntry) {
1485
+ if (!outDir) return;
1486
+ const entries = Object.values(bundle).filter((item) => item.type === "chunk" && item.isEntry);
1487
+ const entry = entries.find((chunk) => chunk.facadeModuleId && isServerEntry(chunk.facadeModuleId)) ?? (entries.length === 1 ? entries[0] : void 0);
1488
+ if (entry) state.entry = resolve(outDir, entry.fileName);
1489
+ }
1490
+ function prerenderRendererModule(state) {
1491
+ return {
1492
+ name: "markless-router:prerender-renderer",
1493
+ setup(nitro) {
1494
+ nitro.hooks.hook("prerender:config", async (config) => {
1495
+ if (!state.entry) return;
1496
+ const dir = join(nitro.options.buildDir, "markless-router");
1497
+ const handler = join(dir, "prerender-renderer.mjs");
1498
+ await mkdir(dir, { recursive: true });
1499
+ await writeFile(handler, [
1500
+ `import * as service from ${JSON.stringify(state.entry)};`,
1501
+ "const server = service.default ?? service;",
1502
+ "export default ({ req }) => server.fetch(req);",
1503
+ ""
1504
+ ].join("\n"));
1505
+ config.renderer = { handler };
1506
+ });
1507
+ }
1508
+ };
1509
+ }
1510
+ //#endregion
1447
1511
  //#region src/vite/index.ts
1448
1512
  const PAGE_SERVER_ONLY_PROPS = [{
1449
1513
  exportName: "default",
@@ -1467,6 +1531,7 @@ const FRAGMENT_ENTRY_PATH_ID = "virtual:markless-router/fragment-entry-path";
1467
1531
  const ROUTE_PRELOADS_ID = "virtual:markless-router/route-preloads";
1468
1532
  const ROUTE_PRELOADS_PLACEHOLDER = "__MARKLESS_ROUTER_ROUTE_PRELOADS__";
1469
1533
  const DOCUMENT_STYLESHEETS_PLACEHOLDER = "__MARKLESS_ROUTER_DOCUMENT_STYLESHEETS__";
1534
+ const LINK_BRIDGE_PATHS_PLACEHOLDER = "__MARKLESS_ROUTER_LINK_BRIDGE_PATHS__";
1470
1535
  const SERVER_ENTRY_ID = "virtual:markless-router/server-entry";
1471
1536
  const ROUTE_HREF_ID = "virtual:markless-router/route-href";
1472
1537
  const ROUTER_OPTIONS_ID = "virtual:markless-router/options";
@@ -1507,7 +1572,7 @@ function router(options = {}) {
1507
1572
  const fragmentEntry = resumeEntryState();
1508
1573
  const routePreloads = routePreloadState();
1509
1574
  return [
1510
- routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry),
1575
+ routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry, serverBuildState()),
1511
1576
  devSourceModuleRequestPlugin(),
1512
1577
  pageServerOnlyPropsPlugin(),
1513
1578
  mdxTransformPlugin(),
@@ -1516,6 +1581,7 @@ function router(options = {}) {
1516
1581
  anchorTransformPlugin(),
1517
1582
  htmlTransformPlugin(),
1518
1583
  virtualModulesPlugin(resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, options, fragmentEntry),
1584
+ linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options),
1519
1585
  nitroPlugins
1520
1586
  ];
1521
1587
  }
@@ -1523,7 +1589,9 @@ function pageServerOnlyPropsPlugin() {
1523
1589
  return {
1524
1590
  name: "markless-router:page-server-only-props",
1525
1591
  configResolved(config) {
1526
- config.plugins.find((plugin) => typeof plugin.api?.declareServerOnlyProps === "function")?.api.declareServerOnlyProps((moduleId) => isPageModuleFile(normalizeRouteFileId(moduleId)) ? PAGE_SERVER_ONLY_PROPS : void 0);
1592
+ const markless = config.plugins.find((plugin) => typeof plugin.api?.declareServerOnlyProps === "function");
1593
+ markless?.api.declareServerOnlyProps((moduleId) => isPageModuleFile(normalizeRouteFileId(moduleId)) ? PAGE_SERVER_ONLY_PROPS : void 0);
1594
+ markless?.api.declareRouteModules?.((moduleId) => isPageModuleFile(normalizeRouteFileId(moduleId)));
1527
1595
  }
1528
1596
  };
1529
1597
  }
@@ -1547,7 +1615,7 @@ function devSourceModuleRequestPlugin() {
1547
1615
  }
1548
1616
  };
1549
1617
  }
1550
- function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState()) {
1618
+ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState(), serverBuild = serverBuildState()) {
1551
1619
  let clientOutDir = "";
1552
1620
  let clientEnvironmentName = "";
1553
1621
  let clientEnvironmentConfig;
@@ -1581,7 +1649,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1581
1649
  ssrEnvironment.build.rolldownOptions.input ??= scopedVirtualEntryId(SERVER_ENTRY_ID, config.root);
1582
1650
  const serverWatch = withWatchIgnores(config.server?.watch);
1583
1651
  return {
1584
- nitro: createNitroConfig(config.nitro, config.root, serverWatch.ignored, env?.command === "build"),
1652
+ nitro: createNitroConfig(config.nitro, config.root, serverWatch.ignored, env?.command === "build", serverBuild),
1585
1653
  server: {
1586
1654
  ...config.server,
1587
1655
  watch: serverWatch
@@ -1642,6 +1710,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1642
1710
  prerenderWakeEntry.fileName = manifest.entries.prerenderWake ? clientAssetFileName(manifest.entries.prerenderWake, manifest.base) : void 0;
1643
1711
  navigationEntry.fileName = clientAssetFileName(manifest.entries.navigation, manifest.base);
1644
1712
  fragmentEntry.fileName = manifest.entries.fragment ? clientAssetFileName(manifest.entries.fragment, manifest.base) : void 0;
1713
+ fragmentEntry.imports = manifest.entries.fragmentImports;
1645
1714
  routePreloads.routes = manifest.routes;
1646
1715
  routePreloads.persisted = true;
1647
1716
  routePreloads.importMap = manifest.importMap;
@@ -1660,7 +1729,17 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1660
1729
  const navigationChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, NAVIGATION_ENTRY_ORIGIN));
1661
1730
  if (navigationChunk) navigationEntry.fileName = navigationChunk.fileName;
1662
1731
  const fragmentChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, FRAGMENT_ENTRY_ORIGIN));
1663
- if (fragmentChunk) fragmentEntry.fileName = fragmentChunk.fileName;
1732
+ if (fragmentChunk) {
1733
+ fragmentEntry.fileName = fragmentChunk.fileName;
1734
+ const everyChunk = new Map(outputChunks(bundle).map((chunk) => [chunk.fileName, chunk]));
1735
+ const closure = /* @__PURE__ */ new Set();
1736
+ includeChunk(closure, {
1737
+ chunks: everyChunk,
1738
+ everyChunk
1739
+ }, fragmentChunk.fileName);
1740
+ closure.delete(fragmentChunk.fileName);
1741
+ fragmentEntry.imports = [...closure].map((fileName) => joinURL(routePreloads.base, fileName));
1742
+ }
1664
1743
  routePreloads.routes = routeModulePreloadsFromBundle({
1665
1744
  base: routePreloads.base,
1666
1745
  bundle,
@@ -1680,15 +1759,19 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1680
1759
  resumeEntry: joinURL(routePreloads.base, resumeEntry.fileName),
1681
1760
  ...prerenderWakeEntry.fileName ? { prerenderWakeEntry: joinURL(routePreloads.base, prerenderWakeEntry.fileName) } : {},
1682
1761
  navigationEntry: joinURL(routePreloads.base, navigationEntry.fileName),
1683
- ...fragmentEntry.fileName ? { fragmentEntry: joinURL(routePreloads.base, fragmentEntry.fileName) } : {},
1762
+ ...fragmentEntry.fileName ? {
1763
+ fragmentEntry: joinURL(routePreloads.base, fragmentEntry.fileName),
1764
+ fragmentImports: fragmentEntry.imports
1765
+ } : {},
1684
1766
  routes: routePreloads.routes
1685
1767
  });
1686
1768
  }
1687
1769
  }
1688
1770
  },
1689
- async writeBundle(_options, bundle) {
1771
+ async writeBundle(options, bundle) {
1690
1772
  const environment = this.environment;
1691
1773
  const isClientEnvironment = environment?.name ? environment.name === clientEnvironmentName : environment?.config === clientEnvironmentConfig;
1774
+ if (productionBuild && environment?.name === "ssr") recordServerEntry(serverBuild, options.dir, bundle, (id) => id.startsWith(`\0${SERVER_ENTRY_ID}`));
1692
1775
  if (!productionBuild || !isClientEnvironment) return;
1693
1776
  if (!pendingClientAssets) throw new Error("Markless Router client build finished without a client-assets manifest.");
1694
1777
  const renames = bundle ? chunkRenamesSince(capturedChunkNames, bundle) : /* @__PURE__ */ new Map();
@@ -1701,6 +1784,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1701
1784
  navigationEntry,
1702
1785
  fragmentEntry
1703
1786
  ]) if (entry.fileName) entry.fileName = renames.get(entry.fileName) ?? entry.fileName;
1787
+ fragmentEntry.imports = pendingClientAssets.entries.fragmentImports;
1704
1788
  }
1705
1789
  const importMap = bundle?.[MARKLESS_IMPORT_MAP_ASSET];
1706
1790
  if (importMap?.type === "asset") pendingClientAssets = {
@@ -1744,6 +1828,7 @@ function requestFileIdForTransform(root, id) {
1744
1828
  if (!isAbsolute(pathname)) return;
1745
1829
  const relativeFileId = root ? withoutLeadingSlash(normalize(relative(root, pathname))) : withoutLeadingSlash(normalize(pathname));
1746
1830
  if (relativeFileId.startsWith("api/") || relativeFileId.startsWith("middleware/")) return relativeFileId;
1831
+ if (relativeFileId.startsWith("pages/")) return void 0;
1747
1832
  return relativeFileId.match(/(?:^|\/)((?:api|middleware)\/.+\.ts)$/)?.[1];
1748
1833
  }
1749
1834
  function requestFileBuildTransformPlugin(root) {
@@ -1760,7 +1845,7 @@ function requestFileBuildTransformPlugin(root) {
1760
1845
  }
1761
1846
  };
1762
1847
  }
1763
- function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false) {
1848
+ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState()) {
1764
1849
  const scanDirs = Array.isArray(nitroConfig?.scanDirs) ? nitroConfig.scanDirs.filter((dir) => typeof dir === "string") : [];
1765
1850
  const watchOptions = withWatchIgnores(nitroConfig?.watchOptions, serverWatchIgnored);
1766
1851
  const publicAssets = Array.isArray(nitroConfig?.publicAssets) ? nitroConfig.publicAssets : [];
@@ -1780,9 +1865,15 @@ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isB
1780
1865
  ...publicAssets
1781
1866
  ],
1782
1867
  routesDir: nitroConfig?.routesDir ?? ".output/markless/router/nitro-routes",
1868
+ modules: [
1869
+ ...nitroConfig?.modules ?? [],
1870
+ fragmentRequestModule(),
1871
+ ...isBuild ? [prerenderRendererModule(serverBuild)] : []
1872
+ ],
1783
1873
  ...isBuild && { routeRules: {
1784
1874
  [`/${MARKLESS_BUILD_PREFIX}**`]: { headers: { "cache-control": "public, max-age=31536000, immutable" } },
1785
- ...Object.fromEntries(MARKLESS_BUILD_METADATA_FILES.map((file) => [`/${file}`, { headers: { "cache-control": "public, max-age=0, must-revalidate" } }])),
1875
+ [`/_markless/links/**`]: { headers: { "cache-control": "public, max-age=31536000, immutable" } },
1876
+ ...Object.fromEntries(MARKLESS_PUBLIC_BUILD_METADATA_FILES.map((file) => [`/${file}`, { headers: { "cache-control": "public, max-age=0, must-revalidate" } }])),
1786
1877
  ...nitroConfig?.routeRules
1787
1878
  } },
1788
1879
  rolldownConfig: withRequestFileBuildPlugin(nitroConfig?.rolldownConfig, root),
@@ -1935,8 +2026,75 @@ function linkIntentEnabled(options) {
1935
2026
  return options.prefetch !== false;
1936
2027
  }
1937
2028
  function entryPathSource(exportName, entry, entryId, root) {
1938
- const path = entry.fileName ? joinURL(entry.base, entry.fileName) : joinURL(entry.base, "@id", scopedVirtualEntryId(entryId, root));
1939
- return `export const ${exportName} = ${JSON.stringify(path)};`;
2029
+ return `export const ${exportName} = ${JSON.stringify(entryPath(entry, entryId, root))};`;
2030
+ }
2031
+ function entryPath(entry, entryId, root) {
2032
+ return entry.fileName ? joinURL(entry.base, entry.fileName) : joinURL(entry.base, "@id", scopedVirtualEntryId(entryId, root));
2033
+ }
2034
+ function linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options) {
2035
+ let root = "";
2036
+ let base = "/";
2037
+ let clientOutDir = "";
2038
+ let debug = false;
2039
+ let modules = [];
2040
+ return {
2041
+ name: "markless-router:link-bridge-files",
2042
+ apply: "build",
2043
+ sharedDuringBuild: true,
2044
+ configResolved(config) {
2045
+ root = config.root;
2046
+ base = config.base;
2047
+ debug = config.define?.__MARKLESS_DEBUG_ENABLED__ === "true";
2048
+ const client = Object.values(config.environments).find((environment) => environment.consumer === "client");
2049
+ if (client) clientOutDir = resolve(config.root, client.build.outDir);
2050
+ },
2051
+ generateBundle: {
2052
+ order: "post",
2053
+ async handler(_options, bundle) {
2054
+ if (this.environment?.name !== "ssr") return;
2055
+ const chunks = outputChunks(bundle);
2056
+ const target = chunks.find((chunk) => chunk.code?.includes(LINK_BRIDGE_PATHS_PLACEHOLDER));
2057
+ if (!target) return;
2058
+ if (!navigationEntry.fileName) throw new Error("Markless Router built its server entry before its navigation entry.");
2059
+ const documentFile = join(root, "document.tsrx");
2060
+ const documentSource = await readFile(documentFile, "utf8").catch(() => void 0);
2061
+ const built = routerLinkBridgeModules({
2062
+ base,
2063
+ navigationEntryPath: entryPath(navigationEntry, NAVIGATION_ENTRY_ID, root),
2064
+ fragmentEntryPath: entryPath(fragmentEntry, FRAGMENT_ENTRY_ID, root),
2065
+ fragmentEntryImports: fragmentEntry.imports,
2066
+ fragmentNavigationSource: minifiedFragmentNavigationSource(),
2067
+ fragmentNavigationRunSource: minifiedFragmentNavigationSource(runFragmentNavigation),
2068
+ prefetch: options.prefetch,
2069
+ documentNavigation: documentNavigationFor(documentSource, documentFile),
2070
+ routeFileIds: bundledPageFileIds(chunks, root),
2071
+ debug
2072
+ });
2073
+ modules = built;
2074
+ target.code = target.code.replace(LINK_BRIDGE_PATHS_PLACEHOLDER, jsStringLiteralContent(JSON.stringify(built.slice(0, 2).map((module) => module.path))));
2075
+ }
2076
+ },
2077
+ async writeBundle() {
2078
+ if (this.environment?.name !== "ssr" || modules.length === 0) return;
2079
+ const prefix = base.replace(/\/?$/, "/");
2080
+ for (const module of modules) {
2081
+ const file = join(clientOutDir, module.path.slice(prefix.length));
2082
+ await mkdir(dirname(file), { recursive: true });
2083
+ await writeFile(file, module.source, "utf8");
2084
+ }
2085
+ }
2086
+ };
2087
+ }
2088
+ function bundledPageFileIds(chunks, root) {
2089
+ const files = /* @__PURE__ */ new Set();
2090
+ for (const chunk of chunks) for (const id of chunk.moduleIds ?? []) {
2091
+ if (id.startsWith("\0")) continue;
2092
+ const pathname = decodePath(parseURL(id).pathname);
2093
+ if (!isAbsolute(pathname)) continue;
2094
+ const file = normalizeRouteFileId(relative(root, pathname));
2095
+ if (isPageModuleFile(file)) files.add(file);
2096
+ }
2097
+ return [...files];
1940
2098
  }
1941
2099
  function serverEntrySource(root, options) {
1942
2100
  const query = rootScopeQuery(root);
@@ -1950,14 +2108,17 @@ function serverEntrySource(root, options) {
1950
2108
  `import { pageModuleLoaders, routeFileIds } from '${ROUTE_DISCOVERY_ID}${query}';`,
1951
2109
  `import { documentNavigation } from '${ROUTER_OPTIONS_ID}${query}';`,
1952
2110
  `const documentModuleLoaders = import.meta.glob(['/document.tsrx']);`,
2111
+ `const linkBridgePathsJson = globalThis.__marklessRouterLinkBridgePathsJson ?? ${JSON.stringify(LINK_BRIDGE_PATHS_PLACEHOLDER)};`,
1953
2112
  `const entry = createServerEntry({`,
1954
2113
  ` dev: import.meta.env.DEV,`,
2114
+ ` linkBridgePaths: linkBridgePathsJson === ${JSON.stringify(LINK_BRIDGE_PATHS_PLACEHOLDER)} ? undefined : JSON.parse(linkBridgePathsJson),`,
1955
2115
  ` base: import.meta.env.BASE_URL,`,
1956
2116
  ` resumeEntryPath,`,
1957
2117
  ` prerenderWakeEntryPath,`,
1958
2118
  ` navigationEntryPath,`,
1959
2119
  ` fragmentEntryPath,`,
1960
2120
  ` fragmentNavigationSource: ${JSON.stringify(minifiedFragmentNavigationSource())},`,
2121
+ ` fragmentNavigationRunSource: ${JSON.stringify(minifiedFragmentNavigationSource(runFragmentNavigation))},`,
1961
2122
  ...options.prefetch === false ? [" prefetch: false,"] : [],
1962
2123
  ` routeModulePreloads,`,
1963
2124
  ` routeSsrModulePreloads,`,
@@ -1973,8 +2134,8 @@ function serverEntrySource(root, options) {
1973
2134
  `export default entry;`
1974
2135
  ].join("\n");
1975
2136
  }
1976
- function minifiedFragmentNavigationSource() {
1977
- const result = minifySync("markless-fragment-navigation.js", `export default (${startFragmentNavigation.toString()});`, {
2137
+ function minifiedFragmentNavigationSource(source = startFragmentNavigation) {
2138
+ const result = minifySync("markless-fragment-navigation.js", `export default (${source.toString()});`, {
1978
2139
  compress: true,
1979
2140
  mangle: true,
1980
2141
  module: true
@@ -2086,6 +2247,10 @@ function routeModulePreloadsFromBundle(input) {
2086
2247
  routeChunkFileNames.add(chunk.fileName);
2087
2248
  }
2088
2249
  const symbolChunks = symbolChunksBySourceFile(chunks);
2250
+ const resolverSourceFiles = new Set(allChunks.flatMap((chunk) => (chunk.moduleIds ?? []).flatMap((moduleId) => {
2251
+ const sourceFile = resolverVirtualModuleSourceFile(moduleId.startsWith("\0") ? moduleId.slice(1) : moduleId);
2252
+ return sourceFile ? [sourceModulePathname(sourceFile)] : [];
2253
+ })));
2089
2254
  const polyfillFileNames = new Set(chunks.filter((chunk) => chunk.facadeModuleId && input.navigationPolyfillIds?.has(chunk.facadeModuleId)).map((chunk) => chunk.fileName));
2090
2255
  const navigation = {};
2091
2256
  const ssr = {};
@@ -2135,8 +2300,10 @@ function routeModulePreloadsFromBundle(input) {
2135
2300
  }
2136
2301
  includeChunk(ssrFileNames, landingScope, landingChunk.fileName, true);
2137
2302
  const routeSourceFiles = sourceFilesForChunks(/* @__PURE__ */ new Set([...navigationFileNames, ...ssrFileNames]), chunksByFileName);
2303
+ let routeHasSymbols = false;
2138
2304
  for (const [sourceFile, symbolFileNames] of symbolChunks) {
2139
2305
  if (!routeSourceFiles.has(sourceFile)) continue;
2306
+ routeHasSymbols = true;
2140
2307
  for (const fileName of symbolFileNames) {
2141
2308
  includeChunk(navigationFileNames, scope, fileName, true);
2142
2309
  includeChunk(ssrFileNames, landingScope, fileName, true);
@@ -2145,6 +2312,7 @@ function routeModulePreloadsFromBundle(input) {
2145
2312
  const styleFileNames = /* @__PURE__ */ new Set([...navigationFileNames, ...ssrFileNames]);
2146
2313
  const planned = plannedLanding?.get(routeFile);
2147
2314
  if (planned) narrowToPlanned(ssrFileNames, planned, landingScope, [input.resumeChunk, input.prerenderWakeChunk]);
2315
+ for (const fileName of ssrFileNames) for (const specifier of warmEvaluatedImports(everyChunk.get(fileName)?.code ?? "")) includeChunk(ssrFileNames, landingScope, normalize(join(dirname(fileName), specifier)));
2148
2316
  navigation[routeFile] = [...navigationFileNames].map((fileName) => joinURL(input.base, fileName));
2149
2317
  const firstEventPath = /* @__PURE__ */ new Set();
2150
2318
  const lazySiblings = [...ssrFileNames].filter((fileName) => {
@@ -2157,7 +2325,7 @@ function routeModulePreloadsFromBundle(input) {
2157
2325
  for (const fileName of routeScopedDynamicImports(entry, routeFile)) includeChunk(firstEventPath, landingScope, fileName);
2158
2326
  }
2159
2327
  for (const fileName of lazySiblings) includeChunk(firstEventPath, landingScope, fileName);
2160
- ssr[routeFile] = [...[...firstEventPath].filter((fileName) => ssrFileNames.has(fileName)), ...[...ssrFileNames].filter((fileName) => !firstEventPath.has(fileName))].map((fileName) => joinURL(input.base, fileName));
2328
+ 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));
2161
2329
  styles[routeFile] = routeStylesheetsForChunks(routeFileChunks, styleFileNames, chunksByFileName, input.base);
2162
2330
  }
2163
2331
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markless/router",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Markless router: client-side navigation with SSR streaming and build-time preload maps.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -64,11 +64,11 @@
64
64
  "pathe": "^2.0.3",
65
65
  "satteri": "0.6.3",
66
66
  "ufo": "^1.6.4",
67
- "@markless/bundler": "0.5.0",
68
- "@markless/web": "0.5.0"
67
+ "@markless/bundler": "0.6.0",
68
+ "@markless/web": "0.6.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@markless/analyzer": "0.5.0"
71
+ "@markless/analyzer": "0.6.0"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "typescript": "^5.9.3",