@timber-js/app 0.2.0-alpha.56 → 0.2.0-alpha.57

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 (35) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{interception-D2djYaIm.js → interception-Cey5DCGr.js} +18 -1
  3. package/dist/_chunks/interception-Cey5DCGr.js.map +1 -0
  4. package/dist/_chunks/{stale-reload-4L-_skC7.js → stale-reload-Db4wqE46.js} +16 -2
  5. package/dist/_chunks/stale-reload-Db4wqE46.js.map +1 -0
  6. package/dist/client/index.js +1 -1
  7. package/dist/client/stale-reload.d.ts.map +1 -1
  8. package/dist/index.js +4 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/routing/index.js +1 -1
  11. package/dist/routing/scanner.d.ts.map +1 -1
  12. package/dist/routing/types.d.ts +10 -0
  13. package/dist/routing/types.d.ts.map +1 -1
  14. package/dist/server/fallback-error.d.ts +2 -1
  15. package/dist/server/fallback-error.d.ts.map +1 -1
  16. package/dist/server/route-matcher.d.ts +7 -0
  17. package/dist/server/route-matcher.d.ts.map +1 -1
  18. package/dist/server/rsc-entry/error-renderer.d.ts +10 -1
  19. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  20. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  21. package/dist/server/rsc-entry/ssr-renderer.d.ts +3 -0
  22. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  23. package/package.json +6 -7
  24. package/src/cli.ts +0 -0
  25. package/src/client/stale-reload.ts +27 -3
  26. package/src/plugins/routing.ts +8 -0
  27. package/src/routing/scanner.ts +30 -0
  28. package/src/routing/types.ts +10 -0
  29. package/src/server/fallback-error.ts +5 -2
  30. package/src/server/route-matcher.ts +7 -0
  31. package/src/server/rsc-entry/error-renderer.ts +105 -2
  32. package/src/server/rsc-entry/index.ts +16 -4
  33. package/src/server/rsc-entry/ssr-renderer.ts +10 -4
  34. package/dist/_chunks/interception-D2djYaIm.js.map +0 -1
  35. package/dist/_chunks/stale-reload-4L-_skC7.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import { a as DEFAULT_PAGE_EXTENSIONS, i as scanRoutes, n as generateRouteMap, o as INTERCEPTION_MARKERS, r as classifySegment, t as collectInterceptionRewrites } from "../_chunks/interception-D2djYaIm.js";
1
+ import { a as DEFAULT_PAGE_EXTENSIONS, i as scanRoutes, n as generateRouteMap, o as INTERCEPTION_MARKERS, r as classifySegment, t as collectInterceptionRewrites } from "../_chunks/interception-Cey5DCGr.js";
2
2
  export { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS, classifySegment, collectInterceptionRewrites, generateRouteMap, scanRoutes };
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/routing/scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EACV,SAAS,EAET,WAAW,EAEX,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAC;AA6CpB;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,aAAkB,GAAG,SAAS,CA4BhF;AAyBD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG;IAChD,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CA8CA"}
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/routing/scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EACV,SAAS,EAET,WAAW,EAEX,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAC;AA6CpB;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,aAAkB,GAAG,SAAS,CAoChF;AAyBD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG;IAChD,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CA8CA"}
@@ -72,6 +72,16 @@ export interface RouteTree {
72
72
  root: SegmentNode;
73
73
  /** All discovered proxy.ts files (should be at most one, in app/) */
74
74
  proxy?: RouteFile;
75
+ /**
76
+ * Global error page: app/global-error.{tsx,ts,jsx,js}
77
+ *
78
+ * Rendered as a standalone full-page replacement (no layout wrapping)
79
+ * when no segment-level error file is found. SSR-only render path.
80
+ * Must provide its own <html> and <body>.
81
+ *
82
+ * See design/10-error-handling.md §"Tier 2 — Global Error Page"
83
+ */
84
+ globalError?: RouteFile;
75
85
  }
76
86
  /** Configuration passed to the scanner */
77
87
  export interface ScannerConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/routing/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,kCAAkC;AAClC,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,SAAS,GACT,WAAW,GACX,oBAAoB,GACpB,OAAO,GACP,MAAM,GACN,cAAc,GACd,SAAS,CAAC;AAEd;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;AAEvE,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,EAAE,kBAAkB,EAAyC,CAAC;AAE/F,kDAAkD;AAClD,MAAM,WAAW,SAAS;IACxB,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iCAAiC;AACjC,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,WAAW,EAAE,WAAW,CAAC;IACzB,wFAAwF;IACxF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAGhC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACrC,gEAAgE;IAChE,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACzC,8CAA8C;IAC9C,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,8FAA8F;IAC9F,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAE3C,sFAAsF;IACtF,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAGxC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,0DAA0D;IAC1D,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACjC;AAED,kDAAkD;AAClD,MAAM,WAAW,SAAS;IACxB,gDAAgD;IAChD,IAAI,EAAE,WAAW,CAAC;IAClB,qEAAqE;IACrE,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED,0CAA0C;AAC1C,MAAM,WAAW,aAAa;IAC5B,4FAA4F;IAC5F,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,uBAAuB,UAA6B,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/routing/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,kCAAkC;AAClC,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,SAAS,GACT,WAAW,GACX,oBAAoB,GACpB,OAAO,GACP,MAAM,GACN,cAAc,GACd,SAAS,CAAC;AAEd;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;AAEvE,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,EAAE,kBAAkB,EAAyC,CAAC;AAE/F,kDAAkD;AAClD,MAAM,WAAW,SAAS;IACxB,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iCAAiC;AACjC,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,WAAW,EAAE,WAAW,CAAC;IACzB,wFAAwF;IACxF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAGhC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACrC,gEAAgE;IAChE,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACzC,8CAA8C;IAC9C,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,8FAA8F;IAC9F,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAE3C,sFAAsF;IACtF,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAGxC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,0DAA0D;IAC1D,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACjC;AAED,kDAAkD;AAClD,MAAM,WAAW,SAAS;IACxB,gDAAgD;IAChD,IAAI,EAAE,WAAW,CAAC;IAClB,qEAAqE;IACrE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,SAAS,CAAC;CACzB;AAED,0CAA0C;AAC1C,MAAM,WAAW,aAAa;IAC5B,4FAA4F;IAC5F,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,uBAAuB,UAA6B,CAAC"}
@@ -11,13 +11,14 @@
11
11
  */
12
12
  import type { ManifestSegmentNode } from '#/server/route-matcher.js';
13
13
  import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
14
+ import type { GlobalErrorFile } from '#/server/rsc-entry/error-renderer.js';
14
15
  /**
15
16
  * Render a fallback error page when the render pipeline throws.
16
17
  *
17
18
  * In dev: styled HTML with error details.
18
19
  * In prod: renders root error pages via renderErrorPage.
19
20
  */
20
- export declare function renderFallbackError(error: unknown, req: Request, responseHeaders: Headers, isDev: boolean, rootSegment: ManifestSegmentNode, clientBootstrap: ClientBootstrapConfig): Promise<Response>;
21
+ export declare function renderFallbackError(error: unknown, req: Request, responseHeaders: Headers, isDev: boolean, rootSegment: ManifestSegmentNode, clientBootstrap: ClientBootstrapConfig, globalError?: GlobalErrorFile): Promise<Response>;
21
22
  /**
22
23
  * Render a dev-mode 500 error page with error message and stack trace.
23
24
  *
@@ -1 +1 @@
1
- {"version":3,"file":"fallback-error.d.ts","sourceRoot":"","sources":["../../src/server/fallback-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAGxE;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,EACxB,KAAK,EAAE,OAAO,EACd,WAAW,EAAE,mBAAmB,EAChC,eAAe,EAAE,qBAAqB,GACrC,OAAO,CAAC,QAAQ,CAAC,CA4BnB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAmF3D"}
1
+ {"version":3,"file":"fallback-error.d.ts","sourceRoot":"","sources":["../../src/server/fallback-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAExE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AAE5E;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,EACxB,KAAK,EAAE,OAAO,EACd,WAAW,EAAE,mBAAmB,EAChC,eAAe,EAAE,qBAAqB,EACtC,WAAW,CAAC,EAAE,eAAe,GAC5B,OAAO,CAAC,QAAQ,CAAC,CA6BnB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAmF3D"}
@@ -46,6 +46,13 @@ export interface ManifestSegmentNode {
46
46
  export interface ManifestRoot {
47
47
  root: ManifestSegmentNode;
48
48
  proxy?: ManifestFile;
49
+ /**
50
+ * Global error page: app/global-error.{tsx,ts,jsx,js}
51
+ * Tier 2 — standalone full-page replacement (no layouts) when no
52
+ * segment-level error file is found. SSR-only render.
53
+ * See design/10-error-handling.md §"Tier 2"
54
+ */
55
+ globalError?: ManifestFile;
49
56
  }
50
57
  /**
51
58
  * Create a route matcher function from a manifest.
@@ -1 +1 @@
1
- {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/server/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAM9B,6DAA6D;AAC7D,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EACP,QAAQ,GACR,SAAS,GACT,WAAW,GACX,oBAAoB,GACpB,OAAO,GACP,MAAM,GACN,cAAc,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC3D,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAE9C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC5C;AAED,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAEzC;AA4MD,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,IAAI,EAAE,iBAAiB,CAAC;IACxB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,CAAC;IACnB,0DAA0D;IAC1D,OAAO,EAAE,mBAAmB,CAAC;IAC7B;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,kBAAkB,GAAG,IAAI,CAMjD"}
1
+ {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/server/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAM9B,6DAA6D;AAC7D,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EACP,QAAQ,GACR,SAAS,GACT,WAAW,GACX,oBAAoB,GACpB,OAAO,GACP,MAAM,GACN,cAAc,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC3D,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAE9C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC5C;AAED,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;CAC5B;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAEzC;AA4MD,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,IAAI,EAAE,iBAAiB,CAAC;IACxB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,CAAC;IACnB,0DAA0D;IAC1D,OAAO,EAAE,mBAAmB,CAAC;IAC7B;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,kBAAkB,GAAG,IAAI,CAMjD"}
@@ -15,6 +15,15 @@ import type { RouteMatch } from '#/server/pipeline.js';
15
15
  import type { ManifestSegmentNode } from '#/server/route-matcher.js';
16
16
  import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
17
17
  import type { LayoutEntry } from '#/server/deny-renderer.js';
18
+ /**
19
+ * A manifest file reference with lazy import and path.
20
+ * Mirrors the shape from ManifestSegmentNode but kept local to avoid
21
+ * exporting internal types from route-matcher.
22
+ */
23
+ export interface GlobalErrorFile {
24
+ load: () => Promise<unknown>;
25
+ filePath: string;
26
+ }
18
27
  /**
19
28
  * Render an error page for unhandled throws or RenderError outside Suspense.
20
29
  *
@@ -22,7 +31,7 @@ import type { LayoutEntry } from '#/server/deny-renderer.js';
22
31
  * - TSX/JSX: SSR-only (bypass RSC, render directly through Fizz)
23
32
  * - MDX: RSC → SSR (server components need Flight, but props are plain)
24
33
  */
25
- export declare function renderErrorPage(error: unknown, status: number, segments: ManifestSegmentNode[], layoutComponents: LayoutEntry[], req: Request, match: RouteMatch, responseHeaders: Headers, clientBootstrap: ClientBootstrapConfig): Promise<Response>;
34
+ export declare function renderErrorPage(error: unknown, status: number, segments: ManifestSegmentNode[], layoutComponents: LayoutEntry[], req: Request, match: RouteMatch, responseHeaders: Headers, clientBootstrap: ClientBootstrapConfig, globalError?: GlobalErrorFile): Promise<Response>;
26
35
  /**
27
36
  * Render a 404 page for URLs that don't match any route.
28
37
  *
@@ -1 +1 @@
1
- {"version":3,"file":"error-renderer.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/error-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAGxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AA4G7D;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,WAAW,EAAE,EAC/B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,EAAE,qBAAqB,GACrC,OAAO,CAAC,QAAQ,CAAC,CAuCnB;AA0GD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,WAAW,EAAE,mBAAmB,EAChC,eAAe,EAAE,OAAO,EACxB,eAAe,EAAE,qBAAqB,GACrC,OAAO,CAAC,QAAQ,CAAC,CA6BnB"}
1
+ {"version":3,"file":"error-renderer.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/error-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAGxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAO7D;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAuGD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,WAAW,EAAE,EAC/B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,EAAE,qBAAqB,EACtC,WAAW,CAAC,EAAE,eAAe,GAC5B,OAAO,CAAC,QAAQ,CAAC,CAoDnB;AAyLD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,WAAW,EAAE,mBAAmB,EAChC,eAAe,EAAE,OAAO,EACxB,eAAe,EAAE,qBAAqB,GACrC,OAAO,CAAC,QAAQ,CAAC,CA6BnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA4DA,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAiCtB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB,EAAE,KAAK,IAAI,GACtF,IAAI,CAEN;AAycD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;8BA1S3C,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA4ShD,wBAAiE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA4DA,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAiCtB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB,EAAE,KAAK,IAAI,GACtF,IAAI,CAEN;AAqdD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;8BA7S3C,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA+ShD,wBAAiE"}
@@ -15,6 +15,7 @@ import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
15
15
  import type { RouteMatch } from '#/server/pipeline.js';
16
16
  import type { LayoutComponentEntry } from '#/server/route-element-builder.js';
17
17
  import type { ManifestSegmentNode } from '#/server/route-matcher.js';
18
+ import { type GlobalErrorFile } from './error-renderer.js';
18
19
  import type { RenderSignals } from './rsc-stream.js';
19
20
  /**
20
21
  * Test-only observable: records how many microtask yields were consumed
@@ -40,6 +41,8 @@ interface SsrRenderOptions {
40
41
  clientJsDisabled: boolean;
41
42
  headHtml: string;
42
43
  deferSuspenseFor: number;
44
+ /** Tier 2 global-error.tsx file, if present in app/. */
45
+ globalError?: GlobalErrorFile;
43
46
  }
44
47
  /**
45
48
  * Render the RSC stream to HTML via SSR.
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-renderer.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/ssr-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAIxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAYrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAYrD;;;;;;;;GAQG;AACH,eAAO,MAAM,gCAAgC,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAiB,CAAC;AAEhF,UAAU,gBAAgB;IACxB,GAAG,EAAE,OAAO,CAAC;IACb,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACtC,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,qBAAqB,CAAC;IACvC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CA8OjF"}
1
+ {"version":3,"file":"ssr-renderer.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/ssr-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAIxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAUrE,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAYrD;;;;;;;;GAQG;AACH,eAAO,MAAM,gCAAgC,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAiB,CAAC;AAEhF,UAAU,gBAAgB;IACxB,GAAG,EAAE,OAAO,CAAC;IACb,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACtC,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,qBAAqB,CAAC;IACvC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,wDAAwD;IACxD,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkPjF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.56",
3
+ "version": "0.2.0-alpha.57",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -83,11 +83,6 @@
83
83
  "publishConfig": {
84
84
  "access": "public"
85
85
  },
86
- "scripts": {
87
- "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
88
- "typecheck": "tsgo --noEmit",
89
- "prepublishOnly": "pnpm run build"
90
- },
91
86
  "dependencies": {
92
87
  "@opentelemetry/api": "^1.9.1",
93
88
  "@opentelemetry/context-async-hooks": "^2.6.1",
@@ -126,5 +121,9 @@
126
121
  },
127
122
  "engines": {
128
123
  "node": ">=22.12.0"
124
+ },
125
+ "scripts": {
126
+ "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
127
+ "typecheck": "tsgo --noEmit"
129
128
  }
130
- }
129
+ }
package/src/cli.ts CHANGED
File without changes
@@ -16,6 +16,16 @@
16
16
 
17
17
  const RELOAD_FLAG_KEY = '__timber_stale_reload';
18
18
 
19
+ /**
20
+ * In-memory fallback counter for environments where sessionStorage is
21
+ * unavailable (private browsing, storage full, extension interference).
22
+ * Incremented each time triggerStaleReload() falls into the catch path.
23
+ * If the counter exceeds 0 on a subsequent call, the reload is suppressed
24
+ * to prevent an infinite loop. Resets naturally on page load (module
25
+ * re-evaluates) and can be manually reset via clearStaleReloadFlag().
26
+ */
27
+ let memoryReloadCount = 0;
28
+
19
29
  /**
20
30
  * Check if an error is a stale client reference error from React's
21
31
  * Flight client. These errors have the message pattern:
@@ -93,9 +103,22 @@ export function triggerStaleReload(): boolean {
93
103
  window.location.reload();
94
104
  return true;
95
105
  } catch {
96
- // sessionStorage may be unavailable (private browsing, storage full, etc.)
97
- // Fall back to reloading without loop protection
98
- console.warn('[timber] Stale client reference detected. Reloading page.');
106
+ // sessionStorage unavailable (private browsing, storage full, etc.)
107
+ // Use in-memory counter as fallback loop guard
108
+ if (memoryReloadCount > 0) {
109
+ console.warn(
110
+ '[timber] Stale client reference detected again after reload. ' +
111
+ 'Not reloading to prevent infinite loop. ' +
112
+ 'This may indicate a deployment issue — try a hard refresh.'
113
+ );
114
+ return false;
115
+ }
116
+
117
+ memoryReloadCount++;
118
+ console.warn(
119
+ '[timber] Stale client reference detected — the server has been ' +
120
+ 'redeployed with new bundles. Reloading to pick up the new version.'
121
+ );
99
122
  window.location.reload();
100
123
  return true;
101
124
  }
@@ -107,6 +130,7 @@ export function triggerStaleReload(): boolean {
107
130
  * reference error should trigger a fresh reload attempt.
108
131
  */
109
132
  export function clearStaleReloadFlag(): void {
133
+ memoryReloadCount = 0;
110
134
  try {
111
135
  sessionStorage.removeItem(RELOAD_FLAG_KEY);
112
136
  } catch {
@@ -423,6 +423,13 @@ function generateManifestModule(tree: RouteTree): string {
423
423
  proxyLine = ` proxy: { load: ${v}, filePath: ${JSON.stringify(tree.proxy.filePath)} },`;
424
424
  }
425
425
 
426
+ // Global error page (Tier 2)
427
+ let globalErrorLine = '';
428
+ if (tree.globalError) {
429
+ const v = addImport(tree.globalError);
430
+ globalErrorLine = ` globalError: { load: ${v}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
431
+ }
432
+
426
433
  // Interception rewrites — computed at build time from the route tree.
427
434
  // Only interceptedPattern and interceptingPrefix are needed at runtime.
428
435
  const rewrites = collectInterceptionRewrites(tree.root);
@@ -439,6 +446,7 @@ function generateManifestModule(tree: RouteTree): string {
439
446
  '',
440
447
  'const manifest = {',
441
448
  proxyLine,
449
+ globalErrorLine,
442
450
  rewritesLine,
443
451
  ` root: ${rootSerialized},`,
444
452
  '};',
@@ -83,6 +83,14 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
83
83
  tree.proxy = proxyFile;
84
84
  }
85
85
 
86
+ // Check for global-error.{tsx,ts,jsx,js} at app root.
87
+ // Tier 2 error page — renders standalone (no layouts) when no segment-level
88
+ // error file is found. See design/10-error-handling.md §"Tier 2".
89
+ const globalErrorFile = findPageExtFile(appDir, 'global-error', extSet);
90
+ if (globalErrorFile) {
91
+ tree.globalError = globalErrorFile;
92
+ }
93
+
86
94
  // Scan the root directory's files
87
95
  scanSegmentFiles(appDir, tree.root, extSet);
88
96
 
@@ -547,3 +555,25 @@ function findFixedFile(dirPath: string, name: string): RouteFile | undefined {
547
555
  }
548
556
  return undefined;
549
557
  }
558
+
559
+ /**
560
+ * Find a file using the configured page extensions (tsx, ts, jsx, js, mdx, etc.).
561
+ * Used for app-root conventions like global-error that aren't per-segment.
562
+ */
563
+ function findPageExtFile(
564
+ dirPath: string,
565
+ name: string,
566
+ extSet: Set<string>
567
+ ): RouteFile | undefined {
568
+ for (const ext of extSet) {
569
+ const fullPath = join(dirPath, `${name}.${ext}`);
570
+ try {
571
+ if (statSync(fullPath).isFile()) {
572
+ return { filePath: fullPath, extension: ext };
573
+ }
574
+ } catch {
575
+ // File doesn't exist
576
+ }
577
+ }
578
+ return undefined;
579
+ }
@@ -91,6 +91,16 @@ export interface RouteTree {
91
91
  root: SegmentNode;
92
92
  /** All discovered proxy.ts files (should be at most one, in app/) */
93
93
  proxy?: RouteFile;
94
+ /**
95
+ * Global error page: app/global-error.{tsx,ts,jsx,js}
96
+ *
97
+ * Rendered as a standalone full-page replacement (no layout wrapping)
98
+ * when no segment-level error file is found. SSR-only render path.
99
+ * Must provide its own <html> and <body>.
100
+ *
101
+ * See design/10-error-handling.md §"Tier 2 — Global Error Page"
102
+ */
103
+ globalError?: RouteFile;
94
104
  }
95
105
 
96
106
  /** Configuration passed to the scanner */
@@ -14,6 +14,7 @@ import type { RouteMatch } from '#/server/pipeline.js';
14
14
  import type { ManifestSegmentNode } from '#/server/route-matcher.js';
15
15
  import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
16
16
  import type { LayoutEntry } from '#/server/deny-renderer.js';
17
+ import type { GlobalErrorFile } from '#/server/rsc-entry/error-renderer.js';
17
18
 
18
19
  /**
19
20
  * Render a fallback error page when the render pipeline throws.
@@ -27,7 +28,8 @@ export async function renderFallbackError(
27
28
  responseHeaders: Headers,
28
29
  isDev: boolean,
29
30
  rootSegment: ManifestSegmentNode,
30
- clientBootstrap: ClientBootstrapConfig
31
+ clientBootstrap: ClientBootstrapConfig,
32
+ globalError?: GlobalErrorFile
31
33
  ): Promise<Response> {
32
34
  if (isDev) {
33
35
  return renderDevErrorPage(error);
@@ -54,7 +56,8 @@ export async function renderFallbackError(
54
56
  req,
55
57
  match,
56
58
  responseHeaders,
57
- clientBootstrap
59
+ clientBootstrap,
60
+ globalError
58
61
  );
59
62
  }
60
63
 
@@ -69,6 +69,13 @@ export interface ManifestSegmentNode {
69
69
  export interface ManifestRoot {
70
70
  root: ManifestSegmentNode;
71
71
  proxy?: ManifestFile;
72
+ /**
73
+ * Global error page: app/global-error.{tsx,ts,jsx,js}
74
+ * Tier 2 — standalone full-page replacement (no layouts) when no
75
+ * segment-level error file is found. SSR-only render.
76
+ * See design/10-error-handling.md §"Tier 2"
77
+ */
78
+ globalError?: ManifestFile;
72
79
  }
73
80
 
74
81
  // ─── Matcher ──────────────────────────────────────────────────────────────
@@ -29,6 +29,16 @@ import { getCookiesForSsr } from '#/server/request-context.js';
29
29
  import { callSsr } from './ssr-bridge.js';
30
30
  import { callSsrErrorPage } from './ssr-error-bridge.js';
31
31
 
32
+ /**
33
+ * A manifest file reference with lazy import and path.
34
+ * Mirrors the shape from ManifestSegmentNode but kept local to avoid
35
+ * exporting internal types from route-matcher.
36
+ */
37
+ export interface GlobalErrorFile {
38
+ load: () => Promise<unknown>;
39
+ filePath: string;
40
+ }
41
+
32
42
  /** MDX/markdown extensions — server components that need RSC rendering. */
33
43
  const MDX_EXTENSIONS = new Set(['.mdx', '.md']);
34
44
 
@@ -145,14 +155,28 @@ export async function renderErrorPage(
145
155
  req: Request,
146
156
  match: RouteMatch,
147
157
  responseHeaders: Headers,
148
- clientBootstrap: ClientBootstrapConfig
158
+ clientBootstrap: ClientBootstrapConfig,
159
+ globalError?: GlobalErrorFile
149
160
  ): Promise<Response> {
150
161
  const h = createElement as (...args: unknown[]) => React.ReactElement;
151
162
 
152
163
  // Walk segments from leaf to root to find the error component
153
164
  const resolution = await resolveErrorFile(status, segments);
154
165
 
155
- // Tier 3: No error component found bare response
166
+ // Tier 2: No segment-level error filetry global-error.tsx
167
+ if (!resolution && globalError) {
168
+ return renderGlobalErrorPage(
169
+ error,
170
+ status,
171
+ globalError,
172
+ req,
173
+ match,
174
+ responseHeaders,
175
+ clientBootstrap
176
+ );
177
+ }
178
+
179
+ // Tier 3: No error component found at any level — bare response
156
180
  if (!resolution) {
157
181
  return new Response(null, { status, headers: responseHeaders });
158
182
  }
@@ -291,6 +315,85 @@ async function renderErrorPageViaMdx(
291
315
  return callSsr(ssrStream, navContext);
292
316
  }
293
317
 
318
+ /**
319
+ * Tier 2 — Render global-error.tsx as a standalone full-page replacement.
320
+ *
321
+ * No layout wrapping. The component must provide its own <html> and <body>.
322
+ * Uses SSR-only render (bypasses RSC Flight) since global-error.tsx is
323
+ * a 'use client' component receiving { error, digest, reset }.
324
+ *
325
+ * MDX global-error files go through RSC → SSR with plain props.
326
+ *
327
+ * See design/10-error-handling.md §"Tier 2 — Global Error Page"
328
+ */
329
+ async function renderGlobalErrorPage(
330
+ error: unknown,
331
+ status: number,
332
+ globalError: GlobalErrorFile,
333
+ req: Request,
334
+ match: RouteMatch,
335
+ responseHeaders: Headers,
336
+ clientBootstrap: ClientBootstrapConfig
337
+ ): Promise<Response> {
338
+ const h = createElement as (...args: unknown[]) => React.ReactElement;
339
+
340
+ if (isMdxFile(globalError.filePath)) {
341
+ // MDX global-error: RSC → SSR with plain props (no Error object)
342
+ const mod = (await globalError.load()) as Record<string, unknown>;
343
+ if (!mod.default) {
344
+ return new Response(null, { status, headers: responseHeaders });
345
+ }
346
+ const component = mod.default as (...args: unknown[]) => unknown;
347
+ const element = h(component, { status });
348
+
349
+ const rscStream = renderToReadableStream(element, {
350
+ onError(err: unknown) {
351
+ logRenderError({ method: req.method, path: new URL(req.url).pathname, error: err });
352
+ },
353
+ debugChannel: createDebugChannelSink(),
354
+ });
355
+
356
+ const [ssrStream, inlineStream] = rscStream.tee();
357
+
358
+ const navContext: NavContext = {
359
+ pathname: new URL(req.url).pathname,
360
+ params: match.params,
361
+ searchParams: Object.fromEntries(new URL(req.url).searchParams),
362
+ statusCode: status,
363
+ responseHeaders,
364
+ headHtml: flightInitScript(),
365
+ bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
366
+ rscStream: inlineStream,
367
+ cookies: getCookiesForSsr(),
368
+ };
369
+
370
+ return callSsr(ssrStream, navContext);
371
+ }
372
+
373
+ // TSX/JSX global-error: SSR-only render, no layouts
374
+ const digest =
375
+ error instanceof RenderError ? { code: error.code, data: error.digest.data } : null;
376
+
377
+ return callSsrErrorPage({
378
+ pathname: new URL(req.url).pathname,
379
+ params: match.params,
380
+ searchParams: Object.fromEntries(new URL(req.url).searchParams),
381
+ statusCode: status,
382
+ responseHeaders,
383
+ headHtml: '',
384
+ bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
385
+ signal: req.signal,
386
+ cookies: getCookiesForSsr(),
387
+ errorProps: {
388
+ error: error instanceof Error ? error : new Error(String(error)),
389
+ digest,
390
+ reset: undefined,
391
+ },
392
+ errorComponentPath: globalError.filePath,
393
+ layoutPaths: [], // No layouts — global-error is standalone
394
+ });
395
+ }
396
+
294
397
  /**
295
398
  * Render a 404 page for URLs that don't match any route.
296
399
  *
@@ -234,7 +234,8 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
234
234
  clientBootstrap,
235
235
  clientJsDisabled,
236
236
  interception,
237
- manifest.root
237
+ manifest.root,
238
+ manifest.globalError
238
239
  );
239
240
  },
240
241
  renderNoMatch: async (req: Request, responseHeaders: Headers) => {
@@ -259,7 +260,15 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
259
260
  }
260
261
  : undefined,
261
262
  renderFallbackError: (error, req, responseHeaders) =>
262
- renderFallback(error, req, responseHeaders, isDev, manifest.root, clientBootstrap),
263
+ renderFallback(
264
+ error,
265
+ req,
266
+ responseHeaders,
267
+ isDev,
268
+ manifest.root,
269
+ clientBootstrap,
270
+ manifest.globalError
271
+ ),
263
272
  };
264
273
 
265
274
  const pipeline = createPipeline(pipelineConfig);
@@ -339,7 +348,8 @@ async function renderRoute(
339
348
  clientBootstrap: ClientBootstrapConfig,
340
349
  clientJsDisabled: boolean,
341
350
  interception?: InterceptionContext,
342
- rootSegment?: ManifestSegmentNode
351
+ rootSegment?: ManifestSegmentNode,
352
+ globalError?: { load: () => Promise<unknown>; filePath: string }
343
353
  ): Promise<Response> {
344
354
  const segments = match.segments as unknown as ManifestSegmentNode[];
345
355
  const leaf = segments[segments.length - 1];
@@ -529,7 +539,8 @@ async function renderRoute(
529
539
  _req,
530
540
  match,
531
541
  responseHeaders,
532
- clientBootstrap
542
+ clientBootstrap,
543
+ globalError
533
544
  );
534
545
  }
535
546
 
@@ -563,6 +574,7 @@ async function renderRoute(
563
574
  clientJsDisabled,
564
575
  headHtml,
565
576
  deferSuspenseFor,
577
+ globalError,
566
578
  });
567
579
  }
568
580
 
@@ -29,7 +29,7 @@ import {
29
29
  isAbortError,
30
30
  } from './helpers.js';
31
31
  import { getCookiesForSsr } from '#/server/request-context.js';
32
- import { renderErrorPage } from './error-renderer.js';
32
+ import { renderErrorPage, type GlobalErrorFile } from './error-renderer.js';
33
33
  import { callSsr } from './ssr-bridge.js';
34
34
  import type { RenderSignals } from './rsc-stream.js';
35
35
  import { recordTiming } from '#/server/server-timing.js';
@@ -66,6 +66,8 @@ interface SsrRenderOptions {
66
66
  clientJsDisabled: boolean;
67
67
  headHtml: string;
68
68
  deferSuspenseFor: number;
69
+ /** Tier 2 global-error.tsx file, if present in app/. */
70
+ globalError?: GlobalErrorFile;
69
71
  }
70
72
 
71
73
  /**
@@ -99,6 +101,7 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
99
101
  clientJsDisabled,
100
102
  headHtml,
101
103
  deferSuspenseFor,
104
+ globalError,
102
105
  } = opts;
103
106
 
104
107
  // Tee the RSC stream — one copy goes to SSR for HTML rendering,
@@ -176,7 +179,8 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
176
179
  req,
177
180
  match,
178
181
  responseHeaders,
179
- clientBootstrap
182
+ clientBootstrap,
183
+ globalError
180
184
  );
181
185
  }
182
186
  return null;
@@ -298,7 +302,8 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
298
302
  req,
299
303
  match,
300
304
  responseHeaders,
301
- clientBootstrap
305
+ clientBootstrap,
306
+ globalError
302
307
  );
303
308
  }
304
309
  // No captured signal — unhandled error in the RSC stream.
@@ -312,7 +317,8 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
312
317
  req,
313
318
  match,
314
319
  responseHeaders,
315
- clientBootstrap
320
+ clientBootstrap,
321
+ globalError
316
322
  );
317
323
  }
318
324