@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.
- package/dist/bin/rango.js +128 -46
- package/dist/vite/index.js +119 -43
- package/package.json +1 -1
- package/skills/links/SKILL.md +3 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/router-setup/SKILL.md +35 -0
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +4 -0
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +17 -1
- package/src/browser/prefetch/fetch.ts +8 -2
- package/src/browser/react/Link.tsx +43 -8
- package/src/browser/react/NavigationProvider.tsx +7 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/use-router.ts +20 -8
- package/src/browser/rsc-router.tsx +8 -0
- package/src/browser/server-action-bridge.ts +5 -0
- package/src/browser/types.ts +18 -4
- package/src/build/generate-manifest.ts +3 -0
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/route-definition/redirect.ts +9 -1
- package/src/router/handler-context.ts +5 -9
- package/src/router/intercept-resolution.ts +6 -2
- package/src/router/loader-resolution.ts +3 -2
- package/src/router/middleware-types.ts +0 -6
- package/src/router/middleware.ts +0 -3
- package/src/router/prerender-match.ts +2 -2
- package/src/router/router-interfaces.ts +25 -4
- package/src/router/router-options.ts +37 -11
- package/src/router.ts +40 -4
- package/src/rsc/handler.ts +10 -1
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/rsc-rendering.ts +5 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/rsc/types.ts +5 -0
- package/src/server/request-context.ts +8 -4
- package/src/ssr/index.tsx +3 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +5 -9
- package/src/types/loader-types.ts +0 -1
- package/src/urls/pattern-types.ts +12 -0
- 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 (
|
|
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
|
|
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) &&
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
695
|
-
|
|
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
|
|
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 === "
|
|
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
|
|
726
|
-
if (!
|
|
786
|
+
const extraction = extractUrlsFromRouter(routerSource);
|
|
787
|
+
if (!extraction) {
|
|
727
788
|
return { routes: {}, searchSchemas: {} };
|
|
728
789
|
}
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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
|
|
748
|
-
if (!
|
|
749
|
-
const
|
|
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 =
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
routerSource
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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?)$/,
|
package/dist/vite/index.js
CHANGED
|
@@ -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.
|
|
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 (
|
|
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
|
|
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) &&
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
}
|
|
2476
|
-
|
|
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
|
|
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 === "
|
|
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
|
|
2507
|
-
if (!
|
|
2567
|
+
const extraction = extractUrlsFromRouter(routerSource);
|
|
2568
|
+
if (!extraction) {
|
|
2508
2569
|
return { routes: {}, searchSchemas: {} };
|
|
2509
2570
|
}
|
|
2510
|
-
const
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2544
|
-
|
|
2545
|
-
routerSource
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
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(
|
|
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
package/skills/links/SKILL.md
CHANGED
|
@@ -139,7 +139,9 @@ function GlobalNav() {
|
|
|
139
139
|
}
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
`href()`
|
|
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
|