@markless/router 0.2.2 → 0.3.1

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/README.md CHANGED
@@ -4,4 +4,4 @@ Markless router: client-side navigation with SSR streaming and build-time
4
4
  preload maps. Used through `@markless/core/router` and the
5
5
  `@markless/router/vite` plugin.
6
6
 
7
- See the repository for documentation: https://github.com/markless-dev/markless
7
+ See the repository for documentation: https://github.com/compiled-run/markless
@@ -28,6 +28,7 @@ function installDebugChannelLayer(root, initialPhase, initiallyActive = true) {
28
28
  const rootKey = priorContainer?.root.key ?? `root:${seed}`;
29
29
  const rootRef = new WeakRef(root);
30
30
  const events = /* @__PURE__ */ new WeakMap();
31
+ const delegatedEvents = /* @__PURE__ */ new Set();
31
32
  const keys = /* @__PURE__ */ new WeakMap();
32
33
  const invalidated = /* @__PURE__ */ new WeakSet();
33
34
  const routerSources = global[Symbol.for("markless.debug.channel.v1.router-sources")] ??= /* @__PURE__ */ new Set();
@@ -99,6 +100,13 @@ function installDebugChannelLayer(root, initialPhase, initiallyActive = true) {
99
100
  }
100
101
  const interaction = events.get(element)?.get(eventName);
101
102
  if (interaction) return freeze({ ...interaction });
103
+ if (delegatedEvents.has(eventName)) return freeze({
104
+ kind: "inline-resumer",
105
+ source: "ssr-inline",
106
+ containerId: id,
107
+ elementKey: keyFor(element),
108
+ eventName
109
+ });
102
110
  if (invalidated.has(element)) return none(element, eventName, "not-registered");
103
111
  try {
104
112
  const prior = previous?.explainInteraction(element, eventName);
@@ -209,6 +217,10 @@ function installDebugChannelLayer(root, initialPhase, initiallyActive = true) {
209
217
  eventName
210
218
  }));
211
219
  },
220
+ delegated(eventNames) {
221
+ if (lifecycle === "disposed") return;
222
+ for (const eventName of eventNames) delegatedEvents.add(eventName);
223
+ },
212
224
  violation: pushViolation,
213
225
  router: (source) => void routerSources.add(source),
214
226
  boundaries(value) {
@@ -236,7 +248,7 @@ function safely(run) {
236
248
  }
237
249
  }
238
250
  function __marklessDebugBootstrapSource() {
239
- return `((root,phase,active)=>{const controls=(${installDebugChannelLayer.toString()})(root,phase,active);return Object.freeze({record:(element,eventName,input)=>controls.record(element,eventName,input),router:(source)=>controls.router(source),activate:()=>controls.activate()})})`;
251
+ return `((root,phase,active)=>{const controls=(${installDebugChannelLayer.toString()})(root,phase,active);return Object.freeze({record:(element,eventName,input)=>controls.record(element,eventName,input),delegated:(eventNames)=>controls.delegated(eventNames),router:(source)=>controls.router(source),activate:()=>controls.activate()})})`;
240
252
  }
241
253
  function __marklessDebugRegisterRouter(root, source) {
242
254
  if (root) safely(() => (moduleRoots.get(root) ?? controlsFor(root, "ssr-resume")).router(source));
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { createExternalDelegateStructureTokens, createExternalDelegateView } from "@markless/web/fns/external-delegate";
2
+
1
3
  //#region src/route-manifest.d.ts
2
4
  interface RouteManifestRoute {
3
5
  readonly pathname: string;
@@ -186,15 +188,25 @@ declare const Html: ((props: {
186
188
  readonly children?: unknown;
187
189
  }): {
188
190
  html: string;
191
+ elementCount: number;
189
192
  };
190
193
  };
191
- declare const Link: ((props?: LinkProps) => unknown) & {
194
+ type LinkComponent = {
195
+ (props?: LinkProps): unknown;
192
196
  renderCsr(props?: LinkProps): {
193
197
  root: HTMLAnchorElement;
198
+ view: ReturnType<typeof createExternalDelegateView>;
199
+ liveHostNodes: Map<string, HTMLAnchorElement>;
194
200
  };
195
- renderSsr(props?: LinkProps): {
201
+ renderSsr(props?: LinkProps, renderContext?: {
202
+ readonly idPrefix?: string;
203
+ }): {
196
204
  html: string;
205
+ elementCount: number;
206
+ structureTokens: ReturnType<typeof createExternalDelegateStructureTokens>;
207
+ view: ReturnType<typeof createExternalDelegateView>;
197
208
  };
198
209
  };
210
+ declare const Link: LinkComponent;
199
211
  //#endregion
200
212
  export { type ApiRequestFileCache, type ApiRequestFileMethod, type ApiRequestFileRoute, AppLocals, DefaultLinkProps, EndpointHttpContext, Html, HttpContext, HttpResponse, Link, LinkNavigationProps, LinkProps, MARKLESS_ROUTER_ROUTE_EVENT, MarklessRouterGeneratedRoutes, MiddlewareHttpContext, PageProps, type RequestFileDefaultExport, type RequestFileDiagnostic, type RequestFileDiagnosticCode, type RequestFileParam, type RequestFileParseResult, type RequestFileTransformResult, type RouteDocumentModule, type RouteManifest, type RouteManifestMatch, type RouteManifestParam, type RouteManifestRoute, type RouteManifestStatusPages, type RoutePageModule, type RouteState, type RouteUpdate, __marklessCreateHttpContext, __marklessRouterStartSpaNavigation, buildRouteManifestFromFileIds, dispatchRouteUpdate, ensureNavigationRuntime, matchRouteManifest, normalizeRequestFileId, normalizeRequestPathname, normalizeRouteFileId, parseRequestFile, routePageProps, startRouteUpdateRenderer, transformRequestFileSource };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { i as normalizeRouteFileId, n as matchRouteManifest, r as normalizeRequestPathname, t as buildRouteManifestFromFileIds } from "./route-manifest-DPgGIyrS.js";
2
2
  import { n as parseRequestFile, r as transformRequestFileSource, t as normalizeRequestFileId } from "./request-files-C-0LLSsx.js";
3
+ import { createExternalDelegateStructureTokens, createExternalDelegateView } from "@markless/web/fns/external-delegate";
3
4
  import { render } from "@markless/web/render";
4
5
  import { MARKLESS_PENDING_MIN_VISIBLE_MS as MARKLESS_NAV_PENDING_MIN_MS, settleOrPendingDeadline } from "@markless/web";
5
6
  //#region src/navigation-hold.ts
@@ -31,6 +32,7 @@ function dispatchRouteUpdate(document, update) {
31
32
  }
32
33
  //#endregion
33
34
  //#region src/route-renderer.ts
35
+ const CURRENT_ROUTE_CONTAINER = "__marklessRouterCurrentRouteContainer";
34
36
  function startRouteUpdateRenderer(document = window.document) {
35
37
  const state = document;
36
38
  if (state["__marklessRouterRouteRendererStarted"]) return;
@@ -43,18 +45,25 @@ async function renderRouteUpdate(document, update) {
43
45
  try {
44
46
  const artifact = update.page.default;
45
47
  if (!artifact || update.signal?.aborted) return;
48
+ if (!artifact.renderData) throw new Error(`MARKLESS_ROUTER_RENDER_DATA_MISSING: Navigated route ${JSON.stringify(update.route.file)} has no linked render-data module.`);
46
49
  const props = routePageProps(update.route);
47
- if (typeof artifact.renderCsr === "function") {
48
- await render({ renderCsr: () => artifact.renderCsr?.(props) }, {
49
- target: document.body,
50
- beforeMount: (container) => holdNavigationSwapUntilSettled({
51
- runtime: container.runtime,
50
+ const state = document;
51
+ const container = await render({
52
+ renderData: artifact.renderData,
53
+ loadSymbol: artifact.loadSymbol,
54
+ props
55
+ }, {
56
+ target: document.body,
57
+ beforeMount: async (incoming) => {
58
+ if (await holdNavigationSwapUntilSettled({
59
+ runtime: incoming.runtime,
52
60
  signal: update.signal
53
- })
54
- });
55
- return;
56
- }
57
- if (typeof artifact.renderSsr === "function") document.body.innerHTML = (await artifact.renderSsr(props)).html;
61
+ }) === false) return false;
62
+ state[CURRENT_ROUTE_CONTAINER]?.runtime?.dispose?.();
63
+ return true;
64
+ }
65
+ });
66
+ if (!update.signal?.aborted) state[CURRENT_ROUTE_CONTAINER] = container;
58
67
  } finally {
59
68
  update.onRendered?.();
60
69
  }
@@ -89,8 +98,8 @@ async function __marklessRouterStartSpaNavigation(options) {
89
98
  navigation.addEventListener("navigate", (event) => {
90
99
  handleNavigateEvent(event, context);
91
100
  });
92
- if (typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__) await import("./debug-channel-C7v9u5SM.js").then((n) => n.n).then((debug) => debug.__marklessDebugRegisterRouter(void 0, "spa-click-listener")).catch(() => {});
93
- if (typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__) await import("./debug-channel-C7v9u5SM.js").then((n) => n.n).then((debug) => debug.__marklessDebugRegisterRouter(void 0, "navigation-event")).catch(() => {});
101
+ if (typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__) await import("./debug-channel-CSzo-Zj8.js").then((n) => n.n).then((debug) => debug.__marklessDebugRegisterRouter(void 0, "spa-click-listener")).catch(() => {});
102
+ if (typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__) await import("./debug-channel-CSzo-Zj8.js").then((n) => n.n).then((debug) => debug.__marklessDebugRegisterRouter(void 0, "navigation-event")).catch(() => {});
94
103
  {
95
104
  const bootUrl = parseSameOriginUrl(runtimeWindow.location.href, runtimeWindow.location.href);
96
105
  const bootHashPath = bootUrl ? hashRoutePath(bootUrl) : void 0;
@@ -228,7 +237,10 @@ function __marklessCreateHttpContext(event) {
228
237
  const Html = Object.assign(function Html(props) {
229
238
  return props.children;
230
239
  }, { renderSsr(props = {}) {
231
- return { html: props.children == null ? "" : String(props.children) };
240
+ return {
241
+ html: props.children == null ? "" : String(props.children),
242
+ elementCount: 0
243
+ };
232
244
  } });
233
245
  const Link = Object.assign(function Link(props = {}) {
234
246
  return props.children;
@@ -237,12 +249,33 @@ const Link = Object.assign(function Link(props = {}) {
237
249
  const anchor = document.createElement("a");
238
250
  for (const [name, value] of linkAnchorAttributes(props)) anchor.setAttribute(name, value);
239
251
  anchor.innerHTML = linkChildrenHtml(props);
240
- return { root: anchor };
252
+ return {
253
+ root: anchor,
254
+ view: linkExternalDelegateView(),
255
+ liveHostNodes: new Map([["router:link", anchor]])
256
+ };
241
257
  },
242
- renderSsr(props = {}) {
243
- return { html: `<a ${linkAnchorAttributes(props).map(([name, value]) => value === "" ? name : `${name}="${escapeHtml(value)}"`).join(" ")}>${linkChildrenHtml(props)}</a>` };
258
+ renderSsr(props = {}, renderContext) {
259
+ return {
260
+ html: `<a ${linkAnchorAttributes(props).map(([name, value]) => value === "" ? name : `${name}="${escapeHtml(value)}"`).join(" ")}>${linkChildrenHtml(props)}</a>`,
261
+ elementCount: 1,
262
+ structureTokens: createExternalDelegateStructureTokens({
263
+ hostNodeId: "router:link",
264
+ idPrefix: renderContext?.idPrefix,
265
+ tagName: "a"
266
+ }),
267
+ view: linkExternalDelegateView()
268
+ };
244
269
  }
245
270
  });
271
+ function linkExternalDelegateView() {
272
+ return createExternalDelegateView({
273
+ eventName: "click",
274
+ hostNodeId: "router:link",
275
+ owner: "router",
276
+ tagName: "a"
277
+ });
278
+ }
246
279
  function linkAnchorAttributes(props) {
247
280
  const attributes = /* @__PURE__ */ new Map();
248
281
  const input = props;
@@ -5,6 +5,7 @@ interface ServerEntryOptions {
5
5
  readonly dev?: boolean;
6
6
  readonly navigationEntryPath?: string;
7
7
  readonly resumeEntryPath?: string;
8
+ readonly prerenderWakeEntryPath?: string;
8
9
  readonly routeModulePreloads?: Record<string, readonly ModulePreloadInput[]>;
9
10
  readonly routeSsrModulePreloads?: Record<string, readonly ModulePreloadInput[]>;
10
11
  readonly routeStylesheets?: Record<string, readonly string[]>;
@@ -1,4 +1,4 @@
1
- import { t as __marklessDebugBootstrapSource } from "../../debug-channel-C7v9u5SM.js";
1
+ import { t as __marklessDebugBootstrapSource } from "../../debug-channel-CSzo-Zj8.js";
2
2
  import { i as normalizeRouteFileId, n as matchRouteManifest, t as buildRouteManifestFromFileIds } from "../../route-manifest-DPgGIyrS.js";
3
3
  import { renderToString } from "@markless/web/render-to-string";
4
4
  import { renderToStream } from "@markless/web/render-to-stream";
@@ -80,20 +80,24 @@ function createServerEntry(options) {
80
80
  });
81
81
  }
82
82
  const pageArtifact = routedPageArtifact(renderSsr, baseArtifact, pageProps, file, options.navigationEntryPath, options.routeStylesheets?.[file]);
83
+ const routedArtifact = options.prerenderWakeEntryPath ? {
84
+ ...pageArtifact,
85
+ prerenderWakeModuleUrl: options.prerenderWakeEntryPath
86
+ } : pageArtifact;
83
87
  const renderOptions = {
84
88
  props: pageProps,
85
89
  resumeModuleUrl: options.resumeEntryPath ?? baseArtifact?.resumeModuleUrl,
86
90
  modulePreloads: (html) => modulePreloadsForPage(file, html, pageProps.url.href, manifest, options.routeModulePreloads, options.routeSsrModulePreloads)
87
91
  };
88
92
  if (options.render === "blocking") {
89
- const pageHtml = splitLeadingHeadHtml(await renderToString(pageArtifact, renderOptions));
93
+ const pageHtml = splitLeadingHeadHtml(await renderToString(routedArtifact, renderOptions));
90
94
  const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml);
91
95
  return new Response(fillDocumentChildren(shell, pageHtml.bodyHtml), {
92
96
  status,
93
97
  headers
94
98
  });
95
99
  }
96
- const stream = await renderToStream(pageArtifact, renderOptions);
100
+ const stream = await renderToStream(routedArtifact, renderOptions);
97
101
  const pageHtml = splitLeadingHeadHtml(stream.shell);
98
102
  const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml);
99
103
  if (stream.pendingArmCount === 0) return new Response(fillDocumentChildren(shell, pageHtml.bodyHtml), {
@@ -104,18 +108,34 @@ function createServerEntry(options) {
104
108
  const prefix = placeholderAt === -1 ? shell : shell.slice(0, placeholderAt) + pageHtml.bodyHtml;
105
109
  const suffix = placeholderAt === -1 ? "" : shell.slice(placeholderAt + 37);
106
110
  const encoder = new TextEncoder();
107
- const body = new ReadableStream({ async start(controller) {
108
- controller.enqueue(encoder.encode(prefix));
109
- try {
110
- for await (const chunk of stream.appends()) controller.enqueue(encoder.encode(chunk));
111
- } catch (error) {
112
- console.error("[markless-router] streaming settle failed:", error);
113
- controller.error(error);
114
- return;
111
+ let cancelled = false;
112
+ let completed = false;
113
+ const body = new ReadableStream({
114
+ async start(controller) {
115
+ controller.enqueue(encoder.encode(prefix));
116
+ try {
117
+ for await (const chunk of stream.appends()) {
118
+ if (cancelled) return;
119
+ controller.enqueue(encoder.encode(chunk));
120
+ }
121
+ } catch (error) {
122
+ if (cancelled) return;
123
+ console.error("[markless-router] streaming settle failed:", error);
124
+ controller.error(error);
125
+ completed = true;
126
+ return;
127
+ }
128
+ if (cancelled) return;
129
+ controller.enqueue(encoder.encode(suffix));
130
+ controller.close();
131
+ completed = true;
132
+ },
133
+ cancel(reason) {
134
+ if (completed) return;
135
+ cancelled = true;
136
+ stream.cancel(reason);
115
137
  }
116
- controller.enqueue(encoder.encode(suffix));
117
- controller.close();
118
- } });
138
+ });
119
139
  return new Response(body, {
120
140
  status,
121
141
  headers
@@ -261,7 +281,20 @@ function fillDocumentChildren(documentShell, children) {
261
281
  return documentShell.slice(0, at) + children + documentShell.slice(at + 37);
262
282
  }
263
283
  async function renderDocumentModule(documentModule, props) {
264
- return (await documentModule?.default?.renderSsr?.(props))?.html;
284
+ const output = await documentModule?.default?.renderSsr?.(props);
285
+ assertDocumentStorageFree(output);
286
+ return output?.html;
287
+ }
288
+ function assertDocumentStorageFree(output) {
289
+ const storage = documentStorageRecords(output?.state);
290
+ if (storage.length === 0) return;
291
+ const keys = storage.map((record) => record.key).join(", ");
292
+ throw new Error(`MARKLESS_ROUTER_DOCUMENT_STORAGE_UNSUPPORTED: the document declares storage cells (${keys}), but the router serves only the document's HTML, so their state payload never reaches the browser and they can never resume. Declare storage() at module scope in a component the page renders instead.`);
293
+ }
294
+ function documentStorageRecords(state) {
295
+ if (!state || typeof state !== "object") return [];
296
+ const storage = state.storage;
297
+ return Array.isArray(storage) ? storage : [];
265
298
  }
266
299
  function insertHeadHtml(documentHtml, headHtml) {
267
300
  if (!headHtml) return documentHtml;
@@ -2,6 +2,8 @@
2
2
  type MdxRoutePart = {
3
3
  readonly kind: 'html';
4
4
  readonly elementCount: number;
5
+ readonly html?: string;
6
+ readonly elementTags?: ReadonlyArray<string>;
5
7
  } | {
6
8
  readonly kind: 'component';
7
9
  readonly componentIndex: number;
@@ -12,7 +14,6 @@ type MdxRenderOutput = {
12
14
  readonly state?: MdxStatePayload;
13
15
  readonly view?: MdxViewPayload;
14
16
  readonly loadSymbol?: (symbolId: string) => unknown;
15
- readonly connectRuntime?: (context: unknown) => unknown;
16
17
  };
17
18
  type MdxChild = {
18
19
  readonly componentIndex: number;
@@ -28,11 +29,34 @@ type MdxSymbolLoader = {
28
29
  readonly prefix: string;
29
30
  readonly loadSymbol: (symbolId: string) => unknown;
30
31
  };
32
+ type MdxRenderDataSurface = {
33
+ readonly rootComponentName: string | null;
34
+ readonly renderData: Readonly<Record<string, unknown>>;
35
+ readonly components: Readonly<Record<string, unknown>>;
36
+ readonly imports: Readonly<Record<string, MdxRenderDataSurface>>;
37
+ };
38
+ type MdxRenderDataChild = {
39
+ readonly componentIndex: number;
40
+ readonly hostPrefix: string;
41
+ readonly symbolPrefix: string;
42
+ readonly props: Readonly<Record<string, unknown>>;
43
+ readonly surface: MdxRenderDataSurface;
44
+ };
45
+ declare function createMdxRenderDataSurface(parts: ReadonlyArray<MdxRoutePart>, children: ReadonlyArray<MdxRenderDataChild>): MdxRenderDataSurface;
46
+ type MdxComputedRecord = {
47
+ readonly deriveSymbolId?: string;
48
+ readonly [key: string]: unknown;
49
+ };
50
+ type MdxStorageRecord = {
51
+ readonly graphNodeId: string;
52
+ readonly key: string;
53
+ };
31
54
  type MdxStatePayload = {
32
55
  readonly version: unknown;
33
56
  readonly cells?: readonly unknown[];
34
- readonly computed?: readonly unknown[];
57
+ readonly computed?: readonly MdxComputedRecord[];
35
58
  readonly sharedDefinitions?: readonly unknown[];
59
+ readonly storage?: readonly MdxStorageRecord[];
36
60
  };
37
61
  type MdxLocator = {
38
62
  readonly hostNodeId: string;
@@ -59,10 +83,8 @@ type MdxViewPayload = {
59
83
  readonly asyncBoundaries?: readonly unknown[];
60
84
  };
61
85
  declare function renderMdxChild(children: MdxChild[], component: MdxComponentArtifact, props: unknown, child: Omit<MdxChild, 'output'>): Promise<string>;
62
- declare function rootFromMdxHtml(html: string): Element;
63
- declare function replaceMdxChild(root: ParentNode, index: number, child: ChildNode | undefined): void;
64
86
  declare function composeMdxState(children: readonly MdxChild[]): MdxStatePayload | undefined;
65
87
  declare function composeMdxView(parts: readonly MdxRoutePart[], children: readonly MdxChild[], initialElementOffset: number): MdxViewPayload | undefined;
66
88
  declare function loadMdxSymbol(symbolId: string, children: readonly MdxChild[], loaders: readonly MdxSymbolLoader[]): unknown;
67
89
  //#endregion
68
- export { MdxChild, MdxComponentArtifact, MdxRenderOutput, MdxRoutePart, MdxSymbolLoader, composeMdxState, composeMdxView, loadMdxSymbol, renderMdxChild, replaceMdxChild, rootFromMdxHtml };
90
+ export { MdxChild, MdxComponentArtifact, MdxRenderOutput, MdxRoutePart, MdxSymbolLoader, composeMdxState, composeMdxView, createMdxRenderDataSurface, loadMdxSymbol, renderMdxChild };
@@ -1,4 +1,125 @@
1
+ function protocolStateVersion(storage) {
2
+ return (storage?.length ?? 0) > 0 ? 2 : 1;
3
+ }
4
+ //#endregion
1
5
  //#region src/vite/runtime/mdx-route.ts
6
+ function createMdxRenderDataSurface(parts, children) {
7
+ const componentName = "MarklessMdxRoute";
8
+ const rootChunkId = `template:${componentName}`;
9
+ const childrenByIndex = new Map(children.map((child) => [child.componentIndex, child]));
10
+ const statics = [];
11
+ const slots = [];
12
+ const hosts = [{
13
+ hostNodeId: "__mdx:root",
14
+ tagName: "main",
15
+ coordinate: {
16
+ kind: "child-index",
17
+ path: [0]
18
+ }
19
+ }];
20
+ const edges = [];
21
+ let staticHtml = "<main data-markless-mdx-root>";
22
+ let slotIndex = 0;
23
+ for (const [partIndex, part] of parts.entries()) {
24
+ if (part.kind === "html") {
25
+ staticHtml += part.html ?? "";
26
+ for (const [tagIndex, tagName] of (part.elementTags ?? []).entries()) hosts.push({
27
+ hostNodeId: `__mdx:static:${partIndex}:${tagIndex}`,
28
+ tagName,
29
+ coordinate: {
30
+ kind: "child-index",
31
+ path: [
32
+ 0,
33
+ partIndex,
34
+ tagIndex
35
+ ]
36
+ }
37
+ });
38
+ continue;
39
+ }
40
+ const child = childrenByIndex.get(part.componentIndex);
41
+ if (!child) throw new Error(`MARKLESS_MDX_RENDER_DATA_CHILD_MISSING: ${part.componentIndex}`);
42
+ const childComponentName = child.surface.rootComponentName;
43
+ if (!childComponentName) throw new Error(`MARKLESS_MDX_RENDER_DATA_ROOT_MISSING: ${part.componentIndex}`);
44
+ const edgeId = `mdx:edge:${part.componentIndex}`;
45
+ staticHtml += `<!--markless-slot:${slotIndex}-->`;
46
+ statics.push(staticHtml);
47
+ staticHtml = "";
48
+ slots.push({
49
+ kind: "child-component",
50
+ componentEdgeId: edgeId,
51
+ childComponentName,
52
+ childTemplateId: `template:${childComponentName}`,
53
+ coordinate: {
54
+ kind: "comment-anchor",
55
+ path: [0, partIndex]
56
+ },
57
+ staticIndex: slotIndex
58
+ });
59
+ edges.push({
60
+ id: edgeId,
61
+ childComponentName,
62
+ hostPrefix: child.hostPrefix,
63
+ symbolPrefix: child.symbolPrefix,
64
+ props: Object.entries(child.props).map(([name, value]) => ({
65
+ name,
66
+ kind: "serializable",
67
+ value
68
+ }))
69
+ });
70
+ slotIndex++;
71
+ }
72
+ statics.push(`${staticHtml}</main>`);
73
+ const state = {
74
+ version: 1,
75
+ cells: [],
76
+ computed: []
77
+ };
78
+ const view = {
79
+ version: 1,
80
+ locators: [],
81
+ events: [],
82
+ domUpdates: [],
83
+ behaviors: [],
84
+ elementHandles: [],
85
+ asyncBoundaries: []
86
+ };
87
+ return {
88
+ rootComponentName: componentName,
89
+ renderData: {
90
+ root: {
91
+ componentName,
92
+ templateId: rootChunkId
93
+ },
94
+ chunks: [{
95
+ id: rootChunkId,
96
+ kind: "template",
97
+ componentName,
98
+ statics,
99
+ hosts,
100
+ slots
101
+ }],
102
+ initialValues: [],
103
+ branches: [],
104
+ repeats: [],
105
+ boundaries: [],
106
+ interactions: []
107
+ },
108
+ components: { [componentName]: {
109
+ name: componentName,
110
+ state,
111
+ view,
112
+ rootChunkId,
113
+ stateGraphNodeIds: [],
114
+ initialValues: [],
115
+ branches: [],
116
+ boundaries: [],
117
+ edges,
118
+ propCellId: null
119
+ } },
120
+ imports: Object.fromEntries(children.flatMap((child) => child.surface.rootComponentName ? [[child.surface.rootComponentName, child.surface]] : []))
121
+ };
122
+ }
2
123
  async function renderMdxChild(children, component, props, child) {
3
124
  const output = await component.renderSsr?.(props);
4
125
  if (output) children.push({
@@ -7,29 +128,19 @@ async function renderMdxChild(children, component, props, child) {
7
128
  });
8
129
  return output?.html ?? "";
9
130
  }
10
- function rootFromMdxHtml(html) {
11
- const template = document.createElement("template");
12
- template.innerHTML = html;
13
- const root = template.content.firstElementChild;
14
- if (!root) throw new Error("Markless Router MDX render did not produce a root element.");
15
- return root;
16
- }
17
- function replaceMdxChild(root, index, child) {
18
- const placeholder = root.querySelector?.(`[data-markless-mdx-child="${index}"]`);
19
- if (placeholder && child) {
20
- placeholder.replaceWith(child);
21
- return;
22
- }
23
- placeholder?.remove();
24
- }
25
131
  function composeMdxState(children) {
26
- const childStates = children.map((child) => child.output?.state).filter(isDefined);
132
+ const childStates = children.map((child) => ({
133
+ child,
134
+ state: child.output?.state
135
+ })).filter((entry) => isDefined(entry.state));
27
136
  if (childStates.length === 0) return;
137
+ const storage = childStates.flatMap(({ state }) => state.storage ?? []);
28
138
  return {
29
- version: childStates[0].version,
30
- cells: childStates.flatMap((state) => state.cells ?? []),
31
- computed: childStates.flatMap((state) => state.computed ?? []),
32
- ...childStates.some((state) => state.sharedDefinitions?.length) ? { sharedDefinitions: childStates.flatMap((state) => state.sharedDefinitions ?? []) } : {}
139
+ version: protocolStateVersion(storage),
140
+ cells: childStates.flatMap(({ state }) => state.cells ?? []),
141
+ computed: childStates.flatMap(({ child, state }) => (state.computed ?? []).map((record) => prefixMdxComputedRecord(record, child))),
142
+ ...childStates.some(({ state }) => state.sharedDefinitions?.length) ? { sharedDefinitions: childStates.flatMap(({ state }) => state.sharedDefinitions ?? []) } : {},
143
+ ...storage.length > 0 ? { storage } : {}
33
144
  };
34
145
  }
35
146
  function composeMdxView(parts, children, initialElementOffset) {
@@ -99,6 +210,12 @@ function appendMdxChildView(context) {
99
210
  hostNodeId: context.child.hostPrefix + handle.hostNodeId
100
211
  });
101
212
  }
213
+ function prefixMdxComputedRecord(record, child) {
214
+ return {
215
+ ...record,
216
+ ...record.deriveSymbolId ? { deriveSymbolId: child.symbolPrefix + record.deriveSymbolId } : {}
217
+ };
218
+ }
102
219
  function prefixMdxSymbolRecord(record, child) {
103
220
  return {
104
221
  ...record,
@@ -110,4 +227,4 @@ function isDefined(value) {
110
227
  return value !== void 0;
111
228
  }
112
229
  //#endregion
113
- export { composeMdxState, composeMdxView, loadMdxSymbol, renderMdxChild, replaceMdxChild, rootFromMdxHtml };
230
+ export { composeMdxState, composeMdxView, createMdxRenderDataSurface, loadMdxSymbol, renderMdxChild };
package/dist/vite.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PluginOption } from "vite";
1
+ import { PluginOption, UserConfig } from "vite";
2
2
 
3
3
  //#region src/vite/index.d.ts
4
4
  interface MarklessRouterOptions {
@@ -6,5 +6,6 @@ interface MarklessRouterOptions {
6
6
  nitro?: boolean;
7
7
  }
8
8
  declare function router(options?: MarklessRouterOptions): PluginOption[];
9
+ type WatchIgnorePattern = Extract<NonNullable<NonNullable<NonNullable<UserConfig['server']>['watch']>['ignored']>, readonly unknown[]>[number];
9
10
  //#endregion
10
- export { MarklessRouterOptions, router };
11
+ export { MarklessRouterOptions, WatchIgnorePattern, router };
package/dist/vite.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { t as buildRouteManifestFromFileIds } from "./route-manifest-DPgGIyrS.js";
2
2
  import { r as transformRequestFileSource } from "./request-files-C-0LLSsx.js";
3
+ import { __marklessRouteHref } from "./vite/route-href.js";
3
4
  import { dirname, extname, isAbsolute, join, normalize, relative, resolve } from "pathe";
4
- import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withoutLeadingSlash } from "ufo";
5
+ import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withQuery, withoutLeadingSlash } from "ufo";
5
6
  import { existsSync } from "node:fs";
6
7
  import { nitro } from "nitro/vite";
7
8
  import { sortUserPlugins } from "vite";
@@ -229,6 +230,7 @@ function transformAnchorSource(code, astInput, routePatterns) {
229
230
  const ast = node$1(astInput);
230
231
  if (!ast) return code;
231
232
  const edits = [];
233
+ let needsHrefHelper = false;
232
234
  const linkNames = marklessRouterLinkImportNames(ast);
233
235
  walk(ast, (current) => {
234
236
  if (current.type !== "JSXOpeningElement") return;
@@ -245,17 +247,58 @@ function transformAnchorSource(code, astInput, routePatterns) {
245
247
  const paramsExpression = jsxExpression(node$1(params.value));
246
248
  if (!paramsExpression) fail$1(missingParamsMessage(hrefValue, routeParams));
247
249
  validateObjectLiteralParams(hrefValue, routeParams, paramsExpression);
248
- edits.push({
250
+ const staticParams = staticObjectValue(paramsExpression);
251
+ if (staticParams) edits.push({
249
252
  ...rangeOf(href),
250
- text: `href={__marklessRouteHref(${JSON.stringify(hrefValue)}, ${slice$1(code, paramsExpression)})}`
253
+ text: `href=${JSON.stringify(__marklessRouteHref(hrefValue, staticParams))}`
251
254
  });
255
+ else {
256
+ needsHrefHelper = true;
257
+ edits.push({
258
+ ...rangeOf(href),
259
+ text: `href={__marklessRouteHref(${JSON.stringify(hrefValue)}, ${slice$1(code, paramsExpression)})}`
260
+ });
261
+ }
252
262
  edits.push({
253
263
  ...attributeRemovalRange(code, params),
254
264
  text: ""
255
265
  });
256
266
  });
257
267
  if (edits.length === 0) return code;
258
- return `${helperImport()}${applyEdits(code, edits)}`;
268
+ return `${needsHrefHelper ? helperImport() : ""}${applyEdits(code, edits)}`;
269
+ }
270
+ function staticObjectValue(expression) {
271
+ if (expression.type !== "ObjectExpression") return void 0;
272
+ const value = {};
273
+ for (const property of nodes$1(expression.properties)) {
274
+ if (property.type !== "Property" || property.computed === true) return void 0;
275
+ const key = node$1(property.key);
276
+ const name = key?.type === "Identifier" ? string$1(key.name) : literalString(key);
277
+ const propertyValue = staticExpressionValue(node$1(property.value));
278
+ if (!name || !propertyValue.known) return void 0;
279
+ value[name] = propertyValue.value;
280
+ }
281
+ return value;
282
+ }
283
+ function staticExpressionValue(expression) {
284
+ if (!expression) return { known: false };
285
+ if (expression.type === "Literal") return {
286
+ known: true,
287
+ value: expression.value
288
+ };
289
+ if (expression.type === "ArrayExpression") {
290
+ const values = [];
291
+ for (const element of nodes$1(expression.elements)) {
292
+ const value = staticExpressionValue(element);
293
+ if (!value.known) return { known: false };
294
+ values.push(value.value);
295
+ }
296
+ return {
297
+ known: true,
298
+ value: values
299
+ };
300
+ }
301
+ return { known: false };
259
302
  }
260
303
  function marklessRouterLinkImportNames(ast) {
261
304
  const names = /* @__PURE__ */ new Set();
@@ -383,6 +426,7 @@ function createClientAssetsManifest(input) {
383
426
  base: input.base,
384
427
  entries: {
385
428
  resume: input.resumeEntry,
429
+ ...input.prerenderWakeEntry ? { prerenderWake: input.prerenderWakeEntry } : {},
386
430
  navigation: input.navigationEntry
387
431
  },
388
432
  routes: {
@@ -459,6 +503,7 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
459
503
  if (!isRecord$1(input.routes)) throw clientAssetsManifestError("has invalid route maps");
460
504
  const entries = {
461
505
  resume: requiredString(input.entries.resume, "entries.resume"),
506
+ ...input.entries.prerenderWake !== void 0 ? { prerenderWake: requiredString(input.entries.prerenderWake, "entries.prerenderWake") } : {},
462
507
  navigation: requiredString(input.entries.navigation, "entries.navigation")
463
508
  };
464
509
  const routes = {
@@ -469,6 +514,7 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
469
514
  assertMatchingRouteKeys(routes);
470
515
  const hrefs = new Set([
471
516
  entries.resume,
517
+ ...entries.prerenderWake !== void 0 ? [entries.prerenderWake] : [],
472
518
  entries.navigation,
473
519
  ...Object.values(routes.navigation).flat(),
474
520
  ...Object.values(routes.ssr).flat(),
@@ -677,7 +723,12 @@ async function transformMdxRoute(source, id) {
677
723
  const route = parseMdxRoute(source, id);
678
724
  const html = route.parts.map((part) => part.kind === "html" ? part.html : "").join("");
679
725
  if (route.components.length === 0) return [
726
+ `import { createMdxRenderDataSurface } from '@markless/router/vite/runtime/mdx-route';`,
727
+ `const marklessMdxParts = ${JSON.stringify(route.parts)};`,
728
+ "const marklessMdxRenderData = createMdxRenderDataSurface(marklessMdxParts, []);",
680
729
  "const marklessMdxPage = {",
730
+ " renderData: marklessMdxRenderData,",
731
+ " storageSeeds: marklessMdxStorageSeeds,",
681
732
  " renderSsr() {",
682
733
  ` return { html: ${JSON.stringify(html)} };`,
683
734
  " }",
@@ -685,43 +736,30 @@ async function transformMdxRoute(source, id) {
685
736
  "export default marklessMdxPage;",
686
737
  ""
687
738
  ].join("\n");
688
- return emitComposedMdxRoute(route);
739
+ return emitComposedMdxRoute(route, id);
689
740
  }
690
- function emitComposedMdxRoute(route) {
741
+ function emitComposedMdxRoute(route, id) {
691
742
  return [
692
743
  `import { resumeFromPayloadDocument } from '@markless/core/web/resume';`,
693
- `import { composeMdxState, composeMdxView, loadMdxSymbol, renderMdxChild, replaceMdxChild, rootFromMdxHtml } from '@markless/router/vite/runtime/mdx-route';`,
744
+ `import { composeMdxState, composeMdxView, createMdxRenderDataSurface, loadMdxSymbol, renderMdxChild } from '@markless/router/vite/runtime/mdx-route';`,
694
745
  ...route.imports,
695
746
  "",
696
747
  `const marklessMdxParts = ${JSON.stringify(route.parts)};`,
697
748
  `const marklessMdxSymbolLoaders = ${renderSymbolLoaders(route.components)};`,
749
+ `const marklessMdxRenderData = ${renderMdxRenderDataLoader(route.components, id)};`,
750
+ `const marklessMdxStorageSeeds = ${renderStorageSeeds(route.components)};`,
698
751
  "",
699
752
  "const marklessMdxPage = {",
753
+ " renderData: marklessMdxRenderData,",
754
+ " storageSeeds: marklessMdxStorageSeeds,",
755
+ " loadSymbol: marklessMdxLoadSymbol,",
700
756
  " async renderSsr(props = {}) {",
701
757
  " const marklessMdxChildren = [];",
702
- ` const html = ${renderHtmlExpression(route, "ssr")};`,
758
+ ` const html = ${renderHtmlExpression(route)};`,
703
759
  " const state = composeMdxState(marklessMdxChildren);",
704
760
  " const view = composeMdxView(marklessMdxParts, marklessMdxChildren, 0);",
705
761
  " return { html, ...(state ? { state } : {}), ...(view ? { view } : {}) };",
706
762
  " },",
707
- " renderCsr(props = {}) {",
708
- " const marklessMdxChildren = [];",
709
- ` const root = rootFromMdxHtml(${renderCsrHtml(route)});`,
710
- ...route.components.flatMap((component, index) => [
711
- ` const marklessMdxChild${index} = ${component.localName}.renderCsr?.(${componentPropsExpression(component)});`,
712
- ` replaceMdxChild(root, ${index}, marklessMdxChild${index}?.root);`,
713
- ` if (marklessMdxChild${index}) marklessMdxChildren.push({ componentIndex: ${index}, hostPrefix: ${JSON.stringify(component.prefix)}, symbolPrefix: ${JSON.stringify(component.prefix)}, output: marklessMdxChild${index} });`
714
- ]),
715
- " const state = composeMdxState(marklessMdxChildren);",
716
- " const view = composeMdxView(marklessMdxParts, marklessMdxChildren, 1);",
717
- " return {",
718
- " root,",
719
- " ...(state ? { state } : {}),",
720
- " ...(view ? { view } : {}),",
721
- " loadSymbol(symbolId) { return marklessMdxLoadSymbol(symbolId, marklessMdxChildren); },",
722
- " connectRuntime(context) { for (const child of marklessMdxChildren) child.output?.connectRuntime?.(context); },",
723
- " };",
724
- " },",
725
763
  ...renderMdxPreload(route.components),
726
764
  "};",
727
765
  "export default marklessMdxPage;",
@@ -776,7 +814,7 @@ function parseMdxRoute(source, id) {
776
814
  return {
777
815
  imports,
778
816
  components,
779
- parts: routePartsFromHast(root.children ?? [], {
817
+ parts: routePartsFromHast("children" in root ? root.children : [], {
780
818
  components,
781
819
  id,
782
820
  importedComponents
@@ -826,6 +864,7 @@ function componentPart(node, context) {
826
864
  prefix: `m${componentIndex}:`,
827
865
  props: children ? [...props, {
828
866
  name: "children",
867
+ value: children,
829
868
  valueExpression: JSON.stringify(children)
830
869
  }] : props
831
870
  });
@@ -841,11 +880,15 @@ function appendHtmlPart(parts, nodes, id) {
841
880
  type: "root",
842
881
  children: staticNodes
843
882
  }, { allowDangerousHtml: true });
844
- if (html) parts.push({
845
- kind: "html",
846
- html,
847
- elementCount: countHastElements(staticNodes)
848
- });
883
+ if (html) {
884
+ const elementTags = hastElementTags(staticNodes);
885
+ parts.push({
886
+ kind: "html",
887
+ html,
888
+ elementCount: elementTags.length,
889
+ elementTags
890
+ });
891
+ }
849
892
  }
850
893
  function renderStaticHtml(nodes, id) {
851
894
  if (nodes.length === 0) return "";
@@ -867,6 +910,7 @@ function normalizeStaticHastNode(node, id) {
867
910
  value: value == null ? "" : String(value)
868
911
  }];
869
912
  }
913
+ if (node.type === "root") return normalizeStaticHastNodes(node.children, id);
870
914
  if ("children" in node && Array.isArray(node.children)) return [{
871
915
  ...node,
872
916
  children: normalizeStaticHastNodes(node.children, id)
@@ -881,15 +925,19 @@ function componentProp(attribute, id) {
881
925
  const value = attribute.value;
882
926
  if (value == null) return {
883
927
  name: attribute.name,
928
+ value: true,
884
929
  valueExpression: "true"
885
930
  };
886
931
  if (typeof value === "string") return {
887
932
  name: attribute.name,
933
+ value,
888
934
  valueExpression: JSON.stringify(value)
889
935
  };
936
+ const literal = literalSafeExpressionValue(value.value, id);
890
937
  return {
891
938
  name: attribute.name,
892
- valueExpression: literalValueExpression(literalSafeExpressionValue(value.value, id), id)
939
+ value: literal,
940
+ valueExpression: literalValueExpression(literal, id)
893
941
  };
894
942
  }
895
943
  function literalSafeExpressionValue(source, id) {
@@ -918,8 +966,15 @@ function parseMdxExpressionValue(source, id) {
918
966
  dropHandle(handle);
919
967
  }
920
968
  }
969
+ function literalPropertyName(key) {
970
+ if (key.type === "Identifier" && typeof key.name === "string") return key.name;
971
+ if (key.type === "Literal" && typeof key.value === "string") return key.value;
972
+ }
973
+ function isEstreeNode(value) {
974
+ return typeof value === "object" && value !== null && typeof value.type === "string";
975
+ }
921
976
  function evaluateLiteralExpression(node, id) {
922
- if (!node) throw literalExpressionError(id);
977
+ if (!isEstreeNode(node)) throw literalExpressionError(id);
923
978
  if (node.type === "Literal") {
924
979
  const value = node.value;
925
980
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) return value;
@@ -938,7 +993,7 @@ function evaluateLiteralExpression(node, id) {
938
993
  for (const property of node.properties) {
939
994
  if (property.type !== "Property" || property.kind !== "init" || property.method || property.computed) throw literalExpressionError(id);
940
995
  const key = property.key;
941
- const name = key.type === "Identifier" ? key.name : key.type === "Literal" && typeof key.value === "string" ? key.value : void 0;
996
+ const name = isEstreeNode(key) ? literalPropertyName(key) : void 0;
942
997
  if (!name) throw literalExpressionError(id);
943
998
  value[name] = evaluateLiteralExpression(property.value, id);
944
999
  }
@@ -956,10 +1011,9 @@ function literalValueExpression(value, id) {
956
1011
  function literalExpressionError(id) {
957
1012
  return /* @__PURE__ */ new Error(`Markless Router MDX only supports literal-safe expressions in attributes and markdown: ${id}`);
958
1013
  }
959
- function renderHtmlExpression(route, mode) {
1014
+ function renderHtmlExpression(route) {
960
1015
  return joinExpressions(route.parts.map((part) => {
961
1016
  if (part.kind === "html") return JSON.stringify(part.html);
962
- if (mode === "csr") return JSON.stringify(`<span data-markless-mdx-child="${part.componentIndex}"></span>`);
963
1017
  const component = route.components[part.componentIndex];
964
1018
  return `(await renderMdxChild(marklessMdxChildren, ${component.localName}, ${componentPropsExpression(component)}, { componentIndex: ${part.componentIndex}, hostPrefix: ${JSON.stringify(component.prefix)}, symbolPrefix: ${JSON.stringify(component.prefix)} }))`;
965
1019
  }));
@@ -968,24 +1022,44 @@ function componentPropsExpression(component) {
968
1022
  if (component.props.length === 0) return "{}";
969
1023
  return `{ ${component.props.map((prop) => `${JSON.stringify(prop.name)}: ${prop.valueExpression}`).join(", ")} }`;
970
1024
  }
971
- function renderCsrHtml(route) {
972
- return `"<main data-markless-mdx-root>" + ${renderHtmlExpression(route, "csr")} + "</main>"`;
973
- }
974
1025
  function joinExpressions(expressions) {
975
1026
  const filtered = expressions.filter((expression) => expression !== "\"\"");
976
1027
  return filtered.length === 0 ? "\"\"" : filtered.join(" + ");
977
1028
  }
978
- function countHastElements(nodes) {
979
- let count = 0;
1029
+ function hastElementTags(nodes) {
1030
+ const tags = [];
980
1031
  for (const node of nodes) {
981
- if (node.type === "element") count += 1;
982
- if ("children" in node && Array.isArray(node.children)) count += countHastElements(node.children);
1032
+ if (node.type === "element") tags.push(String(node.tagName));
1033
+ if ("children" in node && Array.isArray(node.children)) tags.push(...hastElementTags(node.children));
983
1034
  }
984
- return count;
1035
+ return tags;
1036
+ }
1037
+ function renderStorageSeeds(components) {
1038
+ const localNames = [...new Set(components.map((component) => component.localName))];
1039
+ return localNames.length === 0 ? "[]" : `[${localNames.map((localName) => `...(${localName}.storageSeeds ?? [])`).join(", ")}]`;
985
1040
  }
986
1041
  function renderSymbolLoaders(components) {
987
1042
  return `[${components.map((component) => `{ prefix: ${JSON.stringify(component.prefix)}, loadSymbol(symbolId) { return import(${JSON.stringify(`${component.specifier}?markless-symbols`)}).then((mod) => mod.loadSymbol(symbolId.slice(${component.prefix.length}))); } }`).join(", ")}]`;
988
1043
  }
1044
+ function renderMdxRenderDataLoader(components, id) {
1045
+ const reachedFrom = mdxMaterializedReachRoot(id);
1046
+ const imports = components.map((component) => {
1047
+ const source = withQuery(component.specifier, {
1048
+ "markless-render-data": null,
1049
+ ...reachedFrom ? { "markless-reached-from": reachedFrom } : {}
1050
+ });
1051
+ return `import(${JSON.stringify(source)})`;
1052
+ });
1053
+ const exportName = reachedFrom ? "marklessPrerenderData" : "marklessRenderData";
1054
+ 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} }`);
1055
+ return `() => Promise.all([${imports.join(", ")}]).then(modules => createMdxRenderDataSurface(marklessMdxParts, [${children.join(", ")}]))`;
1056
+ }
1057
+ function mdxMaterializedReachRoot(id) {
1058
+ const query = new URLSearchParams(parsePath(id).search);
1059
+ const reachedFrom = query.get("markless-reached-from");
1060
+ if (reachedFrom) return reachedFrom;
1061
+ return query.has("markless-route") ? decodePath(parseURL(id).pathname) : void 0;
1062
+ }
989
1063
  function isMdxEsm(node) {
990
1064
  return node.type === "mdxjsEsm";
991
1065
  }
@@ -1005,6 +1079,9 @@ const CLIENT_ENTRY_ID = "virtual:markless-router/client-entry";
1005
1079
  const RESUME_ENTRY_ID = "virtual:markless-router/resume-entry";
1006
1080
  const RESUME_ENTRY_ORIGIN = "/entries/resume-entry.ts";
1007
1081
  const RESUME_ENTRY_PATH_ID = "virtual:markless-router/resume-entry-path";
1082
+ const PRERENDER_WAKE_ENTRY_ID = "virtual:markless-router/prerender-wake-entry";
1083
+ const PRERENDER_WAKE_ENTRY_ORIGIN = "/entries/prerender-wake-entry.ts";
1084
+ const PRERENDER_WAKE_ENTRY_PATH_ID = "virtual:markless-router/prerender-wake-entry-path";
1008
1085
  const NAVIGATION_ENTRY_ID = "virtual:markless-router/navigation-entry";
1009
1086
  const NAVIGATION_ENTRY_ORIGIN = "/entries/client-entry.ts";
1010
1087
  const NAVIGATION_ENTRY_PATH_ID = "virtual:markless-router/navigation-entry-path";
@@ -1013,7 +1090,7 @@ const ROUTE_PRELOADS_PLACEHOLDER = "__MARKLESS_ROUTER_ROUTE_PRELOADS__";
1013
1090
  const SERVER_ENTRY_ID = "virtual:markless-router/server-entry";
1014
1091
  const ROUTE_HREF_ID = "virtual:markless-router/route-href";
1015
1092
  const ROUTER_OPTIONS_ID = "virtual:markless-router/options";
1016
- const PUBLIC_VIRTUAL_MODULE_ID_RE = /^virtual:markless-router\/(?:routes|client-entry|resume-entry|resume-entry-path|navigation-entry|navigation-entry-path|route-preloads|server-entry|route-href|options)(?:\?.*)?$/;
1093
+ 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|route-preloads|server-entry|route-href|options)(?:\?.*)?$/;
1017
1094
  const VITE_PLUGIN_FILE = decodePath(parseURL(import.meta.url).pathname);
1018
1095
  const VIRTUAL_ENTRY_DIR = VITE_PLUGIN_FILE.endsWith(".ts") ? join(dirname(VITE_PLUGIN_FILE), "entries") : join(dirname(dirname(VITE_PLUGIN_FILE)), "src/vite/entries");
1019
1096
  const DEFAULT_WATCH_IGNORES = [
@@ -1026,11 +1103,13 @@ const virtualEntryFiles = {
1026
1103
  [ROUTE_DISCOVERY_ID]: "route-discovery.ts",
1027
1104
  [CLIENT_ENTRY_ID]: "client-entry.ts",
1028
1105
  [RESUME_ENTRY_ID]: "resume-entry.ts",
1106
+ [PRERENDER_WAKE_ENTRY_ID]: "prerender-wake-entry.ts",
1029
1107
  [NAVIGATION_ENTRY_ID]: "client-entry.ts",
1030
1108
  [SERVER_ENTRY_ID]: "server-entry.ts",
1031
1109
  [ROUTE_HREF_ID]: "route-href.ts"
1032
1110
  };
1033
1111
  function router(options = {}) {
1112
+ prerenderWakeChannelCaptured = process.env.MARKLESS_PRERENDER_WAKE === "1";
1034
1113
  if (options.nitro === false) return [
1035
1114
  mdxTransformPlugin(),
1036
1115
  requestFileTransformPlugin(),
@@ -1041,17 +1120,18 @@ function router(options = {}) {
1041
1120
  ];
1042
1121
  const nitroPlugins = nitro();
1043
1122
  const resumeEntry = resumeEntryState();
1123
+ const prerenderWakeEntry = resumeEntryState();
1044
1124
  const navigationEntry = resumeEntryState();
1045
1125
  const routePreloads = routePreloadState();
1046
1126
  return [
1047
- routerConfigPlugin(nitroPlugins, resumeEntry, navigationEntry, routePreloads),
1127
+ routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads),
1048
1128
  devSourceModuleRequestPlugin(),
1049
1129
  mdxTransformPlugin(),
1050
1130
  requestFileTransformPlugin(),
1051
1131
  routeTypegenPlugin(),
1052
1132
  anchorTransformPlugin(),
1053
1133
  htmlTransformPlugin(),
1054
- virtualModulesPlugin(resumeEntry, navigationEntry, routePreloads),
1134
+ virtualModulesPlugin(resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads),
1055
1135
  nitroPlugins
1056
1136
  ];
1057
1137
  }
@@ -1075,7 +1155,7 @@ function devSourceModuleRequestPlugin() {
1075
1155
  }
1076
1156
  };
1077
1157
  }
1078
- function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry, routePreloads) {
1158
+ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads) {
1079
1159
  let clientOutDir = "";
1080
1160
  let clientEnvironmentName = "";
1081
1161
  let clientEnvironmentConfig;
@@ -1115,6 +1195,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
1115
1195
  }
1116
1196
  for (const entry of [
1117
1197
  resumeEntry,
1198
+ prerenderWakeEntry,
1118
1199
  navigationEntry,
1119
1200
  routePreloads
1120
1201
  ]) entry.base = config.base;
@@ -1132,6 +1213,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
1132
1213
  if (isClientEnvironment) {
1133
1214
  pendingClientAssets = void 0;
1134
1215
  resumeEntry.fileName = void 0;
1216
+ prerenderWakeEntry.fileName = void 0;
1135
1217
  navigationEntry.fileName = void 0;
1136
1218
  routePreloads.routes = {
1137
1219
  navigation: {},
@@ -1145,6 +1227,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
1145
1227
  if (!isRouterServerEnvironment) return;
1146
1228
  const manifest = await readClientAssetsManifest(clientOutDir, routePreloads.base);
1147
1229
  resumeEntry.fileName = clientAssetFileName(manifest.entries.resume, manifest.base);
1230
+ prerenderWakeEntry.fileName = manifest.entries.prerenderWake ? clientAssetFileName(manifest.entries.prerenderWake, manifest.base) : void 0;
1148
1231
  navigationEntry.fileName = clientAssetFileName(manifest.entries.navigation, manifest.base);
1149
1232
  routePreloads.routes = manifest.routes;
1150
1233
  routePreloads.persisted = true;
@@ -1155,21 +1238,26 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
1155
1238
  if (this.environment?.config.consumer !== "client") return;
1156
1239
  const resumeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, RESUME_ENTRY_ORIGIN));
1157
1240
  if (resumeChunk) resumeEntry.fileName = resumeChunk.fileName;
1241
+ const prerenderWakeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, PRERENDER_WAKE_ENTRY_ORIGIN));
1242
+ if (prerenderWakeChunk) prerenderWakeEntry.fileName = prerenderWakeChunk.fileName;
1158
1243
  const navigationChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, NAVIGATION_ENTRY_ORIGIN));
1159
1244
  if (navigationChunk) navigationEntry.fileName = navigationChunk.fileName;
1160
1245
  routePreloads.routes = routeModulePreloadsFromBundle({
1161
1246
  base: routePreloads.base,
1162
1247
  bundle,
1163
1248
  navigationChunk,
1249
+ prerenderWakeChunk,
1164
1250
  resumeChunk,
1165
1251
  root: routePreloads.root
1166
1252
  });
1167
1253
  patchRoutePreloadsInBundle(bundle, routePreloads.routes);
1168
1254
  if (productionBuild) {
1169
- if (!resumeEntry.fileName || !navigationEntry.fileName) throw new Error("Markless Router client build did not emit its resume and navigation entries.");
1255
+ const wakeRequired = prerenderWakeChannelEnabled();
1256
+ if (!resumeEntry.fileName || wakeRequired && !prerenderWakeEntry.fileName || !navigationEntry.fileName) throw new Error(wakeRequired ? "Markless Router client build did not emit its resume, prerender-wake, and navigation entries." : "Markless Router client build did not emit its resume and navigation entries.");
1170
1257
  pendingClientAssets = createClientAssetsManifest({
1171
1258
  base: routePreloads.base,
1172
1259
  resumeEntry: joinURL(routePreloads.base, resumeEntry.fileName),
1260
+ ...prerenderWakeEntry.fileName ? { prerenderWakeEntry: joinURL(routePreloads.base, prerenderWakeEntry.fileName) } : {},
1173
1261
  navigationEntry: joinURL(routePreloads.base, navigationEntry.fileName),
1174
1262
  routes: routePreloads.routes
1175
1263
  });
@@ -1275,15 +1363,18 @@ function withWatchIgnores(watchOptions, extraIgnored = []) {
1275
1363
  ...watchOptionsObject,
1276
1364
  followSymlinks: watchOptionsObject.followSymlinks ?? false,
1277
1365
  ignored: [
1278
- ...toArray(watchOptionsObject.ignored),
1366
+ ...toWatchIgnores(watchOptionsObject.ignored),
1279
1367
  ...extraIgnored,
1280
1368
  ...DEFAULT_WATCH_IGNORES
1281
1369
  ]
1282
1370
  };
1283
1371
  }
1284
- function toArray(value) {
1372
+ function toWatchIgnores(value) {
1285
1373
  if (value === void 0) return [];
1286
- return Array.isArray(value) ? value : [value];
1374
+ return isPatternList(value) ? [...value] : [value];
1375
+ }
1376
+ function isPatternList(value) {
1377
+ return Array.isArray(value);
1287
1378
  }
1288
1379
  function withRequestFileBuildPlugin(config, root) {
1289
1380
  const configObject = isRecord(config) ? config : {};
@@ -1318,18 +1409,30 @@ function routePreloadState() {
1318
1409
  }
1319
1410
  };
1320
1411
  }
1412
+ let prerenderWakeChannelCaptured = false;
1413
+ function prerenderWakeChannelEnabled() {
1414
+ return prerenderWakeChannelCaptured;
1415
+ }
1321
1416
  function routerClientInput(input, root) {
1322
1417
  const resumeEntryId = scopedVirtualEntryId(RESUME_ENTRY_ID, root);
1418
+ const prerenderWakeEntryId = scopedVirtualEntryId(PRERENDER_WAKE_ENTRY_ID, root);
1323
1419
  const navigationEntryId = scopedVirtualEntryId(NAVIGATION_ENTRY_ID, root);
1324
- if (input === void 0) return [resumeEntryId, navigationEntryId];
1420
+ const wakeEntries = prerenderWakeChannelEnabled() ? [prerenderWakeEntryId] : [];
1421
+ if (input === void 0) return [
1422
+ resumeEntryId,
1423
+ ...wakeEntries,
1424
+ navigationEntryId
1425
+ ];
1325
1426
  if (typeof input === "string" || Array.isArray(input)) return [
1326
1427
  resumeEntryId,
1428
+ ...wakeEntries,
1327
1429
  navigationEntryId,
1328
1430
  ...Array.isArray(input) ? input : [input]
1329
1431
  ];
1330
1432
  if (isRecord(input)) return {
1331
1433
  ...input,
1332
1434
  "markless-router-resume": resumeEntryId,
1435
+ ...prerenderWakeChannelEnabled() ? { "markless-router-prerender-wake": prerenderWakeEntryId } : {},
1333
1436
  "markless-router-navigation": navigationEntryId
1334
1437
  };
1335
1438
  return input;
@@ -1337,7 +1440,7 @@ function routerClientInput(input, root) {
1337
1440
  function isVirtualEntryChunk(chunk, origin) {
1338
1441
  return [chunk.facadeModuleId, ...chunk.moduleIds ?? []].some((id) => id ? decodePath(parseURL(id).pathname).endsWith(origin) : false);
1339
1442
  }
1340
- function virtualModulesPlugin(resumeEntry = resumeEntryState(), navigationEntry = resumeEntryState(), routePreloads = routePreloadState()) {
1443
+ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEntry = resumeEntryState(), navigationEntry = resumeEntryState(), routePreloads = routePreloadState()) {
1341
1444
  let root = "";
1342
1445
  return {
1343
1446
  name: "markless-router:routes",
@@ -1349,7 +1452,7 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), navigationEntry
1349
1452
  filter: { id: PUBLIC_VIRTUAL_MODULE_ID_RE },
1350
1453
  handler(id) {
1351
1454
  const baseId = virtualModuleBaseId(id);
1352
- if (baseId === SERVER_ENTRY_ID || baseId === RESUME_ENTRY_PATH_ID || baseId === NAVIGATION_ENTRY_PATH_ID || baseId === ROUTE_PRELOADS_ID || baseId === ROUTER_OPTIONS_ID) return `\0${baseId}${rootScopeQuery(root, id)}`;
1455
+ if (baseId === SERVER_ENTRY_ID || baseId === RESUME_ENTRY_PATH_ID || baseId === PRERENDER_WAKE_ENTRY_PATH_ID || baseId === NAVIGATION_ENTRY_PATH_ID || baseId === ROUTE_PRELOADS_ID || baseId === ROUTER_OPTIONS_ID) return `\0${baseId}${rootScopeQuery(root, id)}`;
1353
1456
  const entryFile = virtualEntryFiles[baseId];
1354
1457
  if (!entryFile) return;
1355
1458
  return `${join(VIRTUAL_ENTRY_DIR, entryFile)}${rootScopeQuery(root, id)}`;
@@ -1358,6 +1461,10 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), navigationEntry
1358
1461
  load(id) {
1359
1462
  if (id.startsWith(`\0${SERVER_ENTRY_ID}`)) return serverEntrySource(root);
1360
1463
  if (id.startsWith(`\0${RESUME_ENTRY_PATH_ID}`)) return entryPathSource("resumeEntryPath", resumeEntry, RESUME_ENTRY_ID, root);
1464
+ if (id.startsWith(`\0${PRERENDER_WAKE_ENTRY_PATH_ID}`)) {
1465
+ if (!prerenderWakeChannelEnabled() || !prerenderWakeEntry.fileName) return "export const prerenderWakeEntryPath = undefined;";
1466
+ return entryPathSource("prerenderWakeEntryPath", prerenderWakeEntry, PRERENDER_WAKE_ENTRY_ID, root);
1467
+ }
1361
1468
  if (id.startsWith(`\0${NAVIGATION_ENTRY_PATH_ID}`)) return entryPathSource("navigationEntryPath", navigationEntry, NAVIGATION_ENTRY_ID, root);
1362
1469
  if (id.startsWith(`\0${ROUTE_PRELOADS_ID}`)) return routePreloadsSource(routePreloads, this?.environment?.config.consumer === "client");
1363
1470
  if (id.startsWith(`\0${ROUTER_OPTIONS_ID}`)) return "export const routerMode = \"path\"; // retained for compat; hash paths are always first-class";
@@ -1373,6 +1480,7 @@ function serverEntrySource(root) {
1373
1480
  return [
1374
1481
  `import { createServerEntry } from '@markless/router/vite/runtime/create-server-entry';`,
1375
1482
  `import { resumeEntryPath } from '${RESUME_ENTRY_PATH_ID}${query}';`,
1483
+ `import { prerenderWakeEntryPath } from '${PRERENDER_WAKE_ENTRY_PATH_ID}${query}';`,
1376
1484
  `import { navigationEntryPath } from '${NAVIGATION_ENTRY_PATH_ID}${query}';`,
1377
1485
  `import { routeModulePreloads, routeSsrModulePreloads, routeStylesheets } from '${ROUTE_PRELOADS_ID}${query}';`,
1378
1486
  `import { pageModuleLoaders, routeFileIds } from '${ROUTE_DISCOVERY_ID}${query}';`,
@@ -1380,6 +1488,7 @@ function serverEntrySource(root) {
1380
1488
  `const entry = createServerEntry({`,
1381
1489
  ` dev: import.meta.env.DEV,`,
1382
1490
  ` resumeEntryPath,`,
1491
+ ` prerenderWakeEntryPath,`,
1383
1492
  ` navigationEntryPath,`,
1384
1493
  ` routeModulePreloads,`,
1385
1494
  ` routeSsrModulePreloads,`,
@@ -1475,6 +1584,10 @@ function routeModulePreloadsFromBundle(input) {
1475
1584
  includeChunk(ssrFileNames, chunksByFileName, input.resumeChunk.fileName);
1476
1585
  for (const fileName of routeScopedDynamicImports(input.resumeChunk, routeFile)) includeChunk(ssrFileNames, chunksByFileName, fileName, true);
1477
1586
  }
1587
+ if (input.prerenderWakeChunk) {
1588
+ includeChunk(ssrFileNames, chunksByFileName, input.prerenderWakeChunk.fileName);
1589
+ for (const fileName of routeScopedDynamicImports(input.prerenderWakeChunk, routeFile)) includeChunk(ssrFileNames, chunksByFileName, fileName, true);
1590
+ }
1478
1591
  includeChunk(ssrFileNames, chunksByFileName, routeChunk.fileName, true);
1479
1592
  const routeSourceFiles = sourceFilesForChunks(new Set([...navigationFileNames, ...ssrFileNames]), chunksByFileName);
1480
1593
  for (const [sourceFile, symbolFileNames] of symbolChunks) {
@@ -1486,7 +1599,7 @@ function routeModulePreloadsFromBundle(input) {
1486
1599
  }
1487
1600
  navigation[routeFile] = [...navigationFileNames].map((fileName) => joinURL(input.base, fileName));
1488
1601
  ssr[routeFile] = [...ssrFileNames].map((fileName) => joinURL(input.base, fileName));
1489
- styles[routeFile] = routeStylesheetsForChunk(routeChunk, chunksByFileName, input.base);
1602
+ styles[routeFile] = routeStylesheetsForChunks(routeFileChunks, chunksByFileName, input.base);
1490
1603
  }
1491
1604
  return {
1492
1605
  navigation,
@@ -1494,7 +1607,7 @@ function routeModulePreloadsFromBundle(input) {
1494
1607
  styles
1495
1608
  };
1496
1609
  }
1497
- function routeStylesheetsForChunk(routeChunk, chunksByFileName, base) {
1610
+ function routeStylesheetsForChunks(routeFileChunks, chunksByFileName, base) {
1498
1611
  const styles = /* @__PURE__ */ new Set();
1499
1612
  const visited = /* @__PURE__ */ new Set();
1500
1613
  const visit = (chunk) => {
@@ -1506,7 +1619,7 @@ function routeStylesheetsForChunk(routeChunk, chunksByFileName, base) {
1506
1619
  }
1507
1620
  for (const stylesheet of chunk.viteMetadata?.importedCss ?? []) styles.add(joinURL(base, stylesheet));
1508
1621
  };
1509
- visit(routeChunk);
1622
+ for (const chunk of routeFileChunks) visit(chunk);
1510
1623
  return [...styles];
1511
1624
  }
1512
1625
  function symbolChunksBySourceFile(chunks) {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@markless/router",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Markless router: client-side navigation with SSR streaming and build-time preload maps.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/markless-dev/markless.git",
8
+ "url": "git+https://github.com/compiled-run/markless.git",
9
9
  "directory": "packages/router"
10
10
  },
11
11
  "files": [
@@ -57,18 +57,18 @@
57
57
  "provenance": true
58
58
  },
59
59
  "dependencies": {
60
- "@tsrx/core": "^0.1.32",
60
+ "@tsrx/core": "^0.1.58",
61
61
  "@virtualstate/navigation": "1.0.1-alpha.212",
62
62
  "hast-util-to-html": "^9.0.5",
63
63
  "nitro": "3.0.260429-beta",
64
64
  "pathe": "^2.0.3",
65
65
  "satteri": "0.6.3",
66
66
  "ufo": "^1.6.4",
67
- "@markless/bundler": "0.2.2",
68
- "@markless/web": "0.2.2"
67
+ "@markless/web": "0.3.1",
68
+ "@markless/bundler": "0.3.1"
69
69
  },
70
70
  "devDependencies": {
71
- "@markless/analyzer": "0.2.2"
71
+ "@markless/analyzer": "0.3.1"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "vite": "8.0.16"
@@ -12,7 +12,9 @@ import {
12
12
  import { preloadRouteModule } from 'virtual:markless-router/route-preloads';
13
13
 
14
14
  const routeDiscovery = createRouteDiscovery(
15
- import.meta.glob(['/pages/**/*.tsrx', '/pages/**/*.mdx']),
15
+ import.meta.glob(['/pages/**/*.tsrx', '/pages/**/*.mdx'], {
16
+ query: '?markless-route',
17
+ }),
16
18
  );
17
19
 
18
20
  export const pageModules = routeDiscovery.pageModuleLoaders;
@@ -0,0 +1,40 @@
1
+ const mdxPageModuleLoaders = import.meta.glob('/pages/**/*.mdx');
2
+ const tsrxPrerenderWakeModuleLoaders = import.meta.glob('/pages/**/*.tsrx', {
3
+ query: '?markless-prerender-wake',
4
+ });
5
+
6
+ export async function resumeContainerEvent(input: {
7
+ readonly root: ParentNode;
8
+ readonly [key: string]: unknown;
9
+ }) {
10
+ const file = routeFileFromRoot(input.root);
11
+ const loadRouteWakeModule = file && routeWakeModuleLoader(file);
12
+ const routeWakeModule = loadRouteWakeModule ? await loadRouteWakeModule() : undefined;
13
+ const resume = (routeWakeModule as RouteWakeModule | undefined)?.resumeContainerEvent;
14
+ if (typeof resume !== 'function') {
15
+ throw new Error(`Markless Router could not wake prerendered route module: ${file ?? '<unknown>'}`);
16
+ }
17
+ await resume(input);
18
+ }
19
+
20
+ interface RouteWakeModule {
21
+ readonly resumeContainerEvent?: (input: unknown) => unknown;
22
+ }
23
+
24
+ function routeWakeModuleLoader(file: string): (() => Promise<unknown>) | undefined {
25
+ const loaders = file.endsWith('.mdx') ? mdxPageModuleLoaders : tsrxPrerenderWakeModuleLoaders;
26
+ return loaders[file] ?? loaders[`/${file}`];
27
+ }
28
+
29
+ function routeFileFromRoot(root: ParentNode): string | undefined {
30
+ const script = root.querySelector?.('script[type="@markless/core/route"]');
31
+ const text = script?.textContent;
32
+ if (!text) return undefined;
33
+
34
+ try {
35
+ const route = JSON.parse(text) as { readonly file?: unknown };
36
+ return typeof route.file === 'string' ? route.file : undefined;
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ }
@@ -1,5 +1,6 @@
1
1
  import { createServerEntry } from '@markless/router/vite/runtime/create-server-entry';
2
2
  import { resumeEntryPath } from 'virtual:markless-router/resume-entry-path';
3
+ import { prerenderWakeEntryPath } from 'virtual:markless-router/prerender-wake-entry-path';
3
4
  import { navigationEntryPath } from 'virtual:markless-router/navigation-entry-path';
4
5
  import {
5
6
  routeModulePreloads,
@@ -12,6 +13,7 @@ const documentModuleLoaders = import.meta.glob(['/document.tsrx']);
12
13
  const entry = createServerEntry({
13
14
  dev: import.meta.env.DEV,
14
15
  resumeEntryPath,
16
+ prerenderWakeEntryPath,
15
17
  navigationEntryPath,
16
18
  routeModulePreloads,
17
19
  routeSsrModulePreloads,