@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.
Files changed (141) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +43 -2
  3. package/dist/bin/rango.js +92 -16
  4. package/dist/vite/index.js +166 -70
  5. package/package.json +19 -18
  6. package/skills/breadcrumbs/SKILL.md +1 -1
  7. package/skills/bundle-analysis/SKILL.md +2 -2
  8. package/skills/cache-guide/SKILL.md +2 -2
  9. package/skills/caching/SKILL.md +16 -9
  10. package/skills/debug-manifest/SKILL.md +4 -2
  11. package/skills/document-cache/SKILL.md +2 -2
  12. package/skills/handler-use/SKILL.md +1 -1
  13. package/skills/hooks/SKILL.md +2 -2
  14. package/skills/host-router/SKILL.md +1 -1
  15. package/skills/intercept/SKILL.md +1 -1
  16. package/skills/loader/SKILL.md +2 -0
  17. package/skills/migrate-react-router/SKILL.md +4 -2
  18. package/skills/mime-routes/SKILL.md +1 -1
  19. package/skills/prerender/SKILL.md +2 -0
  20. package/skills/rango/SKILL.md +12 -11
  21. package/skills/response-routes/SKILL.md +2 -2
  22. package/skills/route/SKILL.md +4 -0
  23. package/skills/router-setup/SKILL.md +3 -0
  24. package/skills/scripts/SKILL.md +179 -0
  25. package/skills/testing/SKILL.md +1 -1
  26. package/skills/testing/bindings.md +20 -6
  27. package/skills/testing/cache-prerender.md +5 -2
  28. package/skills/testing/client-components.md +2 -0
  29. package/skills/testing/e2e-parity.md +1 -1
  30. package/skills/testing/flight.md +8 -9
  31. package/skills/testing/render-handler.md +1 -1
  32. package/skills/testing/response-routes.md +1 -1
  33. package/skills/testing/server-actions.md +11 -11
  34. package/skills/testing/setup.md +3 -0
  35. package/skills/typesafety/SKILL.md +3 -2
  36. package/skills/use-cache/SKILL.md +10 -9
  37. package/src/browser/event-controller.ts +109 -2
  38. package/src/browser/partial-update.ts +12 -0
  39. package/src/browser/prefetch/cache.ts +17 -0
  40. package/src/browser/prefetch/fetch.ts +69 -2
  41. package/src/browser/react/Link.tsx +30 -5
  42. package/src/browser/react/NavigationProvider.tsx +12 -2
  43. package/src/browser/react/location-state-shared.ts +14 -2
  44. package/src/browser/react/use-href.tsx +8 -1
  45. package/src/browser/react/use-link-status.ts +23 -2
  46. package/src/browser/response-adapter.ts +14 -3
  47. package/src/browser/rsc-router.tsx +3 -0
  48. package/src/browser/scroll-restoration.ts +8 -3
  49. package/src/browser/server-action-bridge.ts +46 -11
  50. package/src/browser/types.ts +6 -0
  51. package/src/build/generate-route-types.ts +0 -1
  52. package/src/build/route-trie.ts +33 -9
  53. package/src/build/route-types/include-resolution.ts +7 -1
  54. package/src/build/route-types/router-processing.ts +0 -6
  55. package/src/build/route-types/source-scan.ts +105 -7
  56. package/src/cache/cache-policy.ts +42 -8
  57. package/src/cache/cache-runtime.ts +65 -5
  58. package/src/cache/cache-scope.ts +71 -11
  59. package/src/cache/cache-tag.ts +7 -2
  60. package/src/cache/cf/cf-base64.ts +33 -0
  61. package/src/cache/cf/cf-cache-constants.ts +127 -0
  62. package/src/cache/cf/cf-cache-store.ts +85 -613
  63. package/src/cache/cf/cf-cache-types.ts +349 -0
  64. package/src/cache/cf/cf-kv-utils.ts +46 -0
  65. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  66. package/src/cache/document-cache.ts +11 -0
  67. package/src/cache/handle-snapshot.ts +8 -1
  68. package/src/cache/profile-registry.ts +25 -1
  69. package/src/cache/segment-codec.ts +9 -1
  70. package/src/cache/types.ts +4 -0
  71. package/src/client.rsc.tsx +38 -0
  72. package/src/client.tsx +11 -0
  73. package/src/components/DefaultDocument.tsx +8 -2
  74. package/src/context-var.ts +1 -1
  75. package/src/decode-loader-results.ts +7 -1
  76. package/src/escape-script.ts +52 -0
  77. package/src/handles/MetaTags.tsx +56 -5
  78. package/src/handles/Scripts.tsx +183 -0
  79. package/src/handles/breadcrumbs.ts +29 -11
  80. package/src/handles/is-thenable.ts +19 -0
  81. package/src/handles/meta.ts +46 -0
  82. package/src/handles/script.ts +244 -0
  83. package/src/host/cookie-handler.ts +7 -3
  84. package/src/host/pattern-matcher.ts +16 -2
  85. package/src/index.rsc.ts +5 -0
  86. package/src/index.ts +5 -0
  87. package/src/response-utils.ts +25 -0
  88. package/src/route-definition/dsl-helpers.ts +7 -0
  89. package/src/route-definition/redirect.ts +1 -2
  90. package/src/router/content-negotiation.ts +58 -10
  91. package/src/router/intercept-resolution.ts +9 -0
  92. package/src/router/match-middleware/cache-store.ts +10 -1
  93. package/src/router/middleware.ts +10 -3
  94. package/src/router/pattern-matching.ts +25 -23
  95. package/src/router/prefetch-cache-ttl.ts +51 -0
  96. package/src/router/router-interfaces.ts +7 -0
  97. package/src/router/router-options.ts +23 -0
  98. package/src/router/segment-resolution/fresh.ts +10 -0
  99. package/src/router/segment-resolution/helpers.ts +35 -1
  100. package/src/router/segment-resolution/loader-cache.ts +10 -6
  101. package/src/router/segment-resolution/revalidation.ts +6 -0
  102. package/src/router/segment-resolution.ts +1 -0
  103. package/src/router/trie-matching.ts +14 -9
  104. package/src/router.ts +18 -10
  105. package/src/rsc/handler.ts +52 -13
  106. package/src/rsc/helpers.ts +7 -1
  107. package/src/rsc/index.ts +1 -4
  108. package/src/rsc/loader-fetch.ts +107 -37
  109. package/src/rsc/progressive-enhancement.ts +18 -6
  110. package/src/rsc/response-cache-serve.ts +238 -0
  111. package/src/rsc/response-route-handler.ts +16 -133
  112. package/src/rsc/rsc-rendering.ts +13 -4
  113. package/src/rsc/server-action.ts +52 -6
  114. package/src/rsc/types.ts +7 -0
  115. package/src/search-params.ts +24 -5
  116. package/src/segment-loader-promise.ts +17 -2
  117. package/src/server/loader-registry.ts +16 -18
  118. package/src/server/request-context.ts +47 -20
  119. package/src/testing/dispatch.ts +108 -25
  120. package/src/testing/flight.ts +25 -0
  121. package/src/testing/internal/context.ts +25 -2
  122. package/src/testing/render-handler.ts +3 -1
  123. package/src/testing/render-route.tsx +15 -0
  124. package/src/testing/run-loader.ts +10 -3
  125. package/src/theme/ThemeProvider.tsx +20 -6
  126. package/src/theme/ThemeScript.tsx +7 -3
  127. package/src/theme/constants.ts +54 -3
  128. package/src/theme/theme-script.ts +22 -7
  129. package/src/types/request-scope.ts +8 -3
  130. package/src/vite/plugins/cjs-to-esm.ts +8 -1
  131. package/src/vite/plugins/expose-id-utils.ts +10 -1
  132. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -16
  133. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -5
  134. package/src/vite/plugins/expose-ids/router-transform.ts +6 -1
  135. package/src/vite/plugins/expose-internal-ids.ts +0 -1
  136. package/src/vite/plugins/version-plugin.ts +5 -17
  137. package/src/vite/plugins/virtual-entries.ts +12 -2
  138. package/src/vite/rango.ts +15 -6
  139. package/src/vite/utils/ast-handler-extract.ts +11 -4
  140. package/src/vite/utils/directive-prologue.ts +40 -0
  141. package/src/vite/utils/prerender-utils.ts +17 -2
package/AGENTS.md CHANGED
@@ -7,3 +7,11 @@ Run `/rango` to understand the API. Detailed guides for each feature are in the
7
7
  ## Development rules
8
8
 
9
9
  - Always commit generated files (e.g. `*.gen.ts`) alongside the source changes that produced them.
10
+
11
+ ## Repo-wide rules (read before pushing)
12
+
13
+ This package inherits the repo-wide conventions in the root [`AGENTS.md`](../../AGENTS.md) and [`CLAUDE.md`](../../CLAUDE.md). The ones a package-scoped reader is most likely to miss:
14
+
15
+ - **Pre-push gate** — before EVERY push, run all of the following from the **repo root** and fix any failures: `pnpm run typecheck`, `pnpm run test:unit:all`, `pnpm run lint`, `pnpm run format`.
16
+ - **`test:unit:all` is recursive** — it runs the unit AND Flight/RSC suites for every package and consumer app (cloudflare-basic, mini, vite-rsc-demo, ...), not just `@rangojs/router`. A change can pass this package's own tests while breaking a consumer app's `@rangojs/router/testing` dogfood suite, so do not run only `pnpm --filter @rangojs/router test:unit`.
17
+ - **Dev + prod e2e parity is mandatory** — every e2e test must cover BOTH dev and production modes; never add a dev-only test without its production counterpart. See the dev/prod bucketing convention in the root `AGENTS.md`.
package/README.md CHANGED
@@ -127,20 +127,29 @@ export const router = createRouter().routes(urlpatterns);
127
127
  "use client";
128
128
 
129
129
  import type { ReactNode } from "react";
130
- import { MetaTags } from "@rangojs/router/client";
130
+ import { MetaTags, Scripts } from "@rangojs/router/client";
131
131
 
132
132
  export function Document({ children }: { children: ReactNode }) {
133
133
  return (
134
134
  <html lang="en">
135
135
  <head>
136
136
  <MetaTags />
137
+ <Scripts />
137
138
  </head>
138
- <body>{children}</body>
139
+ <body>
140
+ <Scripts position="body" />
141
+ {children}
142
+ </body>
139
143
  </html>
140
144
  );
141
145
  }
142
146
  ```
143
147
 
148
+ `<MetaTags />` and `<Scripts />` render the tags collected by the built-in `Meta`
149
+ and `Script` handles (see [Meta Tags](#meta-tags) and [Scripts](#scripts)). The
150
+ built-in `DefaultDocument` already includes all three sites, so this is only
151
+ needed for a custom document.
152
+
144
153
  ## Defining Routes
145
154
 
146
155
  Rango is a named-route router first.
@@ -978,6 +987,38 @@ export function BlogPostPage(ctx: HandlerContext) {
978
987
 
979
988
  Render collected tags in the document with `<MetaTags />` from `@rangojs/router/client`.
980
989
 
990
+ ## Scripts
991
+
992
+ Inject `<script>` tags (analytics, GTM, widgets) the same way, using the built-in
993
+ `Script` handle — push a config from a handler, render with `<Scripts />`:
994
+
995
+ ```tsx
996
+ import { Script } from "@rangojs/router";
997
+ import type { HandlerContext } from "@rangojs/router";
998
+ import { Outlet } from "@rangojs/router/client";
999
+
1000
+ export function RootLayout(ctx: HandlerContext) {
1001
+ // Inline bootstrap (GTM/GA4/Segment) — rendered with the request CSP nonce.
1002
+ ctx.use(Script)({ id: "gtm", children: gtmBootstrap("GTM-XXXX") });
1003
+ // External async resource (loads on first encounter, deduped by src).
1004
+ ctx.use(Script)({
1005
+ id: "plausible",
1006
+ src: "https://plausible.io/js/script.js",
1007
+ async: true,
1008
+ attributes: { "data-domain": "example.com" },
1009
+ });
1010
+ return <Outlet />;
1011
+ }
1012
+ ```
1013
+
1014
+ Render with `<Scripts />` (head) and `<Scripts position="body" />` (body) from
1015
+ `@rangojs/router/client` (both are wired in `DefaultDocument`). The request CSP
1016
+ nonce is applied automatically to document-rendered scripts. `ScriptConfig` is a
1017
+ discriminated union (inline / external-async / external-ordered), and inline +
1018
+ ordered scripts are document-load while async externals are React resources — see
1019
+ the [`/scripts` skill](./skills/scripts/SKILL.md) for the full execution contract
1020
+ and CSP guidance.
1021
+
981
1022
  ## CLI: `rango generate`
982
1023
 
983
1024
  Route types are generated automatically by the Vite plugin. The CLI is a manual fallback for generating types outside the dev server (e.g. in CI or for IDE support before first `pnpm dev`):
package/dist/bin/rango.js CHANGED
@@ -339,7 +339,7 @@ function extractIncludesWithDiagnostics(code, sourceFileArg) {
339
339
  return { resolved, unresolvable };
340
340
  }
341
341
  function resolveImportedVariable(code, localName) {
342
- const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
342
+ const importRegex = /import\s*(?:[\w$]+\s*,\s*)?\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
343
343
  let match;
344
344
  while ((match = importRegex.exec(code)) !== null) {
345
345
  const imports = match[1];
@@ -635,25 +635,39 @@ function isLineTerminator(ch) {
635
635
  const c = ch.charCodeAt(0);
636
636
  return c === 10 || c === 13 || c === 8232 || c === 8233;
637
637
  }
638
+ function isRegexPositionAt(code, slashPos, prevChar) {
639
+ if (prevChar === void 0) return true;
640
+ if (prevChar === ")" || prevChar === "]" || prevChar === "}") return false;
641
+ if (!/[\w$]/.test(prevChar)) return true;
642
+ let k = slashPos - 1;
643
+ while (k >= 0 && /\s/.test(code[k])) k--;
644
+ const wordEnd = k + 1;
645
+ while (k >= 0 && /[\w$]/.test(code[k])) k--;
646
+ return REGEX_PRECEDING_KEYWORDS.has(code.slice(k + 1, wordEnd));
647
+ }
638
648
  function makeCodeClassifier(code) {
639
649
  const n = code.length;
640
650
  let i = 0;
641
651
  let skipStart = -1;
642
652
  let skipEnd = -1;
653
+ let lastSig;
643
654
  return (q) => {
644
655
  if (q >= skipStart && q < skipEnd) return false;
645
656
  while (i < n && i <= q) {
646
657
  const c = code[i];
647
658
  const d = i + 1 < n ? code[i + 1] : "";
648
659
  let end = -1;
660
+ let transparent = false;
649
661
  if (c === "/" && d === "/") {
650
662
  let j = i + 2;
651
663
  while (j < n && !isLineTerminator(code[j])) j++;
652
664
  end = j;
665
+ transparent = true;
653
666
  } else if (c === "/" && d === "*") {
654
667
  let j = i + 2;
655
668
  while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++;
656
669
  end = Math.min(n, j + 2);
670
+ transparent = true;
657
671
  } else if (c === '"' || c === "'" || c === "`") {
658
672
  let j = i + 1;
659
673
  while (j < n) {
@@ -668,6 +682,29 @@ function makeCodeClassifier(code) {
668
682
  j++;
669
683
  }
670
684
  end = j;
685
+ } else if (c === "/" && d !== "/" && d !== "*" && isRegexPositionAt(code, i, lastSig)) {
686
+ let j = i + 1;
687
+ let inClass = false;
688
+ let closed = false;
689
+ while (j < n && !isLineTerminator(code[j])) {
690
+ const r = code[j];
691
+ if (r === "\\") {
692
+ j += 2;
693
+ continue;
694
+ }
695
+ if (r === "[") inClass = true;
696
+ else if (r === "]") inClass = false;
697
+ else if (r === "/" && !inClass) {
698
+ j++;
699
+ closed = true;
700
+ break;
701
+ }
702
+ j++;
703
+ }
704
+ if (closed) {
705
+ while (j < n && /[a-z]/.test(code[j])) j++;
706
+ end = j;
707
+ }
671
708
  }
672
709
  if (end >= 0) {
673
710
  if (q < end) {
@@ -676,7 +713,9 @@ function makeCodeClassifier(code) {
676
713
  return false;
677
714
  }
678
715
  i = end;
716
+ if (!transparent) lastSig = "x";
679
717
  } else {
718
+ if (!/\s/.test(c)) lastSig = c;
680
719
  i++;
681
720
  }
682
721
  }
@@ -693,9 +732,26 @@ function firstCodeMatchIndex(code, pattern) {
693
732
  }
694
733
  return -1;
695
734
  }
735
+ var REGEX_PRECEDING_KEYWORDS;
696
736
  var init_source_scan = __esm({
697
737
  "src/build/route-types/source-scan.ts"() {
698
738
  "use strict";
739
+ REGEX_PRECEDING_KEYWORDS = /* @__PURE__ */ new Set([
740
+ "return",
741
+ "typeof",
742
+ "instanceof",
743
+ "in",
744
+ "of",
745
+ "new",
746
+ "delete",
747
+ "void",
748
+ "do",
749
+ "else",
750
+ "yield",
751
+ "await",
752
+ "case",
753
+ "throw"
754
+ ]);
699
755
  }
700
756
  });
701
757
 
@@ -1153,11 +1209,16 @@ async function initializeApp() {
1153
1209
  createTemporaryReferenceSet,
1154
1210
  };
1155
1211
 
1156
- await initBrowserApp({ rscStream, deps });
1212
+ // initBrowserApp resolves the initial payload and returns the browser app
1213
+ // context, including strictMode (default true) from createRouter. StrictMode
1214
+ // is the default; createRouter({ strictMode: false }) ships the opt-out in the
1215
+ // payload metadata. StrictMode emits no DOM, so toggling never changes markup.
1216
+ const { strictMode } = await initBrowserApp({ rscStream, deps });
1157
1217
 
1218
+ const app = createElement(Rango);
1158
1219
  hydrateRoot(
1159
1220
  document,
1160
- createElement(StrictMode, null, createElement(Rango))
1221
+ strictMode === false ? app : createElement(StrictMode, null, app)
1161
1222
  );
1162
1223
  }
1163
1224
 
@@ -1186,13 +1247,37 @@ export const renderHTML = createSSRHandler({
1186
1247
  }
1187
1248
  });
1188
1249
 
1250
+ // src/vite/utils/directive-prologue.ts
1251
+ import { parseAst } from "vite";
1252
+ function hasUseClientDirective(source) {
1253
+ let program;
1254
+ try {
1255
+ program = parseAst(source, { lang: "tsx" });
1256
+ } catch {
1257
+ return false;
1258
+ }
1259
+ for (const node of program.body ?? []) {
1260
+ if (node?.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string") {
1261
+ if (node.expression.value === "use client") return true;
1262
+ continue;
1263
+ }
1264
+ break;
1265
+ }
1266
+ return false;
1267
+ }
1268
+ var init_directive_prologue = __esm({
1269
+ "src/vite/utils/directive-prologue.ts"() {
1270
+ "use strict";
1271
+ }
1272
+ });
1273
+
1189
1274
  // src/vite/plugins/version-plugin.ts
1190
1275
  var version_plugin_exports = {};
1191
1276
  __export(version_plugin_exports, {
1192
1277
  createVersionPlugin: () => createVersionPlugin,
1193
1278
  isViteDepCachePath: () => isViteDepCachePath
1194
1279
  });
1195
- import { parseAst } from "vite";
1280
+ import { parseAst as parseAst2 } from "vite";
1196
1281
  function isCodeModule(id) {
1197
1282
  return /\.(tsx?|jsx?)($|\?)/.test(id);
1198
1283
  }
@@ -1200,23 +1285,13 @@ function normalizeModuleId(id) {
1200
1285
  return id.split("?", 1)[0];
1201
1286
  }
1202
1287
  function getClientModuleSignature(source) {
1288
+ if (!hasUseClientDirective(source)) return void 0;
1203
1289
  let program;
1204
1290
  try {
1205
- program = parseAst(source, { lang: "tsx" });
1291
+ program = parseAst2(source, { lang: "tsx" });
1206
1292
  } catch {
1207
1293
  return void 0;
1208
1294
  }
1209
- let isUseClient = false;
1210
- for (const node of program.body ?? []) {
1211
- if (node?.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string") {
1212
- if (node.expression.value === "use client") {
1213
- isUseClient = true;
1214
- }
1215
- continue;
1216
- }
1217
- break;
1218
- }
1219
- if (!isUseClient) return void 0;
1220
1295
  const exports = /* @__PURE__ */ new Set();
1221
1296
  let hasDefault = false;
1222
1297
  let hasExportAll = false;
@@ -1390,6 +1465,7 @@ var init_version_plugin = __esm({
1390
1465
  "src/vite/plugins/version-plugin.ts"() {
1391
1466
  "use strict";
1392
1467
  init_virtual_entries();
1468
+ init_directive_prologue();
1393
1469
  }
1394
1470
  });
1395
1471