@markless/router 0.8.0 → 0.10.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,9 +1,10 @@
1
- import { a as ROUTE_SCRIPT_TYPE, c as servedHtmlAttributesScript, d as REPLACE_ATTRIBUTE, f as SCROLL_ATTRIBUTE, i as HTML_ATTRIBUTES_SLOT, l as LINK_ATTRIBUTE, n as requestLocals, o as SERVED_HTML_ATTRIBUTES_PROPERTY, r as takeMiddlewareHeaders, u as PREFETCH_ATTRIBUTE } from "./request-locals-BDAhNpEa.js";
1
+ import { a as takeMiddlewareHeaders, c as SERVED_BODY_ATTRIBUTES_PROPERTY, d as servedHtmlAttributesScript, f as LINK_ATTRIBUTE, h as SCROLL_ATTRIBUTE, i as requestLocals, l as SERVED_HTML_ATTRIBUTES_PROPERTY, m as REPLACE_ATTRIBUTE, n as MARKLESS_NAV_SETTLE_DEADLINE_MS, o as HTML_ATTRIBUTES_SLOT, p as PREFETCH_ATTRIBUTE, s as ROUTE_SCRIPT_TYPE } from "./navigation-timing-CzH6VaBR.js";
2
2
  import { i as matchRouteManifest, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId } from "./route-manifest-DmoxXanW.js";
3
3
  import { n as __marklessDebugBootstrapSource } from "./debug-channel-Di6SppZv.js";
4
- import { CONTAINER_ATTRIBUTE, PROTOCOL_CONTAINER_END_COMMENT, PROTOCOL_CONTAINER_START_COMMENT, PROTOCOL_LOCATOR_PATHS_ATTRIBUTE } from "@markless/web/fns/container-region";
4
+ import { CONTAINER_ATTRIBUTE, PROTOCOL_CONTAINER_END_COMMENT, PROTOCOL_CONTAINER_START_COMMENT, PROTOCOL_LOCATOR_PATHS_ATTRIBUTE, PROTOCOL_STREAM_OPEN_PROPERTY } from "@markless/web/fns/container-region";
5
5
  import { CONTAINER_RETIRE_PROPERTY, renderToString, splitSsrContainer } from "@markless/web/render-to-string";
6
6
  import { STREAMED_ARM_SELECTOR, STREAM_ARM_EXECUTOR_GLOBAL, renderToStream } from "@markless/web/render-to-stream";
7
+ import { PROTOCOL_KEEP_ATTRIBUTE, PROTOCOL_OWN_ATTRIBUTE, installKeepParts } from "@markless/web/fns/keep-parts";
7
8
  import { normalizeMarklessDevError, renderMarklessDevErrorDocument } from "@markless/bundler/dev-error";
8
9
  //#region src/vite/link-intent.ts
9
10
  function listenForLinkIntent(root, linkAttribute, preload, prefetchAttribute) {
@@ -55,13 +56,67 @@ function documentHandlersMessage(what) {
55
56
  }
56
57
  //#endregion
57
58
  //#region src/vite/fragment-navigation.ts
58
- function startFragmentNavigation(d, linkAttributeOf, routes, importModule, loadRun, intentOf) {
59
+ function startFragmentNavigation(d, linkAttributeOf, tableAttribute, importModule, loadRun, intentOf) {
59
60
  const w = d.defaultView;
60
61
  if (!w) return;
61
62
  const host = d;
62
- const known = host.__marklessFragmentNav?.routes ?? host.__marklessFragmentWake;
63
- if (known) return void Object.assign(known, routes);
64
- host.__marklessFragmentWake = routes;
63
+ if (host.__marklessFragmentTables) return host.__marklessFragmentTables();
64
+ const read = /* @__PURE__ */ new WeakSet();
65
+ const table = {};
66
+ let unread = true;
67
+ host.__marklessFragmentTables = () => void (unread = true);
68
+ const fill = () => {
69
+ if (!unread) return table;
70
+ unread = false;
71
+ for (const script of d.querySelectorAll(`script[${tableAttribute}]`)) {
72
+ if (read.has(script)) continue;
73
+ read.add(script);
74
+ const [preloads, destinations] = JSON.parse(script.textContent);
75
+ for (const path in destinations) table[path] = destinations[path].map((at) => preloads[at]);
76
+ }
77
+ return table;
78
+ };
79
+ const routes = new Proxy(table, {
80
+ get: (_, key) => fill()[key],
81
+ has: (_, key) => key in fill(),
82
+ ownKeys: () => Reflect.ownKeys(fill()),
83
+ getOwnPropertyDescriptor: (_, key) => Reflect.getOwnPropertyDescriptor(fill(), key)
84
+ });
85
+ const landed = new Promise((done) => {
86
+ const start = () => {
87
+ const pending = [...d.head.querySelectorAll("link[rel=\"modulepreload\"]")].filter((link) => !w.performance.getEntriesByName(link.href, "resource").length);
88
+ let left = pending.length;
89
+ if (!left) return done();
90
+ const arrived = () => void (--left || done());
91
+ for (const link of pending) {
92
+ link.addEventListener("load", arrived, { once: true });
93
+ link.addEventListener("error", arrived, { once: true });
94
+ }
95
+ };
96
+ if (d.readyState === "complete") start();
97
+ else w.addEventListener("load", start, { once: true });
98
+ });
99
+ host.__marklessFragmentIdle = landed.then(() => {
100
+ const intent = intentOf();
101
+ if (!intent) return false;
102
+ const connection = w.navigator.connection;
103
+ let bytes = 0;
104
+ const spans = [];
105
+ for (const entry of [...w.performance.getEntriesByType("navigation"), ...w.performance.getEntriesByType("resource")]) {
106
+ if (!(entry.transferSize > 0)) continue;
107
+ bytes += entry.transferSize;
108
+ spans.push([entry.responseStart, entry.responseEnd]);
109
+ }
110
+ let busy = 0;
111
+ let reach = -Infinity;
112
+ for (const [from, to] of spans.sort((a, b) => a[0] - b[0])) {
113
+ busy += Math.max(0, to - Math.max(from, reach));
114
+ reach = Math.max(reach, to);
115
+ }
116
+ if (bytes && (busy === 0 || bytes / busy * 1e3 >= intent.idleBytesPerSecond)) return true;
117
+ if (bytes >= intent.idleMeasuredBytes) return false;
118
+ return connection?.effectiveType === "4g" && connection.saveData !== true;
119
+ });
65
120
  const scrolled = /* @__PURE__ */ new Set();
66
121
  const arriving = /* @__PURE__ */ new Map();
67
122
  let running;
@@ -71,9 +126,14 @@ function startFragmentNavigation(d, linkAttributeOf, routes, importModule, loadR
71
126
  const href = new URL(typeof item === "string" ? item : item.href, d.baseURI).href;
72
127
  if (arriving.has(href)) return;
73
128
  const link = d.createElement("link");
74
- link.rel = "modulepreload";
129
+ if (typeof item !== "string" && "as" in item) {
130
+ link.rel = "preload";
131
+ link.as = item.as;
132
+ } else {
133
+ link.rel = "modulepreload";
134
+ link.crossOrigin = "anonymous";
135
+ }
75
136
  link.href = href;
76
- link.crossOrigin = "anonymous";
77
137
  arriving.set(href, new Promise((done) => link.onload = link.onerror = done));
78
138
  d.head.appendChild(link);
79
139
  };
@@ -114,7 +174,6 @@ function startFragmentNavigation(d, linkAttributeOf, routes, importModule, loadR
114
174
  };
115
175
  const wake = (event) => {
116
176
  starting ??= loadRun().then((run) => {
117
- delete host.__marklessFragmentWake;
118
177
  return running = run(d, routes, importModule, scrolled, arriving, pressed);
119
178
  });
120
179
  starting.then((api) => api.replay(event), () => starting = void 0);
@@ -167,11 +226,7 @@ function startFragmentNavigation(d, linkAttributeOf, routes, importModule, loadR
167
226
  event.preventDefault();
168
227
  wake().then((api) => api.go(anchor) || w.location.assign(url.href), () => w.location.assign(url.href));
169
228
  };
170
- if (Object.keys(routes).length) {
171
- const idle = () => w.requestIdleCallback ? w.requestIdleCallback(() => wake(), { timeout: 3e3 }) : w.setTimeout(() => wake(), 500);
172
- if (d.readyState === "complete") idle();
173
- else w.addEventListener("load", idle, { once: true });
174
- }
229
+ if (d.querySelector(`script[${tableAttribute}]`)) landed.then(() => w.requestIdleCallback ? w.requestIdleCallback(() => wake(), { timeout: 3e3 }) : w.setTimeout(() => wake(), 500));
175
230
  }
176
231
  function runFragmentNavigation(d, config, routes, importModule, scrolled, arriving, early) {
177
232
  const w = d.defaultView;
@@ -179,6 +234,7 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
179
234
  const location = w.location;
180
235
  const connection = w.navigator.connection;
181
236
  const pageKey = (url) => url.pathname + url.search;
237
+ const fetched = /* @__PURE__ */ new Set();
182
238
  let module;
183
239
  let loaded;
184
240
  const load = () => {
@@ -189,23 +245,38 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
189
245
  return module;
190
246
  };
191
247
  const underEndpoints = (pathname) => config.endpointPrefixes.some((prefix) => (pathname + "/").startsWith(prefix + "/"));
192
- const pages = config.pages.map((page) => [underEndpoints(page), page.split("/").filter(Boolean)]);
248
+ const decoded = (part) => {
249
+ try {
250
+ return decodeURIComponent(part);
251
+ } catch {
252
+ return part;
253
+ }
254
+ };
255
+ const staticPages = /* @__PURE__ */ new Map();
256
+ const patterns = [];
257
+ for (const page of config.pages) {
258
+ const segments = page.split("/").filter(Boolean);
259
+ const under = underEndpoints(page);
260
+ if (segments.some((segment) => segment === "**" || segment === ":")) patterns.push([under, segments]);
261
+ else {
262
+ const key = JSON.stringify(segments);
263
+ staticPages.set(key, [...staticPages.get(key) ?? [], under]);
264
+ }
265
+ }
266
+ const known = /* @__PURE__ */ new Map();
193
267
  const isPage = (pathname) => {
268
+ const cached = known.get(pathname);
269
+ if (cached !== void 0) return cached;
194
270
  const endpoint = underEndpoints(pathname);
195
- const parts = pathname.split("/").filter(Boolean);
196
- return pages.some(([under, page]) => {
271
+ const parts = pathname.split("/").filter(Boolean).map(decoded);
272
+ const answer = !!staticPages.get(JSON.stringify(parts))?.some((under) => !endpoint || under) || patterns.some(([under, page]) => {
197
273
  if (endpoint && !under) return false;
198
274
  const rest = page.indexOf("**");
199
275
  if (rest === -1 ? page.length !== parts.length : parts.length <= rest) return false;
200
- return page.every((segment, index) => {
201
- if (segment === "**" || segment === ":") return true;
202
- try {
203
- return segment === decodeURIComponent(parts[index]);
204
- } catch {
205
- return segment === parts[index];
206
- }
207
- });
276
+ return page.every((segment, index) => segment === "**" || segment === ":" || segment === parts[index]);
208
277
  });
278
+ known.set(pathname, answer);
279
+ return answer;
209
280
  };
210
281
  const eligible = (anchor, forClick = false) => {
211
282
  if (!anchor || anchor.localName !== "a") return;
@@ -241,9 +312,14 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
241
312
  if (present.has(href) || arriving.has(href)) continue;
242
313
  present.add(href);
243
314
  const link = d.createElement("link");
244
- link.rel = "modulepreload";
315
+ if ("as" in entry) {
316
+ link.rel = "preload";
317
+ link.as = entry.as;
318
+ } else {
319
+ link.rel = "modulepreload";
320
+ link.crossOrigin = entry.crossOrigin ?? "anonymous";
321
+ }
245
322
  link.href = href;
246
- link.crossOrigin = entry.crossOrigin ?? "anonymous";
247
323
  if (low) link.fetchPriority = "low";
248
324
  arriving.set(href, new Promise((done) => {
249
325
  link.addEventListener("load", done, { once: true });
@@ -252,13 +328,45 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
252
328
  d.head.appendChild(link);
253
329
  }
254
330
  };
331
+ const parseAsLoaded = (html) => {
332
+ const raw = [];
333
+ const emptied = html.replace(/<!--[\s\S]*?-->|<(script|style|textarea|title)\b[^>]*>[\s\S]*?<\/\1\s*>|<noscript\b([^>]*)>([\s\S]*?)<\/noscript\s*>/gi, (match, _rawText, attributes, content) => {
334
+ if (content === void 0) return match;
335
+ raw.push(content);
336
+ return `<noscript${attributes ?? ""}></noscript>`;
337
+ });
338
+ if (!raw.length) return new DOMParser().parseFromString(html, "text/html");
339
+ const doc = new DOMParser().parseFromString(emptied, "text/html");
340
+ const tags = doc.getElementsByTagName("noscript");
341
+ if (tags.length !== raw.length) return new DOMParser().parseFromString(html, "text/html");
342
+ for (let index = 0; index < tags.length; index++) tags[index].textContent = raw[index];
343
+ return doc;
344
+ };
345
+ const preloadStyles = (doc) => {
346
+ for (const link of doc.head.querySelectorAll("link[rel=\"stylesheet\"][href]")) {
347
+ const href = new URL(link.getAttribute("href"), d.baseURI).href;
348
+ if ([...d.querySelectorAll("link[href]")].some((own) => own.href === href)) continue;
349
+ const preload = d.createElement("link");
350
+ preload.rel = "preload";
351
+ preload.as = "style";
352
+ preload.href = href;
353
+ for (const name of [
354
+ "media",
355
+ "crossorigin",
356
+ "integrity"
357
+ ]) if (link.hasAttribute(name)) preload.setAttribute(name, link.getAttribute(name));
358
+ d.head.append(preload);
359
+ }
360
+ };
255
361
  const parse = (url, text) => {
256
362
  const at = text.indexOf(config.regionEndMarker);
257
363
  const prefix = at === -1 ? text : text.slice(0, at);
364
+ const doc = parseAsLoaded(prefix);
365
+ preloadStyles(doc);
258
366
  return {
259
367
  url,
260
368
  prefix,
261
- doc: new DOMParser().parseFromString(prefix, "text/html"),
369
+ doc,
262
370
  tail: at === -1 ? "" : text.slice(at + config.regionEndMarker.length)
263
371
  };
264
372
  };
@@ -291,7 +399,8 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
291
399
  cancel: () => reader.cancel()
292
400
  };
293
401
  };
294
- const request = (url, speculative, started) => {
402
+ const request = (url, speculative, started, budget = Infinity) => {
403
+ fetched.add(pageKey(url));
295
404
  const controller = started?.controller ?? new AbortController();
296
405
  const read = async () => {
297
406
  const prefix = config.requestPrefix;
@@ -301,9 +410,13 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
301
410
  const response = await (started?.response ?? w.fetch(target.href, {
302
411
  credentials: "same-origin",
303
412
  headers: speculative ? { purpose: "prefetch" } : {},
304
- priority: speculative ? "auto" : "high",
413
+ priority: speculative === "idle" ? "low" : speculative ? "auto" : "high",
305
414
  signal: controller.signal
306
415
  }));
416
+ const cacheControl = response.headers.get("cache-control") ?? "";
417
+ const maxAge = Number(/(?:^|,)\s*max-age\s*=\s*"?(\d+)/i.exec(cacheControl)?.[1]);
418
+ if (/(?:^|,)\s*no-store\b/i.test(cacheControl)) entry.expires = w.performance.now();
419
+ else if (maxAge > 0) entry.expires = entry.at + maxAge * 1e3;
307
420
  const type = response.headers.get("content-type") ?? "";
308
421
  if (!response.ok || !type.includes("text/html") || !response.body) throw new Error(`MARKLESS_ROUTER_FRAGMENT_UNAVAILABLE: ${response.status}`);
309
422
  const finalUrl = new URL(response.url || target.href);
@@ -311,9 +424,11 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
311
424
  if (!finalUrl.pathname.startsWith(`${prefix}/`)) throw new Error("MARKLESS_ROUTER_FRAGMENT_UNAVAILABLE: redirected");
312
425
  finalUrl.pathname = finalUrl.pathname.slice(prefix.length);
313
426
  finalUrl.hash = url.hash;
427
+ if (Number(response.headers.get("content-length") ?? 0) > budget) throw new Error("MARKLESS_ROUTER_FRAGMENT_OVER_IDLE_BUDGET");
314
428
  const reader = response.body.getReader();
315
429
  const decoder = new TextDecoder();
316
430
  let text = "";
431
+ let bytes = 0;
317
432
  for (;;) {
318
433
  if (text.includes(config.regionEndMarker)) return {
319
434
  ...parse(finalUrl.href, text),
@@ -324,6 +439,11 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
324
439
  closed();
325
440
  return parse(finalUrl.href, text + decoder.decode());
326
441
  }
442
+ bytes += chunk.value.byteLength;
443
+ if (bytes > budget) {
444
+ reader.cancel();
445
+ throw new Error("MARKLESS_ROUTER_FRAGMENT_OVER_IDLE_BUDGET");
446
+ }
327
447
  text += decoder.decode(chunk.value, { stream: true });
328
448
  }
329
449
  };
@@ -333,6 +453,7 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
333
453
  };
334
454
  const entry = {
335
455
  at: started?.at ?? w.performance.now(),
456
+ writes: state.writes,
336
457
  result: read(),
337
458
  open: true,
338
459
  abort: () => {
@@ -343,25 +464,36 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
343
464
  entry.result.catch(closed);
344
465
  return entry;
345
466
  };
346
- const prefetch = (url, code = true, started) => {
467
+ const prefetch = (url, code = true, started, budget = Infinity, idle = false) => {
347
468
  const key = pageKey(url);
348
469
  if (!config.prefetch || key === state.shown) return;
349
470
  const existing = state.prefetches.get(key);
350
- if (existing && w.performance.now() - existing.at < config.prefetchTtlMs) return void (code && preloadCode(url, false));
471
+ if (existing && fresh(existing, w.performance.now())) return void (code && preloadCode(url, false));
351
472
  existing?.abort();
352
473
  const open = [...state.prefetches].filter(([, entry]) => entry.open);
353
474
  for (const [stale, entry] of open.slice(0, Math.max(0, open.length + 1 - config.prefetchConnections))) {
354
475
  state.prefetches.delete(stale);
355
476
  entry.abort();
356
477
  }
357
- const entry = request(url, true, started);
478
+ const entry = request(url, idle ? "idle" : true, started, budget);
479
+ entry.result.catch(() => {
480
+ if (state.prefetches.get(key) === entry) state.prefetches.delete(key);
481
+ });
358
482
  state.prefetches.set(key, entry);
483
+ expire(key, entry);
484
+ if (code) preloadCode(url, false);
485
+ };
486
+ const stamps = /* @__PURE__ */ new WeakMap();
487
+ let stamped = 0;
488
+ const expire = (key, entry) => {
489
+ const stamp = ++stamped;
490
+ stamps.set(entry, stamp);
359
491
  w.setTimeout(() => {
360
- if (state.prefetches.get(key) !== entry) return;
492
+ const current = state.prefetches.get(key);
493
+ if (!current || stamps.get(current) !== stamp) return;
361
494
  state.prefetches.delete(key);
362
- entry.abort();
495
+ current.abort();
363
496
  }, config.prefetchTtlMs);
364
- if (code) preloadCode(url, false);
365
497
  };
366
498
  let scanning = false;
367
499
  let idleQueue = [];
@@ -371,8 +503,8 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
371
503
  for (const entry of state.prefetches.values()) if (entry.open) open += 1;
372
504
  if (open >= config.prefetchConnections) return;
373
505
  const url = idleQueue.shift();
374
- if (state.prefetches.has(pageKey(url))) continue;
375
- prefetch(url, false);
506
+ if (state.prefetches.has(pageKey(url)) || fetched.has(pageKey(url))) continue;
507
+ prefetch(url, false, void 0, config.idleFragmentBytes, true);
376
508
  preloadCode(url, true);
377
509
  }
378
510
  };
@@ -393,18 +525,23 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
393
525
  visible.set(pageKey(url), url);
394
526
  }
395
527
  idleQueue = [];
396
- for (const [key, url] of visible) if (idleQueue.length < config.idleFragments && !state.prefetches.has(key)) idleQueue.push(url);
528
+ for (const [key, url] of visible) if (idleQueue.length < config.idleFragments && !state.prefetches.has(key) && !fetched.has(key)) idleQueue.push(url);
397
529
  pumpIdle();
398
530
  };
399
- const idle = () => w.requestIdleCallback ? w.requestIdleCallback(run, { timeout: 3e3 }) : w.setTimeout(run, 500);
400
- if (d.readyState === "complete") idle();
401
- else w.addEventListener("load", idle, { once: true });
531
+ (host.__marklessFragmentIdle ?? Promise.resolve(false)).then((fast) => {
532
+ if (!fast) return void (scanning = false);
533
+ if (w.requestIdleCallback) w.requestIdleCallback(run, { timeout: 3e3 });
534
+ else w.setTimeout(run, 500);
535
+ });
402
536
  };
537
+ const fresh = (entry, now) => entry.writes === state.writes && now - entry.at < config.prefetchTtlMs && (entry.expires === void 0 || now < entry.expires);
403
538
  const state = {
404
539
  config,
405
540
  routes,
406
541
  prefetches: /* @__PURE__ */ new Map(),
407
542
  visited: /* @__PURE__ */ new Map(),
543
+ writes: 0,
544
+ fresh,
408
545
  shown: pageKey(new URL(location.href)),
409
546
  eligible,
410
547
  preloadCode,
@@ -418,8 +555,42 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
418
555
  };
419
556
  host.__marklessFragmentNav = state;
420
557
  if (early) prefetch(new URL(early.url), true, early);
558
+ const wrote = () => {
559
+ state.writes++;
560
+ for (const entry of state.prefetches.values()) entry.abort();
561
+ state.prefetches.clear();
562
+ state.visited.clear();
563
+ };
564
+ d.addEventListener("submit", wrote, true);
565
+ const read = /* @__PURE__ */ new Set([
566
+ "GET",
567
+ "HEAD",
568
+ "OPTIONS"
569
+ ]);
570
+ const send = w.fetch;
571
+ w.fetch = function fetch(input, init) {
572
+ const response = send.call(w, input, init);
573
+ const request = input instanceof Request ? input : void 0;
574
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
575
+ if (!read.has(method)) {
576
+ let same = false;
577
+ try {
578
+ same = new URL(request?.url ?? String(input), location.href).origin === location.origin;
579
+ } catch {}
580
+ if (same) {
581
+ wrote();
582
+ response.then(wrote, wrote);
583
+ }
584
+ }
585
+ return response;
586
+ };
421
587
  let restTimer = 0;
588
+ let resting = "";
422
589
  let previous;
590
+ const stopRest = () => {
591
+ w.clearTimeout(restTimer);
592
+ resting = "";
593
+ };
423
594
  const hover = (event) => {
424
595
  const pointer = event;
425
596
  if (pointer.pointerType !== "mouse") return;
@@ -428,20 +599,27 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
428
599
  y: pointer.clientY,
429
600
  t: pointer.timeStamp
430
601
  };
431
- if (previous && point.x === previous.x && point.y === previous.y) return void w.clearTimeout(restTimer);
602
+ if (previous && point.x === previous.x && point.y === previous.y) {
603
+ if (eligible(anchorOf(event))?.href !== resting) stopRest();
604
+ return;
605
+ }
432
606
  const gap = previous ? point.t - previous.t : 0;
433
607
  const creeping = !!previous && gap > 0 && Math.hypot(point.x - previous.x, point.y - previous.y) / gap < .1;
434
608
  previous = point;
435
- w.clearTimeout(restTimer);
609
+ stopRest();
436
610
  const url = eligible(anchorOf(event));
437
611
  if (!url) return;
438
- if (creeping) prefetch(url);
439
- else restTimer = w.setTimeout(() => prefetch(url), Math.max(config.hoverDwellMs, gap * 1.5));
612
+ if (creeping) return prefetch(url);
613
+ resting = url.href;
614
+ restTimer = w.setTimeout(() => {
615
+ resting = "";
616
+ prefetch(url);
617
+ }, Math.max(config.hoverDwellMs, gap * 1.5));
440
618
  };
441
619
  d.addEventListener("pointermove", hover, { passive: true });
442
- d.addEventListener("pointerout", () => w.clearTimeout(restTimer), { passive: true });
620
+ d.addEventListener("pointerout", stopRest, { passive: true });
443
621
  const press = (event) => {
444
- w.clearTimeout(restTimer);
622
+ stopRest();
445
623
  const anchor = anchorOf(event);
446
624
  const url = eligible(anchor);
447
625
  if (url) prefetch(url);
@@ -456,7 +634,7 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
456
634
  const url = eligible(anchor, true);
457
635
  if (!url) return false;
458
636
  const current = new URL(location.href);
459
- w.clearTimeout(restTimer);
637
+ stopRest();
460
638
  const replace = anchor.hasAttribute(config.replaceAttribute) || url.href === current.href;
461
639
  const manual = anchor.getAttribute(config.scrollAttribute) === "manual";
462
640
  const swap = (entry) => entry.navigate(d, url, replace ? "replace" : "push", manual ? "manual" : "reset");
@@ -495,6 +673,8 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
495
673
  };
496
674
  }
497
675
  const FRAGMENT_PATH_PREFIX = "/_markless/fragment";
676
+ /** Set on every fragment response, so a redirect it answers can be pointed back at a fragment. */
677
+ const FRAGMENT_RESPONSE_HEADER = "x-markless-fragment";
498
678
  function fragmentPageUrl(url) {
499
679
  if (!url.pathname.startsWith(`/_markless/fragment/`)) return void 0;
500
680
  const page = new URL(url);
@@ -504,10 +684,15 @@ function fragmentPageUrl(url) {
504
684
  const FRAGMENT_REGION_END = "<!--markless:region-end-->";
505
685
  const FRAGMENT_APPEND_END = "<!--markless:append-end-->";
506
686
  const FRAGMENT_PREFETCH_TTL_MS = 3e4;
687
+ const FRAGMENT_IDLE_FRAGMENT_BYTES = 262144;
688
+ const FRAGMENT_IDLE_BYTES_PER_SECOND = 25e4;
689
+ const FRAGMENT_IDLE_MEASURED_BYTES = 65536;
507
690
  //#endregion
508
691
  //#region src/vite/runtime/link-bridge-module.ts
509
692
  /** Marks the page's link bridge script: inline in dev, a module stub importing the shared bridge in a build. */
510
693
  const LINK_BRIDGE_SCRIPT_ATTRIBUTE = "data-markless-router-link-resumer";
694
+ /** Marks the page's route table: inert JSON `[preloads, destinations]`, each destination a list of indexes into preloads. */
695
+ const LINK_TABLE_SCRIPT_ATTRIBUTE = "data-markless-router-links";
511
696
  /** The client output folder a build writes its link bridge files into, served as immutable static assets. */
512
697
  const LINK_BRIDGE_DIR = "_markless/links";
513
698
  function linkBridgeModulePath(base, hash) {
@@ -771,12 +956,35 @@ function routerLinkBridgeModules(options, manifest = buildRouteManifestFromFileI
771
956
  return linkBridgeModules(options.base ?? "/", options.navigationEntryPath, fragmentBridge !== void 0 || options.prefetch !== false, options.documentNavigation === "document", fragmentBridge, options.debug);
772
957
  }
773
958
  function createServerEntry(options) {
959
+ installKeepParts();
774
960
  const manifest = buildRouteManifestFromFileIds(options.routeFileIds);
775
961
  assertCurrentRouteAssets(options);
776
962
  const fragmentBridge = fragmentBridgeFor(options, manifest);
777
963
  const fragmentNavigation = fragmentBridge !== void 0;
778
964
  const linkBridgeModules = options.dev || options.linkBridgePaths ? void 0 : routerLinkBridgeModules(options, manifest);
779
965
  const linkBridges = options.dev ? void 0 : options.linkBridgePaths ?? (linkBridgeModules && [linkBridgeModules[0].path, linkBridgeModules[1].path]);
966
+ const documentHeads = /* @__PURE__ */ new Map();
967
+ function pageStylesheetsAt(url, match, documentModule, request) {
968
+ const own = pageStylesheets(options, match.route.file) ?? [];
969
+ if (options.documentNavigation !== "document" || !documentModule) return Promise.resolve(own);
970
+ const key = url.pathname + url.search;
971
+ let head = documentHeads.get(key);
972
+ if (!head) {
973
+ if (documentHeads.size >= 1e3) documentHeads.delete(documentHeads.keys().next().value);
974
+ head = renderDocumentShell(documentModule, {
975
+ http: createPageHttp(request, url).context,
976
+ params: match.params,
977
+ url: {
978
+ href: url.href,
979
+ pathname: url.pathname,
980
+ search: url.search
981
+ },
982
+ status: 200
983
+ }, "").then(stylesheetHrefs, () => []);
984
+ documentHeads.set(key, head);
985
+ }
986
+ return head.then((hrefs) => [.../* @__PURE__ */ new Set([...own, ...hrefs])]);
987
+ }
780
988
  async function fetch(request) {
781
989
  const requestUrl = new URL(request.url);
782
990
  const fragmentUrl = fragmentNavigation ? fragmentPageUrl(requestUrl) : void 0;
@@ -854,7 +1062,10 @@ function createServerEntry(options) {
854
1062
  const documentLoads = options.documentNavigation === "document";
855
1063
  const linkIntent = fragmentNavigation || options.prefetch !== false;
856
1064
  const destinationPreloads = documentLoads || fragmentNavigation ? options.routeSsrModulePreloads : options.routeModulePreloads;
857
- 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);
1065
+ const pageArtifact = routedPageArtifact(renderSsr, baseArtifact, pageProps, {
1066
+ file,
1067
+ ...options.routeResumeModules?.[file] ? { resume: options.routeResumeModules[file] } : {}
1068
+ }, options.navigationEntryPath, pageStylesheets(options, file), documentModule?.default?.headInjections ?? [], linkIntent ? (html) => linkIntentPreloads(html, pageProps.url.href, manifest, destinationPreloads, fragmentNavigation, fragmentNavigation ? (destination, match) => pageStylesheetsAt(destination, match, documentModule, request) : void 0) : void 0, documentLoads, fragmentBridge, linkBridges);
858
1069
  let rendersStorage = false;
859
1070
  const routedArtifact = {
860
1071
  ...pageArtifact,
@@ -876,7 +1087,7 @@ function createServerEntry(options) {
876
1087
  resumeModuleUrl: options.resumeEntryPath ?? baseArtifact?.resumeModuleUrl,
877
1088
  modulePreloads: (html, resumes) => landingModulePreloads(resumes ? options.routeSsrModulePreloads?.[file] : void 0, linkBridges?.[html.includes("href=\"#/") ? 1 : 0])
878
1089
  };
879
- const headStart = (documentLoads && fragmentNavigation && !fragment ? servedHtmlAttributesScript() : "") + (fragment ? "" : options.importMap ?? "");
1090
+ const headStart = (fragmentNavigation && !fragment ? servedHtmlAttributesScript() : "") + (fragment ? "" : options.importMap ?? "");
880
1091
  if (options.render === "blocking") {
881
1092
  const pageHtml = splitPageHtml(await renderToString(routedArtifact, renderOptions));
882
1093
  privateToVisitor();
@@ -963,7 +1174,7 @@ function pageStylesheets(options, file) {
963
1174
  if (documentHrefs.length === 0) return routeHrefs;
964
1175
  return [.../* @__PURE__ */ new Set([...documentHrefs, ...routeHrefs ?? []])];
965
1176
  }
966
- function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigationEntryPath, stylesheetHrefs, documentHeadInjections = [], intentPreloads, documentLoads = false, fragment, linkBridgePaths) {
1177
+ function routedPageArtifact(renderSsr, baseArtifact, pageProps, route, navigationEntryPath, stylesheetHrefs, documentHeadInjections = [], intentPreloads, documentLoads = false, fragment, linkBridgePaths) {
967
1178
  const headInjections = dedupeHeadLinks([
968
1179
  ...documentHeadInjections,
969
1180
  ...baseArtifact?.headInjections ?? [],
@@ -984,8 +1195,8 @@ function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigation
984
1195
  async renderSsr(renderProps, renderContext) {
985
1196
  const output = await renderSsr(renderProps ?? pageProps, renderContext);
986
1197
  if (!output) return { html: "" };
987
- const routeScript = output.state || output.view ? renderRouteScript(file) : "";
988
- const linkBridge = navigationEntryPath ? renderLinkBridgeScript(navigationEntryPath, intentPreloads?.(output.html), documentLoads, fragment, output.html.includes("href=\"#/"), linkBridgePaths) : "";
1198
+ const routeScript = output.state || output.view ? renderRouteScript(route) : "";
1199
+ const linkBridge = navigationEntryPath ? renderLinkBridgeScript(navigationEntryPath, await intentPreloads?.(output.html), documentLoads, fragment, output.html.includes("href=\"#/"), linkBridgePaths) : "";
989
1200
  const stateWithProps = withPagePropsCell(output.state, pageProps);
990
1201
  return routeScript || linkBridge || stateWithProps !== output.state ? {
991
1202
  ...output,
@@ -1013,8 +1224,22 @@ function withPagePropsCell(state, props) {
1013
1224
  function landingModulePreloads(routePreloads, linkBridgePath) {
1014
1225
  const preloads = [];
1015
1226
  addModulePreloads(preloads, /* @__PURE__ */ new Set(), routePreloads);
1016
- if (linkBridgePath) preloads.push(linkBridgePath);
1017
- return preloads.length > 0 ? preloads : void 0;
1227
+ const landing = preloads.map(highFetchPriority);
1228
+ if (linkBridgePath) landing.push({
1229
+ href: linkBridgePath,
1230
+ fetchPriority: "low"
1231
+ });
1232
+ return landing.length > 0 ? landing : void 0;
1233
+ }
1234
+ function highFetchPriority(preload) {
1235
+ if (typeof preload === "string") return {
1236
+ href: preload,
1237
+ fetchPriority: "high"
1238
+ };
1239
+ return preload.fetchPriority ? preload : {
1240
+ ...preload,
1241
+ fetchPriority: "high"
1242
+ };
1018
1243
  }
1019
1244
  function routerLinkHrefs(html) {
1020
1245
  return [...html.matchAll(/<a\b(?=[^>]*\bdata-markless-router-link(?:[\s=>]|$))(?=[^>]*\bhref="([^"]*)")[^>]*>/g)].map((match) => unescapeHtmlAttribute(match[1] ?? ""));
@@ -1022,16 +1247,28 @@ function routerLinkHrefs(html) {
1022
1247
  function anchorHrefs(html) {
1023
1248
  return [...html.matchAll(/<a\b(?=[^>]*\bhref="([^"]*)")[^>]*>/g)].map((match) => unescapeHtmlAttribute(match[1] ?? ""));
1024
1249
  }
1025
- function linkIntentPreloads(html, baseHref, manifest, routes, plainAnchors = false) {
1250
+ async function linkIntentPreloads(html, baseHref, manifest, routes, plainAnchors = false, stylesheetsOf) {
1026
1251
  const destinations = Object.create(null);
1252
+ const here = new URL(baseHref);
1253
+ const herePage = stylesheetsOf && matchRouteManifest(here.pathname, manifest);
1254
+ const have = new Set(herePage ? await stylesheetsOf(here, herePage) : []);
1027
1255
  for (const href of plainAnchors ? anchorHrefs(html) : routerLinkHrefs(html)) {
1028
1256
  const url = parseSameOriginUrl(href, baseHref);
1029
1257
  const match = url && matchRouteManifest(url.pathname, manifest);
1030
1258
  const preloads = match && routes?.[match.route.file];
1031
- if (url && preloads && (preloads.length > 0 || plainAnchors)) destinations[url.pathname] = preloads;
1259
+ if (!url || !preloads || preloads.length === 0 && !plainAnchors || url.pathname in destinations) continue;
1260
+ const styles = stylesheetsOf ? (await stylesheetsOf(url, match)).filter((href) => !have.has(href)) : [];
1261
+ destinations[url.pathname] = styles.length ? [...preloads, ...styles.map((href) => ({
1262
+ href,
1263
+ as: "style"
1264
+ }))] : preloads;
1032
1265
  }
1033
1266
  return destinations;
1034
1267
  }
1268
+ const STYLESHEET_LINK = /<link\b(?=[^>]*\brel="(?:[^"]*\s)?stylesheet(?:\s[^"]*)?")(?=[^>]*\bhref="([^"]*)")[^>]*>/g;
1269
+ function stylesheetHrefs(html) {
1270
+ return [...html.matchAll(STYLESHEET_LINK)].map((match) => unescapeHtmlAttribute(match[1]));
1271
+ }
1035
1272
  function indexedPreloads(intentPreloads) {
1036
1273
  const preloads = [];
1037
1274
  const indexes = /* @__PURE__ */ new Map();
@@ -1142,18 +1379,20 @@ function insertHeadHtml(documentHtml, headHtml) {
1142
1379
  if (headEnd === -1) return `${headHtml}${documentHtml}`;
1143
1380
  return `${documentHtml.slice(0, headEnd)}${headHtml}${documentHtml.slice(headEnd)}`;
1144
1381
  }
1145
- function renderRouteScript(file) {
1146
- return `<script type="${ROUTE_SCRIPT_TYPE}">${escapeScriptJson({ file })}<\/script>`;
1382
+ function renderRouteScript(route) {
1383
+ return `<script type="${ROUTE_SCRIPT_TYPE}">${escapeScriptJson(route)}<\/script>`;
1147
1384
  }
1148
1385
  function linkBridgeBody(resumeEntryPath, linkIntent, documentLoads, fragment, page, debug = debugDefined()) {
1149
1386
  const inline = !("module" in page);
1150
1387
  const fragmentNavigation = fragment !== void 0;
1151
1388
  const fragmentSource = fragment ? `
1152
- (${fragment.source})(d,()=>${JSON.stringify(LINK_ATTRIBUTE)},${inline ? escapeScriptJson(page.intentPreloads ?? {}) : "r"},(href)=>import(href),${"runPath" in page && page.runPath ? `()=>import(${JSON.stringify(page.runPath)}).then((m)=>m.default)` : `()=>Promise.resolve(${boundFragmentRun(fragment, documentLoads)})`},()=>(${fragment.prefetch ? escapeScriptJson({
1389
+ (${fragment.source})(d,()=>${JSON.stringify(LINK_ATTRIBUTE)},${JSON.stringify(LINK_TABLE_SCRIPT_ATTRIBUTE)},(href)=>import(href),${"runPath" in page && page.runPath ? `()=>import(${JSON.stringify(page.runPath)}).then((m)=>m.default)` : `()=>Promise.resolve(${boundFragmentRun(fragment, documentLoads)})`},()=>(${fragment.prefetch ? escapeScriptJson({
1153
1390
  code: [fragment.entry, ...fragment.entryImports],
1154
1391
  hoverDwellMs: 20,
1155
1392
  requestPrefix: FRAGMENT_PATH_PREFIX,
1156
- prefetchAttribute: PREFETCH_ATTRIBUTE
1393
+ prefetchAttribute: PREFETCH_ATTRIBUTE,
1394
+ idleBytesPerSecond: FRAGMENT_IDLE_BYTES_PER_SECOND,
1395
+ idleMeasuredBytes: FRAGMENT_IDLE_MEASURED_BYTES
1157
1396
  }) : "null"}));` : "";
1158
1397
  const indexed = !fragmentNavigation && linkIntent ? inline ? page.intentPreloads && indexedPreloads(page.intentPreloads) : {
1159
1398
  preloads: "preloads",
@@ -1260,21 +1499,22 @@ function renderLinkBridgeScript(resumeEntryPath, intentPreloads, documentLoads =
1260
1499
  if (!r) return;
1261
1500
  try { globalThis[Symbol.for('markless.debug.channel.v1.bootstrap')] = ${__marklessDebugBootstrapSource()}(r, 'ssr-inline', false); } catch {}
1262
1501
  })();`)}<\/script>` : "";
1502
+ const table = fragment && intentPreloads && Object.keys(intentPreloads).length ? (({ preloads, destinations }) => `<script type="application/json" ${LINK_TABLE_SCRIPT_ATTRIBUTE}>${escapeScriptJson([preloads, destinations])}<\/script>`)(indexedPreloads(intentPreloads)) : "";
1263
1503
  const modulePath = modulePaths?.[hashLinks ? 1 : 0];
1264
1504
  if (modulePath) {
1265
1505
  const indexed = !fragment && intentPreloads ? indexedPreloads(intentPreloads) : void 0;
1266
- const args = fragment ? [escapeScriptJson(intentPreloads ?? {})] : indexed ? [
1506
+ const args = indexed ? [
1267
1507
  "0",
1268
1508
  escapeScriptJson(indexed.preloads),
1269
1509
  escapeScriptJson(indexed.destinations)
1270
1510
  ] : [];
1271
- return `${debugBootstrap}<script type="module" ${LINK_BRIDGE_SCRIPT_ATTRIBUTE}>${escapeInlineScript(`import b from${JSON.stringify(modulePath)};b(${args.join(",")})`)}<\/script>`;
1511
+ return `${debugBootstrap}${table}<script type="module" ${LINK_BRIDGE_SCRIPT_ATTRIBUTE}>${escapeInlineScript(`import b from${JSON.stringify(modulePath)};b(${args.join(",")})`)}<\/script>`;
1272
1512
  }
1273
1513
  const body = linkBridgeBody(resumeEntryPath, intentPreloads !== void 0, documentLoads, fragment, {
1274
1514
  intentPreloads,
1275
1515
  hashLinks
1276
1516
  });
1277
- 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}
1517
+ return `${debugBootstrap}${table}<script ${LINK_BRIDGE_SCRIPT_ATTRIBUTE}>${escapeInlineScript(`(()=>{const d=document,s=d.currentScript,r=s&&s.closest('[data-async-container]');if(!r)return;${body}
1278
1518
  })();`)}<\/script>`;
1279
1519
  }
1280
1520
  function boundFragmentRun(fragment, documentLoads) {
@@ -1290,21 +1530,26 @@ function fragmentConfig(fragment, documentLoads) {
1290
1530
  scrollAttribute: SCROLL_ATTRIBUTE,
1291
1531
  routeScriptType: ROUTE_SCRIPT_TYPE,
1292
1532
  servedHtmlAttributesProperty: SERVED_HTML_ATTRIBUTES_PROPERTY,
1533
+ servedBodyAttributesProperty: SERVED_BODY_ATTRIBUTES_PROPERTY,
1293
1534
  retireProperty: CONTAINER_RETIRE_PROPERTY,
1294
1535
  streamExecutorGlobal: STREAM_ARM_EXECUTOR_GLOBAL,
1295
1536
  streamedArmSelector: STREAMED_ARM_SELECTOR,
1537
+ streamOpenProperty: PROTOCOL_STREAM_OPEN_PROPERTY,
1538
+ pendingDeadlineMs: MARKLESS_NAV_SETTLE_DEADLINE_MS,
1296
1539
  requestPrefix: FRAGMENT_PATH_PREFIX,
1297
1540
  regionEndMarker: FRAGMENT_REGION_END,
1298
1541
  appendMarker: FRAGMENT_APPEND_END,
1299
1542
  pageEndComment: PROTOCOL_CONTAINER_END_COMMENT,
1300
1543
  pageStartComment: PROTOCOL_CONTAINER_START_COMMENT,
1301
1544
  containerAttributes: [CONTAINER_ATTRIBUTE, PROTOCOL_LOCATOR_PATHS_ATTRIBUTE],
1545
+ keepAttributes: [PROTOCOL_KEEP_ATTRIBUTE, PROTOCOL_OWN_ATTRIBUTE],
1302
1546
  documentLoads,
1303
1547
  hoverDwellMs: 20,
1304
1548
  prefetchTtlMs: FRAGMENT_PREFETCH_TTL_MS,
1305
1549
  prefetchConnections: 2,
1306
1550
  endpointPrefixes: [API_PATHNAME_PREFIX],
1307
1551
  idleFragments: 6,
1552
+ idleFragmentBytes: FRAGMENT_IDLE_FRAGMENT_BYTES,
1308
1553
  idleWithoutConnectionInfo: true,
1309
1554
  prefetch: fragment.prefetch,
1310
1555
  pages: fragment.pages
@@ -1349,4 +1594,4 @@ function insertImportMap(html, importMap) {
1349
1594
  return `${html.slice(0, at)}${importMap}${html.slice(at)}`;
1350
1595
  }
1351
1596
  //#endregion
1352
- export { runFragmentNavigation as a, listenForLinkIntent as c, FRAGMENT_PATH_PREFIX as i, routerLinkBridgeModules as n, startFragmentNavigation as o, LINK_BRIDGE_DIR as r, documentHandlersMessage as s, createServerEntry as t };
1597
+ export { FRAGMENT_RESPONSE_HEADER as a, documentHandlersMessage as c, FRAGMENT_PATH_PREFIX as i, listenForLinkIntent as l, routerLinkBridgeModules as n, runFragmentNavigation as o, LINK_BRIDGE_DIR as r, startFragmentNavigation as s, createServerEntry as t };