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

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 (68) hide show
  1. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -1
  2. package/dist/_chunks/define-D5STJpIr.js +121 -0
  3. package/dist/_chunks/define-D5STJpIr.js.map +1 -0
  4. package/dist/_chunks/{define-cookie-k9btcEfI.js → define-cookie-DtAavax4.js} +4 -4
  5. package/dist/_chunks/{define-cookie-k9btcEfI.js.map → define-cookie-DtAavax4.js.map} +1 -1
  6. package/dist/_chunks/{error-boundary-B9vT_YK_.js → error-boundary-DpZJBCqh.js} +1 -1
  7. package/dist/_chunks/{error-boundary-B9vT_YK_.js.map → error-boundary-DpZJBCqh.js.map} +1 -1
  8. package/dist/_chunks/{request-context-0h-6Voad.js → request-context-0wfZsnhh.js} +3 -1
  9. package/dist/_chunks/request-context-0wfZsnhh.js.map +1 -0
  10. package/dist/_chunks/{segment-context-Bmugn-ao.js → segment-context-CyaM1mrD.js} +1 -1
  11. package/dist/_chunks/{segment-context-Bmugn-ao.js.map → segment-context-CyaM1mrD.js.map} +1 -1
  12. package/dist/_chunks/{stale-reload-Db4wqE46.js → stale-reload-DKN3aXxR.js} +1 -1
  13. package/dist/_chunks/{stale-reload-Db4wqE46.js.map → stale-reload-DKN3aXxR.js.map} +1 -1
  14. package/dist/_chunks/{tracing-JI4cYUdz.js → tracing-VYETCQsg.js} +1 -1
  15. package/dist/_chunks/{tracing-JI4cYUdz.js.map → tracing-VYETCQsg.js.map} +1 -1
  16. package/dist/_chunks/{wrappers-C9XPg7-U.js → wrappers-BaG1bnM3.js} +1 -1
  17. package/dist/_chunks/{wrappers-C9XPg7-U.js.map → wrappers-BaG1bnM3.js.map} +1 -1
  18. package/dist/cache/index.js +1 -1
  19. package/dist/client/error-boundary.js +1 -1
  20. package/dist/client/error-reconstituter.d.ts +54 -0
  21. package/dist/client/error-reconstituter.d.ts.map +1 -0
  22. package/dist/client/index.js +4 -4
  23. package/dist/client/segment-outlet.d.ts +63 -0
  24. package/dist/client/segment-outlet.d.ts.map +1 -0
  25. package/dist/cookies/index.js +1 -1
  26. package/dist/index.d.ts +15 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +172 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/params/define.d.ts +24 -0
  31. package/dist/params/define.d.ts.map +1 -1
  32. package/dist/params/index.js +2 -103
  33. package/dist/plugins/dev-browser-logs.d.ts +84 -0
  34. package/dist/plugins/dev-browser-logs.d.ts.map +1 -0
  35. package/dist/search-params/index.js +1 -1
  36. package/dist/server/als-registry.d.ts +7 -0
  37. package/dist/server/als-registry.d.ts.map +1 -1
  38. package/dist/server/deny-renderer.d.ts.map +1 -1
  39. package/dist/server/index.js +4 -4
  40. package/dist/server/request-context.d.ts.map +1 -1
  41. package/dist/server/rsc-entry/error-renderer.d.ts +9 -9
  42. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  43. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  44. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  45. package/dist/server/stream-utils.d.ts +36 -0
  46. package/dist/server/stream-utils.d.ts.map +1 -0
  47. package/package.json +1 -1
  48. package/src/client/browser-entry.ts +6 -7
  49. package/src/client/error-reconstituter.tsx +65 -0
  50. package/src/client/segment-outlet.tsx +86 -0
  51. package/src/index.ts +17 -0
  52. package/src/params/define.ts +60 -0
  53. package/src/plugins/dev-browser-logs.ts +274 -0
  54. package/src/server/als-registry.ts +7 -0
  55. package/src/server/deny-renderer.ts +2 -1
  56. package/src/server/request-context.ts +6 -0
  57. package/src/server/rsc-entry/error-renderer.ts +108 -173
  58. package/src/server/rsc-entry/index.ts +14 -10
  59. package/src/server/rsc-entry/ssr-renderer.ts +5 -1
  60. package/src/server/stream-utils.ts +209 -0
  61. package/dist/_chunks/request-context-0h-6Voad.js.map +0 -1
  62. package/dist/params/index.js.map +0 -1
  63. package/dist/server/rsc-entry/ssr-error-bridge.d.ts +0 -12
  64. package/dist/server/rsc-entry/ssr-error-bridge.d.ts.map +0 -1
  65. package/dist/server/ssr-error-entry.d.ts +0 -65
  66. package/dist/server/ssr-error-entry.d.ts.map +0 -1
  67. package/src/server/rsc-entry/ssr-error-bridge.ts +0 -20
  68. package/src/server/ssr-error-entry.ts +0 -237
@@ -1 +0,0 @@
1
- {"version":3,"file":"request-context-0h-6Voad.js","names":[],"sources":["../../src/server/request-context.ts"],"sourcesContent":["/**\n * Request Context — per-request ALS store for headers() and cookies().\n *\n * Follows the same pattern as tracing.ts: a module-level AsyncLocalStorage\n * instance, public accessor functions that throw outside request scope,\n * and a framework-internal `runWithRequestContext()` to establish scope.\n *\n * See design/04-authorization.md §\"AccessContext does not include cookies or headers\"\n * and design/11-platform.md §\"AsyncLocalStorage\".\n * See design/29-cookies.md for cookie mutation semantics.\n */\n\nimport { requestContextAls, type RequestContextStore, type CookieEntry } from './als-registry.js';\nimport { isDebug } from './debug.js';\nimport { _setRawSearchParamsFn } from '#/search-params/define.js';\n\n// Re-export the ALS for framework-internal consumers that need direct access.\nexport { requestContextAls };\n\n// No fallback needed — we use enterWith() instead of run() to ensure\n// the ALS context persists for the entire request lifecycle including\n// async stream consumption by React's renderToReadableStream.\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns a read-only view of the current request's headers.\n *\n * Available in middleware, access checks, server components, and server actions.\n * Throws if called outside a request context (security principle #2: no global fallback).\n */\nexport function headers(): ReadonlyHeaders {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] headers() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n return store.headers;\n}\n\n/**\n * Returns a cookie accessor for the current request.\n *\n * Available in middleware, access checks, server components, and server actions.\n * Throws if called outside a request context (security principle #2: no global fallback).\n *\n * Read methods (.get, .has, .getAll) are always available and reflect\n * read-your-own-writes from .set() calls in the same request.\n *\n * Mutation methods (.set, .delete, .clear) are only available in mutable\n * contexts (middleware.ts, server actions, route.ts handlers). Calling them\n * in read-only contexts (access.ts, server components) throws.\n *\n * See design/29-cookies.md\n */\nexport function cookies(): RequestCookies {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] cookies() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n\n // Parse cookies lazily on first access\n if (!store.parsedCookies) {\n store.parsedCookies = parseCookieHeader(store.cookieHeader);\n }\n\n const map = store.parsedCookies;\n return {\n get(name: string): string | undefined {\n return map.get(name);\n },\n has(name: string): boolean {\n return map.has(name);\n },\n getAll(): Array<{ name: string; value: string }> {\n return Array.from(map.entries()).map(([name, value]) => ({ name, value }));\n },\n get size(): number {\n return map.size;\n },\n\n set(name: string, value: string, options?: CookieOptions): void {\n assertMutable(store, 'set');\n if (store.flushed) {\n if (isDebug()) {\n console.warn(\n `[timber] warn: cookies().set('${name}') called after response headers were committed.\\n` +\n ` The cookie will NOT be sent. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n }\n return;\n }\n const opts = { ...DEFAULT_COOKIE_OPTIONS, ...options };\n store.cookieJar.set(name, { name, value, options: opts });\n // Read-your-own-writes: update the parsed cookies map\n map.set(name, value);\n },\n\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void {\n assertMutable(store, 'delete');\n if (store.flushed) {\n if (isDebug()) {\n console.warn(\n `[timber] warn: cookies().delete('${name}') called after response headers were committed.\\n` +\n ` The cookie will NOT be deleted. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n }\n return;\n }\n const opts: CookieOptions = {\n ...DEFAULT_COOKIE_OPTIONS,\n ...options,\n maxAge: 0,\n expires: new Date(0),\n };\n store.cookieJar.set(name, { name, value: '', options: opts });\n // Remove from read view\n map.delete(name);\n },\n\n clear(): void {\n assertMutable(store, 'clear');\n if (store.flushed) return;\n // Delete every incoming cookie\n for (const name of Array.from(map.keys())) {\n store.cookieJar.set(name, {\n name,\n value: '',\n options: { ...DEFAULT_COOKIE_OPTIONS, maxAge: 0, expires: new Date(0) },\n });\n }\n map.clear();\n },\n\n toString(): string {\n return Array.from(map.entries())\n .map(([name, value]) => `${name}=${value}`)\n .join('; ');\n },\n };\n}\n\n/**\n * Returns a Promise resolving to the current request's raw URLSearchParams.\n *\n * For typed, parsed search params, import the definition from params.ts\n * and call `.load()` or `.parse()`:\n *\n * ```ts\n * import { searchParams } from './params'\n * const parsed = await searchParams.load()\n * ```\n *\n * Or explicitly:\n *\n * ```ts\n * import { rawSearchParams } from '@timber-js/app/server'\n * import { searchParams } from './params'\n * const parsed = searchParams.parse(await rawSearchParams())\n * ```\n *\n * Throws if called outside a request context.\n */\nexport function rawSearchParams(): Promise<URLSearchParams> {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] rawSearchParams() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n return store.searchParamsPromise;\n}\n\n// Eagerly register rawSearchParams with the search-params module so\n// searchParams.load() can call it synchronously without a dynamic import.\n// Dynamic imports lose ALS context in React's RSC Flight renderer,\n// breaking rawSearchParams() in parallel slot pages. See TIM-523.\n_setRawSearchParamsFn(rawSearchParams);\n\n/**\n * Returns a Promise resolving to the current request's coerced segment params.\n *\n * Segment params are set by the pipeline after route matching and param\n * coercion (via params.ts codecs). When no params.ts exists, values are\n * raw strings. When codecs are defined, values are already coerced\n * (e.g., `id` is a `number` if `defineSegmentParams({ id: z.coerce.number() })`).\n *\n * This is the primary way page and layout components access route params:\n *\n * ```ts\n * import { rawSegmentParams } from '@timber-js/app/server'\n *\n * export default async function Page() {\n * const { slug } = await rawSegmentParams()\n * // ...\n * }\n * ```\n *\n * Throws if called outside a request context.\n */\nexport function rawSegmentParams(): Promise<Record<string, string | string[]>> {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] rawSegmentParams() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n if (!store.segmentParamsPromise) {\n throw new Error(\n '[timber] rawSegmentParams() called before route matching completed. ' +\n 'Segment params are not available until after the route is matched.'\n );\n }\n return store.segmentParamsPromise;\n}\n\n/**\n * Set the segment params promise on the current request context.\n * Called by the pipeline after route matching and param coercion.\n *\n * @internal — framework use only\n */\nexport function setSegmentParams(params: Record<string, string | string[]>): void {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] setSegmentParams() called outside of a request context.');\n }\n store.segmentParamsPromise = Promise.resolve(params);\n}\n\n/**\n * Returns the raw search string from the current request URL (e.g. \"?foo=bar\").\n * Synchronous — safe for use in `redirect()` which throws synchronously.\n *\n * Returns empty string if called outside a request context (non-throwing for\n * use in redirect's optional preserveSearchParams path).\n *\n * @internal — used by redirect() for preserveSearchParams support.\n */\nexport function getRequestSearchString(): string {\n const store = requestContextAls.getStore();\n return store?.searchString ?? '';\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\n/**\n * Read-only Headers interface. The standard Headers class is mutable;\n * this type narrows it to read-only methods. The underlying object is\n * still a Headers instance, but user code should not mutate it.\n */\nexport type ReadonlyHeaders = Pick<\n Headers,\n 'get' | 'has' | 'entries' | 'keys' | 'values' | 'forEach' | typeof Symbol.iterator\n>;\n\n/** Options for setting a cookie. See design/29-cookies.md. */\nexport interface CookieOptions {\n /** Domain scope. Default: omitted (current domain only). */\n domain?: string;\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Expiration date. Mutually exclusive with maxAge. */\n expires?: Date;\n /** Max age in seconds. Mutually exclusive with expires. */\n maxAge?: number;\n /** Prevent client-side JS access. Default: true. */\n httpOnly?: boolean;\n /** Only send over HTTPS. Default: true. */\n secure?: boolean;\n /** Cross-site request policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Partitioned (CHIPS) — isolate cookie per top-level site. Default: false. */\n partitioned?: boolean;\n}\n\nconst DEFAULT_COOKIE_OPTIONS: CookieOptions = {\n path: '/',\n httpOnly: true,\n secure: true,\n sameSite: 'lax',\n};\n\n/**\n * Cookie accessor returned by `cookies()`.\n *\n * Read methods are always available. Mutation methods throw in read-only\n * contexts (access.ts, server components).\n */\nexport interface RequestCookies {\n /** Get a cookie value by name. Returns undefined if not present. */\n get(name: string): string | undefined;\n /** Check if a cookie exists. */\n has(name: string): boolean;\n /** Get all cookies as an array of { name, value } pairs. */\n getAll(): Array<{ name: string; value: string }>;\n /** Number of cookies. */\n readonly size: number;\n /** Set a cookie. Only available in mutable contexts (middleware, actions, route handlers). */\n set(name: string, value: string, options?: CookieOptions): void;\n /** Delete a cookie. Only available in mutable contexts. */\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void;\n /** Delete all cookies. Only available in mutable contexts. */\n clear(): void;\n /** Serialize cookies as a Cookie header string. */\n toString(): string;\n}\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Run a callback within a request context. Used by the pipeline to establish\n * per-request ALS scope so that `headers()` and `cookies()` work.\n *\n * @param req - The incoming Request object.\n * @param fn - The function to run within the request context.\n */\nexport function runWithRequestContext<T>(req: Request, fn: () => T): T {\n const originalCopy = new Headers(req.headers);\n const parsedUrl = new URL(req.url);\n const store: RequestContextStore = {\n headers: freezeHeaders(req.headers),\n originalHeaders: originalCopy,\n cookieHeader: req.headers.get('cookie') ?? '',\n searchParamsPromise: Promise.resolve(parsedUrl.searchParams),\n searchString: parsedUrl.search,\n cookieJar: new Map(),\n flushed: false,\n mutableContext: false,\n };\n return requestContextAls.run(store, fn);\n}\n\n/**\n * Enable cookie mutation for the current context. Called by the framework\n * when entering middleware.ts, server actions, or route.ts handlers.\n *\n * See design/29-cookies.md §\"Context Tracking\"\n */\nexport function setMutableCookieContext(mutable: boolean): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.mutableContext = mutable;\n }\n}\n\n/**\n * Mark the response as flushed (headers committed). After this point,\n * cookie mutations log a warning instead of throwing.\n *\n * See design/29-cookies.md §\"Streaming Constraint: Post-Flush Cookie Warning\"\n */\nexport function markResponseFlushed(): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.flushed = true;\n }\n}\n\n/**\n * Build a Map of cookie name → value reflecting the current request's\n * read-your-own-writes state. Includes incoming cookies plus any\n * mutations from cookies().set() / cookies().delete() in the same request.\n *\n * Used by SSR renderers to populate NavContext.cookies so that\n * useCookie()'s server snapshot matches the actual response state.\n *\n * See design/29-cookies.md §\"Read-Your-Own-Writes\"\n * See design/triage/TIM-441-cookie-api-triage.md §4\n */\nexport function getCookiesForSsr(): Map<string, string> {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] getCookiesForSsr() called outside of a request context.');\n }\n\n // Trigger lazy parsing if not yet done\n if (!store.parsedCookies) {\n store.parsedCookies = parseCookieHeader(store.cookieHeader);\n }\n\n // The parsedCookies map already reflects read-your-own-writes:\n // - cookies().set() updates the map via map.set(name, value)\n // - cookies().delete() removes from the map via map.delete(name)\n // Return a copy so callers can't mutate the internal map.\n return new Map(store.parsedCookies);\n}\n\n/**\n * Collect all Set-Cookie headers from the cookie jar.\n * Called by the framework at flush time to apply cookies to the response.\n *\n * Returns an array of serialized Set-Cookie header values.\n */\nexport function getSetCookieHeaders(): string[] {\n const store = requestContextAls.getStore();\n if (!store) return [];\n return Array.from(store.cookieJar.values()).map(serializeCookieEntry);\n}\n\n/**\n * Apply middleware-injected request headers to the current request context.\n *\n * Called by the pipeline after middleware.ts runs. Merges overlay headers\n * on top of the original request headers so downstream code (access.ts,\n * server components, server actions) sees them via `headers()`.\n *\n * The original request headers are never mutated — a new frozen Headers\n * object is created with the overlay applied on top.\n *\n * See design/07-routing.md §\"Request Header Injection\"\n */\nexport function applyRequestHeaderOverlay(overlay: Headers): void {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] applyRequestHeaderOverlay() called outside of a request context.');\n }\n\n // Check if the overlay has any headers — skip if empty\n let hasOverlay = false;\n overlay.forEach(() => {\n hasOverlay = true;\n });\n if (!hasOverlay) return;\n\n // Merge: start with original headers, overlay on top\n const merged = new Headers(store.originalHeaders);\n overlay.forEach((value, key) => {\n merged.set(key, value);\n });\n store.headers = freezeHeaders(merged);\n}\n\n// ─── Read-Only Headers ────────────────────────────────────────────────────\n\nconst MUTATING_METHODS = new Set(['set', 'append', 'delete']);\n\n/**\n * Wrap a Headers object in a Proxy that throws on mutating methods.\n * Object.freeze doesn't work on Headers (native internal slots), so we\n * intercept property access and reject set/append/delete at runtime.\n *\n * Read methods (get, has, entries, etc.) must be bound to the underlying\n * Headers instance because they access private #headersList slots.\n */\nfunction freezeHeaders(source: Headers): Headers {\n const copy = new Headers(source);\n return new Proxy(copy, {\n get(target, prop) {\n if (typeof prop === 'string' && MUTATING_METHODS.has(prop)) {\n return () => {\n throw new Error(\n `[timber] headers() returns a read-only Headers object. ` +\n `Calling .${prop}() is not allowed. ` +\n `Use ctx.requestHeaders in middleware to inject headers for downstream components.`\n );\n };\n }\n const value = Reflect.get(target, prop);\n // Bind methods to the real Headers instance so private slot access works\n if (typeof value === 'function') {\n return value.bind(target);\n }\n return value;\n },\n });\n}\n\n// ─── Cookie Helpers ───────────────────────────────────────────────────────\n\n/** Throw if cookie mutation is attempted in a read-only context. */\nfunction assertMutable(store: RequestContextStore, method: string): void {\n if (!store.mutableContext) {\n throw new Error(\n `[timber] cookies().${method}() cannot be called in this context.\\n` +\n ` Set cookies in middleware.ts, server actions, or route.ts handlers.`\n );\n }\n}\n\n/**\n * Parse a Cookie header string into a Map of name → value pairs.\n * Follows RFC 6265 §4.2.1: cookies are semicolon-separated key=value pairs.\n */\nfunction parseCookieHeader(header: string): Map<string, string> {\n const map = new Map<string, string>();\n if (!header) return map;\n\n for (const pair of header.split(';')) {\n const eqIndex = pair.indexOf('=');\n if (eqIndex === -1) continue;\n const name = pair.slice(0, eqIndex).trim();\n const value = pair.slice(eqIndex + 1).trim();\n if (name) {\n map.set(name, value);\n }\n }\n\n return map;\n}\n\n/** Serialize a CookieEntry into a Set-Cookie header value. */\nfunction serializeCookieEntry(entry: CookieEntry): string {\n const parts = [`${entry.name}=${entry.value}`];\n const opts = entry.options;\n\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.path) parts.push(`Path=${opts.path}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.httpOnly) parts.push('HttpOnly');\n if (opts.secure) parts.push('Secure');\n if (opts.sameSite) {\n parts.push(`SameSite=${opts.sameSite.charAt(0).toUpperCase()}${opts.sameSite.slice(1)}`);\n }\n if (opts.partitioned) parts.push('Partitioned');\n\n return parts.join('; ');\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/params/define.ts"],"sourcesContent":["/**\n * defineParams — factory for typed route param coercion.\n *\n * Creates a ParamsDefinition that coerces raw string params from the\n * URL into typed values. Used by exporting from layout.tsx (segment-level)\n * or page.tsx (fallback).\n *\n * Reuses the shared Codec<T> protocol with Standard Schema auto-detection,\n * same pattern as defineSearchParams. Runtime constraints are stricter:\n * - serialize must return string (not null — path segments can't be omitted)\n * - parse throwing → 404 (invalid param value)\n *\n * Design doc: design/07a-route-params-triage.md\n */\n\nimport type { Codec } from '#/codec.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Infer the output type from a Codec or StandardSchemaV1. */\nexport type InferParamField<V> =\n V extends Codec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;\n\n/** Acceptable field value for defineParams: a Codec or a Standard Schema. */\nexport type ParamField<T = unknown> = Codec<T> | StandardSchemaV1<T>;\n\n/**\n * A typed route params definition.\n *\n * Returned by defineParams(). Provides parse (string → typed) and\n * serialize (typed → string) for each declared param.\n */\nexport interface ParamsDefinition<T extends Record<string, unknown>> {\n /** Parse raw string params into typed values. Throws on invalid values. */\n parse(raw: Record<string, string | string[]>): T;\n\n /** Serialize typed values back to strings for URL construction. */\n serialize(values: T): Record<string, string>;\n\n /** Read-only codec map. */\n codecs: { [K in keyof T]: Codec<T[K]> };\n}\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset — same as in search-params/define.ts)\n// ---------------------------------------------------------------------------\n\ninterface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(\n value: unknown\n ):\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n | Promise<\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n >;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction isStandardSchema(value: unknown): value is StandardSchemaV1 {\n return (\n typeof value === 'object' &&\n value !== null &&\n '~standard' in value &&\n typeof (value as StandardSchemaV1)['~standard']?.validate === 'function'\n );\n}\n\nfunction isCodec(value: unknown): value is Codec<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as Codec<unknown>).parse === 'function' &&\n typeof (value as Codec<unknown>).serialize === 'function'\n );\n}\n\n/**\n * Validate sync for Standard Schema (same helper as search-params/codecs.ts).\n */\nfunction validateSync<Output>(\n schema: StandardSchemaV1<Output>,\n value: unknown\n):\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> } {\n const result = schema['~standard'].validate(value);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] defineParams: schema returned a Promise — only sync schemas are supported.'\n );\n }\n return result;\n}\n\n/**\n * Wrap a Standard Schema into a Codec for route params.\n *\n * Unlike fromSchema for search params:\n * - Parse throws on failure (no fallback to default)\n * - Serialize returns string (not null)\n */\nfunction fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // Route params are always strings (single segment) or string[] (catch-all)\n const input = Array.isArray(value) ? value : value;\n\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // For route params, parse failure means the param is invalid → throw\n const messages = result.issues.map((i) => i.message).join(', ');\n throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n // Catch-all segments produce arrays — join with '/' for path reconstruction\n if (Array.isArray(value)) {\n return value.join('/');\n }\n return String(value);\n },\n };\n}\n\n/**\n * Resolve a field value to a Codec. Auto-detects Standard Schema objects.\n */\nfunction resolveField(fieldName: string, value: ParamField): Codec<unknown> {\n if (isCodec(value)) {\n return value;\n }\n\n if (isStandardSchema(value)) {\n return fromParamSchema(fieldName, value);\n }\n\n throw new Error(\n `[timber] defineParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n/**\n * Validate that no codec's serialize returns null.\n * Route params are structural — they must produce a valid path segment.\n */\nfunction validateSerialize(codecMap: Record<string, Codec<unknown>>): void {\n for (const [key, codec] of Object.entries(codecMap)) {\n // Test serialize with a sample parsed value to check for null\n // We can't exhaustively test, but we can check that serialize(parse(\"test\"))\n // doesn't return null for a basic input.\n try {\n const testValue = codec.parse('test');\n const serialized = codec.serialize(testValue);\n if (serialized === null) {\n throw new Error(\n `[timber] defineParams: field '${key}' codec.serialize() returned null.\\n` +\n ` Route params are path segments — they cannot be omitted.\\n` +\n ` Ensure serialize() always returns a string.`\n );\n }\n } catch (e) {\n // parse('test') may throw for strict codecs (e.g., number-only).\n // That's fine — it means the codec validates. We only care about\n // serialize returning null, which we can't test without a valid value.\n if (e instanceof Error && e.message.includes('returned null')) {\n throw e;\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a ParamsDefinition from a map of codecs and/or Standard Schema objects.\n *\n * ```ts\n * // app/products/[id]/layout.tsx\n * import { defineParams } from '@timber-js/app/params'\n * import { z } from 'zod/v4'\n *\n * export const params = defineParams({\n * id: z.coerce.number().int().positive(),\n * })\n *\n * export default function Layout({ children }) { return children }\n * ```\n */\nexport function defineSegmentParams<C extends Record<string, ParamField>>(\n codecs: C\n): ParamsDefinition<{ [K in keyof C]: InferParamField<C[K]> }> {\n type T = { [K in keyof C]: InferParamField<C[K]> };\n\n const resolvedCodecs: Record<string, Codec<unknown>> = {};\n\n for (const [key, value] of Object.entries(codecs)) {\n resolvedCodecs[key] = resolveField(key, value as ParamField);\n }\n\n // Validate that serialize doesn't return null\n validateSerialize(resolvedCodecs);\n\n // ---- parse ----\n function parse(raw: Record<string, string | string[]>): T {\n const result: Record<string, unknown> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const rawValue = raw[key];\n // Route params are always present (the route matched)\n result[key] = codec.parse(rawValue);\n }\n\n return result as T;\n }\n\n // ---- serialize ----\n function serialize(values: T): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const serialized = codec.serialize(values[key as keyof T] as unknown);\n if (serialized === null) {\n throw new Error(\n `[timber] params.serialize: field '${key}' serialized to null. ` +\n `Route params must produce a valid path segment.`\n );\n }\n result[key] = serialized;\n }\n\n return result;\n }\n\n const definition: ParamsDefinition<T> = {\n parse,\n serialize,\n codecs: resolvedCodecs as { [K in keyof T]: Codec<T[K]> },\n };\n\n return definition;\n}\n"],"mappings":";;;AAmEA,SAAS,iBAAiB,OAA2C;AACnE,QACE,OAAO,UAAU,YACjB,UAAU,QACV,eAAe,SACf,OAAQ,MAA2B,cAAc,aAAa;;AAIlE,SAAS,QAAQ,OAAyC;AACxD,QACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAAyB,UAAU,cAC3C,OAAQ,MAAyB,cAAc;;;;;AAOnD,SAAS,aACP,QACA,OAGoE;CACpE,MAAM,SAAS,OAAO,aAAa,SAAS,MAAM;AAClD,KAAI,kBAAkB,QACpB,OAAM,IAAI,MACR,sFACD;AAEH,QAAO;;;;;;;;;AAUT,SAAS,gBAAmB,WAAmB,QAAuC;AACpF,QAAO;EACL,MAAM,OAAyC;GAI7C,MAAM,SAAS,aAAa,QAFd,MAAM,QAAQ,MAAM,GAAG,QAAQ,MAEH;AAC1C,OAAI,CAAC,OAAO,OACV,QAAO,OAAO;GAIhB,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;AAC/D,SAAM,IAAI,MAAM,mBAAmB,UAAU,qBAAqB,WAAW;;EAG/E,UAAU,OAAyB;AACjC,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;AAGT,OAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,KAAK,IAAI;AAExB,UAAO,OAAO,MAAM;;EAEvB;;;;;AAMH,SAAS,aAAa,WAAmB,OAAmC;AAC1E,KAAI,QAAQ,MAAM,CAChB,QAAO;AAGT,KAAI,iBAAiB,MAAM,CACzB,QAAO,gBAAgB,WAAW,MAAM;AAG1C,OAAM,IAAI,MACR,iCAAiC,UAAU,uJAG5C;;;;;;AAOH,SAAS,kBAAkB,UAAgD;AACzE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,CAIjD,KAAI;EACF,MAAM,YAAY,MAAM,MAAM,OAAO;AAErC,MADmB,MAAM,UAAU,UAAU,KAC1B,KACjB,OAAM,IAAI,MACR,iCAAiC,IAAI,+IAGtC;UAEI,GAAG;AAIV,MAAI,aAAa,SAAS,EAAE,QAAQ,SAAS,gBAAgB,CAC3D,OAAM;;;;;;;;;;;;;;;;;;AAyBd,SAAgB,oBACd,QAC6D;CAG7D,MAAM,iBAAiD,EAAE;AAEzD,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,CAC/C,gBAAe,OAAO,aAAa,KAAK,MAAoB;AAI9D,mBAAkB,eAAe;CAGjC,SAAS,MAAM,KAA2C;EACxD,MAAM,SAAkC,EAAE;AAE1C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,WAAW,IAAI;AAErB,UAAO,OAAO,MAAM,MAAM,SAAS;;AAGrC,SAAO;;CAIT,SAAS,UAAU,QAAmC;EACpD,MAAM,SAAiC,EAAE;AAEzC,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,aAAa,MAAM,UAAU,OAAO,KAA2B;AACrE,OAAI,eAAe,KACjB,OAAM,IAAI,MACR,qCAAqC,IAAI,uEAE1C;AAEH,UAAO,OAAO;;AAGhB,SAAO;;AAST,QANwC;EACtC;EACA;EACA,QAAQ;EACT"}
@@ -1,12 +0,0 @@
1
- /**
2
- * SSR Error Bridge — loads the SSR error entry and renders error pages directly.
3
- *
4
- * Bypasses the RSC Flight pipeline entirely. Component module paths are
5
- * passed to the SSR environment which imports them (resolving 'use client'
6
- * references to actual modules) and builds the element tree for Fizz.
7
- *
8
- * Design docs: 10-error-handling.md §"Three-Tier Error Page Rendering"
9
- */
10
- import type { ErrorPageContext } from '#/server/ssr-error-entry.js';
11
- export declare function callSsrErrorPage(ctx: ErrorPageContext): Promise<Response>;
12
- //# sourceMappingURL=ssr-error-bridge.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ssr-error-bridge.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/ssr-error-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAEpE,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAK/E"}
@@ -1,65 +0,0 @@
1
- /**
2
- * SSR Error Entry — Renders error pages directly through Fizz (no RSC).
3
- *
4
- * Error pages bypass the RSC Flight pipeline because Error objects are not
5
- * RSC-serializable (React Flight throws "Only plain objects can be passed
6
- * to Client Components"). Instead, this entry receives component module
7
- * paths, imports them in the SSR environment, builds the element tree,
8
- * and renders through Fizz.
9
- *
10
- * Layout components may be server components that import 'use client'
11
- * children — in the SSR environment, client references resolve to actual
12
- * component modules (unlike the RSC environment where they're opaque
13
- * reference objects). This is why we import in SSR, not pass from RSC.
14
- *
15
- * Design docs: 10-error-handling.md §"Three-Tier Error Page Rendering"
16
- */
17
- /**
18
- * Context for SSR-only error page rendering.
19
- * Subset of NavContext — no RSC stream, no Flight injection.
20
- */
21
- export interface ErrorPageContext {
22
- /** The requested pathname */
23
- pathname: string;
24
- /** Extracted route params */
25
- params: Record<string, string | string[]>;
26
- /** Search params from the URL */
27
- searchParams: Record<string, string>;
28
- /** The committed HTTP status code */
29
- statusCode: number;
30
- /** Response headers from middleware/proxy */
31
- responseHeaders: Headers;
32
- /** Pre-rendered metadata HTML to inject before </head> */
33
- headHtml: string;
34
- /** Inline JS for React's bootstrapScriptContent */
35
- bootstrapScriptContent: string;
36
- /** Request abort signal */
37
- signal?: AbortSignal;
38
- /** Request cookies for useCookie() during SSR */
39
- cookies?: Map<string, string>;
40
- /** Error component props: { error, digest, reset } */
41
- errorProps: {
42
- error: Error;
43
- digest: {
44
- code: string;
45
- data: unknown;
46
- } | null;
47
- reset: undefined;
48
- };
49
- /** File path of the error component module (imported in SSR env) */
50
- errorComponentPath: string;
51
- /** File paths of layout component modules, ordered root → leaf */
52
- layoutPaths: string[];
53
- }
54
- /**
55
- * Render an error page directly through Fizz (no RSC Flight stream).
56
- *
57
- * Imports the error component and layout components in the SSR environment,
58
- * builds the element tree, wraps with SSR-specific components (for useId
59
- * matching), and renders to HTML.
60
- *
61
- * Key difference from handleSsr: NO RSC stream decode, NO Flight injection.
62
- */
63
- export declare function handleSsrErrorPage(ctx: ErrorPageContext): Promise<Response>;
64
- export default handleSsrErrorPage;
65
- //# sourceMappingURL=ssr-error-entry.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ssr-error-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-error-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAwDH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,sBAAsB,EAAE,MAAM,CAAC;IAC/B,2BAA2B;IAC3B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,sDAAsD;IACtD,UAAU,EAAE;QACV,KAAK,EAAE,KAAK,CAAC;QACb,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,OAAO,CAAA;SAAE,GAAG,IAAI,CAAC;QAC/C,KAAK,EAAE,SAAS,CAAC;KAClB,CAAC;IACF,oEAAoE;IACpE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kEAAkE;IAClE,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiDjF;AAwED,eAAe,kBAAkB,CAAC"}
@@ -1,20 +0,0 @@
1
- /**
2
- * SSR Error Bridge — loads the SSR error entry and renders error pages directly.
3
- *
4
- * Bypasses the RSC Flight pipeline entirely. Component module paths are
5
- * passed to the SSR environment which imports them (resolving 'use client'
6
- * references to actual modules) and builds the element tree for Fizz.
7
- *
8
- * Design docs: 10-error-handling.md §"Three-Tier Error Page Rendering"
9
- */
10
-
11
- /// <reference types="@vitejs/plugin-rsc/types" />
12
-
13
- import type { ErrorPageContext } from '#/server/ssr-error-entry.js';
14
-
15
- export async function callSsrErrorPage(ctx: ErrorPageContext): Promise<Response> {
16
- const ssrErrorEntry = await import.meta.viteRsc.import<
17
- typeof import('#/server/ssr-error-entry.js')
18
- >('../ssr-error-entry.js', { environment: 'ssr' });
19
- return ssrErrorEntry.handleSsrErrorPage(ctx);
20
- }
@@ -1,237 +0,0 @@
1
- /**
2
- * SSR Error Entry — Renders error pages directly through Fizz (no RSC).
3
- *
4
- * Error pages bypass the RSC Flight pipeline because Error objects are not
5
- * RSC-serializable (React Flight throws "Only plain objects can be passed
6
- * to Client Components"). Instead, this entry receives component module
7
- * paths, imports them in the SSR environment, builds the element tree,
8
- * and renders through Fizz.
9
- *
10
- * Layout components may be server components that import 'use client'
11
- * children — in the SSR environment, client references resolve to actual
12
- * component modules (unlike the RSC environment where they're opaque
13
- * reference objects). This is why we import in SSR, not pass from RSC.
14
- *
15
- * Design docs: 10-error-handling.md §"Three-Tier Error Page Rendering"
16
- */
17
-
18
- // @ts-expect-error — virtual module provided by timber-entries plugin
19
- import config from 'virtual:timber-config';
20
- import { createElement, type ReactNode } from 'react';
21
- import { AsyncLocalStorage } from 'node:async_hooks';
22
-
23
- import {
24
- renderSsrStream,
25
- renderSsrNodeStream,
26
- nodeReadableToWeb,
27
- useNodeStreams,
28
- buildSsrResponse,
29
- } from './ssr-render.js';
30
- import { logRenderError } from './logger.js';
31
- import { createBufferedTransformStream, injectHead } from './html-injectors.js';
32
- import { wrapSsrElement } from './ssr-wrappers.js';
33
- import { registerSsrDataProvider, type SsrData } from '#/client/ssr-data.js';
34
- import { setCurrentParams } from '#/client/use-params.js';
35
- import { withSpan } from './tracing.js';
36
-
37
- // ─── SSR Data ALS (shared pattern with ssr-entry.ts) ──────────────────────
38
-
39
- const ssrDataAls = new AsyncLocalStorage<SsrData>();
40
- registerSsrDataProvider(() => ssrDataAls.getStore());
41
-
42
- // Pre-import Node.js stream modules at module load time (same as ssr-entry.ts).
43
- let _nodeStreamImports: {
44
- createNodeBufferedTransform: typeof import('./node-stream-transforms.js').createNodeBufferedTransform;
45
- createNodeHeadInjector: typeof import('./node-stream-transforms.js').createNodeHeadInjector;
46
- createNodeMoveSuffixTransform: typeof import('./node-stream-transforms.js').createNodeMoveSuffixTransform;
47
- createNodeErrorHandler: typeof import('./node-stream-transforms.js').createNodeErrorHandler;
48
- pipeline: typeof import('node:stream/promises').pipeline;
49
- PassThrough: typeof import('node:stream').PassThrough;
50
- } | null = null;
51
-
52
- if (useNodeStreams) {
53
- try {
54
- const [transforms, streamPromises, stream] = await Promise.all([
55
- import('./node-stream-transforms.js'),
56
- import('node:stream/promises'),
57
- import('node:stream'),
58
- ]);
59
- _nodeStreamImports = {
60
- createNodeBufferedTransform: transforms.createNodeBufferedTransform,
61
- createNodeHeadInjector: transforms.createNodeHeadInjector,
62
- createNodeMoveSuffixTransform: transforms.createNodeMoveSuffixTransform,
63
- createNodeErrorHandler: transforms.createNodeErrorHandler,
64
- pipeline: streamPromises.pipeline,
65
- PassThrough: stream.PassThrough,
66
- };
67
- } catch {
68
- // Fall back to Web Streams path
69
- }
70
- }
71
-
72
- /**
73
- * Context for SSR-only error page rendering.
74
- * Subset of NavContext — no RSC stream, no Flight injection.
75
- */
76
- export interface ErrorPageContext {
77
- /** The requested pathname */
78
- pathname: string;
79
- /** Extracted route params */
80
- params: Record<string, string | string[]>;
81
- /** Search params from the URL */
82
- searchParams: Record<string, string>;
83
- /** The committed HTTP status code */
84
- statusCode: number;
85
- /** Response headers from middleware/proxy */
86
- responseHeaders: Headers;
87
- /** Pre-rendered metadata HTML to inject before </head> */
88
- headHtml: string;
89
- /** Inline JS for React's bootstrapScriptContent */
90
- bootstrapScriptContent: string;
91
- /** Request abort signal */
92
- signal?: AbortSignal;
93
- /** Request cookies for useCookie() during SSR */
94
- cookies?: Map<string, string>;
95
- /** Error component props: { error, digest, reset } */
96
- errorProps: {
97
- error: Error;
98
- digest: { code: string; data: unknown } | null;
99
- reset: undefined;
100
- };
101
- /** File path of the error component module (imported in SSR env) */
102
- errorComponentPath: string;
103
- /** File paths of layout component modules, ordered root → leaf */
104
- layoutPaths: string[];
105
- }
106
-
107
- /**
108
- * Render an error page directly through Fizz (no RSC Flight stream).
109
- *
110
- * Imports the error component and layout components in the SSR environment,
111
- * builds the element tree, wraps with SSR-specific components (for useId
112
- * matching), and renders to HTML.
113
- *
114
- * Key difference from handleSsr: NO RSC stream decode, NO Flight injection.
115
- */
116
- export async function handleSsrErrorPage(ctx: ErrorPageContext): Promise<Response> {
117
- return withSpan('timber.ssr.error-page', { 'timber.environment': 'ssr' }, async () => {
118
- const _runtimeConfig = config;
119
-
120
- const ssrData: SsrData = {
121
- pathname: ctx.pathname,
122
- searchParams: ctx.searchParams,
123
- cookies: ctx.cookies ?? new Map(),
124
- params: ctx.params,
125
- };
126
-
127
- return ssrDataAls.run(ssrData, async () => {
128
- setCurrentParams(ctx.params);
129
-
130
- const h = createElement as (...args: unknown[]) => React.ReactElement;
131
-
132
- // Import error component in SSR environment.
133
- // In SSR, 'use client' components resolve to actual module functions
134
- // (not opaque client references like in RSC).
135
- const errorMod = (await import(/* @vite-ignore */ ctx.errorComponentPath)) as Record<
136
- string,
137
- unknown
138
- >;
139
- const ErrorComponent = errorMod.default as (...args: unknown[]) => ReactNode;
140
-
141
- // Build innermost element: error component with props
142
- let element: React.ReactElement = h(ErrorComponent, ctx.errorProps);
143
-
144
- // Import and wrap with layouts (root → leaf order, wrap inside-out)
145
- for (let i = ctx.layoutPaths.length - 1; i >= 0; i--) {
146
- const layoutMod = (await import(/* @vite-ignore */ ctx.layoutPaths[i])) as Record<
147
- string,
148
- unknown
149
- >;
150
- const LayoutComponent = layoutMod.default as (...args: unknown[]) => ReactNode;
151
- element = h(LayoutComponent, null, element);
152
- }
153
-
154
- // Wrap with SSR-specific components for useId matching.
155
- const hasTopLoader = _runtimeConfig.topLoader?.enabled !== false;
156
- const wrappedElement = wrapSsrElement(element, ctx.searchParams, hasTopLoader);
157
-
158
- // Render to HTML — same dual-path as ssr-entry.ts but NO Flight injection.
159
- if (_nodeStreamImports) {
160
- return renderViaNodeStreams(wrappedElement, ctx, _runtimeConfig);
161
- }
162
- return renderViaWebStreams(wrappedElement, ctx, _runtimeConfig);
163
- });
164
- });
165
- }
166
-
167
- // ─── Render Paths ─────────────────────────────────────────────────────────
168
-
169
- async function renderViaNodeStreams(
170
- element: ReactNode,
171
- ctx: ErrorPageContext,
172
- runtimeConfig: Record<string, unknown>
173
- ): Promise<Response> {
174
- const {
175
- createNodeBufferedTransform,
176
- createNodeHeadInjector,
177
- createNodeMoveSuffixTransform,
178
- createNodeErrorHandler,
179
- pipeline,
180
- PassThrough,
181
- } = _nodeStreamImports!;
182
-
183
- const renderTimeoutMs = (runtimeConfig.renderTimeoutMs as number | undefined) ?? undefined;
184
-
185
- let nodeHtmlStream: import('node:stream').Readable;
186
- try {
187
- nodeHtmlStream = await renderSsrNodeStream(element, {
188
- bootstrapScriptContent: ctx.bootstrapScriptContent || undefined,
189
- signal: ctx.signal,
190
- renderTimeoutMs,
191
- });
192
- } catch (renderError) {
193
- logRenderError({ method: '', path: '', error: renderError });
194
- return new Response(null, { status: ctx.statusCode, headers: ctx.responseHeaders });
195
- }
196
-
197
- // Pipeline: buffer → errorHandler → headInjector → moveSuffix → output
198
- // NO flightInjector — there's no RSC stream to inline.
199
- const bufferedTransform = createNodeBufferedTransform();
200
- const errorHandler = createNodeErrorHandler(ctx.signal);
201
- const headInjector = createNodeHeadInjector(ctx.headHtml);
202
- const moveSuffix = createNodeMoveSuffixTransform();
203
-
204
- const output = new PassThrough();
205
- pipeline(nodeHtmlStream, bufferedTransform, errorHandler, headInjector, moveSuffix, output).catch(
206
- () => {}
207
- );
208
-
209
- const webStream = nodeReadableToWeb(output);
210
- return buildSsrResponse(webStream, ctx.statusCode, ctx.responseHeaders);
211
- }
212
-
213
- async function renderViaWebStreams(
214
- element: ReactNode,
215
- ctx: ErrorPageContext,
216
- runtimeConfig: Record<string, unknown>
217
- ): Promise<Response> {
218
- const renderTimeoutMs = (runtimeConfig.renderTimeoutMs as number | undefined) ?? undefined;
219
-
220
- let htmlStream: ReadableStream<Uint8Array>;
221
- try {
222
- htmlStream = await renderSsrStream(element, {
223
- bootstrapScriptContent: ctx.bootstrapScriptContent || undefined,
224
- signal: ctx.signal,
225
- renderTimeoutMs,
226
- });
227
- } catch (renderError) {
228
- logRenderError({ method: '', path: '', error: renderError });
229
- return new Response(null, { status: ctx.statusCode, headers: ctx.responseHeaders });
230
- }
231
-
232
- let outputStream = htmlStream.pipeThrough(createBufferedTransformStream());
233
- outputStream = injectHead(outputStream, ctx.headHtml);
234
- return buildSsrResponse(outputStream, ctx.statusCode, ctx.responseHeaders);
235
- }
236
-
237
- export default handleSsrErrorPage;