@rangojs/router 0.0.0-experimental.132 → 0.0.0-experimental.133
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/AGENTS.md +8 -0
- package/README.md +43 -2
- package/dist/bin/rango.js +92 -16
- package/dist/vite/index.js +166 -70
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +1 -1
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +2 -2
- package/skills/caching/SKILL.md +16 -9
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +2 -2
- package/skills/host-router/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +1 -1
- package/skills/loader/SKILL.md +2 -0
- package/skills/migrate-react-router/SKILL.md +4 -2
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/prerender/SKILL.md +2 -0
- package/skills/rango/SKILL.md +12 -11
- package/skills/response-routes/SKILL.md +2 -2
- package/skills/route/SKILL.md +4 -0
- package/skills/router-setup/SKILL.md +3 -0
- package/skills/scripts/SKILL.md +179 -0
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing/bindings.md +20 -6
- package/skills/testing/cache-prerender.md +5 -2
- package/skills/testing/client-components.md +2 -0
- package/skills/testing/e2e-parity.md +1 -1
- package/skills/testing/flight.md +8 -9
- package/skills/testing/render-handler.md +1 -1
- package/skills/testing/response-routes.md +1 -1
- package/skills/testing/server-actions.md +11 -11
- package/skills/testing/setup.md +3 -0
- package/skills/typesafety/SKILL.md +3 -2
- package/skills/use-cache/SKILL.md +10 -9
- package/src/browser/event-controller.ts +109 -2
- package/src/browser/partial-update.ts +12 -0
- package/src/browser/prefetch/cache.ts +17 -0
- package/src/browser/prefetch/fetch.ts +69 -2
- package/src/browser/react/Link.tsx +30 -5
- package/src/browser/react/NavigationProvider.tsx +12 -2
- package/src/browser/react/location-state-shared.ts +14 -2
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +23 -2
- package/src/browser/response-adapter.ts +14 -3
- package/src/browser/rsc-router.tsx +3 -0
- package/src/browser/scroll-restoration.ts +8 -3
- package/src/browser/server-action-bridge.ts +46 -11
- package/src/browser/types.ts +6 -0
- package/src/build/generate-route-types.ts +0 -1
- package/src/build/route-trie.ts +33 -9
- package/src/build/route-types/include-resolution.ts +7 -1
- package/src/build/route-types/router-processing.ts +0 -6
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/cache/cache-policy.ts +42 -8
- package/src/cache/cache-runtime.ts +65 -5
- package/src/cache/cache-scope.ts +71 -11
- package/src/cache/cache-tag.ts +7 -2
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +85 -613
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/document-cache.ts +11 -0
- package/src/cache/handle-snapshot.ts +8 -1
- package/src/cache/profile-registry.ts +25 -1
- package/src/cache/segment-codec.ts +9 -1
- package/src/cache/types.ts +4 -0
- package/src/client.rsc.tsx +38 -0
- package/src/client.tsx +11 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +1 -1
- package/src/decode-loader-results.ts +7 -1
- package/src/escape-script.ts +52 -0
- package/src/handles/MetaTags.tsx +56 -5
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +29 -11
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +46 -0
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +7 -3
- package/src/host/pattern-matcher.ts +16 -2
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +5 -0
- package/src/response-utils.ts +25 -0
- package/src/route-definition/dsl-helpers.ts +7 -0
- package/src/route-definition/redirect.ts +1 -2
- package/src/router/content-negotiation.ts +58 -10
- package/src/router/intercept-resolution.ts +9 -0
- package/src/router/match-middleware/cache-store.ts +10 -1
- package/src/router/middleware.ts +10 -3
- package/src/router/pattern-matching.ts +25 -23
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +23 -0
- package/src/router/segment-resolution/fresh.ts +10 -0
- package/src/router/segment-resolution/helpers.ts +35 -1
- package/src/router/segment-resolution/loader-cache.ts +10 -6
- package/src/router/segment-resolution/revalidation.ts +6 -0
- package/src/router/segment-resolution.ts +1 -0
- package/src/router/trie-matching.ts +14 -9
- package/src/router.ts +18 -10
- package/src/rsc/handler.ts +52 -13
- package/src/rsc/helpers.ts +7 -1
- package/src/rsc/index.ts +1 -4
- package/src/rsc/loader-fetch.ts +107 -37
- package/src/rsc/progressive-enhancement.ts +18 -6
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-route-handler.ts +16 -133
- package/src/rsc/rsc-rendering.ts +13 -4
- package/src/rsc/server-action.ts +52 -6
- package/src/rsc/types.ts +7 -0
- package/src/search-params.ts +24 -5
- package/src/segment-loader-promise.ts +17 -2
- package/src/server/loader-registry.ts +16 -18
- package/src/server/request-context.ts +47 -20
- package/src/testing/dispatch.ts +108 -25
- package/src/testing/flight.ts +25 -0
- package/src/testing/internal/context.ts +25 -2
- package/src/testing/render-handler.ts +3 -1
- package/src/testing/render-route.tsx +15 -0
- package/src/testing/run-loader.ts +10 -3
- package/src/theme/ThemeProvider.tsx +20 -6
- package/src/theme/ThemeScript.tsx +7 -3
- package/src/theme/constants.ts +54 -3
- package/src/theme/theme-script.ts +22 -7
- package/src/types/request-scope.ts +8 -3
- package/src/vite/plugins/cjs-to-esm.ts +8 -1
- package/src/vite/plugins/expose-id-utils.ts +10 -1
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -16
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +6 -1
- package/src/vite/plugins/expose-internal-ids.ts +0 -1
- package/src/vite/plugins/version-plugin.ts +5 -17
- package/src/vite/plugins/virtual-entries.ts +12 -2
- package/src/vite/rango.ts +15 -6
- package/src/vite/utils/ast-handler-extract.ts +11 -4
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/prerender-utils.ts +17 -2
package/dist/vite/index.js
CHANGED
|
@@ -165,7 +165,9 @@ function countArgs(code, startPos, endPos) {
|
|
|
165
165
|
while (i < endPos) {
|
|
166
166
|
const skipped = skipStringOrComment(code, i);
|
|
167
167
|
if (skipped > i) {
|
|
168
|
-
|
|
168
|
+
const ch = code[i];
|
|
169
|
+
const isComment = ch === "/" && i + 1 < code.length && (code[i + 1] === "/" || code[i + 1] === "*");
|
|
170
|
+
if (!isComment) hasContent = true;
|
|
169
171
|
i = skipped;
|
|
170
172
|
continue;
|
|
171
173
|
}
|
|
@@ -530,10 +532,10 @@ import path4 from "node:path";
|
|
|
530
532
|
function isDirectivePrologueStatement(node) {
|
|
531
533
|
return node?.type === "ExpressionStatement" && typeof node.directive === "string";
|
|
532
534
|
}
|
|
533
|
-
function findImportInsertionPos(code,
|
|
535
|
+
function findImportInsertionPos(code, parseAst5) {
|
|
534
536
|
let program;
|
|
535
537
|
try {
|
|
536
|
-
program =
|
|
538
|
+
program = parseAst5(code, { lang: "tsx" });
|
|
537
539
|
} catch {
|
|
538
540
|
return 0;
|
|
539
541
|
}
|
|
@@ -570,10 +572,10 @@ function walkNode(node, parent, ancestors, enter) {
|
|
|
570
572
|
}
|
|
571
573
|
ancestors.pop();
|
|
572
574
|
}
|
|
573
|
-
function findHandlerCalls(code, fnName,
|
|
575
|
+
function findHandlerCalls(code, fnName, parseAst5) {
|
|
574
576
|
let program;
|
|
575
577
|
try {
|
|
576
|
-
program =
|
|
578
|
+
program = parseAst5(code, { lang: "tsx" });
|
|
577
579
|
} catch {
|
|
578
580
|
return [];
|
|
579
581
|
}
|
|
@@ -645,18 +647,18 @@ function getImportedLocalNamesFromProgram(program, importedName) {
|
|
|
645
647
|
}
|
|
646
648
|
return localNames;
|
|
647
649
|
}
|
|
648
|
-
function getImportedLocalNames(code, importedName,
|
|
650
|
+
function getImportedLocalNames(code, importedName, parseAst5) {
|
|
649
651
|
try {
|
|
650
|
-
const program =
|
|
652
|
+
const program = parseAst5(code, { lang: "tsx" });
|
|
651
653
|
return getImportedLocalNamesFromProgram(program, importedName);
|
|
652
654
|
} catch {
|
|
653
655
|
return /* @__PURE__ */ new Set();
|
|
654
656
|
}
|
|
655
657
|
}
|
|
656
|
-
function extractImportDeclarations(code,
|
|
658
|
+
function extractImportDeclarations(code, parseAst5) {
|
|
657
659
|
let program;
|
|
658
660
|
try {
|
|
659
|
-
program =
|
|
661
|
+
program = parseAst5(code, { lang: "tsx" });
|
|
660
662
|
} catch {
|
|
661
663
|
return [];
|
|
662
664
|
}
|
|
@@ -680,13 +682,18 @@ function isInertExpression(node) {
|
|
|
680
682
|
(e) => e === null || isInertExpression(e)
|
|
681
683
|
);
|
|
682
684
|
case "ObjectExpression":
|
|
683
|
-
return (node.properties ?? []).every(
|
|
684
|
-
(p
|
|
685
|
-
|
|
685
|
+
return (node.properties ?? []).every((p) => {
|
|
686
|
+
if (p.type === "SpreadElement" || p.type === "RestElement") {
|
|
687
|
+
return isInertExpression(p.argument);
|
|
688
|
+
}
|
|
689
|
+
return p.type === "Property" && (!p.computed || isInertExpression(p.key)) && isInertExpression(p.value);
|
|
690
|
+
});
|
|
686
691
|
case "UnaryExpression":
|
|
687
692
|
return isInertExpression(node.argument);
|
|
688
693
|
case "BinaryExpression":
|
|
689
694
|
return isInertExpression(node.left) && isInertExpression(node.right);
|
|
695
|
+
case "LogicalExpression":
|
|
696
|
+
return isInertExpression(node.left) && isInertExpression(node.right);
|
|
690
697
|
case "ConditionalExpression":
|
|
691
698
|
return isInertExpression(node.test) && isInertExpression(node.consequent) && isInertExpression(node.alternate);
|
|
692
699
|
case "SpreadElement":
|
|
@@ -708,10 +715,10 @@ function isSafeVariableDeclaration(node, handlerNames) {
|
|
|
708
715
|
(d) => isSafeDeclaratorInit(d.init) && !(d.init?.type === "CallExpression" && d.init.callee?.type === "Identifier" && handlerNames.has(d.init.callee.name))
|
|
709
716
|
);
|
|
710
717
|
}
|
|
711
|
-
function extractModuleLevelDeclarations(code,
|
|
718
|
+
function extractModuleLevelDeclarations(code, parseAst5, handlerNames) {
|
|
712
719
|
let program;
|
|
713
720
|
try {
|
|
714
|
-
program =
|
|
721
|
+
program = parseAst5(code, { lang: "tsx" });
|
|
715
722
|
} catch {
|
|
716
723
|
return [];
|
|
717
724
|
}
|
|
@@ -742,17 +749,17 @@ function extractModuleLevelDeclarations(code, parseAst4, handlerNames) {
|
|
|
742
749
|
}
|
|
743
750
|
return declarations;
|
|
744
751
|
}
|
|
745
|
-
function transformInlineHandlers(fnName, virtualPrefix, s, code, filePath, virtualRegistry, moduleId,
|
|
746
|
-
const sites = findHandlerCalls(code, fnName,
|
|
752
|
+
function transformInlineHandlers(fnName, virtualPrefix, s, code, filePath, virtualRegistry, moduleId, parseAst5) {
|
|
753
|
+
const sites = findHandlerCalls(code, fnName, parseAst5);
|
|
747
754
|
const inlineSites = sites.filter((site) => site.exportInfo === null);
|
|
748
755
|
if (inlineSites.length === 0) return false;
|
|
749
|
-
const imports = extractImportDeclarations(code,
|
|
750
|
-
const staticNames = getImportedLocalNames(code, "Static",
|
|
751
|
-
const prerenderNames = getImportedLocalNames(code, "Prerender",
|
|
756
|
+
const imports = extractImportDeclarations(code, parseAst5);
|
|
757
|
+
const staticNames = getImportedLocalNames(code, "Static", parseAst5);
|
|
758
|
+
const prerenderNames = getImportedLocalNames(code, "Prerender", parseAst5);
|
|
752
759
|
const handlerNames = /* @__PURE__ */ new Set([...staticNames, ...prerenderNames]);
|
|
753
760
|
const declarations = extractModuleLevelDeclarations(
|
|
754
761
|
code,
|
|
755
|
-
|
|
762
|
+
parseAst5,
|
|
756
763
|
handlerNames
|
|
757
764
|
);
|
|
758
765
|
const importStatements = [];
|
|
@@ -775,7 +782,7 @@ function transformInlineHandlers(fnName, virtualPrefix, s, code, filePath, virtu
|
|
|
775
782
|
}
|
|
776
783
|
if (importStatements.length > 0) {
|
|
777
784
|
const importBlock = importStatements.join("\n") + "\n";
|
|
778
|
-
const insertionPos = findImportInsertionPos(code,
|
|
785
|
+
const insertionPos = findImportInsertionPos(code, parseAst5);
|
|
779
786
|
if (insertionPos === 0) {
|
|
780
787
|
s.prepend(importBlock);
|
|
781
788
|
} else {
|
|
@@ -808,25 +815,55 @@ function isLineTerminator(ch) {
|
|
|
808
815
|
const c = ch.charCodeAt(0);
|
|
809
816
|
return c === 10 || c === 13 || c === 8232 || c === 8233;
|
|
810
817
|
}
|
|
818
|
+
var REGEX_PRECEDING_KEYWORDS = /* @__PURE__ */ new Set([
|
|
819
|
+
"return",
|
|
820
|
+
"typeof",
|
|
821
|
+
"instanceof",
|
|
822
|
+
"in",
|
|
823
|
+
"of",
|
|
824
|
+
"new",
|
|
825
|
+
"delete",
|
|
826
|
+
"void",
|
|
827
|
+
"do",
|
|
828
|
+
"else",
|
|
829
|
+
"yield",
|
|
830
|
+
"await",
|
|
831
|
+
"case",
|
|
832
|
+
"throw"
|
|
833
|
+
]);
|
|
834
|
+
function isRegexPositionAt(code, slashPos, prevChar) {
|
|
835
|
+
if (prevChar === void 0) return true;
|
|
836
|
+
if (prevChar === ")" || prevChar === "]" || prevChar === "}") return false;
|
|
837
|
+
if (!/[\w$]/.test(prevChar)) return true;
|
|
838
|
+
let k = slashPos - 1;
|
|
839
|
+
while (k >= 0 && /\s/.test(code[k])) k--;
|
|
840
|
+
const wordEnd = k + 1;
|
|
841
|
+
while (k >= 0 && /[\w$]/.test(code[k])) k--;
|
|
842
|
+
return REGEX_PRECEDING_KEYWORDS.has(code.slice(k + 1, wordEnd));
|
|
843
|
+
}
|
|
811
844
|
function makeCodeClassifier(code) {
|
|
812
845
|
const n = code.length;
|
|
813
846
|
let i = 0;
|
|
814
847
|
let skipStart = -1;
|
|
815
848
|
let skipEnd = -1;
|
|
849
|
+
let lastSig;
|
|
816
850
|
return (q) => {
|
|
817
851
|
if (q >= skipStart && q < skipEnd) return false;
|
|
818
852
|
while (i < n && i <= q) {
|
|
819
853
|
const c = code[i];
|
|
820
854
|
const d = i + 1 < n ? code[i + 1] : "";
|
|
821
855
|
let end = -1;
|
|
856
|
+
let transparent = false;
|
|
822
857
|
if (c === "/" && d === "/") {
|
|
823
858
|
let j = i + 2;
|
|
824
859
|
while (j < n && !isLineTerminator(code[j])) j++;
|
|
825
860
|
end = j;
|
|
861
|
+
transparent = true;
|
|
826
862
|
} else if (c === "/" && d === "*") {
|
|
827
863
|
let j = i + 2;
|
|
828
864
|
while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++;
|
|
829
865
|
end = Math.min(n, j + 2);
|
|
866
|
+
transparent = true;
|
|
830
867
|
} else if (c === '"' || c === "'" || c === "`") {
|
|
831
868
|
let j = i + 1;
|
|
832
869
|
while (j < n) {
|
|
@@ -841,6 +878,29 @@ function makeCodeClassifier(code) {
|
|
|
841
878
|
j++;
|
|
842
879
|
}
|
|
843
880
|
end = j;
|
|
881
|
+
} else if (c === "/" && d !== "/" && d !== "*" && isRegexPositionAt(code, i, lastSig)) {
|
|
882
|
+
let j = i + 1;
|
|
883
|
+
let inClass = false;
|
|
884
|
+
let closed = false;
|
|
885
|
+
while (j < n && !isLineTerminator(code[j])) {
|
|
886
|
+
const r = code[j];
|
|
887
|
+
if (r === "\\") {
|
|
888
|
+
j += 2;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (r === "[") inClass = true;
|
|
892
|
+
else if (r === "]") inClass = false;
|
|
893
|
+
else if (r === "/" && !inClass) {
|
|
894
|
+
j++;
|
|
895
|
+
closed = true;
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
j++;
|
|
899
|
+
}
|
|
900
|
+
if (closed) {
|
|
901
|
+
while (j < n && /[a-z]/.test(code[j])) j++;
|
|
902
|
+
end = j;
|
|
903
|
+
}
|
|
844
904
|
}
|
|
845
905
|
if (end >= 0) {
|
|
846
906
|
if (q < end) {
|
|
@@ -849,7 +909,9 @@ function makeCodeClassifier(code) {
|
|
|
849
909
|
return false;
|
|
850
910
|
}
|
|
851
911
|
i = end;
|
|
912
|
+
if (!transparent) lastSig = "x";
|
|
852
913
|
} else {
|
|
914
|
+
if (!/\s/.test(c)) lastSig = c;
|
|
853
915
|
i++;
|
|
854
916
|
}
|
|
855
917
|
}
|
|
@@ -1134,11 +1196,13 @@ function generateClientLoaderStubs(bindings, code, filePath, isBuild) {
|
|
|
1134
1196
|
if (!isExportOnlyFile(code, bindings)) return null;
|
|
1135
1197
|
const lines = [];
|
|
1136
1198
|
for (const binding of bindings) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1199
|
+
const primaryName = binding.exportNames[0];
|
|
1200
|
+
const loaderId = makeStubId(filePath, primaryName, isBuild);
|
|
1201
|
+
lines.push(
|
|
1202
|
+
`export const ${primaryName} = { __brand: "loader", $$id: "${loaderId}" };`
|
|
1203
|
+
);
|
|
1204
|
+
for (const alias of binding.exportNames.slice(1)) {
|
|
1205
|
+
lines.push(`export const ${alias} = ${primaryName};`);
|
|
1142
1206
|
}
|
|
1143
1207
|
}
|
|
1144
1208
|
return { code: lines.join("\n") + "\n" };
|
|
@@ -1159,22 +1223,13 @@ ${binding.localName}.$$id = "${loaderId}";`;
|
|
|
1159
1223
|
}
|
|
1160
1224
|
|
|
1161
1225
|
// src/vite/plugins/expose-ids/handler-transform.ts
|
|
1162
|
-
function
|
|
1163
|
-
const content = code.slice(startPos, endPos).trim();
|
|
1164
|
-
return { hasArgs: content.length > 0 };
|
|
1165
|
-
}
|
|
1166
|
-
function transformHandles(bindings, s, code, filePath, isBuild) {
|
|
1226
|
+
function transformHandles(bindings, s, filePath, isBuild) {
|
|
1167
1227
|
let hasChanges = false;
|
|
1168
1228
|
for (const binding of bindings) {
|
|
1169
1229
|
const exportName = binding.exportNames[0];
|
|
1170
|
-
const args = analyzeCreateHandleArgs(
|
|
1171
|
-
code,
|
|
1172
|
-
binding.callOpenParenPos + 1,
|
|
1173
|
-
binding.callCloseParenPos
|
|
1174
|
-
);
|
|
1175
1230
|
const handleId = makeStubId(filePath, exportName, isBuild);
|
|
1176
1231
|
let paramInjection;
|
|
1177
|
-
if (
|
|
1232
|
+
if (binding.argCount === 0) {
|
|
1178
1233
|
paramInjection = `undefined, "${handleId}"`;
|
|
1179
1234
|
} else {
|
|
1180
1235
|
paramInjection = `, "${handleId}"`;
|
|
@@ -1287,7 +1342,7 @@ function transformRouter(code, filePath, routerFnNames, absolutePath, warn) {
|
|
|
1287
1342
|
if (parenPos === -1) continue;
|
|
1288
1343
|
const closeParen = findMatchingParen(code, parenPos + 1);
|
|
1289
1344
|
const callArgs = code.slice(parenPos + 1, closeParen);
|
|
1290
|
-
if (callArgs.includes(
|
|
1345
|
+
if (callArgs.includes(`$$routeNames: ${routeNamesVar}`)) continue;
|
|
1291
1346
|
const sourceFilePath = absolutePath ?? filePath;
|
|
1292
1347
|
const lineNumber = code.slice(0, callStart).split("\n").length;
|
|
1293
1348
|
const hash = createHash("sha256").update(`${filePath}:${lineNumber}`).digest("hex").slice(0, 8);
|
|
@@ -1831,7 +1886,6 @@ ${lazyImports.join(",\n")}
|
|
|
1831
1886
|
changed = transformHandles(
|
|
1832
1887
|
getBindings(code, fnNames),
|
|
1833
1888
|
s,
|
|
1834
|
-
code,
|
|
1835
1889
|
filePath,
|
|
1836
1890
|
isBuild
|
|
1837
1891
|
) || changed;
|
|
@@ -1908,8 +1962,8 @@ function useCacheTransform() {
|
|
|
1908
1962
|
} = rscTransforms;
|
|
1909
1963
|
let ast;
|
|
1910
1964
|
try {
|
|
1911
|
-
const { parseAst:
|
|
1912
|
-
ast =
|
|
1965
|
+
const { parseAst: parseAst5 } = await import("vite");
|
|
1966
|
+
ast = parseAst5(code, { lang: "tsx" });
|
|
1913
1967
|
} catch {
|
|
1914
1968
|
return;
|
|
1915
1969
|
}
|
|
@@ -2141,11 +2195,16 @@ async function initializeApp() {
|
|
|
2141
2195
|
createTemporaryReferenceSet,
|
|
2142
2196
|
};
|
|
2143
2197
|
|
|
2144
|
-
|
|
2198
|
+
// initBrowserApp resolves the initial payload and returns the browser app
|
|
2199
|
+
// context, including strictMode (default true) from createRouter. StrictMode
|
|
2200
|
+
// is the default; createRouter({ strictMode: false }) ships the opt-out in the
|
|
2201
|
+
// payload metadata. StrictMode emits no DOM, so toggling never changes markup.
|
|
2202
|
+
const { strictMode } = await initBrowserApp({ rscStream, deps });
|
|
2145
2203
|
|
|
2204
|
+
const app = createElement(Rango);
|
|
2146
2205
|
hydrateRoot(
|
|
2147
2206
|
document,
|
|
2148
|
-
createElement(StrictMode, null,
|
|
2207
|
+
strictMode === false ? app : createElement(StrictMode, null, app)
|
|
2149
2208
|
);
|
|
2150
2209
|
}
|
|
2151
2210
|
|
|
@@ -2198,6 +2257,11 @@ export default function handler(request, env) {
|
|
|
2198
2257
|
_handler = createRSCHandler({
|
|
2199
2258
|
router,
|
|
2200
2259
|
version: VERSION,
|
|
2260
|
+
// Forward the router's CSP nonce provider. createRSCHandler reads the
|
|
2261
|
+
// provider only from options.nonce; without this, createRouter({ nonce })
|
|
2262
|
+
// is silently dropped on the Node preset (the Cloudflare path wires it via
|
|
2263
|
+
// router.fetch). router.nonce is undefined when unconfigured, a safe no-op.
|
|
2264
|
+
nonce: router.nonce,
|
|
2201
2265
|
deps: {
|
|
2202
2266
|
renderToReadableStream,
|
|
2203
2267
|
decodeReply,
|
|
@@ -2232,7 +2296,7 @@ import { resolve } from "node:path";
|
|
|
2232
2296
|
// package.json
|
|
2233
2297
|
var package_default = {
|
|
2234
2298
|
name: "@rangojs/router",
|
|
2235
|
-
version: "0.0.0-experimental.
|
|
2299
|
+
version: "0.0.0-experimental.133",
|
|
2236
2300
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2237
2301
|
keywords: [
|
|
2238
2302
|
"react",
|
|
@@ -2785,7 +2849,7 @@ function extractIncludesWithDiagnostics(code, sourceFileArg) {
|
|
|
2785
2849
|
return { resolved, unresolvable };
|
|
2786
2850
|
}
|
|
2787
2851
|
function resolveImportedVariable(code, localName) {
|
|
2788
|
-
const importRegex = /import\s
|
|
2852
|
+
const importRegex = /import\s*(?:[\w$]+\s*,\s*)?\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
|
|
2789
2853
|
let match;
|
|
2790
2854
|
while ((match = importRegex.exec(code)) !== null) {
|
|
2791
2855
|
const imports = match[1];
|
|
@@ -3313,7 +3377,28 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
|
|
|
3313
3377
|
}
|
|
3314
3378
|
|
|
3315
3379
|
// src/vite/plugins/version-plugin.ts
|
|
3380
|
+
import { parseAst as parseAst4 } from "vite";
|
|
3381
|
+
|
|
3382
|
+
// src/vite/utils/directive-prologue.ts
|
|
3316
3383
|
import { parseAst as parseAst3 } from "vite";
|
|
3384
|
+
function hasUseClientDirective(source) {
|
|
3385
|
+
let program;
|
|
3386
|
+
try {
|
|
3387
|
+
program = parseAst3(source, { lang: "tsx" });
|
|
3388
|
+
} catch {
|
|
3389
|
+
return false;
|
|
3390
|
+
}
|
|
3391
|
+
for (const node of program.body ?? []) {
|
|
3392
|
+
if (node?.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string") {
|
|
3393
|
+
if (node.expression.value === "use client") return true;
|
|
3394
|
+
continue;
|
|
3395
|
+
}
|
|
3396
|
+
break;
|
|
3397
|
+
}
|
|
3398
|
+
return false;
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// src/vite/plugins/version-plugin.ts
|
|
3317
3402
|
function isCodeModule(id) {
|
|
3318
3403
|
return /\.(tsx?|jsx?)($|\?)/.test(id);
|
|
3319
3404
|
}
|
|
@@ -3321,23 +3406,13 @@ function normalizeModuleId(id) {
|
|
|
3321
3406
|
return id.split("?", 1)[0];
|
|
3322
3407
|
}
|
|
3323
3408
|
function getClientModuleSignature(source) {
|
|
3409
|
+
if (!hasUseClientDirective(source)) return void 0;
|
|
3324
3410
|
let program;
|
|
3325
3411
|
try {
|
|
3326
|
-
program =
|
|
3412
|
+
program = parseAst4(source, { lang: "tsx" });
|
|
3327
3413
|
} catch {
|
|
3328
3414
|
return void 0;
|
|
3329
3415
|
}
|
|
3330
|
-
let isUseClient = false;
|
|
3331
|
-
for (const node of program.body ?? []) {
|
|
3332
|
-
if (node?.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string") {
|
|
3333
|
-
if (node.expression.value === "use client") {
|
|
3334
|
-
isUseClient = true;
|
|
3335
|
-
}
|
|
3336
|
-
continue;
|
|
3337
|
-
}
|
|
3338
|
-
break;
|
|
3339
|
-
}
|
|
3340
|
-
if (!isUseClient) return void 0;
|
|
3341
3416
|
const exports = /* @__PURE__ */ new Set();
|
|
3342
3417
|
let hasDefault = false;
|
|
3343
3418
|
let hasExportAll = false;
|
|
@@ -3890,13 +3965,17 @@ function createVersionInjectorPlugin(rscEntryPath) {
|
|
|
3890
3965
|
// src/vite/plugins/cjs-to-esm.ts
|
|
3891
3966
|
var debug8 = createRangoDebugger(NS.transform);
|
|
3892
3967
|
function createCjsToEsmPlugin() {
|
|
3968
|
+
let isProduction = false;
|
|
3893
3969
|
return {
|
|
3894
3970
|
name: "@rangojs/router:cjs-to-esm",
|
|
3895
3971
|
enforce: "pre",
|
|
3972
|
+
configResolved(config) {
|
|
3973
|
+
isProduction = config.isProduction;
|
|
3974
|
+
},
|
|
3896
3975
|
transform(code, id) {
|
|
3897
3976
|
const cleanId = id.split("?")[0].replaceAll("\\", "/");
|
|
3898
3977
|
if (cleanId.includes("vendor/react-server-dom/client.browser.js")) {
|
|
3899
|
-
const isProd =
|
|
3978
|
+
const isProd = isProduction;
|
|
3900
3979
|
const cjsFile = isProd ? "./cjs/react-server-dom-webpack-client.browser.production.js" : "./cjs/react-server-dom-webpack-client.browser.development.js";
|
|
3901
3980
|
debug8?.("cjs-to-esm entry redirect %s", id);
|
|
3902
3981
|
return {
|
|
@@ -4353,6 +4432,9 @@ function sortSuffixParams(node) {
|
|
|
4353
4432
|
sorted[suffix] = node.xp[suffix];
|
|
4354
4433
|
}
|
|
4355
4434
|
node.xp = sorted;
|
|
4435
|
+
for (const child of Object.values(node.xp)) {
|
|
4436
|
+
sortSuffixParams(child.c);
|
|
4437
|
+
}
|
|
4356
4438
|
}
|
|
4357
4439
|
if (node.s) {
|
|
4358
4440
|
for (const child of Object.values(node.s)) {
|
|
@@ -4362,11 +4444,6 @@ function sortSuffixParams(node) {
|
|
|
4362
4444
|
if (node.p) {
|
|
4363
4445
|
sortSuffixParams(node.p.c);
|
|
4364
4446
|
}
|
|
4365
|
-
if (node.xp) {
|
|
4366
|
-
for (const child of Object.values(node.xp)) {
|
|
4367
|
-
sortSuffixParams(child.c);
|
|
4368
|
-
}
|
|
4369
|
-
}
|
|
4370
4447
|
}
|
|
4371
4448
|
function buildPerRouterTrie(manifest) {
|
|
4372
4449
|
const ancestry = manifest._routeAncestry;
|
|
@@ -4405,12 +4482,15 @@ function insertRoute(node, segments, index, leaf) {
|
|
|
4405
4482
|
};
|
|
4406
4483
|
insertSegments(node, segments, index, leafBase, []);
|
|
4407
4484
|
}
|
|
4485
|
+
function toVariant(leaf, responseType) {
|
|
4486
|
+
return leaf.pa ? { routeKey: leaf.n, responseType, pa: leaf.pa } : { routeKey: leaf.n, responseType };
|
|
4487
|
+
}
|
|
4408
4488
|
function mergeLeaves(existing, leaf) {
|
|
4409
4489
|
if (!existing) return leaf;
|
|
4410
4490
|
if (existing.rt && leaf.rt) {
|
|
4411
4491
|
const merged = leaf;
|
|
4412
4492
|
merged.nv = existing.nv || [];
|
|
4413
|
-
merged.nv.push(
|
|
4493
|
+
merged.nv.push(toVariant(existing, existing.rt));
|
|
4414
4494
|
return merged;
|
|
4415
4495
|
}
|
|
4416
4496
|
if (leaf.rt && !existing.rt) {
|
|
@@ -4418,13 +4498,13 @@ function mergeLeaves(existing, leaf) {
|
|
|
4418
4498
|
existing.nv = [];
|
|
4419
4499
|
existing.rf = true;
|
|
4420
4500
|
}
|
|
4421
|
-
existing.nv.push(
|
|
4501
|
+
existing.nv.push(toVariant(leaf, leaf.rt));
|
|
4422
4502
|
return existing;
|
|
4423
4503
|
}
|
|
4424
4504
|
if (!leaf.rt && existing.rt) {
|
|
4425
4505
|
if (!leaf.nv) leaf.nv = [];
|
|
4426
4506
|
if (existing.nv) leaf.nv.push(...existing.nv);
|
|
4427
|
-
leaf.nv.push(
|
|
4507
|
+
leaf.nv.push(toVariant(existing, existing.rt));
|
|
4428
4508
|
return leaf;
|
|
4429
4509
|
}
|
|
4430
4510
|
return leaf;
|
|
@@ -4621,13 +4701,24 @@ async function runWithConcurrency(items, concurrency, fn) {
|
|
|
4621
4701
|
return;
|
|
4622
4702
|
}
|
|
4623
4703
|
let nextIndex = 0;
|
|
4704
|
+
let firstError;
|
|
4705
|
+
let failed = false;
|
|
4624
4706
|
async function worker() {
|
|
4625
|
-
while (nextIndex < items.length) {
|
|
4707
|
+
while (nextIndex < items.length && !failed) {
|
|
4626
4708
|
const idx = nextIndex++;
|
|
4627
|
-
|
|
4709
|
+
try {
|
|
4710
|
+
await fn(items[idx]);
|
|
4711
|
+
} catch (err) {
|
|
4712
|
+
if (!failed) {
|
|
4713
|
+
failed = true;
|
|
4714
|
+
firstError = err;
|
|
4715
|
+
}
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4628
4718
|
}
|
|
4629
4719
|
}
|
|
4630
4720
|
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
4721
|
+
if (failed) throw firstError;
|
|
4631
4722
|
}
|
|
4632
4723
|
function groupByConcurrency(entries) {
|
|
4633
4724
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -7151,6 +7242,12 @@ ${list}`);
|
|
|
7151
7242
|
"@vitejs/plugin-rsc/vendor/react-server-dom/server.edge"
|
|
7152
7243
|
)
|
|
7153
7244
|
],
|
|
7245
|
+
// Vite 8 does not propagate the top-level optimizeDeps.exclude
|
|
7246
|
+
// (set in config()) to non-client envs, so the rsc env must set
|
|
7247
|
+
// it explicitly — mirroring the node ssr env and the cloudflare
|
|
7248
|
+
// rsc env. Without it a strict-pnpm npm-installed app can try to
|
|
7249
|
+
// pre-bundle the router's own subpath entries and fail.
|
|
7250
|
+
exclude: excludeDeps,
|
|
7154
7251
|
rolldownOptions: sharedRolldownOptions
|
|
7155
7252
|
}
|
|
7156
7253
|
}
|
|
@@ -7193,8 +7290,7 @@ ${list}`);
|
|
|
7193
7290
|
return;
|
|
7194
7291
|
try {
|
|
7195
7292
|
const source = readFileSync7(file, "utf-8");
|
|
7196
|
-
|
|
7197
|
-
if (trimmed.startsWith('"use client"') || trimmed.startsWith("'use client'")) {
|
|
7293
|
+
if (hasUseClientDirective(source)) {
|
|
7198
7294
|
return [];
|
|
7199
7295
|
}
|
|
7200
7296
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.133",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -162,6 +162,17 @@
|
|
|
162
162
|
"access": "public",
|
|
163
163
|
"tag": "experimental"
|
|
164
164
|
},
|
|
165
|
+
"scripts": {
|
|
166
|
+
"build": "pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
167
|
+
"prepublishOnly": "pnpm build",
|
|
168
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
169
|
+
"test": "playwright test",
|
|
170
|
+
"test:ui": "playwright test --ui",
|
|
171
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
172
|
+
"test:unit": "vitest run",
|
|
173
|
+
"test:unit:watch": "vitest",
|
|
174
|
+
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
175
|
+
},
|
|
165
176
|
"dependencies": {
|
|
166
177
|
"@types/debug": "^4.1.12",
|
|
167
178
|
"@vitejs/plugin-rsc": "^0.5.26",
|
|
@@ -173,19 +184,19 @@
|
|
|
173
184
|
},
|
|
174
185
|
"devDependencies": {
|
|
175
186
|
"@playwright/test": "^1.49.1",
|
|
187
|
+
"@shared/e2e": "workspace:*",
|
|
176
188
|
"@testing-library/dom": "^10.4.1",
|
|
177
189
|
"@testing-library/react": "^16.3.2",
|
|
178
190
|
"@types/node": "^24.10.1",
|
|
179
|
-
"@types/react": "
|
|
180
|
-
"@types/react-dom": "
|
|
191
|
+
"@types/react": "catalog:",
|
|
192
|
+
"@types/react-dom": "catalog:",
|
|
181
193
|
"esbuild": "^0.27.0",
|
|
182
194
|
"happy-dom": "^20.10.1",
|
|
183
195
|
"jiti": "^2.6.1",
|
|
184
|
-
"react": "
|
|
185
|
-
"react-dom": "
|
|
196
|
+
"react": "catalog:",
|
|
197
|
+
"react-dom": "catalog:",
|
|
186
198
|
"typescript": "^5.3.0",
|
|
187
|
-
"vitest": "^4.0.0"
|
|
188
|
-
"@shared/e2e": "0.0.1"
|
|
199
|
+
"vitest": "^4.0.0"
|
|
189
200
|
},
|
|
190
201
|
"peerDependencies": {
|
|
191
202
|
"@cloudflare/vite-plugin": "^1.38.0",
|
|
@@ -216,15 +227,5 @@
|
|
|
216
227
|
},
|
|
217
228
|
"engines": {
|
|
218
229
|
"node": "^20.19.0 || >=22.12.0"
|
|
219
|
-
},
|
|
220
|
-
"scripts": {
|
|
221
|
-
"build": "pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
222
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
223
|
-
"test": "playwright test",
|
|
224
|
-
"test:ui": "playwright test --ui",
|
|
225
|
-
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
226
|
-
"test:unit": "vitest run",
|
|
227
|
-
"test:unit:watch": "vitest",
|
|
228
|
-
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
229
230
|
}
|
|
230
|
-
}
|
|
231
|
+
}
|
|
@@ -206,7 +206,7 @@ path("/dashboard", (ctx) => {
|
|
|
206
206
|
```tsx
|
|
207
207
|
// Client component
|
|
208
208
|
"use client";
|
|
209
|
-
import { useHandle,
|
|
209
|
+
import { useHandle, Breadcrumbs } from "@rangojs/router/client";
|
|
210
210
|
|
|
211
211
|
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
212
212
|
const crumbs = useHandle(handle);
|
|
@@ -41,7 +41,7 @@ import { join } from "node:path";
|
|
|
41
41
|
// ... your other imports ...
|
|
42
42
|
|
|
43
43
|
function analyze(): PluginOption[] {
|
|
44
|
-
if (!process.env.
|
|
44
|
+
if (!process.env.RANGO_ANALYZE) return [];
|
|
45
45
|
return (["client", "ssr", "rsc"] as const).map((envName) => {
|
|
46
46
|
const inner = visualizer({
|
|
47
47
|
filename: join("bundle-stats", `${envName}.html`),
|
|
@@ -79,7 +79,7 @@ Add `bundle-stats/` to your `.gitignore`.
|
|
|
79
79
|
## Step 3: Build with the analyzer enabled
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
|
|
82
|
+
RANGO_ANALYZE=1 pnpm exec vite build
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
You'll get three HTML reports in `bundle-stats/`:
|
|
@@ -424,7 +424,7 @@ subsequent siblings. Everything below the cache boundary is cached as one unit:
|
|
|
424
424
|
|
|
425
425
|
```typescript
|
|
426
426
|
path("/dashboard", DashboardPage, { name: "dashboard" }, () => [
|
|
427
|
-
cache(
|
|
427
|
+
cache({ ttl: 300 }),
|
|
428
428
|
layout(DashboardSidebar, () => [
|
|
429
429
|
parallel("@stats", StatsPanel),
|
|
430
430
|
parallel("@activity", ActivityFeed),
|
|
@@ -444,7 +444,7 @@ boundary are not cached and always re-render:
|
|
|
444
444
|
layout(RootLayout, () => [
|
|
445
445
|
// RootLayout is NOT cached — runs every request
|
|
446
446
|
path("/products/:slug", ProductPage, { name: "product" }, () => [
|
|
447
|
-
cache(
|
|
447
|
+
cache({ ttl: 300 }),
|
|
448
448
|
layout(ProductSidebar),
|
|
449
449
|
parallel("@reviews", ReviewsPanel),
|
|
450
450
|
parallel("@related", RelatedProducts),
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -153,10 +153,11 @@ Purge-by-tag is available on all plans (since April 2025), subject to per-plan
|
|
|
153
153
|
rate limits, so the batched single call matters. With a purge wired, `tagCacheTtl`
|
|
154
154
|
becomes a pure read-cost reducer + fallback window.
|
|
155
155
|
|
|
156
|
-
## Named
|
|
156
|
+
## Named Cache Profiles
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
Define named profiles in `createRouter({ cacheProfiles })` so the same TTL/SWR
|
|
159
|
+
values can be shared across the DSL and `"use cache"` functions without repetition.
|
|
160
|
+
Unknown names throw at boot time.
|
|
160
161
|
|
|
161
162
|
```typescript
|
|
162
163
|
// Define profiles in router
|
|
@@ -167,19 +168,25 @@ createRouter({
|
|
|
167
168
|
long: { ttl: 3600, swr: 7200 },
|
|
168
169
|
},
|
|
169
170
|
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
In the DSL, pass the profile's options directly to `cache()`:
|
|
170
174
|
|
|
171
|
-
|
|
175
|
+
```typescript
|
|
172
176
|
export const urlpatterns = urls(({ path, cache }) => [
|
|
173
|
-
cache(
|
|
177
|
+
cache({ ttl: 3600, swr: 7200 }, () => [
|
|
178
|
+
path("/blog", BlogIndex, { name: "blog" }),
|
|
179
|
+
]),
|
|
174
180
|
|
|
175
|
-
//
|
|
176
|
-
cache(
|
|
181
|
+
// Orphan cache boundary (covers subsequent siblings)
|
|
182
|
+
cache({ ttl: 60, swr: 120 }),
|
|
177
183
|
path("/feed", FeedPage, { name: "feed" }),
|
|
178
184
|
]);
|
|
179
185
|
```
|
|
180
186
|
|
|
181
|
-
|
|
182
|
-
|
|
187
|
+
The DSL `cache()` helper does NOT accept a string profile name — strings are only
|
|
188
|
+
valid in the `"use cache: <name>"` directive inside server functions. See
|
|
189
|
+
`/use-cache` for function-level caching with named profiles.
|
|
183
190
|
|
|
184
191
|
## Loader-Level Caching
|
|
185
192
|
|