@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/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
setRequestContextParams,
|
|
12
12
|
} from "../server/request-context.js";
|
|
13
13
|
import { appendMetric } from "../router/metrics.js";
|
|
14
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
14
15
|
import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
|
|
15
16
|
import type { RscPayload } from "./types.js";
|
|
16
17
|
import type { MatchResult } from "../types.js";
|
|
@@ -21,7 +22,34 @@ import {
|
|
|
21
22
|
} from "./helpers.js";
|
|
22
23
|
import type { HandlerContext } from "./handler-context.js";
|
|
23
24
|
|
|
24
|
-
export
|
|
25
|
+
export function handleRscRendering<TEnv>(
|
|
26
|
+
ctx: HandlerContext<TEnv>,
|
|
27
|
+
request: Request,
|
|
28
|
+
env: TEnv,
|
|
29
|
+
url: URL,
|
|
30
|
+
isPartial: boolean,
|
|
31
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
32
|
+
nonce: string | undefined,
|
|
33
|
+
): Promise<Response> {
|
|
34
|
+
// Instrument the whole render phase once through the unified API: it records
|
|
35
|
+
// the "render:total" perf metric AND opens the "rango.render" span from the
|
|
36
|
+
// same boundary (match -> serialize -> SSR), so the two surfaces agree.
|
|
37
|
+
// Loaders kicked off during matching nest under the span; the SSR HTML pass
|
|
38
|
+
// below opens "rango.ssr" the same way.
|
|
39
|
+
return observePhase(PHASES.render, () =>
|
|
40
|
+
handleRscRenderingInner(
|
|
41
|
+
ctx,
|
|
42
|
+
request,
|
|
43
|
+
env,
|
|
44
|
+
url,
|
|
45
|
+
isPartial,
|
|
46
|
+
handleStore,
|
|
47
|
+
nonce,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function handleRscRenderingInner<TEnv>(
|
|
25
53
|
ctx: HandlerContext<TEnv>,
|
|
26
54
|
request: Request,
|
|
27
55
|
env: TEnv,
|
|
@@ -158,7 +186,6 @@ export async function handleRscRendering<TEnv>(
|
|
|
158
186
|
}
|
|
159
187
|
|
|
160
188
|
const metricsStore = reqCtx._metricsStore;
|
|
161
|
-
const renderStart = performance.now();
|
|
162
189
|
|
|
163
190
|
// Serialize to RSC stream
|
|
164
191
|
const rscSerializeStart = performance.now();
|
|
@@ -177,8 +204,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
177
204
|
);
|
|
178
205
|
|
|
179
206
|
if (isRscRequest(request, url, isPartial)) {
|
|
180
|
-
|
|
181
|
-
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
207
|
+
// render:total is recorded by the observePhase wrapper around this function.
|
|
182
208
|
const rscHeaders: Record<string, string> = {
|
|
183
209
|
"content-type": "text/x-component;charset=utf-8",
|
|
184
210
|
vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
|
|
@@ -220,16 +246,14 @@ export async function handleRscRendering<TEnv>(
|
|
|
220
246
|
metricsStore,
|
|
221
247
|
);
|
|
222
248
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const renderDur = performance.now() - renderStart;
|
|
232
|
-
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
249
|
+
// ssr-render-html metric + rango.ssr span from one boundary. render:total is
|
|
250
|
+
// recorded by the observePhase wrapper around this function.
|
|
251
|
+
const htmlStream = await observePhase(PHASES.ssr, () =>
|
|
252
|
+
ssrModule.renderHTML(rscStream, {
|
|
253
|
+
nonce,
|
|
254
|
+
streamMode,
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
233
257
|
|
|
234
258
|
return createResponseWithMergedHeaders(htmlStream, {
|
|
235
259
|
headers: { "content-type": "text/html;charset=utf-8" },
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
setRequestContextParams,
|
|
21
21
|
} from "../server/request-context.js";
|
|
22
22
|
import { appendMetric } from "../router/metrics.js";
|
|
23
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
23
24
|
import type { RscPayload } from "./types.js";
|
|
24
25
|
import {
|
|
25
26
|
hasBodyContent,
|
|
@@ -256,7 +257,32 @@ export async function executeServerAction<TEnv>(
|
|
|
256
257
|
* provide. Redirects are the only non-partial outcome and are handled via
|
|
257
258
|
* X-RSC-Redirect headers before Flight deserialization.
|
|
258
259
|
*/
|
|
259
|
-
export
|
|
260
|
+
export function revalidateAfterAction<TEnv>(
|
|
261
|
+
ctx: HandlerContext<TEnv>,
|
|
262
|
+
request: Request,
|
|
263
|
+
env: TEnv,
|
|
264
|
+
url: URL,
|
|
265
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
266
|
+
continuation: ActionContinuation,
|
|
267
|
+
): Promise<Response> {
|
|
268
|
+
// Instrument the action-revalidation render through the unified phase API,
|
|
269
|
+
// exactly like a normal navigation render (handleRscRendering). It records
|
|
270
|
+
// "render:total" AND opens "rango.render" from one boundary covering
|
|
271
|
+
// matchPartial -> serialize, so the revalidation loaders' rango.loader spans
|
|
272
|
+
// nest under a rango.render parent instead of dangling at the request root.
|
|
273
|
+
return observePhase(PHASES.render, () =>
|
|
274
|
+
revalidateAfterActionInner(
|
|
275
|
+
ctx,
|
|
276
|
+
request,
|
|
277
|
+
env,
|
|
278
|
+
url,
|
|
279
|
+
handleStore,
|
|
280
|
+
continuation,
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function revalidateAfterActionInner<TEnv>(
|
|
260
286
|
ctx: HandlerContext<TEnv>,
|
|
261
287
|
request: Request,
|
|
262
288
|
env: TEnv,
|
|
@@ -335,13 +361,8 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
335
361
|
});
|
|
336
362
|
const rscSerializeDur = performance.now() - renderStart;
|
|
337
363
|
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
364
|
+
// render:total is recorded by the observePhase wrapper in revalidateAfterAction.
|
|
338
365
|
appendMetric(metricsStore, "rsc-serialize", renderStart, rscSerializeDur);
|
|
339
|
-
appendMetric(
|
|
340
|
-
metricsStore,
|
|
341
|
-
"render:total",
|
|
342
|
-
renderStart,
|
|
343
|
-
performance.now() - renderStart,
|
|
344
|
-
);
|
|
345
366
|
|
|
346
367
|
return createResponseWithMergedHeaders(rscStream, {
|
|
347
368
|
status: actionStatus,
|
package/src/segment-system.tsx
CHANGED
|
@@ -504,7 +504,10 @@ export async function renderSegments(
|
|
|
504
504
|
// slot name is user-controlled (`@${string}`) and may contain an uppercase "D"
|
|
505
505
|
// (e.g. "@Detail"). Strip from the first `D<index>.` separator so the slot name
|
|
506
506
|
// is preserved; splitting on a bare "D" mis-cut "@Detail" to "@" and silently
|
|
507
|
-
// dropped the loader's data.
|
|
507
|
+
// dropped the loader's data. The first-`D<index>.` strip is only correct because
|
|
508
|
+
// slot names cannot contain "." -- assertValidSlotName (route-definition/
|
|
509
|
+
// dsl-helpers.ts) rejects a "." at definition time, so a name like "@D3.foo"
|
|
510
|
+
// (which WOULD mis-cut here) can never reach this function.
|
|
508
511
|
function loaderParentId(loaderSegmentId: string): string {
|
|
509
512
|
return loaderSegmentId.replace(/D\d+\..*$/, "");
|
|
510
513
|
}
|
|
@@ -41,11 +41,13 @@ import {
|
|
|
41
41
|
} from "./handle-store.js";
|
|
42
42
|
import { isHandle } from "../handle.js";
|
|
43
43
|
import { withDefer } from "../defer.js";
|
|
44
|
-
import {
|
|
44
|
+
import { type MetricsStore } from "./context.js";
|
|
45
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
45
46
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
46
47
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
47
48
|
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
48
49
|
import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
|
|
50
|
+
import type { ResolvedTracing } from "../router/tracing.js";
|
|
49
51
|
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
50
52
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
51
53
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
@@ -363,6 +365,9 @@ export interface RequestContext<
|
|
|
363
365
|
/** @internal Request-scoped performance metrics store */
|
|
364
366
|
_metricsStore?: MetricsStore;
|
|
365
367
|
|
|
368
|
+
/** @internal Resolved platform phase-span tracing for this request (Cloudflare or OTel) */
|
|
369
|
+
_tracing?: ResolvedTracing;
|
|
370
|
+
|
|
366
371
|
/** @internal Router basename for this request (used by redirect()) */
|
|
367
372
|
_basename?: string;
|
|
368
373
|
|
|
@@ -1114,10 +1119,13 @@ export function createUseFunction<TEnv>(
|
|
|
1114
1119
|
},
|
|
1115
1120
|
};
|
|
1116
1121
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1122
|
+
// Meter through the same unified phase API as the loader-resolution funnel
|
|
1123
|
+
// (observePhase), so a loader resolved via this base request-context ctx.use
|
|
1124
|
+
// co-emits the "loader:<id>" perf metric AND the "rango.loader" span — no
|
|
1125
|
+
// drift between the two ctx.use implementations.
|
|
1126
|
+
const promise = observePhase(PHASES.loader(loader.$$id), () =>
|
|
1127
|
+
Promise.resolve(loaderFn(loaderCtx)),
|
|
1128
|
+
);
|
|
1121
1129
|
|
|
1122
1130
|
loaderPromises.set(loader.$$id, promise);
|
|
1123
1131
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
runWithConcurrency,
|
|
14
14
|
groupByConcurrency,
|
|
15
15
|
notifyOnError,
|
|
16
|
+
resolvePrerenderError,
|
|
16
17
|
stageBuildAssetModule,
|
|
17
18
|
} from "../utils/prerender-utils.js";
|
|
18
19
|
import type { DiscoveryState } from "./state.js";
|
|
@@ -231,6 +232,10 @@ export async function expandPrerenderRoutes(
|
|
|
231
232
|
const manifestEntries: Record<string, string> = {};
|
|
232
233
|
let doneCount = 0;
|
|
233
234
|
let skipCount = 0;
|
|
235
|
+
// #587: a render error reaches here (matchForPrerender now re-throws it rather
|
|
236
|
+
// than baking the error boundary). Default "fail" fails the build; "warn" logs
|
|
237
|
+
// and skips baking the URL.
|
|
238
|
+
const prerenderOnError = state.opts?.prerenderOnError ?? "fail";
|
|
234
239
|
const startTotal = performance.now();
|
|
235
240
|
|
|
236
241
|
// Group entries by concurrency for batched rendering.
|
|
@@ -297,35 +302,22 @@ export async function expandPrerenderRoutes(
|
|
|
297
302
|
doneCount++;
|
|
298
303
|
break;
|
|
299
304
|
} catch (err: any) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
`[rango] SKIP ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
|
|
304
|
-
);
|
|
305
|
-
skipCount++;
|
|
306
|
-
notifyOnError(
|
|
307
|
-
registry,
|
|
308
|
-
err,
|
|
309
|
-
"prerender",
|
|
310
|
-
entry.routeName,
|
|
311
|
-
entry.urlPath,
|
|
312
|
-
true,
|
|
313
|
-
);
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
// Regular error: log, notify, and fail the build
|
|
305
|
+
// Skip, or a render error under prerender.onError "warn": skip the
|
|
306
|
+
// URL (never bake the error page, #587). A render error under the
|
|
307
|
+
// default "fail" re-throws below to fail the build.
|
|
317
308
|
const elapsed = (performance.now() - startUrl).toFixed(0);
|
|
318
|
-
|
|
319
|
-
`[rango] FAIL ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
|
|
320
|
-
);
|
|
321
|
-
notifyOnError(
|
|
309
|
+
resolvePrerenderError(
|
|
322
310
|
registry,
|
|
323
311
|
err,
|
|
312
|
+
prerenderOnError,
|
|
313
|
+
entry.urlPath.padEnd(40),
|
|
314
|
+
elapsed,
|
|
324
315
|
"prerender",
|
|
325
316
|
entry.routeName,
|
|
326
317
|
entry.urlPath,
|
|
327
318
|
);
|
|
328
|
-
|
|
319
|
+
skipCount++;
|
|
320
|
+
break;
|
|
329
321
|
}
|
|
330
322
|
}
|
|
331
323
|
},
|
|
@@ -378,6 +370,7 @@ export async function renderStaticHandlers(
|
|
|
378
370
|
let staticDone = 0;
|
|
379
371
|
let staticSkip = 0;
|
|
380
372
|
let totalStaticCount = 0;
|
|
373
|
+
const prerenderOnError = state.opts?.prerenderOnError ?? "fail";
|
|
381
374
|
|
|
382
375
|
// Count handlers for the log header
|
|
383
376
|
for (const [, exportNames] of state.resolvedStaticModules) {
|
|
@@ -434,23 +427,19 @@ export async function renderStaticHandlers(
|
|
|
434
427
|
break;
|
|
435
428
|
}
|
|
436
429
|
} catch (err: any) {
|
|
437
|
-
|
|
438
|
-
const elapsed = (performance.now() - startHandler).toFixed(0);
|
|
439
|
-
console.log(
|
|
440
|
-
`[rango] SKIP ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
|
|
441
|
-
);
|
|
442
|
-
staticSkip++;
|
|
443
|
-
notifyOnError(registry, err, "static", undefined, undefined, true);
|
|
444
|
-
handled = true;
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
// Regular error: log, notify, and fail the build
|
|
430
|
+
// Same Skip/warn/fail policy as the prerender loop (shared helper).
|
|
448
431
|
const elapsed = (performance.now() - startHandler).toFixed(0);
|
|
449
|
-
|
|
450
|
-
|
|
432
|
+
resolvePrerenderError(
|
|
433
|
+
registry,
|
|
434
|
+
err,
|
|
435
|
+
prerenderOnError,
|
|
436
|
+
name.padEnd(40),
|
|
437
|
+
elapsed,
|
|
438
|
+
"static",
|
|
451
439
|
);
|
|
452
|
-
|
|
453
|
-
|
|
440
|
+
staticSkip++;
|
|
441
|
+
handled = true;
|
|
442
|
+
break;
|
|
454
443
|
}
|
|
455
444
|
}
|
|
456
445
|
if (!handled) {
|
|
@@ -25,6 +25,12 @@ export interface PluginOptions {
|
|
|
25
25
|
* Compiled into `DiscoveryState.scanFilter` once `projectRoot` is known.
|
|
26
26
|
*/
|
|
27
27
|
discovery?: { include?: string[]; exclude?: string[] };
|
|
28
|
+
/**
|
|
29
|
+
* What to do when a Prerender/Static render throws at build time, from
|
|
30
|
+
* rango({ prerender: { onError } }). "fail" (default) fails the build; "warn"
|
|
31
|
+
* logs and skips baking the URL. See {@link import("../plugin-types.js").RangoOptions}.
|
|
32
|
+
*/
|
|
33
|
+
prerenderOnError?: "fail" | "warn";
|
|
28
34
|
/**
|
|
29
35
|
* Shared context the built-in clientChunks strategy reads. Discovery populates
|
|
30
36
|
* it (registered fallback hashes + single-router name) before the client build
|
package/src/vite/plugin-types.ts
CHANGED
|
@@ -139,6 +139,31 @@ interface RangoBaseOptions {
|
|
|
139
139
|
include?: string[];
|
|
140
140
|
exclude?: string[];
|
|
141
141
|
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* What to do when a `Prerender` route's or `Static` handler's render throws at
|
|
145
|
+
* build time. Otherwise the route error boundary catches it and the rendered
|
|
146
|
+
* error page is baked as the artifact, then served as an HTTP 200 — a silent,
|
|
147
|
+
* user-visible breakage (issue #587). Independent of this setting, a render may
|
|
148
|
+
* `throw new Skip()` (from `@rangojs/router`) to skip a single URL/handler.
|
|
149
|
+
*
|
|
150
|
+
* - `"fail"` (**default**): fail the build, naming the URL/handler and the
|
|
151
|
+
* original render error.
|
|
152
|
+
* - `"warn"`: log a warning and skip baking the artifact — it is never served as a
|
|
153
|
+
* baked 200 error page. `"warn"` is a build-unblock, NOT a runtime contract for
|
|
154
|
+
* the skipped entry: the route falls through to normal resolution, which may
|
|
155
|
+
* render live (its handler is still bundled — e.g. when nothing else baked) or
|
|
156
|
+
* 404 (once prerender handler eviction has run for other baked entries), so the
|
|
157
|
+
* outcome depends on the rest of the build, and a skipped `Static()` handler's
|
|
158
|
+
* evicted code can surface as an error. For DEFINED runtime behavior use
|
|
159
|
+
* `Passthrough()` (a live fallback) or `throw new Skip()` (an intentional skip);
|
|
160
|
+
* otherwise prefer the default `"fail"`.
|
|
161
|
+
*
|
|
162
|
+
* @default "fail"
|
|
163
|
+
*/
|
|
164
|
+
prerender?: {
|
|
165
|
+
onError?: "fail" | "warn";
|
|
166
|
+
};
|
|
142
167
|
}
|
|
143
168
|
|
|
144
169
|
/**
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { normalizePath, findMatchingParen } from "../expose-id-utils.js";
|
|
6
6
|
import { getImportedFnNames } from "./export-analysis.js";
|
|
7
|
+
import { codeMatchIndices } from "../../../build/route-types/source-scan.js";
|
|
7
8
|
import { createRangoDebugger, createCounter, NS } from "../../debug.js";
|
|
8
9
|
|
|
9
10
|
const debug = createRangoDebugger(NS.transform);
|
|
@@ -28,7 +29,16 @@ export function transformRouter(
|
|
|
28
29
|
const routeNamesImport = `./${basename}.named-routes.gen.js`;
|
|
29
30
|
const routeNamesVar = `__rsc_rn`;
|
|
30
31
|
|
|
32
|
+
// Only inject at call sites in REAL code, not inside comments or string
|
|
33
|
+
// literals — e.g. a `createRouter({ ... })` snippet in a JSDoc example must
|
|
34
|
+
// not get a `$$id`/`.named-routes.gen` import injected. The sibling analysis
|
|
35
|
+
// path (export-analysis.ts) already uses this same comment/string-aware scan;
|
|
36
|
+
// the raw `pat.exec(code)` loop below would otherwise match in comments too.
|
|
37
|
+
const codeOffsets = new Set(codeMatchIndices(code, pat));
|
|
38
|
+
pat.lastIndex = 0;
|
|
39
|
+
|
|
31
40
|
while ((match = pat.exec(code)) !== null) {
|
|
41
|
+
if (!codeOffsets.has(match.index)) continue;
|
|
32
42
|
const callStart = match.index;
|
|
33
43
|
const parenPos = match.index + match[0].length - 1;
|
|
34
44
|
|
package/src/vite/rango.ts
CHANGED
|
@@ -428,6 +428,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
428
428
|
enableBuildPrerender: prerenderEnabled,
|
|
429
429
|
buildEnv: options?.buildEnv,
|
|
430
430
|
preset,
|
|
431
|
+
prerenderOnError: options?.prerender?.onError,
|
|
431
432
|
discovery: options?.discovery,
|
|
432
433
|
clientChunkCtx,
|
|
433
434
|
}),
|
|
@@ -912,9 +912,15 @@ export function createRouterDiscoveryPlugin(
|
|
|
912
912
|
logResult(200, `match ${result.routeName}`);
|
|
913
913
|
return;
|
|
914
914
|
} catch (err: any) {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
)
|
|
915
|
+
// matchForPrerender now re-throws render failures instead of baking
|
|
916
|
+
// an error page (issue #587). In dev there is no frozen artifact, so
|
|
917
|
+
// we fall through (404 -> live handler). A `throw new Skip()` is the
|
|
918
|
+
// expected "skip this URL" signal, not a failure, so it stays quiet.
|
|
919
|
+
if (err?.name !== "Skip") {
|
|
920
|
+
console.warn(
|
|
921
|
+
`[rango] Dev prerender error for ${pathname} (serving live instead): ${err.message}`,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
918
924
|
}
|
|
919
925
|
}
|
|
920
926
|
|
|
@@ -137,6 +137,42 @@ export function notifyOnError(
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a thrown build-time render error into the prerender build's policy and
|
|
142
|
+
* log a per-entry line. A `Skip` (or any render error under `prerender.onError:
|
|
143
|
+
* "warn"`) logs and returns so the caller skips the entry; a render error under
|
|
144
|
+
* the default "fail" logs FAIL, notifies `onError`, and re-throws to fail the
|
|
145
|
+
* build. Shared by `expandPrerenderRoutes` (prerender) and `renderStaticHandlers`
|
|
146
|
+
* (static) so the Skip/warn/fail policy lives in one place. `label` is the padded
|
|
147
|
+
* URL / handler name for the log line; `elapsed` is the per-entry duration string.
|
|
148
|
+
*/
|
|
149
|
+
export function resolvePrerenderError(
|
|
150
|
+
registry: Map<string, any>,
|
|
151
|
+
error: any,
|
|
152
|
+
onError: "fail" | "warn",
|
|
153
|
+
label: string,
|
|
154
|
+
elapsed: string,
|
|
155
|
+
phase: "prerender" | "static",
|
|
156
|
+
routeKey?: string,
|
|
157
|
+
pathname?: string,
|
|
158
|
+
): void {
|
|
159
|
+
const isSkip = error?.name === "Skip";
|
|
160
|
+
if (isSkip || onError === "warn") {
|
|
161
|
+
if (isSkip) {
|
|
162
|
+
console.log(`[rango] SKIP ${label} (${elapsed}ms) - ${error.message}`);
|
|
163
|
+
} else {
|
|
164
|
+
console.warn(
|
|
165
|
+
`[rango] WARN ${label} (${elapsed}ms) - render error, not pre-rendered (prerender.onError: "warn"): ${error.message}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
notifyOnError(registry, error, phase, routeKey, pathname, true);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
console.error(`[rango] FAIL ${label} (${elapsed}ms) - ${error.message}`);
|
|
172
|
+
notifyOnError(registry, error, phase, routeKey, pathname);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
|
|
140
176
|
function getStagedAssetDir(projectRoot: string): string {
|
|
141
177
|
return resolve(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
|
|
142
178
|
}
|