@timber-js/app 0.2.0-alpha.146 → 0.2.0-alpha.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{cli-schema-sync-DH-M8Poy.js → cli-schema-sync-CBiMEtqa.js} +1 -1
- package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-CQaxupHv.js} +47 -54
- package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +1 -0
- package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-Ck4Y45XN.js} +49 -8
- package/dist/_chunks/walkers-Ck4Y45XN.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +13 -4
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/error-reconstituter.d.ts +8 -1
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/routing/codegen-types.d.ts +4 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/deny-boundary.d.ts +27 -2
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/internal.js +226 -178
- package/dist/server/internal.js.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +6 -0
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/utils/mdx-file.d.ts +15 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/client/error-boundary.tsx +14 -0
- package/src/client/error-reconstituter.tsx +15 -1
- package/src/routing/codegen-types.ts +4 -0
- package/src/routing/codegen.ts +63 -6
- package/src/routing/scanner.ts +80 -63
- package/src/server/deny-boundary.ts +106 -13
- package/src/server/deny-renderer.ts +40 -10
- package/src/server/error-boundary-wrapper.ts +1 -14
- package/src/server/route-matcher.ts +22 -6
- package/src/server/rsc-entry/error-renderer.ts +1 -12
- package/src/server/status-code-resolver.ts +3 -2
- package/src/server/tree-match.ts +15 -5
- package/src/server/utils/mdx-file.ts +22 -0
- package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
- package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
|
@@ -99,10 +99,19 @@ var TimberErrorBoundary = class extends Component {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
if (this.props.fallbackElement != null) return this.props.fallbackElement;
|
|
102
|
-
if (parsed?.type === "deny")
|
|
103
|
-
status
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
if (parsed?.type === "deny") {
|
|
103
|
+
if (this.props.status != null) return createElement(this.props.fallbackComponent, {
|
|
104
|
+
status: parsed.status,
|
|
105
|
+
dangerouslyPassData: parsed.data
|
|
106
|
+
});
|
|
107
|
+
return createElement(this.props.fallbackComponent, {
|
|
108
|
+
error,
|
|
109
|
+
digest: null,
|
|
110
|
+
reset: this.reset,
|
|
111
|
+
status: parsed.status,
|
|
112
|
+
dangerouslyPassData: parsed.data
|
|
113
|
+
});
|
|
114
|
+
}
|
|
106
115
|
const digest = parsed?.type === "render-error" ? {
|
|
107
116
|
code: parsed.code,
|
|
108
117
|
data: parsed.data
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-boundary.js","names":[],"sources":["../../src/client/error-boundary.tsx"],"sourcesContent":["'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, dangerouslyPassData }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createElement, type ReactNode } from 'react';\nimport { getSsrData } from './ssr-data.js';\nimport { getRouterOrNull } from './router-ref.js';\n\n// ─── Page Unload Detection ───────────────────────────────────────────────────\n// Track whether the page is being unloaded (user refreshed or navigated away).\n// When this is true, error boundaries suppress activation — the error is from\n// the aborted connection, not an application error.\nlet _isUnloading = false;\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _isUnloading = true;\n });\n window.addEventListener('pagehide', () => {\n _isUnloading = true;\n });\n}\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent?: (...args: unknown[]) => ReactNode;\n /**\n * Pre-rendered fallback element. Used for MDX status files which are server\n * components and cannot be passed as function props across the RSC→client\n * boundary. When set, rendered directly instead of calling fallbackComponent.\n *\n * See design/10-error-handling.md §\"Status-Code File Variants\" — MDX status\n * files are server components by default (zero client JS).\n */\n fallbackElement?: ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n /**\n * When true, this boundary wraps a parallel slot. Slot denials are\n * graceful degradation — they must NOT change the HTTP status code.\n */\n isSlotBoundary?: boolean;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// Module-level guard: the first boundary to catch a given redirect digest\n// schedules the navigation and marks it handled here. Subsequent renders\n// (React retries the error-boundary render path multiple times in dev, and\n// outer boundaries that also see the error) become no-ops, preventing\n// duplicate router.navigate() calls.\nconst _handledRedirects = new WeakSet<object>();\n\n// Companion guard keyed by location: client-render redirect() (the\n// next/navigation shim) throws a FRESH error instance on every render\n// attempt, and React replays the errored render — so the WeakSet alone\n// would schedule the navigation twice. While a navigation to a location\n// is pending, further redirect errors for the same location are no-ops.\nlet _pendingRedirectLocation: string | null = null;\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (_isUnloading) {\n return { hasError: false, error: null };\n }\n\n return { hasError: true, error };\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return this.props.children;\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal handling splits by environment:\n //\n // - SSR (no window): re-throw so the Fizz shell fails and the server\n // pipeline catch block can produce a proper HTTP 3xx response. See\n // design/04-authorization.md.\n //\n // - Client (window exists): a RedirectSignal that leaked into the RSC\n // Flight payload during client navigation would otherwise bubble past\n // every error boundary to `window.onerror`, which in dev triggers the\n // Vite error overlay (false positive — navigation itself works).\n // Instead, ask the router to perform an SPA navigation to the target\n // and render nothing while it takes over. The router's own catch path\n // handles cross-origin / hard navigations. See TIM-838.\n if (parsed?.type === 'redirect') {\n if (typeof window === 'undefined') {\n throw error;\n }\n // Dedupe: React may render the error-boundary fallback path more\n // than once for a single caught error (dev-mode double render, and\n // additional boundaries above us in the tree). Only schedule the\n // navigation on the first render that sees this error instance.\n const alreadyHandled =\n _handledRedirects.has(error) || _pendingRedirectLocation === parsed.location;\n if (!alreadyHandled) {\n _handledRedirects.add(error);\n const router = getRouterOrNull();\n if (router) {\n _pendingRedirectLocation = parsed.location;\n // Schedule the navigation outside the render phase to avoid\n // setState-during-render warnings from router state updates.\n queueMicrotask(() => {\n router\n .navigate(parsed.location, { replace: true })\n .catch(() => {\n // Fall back to a hard navigation if the soft navigation fails.\n window.location.href = parsed.location;\n })\n .finally(() => {\n _pendingRedirectLocation = null;\n });\n });\n } else {\n // No router available (shouldn't happen post-bootstrap) — fall\n // back to a hard navigation so the user still ends up at the\n // target.\n window.location.href = parsed.location;\n }\n }\n return null;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Report DenySignal handling so the pipeline skips the redundant\n // renderDenyPage() re-render — but ONLY when this boundary can fully\n // handle the deny in-tree:\n //\n // ✅ TSX boundaries (fallbackComponent) — can receive runtime props\n // (status, dangerouslyPassData) dynamically in render()\n // ❌ MDX boundaries (fallbackElement) — pre-rendered at tree-build time,\n // cannot receive runtime deny props. Must fall through to re-render.\n //\n // For qualifying segment boundaries: also set the HTTP status code from\n // the DenySignal so the Response has the correct 4xx status. This runs\n // synchronously during Fizz rendering, BEFORE onShellReady.\n //\n // For slot boundaries: set _denyHandledByBoundary but do NOT change\n // statusCode — slot denials are graceful degradation.\n //\n // See TIM-664, LOCAL-298.\n if (parsed?.type === 'deny') {\n const canHandleInTree = this.props.fallbackElement == null;\n\n if (canHandleInTree || this.props.isSlotBoundary) {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n if (!this.props.isSlotBoundary) {\n ssrData._navContext.statusCode = parsed.status;\n }\n }\n }\n }\n\n // Pre-rendered fallback element (MDX status files) — render directly.\n // MDX components are server components that cannot be passed as function\n // props across the RSC→client boundary. Instead, they are pre-rendered\n // as elements in the RSC environment and passed here as fallbackElement.\n if (this.props.fallbackElement != null) {\n return this.props.fallbackElement;\n }\n\n // Render the fallback component with the right props shape.\n if (parsed?.type === 'deny') {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch (e) {\n console.debug('[timber] error digest is not valid JSON:', e instanceof Error ? e.message : e);\n }\n return null;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"error-boundary.js","names":[],"sources":["../../src/client/error-boundary.tsx"],"sourcesContent":["'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, dangerouslyPassData }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createElement, type ReactNode } from 'react';\nimport { getSsrData } from './ssr-data.js';\nimport { getRouterOrNull } from './router-ref.js';\n\n// ─── Page Unload Detection ───────────────────────────────────────────────────\n// Track whether the page is being unloaded (user refreshed or navigated away).\n// When this is true, error boundaries suppress activation — the error is from\n// the aborted connection, not an application error.\nlet _isUnloading = false;\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _isUnloading = true;\n });\n window.addEventListener('pagehide', () => {\n _isUnloading = true;\n });\n}\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent?: (...args: unknown[]) => ReactNode;\n /**\n * Pre-rendered fallback element. Used for MDX status files which are server\n * components and cannot be passed as function props across the RSC→client\n * boundary. When set, rendered directly instead of calling fallbackComponent.\n *\n * See design/10-error-handling.md §\"Status-Code File Variants\" — MDX status\n * files are server components by default (zero client JS).\n */\n fallbackElement?: ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n /**\n * When true, this boundary wraps a parallel slot. Slot denials are\n * graceful degradation — they must NOT change the HTTP status code.\n */\n isSlotBoundary?: boolean;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// Module-level guard: the first boundary to catch a given redirect digest\n// schedules the navigation and marks it handled here. Subsequent renders\n// (React retries the error-boundary render path multiple times in dev, and\n// outer boundaries that also see the error) become no-ops, preventing\n// duplicate router.navigate() calls.\nconst _handledRedirects = new WeakSet<object>();\n\n// Companion guard keyed by location: client-render redirect() (the\n// next/navigation shim) throws a FRESH error instance on every render\n// attempt, and React replays the errored render — so the WeakSet alone\n// would schedule the navigation twice. While a navigation to a location\n// is pending, further redirect errors for the same location are no-ops.\nlet _pendingRedirectLocation: string | null = null;\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (_isUnloading) {\n return { hasError: false, error: null };\n }\n\n return { hasError: true, error };\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return this.props.children;\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal handling splits by environment:\n //\n // - SSR (no window): re-throw so the Fizz shell fails and the server\n // pipeline catch block can produce a proper HTTP 3xx response. See\n // design/04-authorization.md.\n //\n // - Client (window exists): a RedirectSignal that leaked into the RSC\n // Flight payload during client navigation would otherwise bubble past\n // every error boundary to `window.onerror`, which in dev triggers the\n // Vite error overlay (false positive — navigation itself works).\n // Instead, ask the router to perform an SPA navigation to the target\n // and render nothing while it takes over. The router's own catch path\n // handles cross-origin / hard navigations. See TIM-838.\n if (parsed?.type === 'redirect') {\n if (typeof window === 'undefined') {\n throw error;\n }\n // Dedupe: React may render the error-boundary fallback path more\n // than once for a single caught error (dev-mode double render, and\n // additional boundaries above us in the tree). Only schedule the\n // navigation on the first render that sees this error instance.\n const alreadyHandled =\n _handledRedirects.has(error) || _pendingRedirectLocation === parsed.location;\n if (!alreadyHandled) {\n _handledRedirects.add(error);\n const router = getRouterOrNull();\n if (router) {\n _pendingRedirectLocation = parsed.location;\n // Schedule the navigation outside the render phase to avoid\n // setState-during-render warnings from router state updates.\n queueMicrotask(() => {\n router\n .navigate(parsed.location, { replace: true })\n .catch(() => {\n // Fall back to a hard navigation if the soft navigation fails.\n window.location.href = parsed.location;\n })\n .finally(() => {\n _pendingRedirectLocation = null;\n });\n });\n } else {\n // No router available (shouldn't happen post-bootstrap) — fall\n // back to a hard navigation so the user still ends up at the\n // target.\n window.location.href = parsed.location;\n }\n }\n return null;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Report DenySignal handling so the pipeline skips the redundant\n // renderDenyPage() re-render — but ONLY when this boundary can fully\n // handle the deny in-tree:\n //\n // ✅ TSX boundaries (fallbackComponent) — can receive runtime props\n // (status, dangerouslyPassData) dynamically in render()\n // ❌ MDX boundaries (fallbackElement) — pre-rendered at tree-build time,\n // cannot receive runtime deny props. Must fall through to re-render.\n //\n // For qualifying segment boundaries: also set the HTTP status code from\n // the DenySignal so the Response has the correct 4xx status. This runs\n // synchronously during Fizz rendering, BEFORE onShellReady.\n //\n // For slot boundaries: set _denyHandledByBoundary but do NOT change\n // statusCode — slot denials are graceful degradation.\n //\n // See TIM-664, LOCAL-298.\n if (parsed?.type === 'deny') {\n const canHandleInTree = this.props.fallbackElement == null;\n\n if (canHandleInTree || this.props.isSlotBoundary) {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n if (!this.props.isSlotBoundary) {\n ssrData._navContext.statusCode = parsed.status;\n }\n }\n }\n }\n\n // Pre-rendered fallback element (MDX status files) — render directly.\n // MDX components are server components that cannot be passed as function\n // props across the RSC→client boundary. Instead, they are pre-rendered\n // as elements in the RSC environment and passed here as fallbackElement.\n if (this.props.fallbackElement != null) {\n return this.props.fallbackElement;\n }\n\n // Render the fallback component with the right props shape.\n if (parsed?.type === 'deny') {\n // Status-code file boundaries (404.tsx, 4xx.tsx, …) get the documented\n // 4xx props. error.tsx boundaries (no status filter) get their\n // documented { error, digest, reset } contract — plus the deny extras\n // for dual-shape implementations. Without the error prop, an error.tsx\n // reading error.message crashes on deny fall-through. See TIM-1081.\n if (this.props.status != null) {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest: null,\n reset: this.reset,\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch (e) {\n console.debug('[timber] error digest is not valid JSON:', e instanceof Error ? e.message : e);\n }\n return null;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":"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"}
|
|
@@ -28,6 +28,9 @@ export interface SerializableError {
|
|
|
28
28
|
* - digest: plain JSON or null
|
|
29
29
|
* - reset: undefined (only meaningful on client after boundary catch)
|
|
30
30
|
* - component: client module reference (RSC Flight serializes as opaque ref)
|
|
31
|
+
* - status / dangerouslyPassData: set only when error.tsx serves a deny()
|
|
32
|
+
* as the last entry in the 4xx fallback chain — forwarded so dual-shape
|
|
33
|
+
* error.tsx implementations can branch on the deny status (TIM-1081)
|
|
31
34
|
*/
|
|
32
35
|
interface ErrorReconstituterProps {
|
|
33
36
|
error: SerializableError;
|
|
@@ -43,12 +46,16 @@ interface ErrorReconstituterProps {
|
|
|
43
46
|
data: unknown;
|
|
44
47
|
} | null;
|
|
45
48
|
reset: (() => void) | undefined;
|
|
49
|
+
status?: number;
|
|
50
|
+
dangerouslyPassData?: unknown;
|
|
46
51
|
}>;
|
|
52
|
+
status?: number;
|
|
53
|
+
dangerouslyPassData?: unknown;
|
|
47
54
|
}
|
|
48
55
|
/**
|
|
49
56
|
* Reconstitute a SerializableError into a real Error instance and render
|
|
50
57
|
* the user's error component with the proper props.
|
|
51
58
|
*/
|
|
52
|
-
export declare function ErrorReconstituter({ error: serialized, digest, reset, component, }: ErrorReconstituterProps): ReactNode;
|
|
59
|
+
export declare function ErrorReconstituter({ error: serialized, digest, reset, component, status, dangerouslyPassData, }: ErrorReconstituterProps): ReactNode;
|
|
53
60
|
export {};
|
|
54
61
|
//# sourceMappingURL=error-reconstituter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-reconstituter.d.ts","sourceRoot":"","sources":["../../src/client/error-reconstituter.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAE1E;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED
|
|
1
|
+
{"version":3,"file":"error-reconstituter.d.ts","sourceRoot":"","sources":["../../src/client/error-reconstituter.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAE1E;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,UAAU,uBAAuB;IAC/B,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/C,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,aAAa,CAAC;QACvB,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,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;QAChC,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,mBAAmB,CAAC,EAAE,OAAO,CAAC;KAC/B,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EAAE,UAAU,EACjB,MAAM,EACN,KAAK,EACL,SAAS,EACT,MAAM,EACN,mBAAmB,GACpB,EAAE,uBAAuB,GAAG,SAAS,CAarC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as setViteServer } from "./_chunks/warnings-CNPXaypM.js";
|
|
2
2
|
import { a as isDynamicMetadataExtension, o as isMetadataRouteServePath } from "./_chunks/metadata-routes-BjepjefT.js";
|
|
3
3
|
import { h as swallow } from "./_chunks/logger-Dt-jQaBO.js";
|
|
4
|
-
import { c as loadTimberConfigFile, d as resolveBuildDir, f as resolveClientJavascript, l as mergeFileConfig, m as scanRoutes, o as DEFAULT_BUILD_DIR, p as createNoopTimer, s as createPluginContext, u as resolveAppDir } from "./_chunks/cli-schema-sync-
|
|
5
|
-
import { a as generateRouteMap, i as collectInterceptionRewrites, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-
|
|
4
|
+
import { c as loadTimberConfigFile, d as resolveBuildDir, f as resolveClientJavascript, l as mergeFileConfig, m as scanRoutes, o as DEFAULT_BUILD_DIR, p as createNoopTimer, s as createPluginContext, u as resolveAppDir } from "./_chunks/cli-schema-sync-CQaxupHv.js";
|
|
5
|
+
import { a as generateRouteMap, i as collectInterceptionRewrites, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-Ck4Y45XN.js";
|
|
6
6
|
import { t as fnv1aHash } from "./_chunks/fast-hash-D6hIVt1Y.js";
|
|
7
7
|
import { t as formatSize } from "./_chunks/format-CfwjgPz9.js";
|
|
8
8
|
import { basename, dirname, extname, join, normalize, relative, resolve } from "node:path";
|
|
@@ -20,6 +20,10 @@ export interface RouteEntry {
|
|
|
20
20
|
searchParamsPagePath?: string;
|
|
21
21
|
/** Whether this is an API route (route.ts) vs page route */
|
|
22
22
|
isApiRoute: boolean;
|
|
23
|
+
/** Whether this entry lives inside a parallel slot (@name). Slot entries
|
|
24
|
+
* share their parent's urlPath — urlPath-keyed emissions dedupe them,
|
|
25
|
+
* preferring the non-slot entry. segmentPath-keyed emissions keep all. */
|
|
26
|
+
isSlotRoute?: boolean;
|
|
23
27
|
/** Whether a global schema (app/schema.ts) exists for this app */
|
|
24
28
|
hasSchema?: boolean;
|
|
25
29
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codegen-types.d.ts","sourceRoot":"","sources":["../../src/routing/codegen-types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,4DAA4D;AAC5D,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB;oFACgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,gDAAgD;IAChD,eAAe,EAAE,OAAO,CAAC;IACzB,kFAAkF;IAClF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,4DAA4D;IAC5D,UAAU,EAAE,OAAO,CAAC;IACpB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb;kDAC8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,sBAAsB,CAAC;IACrD;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;;OAQG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B"}
|
|
1
|
+
{"version":3,"file":"codegen-types.d.ts","sourceRoot":"","sources":["../../src/routing/codegen-types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,4DAA4D;AAC5D,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB;oFACgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,gDAAgD;IAChD,eAAe,EAAE,OAAO,CAAC;IACzB,kFAAkF;IAClF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,4DAA4D;IAC5D,UAAU,EAAE,OAAO,CAAC;IACpB;;+EAE2E;IAC3E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb;kDAC8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,sBAAsB,CAAC;IACrD;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;;OAQG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B"}
|
package/dist/routing/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { i as INTERCEPTION_MARKERS, n as classifyUrlSegment, r as DEFAULT_PAGE_EXTENSIONS, t as classifySegment } from "../_chunks/segment-classify-CDDRVKs7.js";
|
|
2
|
-
import { m as scanRoutes } from "../_chunks/cli-schema-sync-
|
|
3
|
-
import { a as generateRouteMap, i as collectInterceptionRewrites, n as collectDynamicSegmentsFromTree, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "../_chunks/walkers-
|
|
2
|
+
import { m as scanRoutes } from "../_chunks/cli-schema-sync-CQaxupHv.js";
|
|
3
|
+
import { a as generateRouteMap, i as collectInterceptionRewrites, n as collectDynamicSegmentsFromTree, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "../_chunks/walkers-Ck4Y45XN.js";
|
|
4
4
|
export { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS, classifySegment, classifyUrlSegment, collectDynamicSegmentsFromTree, collectInterceptionRewrites, collectLeafRoutes, generateRouteMap, scanRoutes, validateSchemaAgainstRoutes };
|
|
@@ -35,6 +35,15 @@ export interface DenyPageEntry {
|
|
|
35
35
|
status: number | null;
|
|
36
36
|
/** The component to render (server or client — both work). */
|
|
37
37
|
component: (...args: unknown[]) => unknown;
|
|
38
|
+
/**
|
|
39
|
+
* How the entry matched: a status-code file (404.tsx/4xx.tsx), a legacy
|
|
40
|
+
* compat file (not-found.tsx/forbidden.tsx/unauthorized.tsx), or the
|
|
41
|
+
* error.tsx catch-all. error.tsx entries render with their documented
|
|
42
|
+
* { error, digest, reset } contract, not bare deny props. See TIM-1081.
|
|
43
|
+
*/
|
|
44
|
+
kind: 'status' | 'legacy' | 'error';
|
|
45
|
+
/** MDX files are server components — rendered with plain props, never ErrorReconstituter. */
|
|
46
|
+
isMdx: boolean;
|
|
38
47
|
}
|
|
39
48
|
/**
|
|
40
49
|
* Build the deny page fallback chain from the segment chain.
|
|
@@ -43,10 +52,14 @@ export interface DenyPageEntry {
|
|
|
43
52
|
* status-code file components in fallback order:
|
|
44
53
|
* 1. Specific status files (403.tsx, 404.tsx) — exact match
|
|
45
54
|
* 2. Category catch-alls (4xx.tsx) — matches any 4xx
|
|
46
|
-
* 3.
|
|
55
|
+
* 3. Legacy compat files (not-found.tsx → 404, forbidden.tsx → 403,
|
|
56
|
+
* unauthorized.tsx → 401) — exact match
|
|
57
|
+
* 4. error.tsx — catches everything
|
|
47
58
|
*
|
|
48
59
|
* Each segment is checked in this order. The chain is ordered so the
|
|
49
|
-
* FIRST match wins at catch time.
|
|
60
|
+
* FIRST match wins at catch time. This mirrors resolveStatusFile's 4xx
|
|
61
|
+
* component chain (status-code-resolver.ts) — the two walks must stay in
|
|
62
|
+
* sync or in-tree and re-render deny paths resolve different files.
|
|
50
63
|
*/
|
|
51
64
|
export declare function buildDenyPageChain(segments: ManifestSegmentNode[], startIndex: number): Promise<DenyPageEntry[]>;
|
|
52
65
|
/**
|
|
@@ -54,6 +67,18 @@ export declare function buildDenyPageChain(segments: ManifestSegmentNode[], star
|
|
|
54
67
|
* Returns a React element for the matching component, or null if no match.
|
|
55
68
|
*/
|
|
56
69
|
export declare function renderMatchingDenyPage(chain: DenyPageEntry[], status: number, data: unknown): React.ReactElement | null;
|
|
70
|
+
/**
|
|
71
|
+
* Build the element for a matched deny chain entry with the props contract
|
|
72
|
+
* the file's convention documents:
|
|
73
|
+
*
|
|
74
|
+
* - Status-code and legacy files: { status, dangerouslyPassData }
|
|
75
|
+
* - error.tsx (TSX): { error, digest, reset } via ErrorReconstituter, plus
|
|
76
|
+
* { status, dangerouslyPassData } for dual-shape implementations. Without
|
|
77
|
+
* the Error prop, any error.tsx written per the docs (reading
|
|
78
|
+
* error.message) crashes — turning a clean deny(404) into a 500. TIM-1081.
|
|
79
|
+
* - error.mdx: plain { status } — server component, static content.
|
|
80
|
+
*/
|
|
81
|
+
export declare function renderDenyEntry(entry: DenyPageEntry, status: number, data: unknown): React.ReactElement;
|
|
57
82
|
/**
|
|
58
83
|
* Async server component that wraps a page call with DenySignal catching.
|
|
59
84
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deny-boundary.d.ts","sourceRoot":"","sources":["../../src/server/deny-boundary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;
|
|
1
|
+
{"version":3,"file":"deny-boundary.d.ts","sourceRoot":"","sources":["../../src/server/deny-boundary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAYH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAI9D,sDAAsD;AACtD,MAAM,WAAW,aAAa;IAC5B,yFAAyF;IACzF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8DAA8D;IAC9D,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C;;;;;OAKG;IACH,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,6FAA6F;IAC7F,KAAK,EAAE,OAAO,CAAC;CAChB;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,EAAE,CAAC,CAuF1B;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,aAAa,EAAE,EACtB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,GACZ,KAAK,CAAC,YAAY,GAAG,IAAI,CAgB3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,GACZ,KAAK,CAAC,YAAY,CA2BpB;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,IAAI,EACJ,KAAK,EACL,SAAS,GACV,EAAE;IACD,0CAA0C;IAC1C,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IACtC,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,SAAS,EAAE,aAAa,EAAE,CAAC;CAC5B,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAiB9B;AAID;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAKlD;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAElD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deny-renderer.d.ts","sourceRoot":"","sources":["../../src/server/deny-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"deny-renderer.d.ts","sourceRoot":"","sources":["../../src/server/deny-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAS7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAOjE,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,iEAAiE;AACjE,MAAM,MAAM,mBAAmB,GAAG,MAAM;IACtC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,cAAc,CAAC;CAC1B,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,SAAS,GAAG,CACtB,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,KACnB,OAAO,CAAC,QAAQ,CAAC,CAAC;AAiCvB;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,UAAU,EAChB,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,sBAAsB,EAAE,mBAAmB,EAC3C,OAAO,EAAE,SAAS,GACjB,OAAO,CAAC,QAAQ,CAAC,CAoGnB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,WAAW,EAAE,EAC/B,eAAe,EAAE,OAAO,EACxB,sBAAsB,EAAE,mBAAmB,GAC1C,OAAO,CAAC,QAAQ,CAAC,CA4CnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-boundary-wrapper.d.ts","sourceRoot":"","sources":["../../src/server/error-boundary-wrapper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"error-boundary-wrapper.d.ts","sourceRoot":"","sources":["../../src/server/error-boundary-wrapper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAI9D;;;;;;;;;;;GAWG;AACH,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,KAAK,CAAC,YAAY,EAC3B,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,YAAY;AAC7C,8FAA8F;AAC9F,kBAAkB,UAAQ,GACzB,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CA6E7B"}
|