@rangojs/router 0.0.0-experimental.133 → 0.0.0-experimental.135
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 +7 -2
- package/dist/vite/index.js +41 -27
- package/package.json +23 -24
- package/skills/composability/SKILL.md +0 -1
- package/skills/handler-use/SKILL.md +7 -7
- package/skills/intercept/SKILL.md +38 -13
- package/skills/loader/SKILL.md +10 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/migrate-react-router/SKILL.md +144 -1
- package/skills/prerender/SKILL.md +20 -17
- package/skills/router-setup/SKILL.md +1 -2
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/render-handler.md +15 -14
- package/skills/use-cache/SKILL.md +11 -0
- package/skills/view-transitions/SKILL.md +43 -0
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +27 -1
- package/src/browser/navigation-store.ts +82 -8
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +43 -3
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +32 -4
- package/src/browser/react/NavigationProvider.tsx +195 -4
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/types.ts +32 -1
- package/src/cache/cache-runtime.ts +26 -5
- package/src/cache/document-cache.ts +17 -1
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/handles/MetaTags.tsx +6 -0
- package/src/index.rsc.ts +6 -1
- package/src/index.ts +6 -4
- package/src/internal-debug.ts +11 -8
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +38 -39
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-types.ts +0 -5
- package/src/router/match-api.ts +5 -1
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/segment-resolution/fresh.ts +4 -0
- package/src/router/segment-resolution/loader-cache.ts +14 -2
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/rsc/progressive-enhancement.ts +56 -2
- package/src/rsc/rsc-rendering.ts +7 -2
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/segment-system.tsx +59 -8
- package/src/server/context.ts +13 -0
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +52 -3
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/segments.ts +100 -0
- package/src/urls/path-helper-types.ts +10 -7
- package/src/urls/urls-function.ts +0 -1
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +28 -9
- package/src/vite/router-discovery.ts +8 -13
- package/src/network-error-thrower.tsx +0 -18
package/dist/bin/rango.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __esm = (fn, res) => function __init() {
|
|
5
|
-
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
6
11
|
};
|
|
7
12
|
var __export = (target, all) => {
|
|
8
13
|
for (var name in all)
|
package/dist/vite/index.js
CHANGED
|
@@ -2224,7 +2224,14 @@ export const renderHTML = createSSRHandler({
|
|
|
2224
2224
|
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
2225
2225
|
});
|
|
2226
2226
|
`.trim();
|
|
2227
|
+
var RSC_ENTRY_BOOTSTRAP_IMPORTS = [
|
|
2228
|
+
"virtual:rsc-router/routes-manifest",
|
|
2229
|
+
"virtual:rsc-router/loader-manifest"
|
|
2230
|
+
];
|
|
2227
2231
|
function getVirtualEntryRSC(routerPath) {
|
|
2232
|
+
const bootstrapImports = RSC_ENTRY_BOOTSTRAP_IMPORTS.map(
|
|
2233
|
+
(id) => `import "${id}";`
|
|
2234
|
+
).join("\n");
|
|
2228
2235
|
return `
|
|
2229
2236
|
import {
|
|
2230
2237
|
renderToReadableStream,
|
|
@@ -2238,15 +2245,9 @@ import { router } from "${routerPath}";
|
|
|
2238
2245
|
import { createRSCHandler } from "@rangojs/router/internal/rsc-handler";
|
|
2239
2246
|
import { VERSION } from "@rangojs/router:version";
|
|
2240
2247
|
|
|
2241
|
-
//
|
|
2242
|
-
//
|
|
2243
|
-
|
|
2244
|
-
import "virtual:rsc-router/loader-manifest";
|
|
2245
|
-
|
|
2246
|
-
// Import pre-generated route manifest so href() works immediately on cold start.
|
|
2247
|
-
// In build mode, this contains the full route map generated at build time.
|
|
2248
|
-
// In dev mode, this is a no-op (manifest is populated in-memory by the discovery plugin).
|
|
2249
|
-
import "virtual:rsc-router/routes-manifest";
|
|
2248
|
+
// Startup bootstrap imports (routes + loader manifests). See
|
|
2249
|
+
// RSC_ENTRY_BOOTSTRAP_IMPORTS \u2014 the same list the custom-entry injector uses.
|
|
2250
|
+
${bootstrapImports}
|
|
2250
2251
|
|
|
2251
2252
|
// Lazily create the handler on first request so that ESM live bindings
|
|
2252
2253
|
// have resolved by the time we read \`router\`. During HMR the module may
|
|
@@ -2296,7 +2297,7 @@ import { resolve } from "node:path";
|
|
|
2296
2297
|
// package.json
|
|
2297
2298
|
var package_default = {
|
|
2298
2299
|
name: "@rangojs/router",
|
|
2299
|
-
version: "0.0.0-experimental.
|
|
2300
|
+
version: "0.0.0-experimental.135",
|
|
2300
2301
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2301
2302
|
keywords: [
|
|
2302
2303
|
"react",
|
|
@@ -2471,7 +2472,7 @@ var package_default = {
|
|
|
2471
2472
|
},
|
|
2472
2473
|
dependencies: {
|
|
2473
2474
|
"@types/debug": "^4.1.12",
|
|
2474
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
2475
|
+
"@vitejs/plugin-rsc": "^0.5.27",
|
|
2475
2476
|
debug: "^4.4.1",
|
|
2476
2477
|
"magic-string": "^0.30.17",
|
|
2477
2478
|
picomatch: "^4.0.4",
|
|
@@ -2486,22 +2487,22 @@ var package_default = {
|
|
|
2486
2487
|
"@types/node": "^24.10.1",
|
|
2487
2488
|
"@types/react": "catalog:",
|
|
2488
2489
|
"@types/react-dom": "catalog:",
|
|
2489
|
-
esbuild: "^0.
|
|
2490
|
+
esbuild: "^0.28.1",
|
|
2490
2491
|
"happy-dom": "^20.10.1",
|
|
2491
2492
|
jiti: "^2.6.1",
|
|
2492
2493
|
react: "catalog:",
|
|
2493
2494
|
"react-dom": "catalog:",
|
|
2494
2495
|
typescript: "^5.3.0",
|
|
2495
|
-
vitest: "^4.
|
|
2496
|
+
vitest: "^4.1.9"
|
|
2496
2497
|
},
|
|
2497
2498
|
peerDependencies: {
|
|
2498
|
-
"@cloudflare/vite-plugin": "^1.
|
|
2499
|
+
"@cloudflare/vite-plugin": "^1.42.1",
|
|
2499
2500
|
"@playwright/test": "^1.49.1",
|
|
2500
2501
|
"@testing-library/react": ">=16",
|
|
2501
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
2502
|
+
"@vitejs/plugin-rsc": "^0.5.27",
|
|
2502
2503
|
react: ">=19.2.6 <20",
|
|
2503
2504
|
"react-dom": ">=19.2.6 <20",
|
|
2504
|
-
vite: "^8.0.
|
|
2505
|
+
vite: "^8.0.16",
|
|
2505
2506
|
vitest: ">=3"
|
|
2506
2507
|
},
|
|
2507
2508
|
peerDependenciesMeta: {
|
|
@@ -3927,9 +3928,9 @@ function createVersionInjectorPlugin(rscEntryPath) {
|
|
|
3927
3928
|
if (normalizedId !== normalizedEntry) {
|
|
3928
3929
|
return null;
|
|
3929
3930
|
}
|
|
3930
|
-
const prepend =
|
|
3931
|
-
`import "
|
|
3932
|
-
|
|
3931
|
+
const prepend = RSC_ENTRY_BOOTSTRAP_IMPORTS.map(
|
|
3932
|
+
(id2) => `import "${id2}";`
|
|
3933
|
+
);
|
|
3933
3934
|
let newCode = code;
|
|
3934
3935
|
const needsVersion = code.includes("createRSCHandler") && !code.includes("@rangojs/router:version") && /createRSCHandler\s*\(\s*\{/.test(code);
|
|
3935
3936
|
if (needsVersion) {
|
|
@@ -4031,6 +4032,19 @@ import { readFileSync as readFileSync6 } from "node:fs";
|
|
|
4031
4032
|
import { createRequire as createRequire2, register } from "node:module";
|
|
4032
4033
|
import { pathToFileURL } from "node:url";
|
|
4033
4034
|
|
|
4035
|
+
// src/vite/inject-client-debug.ts
|
|
4036
|
+
function injectClientDebugFlag(id) {
|
|
4037
|
+
if (!id.includes("internal-debug")) return null;
|
|
4038
|
+
const norm = id.replace(/\\/g, "/");
|
|
4039
|
+
const isInternalDebug = /\/internal-debug\.[cm]?[jt]sx?(\?|$)/.test(norm) && (norm.includes("/@rangojs/router/") || norm.includes("/packages/rangojs-router/"));
|
|
4040
|
+
if (!isInternalDebug) return null;
|
|
4041
|
+
return {
|
|
4042
|
+
code: `export const INTERNAL_RANGO_DEBUG = ${!!process.env.INTERNAL_RANGO_DEBUG};
|
|
4043
|
+
`,
|
|
4044
|
+
map: null
|
|
4045
|
+
};
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4034
4048
|
// src/vite/plugins/virtual-stub-plugin.ts
|
|
4035
4049
|
function createVirtualStubPlugin() {
|
|
4036
4050
|
const STUB_PREFIXES = [
|
|
@@ -4318,7 +4332,7 @@ function checkSelfGenWrite(state, filePath, consume) {
|
|
|
4318
4332
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4319
4333
|
|
|
4320
4334
|
// src/internal-debug.ts
|
|
4321
|
-
var INTERNAL_RANGO_DEBUG = typeof
|
|
4335
|
+
var INTERNAL_RANGO_DEBUG = typeof process !== "undefined" && Boolean(process.env?.INTERNAL_RANGO_DEBUG);
|
|
4322
4336
|
|
|
4323
4337
|
// src/router/logging.ts
|
|
4324
4338
|
var routerLogContext = new AsyncLocalStorage();
|
|
@@ -6102,13 +6116,13 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
6102
6116
|
let viteMode = "production";
|
|
6103
6117
|
return {
|
|
6104
6118
|
name: "@rangojs/router:discovery",
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
return
|
|
6119
|
+
// Make INTERNAL_RANGO_DEBUG reach the CLIENT debug logs by just setting the
|
|
6120
|
+
// env var. See injectClientDebugFlag: bakes the resolved flag into the
|
|
6121
|
+
// internal-debug module so FE debug no longer depends on Vite delivering the
|
|
6122
|
+
// `__RANGO_DEBUG__` define to the client (which it does only as an injected
|
|
6123
|
+
// global whose presence varies across consumer setups). Runs in dev and build.
|
|
6124
|
+
transform(_code, id) {
|
|
6125
|
+
return injectClientDebugFlag(id);
|
|
6112
6126
|
},
|
|
6113
6127
|
configResolved(config) {
|
|
6114
6128
|
s.projectRoot = config.root;
|
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.135",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -162,20 +162,9 @@
|
|
|
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
|
-
},
|
|
176
165
|
"dependencies": {
|
|
177
166
|
"@types/debug": "^4.1.12",
|
|
178
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
167
|
+
"@vitejs/plugin-rsc": "^0.5.27",
|
|
179
168
|
"debug": "^4.4.1",
|
|
180
169
|
"magic-string": "^0.30.17",
|
|
181
170
|
"picomatch": "^4.0.4",
|
|
@@ -184,28 +173,28 @@
|
|
|
184
173
|
},
|
|
185
174
|
"devDependencies": {
|
|
186
175
|
"@playwright/test": "^1.49.1",
|
|
187
|
-
"@shared/e2e": "workspace:*",
|
|
188
176
|
"@testing-library/dom": "^10.4.1",
|
|
189
177
|
"@testing-library/react": "^16.3.2",
|
|
190
178
|
"@types/node": "^24.10.1",
|
|
191
|
-
"@types/react": "
|
|
192
|
-
"@types/react-dom": "
|
|
193
|
-
"esbuild": "^0.
|
|
179
|
+
"@types/react": "^19.2.7",
|
|
180
|
+
"@types/react-dom": "^19.2.3",
|
|
181
|
+
"esbuild": "^0.28.1",
|
|
194
182
|
"happy-dom": "^20.10.1",
|
|
195
183
|
"jiti": "^2.6.1",
|
|
196
|
-
"react": "
|
|
197
|
-
"react-dom": "
|
|
184
|
+
"react": "^19.2.6",
|
|
185
|
+
"react-dom": "^19.2.6",
|
|
198
186
|
"typescript": "^5.3.0",
|
|
199
|
-
"vitest": "^4.
|
|
187
|
+
"vitest": "^4.1.9",
|
|
188
|
+
"@shared/e2e": "0.0.1"
|
|
200
189
|
},
|
|
201
190
|
"peerDependencies": {
|
|
202
|
-
"@cloudflare/vite-plugin": "^1.
|
|
191
|
+
"@cloudflare/vite-plugin": "^1.42.1",
|
|
203
192
|
"@playwright/test": "^1.49.1",
|
|
204
193
|
"@testing-library/react": ">=16",
|
|
205
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
194
|
+
"@vitejs/plugin-rsc": "^0.5.27",
|
|
206
195
|
"react": ">=19.2.6 <20",
|
|
207
196
|
"react-dom": ">=19.2.6 <20",
|
|
208
|
-
"vite": "^8.0.
|
|
197
|
+
"vite": "^8.0.16",
|
|
209
198
|
"vitest": ">=3"
|
|
210
199
|
},
|
|
211
200
|
"peerDependenciesMeta": {
|
|
@@ -227,5 +216,15 @@
|
|
|
227
216
|
},
|
|
228
217
|
"engines": {
|
|
229
218
|
"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"
|
|
230
229
|
}
|
|
231
|
-
}
|
|
230
|
+
}
|
|
@@ -51,13 +51,13 @@ Now `ProductPage` carries its loader, loading state, and response-header middlew
|
|
|
51
51
|
|
|
52
52
|
`handler.use()` is the same callback shape regardless of where the handler runs, but the runtime validates that the items it returns are valid for the mount site. Driven by `MOUNT_SITE_ALLOWED_TYPES` in [resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts):
|
|
53
53
|
|
|
54
|
-
| Mount site | Allowed item types
|
|
55
|
-
| ------------------------------------------------- |
|
|
56
|
-
| `path()` / `route()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition`
|
|
57
|
-
| `layout()` | All of the above, plus `route`, `include`
|
|
58
|
-
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition`
|
|
59
|
-
| `intercept()` | `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `layout`, `route`, `when
|
|
60
|
-
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache`
|
|
54
|
+
| Mount site | Allowed item types |
|
|
55
|
+
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| `path()` / `route()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
|
|
57
|
+
| `layout()` | All of the above, plus `route`, `include` |
|
|
58
|
+
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
|
|
59
|
+
| `intercept()` | `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `layout`, `route`, `transition` (conditional activation is config-only: `intercept(..., { when })`) |
|
|
60
|
+
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
|
|
61
61
|
|
|
62
62
|
For per-item semantics see the dedicated skills: [middleware](../middleware/SKILL.md), [loader](../loader/SKILL.md), [parallel](../parallel/SKILL.md), [intercept](../intercept/SKILL.md), [layout](../layout/SKILL.md), [view-transitions](../view-transitions/SKILL.md).
|
|
63
63
|
|
|
@@ -142,18 +142,41 @@ layout(ProductLayout, () => [
|
|
|
142
142
|
]);
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
## Conditional Intercept with when
|
|
145
|
+
## Conditional Intercept with the `when` config
|
|
146
146
|
|
|
147
|
-
Only intercept based on navigation context
|
|
147
|
+
Only intercept based on navigation context. `when` is the 4th argument
|
|
148
|
+
(an `InterceptConfig` object); the other use-items go in the 5th-argument
|
|
149
|
+
callback.
|
|
148
150
|
|
|
149
151
|
```typescript
|
|
150
152
|
intercept(
|
|
151
153
|
"@modal",
|
|
152
154
|
"product",
|
|
153
155
|
<ProductModal />,
|
|
156
|
+
// Only intercept when coming from a different section
|
|
157
|
+
{ when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
|
|
158
|
+
() => [
|
|
159
|
+
loader(ProductLoader),
|
|
160
|
+
]
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`when` is a match-time selector receiving `{ from, to, params, segments, ... }`.
|
|
165
|
+
Pass an array of predicates for AND logic (all must return true). Omit `when`
|
|
166
|
+
entirely and the intercept always activates.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
intercept(
|
|
170
|
+
"@modal",
|
|
171
|
+
"product",
|
|
172
|
+
<ProductModal />,
|
|
173
|
+
{
|
|
174
|
+
when: [
|
|
175
|
+
({ from }) => from.pathname.startsWith("/shop"),
|
|
176
|
+
({ params }) => params.slug !== "featured",
|
|
177
|
+
],
|
|
178
|
+
},
|
|
154
179
|
() => [
|
|
155
|
-
// Only intercept when coming from a different section
|
|
156
|
-
when(({ from }) => !from.pathname.startsWith("/shop/product/")),
|
|
157
180
|
loader(ProductLoader),
|
|
158
181
|
]
|
|
159
182
|
)
|
|
@@ -242,10 +265,13 @@ layout(ShopLayout, () => [
|
|
|
242
265
|
]),
|
|
243
266
|
|
|
244
267
|
// This intercept is also pre-rendered at build time
|
|
245
|
-
intercept(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
268
|
+
intercept(
|
|
269
|
+
"@modal",
|
|
270
|
+
".detail",
|
|
271
|
+
<ProductModal />,
|
|
272
|
+
{ when: ({ from }) => from.pathname.startsWith("/shop") },
|
|
273
|
+
() => [loader(ProductLoader)],
|
|
274
|
+
),
|
|
249
275
|
])
|
|
250
276
|
```
|
|
251
277
|
|
|
@@ -253,8 +279,8 @@ Build-time behavior:
|
|
|
253
279
|
|
|
254
280
|
- The intercept handler (`<ProductModal />`) is resolved with BuildContext
|
|
255
281
|
- Result is stored under the key `"detail/paramHash/i"` (intercept variant)
|
|
256
|
-
- `when
|
|
257
|
-
- `when
|
|
282
|
+
- `when` config conditions are skipped at build time (all intercepts pre-rendered unconditionally)
|
|
283
|
+
- `when` is still evaluated at runtime by the intercept-resolution middleware
|
|
258
284
|
|
|
259
285
|
Runtime behavior:
|
|
260
286
|
|
|
@@ -305,7 +331,6 @@ export const shopPatterns = urls(({
|
|
|
305
331
|
intercept,
|
|
306
332
|
loader,
|
|
307
333
|
loading,
|
|
308
|
-
when,
|
|
309
334
|
}) => [
|
|
310
335
|
layout(<ShopLayout />, () => [
|
|
311
336
|
parallel({
|
|
@@ -317,8 +342,8 @@ export const shopPatterns = urls(({
|
|
|
317
342
|
"@modal",
|
|
318
343
|
"product", // Route name (without prefix)
|
|
319
344
|
<ProductModalContent />,
|
|
345
|
+
{ when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
|
|
320
346
|
() => [
|
|
321
|
-
when(({ from }) => !from.pathname.startsWith("/shop/product/")),
|
|
322
347
|
layout(<ModalWrapper />),
|
|
323
348
|
loading(<ProductModalSkeleton />),
|
|
324
349
|
loader(ProductLoader, () => [cache()]),
|
|
@@ -338,7 +363,7 @@ export const shopPatterns = urls(({
|
|
|
338
363
|
|
|
339
364
|
## Handler-attached `.use`
|
|
340
365
|
|
|
341
|
-
Intercept handlers can carry their own middleware, loaders, loading state, error/notFound boundaries, and even nested `layout`/`route
|
|
366
|
+
Intercept handlers can carry their own middleware, loaders, loading state, error/notFound boundaries, and even nested `layout`/`route` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome. (Conditional activation is set via the `when` config on the mount-site `intercept()` call, not inside `.use`.)
|
|
342
367
|
|
|
343
368
|
```typescript
|
|
344
369
|
const QuickViewModal: Handler = async (ctx) => {
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -671,6 +671,16 @@ export const SearchLoader = createLoader(async (ctx) => {
|
|
|
671
671
|
}, true); // true = fetchable
|
|
672
672
|
```
|
|
673
673
|
|
|
674
|
+
> **No registration needed — and no worker-entry import.** A fetchable loader
|
|
675
|
+
> does not have to be registered with `loader()` in the route DSL, and it does
|
|
676
|
+
> not have to be imported by any server module. Importing it into the client
|
|
677
|
+
> component that calls `useFetchLoader()` / `load()` is enough. Rango discovers
|
|
678
|
+
> every `createLoader(fn, true)` at build time and registers it for the
|
|
679
|
+
> `_rsc_loader` endpoint, so a loader reachable only through a client component
|
|
680
|
+
> still resolves in production — on both the generated entry and a hand-written
|
|
681
|
+
> worker entry (e.g. a Cloudflare `worker.rsc.tsx`). You do **not** need to
|
|
682
|
+
> force-import the loader in your worker entry to make it resolve.
|
|
683
|
+
|
|
674
684
|
### Fetchable Loader with Middleware
|
|
675
685
|
|
|
676
686
|
Pass an options object instead of `true` to attach per-loader middleware.
|
|
@@ -193,9 +193,9 @@ The main content always goes through `<Outlet />` via the `path()` handler.
|
|
|
193
193
|
// Rango: explicit intercept in layout
|
|
194
194
|
layout(<ShopLayout />, () => [
|
|
195
195
|
path("/product/:id", ProductPage, { name: "product" }),
|
|
196
|
-
intercept("@modal", ".product", <ProductModal />,
|
|
197
|
-
when(
|
|
198
|
-
|
|
196
|
+
intercept("@modal", ".product", <ProductModal />, {
|
|
197
|
+
when: ({ from }) => from.pathname.startsWith("/shop"),
|
|
198
|
+
}),
|
|
199
199
|
])
|
|
200
200
|
```
|
|
201
201
|
|
|
@@ -740,7 +740,135 @@ function ThemeToggle() {
|
|
|
740
740
|
|
|
741
741
|
See `/theme` for full API including system detection and cookie persistence.
|
|
742
742
|
|
|
743
|
-
## 10.
|
|
743
|
+
## 10. Cloudflare Workers: streaming, dev tooling, and deploy
|
|
744
|
+
|
|
745
|
+
This is the part of an RR7-on-Cloudflare migration that the API tables above do
|
|
746
|
+
**not** cover, and it produces symptoms that look like router bugs but aren't.
|
|
747
|
+
RR7 serves a fully-rendered HTML/Turbo-Stream response; Rango serves a **streamed**
|
|
748
|
+
RSC/SSR response. Anything in the request path that was harmless for RR7 but
|
|
749
|
+
**buffers the response** silently kills that stream — the `loading()` /
|
|
750
|
+
`<Suspense>` fallback never shows and the page appears "awaited" (old content
|
|
751
|
+
held until everything resolves).
|
|
752
|
+
|
|
753
|
+
### 10a. Never buffer the Rango response in a custom worker entry
|
|
754
|
+
|
|
755
|
+
RR7 Cloudflare entries commonly **rewrite the response body** — `await
|
|
756
|
+
response.text()` / `await response.arrayBuffer()`, `HTMLRewriter`, cookie/URL
|
|
757
|
+
rewriting, hybrid-proxy bridging. That is fine for RR7's complete HTML, but if it
|
|
758
|
+
runs on the **Rango** path it consumes the stream and you lose streaming
|
|
759
|
+
entirely.
|
|
760
|
+
|
|
761
|
+
Rule: in the worker entry, the Rango response must pass through as a **stream**.
|
|
762
|
+
Buffer only on non-Rango branches (a legacy/SFRA proxy, an error page).
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
// BAD — buffers the whole RSC stream before returning (no streaming, no fallback)
|
|
766
|
+
const response = await router.fetch(request, { env, ctx });
|
|
767
|
+
const body = await response.text(); // <- drains the stream
|
|
768
|
+
return new Response(rewrite(body), response);
|
|
769
|
+
|
|
770
|
+
// GOOD — pass the body through as a stream (re-wrap headers only)
|
|
771
|
+
const response = await router.fetch(request, { env, ctx });
|
|
772
|
+
return new Response(response.body, response); // <- streams; safe to add headers
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
If you keep a legacy proxy (e.g. an SFRA/host fallback) that legitimately buffers
|
|
776
|
+
to rewrite HTML, gate it so it only runs for proxied paths — never for the
|
|
777
|
+
`router.fetch()` result.
|
|
778
|
+
|
|
779
|
+
Keep the custom entry itself (host/site resolution, sessions, proxy fallback) —
|
|
780
|
+
that's real product logic, not a deviation. Just don't let it touch the stream.
|
|
781
|
+
|
|
782
|
+
### 10b. Use `vite dev` / `vite preview`, not `wrangler dev`, for local work
|
|
783
|
+
|
|
784
|
+
The `@cloudflare/vite-plugin` runs **your worker entry** inside miniflare for both
|
|
785
|
+
`vite dev` and `vite preview`, with bindings + `.dev.vars`, and it **streams**.
|
|
786
|
+
`wrangler dev` does **not** stream RSC locally: it gzip-compresses the response by
|
|
787
|
+
buffering the entire body before sending. Measured on a bare streaming Worker
|
|
788
|
+
under `wrangler dev`: `Accept-Encoding: gzip` → first byte at the full delay
|
|
789
|
+
(buffered); `Accept-Encoding: identity` → first byte at ~3ms (streams). The
|
|
790
|
+
deployed Cloudflare edge does **streaming** compression, so this is a
|
|
791
|
+
**local-`wrangler dev`-only artifact** — do not mistake it for a production
|
|
792
|
+
streaming bug (verify the deployed edge with `curl` and watch chunk timing /
|
|
793
|
+
`transfer-encoding: chunked`).
|
|
794
|
+
|
|
795
|
+
```jsonc
|
|
796
|
+
// package.json — the standard rango-on-Workers shape
|
|
797
|
+
"dev": "vite", // streams, bindings, .dev.vars live
|
|
798
|
+
"build": "vite build",
|
|
799
|
+
"preview": "vite preview", // runs the built worker in workerd; also streams
|
|
800
|
+
// wrangler is only for: wrangler deploy / wrangler secret
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
If a workflow truly requires `wrangler dev`, declare `Content-Encoding: identity`
|
|
804
|
+
on streamed responses, gated to local hostnames (so the edge still compresses):
|
|
805
|
+
|
|
806
|
+
```typescript
|
|
807
|
+
if (
|
|
808
|
+
isLocalHostname(url.hostname) &&
|
|
809
|
+
response.body &&
|
|
810
|
+
!response.headers.has("content-encoding")
|
|
811
|
+
) {
|
|
812
|
+
const ct = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
813
|
+
if (ct.includes("text/html") || ct.includes("text/x-component")) {
|
|
814
|
+
const headers = new Headers(response.headers);
|
|
815
|
+
headers.set("content-encoding", "identity"); // wrangler skips gzip -> streams
|
|
816
|
+
return new Response(response.body, {
|
|
817
|
+
status: response.status,
|
|
818
|
+
statusText: response.statusText,
|
|
819
|
+
headers,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
### 10c. `.dev.vars` loads differently per tool
|
|
826
|
+
|
|
827
|
+
A frequent "my env vars vanished after migration" symptom — it's the tool, not
|
|
828
|
+
the file:
|
|
829
|
+
|
|
830
|
+
| Command | How `.dev.vars` is loaded |
|
|
831
|
+
| -------------- | ------------------------------------------------------------------------------------ |
|
|
832
|
+
| `wrangler dev` | Loaded natively at runtime (RR7's path — why it "worked before") |
|
|
833
|
+
| `vite dev` | Loaded **live** by the plugin (+ `.dev.vars.<CLOUDFLARE_ENV>` if a named env is set) |
|
|
834
|
+
| `vite preview` | Read from the **build output** `dist/<env>/.dev.vars`, written at `vite build` time |
|
|
835
|
+
|
|
836
|
+
So `vite preview` only sees vars that were present when you built — re-run
|
|
837
|
+
`vite build` after editing `.dev.vars`. (`vite dev` is the simplest: live vars +
|
|
838
|
+
streaming, no rebuild.)
|
|
839
|
+
|
|
840
|
+
### 10d. Build output moves from `build/` to `dist/`
|
|
841
|
+
|
|
842
|
+
RR7's `react-router build` writes to `build/`; Rango's `vite build` writes to
|
|
843
|
+
`dist/` (`dist/client`, `dist/rsc`, …). The leftover `build/` paths in
|
|
844
|
+
`wrangler.toml` and cleanup scripts must move, or you deploy stale/empty assets:
|
|
845
|
+
|
|
846
|
+
```toml
|
|
847
|
+
# wrangler.toml — was RR7's react-router build output
|
|
848
|
+
- assets = { directory = "./build/client/", binding = "ASSETS" }
|
|
849
|
+
+ assets = { directory = "./dist/client/", binding = "ASSETS" }
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
```jsonc
|
|
853
|
+
// package.json — clean the real output dir
|
|
854
|
+
- "cleanup": "rimraf ./build ./public/build"
|
|
855
|
+
+ "cleanup": "rimraf ./dist ./build ./public/build"
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
### 10e. Deploy the built worker, not the source entry
|
|
859
|
+
|
|
860
|
+
`vite build` emits the **deployable** Worker config at `dist/<env>/wrangler.json`
|
|
861
|
+
with `main: "index.js"` (the bundled worker), `no_bundle: true`, and
|
|
862
|
+
`assets: "../client"`. A plain `wrangler deploy` run from the repo root uses the
|
|
863
|
+
**root** `wrangler.toml`, where `main` points at your **source** entry
|
|
864
|
+
(`app/.../worker.ts`) — and wrangler's bundler cannot resolve Rango's
|
|
865
|
+
`virtual:rsc-router/*` modules, so deploying the source entry fails or ships a
|
|
866
|
+
broken worker. Deploy the build output instead (e.g.
|
|
867
|
+
`wrangler deploy -c dist/<env>/wrangler.json`) or use the plugin's deploy flow.
|
|
868
|
+
RR7's `wrangler deploy` from root worked only because its worker was already
|
|
869
|
+
fully bundled by `react-router build`.
|
|
870
|
+
|
|
871
|
+
## 11. Key Conceptual Differences
|
|
744
872
|
|
|
745
873
|
| Concept | React Router | Rango |
|
|
746
874
|
| ------------------- | ----------------------------------- | -------------------------------------- |
|
|
@@ -769,3 +897,18 @@ See `/theme` for full API including system detection and cookie persistence.
|
|
|
769
897
|
11. [ ] Update metadata to use `Meta` handle + `<MetaTags />`
|
|
770
898
|
12. [ ] Replace custom theme provider with `theme: true` in createRouter (see `/theme`)
|
|
771
899
|
13. [ ] Run `npx rango generate src/` to generate route types
|
|
900
|
+
|
|
901
|
+
**Cloudflare Workers (if migrating an RR7-on-Workers app):**
|
|
902
|
+
|
|
903
|
+
14. [ ] Audit the custom worker entry — the `router.fetch()` response must pass
|
|
904
|
+
through as a **stream** (`new Response(response.body, response)`); remove
|
|
905
|
+
any `.text()`/`.arrayBuffer()`/`HTMLRewriter` buffering from the Rango
|
|
906
|
+
path (keep it only on legacy/proxy branches). See §10a.
|
|
907
|
+
15. [ ] Switch local dev to `vite dev` / `vite preview` (they stream + load
|
|
908
|
+
`.dev.vars` + provide bindings); stop using `wrangler dev` for local
|
|
909
|
+
verification — it gzip-buffers and kills streaming. See §10b.
|
|
910
|
+
16. [ ] Move build output paths from RR7's `build/` to vite's `dist/` in
|
|
911
|
+
`wrangler.toml` (`assets` → `./dist/client/`) and cleanup scripts. See §10d.
|
|
912
|
+
17. [ ] Fix deploy to use the built config `dist/<env>/wrangler.json`
|
|
913
|
+
(`no_bundle: true`), not a root `wrangler deploy` against the source
|
|
914
|
+
worker entry (which can't bundle Rango's virtual modules). See §10e.
|
|
@@ -243,16 +243,16 @@ path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
|
|
|
243
243
|
|
|
244
244
|
## Interaction with DSL Items
|
|
245
245
|
|
|
246
|
-
| DSL item | Behavior with Prerender
|
|
247
|
-
| -------------- |
|
|
248
|
-
| `loader()` | Live at runtime, bundled normally. Use `cache()` for caching.
|
|
249
|
-
| `revalidate()` | Not allowed without Passthrough. Allowed with Passthrough.
|
|
250
|
-
| `cache()` | Orthogonal -- use on parent layouts and loaders.
|
|
251
|
-
| `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live.
|
|
252
|
-
| `parallel()` | Parallel slots inside path are pre-rendered.
|
|
253
|
-
| `middleware()` | Skipped during pre-render (no request). Runs at request time for loaders.
|
|
254
|
-
| `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough.
|
|
255
|
-
| `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when
|
|
246
|
+
| DSL item | Behavior with Prerender |
|
|
247
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
248
|
+
| `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
|
|
249
|
+
| `revalidate()` | Not allowed without Passthrough. Allowed with Passthrough. |
|
|
250
|
+
| `cache()` | Orthogonal -- use on parent layouts and loaders. |
|
|
251
|
+
| `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live. |
|
|
252
|
+
| `parallel()` | Parallel slots inside path are pre-rendered. |
|
|
253
|
+
| `middleware()` | Skipped during pre-render (no request). Runs at request time for loaders. |
|
|
254
|
+
| `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
|
|
255
|
+
| `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally). |
|
|
256
256
|
|
|
257
257
|
When Passthrough revalidation is enabled, remember that revalidation is
|
|
258
258
|
still partial: opting a child segment into revalidation does not
|
|
@@ -609,12 +609,12 @@ At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
|
|
|
609
609
|
(filtered by `namespace?.startsWith("intercept:")`) and sets up slots.
|
|
610
610
|
- **Direct navigation**: looks up `paramHash` (no suffix). Standard prerender path.
|
|
611
611
|
- **Intercept miss (no `/i` entry)**: falls through to the normal pipeline so
|
|
612
|
-
intercept-resolution middleware runs live. This handles `when
|
|
612
|
+
intercept-resolution middleware runs live. This handles `when` config conditions
|
|
613
613
|
that prevented pre-rendering.
|
|
614
614
|
|
|
615
|
-
The `when
|
|
615
|
+
The `when` config selector receives an `InterceptSelectorContext` with `from.pathname`
|
|
616
616
|
which is unknown at build time. All intercepts are pre-rendered unconditionally;
|
|
617
|
-
`when
|
|
617
|
+
`when` is evaluated at runtime by the intercept-resolution middleware.
|
|
618
618
|
|
|
619
619
|
### Example: Pre-rendered route with intercept
|
|
620
620
|
|
|
@@ -633,10 +633,13 @@ layout(ShopLayout, () => [
|
|
|
633
633
|
|
|
634
634
|
// Intercept detail from shop index into a modal.
|
|
635
635
|
// At build time, this is resolved and stored under the /i key.
|
|
636
|
-
intercept(
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
636
|
+
intercept(
|
|
637
|
+
"@modal",
|
|
638
|
+
".detail",
|
|
639
|
+
<ProductModal />,
|
|
640
|
+
{ when: ({ from }) => from.pathname === "/shop" },
|
|
641
|
+
() => [loader(ProductLoader)],
|
|
642
|
+
),
|
|
640
643
|
])
|
|
641
644
|
```
|
|
642
645
|
|
|
@@ -60,8 +60,7 @@ urls(
|
|
|
60
60
|
cache, // Configure caching
|
|
61
61
|
middleware, // Add middleware
|
|
62
62
|
revalidate, // Control revalidation
|
|
63
|
-
intercept, // Intercept routes for modals
|
|
64
|
-
when, // Conditional rendering
|
|
63
|
+
intercept, // Intercept routes for modals (conditional via intercept(..., { when }))
|
|
65
64
|
errorBoundary, // Add an error boundary
|
|
66
65
|
notFoundBoundary, // Add a not-found boundary
|
|
67
66
|
transition, // Configure view transitions
|