@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1b930379

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.
Files changed (84) hide show
  1. package/README.md +46 -12
  2. package/dist/bin/rango.js +109 -15
  3. package/dist/vite/index.js +323 -121
  4. package/package.json +15 -16
  5. package/skills/breadcrumbs/SKILL.md +250 -0
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +33 -31
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/loader/SKILL.md +55 -15
  11. package/skills/prerender/SKILL.md +2 -2
  12. package/skills/rango/SKILL.md +0 -1
  13. package/skills/route/SKILL.md +3 -4
  14. package/skills/router-setup/SKILL.md +8 -3
  15. package/skills/typesafety/SKILL.md +25 -23
  16. package/src/__internal.ts +92 -0
  17. package/src/bin/rango.ts +18 -0
  18. package/src/browser/link-interceptor.ts +4 -0
  19. package/src/browser/navigation-bridge.ts +95 -5
  20. package/src/browser/navigation-client.ts +97 -72
  21. package/src/browser/prefetch/cache.ts +112 -25
  22. package/src/browser/prefetch/fetch.ts +28 -30
  23. package/src/browser/prefetch/policy.ts +6 -0
  24. package/src/browser/react/Link.tsx +19 -7
  25. package/src/browser/rsc-router.tsx +11 -2
  26. package/src/browser/server-action-bridge.ts +448 -432
  27. package/src/browser/types.ts +24 -0
  28. package/src/build/generate-route-types.ts +2 -0
  29. package/src/build/route-trie.ts +19 -3
  30. package/src/build/route-types/router-processing.ts +125 -15
  31. package/src/client.rsc.tsx +2 -1
  32. package/src/client.tsx +1 -46
  33. package/src/handles/breadcrumbs.ts +66 -0
  34. package/src/handles/index.ts +1 -0
  35. package/src/host/index.ts +0 -3
  36. package/src/index.rsc.ts +5 -36
  37. package/src/index.ts +32 -66
  38. package/src/prerender/store.ts +56 -15
  39. package/src/route-definition/index.ts +0 -3
  40. package/src/router/handler-context.ts +30 -3
  41. package/src/router/loader-resolution.ts +1 -1
  42. package/src/router/match-api.ts +1 -1
  43. package/src/router/match-result.ts +0 -9
  44. package/src/router/metrics.ts +233 -13
  45. package/src/router/middleware-types.ts +53 -10
  46. package/src/router/middleware.ts +170 -81
  47. package/src/router/pattern-matching.ts +20 -5
  48. package/src/router/prerender-match.ts +4 -0
  49. package/src/router/revalidation.ts +27 -7
  50. package/src/router/router-interfaces.ts +14 -1
  51. package/src/router/router-options.ts +13 -8
  52. package/src/router/segment-resolution/fresh.ts +18 -0
  53. package/src/router/segment-resolution/helpers.ts +1 -1
  54. package/src/router/segment-resolution/revalidation.ts +22 -9
  55. package/src/router/trie-matching.ts +20 -2
  56. package/src/router.ts +29 -9
  57. package/src/rsc/handler.ts +106 -11
  58. package/src/rsc/index.ts +0 -20
  59. package/src/rsc/progressive-enhancement.ts +21 -8
  60. package/src/rsc/rsc-rendering.ts +30 -43
  61. package/src/rsc/server-action.ts +14 -10
  62. package/src/rsc/ssr-setup.ts +128 -0
  63. package/src/rsc/types.ts +2 -0
  64. package/src/search-params.ts +16 -13
  65. package/src/server/context.ts +8 -2
  66. package/src/server/request-context.ts +38 -16
  67. package/src/server.ts +6 -0
  68. package/src/theme/index.ts +4 -13
  69. package/src/types/handler-context.ts +12 -16
  70. package/src/types/route-config.ts +17 -8
  71. package/src/types/segments.ts +0 -5
  72. package/src/vite/discovery/bundle-postprocess.ts +31 -56
  73. package/src/vite/discovery/discover-routers.ts +18 -4
  74. package/src/vite/discovery/prerender-collection.ts +34 -14
  75. package/src/vite/discovery/state.ts +4 -7
  76. package/src/vite/index.ts +4 -3
  77. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  78. package/src/vite/plugins/refresh-cmd.ts +65 -0
  79. package/src/vite/rango.ts +11 -0
  80. package/src/vite/router-discovery.ts +16 -0
  81. package/src/vite/utils/prerender-utils.ts +60 -0
  82. package/skills/testing/SKILL.md +0 -226
  83. package/src/route-definition/route-function.ts +0 -119
  84. /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/index.ts CHANGED
@@ -10,9 +10,6 @@
10
10
  * import from "@rangojs/router/client"
11
11
  */
12
12
 
13
- // Universal rendering utilities (work on both server and client)
14
- export { renderSegments } from "./segment-system.js";
15
-
16
13
  // Error classes (can be used on both server and client)
17
14
  export {
18
15
  RouteNotFoundError,
@@ -22,9 +19,6 @@ export {
22
19
  HandlerError,
23
20
  BuildError,
24
21
  InvalidHandlerError,
25
- NetworkError,
26
- isNetworkError,
27
- sanitizeError,
28
22
  RouterError,
29
23
  Skip,
30
24
  isSkip,
@@ -41,7 +35,6 @@ export type {
41
35
  TrailingSlashMode,
42
36
  // Handler types
43
37
  Handler, // Supports params object, path pattern, or route name
44
- ScopedRouteMap, // Scoped view of GeneratedRouteMap for Handler<"localName", ScopedRouteMap<"prefix">>
45
38
  HandlerContext,
46
39
  ExtractParams,
47
40
  GenericParams,
@@ -115,25 +108,32 @@ export type {
115
108
  // Middleware context types
116
109
  export type { MiddlewareContext, CookieOptions } from "./router/middleware.js";
117
110
 
111
+ function serverOnlyStubError(name: string): Error {
112
+ return new Error(
113
+ `${name}() is only available from "@rangojs/router" in a react-server/RSC environment. ` +
114
+ `For client hooks and components, import from "@rangojs/router/client".`,
115
+ );
116
+ }
117
+
118
118
  /**
119
119
  * Error-throwing stub for server-only `urls` function.
120
120
  */
121
121
  export function urls(): never {
122
- throw new Error("urls() is server-only and requires RSC context.");
122
+ throw serverOnlyStubError("urls");
123
123
  }
124
124
 
125
125
  /**
126
126
  * Error-throwing stub for server-only `createRouter` function.
127
127
  */
128
128
  export function createRouter(): never {
129
- throw new Error("createRouter() is server-only and requires RSC context.");
129
+ throw serverOnlyStubError("createRouter");
130
130
  }
131
131
 
132
132
  /**
133
133
  * Error-throwing stub for server-only `redirect` function.
134
134
  */
135
135
  export function redirect(): never {
136
- throw new Error("redirect() is server-only and requires RSC context.");
136
+ throw serverOnlyStubError("redirect");
137
137
  }
138
138
 
139
139
  // Handle API (universal - works on both server and client)
@@ -149,102 +149,80 @@ export { nonce } from "./rsc/nonce.js";
149
149
  * Error-throwing stub for server-only `Prerender` function.
150
150
  */
151
151
  export function Prerender(): never {
152
- throw new Error("Prerender() is server-only and requires RSC context.");
152
+ throw serverOnlyStubError("Prerender");
153
153
  }
154
154
 
155
155
  /**
156
156
  * Error-throwing stub for server-only `Static` function.
157
157
  */
158
158
  export function Static(): never {
159
- throw new Error("Static() is server-only and requires RSC context.");
159
+ throw serverOnlyStubError("Static");
160
160
  }
161
161
 
162
162
  /**
163
163
  * Error-throwing stub for server-only `getRequestContext` function.
164
164
  */
165
165
  export function getRequestContext(): never {
166
- throw new Error(
167
- "getRequestContext() is server-only and requires RSC context.",
168
- );
166
+ throw serverOnlyStubError("getRequestContext");
169
167
  }
170
168
 
171
169
  /**
172
170
  * Error-throwing stub for server-only `cookies` function.
173
171
  */
174
172
  export function cookies(): never {
175
- throw new Error("cookies() is server-only and requires RSC context.");
173
+ throw serverOnlyStubError("cookies");
176
174
  }
177
175
 
178
176
  /**
179
177
  * Error-throwing stub for server-only `headers` function.
180
178
  */
181
179
  export function headers(): never {
182
- throw new Error("headers() is server-only and requires RSC context.");
180
+ throw serverOnlyStubError("headers");
183
181
  }
184
182
 
185
183
  /**
186
184
  * Error-throwing stub for server-only `createReverse` function.
187
185
  */
188
186
  export function createReverse(): never {
189
- throw new Error("createReverse() is server-only and requires RSC context.");
190
- }
191
-
192
- /**
193
- * Error-throwing stub for server-only `enableMatchDebug` function.
194
- */
195
- export function enableMatchDebug(): never {
196
- throw new Error(
197
- "enableMatchDebug() is server-only and requires RSC context.",
198
- );
199
- }
200
-
201
- /**
202
- * Error-throwing stub for server-only `getMatchDebugStats` function.
203
- */
204
- export function getMatchDebugStats(): never {
205
- throw new Error(
206
- "getMatchDebugStats() is server-only and requires RSC context.",
207
- );
187
+ throw serverOnlyStubError("createReverse");
208
188
  }
209
189
 
210
190
  // Error-throwing stubs for server-only route helpers
211
191
  export function layout(): never {
212
- throw new Error("layout() is server-only and requires RSC context.");
192
+ throw serverOnlyStubError("layout");
213
193
  }
214
194
  export function cache(): never {
215
- throw new Error("cache() is server-only and requires RSC context.");
195
+ throw serverOnlyStubError("cache");
216
196
  }
217
197
  export function middleware(): never {
218
- throw new Error("middleware() is server-only and requires RSC context.");
198
+ throw serverOnlyStubError("middleware");
219
199
  }
220
200
  export function revalidate(): never {
221
- throw new Error("revalidate() is server-only and requires RSC context.");
201
+ throw serverOnlyStubError("revalidate");
222
202
  }
223
203
  export function loader(): never {
224
- throw new Error("loader() is server-only and requires RSC context.");
204
+ throw serverOnlyStubError("loader");
225
205
  }
226
206
  export function loading(): never {
227
- throw new Error("loading() is server-only and requires RSC context.");
207
+ throw serverOnlyStubError("loading");
228
208
  }
229
209
  export function parallel(): never {
230
- throw new Error("parallel() is server-only and requires RSC context.");
210
+ throw serverOnlyStubError("parallel");
231
211
  }
232
212
  export function intercept(): never {
233
- throw new Error("intercept() is server-only and requires RSC context.");
213
+ throw serverOnlyStubError("intercept");
234
214
  }
235
215
  export function when(): never {
236
- throw new Error("when() is server-only and requires RSC context.");
216
+ throw serverOnlyStubError("when");
237
217
  }
238
218
  export function errorBoundary(): never {
239
- throw new Error("errorBoundary() is server-only and requires RSC context.");
219
+ throw serverOnlyStubError("errorBoundary");
240
220
  }
241
221
  export function notFoundBoundary(): never {
242
- throw new Error(
243
- "notFoundBoundary() is server-only and requires RSC context.",
244
- );
222
+ throw serverOnlyStubError("notFoundBoundary");
245
223
  }
246
224
  export function transition(): never {
247
- throw new Error("transition() is server-only and requires RSC context.");
225
+ throw serverOnlyStubError("transition");
248
226
  }
249
227
 
250
228
  // Request context type (safe for client)
@@ -260,14 +238,15 @@ export type {
260
238
  // Meta types
261
239
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
262
240
 
241
+ // Breadcrumb types
242
+ export type { BreadcrumbItem } from "./handles/breadcrumbs.js";
243
+
263
244
  // Reverse type utilities for type-safe URL generation (Django-style URL reversal)
264
245
  export type {
265
246
  ScopedReverseFunction,
266
247
  ReverseFunction,
267
248
  ExtractLocalRoutes,
268
249
  ParamsFor,
269
- SanitizePrefix,
270
- MergeRoutes,
271
250
  } from "./reverse.js";
272
251
  // scopedReverse() helper for handlers to get locally-typed reverse
273
252
  export { scopedReverse } from "./reverse.js";
@@ -287,20 +266,7 @@ export type { PathResponse } from "./href-client.js";
287
266
  export { createConsoleSink } from "./router/telemetry.js";
288
267
  export { createOTelSink } from "./router/telemetry-otel.js";
289
268
  export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
290
- export type {
291
- TelemetrySink,
292
- TelemetryEvent,
293
- RequestStartEvent,
294
- RequestEndEvent,
295
- RequestErrorEvent,
296
- RequestTimeoutEvent,
297
- LoaderStartEvent,
298
- LoaderEndEvent,
299
- LoaderErrorEvent,
300
- HandlerErrorEvent,
301
- CacheDecisionEvent,
302
- RevalidationDecisionEvent,
303
- } from "./router/telemetry.js";
269
+ export type { TelemetrySink, TelemetryEvent } from "./router/telemetry.js";
304
270
 
305
271
  // Timeout types and error class
306
272
  export { RouterTimeoutError } from "./router/timeout.js";
@@ -2,9 +2,10 @@
2
2
  * Prerender Store
3
3
  *
4
4
  * Reads pre-rendered segment data from the worker bundle at build time.
5
- * The data is stored as globalThis.__PRERENDER_MANIFEST, a map of
6
- * "<routeName>/<paramHash>" to dynamic import functions that resolve
7
- * individual prerender entry modules.
5
+ * The manifest module is lazily loaded via globalThis.__loadPrerenderManifestModule,
6
+ * a function injected into the RSC entry that returns the manifest module
7
+ * containing a key-to-specifier map and a `loadPrerenderAsset` function
8
+ * that anchors import() resolution relative to the manifest file.
8
9
  */
9
10
 
10
11
  import type {
@@ -34,11 +35,20 @@ export interface StaticStore {
34
35
  get(handlerId: string): Promise<StaticEntry | null>;
35
36
  }
36
37
 
38
+ interface PrerenderManifestModule {
39
+ default: Record<string, string>;
40
+ loadPrerenderAsset: (
41
+ specifier: string,
42
+ ) => Promise<{ default: PrerenderEntry }>;
43
+ }
44
+
37
45
  declare global {
38
- // Injected by closeBundle post-processing: map of key -> () => import("./assets/__pr-*.js")
46
+ // Injected by closeBundle post-processing: lazy loader for the prerender
47
+ // manifest module. The module exports a key→specifier map and a
48
+ // loadPrerenderAsset function that anchors import() relative to the manifest.
39
49
  // eslint-disable-next-line no-var
40
- var __PRERENDER_MANIFEST:
41
- | Record<string, () => Promise<{ default: PrerenderEntry }>>
50
+ var __loadPrerenderManifestModule:
51
+ | (() => Promise<PrerenderManifestModule>)
42
52
  | undefined;
43
53
  // Injected by closeBundle post-processing: map of handlerId -> () => import("./assets/__st-*.js")
44
54
  // Asset default export is either a string (no handles) or { encoded, handles } object.
@@ -78,17 +88,28 @@ export function createDevPrerenderStore(devUrl: string): PrerenderStore {
78
88
  /**
79
89
  * Create a prerender store.
80
90
  * Dev mode: on-demand fetch from Vite dev server (node:fs works there).
81
- * Production: backed by globalThis.__PRERENDER_MANIFEST injected at build time.
91
+ * Production: backed by globalThis.__loadPrerenderManifestModule which lazily
92
+ * loads the manifest module on first access.
82
93
  * Returns null if no prerender data is available.
83
94
  */
84
95
  export function createPrerenderStore(): PrerenderStore | null {
85
96
  if (globalThis.__PRERENDER_DEV_URL) {
86
97
  return createDevPrerenderStore(globalThis.__PRERENDER_DEV_URL);
87
98
  }
88
- const manifest = globalThis.__PRERENDER_MANIFEST;
89
- if (!manifest || Object.keys(manifest).length === 0) return null;
99
+ if (!globalThis.__loadPrerenderManifestModule) return null;
90
100
 
91
101
  const cache = new Map<string, Promise<PrerenderEntry | null>>();
102
+ let manifestModulePromise: Promise<PrerenderManifestModule | null> | null =
103
+ null;
104
+
105
+ function loadManifestModule(): Promise<PrerenderManifestModule | null> {
106
+ if (!manifestModulePromise) {
107
+ manifestModulePromise = globalThis.__loadPrerenderManifestModule!().catch(
108
+ () => null,
109
+ );
110
+ }
111
+ return manifestModulePromise;
112
+ }
92
113
 
93
114
  return {
94
115
  get(routeName: string, paramHash: string): Promise<PrerenderEntry | null> {
@@ -96,18 +117,38 @@ export function createPrerenderStore(): PrerenderStore | null {
96
117
  const cached = cache.get(key);
97
118
  if (cached) return cached;
98
119
 
99
- const loader = manifest[key];
100
- if (!loader) return Promise.resolve(null);
101
-
102
- const promise = loader()
103
- .then((mod) => mod.default)
104
- .catch(() => null);
120
+ const promise = loadManifestModule().then((mod) => {
121
+ if (!mod) return null;
122
+ const specifier = mod.default[key];
123
+ if (!specifier) return null;
124
+ return mod
125
+ .loadPrerenderAsset(specifier)
126
+ .then((asset) => asset.default)
127
+ .catch(() => null);
128
+ });
105
129
  cache.set(key, promise);
106
130
  return promise;
107
131
  },
108
132
  };
109
133
  }
110
134
 
135
+ /**
136
+ * Load the prerender manifest index for test introspection.
137
+ * Returns the key→specifier map or null if unavailable.
138
+ */
139
+ export async function loadPrerenderManifestIndex(): Promise<Record<
140
+ string,
141
+ string
142
+ > | null> {
143
+ if (!globalThis.__loadPrerenderManifestModule) return null;
144
+ try {
145
+ const mod = await globalThis.__loadPrerenderManifestModule();
146
+ return mod.default;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
111
152
  /**
112
153
  * Create a static segment store.
113
154
  * Production only: backed by globalThis.__STATIC_MANIFEST injected at build time.
@@ -1,6 +1,3 @@
1
- // Route definition
2
- export { route, type RouteDefinitionResult } from "./route-function.js";
3
-
4
1
  // Type definitions
5
2
  export type { RouteHelpers } from "./helpers-types.js";
6
3
  export type {
@@ -9,7 +9,7 @@ import { _getRequestContext } from "../server/request-context.js";
9
9
  import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
10
10
  import { parseSearchParams, serializeSearchParams } from "../search-params.js";
11
11
  import { contextGet, contextSet } from "../context-var.js";
12
- import { NOCACHE_SYMBOL } from "../cache/taint.js";
12
+ import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
13
13
  import { isAutoGeneratedRouteName } from "../route-name.js";
14
14
  import { PRERENDER_PASSTHROUGH } from "../prerender.js";
15
15
 
@@ -213,7 +213,28 @@ export function createHandlerContext<TEnv>(
213
213
  const stubResponse =
214
214
  requestContext?.res ?? new Response(null, { status: 200 });
215
215
 
216
- const ctx: InternalHandlerContext<any, TEnv> = {
216
+ // Guard mutating Headers methods so they throw inside "use cache" functions.
217
+ // Uses lazy `ctx` reference (assigned below) — only the specific handler ctx
218
+ // is stamped by cache-runtime, not the shared request context.
219
+ const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
220
+ let ctx: InternalHandlerContext<any, TEnv>;
221
+ const guardedHeaders = new Proxy(stubResponse.headers, {
222
+ get(target, prop, receiver) {
223
+ const value = Reflect.get(target, prop, receiver);
224
+ if (typeof value === "function") {
225
+ if (MUTATING_HEADERS_METHODS.has(prop as string)) {
226
+ return (...args: any[]) => {
227
+ assertNotInsideCacheExec(ctx, "headers");
228
+ return value.apply(target, args);
229
+ };
230
+ }
231
+ return value.bind(target);
232
+ }
233
+ return value;
234
+ },
235
+ });
236
+
237
+ ctx = {
217
238
  params,
218
239
  build: false,
219
240
  request,
@@ -221,6 +242,7 @@ export function createHandlerContext<TEnv>(
221
242
  search: searchSchema ? resolvedSearchParams : {},
222
243
  pathname,
223
244
  url,
245
+ originalUrl: new URL(request.url),
224
246
  env: bindings,
225
247
  var: variables,
226
248
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as HandlerContext<
@@ -228,10 +250,11 @@ export function createHandlerContext<TEnv>(
228
250
  TEnv
229
251
  >["get"],
230
252
  set: ((keyOrVar: any, value: any) => {
253
+ assertNotInsideCacheExec(ctx, "set");
231
254
  contextSet(variables, keyOrVar, value);
232
255
  }) as HandlerContext<any, TEnv>["set"],
233
256
  res: stubResponse, // Stub response for setting headers
234
- headers: stubResponse.headers, // Shorthand for res.headers
257
+ headers: guardedHeaders, // Guarded shorthand for res.headers
235
258
  // Placeholder use() - will be replaced with actual implementation during request
236
259
  use: () => {
237
260
  throw new Error("ctx.use() called before loaders were initialized");
@@ -304,6 +327,7 @@ export function createPrerenderContext<TEnv>(
304
327
  search: {},
305
328
  pathname,
306
329
  url: syntheticUrl,
330
+ originalUrl: syntheticUrl,
307
331
  get env(): TEnv {
308
332
  return throwUnavailable("env");
309
333
  },
@@ -385,6 +409,9 @@ export function createStaticContext<TEnv>(
385
409
  get url(): URL {
386
410
  return throwUnavailable("url");
387
411
  },
412
+ get originalUrl(): URL {
413
+ return throwUnavailable("originalUrl");
414
+ },
388
415
  get env(): TEnv {
389
416
  return throwUnavailable("env");
390
417
  },
@@ -262,7 +262,7 @@ function createLoaderExecutor<TEnv>(
262
262
  reverse: ctx.reverse as LoaderContext["reverse"],
263
263
  };
264
264
 
265
- const doneLoader = track(`loader:${loader.$$id}`);
265
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
266
266
  const promise = Promise.resolve(
267
267
  loaderFn(loaderCtx as LoaderContext<any, TEnv>),
268
268
  ).finally(() => {
@@ -591,7 +591,7 @@ export async function matchError<TEnv>(
591
591
 
592
592
  const reqCtx = getRequestContext();
593
593
  if (reqCtx) {
594
- reqCtx.setStatus(500);
594
+ reqCtx._setStatus(500);
595
595
  }
596
596
 
597
597
  const effectiveFallback = fallback || DefaultErrorFallback;
@@ -108,7 +108,6 @@
108
108
  */
109
109
  import type { MatchResult, ResolvedSegment } from "../types.js";
110
110
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
111
- import { generateServerTiming, logMetrics } from "./metrics.js";
112
111
  import { debugLog } from "./logging.js";
113
112
 
114
113
  /**
@@ -186,20 +185,12 @@ export function buildMatchResult<TEnv>(
186
185
  segmentIds: segmentsToRender.map((s) => s.id),
187
186
  });
188
187
 
189
- // Output metrics if enabled
190
- let serverTiming: string | undefined;
191
- if (ctx.metricsStore) {
192
- logMetrics(ctx.request.method, ctx.pathname, ctx.metricsStore);
193
- serverTiming = generateServerTiming(ctx.metricsStore);
194
- }
195
-
196
188
  return {
197
189
  segments: segmentsToRender,
198
190
  matched: allIds,
199
191
  diff: segmentsToRender.map((s) => s.id),
200
192
  params: ctx.matched.params,
201
193
  routeName: ctx.routeKey,
202
- serverTiming,
203
194
  slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
204
195
  routeMiddleware:
205
196
  ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,