evolit 0.1.0-alpha.20 → 0.1.0-alpha.22

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
@@ -76,6 +76,70 @@ The current runtime is split into a few small layers:
76
76
  - `src/scaffold.js`: creates new site projects from templates
77
77
  - `src/cli.js`: framework entrypoint
78
78
 
79
+ ## Client Navigation
80
+
81
+ Evolit hydrates a small browser router automatically for SSR page documents. It requests a route
82
+ delta, replaces only the changed route segment, and keeps parent layouts mounted when possible.
83
+ Plain `<a>` elements remain valid SSR HTML; same-origin links are progressively intercepted after
84
+ hydration.
85
+
86
+ Use `useNavigation()` inside a LitSX browser component for imperative navigation and pending UI:
87
+
88
+ ```jsx
89
+ import { useNavigation } from "evolit/navigation";
90
+
91
+ export default function CollectionControls() {
92
+ const navigation = useNavigation();
93
+
94
+ function changeSort(event) {
95
+ const searchParams = new URLSearchParams(window.location.search);
96
+ searchParams.delete("page");
97
+ searchParams.delete("skip");
98
+ searchParams.set("sort", event.target.value);
99
+ navigation.push(navigation.createHref("/explore/home-garden", searchParams));
100
+ }
101
+
102
+ return <select onChange={changeSort} disabled={navigation.status === "pending"}>…</select>;
103
+ }
104
+ ```
105
+
106
+ The hook returns `{ status, url, pendingUrl, error, push, replace, refresh, createHref }`:
107
+
108
+ - `push(target)` adds a browser-history entry.
109
+ - `replace(target)` updates the current entry, useful for visual-only query state.
110
+ - `refresh()` bypasses the client delta cache for the current URL.
111
+ - `createHref(pathname, searchParams)` creates a relative internal URL. It accepts standard
112
+ `URLSearchParams`, preserving repeated keys such as `facet=brand&facet=material`.
113
+
114
+ `createHref` can also be imported directly from `evolit/navigation`; it is browser-free and safe to
115
+ share with server-evaluated route code. `useNavigation()` itself is browser-only and must only run
116
+ from a connected client component.
117
+
118
+ ### Progressive links and forms
119
+
120
+ Links work without JavaScript. With JavaScript, Evolit intercepts ordinary same-origin left-clicks.
121
+ Set `data-evolit-navigation="false"` on a link to keep native navigation.
122
+
123
+ Internal `<form method="get">` elements are treated the same way: their successful controls become
124
+ `URLSearchParams` and navigate through a delta. Without JavaScript the browser submits the exact
125
+ same GET form normally. Evolit intentionally does not intercept `POST`, file-upload, external,
126
+ targeted, or opted-out forms.
127
+
128
+ ### Navigation cache and document updates
129
+
130
+ The browser cache is scoped to browser-history entries, not a global URL map. Going back or forward
131
+ can reuse the delta for that exact entry; opening a new branch after going back discards its known
132
+ forward branch. This avoids an unbounded catalog cache in a long-lived tab.
133
+
134
+ - `dynamic` routes are never cached in the browser.
135
+ - `revalidate` entries remain reusable only until their route TTL expires.
136
+ - `static` entries remain reusable while their history entry exists in the current tab session.
137
+
138
+ Each delta also synchronizes route `<title>`, route-specific `<head>` markup, managed styles and
139
+ module preloads, `html`/`body` attributes, scroll position, hash targets, and focus. If a response
140
+ cannot be represented as an Evolit delta —for example a 404 from another adapter— navigation falls
141
+ back to a normal document load.
142
+
79
143
  ## Route Cache Policies
80
144
 
81
145
  Route modules can export a `routeConfig` object with a `cache` policy:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolit",
3
- "version": "0.1.0-alpha.20",
3
+ "version": "0.1.0-alpha.22",
4
4
  "description": "A convention-driven application framework for LitSX and web components.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.10.3",
@@ -20,6 +20,10 @@
20
20
  "main": "./src/index.js",
21
21
  "exports": {
22
22
  ".": "./src/index.js",
23
+ "./navigation": {
24
+ "browser": "./src/navigation-client.js",
25
+ "default": "./src/navigation-server.js"
26
+ },
23
27
  "./server": {
24
28
  "browser": "./src/request-context-browser.js",
25
29
  "default": "./src/server-api.js"
@@ -29,7 +33,8 @@
29
33
  "dev": "node ./src/cli.js dev",
30
34
  "build": "node ./src/cli.js build",
31
35
  "start": "node ./src/cli.js start",
32
- "test": "node --test",
36
+ "test": "node --test test/*.test.js",
37
+ "test:browser": "playwright test",
33
38
  "typecheck": "litsx-tsc -p jsconfig.json --noEmit",
34
39
  "release:check": "yarn test && yarn typecheck && yarn pack --dry-run"
35
40
  },
@@ -58,7 +63,17 @@
58
63
  "typescript": "^6.0.0",
59
64
  "ws": "^8.18.3"
60
65
  },
66
+ "peerDependencies": {
67
+ "@litsx/urql": "^0.3.0"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "@litsx/urql": {
71
+ "optional": true
72
+ }
73
+ },
61
74
  "devDependencies": {
75
+ "@playwright/test": "^1.62.0",
76
+ "@webcomponents/scoped-custom-element-registry": "^0.0.10",
62
77
  "vite": "^8.1.5"
63
78
  }
64
79
  }
package/src/build.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  emitBundledClientAssets,
16
16
  emitHashedClientAssets,
17
17
  normalizeHydrationDataForClient,
18
+ resolveHydrationRootClientImports,
18
19
  resolveRouteClientImports,
19
20
  resolveSharedVendorModuleUrl,
20
21
  rewriteHydrationDataScript,
@@ -39,6 +40,7 @@ import {
39
40
  import { serializeRouteCachePolicy } from "./route-config.js";
40
41
  import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
41
42
  import { ensureDirectory, writeJson } from "./fs-utils.js";
43
+ import { appendSsrUrqlData, runWithOptionalSsrUrqlScope } from "./urql-ssr.js";
42
44
 
43
45
  const CONTENT_TYPE_BY_EXTENSION = new Map([
44
46
  [".css", "text/css; charset=utf-8"],
@@ -247,12 +249,18 @@ export async function buildProject(projectRoot) {
247
249
  ...(Array.isArray(result.clientImports) ? result.clientImports : []),
248
250
  ...resolveRouteClientImports(routeResult, projectRoot, clientAssets),
249
251
  ];
250
- const urls = collectTransitiveAssetPreloads(clientImports, clientAssets);
252
+ const hydratedClientImports = Array.isArray(result.clientImports) ? result.clientImports : [];
253
+ const urls = hydratedClientImports.length > 0
254
+ ? [...new Set([
255
+ ...hydratedClientImports,
256
+ ...collectTransitiveAssetPreloads(hydratedClientImports, clientAssets),
257
+ ])]
258
+ : [];
251
259
  const styleUrls = collectTransitiveStyleUrls(clientImports, clientAssets);
252
260
 
253
261
  return [
254
- ...urls.map((href) => `<link rel="modulepreload" href="${href}">`),
255
- ...styleUrls.map((href) => `<link rel="stylesheet" href="${href}">`),
262
+ ...urls.map((href) => `<link rel="modulepreload" href="${href}" data-evolit-route-asset="preload">`),
263
+ ...styleUrls.map((href) => `<link rel="stylesheet" href="${href}" data-evolit-route-asset="style">`),
256
264
  ].join("\n");
257
265
  },
258
266
  resolveBootstrap({ routeResult, result }) {
@@ -282,7 +290,10 @@ export async function buildProject(projectRoot) {
282
290
  normalizeHydrationDataForClient(
283
291
  result.hydrationData,
284
292
  projectRoot,
285
- routeClientImports,
293
+ [
294
+ ...routeClientImports,
295
+ ...resolveHydrationRootClientImports(result.hydrationData, assetResolver),
296
+ ],
286
297
  ),
287
298
  );
288
299
  },
@@ -331,12 +342,23 @@ export async function buildProject(projectRoot) {
331
342
  seenPrerenderTargets.add(targetPathname);
332
343
 
333
344
  const targetRequest = new Request(`http://evolit.local${targetPathname}`);
334
- const routeResult = await routeResolver.resolveRequest(targetRequest);
335
- if (routeResult.type !== "route" || routeResult.cachePolicy.mode === "dynamic") {
345
+ const { routeResult, response } = await runWithOptionalSsrUrqlScope(async (urqlAdapter) => {
346
+ const resolvedRouteResult = await routeResolver.resolveRequest(targetRequest);
347
+ if (resolvedRouteResult.type !== "route" || resolvedRouteResult.cachePolicy.mode === "dynamic") {
348
+ return { routeResult: resolvedRouteResult, response: null };
349
+ }
350
+
351
+ const renderedResponse = await renderRouteTreeWithAdapter(resolvedRouteResult, ssrAdapter);
352
+ return {
353
+ routeResult: resolvedRouteResult,
354
+ response: urqlAdapter
355
+ ? appendSsrUrqlData(renderedResponse, await urqlAdapter.getUrqlSsrData())
356
+ : renderedResponse,
357
+ };
358
+ });
359
+ if (!response) {
336
360
  continue;
337
361
  }
338
-
339
- const response = await renderRouteTreeWithAdapter(routeResult, ssrAdapter);
340
362
  if (response.status !== 200) {
341
363
  continue;
342
364
  }
@@ -3,6 +3,7 @@ import { existsSync, realpathSync } from "node:fs";
3
3
  import crypto from "node:crypto";
4
4
  import path from "node:path";
5
5
  import { createRequire } from "node:module";
6
+ import { fileURLToPath } from "node:url";
6
7
  import MagicString from "magic-string";
7
8
  import { rollup } from "rollup";
8
9
  import { nodeResolve } from "@rollup/plugin-node-resolve";
@@ -24,10 +25,12 @@ const MODULE_SPECIFIER_PATTERN =
24
25
  /\b(?:import|export)\s*[^"']*?from\s*["']([^"']+)["']|\bimport\s*["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
25
26
 
26
27
  const requireFromHere = createRequire(import.meta.url);
28
+ const EVOLIT_NAVIGATION_BROWSER_ENTRY = fileURLToPath(new URL("./navigation-client.js", import.meta.url));
27
29
  const SHARED_VENDOR_SPECIFIERS = [
28
30
  "@litsx/core",
29
31
  "@litsx/core/elements",
30
32
  "@litsx/ssr/hydration",
33
+ "evolit/navigation",
31
34
  "lit",
32
35
  ];
33
36
  const browserSpecifierFilePathCache = new Map();
@@ -516,6 +519,15 @@ export async function buildSharedVendorRuntime(
516
519
  const bundle = await rollup({
517
520
  input: inputEntries,
518
521
  plugins: [
522
+ {
523
+ name: "evolit-browser-exports",
524
+ resolveId(source) {
525
+ // A consuming application does not normally have an `evolit`
526
+ // package nested in its node_modules. Resolve Evolit's own
527
+ // browser-only public entry before node-resolve marks it external.
528
+ return source === "evolit/navigation" ? EVOLIT_NAVIGATION_BROWSER_ENTRY : null;
529
+ },
530
+ },
519
531
  nodeResolve({
520
532
  browser: true,
521
533
  exportConditions: ["browser", "import", "default"],
@@ -1084,6 +1096,7 @@ export function createHydrationBootstrap({
1084
1096
  hydrationData,
1085
1097
  assetResolver,
1086
1098
  hydrationModuleUrl = "@litsx/ssr/hydration",
1099
+ navigationModuleUrl = null,
1087
1100
  }) {
1088
1101
  const normalizedHydrationData = normalizeHydrationDataForClient(hydrationData);
1089
1102
  const rootModuleUrls = [...new Set([
@@ -1113,6 +1126,7 @@ export function createHydrationBootstrap({
1113
1126
 
1114
1127
  const source = `
1115
1128
  import { hydratePage, registerHydrationModules } from ${JSON.stringify(hydrationModuleUrl)};
1129
+ ${navigationModuleUrl ? `import { getNavigation } from ${JSON.stringify(navigationModuleUrl)};` : ""}
1116
1130
 
1117
1131
  await hydratePage({
1118
1132
  clientImports: ${JSON.stringify(clientImports)},
@@ -1120,6 +1134,7 @@ await hydratePage({
1120
1134
  ${moduleLoaders}
1121
1135
  ])
1122
1136
  });
1137
+ ${navigationModuleUrl ? "getNavigation();" : ""}
1123
1138
  `;
1124
1139
 
1125
1140
  return escapeInlineScriptText(source);
@@ -2017,6 +2032,25 @@ export function resolveRouteClientImports(routeResult, projectRoot, assetManifes
2017
2032
  }).filter((publicUrl) => typeof publicUrl === "string"))];
2018
2033
  }
2019
2034
 
2035
+ /**
2036
+ * Resolve the client modules which define the hydrated custom-element roots.
2037
+ *
2038
+ * Route entry modules are not required to re-export their LitSX components.
2039
+ * The document bootstrap has always loaded these root modules independently;
2040
+ * navigation deltas need the same information in their hydration payload.
2041
+ */
2042
+ export function resolveHydrationRootClientImports(hydrationData, assetResolver) {
2043
+ if (!hydrationData || typeof hydrationData !== "object" || typeof assetResolver !== "function") {
2044
+ return [];
2045
+ }
2046
+
2047
+ return [...new Set(
2048
+ (Array.isArray(hydrationData.roots) ? hydrationData.roots : [])
2049
+ .map((root) => assetResolver(root?.moduleId))
2050
+ .filter((publicUrl) => typeof publicUrl === "string" && publicUrl.length > 0),
2051
+ )];
2052
+ }
2053
+
2020
2054
  export function collectTransitiveStyleUrls(publicUrls, assetManifest) {
2021
2055
  const normalizedManifest = normalizeClientAssetManifest(assetManifest);
2022
2056
  if (!normalizedManifest) {
@@ -17,6 +17,7 @@ import {
17
17
  normalizeHydrationDataForClient,
18
18
  normalizeClientAssetManifest,
19
19
  resetDevelopmentAssetCaches,
20
+ resolveHydrationRootClientImports,
20
21
  resolveRouteClientImports,
21
22
  resolveSharedVendorModuleUrl,
22
23
  resolveBrowserPackageAssetFilePath,
@@ -32,6 +33,8 @@ import {
32
33
  resolveResponseCacheRuntime,
33
34
  } from "./response-cache.js";
34
35
  import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
36
+ import { createNavigationResponseFromDocument } from "./route-segments.js";
37
+ import { appendSsrUrqlData, runWithOptionalSsrUrqlScope } from "./urql-ssr.js";
35
38
  const CONTENT_TYPE_BY_EXTENSION = new Map([
36
39
  [".css", "text/css; charset=utf-8"],
37
40
  [".svg", "image/svg+xml"],
@@ -232,6 +235,11 @@ export async function createRequestRenderer({
232
235
  mode,
233
236
  "@litsx/ssr/hydration",
234
237
  );
238
+ let currentNavigationModuleUrl = await resolveSharedVendorModuleUrl(
239
+ projectRoot,
240
+ mode,
241
+ "evolit/navigation",
242
+ );
235
243
  const devBundledEntries = new Set();
236
244
  const devPreparedClientModules = new Set();
237
245
  const devClientModuleDependencies = new Map();
@@ -244,6 +252,7 @@ export async function createRequestRenderer({
244
252
  selectivelyInvalidatedClientEntries: 0,
245
253
  };
246
254
  const usesFrameworkRouteResolver = !routeResolver;
255
+ let sharedSegmentRenderCache = null;
247
256
 
248
257
  function enqueueDevClientAssetWork(work) {
249
258
  const nextWork = devClientAssetWork.then(work, work);
@@ -261,12 +270,18 @@ export async function createRequestRenderer({
261
270
  return path.resolve(projectRoot, clientModule);
262
271
  }
263
272
  async function createFrameworkRouteResolver() {
264
- return createRouteResolver(projectRoot, mode, {
273
+ const resolver = await createRouteResolver(projectRoot, mode, {
265
274
  onDevelopmentEvent,
275
+ segmentRenderCache: sharedSegmentRenderCache,
266
276
  getStaticAssetPublicUrls() {
267
277
  return createStaticAssetPublicUrlMap(currentAssetManifest);
268
278
  },
279
+ assetResolver(moduleId) {
280
+ return currentAssetResolver(moduleId);
281
+ },
269
282
  });
283
+ sharedSegmentRenderCache ??= resolver.segmentRenderCache;
284
+ return resolver;
270
285
  }
271
286
  let effectiveRouteResolver = routeResolver ?? await createFrameworkRouteResolver();
272
287
  const ssrAdapter = createSsrAdapter({
@@ -278,8 +293,12 @@ export async function createRequestRenderer({
278
293
  ...(Array.isArray(result.clientImports) ? result.clientImports : []),
279
294
  ...resolveRouteClientImports(routeResult, projectRoot, currentAssetManifest),
280
295
  ];
281
- const urls = currentAssetManifest
282
- ? collectTransitiveAssetPreloads(clientImports, currentAssetManifest)
296
+ const hydratedClientImports = Array.isArray(result.clientImports) ? result.clientImports : [];
297
+ const urls = currentAssetManifest && hydratedClientImports.length > 0
298
+ ? [...new Set([
299
+ ...hydratedClientImports,
300
+ ...collectTransitiveAssetPreloads(hydratedClientImports, currentAssetManifest),
301
+ ])]
283
302
  : [];
284
303
  const styleUrls = currentAssetManifest
285
304
  ? collectTransitiveStyleUrls(clientImports, currentAssetManifest)
@@ -289,8 +308,8 @@ export async function createRequestRenderer({
289
308
  }
290
309
 
291
310
  return [
292
- ...urls.map((href) => `<link rel="modulepreload" href="${href}">`),
293
- ...styleUrls.map((href) => `<link rel="stylesheet" href="${href}">`),
311
+ ...urls.map((href) => `<link rel="modulepreload" href="${href}" data-evolit-route-asset="preload">`),
312
+ ...styleUrls.map((href) => `<link rel="stylesheet" href="${href}" data-evolit-route-asset="style">`),
294
313
  ].join("\n");
295
314
  },
296
315
  resolveBootstrap({ routeResult, result }) {
@@ -309,6 +328,7 @@ export async function createRequestRenderer({
309
328
  return currentAssetResolver(moduleId);
310
329
  },
311
330
  hydrationModuleUrl: currentHydrationModuleUrl,
331
+ navigationModuleUrl: currentNavigationModuleUrl,
312
332
  });
313
333
  },
314
334
  transformDocument({ routeResult, result, document }) {
@@ -322,7 +342,13 @@ export async function createRequestRenderer({
322
342
  normalizeHydrationDataForClient(
323
343
  result.hydrationData,
324
344
  projectRoot,
325
- routeClientImports,
345
+ [
346
+ ...routeClientImports,
347
+ ...resolveHydrationRootClientImports(
348
+ result.hydrationData,
349
+ currentAssetResolver,
350
+ ),
351
+ ],
326
352
  ),
327
353
  );
328
354
  },
@@ -380,6 +406,8 @@ export async function createRequestRenderer({
380
406
  });
381
407
 
382
408
  if (usesFrameworkRouteResolver) {
409
+ const segmentEntryCount = effectiveRouteResolver.invalidateSegmentCache?.(changedPaths) ?? 0;
410
+ reportDevelopmentEvent({ type: "segment-cache-invalidated", entryCount: segmentEntryCount });
383
411
  effectiveRouteResolver = await createFrameworkRouteResolver();
384
412
  }
385
413
  });
@@ -475,22 +503,27 @@ export async function createRequestRenderer({
475
503
  return effectiveRouteResolver.resolveRoutePolicy(request);
476
504
  },
477
505
  async renderRoute(request, routePolicyResult = null) {
478
- const shouldPrepareBeforeResolve = mode === "development" && !currentAssetManifest;
479
- let resolvedRoutePolicyResult = routePolicyResult;
480
- if (shouldPrepareBeforeResolve) {
481
- resolvedRoutePolicyResult ??= await effectiveRouteResolver.resolveRoutePolicy(request);
482
- await this.prepareRouteClientArtifacts(resolvedRoutePolicyResult);
483
- }
506
+ return runWithOptionalSsrUrqlScope(async (urqlAdapter) => {
507
+ const shouldPrepareBeforeResolve = mode === "development" && !currentAssetManifest;
508
+ let resolvedRoutePolicyResult = routePolicyResult;
509
+ if (shouldPrepareBeforeResolve) {
510
+ resolvedRoutePolicyResult ??= await effectiveRouteResolver.resolveRoutePolicy(request);
511
+ await this.prepareRouteClientArtifacts(resolvedRoutePolicyResult);
512
+ }
484
513
 
485
- const routeResult = await effectiveRouteResolver.resolveRequest(request, resolvedRoutePolicyResult);
486
- if (!shouldPrepareBeforeResolve || routeResult.boundaryModule) {
487
- await this.prepareRouteClientArtifacts(routeResult);
488
- }
489
- const response = await renderRouteTreeWithAdapter(routeResult, ssrAdapter);
490
- return {
491
- routeResult,
492
- response,
493
- };
514
+ const routeResult = await effectiveRouteResolver.resolveRequest(request, resolvedRoutePolicyResult);
515
+ if (!shouldPrepareBeforeResolve || routeResult.boundaryModule) {
516
+ await this.prepareRouteClientArtifacts(routeResult);
517
+ }
518
+ const renderedResponse = await renderRouteTreeWithAdapter(routeResult, ssrAdapter);
519
+ const response = urqlAdapter
520
+ ? appendSsrUrqlData(renderedResponse, await urqlAdapter.getUrqlSsrData())
521
+ : renderedResponse;
522
+ return {
523
+ routeResult,
524
+ response,
525
+ };
526
+ });
494
527
  },
495
528
  get assetManifest() {
496
529
  return currentAssetManifest;
@@ -536,6 +569,16 @@ export async function createDeploymentRuntime({
536
569
  const runtimeState = {
537
570
  assetManifest: mode === "development" ? null : normalizeClientAssetManifest(assetManifest),
538
571
  };
572
+
573
+ function formatResponseForRequest(request, response) {
574
+ const representation = request.headers.get("accept")?.includes("application/vnd.evolit.navigation+json")
575
+ ? createNavigationResponseFromDocument(response)
576
+ : response;
577
+ // The same URL has an HTML document representation and a navigation
578
+ // delta representation. Tell intermediary HTTP caches that Accept selects
579
+ // between them.
580
+ return addNavigationVaryHeader(representation);
581
+ }
539
582
  const assets = createPublicAssetOrigin({
540
583
  projectRoot,
541
584
  mode,
@@ -579,7 +622,7 @@ export async function createDeploymentRuntime({
579
622
  const cachedResponse = await renderer.responseCacheController.read(request, routePolicyResult);
580
623
  if (cachedResponse) {
581
624
  if (cachedResponse.fresh) {
582
- return cachedResponse.response;
625
+ return formatResponseForRequest(request, cachedResponse.response);
583
626
  }
584
627
 
585
628
  if (
@@ -603,12 +646,15 @@ export async function createDeploymentRuntime({
603
646
  revalidationTasks.set(cachedResponse.cacheKey, revalidationTask);
604
647
  }
605
648
 
606
- return cachedResponse.response;
649
+ return formatResponseForRequest(request, cachedResponse.response);
607
650
  }
608
651
 
609
652
  const { routeResult, response } = await renderer.renderRoute(request, routePolicyResult);
610
653
  runtimeState.assetManifest = normalizeClientAssetManifest(renderer.assetManifest);
611
- return renderer.responseCacheController.write(request, routeResult, response);
654
+ return formatResponseForRequest(
655
+ request,
656
+ await renderer.responseCacheController.write(request, routeResult, response),
657
+ );
612
658
  },
613
659
  async close() {
614
660
  const revalidationTasks = this.revalidationTasks;
@@ -618,3 +664,16 @@ export async function createDeploymentRuntime({
618
664
  },
619
665
  };
620
666
  }
667
+
668
+ function addNavigationVaryHeader(response) {
669
+ if (!response?.headers) return response;
670
+ const fields = String(response.headers.vary ?? "")
671
+ .split(",")
672
+ .map((field) => field.trim())
673
+ .filter(Boolean);
674
+ if (!fields.some((field) => field.toLowerCase() === "accept")) fields.push("accept");
675
+ return {
676
+ ...response,
677
+ headers: { ...response.headers, vary: fields.join(", ") },
678
+ };
679
+ }
@@ -37,6 +37,15 @@ export function createDevelopmentEventReporter() {
37
37
  case "client-assets-cache-hit":
38
38
  console.log(`${terminal.cyan("[evolit]")} ${terminal.blue("●")} Client assets served from cache.`);
39
39
  break;
40
+ case "segment-cache-hit":
41
+ console.log(`${terminal.cyan("[evolit]")} ${terminal.blue("●")} Reused route segment ${terminal.dim(event.modulePath)}.`);
42
+ break;
43
+ case "segment-cache-miss":
44
+ console.log(`${terminal.cyan("[evolit]")} ${terminal.magenta("○")} Rendering route segment ${terminal.dim(event.modulePath)} ${terminal.dim(`(${event.reason})`)}.`);
45
+ break;
46
+ case "segment-cache-invalidated":
47
+ console.log(`${terminal.cyan("[evolit]")} ${terminal.yellow("△")} Invalidated ${event.entryCount} cached route segments.`);
48
+ break;
40
49
  case "invalidated":
41
50
  console.log(
42
51
  `${terminal.cyan("[evolit]")} ${terminal.yellow("△")} Source change detected${event.changedPathCount == null ? "" : ` (${event.changedPathCount} files)`}; invalidated ${event.affectedClientEntryCount} client entries.`,