@rangojs/router 0.0.0-experimental.2

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 (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,554 @@
1
+ /**
2
+ * Request Context - AsyncLocalStorage for passing request-scoped data throughout rendering
3
+ *
4
+ * This is the unified context used everywhere:
5
+ * - Middleware execution
6
+ * - Route handlers and loaders
7
+ * - Server components during rendering
8
+ * - Error boundaries and streaming
9
+ *
10
+ * Available via getRequestContext() anywhere in the request lifecycle.
11
+ */
12
+
13
+ import { AsyncLocalStorage } from "node:async_hooks";
14
+ import type { CookieOptions } from "../router/middleware.js";
15
+ import type { LoaderDefinition, LoaderContext } from "../types.js";
16
+ import type { Handle } from "../handle.js";
17
+ import { createHandleStore, type HandleStore } from "./handle-store.js";
18
+ import { isHandle } from "../handle.js";
19
+ import { track } from "./context.js";
20
+ import type { SegmentCacheStore } from "../cache/types.js";
21
+ import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
22
+ import { THEME_COOKIE } from "../theme/constants.js";
23
+
24
+ /**
25
+ * Unified request context available via getRequestContext()
26
+ *
27
+ * This is the same context passed to middleware and handlers.
28
+ * Use this when you need access to request data outside of route handlers.
29
+ */
30
+ export interface RequestContext<
31
+ TEnv = unknown,
32
+ TParams = Record<string, string>,
33
+ > {
34
+ /** Platform bindings (Cloudflare env, etc.) */
35
+ env: TEnv;
36
+ /** Original HTTP request */
37
+ request: Request;
38
+ /** Parsed URL (system params like _rsc* are NOT filtered here) */
39
+ url: URL;
40
+ /** URL pathname */
41
+ pathname: string;
42
+ /** URL search params (system params like _rsc* are NOT filtered here) */
43
+ searchParams: URLSearchParams;
44
+ /** Variables set by middleware (same as ctx.var) */
45
+ var: Record<string, any>;
46
+ /** Get a variable set by middleware */
47
+ get: <K extends string>(key: K) => any;
48
+ /** Set a variable (shared with middleware and handlers) */
49
+ set: <K extends string>(key: K, value: any) => void;
50
+ /**
51
+ * Route params (populated after route matching)
52
+ * Initially empty, then set to matched params
53
+ */
54
+ params: TParams;
55
+ /**
56
+ * Stub response for setting headers/cookies
57
+ * Headers set here are merged into the final response
58
+ */
59
+ res: Response;
60
+
61
+ /** Get a cookie value from the request */
62
+ cookie(name: string): string | undefined;
63
+ /** Get all cookies from the request */
64
+ cookies(): Record<string, string>;
65
+ /** Set a cookie on the response */
66
+ setCookie(name: string, value: string, options?: CookieOptions): void;
67
+ /** Delete a cookie */
68
+ deleteCookie(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
69
+ /** Set a response header */
70
+ header(name: string, value: string): void;
71
+
72
+ /**
73
+ * Access loader data or push handle data.
74
+ *
75
+ * For loaders: Returns a promise that resolves to the loader data.
76
+ * Loaders are executed in parallel and memoized per request.
77
+ *
78
+ * For handles: Returns a push function to add data for this segment.
79
+ * Handle data accumulates across all matched route segments.
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * // Loader usage
84
+ * const cart = await ctx.use(CartLoader);
85
+ *
86
+ * // Handle usage
87
+ * const push = ctx.use(Breadcrumbs);
88
+ * push({ label: "Shop", href: "/shop" });
89
+ * ```
90
+ */
91
+ use: {
92
+ <T, TLoaderParams = any>(loader: LoaderDefinition<T, TLoaderParams>): Promise<T>;
93
+ <TData, TAccumulated = TData[]>(handle: Handle<TData, TAccumulated>): (
94
+ data: TData | Promise<TData> | (() => Promise<TData>)
95
+ ) => void;
96
+ };
97
+
98
+ /** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
99
+ method: string;
100
+
101
+ /** @internal Handle store for tracking handle data across segments */
102
+ _handleStore: HandleStore;
103
+
104
+ /** @internal Cache store for segment caching (optional, used by CacheScope) */
105
+ _cacheStore?: SegmentCacheStore;
106
+
107
+ /**
108
+ * Schedule work to run after the response is sent.
109
+ * On Cloudflare Workers, uses ctx.waitUntil().
110
+ * On Node.js, runs as fire-and-forget.
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * ctx.waitUntil(async () => {
115
+ * await cacheStore.set(key, data, ttl);
116
+ * });
117
+ * ```
118
+ */
119
+ waitUntil(fn: () => Promise<void>): void;
120
+
121
+ /**
122
+ * Register a callback to run when the response is created.
123
+ * Callbacks are sync and receive the response. They can:
124
+ * - Inspect response status/headers
125
+ * - Return a modified response
126
+ * - Schedule async work via waitUntil
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * ctx.onResponse((res) => {
131
+ * if (res.status === 200) {
132
+ * ctx.waitUntil(async () => await cacheIt());
133
+ * }
134
+ * return res;
135
+ * });
136
+ * ```
137
+ */
138
+ onResponse(callback: (response: Response) => Response): void;
139
+
140
+ /** @internal Registered onResponse callbacks */
141
+ _onResponseCallbacks: Array<(response: Response) => Response>;
142
+
143
+ /**
144
+ * Current theme setting (only available when theme is enabled in router config)
145
+ *
146
+ * Returns the theme value from the cookie, or the default theme if not set.
147
+ * This is the user's preference ("light", "dark", or "system"), not the resolved value.
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * route("settings", (ctx) => {
152
+ * const currentTheme = ctx.theme; // "light" | "dark" | "system" | undefined
153
+ * return <SettingsPage theme={currentTheme} />;
154
+ * });
155
+ * ```
156
+ */
157
+ theme?: Theme;
158
+
159
+ /**
160
+ * Set the theme (only available when theme is enabled in router config)
161
+ *
162
+ * Sets a cookie with the new theme value. The change takes effect on the next request.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * route("settings", (ctx) => {
167
+ * if (ctx.method === "POST") {
168
+ * const formData = await ctx.request.formData();
169
+ * const newTheme = formData.get("theme") as Theme;
170
+ * ctx.setTheme(newTheme);
171
+ * }
172
+ * return <SettingsPage />;
173
+ * });
174
+ * ```
175
+ */
176
+ setTheme?: (theme: Theme) => void;
177
+
178
+ /** @internal Theme configuration (null if theme not enabled) */
179
+ _themeConfig?: ResolvedThemeConfig | null;
180
+ }
181
+
182
+ // AsyncLocalStorage instance for request context
183
+ const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
184
+
185
+ /**
186
+ * Run a function within a request context
187
+ * Used by the RSC handler to provide context to server actions
188
+ */
189
+ export function runWithRequestContext<TEnv, T>(
190
+ context: RequestContext<TEnv>,
191
+ fn: () => T
192
+ ): T {
193
+ return requestContextStorage.run(context, fn);
194
+ }
195
+
196
+ /**
197
+ * Get the current request context
198
+ * Returns undefined if not running within a request context
199
+ */
200
+ export function getRequestContext<TEnv = unknown>():
201
+ | RequestContext<TEnv>
202
+ | undefined {
203
+ return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
204
+ }
205
+
206
+ /**
207
+ * Update params on the current request context
208
+ * Called after route matching to populate route params
209
+ */
210
+ export function setRequestContextParams(params: Record<string, string>): void {
211
+ const ctx = requestContextStorage.getStore();
212
+ if (ctx) {
213
+ ctx.params = params;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Get the current request context, throwing if not available
219
+ * Use this when context is required (e.g., in loader actions)
220
+ */
221
+ export function requireRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
222
+ const ctx = getRequestContext<TEnv>();
223
+ if (!ctx) {
224
+ throw new Error(
225
+ "Request context not available. This function must be called from within a server action " +
226
+ "executed through the RSC handler."
227
+ );
228
+ }
229
+ return ctx;
230
+ }
231
+
232
+ /**
233
+ * Cloudflare Workers ExecutionContext (subset we need)
234
+ */
235
+ export interface ExecutionContext {
236
+ waitUntil(promise: Promise<any>): void;
237
+ passThroughOnException(): void;
238
+ }
239
+
240
+ /**
241
+ * Options for creating a request context
242
+ */
243
+ export interface CreateRequestContextOptions<TEnv> {
244
+ env: TEnv;
245
+ request: Request;
246
+ url: URL;
247
+ variables: Record<string, any>;
248
+ /** Optional cache store for segment caching (used by CacheScope) */
249
+ cacheStore?: SegmentCacheStore;
250
+ /** Optional Cloudflare execution context for waitUntil support */
251
+ executionContext?: ExecutionContext;
252
+ /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
253
+ themeConfig?: ResolvedThemeConfig | null;
254
+ }
255
+
256
+ /**
257
+ * Create a full request context with all methods implemented
258
+ *
259
+ * This is used by the RSC handler to create the unified context that's:
260
+ * - Available via getRequestContext() throughout the request
261
+ * - Passed to middleware as ctx
262
+ * - Passed to handlers as ctx
263
+ */
264
+ export function createRequestContext<TEnv>(
265
+ options: CreateRequestContextOptions<TEnv>
266
+ ): RequestContext<TEnv> {
267
+ const { env, request, url, variables, cacheStore, executionContext, themeConfig } = options;
268
+ const cookieHeader = request.headers.get("Cookie");
269
+ let parsedCookies: Record<string, string> | null = null;
270
+
271
+ // Create stub response for collecting headers/cookies
272
+ const stubResponse = new Response(null, { status: 200 });
273
+
274
+ // Create handle store and loader memoization for this request
275
+ const handleStore = createHandleStore();
276
+ const loaderPromises = new Map<string, Promise<any>>();
277
+
278
+ // Lazy parse cookies
279
+ const getParsedCookies = (): Record<string, string> => {
280
+ if (!parsedCookies) {
281
+ parsedCookies = parseCookiesFromHeader(cookieHeader);
282
+ }
283
+ return parsedCookies;
284
+ };
285
+
286
+ // Theme helpers (only used when themeConfig is provided)
287
+ const getTheme = (): Theme | undefined => {
288
+ if (!themeConfig) return undefined;
289
+
290
+ const stored = getParsedCookies()[themeConfig.storageKey];
291
+ if (stored) {
292
+ // Validate stored value
293
+ if (stored === "system" && themeConfig.enableSystem) {
294
+ return "system";
295
+ }
296
+ if (themeConfig.themes.includes(stored)) {
297
+ return stored as Theme;
298
+ }
299
+ }
300
+ return themeConfig.defaultTheme;
301
+ };
302
+
303
+ const setTheme = (theme: Theme): void => {
304
+ if (!themeConfig) return;
305
+
306
+ // Validate theme value
307
+ if (theme !== "system" && !themeConfig.themes.includes(theme)) {
308
+ console.warn(`[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`);
309
+ return;
310
+ }
311
+
312
+ // Set cookie
313
+ stubResponse.headers.append(
314
+ "Set-Cookie",
315
+ serializeCookieValue(themeConfig.storageKey, theme, {
316
+ path: THEME_COOKIE.path,
317
+ maxAge: THEME_COOKIE.maxAge,
318
+ sameSite: THEME_COOKIE.sameSite,
319
+ })
320
+ );
321
+ };
322
+
323
+ // Build the context object first (without use), then add use
324
+ const ctx: RequestContext<TEnv> = {
325
+ env,
326
+ request,
327
+ url,
328
+ pathname: url.pathname,
329
+ searchParams: url.searchParams,
330
+ var: variables,
331
+ get: <K extends string>(key: K) => variables[key],
332
+ set: <K extends string>(key: K, value: any) => {
333
+ variables[key] = value;
334
+ },
335
+ params: {} as Record<string, string>,
336
+ res: stubResponse,
337
+
338
+ cookie(name: string): string | undefined {
339
+ return getParsedCookies()[name];
340
+ },
341
+
342
+ cookies(): Record<string, string> {
343
+ return { ...getParsedCookies() };
344
+ },
345
+
346
+ setCookie(name: string, value: string, options?: CookieOptions): void {
347
+ stubResponse.headers.append(
348
+ "Set-Cookie",
349
+ serializeCookieValue(name, value, options)
350
+ );
351
+ },
352
+
353
+ deleteCookie(
354
+ name: string,
355
+ options?: Pick<CookieOptions, "domain" | "path">
356
+ ): void {
357
+ stubResponse.headers.append(
358
+ "Set-Cookie",
359
+ serializeCookieValue(name, "", { ...options, maxAge: 0 })
360
+ );
361
+ },
362
+
363
+ header(name: string, value: string): void {
364
+ stubResponse.headers.set(name, value);
365
+ },
366
+
367
+ // Placeholder - will be replaced below
368
+ use: null as any,
369
+
370
+ method: request.method,
371
+
372
+ _handleStore: handleStore,
373
+ _cacheStore: cacheStore,
374
+
375
+ waitUntil(fn: () => Promise<void>): void {
376
+ if (executionContext?.waitUntil) {
377
+ // Cloudflare Workers: use native waitUntil
378
+ executionContext.waitUntil(fn());
379
+ } else {
380
+ // Node.js / dev: fire-and-forget with error logging
381
+ fn().catch((err) => console.error("[waitUntil] Background task failed:", err));
382
+ }
383
+ },
384
+
385
+ _onResponseCallbacks: [],
386
+
387
+ onResponse(callback: (response: Response) => Response): void {
388
+ this._onResponseCallbacks.push(callback);
389
+ },
390
+
391
+ // Theme properties (only set when themeConfig is provided)
392
+ theme: themeConfig ? getTheme() : undefined,
393
+ setTheme: themeConfig ? setTheme : undefined,
394
+ _themeConfig: themeConfig,
395
+ };
396
+
397
+ // Now create use() with access to ctx
398
+ ctx.use = createUseFunction({
399
+ handleStore,
400
+ loaderPromises,
401
+ getContext: () => ctx,
402
+ });
403
+
404
+ return ctx;
405
+ }
406
+
407
+ /**
408
+ * Parse cookies from Cookie header
409
+ */
410
+ function parseCookiesFromHeader(
411
+ cookieHeader: string | null
412
+ ): Record<string, string> {
413
+ if (!cookieHeader) return {};
414
+
415
+ const cookies: Record<string, string> = {};
416
+ const pairs = cookieHeader.split(";");
417
+
418
+ for (const pair of pairs) {
419
+ const [name, ...rest] = pair.trim().split("=");
420
+ if (name) {
421
+ cookies[name] = decodeURIComponent(rest.join("="));
422
+ }
423
+ }
424
+
425
+ return cookies;
426
+ }
427
+
428
+ /**
429
+ * Serialize a cookie for Set-Cookie header
430
+ */
431
+ function serializeCookieValue(
432
+ name: string,
433
+ value: string,
434
+ options: CookieOptions = {}
435
+ ): string {
436
+ let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
437
+
438
+ if (options.domain) cookie += `; Domain=${options.domain}`;
439
+ if (options.path) cookie += `; Path=${options.path}`;
440
+ if (options.maxAge !== undefined) cookie += `; Max-Age=${options.maxAge}`;
441
+ if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
442
+ if (options.httpOnly) cookie += "; HttpOnly";
443
+ if (options.secure) cookie += "; Secure";
444
+ if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
445
+
446
+ return cookie;
447
+ }
448
+
449
+ /**
450
+ * Options for creating the use() function
451
+ */
452
+ export interface CreateUseFunctionOptions<TEnv> {
453
+ handleStore: HandleStore;
454
+ loaderPromises: Map<string, Promise<any>>;
455
+ getContext: () => RequestContext<TEnv>;
456
+ }
457
+
458
+ /**
459
+ * Create the use() function for loader and handle composition.
460
+ *
461
+ * This is the unified implementation used by both RequestContext and HandlerContext.
462
+ * - For loaders: executes and memoizes loader functions
463
+ * - For handles: returns a push function to add handle data
464
+ */
465
+ export function createUseFunction<TEnv>(
466
+ options: CreateUseFunctionOptions<TEnv>
467
+ ): RequestContext["use"] {
468
+ const { handleStore, loaderPromises, getContext } = options;
469
+
470
+ return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
471
+ // Handle case: return a push function
472
+ if (isHandle(item)) {
473
+ const handle = item;
474
+ const ctx = getContext();
475
+ const segmentId = (ctx as any)._currentSegmentId;
476
+
477
+ if (!segmentId) {
478
+ throw new Error(
479
+ `Handle "${handle.$$id}" used outside of handler context. ` +
480
+ `Handles must be used within route/layout handlers.`
481
+ );
482
+ }
483
+
484
+ // Return a push function bound to this handle and segment
485
+ return (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
486
+ // If it's a function, call it immediately to get the promise
487
+ const valueOrPromise = typeof dataOrFn === "function"
488
+ ? (dataOrFn as () => Promise<unknown>)()
489
+ : dataOrFn;
490
+
491
+ // Push directly - promises will be serialized by RSC and streamed
492
+ handleStore.push(handle.$$id, segmentId, valueOrPromise);
493
+ };
494
+ }
495
+
496
+ // Loader case
497
+ const loader = item as LoaderDefinition<any, any>;
498
+
499
+ // Return cached promise if already started
500
+ if (loaderPromises.has(loader.$$id)) {
501
+ return loaderPromises.get(loader.$$id);
502
+ }
503
+
504
+ // Get loader function - either from loader object or fetchable registry
505
+ let loaderFn = loader.fn;
506
+ if (!loaderFn) {
507
+ // Lazy import to avoid circular dependency
508
+ const { getFetchableLoader } = require("../loader.rsc.js");
509
+ const fetchable = getFetchableLoader(loader.$$id);
510
+ if (fetchable) {
511
+ loaderFn = fetchable.fn;
512
+ }
513
+ }
514
+
515
+ if (!loaderFn) {
516
+ throw new Error(
517
+ `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`
518
+ );
519
+ }
520
+
521
+ const ctx = getContext();
522
+
523
+ // Create loader context with recursive use() support
524
+ const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
525
+ params: ctx.params,
526
+ request: ctx.request,
527
+ searchParams: ctx.searchParams,
528
+ pathname: ctx.pathname,
529
+ url: ctx.url,
530
+ env: ctx.env as any,
531
+ var: ctx.var as any,
532
+ get: ctx.get as any,
533
+ use: <TDep, TDepParams = any>(
534
+ dep: LoaderDefinition<TDep, TDepParams>
535
+ ): Promise<TDep> => {
536
+ // Recursive call - will start dep loader if not already started
537
+ return ctx.use(dep);
538
+ },
539
+ method: "GET",
540
+ body: undefined,
541
+ };
542
+
543
+ // Start loader execution with tracking
544
+ const doneLoader = track(`loader:${loader.$$id}`);
545
+ const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
546
+ doneLoader();
547
+ });
548
+
549
+ // Memoize for subsequent calls
550
+ loaderPromises.set(loader.$$id, promise);
551
+
552
+ return promise;
553
+ }) as RequestContext["use"];
554
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ import { Outlet } from "../client.js";
3
+
4
+ const MapRootLayout = (
5
+ <>
6
+ <Outlet />
7
+ </>
8
+ ) as ReactNode;
9
+
10
+ export default MapRootLayout;
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "lib": ["ES2020"],
5
+ "types": ["node", "vite/client"],
6
+ "typeRoots": ["../../node_modules/@types"],
7
+ "moduleResolution": "bundler",
8
+ "rootDir": ".",
9
+ "outDir": "../../dist/server",
10
+ "composite": true,
11
+ "verbatimModuleSyntax": true
12
+ },
13
+ "include": ["./**/*"]
14
+ }