evolit 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -140,12 +140,18 @@ export default function CollectionControls() {
140
140
  }
141
141
  ```
142
142
 
143
- The hook returns `{ status, url, pendingUrl, error, push, replace, refresh, createHref }`:
143
+ The hook returns
144
+ `{ status, url, pendingUrl, error, context, push, replace, replaceContext, refresh, createHref }`:
144
145
 
145
146
  - `push(target, { scroll: false })` adds a browser-history entry. Pass
146
147
  `scroll: false` to keep the current viewport position.
147
148
  - `replace(target, { scroll: false })` updates the current entry, useful for
148
149
  visual-only query state. It accepts the same scroll option.
150
+ - `push(target, { context })` and `replace(target, { context })` attach JSON-safe transient state
151
+ to that history entry and make it available to the destination page and layouts as their
152
+ `navigationContext` prop during SSR.
153
+ - `replaceContext(contextOrUpdater)` updates only the current history entry and hook state. It does
154
+ not change the URL or fetch a route delta.
149
155
  - `refresh()` bypasses the client delta cache for the current URL.
150
156
  - `createHref(pathname, searchParams)` creates a relative internal URL. It accepts standard
151
157
  `URLSearchParams`, preserving repeated keys such as `facet=brand&facet=material`.
@@ -154,6 +160,12 @@ The hook returns `{ status, url, pendingUrl, error, push, replace, refresh, crea
154
160
  share with server-evaluated route code. `useNavigation()` itself is browser-only and must only run
155
161
  from a connected client component.
156
162
 
163
+ Navigation context is intended for compact UI state that must survive back/forward navigation but
164
+ does not belong in the public URL. It must be a JSON-safe object and is limited to 8 KiB after UTF-8
165
+ serialization. Evolit includes it in browser, response, and segment cache identity so the same URL
166
+ cannot reuse markup rendered for a different context. Invalid context is rejected at the client
167
+ boundary; malformed context received by the server is ignored.
168
+
157
169
  Client components can also read the active route state with browser-only hooks:
158
170
 
159
171
  ```jsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolit",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "A convention-driven application framework for LitSX and web components.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.10.3",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "@jridgewell/remapping": "^2.3.5",
62
- "@litsx/compiler": "1.0.0-next.12",
62
+ "@litsx/compiler": "1.0.0-next.13",
63
63
  "@litsx/core": "1.0.0-next.8",
64
64
  "@litsx/ssr": "1.0.0-next.5",
65
65
  "@rollup/plugin-commonjs": "^29.0.0",
@@ -7,6 +7,11 @@ import {
7
7
  registerHydrationModules,
8
8
  } from "@litsx/ssr/hydration";
9
9
  import { createHref as createBaseHref } from "./navigation-url.js";
10
+ import {
11
+ EVOLIT_NAVIGATION_CONTEXT_HEADER,
12
+ encodeNavigationContext,
13
+ normalizeNavigationContext,
14
+ } from "./navigation-context.js";
10
15
  import {
11
16
  getNavigationExtensions,
12
17
  registerNavigationExtensions,
@@ -305,10 +310,18 @@ function toHref(target, location) {
305
310
  return new URL(target, location.href).href;
306
311
  }
307
312
 
308
- function toCacheKey(href) {
313
+ function toCacheKey(href, encodedContext = null) {
309
314
  const url = new URL(href);
310
315
  url.hash = "";
311
- return url.href;
316
+ return `${url.href}\ncontext:${encodedContext ?? ""}`;
317
+ }
318
+
319
+ function readStoredNavigationContext(value) {
320
+ try {
321
+ return normalizeNavigationContext(value ?? null);
322
+ } catch {
323
+ return null;
324
+ }
312
325
  }
313
326
 
314
327
  function getDeltaExpiry(delta, now = Date.now()) {
@@ -406,7 +419,15 @@ export function createBrowserNavigation(options = {}) {
406
419
  const historyEntries = new Map();
407
420
  let controller = null;
408
421
  let navigationSequence = 0;
409
- let state = { status: "idle", url: windowRef.location.href, pendingUrl: null, error: null };
422
+ let state = {
423
+ status: "idle",
424
+ url: windowRef.location.href,
425
+ pendingUrl: null,
426
+ error: null,
427
+ context: readStoredNavigationContext(
428
+ windowRef.history.state?.__evolitNavigationContext ?? null,
429
+ ),
430
+ };
410
431
  const emit = () => listeners.forEach((listener) => listener(state));
411
432
  const notifyLocationChange = () => {
412
433
  const EventConstructor = windowRef.CustomEvent ?? globalThis.CustomEvent;
@@ -445,7 +466,17 @@ export function createBrowserNavigation(options = {}) {
445
466
 
446
467
  function ensureHistoryEntry(entryId = windowRef.history.state?.__evolitNavigationEntry) {
447
468
  const id = typeof entryId === "string" ? entryId : createHistoryEntryId();
448
- if (!historyEntries.has(id)) historyEntries.set(id, { parentId: null, url: null, delta: null, expiresAt: null });
469
+ if (!historyEntries.has(id)) {
470
+ historyEntries.set(id, {
471
+ parentId: null,
472
+ url: null,
473
+ delta: null,
474
+ expiresAt: null,
475
+ context: readStoredNavigationContext(
476
+ windowRef.history.state?.__evolitNavigationContext ?? null,
477
+ ),
478
+ });
479
+ }
449
480
  if (windowRef.history.state?.__evolitNavigationEntry !== id) {
450
481
  windowRef.history.replaceState(
451
482
  { ...(windowRef.history.state ?? {}), __evolitNavigationEntry: id },
@@ -490,6 +521,7 @@ export function createBrowserNavigation(options = {}) {
490
521
 
491
522
  async function navigate(target, mode = "push", fromPopState = false, options = {}) {
492
523
  const previousHref = windowRef.location.href;
524
+ const previousContext = state.context;
493
525
  const requestedHref = toHref(target, windowRef.location);
494
526
  const initialHref = toHref(transformNavigationUrl(requestedHref, { type: "navigation" }), windowRef.location);
495
527
  const navigationExtensions = getNavigationExtensions();
@@ -502,7 +534,11 @@ export function createBrowserNavigation(options = {}) {
502
534
  : { url: initialHref, cancelled: false };
503
535
  if (beforeNavigation.cancelled) return null;
504
536
  const href = toHref(beforeNavigation.url, windowRef.location);
505
- const cacheKey = toCacheKey(href);
537
+ const navigationContext = normalizeNavigationContext(
538
+ options.context === undefined ? null : options.context,
539
+ );
540
+ const encodedContext = encodeNavigationContext(navigationContext);
541
+ const cacheKey = toCacheKey(href, encodedContext);
506
542
  const scrollPosition = options.scroll === false
507
543
  ? currentScrollPosition(windowRef)
508
544
  : options.scrollPosition;
@@ -513,17 +549,33 @@ export function createBrowserNavigation(options = {}) {
513
549
  const isCurrent = () => navigationId === navigationSequence && !navigationController.signal.aborted;
514
550
  const currentEntryId = ensureHistoryEntry();
515
551
  let entryId = options.historyEntryId;
552
+ let createdEntry = false;
516
553
  if (fromPopState) {
517
554
  entryId = ensureHistoryEntry(entryId);
518
555
  } else if (mode === "push") {
519
556
  saveCurrentScrollPosition();
520
557
  removeForwardHistoryEntries(currentEntryId);
521
558
  entryId = createHistoryEntryId();
522
- historyEntries.set(entryId, { parentId: currentEntryId, url: null, delta: null, expiresAt: null });
559
+ createdEntry = true;
560
+ historyEntries.set(entryId, {
561
+ parentId: currentEntryId,
562
+ url: null,
563
+ delta: null,
564
+ expiresAt: null,
565
+ context: navigationContext,
566
+ });
523
567
  } else {
524
568
  entryId = currentEntryId;
569
+ const entry = historyEntries.get(entryId);
570
+ if (entry) entry.context = navigationContext;
525
571
  }
526
- state = { ...state, status: "pending", pendingUrl: href, error: null };
572
+ state = {
573
+ ...state,
574
+ status: "pending",
575
+ pendingUrl: href,
576
+ error: null,
577
+ context: navigationContext,
578
+ };
527
579
  emit();
528
580
  try {
529
581
  const cached = getCachedDelta(entryId, cacheKey, options.force);
@@ -531,7 +583,12 @@ export function createBrowserNavigation(options = {}) {
531
583
  ? cached
532
584
  : await (async () => {
533
585
  const response = await windowRef.fetch(href, {
534
- headers: { accept: "application/vnd.evolit.navigation+json" },
586
+ headers: {
587
+ accept: "application/vnd.evolit.navigation+json",
588
+ ...(encodedContext
589
+ ? { [EVOLIT_NAVIGATION_CONTEXT_HEADER]: encodedContext }
590
+ : {}),
591
+ },
535
592
  // A document response may already be fresh in the browser HTTP cache.
536
593
  // Navigation is a different representation of that URL, so it must
537
594
  // reach the server rather than reusing the cached HTML document.
@@ -558,7 +615,9 @@ export function createBrowserNavigation(options = {}) {
558
615
  return nextDelta;
559
616
  })();
560
617
  if (!delta || !isCurrent()) return null;
561
- if (delta.type === "redirect") return navigate(delta.location, "replace", false);
618
+ if (delta.type === "redirect") {
619
+ return navigate(delta.location, "replace", false, { context: navigationContext });
620
+ }
562
621
  const applied = await applyDelta(delta, {
563
622
  signal: navigationController.signal,
564
623
  moduleVersion: options.moduleVersion,
@@ -576,19 +635,26 @@ export function createBrowserNavigation(options = {}) {
576
635
  ...(mode === "replace" ? windowRef.history.state : {}),
577
636
  __evolitNavigationEntry: entryId,
578
637
  __evolitScroll: scrollPosition ?? { x: 0, y: 0 },
638
+ __evolitNavigationContext: navigationContext,
579
639
  },
580
640
  "",
581
641
  canonicalHref,
582
642
  );
583
643
  }
584
- cacheDelta(entryId, toCacheKey(canonicalHref), delta);
644
+ cacheDelta(entryId, toCacheKey(canonicalHref, encodedContext), delta);
585
645
  restoreScrollAndFocus(
586
646
  windowRef,
587
647
  canonicalHref,
588
648
  scrollPosition,
589
649
  options.scroll === false,
590
650
  );
591
- state = { status: "idle", url: canonicalHref, pendingUrl: null, error: null };
651
+ state = {
652
+ status: "idle",
653
+ url: canonicalHref,
654
+ pendingUrl: null,
655
+ error: null,
656
+ context: navigationContext,
657
+ };
592
658
  emit();
593
659
  notifyLocationChange();
594
660
  if (navigationExtensions.length > 0) {
@@ -603,16 +669,53 @@ export function createBrowserNavigation(options = {}) {
603
669
  return delta;
604
670
  } catch (error) {
605
671
  if (error?.name === "AbortError" || !isCurrent()) return null;
606
- state = { ...state, status: "error", pendingUrl: null, error };
672
+ if (!fromPopState) {
673
+ if (createdEntry) historyEntries.delete(entryId);
674
+ else {
675
+ const entry = historyEntries.get(entryId);
676
+ if (entry) entry.context = previousContext;
677
+ }
678
+ }
679
+ state = {
680
+ ...state,
681
+ status: "error",
682
+ pendingUrl: null,
683
+ error,
684
+ context: fromPopState ? navigationContext : previousContext,
685
+ };
607
686
  emit();
608
687
  throw error;
609
688
  }
610
689
  }
611
690
 
691
+ async function replaceContext(nextContext) {
692
+ const currentContext = normalizeNavigationContext(state.context);
693
+ const resolvedContext = normalizeNavigationContext(
694
+ typeof nextContext === "function" ? nextContext(currentContext) : nextContext,
695
+ );
696
+ const entryId = ensureHistoryEntry();
697
+ const entry = historyEntries.get(entryId);
698
+
699
+ if (entry) entry.context = resolvedContext;
700
+ windowRef.history.replaceState(
701
+ {
702
+ ...(windowRef.history.state ?? {}),
703
+ __evolitNavigationEntry: entryId,
704
+ __evolitNavigationContext: resolvedContext,
705
+ },
706
+ "",
707
+ windowRef.location.href,
708
+ );
709
+ state = { ...state, context: resolvedContext };
710
+ emit();
711
+ return resolvedContext;
712
+ }
713
+
612
714
  windowRef.addEventListener("popstate", (event) => {
613
715
  void navigate(windowRef.location.href, "replace", true, {
614
716
  historyEntryId: event.state?.__evolitNavigationEntry,
615
717
  scrollPosition: event.state?.__evolitScroll ?? null,
718
+ context: readStoredNavigationContext(event.state?.__evolitNavigationContext),
616
719
  });
617
720
  });
618
721
  windowRef.addEventListener(DEVELOPMENT_REFRESH_EVENT, (event) => {
@@ -635,7 +738,13 @@ export function createBrowserNavigation(options = {}) {
635
738
  requireSegmentChange: true,
636
739
  }).then((applied) => {
637
740
  if (applied === false || refreshController.signal.aborted) return false;
638
- state = { status: "idle", url: windowRef.location.href, pendingUrl: null, error: null };
741
+ state = {
742
+ status: "idle",
743
+ url: windowRef.location.href,
744
+ pendingUrl: null,
745
+ error: null,
746
+ context: state.context,
747
+ };
639
748
  emit();
640
749
  return true;
641
750
  }).catch((error) => {
@@ -667,7 +776,12 @@ export function createBrowserNavigation(options = {}) {
667
776
  subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
668
777
  push: (target, options) => navigate(target, "push", false, options),
669
778
  replace: (target, options) => navigate(target, "replace", false, options),
670
- refresh: (options) => navigate(windowRef.location.href, "replace", false, { ...options, force: true }),
779
+ replaceContext,
780
+ refresh: (options = {}) => navigate(windowRef.location.href, "replace", false, {
781
+ ...options,
782
+ context: options.context ?? state.context,
783
+ force: true,
784
+ }),
671
785
  createHref,
672
786
  };
673
787
  }
@@ -686,6 +800,7 @@ export function useNavigation() {
686
800
  ...state,
687
801
  push: navigation.push,
688
802
  replace: navigation.replace,
803
+ replaceContext: navigation.replaceContext,
689
804
  refresh: navigation.refresh,
690
805
  createHref: navigation.createHref,
691
806
  };
@@ -0,0 +1,87 @@
1
+ export const EVOLIT_NAVIGATION_CONTEXT_HEADER = "x-evolit-navigation-context";
2
+ export const MAX_NAVIGATION_CONTEXT_BYTES = 8 * 1024;
3
+
4
+ function assertJsonSafe(value, path, seen, depth) {
5
+ if (depth > 32) {
6
+ throw new TypeError(`Navigation context at ${path} is not JSON-safe.`);
7
+ }
8
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
9
+ return;
10
+ }
11
+ if (typeof value === "number") {
12
+ if (Number.isFinite(value)) return;
13
+ throw new TypeError(`Navigation context at ${path} is not JSON-safe.`);
14
+ }
15
+ if (typeof value !== "object") {
16
+ throw new TypeError(`Navigation context at ${path} is not JSON-safe.`);
17
+ }
18
+ if (seen.has(value)) {
19
+ throw new TypeError(`Navigation context at ${path} is not JSON-safe.`);
20
+ }
21
+
22
+ const prototype = Object.getPrototypeOf(value);
23
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
24
+ throw new TypeError(`Navigation context at ${path} is not JSON-safe.`);
25
+ }
26
+
27
+ seen.add(value);
28
+ if (Array.isArray(value)) {
29
+ value.forEach((item, index) => assertJsonSafe(item, `${path}[${index}]`, seen, depth + 1));
30
+ } else {
31
+ for (const [key, item] of Object.entries(value)) {
32
+ assertJsonSafe(item, `${path}.${key}`, seen, depth + 1);
33
+ }
34
+ }
35
+ seen.delete(value);
36
+ }
37
+
38
+ function serializeNavigationContext(context) {
39
+ if (context === null || context === undefined) return null;
40
+ if (typeof context !== "object" || Array.isArray(context)) {
41
+ throw new TypeError("Navigation context must be a JSON-safe object.");
42
+ }
43
+
44
+ assertJsonSafe(context, "context", new Set(), 0);
45
+ const serialized = JSON.stringify(context);
46
+ const bytes = new TextEncoder().encode(serialized);
47
+
48
+ if (bytes.byteLength > MAX_NAVIGATION_CONTEXT_BYTES) {
49
+ throw new RangeError("Navigation context cannot exceed 8 KiB when serialized.");
50
+ }
51
+
52
+ return { serialized, bytes };
53
+ }
54
+
55
+ export function normalizeNavigationContext(context) {
56
+ const result = serializeNavigationContext(context);
57
+ return result ? JSON.parse(result.serialized) : null;
58
+ }
59
+
60
+ export function encodeNavigationContext(context) {
61
+ const result = serializeNavigationContext(context);
62
+ if (!result) return null;
63
+ const base64 = btoa(String.fromCharCode(...result.bytes));
64
+ return base64.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
65
+ }
66
+
67
+ export function decodeNavigationContext(encoded) {
68
+ if (typeof encoded !== "string" || encoded.length === 0) return null;
69
+ if (encoded.length > Math.ceil(MAX_NAVIGATION_CONTEXT_BYTES * 4 / 3) + 4) return null;
70
+
71
+ try {
72
+ const base64 = encoded.replaceAll("-", "+").replaceAll("_", "/");
73
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
74
+ const binary = atob(padded);
75
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
76
+ const context = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
77
+ return normalizeNavigationContext(context);
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ export function readNavigationContext(request) {
84
+ return decodeNavigationContext(
85
+ request?.headers?.get?.(EVOLIT_NAVIGATION_CONTEXT_HEADER) ?? null,
86
+ );
87
+ }
package/src/render.js CHANGED
@@ -206,6 +206,7 @@ async function renderComponentTree(
206
206
  await component({
207
207
  params: requestContext.params,
208
208
  searchParams: requestContext.searchParams,
209
+ navigationContext: requestContext.navigationContext,
209
210
  request,
210
211
  ...extraProps,
211
212
  }, incomingRef),
@@ -221,6 +222,7 @@ async function renderComponentTree(
221
222
  return wrapRouteSegment(segment, await layoutComponents[index]({
222
223
  params: requestContext.params,
223
224
  searchParams: requestContext.searchParams,
225
+ navigationContext: requestContext.navigationContext,
224
226
  request,
225
227
  children,
226
228
  }, incomingRef));
@@ -292,7 +294,14 @@ function createTrackedRouteValues(values) {
292
294
  };
293
295
  }
294
296
 
295
- function createSegmentCacheKey(segment, idPrefix, profile, params, searchParams) {
297
+ function createSegmentCacheKey(
298
+ segment,
299
+ idPrefix,
300
+ profile,
301
+ params,
302
+ searchParams,
303
+ navigationContext,
304
+ ) {
296
305
  const select = (values) => Object.fromEntries(
297
306
  (profile.all ? Object.keys(values) : profile.keys)
298
307
  .sort()
@@ -303,10 +312,11 @@ function createSegmentCacheKey(segment, idPrefix, profile, params, searchParams)
303
312
  idPrefix,
304
313
  params: select(params),
305
314
  searchParams: select(searchParams),
315
+ navigationContext: navigationContext ?? null,
306
316
  });
307
317
  }
308
318
 
309
- function createSegmentInputKey(profile, params, searchParams) {
319
+ function createSegmentInputKey(profile, params, searchParams, navigationContext) {
310
320
  const select = (values) => Object.fromEntries(
311
321
  (profile.all ? Object.keys(values) : profile.keys)
312
322
  .sort()
@@ -315,6 +325,7 @@ function createSegmentInputKey(profile, params, searchParams) {
315
325
  return JSON.stringify({
316
326
  params: select(params),
317
327
  searchParams: select(searchParams),
328
+ navigationContext: navigationContext ?? null,
318
329
  });
319
330
  }
320
331
 
@@ -331,13 +342,20 @@ function createSegmentRenderCache(projectRoot, onDevelopmentEvent) {
331
342
  const profiles = new Map();
332
343
 
333
344
  return {
334
- get(segment, idPrefix, params, searchParams) {
345
+ get(segment, idPrefix, params, searchParams, navigationContext) {
335
346
  const profile = profiles.get(segment.modulePath);
336
347
  if (!profile) {
337
348
  onDevelopmentEvent?.({ type: "segment-cache-miss", modulePath: segment.modulePath, reason: "cold" });
338
349
  return null;
339
350
  }
340
- const key = createSegmentCacheKey(segment, idPrefix, profile, params, searchParams);
351
+ const key = createSegmentCacheKey(
352
+ segment,
353
+ idPrefix,
354
+ profile,
355
+ params,
356
+ searchParams,
357
+ navigationContext,
358
+ );
341
359
  const entry = entries.get(key);
342
360
  if (!entry || entry.expiresAt <= Date.now()) {
343
361
  if (entry) entries.delete(key);
@@ -354,11 +372,27 @@ function createSegmentRenderCache(projectRoot, onDevelopmentEvent) {
354
372
  getProfile(segment) {
355
373
  return profiles.get(segment.modulePath) ?? null;
356
374
  },
357
- set(segment, idPrefix, params, searchParams, profile, result, cachePolicy) {
375
+ set(
376
+ segment,
377
+ idPrefix,
378
+ params,
379
+ searchParams,
380
+ navigationContext,
381
+ profile,
382
+ result,
383
+ cachePolicy,
384
+ ) {
358
385
  const expiresAt = getSegmentCacheExpiry(cachePolicy);
359
386
  if (expiresAt == null) return;
360
387
  profiles.set(segment.modulePath, profile);
361
- const key = createSegmentCacheKey(segment, idPrefix, profile, params, searchParams);
388
+ const key = createSegmentCacheKey(
389
+ segment,
390
+ idPrefix,
391
+ profile,
392
+ params,
393
+ searchParams,
394
+ navigationContext,
395
+ );
362
396
  entries.set(key, { result, expiresAt, modulePath: segment.modulePath });
363
397
  if (entries.size > 256) entries.delete(entries.keys().next().value);
364
398
  },
@@ -425,6 +459,7 @@ async function renderSegmentedComponentTree(
425
459
  const pageValue = await component({
426
460
  params: requestContext.params,
427
461
  searchParams: requestContext.searchParams,
462
+ navigationContext: requestContext.navigationContext,
428
463
  request,
429
464
  ...extraProps,
430
465
  }, incomingRef);
@@ -445,6 +480,7 @@ async function renderSegmentedComponentTree(
445
480
  idPrefix,
446
481
  requestContext.params,
447
482
  requestContext.searchParams,
483
+ requestContext.navigationContext,
448
484
  ) ?? null;
449
485
  if (!layoutResult) {
450
486
  const trackedParams = createTrackedRouteValues(requestContext.params);
@@ -453,6 +489,7 @@ async function renderSegmentedComponentTree(
453
489
  const layoutValue = await layoutComponents[index]({
454
490
  params: trackedParams.value,
455
491
  searchParams: trackedSearchParams.value,
492
+ navigationContext: requestContext.navigationContext,
456
493
  request,
457
494
  children: withForwardedChildRef(html`${unsafeHTML(childrenMarker)}`, childRef),
458
495
  }, incomingRef);
@@ -474,6 +511,7 @@ async function renderSegmentedComponentTree(
474
511
  idPrefix,
475
512
  requestContext.params,
476
513
  requestContext.searchParams,
514
+ requestContext.navigationContext,
477
515
  profile,
478
516
  layoutResult,
479
517
  options.cachePolicy,
@@ -485,6 +523,7 @@ async function renderSegmentedComponentTree(
485
523
  profile,
486
524
  requestContext.params,
487
525
  requestContext.searchParams,
526
+ requestContext.navigationContext,
488
527
  );
489
528
  }
490
529
  results.push(layoutResult);
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { readNavigationContext } from "./navigation-context.js";
2
3
 
3
4
  const REQUEST_CONTEXT_STORAGE = Symbol.for("evolit.request-context.storage");
4
5
  const HTTP_SIGNAL_CLASS = Symbol.for("evolit.request-context.http-signal");
@@ -106,6 +107,7 @@ export function createRequestContext({
106
107
  request,
107
108
  params = {},
108
109
  searchParams = {},
110
+ navigationContext = readNavigationContext(request),
109
111
  extensionValues = {},
110
112
  didUseDynamicRequestData = false,
111
113
  }) {
@@ -113,6 +115,7 @@ export function createRequestContext({
113
115
  request,
114
116
  params: Object.freeze({ ...params }),
115
117
  searchParams: Object.freeze({ ...searchParams }),
118
+ navigationContext,
116
119
  responseHeaders: new Headers(),
117
120
  responseCookies: [],
118
121
  didUseDynamicRequestData: didUseDynamicRequestData === true,
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
3
3
  import path from "node:path";
4
4
  import { BUILD_DIRECTORY, INTERNAL_DIRECTORY, ROUTE_CACHE_DIRECTORY } from "./constants.js";
5
5
  import { ensureDirectory, pathExists, readJson, writeJson } from "./fs-utils.js";
6
+ import { encodeNavigationContext, readNavigationContext } from "./navigation-context.js";
6
7
 
7
8
  function createCacheFileName(cacheKey) {
8
9
  return `${crypto.createHash("sha1").update(cacheKey).digest("hex")}.json`;
@@ -160,7 +161,8 @@ export class ObjectStorageResponseCacheStore {
160
161
 
161
162
  export function createDefaultRouteCacheKey(request) {
162
163
  const url = new URL(request.url);
163
- return `${url.pathname}${url.search}`;
164
+ const context = encodeNavigationContext(readNavigationContext(request));
165
+ return `${url.pathname}${url.search}${context ? `\ncontext:${context}` : ""}`;
164
166
  }
165
167
 
166
168
  export function getRouteCacheArtifactFileName(cacheKey) {