@markless/router 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -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,6 +29,20 @@ 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;
31
46
  type MdxStatePayload = {
32
47
  readonly version: unknown;
33
48
  readonly cells?: readonly unknown[];
@@ -59,10 +74,8 @@ type MdxViewPayload = {
59
74
  readonly asyncBoundaries?: readonly unknown[];
60
75
  };
61
76
  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
77
  declare function composeMdxState(children: readonly MdxChild[]): MdxStatePayload | undefined;
65
78
  declare function composeMdxView(parts: readonly MdxRoutePart[], children: readonly MdxChild[], initialElementOffset: number): MdxViewPayload | undefined;
66
79
  declare function loadMdxSymbol(symbolId: string, children: readonly MdxChild[], loaders: readonly MdxSymbolLoader[]): unknown;
67
80
  //#endregion
68
- export { MdxChild, MdxComponentArtifact, MdxRenderOutput, MdxRoutePart, MdxSymbolLoader, composeMdxState, composeMdxView, loadMdxSymbol, renderMdxChild, replaceMdxChild, rootFromMdxHtml };
81
+ export { MdxChild, MdxComponentArtifact, MdxRenderOutput, MdxRoutePart, MdxSymbolLoader, composeMdxState, composeMdxView, createMdxRenderDataSurface, loadMdxSymbol, renderMdxChild };
@@ -1,4 +1,121 @@
1
1
  //#region src/vite/runtime/mdx-route.ts
2
+ function createMdxRenderDataSurface(parts, children) {
3
+ const componentName = "MarklessMdxRoute";
4
+ const rootChunkId = `template:${componentName}`;
5
+ const childrenByIndex = new Map(children.map((child) => [child.componentIndex, child]));
6
+ const statics = [];
7
+ const slots = [];
8
+ const hosts = [{
9
+ hostNodeId: "__mdx:root",
10
+ tagName: "main",
11
+ coordinate: {
12
+ kind: "child-index",
13
+ path: [0]
14
+ }
15
+ }];
16
+ const edges = [];
17
+ let staticHtml = "<main data-markless-mdx-root>";
18
+ let slotIndex = 0;
19
+ for (const [partIndex, part] of parts.entries()) {
20
+ if (part.kind === "html") {
21
+ staticHtml += part.html ?? "";
22
+ for (const [tagIndex, tagName] of (part.elementTags ?? []).entries()) hosts.push({
23
+ hostNodeId: `__mdx:static:${partIndex}:${tagIndex}`,
24
+ tagName,
25
+ coordinate: {
26
+ kind: "child-index",
27
+ path: [
28
+ 0,
29
+ partIndex,
30
+ tagIndex
31
+ ]
32
+ }
33
+ });
34
+ continue;
35
+ }
36
+ const child = childrenByIndex.get(part.componentIndex);
37
+ if (!child) throw new Error(`MARKLESS_MDX_RENDER_DATA_CHILD_MISSING: ${part.componentIndex}`);
38
+ const childComponentName = child.surface.rootComponentName;
39
+ if (!childComponentName) throw new Error(`MARKLESS_MDX_RENDER_DATA_ROOT_MISSING: ${part.componentIndex}`);
40
+ const edgeId = `mdx:edge:${part.componentIndex}`;
41
+ staticHtml += `<!--markless-slot:${slotIndex}-->`;
42
+ statics.push(staticHtml);
43
+ staticHtml = "";
44
+ slots.push({
45
+ kind: "child-component",
46
+ componentEdgeId: edgeId,
47
+ childComponentName,
48
+ childTemplateId: `template:${childComponentName}`,
49
+ coordinate: {
50
+ kind: "comment-anchor",
51
+ path: [0, partIndex]
52
+ },
53
+ staticIndex: slotIndex
54
+ });
55
+ edges.push({
56
+ id: edgeId,
57
+ childComponentName,
58
+ hostPrefix: child.hostPrefix,
59
+ symbolPrefix: child.symbolPrefix,
60
+ props: Object.entries(child.props).map(([name, value]) => ({
61
+ name,
62
+ kind: "serializable",
63
+ value
64
+ }))
65
+ });
66
+ slotIndex++;
67
+ }
68
+ statics.push(`${staticHtml}</main>`);
69
+ const state = {
70
+ version: 1,
71
+ cells: [],
72
+ computed: []
73
+ };
74
+ const view = {
75
+ version: 1,
76
+ locators: [],
77
+ events: [],
78
+ domUpdates: [],
79
+ behaviors: [],
80
+ elementHandles: [],
81
+ asyncBoundaries: []
82
+ };
83
+ return {
84
+ rootComponentName: componentName,
85
+ renderData: {
86
+ root: {
87
+ componentName,
88
+ templateId: rootChunkId
89
+ },
90
+ chunks: [{
91
+ id: rootChunkId,
92
+ kind: "template",
93
+ componentName,
94
+ statics,
95
+ hosts,
96
+ slots
97
+ }],
98
+ initialValues: [],
99
+ branches: [],
100
+ repeats: [],
101
+ boundaries: [],
102
+ interactions: []
103
+ },
104
+ components: { [componentName]: {
105
+ name: componentName,
106
+ state,
107
+ view,
108
+ rootChunkId,
109
+ stateGraphNodeIds: [],
110
+ initialValues: [],
111
+ branches: [],
112
+ boundaries: [],
113
+ edges,
114
+ propCellId: null
115
+ } },
116
+ imports: Object.fromEntries(children.flatMap((child) => child.surface.rootComponentName ? [[child.surface.rootComponentName, child.surface]] : []))
117
+ };
118
+ }
2
119
  async function renderMdxChild(children, component, props, child) {
3
120
  const output = await component.renderSsr?.(props);
4
121
  if (output) children.push({
@@ -7,21 +124,6 @@ async function renderMdxChild(children, component, props, child) {
7
124
  });
8
125
  return output?.html ?? "";
9
126
  }
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
127
  function composeMdxState(children) {
26
128
  const childStates = children.map((child) => child.output?.state).filter(isDefined);
27
129
  if (childStates.length === 0) return;
@@ -110,4 +212,4 @@ function isDefined(value) {
110
212
  return value !== void 0;
111
213
  }
112
214
  //#endregion
113
- export { composeMdxState, composeMdxView, loadMdxSymbol, renderMdxChild, replaceMdxChild, rootFromMdxHtml };
215
+ 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,11 @@ 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,",
681
731
  " renderSsr() {",
682
732
  ` return { html: ${JSON.stringify(html)} };`,
683
733
  " }",
@@ -685,43 +735,28 @@ async function transformMdxRoute(source, id) {
685
735
  "export default marklessMdxPage;",
686
736
  ""
687
737
  ].join("\n");
688
- return emitComposedMdxRoute(route);
738
+ return emitComposedMdxRoute(route, id);
689
739
  }
690
- function emitComposedMdxRoute(route) {
740
+ function emitComposedMdxRoute(route, id) {
691
741
  return [
692
742
  `import { resumeFromPayloadDocument } from '@markless/core/web/resume';`,
693
- `import { composeMdxState, composeMdxView, loadMdxSymbol, renderMdxChild, replaceMdxChild, rootFromMdxHtml } from '@markless/router/vite/runtime/mdx-route';`,
743
+ `import { composeMdxState, composeMdxView, createMdxRenderDataSurface, loadMdxSymbol, renderMdxChild } from '@markless/router/vite/runtime/mdx-route';`,
694
744
  ...route.imports,
695
745
  "",
696
746
  `const marklessMdxParts = ${JSON.stringify(route.parts)};`,
697
747
  `const marklessMdxSymbolLoaders = ${renderSymbolLoaders(route.components)};`,
748
+ `const marklessMdxRenderData = ${renderMdxRenderDataLoader(route.components, id)};`,
698
749
  "",
699
750
  "const marklessMdxPage = {",
751
+ " renderData: marklessMdxRenderData,",
752
+ " loadSymbol: marklessMdxLoadSymbol,",
700
753
  " async renderSsr(props = {}) {",
701
754
  " const marklessMdxChildren = [];",
702
- ` const html = ${renderHtmlExpression(route, "ssr")};`,
755
+ ` const html = ${renderHtmlExpression(route)};`,
703
756
  " const state = composeMdxState(marklessMdxChildren);",
704
757
  " const view = composeMdxView(marklessMdxParts, marklessMdxChildren, 0);",
705
758
  " return { html, ...(state ? { state } : {}), ...(view ? { view } : {}) };",
706
759
  " },",
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
760
  ...renderMdxPreload(route.components),
726
761
  "};",
727
762
  "export default marklessMdxPage;",
@@ -776,7 +811,7 @@ function parseMdxRoute(source, id) {
776
811
  return {
777
812
  imports,
778
813
  components,
779
- parts: routePartsFromHast(root.children ?? [], {
814
+ parts: routePartsFromHast("children" in root ? root.children : [], {
780
815
  components,
781
816
  id,
782
817
  importedComponents
@@ -826,6 +861,7 @@ function componentPart(node, context) {
826
861
  prefix: `m${componentIndex}:`,
827
862
  props: children ? [...props, {
828
863
  name: "children",
864
+ value: children,
829
865
  valueExpression: JSON.stringify(children)
830
866
  }] : props
831
867
  });
@@ -841,11 +877,15 @@ function appendHtmlPart(parts, nodes, id) {
841
877
  type: "root",
842
878
  children: staticNodes
843
879
  }, { allowDangerousHtml: true });
844
- if (html) parts.push({
845
- kind: "html",
846
- html,
847
- elementCount: countHastElements(staticNodes)
848
- });
880
+ if (html) {
881
+ const elementTags = hastElementTags(staticNodes);
882
+ parts.push({
883
+ kind: "html",
884
+ html,
885
+ elementCount: elementTags.length,
886
+ elementTags
887
+ });
888
+ }
849
889
  }
850
890
  function renderStaticHtml(nodes, id) {
851
891
  if (nodes.length === 0) return "";
@@ -867,6 +907,7 @@ function normalizeStaticHastNode(node, id) {
867
907
  value: value == null ? "" : String(value)
868
908
  }];
869
909
  }
910
+ if (node.type === "root") return normalizeStaticHastNodes(node.children, id);
870
911
  if ("children" in node && Array.isArray(node.children)) return [{
871
912
  ...node,
872
913
  children: normalizeStaticHastNodes(node.children, id)
@@ -881,15 +922,19 @@ function componentProp(attribute, id) {
881
922
  const value = attribute.value;
882
923
  if (value == null) return {
883
924
  name: attribute.name,
925
+ value: true,
884
926
  valueExpression: "true"
885
927
  };
886
928
  if (typeof value === "string") return {
887
929
  name: attribute.name,
930
+ value,
888
931
  valueExpression: JSON.stringify(value)
889
932
  };
933
+ const literal = literalSafeExpressionValue(value.value, id);
890
934
  return {
891
935
  name: attribute.name,
892
- valueExpression: literalValueExpression(literalSafeExpressionValue(value.value, id), id)
936
+ value: literal,
937
+ valueExpression: literalValueExpression(literal, id)
893
938
  };
894
939
  }
895
940
  function literalSafeExpressionValue(source, id) {
@@ -918,8 +963,15 @@ function parseMdxExpressionValue(source, id) {
918
963
  dropHandle(handle);
919
964
  }
920
965
  }
966
+ function literalPropertyName(key) {
967
+ if (key.type === "Identifier" && typeof key.name === "string") return key.name;
968
+ if (key.type === "Literal" && typeof key.value === "string") return key.value;
969
+ }
970
+ function isEstreeNode(value) {
971
+ return typeof value === "object" && value !== null && typeof value.type === "string";
972
+ }
921
973
  function evaluateLiteralExpression(node, id) {
922
- if (!node) throw literalExpressionError(id);
974
+ if (!isEstreeNode(node)) throw literalExpressionError(id);
923
975
  if (node.type === "Literal") {
924
976
  const value = node.value;
925
977
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) return value;
@@ -938,7 +990,7 @@ function evaluateLiteralExpression(node, id) {
938
990
  for (const property of node.properties) {
939
991
  if (property.type !== "Property" || property.kind !== "init" || property.method || property.computed) throw literalExpressionError(id);
940
992
  const key = property.key;
941
- const name = key.type === "Identifier" ? key.name : key.type === "Literal" && typeof key.value === "string" ? key.value : void 0;
993
+ const name = isEstreeNode(key) ? literalPropertyName(key) : void 0;
942
994
  if (!name) throw literalExpressionError(id);
943
995
  value[name] = evaluateLiteralExpression(property.value, id);
944
996
  }
@@ -956,10 +1008,9 @@ function literalValueExpression(value, id) {
956
1008
  function literalExpressionError(id) {
957
1009
  return /* @__PURE__ */ new Error(`Markless Router MDX only supports literal-safe expressions in attributes and markdown: ${id}`);
958
1010
  }
959
- function renderHtmlExpression(route, mode) {
1011
+ function renderHtmlExpression(route) {
960
1012
  return joinExpressions(route.parts.map((part) => {
961
1013
  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
1014
  const component = route.components[part.componentIndex];
964
1015
  return `(await renderMdxChild(marklessMdxChildren, ${component.localName}, ${componentPropsExpression(component)}, { componentIndex: ${part.componentIndex}, hostPrefix: ${JSON.stringify(component.prefix)}, symbolPrefix: ${JSON.stringify(component.prefix)} }))`;
965
1016
  }));
@@ -968,24 +1019,40 @@ function componentPropsExpression(component) {
968
1019
  if (component.props.length === 0) return "{}";
969
1020
  return `{ ${component.props.map((prop) => `${JSON.stringify(prop.name)}: ${prop.valueExpression}`).join(", ")} }`;
970
1021
  }
971
- function renderCsrHtml(route) {
972
- return `"<main data-markless-mdx-root>" + ${renderHtmlExpression(route, "csr")} + "</main>"`;
973
- }
974
1022
  function joinExpressions(expressions) {
975
1023
  const filtered = expressions.filter((expression) => expression !== "\"\"");
976
1024
  return filtered.length === 0 ? "\"\"" : filtered.join(" + ");
977
1025
  }
978
- function countHastElements(nodes) {
979
- let count = 0;
1026
+ function hastElementTags(nodes) {
1027
+ const tags = [];
980
1028
  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);
1029
+ if (node.type === "element") tags.push(String(node.tagName));
1030
+ if ("children" in node && Array.isArray(node.children)) tags.push(...hastElementTags(node.children));
983
1031
  }
984
- return count;
1032
+ return tags;
985
1033
  }
986
1034
  function renderSymbolLoaders(components) {
987
1035
  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
1036
  }
1037
+ function renderMdxRenderDataLoader(components, id) {
1038
+ const reachedFrom = mdxMaterializedReachRoot(id);
1039
+ const imports = components.map((component) => {
1040
+ const source = withQuery(component.specifier, {
1041
+ "markless-render-data": null,
1042
+ ...reachedFrom ? { "markless-reached-from": reachedFrom } : {}
1043
+ });
1044
+ return `import(${JSON.stringify(source)})`;
1045
+ });
1046
+ const exportName = reachedFrom ? "marklessPrerenderData" : "marklessRenderData";
1047
+ 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} }`);
1048
+ return `() => Promise.all([${imports.join(", ")}]).then(modules => createMdxRenderDataSurface(marklessMdxParts, [${children.join(", ")}]))`;
1049
+ }
1050
+ function mdxMaterializedReachRoot(id) {
1051
+ const query = new URLSearchParams(parsePath(id).search);
1052
+ const reachedFrom = query.get("markless-reached-from");
1053
+ if (reachedFrom) return reachedFrom;
1054
+ return query.has("markless-route") ? decodePath(parseURL(id).pathname) : void 0;
1055
+ }
989
1056
  function isMdxEsm(node) {
990
1057
  return node.type === "mdxjsEsm";
991
1058
  }
@@ -1005,6 +1072,9 @@ const CLIENT_ENTRY_ID = "virtual:markless-router/client-entry";
1005
1072
  const RESUME_ENTRY_ID = "virtual:markless-router/resume-entry";
1006
1073
  const RESUME_ENTRY_ORIGIN = "/entries/resume-entry.ts";
1007
1074
  const RESUME_ENTRY_PATH_ID = "virtual:markless-router/resume-entry-path";
1075
+ const PRERENDER_WAKE_ENTRY_ID = "virtual:markless-router/prerender-wake-entry";
1076
+ const PRERENDER_WAKE_ENTRY_ORIGIN = "/entries/prerender-wake-entry.ts";
1077
+ const PRERENDER_WAKE_ENTRY_PATH_ID = "virtual:markless-router/prerender-wake-entry-path";
1008
1078
  const NAVIGATION_ENTRY_ID = "virtual:markless-router/navigation-entry";
1009
1079
  const NAVIGATION_ENTRY_ORIGIN = "/entries/client-entry.ts";
1010
1080
  const NAVIGATION_ENTRY_PATH_ID = "virtual:markless-router/navigation-entry-path";
@@ -1013,7 +1083,7 @@ const ROUTE_PRELOADS_PLACEHOLDER = "__MARKLESS_ROUTER_ROUTE_PRELOADS__";
1013
1083
  const SERVER_ENTRY_ID = "virtual:markless-router/server-entry";
1014
1084
  const ROUTE_HREF_ID = "virtual:markless-router/route-href";
1015
1085
  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)(?:\?.*)?$/;
1086
+ 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
1087
  const VITE_PLUGIN_FILE = decodePath(parseURL(import.meta.url).pathname);
1018
1088
  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
1089
  const DEFAULT_WATCH_IGNORES = [
@@ -1026,11 +1096,13 @@ const virtualEntryFiles = {
1026
1096
  [ROUTE_DISCOVERY_ID]: "route-discovery.ts",
1027
1097
  [CLIENT_ENTRY_ID]: "client-entry.ts",
1028
1098
  [RESUME_ENTRY_ID]: "resume-entry.ts",
1099
+ [PRERENDER_WAKE_ENTRY_ID]: "prerender-wake-entry.ts",
1029
1100
  [NAVIGATION_ENTRY_ID]: "client-entry.ts",
1030
1101
  [SERVER_ENTRY_ID]: "server-entry.ts",
1031
1102
  [ROUTE_HREF_ID]: "route-href.ts"
1032
1103
  };
1033
1104
  function router(options = {}) {
1105
+ prerenderWakeChannelCaptured = process.env.MARKLESS_PRERENDER_WAKE === "1";
1034
1106
  if (options.nitro === false) return [
1035
1107
  mdxTransformPlugin(),
1036
1108
  requestFileTransformPlugin(),
@@ -1041,17 +1113,18 @@ function router(options = {}) {
1041
1113
  ];
1042
1114
  const nitroPlugins = nitro();
1043
1115
  const resumeEntry = resumeEntryState();
1116
+ const prerenderWakeEntry = resumeEntryState();
1044
1117
  const navigationEntry = resumeEntryState();
1045
1118
  const routePreloads = routePreloadState();
1046
1119
  return [
1047
- routerConfigPlugin(nitroPlugins, resumeEntry, navigationEntry, routePreloads),
1120
+ routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads),
1048
1121
  devSourceModuleRequestPlugin(),
1049
1122
  mdxTransformPlugin(),
1050
1123
  requestFileTransformPlugin(),
1051
1124
  routeTypegenPlugin(),
1052
1125
  anchorTransformPlugin(),
1053
1126
  htmlTransformPlugin(),
1054
- virtualModulesPlugin(resumeEntry, navigationEntry, routePreloads),
1127
+ virtualModulesPlugin(resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads),
1055
1128
  nitroPlugins
1056
1129
  ];
1057
1130
  }
@@ -1075,7 +1148,7 @@ function devSourceModuleRequestPlugin() {
1075
1148
  }
1076
1149
  };
1077
1150
  }
1078
- function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry, routePreloads) {
1151
+ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads) {
1079
1152
  let clientOutDir = "";
1080
1153
  let clientEnvironmentName = "";
1081
1154
  let clientEnvironmentConfig;
@@ -1115,6 +1188,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
1115
1188
  }
1116
1189
  for (const entry of [
1117
1190
  resumeEntry,
1191
+ prerenderWakeEntry,
1118
1192
  navigationEntry,
1119
1193
  routePreloads
1120
1194
  ]) entry.base = config.base;
@@ -1132,6 +1206,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
1132
1206
  if (isClientEnvironment) {
1133
1207
  pendingClientAssets = void 0;
1134
1208
  resumeEntry.fileName = void 0;
1209
+ prerenderWakeEntry.fileName = void 0;
1135
1210
  navigationEntry.fileName = void 0;
1136
1211
  routePreloads.routes = {
1137
1212
  navigation: {},
@@ -1145,6 +1220,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
1145
1220
  if (!isRouterServerEnvironment) return;
1146
1221
  const manifest = await readClientAssetsManifest(clientOutDir, routePreloads.base);
1147
1222
  resumeEntry.fileName = clientAssetFileName(manifest.entries.resume, manifest.base);
1223
+ prerenderWakeEntry.fileName = manifest.entries.prerenderWake ? clientAssetFileName(manifest.entries.prerenderWake, manifest.base) : void 0;
1148
1224
  navigationEntry.fileName = clientAssetFileName(manifest.entries.navigation, manifest.base);
1149
1225
  routePreloads.routes = manifest.routes;
1150
1226
  routePreloads.persisted = true;
@@ -1155,21 +1231,26 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
1155
1231
  if (this.environment?.config.consumer !== "client") return;
1156
1232
  const resumeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, RESUME_ENTRY_ORIGIN));
1157
1233
  if (resumeChunk) resumeEntry.fileName = resumeChunk.fileName;
1234
+ const prerenderWakeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, PRERENDER_WAKE_ENTRY_ORIGIN));
1235
+ if (prerenderWakeChunk) prerenderWakeEntry.fileName = prerenderWakeChunk.fileName;
1158
1236
  const navigationChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, NAVIGATION_ENTRY_ORIGIN));
1159
1237
  if (navigationChunk) navigationEntry.fileName = navigationChunk.fileName;
1160
1238
  routePreloads.routes = routeModulePreloadsFromBundle({
1161
1239
  base: routePreloads.base,
1162
1240
  bundle,
1163
1241
  navigationChunk,
1242
+ prerenderWakeChunk,
1164
1243
  resumeChunk,
1165
1244
  root: routePreloads.root
1166
1245
  });
1167
1246
  patchRoutePreloadsInBundle(bundle, routePreloads.routes);
1168
1247
  if (productionBuild) {
1169
- if (!resumeEntry.fileName || !navigationEntry.fileName) throw new Error("Markless Router client build did not emit its resume and navigation entries.");
1248
+ const wakeRequired = prerenderWakeChannelEnabled();
1249
+ 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
1250
  pendingClientAssets = createClientAssetsManifest({
1171
1251
  base: routePreloads.base,
1172
1252
  resumeEntry: joinURL(routePreloads.base, resumeEntry.fileName),
1253
+ ...prerenderWakeEntry.fileName ? { prerenderWakeEntry: joinURL(routePreloads.base, prerenderWakeEntry.fileName) } : {},
1173
1254
  navigationEntry: joinURL(routePreloads.base, navigationEntry.fileName),
1174
1255
  routes: routePreloads.routes
1175
1256
  });
@@ -1275,15 +1356,18 @@ function withWatchIgnores(watchOptions, extraIgnored = []) {
1275
1356
  ...watchOptionsObject,
1276
1357
  followSymlinks: watchOptionsObject.followSymlinks ?? false,
1277
1358
  ignored: [
1278
- ...toArray(watchOptionsObject.ignored),
1359
+ ...toWatchIgnores(watchOptionsObject.ignored),
1279
1360
  ...extraIgnored,
1280
1361
  ...DEFAULT_WATCH_IGNORES
1281
1362
  ]
1282
1363
  };
1283
1364
  }
1284
- function toArray(value) {
1365
+ function toWatchIgnores(value) {
1285
1366
  if (value === void 0) return [];
1286
- return Array.isArray(value) ? value : [value];
1367
+ return isPatternList(value) ? [...value] : [value];
1368
+ }
1369
+ function isPatternList(value) {
1370
+ return Array.isArray(value);
1287
1371
  }
1288
1372
  function withRequestFileBuildPlugin(config, root) {
1289
1373
  const configObject = isRecord(config) ? config : {};
@@ -1318,18 +1402,30 @@ function routePreloadState() {
1318
1402
  }
1319
1403
  };
1320
1404
  }
1405
+ let prerenderWakeChannelCaptured = false;
1406
+ function prerenderWakeChannelEnabled() {
1407
+ return prerenderWakeChannelCaptured;
1408
+ }
1321
1409
  function routerClientInput(input, root) {
1322
1410
  const resumeEntryId = scopedVirtualEntryId(RESUME_ENTRY_ID, root);
1411
+ const prerenderWakeEntryId = scopedVirtualEntryId(PRERENDER_WAKE_ENTRY_ID, root);
1323
1412
  const navigationEntryId = scopedVirtualEntryId(NAVIGATION_ENTRY_ID, root);
1324
- if (input === void 0) return [resumeEntryId, navigationEntryId];
1413
+ const wakeEntries = prerenderWakeChannelEnabled() ? [prerenderWakeEntryId] : [];
1414
+ if (input === void 0) return [
1415
+ resumeEntryId,
1416
+ ...wakeEntries,
1417
+ navigationEntryId
1418
+ ];
1325
1419
  if (typeof input === "string" || Array.isArray(input)) return [
1326
1420
  resumeEntryId,
1421
+ ...wakeEntries,
1327
1422
  navigationEntryId,
1328
1423
  ...Array.isArray(input) ? input : [input]
1329
1424
  ];
1330
1425
  if (isRecord(input)) return {
1331
1426
  ...input,
1332
1427
  "markless-router-resume": resumeEntryId,
1428
+ ...prerenderWakeChannelEnabled() ? { "markless-router-prerender-wake": prerenderWakeEntryId } : {},
1333
1429
  "markless-router-navigation": navigationEntryId
1334
1430
  };
1335
1431
  return input;
@@ -1337,7 +1433,7 @@ function routerClientInput(input, root) {
1337
1433
  function isVirtualEntryChunk(chunk, origin) {
1338
1434
  return [chunk.facadeModuleId, ...chunk.moduleIds ?? []].some((id) => id ? decodePath(parseURL(id).pathname).endsWith(origin) : false);
1339
1435
  }
1340
- function virtualModulesPlugin(resumeEntry = resumeEntryState(), navigationEntry = resumeEntryState(), routePreloads = routePreloadState()) {
1436
+ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEntry = resumeEntryState(), navigationEntry = resumeEntryState(), routePreloads = routePreloadState()) {
1341
1437
  let root = "";
1342
1438
  return {
1343
1439
  name: "markless-router:routes",
@@ -1349,7 +1445,7 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), navigationEntry
1349
1445
  filter: { id: PUBLIC_VIRTUAL_MODULE_ID_RE },
1350
1446
  handler(id) {
1351
1447
  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)}`;
1448
+ 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
1449
  const entryFile = virtualEntryFiles[baseId];
1354
1450
  if (!entryFile) return;
1355
1451
  return `${join(VIRTUAL_ENTRY_DIR, entryFile)}${rootScopeQuery(root, id)}`;
@@ -1358,6 +1454,10 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), navigationEntry
1358
1454
  load(id) {
1359
1455
  if (id.startsWith(`\0${SERVER_ENTRY_ID}`)) return serverEntrySource(root);
1360
1456
  if (id.startsWith(`\0${RESUME_ENTRY_PATH_ID}`)) return entryPathSource("resumeEntryPath", resumeEntry, RESUME_ENTRY_ID, root);
1457
+ if (id.startsWith(`\0${PRERENDER_WAKE_ENTRY_PATH_ID}`)) {
1458
+ if (!prerenderWakeChannelEnabled() || !prerenderWakeEntry.fileName) return "export const prerenderWakeEntryPath = undefined;";
1459
+ return entryPathSource("prerenderWakeEntryPath", prerenderWakeEntry, PRERENDER_WAKE_ENTRY_ID, root);
1460
+ }
1361
1461
  if (id.startsWith(`\0${NAVIGATION_ENTRY_PATH_ID}`)) return entryPathSource("navigationEntryPath", navigationEntry, NAVIGATION_ENTRY_ID, root);
1362
1462
  if (id.startsWith(`\0${ROUTE_PRELOADS_ID}`)) return routePreloadsSource(routePreloads, this?.environment?.config.consumer === "client");
1363
1463
  if (id.startsWith(`\0${ROUTER_OPTIONS_ID}`)) return "export const routerMode = \"path\"; // retained for compat; hash paths are always first-class";
@@ -1373,6 +1473,7 @@ function serverEntrySource(root) {
1373
1473
  return [
1374
1474
  `import { createServerEntry } from '@markless/router/vite/runtime/create-server-entry';`,
1375
1475
  `import { resumeEntryPath } from '${RESUME_ENTRY_PATH_ID}${query}';`,
1476
+ `import { prerenderWakeEntryPath } from '${PRERENDER_WAKE_ENTRY_PATH_ID}${query}';`,
1376
1477
  `import { navigationEntryPath } from '${NAVIGATION_ENTRY_PATH_ID}${query}';`,
1377
1478
  `import { routeModulePreloads, routeSsrModulePreloads, routeStylesheets } from '${ROUTE_PRELOADS_ID}${query}';`,
1378
1479
  `import { pageModuleLoaders, routeFileIds } from '${ROUTE_DISCOVERY_ID}${query}';`,
@@ -1380,6 +1481,7 @@ function serverEntrySource(root) {
1380
1481
  `const entry = createServerEntry({`,
1381
1482
  ` dev: import.meta.env.DEV,`,
1382
1483
  ` resumeEntryPath,`,
1484
+ ` prerenderWakeEntryPath,`,
1383
1485
  ` navigationEntryPath,`,
1384
1486
  ` routeModulePreloads,`,
1385
1487
  ` routeSsrModulePreloads,`,
@@ -1475,6 +1577,10 @@ function routeModulePreloadsFromBundle(input) {
1475
1577
  includeChunk(ssrFileNames, chunksByFileName, input.resumeChunk.fileName);
1476
1578
  for (const fileName of routeScopedDynamicImports(input.resumeChunk, routeFile)) includeChunk(ssrFileNames, chunksByFileName, fileName, true);
1477
1579
  }
1580
+ if (input.prerenderWakeChunk) {
1581
+ includeChunk(ssrFileNames, chunksByFileName, input.prerenderWakeChunk.fileName);
1582
+ for (const fileName of routeScopedDynamicImports(input.prerenderWakeChunk, routeFile)) includeChunk(ssrFileNames, chunksByFileName, fileName, true);
1583
+ }
1478
1584
  includeChunk(ssrFileNames, chunksByFileName, routeChunk.fileName, true);
1479
1585
  const routeSourceFiles = sourceFilesForChunks(new Set([...navigationFileNames, ...ssrFileNames]), chunksByFileName);
1480
1586
  for (const [sourceFile, symbolFileNames] of symbolChunks) {
@@ -1486,7 +1592,7 @@ function routeModulePreloadsFromBundle(input) {
1486
1592
  }
1487
1593
  navigation[routeFile] = [...navigationFileNames].map((fileName) => joinURL(input.base, fileName));
1488
1594
  ssr[routeFile] = [...ssrFileNames].map((fileName) => joinURL(input.base, fileName));
1489
- styles[routeFile] = routeStylesheetsForChunk(routeChunk, chunksByFileName, input.base);
1595
+ styles[routeFile] = routeStylesheetsForChunks(routeFileChunks, chunksByFileName, input.base);
1490
1596
  }
1491
1597
  return {
1492
1598
  navigation,
@@ -1494,7 +1600,7 @@ function routeModulePreloadsFromBundle(input) {
1494
1600
  styles
1495
1601
  };
1496
1602
  }
1497
- function routeStylesheetsForChunk(routeChunk, chunksByFileName, base) {
1603
+ function routeStylesheetsForChunks(routeFileChunks, chunksByFileName, base) {
1498
1604
  const styles = /* @__PURE__ */ new Set();
1499
1605
  const visited = /* @__PURE__ */ new Set();
1500
1606
  const visit = (chunk) => {
@@ -1506,7 +1612,7 @@ function routeStylesheetsForChunk(routeChunk, chunksByFileName, base) {
1506
1612
  }
1507
1613
  for (const stylesheet of chunk.viteMetadata?.importedCss ?? []) styles.add(joinURL(base, stylesheet));
1508
1614
  };
1509
- visit(routeChunk);
1615
+ for (const chunk of routeFileChunks) visit(chunk);
1510
1616
  return [...styles];
1511
1617
  }
1512
1618
  function symbolChunksBySourceFile(chunks) {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@markless/router",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
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.1",
68
- "@markless/web": "0.2.1"
67
+ "@markless/bundler": "0.3.0",
68
+ "@markless/web": "0.3.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@markless/analyzer": "0.2.1"
71
+ "@markless/analyzer": "0.3.0"
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,