@markless/router 0.9.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, d as PREFETCH_ATTRIBUTE, f as REPLACE_ATTRIBUTE, i as HTML_ATTRIBUTES_SLOT, l as servedHtmlAttributesScript, n as requestLocals, o as SERVED_BODY_ATTRIBUTES_PROPERTY, p as SCROLL_ATTRIBUTE, r as takeMiddlewareHeaders, s as SERVED_HTML_ATTRIBUTES_PROPERTY, u as LINK_ATTRIBUTE } from "./request-locals-BPfJaoM_.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 });
@@ -266,13 +342,31 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
266
342
  for (let index = 0; index < tags.length; index++) tags[index].textContent = raw[index];
267
343
  return doc;
268
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
+ };
269
361
  const parse = (url, text) => {
270
362
  const at = text.indexOf(config.regionEndMarker);
271
363
  const prefix = at === -1 ? text : text.slice(0, at);
364
+ const doc = parseAsLoaded(prefix);
365
+ preloadStyles(doc);
272
366
  return {
273
367
  url,
274
368
  prefix,
275
- doc: parseAsLoaded(prefix),
369
+ doc,
276
370
  tail: at === -1 ? "" : text.slice(at + config.regionEndMarker.length)
277
371
  };
278
372
  };
@@ -305,7 +399,8 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
305
399
  cancel: () => reader.cancel()
306
400
  };
307
401
  };
308
- const request = (url, speculative, started) => {
402
+ const request = (url, speculative, started, budget = Infinity) => {
403
+ fetched.add(pageKey(url));
309
404
  const controller = started?.controller ?? new AbortController();
310
405
  const read = async () => {
311
406
  const prefix = config.requestPrefix;
@@ -315,9 +410,13 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
315
410
  const response = await (started?.response ?? w.fetch(target.href, {
316
411
  credentials: "same-origin",
317
412
  headers: speculative ? { purpose: "prefetch" } : {},
318
- priority: speculative ? "auto" : "high",
413
+ priority: speculative === "idle" ? "low" : speculative ? "auto" : "high",
319
414
  signal: controller.signal
320
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;
321
420
  const type = response.headers.get("content-type") ?? "";
322
421
  if (!response.ok || !type.includes("text/html") || !response.body) throw new Error(`MARKLESS_ROUTER_FRAGMENT_UNAVAILABLE: ${response.status}`);
323
422
  const finalUrl = new URL(response.url || target.href);
@@ -325,9 +424,11 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
325
424
  if (!finalUrl.pathname.startsWith(`${prefix}/`)) throw new Error("MARKLESS_ROUTER_FRAGMENT_UNAVAILABLE: redirected");
326
425
  finalUrl.pathname = finalUrl.pathname.slice(prefix.length);
327
426
  finalUrl.hash = url.hash;
427
+ if (Number(response.headers.get("content-length") ?? 0) > budget) throw new Error("MARKLESS_ROUTER_FRAGMENT_OVER_IDLE_BUDGET");
328
428
  const reader = response.body.getReader();
329
429
  const decoder = new TextDecoder();
330
430
  let text = "";
431
+ let bytes = 0;
331
432
  for (;;) {
332
433
  if (text.includes(config.regionEndMarker)) return {
333
434
  ...parse(finalUrl.href, text),
@@ -338,6 +439,11 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
338
439
  closed();
339
440
  return parse(finalUrl.href, text + decoder.decode());
340
441
  }
442
+ bytes += chunk.value.byteLength;
443
+ if (bytes > budget) {
444
+ reader.cancel();
445
+ throw new Error("MARKLESS_ROUTER_FRAGMENT_OVER_IDLE_BUDGET");
446
+ }
341
447
  text += decoder.decode(chunk.value, { stream: true });
342
448
  }
343
449
  };
@@ -347,6 +453,7 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
347
453
  };
348
454
  const entry = {
349
455
  at: started?.at ?? w.performance.now(),
456
+ writes: state.writes,
350
457
  result: read(),
351
458
  open: true,
352
459
  abort: () => {
@@ -357,25 +464,36 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
357
464
  entry.result.catch(closed);
358
465
  return entry;
359
466
  };
360
- const prefetch = (url, code = true, started) => {
467
+ const prefetch = (url, code = true, started, budget = Infinity, idle = false) => {
361
468
  const key = pageKey(url);
362
469
  if (!config.prefetch || key === state.shown) return;
363
470
  const existing = state.prefetches.get(key);
364
- 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));
365
472
  existing?.abort();
366
473
  const open = [...state.prefetches].filter(([, entry]) => entry.open);
367
474
  for (const [stale, entry] of open.slice(0, Math.max(0, open.length + 1 - config.prefetchConnections))) {
368
475
  state.prefetches.delete(stale);
369
476
  entry.abort();
370
477
  }
371
- 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
+ });
372
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);
373
491
  w.setTimeout(() => {
374
- if (state.prefetches.get(key) !== entry) return;
492
+ const current = state.prefetches.get(key);
493
+ if (!current || stamps.get(current) !== stamp) return;
375
494
  state.prefetches.delete(key);
376
- entry.abort();
495
+ current.abort();
377
496
  }, config.prefetchTtlMs);
378
- if (code) preloadCode(url, false);
379
497
  };
380
498
  let scanning = false;
381
499
  let idleQueue = [];
@@ -385,8 +503,8 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
385
503
  for (const entry of state.prefetches.values()) if (entry.open) open += 1;
386
504
  if (open >= config.prefetchConnections) return;
387
505
  const url = idleQueue.shift();
388
- if (state.prefetches.has(pageKey(url))) continue;
389
- prefetch(url, false);
506
+ if (state.prefetches.has(pageKey(url)) || fetched.has(pageKey(url))) continue;
507
+ prefetch(url, false, void 0, config.idleFragmentBytes, true);
390
508
  preloadCode(url, true);
391
509
  }
392
510
  };
@@ -407,18 +525,23 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
407
525
  visible.set(pageKey(url), url);
408
526
  }
409
527
  idleQueue = [];
410
- 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);
411
529
  pumpIdle();
412
530
  };
413
- const idle = () => w.requestIdleCallback ? w.requestIdleCallback(run, { timeout: 3e3 }) : w.setTimeout(run, 500);
414
- if (d.readyState === "complete") idle();
415
- 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
+ });
416
536
  };
537
+ const fresh = (entry, now) => entry.writes === state.writes && now - entry.at < config.prefetchTtlMs && (entry.expires === void 0 || now < entry.expires);
417
538
  const state = {
418
539
  config,
419
540
  routes,
420
541
  prefetches: /* @__PURE__ */ new Map(),
421
542
  visited: /* @__PURE__ */ new Map(),
543
+ writes: 0,
544
+ fresh,
422
545
  shown: pageKey(new URL(location.href)),
423
546
  eligible,
424
547
  preloadCode,
@@ -432,8 +555,42 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
432
555
  };
433
556
  host.__marklessFragmentNav = state;
434
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
+ };
435
587
  let restTimer = 0;
588
+ let resting = "";
436
589
  let previous;
590
+ const stopRest = () => {
591
+ w.clearTimeout(restTimer);
592
+ resting = "";
593
+ };
437
594
  const hover = (event) => {
438
595
  const pointer = event;
439
596
  if (pointer.pointerType !== "mouse") return;
@@ -442,20 +599,27 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
442
599
  y: pointer.clientY,
443
600
  t: pointer.timeStamp
444
601
  };
445
- 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
+ }
446
606
  const gap = previous ? point.t - previous.t : 0;
447
607
  const creeping = !!previous && gap > 0 && Math.hypot(point.x - previous.x, point.y - previous.y) / gap < .1;
448
608
  previous = point;
449
- w.clearTimeout(restTimer);
609
+ stopRest();
450
610
  const url = eligible(anchorOf(event));
451
611
  if (!url) return;
452
- if (creeping) prefetch(url);
453
- 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));
454
618
  };
455
619
  d.addEventListener("pointermove", hover, { passive: true });
456
- d.addEventListener("pointerout", () => w.clearTimeout(restTimer), { passive: true });
620
+ d.addEventListener("pointerout", stopRest, { passive: true });
457
621
  const press = (event) => {
458
- w.clearTimeout(restTimer);
622
+ stopRest();
459
623
  const anchor = anchorOf(event);
460
624
  const url = eligible(anchor);
461
625
  if (url) prefetch(url);
@@ -470,7 +634,7 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
470
634
  const url = eligible(anchor, true);
471
635
  if (!url) return false;
472
636
  const current = new URL(location.href);
473
- w.clearTimeout(restTimer);
637
+ stopRest();
474
638
  const replace = anchor.hasAttribute(config.replaceAttribute) || url.href === current.href;
475
639
  const manual = anchor.getAttribute(config.scrollAttribute) === "manual";
476
640
  const swap = (entry) => entry.navigate(d, url, replace ? "replace" : "push", manual ? "manual" : "reset");
@@ -509,6 +673,8 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
509
673
  };
510
674
  }
511
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";
512
678
  function fragmentPageUrl(url) {
513
679
  if (!url.pathname.startsWith(`/_markless/fragment/`)) return void 0;
514
680
  const page = new URL(url);
@@ -518,10 +684,15 @@ function fragmentPageUrl(url) {
518
684
  const FRAGMENT_REGION_END = "<!--markless:region-end-->";
519
685
  const FRAGMENT_APPEND_END = "<!--markless:append-end-->";
520
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;
521
690
  //#endregion
522
691
  //#region src/vite/runtime/link-bridge-module.ts
523
692
  /** Marks the page's link bridge script: inline in dev, a module stub importing the shared bridge in a build. */
524
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";
525
696
  /** The client output folder a build writes its link bridge files into, served as immutable static assets. */
526
697
  const LINK_BRIDGE_DIR = "_markless/links";
527
698
  function linkBridgeModulePath(base, hash) {
@@ -785,12 +956,35 @@ function routerLinkBridgeModules(options, manifest = buildRouteManifestFromFileI
785
956
  return linkBridgeModules(options.base ?? "/", options.navigationEntryPath, fragmentBridge !== void 0 || options.prefetch !== false, options.documentNavigation === "document", fragmentBridge, options.debug);
786
957
  }
787
958
  function createServerEntry(options) {
959
+ installKeepParts();
788
960
  const manifest = buildRouteManifestFromFileIds(options.routeFileIds);
789
961
  assertCurrentRouteAssets(options);
790
962
  const fragmentBridge = fragmentBridgeFor(options, manifest);
791
963
  const fragmentNavigation = fragmentBridge !== void 0;
792
964
  const linkBridgeModules = options.dev || options.linkBridgePaths ? void 0 : routerLinkBridgeModules(options, manifest);
793
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
+ }
794
988
  async function fetch(request) {
795
989
  const requestUrl = new URL(request.url);
796
990
  const fragmentUrl = fragmentNavigation ? fragmentPageUrl(requestUrl) : void 0;
@@ -868,7 +1062,10 @@ function createServerEntry(options) {
868
1062
  const documentLoads = options.documentNavigation === "document";
869
1063
  const linkIntent = fragmentNavigation || options.prefetch !== false;
870
1064
  const destinationPreloads = documentLoads || fragmentNavigation ? options.routeSsrModulePreloads : options.routeModulePreloads;
871
- 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);
872
1069
  let rendersStorage = false;
873
1070
  const routedArtifact = {
874
1071
  ...pageArtifact,
@@ -977,7 +1174,7 @@ function pageStylesheets(options, file) {
977
1174
  if (documentHrefs.length === 0) return routeHrefs;
978
1175
  return [.../* @__PURE__ */ new Set([...documentHrefs, ...routeHrefs ?? []])];
979
1176
  }
980
- 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) {
981
1178
  const headInjections = dedupeHeadLinks([
982
1179
  ...documentHeadInjections,
983
1180
  ...baseArtifact?.headInjections ?? [],
@@ -998,8 +1195,8 @@ function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigation
998
1195
  async renderSsr(renderProps, renderContext) {
999
1196
  const output = await renderSsr(renderProps ?? pageProps, renderContext);
1000
1197
  if (!output) return { html: "" };
1001
- const routeScript = output.state || output.view ? renderRouteScript(file) : "";
1002
- 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) : "";
1003
1200
  const stateWithProps = withPagePropsCell(output.state, pageProps);
1004
1201
  return routeScript || linkBridge || stateWithProps !== output.state ? {
1005
1202
  ...output,
@@ -1027,17 +1224,21 @@ function withPagePropsCell(state, props) {
1027
1224
  function landingModulePreloads(routePreloads, linkBridgePath) {
1028
1225
  const preloads = [];
1029
1226
  addModulePreloads(preloads, /* @__PURE__ */ new Set(), routePreloads);
1030
- if (linkBridgePath) preloads.push(linkBridgePath);
1031
- return preloads.length > 0 ? preloads.map(lowFetchPriority) : 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;
1032
1233
  }
1033
- function lowFetchPriority(preload) {
1234
+ function highFetchPriority(preload) {
1034
1235
  if (typeof preload === "string") return {
1035
1236
  href: preload,
1036
- fetchPriority: "low"
1237
+ fetchPriority: "high"
1037
1238
  };
1038
1239
  return preload.fetchPriority ? preload : {
1039
1240
  ...preload,
1040
- fetchPriority: "low"
1241
+ fetchPriority: "high"
1041
1242
  };
1042
1243
  }
1043
1244
  function routerLinkHrefs(html) {
@@ -1046,16 +1247,28 @@ function routerLinkHrefs(html) {
1046
1247
  function anchorHrefs(html) {
1047
1248
  return [...html.matchAll(/<a\b(?=[^>]*\bhref="([^"]*)")[^>]*>/g)].map((match) => unescapeHtmlAttribute(match[1] ?? ""));
1048
1249
  }
1049
- function linkIntentPreloads(html, baseHref, manifest, routes, plainAnchors = false) {
1250
+ async function linkIntentPreloads(html, baseHref, manifest, routes, plainAnchors = false, stylesheetsOf) {
1050
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) : []);
1051
1255
  for (const href of plainAnchors ? anchorHrefs(html) : routerLinkHrefs(html)) {
1052
1256
  const url = parseSameOriginUrl(href, baseHref);
1053
1257
  const match = url && matchRouteManifest(url.pathname, manifest);
1054
1258
  const preloads = match && routes?.[match.route.file];
1055
- 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;
1056
1265
  }
1057
1266
  return destinations;
1058
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
+ }
1059
1272
  function indexedPreloads(intentPreloads) {
1060
1273
  const preloads = [];
1061
1274
  const indexes = /* @__PURE__ */ new Map();
@@ -1166,18 +1379,20 @@ function insertHeadHtml(documentHtml, headHtml) {
1166
1379
  if (headEnd === -1) return `${headHtml}${documentHtml}`;
1167
1380
  return `${documentHtml.slice(0, headEnd)}${headHtml}${documentHtml.slice(headEnd)}`;
1168
1381
  }
1169
- function renderRouteScript(file) {
1170
- return `<script type="${ROUTE_SCRIPT_TYPE}">${escapeScriptJson({ file })}<\/script>`;
1382
+ function renderRouteScript(route) {
1383
+ return `<script type="${ROUTE_SCRIPT_TYPE}">${escapeScriptJson(route)}<\/script>`;
1171
1384
  }
1172
1385
  function linkBridgeBody(resumeEntryPath, linkIntent, documentLoads, fragment, page, debug = debugDefined()) {
1173
1386
  const inline = !("module" in page);
1174
1387
  const fragmentNavigation = fragment !== void 0;
1175
1388
  const fragmentSource = fragment ? `
1176
- (${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({
1177
1390
  code: [fragment.entry, ...fragment.entryImports],
1178
1391
  hoverDwellMs: 20,
1179
1392
  requestPrefix: FRAGMENT_PATH_PREFIX,
1180
- prefetchAttribute: PREFETCH_ATTRIBUTE
1393
+ prefetchAttribute: PREFETCH_ATTRIBUTE,
1394
+ idleBytesPerSecond: FRAGMENT_IDLE_BYTES_PER_SECOND,
1395
+ idleMeasuredBytes: FRAGMENT_IDLE_MEASURED_BYTES
1181
1396
  }) : "null"}));` : "";
1182
1397
  const indexed = !fragmentNavigation && linkIntent ? inline ? page.intentPreloads && indexedPreloads(page.intentPreloads) : {
1183
1398
  preloads: "preloads",
@@ -1284,21 +1499,22 @@ function renderLinkBridgeScript(resumeEntryPath, intentPreloads, documentLoads =
1284
1499
  if (!r) return;
1285
1500
  try { globalThis[Symbol.for('markless.debug.channel.v1.bootstrap')] = ${__marklessDebugBootstrapSource()}(r, 'ssr-inline', false); } catch {}
1286
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)) : "";
1287
1503
  const modulePath = modulePaths?.[hashLinks ? 1 : 0];
1288
1504
  if (modulePath) {
1289
1505
  const indexed = !fragment && intentPreloads ? indexedPreloads(intentPreloads) : void 0;
1290
- const args = fragment ? [escapeScriptJson(intentPreloads ?? {})] : indexed ? [
1506
+ const args = indexed ? [
1291
1507
  "0",
1292
1508
  escapeScriptJson(indexed.preloads),
1293
1509
  escapeScriptJson(indexed.destinations)
1294
1510
  ] : [];
1295
- 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>`;
1296
1512
  }
1297
1513
  const body = linkBridgeBody(resumeEntryPath, intentPreloads !== void 0, documentLoads, fragment, {
1298
1514
  intentPreloads,
1299
1515
  hashLinks
1300
1516
  });
1301
- 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}
1302
1518
  })();`)}<\/script>`;
1303
1519
  }
1304
1520
  function boundFragmentRun(fragment, documentLoads) {
@@ -1318,18 +1534,22 @@ function fragmentConfig(fragment, documentLoads) {
1318
1534
  retireProperty: CONTAINER_RETIRE_PROPERTY,
1319
1535
  streamExecutorGlobal: STREAM_ARM_EXECUTOR_GLOBAL,
1320
1536
  streamedArmSelector: STREAMED_ARM_SELECTOR,
1537
+ streamOpenProperty: PROTOCOL_STREAM_OPEN_PROPERTY,
1538
+ pendingDeadlineMs: MARKLESS_NAV_SETTLE_DEADLINE_MS,
1321
1539
  requestPrefix: FRAGMENT_PATH_PREFIX,
1322
1540
  regionEndMarker: FRAGMENT_REGION_END,
1323
1541
  appendMarker: FRAGMENT_APPEND_END,
1324
1542
  pageEndComment: PROTOCOL_CONTAINER_END_COMMENT,
1325
1543
  pageStartComment: PROTOCOL_CONTAINER_START_COMMENT,
1326
1544
  containerAttributes: [CONTAINER_ATTRIBUTE, PROTOCOL_LOCATOR_PATHS_ATTRIBUTE],
1545
+ keepAttributes: [PROTOCOL_KEEP_ATTRIBUTE, PROTOCOL_OWN_ATTRIBUTE],
1327
1546
  documentLoads,
1328
1547
  hoverDwellMs: 20,
1329
1548
  prefetchTtlMs: FRAGMENT_PREFETCH_TTL_MS,
1330
1549
  prefetchConnections: 2,
1331
1550
  endpointPrefixes: [API_PATHNAME_PREFIX],
1332
1551
  idleFragments: 6,
1552
+ idleFragmentBytes: FRAGMENT_IDLE_FRAGMENT_BYTES,
1333
1553
  idleWithoutConnectionInfo: true,
1334
1554
  prefetch: fragment.prefetch,
1335
1555
  pages: fragment.pages
@@ -1374,4 +1594,4 @@ function insertImportMap(html, importMap) {
1374
1594
  return `${html.slice(0, at)}${importMap}${html.slice(at)}`;
1375
1595
  }
1376
1596
  //#endregion
1377
- 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 };