@rangojs/router 0.0.0-experimental.b02a2fec → 0.0.0-experimental.b30bbf02

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 (112) hide show
  1. package/README.md +112 -17
  2. package/dist/vite/index.js +1338 -462
  3. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  4. package/package.json +7 -5
  5. package/skills/breadcrumbs/SKILL.md +3 -1
  6. package/skills/handler-use/SKILL.md +362 -0
  7. package/skills/hooks/SKILL.md +33 -20
  8. package/skills/intercept/SKILL.md +20 -0
  9. package/skills/layout/SKILL.md +22 -0
  10. package/skills/links/SKILL.md +90 -16
  11. package/skills/loader/SKILL.md +70 -3
  12. package/skills/middleware/SKILL.md +34 -3
  13. package/skills/migrate-nextjs/SKILL.md +562 -0
  14. package/skills/migrate-react-router/SKILL.md +769 -0
  15. package/skills/parallel/SKILL.md +66 -0
  16. package/skills/rango/SKILL.md +25 -22
  17. package/skills/response-routes/SKILL.md +8 -0
  18. package/skills/route/SKILL.md +24 -0
  19. package/skills/server-actions/SKILL.md +739 -0
  20. package/skills/streams-and-websockets/SKILL.md +283 -0
  21. package/skills/typesafety/SKILL.md +3 -1
  22. package/src/browser/app-shell.ts +52 -0
  23. package/src/browser/event-controller.ts +44 -4
  24. package/src/browser/navigation-bridge.ts +71 -5
  25. package/src/browser/navigation-client.ts +64 -13
  26. package/src/browser/navigation-store.ts +25 -1
  27. package/src/browser/partial-update.ts +34 -3
  28. package/src/browser/prefetch/cache.ts +129 -21
  29. package/src/browser/prefetch/fetch.ts +148 -16
  30. package/src/browser/prefetch/queue.ts +36 -5
  31. package/src/browser/rango-state.ts +53 -13
  32. package/src/browser/react/Link.tsx +30 -2
  33. package/src/browser/react/NavigationProvider.tsx +70 -18
  34. package/src/browser/react/filter-segment-order.ts +51 -7
  35. package/src/browser/react/use-navigation.ts +22 -2
  36. package/src/browser/react/use-params.ts +11 -1
  37. package/src/browser/react/use-router.ts +8 -1
  38. package/src/browser/react/use-segments.ts +11 -8
  39. package/src/browser/rsc-router.tsx +34 -6
  40. package/src/browser/segment-reconciler.ts +36 -14
  41. package/src/browser/types.ts +19 -0
  42. package/src/build/route-trie.ts +50 -24
  43. package/src/cache/cf/cf-cache-store.ts +5 -7
  44. package/src/client.tsx +82 -174
  45. package/src/index.rsc.ts +3 -0
  46. package/src/index.ts +40 -9
  47. package/src/outlet-context.ts +1 -1
  48. package/src/response-utils.ts +28 -0
  49. package/src/reverse.ts +7 -3
  50. package/src/route-definition/dsl-helpers.ts +175 -23
  51. package/src/route-definition/helpers-types.ts +63 -14
  52. package/src/route-definition/resolve-handler-use.ts +6 -0
  53. package/src/route-types.ts +7 -0
  54. package/src/router/handler-context.ts +24 -4
  55. package/src/router/lazy-includes.ts +6 -6
  56. package/src/router/loader-resolution.ts +3 -0
  57. package/src/router/manifest.ts +22 -13
  58. package/src/router/match-api.ts +4 -3
  59. package/src/router/match-handlers.ts +1 -0
  60. package/src/router/match-result.ts +21 -2
  61. package/src/router/middleware-types.ts +2 -22
  62. package/src/router/middleware.ts +54 -7
  63. package/src/router/pattern-matching.ts +87 -17
  64. package/src/router/revalidation.ts +15 -1
  65. package/src/router/segment-resolution/fresh.ts +8 -0
  66. package/src/router/segment-resolution/revalidation.ts +128 -100
  67. package/src/router/trie-matching.ts +18 -13
  68. package/src/router/url-params.ts +49 -0
  69. package/src/router.ts +1 -2
  70. package/src/rsc/handler.ts +8 -4
  71. package/src/rsc/helpers.ts +69 -41
  72. package/src/rsc/progressive-enhancement.ts +4 -0
  73. package/src/rsc/response-route-handler.ts +14 -1
  74. package/src/rsc/rsc-rendering.ts +10 -0
  75. package/src/rsc/server-action.ts +4 -0
  76. package/src/rsc/types.ts +6 -0
  77. package/src/segment-content-promise.ts +67 -0
  78. package/src/segment-loader-promise.ts +122 -0
  79. package/src/segment-system.tsx +11 -61
  80. package/src/server/context.ts +26 -3
  81. package/src/server/request-context.ts +10 -42
  82. package/src/ssr/index.tsx +5 -1
  83. package/src/types/handler-context.ts +12 -39
  84. package/src/types/loader-types.ts +5 -6
  85. package/src/types/request-scope.ts +126 -0
  86. package/src/types/route-entry.ts +11 -0
  87. package/src/types/segments.ts +17 -1
  88. package/src/urls/include-helper.ts +24 -14
  89. package/src/urls/path-helper-types.ts +30 -4
  90. package/src/urls/response-types.ts +2 -10
  91. package/src/vite/debug.ts +184 -0
  92. package/src/vite/discovery/discover-routers.ts +31 -3
  93. package/src/vite/discovery/gate-state.ts +171 -0
  94. package/src/vite/discovery/prerender-collection.ts +48 -1
  95. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  96. package/src/vite/plugins/cjs-to-esm.ts +5 -0
  97. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  98. package/src/vite/plugins/client-ref-hashing.ts +16 -4
  99. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  100. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  101. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  102. package/src/vite/plugins/expose-action-id.ts +52 -28
  103. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  104. package/src/vite/plugins/expose-internal-ids.ts +516 -486
  105. package/src/vite/plugins/performance-tracks.ts +17 -9
  106. package/src/vite/plugins/use-cache-transform.ts +56 -43
  107. package/src/vite/plugins/version-injector.ts +37 -11
  108. package/src/vite/rango.ts +49 -14
  109. package/src/vite/router-discovery.ts +558 -53
  110. package/src/vite/utils/banner.ts +1 -1
  111. package/src/vite/utils/package-resolution.ts +41 -1
  112. package/src/vite/utils/prerender-utils.ts +20 -6
@@ -14,6 +14,9 @@
14
14
 
15
15
  import type { Plugin } from "vite";
16
16
  import { readFile } from "node:fs/promises";
17
+ import { createRangoDebugger, createCounter, NS } from "../debug.js";
18
+
19
+ const debug = createRangoDebugger(NS.transform);
17
20
 
18
21
  const RSDW_PATCH_RE =
19
22
  /((?:var|let|const)\s+\w+\s*=\s*root\._children\s*,\s*(\w+)\s*=\s*root\._debugInfo\s*[;,])/;
@@ -69,20 +72,25 @@ export function performanceTracksOptimizeDepsPlugin(): {
69
72
  }
70
73
 
71
74
  export function performanceTracksPlugin(): Plugin {
75
+ const counter = createCounter(debug, "performance-tracks");
72
76
  return {
73
77
  name: "@rangojs/router:performance-tracks",
74
78
 
79
+ buildEnd() {
80
+ counter?.flush();
81
+ },
82
+
75
83
  transform(code, id) {
76
84
  if (!id.includes("react-server-dom") || !id.includes("client")) return;
77
- const patched = patchRsdwClientDebugInfoRecovery(code);
78
- if (!patched.debugInfoVar) return;
79
- if (process.env.INTERNAL_RANGO_DEBUG)
80
- console.log(
81
- "[perf-tracks] patched RSDW client (var:",
82
- patched.debugInfoVar,
83
- ")",
84
- );
85
- return patched.code;
85
+ const start = counter ? performance.now() : 0;
86
+ try {
87
+ const patched = patchRsdwClientDebugInfoRecovery(code);
88
+ if (!patched.debugInfoVar) return;
89
+ debug?.("patched RSDW client (var: %s)", patched.debugInfoVar);
90
+ return patched.code;
91
+ } finally {
92
+ counter?.record(id, performance.now() - start);
93
+ }
86
94
  },
87
95
  };
88
96
  }
@@ -20,6 +20,9 @@ import type { Plugin } from "vite";
20
20
  import path from "node:path";
21
21
  import MagicString from "magic-string";
22
22
  import { normalizePath, hashId } from "./expose-id-utils.js";
23
+ import { createRangoDebugger, createCounter, NS } from "../debug.js";
24
+
25
+ const debug = createRangoDebugger(NS.transform);
23
26
 
24
27
  const CACHE_RUNTIME_IMPORT = "@rangojs/router/cache-runtime";
25
28
 
@@ -32,6 +35,7 @@ export function useCacheTransform(): Plugin {
32
35
  let isBuild = false;
33
36
  let rscTransforms: typeof import("@vitejs/plugin-rsc/transforms") | null =
34
37
  null;
38
+ const counter = createCounter(debug, "use-cache");
35
39
 
36
40
  return {
37
41
  name: "@rangojs/router:use-cache",
@@ -42,6 +46,10 @@ export function useCacheTransform(): Plugin {
42
46
  isBuild = config.command === "build";
43
47
  },
44
48
 
49
+ buildEnd() {
50
+ counter?.flush();
51
+ },
52
+
45
53
  async transform(code, id) {
46
54
  // Only process in RSC environment
47
55
  if (this.environment?.name !== "rsc") return;
@@ -55,63 +63,68 @@ export function useCacheTransform(): Plugin {
55
63
  // Only JS/TS files
56
64
  if (!/\.(tsx?|jsx?|mjs)$/.test(id)) return;
57
65
 
58
- // Lazy-load transform helpers
59
- if (!rscTransforms) {
66
+ const start = counter ? performance.now() : 0;
67
+ try {
68
+ // Lazy-load transform helpers
69
+ if (!rscTransforms) {
70
+ try {
71
+ rscTransforms = await import("@vitejs/plugin-rsc/transforms");
72
+ } catch {
73
+ return;
74
+ }
75
+ }
76
+
77
+ const {
78
+ hasDirective,
79
+ transformWrapExport,
80
+ transformHoistInlineDirective,
81
+ } = rscTransforms;
82
+
83
+ // Parse AST
84
+ let ast: any;
60
85
  try {
61
- rscTransforms = await import("@vitejs/plugin-rsc/transforms");
86
+ const { parseAst } = await import("vite");
87
+ ast = parseAst(code);
62
88
  } catch {
63
89
  return;
64
90
  }
65
- }
66
-
67
- const {
68
- hasDirective,
69
- transformWrapExport,
70
- transformHoistInlineDirective,
71
- } = rscTransforms;
72
91
 
73
- // Parse AST
74
- let ast: any;
75
- try {
76
- const { parseAst } = await import("vite");
77
- ast = parseAst(code);
78
- } catch {
79
- return;
80
- }
81
-
82
- const filePath = normalizePath(path.relative(projectRoot, id));
83
- const isLayoutOrTemplate = LAYOUT_TEMPLATE_PATTERN.test(id);
92
+ const filePath = normalizePath(path.relative(projectRoot, id));
93
+ const isLayoutOrTemplate = LAYOUT_TEMPLATE_PATTERN.test(id);
94
+
95
+ // Check for file-level "use cache"
96
+ if (hasDirective(ast.body, "use cache")) {
97
+ return transformFileLevelUseCache(
98
+ code,
99
+ ast,
100
+ filePath,
101
+ id,
102
+ isBuild,
103
+ isLayoutOrTemplate,
104
+ transformWrapExport,
105
+ );
106
+ }
84
107
 
85
- // Check for file-level "use cache"
86
- if (hasDirective(ast.body, "use cache")) {
87
- return transformFileLevelUseCache(
108
+ // Check for function-level "use cache" / "use cache: profileName"
109
+ // (only if there's no file-level directive but code still contains the string)
110
+ const functionResult = transformFunctionLevelUseCache(
88
111
  code,
89
112
  ast,
90
113
  filePath,
91
114
  id,
92
115
  isBuild,
93
- isLayoutOrTemplate,
94
- transformWrapExport,
116
+ transformHoistInlineDirective,
95
117
  );
96
- }
97
-
98
- // Check for function-level "use cache" / "use cache: profileName"
99
- // (only if there's no file-level directive but code still contains the string)
100
- const functionResult = transformFunctionLevelUseCache(
101
- code,
102
- ast,
103
- filePath,
104
- id,
105
- isBuild,
106
- transformHoistInlineDirective,
107
- );
108
118
 
109
- // Always check for near-miss directives, even when valid directives
110
- // exist. A file may contain both valid and invalid "use cache" directives
111
- // in different functions — the invalid ones should still warn.
112
- warnOnNearMissDirectives(ast, id, this.warn.bind(this));
119
+ // Always check for near-miss directives, even when valid directives
120
+ // exist. A file may contain both valid and invalid "use cache" directives
121
+ // in different functions — the invalid ones should still warn.
122
+ warnOnNearMissDirectives(ast, id, this.warn.bind(this));
113
123
 
114
- if (functionResult) return functionResult;
124
+ if (functionResult) return functionResult;
125
+ } finally {
126
+ counter?.record(id, performance.now() - start);
127
+ }
115
128
  },
116
129
  };
117
130
  }
@@ -47,16 +47,26 @@ export function createVersionInjectorPlugin(
47
47
  return null;
48
48
  }
49
49
 
50
- // Prepend imports at the top of the file. ES imports are hoisted
51
- // by the module system, so source position is irrelevant.
52
- const prepend: string[] = [];
53
- let newCode = code;
54
-
55
- if (!code.includes("virtual:rsc-router/routes-manifest")) {
56
- prepend.push(`import "virtual:rsc-router/routes-manifest";`);
57
- }
50
+ // Always prepend `import "virtual:rsc-router/routes-manifest"` as the
51
+ // first side-effect import. The manifest virtual module's `load()` hook
52
+ // awaits `s.discoveryDone` so that, by the time the rest of the entry
53
+ // including any module-level `router.reverse()` calls under `./router.js`
54
+ // evaluates, runtime discovery has rewritten `router.named-routes.gen.ts`
55
+ // with the full route table.
56
+ //
57
+ // ES module evaluation order matters here: while imports are *parsed*
58
+ // hoisted, side-effect imports are evaluated in source order in the
59
+ // dependency graph. A user-authored `import "virtual:rsc-router/..."`
60
+ // placed after `import "./router.js"` runs too late: the manifest
61
+ // gate fires after router.tsx has already crashed on a stale gen file.
62
+ // We always prepend; ESM dedups any user-written duplicate, so module
63
+ // initialization still runs once.
64
+ const prepend: string[] = [
65
+ `import "virtual:rsc-router/routes-manifest";`,
66
+ ];
58
67
 
59
68
  // Auto-inject VERSION if file uses createRSCHandler without version
69
+ let newCode = code;
60
70
  const needsVersion =
61
71
  code.includes("createRSCHandler") &&
62
72
  !code.includes("@rangojs/router:version") &&
@@ -70,9 +80,25 @@ export function createVersionInjectorPlugin(
70
80
  );
71
81
  }
72
82
 
73
- if (prepend.length === 0 && newCode === code) return null;
74
-
75
- newCode = prepend.join("\n") + (prepend.length > 0 ? "\n" : "") + newCode;
83
+ // Insert after any leading `/// <reference ... />` triple-slash
84
+ // directives (and surrounding blank lines). TypeScript requires those
85
+ // directives to precede all other code; putting our imports above
86
+ // them silently demotes the directives to plain comments.
87
+ const lines = newCode.split("\n");
88
+ let insertAt = 0;
89
+ while (insertAt < lines.length) {
90
+ const trimmed = lines[insertAt]!.trim();
91
+ if (trimmed === "" || /^\/\/\/\s*<reference\b/.test(trimmed)) {
92
+ insertAt++;
93
+ } else {
94
+ break;
95
+ }
96
+ }
97
+ newCode = [
98
+ ...lines.slice(0, insertAt),
99
+ ...prepend,
100
+ ...lines.slice(insertAt),
101
+ ].join("\n");
76
102
 
77
103
  return {
78
104
  code: newCode,
package/src/vite/rango.ts CHANGED
@@ -12,6 +12,8 @@ import { VIRTUAL_IDS } from "./plugins/virtual-entries.js";
12
12
  import {
13
13
  getExcludeDeps,
14
14
  getPackageAliases,
15
+ getPublishedPackageName,
16
+ getVendorAliases,
15
17
  } from "./utils/package-resolution.js";
16
18
  import { findRouterFiles } from "../build/generate-route-types.js";
17
19
  import { createVersionPlugin } from "./plugins/version-plugin.js";
@@ -27,6 +29,9 @@ import { createVersionInjectorPlugin } from "./plugins/version-injector.js";
27
29
  import { createCjsToEsmPlugin } from "./plugins/cjs-to-esm.js";
28
30
  import { createRouterDiscoveryPlugin } from "./router-discovery.js";
29
31
  import { performanceTracksPlugin } from "./plugins/performance-tracks.js";
32
+ import { createRangoDebugger, NS } from "./debug.js";
33
+
34
+ const debugConfig = createRangoDebugger(NS.config);
30
35
 
31
36
  /**
32
37
  * Vite plugin for @rangojs/router.
@@ -53,25 +58,41 @@ import { performanceTracksPlugin } from "./plugins/performance-tracks.js";
53
58
  * ```
54
59
  */
55
60
  export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
61
+ const rangoStart = performance.now();
56
62
  const resolvedOptions: RangoOptions = options ?? { preset: "node" };
57
63
  const preset = resolvedOptions.preset ?? "node";
58
64
  const showBanner = resolvedOptions.banner ?? true;
65
+ debugConfig?.("rango(%s) setup start", preset);
59
66
 
60
67
  const plugins: PluginOption[] = [];
61
68
 
62
- // Get package resolution info (workspace vs npm install)
63
- const rangoAliases = getPackageAliases();
69
+ // Get package resolution info (workspace vs npm install).
70
+ // Vendor aliases redirect the bare plugin-rsc vendor specs (which plugin-rsc
71
+ // itself injects into optimizeDeps.include) to absolute paths resolved from
72
+ // this package — so strict-pnpm consumers don't hit "Failed to resolve
73
+ // dependency" warnings when those deps aren't hoisted to their app root.
74
+ const rangoAliases = { ...getPackageAliases(), ...getVendorAliases() };
64
75
  const excludeDeps = [
65
76
  ...getExcludeDeps(),
66
- // The public browser entry re-exports the RSDW browser client.
67
- // Excluding both keeps Vite from freezing the unpatched bundle into
68
- // .vite/deps before our source transforms run.
77
+ // plugin-rsc itself injects these into the client env's
78
+ // optimizeDeps.include, which overrides exclude for the dep's own
79
+ // pre-bundle entry. What exclude still controls is how *other*
80
+ // pre-bundled deps treat imports of these specs (external vs inlined)
81
+ // via esbuildCjsExternalPlugin. The cjs-to-esm transform in
82
+ // plugins/cjs-to-esm.ts is the fallback for strict-pnpm consumers,
83
+ // where client.browser's bare include fails to resolve and Vite ends up
84
+ // serving the raw CJS file at dev-serve time.
69
85
  "@vitejs/plugin-rsc/browser",
70
- // Keep the browser RSDW client out of Vite's dep optimizer so our
71
- // cjs-to-esm transform can patch the real file.
72
86
  "@vitejs/plugin-rsc/vendor/react-server-dom/client.browser",
73
87
  ];
74
88
 
89
+ // Vite supports a nested `A > B` syntax in optimizeDeps.include that resolves
90
+ // B from A's location. We anchor transitive deps (rsc-html-stream,
91
+ // @vitejs/plugin-rsc/vendor/*) to @rangojs/router so pnpm consumers — where
92
+ // these aren't visible at the app root — can still pre-bundle them.
93
+ const pkg = getPublishedPackageName();
94
+ const nested = (spec: string) => `${pkg} > ${spec}`;
95
+
75
96
  // Mutable ref for router path (node preset only).
76
97
  // Set immediately when user-specified, or populated by the auto-discover
77
98
  // config() hook using Vite's resolved root.
@@ -126,7 +147,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
126
147
  // Pre-bundle rsc-html-stream to prevent discovery during first request
127
148
  // Exclude rsc-router modules to ensure same Context instance
128
149
  optimizeDeps: {
129
- include: ["rsc-html-stream/client"],
150
+ include: [nested("rsc-html-stream/client")],
130
151
  exclude: excludeDeps,
131
152
  esbuildOptions: sharedEsbuildOptions,
132
153
  },
@@ -151,8 +172,10 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
151
172
  "react-dom/static.edge",
152
173
  "react/jsx-runtime",
153
174
  "react/jsx-dev-runtime",
154
- "rsc-html-stream/server",
155
- "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
175
+ nested("rsc-html-stream/server"),
176
+ nested(
177
+ "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
178
+ ),
156
179
  ],
157
180
  exclude: excludeDeps,
158
181
  esbuildOptions: sharedEsbuildOptions,
@@ -167,7 +190,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
167
190
  "react",
168
191
  "react/jsx-runtime",
169
192
  "react/jsx-dev-runtime",
170
- "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge",
193
+ nested(
194
+ "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge",
195
+ ),
171
196
  ],
172
197
  exclude: excludeDeps,
173
198
  esbuildOptions: sharedEsbuildOptions,
@@ -280,7 +305,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
280
305
  "react-dom",
281
306
  "react/jsx-runtime",
282
307
  "react/jsx-dev-runtime",
283
- "rsc-html-stream/client",
308
+ nested("rsc-html-stream/client"),
284
309
  ],
285
310
  exclude: excludeDeps,
286
311
  esbuildOptions: sharedEsbuildOptions,
@@ -297,7 +322,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
297
322
  "react-dom/static.edge",
298
323
  "react/jsx-runtime",
299
324
  "react/jsx-dev-runtime",
300
- "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
325
+ nested(
326
+ "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
327
+ ),
301
328
  ],
302
329
  exclude: excludeDeps,
303
330
  esbuildOptions: sharedEsbuildOptions,
@@ -310,7 +337,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
310
337
  "react",
311
338
  "react/jsx-runtime",
312
339
  "react/jsx-dev-runtime",
313
- "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge",
340
+ nested(
341
+ "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge",
342
+ ),
314
343
  ],
315
344
  esbuildOptions: sharedEsbuildOptions,
316
345
  },
@@ -458,5 +487,11 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
458
487
  }),
459
488
  );
460
489
 
490
+ debugConfig?.(
491
+ "rango(%s) setup done: %d plugin(s) (%sms)",
492
+ preset,
493
+ plugins.length,
494
+ (performance.now() - rangoStart).toFixed(1),
495
+ );
461
496
  return plugins;
462
497
  }