@rangojs/router 0.0.0-experimental.29 → 0.0.0-experimental.30

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.
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
1745
1745
  // package.json
1746
1746
  var package_default = {
1747
1747
  name: "@rangojs/router",
1748
- version: "0.0.0-experimental.29",
1748
+ version: "0.0.0-experimental.30",
1749
1749
  description: "Django-inspired RSC router with composable URL patterns",
1750
1750
  keywords: [
1751
1751
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.29",
3
+ "version": "0.0.0-experimental.30",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -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,6 +213,24 @@ export function createHandlerContext<TEnv>(
213
213
  const stubResponse =
214
214
  requestContext?.res ?? new Response(null, { status: 200 });
215
215
 
216
+ // Guard mutating Headers methods so they throw inside "use cache" functions.
217
+ const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
218
+ const guardedHeaders = new Proxy(stubResponse.headers, {
219
+ get(target, prop, receiver) {
220
+ const value = Reflect.get(target, prop, receiver);
221
+ if (typeof value === "function") {
222
+ if (MUTATING_HEADERS_METHODS.has(prop as string)) {
223
+ return (...args: any[]) => {
224
+ assertNotInsideCacheExec(requestContext, "headers");
225
+ return value.apply(target, args);
226
+ };
227
+ }
228
+ return value.bind(target);
229
+ }
230
+ return value;
231
+ },
232
+ });
233
+
216
234
  const ctx: InternalHandlerContext<any, TEnv> = {
217
235
  params,
218
236
  build: false,
@@ -221,6 +239,7 @@ export function createHandlerContext<TEnv>(
221
239
  search: searchSchema ? resolvedSearchParams : {},
222
240
  pathname,
223
241
  url,
242
+ originalUrl: new URL(request.url),
224
243
  env: bindings,
225
244
  var: variables,
226
245
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as HandlerContext<
@@ -228,10 +247,11 @@ export function createHandlerContext<TEnv>(
228
247
  TEnv
229
248
  >["get"],
230
249
  set: ((keyOrVar: any, value: any) => {
250
+ assertNotInsideCacheExec(requestContext, "set");
231
251
  contextSet(variables, keyOrVar, value);
232
252
  }) as HandlerContext<any, TEnv>["set"],
233
253
  res: stubResponse, // Stub response for setting headers
234
- headers: stubResponse.headers, // Shorthand for res.headers
254
+ headers: guardedHeaders, // Guarded shorthand for res.headers
235
255
  // Placeholder use() - will be replaced with actual implementation during request
236
256
  use: () => {
237
257
  throw new Error("ctx.use() called before loaders were initialized");
@@ -304,6 +324,7 @@ export function createPrerenderContext<TEnv>(
304
324
  search: {},
305
325
  pathname,
306
326
  url: syntheticUrl,
327
+ originalUrl: syntheticUrl,
307
328
  get env(): TEnv {
308
329
  return throwUnavailable("env");
309
330
  },
@@ -385,6 +406,9 @@ export function createStaticContext<TEnv>(
385
406
  get url(): URL {
386
407
  return throwUnavailable("url");
387
408
  },
409
+ get originalUrl(): URL {
410
+ return throwUnavailable("originalUrl");
411
+ },
388
412
  get env(): TEnv {
389
413
  return throwUnavailable("env");
390
414
  },
@@ -57,9 +57,15 @@ export interface MiddlewareContext<
57
57
  /** Original request */
58
58
  request: Request;
59
59
 
60
- /** Parsed URL */
60
+ /** Parsed URL (with internal `_rsc*` params stripped) */
61
61
  url: URL;
62
62
 
63
+ /**
64
+ * The original request URL with all parameters intact, including
65
+ * internal `_rsc*` transport params.
66
+ */
67
+ originalUrl: URL;
68
+
63
69
  /** URL pathname */
64
70
  pathname: string;
65
71
 
@@ -73,16 +79,7 @@ export interface MiddlewareContext<
73
79
  params: TParams;
74
80
 
75
81
  /**
76
- * Response stub (read-only). Before `next()`, returns the shared response stub
77
- * where headers and cookies accumulate. After `next()`, returns the downstream response.
78
- *
79
- * Use `ctx.header()` to set response headers, or `cookies()` for cookie mutations.
80
- * To replace the response entirely, return a new `Response` from the middleware.
81
- */
82
- readonly res: Response;
83
-
84
- /**
85
- * Shorthand for ctx.res.headers — response headers.
82
+ * Response headers.
86
83
  * Before `next()`, returns headers from the shared response stub.
87
84
  * After `next()`, returns headers from the downstream response.
88
85
  */
@@ -101,11 +98,10 @@ export interface MiddlewareContext<
101
98
  var: DefaultVars;
102
99
 
103
100
  /**
104
- * Set a response header - can be called before or after `next()`
101
+ * Set a response header - can be called before or after `next()`.
105
102
  *
106
103
  * When called before `next()`, headers are queued and merged into the final response.
107
104
  * When called after `next()`, headers are set directly on the response.
108
- * Shorthand for `ctx.res.headers.set()`.
109
105
  */
110
106
  header(name: string, value: string): void;
111
107
 
@@ -202,7 +198,7 @@ export interface MiddlewareEntry<TEnv = any> {
202
198
  }
203
199
 
204
200
  /**
205
- * Mutable response holder - allows ctx.res to be updated after next() is called
201
+ * Mutable response holder - tracks the current response through the middleware chain.
206
202
  */
207
203
  export interface ResponseHolder {
208
204
  response: Response | null;
@@ -163,9 +163,25 @@ export function createMiddlewareContext<TEnv>(
163
163
  // Cookie operations are handled by the standalone cookies() function which
164
164
  // delegates to the shared RequestContext internally.
165
165
  // The runtime implementation - types are enforced at call sites via MiddlewareContext<TEnv>
166
+ // Internal helper: resolve the current response (stub before next(), real after).
167
+ // Not exposed on the public MiddlewareContext type — use ctx.headers instead.
168
+ const getResponse = (): Response => {
169
+ if (isPreNext()) {
170
+ const reqCtx = _getRequestContext();
171
+ if (reqCtx) return reqCtx.res;
172
+ }
173
+ if (!responseHolder.response) {
174
+ throw new Error(
175
+ "Response is not available - responseHolder was not initialized",
176
+ );
177
+ }
178
+ return responseHolder.response;
179
+ };
180
+
166
181
  return {
167
182
  request,
168
183
  url,
184
+ originalUrl: new URL(request.url),
169
185
  pathname: url.pathname,
170
186
  searchParams: url.searchParams,
171
187
  env: env as MiddlewareContext<TEnv>["env"],
@@ -180,28 +196,8 @@ export function createMiddlewareContext<TEnv>(
180
196
  ) as MiddlewareContext<TEnv>["routeName"];
181
197
  },
182
198
 
183
- get res(): Response {
184
- // Before next(): return shared RequestContext stub so headers
185
- // set via ctx.header() are visible on ctx.res.
186
- if (isPreNext()) {
187
- const reqCtx = _getRequestContext();
188
- if (reqCtx) return reqCtx.res;
189
- }
190
- if (!responseHolder.response) {
191
- throw new Error(
192
- "ctx.res is not available - responseHolder was not initialized",
193
- );
194
- }
195
- return responseHolder.response;
196
- },
197
- set res(_: Response) {
198
- throw new Error(
199
- "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
200
- );
201
- },
202
-
203
199
  get headers(): Headers {
204
- return this.res.headers;
200
+ return getResponse().headers;
205
201
  },
206
202
 
207
203
  get: ((keyOrVar: any) =>
@@ -302,9 +298,9 @@ export function matchMiddleware<TEnv>(
302
298
  *
303
299
  * Features:
304
300
  * - `await next()` returns actual Response
305
- * - `ctx.res` available after `await next()` (like Hono's `c.res`)
306
- * - `ctx.header()` shorthand for setting headers
307
- * - Forgiving: if middleware doesn't return, uses `ctx.res`
301
+ * - `ctx.headers` available before and after `await next()`
302
+ * - `ctx.header()` shorthand for setting a single header
303
+ * - Forgiving: if middleware doesn't return, uses the downstream response
308
304
  * - Short-circuit: return Response to stop chain
309
305
  * - Error catching: try/catch around `next()` works
310
306
  */
@@ -101,6 +101,7 @@ export async function matchForPrerender<TEnv = any>(
101
101
  env: {} as TEnv,
102
102
  request: new Request("http://prerender" + pathname),
103
103
  url: new URL("http://prerender" + pathname),
104
+ originalUrl: new URL("http://prerender" + pathname),
104
105
  pathname,
105
106
  searchParams: new URLSearchParams(),
106
107
  var: variables,
@@ -331,6 +332,7 @@ export async function renderStaticSegment<TEnv = any>(
331
332
  env: {} as TEnv,
332
333
  request: syntheticRequest,
333
334
  url: syntheticUrl,
335
+ originalUrl: syntheticUrl,
334
336
  pathname: "/",
335
337
  searchParams: syntheticUrl.searchParams,
336
338
  var: {},
@@ -49,8 +49,13 @@ export interface RequestContext<
49
49
  env: TEnv;
50
50
  /** Original HTTP request */
51
51
  request: Request;
52
- /** Parsed URL (system params like _rsc* are NOT filtered here) */
52
+ /** Parsed URL (with internal `_rsc*` params stripped) */
53
53
  url: URL;
54
+ /**
55
+ * The original request URL with all parameters intact, including
56
+ * internal `_rsc*` transport params.
57
+ */
58
+ originalUrl: URL;
54
59
  /** URL pathname */
55
60
  pathname: string;
56
61
  /** URL search params (system params like _rsc* are NOT filtered here) */
@@ -72,12 +77,7 @@ export interface RequestContext<
72
77
  * Initially empty, then set to matched params
73
78
  */
74
79
  params: TParams;
75
- /**
76
- * Stub response for setting headers/cookies (read-only).
77
- * Headers set here are merged into the final response.
78
- * Use header() or setStatus() to mutate response headers/status.
79
- * Use cookies().set()/cookies().delete() for cookie mutations.
80
- */
80
+ /** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
81
81
  readonly res: Response;
82
82
 
83
83
  /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
@@ -301,6 +301,7 @@ export type PublicRequestContext<
301
301
  | "_reportBackgroundError"
302
302
  | "_debugPerformance"
303
303
  | "_metricsStore"
304
+ | "res"
304
305
  >;
305
306
 
306
307
  // AsyncLocalStorage instance for request context
@@ -556,6 +557,7 @@ export function createRequestContext<TEnv>(
556
557
  env,
557
558
  request,
558
559
  url,
560
+ originalUrl: new URL(request.url),
559
561
  pathname: url.pathname,
560
562
  searchParams: url.searchParams,
561
563
  var: variables,
@@ -228,9 +228,17 @@ export type HandlerContext<
228
228
  */
229
229
  pathname: string;
230
230
  /**
231
- * The full URL object (with system params filtered).
231
+ * The full URL object (with internal `_rsc*` params stripped).
232
+ * Use this for application logic — routing, link generation, display.
232
233
  */
233
234
  url: URL;
235
+ /**
236
+ * The original request URL with all parameters intact, including
237
+ * internal `_rsc*` transport params. Use `ctx.url` for application
238
+ * logic — this is only needed for advanced cases like debugging
239
+ * or custom cache keying.
240
+ */
241
+ originalUrl: URL;
234
242
  /**
235
243
  * Platform bindings (DB, KV, secrets, etc.).
236
244
  * Access resources like `ctx.env.DB`, `ctx.env.KV`.
@@ -267,21 +275,7 @@ export type HandlerContext<
267
275
  <T>(contextVar: ContextVar<T>, value: T): void;
268
276
  } & (<K extends keyof DefaultVars>(key: K, value: DefaultVars[K]) => void);
269
277
  /**
270
- * Stub response for setting headers/cookies.
271
- * Headers set here are merged into the final response.
272
- *
273
- * @example
274
- * ```typescript
275
- * route("product", (ctx) => {
276
- * ctx.res.headers.set("Cache-Control", "s-maxage=60");
277
- * return <ProductPage />;
278
- * });
279
- * ```
280
- */
281
- res: Response;
282
- /**
283
- * Shorthand for ctx.res.headers - response headers.
284
- * Headers set here are merged into the final response.
278
+ * Response headers. Headers set here are merged into the final response.
285
279
  *
286
280
  * @example
287
281
  * ```typescript
@@ -436,6 +430,8 @@ export type InternalHandlerContext<
436
430
  TEnv = DefaultEnv,
437
431
  TSearch extends SearchSchema = {},
438
432
  > = HandlerContext<TParams, TEnv, TSearch> & {
433
+ /** @internal Stub response for collecting headers/cookies. */
434
+ res: Response;
439
435
  /** Prerender-only control flow helper, attached when the runtime context supports it. */
440
436
  passthrough?: () => unknown;
441
437
  /** Current segment ID for handle data attribution. */