@rangojs/router 0.0.0-experimental.126 → 0.0.0-experimental.128

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/bin/rango.js +5 -1
  2. package/dist/vite/index.js +55 -40
  3. package/package.json +23 -19
  4. package/skills/observability/SKILL.md +39 -4
  5. package/skills/prerender/SKILL.md +30 -11
  6. package/skills/router-setup/SKILL.md +23 -3
  7. package/src/build/route-types/codegen.ts +12 -1
  8. package/src/cache/cache-scope.ts +20 -0
  9. package/src/cloudflare/index.ts +11 -0
  10. package/src/cloudflare/tracing.ts +112 -0
  11. package/src/index.rsc.ts +19 -2
  12. package/src/index.ts +16 -1
  13. package/src/route-definition/dsl-helpers.ts +19 -0
  14. package/src/router/instrument.ts +440 -0
  15. package/src/router/loader-resolution.ts +15 -10
  16. package/src/router/match-middleware/cache-lookup.ts +9 -14
  17. package/src/router/match-middleware/cache-store.ts +12 -0
  18. package/src/router/middleware.ts +23 -2
  19. package/src/router/prerender-match.ts +5 -2
  20. package/src/router/router-context.ts +2 -1
  21. package/src/router/router-interfaces.ts +8 -0
  22. package/src/router/router-options.ts +58 -4
  23. package/src/router/segment-resolution/fresh.ts +15 -18
  24. package/src/router/segment-resolution/helpers.ts +6 -0
  25. package/src/router/segment-resolution/loader-cache.ts +5 -0
  26. package/src/router/segment-resolution/revalidation.ts +9 -18
  27. package/src/router/segment-wrappers.ts +3 -2
  28. package/src/router/telemetry-otel.ts +161 -179
  29. package/src/router/tracing.ts +203 -0
  30. package/src/router.ts +9 -0
  31. package/src/rsc/handler.ts +140 -134
  32. package/src/rsc/loader-fetch.ts +7 -1
  33. package/src/rsc/progressive-enhancement.ts +9 -2
  34. package/src/rsc/rsc-rendering.ts +38 -14
  35. package/src/rsc/server-action.ts +28 -7
  36. package/src/segment-system.tsx +4 -1
  37. package/src/server/request-context.ts +23 -5
  38. package/src/vite/discovery/prerender-collection.ts +26 -37
  39. package/src/vite/discovery/state.ts +6 -0
  40. package/src/vite/plugin-types.ts +25 -0
  41. package/src/vite/plugins/expose-ids/router-transform.ts +10 -0
  42. package/src/vite/rango.ts +1 -0
  43. package/src/vite/router-discovery.ts +9 -3
  44. package/src/vite/utils/prerender-utils.ts +36 -0
@@ -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
- if (err.name === "Skip") {
301
- const elapsed = (performance.now() - startUrl).toFixed(0);
302
- console.log(
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
- console.error(
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
- throw err;
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
- if (err.name === "Skip") {
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
- console.error(
450
- `[rango] FAIL ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
432
+ resolvePrerenderError(
433
+ registry,
434
+ err,
435
+ prerenderOnError,
436
+ name.padEnd(40),
437
+ elapsed,
438
+ "static",
451
439
  );
452
- notifyOnError(registry, err, "static");
453
- throw err;
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
@@ -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
- console.warn(
916
- `[rango] Dev prerender failed for ${pathname}: ${err.message}`,
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
  }