@rangojs/router 0.0.0-experimental.126 → 0.0.0-experimental.127
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 +5 -1
- package/dist/vite/index.js +55 -40
- package/package.json +23 -19
- package/skills/observability/SKILL.md +12 -3
- package/skills/prerender/SKILL.md +30 -11
- package/skills/router-setup/SKILL.md +11 -3
- package/src/build/route-types/codegen.ts +12 -1
- package/src/cache/cache-scope.ts +20 -0
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/index.rsc.ts +19 -2
- package/src/index.ts +16 -1
- package/src/route-definition/dsl-helpers.ts +19 -0
- package/src/router/instrument.ts +230 -0
- package/src/router/loader-resolution.ts +15 -10
- package/src/router/match-middleware/cache-lookup.ts +9 -14
- package/src/router/match-middleware/cache-store.ts +12 -0
- package/src/router/middleware.ts +23 -2
- package/src/router/prerender-match.ts +5 -2
- package/src/router/router-context.ts +2 -1
- package/src/router/router-interfaces.ts +8 -0
- package/src/router/router-options.ts +58 -4
- package/src/router/segment-resolution/fresh.ts +15 -18
- package/src/router/segment-resolution/helpers.ts +6 -0
- package/src/router/segment-resolution/loader-cache.ts +5 -0
- package/src/router/segment-resolution/revalidation.ts +9 -18
- package/src/router/segment-wrappers.ts +3 -2
- package/src/router/telemetry-otel.ts +161 -179
- package/src/router/tracing.ts +198 -0
- package/src/router.ts +9 -0
- package/src/rsc/handler.ts +132 -134
- package/src/rsc/loader-fetch.ts +7 -1
- package/src/rsc/progressive-enhancement.ts +9 -2
- package/src/rsc/rsc-rendering.ts +38 -14
- package/src/rsc/server-action.ts +28 -7
- package/src/segment-system.tsx +4 -1
- package/src/server/request-context.ts +13 -5
- package/src/vite/discovery/prerender-collection.ts +26 -37
- package/src/vite/discovery/state.ts +6 -0
- package/src/vite/plugin-types.ts +25 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +10 -0
- package/src/vite/rango.ts +1 -0
- package/src/vite/router-discovery.ts +9 -3
- package/src/vite/utils/prerender-utils.ts +36 -0
package/dist/bin/rango.js
CHANGED
|
@@ -190,9 +190,13 @@ export const NamedRoutes = {
|
|
|
190
190
|
${objectBody}
|
|
191
191
|
} as const;
|
|
192
192
|
|
|
193
|
+
// Aliased so the augmentation below does not pay a homomorphic mapped-type
|
|
194
|
+
// instantiation per route; \`as const\` already makes the members readonly.
|
|
195
|
+
type NamedRoutesShape = typeof NamedRoutes;
|
|
196
|
+
|
|
193
197
|
declare global {
|
|
194
198
|
namespace Rango {
|
|
195
|
-
interface GeneratedRouteMap extends
|
|
199
|
+
interface GeneratedRouteMap extends NamedRoutesShape {}
|
|
196
200
|
}
|
|
197
201
|
}
|
|
198
202
|
`;
|
package/dist/vite/index.js
CHANGED
|
@@ -1192,7 +1192,10 @@ function transformRouter(code, filePath, routerFnNames, absolutePath) {
|
|
|
1192
1192
|
const basename2 = path3.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
1193
1193
|
const routeNamesImport = `./${basename2}.named-routes.gen.js`;
|
|
1194
1194
|
const routeNamesVar = `__rsc_rn`;
|
|
1195
|
+
const codeOffsets = new Set(codeMatchIndices(code, pat));
|
|
1196
|
+
pat.lastIndex = 0;
|
|
1195
1197
|
while ((match = pat.exec(code)) !== null) {
|
|
1198
|
+
if (!codeOffsets.has(match.index)) continue;
|
|
1196
1199
|
const callStart = match.index;
|
|
1197
1200
|
const parenPos = match.index + match[0].length - 1;
|
|
1198
1201
|
const closeParen = findMatchingParen(code, parenPos + 1);
|
|
@@ -2130,7 +2133,7 @@ import { resolve } from "node:path";
|
|
|
2130
2133
|
// package.json
|
|
2131
2134
|
var package_default = {
|
|
2132
2135
|
name: "@rangojs/router",
|
|
2133
|
-
version: "0.0.0-experimental.
|
|
2136
|
+
version: "0.0.0-experimental.127",
|
|
2134
2137
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2135
2138
|
keywords: [
|
|
2136
2139
|
"react",
|
|
@@ -2240,6 +2243,11 @@ var package_default = {
|
|
|
2240
2243
|
"react-server": "./src/cache/cache-runtime.ts",
|
|
2241
2244
|
default: "./src/cache/cache-runtime.ts"
|
|
2242
2245
|
},
|
|
2246
|
+
"./cloudflare": {
|
|
2247
|
+
types: "./src/cloudflare/index.ts",
|
|
2248
|
+
"react-server": "./src/cloudflare/index.ts",
|
|
2249
|
+
default: "./src/cloudflare/index.ts"
|
|
2250
|
+
},
|
|
2243
2251
|
"./theme": {
|
|
2244
2252
|
types: "./src/theme/index.ts",
|
|
2245
2253
|
default: "./src/theme/index.ts"
|
|
@@ -2532,9 +2540,13 @@ export const NamedRoutes = {
|
|
|
2532
2540
|
${objectBody}
|
|
2533
2541
|
} as const;
|
|
2534
2542
|
|
|
2543
|
+
// Aliased so the augmentation below does not pay a homomorphic mapped-type
|
|
2544
|
+
// instantiation per route; \`as const\` already makes the members readonly.
|
|
2545
|
+
type NamedRoutesShape = typeof NamedRoutes;
|
|
2546
|
+
|
|
2535
2547
|
declare global {
|
|
2536
2548
|
namespace Rango {
|
|
2537
|
-
interface GeneratedRouteMap extends
|
|
2549
|
+
interface GeneratedRouteMap extends NamedRoutesShape {}
|
|
2538
2550
|
}
|
|
2539
2551
|
}
|
|
2540
2552
|
`;
|
|
@@ -4521,6 +4533,23 @@ function notifyOnError(registry, error, phase, routeKey, pathname, skipped) {
|
|
|
4521
4533
|
break;
|
|
4522
4534
|
}
|
|
4523
4535
|
}
|
|
4536
|
+
function resolvePrerenderError(registry, error, onError, label, elapsed, phase, routeKey, pathname) {
|
|
4537
|
+
const isSkip = error?.name === "Skip";
|
|
4538
|
+
if (isSkip || onError === "warn") {
|
|
4539
|
+
if (isSkip) {
|
|
4540
|
+
console.log(`[rango] SKIP ${label} (${elapsed}ms) - ${error.message}`);
|
|
4541
|
+
} else {
|
|
4542
|
+
console.warn(
|
|
4543
|
+
`[rango] WARN ${label} (${elapsed}ms) - render error, not pre-rendered (prerender.onError: "warn"): ${error.message}`
|
|
4544
|
+
);
|
|
4545
|
+
}
|
|
4546
|
+
notifyOnError(registry, error, phase, routeKey, pathname, true);
|
|
4547
|
+
return;
|
|
4548
|
+
}
|
|
4549
|
+
console.error(`[rango] FAIL ${label} (${elapsed}ms) - ${error.message}`);
|
|
4550
|
+
notifyOnError(registry, error, phase, routeKey, pathname);
|
|
4551
|
+
throw error;
|
|
4552
|
+
}
|
|
4524
4553
|
function getStagedAssetDir(projectRoot) {
|
|
4525
4554
|
return resolve5(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
|
|
4526
4555
|
}
|
|
@@ -4722,6 +4751,7 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
|
|
|
4722
4751
|
const manifestEntries = {};
|
|
4723
4752
|
let doneCount = 0;
|
|
4724
4753
|
let skipCount = 0;
|
|
4754
|
+
const prerenderOnError = state.opts?.prerenderOnError ?? "fail";
|
|
4725
4755
|
const startTotal = performance.now();
|
|
4726
4756
|
const groups = groupByConcurrency(entries);
|
|
4727
4757
|
for (const group of groups) {
|
|
@@ -4781,34 +4811,19 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
|
|
|
4781
4811
|
doneCount++;
|
|
4782
4812
|
break;
|
|
4783
4813
|
} catch (err) {
|
|
4784
|
-
if (err.name === "Skip") {
|
|
4785
|
-
const elapsed2 = (performance.now() - startUrl).toFixed(0);
|
|
4786
|
-
console.log(
|
|
4787
|
-
`[rango] SKIP ${entry.urlPath.padEnd(40)} (${elapsed2}ms) - ${err.message}`
|
|
4788
|
-
);
|
|
4789
|
-
skipCount++;
|
|
4790
|
-
notifyOnError(
|
|
4791
|
-
registry,
|
|
4792
|
-
err,
|
|
4793
|
-
"prerender",
|
|
4794
|
-
entry.routeName,
|
|
4795
|
-
entry.urlPath,
|
|
4796
|
-
true
|
|
4797
|
-
);
|
|
4798
|
-
break;
|
|
4799
|
-
}
|
|
4800
4814
|
const elapsed = (performance.now() - startUrl).toFixed(0);
|
|
4801
|
-
|
|
4802
|
-
`[rango] FAIL ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`
|
|
4803
|
-
);
|
|
4804
|
-
notifyOnError(
|
|
4815
|
+
resolvePrerenderError(
|
|
4805
4816
|
registry,
|
|
4806
4817
|
err,
|
|
4818
|
+
prerenderOnError,
|
|
4819
|
+
entry.urlPath.padEnd(40),
|
|
4820
|
+
elapsed,
|
|
4807
4821
|
"prerender",
|
|
4808
4822
|
entry.routeName,
|
|
4809
4823
|
entry.urlPath
|
|
4810
4824
|
);
|
|
4811
|
-
|
|
4825
|
+
skipCount++;
|
|
4826
|
+
break;
|
|
4812
4827
|
}
|
|
4813
4828
|
}
|
|
4814
4829
|
}
|
|
@@ -4843,6 +4858,7 @@ async function renderStaticHandlers(state, rscEnv, registry) {
|
|
|
4843
4858
|
let staticDone = 0;
|
|
4844
4859
|
let staticSkip = 0;
|
|
4845
4860
|
let totalStaticCount = 0;
|
|
4861
|
+
const prerenderOnError = state.opts?.prerenderOnError ?? "fail";
|
|
4846
4862
|
for (const [, exportNames] of state.resolvedStaticModules) {
|
|
4847
4863
|
totalStaticCount += exportNames.length;
|
|
4848
4864
|
}
|
|
@@ -4890,22 +4906,18 @@ async function renderStaticHandlers(state, rscEnv, registry) {
|
|
|
4890
4906
|
break;
|
|
4891
4907
|
}
|
|
4892
4908
|
} catch (err) {
|
|
4893
|
-
if (err.name === "Skip") {
|
|
4894
|
-
const elapsed2 = (performance.now() - startHandler).toFixed(0);
|
|
4895
|
-
console.log(
|
|
4896
|
-
`[rango] SKIP ${name.padEnd(40)} (${elapsed2}ms) - ${err.message}`
|
|
4897
|
-
);
|
|
4898
|
-
staticSkip++;
|
|
4899
|
-
notifyOnError(registry, err, "static", void 0, void 0, true);
|
|
4900
|
-
handled = true;
|
|
4901
|
-
break;
|
|
4902
|
-
}
|
|
4903
4909
|
const elapsed = (performance.now() - startHandler).toFixed(0);
|
|
4904
|
-
|
|
4905
|
-
|
|
4910
|
+
resolvePrerenderError(
|
|
4911
|
+
registry,
|
|
4912
|
+
err,
|
|
4913
|
+
prerenderOnError,
|
|
4914
|
+
name.padEnd(40),
|
|
4915
|
+
elapsed,
|
|
4916
|
+
"static"
|
|
4906
4917
|
);
|
|
4907
|
-
|
|
4908
|
-
|
|
4918
|
+
staticSkip++;
|
|
4919
|
+
handled = true;
|
|
4920
|
+
break;
|
|
4909
4921
|
}
|
|
4910
4922
|
}
|
|
4911
4923
|
if (!handled) {
|
|
@@ -6271,9 +6283,11 @@ ${err.stack}`
|
|
|
6271
6283
|
logResult(200, `match ${result.routeName}`);
|
|
6272
6284
|
return;
|
|
6273
6285
|
} catch (err) {
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6286
|
+
if (err?.name !== "Skip") {
|
|
6287
|
+
console.warn(
|
|
6288
|
+
`[rango] Dev prerender error for ${pathname} (serving live instead): ${err.message}`
|
|
6289
|
+
);
|
|
6290
|
+
}
|
|
6277
6291
|
}
|
|
6278
6292
|
}
|
|
6279
6293
|
res.statusCode = 404;
|
|
@@ -7063,6 +7077,7 @@ ${list}`);
|
|
|
7063
7077
|
enableBuildPrerender: prerenderEnabled,
|
|
7064
7078
|
buildEnv: options?.buildEnv,
|
|
7065
7079
|
preset,
|
|
7080
|
+
prerenderOnError: options?.prerender?.onError,
|
|
7066
7081
|
discovery: options?.discovery,
|
|
7067
7082
|
clientChunkCtx
|
|
7068
7083
|
})
|
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.127",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -110,6 +110,11 @@
|
|
|
110
110
|
"react-server": "./src/cache/cache-runtime.ts",
|
|
111
111
|
"default": "./src/cache/cache-runtime.ts"
|
|
112
112
|
},
|
|
113
|
+
"./cloudflare": {
|
|
114
|
+
"types": "./src/cloudflare/index.ts",
|
|
115
|
+
"react-server": "./src/cloudflare/index.ts",
|
|
116
|
+
"default": "./src/cloudflare/index.ts"
|
|
117
|
+
},
|
|
113
118
|
"./theme": {
|
|
114
119
|
"types": "./src/theme/index.ts",
|
|
115
120
|
"default": "./src/theme/index.ts"
|
|
@@ -157,17 +162,6 @@
|
|
|
157
162
|
"access": "public",
|
|
158
163
|
"tag": "experimental"
|
|
159
164
|
},
|
|
160
|
-
"scripts": {
|
|
161
|
-
"build": "pnpm dlx 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 dlx esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm dlx 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",
|
|
162
|
-
"prepublishOnly": "pnpm build",
|
|
163
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
164
|
-
"test": "playwright test",
|
|
165
|
-
"test:ui": "playwright test --ui",
|
|
166
|
-
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
167
|
-
"test:unit": "vitest run",
|
|
168
|
-
"test:unit:watch": "vitest",
|
|
169
|
-
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
170
|
-
},
|
|
171
165
|
"dependencies": {
|
|
172
166
|
"@types/debug": "^4.1.12",
|
|
173
167
|
"@vitejs/plugin-rsc": "^0.5.26",
|
|
@@ -179,19 +173,19 @@
|
|
|
179
173
|
},
|
|
180
174
|
"devDependencies": {
|
|
181
175
|
"@playwright/test": "^1.49.1",
|
|
182
|
-
"@shared/e2e": "workspace:*",
|
|
183
176
|
"@testing-library/dom": "^10.4.1",
|
|
184
177
|
"@testing-library/react": "^16.3.2",
|
|
185
178
|
"@types/node": "^24.10.1",
|
|
186
|
-
"@types/react": "
|
|
187
|
-
"@types/react-dom": "
|
|
179
|
+
"@types/react": "^19.2.7",
|
|
180
|
+
"@types/react-dom": "^19.2.3",
|
|
188
181
|
"esbuild": "^0.27.0",
|
|
189
182
|
"happy-dom": "^20.10.1",
|
|
190
183
|
"jiti": "^2.6.1",
|
|
191
|
-
"react": "
|
|
192
|
-
"react-dom": "
|
|
184
|
+
"react": "^19.2.6",
|
|
185
|
+
"react-dom": "^19.2.6",
|
|
193
186
|
"typescript": "^5.3.0",
|
|
194
|
-
"vitest": "^4.0.0"
|
|
187
|
+
"vitest": "^4.0.0",
|
|
188
|
+
"@shared/e2e": "0.0.1"
|
|
195
189
|
},
|
|
196
190
|
"peerDependencies": {
|
|
197
191
|
"@cloudflare/vite-plugin": "^1.38.0",
|
|
@@ -219,5 +213,15 @@
|
|
|
219
213
|
"vitest": {
|
|
220
214
|
"optional": true
|
|
221
215
|
}
|
|
216
|
+
},
|
|
217
|
+
"scripts": {
|
|
218
|
+
"build": "pnpm dlx 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 dlx esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm dlx 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",
|
|
219
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
220
|
+
"test": "playwright test",
|
|
221
|
+
"test:ui": "playwright test --ui",
|
|
222
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
223
|
+
"test:unit": "vitest run",
|
|
224
|
+
"test:unit:watch": "vitest",
|
|
225
|
+
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
222
226
|
}
|
|
223
|
-
}
|
|
227
|
+
}
|
|
@@ -73,16 +73,25 @@ const router = createRouter({
|
|
|
73
73
|
});
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
For OpenTelemetry
|
|
76
|
+
For OpenTelemetry — phase spans come from the `tracing` slot
|
|
77
|
+
(`createOTelTracing`), discrete-fact spans from the `telemetry` sink
|
|
78
|
+
(`createOTelSink`):
|
|
77
79
|
|
|
78
80
|
```typescript
|
|
79
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
createRouter,
|
|
83
|
+
createOTelTracing,
|
|
84
|
+
createOTelSink,
|
|
85
|
+
} from "@rangojs/router";
|
|
80
86
|
import { trace } from "@opentelemetry/api";
|
|
81
87
|
|
|
88
|
+
const tracer = trace.getTracer("my-app");
|
|
89
|
+
|
|
82
90
|
const router = createRouter({
|
|
83
91
|
document: Document,
|
|
84
92
|
urls: urlpatterns,
|
|
85
|
-
|
|
93
|
+
tracing: createOTelTracing(tracer), // request/loader/render/… phase spans
|
|
94
|
+
telemetry: createOTelSink(tracer), // handler errors, cache decisions, …
|
|
86
95
|
});
|
|
87
96
|
```
|
|
88
97
|
|
|
@@ -343,14 +343,31 @@ export const TocSidebar = Static(() => {
|
|
|
343
343
|
|
|
344
344
|
### Error behavior at build time
|
|
345
345
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
346
|
+
When a render throws a non-`Skip` error, it is **surfaced to the build** — never
|
|
347
|
+
baked into a frozen error page served as a 200 (issue #587). What happens next is
|
|
348
|
+
controlled by `prerender.onError` in your `rango()` options:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
rango({ prerender: { onError: "warn" } }); // default is "fail"
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
| Handler outcome | `onError: "fail"` (default) | `onError: "warn"` |
|
|
355
|
+
| --------------------------- | -------------------------------------------- | -------------------------------- |
|
|
356
|
+
| JSX / `null` | Normal prerender entry, log OK | Normal prerender entry, log OK |
|
|
357
|
+
| `return ctx.passthrough()` | Skip entry, log PASS (Passthrough routes) | Skip entry, log PASS |
|
|
358
|
+
| `throw new Skip("reason")` | Skip entry, log SKIP, continue | Skip entry, log SKIP, continue |
|
|
359
|
+
| `throw new Error("reason")` | Log FAIL, stop ALL pre-rendering, fail build | Log WARN, skip the URL, continue |
|
|
360
|
+
|
|
361
|
+
With `"warn"` the errored entry is logged and left un-baked (never served as a baked
|
|
362
|
+
200 error page). `"warn"` is a build-unblock, not a runtime contract: the route falls
|
|
363
|
+
through to normal resolution — it may render live (its handler is still bundled) or
|
|
364
|
+
404 (once other baked entries trigger prerender handler eviction), so the outcome
|
|
365
|
+
depends on the rest of the build, and a skipped `Static()` handler's evicted code can
|
|
366
|
+
surface as an error. For DEFINED runtime behavior reach for `Passthrough()` (a live
|
|
367
|
+
fallback) or `throw new Skip()` (an intentional skip — works in the render fn, not
|
|
368
|
+
only `getParams()`); otherwise prefer the default `"fail"`.
|
|
369
|
+
|
|
370
|
+
Both `Skip` and hard errors propagate to the router's `onError` callback with phase
|
|
354
371
|
`"prerender"` or `"static"`.
|
|
355
372
|
|
|
356
373
|
### Build logs
|
|
@@ -370,9 +387,11 @@ The build produces per-URL timing logs:
|
|
|
370
387
|
[rango] Static render complete: 2 done, 1 skipped (120ms total)
|
|
371
388
|
```
|
|
372
389
|
|
|
373
|
-
A `FAIL` line is logged per-URL when a handler throws a non-Skip error
|
|
374
|
-
error is re-thrown immediately, so no
|
|
375
|
-
stops at the first failure.
|
|
390
|
+
A `FAIL` line is logged per-URL when a handler throws a non-Skip error (with the
|
|
391
|
+
default `prerender.onError: "fail"`). The error is re-thrown immediately, so no
|
|
392
|
+
summary line is printed — the build stops at the first failure. Under
|
|
393
|
+
`prerender.onError: "warn"` the same case logs a `WARN` line, skips that URL, and
|
|
394
|
+
the build continues.
|
|
376
395
|
|
|
377
396
|
### Dev mode behavior
|
|
378
397
|
|
|
@@ -469,14 +469,22 @@ const router = createRouter({
|
|
|
469
469
|
```
|
|
470
470
|
|
|
471
471
|
```typescript
|
|
472
|
-
// OpenTelemetry for production
|
|
473
|
-
|
|
472
|
+
// OpenTelemetry for production: phase spans via the tracing slot,
|
|
473
|
+
// discrete-fact spans via the telemetry sink.
|
|
474
|
+
import {
|
|
475
|
+
createRouter,
|
|
476
|
+
createOTelTracing,
|
|
477
|
+
createOTelSink,
|
|
478
|
+
} from "@rangojs/router";
|
|
474
479
|
import { trace } from "@opentelemetry/api";
|
|
475
480
|
|
|
481
|
+
const tracer = trace.getTracer("my-app");
|
|
482
|
+
|
|
476
483
|
const router = createRouter({
|
|
477
484
|
document: Document,
|
|
478
485
|
urls: urlpatterns,
|
|
479
|
-
|
|
486
|
+
tracing: createOTelTracing(tracer),
|
|
487
|
+
telemetry: createOTelSink(tracer),
|
|
480
488
|
});
|
|
481
489
|
```
|
|
482
490
|
|
|
@@ -88,14 +88,25 @@ export function generateRouteTypesSource(
|
|
|
88
88
|
})
|
|
89
89
|
.join("\n");
|
|
90
90
|
|
|
91
|
+
// The global augmentation extends an alias of `typeof NamedRoutes` rather than
|
|
92
|
+
// `Readonly<typeof NamedRoutes>`. `as const` already makes the members readonly,
|
|
93
|
+
// so the two are behaviour-identical, but `Readonly<>` is a homomorphic mapped
|
|
94
|
+
// type the compiler instantiates once per route at the augmentation site
|
|
95
|
+
// (~4 instantiations/route). An interface `extends` clause cannot name a bare
|
|
96
|
+
// `typeof` query, so the alias is what lets us drop the wrapper. Measured: ~35%
|
|
97
|
+
// fewer type instantiations on a 14k-route app; negligible (but free) on small.
|
|
91
98
|
return `// Auto-generated by @rangojs/router - do not edit
|
|
92
99
|
export const NamedRoutes = {
|
|
93
100
|
${objectBody}
|
|
94
101
|
} as const;
|
|
95
102
|
|
|
103
|
+
// Aliased so the augmentation below does not pay a homomorphic mapped-type
|
|
104
|
+
// instantiation per route; \`as const\` already makes the members readonly.
|
|
105
|
+
type NamedRoutesShape = typeof NamedRoutes;
|
|
106
|
+
|
|
96
107
|
declare global {
|
|
97
108
|
namespace Rango {
|
|
98
|
-
interface GeneratedRouteMap extends
|
|
109
|
+
interface GeneratedRouteMap extends NamedRoutesShape {}
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
112
|
`;
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -299,6 +299,26 @@ export class CacheScope {
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Record this scope's segment-DSL cache({ tags }) into the request tag union
|
|
304
|
+
* synchronously, under the same gate cacheRoute() uses for a write.
|
|
305
|
+
*
|
|
306
|
+
* cacheRoute() already records these tags, but it is invoked inside
|
|
307
|
+
* requestCtx.waitUntil() by the cache-store middleware (and the proactive path
|
|
308
|
+
* re-resolves the whole tree before calling it), so its recording is deferred
|
|
309
|
+
* and RACES the document cache's post-body-drain snapshot of _requestTags. On a
|
|
310
|
+
* first-write (segment-cache miss) the document tag union could miss these
|
|
311
|
+
* tags, and updateTag()/revalidateTag() would then fail to invalidate the
|
|
312
|
+
* cached document until a later write reseeded it. Calling this synchronously
|
|
313
|
+
* in the request pipeline (before the snapshot) closes that window. Idempotent
|
|
314
|
+
* (the tag union is a Set), so the duplicate record in cacheRoute is harmless.
|
|
315
|
+
*/
|
|
316
|
+
recordTags(requestCtx: RequestContext | undefined): void {
|
|
317
|
+
if (!this.enabled) return;
|
|
318
|
+
if (!this.conditionAllows("write")) return;
|
|
319
|
+
recordRequestTags(resolveCacheTags(this.config, requestCtx), requestCtx);
|
|
320
|
+
}
|
|
321
|
+
|
|
302
322
|
/**
|
|
303
323
|
* Cache all segments for a route (non-blocking via waitUntil)
|
|
304
324
|
* Single cache entry per route request.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare preset integration surface for @rangojs/router.
|
|
3
|
+
*
|
|
4
|
+
* Imported via `@rangojs/router/cloudflare`. Cloudflare-only helpers live here
|
|
5
|
+
* so Node/other-platform consumers never pull them in.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
createCloudflareTracing,
|
|
10
|
+
type CloudflareTracingOptions,
|
|
11
|
+
} from "./tracing.js";
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare custom-spans integration.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the router's performance phases (request, middleware, action,
|
|
5
|
+
* loaders, render, ssr) onto Cloudflare Workers custom spans so they show up in the
|
|
6
|
+
* trace waterfall and OpenTelemetry exports next to the platform's automatic
|
|
7
|
+
* spans (KV reads, D1 queries, fetch calls), with correct parent-child nesting.
|
|
8
|
+
*
|
|
9
|
+
* Usage (Cloudflare preset only):
|
|
10
|
+
*
|
|
11
|
+
* import { createRouter } from "@rangojs/router";
|
|
12
|
+
* import { createCloudflareTracing } from "@rangojs/router/cloudflare";
|
|
13
|
+
*
|
|
14
|
+
* export const router = createRouter({
|
|
15
|
+
* tracing: createCloudflareTracing(),
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* The runner reads `executionContext.tracing` (the same object as
|
|
19
|
+
* `import { tracing } from "cloudflare:workers"`) at call time. It is therefore
|
|
20
|
+
* dependency-free: no `cloudflare:workers` import and no hard dependency on
|
|
21
|
+
* `@cloudflare/workers-types`, matching the convention in cache/cf. When the
|
|
22
|
+
* worker is not running on a tracing-enabled Cloudflare runtime — Node, dev
|
|
23
|
+
* without a tracing destination, an older runtime — `executionContext.tracing`
|
|
24
|
+
* is undefined and every span call falls through to the work directly, so the
|
|
25
|
+
* request behaves exactly as if tracing were off. Whether spans are actually
|
|
26
|
+
* recorded is governed by the `observability`/tracing block in wrangler config.
|
|
27
|
+
*
|
|
28
|
+
* Span duration note: a phase span ends when its callback's returned value (or
|
|
29
|
+
* promise) settles, which for the streaming phases (request/render/ssr) is when
|
|
30
|
+
* the Response or HTML/RSC stream is *constructed*, not when the body finishes
|
|
31
|
+
* draining. Loader/Suspense work that settles while the body streams therefore
|
|
32
|
+
* extends past the parent span's end, and platform spans emitted during that
|
|
33
|
+
* drain (deferred SSR, waitUntil-scheduled cache writes) are siblings of the
|
|
34
|
+
* automatic request span rather than children of rango.*. Phase spans bound
|
|
35
|
+
* setup-to-stream-handoff; they are not a full request-duration measure.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
39
|
+
import {
|
|
40
|
+
type RouterTracingConfig,
|
|
41
|
+
type SpanRunner,
|
|
42
|
+
type TracePhaseToggles,
|
|
43
|
+
NOOP_TRACE_SPAN,
|
|
44
|
+
} from "../router/tracing.js";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Minimal local view of Cloudflare's `Span`. Declared here (not imported from
|
|
48
|
+
* `@cloudflare/workers-types`) to avoid a hard type dependency.
|
|
49
|
+
*/
|
|
50
|
+
interface CloudflareSpan {
|
|
51
|
+
readonly isTraced: boolean;
|
|
52
|
+
setAttribute(key: string, value?: boolean | number | string): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Minimal local view of Cloudflare's `Tracing` (only enterSpan is used). */
|
|
56
|
+
interface CloudflareTracing {
|
|
57
|
+
enterSpan<T>(name: string, callback: (span: CloudflareSpan) => T): T;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Options for createCloudflareTracing. */
|
|
61
|
+
export interface CloudflareTracingOptions {
|
|
62
|
+
/** Master switch. Defaults to true. */
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
/** Per-phase span toggles. Omitted phases default to enabled. */
|
|
65
|
+
spans?: TracePhaseToggles;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the per-request Cloudflare tracer from the active execution context.
|
|
70
|
+
* Returns undefined off-Cloudflare or when tracing is not enabled for the
|
|
71
|
+
* worker, in which case the caller runs the work without a span.
|
|
72
|
+
*/
|
|
73
|
+
function getRequestTracer(): CloudflareTracing | undefined {
|
|
74
|
+
const executionContext = _getRequestContext()?.executionContext as
|
|
75
|
+
| { tracing?: CloudflareTracing }
|
|
76
|
+
| undefined;
|
|
77
|
+
const tracing = executionContext?.tracing;
|
|
78
|
+
return tracing && typeof tracing.enterSpan === "function"
|
|
79
|
+
? tracing
|
|
80
|
+
: undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cloudflareSpanRunner: SpanRunner = (name, fn) => {
|
|
84
|
+
const tracer = getRequestTracer();
|
|
85
|
+
if (!tracer) return fn(NOOP_TRACE_SPAN);
|
|
86
|
+
// enterSpan runs the callback now and ends the span when its return value
|
|
87
|
+
// (or returned promise) settles; spans nest by JS async context. fn's param
|
|
88
|
+
// is the narrower TraceSpan, so the wider CloudflareSpan satisfies it directly.
|
|
89
|
+
return tracer.enterSpan(name, fn);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create the tracing config for a Cloudflare router. Pass the result to
|
|
94
|
+
* `createRouter({ tracing })`. Spans are emitted for the request, middleware,
|
|
95
|
+
* action, loaders, render, and ssr phases; pass `spans` to turn individual
|
|
96
|
+
* phases off.
|
|
97
|
+
*
|
|
98
|
+
* @see createOTelTracing (`@rangojs/router`) for the same slot on any platform
|
|
99
|
+
* with an OpenTelemetry SDK.
|
|
100
|
+
*/
|
|
101
|
+
export function createCloudflareTracing(
|
|
102
|
+
options: CloudflareTracingOptions = {},
|
|
103
|
+
): RouterTracingConfig {
|
|
104
|
+
return {
|
|
105
|
+
runner: cloudflareSpanRunner,
|
|
106
|
+
enabled: options.enabled ?? true,
|
|
107
|
+
spans: options.spans,
|
|
108
|
+
};
|
|
109
|
+
}
|
package/src/index.rsc.ts
CHANGED
|
@@ -258,10 +258,17 @@ export {
|
|
|
258
258
|
// Path and response types are ambient on the `Rango` namespace (`Rango.Path`,
|
|
259
259
|
// `Rango.PathResponse`, declared in href-client.ts) — no import needed.
|
|
260
260
|
|
|
261
|
-
// Telemetry sink
|
|
261
|
+
// Telemetry sink (event-shaped facts)
|
|
262
262
|
export { createConsoleSink } from "./router/telemetry.js";
|
|
263
263
|
export { createOTelSink } from "./router/telemetry-otel.js";
|
|
264
|
-
|
|
264
|
+
// OTel phase-span adapter for the `tracing` slot (the canonical span layer).
|
|
265
|
+
export { createOTelTracing } from "./router/telemetry-otel.js";
|
|
266
|
+
export type {
|
|
267
|
+
OTelTracer,
|
|
268
|
+
OTelActiveSpanTracer,
|
|
269
|
+
OTelSpan,
|
|
270
|
+
OTelTracingOptions,
|
|
271
|
+
} from "./router/telemetry-otel.js";
|
|
265
272
|
// The full TelemetryEvent union PLUS its member types, so a consumer writing a
|
|
266
273
|
// TelemetrySink can annotate a per-`type` handler (or construct an event literal
|
|
267
274
|
// in a test) instead of only narrowing the opaque union.
|
|
@@ -283,6 +290,16 @@ export type {
|
|
|
283
290
|
OriginCheckRejectedEvent,
|
|
284
291
|
} from "./router/telemetry.js";
|
|
285
292
|
|
|
293
|
+
// Span tracing config types a consumer annotates. SpanRunner/TraceSpan are the
|
|
294
|
+
// internal runner contract (consumers go through createOTelTracing /
|
|
295
|
+
// createCloudflareTracing, which return a ready RouterTracingConfig) and are not
|
|
296
|
+
// exported.
|
|
297
|
+
export type {
|
|
298
|
+
RouterTracingConfig,
|
|
299
|
+
TracePhase,
|
|
300
|
+
TracePhaseToggles,
|
|
301
|
+
} from "./router/tracing.js";
|
|
302
|
+
|
|
286
303
|
// Timeout types and error class
|
|
287
304
|
export { RouterTimeoutError } from "./router/timeout.js";
|
|
288
305
|
export type {
|
package/src/index.ts
CHANGED
|
@@ -344,7 +344,12 @@ export {
|
|
|
344
344
|
// bundle analysis output and slow build-time module resolution. Consumers
|
|
345
345
|
// who need the values in non-RSC contexts can import from
|
|
346
346
|
// `@rangojs/router/server`.
|
|
347
|
-
export type {
|
|
347
|
+
export type {
|
|
348
|
+
OTelTracer,
|
|
349
|
+
OTelActiveSpanTracer,
|
|
350
|
+
OTelSpan,
|
|
351
|
+
OTelTracingOptions,
|
|
352
|
+
} from "./router/telemetry-otel.js";
|
|
348
353
|
// The full TelemetryEvent union PLUS its member types, so a consumer writing a
|
|
349
354
|
// TelemetrySink can annotate a per-`type` handler (or construct an event literal
|
|
350
355
|
// in a test) instead of only narrowing the opaque union.
|
|
@@ -366,6 +371,16 @@ export type {
|
|
|
366
371
|
OriginCheckRejectedEvent,
|
|
367
372
|
} from "./router/telemetry.js";
|
|
368
373
|
|
|
374
|
+
// Span tracing config types a consumer annotates. SpanRunner/TraceSpan are the
|
|
375
|
+
// internal runner contract (consumers go through createOTelTracing /
|
|
376
|
+
// createCloudflareTracing, which return a ready RouterTracingConfig) and are not
|
|
377
|
+
// exported.
|
|
378
|
+
export type {
|
|
379
|
+
RouterTracingConfig,
|
|
380
|
+
TracePhase,
|
|
381
|
+
TracePhaseToggles,
|
|
382
|
+
} from "./router/tracing.js";
|
|
383
|
+
|
|
369
384
|
// Timeout types and error class
|
|
370
385
|
export { RouterTimeoutError } from "./router/timeout.js";
|
|
371
386
|
export type {
|