@timber-js/app 0.2.0-alpha.145 → 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/{actions-DzFMnUAR.js → actions-OIZ8siQu.js} +28 -43
- package/dist/_chunks/actions-OIZ8siQu.js.map +1 -0
- 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/{handler-store-DjISKYEB.js → handler-store-BVDgyTh7.js} +26 -2
- package/dist/_chunks/handler-store-BVDgyTh7.js.map +1 -0
- package/dist/_chunks/{href-validation-EZktELp8.js → href-validation-DyZIzfMJ.js} +18 -2
- package/dist/_chunks/{href-validation-EZktELp8.js.map → href-validation-DyZIzfMJ.js.map} +1 -1
- package/dist/_chunks/{navigation-root-DLc4s4ZC.js → navigation-root-bUDZLw74.js} +23 -2
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -0
- package/dist/_chunks/tracing-M_zKubhk.js.map +1 -1
- package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-Ck4Y45XN.js} +49 -8
- package/dist/_chunks/walkers-Ck4Y45XN.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +1 -15
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +10 -28
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +18 -5
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.js +39 -12
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/redis-handler.d.ts +72 -8
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/stable-stringify.d.ts +9 -0
- package/dist/cache/stable-stringify.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +3 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/browser-entry/scroll.d.ts +9 -0
- package/dist/client/browser-entry/scroll.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +24 -10
- 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/client/index.js +3 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +99 -24
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +13 -0
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts +7 -0
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/dev-tools/404-page.d.ts +6 -2
- package/dist/dev-tools/404-page.d.ts.map +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +77 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -0
- package/dist/dev-tools/error-page.d.ts +4 -30
- package/dist/dev-tools/error-page.d.ts.map +1 -1
- package/dist/dev-tools/index.d.ts +2 -1
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/dev-tools/instrumentation.d.ts +11 -1
- package/dist/dev-tools/instrumentation.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +432 -255
- package/dist/index.js.map +1 -1
- package/dist/plugins/cache.d.ts +43 -9
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/cloudflare-wasm.d.ts +4 -1
- package/dist/plugins/cloudflare-wasm.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts +16 -0
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts +23 -0
- package/dist/plugins/shims.d.ts.map +1 -1
- 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/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +3 -2
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts.map +1 -1
- package/dist/server/cookie-parsing.d.ts.map +1 -1
- 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/fallback-error.d.ts +2 -2
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.js +227 -179
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +24 -6
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +11 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +1 -11
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +6 -4
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.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/tracing.d.ts +2 -1
- package/dist/server/tracing.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/dist/shared/head-header.d.ts +54 -0
- package/dist/shared/head-header.d.ts.map +1 -0
- package/dist/shared/href-validation.d.ts +11 -0
- package/dist/shared/href-validation.d.ts.map +1 -1
- package/dist/shared/redirect-type.d.ts +17 -0
- package/dist/shared/redirect-type.d.ts.map +1 -0
- package/dist/shims/headers.d.ts +52 -1
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +33 -11
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/adapters/cloudflare.ts +13 -47
- package/src/adapters/compress-module.ts +9 -3
- package/src/adapters/nitro.ts +9 -2
- package/src/cache/redis-handler.ts +77 -13
- package/src/cache/stable-stringify.ts +65 -2
- package/src/cache/timber-cache.ts +25 -13
- package/src/cli.ts +0 -0
- package/src/client/browser-entry/action-dispatch.ts +7 -2
- package/src/client/browser-entry/router-init.ts +2 -1
- package/src/client/browser-entry/scroll.ts +23 -0
- package/src/client/error-boundary.tsx +33 -5
- package/src/client/error-reconstituter.tsx +15 -1
- package/src/client/link.tsx +4 -1
- package/src/client/navigation-api.ts +5 -1
- package/src/client/navigation-root.tsx +16 -0
- package/src/client/router.ts +107 -29
- package/src/client/rsc-fetch.ts +6 -6
- package/src/dev-tools/404-page.ts +22 -123
- package/src/dev-tools/dev-page-shell.ts +256 -0
- package/src/dev-tools/error-page.ts +27 -203
- package/src/dev-tools/index.ts +7 -1
- package/src/dev-tools/instrumentation.ts +39 -2
- package/src/index.ts +1 -0
- package/src/plugins/cache.ts +79 -21
- package/src/plugins/cloudflare-wasm.ts +48 -10
- package/src/plugins/dev-server.ts +32 -8
- package/src/plugins/server-bundle.ts +25 -9
- package/src/plugins/shims.ts +94 -52
- 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/action-handler.ts +10 -2
- package/src/server/actions.ts +3 -2
- package/src/server/compress.ts +11 -2
- package/src/server/cookie-context.ts +11 -0
- package/src/server/cookie-parsing.ts +16 -4
- 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/fallback-error.ts +9 -4
- package/src/server/node-stream-transforms.ts +61 -24
- package/src/server/pipeline.ts +12 -0
- package/src/server/primitives.ts +10 -31
- package/src/server/route-matcher.ts +22 -6
- package/src/server/rsc-entry/error-renderer.ts +1 -12
- package/src/server/rsc-entry/index.ts +7 -2
- package/src/server/rsc-entry/rsc-payload.ts +4 -3
- package/src/server/slot-resolver.ts +27 -14
- package/src/server/ssr-entry.ts +17 -28
- package/src/server/status-code-resolver.ts +3 -2
- package/src/server/tracing.ts +17 -1
- package/src/server/tree-match.ts +15 -5
- package/src/server/utils/mdx-file.ts +22 -0
- package/src/shared/head-header.ts +69 -0
- package/src/shared/href-validation.ts +24 -0
- package/src/shared/redirect-type.ts +17 -0
- package/src/shims/headers.ts +119 -2
- package/src/shims/navigation-client.ts +48 -27
- package/LICENSE +0 -8
- package/dist/_chunks/actions-DzFMnUAR.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
- package/dist/_chunks/handler-store-DjISKYEB.js.map +0 -1
- package/dist/_chunks/navigation-root-DLc4s4ZC.js.map +0 -1
- package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
|
@@ -19,8 +19,10 @@ import { createElement } from 'react';
|
|
|
19
19
|
import { renderToReadableStream } from '../rsc-runtime/rsc.js';
|
|
20
20
|
|
|
21
21
|
import { DenySignal } from './primitives.js';
|
|
22
|
+
import { renderDenyEntry, type DenyPageEntry } from './deny-boundary.js';
|
|
22
23
|
import { logRenderError } from './logger.js';
|
|
23
24
|
import { loadModule } from './safe-load.js';
|
|
25
|
+
import { isMdxFilePath } from './utils/mdx-file.js';
|
|
24
26
|
import { isDebug } from './debug.js';
|
|
25
27
|
import { escapeHtml } from './utils/escape-html.js';
|
|
26
28
|
import { resolveMetadata, renderMetadataToElements } from './metadata.js';
|
|
@@ -62,6 +64,29 @@ function isApiRoute(segments: ReadonlyArray<ManifestSegmentNode>): boolean {
|
|
|
62
64
|
return !!leaf?.route;
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Build the deny page element for a resolved status file with the correct
|
|
69
|
+
* props contract for the file's convention. Delegates to renderDenyEntry
|
|
70
|
+
* (deny-boundary.ts) so the in-tree and re-render deny paths render
|
|
71
|
+
* identical elements — in particular, error.tsx serving a deny gets
|
|
72
|
+
* { error, digest, reset } via ErrorReconstituter, not bare deny props.
|
|
73
|
+
* See TIM-1081.
|
|
74
|
+
*/
|
|
75
|
+
function buildDenyElement(
|
|
76
|
+
deny: DenySignal,
|
|
77
|
+
component: (...args: unknown[]) => unknown,
|
|
78
|
+
resolutionKind: 'exact' | 'category' | 'legacy' | 'error',
|
|
79
|
+
filePath: string
|
|
80
|
+
): React.ReactElement {
|
|
81
|
+
const entry: DenyPageEntry = {
|
|
82
|
+
status: deny.status,
|
|
83
|
+
component,
|
|
84
|
+
kind: resolutionKind === 'error' ? 'error' : resolutionKind === 'legacy' ? 'legacy' : 'status',
|
|
85
|
+
isMdx: isMdxFilePath(filePath),
|
|
86
|
+
};
|
|
87
|
+
return renderDenyEntry(entry, deny.status, deny.data);
|
|
88
|
+
}
|
|
89
|
+
|
|
65
90
|
/**
|
|
66
91
|
* Render a status-code error page for a DenySignal.
|
|
67
92
|
*
|
|
@@ -120,12 +145,15 @@ export async function renderDenyPage(
|
|
|
120
145
|
const ErrorPageComponent = mod.default as (...args: unknown[]) => unknown;
|
|
121
146
|
const h = createElement as (...args: unknown[]) => React.ReactElement;
|
|
122
147
|
|
|
123
|
-
// 4xx status-code pages receive { status, dangerouslyPassData }
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
148
|
+
// 4xx status-code pages receive { status, dangerouslyPassData }; an
|
|
149
|
+
// error.tsx match gets { error, digest, reset } via ErrorReconstituter.
|
|
150
|
+
// See design/10-error-handling.md §"Status-Code File Props" and TIM-1081.
|
|
151
|
+
let element = buildDenyElement(
|
|
152
|
+
deny,
|
|
153
|
+
ErrorPageComponent,
|
|
154
|
+
resolution.kind,
|
|
155
|
+
resolution.file.filePath
|
|
156
|
+
);
|
|
129
157
|
|
|
130
158
|
// Always wrap in layout shell.
|
|
131
159
|
// NOTE: Shell opt-out (export const shell = false) is a future feature
|
|
@@ -207,10 +235,12 @@ export async function renderDenyPageAsRsc(
|
|
|
207
235
|
const ErrorPageComponent = mod.default as (...args: unknown[]) => unknown;
|
|
208
236
|
const h = createElement as (...args: unknown[]) => React.ReactElement;
|
|
209
237
|
|
|
210
|
-
let element =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
238
|
+
let element = buildDenyElement(
|
|
239
|
+
deny,
|
|
240
|
+
ErrorPageComponent,
|
|
241
|
+
resolution.kind,
|
|
242
|
+
resolution.file.filePath
|
|
243
|
+
);
|
|
214
244
|
|
|
215
245
|
// Always wrap in layout shell.
|
|
216
246
|
const resolvedSegments = new Set(segments.slice(0, resolution.segmentIndex + 1));
|
|
@@ -8,20 +8,7 @@
|
|
|
8
8
|
import { TimberErrorBoundary } from '../client/error-boundary.js';
|
|
9
9
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
10
10
|
import { loadModule } from './safe-load.js';
|
|
11
|
-
|
|
12
|
-
/** MDX/markdown extensions — server components that cannot be passed as function props. */
|
|
13
|
-
const MDX_EXTENSIONS = new Set(['.mdx', '.md']);
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Check if a manifest file path ends with an MDX/markdown extension.
|
|
17
|
-
* MDX components are server components and cannot cross the RSC→client
|
|
18
|
-
* boundary as function props to TimberErrorBoundary.
|
|
19
|
-
*/
|
|
20
|
-
function isMdxFilePath(filePath: string): boolean {
|
|
21
|
-
const dotIndex = filePath.lastIndexOf('.');
|
|
22
|
-
if (dotIndex === -1) return false;
|
|
23
|
-
return MDX_EXTENSIONS.has(filePath.slice(dotIndex));
|
|
24
|
-
}
|
|
11
|
+
import { isMdxFilePath } from './utils/mdx-file.js';
|
|
25
12
|
|
|
26
13
|
/**
|
|
27
14
|
* Wrap an element in error boundaries defined by a route segment.
|
|
@@ -34,14 +34,15 @@ export async function renderFallbackError(
|
|
|
34
34
|
rootSegment: ManifestSegmentNode,
|
|
35
35
|
clientBootstrap: ClientBootstrapConfig,
|
|
36
36
|
globalError?: GlobalErrorFile,
|
|
37
|
-
projectRoot?: string
|
|
37
|
+
projectRoot?: string,
|
|
38
|
+
hmrOptions?: import('../dev-tools/dev-page-shell.js').DevErrorHmrOptions
|
|
38
39
|
): Promise<Response> {
|
|
39
40
|
// Source-map the error's stack trace in dev mode so the error page
|
|
40
41
|
// shows original source positions. See TIM-811.
|
|
41
42
|
sourceMapError(error);
|
|
42
43
|
|
|
43
44
|
if (isDev) {
|
|
44
|
-
return renderDevErrorPage(error, projectRoot);
|
|
45
|
+
return renderDevErrorPage(error, projectRoot, hmrOptions);
|
|
45
46
|
}
|
|
46
47
|
// Lazy import to avoid loading error-renderer in the pipeline module
|
|
47
48
|
const { renderErrorPage } = await import('./rsc-entry/error-renderer.js');
|
|
@@ -91,7 +92,11 @@ export async function renderFallbackError(
|
|
|
91
92
|
*
|
|
92
93
|
* Dev-only — the dynamic import has zero production cost.
|
|
93
94
|
*/
|
|
94
|
-
export async function renderDevErrorPage(
|
|
95
|
+
export async function renderDevErrorPage(
|
|
96
|
+
error: unknown,
|
|
97
|
+
projectRoot?: string,
|
|
98
|
+
hmrOptions?: import('../dev-tools/dev-page-shell.js').DevErrorHmrOptions
|
|
99
|
+
): Promise<Response> {
|
|
95
100
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
96
101
|
const root = projectRoot ?? process.cwd();
|
|
97
102
|
|
|
@@ -100,7 +105,7 @@ export async function renderDevErrorPage(error: unknown, projectRoot?: string):
|
|
|
100
105
|
// Dynamic import — keeps dev-error-page.ts and its transitive deps
|
|
101
106
|
// (dev-error-overlay.ts, @jridgewell/trace-mapping) out of production bundles.
|
|
102
107
|
const { generateDevErrorPage } = await import('../dev-tools/error-page.js');
|
|
103
|
-
html = generateDevErrorPage(err, 'render', root);
|
|
108
|
+
html = generateDevErrorPage(err, 'render', root, hmrOptions);
|
|
104
109
|
// Inject Vite client script so the error overlay fires when HMR connects.
|
|
105
110
|
html = html.replace(
|
|
106
111
|
'</head>',
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* Readable.toWeb() conversion for the Response body.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { Transform } from 'node:stream';
|
|
21
|
+
import { PassThrough, Transform, type Readable } from 'node:stream';
|
|
22
|
+
import { pipeline } from 'node:stream/promises';
|
|
22
23
|
import { createGzip, constants } from 'node:zlib';
|
|
23
24
|
|
|
24
25
|
import {
|
|
@@ -265,37 +266,73 @@ export function createNodeFlightInjector(
|
|
|
265
266
|
const NOINDEX_SCRIPT =
|
|
266
267
|
'<script>document.head.appendChild(Object.assign(document.createElement("meta"),{name:"robots",content:"noindex"}))</script>';
|
|
267
268
|
|
|
269
|
+
function isAbortError(error: unknown): boolean {
|
|
270
|
+
if (error instanceof DOMException && error.name === 'AbortError') return true;
|
|
271
|
+
if (error instanceof Error && error.name === 'AbortError') return true;
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
268
275
|
/**
|
|
269
|
-
* Node
|
|
276
|
+
* Run the Node SSR post-processing pipeline with post-shell error handling.
|
|
277
|
+
*
|
|
278
|
+
* Equivalent to wrapStreamWithErrorHandling() in ssr-render.ts: when the
|
|
279
|
+
* stream errors after the shell has flushed (deny/throw inside Suspense,
|
|
280
|
+
* an RSC read timeout in the flight injector, or an upstream Fizz error),
|
|
281
|
+
* the response closes cleanly with a trailing noindex script instead of
|
|
282
|
+
* aborting the connection mid-body. Abort errors (client disconnect,
|
|
283
|
+
* render-timeout abort) close the output silently — no noindex, no log.
|
|
270
284
|
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
285
|
+
* Why this is a pump function and not a Transform stage (TIM-1019):
|
|
286
|
+
* stream.promises.pipeline() destroys every stage in the chain when any
|
|
287
|
+
* stage errors, so a Transform *inside* the pipeline can never append
|
|
288
|
+
* bytes after a failure — by the time its 'error' listener fires, the
|
|
289
|
+
* transform and everything downstream are already destroyed and push()/
|
|
290
|
+
* end() are no-ops. The output PassThrough therefore lives OUTSIDE the
|
|
291
|
+
* pipeline() call: the last stage pipes into it (pipe() ends the output
|
|
292
|
+
* on upstream success but leaves it open on upstream error), and the
|
|
293
|
+
* pipeline's rejection handler appends the noindex script.
|
|
294
|
+
*
|
|
295
|
+
* Consumer cancellation (e.g. Readable.toWeb() cancel on client
|
|
296
|
+
* disconnect) is wired back into the pipeline via an AbortSignal so the
|
|
297
|
+
* source and all stages are torn down — the teardown that membership in
|
|
298
|
+
* pipeline() used to provide.
|
|
274
299
|
*/
|
|
275
|
-
export function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
300
|
+
export function runNodePipelineWithErrorHandling(
|
|
301
|
+
source: Readable,
|
|
302
|
+
transforms: readonly [Transform, ...Transform[]],
|
|
303
|
+
signal?: AbortSignal
|
|
304
|
+
): PassThrough {
|
|
305
|
+
const output = new PassThrough();
|
|
306
|
+
const last = transforms[transforms.length - 1]!;
|
|
307
|
+
last.pipe(output);
|
|
308
|
+
|
|
309
|
+
// If the consumer destroys the output (client disconnect), abort the
|
|
310
|
+
// pipeline so the Fizz source and flight injector release their
|
|
311
|
+
// resources. On normal completion the pipeline has already settled and
|
|
312
|
+
// the abort is a no-op.
|
|
313
|
+
const consumerGone = new AbortController();
|
|
314
|
+
output.once('close', () => consumerGone.abort());
|
|
315
|
+
|
|
316
|
+
// The cast satisfies pipeline()'s [source, ...transforms, destination]
|
|
317
|
+
// tuple overload — TS can't prove the shape across a spread, but the
|
|
318
|
+
// signature above guarantees a non-empty transform list at compile time.
|
|
319
|
+
const stages = [source, ...transforms] as unknown as readonly [
|
|
320
|
+
Readable,
|
|
321
|
+
...Transform[],
|
|
322
|
+
Transform,
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
pipeline(stages, { signal: consumerGone.signal }).catch((error: unknown) => {
|
|
326
|
+
if (output.writableEnded || output.destroyed) return;
|
|
327
|
+
if (isAbortError(error) || signal?.aborted || consumerGone.signal.aborted) {
|
|
328
|
+
output.end();
|
|
290
329
|
return;
|
|
291
330
|
}
|
|
292
|
-
|
|
293
331
|
logStreamingError({ error });
|
|
294
|
-
|
|
295
|
-
transform.end();
|
|
332
|
+
output.end(Buffer.from(NOINDEX_SCRIPT, 'utf-8'));
|
|
296
333
|
});
|
|
297
334
|
|
|
298
|
-
return
|
|
335
|
+
return output;
|
|
299
336
|
}
|
|
300
337
|
|
|
301
338
|
// ─── Compression ─────────────────────────────────────────────────────────────
|
package/src/server/pipeline.ts
CHANGED
|
@@ -187,6 +187,18 @@ export interface PipelineConfig {
|
|
|
187
187
|
debugComponents?: Array<{ name: string; env: string | null; stack: unknown[] | null }>
|
|
188
188
|
) => void;
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* HMR connection options for dev-only HTML pages (dev 404 page, fallback
|
|
192
|
+
* error page) — set by the dev server after the RSC entry module is
|
|
193
|
+
* imported, like `devPipelineErrorHandler`. The RSC environment can't
|
|
194
|
+
* read the Vite server config (separate module graph), so the dev server
|
|
195
|
+
* plumbs the resolved HMR endpoint (protocol/host/port/path/token) here
|
|
196
|
+
* for the pages' auto-reload WebSocket. See TIM-1067.
|
|
197
|
+
*
|
|
198
|
+
* Undefined in production — zero overhead.
|
|
199
|
+
*/
|
|
200
|
+
devHmrOptions?: import('../dev-tools/dev-page-shell.js').DevErrorHmrOptions;
|
|
201
|
+
|
|
190
202
|
/**
|
|
191
203
|
* Fallback error renderer — called when a catastrophic error escapes the
|
|
192
204
|
* render phase. Produces an HTML Response instead of a bare empty 500.
|
package/src/server/primitives.ts
CHANGED
|
@@ -8,7 +8,10 @@ import { getWaitUntil as _getWaitUntil } from './waituntil-bridge.js';
|
|
|
8
8
|
import { isDebug } from './debug.js';
|
|
9
9
|
import { getRequestSearchString } from './request-context.js';
|
|
10
10
|
import { mergePreservedSearchParams } from '../shared/merge-search-params.js';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
assertRelativeRedirectPath,
|
|
13
|
+
validateExternalRedirectUrl,
|
|
14
|
+
} from '../shared/href-validation.js';
|
|
12
15
|
|
|
13
16
|
// ─── Dev-mode validation ────────────────────────────────────────────────────
|
|
14
17
|
|
|
@@ -207,17 +210,9 @@ export function notFound(): never {
|
|
|
207
210
|
deny(404);
|
|
208
211
|
}
|
|
209
212
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
* Provided for API compatibility with libraries that import `RedirectType`
|
|
214
|
-
* from `next/navigation`. In timber, `redirect()` always uses `replace`
|
|
215
|
-
* semantics (no history entry for the redirect itself).
|
|
216
|
-
*/
|
|
217
|
-
export const RedirectType = {
|
|
218
|
-
push: 'push',
|
|
219
|
-
replace: 'replace',
|
|
220
|
-
} as const;
|
|
213
|
+
// Single source of truth shared with the client next/navigation shim —
|
|
214
|
+
// see shared/redirect-type.ts.
|
|
215
|
+
export { RedirectType } from '../shared/redirect-type.js';
|
|
221
216
|
|
|
222
217
|
// ─── RedirectSignal ─────────────────────────────────────────────────────────
|
|
223
218
|
|
|
@@ -237,12 +232,6 @@ export class RedirectSignal extends Error {
|
|
|
237
232
|
}
|
|
238
233
|
}
|
|
239
234
|
|
|
240
|
-
/** Pattern matching absolute URLs: http(s):// or protocol-relative // */
|
|
241
|
-
const ABSOLUTE_URL_RE = /^(?:[a-zA-Z][a-zA-Z\d+\-.]*:|\/\/)/;
|
|
242
|
-
|
|
243
|
-
// eslint-disable-next-line no-control-regex -- must match all C0 controls to prevent bypass
|
|
244
|
-
const REDIRECT_CONTROL_CHARS_RE = /[\x00-\x1f]/g;
|
|
245
|
-
|
|
246
235
|
/**
|
|
247
236
|
* Options for redirect() — alternative to passing a bare status code.
|
|
248
237
|
*/
|
|
@@ -303,19 +292,9 @@ export function redirect(path: string, statusOrOptions?: number | RedirectOption
|
|
|
303
292
|
if (status < 300 || status > 399) {
|
|
304
293
|
throw new Error(`redirect() requires a 3xx status code, got ${status}.`);
|
|
305
294
|
}
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
// with forward slashes. Browsers normalize \ → / during URL resolution, so
|
|
310
|
-
// "/\evil.com" becomes "//evil.com" (protocol-relative open redirect).
|
|
311
|
-
const cleaned = path.replace(REDIRECT_CONTROL_CHARS_RE, '').trimStart().replaceAll('\\', '/');
|
|
312
|
-
|
|
313
|
-
if (ABSOLUTE_URL_RE.test(cleaned)) {
|
|
314
|
-
throw new Error(
|
|
315
|
-
`redirect() only accepts relative URLs. Got absolute URL: "${path}". ` +
|
|
316
|
-
'Use redirectExternal(url, allowList) for external redirects.'
|
|
317
|
-
);
|
|
318
|
-
}
|
|
295
|
+
// Relative-only validation shared with the client next/navigation shim —
|
|
296
|
+
// strips C0 controls and normalizes backslashes before the scheme check.
|
|
297
|
+
assertRelativeRedirectPath(path);
|
|
319
298
|
|
|
320
299
|
let resolvedPath = path;
|
|
321
300
|
if (preserveSearchParams) {
|
|
@@ -185,7 +185,7 @@ function matchMetadataPathname(
|
|
|
185
185
|
// Non-nestable routes (robots, manifest, favicon) only serve from root
|
|
186
186
|
if (!convention.nestable && segment !== root) return null;
|
|
187
187
|
|
|
188
|
-
const file = findMetadataRouteInSegment(segment, baseName);
|
|
188
|
+
const file = findMetadataRouteInSegment(segment, baseName, convention.nestable);
|
|
189
189
|
if (!file) return null;
|
|
190
190
|
|
|
191
191
|
const ext = file.filePath.slice(file.filePath.lastIndexOf('.') + 1);
|
|
@@ -205,19 +205,35 @@ function matchMetadataPathname(
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
|
-
* Find a metadata route file in a segment, including
|
|
209
|
-
*
|
|
208
|
+
* Find a metadata route file in a segment, including children that match
|
|
209
|
+
* zero URL segments at the segment's own path:
|
|
210
|
+
*
|
|
211
|
+
* - Group children are transparent — they don't add URL depth.
|
|
212
|
+
* - Optional catch-all children ([[...slug]]) match zero segments, so
|
|
213
|
+
* `app/docs/[[...path]]/icon.tsx` serves `/docs/icon`. The non-zero
|
|
214
|
+
* case is handled by the tree walker (requireTerminal:false), but the
|
|
215
|
+
* zero-segment case never reaches the walker — the parent node accepts
|
|
216
|
+
* the fully-consumed prefix first, so the fallback lives here.
|
|
217
|
+
*
|
|
218
|
+
* Optional catch-all descent applies to NESTABLE conventions only. A
|
|
219
|
+
* non-nestable file (robots, manifest, favicon) inside `[[...slug]]` is
|
|
220
|
+
* not at the root — serving it there would bypass the root-only rule.
|
|
221
|
+
* Group descent is unconditional: groups are transparent and a group
|
|
222
|
+
* child of the root IS at the root.
|
|
223
|
+
*
|
|
224
|
+
* The segment's own metadata file wins over a zero-segment child's.
|
|
210
225
|
*/
|
|
211
226
|
function findMetadataRouteInSegment(
|
|
212
227
|
node: ManifestSegmentNode,
|
|
213
|
-
baseName: string
|
|
228
|
+
baseName: string,
|
|
229
|
+
nestable: boolean
|
|
214
230
|
): ManifestFile | null {
|
|
215
231
|
if (node.metadataRoutes?.[baseName]) {
|
|
216
232
|
return node.metadataRoutes[baseName];
|
|
217
233
|
}
|
|
218
234
|
for (const child of node.children) {
|
|
219
|
-
if (child.segmentType === 'group') {
|
|
220
|
-
const found = findMetadataRouteInSegment(child, baseName);
|
|
235
|
+
if (child.segmentType === 'group' || (nestable && child.segmentType === 'optional-catch-all')) {
|
|
236
|
+
const found = findMetadataRouteInSegment(child, baseName, nestable);
|
|
221
237
|
if (found) return found;
|
|
222
238
|
}
|
|
223
239
|
}
|
|
@@ -33,6 +33,7 @@ import { ErrorReconstituter } from '../../client/error-reconstituter.js';
|
|
|
33
33
|
import type { SerializableError } from '../../client/error-reconstituter.js';
|
|
34
34
|
import { loadModule } from '../safe-load.js';
|
|
35
35
|
import { sourceMapError } from '../dev-source-map.js';
|
|
36
|
+
import { isMdxFilePath as isMdxFile } from '../utils/mdx-file.js';
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* A manifest file reference with lazy import and path.
|
|
@@ -44,18 +45,6 @@ export interface GlobalErrorFile {
|
|
|
44
45
|
filePath: string;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/** MDX/markdown extensions — server components that need RSC rendering. */
|
|
48
|
-
const MDX_EXTENSIONS = new Set(['.mdx', '.md']);
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Check if a file path ends with an MDX/markdown extension.
|
|
52
|
-
*/
|
|
53
|
-
function isMdxFile(filePath: string): boolean {
|
|
54
|
-
const dotIndex = filePath.lastIndexOf('.');
|
|
55
|
-
if (dotIndex === -1) return false;
|
|
56
|
-
return MDX_EXTENSIONS.has(filePath.slice(dotIndex));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
48
|
/**
|
|
60
49
|
* Result of walking the segment chain for an error file.
|
|
61
50
|
*/
|
|
@@ -268,7 +268,9 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
268
268
|
const { generateDev404Page, collectRoutes } = await import('../../dev-tools/404-page.js');
|
|
269
269
|
const routes = collectRoutes(manifest.root);
|
|
270
270
|
const pathname = new URL(req.url).pathname;
|
|
271
|
-
|
|
271
|
+
// devHmrOptions is set by the dev server after this module is
|
|
272
|
+
// imported (read at request time, not closure-capture time).
|
|
273
|
+
const html = generateDev404Page(pathname, routes, pipelineConfig.devHmrOptions);
|
|
272
274
|
return new Response(html, {
|
|
273
275
|
status: 404,
|
|
274
276
|
headers: {
|
|
@@ -316,7 +318,10 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
316
318
|
// (TIM-807). Passing manifest.root (segment tree object) here would
|
|
317
319
|
// stringify to "[object Object]" and misclassify app frames as
|
|
318
320
|
// internal, dropping source locations from the dev error page (TIM-808).
|
|
319
|
-
isDev ? manifest.viteRoot : undefined
|
|
321
|
+
isDev ? manifest.viteRoot : undefined,
|
|
322
|
+
// HMR options for the dev error page's auto-reload WebSocket —
|
|
323
|
+
// set by the dev server after import, read at request time.
|
|
324
|
+
isDev ? pipelineConfig.devHmrOptions : undefined
|
|
320
325
|
),
|
|
321
326
|
renderDenyFallback: async (deny, req, responseHeaders, matchedRoute) => {
|
|
322
327
|
// Render the deny page (403.tsx, 404.tsx, etc.) for DenySignals
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { LayoutEntry } from '../deny-renderer.js';
|
|
13
13
|
import { renderDenyPageAsRsc } from '../deny-renderer.js';
|
|
14
14
|
import type { RouteMatch } from '../pipeline.js';
|
|
15
|
+
import { encodeHeadHeader, HEAD_HEADER } from '../../shared/head-header.js';
|
|
15
16
|
import type { RedirectSignal } from '../primitives.js';
|
|
16
17
|
import type { HeadElement, LayoutComponentEntry } from '../route-element-builder.js';
|
|
17
18
|
import type { ManifestSegmentNode } from '../route-matcher.js';
|
|
@@ -175,9 +176,9 @@ export async function buildRscPayloadResponse(
|
|
|
175
176
|
|
|
176
177
|
// Send resolved head elements so the client can update document.title
|
|
177
178
|
// and <meta> tags after SPA navigation. See design/16-metadata.md.
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
180
|
-
responseHeaders.set(
|
|
179
|
+
const encodedHead = encodeHeadHeader(headElements);
|
|
180
|
+
if (encodedHead !== null) {
|
|
181
|
+
responseHeaders.set(HEAD_HEADER, encodedHead);
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
// Send segment metadata so the client can populate its segment cache
|
|
@@ -68,10 +68,12 @@ async function renderDefaultFallback(
|
|
|
68
68
|
* Finds a matching page in the slot's sub-tree for the current route.
|
|
69
69
|
* Falls back to default.tsx if no match, or null if no default.
|
|
70
70
|
*
|
|
71
|
-
* When a match is found, the element is wrapped
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* When a match is found, the element is wrapped per segment in the slot's
|
|
72
|
+
* matched chain with (innermost → outermost):
|
|
73
|
+
* 1. Error boundaries
|
|
74
|
+
* 2. Layout
|
|
75
|
+
* 3. SlotAccessGate if the segment has access.ts (gate OUTSIDE the layout,
|
|
76
|
+
* so a denied layout never executes — see TIM-1074)
|
|
75
77
|
*/
|
|
76
78
|
export async function resolveSlotElement(
|
|
77
79
|
slotNode: ManifestSegmentNode,
|
|
@@ -155,20 +157,25 @@ export async function resolveSlotElement(
|
|
|
155
157
|
element = h(SafeSlotPage, {});
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
// Wrap with error boundaries
|
|
159
|
-
// (everything between slot root and leaf).
|
|
160
|
-
// order as route-element-builder.ts
|
|
161
|
-
// (index 0) is handled
|
|
160
|
+
// Wrap with error boundaries, layouts, and access gates from
|
|
161
|
+
// intermediate slot segments (everything between slot root and leaf).
|
|
162
|
+
// Process innermost-first, same order as route-element-builder.ts
|
|
163
|
+
// handles main segments. The slot root (index 0) is handled below.
|
|
162
164
|
element = await wrapWithIntermediateSegments(slotMatch.chain, element, h);
|
|
163
165
|
|
|
164
|
-
// Wrap
|
|
165
|
-
//
|
|
166
|
+
// Wrap with slot root's layout — INSIDE the access gate, so the layout
|
|
167
|
+
// server component never executes when access.ts denies. See TIM-1074.
|
|
168
|
+
element = await wrapWithLayout(slotNode, element, h);
|
|
169
|
+
|
|
170
|
+
// Wrap in SlotAccessGate if slot root has access.ts. The gate is
|
|
171
|
+
// OUTSIDE the layout, matching the segment model in
|
|
172
|
+
// route-element-builder.ts: a denied layout must not render, or it
|
|
173
|
+
// would fetch/serialize protected data into the RSC Flight stream.
|
|
174
|
+
// On denial: denied.tsx → default.tsx → null (graceful degradation),
|
|
175
|
+
// rendered WITHOUT the denied slot's own layout.
|
|
166
176
|
// See design/04-authorization.md §"Slot-Level Auth".
|
|
167
177
|
element = await wrapWithAccessGate(slotNode, element, h);
|
|
168
178
|
|
|
169
|
-
// Wrap with slot root's layout (outermost, outside access gate)
|
|
170
|
-
element = await wrapWithLayout(slotNode, element, h);
|
|
171
|
-
|
|
172
179
|
// Wrap with slot root's error boundaries (outermost)
|
|
173
180
|
element = await wrapSegmentWithErrorBoundaries(slotNode, element, h);
|
|
174
181
|
|
|
@@ -204,6 +211,12 @@ export async function resolveSlotElement(
|
|
|
204
211
|
* not just the slot root. Without this, nested slot children (e.g.,
|
|
205
212
|
* @admin/reports/access.ts) and intercepting route children with access.ts
|
|
206
213
|
* would bypass authorization. See TIM-999.
|
|
214
|
+
*
|
|
215
|
+
* Wrap order per segment (innermost → outermost): error boundaries → layout
|
|
216
|
+
* → access gate. The gate must be OUTSIDE the layout so a denying access.ts
|
|
217
|
+
* prevents the layout server component from executing — otherwise the layout
|
|
218
|
+
* would fetch/serialize protected data into the RSC Flight stream. Matches
|
|
219
|
+
* route-element-builder.ts. See TIM-1074.
|
|
207
220
|
*/
|
|
208
221
|
async function wrapWithIntermediateSegments(
|
|
209
222
|
chain: ManifestSegmentNode[],
|
|
@@ -213,8 +226,8 @@ async function wrapWithIntermediateSegments(
|
|
|
213
226
|
for (let i = chain.length - 1; i > 0; i--) {
|
|
214
227
|
const seg = chain[i];
|
|
215
228
|
element = await wrapSegmentWithErrorBoundaries(seg, element, h);
|
|
216
|
-
element = await wrapWithAccessGate(seg, element, h);
|
|
217
229
|
element = await wrapWithLayout(seg, element, h);
|
|
230
|
+
element = await wrapWithAccessGate(seg, element, h);
|
|
218
231
|
}
|
|
219
232
|
return element;
|
|
220
233
|
}
|
package/src/server/ssr-entry.ts
CHANGED
|
@@ -55,9 +55,7 @@ let _nodeStreamImports: {
|
|
|
55
55
|
createNodeHeadInjector: typeof import('./node-stream-transforms.js').createNodeHeadInjector;
|
|
56
56
|
createNodeMoveSuffixTransform: typeof import('./node-stream-transforms.js').createNodeMoveSuffixTransform;
|
|
57
57
|
createNodeFlightInjector: typeof import('./node-stream-transforms.js').createNodeFlightInjector;
|
|
58
|
-
|
|
59
|
-
pipeline: typeof import('node:stream/promises').pipeline;
|
|
60
|
-
PassThrough: typeof import('node:stream').PassThrough;
|
|
58
|
+
runNodePipelineWithErrorHandling: typeof import('./node-stream-transforms.js').runNodePipelineWithErrorHandling;
|
|
61
59
|
ReadableFromWeb: (
|
|
62
60
|
webStream: import('stream/web').ReadableStream
|
|
63
61
|
) => import('node:stream').Readable;
|
|
@@ -65,9 +63,8 @@ let _nodeStreamImports: {
|
|
|
65
63
|
|
|
66
64
|
if (useNodeStreams) {
|
|
67
65
|
try {
|
|
68
|
-
const [transforms,
|
|
66
|
+
const [transforms, stream] = await Promise.all([
|
|
69
67
|
import('./node-stream-transforms.js'),
|
|
70
|
-
import('node:stream/promises'),
|
|
71
68
|
import('node:stream'),
|
|
72
69
|
]);
|
|
73
70
|
_nodeStreamImports = {
|
|
@@ -75,9 +72,7 @@ if (useNodeStreams) {
|
|
|
75
72
|
createNodeHeadInjector: transforms.createNodeHeadInjector,
|
|
76
73
|
createNodeMoveSuffixTransform: transforms.createNodeMoveSuffixTransform,
|
|
77
74
|
createNodeFlightInjector: transforms.createNodeFlightInjector,
|
|
78
|
-
|
|
79
|
-
pipeline: streamPromises.pipeline,
|
|
80
|
-
PassThrough: stream.PassThrough,
|
|
75
|
+
runNodePipelineWithErrorHandling: transforms.runNodePipelineWithErrorHandling,
|
|
81
76
|
ReadableFromWeb: (webStream: import('stream/web').ReadableStream) =>
|
|
82
77
|
stream.Readable.fromWeb(webStream) as import('node:stream').Readable,
|
|
83
78
|
};
|
|
@@ -192,9 +187,7 @@ export async function handleSsr(
|
|
|
192
187
|
createNodeHeadInjector,
|
|
193
188
|
createNodeMoveSuffixTransform,
|
|
194
189
|
createNodeFlightInjector,
|
|
195
|
-
|
|
196
|
-
pipeline,
|
|
197
|
-
PassThrough,
|
|
190
|
+
runNodePipelineWithErrorHandling,
|
|
198
191
|
} = _nodeStreamImports;
|
|
199
192
|
|
|
200
193
|
const renderTimeoutMs = _runtimeConfig.renderTimeoutMs ?? undefined;
|
|
@@ -224,15 +217,18 @@ export async function handleSsr(
|
|
|
224
217
|
// React 19.3+ emits <!DOCTYPE html> automatically when the root
|
|
225
218
|
// element is <html>, so no framework-level doctype prepend needed.
|
|
226
219
|
//
|
|
227
|
-
// Pipeline: buffer →
|
|
220
|
+
// Pipeline: buffer → headInjector → flightInjector → moveSuffix → output
|
|
228
221
|
//
|
|
229
222
|
// 1. buffer: coalesces Fizz micro-chunks into coherent fragments (TIM-528)
|
|
230
|
-
// 2.
|
|
231
|
-
// 3.
|
|
232
|
-
// 4.
|
|
233
|
-
//
|
|
223
|
+
// 2. headInjector: injects metadata before </head>
|
|
224
|
+
// 3. flightInjector: interleaves RSC scripts between HTML chunks (TIM-529)
|
|
225
|
+
// 4. moveSuffix: strips </body></html>, re-emits at end (TIM-530)
|
|
226
|
+
//
|
|
227
|
+
// Post-shell error handling lives in the pump, not in a pipeline
|
|
228
|
+
// stage: pipeline() destroys every stage on failure, so only an
|
|
229
|
+
// output kept outside the pipeline can append the noindex script
|
|
230
|
+
// and close cleanly (TIM-1019).
|
|
234
231
|
const bufferedTransform = createNodeBufferedTransform();
|
|
235
|
-
const errorHandler = createNodeErrorHandler(navContext.signal);
|
|
236
232
|
const headInjector = createNodeHeadInjector(navContext.headHtml);
|
|
237
233
|
const flightInjector = createNodeFlightInjector(navContext.rscStream, {
|
|
238
234
|
renderTimeoutMs,
|
|
@@ -240,18 +236,11 @@ export async function handleSsr(
|
|
|
240
236
|
});
|
|
241
237
|
const moveSuffix = createNodeMoveSuffixTransform();
|
|
242
238
|
|
|
243
|
-
const output =
|
|
244
|
-
pipeline(
|
|
239
|
+
const output = runNodePipelineWithErrorHandling(
|
|
245
240
|
nodeHtmlStream,
|
|
246
|
-
bufferedTransform,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
flightInjector,
|
|
250
|
-
moveSuffix,
|
|
251
|
-
output
|
|
252
|
-
).catch(() => {
|
|
253
|
-
// Pipeline errors are handled by errorHandler transform
|
|
254
|
-
});
|
|
241
|
+
[bufferedTransform, headInjector, flightInjector, moveSuffix],
|
|
242
|
+
navContext.signal
|
|
243
|
+
);
|
|
255
244
|
const _pipelineEnd = performance.now();
|
|
256
245
|
|
|
257
246
|
// Record SSR sub-timings for Server-Timing header (detailed mode).
|
|
@@ -75,9 +75,10 @@ export interface SlotDeniedResolution<TFile> {
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* Maps legacy file convention names to their corresponding HTTP status codes.
|
|
78
|
-
* Only used in the 4xx component fallback chain.
|
|
78
|
+
* Only used in the 4xx component fallback chain. Exported so the in-tree
|
|
79
|
+
* deny chain (deny-boundary.ts) uses the same mapping — see TIM-1081.
|
|
79
80
|
*/
|
|
80
|
-
const LEGACY_FILE_TO_STATUS: Record<string, number> = {
|
|
81
|
+
export const LEGACY_FILE_TO_STATUS: Record<string, number> = {
|
|
81
82
|
'not-found': 404,
|
|
82
83
|
'forbidden': 403,
|
|
83
84
|
'unauthorized': 401,
|