@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
package/dist/bin/rango.js CHANGED
@@ -450,7 +450,7 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
450
450
  }
451
451
  return routeMap;
452
452
  }
453
- function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut) {
453
+ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut, inlineBlock) {
454
454
  visited = visited ?? /* @__PURE__ */ new Set();
455
455
  const realPath = resolve(filePath);
456
456
  const key = variableName ? `${realPath}:${variableName}` : realPath;
@@ -466,7 +466,9 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
466
466
  return { routes: {}, searchSchemas: {} };
467
467
  }
468
468
  let block;
469
- if (variableName) {
469
+ if (inlineBlock) {
470
+ block = inlineBlock;
471
+ } else if (variableName) {
470
472
  const extracted = extractUrlsBlockForVariable(source, variableName);
471
473
  if (!extracted) return { routes: {}, searchSchemas: {} };
472
474
  block = extracted;
@@ -671,7 +673,7 @@ Router root: ${conflict.ancestor}
671
673
  Nested router: ${conflict.nested}
672
674
  Move the nested router into a sibling directory or configure it as a separate app root.`;
673
675
  }
674
- function extractUrlsVariableFromRouter(code) {
676
+ function extractUrlsFromRouter(code) {
675
677
  const sourceFile = ts5.createSourceFile(
676
678
  "router.tsx",
677
679
  code,
@@ -685,24 +687,70 @@ function extractUrlsVariableFromRouter(code) {
685
687
  const callee = node.expression;
686
688
  return ts5.isIdentifier(callee) && callee.text === "createRouter";
687
689
  }
690
+ function isInlineBuilder(node) {
691
+ return ts5.isArrowFunction(node) || ts5.isFunctionExpression(node);
692
+ }
693
+ function isRoutesOnCreateRouter(node) {
694
+ if (!ts5.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "routes")
695
+ return false;
696
+ let inner = node.expression.expression;
697
+ while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
698
+ inner = inner.expression.expression;
699
+ }
700
+ return isCreateRouterCall(inner);
701
+ }
688
702
  function visit(node) {
689
703
  if (result) return;
690
- if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && node.expression.name.text === "routes" && node.arguments.length >= 1 && ts5.isIdentifier(node.arguments[0])) {
691
- let inner = node.expression.expression;
692
- while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
693
- inner = inner.expression.expression;
694
- }
695
- if (isCreateRouterCall(inner)) {
696
- result = node.arguments[0].text;
697
- return;
704
+ if (ts5.isCallExpression(node) && node.arguments.length >= 1 && isRoutesOnCreateRouter(node)) {
705
+ const arg = node.arguments[0];
706
+ if (ts5.isIdentifier(arg)) {
707
+ result = { kind: "variable", name: arg.text };
708
+ } else if (isInlineBuilder(arg)) {
709
+ result = { kind: "inline", block: arg.getText(sourceFile) };
698
710
  }
711
+ return;
699
712
  }
700
713
  if (isCreateRouterCall(node)) {
701
714
  const callExpr = node;
702
- for (const arg of callExpr.arguments) {
715
+ for (const callArg of callExpr.arguments) {
716
+ if (ts5.isObjectLiteralExpression(callArg)) {
717
+ for (const prop of callArg.properties) {
718
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls") {
719
+ if (ts5.isIdentifier(prop.initializer)) {
720
+ result = { kind: "variable", name: prop.initializer.text };
721
+ } else if (isInlineBuilder(prop.initializer)) {
722
+ result = {
723
+ kind: "inline",
724
+ block: prop.initializer.getText(sourceFile)
725
+ };
726
+ }
727
+ return;
728
+ }
729
+ }
730
+ }
731
+ }
732
+ }
733
+ ts5.forEachChild(node, visit);
734
+ }
735
+ visit(sourceFile);
736
+ return result;
737
+ }
738
+ function extractBasenameFromRouter(code) {
739
+ const sourceFile = ts5.createSourceFile(
740
+ "router.tsx",
741
+ code,
742
+ ts5.ScriptTarget.Latest,
743
+ true,
744
+ ts5.ScriptKind.TSX
745
+ );
746
+ let result;
747
+ function visit(node) {
748
+ if (result !== void 0) return;
749
+ if (ts5.isCallExpression(node) && ts5.isIdentifier(node.expression) && node.expression.text === "createRouter") {
750
+ for (const arg of node.arguments) {
703
751
  if (ts5.isObjectLiteralExpression(arg)) {
704
752
  for (const prop of arg.properties) {
705
- if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls" && ts5.isIdentifier(prop.initializer)) {
753
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "basename" && ts5.isStringLiteral(prop.initializer)) {
706
754
  result = prop.initializer.text;
707
755
  return;
708
756
  }
@@ -715,6 +763,19 @@ function extractUrlsVariableFromRouter(code) {
715
763
  visit(sourceFile);
716
764
  return result;
717
765
  }
766
+ function applyBasenameToRoutes(result, basename2) {
767
+ const prefixed = {};
768
+ for (const [name, pattern] of Object.entries(result.routes)) {
769
+ if (pattern === "/") {
770
+ prefixed[name] = basename2;
771
+ } else if (basename2.endsWith("/") && pattern.startsWith("/")) {
772
+ prefixed[name] = basename2 + pattern.slice(1);
773
+ } else {
774
+ prefixed[name] = basename2 + pattern;
775
+ }
776
+ }
777
+ return { routes: prefixed, searchSchemas: result.searchSchemas };
778
+ }
718
779
  function buildCombinedRouteMapForRouterFile(routerFilePath) {
719
780
  let routerSource;
720
781
  try {
@@ -722,19 +783,40 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
722
783
  } catch {
723
784
  return { routes: {}, searchSchemas: {} };
724
785
  }
725
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
726
- if (!urlsVarName) {
786
+ const extraction = extractUrlsFromRouter(routerSource);
787
+ if (!extraction) {
727
788
  return { routes: {}, searchSchemas: {} };
728
789
  }
729
- const imported = resolveImportedVariable(routerSource, urlsVarName);
730
- if (imported) {
731
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
732
- if (!targetFile) {
733
- return { routes: {}, searchSchemas: {} };
790
+ const rawBasename = extractBasenameFromRouter(routerSource);
791
+ const basename2 = rawBasename ? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "") : void 0;
792
+ let result;
793
+ if (extraction.kind === "inline") {
794
+ result = buildCombinedRouteMapWithSearch(
795
+ routerFilePath,
796
+ void 0,
797
+ void 0,
798
+ void 0,
799
+ extraction.block
800
+ );
801
+ } else {
802
+ const imported = resolveImportedVariable(routerSource, extraction.name);
803
+ if (imported) {
804
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
805
+ if (!targetFile) {
806
+ return { routes: {}, searchSchemas: {} };
807
+ }
808
+ result = buildCombinedRouteMapWithSearch(
809
+ targetFile,
810
+ imported.exportedName
811
+ );
812
+ } else {
813
+ result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
734
814
  }
735
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
736
815
  }
737
- return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
816
+ if (basename2) {
817
+ result = applyBasenameToRoutes(result, basename2);
818
+ }
819
+ return result;
738
820
  }
739
821
  function detectUnresolvableIncludes(routerFilePath) {
740
822
  const realPath = resolve2(routerFilePath);
@@ -744,9 +826,20 @@ function detectUnresolvableIncludes(routerFilePath) {
744
826
  } catch {
745
827
  return [];
746
828
  }
747
- const urlsVarName = extractUrlsVariableFromRouter(source);
748
- if (!urlsVarName) return [];
749
- const imported = resolveImportedVariable(source, urlsVarName);
829
+ const extraction = extractUrlsFromRouter(source);
830
+ if (!extraction) return [];
831
+ const diagnostics = [];
832
+ if (extraction.kind === "inline") {
833
+ buildCombinedRouteMapWithSearch(
834
+ realPath,
835
+ void 0,
836
+ /* @__PURE__ */ new Set(),
837
+ diagnostics,
838
+ extraction.block
839
+ );
840
+ return diagnostics;
841
+ }
842
+ const imported = resolveImportedVariable(source, extraction.name);
750
843
  let targetFile;
751
844
  let exportedName;
752
845
  if (imported) {
@@ -766,9 +859,8 @@ function detectUnresolvableIncludes(routerFilePath) {
766
859
  exportedName = imported.exportedName;
767
860
  } else {
768
861
  targetFile = realPath;
769
- exportedName = urlsVarName;
862
+ exportedName = extraction.name;
770
863
  }
771
- const diagnostics = [];
772
864
  buildCombinedRouteMapWithSearch(
773
865
  targetFile,
774
866
  exportedName,
@@ -816,25 +908,15 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
816
908
  throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
817
909
  }
818
910
  for (const routerFilePath of routerFilePaths) {
819
- let routerSource;
820
- try {
821
- routerSource = readFileSync3(routerFilePath, "utf-8");
822
- } catch {
823
- continue;
824
- }
825
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
826
- if (!urlsVarName) continue;
827
- let result;
828
- const imported = resolveImportedVariable(routerSource, urlsVarName);
829
- if (imported) {
830
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
831
- if (!targetFile) continue;
832
- result = buildCombinedRouteMapWithSearch(
833
- targetFile,
834
- imported.exportedName
835
- );
836
- } else {
837
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
911
+ const result = buildCombinedRouteMapForRouterFile(routerFilePath);
912
+ if (Object.keys(result.routes).length === 0 && Object.keys(result.searchSchemas).length === 0) {
913
+ let routerSource;
914
+ try {
915
+ routerSource = readFileSync3(routerFilePath, "utf-8");
916
+ } catch {
917
+ continue;
918
+ }
919
+ if (!extractUrlsFromRouter(routerSource)) continue;
838
920
  }
839
921
  const routerBasename = pathBasename(routerFilePath).replace(
840
922
  /\.(tsx?|jsx?)$/,
@@ -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.55",
1748
+ version: "0.0.0-experimental.56",
1749
1749
  description: "Django-inspired RSC router with composable URL patterns",
1750
1750
  keywords: [
1751
1751
  "react",
@@ -2317,7 +2317,7 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
2317
2317
  }
2318
2318
  return routeMap;
2319
2319
  }
2320
- function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut) {
2320
+ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut, inlineBlock) {
2321
2321
  visited = visited ?? /* @__PURE__ */ new Set();
2322
2322
  const realPath = resolve2(filePath);
2323
2323
  const key = variableName ? `${realPath}:${variableName}` : realPath;
@@ -2333,7 +2333,9 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
2333
2333
  return { routes: {}, searchSchemas: {} };
2334
2334
  }
2335
2335
  let block;
2336
- if (variableName) {
2336
+ if (inlineBlock) {
2337
+ block = inlineBlock;
2338
+ } else if (variableName) {
2337
2339
  const extracted = extractUrlsBlockForVariable(source, variableName);
2338
2340
  if (!extracted) return { routes: {}, searchSchemas: {} };
2339
2341
  block = extracted;
@@ -2452,7 +2454,7 @@ Router root: ${conflict.ancestor}
2452
2454
  Nested router: ${conflict.nested}
2453
2455
  Move the nested router into a sibling directory or configure it as a separate app root.`;
2454
2456
  }
2455
- function extractUrlsVariableFromRouter(code) {
2457
+ function extractUrlsFromRouter(code) {
2456
2458
  const sourceFile = ts5.createSourceFile(
2457
2459
  "router.tsx",
2458
2460
  code,
@@ -2466,24 +2468,70 @@ function extractUrlsVariableFromRouter(code) {
2466
2468
  const callee = node.expression;
2467
2469
  return ts5.isIdentifier(callee) && callee.text === "createRouter";
2468
2470
  }
2471
+ function isInlineBuilder(node) {
2472
+ return ts5.isArrowFunction(node) || ts5.isFunctionExpression(node);
2473
+ }
2474
+ function isRoutesOnCreateRouter(node) {
2475
+ if (!ts5.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "routes")
2476
+ return false;
2477
+ let inner = node.expression.expression;
2478
+ while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
2479
+ inner = inner.expression.expression;
2480
+ }
2481
+ return isCreateRouterCall(inner);
2482
+ }
2469
2483
  function visit(node) {
2470
2484
  if (result) return;
2471
- if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && node.expression.name.text === "routes" && node.arguments.length >= 1 && ts5.isIdentifier(node.arguments[0])) {
2472
- let inner = node.expression.expression;
2473
- while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
2474
- inner = inner.expression.expression;
2475
- }
2476
- if (isCreateRouterCall(inner)) {
2477
- result = node.arguments[0].text;
2478
- return;
2485
+ if (ts5.isCallExpression(node) && node.arguments.length >= 1 && isRoutesOnCreateRouter(node)) {
2486
+ const arg = node.arguments[0];
2487
+ if (ts5.isIdentifier(arg)) {
2488
+ result = { kind: "variable", name: arg.text };
2489
+ } else if (isInlineBuilder(arg)) {
2490
+ result = { kind: "inline", block: arg.getText(sourceFile) };
2479
2491
  }
2492
+ return;
2480
2493
  }
2481
2494
  if (isCreateRouterCall(node)) {
2482
2495
  const callExpr = node;
2483
- for (const arg of callExpr.arguments) {
2496
+ for (const callArg of callExpr.arguments) {
2497
+ if (ts5.isObjectLiteralExpression(callArg)) {
2498
+ for (const prop of callArg.properties) {
2499
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls") {
2500
+ if (ts5.isIdentifier(prop.initializer)) {
2501
+ result = { kind: "variable", name: prop.initializer.text };
2502
+ } else if (isInlineBuilder(prop.initializer)) {
2503
+ result = {
2504
+ kind: "inline",
2505
+ block: prop.initializer.getText(sourceFile)
2506
+ };
2507
+ }
2508
+ return;
2509
+ }
2510
+ }
2511
+ }
2512
+ }
2513
+ }
2514
+ ts5.forEachChild(node, visit);
2515
+ }
2516
+ visit(sourceFile);
2517
+ return result;
2518
+ }
2519
+ function extractBasenameFromRouter(code) {
2520
+ const sourceFile = ts5.createSourceFile(
2521
+ "router.tsx",
2522
+ code,
2523
+ ts5.ScriptTarget.Latest,
2524
+ true,
2525
+ ts5.ScriptKind.TSX
2526
+ );
2527
+ let result;
2528
+ function visit(node) {
2529
+ if (result !== void 0) return;
2530
+ if (ts5.isCallExpression(node) && ts5.isIdentifier(node.expression) && node.expression.text === "createRouter") {
2531
+ for (const arg of node.arguments) {
2484
2532
  if (ts5.isObjectLiteralExpression(arg)) {
2485
2533
  for (const prop of arg.properties) {
2486
- if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls" && ts5.isIdentifier(prop.initializer)) {
2534
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "basename" && ts5.isStringLiteral(prop.initializer)) {
2487
2535
  result = prop.initializer.text;
2488
2536
  return;
2489
2537
  }
@@ -2496,6 +2544,19 @@ function extractUrlsVariableFromRouter(code) {
2496
2544
  visit(sourceFile);
2497
2545
  return result;
2498
2546
  }
2547
+ function applyBasenameToRoutes(result, basename3) {
2548
+ const prefixed = {};
2549
+ for (const [name, pattern] of Object.entries(result.routes)) {
2550
+ if (pattern === "/") {
2551
+ prefixed[name] = basename3;
2552
+ } else if (basename3.endsWith("/") && pattern.startsWith("/")) {
2553
+ prefixed[name] = basename3 + pattern.slice(1);
2554
+ } else {
2555
+ prefixed[name] = basename3 + pattern;
2556
+ }
2557
+ }
2558
+ return { routes: prefixed, searchSchemas: result.searchSchemas };
2559
+ }
2499
2560
  function buildCombinedRouteMapForRouterFile(routerFilePath) {
2500
2561
  let routerSource;
2501
2562
  try {
@@ -2503,19 +2564,40 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
2503
2564
  } catch {
2504
2565
  return { routes: {}, searchSchemas: {} };
2505
2566
  }
2506
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
2507
- if (!urlsVarName) {
2567
+ const extraction = extractUrlsFromRouter(routerSource);
2568
+ if (!extraction) {
2508
2569
  return { routes: {}, searchSchemas: {} };
2509
2570
  }
2510
- const imported = resolveImportedVariable(routerSource, urlsVarName);
2511
- if (imported) {
2512
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
2513
- if (!targetFile) {
2514
- return { routes: {}, searchSchemas: {} };
2571
+ const rawBasename = extractBasenameFromRouter(routerSource);
2572
+ const basename3 = rawBasename ? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "") : void 0;
2573
+ let result;
2574
+ if (extraction.kind === "inline") {
2575
+ result = buildCombinedRouteMapWithSearch(
2576
+ routerFilePath,
2577
+ void 0,
2578
+ void 0,
2579
+ void 0,
2580
+ extraction.block
2581
+ );
2582
+ } else {
2583
+ const imported = resolveImportedVariable(routerSource, extraction.name);
2584
+ if (imported) {
2585
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
2586
+ if (!targetFile) {
2587
+ return { routes: {}, searchSchemas: {} };
2588
+ }
2589
+ result = buildCombinedRouteMapWithSearch(
2590
+ targetFile,
2591
+ imported.exportedName
2592
+ );
2593
+ } else {
2594
+ result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
2515
2595
  }
2516
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
2517
2596
  }
2518
- return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
2597
+ if (basename3) {
2598
+ result = applyBasenameToRoutes(result, basename3);
2599
+ }
2600
+ return result;
2519
2601
  }
2520
2602
  function findRouterFiles(root, filter) {
2521
2603
  const result = [];
@@ -2540,25 +2622,15 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
2540
2622
  throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
2541
2623
  }
2542
2624
  for (const routerFilePath of routerFilePaths) {
2543
- let routerSource;
2544
- try {
2545
- routerSource = readFileSync2(routerFilePath, "utf-8");
2546
- } catch {
2547
- continue;
2548
- }
2549
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
2550
- if (!urlsVarName) continue;
2551
- let result;
2552
- const imported = resolveImportedVariable(routerSource, urlsVarName);
2553
- if (imported) {
2554
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
2555
- if (!targetFile) continue;
2556
- result = buildCombinedRouteMapWithSearch(
2557
- targetFile,
2558
- imported.exportedName
2559
- );
2560
- } else {
2561
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
2625
+ const result = buildCombinedRouteMapForRouterFile(routerFilePath);
2626
+ if (Object.keys(result.routes).length === 0 && Object.keys(result.searchSchemas).length === 0) {
2627
+ let routerSource;
2628
+ try {
2629
+ routerSource = readFileSync2(routerFilePath, "utf-8");
2630
+ } catch {
2631
+ continue;
2632
+ }
2633
+ if (!extractUrlsFromRouter(routerSource)) continue;
2562
2634
  }
2563
2635
  const routerBasename = pathBasename(routerFilePath).replace(
2564
2636
  /\.(tsx?|jsx?)$/,
@@ -3853,7 +3925,11 @@ async function discoverRouters(state, rscEnv) {
3853
3925
  if (!router.urlpatterns || !generateManifestFull) {
3854
3926
  continue;
3855
3927
  }
3856
- const manifest = generateManifestFull(router.urlpatterns, routerMountIndex);
3928
+ const manifest = generateManifestFull(
3929
+ router.urlpatterns,
3930
+ routerMountIndex,
3931
+ router.__basename ? { urlPrefix: router.__basename } : void 0
3932
+ );
3857
3933
  routerMountIndex++;
3858
3934
  allManifests.push({ id, manifest });
3859
3935
  const routeCount = Object.keys(manifest.routeManifest).length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.55",
3
+ "version": "0.0.0-experimental.56",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -139,7 +139,9 @@ function GlobalNav() {
139
139
  }
140
140
  ```
141
141
 
142
- `href()` is an identity function at runtime but provides compile-time validation via `ValidPaths` type. Paths are validated against registered route patterns using `PatternToPath`.
142
+ `href()` provides compile-time validation via `ValidPaths` type. Paths are validated against registered route patterns using `PatternToPath`.
143
+
144
+ `href()` is a raw path helper — it is **not** basename-aware. It returns the path as-is (or with the include mount prefix via `useHref()`). For basename-aware navigation, use `Link`, `useRouter().push()`, or `reverse()`, which auto-prefix root-relative paths with the router's basename.
143
145
 
144
146
  ## Client: useHref()
145
147
 
@@ -26,6 +26,8 @@ const router = createRouter<AppEnv>({})
26
26
  .routes(urlpatterns);
27
27
  ```
28
28
 
29
+ When the router has a `basename`, pattern-scoped `.use()` patterns are automatically prefixed. For example, with `basename: "/app"`, `.use("/admin/*", mw)` matches `/app/admin/*`.
30
+
29
31
  ### Route middleware (`middleware()` in `urls()`)
30
32
 
31
33
  Registered inside `urls()` callback. Wraps **rendering only** -- it does NOT wrap server action execution. Actions run before route middleware, so when route middleware executes during post-action revalidation, it can observe state that the action set (cookies, context variables, headers).
@@ -78,6 +78,11 @@ interface RSCRouterOptions<TEnv> {
78
78
  // Document component wrapping entire app
79
79
  document?: ComponentType<{ children: ReactNode }>;
80
80
 
81
+ // URL prefix for sub-path deployments (e.g. "/admin")
82
+ // All routes, reverse(), href(), Link, redirect(), and router.use()
83
+ // patterns are automatically prefixed. Route names stay unprefixed.
84
+ basename?: string;
85
+
81
86
  // Enable per-request performance timeline (console waterfall + Server-Timing header)
82
87
  debugPerformance?: boolean;
83
88
 
@@ -124,6 +129,36 @@ interface RSCRouterOptions<TEnv> {
124
129
  }
125
130
  ```
126
131
 
132
+ ## Basename (Sub-Path Deployment)
133
+
134
+ When your app is served under a sub-path (e.g. `/admin` or `/v2`), set `basename`:
135
+
136
+ ```typescript
137
+ const router = createRouter({
138
+ basename: "/admin",
139
+ document: Document,
140
+ }).routes(({ path, include }) => [
141
+ path("/", Dashboard, { name: "home" }), // matches /admin
142
+ path("/users", Users, { name: "users" }), // matches /admin/users
143
+ include("/api", apiPatterns, { name: "api" }), // matches /admin/api/*
144
+ ]);
145
+
146
+ router.reverse("home"); // "/admin"
147
+ router.reverse("users"); // "/admin/users"
148
+ ```
149
+
150
+ Router-owned APIs are basename-aware:
151
+
152
+ - `reverse()` returns prefixed paths
153
+ - `<Link to="/users">` renders `<a href="/admin/users">`
154
+ - `redirect("/login")` redirects to `"/admin/login"`
155
+ - `router.use("/users/*", mw)` matches `/admin/users/*`
156
+ - `useRouter().push("/users")` navigates to `/admin/users`
157
+ - Route names stay unprefixed (`"home"`, not `"admin.home"`)
158
+
159
+ Note: `href()` is a raw path helper and does **not** auto-prefix with basename.
160
+ Use `reverse()` or `<Link>` for basename-aware URLs.
161
+
127
162
  ## Using the Request Handler
128
163
 
129
164
  The router provides a `fetch` method to handle RSC requests:
@@ -448,6 +448,12 @@ export function createNavigationBridge(
448
448
  store.setCurrentUrl(url);
449
449
  store.setPath(new URL(url).pathname);
450
450
 
451
+ // Restore router identity from cache so subsequent navigations
452
+ // don't falsely detect an app switch.
453
+ if (cached?.routerId) {
454
+ store.setRouterId?.(cached.routerId);
455
+ }
456
+
451
457
  // Render from cache - force await to skip loading fallbacks
452
458
  try {
453
459
  const root = await renderSegments(cachedSegments, {
@@ -61,6 +61,7 @@ export function createNavigationClient(
61
61
  staleRevalidation,
62
62
  interceptSourceUrl,
63
63
  version,
64
+ routerId,
64
65
  hmr,
65
66
  } = options;
66
67
 
@@ -88,6 +89,9 @@ export function createNavigationClient(
88
89
  if (version) {
89
90
  fetchUrl.searchParams.set("_rsc_v", version);
90
91
  }
92
+ if (routerId) {
93
+ fetchUrl.searchParams.set("_rsc_rid", routerId);
94
+ }
91
95
 
92
96
  // Check completed in-memory prefetch cache before making a network request.
93
97
  // The cache key includes the source URL (previousUrl) because the