@rangojs/router 0.0.0-experimental.55 → 0.0.0-experimental.56

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 (46) hide show
  1. package/dist/bin/rango.js +128 -46
  2. package/dist/vite/index.js +119 -43
  3. package/package.json +1 -1
  4. package/skills/links/SKILL.md +3 -1
  5. package/skills/middleware/SKILL.md +2 -0
  6. package/skills/router-setup/SKILL.md +35 -0
  7. package/src/browser/navigation-bridge.ts +6 -0
  8. package/src/browser/navigation-client.ts +4 -0
  9. package/src/browser/navigation-store.ts +43 -8
  10. package/src/browser/partial-update.ts +17 -1
  11. package/src/browser/prefetch/fetch.ts +8 -2
  12. package/src/browser/react/Link.tsx +43 -8
  13. package/src/browser/react/NavigationProvider.tsx +7 -0
  14. package/src/browser/react/context.ts +6 -0
  15. package/src/browser/react/use-router.ts +20 -8
  16. package/src/browser/rsc-router.tsx +8 -0
  17. package/src/browser/server-action-bridge.ts +5 -0
  18. package/src/browser/types.ts +18 -4
  19. package/src/build/generate-manifest.ts +3 -0
  20. package/src/build/generate-route-types.ts +3 -0
  21. package/src/build/route-types/include-resolution.ts +8 -1
  22. package/src/build/route-types/router-processing.ts +211 -72
  23. package/src/route-definition/redirect.ts +9 -1
  24. package/src/router/handler-context.ts +5 -9
  25. package/src/router/intercept-resolution.ts +6 -2
  26. package/src/router/loader-resolution.ts +3 -2
  27. package/src/router/middleware-types.ts +0 -6
  28. package/src/router/middleware.ts +0 -3
  29. package/src/router/prerender-match.ts +2 -2
  30. package/src/router/router-interfaces.ts +25 -4
  31. package/src/router/router-options.ts +37 -11
  32. package/src/router.ts +40 -4
  33. package/src/rsc/handler.ts +10 -1
  34. package/src/rsc/manifest-init.ts +5 -1
  35. package/src/rsc/progressive-enhancement.ts +4 -0
  36. package/src/rsc/rsc-rendering.ts +5 -0
  37. package/src/rsc/server-action.ts +2 -0
  38. package/src/rsc/ssr-setup.ts +1 -1
  39. package/src/rsc/types.ts +5 -0
  40. package/src/server/request-context.ts +8 -4
  41. package/src/ssr/index.tsx +3 -0
  42. package/src/types/cache-types.ts +4 -4
  43. package/src/types/handler-context.ts +5 -9
  44. package/src/types/loader-types.ts +0 -1
  45. package/src/urls/pattern-types.ts +12 -0
  46. package/src/vite/discovery/discover-routers.ts +5 -1
@@ -157,13 +157,26 @@ export function formatNestedRouterConflictError(
157
157
  // ---------------------------------------------------------------------------
158
158
 
159
159
  /**
160
- * Extract the url patterns variable from a router file using AST.
161
- * Detects two patterns:
160
+ * Result of extracting URL patterns from a router file.
161
+ * - "variable": a named variable reference (e.g., `.routes(patterns)` or `urls: patterns`)
162
+ * - "inline": an inline builder function (e.g., `.routes(({ path }) => [...])` or `urls: ({ path }) => [...]`)
163
+ */
164
+ export type UrlsExtractionResult =
165
+ | { kind: "variable"; name: string }
166
+ | { kind: "inline"; block: string };
167
+
168
+ /**
169
+ * Extract the url patterns from a router file using AST.
170
+ * Detects four patterns:
162
171
  * 1. createRouter(...).routes(variableName)
163
172
  * 2. createRouter({ urls: variableName, ... })
164
- * Returns the local variable name.
173
+ * 3. createRouter(...).routes(({ path, ... }) => [...])
174
+ * 4. createRouter({ urls: ({ path, ... }) => [...], ... })
175
+ * Returns either a variable name or an inline code block.
165
176
  */
166
- export function extractUrlsVariableFromRouter(code: string): string | null {
177
+ export function extractUrlsFromRouter(
178
+ code: string,
179
+ ): UrlsExtractionResult | null {
167
180
  const sourceFile = ts.createSourceFile(
168
181
  "router.tsx",
169
182
  code,
@@ -171,7 +184,7 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
171
184
  true,
172
185
  ts.ScriptKind.TSX,
173
186
  );
174
- let result: string | null = null;
187
+ let result: UrlsExtractionResult | null = null;
175
188
 
176
189
  function isCreateRouterCall(node: ts.Node): boolean {
177
190
  if (!ts.isCallExpression(node)) return false;
@@ -179,44 +192,108 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
179
192
  return ts.isIdentifier(callee) && callee.text === "createRouter";
180
193
  }
181
194
 
195
+ /** Check if a node is an arrow/function expression (inline builder). */
196
+ function isInlineBuilder(node: ts.Node): boolean {
197
+ return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
198
+ }
199
+
200
+ /** Check if a .routes() call chains from createRouter(). */
201
+ function isRoutesOnCreateRouter(node: ts.CallExpression): boolean {
202
+ if (
203
+ !ts.isPropertyAccessExpression(node.expression) ||
204
+ node.expression.name.text !== "routes"
205
+ )
206
+ return false;
207
+ let inner: ts.Expression = node.expression.expression;
208
+ while (
209
+ ts.isCallExpression(inner) &&
210
+ ts.isPropertyAccessExpression(inner.expression)
211
+ ) {
212
+ inner = inner.expression.expression;
213
+ }
214
+ return isCreateRouterCall(inner);
215
+ }
216
+
182
217
  function visit(node: ts.Node) {
183
218
  if (result) return;
184
219
 
185
- // Pattern 1: createRouter(...).routes(variableName)
186
- // The AST shape is CallExpression(.routes) -> PropertyAccessExpression -> CallExpression(createRouter)
220
+ // Pattern 1 & 3: createRouter(...).routes(variableName | builder)
187
221
  if (
188
222
  ts.isCallExpression(node) &&
189
- ts.isPropertyAccessExpression(node.expression) &&
190
- node.expression.name.text === "routes" &&
191
223
  node.arguments.length >= 1 &&
192
- ts.isIdentifier(node.arguments[0])
224
+ isRoutesOnCreateRouter(node)
193
225
  ) {
194
- // Walk up the chain: createRouter().middleware(...).routes(x) etc.
195
- // The innermost call should be createRouter(...)
196
- let inner: ts.Expression = node.expression.expression;
197
- while (
198
- ts.isCallExpression(inner) &&
199
- ts.isPropertyAccessExpression(inner.expression)
200
- ) {
201
- inner = inner.expression.expression;
202
- }
203
- if (isCreateRouterCall(inner)) {
204
- result = (node.arguments[0] as ts.Identifier).text;
205
- return;
226
+ const arg = node.arguments[0];
227
+ if (ts.isIdentifier(arg)) {
228
+ result = { kind: "variable", name: arg.text };
229
+ } else if (isInlineBuilder(arg)) {
230
+ result = { kind: "inline", block: arg.getText(sourceFile) };
206
231
  }
232
+ return;
207
233
  }
208
234
 
209
- // Pattern 2: createRouter({ urls: variableName, ... })
235
+ // Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
210
236
  if (isCreateRouterCall(node)) {
211
237
  const callExpr = node as ts.CallExpression;
212
- for (const arg of callExpr.arguments) {
238
+ for (const callArg of callExpr.arguments) {
239
+ if (ts.isObjectLiteralExpression(callArg)) {
240
+ for (const prop of callArg.properties) {
241
+ if (
242
+ ts.isPropertyAssignment(prop) &&
243
+ ts.isIdentifier(prop.name) &&
244
+ prop.name.text === "urls"
245
+ ) {
246
+ if (ts.isIdentifier(prop.initializer)) {
247
+ result = { kind: "variable", name: prop.initializer.text };
248
+ } else if (isInlineBuilder(prop.initializer)) {
249
+ result = {
250
+ kind: "inline",
251
+ block: prop.initializer.getText(sourceFile),
252
+ };
253
+ }
254
+ return;
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ ts.forEachChild(node, visit);
262
+ }
263
+
264
+ visit(sourceFile);
265
+ return result;
266
+ }
267
+
268
+ /**
269
+ * Extract the `basename` string literal from createRouter({ basename: "..." }).
270
+ * Returns the basename value or undefined if not present.
271
+ */
272
+ export function extractBasenameFromRouter(code: string): string | undefined {
273
+ const sourceFile = ts.createSourceFile(
274
+ "router.tsx",
275
+ code,
276
+ ts.ScriptTarget.Latest,
277
+ true,
278
+ ts.ScriptKind.TSX,
279
+ );
280
+ let result: string | undefined;
281
+
282
+ function visit(node: ts.Node) {
283
+ if (result !== undefined) return;
284
+ if (
285
+ ts.isCallExpression(node) &&
286
+ ts.isIdentifier(node.expression) &&
287
+ node.expression.text === "createRouter"
288
+ ) {
289
+ for (const arg of node.arguments) {
213
290
  if (ts.isObjectLiteralExpression(arg)) {
214
291
  for (const prop of arg.properties) {
215
292
  if (
216
293
  ts.isPropertyAssignment(prop) &&
217
294
  ts.isIdentifier(prop.name) &&
218
- prop.name.text === "urls" &&
219
- ts.isIdentifier(prop.initializer)
295
+ prop.name.text === "basename" &&
296
+ ts.isStringLiteral(prop.initializer)
220
297
  ) {
221
298
  result = prop.initializer.text;
222
299
  return;
@@ -225,7 +302,6 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
225
302
  }
226
303
  }
227
304
  }
228
-
229
305
  ts.forEachChild(node, visit);
230
306
  }
231
307
 
@@ -233,9 +309,40 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
233
309
  return result;
234
310
  }
235
311
 
312
+ /** @deprecated Use extractUrlsFromRouter instead */
313
+ export function extractUrlsVariableFromRouter(code: string): string | null {
314
+ const result = extractUrlsFromRouter(code);
315
+ return result?.kind === "variable" ? result.name : null;
316
+ }
317
+
318
+ /** Apply a basename prefix to all route patterns in a result set. */
319
+ function applyBasenameToRoutes(
320
+ result: {
321
+ routes: Record<string, string>;
322
+ searchSchemas: Record<string, Record<string, string>>;
323
+ },
324
+ basename: string,
325
+ ): {
326
+ routes: Record<string, string>;
327
+ searchSchemas: Record<string, Record<string, string>>;
328
+ } {
329
+ const prefixed: Record<string, string> = {};
330
+ for (const [name, pattern] of Object.entries(result.routes)) {
331
+ if (pattern === "/") {
332
+ prefixed[name] = basename;
333
+ } else if (basename.endsWith("/") && pattern.startsWith("/")) {
334
+ prefixed[name] = basename + pattern.slice(1);
335
+ } else {
336
+ prefixed[name] = basename + pattern;
337
+ }
338
+ }
339
+ return { routes: prefixed, searchSchemas: result.searchSchemas };
340
+ }
341
+
236
342
  /**
237
343
  * Resolve routes and search schemas from a router source file by following the
238
- * variable passed to `.routes(...)` or `urls: ...` in createRouter options.
344
+ * variable passed to `.routes(...)` or `urls: ...` in createRouter options,
345
+ * or by parsing an inline builder function directly.
239
346
  */
240
347
  export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
241
348
  routes: Record<string, string>;
@@ -248,21 +355,54 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
248
355
  return { routes: {}, searchSchemas: {} };
249
356
  }
250
357
 
251
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
252
- if (!urlsVarName) {
358
+ const extraction = extractUrlsFromRouter(routerSource);
359
+ if (!extraction) {
253
360
  return { routes: {}, searchSchemas: {} };
254
361
  }
255
362
 
256
- const imported = resolveImportedVariable(routerSource, urlsVarName);
257
- if (imported) {
258
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
259
- if (!targetFile) {
260
- return { routes: {}, searchSchemas: {} };
363
+ // Detect basename from createRouter({ basename: "..." })
364
+ const rawBasename = extractBasenameFromRouter(routerSource);
365
+ const basename = rawBasename
366
+ ? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
367
+ : undefined;
368
+
369
+ let result: {
370
+ routes: Record<string, string>;
371
+ searchSchemas: Record<string, Record<string, string>>;
372
+ };
373
+
374
+ // Inline builder: extract routes directly from the function body
375
+ if (extraction.kind === "inline") {
376
+ result = buildCombinedRouteMapWithSearch(
377
+ routerFilePath,
378
+ undefined,
379
+ undefined,
380
+ undefined,
381
+ extraction.block,
382
+ );
383
+ } else {
384
+ // Variable reference: follow imports or same-file declaration
385
+ const imported = resolveImportedVariable(routerSource, extraction.name);
386
+ if (imported) {
387
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
388
+ if (!targetFile) {
389
+ return { routes: {}, searchSchemas: {} };
390
+ }
391
+ result = buildCombinedRouteMapWithSearch(
392
+ targetFile,
393
+ imported.exportedName,
394
+ );
395
+ } else {
396
+ result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
261
397
  }
262
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
263
398
  }
264
399
 
265
- return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
400
+ // Apply basename prefix to all extracted route patterns
401
+ if (basename) {
402
+ result = applyBasenameToRoutes(result, basename);
403
+ }
404
+
405
+ return result;
266
406
  }
267
407
 
268
408
  // ---------------------------------------------------------------------------
@@ -285,12 +425,26 @@ export function detectUnresolvableIncludes(
285
425
  return [];
286
426
  }
287
427
 
288
- // Extract the urls variable from the router file
289
- const urlsVarName = extractUrlsVariableFromRouter(source);
290
- if (!urlsVarName) return [];
428
+ // Extract the urls source from the router file
429
+ const extraction = extractUrlsFromRouter(source);
430
+ if (!extraction) return [];
291
431
 
292
- // Resolve where the urls variable comes from
293
- const imported = resolveImportedVariable(source, urlsVarName);
432
+ const diagnostics: UnresolvableInclude[] = [];
433
+
434
+ if (extraction.kind === "inline") {
435
+ // Inline builder: parse directly
436
+ buildCombinedRouteMapWithSearch(
437
+ realPath,
438
+ undefined,
439
+ new Set(),
440
+ diagnostics,
441
+ extraction.block,
442
+ );
443
+ return diagnostics;
444
+ }
445
+
446
+ // Variable reference: resolve where it comes from
447
+ const imported = resolveImportedVariable(source, extraction.name);
294
448
  let targetFile: string;
295
449
  let exportedName: string | undefined;
296
450
 
@@ -312,10 +466,9 @@ export function detectUnresolvableIncludes(
312
466
  } else {
313
467
  // Same-file urls() definition
314
468
  targetFile = realPath;
315
- exportedName = urlsVarName;
469
+ exportedName = extraction.name;
316
470
  }
317
471
 
318
- const diagnostics: UnresolvableInclude[] = [];
319
472
  buildCombinedRouteMapWithSearch(
320
473
  targetFile,
321
474
  exportedName,
@@ -397,34 +550,20 @@ export function writeCombinedRouteTypes(
397
550
  }
398
551
 
399
552
  for (const routerFilePath of routerFilePaths) {
400
- let routerSource: string;
401
- try {
402
- routerSource = readFileSync(routerFilePath, "utf-8");
403
- } catch {
404
- continue;
405
- }
406
- // Extract the urls variable name from .routes(varName) or urls: varName
407
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
408
- if (!urlsVarName) continue;
409
-
410
- // Resolve the variable to its source module
411
- let result: {
412
- routes: Record<string, string>;
413
- searchSchemas: Record<string, Record<string, string>>;
414
- };
415
-
416
- const imported = resolveImportedVariable(routerSource, urlsVarName);
417
- if (imported) {
418
- // Variable is imported from another module
419
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
420
- if (!targetFile) continue;
421
- result = buildCombinedRouteMapWithSearch(
422
- targetFile,
423
- imported.exportedName,
424
- );
425
- } else {
426
- // Variable is defined in the same file
427
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
553
+ const result = buildCombinedRouteMapForRouterFile(routerFilePath);
554
+ if (
555
+ Object.keys(result.routes).length === 0 &&
556
+ Object.keys(result.searchSchemas).length === 0
557
+ ) {
558
+ // Check if the file even has a createRouter call — if not, skip entirely.
559
+ // If it does, fall through to write an empty placeholder below.
560
+ let routerSource: string;
561
+ try {
562
+ routerSource = readFileSync(routerFilePath, "utf-8");
563
+ } catch {
564
+ continue;
565
+ }
566
+ if (!extractUrlsFromRouter(routerSource)) continue;
428
567
  }
429
568
 
430
569
  const routerBasename = pathBasename(routerFilePath).replace(
@@ -2,6 +2,7 @@ import type { LocationStateEntry } from "../browser/react/location-state-shared.
2
2
  import {
3
3
  requireRequestContext,
4
4
  getRequestContext,
5
+ _getRequestContext,
5
6
  } from "../server/request-context.js";
6
7
 
7
8
  /**
@@ -83,10 +84,17 @@ export function redirect(
83
84
  }
84
85
  }
85
86
 
87
+ // Auto-prefix root-relative URLs with basename for app-local redirects.
88
+ const bn = _getRequestContext()?._basename;
89
+ let resolvedUrl = url;
90
+ if (bn && url.startsWith("/") && !url.startsWith(bn + "/") && url !== bn) {
91
+ resolvedUrl = url === "/" ? bn : bn + url;
92
+ }
93
+
86
94
  return new Response(null, {
87
95
  status,
88
96
  headers: {
89
- Location: url,
97
+ Location: resolvedUrl,
90
98
  "X-RSC-Redirect": "soft",
91
99
  },
92
100
  });
@@ -207,7 +207,7 @@ export function createHandlerContext<TEnv>(
207
207
  // Get variables from request context - this is the unified context
208
208
  // shared between middleware and route handlers
209
209
  const requestContext = _getRequestContext();
210
- const variables: any = requestContext?.var ?? {};
210
+ const variables: any = requestContext?._variables ?? {};
211
211
 
212
212
  // If route has a search schema, parse URLSearchParams into typed object
213
213
  const searchSchema = routeName ? getSearchSchema(routeName) : undefined;
@@ -257,7 +257,7 @@ export function createHandlerContext<TEnv>(
257
257
  url,
258
258
  originalUrl: new URL(request.url),
259
259
  env: bindings,
260
- var: variables,
260
+ _variables: variables,
261
261
  get: ((keyOrVar: any) => {
262
262
  // Read-time guard: non-cacheable var inside cache() → throw.
263
263
  // Works for both ContextVar tokens and string keys.
@@ -320,7 +320,7 @@ export function createHandlerContext<TEnv>(
320
320
  *
321
321
  * Returns an InternalHandlerContext where params, pathname, url, searchParams,
322
322
  * search, reverse, and use(handle) work. Request-time properties
323
- * (request, env, headers, cookies, var, get, set, res) throw with a clear error.
323
+ * (request, env, headers, cookies, get, set, res) throw with a clear error.
324
324
  */
325
325
  export function createPrerenderContext<TEnv>(
326
326
  params: Record<string, string>,
@@ -354,9 +354,7 @@ export function createPrerenderContext<TEnv>(
354
354
  get env(): TEnv {
355
355
  return throwUnavailable("env");
356
356
  },
357
- get var(): any {
358
- return throwUnavailable("var");
359
- },
357
+ _variables: variables,
360
358
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
361
359
  set: ((keyOrVar: any, value: any) => {
362
360
  contextSet(variables, keyOrVar, value);
@@ -438,9 +436,7 @@ export function createStaticContext<TEnv>(
438
436
  get env(): TEnv {
439
437
  return throwUnavailable("env");
440
438
  },
441
- get var(): any {
442
- return throwUnavailable("var");
443
- },
439
+ _variables: variables,
444
440
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
445
441
  set: ((keyOrVar: any, value: any) => {
446
442
  contextSet(variables, keyOrVar, value);
@@ -11,7 +11,11 @@ import type {
11
11
  InterceptEntry,
12
12
  InterceptSelectorContext,
13
13
  } from "../server/context";
14
- import type { HandlerContext, ResolvedSegment } from "../types";
14
+ import type {
15
+ HandlerContext,
16
+ InternalHandlerContext,
17
+ ResolvedSegment,
18
+ } from "../types";
15
19
  import { evaluateRevalidation } from "./revalidation.js";
16
20
  import { getRequestContext } from "../server/request-context.js";
17
21
  import { executeInterceptMiddleware } from "./middleware.js";
@@ -134,7 +138,7 @@ export async function resolveInterceptEntry<TEnv>(
134
138
  context.request,
135
139
  context.env,
136
140
  params,
137
- context.var as Record<string, any>,
141
+ (context as InternalHandlerContext<any, TEnv>)._variables,
138
142
  requestCtx.res,
139
143
  createReverseFunction(getGlobalRouteMap()),
140
144
  );
@@ -242,6 +242,7 @@ function createLoaderExecutor<TEnv>(
242
242
  pendingLoaders.add(loader.$$id);
243
243
 
244
244
  const currentLoaderId = loader.$$id;
245
+ const variables = (ctx as InternalHandlerContext<any, TEnv>)._variables;
245
246
  // Loader functions are always fresh (never cached), so they get an
246
247
  // unguarded get that bypasses non-cacheable read guards. This applies
247
248
  // to ALL loaders — DSL and handler-called — because the loader
@@ -256,8 +257,8 @@ function createLoaderExecutor<TEnv>(
256
257
  pathname: ctx.pathname,
257
258
  url: ctx.url,
258
259
  env: ctx.env,
259
- var: ctx.var,
260
- get: ((keyOrVar: any) => contextGet(ctx.var, keyOrVar)) as typeof ctx.get,
260
+ get: ((keyOrVar: any) =>
261
+ contextGet(variables, keyOrVar)) as typeof ctx.get,
261
262
  use: <TDep, TDepParams = any>(
262
263
  dep: LoaderDefinition<TDep, TDepParams>,
263
264
  ): Promise<TDep> => {
@@ -95,12 +95,6 @@ export interface MiddlewareContext<
95
95
  /** Set a context variable (shared with route handlers) */
96
96
  set: SetVariableFn;
97
97
 
98
- /**
99
- * Middleware-injected variables.
100
- * Same shared dictionary as `ctx.get()`/`ctx.set()`.
101
- */
102
- var: DefaultVars;
103
-
104
98
  /**
105
99
  * Set a response header - can be called before or after `next()`.
106
100
  *
@@ -207,9 +207,6 @@ export function createMiddlewareContext<TEnv>(
207
207
  set: ((keyOrVar: any, value: unknown, options?: any) => {
208
208
  contextSet(variables, keyOrVar, value, options);
209
209
  }) as MiddlewareContext<TEnv>["set"],
210
-
211
- var: variables as MiddlewareContext<TEnv>["var"],
212
-
213
210
  header(name: string, value: string): void {
214
211
  // Before next(): delegate to shared RequestContext stub
215
212
  if (isPreNext()) {
@@ -104,7 +104,7 @@ export async function matchForPrerender<TEnv = any>(
104
104
  originalUrl: new URL("http://prerender" + pathname),
105
105
  pathname,
106
106
  searchParams: new URLSearchParams(),
107
- var: variables,
107
+ _variables: variables,
108
108
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
109
109
  set: ((keyOrVar: any, value: any) => {
110
110
  contextSet(variables, keyOrVar, value);
@@ -336,7 +336,7 @@ export async function renderStaticSegment<TEnv = any>(
336
336
  originalUrl: syntheticUrl,
337
337
  pathname: "/",
338
338
  searchParams: syntheticUrl.searchParams,
339
- var: {},
339
+ _variables: {},
340
340
  get: () => undefined as any,
341
341
  set: () => {},
342
342
  params: {},
@@ -2,6 +2,7 @@ import type { ComponentType, ReactNode } from "react";
2
2
  import type { SerializedManifest } from "../debug.js";
3
3
  import type { ReverseFunction } from "../reverse.js";
4
4
  import type { UrlPatterns } from "../urls.js";
5
+ import type { UrlBuilder } from "../urls/pattern-types.js";
5
6
  import type { EntryData } from "../server/context";
6
7
  import type { ErrorInfo, MatchResult } from "../types";
7
8
  import type { NonceProvider } from "../rsc/types.js";
@@ -68,12 +69,24 @@ export interface RSCRouter<
68
69
  readonly id: string;
69
70
 
70
71
  /**
71
- * Register routes using URL patterns from urls()
72
+ * URL prefix applied to all routes. Undefined when no basename is configured.
73
+ */
74
+ readonly basename: string | undefined;
75
+
76
+ /**
77
+ * Register routes using URL patterns from urls() or a builder function
72
78
  *
73
79
  * @example
74
80
  * ```typescript
75
- * createRouter({})
76
- * .routes(urlpatterns)
81
+ * // With urls()
82
+ * createRouter({}).routes(urlpatterns)
83
+ *
84
+ * // With builder function (urls() is implicit)
85
+ * createRouter({}).routes(({ path, layout }) => [
86
+ * layout(RootLayout, () => [
87
+ * path("/", HomePage),
88
+ * ]),
89
+ * ])
77
90
  * ```
78
91
  */
79
92
  routes<T extends UrlPatterns<TEnv, any>>(
@@ -85,6 +98,7 @@ export interface RSCRouter<
85
98
  ? MergeRoutesWithResponses<NonNullable<T["_routes"]>, T["_responses"]>
86
99
  : Record<string, string>)
87
100
  >;
101
+ routes(builder: UrlBuilder<TEnv>): RSCRouter<TEnv, TRoutes>;
88
102
 
89
103
  /**
90
104
  * Add global middleware that runs on all routes
@@ -188,8 +202,11 @@ export interface RSCRouterInternal<
188
202
  */
189
203
  readonly id: string;
190
204
 
205
+ /** URL prefix applied to all routes. */
206
+ readonly basename: string | undefined;
207
+
191
208
  /**
192
- * Register routes using URL patterns from urls()
209
+ * Register routes using URL patterns from urls() or a builder function
193
210
  */
194
211
  routes<T extends UrlPatterns<TEnv, any>>(
195
212
  patterns: T,
@@ -200,6 +217,7 @@ export interface RSCRouterInternal<
200
217
  ? MergeRoutesWithResponses<NonNullable<T["_routes"]>, T["_responses"]>
201
218
  : Record<string, string>)
202
219
  >;
220
+ routes(builder: UrlBuilder<TEnv>): RSCRouter<TEnv, TRoutes>;
203
221
 
204
222
  /**
205
223
  * Add global middleware that runs on all routes
@@ -338,6 +356,9 @@ export interface RSCRouterInternal<
338
356
  */
339
357
  readonly __sourceFile?: string;
340
358
 
359
+ /** @internal basename for runtime manifest generation */
360
+ readonly __basename?: string;
361
+
341
362
  match(
342
363
  request: Request,
343
364
  input?: RouterRequestInput<TEnv>,