@markless/router 0.5.0 → 0.7.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,64 @@
1
1
  import { i as protocolStateVersion, n as protocolInstanceQualifies, t as protocolInstancePath } from "../../protocol-constants-B4WxrZAS.js";
2
+ //#endregion
3
+ //#region ../serializer/src/protocol-record-keys.ts
4
+ /** The field names each templated record list may spell, in the order its templates index them. */
5
+ const PROTOCOL_RECORD_KEYS = {
6
+ "markless/state": {
7
+ computed: "graphNodeId name async deriveSymbolId value version root $type $ref source flags records id type fields items entries values bytes arrayType buffer byteOffset length byteLength directValue dependencies path snapshot status key error",
8
+ sharedSeeds: "graphNodeId deriveSymbolId dependencies path reads",
9
+ sharedDefinitions: "id name exportedName scope version graphNodeIds projectionIds dependencies definitionId definitionName returnProperties kind graphNodeId path"
10
+ },
11
+ "markless/view": {
12
+ domUpdates: "hostNodeId source graphNodeId path target kind prefix suffix trueValue falseValue textNode name constantClass leadingClass symbolId",
13
+ behaviors: "hostNodeId source functionSource inputSources inputValues inputGraphReads inputIndex graphNodeId path symbolId usesGraph",
14
+ elementHandles: "hostNodeId handleId name plural"
15
+ }
16
+ };
17
+ //#endregion
18
+ //#region ../serializer/src/protocol-templated-lists.ts
19
+ /**
20
+ * The record lists a payload script may serve as record templates instead of a plain array. The
21
+ * inline resumer reads the others at page load, before any decoder has loaded, so they stay plain.
22
+ */
23
+ const PROTOCOL_TEMPLATED_LISTS = {
24
+ "markless/state": [
25
+ "computed",
26
+ "sharedSeeds",
27
+ "sharedDefinitions"
28
+ ],
29
+ "markless/view": [
30
+ "domUpdates",
31
+ "behaviors",
32
+ "elementHandles"
33
+ ]
34
+ };
35
+ //#endregion
36
+ //#region ../serializer/src/protocol-record-templates.ts
37
+ function expandRecordTemplates(list, keys = "") {
38
+ if (Array.isArray(list)) return list;
39
+ const { t, r } = list;
40
+ const names = keys.split(" ");
41
+ const parts = t.map((text) => text.replace(/~(\d+):/g, (_, at) => `"${names[+at]}":`).split(/\^(\d+)/));
42
+ return r.map((ref) => {
43
+ if (typeof ref === "number") return JSON.parse(parts[ref][0]);
44
+ if (!Array.isArray(ref)) return ref;
45
+ const part = parts[ref[0]];
46
+ let text = part[0];
47
+ for (let at = 1; at < part.length; at += 2) text += ref[+part[at] + 1] + part[at + 1];
48
+ return JSON.parse(text);
49
+ });
50
+ }
51
+ /** Expands, in place, every templated list of a freshly parsed payload script. */
52
+ function decodeProtocolPayloadLists(type, payload) {
53
+ const record = payload;
54
+ const keys = PROTOCOL_RECORD_KEYS[type];
55
+ if (record) for (const key of PROTOCOL_TEMPLATED_LISTS[type]) {
56
+ const list = record[key];
57
+ if (list && !Array.isArray(list)) record[key] = expandRecordTemplates(list, keys[key]);
58
+ }
59
+ return payload;
60
+ }
61
+ //#endregion
2
62
  //#region ../serializer/src/storage-cookie.ts
3
63
  const STORAGE_RENDER_CONTEXT_READER = "storageValue";
4
64
  //#endregion
@@ -582,9 +642,11 @@ const marklessDecodeScalarCell = (c, g, s) => {
582
642
  const marklessFindElementAtDomOrderIndex = (r, i) => {
583
643
  let c = r.__marklessCensus;
584
644
  if (!c) {
585
- const w = document.createTreeWalker(r, 1);
645
+ const w = document.createTreeWalker(r, 129);
586
646
  c = r.__marklessCensus = r.nodeType === 1 ? [r] : [];
587
- for (let n; n = w.nextNode();) c.push(n);
647
+ for (let n; n = w.nextNode();) if (n.nodeType !== 8) c.push(n);
648
+ else if (n.data === "markless:page-start") c.length = 1;
649
+ else if (n.data === "markless:page-end") break;
588
650
  }
589
651
  return c[i];
590
652
  };
@@ -744,7 +806,7 @@ function locate(root, view, host) {
744
806
  }
745
807
  function readScript(root, type) {
746
808
  const text = root.querySelector(`script[type="${type}"]`)?.textContent;
747
- return text ? JSON.parse(text) : void 0;
809
+ return text ? decodeProtocolPayloadLists(type, JSON.parse(text)) : void 0;
748
810
  }
749
811
  function references(value, id) {
750
812
  return value === id || !!value && typeof value === "object" && Object.values(value).some((child) => references(child, id));
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 { d as REPLACE_ATTRIBUTE, f as SCROLL_ATTRIBUTE, l as LINK_ATTRIBUTE, u as PREFETCH_ATTRIBUTE } from "./request-locals-BDAhNpEa.js";
2
+ import { i as matchRouteManifest, 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-yboa-2m6.js";
7
6
  import { dirname, extname, isAbsolute, join, normalize, relative, resolve } from "pathe";
8
- import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withQuery, withoutLeadingSlash } from "ufo";
9
- import { existsSync } from "node:fs";
7
+ import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withQuery, withoutBase, withoutLeadingSlash } from "ufo";
8
+ import { existsSync, readdirSync } 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,8 +1572,9 @@ 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(),
1577
+ devPageForUrlPlugin(options),
1512
1578
  pageServerOnlyPropsPlugin(),
1513
1579
  mdxTransformPlugin(),
1514
1580
  requestFileTransformPlugin(),
@@ -1516,6 +1582,7 @@ function router(options = {}) {
1516
1582
  anchorTransformPlugin(),
1517
1583
  htmlTransformPlugin(),
1518
1584
  virtualModulesPlugin(resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, options, fragmentEntry),
1585
+ linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options),
1519
1586
  nitroPlugins
1520
1587
  ];
1521
1588
  }
@@ -1523,10 +1590,43 @@ function pageServerOnlyPropsPlugin() {
1523
1590
  return {
1524
1591
  name: "markless-router:page-server-only-props",
1525
1592
  configResolved(config) {
1526
- config.plugins.find((plugin) => typeof plugin.api?.declareServerOnlyProps === "function")?.api.declareServerOnlyProps((moduleId) => isPageModuleFile(normalizeRouteFileId(moduleId)) ? PAGE_SERVER_ONLY_PROPS : void 0);
1593
+ const markless = config.plugins.find((plugin) => typeof plugin.api?.declareServerOnlyProps === "function");
1594
+ markless?.api.declareServerOnlyProps((moduleId) => isPageModuleFile(normalizeRouteFileId(moduleId)) ? PAGE_SERVER_ONLY_PROPS : void 0);
1595
+ markless?.api.declareRouteModules?.((moduleId) => isPageModuleFile(normalizeRouteFileId(moduleId)));
1596
+ }
1597
+ };
1598
+ }
1599
+ function devPageForUrlPlugin(options) {
1600
+ return {
1601
+ name: "markless-router:dev-page-for-url",
1602
+ apply: "serve",
1603
+ configResolved(config) {
1604
+ config.plugins.find((plugin) => typeof plugin.api?.declarePageForUrl === "function")?.api.declarePageForUrl((href) => devPageForUrl(config.root, config.base, options.mode, href));
1527
1605
  }
1528
1606
  };
1529
1607
  }
1608
+ function devPageForUrl(root, base, mode, href) {
1609
+ let url;
1610
+ try {
1611
+ url = new URL(href);
1612
+ } catch {
1613
+ return;
1614
+ }
1615
+ const pagesDir = join(root, "pages");
1616
+ if (!existsSync(pagesDir)) return void 0;
1617
+ const fileIds = readdirSync(pagesDir, {
1618
+ recursive: true,
1619
+ encoding: "utf8"
1620
+ }).map((file) => join("pages", file));
1621
+ let manifest;
1622
+ try {
1623
+ manifest = buildRouteManifestFromFileIds(fileIds);
1624
+ } catch {
1625
+ return;
1626
+ }
1627
+ const pathname = mode === "hash" && url.hash.startsWith("#/") ? url.hash.slice(1) : withoutBase(url.pathname, base);
1628
+ return matchRouteManifest(pathname, manifest)?.route.file ?? manifest.statusPages.notFound;
1629
+ }
1530
1630
  function devSourceModuleRequestPlugin() {
1531
1631
  let root = "";
1532
1632
  return {
@@ -1547,7 +1647,7 @@ function devSourceModuleRequestPlugin() {
1547
1647
  }
1548
1648
  };
1549
1649
  }
1550
- function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState()) {
1650
+ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState(), serverBuild = serverBuildState()) {
1551
1651
  let clientOutDir = "";
1552
1652
  let clientEnvironmentName = "";
1553
1653
  let clientEnvironmentConfig;
@@ -1581,7 +1681,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1581
1681
  ssrEnvironment.build.rolldownOptions.input ??= scopedVirtualEntryId(SERVER_ENTRY_ID, config.root);
1582
1682
  const serverWatch = withWatchIgnores(config.server?.watch);
1583
1683
  return {
1584
- nitro: createNitroConfig(config.nitro, config.root, serverWatch.ignored, env?.command === "build"),
1684
+ nitro: createNitroConfig(config.nitro, config.root, serverWatch.ignored, env?.command === "build", serverBuild),
1585
1685
  server: {
1586
1686
  ...config.server,
1587
1687
  watch: serverWatch
@@ -1642,6 +1742,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1642
1742
  prerenderWakeEntry.fileName = manifest.entries.prerenderWake ? clientAssetFileName(manifest.entries.prerenderWake, manifest.base) : void 0;
1643
1743
  navigationEntry.fileName = clientAssetFileName(manifest.entries.navigation, manifest.base);
1644
1744
  fragmentEntry.fileName = manifest.entries.fragment ? clientAssetFileName(manifest.entries.fragment, manifest.base) : void 0;
1745
+ fragmentEntry.imports = manifest.entries.fragmentImports;
1645
1746
  routePreloads.routes = manifest.routes;
1646
1747
  routePreloads.persisted = true;
1647
1748
  routePreloads.importMap = manifest.importMap;
@@ -1660,7 +1761,17 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1660
1761
  const navigationChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, NAVIGATION_ENTRY_ORIGIN));
1661
1762
  if (navigationChunk) navigationEntry.fileName = navigationChunk.fileName;
1662
1763
  const fragmentChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, FRAGMENT_ENTRY_ORIGIN));
1663
- if (fragmentChunk) fragmentEntry.fileName = fragmentChunk.fileName;
1764
+ if (fragmentChunk) {
1765
+ fragmentEntry.fileName = fragmentChunk.fileName;
1766
+ const everyChunk = new Map(outputChunks(bundle).map((chunk) => [chunk.fileName, chunk]));
1767
+ const closure = /* @__PURE__ */ new Set();
1768
+ includeChunk(closure, {
1769
+ chunks: everyChunk,
1770
+ everyChunk
1771
+ }, fragmentChunk.fileName);
1772
+ closure.delete(fragmentChunk.fileName);
1773
+ fragmentEntry.imports = [...closure].map((fileName) => joinURL(routePreloads.base, fileName));
1774
+ }
1664
1775
  routePreloads.routes = routeModulePreloadsFromBundle({
1665
1776
  base: routePreloads.base,
1666
1777
  bundle,
@@ -1680,15 +1791,19 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1680
1791
  resumeEntry: joinURL(routePreloads.base, resumeEntry.fileName),
1681
1792
  ...prerenderWakeEntry.fileName ? { prerenderWakeEntry: joinURL(routePreloads.base, prerenderWakeEntry.fileName) } : {},
1682
1793
  navigationEntry: joinURL(routePreloads.base, navigationEntry.fileName),
1683
- ...fragmentEntry.fileName ? { fragmentEntry: joinURL(routePreloads.base, fragmentEntry.fileName) } : {},
1794
+ ...fragmentEntry.fileName ? {
1795
+ fragmentEntry: joinURL(routePreloads.base, fragmentEntry.fileName),
1796
+ fragmentImports: fragmentEntry.imports
1797
+ } : {},
1684
1798
  routes: routePreloads.routes
1685
1799
  });
1686
1800
  }
1687
1801
  }
1688
1802
  },
1689
- async writeBundle(_options, bundle) {
1803
+ async writeBundle(options, bundle) {
1690
1804
  const environment = this.environment;
1691
1805
  const isClientEnvironment = environment?.name ? environment.name === clientEnvironmentName : environment?.config === clientEnvironmentConfig;
1806
+ if (productionBuild && environment?.name === "ssr") recordServerEntry(serverBuild, options.dir, bundle, (id) => id.startsWith(`\0${SERVER_ENTRY_ID}`));
1692
1807
  if (!productionBuild || !isClientEnvironment) return;
1693
1808
  if (!pendingClientAssets) throw new Error("Markless Router client build finished without a client-assets manifest.");
1694
1809
  const renames = bundle ? chunkRenamesSince(capturedChunkNames, bundle) : /* @__PURE__ */ new Map();
@@ -1701,6 +1816,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1701
1816
  navigationEntry,
1702
1817
  fragmentEntry
1703
1818
  ]) if (entry.fileName) entry.fileName = renames.get(entry.fileName) ?? entry.fileName;
1819
+ fragmentEntry.imports = pendingClientAssets.entries.fragmentImports;
1704
1820
  }
1705
1821
  const importMap = bundle?.[MARKLESS_IMPORT_MAP_ASSET];
1706
1822
  if (importMap?.type === "asset") pendingClientAssets = {
@@ -1744,6 +1860,7 @@ function requestFileIdForTransform(root, id) {
1744
1860
  if (!isAbsolute(pathname)) return;
1745
1861
  const relativeFileId = root ? withoutLeadingSlash(normalize(relative(root, pathname))) : withoutLeadingSlash(normalize(pathname));
1746
1862
  if (relativeFileId.startsWith("api/") || relativeFileId.startsWith("middleware/")) return relativeFileId;
1863
+ if (relativeFileId.startsWith("pages/")) return void 0;
1747
1864
  return relativeFileId.match(/(?:^|\/)((?:api|middleware)\/.+\.ts)$/)?.[1];
1748
1865
  }
1749
1866
  function requestFileBuildTransformPlugin(root) {
@@ -1760,7 +1877,7 @@ function requestFileBuildTransformPlugin(root) {
1760
1877
  }
1761
1878
  };
1762
1879
  }
1763
- function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false) {
1880
+ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState()) {
1764
1881
  const scanDirs = Array.isArray(nitroConfig?.scanDirs) ? nitroConfig.scanDirs.filter((dir) => typeof dir === "string") : [];
1765
1882
  const watchOptions = withWatchIgnores(nitroConfig?.watchOptions, serverWatchIgnored);
1766
1883
  const publicAssets = Array.isArray(nitroConfig?.publicAssets) ? nitroConfig.publicAssets : [];
@@ -1780,9 +1897,15 @@ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isB
1780
1897
  ...publicAssets
1781
1898
  ],
1782
1899
  routesDir: nitroConfig?.routesDir ?? ".output/markless/router/nitro-routes",
1900
+ modules: [
1901
+ ...nitroConfig?.modules ?? [],
1902
+ fragmentRequestModule(),
1903
+ ...isBuild ? [prerenderRendererModule(serverBuild)] : []
1904
+ ],
1783
1905
  ...isBuild && { routeRules: {
1784
1906
  [`/${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" } }])),
1907
+ [`/_markless/links/**`]: { headers: { "cache-control": "public, max-age=31536000, immutable" } },
1908
+ ...Object.fromEntries(MARKLESS_PUBLIC_BUILD_METADATA_FILES.map((file) => [`/${file}`, { headers: { "cache-control": "public, max-age=0, must-revalidate" } }])),
1786
1909
  ...nitroConfig?.routeRules
1787
1910
  } },
1788
1911
  rolldownConfig: withRequestFileBuildPlugin(nitroConfig?.rolldownConfig, root),
@@ -1935,8 +2058,75 @@ function linkIntentEnabled(options) {
1935
2058
  return options.prefetch !== false;
1936
2059
  }
1937
2060
  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)};`;
2061
+ return `export const ${exportName} = ${JSON.stringify(entryPath(entry, entryId, root))};`;
2062
+ }
2063
+ function entryPath(entry, entryId, root) {
2064
+ return entry.fileName ? joinURL(entry.base, entry.fileName) : joinURL(entry.base, "@id", scopedVirtualEntryId(entryId, root));
2065
+ }
2066
+ function linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options) {
2067
+ let root = "";
2068
+ let base = "/";
2069
+ let clientOutDir = "";
2070
+ let debug = false;
2071
+ let modules = [];
2072
+ return {
2073
+ name: "markless-router:link-bridge-files",
2074
+ apply: "build",
2075
+ sharedDuringBuild: true,
2076
+ configResolved(config) {
2077
+ root = config.root;
2078
+ base = config.base;
2079
+ debug = config.define?.__MARKLESS_DEBUG_ENABLED__ === "true";
2080
+ const client = Object.values(config.environments).find((environment) => environment.consumer === "client");
2081
+ if (client) clientOutDir = resolve(config.root, client.build.outDir);
2082
+ },
2083
+ generateBundle: {
2084
+ order: "post",
2085
+ async handler(_options, bundle) {
2086
+ if (this.environment?.name !== "ssr") return;
2087
+ const chunks = outputChunks(bundle);
2088
+ const target = chunks.find((chunk) => chunk.code?.includes(LINK_BRIDGE_PATHS_PLACEHOLDER));
2089
+ if (!target) return;
2090
+ if (!navigationEntry.fileName) throw new Error("Markless Router built its server entry before its navigation entry.");
2091
+ const documentFile = join(root, "document.tsrx");
2092
+ const documentSource = await readFile(documentFile, "utf8").catch(() => void 0);
2093
+ const built = routerLinkBridgeModules({
2094
+ base,
2095
+ navigationEntryPath: entryPath(navigationEntry, NAVIGATION_ENTRY_ID, root),
2096
+ fragmentEntryPath: entryPath(fragmentEntry, FRAGMENT_ENTRY_ID, root),
2097
+ fragmentEntryImports: fragmentEntry.imports,
2098
+ fragmentNavigationSource: minifiedFragmentNavigationSource(),
2099
+ fragmentNavigationRunSource: minifiedFragmentNavigationSource(runFragmentNavigation),
2100
+ prefetch: options.prefetch,
2101
+ documentNavigation: documentNavigationFor(documentSource, documentFile),
2102
+ routeFileIds: bundledPageFileIds(chunks, root),
2103
+ debug
2104
+ });
2105
+ modules = built;
2106
+ target.code = target.code.replace(LINK_BRIDGE_PATHS_PLACEHOLDER, jsStringLiteralContent(JSON.stringify(built.slice(0, 2).map((module) => module.path))));
2107
+ }
2108
+ },
2109
+ async writeBundle() {
2110
+ if (this.environment?.name !== "ssr" || modules.length === 0) return;
2111
+ const prefix = base.replace(/\/?$/, "/");
2112
+ for (const module of modules) {
2113
+ const file = join(clientOutDir, module.path.slice(prefix.length));
2114
+ await mkdir(dirname(file), { recursive: true });
2115
+ await writeFile(file, module.source, "utf8");
2116
+ }
2117
+ }
2118
+ };
2119
+ }
2120
+ function bundledPageFileIds(chunks, root) {
2121
+ const files = /* @__PURE__ */ new Set();
2122
+ for (const chunk of chunks) for (const id of chunk.moduleIds ?? []) {
2123
+ if (id.startsWith("\0")) continue;
2124
+ const pathname = decodePath(parseURL(id).pathname);
2125
+ if (!isAbsolute(pathname)) continue;
2126
+ const file = normalizeRouteFileId(relative(root, pathname));
2127
+ if (isPageModuleFile(file)) files.add(file);
2128
+ }
2129
+ return [...files];
1940
2130
  }
1941
2131
  function serverEntrySource(root, options) {
1942
2132
  const query = rootScopeQuery(root);
@@ -1950,14 +2140,17 @@ function serverEntrySource(root, options) {
1950
2140
  `import { pageModuleLoaders, routeFileIds } from '${ROUTE_DISCOVERY_ID}${query}';`,
1951
2141
  `import { documentNavigation } from '${ROUTER_OPTIONS_ID}${query}';`,
1952
2142
  `const documentModuleLoaders = import.meta.glob(['/document.tsrx']);`,
2143
+ `const linkBridgePathsJson = globalThis.__marklessRouterLinkBridgePathsJson ?? ${JSON.stringify(LINK_BRIDGE_PATHS_PLACEHOLDER)};`,
1953
2144
  `const entry = createServerEntry({`,
1954
2145
  ` dev: import.meta.env.DEV,`,
2146
+ ` linkBridgePaths: linkBridgePathsJson === ${JSON.stringify(LINK_BRIDGE_PATHS_PLACEHOLDER)} ? undefined : JSON.parse(linkBridgePathsJson),`,
1955
2147
  ` base: import.meta.env.BASE_URL,`,
1956
2148
  ` resumeEntryPath,`,
1957
2149
  ` prerenderWakeEntryPath,`,
1958
2150
  ` navigationEntryPath,`,
1959
2151
  ` fragmentEntryPath,`,
1960
2152
  ` fragmentNavigationSource: ${JSON.stringify(minifiedFragmentNavigationSource())},`,
2153
+ ` fragmentNavigationRunSource: ${JSON.stringify(minifiedFragmentNavigationSource(runFragmentNavigation))},`,
1961
2154
  ...options.prefetch === false ? [" prefetch: false,"] : [],
1962
2155
  ` routeModulePreloads,`,
1963
2156
  ` routeSsrModulePreloads,`,
@@ -1973,8 +2166,8 @@ function serverEntrySource(root, options) {
1973
2166
  `export default entry;`
1974
2167
  ].join("\n");
1975
2168
  }
1976
- function minifiedFragmentNavigationSource() {
1977
- const result = minifySync("markless-fragment-navigation.js", `export default (${startFragmentNavigation.toString()});`, {
2169
+ function minifiedFragmentNavigationSource(source = startFragmentNavigation) {
2170
+ const result = minifySync("markless-fragment-navigation.js", `export default (${source.toString()});`, {
1978
2171
  compress: true,
1979
2172
  mangle: true,
1980
2173
  module: true
@@ -2086,6 +2279,10 @@ function routeModulePreloadsFromBundle(input) {
2086
2279
  routeChunkFileNames.add(chunk.fileName);
2087
2280
  }
2088
2281
  const symbolChunks = symbolChunksBySourceFile(chunks);
2282
+ const resolverSourceFiles = new Set(allChunks.flatMap((chunk) => (chunk.moduleIds ?? []).flatMap((moduleId) => {
2283
+ const sourceFile = resolverVirtualModuleSourceFile(moduleId.startsWith("\0") ? moduleId.slice(1) : moduleId);
2284
+ return sourceFile ? [sourceModulePathname(sourceFile)] : [];
2285
+ })));
2089
2286
  const polyfillFileNames = new Set(chunks.filter((chunk) => chunk.facadeModuleId && input.navigationPolyfillIds?.has(chunk.facadeModuleId)).map((chunk) => chunk.fileName));
2090
2287
  const navigation = {};
2091
2288
  const ssr = {};
@@ -2135,8 +2332,10 @@ function routeModulePreloadsFromBundle(input) {
2135
2332
  }
2136
2333
  includeChunk(ssrFileNames, landingScope, landingChunk.fileName, true);
2137
2334
  const routeSourceFiles = sourceFilesForChunks(/* @__PURE__ */ new Set([...navigationFileNames, ...ssrFileNames]), chunksByFileName);
2335
+ let routeHasSymbols = false;
2138
2336
  for (const [sourceFile, symbolFileNames] of symbolChunks) {
2139
2337
  if (!routeSourceFiles.has(sourceFile)) continue;
2338
+ routeHasSymbols = true;
2140
2339
  for (const fileName of symbolFileNames) {
2141
2340
  includeChunk(navigationFileNames, scope, fileName, true);
2142
2341
  includeChunk(ssrFileNames, landingScope, fileName, true);
@@ -2145,6 +2344,7 @@ function routeModulePreloadsFromBundle(input) {
2145
2344
  const styleFileNames = /* @__PURE__ */ new Set([...navigationFileNames, ...ssrFileNames]);
2146
2345
  const planned = plannedLanding?.get(routeFile);
2147
2346
  if (planned) narrowToPlanned(ssrFileNames, planned, landingScope, [input.resumeChunk, input.prerenderWakeChunk]);
2347
+ for (const fileName of ssrFileNames) for (const specifier of warmEvaluatedImports(everyChunk.get(fileName)?.code ?? "")) includeChunk(ssrFileNames, landingScope, normalize(join(dirname(fileName), specifier)));
2148
2348
  navigation[routeFile] = [...navigationFileNames].map((fileName) => joinURL(input.base, fileName));
2149
2349
  const firstEventPath = /* @__PURE__ */ new Set();
2150
2350
  const lazySiblings = [...ssrFileNames].filter((fileName) => {
@@ -2157,7 +2357,7 @@ function routeModulePreloadsFromBundle(input) {
2157
2357
  for (const fileName of routeScopedDynamicImports(entry, routeFile)) includeChunk(firstEventPath, landingScope, fileName);
2158
2358
  }
2159
2359
  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));
2360
+ 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
2361
  styles[routeFile] = routeStylesheetsForChunks(routeFileChunks, styleFileNames, chunksByFileName, input.base);
2162
2362
  }
2163
2363
  return {