@markless/router 0.4.0 → 0.5.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.
@@ -1,25 +1,125 @@
1
- import { t as __marklessDebugBootstrapSource } from "../../debug-channel-CSzo-Zj8.js";
2
- import { i as normalizeRouteFileId, n as matchRouteManifest, t as buildRouteManifestFromFileIds } from "../../route-manifest-DPgGIyrS.js";
3
- import { renderToString } from "@markless/web/render-to-string";
4
- import { renderToStream } from "@markless/web/render-to-stream";
1
+ import { i as SCROLL_ATTRIBUTE, n as PREFETCH_ATTRIBUTE, r as REPLACE_ATTRIBUTE, t as LINK_ATTRIBUTE } from "../../link-attributes-nY1Qkmx2.js";
2
+ import { i as ROUTE_SCRIPT_TYPE, n as requestLocals, r as HTML_ATTRIBUTES_SLOT } from "../../request-locals-BFPF6Vdm.js";
3
+ import { a as normalizeRouteFileId, r as matchRouteManifest, t as buildRouteManifestFromFileIds } from "../../route-manifest-Bw_nFohL.js";
4
+ import { a as startFragmentNavigation, i as FRAGMENT_REQUEST_HEADER, n as FRAGMENT_PREFETCH_TTL_MS, o as appendModulePreloads, r as FRAGMENT_REGION_END, s as listenForLinkIntent, t as FRAGMENT_APPEND_END } from "../../fragment-navigation-CIiNnFFc.js";
5
+ import { n as __marklessDebugBootstrapSource } from "../../debug-channel-Di6SppZv.js";
6
+ import { CONTAINER_RETIRE_PROPERTY, renderToString } from "@markless/web/render-to-string";
7
+ import { STREAMED_ARM_SELECTOR, STREAM_ARM_EXECUTOR_GLOBAL, renderToStream } from "@markless/web/render-to-stream";
5
8
  import { normalizeMarklessDevError, renderMarklessDevErrorDocument } from "@markless/bundler/dev-error";
9
+ //#region src/vite/runtime/link-bridge-module.ts
10
+ /** Marks the page's link bridge script: inline in dev, a module stub importing the shared bridge in a build. */
11
+ const LINK_BRIDGE_SCRIPT_ATTRIBUTE = "data-markless-router-link-resumer";
12
+ function linkBridgeModulePath(base, hash) {
13
+ return `${base.replace(/\/?$/, "/")}_markless/links.${hash}.js`;
14
+ }
15
+ //#endregion
16
+ //#region src/vite/runtime/page-http.ts
17
+ const RESPONSE_COMMITTED_CODE = "MARKLESS_ROUTER_RESPONSE_COMMITTED";
18
+ function createPageHttp(request, url) {
19
+ let committed;
20
+ let status;
21
+ let statusText;
22
+ const refuse = (write) => {
23
+ throw new Error(`${RESPONSE_COMMITTED_CODE}: ${write} ran after the page's response started streaming to the browser, so its status and headers were already sent. Set http.response before the first byte goes out: in the page body, before any await that outlives the first flush.`);
24
+ };
25
+ const headers = new PageHeaders((write, unchanged) => {
26
+ if (committed && !unchanged(committed.headers)) refuse(write);
27
+ });
28
+ const response = {
29
+ headers,
30
+ get status() {
31
+ return status;
32
+ },
33
+ set status(value) {
34
+ if (committed && value !== committed.status) refuse(`http.response.status = ${value}`);
35
+ status = value;
36
+ },
37
+ get statusText() {
38
+ return statusText;
39
+ },
40
+ set statusText(value) {
41
+ if (committed && value !== committed.statusText) refuse(`http.response.statusText = ${JSON.stringify(value)}`);
42
+ statusText = value;
43
+ }
44
+ };
45
+ return {
46
+ context: {
47
+ locals: requestLocals(request) ?? {},
48
+ request,
49
+ response,
50
+ url
51
+ },
52
+ commit(fallbackStatus, routerHeaders) {
53
+ const sent = new Headers(routerHeaders);
54
+ for (const [name, value] of headers) if (name !== "set-cookie") sent.set(name, value);
55
+ for (const cookie of headers.getSetCookie()) sent.append("set-cookie", cookie);
56
+ committed = {
57
+ status: status ?? fallbackStatus,
58
+ statusText,
59
+ headers: sent
60
+ };
61
+ return {
62
+ status: committed.status,
63
+ ...statusText === void 0 ? {} : { statusText },
64
+ headers: sent
65
+ };
66
+ }
67
+ };
68
+ }
69
+ var PageHeaders = class extends Headers {
70
+ #guard;
71
+ constructor(guard) {
72
+ super();
73
+ this.#guard = guard;
74
+ }
75
+ set(name, value) {
76
+ this.#guard(`http.response.headers.set(${JSON.stringify(name)})`, (sent) => name.toLowerCase() === "set-cookie" ? sent.getSetCookie().includes(value) : sent.get(name) === value);
77
+ super.set(name, value);
78
+ }
79
+ append(name, value) {
80
+ this.#guard(`http.response.headers.append(${JSON.stringify(name)})`, (sent) => name.toLowerCase() === "set-cookie" ? sent.getSetCookie().includes(value) : (sent.get(name)?.split(", ") ?? []).includes(value));
81
+ if (!headerValues(this, name).includes(value)) super.append(name, value);
82
+ }
83
+ delete(name) {
84
+ this.#guard(`http.response.headers.delete(${JSON.stringify(name)})`, (sent) => !sent.has(name));
85
+ super.delete(name);
86
+ }
87
+ };
88
+ function headerValues(headers, name) {
89
+ return name.toLowerCase() === "set-cookie" ? headers.getSetCookie() : headers.get(name)?.split(", ") ?? [];
90
+ }
91
+ //#endregion
6
92
  //#region src/vite/runtime/create-server-entry.ts
7
93
  const DOCUMENT_CHILDREN_PLACEHOLDER = "__markless_router_document_children__";
8
94
  function createServerEntry(options) {
9
95
  const manifest = buildRouteManifestFromFileIds(options.routeFileIds);
10
96
  assertCurrentRouteAssets(options);
97
+ const fragmentBridge = options.fragmentEntryPath !== void 0 && options.navigation !== "render" ? {
98
+ entry: options.fragmentEntryPath,
99
+ source: options.fragmentNavigationSource ?? startFragmentNavigation.toString(),
100
+ prefetch: options.prefetch !== false,
101
+ pages: manifest.routes.map((route) => route.pathname.replace(/:[^/]+/g, ":"))
102
+ } : void 0;
103
+ const fragmentNavigation = fragmentBridge !== void 0;
104
+ const linkBridges = !options.dev && options.navigationEntryPath ? linkBridgeModules(options.base ?? "/", options.navigationEntryPath, fragmentNavigation || options.prefetch !== false, options.documentNavigation === "document", fragmentBridge) : void 0;
11
105
  async function fetch(request) {
12
106
  const url = new URL(request.url);
107
+ const linkBridge = linkBridges?.find((module) => module.path === url.pathname);
108
+ if (linkBridge) return new Response(linkBridge.source, { headers: {
109
+ "content-type": "text/javascript;charset=utf-8",
110
+ "cache-control": "public, max-age=31536000, s-maxage=31536000, immutable"
111
+ } });
112
+ const fragment = fragmentNavigation && request.headers.get("x-markless-fragment") === "1";
13
113
  if (isNitroApiPathname(url.pathname)) return new Response("Not found", { status: 404 });
14
114
  const match = matchRouteManifest(url.pathname, manifest);
15
- if (!match) return renderStatusPage(url, manifest.statusPages.notFound, 404, "Not found");
115
+ if (!match) return renderStatusPage(url, manifest.statusPages.notFound, 404, "Not found", fragment, request);
16
116
  try {
17
- return await renderPage(url, match.route.file, match.params, 200);
117
+ return await renderPage(url, match.route.file, match.params, 200, fragment, request);
18
118
  } catch (error) {
19
119
  console.error("[markless-router] page render failed:", error);
20
120
  if (options.dev) return renderDevelopmentError(url, error);
21
121
  try {
22
- return await renderStatusPage(url, manifest.statusPages.error, 500, "Internal Server Error");
122
+ return await renderStatusPage(url, manifest.statusPages.error, 500, "Internal Server Error", fragment, request);
23
123
  } catch (errorPageError) {
24
124
  console.error("[markless-router] error page render failed:", errorPageError);
25
125
  return new Response("Internal Server Error", {
@@ -50,15 +150,18 @@ function createServerEntry(options) {
50
150
  });
51
151
  }
52
152
  }
53
- async function renderStatusPage(url, file, status, fallbackText) {
153
+ async function renderStatusPage(url, file, status, fallbackText, fragment, request) {
54
154
  if (!file) return new Response(fallbackText, { status });
55
- return renderPage(url, file, {}, status);
155
+ return renderPage(url, file, {}, status, fragment, request);
56
156
  }
57
- async function renderPage(url, file, params, status) {
157
+ async function renderPage(url, file, params, status, fragment, request) {
58
158
  const loadPageModule = options.pageModuleLoaders[file];
59
159
  if (!loadPageModule) return new Response(`Page module not found: ${file}`, { status: 500 });
160
+ const cookie = request.headers.get("cookie");
60
161
  const pageModule = await loadPageModule();
162
+ const http = createPageHttp(request, url);
61
163
  const pageProps = {
164
+ http: http.context,
62
165
  params,
63
166
  url: {
64
167
  href: url.href,
@@ -69,45 +172,60 @@ function createServerEntry(options) {
69
172
  };
70
173
  const documentModule = options.documentModuleLoader ? await options.documentModuleLoader() : void 0;
71
174
  const headers = { "content-type": "text/html;charset=utf-8" };
175
+ if (fragmentNavigation) headers.vary = FRAGMENT_REQUEST_HEADER;
72
176
  const baseArtifact = pageModule.default;
73
177
  const renderSsr = baseArtifact?.renderSsr ?? pageModule.marklessRenderSsr;
74
178
  if (!renderSsr) {
75
179
  const shell = await renderDocumentShell(documentModule, pageProps, "");
76
180
  const message = `Page module must export an Markless compiled artifact: ${escapeHtml(file)}`;
77
- return new Response(fillDocumentChildren(shell, message), {
78
- status,
79
- headers
80
- });
181
+ return new Response(fillDocumentChildren(shell, message), http.commit(status, headers));
81
182
  }
82
- const pageArtifact = routedPageArtifact(renderSsr, baseArtifact, pageProps, file, options.navigationEntryPath, options.routeStylesheets?.[file]);
83
- const routedArtifact = options.prerenderWakeEntryPath ? {
183
+ const documentLoads = options.documentNavigation === "document";
184
+ const linkIntent = fragmentNavigation || options.prefetch !== false;
185
+ const destinationPreloads = documentLoads || fragmentNavigation ? options.routeSsrModulePreloads : options.routeModulePreloads;
186
+ const pageArtifact = routedPageArtifact(renderSsr, baseArtifact, pageProps, file, options.navigationEntryPath, pageStylesheets(options, file), documentModule?.default?.headInjections ?? [], linkIntent ? (html) => linkIntentPreloads(html, pageProps.url.href, manifest, destinationPreloads, fragmentNavigation) : void 0, documentLoads, fragmentBridge, linkBridges);
187
+ let rendersStorage = false;
188
+ const routedArtifact = {
84
189
  ...pageArtifact,
85
- prerenderWakeModuleUrl: options.prerenderWakeEntryPath
86
- } : pageArtifact;
190
+ ...options.prerenderWakeEntryPath ? { prerenderWakeModuleUrl: options.prerenderWakeEntryPath } : {},
191
+ async renderSsr(renderProps, renderContext) {
192
+ const output = await pageArtifact.renderSsr(renderProps, renderContext);
193
+ if (documentStorageRecords(output.state).length > 0) rendersStorage = true;
194
+ return output;
195
+ }
196
+ };
197
+ const privateToVisitor = () => {
198
+ if (!rendersStorage) return;
199
+ headers["cache-control"] = "private";
200
+ headers.vary = headers.vary ? `${headers.vary}, cookie` : "cookie";
201
+ };
87
202
  const renderOptions = {
203
+ cookie,
88
204
  props: pageProps,
89
205
  resumeModuleUrl: options.resumeEntryPath ?? baseArtifact?.resumeModuleUrl,
90
- modulePreloads: (html) => modulePreloadsForPage(file, html, pageProps.url.href, manifest, options.routeModulePreloads, options.routeSsrModulePreloads)
206
+ modulePreloads: (html) => landingModulePreloads(options.routeSsrModulePreloads?.[file], linkBridges?.[html.includes("href=\"#/") ? 1 : 0].path)
91
207
  };
92
208
  if (options.render === "blocking") {
93
209
  const pageHtml = splitLeadingHeadHtml(await renderToString(routedArtifact, renderOptions));
94
- const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml);
95
- return new Response(fillDocumentChildren(shell, pageHtml.bodyHtml), {
96
- status,
97
- headers
98
- });
210
+ privateToVisitor();
211
+ const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml, options.importMap);
212
+ const html = fragment ? fragmentDocument(shell, pageHtml.bodyHtml, documentLoads) + FRAGMENT_REGION_END : fillDocumentChildren(shell, pageHtml.bodyHtml);
213
+ return new Response(html, http.commit(status, headers));
99
214
  }
100
215
  const stream = await renderToStream(routedArtifact, renderOptions);
216
+ privateToVisitor();
101
217
  const pageHtml = splitLeadingHeadHtml(stream.shell);
102
- const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml);
103
- if (stream.pendingArmCount === 0) return new Response(fillDocumentChildren(shell, pageHtml.bodyHtml), {
104
- status,
105
- headers
106
- });
218
+ const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml, options.importMap);
219
+ if (stream.pendingArmCount === 0) {
220
+ const html = fragment ? fragmentDocument(shell, pageHtml.bodyHtml, documentLoads) + FRAGMENT_REGION_END : fillDocumentChildren(shell, pageHtml.bodyHtml);
221
+ return new Response(html, http.commit(status, headers));
222
+ }
107
223
  const placeholderAt = shell.indexOf(DOCUMENT_CHILDREN_PLACEHOLDER);
108
- const prefix = placeholderAt === -1 ? shell : shell.slice(0, placeholderAt) + pageHtml.bodyHtml;
109
- const suffix = placeholderAt === -1 ? "" : shell.slice(placeholderAt + 37);
224
+ const prefix = fragment ? fragmentDocument(shell, pageHtml.bodyHtml, documentLoads) + FRAGMENT_REGION_END : placeholderAt === -1 ? shell : shell.slice(0, placeholderAt) + pageHtml.bodyHtml;
225
+ const suffix = fragment || placeholderAt === -1 ? "" : shell.slice(placeholderAt + 37);
226
+ const appendEnd = fragment ? FRAGMENT_APPEND_END : "";
110
227
  const encoder = new TextEncoder();
228
+ const sent = http.commit(status, headers);
111
229
  let cancelled = false;
112
230
  let completed = false;
113
231
  const body = new ReadableStream({
@@ -116,7 +234,7 @@ function createServerEntry(options) {
116
234
  try {
117
235
  for await (const chunk of stream.appends()) {
118
236
  if (cancelled) return;
119
- controller.enqueue(encoder.encode(chunk));
237
+ controller.enqueue(encoder.encode(chunk + appendEnd));
120
238
  }
121
239
  } catch (error) {
122
240
  if (cancelled) return;
@@ -126,7 +244,7 @@ function createServerEntry(options) {
126
244
  return;
127
245
  }
128
246
  if (cancelled) return;
129
- controller.enqueue(encoder.encode(suffix));
247
+ if (suffix) controller.enqueue(encoder.encode(suffix));
130
248
  controller.close();
131
249
  completed = true;
132
250
  },
@@ -136,10 +254,7 @@ function createServerEntry(options) {
136
254
  stream.cancel(reason);
137
255
  }
138
256
  });
139
- return new Response(body, {
140
- status,
141
- headers
142
- });
257
+ return new Response(body, sent);
143
258
  }
144
259
  return { fetch };
145
260
  }
@@ -161,15 +276,35 @@ function assertCurrentRouteAssets(options) {
161
276
  if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error(`Markless Router ${label} client-asset routes are stale; expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}. Rebuild the client environment before the server environment.`);
162
277
  }
163
278
  }
164
- function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigationEntryPath, stylesheetHrefs) {
165
- const headInjections = [...baseArtifact?.headInjections ?? [], ...(stylesheetHrefs ?? []).map((href) => ({
166
- tag: "link",
167
- location: "head",
168
- attributes: {
169
- rel: "stylesheet",
170
- href
171
- }
172
- }))];
279
+ function dedupeHeadLinks(injections) {
280
+ const seen = /* @__PURE__ */ new Set();
281
+ return injections.filter((injection) => {
282
+ const href = injection.tag === "link" ? injection.attributes?.href : void 0;
283
+ if (typeof href !== "string") return true;
284
+ if (seen.has(href)) return false;
285
+ seen.add(href);
286
+ return true;
287
+ });
288
+ }
289
+ function pageStylesheets(options, file) {
290
+ const documentHrefs = options.documentStylesheets ?? [];
291
+ const routeHrefs = options.routeStylesheets?.[file];
292
+ if (documentHrefs.length === 0) return routeHrefs;
293
+ return [.../* @__PURE__ */ new Set([...documentHrefs, ...routeHrefs ?? []])];
294
+ }
295
+ function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigationEntryPath, stylesheetHrefs, documentHeadInjections = [], intentPreloads, documentLoads = false, fragment, linkBridgeModules) {
296
+ const headInjections = dedupeHeadLinks([
297
+ ...documentHeadInjections,
298
+ ...baseArtifact?.headInjections ?? [],
299
+ ...(stylesheetHrefs ?? []).map((href) => ({
300
+ tag: "link",
301
+ location: "head",
302
+ attributes: {
303
+ rel: "stylesheet",
304
+ href
305
+ }
306
+ }))
307
+ ]);
173
308
  return {
174
309
  resumeModuleUrl: baseArtifact?.resumeModuleUrl,
175
310
  inlineResumerSources: baseArtifact?.inlineResumerSources,
@@ -179,7 +314,7 @@ function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigation
179
314
  const output = await renderSsr(renderProps ?? pageProps, renderContext);
180
315
  if (!output) return { html: "" };
181
316
  const routeScript = output.state || output.view ? renderRouteScript(file) : "";
182
- const linkBridge = navigationEntryPath ? renderLinkBridgeScript(navigationEntryPath) : "";
317
+ const linkBridge = navigationEntryPath ? renderLinkBridgeScript(navigationEntryPath, intentPreloads?.(output.html), documentLoads, fragment, output.html.includes("href=\"#/"), linkBridgeModules) : "";
183
318
  const stateWithProps = withPagePropsCell(output.state, pageProps);
184
319
  return routeScript || linkBridge || stateWithProps !== output.state ? {
185
320
  ...output,
@@ -191,6 +326,7 @@ function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigation
191
326
  }
192
327
  function withPagePropsCell(state, props) {
193
328
  if (!state || typeof state !== "object") return state;
329
+ const { http: _serverOnly, ...browserProps } = props;
194
330
  const cells = state.cells;
195
331
  if (!Array.isArray(cells) || cells.some((cell) => cell?.graphNodeId === "prop:props")) return state;
196
332
  return {
@@ -199,25 +335,50 @@ function withPagePropsCell(state, props) {
199
335
  graphNodeId: "prop:props",
200
336
  name: "props",
201
337
  valueKind: "object",
202
- directValue: props
338
+ directValue: browserProps
203
339
  }]
204
340
  };
205
341
  }
206
- function modulePreloadsForPage(file, html, baseHref, manifest, routeModulePreloads, routeSsrModulePreloads) {
342
+ function landingModulePreloads(routePreloads, linkBridgePath) {
207
343
  const preloads = [];
208
- const seen = /* @__PURE__ */ new Set();
209
- addModulePreloads(preloads, seen, routeSsrModulePreloads?.[file]);
210
- if (!routeModulePreloads || !html.includes("data-markless-router-link")) return preloads.length > 0 ? preloads : void 0;
211
- for (const href of routerLinkHrefs(html)) {
212
- const url = parseSameOriginUrl(href, baseHref);
213
- const match = url && matchRouteManifest(url.pathname, manifest);
214
- addModulePreloads(preloads, seen, match && routeModulePreloads[match.route.file]);
215
- }
344
+ addModulePreloads(preloads, /* @__PURE__ */ new Set(), routePreloads);
345
+ if (linkBridgePath) preloads.push(linkBridgePath);
216
346
  return preloads.length > 0 ? preloads : void 0;
217
347
  }
218
348
  function routerLinkHrefs(html) {
219
349
  return [...html.matchAll(/<a\b(?=[^>]*\bdata-markless-router-link(?:[\s=>]|$))(?=[^>]*\bhref="([^"]*)")[^>]*>/g)].map((match) => unescapeHtmlAttribute(match[1] ?? ""));
220
350
  }
351
+ function anchorHrefs(html) {
352
+ return [...html.matchAll(/<a\b(?=[^>]*\bhref="([^"]*)")[^>]*>/g)].map((match) => unescapeHtmlAttribute(match[1] ?? ""));
353
+ }
354
+ function linkIntentPreloads(html, baseHref, manifest, routes, plainAnchors = false) {
355
+ const destinations = Object.create(null);
356
+ for (const href of plainAnchors ? anchorHrefs(html) : routerLinkHrefs(html)) {
357
+ const url = parseSameOriginUrl(href, baseHref);
358
+ const match = url && matchRouteManifest(url.pathname, manifest);
359
+ const preloads = match && routes?.[match.route.file];
360
+ if (url && preloads?.length) destinations[url.pathname] = preloads;
361
+ }
362
+ return destinations;
363
+ }
364
+ function indexedPreloads(intentPreloads) {
365
+ const preloads = [];
366
+ const indexes = /* @__PURE__ */ new Map();
367
+ const destinations = Object.create(null);
368
+ for (const [pathname, list] of Object.entries(intentPreloads)) destinations[pathname] = list.map((preload) => {
369
+ const key = JSON.stringify(preload);
370
+ let index = indexes.get(key);
371
+ if (index === void 0) {
372
+ index = preloads.push(preload) - 1;
373
+ indexes.set(key, index);
374
+ }
375
+ return index;
376
+ });
377
+ return {
378
+ preloads,
379
+ destinations
380
+ };
381
+ }
221
382
  function parseSameOriginUrl(href, base) {
222
383
  try {
223
384
  const current = new URL(base);
@@ -249,16 +410,17 @@ function splitLeadingHeadHtml(html) {
249
410
  headHtml: html.slice(0, containerStart)
250
411
  };
251
412
  }
252
- async function renderDocumentShell(documentModule, pageProps, headHtml) {
253
- const attributes = htmlAttributes(documentModule, pageProps);
413
+ async function renderDocumentShell(documentModule, pageProps, headHtml, importMap = "") {
414
+ const written = {};
254
415
  const documentHtml = await renderDocumentModule(documentModule, {
255
416
  ...pageProps,
256
417
  children: DOCUMENT_CHILDREN_PLACEHOLDER
257
- });
418
+ }, written);
419
+ const attributes = htmlAttributes(documentModule, pageProps, written);
258
420
  if (documentHtml !== void 0) return [
259
421
  "<!doctype html>",
260
422
  `<html${renderAttributes(attributes)}>`,
261
- insertHeadHtml(documentHtml, headHtml),
423
+ insertImportMap(insertHeadHtml(documentHtml, headHtml), importMap),
262
424
  "</html>"
263
425
  ].join("");
264
426
  return [
@@ -266,6 +428,7 @@ async function renderDocumentShell(documentModule, pageProps, headHtml) {
266
428
  `<html${renderAttributes(attributes)}>`,
267
429
  "<head>",
268
430
  "<meta charset=\"utf-8\">",
431
+ importMap,
269
432
  "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
270
433
  headHtml,
271
434
  "</head>",
@@ -280,8 +443,14 @@ function fillDocumentChildren(documentShell, children) {
280
443
  if (at === -1) return documentShell;
281
444
  return documentShell.slice(0, at) + children + documentShell.slice(at + 37);
282
445
  }
283
- async function renderDocumentModule(documentModule, props) {
284
- const output = await documentModule?.default?.renderSsr?.(props);
446
+ function fragmentDocument(shell, bodyHtml, documentLoads) {
447
+ const bodyAt = documentLoads ? -1 : shell.search(/<body[\s>]/i);
448
+ const bodyOpenEnd = bodyAt === -1 ? -1 : shell.indexOf(">", bodyAt);
449
+ if (bodyOpenEnd === -1) return fillDocumentChildren(shell, bodyHtml);
450
+ return `${shell.slice(0, bodyOpenEnd + 1)}${bodyHtml}</body></html>`;
451
+ }
452
+ async function renderDocumentModule(documentModule, props, htmlAttributes) {
453
+ const output = await documentModule?.default?.renderSsr?.(props, { [HTML_ATTRIBUTES_SLOT]: htmlAttributes });
285
454
  assertDocumentStorageFree(output);
286
455
  return output?.html;
287
456
  }
@@ -303,31 +472,28 @@ function insertHeadHtml(documentHtml, headHtml) {
303
472
  return `${documentHtml.slice(0, headEnd)}${headHtml}${documentHtml.slice(headEnd)}`;
304
473
  }
305
474
  function renderRouteScript(file) {
306
- return `<script type="@markless/core/route">${escapeScriptJson({ file })}<\/script>`;
475
+ return `<script type="${ROUTE_SCRIPT_TYPE}">${escapeScriptJson({ file })}<\/script>`;
307
476
  }
308
- function renderLinkBridgeScript(resumeEntryPath) {
309
- const debugBootstrap = typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__ ? `<script data-markless-router-debug-bootstrap>${escapeInlineScript(`(() => {
310
- const d = document;
311
- const s = d.currentScript;
312
- const r = s && s.closest('[data-async-container]');
313
- if (!r) return;
314
- try { globalThis[Symbol.for('markless.debug.channel.v1.bootstrap')] = ${__marklessDebugBootstrapSource()}(r, 'ssr-inline', false); } catch {}
315
- })();`)}<\/script>` : "";
316
- const debugRegistration = typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__ ? `
317
- try {
318
- const key = Symbol.for('markless.debug.channel.v1.bootstrap');
319
- const md = globalThis[key];
320
- if (md) { md.router('ssr-link-bridge'); md.activate(); }
321
- } catch {}` : "";
322
- return `${debugBootstrap}<script data-markless-router-link-resumer>${escapeInlineScript(`(() => {
323
- const d = document;
324
- const s = d.currentScript;
325
- const r = s && s.closest('[data-async-container]');
326
- if (!r || r.__marklessRouterLinkResumerStarted) return;
327
- r.__marklessRouterLinkResumerStarted = true;
328
- const linkAttr = 'data-markless-router-link';
329
- const replaceAttr = 'data-markless-router-replace';
330
- const scrollAttr = 'data-markless-router-scroll';
477
+ function linkBridgeBody(resumeEntryPath, linkIntent, documentLoads, fragment, page) {
478
+ const inline = !("module" in page);
479
+ const fragmentNavigation = fragment !== void 0;
480
+ const fragmentSource = fragment ? `
481
+ (${fragment.source})(d,()=>(${escapeScriptJson(fragmentConfig(fragment, documentLoads))}),${inline ? escapeScriptJson(page.intentPreloads ?? {}) : "r"},(href)=>import(href));` : "";
482
+ const indexed = !fragmentNavigation && linkIntent ? inline ? page.intentPreloads && indexedPreloads(page.intentPreloads) : {
483
+ preloads: "preloads",
484
+ destinations: "destinations"
485
+ } : void 0;
486
+ const intentSource = indexed ? `
487
+ const preloads = ${inline ? escapeScriptJson(indexed.preloads) : "p"};
488
+ const destinations = ${inline ? escapeScriptJson(indexed.destinations) : "q"};
489
+ const destination = (url) => destinations[url.pathname]?.map((index) => preloads[index]);
490
+ const append = (${appendModulePreloads.toString()});
491
+ (${listenForLinkIntent.toString()})(d, linkAttr, (url) => {
492
+ append(d, destination(url));
493
+ }, prefetchAttr);` : "";
494
+ const hashClickSource = fragment && !page.hashLinks ? "" : `
495
+ if (d.__marklessRouterHashClickStarted) return;
496
+ d.__marklessRouterHashClickStarted = true;
331
497
  const anchorFrom = (event) => {
332
498
  const target = event.composedPath && event.composedPath()[0] || event.target;
333
499
  return target && target.closest ? target.closest('a[href]') : target && target.parentElement && target.parentElement.closest ? target.parentElement.closest('a[href]') : null;
@@ -340,10 +506,7 @@ function renderLinkBridgeScript(resumeEntryPath) {
340
506
  return null;
341
507
  }
342
508
  };
343
- if (location.hash && location.hash.startsWith('#/')) {
344
- import(${JSON.stringify(resumeEntryPath)}).catch(() => {});
345
- }
346
- r.addEventListener('click', async (event) => {
509
+ d.addEventListener('click', async (event) => {
347
510
  if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return;
348
511
  const anchor = anchorFrom(event);
349
512
  if (!anchor || !anchor.hasAttribute(linkAttr) || anchor.hasAttribute('download')) return;
@@ -351,7 +514,7 @@ function renderLinkBridgeScript(resumeEntryPath) {
351
514
  if (target && target !== '_self') return;
352
515
  if (anchor.relList && anchor.relList.contains('external')) return;
353
516
  const url = sameOrigin(anchor.href);
354
- if (!url) return;
517
+ if (!url${documentLoads || fragmentNavigation ? " || !url.hash.startsWith('#/')" : ""}) return;
355
518
  event.preventDefault();
356
519
  try {
357
520
  const mod = await import(${JSON.stringify(resumeEntryPath)});
@@ -369,11 +532,95 @@ function renderLinkBridgeScript(resumeEntryPath) {
369
532
  setTimeout(() => { throw error; });
370
533
  location.assign(url.href);
371
534
  }
372
- }, true);${debugRegistration}
535
+ }, true);`;
536
+ const debugRegistration = typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__ ? `
537
+ try {
538
+ const key = Symbol.for('markless.debug.channel.v1.bootstrap');
539
+ const md = globalThis[key];
540
+ if (md) { md.router('ssr-link-bridge'); md.activate(); }
541
+ } catch {}` : "";
542
+ return `${fragmentSource}${fragment && !hashClickSource ? "" : `
543
+ if(d.__marklessRouterLinkResumerStarted${fragment ? "&&d.__marklessRouterHashClickStarted" : ""})return;d.__marklessRouterLinkResumerStarted=true;`}${intentSource || hashClickSource ? `
544
+ const linkAttr = ${JSON.stringify(LINK_ATTRIBUTE)};` : ""}${intentSource ? `
545
+ const prefetchAttr = ${JSON.stringify(PREFETCH_ATTRIBUTE)};` : ""}${hashClickSource ? `
546
+ const replaceAttr = ${JSON.stringify(REPLACE_ATTRIBUTE)};
547
+ const scrollAttr = ${JSON.stringify(SCROLL_ATTRIBUTE)};` : ""}${intentSource}
548
+ if(location.hash&&location.hash.startsWith('#/'))import(${JSON.stringify(resumeEntryPath)}).catch(()=>{});${hashClickSource}${debugRegistration}`;
549
+ }
550
+ function linkBridgeModules(base, resumeEntryPath, linkIntent, documentLoads, fragment) {
551
+ const variant = (hashLinks) => {
552
+ const source = `export default(r,p,q)=>{const d=document;${linkBridgeBody(resumeEntryPath, linkIntent, documentLoads, fragment, {
553
+ module: true,
554
+ hashLinks
555
+ })}\n};`;
556
+ return {
557
+ path: linkBridgeModulePath(base, contentHash(source)),
558
+ source
559
+ };
560
+ };
561
+ const withoutHashLinks = variant(false);
562
+ return [withoutHashLinks, fragment ? variant(true) : withoutHashLinks];
563
+ }
564
+ function contentHash(text) {
565
+ let hash = 2166136261;
566
+ for (let at = 0; at < text.length; at++) hash = Math.imul(hash ^ text.charCodeAt(at), 16777619);
567
+ return (hash >>> 0).toString(36);
568
+ }
569
+ function renderLinkBridgeScript(resumeEntryPath, intentPreloads, documentLoads = false, fragment, hashLinks = false, modules) {
570
+ const debugBootstrap = typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__ ? `<script data-markless-router-debug-bootstrap>${escapeInlineScript(`(() => {
571
+ const d = document;
572
+ const s = d.currentScript;
573
+ const r = s && s.closest('[data-async-container]');
574
+ if (!r) return;
575
+ try { globalThis[Symbol.for('markless.debug.channel.v1.bootstrap')] = ${__marklessDebugBootstrapSource()}(r, 'ssr-inline', false); } catch {}
576
+ })();`)}<\/script>` : "";
577
+ const module = modules?.[hashLinks ? 1 : 0];
578
+ if (module) {
579
+ const indexed = !fragment && intentPreloads ? indexedPreloads(intentPreloads) : void 0;
580
+ const args = fragment ? [escapeScriptJson(intentPreloads ?? {})] : indexed ? [
581
+ "0",
582
+ escapeScriptJson(indexed.preloads),
583
+ escapeScriptJson(indexed.destinations)
584
+ ] : [];
585
+ return `${debugBootstrap}<script type="module" ${LINK_BRIDGE_SCRIPT_ATTRIBUTE}>${escapeInlineScript(`import b from${JSON.stringify(module.path)};b(${args.join(",")})`)}<\/script>`;
586
+ }
587
+ const body = linkBridgeBody(resumeEntryPath, intentPreloads !== void 0, documentLoads, fragment, {
588
+ intentPreloads,
589
+ hashLinks
590
+ });
591
+ return `${debugBootstrap}<script ${LINK_BRIDGE_SCRIPT_ATTRIBUTE}>${escapeInlineScript(`(()=>{const d=document,s=d.currentScript,r=s&&s.closest('[data-async-container]');if(!r)return;${body}
373
592
  })();`)}<\/script>`;
374
593
  }
375
- function htmlAttributes(documentModule, pageProps) {
376
- const attributes = documentModule?.__marklessRouterHtmlAttributes?.(pageProps) ?? { lang: "en" };
594
+ function fragmentConfig(fragment, documentLoads) {
595
+ return {
596
+ entry: fragment.entry,
597
+ linkAttribute: LINK_ATTRIBUTE,
598
+ prefetchAttribute: PREFETCH_ATTRIBUTE,
599
+ replaceAttribute: REPLACE_ATTRIBUTE,
600
+ scrollAttribute: SCROLL_ATTRIBUTE,
601
+ routeScriptType: ROUTE_SCRIPT_TYPE,
602
+ retireProperty: CONTAINER_RETIRE_PROPERTY,
603
+ streamExecutorGlobal: STREAM_ARM_EXECUTOR_GLOBAL,
604
+ streamedArmSelector: STREAMED_ARM_SELECTOR,
605
+ requestHeader: FRAGMENT_REQUEST_HEADER,
606
+ regionEndMarker: FRAGMENT_REGION_END,
607
+ appendMarker: FRAGMENT_APPEND_END,
608
+ documentLoads,
609
+ hoverDwellMs: 20,
610
+ prefetchTtlMs: FRAGMENT_PREFETCH_TTL_MS,
611
+ prefetchConnections: 2,
612
+ endpointPrefixes: [API_PATHNAME_PREFIX],
613
+ idleFragments: 2,
614
+ idleWithoutConnectionInfo: true,
615
+ prefetch: fragment.prefetch,
616
+ pages: fragment.pages
617
+ };
618
+ }
619
+ function htmlAttributes(documentModule, pageProps, written) {
620
+ const attributes = documentModule?.__marklessRouterHtmlAttributes?.(pageProps) ?? {
621
+ lang: "en",
622
+ ...written
623
+ };
377
624
  const normalized = {};
378
625
  for (const [name, value] of Object.entries(attributes)) {
379
626
  if (value === false || value === null || value === void 0) continue;
@@ -393,8 +640,19 @@ function escapeScriptJson(value) {
393
640
  function escapeInlineScript(value) {
394
641
  return value.replace(/<\/script/gi, "<\\/script");
395
642
  }
643
+ const API_PATHNAME_PREFIX = "/api";
396
644
  function isNitroApiPathname(pathname) {
397
- return pathname === "/api" || pathname.startsWith("/api/");
645
+ return pathname === API_PATHNAME_PREFIX || pathname.startsWith(`${API_PATHNAME_PREFIX}/`);
646
+ }
647
+ function insertImportMap(html, importMap) {
648
+ if (!importMap) return html;
649
+ const head = /<head(?:\s[^>]*)?>/i.exec(html);
650
+ if (!head) return `${importMap}${html}`;
651
+ const afterHead = head.index + head[0].length;
652
+ const headEnd = html.indexOf("</head>", afterHead);
653
+ const charset = /<meta\s[^>]*charset[^>]*>(?:<\/meta>)?/i.exec(html.slice(afterHead));
654
+ const at = charset && (headEnd === -1 || afterHead + charset.index < headEnd) ? afterHead + charset.index + charset[0].length : afterHead;
655
+ return `${html.slice(0, at)}${importMap}${html.slice(at)}`;
398
656
  }
399
657
  //#endregion
400
658
  export { createServerEntry };