@timber-js/app 0.2.0-alpha.200 → 0.2.0-alpha.201
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/bin/timber.mjs +15 -1
- package/dist/_chunks/{actions-d1hCqnU3.js → actions-HUdJADAD.js} +3 -3
- package/dist/_chunks/{actions-d1hCqnU3.js.map → actions-HUdJADAD.js.map} +1 -1
- package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/{cache-api-ByagcC-J.js → cache-api-CAPbZTga.js} +2 -2
- package/dist/_chunks/{cache-api-ByagcC-J.js.map → cache-api-CAPbZTga.js.map} +1 -1
- package/dist/_chunks/{chains-Bpb0W4ax.js → chains-CBNA0Ozj.js} +3 -3
- package/dist/_chunks/{chains-Bpb0W4ax.js.map → chains-CBNA0Ozj.js.map} +1 -1
- package/dist/_chunks/{cli-check-D6VolrDV.js → cli-check-BzMGuIH6.js} +3 -3
- package/dist/_chunks/{cli-check-D6VolrDV.js.map → cli-check-BzMGuIH6.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js → cli-schema-sync-DnXqcIIj.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js.map → cli-schema-sync-DnXqcIIj.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-DxX1SU0g.js} +3 -3
- package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-DxX1SU0g.js.map} +1 -1
- package/dist/_chunks/{convention-lint-fRkwVwEH.js → convention-lint-DHOFvX5s.js} +5 -95
- package/dist/_chunks/convention-lint-DHOFvX5s.js.map +1 -0
- package/dist/_chunks/csp-nonce-hOGniaG4.js +227 -0
- package/dist/_chunks/csp-nonce-hOGniaG4.js.map +1 -0
- package/dist/_chunks/dev-server-DioP7tkQ.js +2288 -0
- package/dist/_chunks/dev-server-DioP7tkQ.js.map +1 -0
- package/dist/_chunks/{error-boundary-BfPHZjm0.js → error-boundary-BQKxl6EX.js} +11 -17
- package/dist/_chunks/{error-boundary-BfPHZjm0.js.map → error-boundary-BQKxl6EX.js.map} +1 -1
- package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-Cv3njEH8.js} +2 -2
- package/dist/_chunks/{graph-cache-CP4GEmf9.js.map → graph-cache-Cv3njEH8.js.map} +1 -1
- package/dist/_chunks/{live-graph-D_2D32Ad.js → live-graph-VjHFF5EV.js} +3 -3
- package/dist/_chunks/{live-graph-D_2D32Ad.js.map → live-graph-VjHFF5EV.js.map} +1 -1
- package/dist/_chunks/{logger-uLBuGKDI.js → logger-DqJ2VoAY.js} +450 -458
- package/dist/_chunks/logger-DqJ2VoAY.js.map +1 -0
- package/dist/_chunks/{segment-keys-lqtdookO.js → metadata-routes-DSDjM_hJ.js} +2 -61
- package/dist/_chunks/metadata-routes-DSDjM_hJ.js.map +1 -0
- package/dist/_chunks/{poison-scan-Bm9Yyqk9.js → poison-scan-lEbz4pQE.js} +2 -2
- package/dist/_chunks/{poison-scan-Bm9Yyqk9.js.map → poison-scan-lEbz4pQE.js.map} +1 -1
- package/dist/_chunks/{scanner-AiazgH_f.js → scanner-B_tnqFcF.js} +37 -137
- package/dist/_chunks/scanner-B_tnqFcF.js.map +1 -0
- package/dist/_chunks/segment-keys-D5hu1hz4.js +62 -0
- package/dist/_chunks/segment-keys-D5hu1hz4.js.map +1 -0
- package/dist/_chunks/tree-match-CdbvYTBz.js +122 -0
- package/dist/_chunks/tree-match-CdbvYTBz.js.map +1 -0
- package/dist/_chunks/{walkers-B6XUtmqK.js → walkers-Cm3PC5JT.js} +2 -2
- package/dist/_chunks/{walkers-B6XUtmqK.js.map → walkers-Cm3PC5JT.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +3 -3
- package/dist/cache/index.js +1 -1
- package/dist/cli.d.ts +7 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +44 -7
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +20 -1
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +3 -15
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/error-reconstituter.d.ts +4 -4
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/internal.js +1 -1
- package/dist/dev-tools/debug-channel.d.ts +55 -0
- package/dist/dev-tools/debug-channel.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -2119
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts +7 -0
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/scanner.d.ts +4 -4
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts +2 -2
- package/dist/server/deny-boundary.d.ts +3 -5
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +0 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +5 -12
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +20 -195
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline.d.ts +8 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +15 -13
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +3 -7
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +6 -27
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +1 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +1 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/docs/api/30-api-server.mdx +5 -5
- package/docs/api/34-api-config.mdx +2 -2
- package/docs/api/36-cli.mdx +1 -1
- package/docs/learn/12-error-handling.mdx +7 -8
- package/package.json +1 -2
- package/src/cli.ts +62 -8
- package/src/client/browser-entry/action-dispatch.ts +12 -0
- package/src/client/browser-entry/action-queue.ts +70 -8
- package/src/client/browser-entry/rsc-stream.ts +78 -22
- package/src/client/error-boundary.tsx +13 -39
- package/src/client/error-reconstituter.tsx +5 -5
- package/src/dev-tools/debug-channel.ts +151 -0
- package/src/index.ts +8 -12
- package/src/plugins/dev-server.ts +58 -7
- package/src/plugins/mdx.ts +2 -1
- package/src/routing/convention-lint.ts +4 -111
- package/src/routing/scanner.ts +33 -22
- package/src/server/access-gate.tsx +3 -3
- package/src/server/deny-boundary.ts +5 -23
- package/src/server/deny-renderer.ts +4 -7
- package/src/server/error-boundary-wrapper.ts +11 -35
- package/src/server/pipeline.ts +9 -0
- package/src/server/primitives.ts +20 -26
- package/src/server/rsc-entry/error-renderer.ts +15 -43
- package/src/server/rsc-entry/helpers.ts +10 -67
- package/src/server/rsc-entry/index.ts +1 -0
- package/src/server/rsc-entry/render-route.ts +12 -1
- package/src/server/rsc-entry/rsc-stream.ts +27 -24
- package/src/server/rsc-entry/ssr-renderer.ts +7 -1
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +0 -1
- package/dist/_chunks/logger-uLBuGKDI.js.map +0 -1
- package/dist/_chunks/scanner-AiazgH_f.js.map +0 -1
- package/dist/_chunks/segment-keys-lqtdookO.js.map +0 -1
- package/dist/server/utils/mdx-file.d.ts +0 -17
- package/dist/server/utils/mdx-file.d.ts.map +0 -1
- package/src/server/utils/mdx-file.ts +0 -22
|
@@ -17,10 +17,9 @@
|
|
|
17
17
|
* Design doc: 07-routing.md, 10-error-handling.md
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { existsSync
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
21
|
import type { RouteFile, RouteTree, SegmentNode } from './types.ts';
|
|
22
22
|
import { swallow } from '../server/logger.ts';
|
|
23
|
-
import { isMdxFilePath } from '../server/utils/mdx-file.ts';
|
|
24
23
|
import {
|
|
25
24
|
fileDefaultExportIsLocalValue,
|
|
26
25
|
fileHasAnyExport,
|
|
@@ -80,11 +79,6 @@ export function lintConventions(tree: RouteTree, appDir: string): ConventionWarn
|
|
|
80
79
|
checkFileUseClientDirective(tree.globalError, 'global-error', warnings);
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
// Check 8: Wildcard MDX status files (4xx.mdx, 5xx.mdx, error.mdx) that
|
|
84
|
-
// reference `props` — props resolve on deny and throw paths but are empty
|
|
85
|
-
// on the boundary path. See design/10-error-handling.md §"MDX Props by Path".
|
|
86
|
-
checkWildcardMdxProps(tree.root, warnings);
|
|
87
|
-
|
|
88
82
|
return warnings;
|
|
89
83
|
}
|
|
90
84
|
|
|
@@ -335,10 +329,6 @@ function checkFileDefaultExport(
|
|
|
335
329
|
* error path. The raw message ("Functions cannot be passed directly to Client
|
|
336
330
|
* Components") names neither the file nor the fix (TIM-1329).
|
|
337
331
|
*
|
|
338
|
-
* MDX status files are exempt: they are server components by design and the
|
|
339
|
-
* renderer pre-renders them into `fallbackElement` instead. See TIM-503 and
|
|
340
|
-
* `server/error-boundary-wrapper.ts`.
|
|
341
|
-
*
|
|
342
332
|
* `.json` status files never reach React at all, and are not in `statusFiles`.
|
|
343
333
|
*/
|
|
344
334
|
function checkClientBoundaryFiles(node: SegmentNode, warnings: ConventionWarning[]): void {
|
|
@@ -392,17 +382,9 @@ function checkFileUseClientDirective(
|
|
|
392
382
|
convention: string,
|
|
393
383
|
warnings: ConventionWarning[]
|
|
394
384
|
): void {
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
// The exemption is the *complement* of the renderer's own test, using the
|
|
400
|
-
// renderer's own helper: everything `error-boundary-wrapper.ts` and
|
|
401
|
-
// `error-renderer.ts` do not treat as MDX, they pass as a component. An
|
|
402
|
-
// independent "is this a script extension?" list here would be a second
|
|
403
|
-
// expression of that fact, free to drift into a gap where a file is a
|
|
404
|
-
// component to the renderer and exempt to the linter.
|
|
405
|
-
if (isMdxFilePath(file.filePath)) return;
|
|
385
|
+
// Only script extensions need the 'use client' check — .json status files
|
|
386
|
+
// never reach React at all, and are not in `statusFiles`.
|
|
387
|
+
if (!isScriptExtension(file.extension)) return;
|
|
406
388
|
|
|
407
389
|
const filePath = file.filePath;
|
|
408
390
|
const label = `${convention}.${file.extension}`;
|
|
@@ -449,95 +431,6 @@ function checkFileUseClientDirective(
|
|
|
449
431
|
});
|
|
450
432
|
}
|
|
451
433
|
|
|
452
|
-
// ─── Check: Wildcard MDX Props ──────────────────────────────────────────────
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Wildcard status file keys in `statusFiles` whose props differ by path.
|
|
456
|
-
* `error` is not here — the scanner stores it in `node.error`, not
|
|
457
|
-
* `node.statusFiles`, and the check handles it separately below.
|
|
458
|
-
*/
|
|
459
|
-
const WILDCARD_MDX_STATUS_KEYS = new Set(['4xx', '5xx']);
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Warn when a wildcard MDX status file (`4xx.mdx`, `5xx.mdx`, `error.mdx`)
|
|
463
|
-
* appears to reference `props` in its source.
|
|
464
|
-
*
|
|
465
|
-
* These files are pre-rendered into `fallbackElement` on the error-boundary
|
|
466
|
-
* path, before the error happens, so they receive empty or partial props.
|
|
467
|
-
* On the deny and error-renderer paths they receive the real props. An author
|
|
468
|
-
* writing `{props.status}` gets the number on some paths and `undefined` on
|
|
469
|
-
* others — same source, different output.
|
|
470
|
-
*
|
|
471
|
-
* A text scan for `props` is acceptable here because the check is `level:
|
|
472
|
-
* 'warn'` — a false positive costs a printed line, not a refused build. A
|
|
473
|
-
* sound check would need the MDX compiler, which is optional, async, and
|
|
474
|
-
* scoped to the RSC environment. See TIM-1333.
|
|
475
|
-
*
|
|
476
|
-
* Exact-code MDX files (e.g. `503.mdx`) are unaffected — all four paths
|
|
477
|
-
* agree on their status.
|
|
478
|
-
*/
|
|
479
|
-
function checkWildcardMdxProps(node: SegmentNode, warnings: ConventionWarning[]): void {
|
|
480
|
-
if (node.statusFiles) {
|
|
481
|
-
for (const [key, file] of Object.entries(node.statusFiles)) {
|
|
482
|
-
if (WILDCARD_MDX_STATUS_KEYS.has(key) && isMdxFilePath(file.filePath)) {
|
|
483
|
-
checkMdxFileForProps(file.filePath, key, warnings);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
if (node.error && isMdxFilePath(node.error.filePath)) {
|
|
488
|
-
checkMdxFileForProps(node.error.filePath, 'error', warnings);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
for (const child of node.children) {
|
|
492
|
-
checkWildcardMdxProps(child, warnings);
|
|
493
|
-
}
|
|
494
|
-
for (const slot of Object.values(node.slots)) {
|
|
495
|
-
checkWildcardMdxProps(slot, warnings);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Check if an MDX file's source appears to reference `props`.
|
|
501
|
-
*
|
|
502
|
-
* MDX passes a `props` object to the default export function. A reference to
|
|
503
|
-
* `props.status` or `props.dangerouslyPassData` in a wildcard file works on
|
|
504
|
-
* the deny/throw paths but renders `undefined` on the boundary path.
|
|
505
|
-
*/
|
|
506
|
-
function checkMdxFileForProps(
|
|
507
|
-
filePath: string,
|
|
508
|
-
convention: string,
|
|
509
|
-
warnings: ConventionWarning[]
|
|
510
|
-
): void {
|
|
511
|
-
let source: string;
|
|
512
|
-
try {
|
|
513
|
-
source = readFileSync(filePath, 'utf-8');
|
|
514
|
-
} catch {
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// MDX exposes props via the default export: export default function(props) { ... }
|
|
519
|
-
// and via JSX expressions: {props.status}. A text scan is intentionally
|
|
520
|
-
// broad — false positives are cheap at warn level.
|
|
521
|
-
if (!/\bprops\b/.test(source)) return;
|
|
522
|
-
|
|
523
|
-
const label = `${convention}.${filePath.endsWith('.md') ? 'md' : 'mdx'}`;
|
|
524
|
-
warnings.push({
|
|
525
|
-
id: `MDX_WILDCARD_PROPS:${filePath}`,
|
|
526
|
-
summary: `${label} references props, which are empty on the error boundary path: ${filePath}`,
|
|
527
|
-
details:
|
|
528
|
-
` File: ${filePath}\n\n` +
|
|
529
|
-
` Wildcard MDX status files (${convention}.mdx) are pre-rendered into\n` +
|
|
530
|
-
' fallbackElement before the error occurs. On that path, props are\n' +
|
|
531
|
-
` empty${convention === 'error' ? '' : ' (no status, no dangerouslyPassData)'}. On the deny() and error-renderer\n` +
|
|
532
|
-
' paths, props.status resolves correctly.\n\n' +
|
|
533
|
-
' If this file uses only static content and "props" appears in prose,\n' +
|
|
534
|
-
' this warning is a false positive — ignore it.\n\n' +
|
|
535
|
-
' To fix: Use only static content in wildcard MDX status files, or\n' +
|
|
536
|
-
' use an exact-code file (e.g. 404.mdx) where all paths agree.\n',
|
|
537
|
-
level: 'warn',
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
434
|
// ─── Check: App Directory Exists ────────────────────────────────────────────
|
|
542
435
|
|
|
543
436
|
/**
|
package/src/routing/scanner.ts
CHANGED
|
@@ -29,8 +29,9 @@ import { ENCODED_SEPARATOR_RE, NULL_BYTE_RE } from '../server/canonicalize.ts';
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)
|
|
32
|
+
* `error` is excluded — it only accepts script extensions (TIM-1505).
|
|
32
33
|
*/
|
|
33
|
-
const PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', '
|
|
34
|
+
const PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', 'default', 'denied']);
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* File convention names that are always .ts/.tsx (never .mdx etc.)
|
|
@@ -46,10 +47,10 @@ const STATUS_CODE_PATTERN = /^(\d{3}|[45]xx)$/;
|
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Whether a basename + extension pair is a route-convention file the
|
|
49
|
-
* framework itself imports: page/layout/
|
|
50
|
-
* global-error on
|
|
51
|
-
* .ts/.tsx/.js/.jsx only, and
|
|
52
|
-
*
|
|
50
|
+
* framework itself imports: page/layout/default/denied on pageExtensions,
|
|
51
|
+
* error/global-error and status-code files on script extensions only,
|
|
52
|
+
* middleware/access/route/proxy on .ts/.tsx/.js/.jsx only, and
|
|
53
|
+
* status-code .json files — mirroring this scanner's own recognition
|
|
53
54
|
* rules per family.
|
|
54
55
|
*
|
|
55
56
|
* Exported for the analyze classifier so its provisional classification
|
|
@@ -62,9 +63,11 @@ export function isRouteConventionFile(
|
|
|
62
63
|
pageExtensions: readonly string[] = DEFAULT_PAGE_EXTENSIONS
|
|
63
64
|
): boolean {
|
|
64
65
|
const isPageExt = pageExtensions.includes(ext);
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
if (
|
|
66
|
+
const isScriptExt = /^[jt]sx?$/.test(ext);
|
|
67
|
+
if (PAGE_EXT_CONVENTIONS.has(base) && isPageExt) return true;
|
|
68
|
+
if ((base === 'error' || base === 'global-error') && isScriptExt) return true;
|
|
69
|
+
if ((FIXED_CONVENTIONS.has(base) || base === 'proxy') && isScriptExt) return true;
|
|
70
|
+
if (STATUS_CODE_PATTERN.test(base) && (isScriptExt || ext === 'json')) return true;
|
|
68
71
|
return false;
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -91,8 +94,9 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
|
|
|
91
94
|
|
|
92
95
|
// Check for global-error.{tsx,ts,jsx,js} at app root.
|
|
93
96
|
// Tier 2 error page — renders standalone (no layouts) when no segment-level
|
|
94
|
-
// error file is found.
|
|
95
|
-
|
|
97
|
+
// error file is found. Script extensions only — MDX not supported (TIM-1505).
|
|
98
|
+
// See design/10-error-handling.md §"Tier 2".
|
|
99
|
+
const globalErrorFile = findScriptExtFile(appDir, 'global-error');
|
|
96
100
|
if (globalErrorFile) {
|
|
97
101
|
tree.globalError = globalErrorFile;
|
|
98
102
|
}
|
|
@@ -197,9 +201,9 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
|
|
|
197
201
|
const ext = extname(entry).slice(1); // remove leading dot
|
|
198
202
|
const name = basename(entry, `.${ext}`);
|
|
199
203
|
|
|
200
|
-
// Page-extension conventions (page, layout,
|
|
204
|
+
// Page-extension conventions (page, layout, default, denied)
|
|
201
205
|
if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {
|
|
202
|
-
const key = name as 'page' | 'layout' | '
|
|
206
|
+
const key = name as 'page' | 'layout' | 'default' | 'denied';
|
|
203
207
|
const existing = node[key];
|
|
204
208
|
if (existing) {
|
|
205
209
|
throwDuplicateConvention(name, existing.filePath, fullPath);
|
|
@@ -208,6 +212,16 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
|
|
|
208
212
|
continue;
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
// error.tsx — script extensions only, not MDX/MD (TIM-1505).
|
|
216
|
+
if (name === 'error' && /^[jt]sx?$/.test(ext)) {
|
|
217
|
+
const existing = node.error;
|
|
218
|
+
if (existing) {
|
|
219
|
+
throwDuplicateConvention('error', existing.filePath, fullPath);
|
|
220
|
+
}
|
|
221
|
+
node.error = { filePath: fullPath, extension: ext };
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
211
225
|
// Fixed conventions (middleware, access, route) — only
|
|
212
226
|
// .ts/.tsx/.js/.jsx, never pageExtensions like .mdx. The regex is
|
|
213
227
|
// anchored: mts/cts/mjs/cjs (and stray tail matches like `.foots`)
|
|
@@ -232,8 +246,8 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
|
|
|
232
246
|
continue;
|
|
233
247
|
}
|
|
234
248
|
|
|
235
|
-
// Status-code files (401.tsx, 4xx.tsx, 503.tsx, 5xx.tsx)
|
|
236
|
-
if (STATUS_CODE_PATTERN.test(name) &&
|
|
249
|
+
// Status-code files (401.tsx, 4xx.tsx, 503.tsx, 5xx.tsx) — script extensions only (TIM-1505)
|
|
250
|
+
if (STATUS_CODE_PATTERN.test(name) && /^[jt]sx?$/.test(ext)) {
|
|
237
251
|
if (!node.statusFiles) {
|
|
238
252
|
node.statusFiles = {};
|
|
239
253
|
}
|
|
@@ -652,15 +666,12 @@ function findFixedFile(dirPath: string, name: string): RouteFile | undefined {
|
|
|
652
666
|
}
|
|
653
667
|
|
|
654
668
|
/**
|
|
655
|
-
* Find a file
|
|
656
|
-
* Used for app-root conventions like global-error that
|
|
669
|
+
* Find a file with script extensions (tsx, ts, jsx, js) in a directory.
|
|
670
|
+
* Used for app-root conventions like global-error that only accept script
|
|
671
|
+
* extensions (TIM-1505).
|
|
657
672
|
*/
|
|
658
|
-
function
|
|
659
|
-
|
|
660
|
-
name: string,
|
|
661
|
-
extSet: Set<string>
|
|
662
|
-
): RouteFile | undefined {
|
|
663
|
-
for (const ext of extSet) {
|
|
673
|
+
function findScriptExtFile(dirPath: string, name: string): RouteFile | undefined {
|
|
674
|
+
for (const ext of ['tsx', 'ts', 'jsx', 'js']) {
|
|
664
675
|
const fullPath = join(dirPath, `${name}.${ext}`);
|
|
665
676
|
try {
|
|
666
677
|
if (statSync(fullPath).isFile()) {
|
|
@@ -86,7 +86,7 @@ export interface AccessGateProps {
|
|
|
86
86
|
* On denial, renders denied.tsx → default.tsx → null instead of failing the page.
|
|
87
87
|
*
|
|
88
88
|
* DeniedComponent is passed instead of a pre-built element so that
|
|
89
|
-
* SlotAccessGate can forward DenySignal.data as
|
|
89
|
+
* SlotAccessGate can forward DenySignal.data as the data prop
|
|
90
90
|
* and slotName as the slot prop after catching the signal.
|
|
91
91
|
*/
|
|
92
92
|
export interface SlotAccessGateProps {
|
|
@@ -205,7 +205,7 @@ async function accessGateFallback(
|
|
|
205
205
|
* a protocol concern. The parent layout and sibling slots still render.
|
|
206
206
|
*
|
|
207
207
|
* DeniedComponent is passed instead of a pre-built element so that
|
|
208
|
-
* DenySignal.data can be forwarded as the
|
|
208
|
+
* DenySignal.data can be forwarded as the data prop
|
|
209
209
|
* and the slot name can be passed as the slot prop. See TIM-488.
|
|
210
210
|
*
|
|
211
211
|
* redirect() in slot access.ts is a dev-mode error — redirecting from a
|
|
@@ -276,6 +276,6 @@ function buildDeniedFallback(
|
|
|
276
276
|
if (!DeniedComponent) return null;
|
|
277
277
|
return createElement(DeniedComponent, {
|
|
278
278
|
slot: slotName,
|
|
279
|
-
|
|
279
|
+
data,
|
|
280
280
|
});
|
|
281
281
|
}
|
|
@@ -44,7 +44,6 @@ import { loadModule } from './safe-load.ts';
|
|
|
44
44
|
import type { ManifestLoader } from './safe-load.ts';
|
|
45
45
|
import { withSpan } from './tracing.ts';
|
|
46
46
|
import { isValidElementType, type LoadedComponent } from './utils/element-type.ts';
|
|
47
|
-
import { isMdxFilePath } from './utils/mdx-file.ts';
|
|
48
47
|
import type { ManifestSegmentNode } from './route-matcher.ts';
|
|
49
48
|
|
|
50
49
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
@@ -61,8 +60,6 @@ export interface DenyPageEntry {
|
|
|
61
60
|
* { error, digest, reset } contract, not bare deny props. See TIM-1081.
|
|
62
61
|
*/
|
|
63
62
|
kind: 'status' | 'error';
|
|
64
|
-
/** MDX files are server components — rendered with plain props, never ErrorReconstituter. */
|
|
65
|
-
isMdx: boolean;
|
|
66
63
|
/**
|
|
67
64
|
* Position in the segment chain (0 = root, ascending toward leaf).
|
|
68
65
|
* Used by `matchDenyEntry` to reconstruct per-segment ordering for 5xx
|
|
@@ -105,7 +102,7 @@ export interface DenyPageEntry {
|
|
|
105
102
|
* resolution that already carries the position, and would otherwise have to
|
|
106
103
|
* invent an owner it never reads.
|
|
107
104
|
*/
|
|
108
|
-
export type DenyEntryShape = Pick<DenyPageEntry, 'component' | 'kind'
|
|
105
|
+
export type DenyEntryShape = Pick<DenyPageEntry, 'component' | 'kind'>;
|
|
109
106
|
|
|
110
107
|
/**
|
|
111
108
|
* Where a matched deny page must render, relative to the catcher that
|
|
@@ -185,7 +182,6 @@ export async function buildDenyPageChain(
|
|
|
185
182
|
status,
|
|
186
183
|
component,
|
|
187
184
|
kind: 'status',
|
|
188
|
-
isMdx: isMdxFilePath(file.filePath),
|
|
189
185
|
segmentIndex: i,
|
|
190
186
|
ownerKey: segmentKeys[i],
|
|
191
187
|
});
|
|
@@ -204,7 +200,6 @@ export async function buildDenyPageChain(
|
|
|
204
200
|
status: categoryStatus,
|
|
205
201
|
component,
|
|
206
202
|
kind: 'status',
|
|
207
|
-
isMdx: isMdxFilePath(file.filePath),
|
|
208
203
|
segmentIndex: i,
|
|
209
204
|
ownerKey: segmentKeys[i],
|
|
210
205
|
});
|
|
@@ -225,7 +220,6 @@ export async function buildDenyPageChain(
|
|
|
225
220
|
status: null,
|
|
226
221
|
component,
|
|
227
222
|
kind: 'error',
|
|
228
|
-
isMdx: isMdxFilePath(segment.error.filePath),
|
|
229
223
|
segmentIndex: i,
|
|
230
224
|
ownerKey: segmentKeys[i],
|
|
231
225
|
});
|
|
@@ -333,13 +327,6 @@ export function placeDenyPage(
|
|
|
333
327
|
if (!entry) return null;
|
|
334
328
|
|
|
335
329
|
if (entry.ownerKey !== selfKey) {
|
|
336
|
-
// An MDX owner cannot be addressed. Its boundary holds a `fallbackElement`
|
|
337
|
-
// pre-rendered at tree-build time — before this status or its data
|
|
338
|
-
// existed — so hoisting to it would render a page that never learns what
|
|
339
|
-
// was denied (a category `4xx.mdx` gets no `status` prop at all). Leave it
|
|
340
|
-
// unaddressed so the re-render fallback serves it, which is the documented
|
|
341
|
-
// MDX path and resolves the position correctly on its own. See TIM-503.
|
|
342
|
-
if (entry.isMdx) return null;
|
|
343
330
|
return { kind: 'hoist', ownerKey: entry.ownerKey };
|
|
344
331
|
}
|
|
345
332
|
|
|
@@ -352,9 +339,9 @@ export function placeDenyPage(
|
|
|
352
339
|
* Build the element for a matched deny chain entry with the props contract
|
|
353
340
|
* the file's convention documents:
|
|
354
341
|
*
|
|
355
|
-
* - Status-code files: { status,
|
|
342
|
+
* - Status-code files: { status, data }
|
|
356
343
|
* - error.tsx (TSX): { error, digest, reset } via ErrorReconstituter, plus
|
|
357
|
-
* { status,
|
|
344
|
+
* { status, data } for dual-shape implementations. Without
|
|
358
345
|
* the Error prop, any error.tsx written per the docs (reading
|
|
359
346
|
* error.message) crashes — turning a clean deny(404) into a 500. TIM-1081.
|
|
360
347
|
* - error.mdx: plain { status } — server component, static content.
|
|
@@ -379,12 +366,7 @@ function renderDenyPage(
|
|
|
379
366
|
h: (...args: unknown[]) => React.ReactElement
|
|
380
367
|
): React.ReactElement {
|
|
381
368
|
if (entry.kind !== 'error') {
|
|
382
|
-
return h(entry.component, { status,
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// MDX error pages receive plain props — no Error serialization possible.
|
|
386
|
-
if (entry.isMdx) {
|
|
387
|
-
return h(entry.component, { status });
|
|
369
|
+
return h(entry.component, { status, data });
|
|
388
370
|
}
|
|
389
371
|
|
|
390
372
|
// The message is derived solely from the status code (already on the
|
|
@@ -400,7 +382,7 @@ function renderDenyPage(
|
|
|
400
382
|
reset: undefined,
|
|
401
383
|
component: entry.component,
|
|
402
384
|
status,
|
|
403
|
-
|
|
385
|
+
data,
|
|
404
386
|
});
|
|
405
387
|
}
|
|
406
388
|
|
|
@@ -26,7 +26,6 @@ import { renderDenyEntry, type DenyEntryShape } from './deny-boundary.ts';
|
|
|
26
26
|
import { getCookiesForSsr } from './cookie-context.ts';
|
|
27
27
|
import { logRenderError } from './logger.ts';
|
|
28
28
|
import { loadModule } from './safe-load.ts';
|
|
29
|
-
import { isMdxFilePath } from './utils/mdx-file.ts';
|
|
30
29
|
import { isDebug } from './debug.ts';
|
|
31
30
|
import { resolveMetadata, renderMetadataToElements } from './metadata.ts';
|
|
32
31
|
import { headElementsToReact } from './metadata.ts';
|
|
@@ -51,7 +50,6 @@ export interface LayoutEntry {
|
|
|
51
50
|
|
|
52
51
|
/** Callback to create a debug channel sink for RSC rendering. */
|
|
53
52
|
export type DebugChannelFactory = () => {
|
|
54
|
-
readable: ReadableStream;
|
|
55
53
|
writable: WritableStream;
|
|
56
54
|
};
|
|
57
55
|
|
|
@@ -86,7 +84,6 @@ function buildDenyElement(
|
|
|
86
84
|
const entry: DenyEntryShape = {
|
|
87
85
|
component,
|
|
88
86
|
kind: resolutionKind === 'error' ? 'error' : 'status',
|
|
89
|
-
isMdx: isMdxFilePath(filePath),
|
|
90
87
|
};
|
|
91
88
|
return renderDenyEntry(entry, deny.status, deny.data);
|
|
92
89
|
}
|
|
@@ -183,7 +180,7 @@ export async function renderDenyPage(
|
|
|
183
180
|
const ErrorPageComponent = mod.default as (...args: unknown[]) => unknown;
|
|
184
181
|
const h = createElement as (...args: unknown[]) => React.ReactElement;
|
|
185
182
|
|
|
186
|
-
// 4xx status-code pages receive { status,
|
|
183
|
+
// 4xx status-code pages receive { status, data }; an
|
|
187
184
|
// error.tsx match gets { error, digest, reset } via ErrorReconstituter.
|
|
188
185
|
// See design/10-error-handling.md §"Status-Code File Props" and TIM-1081.
|
|
189
186
|
let element = buildDenyElement(
|
|
@@ -365,9 +362,9 @@ async function renderDenyPageJson(
|
|
|
365
362
|
// A .json status file that fails to load is not a usable match — return
|
|
366
363
|
// null so the caller continues to the framework default rather than
|
|
367
364
|
// rejecting out of the chain.
|
|
368
|
-
// Dev warning: deny(status,
|
|
369
|
-
// static bodies — the data is silently dropped. The same
|
|
370
|
-
// deliver the data to a .tsx file's
|
|
365
|
+
// Dev warning: deny(status, { dangerouslyPassData }) carries data but JSON
|
|
366
|
+
// status files are static bodies — the data is silently dropped. The same
|
|
367
|
+
// deny() would deliver the data to a .tsx file's data prop. See
|
|
371
368
|
// design/10-error-handling.md §"Status-Code File Variants".
|
|
372
369
|
if (isDebug() && deny.data !== undefined) {
|
|
373
370
|
console.warn(
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* Error boundary wrapper — wraps a React element in the error boundaries
|
|
3
3
|
* declared by a route segment (`{status}.tsx`, `4xx.tsx`/`5xx.tsx`, `error.tsx`).
|
|
4
4
|
*
|
|
5
|
-
* This is the single implementation of that walk. It is driven by
|
|
6
|
-
*
|
|
5
|
+
* This is the single implementation of that walk. It is driven by the
|
|
6
|
+
* request-time renderer walking the runtime manifest (`SegmentNode<ManifestFile>`):
|
|
7
7
|
*
|
|
8
|
-
* - `route-element-builder.ts` / `slot-resolver.ts`
|
|
9
|
-
* renderer, walking the runtime manifest (`SegmentNode<ManifestFile>`).
|
|
8
|
+
* - `route-element-builder.ts` / `slot-resolver.ts`
|
|
10
9
|
*
|
|
11
10
|
* `errorBoundaryComponent` is injected rather than imported: `TimberErrorBoundary`
|
|
12
11
|
* is a `'use client'` module, and importing it here would pull client code into
|
|
@@ -19,12 +18,6 @@
|
|
|
19
18
|
import type { ReactNode } from 'react';
|
|
20
19
|
import type { SegmentNode } from '../routing/types.ts';
|
|
21
20
|
import { isValidElementType, type LoadedComponent } from './utils/element-type.ts';
|
|
22
|
-
import { isMdxFilePath } from './utils/mdx-file.ts';
|
|
23
|
-
|
|
24
|
-
/** The minimum a route file must carry for the walk: a path to classify as MDX. */
|
|
25
|
-
interface BoundaryFile {
|
|
26
|
-
filePath: string;
|
|
27
|
-
}
|
|
28
21
|
|
|
29
22
|
/**
|
|
30
23
|
* Framework-injected error boundary wrapper.
|
|
@@ -36,10 +29,8 @@ interface BoundaryFile {
|
|
|
36
29
|
* construction site and may not import types from a 'use client' module.
|
|
37
30
|
*/
|
|
38
31
|
export interface ErrorBoundaryProps {
|
|
39
|
-
/** The component to render when an error is caught
|
|
32
|
+
/** The component to render when an error is caught. */
|
|
40
33
|
fallbackComponent?: LoadedComponent;
|
|
41
|
-
/** Pre-rendered fallback element (MDX status files — see TIM-503). */
|
|
42
|
-
fallbackElement?: ReactNode;
|
|
43
34
|
/** Status code filter: 400 = any 4xx, 500 = any 5xx, specific number = exact match. */
|
|
44
35
|
status?: number;
|
|
45
36
|
/**
|
|
@@ -119,24 +110,15 @@ async function loadBoundaryComponent<TFile>(
|
|
|
119
110
|
/**
|
|
120
111
|
* Wrap `element` in one error boundary rendering `Component` on failure.
|
|
121
112
|
*
|
|
122
|
-
*
|
|
123
|
-
* props to `TimberErrorBoundary` (a 'use client' component). They are
|
|
124
|
-
* pre-rendered as elements and passed as `fallbackElement` instead. See TIM-503.
|
|
125
|
-
*
|
|
126
|
-
* @param status boundary status filter — omitted entirely for `error.tsx`
|
|
127
|
-
* @param fallbackProps props handed to an MDX fallback when it is pre-rendered
|
|
113
|
+
* @param status boundary status filter — omitted entirely for `error.tsx`
|
|
128
114
|
*/
|
|
129
|
-
function wrapInBoundary<TFile
|
|
115
|
+
function wrapInBoundary<TFile, TElement extends ReactNode>(
|
|
130
116
|
element: TElement,
|
|
131
117
|
Component: LoadedComponent,
|
|
132
|
-
file: TFile,
|
|
133
118
|
status: number | undefined,
|
|
134
|
-
fallbackProps: object,
|
|
135
119
|
config: ErrorBoundaryWrapConfig<TFile, TElement>
|
|
136
120
|
): TElement {
|
|
137
|
-
const props: ErrorBoundaryProps =
|
|
138
|
-
? { fallbackElement: config.createElement(Component, fallbackProps), children: element }
|
|
139
|
-
: { fallbackComponent: Component, children: element };
|
|
121
|
+
const props: ErrorBoundaryProps = { fallbackComponent: Component, children: element };
|
|
140
122
|
if (status !== undefined) props.status = status;
|
|
141
123
|
if (config.segmentKey !== undefined) props.segmentKey = config.segmentKey;
|
|
142
124
|
if (config.denyOnly) props.denyOnly = true;
|
|
@@ -158,10 +140,7 @@ function wrapInBoundary<TFile extends BoundaryFile, TElement extends ReactNode>(
|
|
|
158
140
|
*
|
|
159
141
|
* See design/10-error-handling.md §"Format-Aware Fallback Chains".
|
|
160
142
|
*/
|
|
161
|
-
export async function wrapSegmentWithErrorBoundaries<
|
|
162
|
-
TFile extends BoundaryFile,
|
|
163
|
-
TElement extends ReactNode,
|
|
164
|
-
>(
|
|
143
|
+
export async function wrapSegmentWithErrorBoundaries<TFile, TElement extends ReactNode>(
|
|
165
144
|
segment: SegmentNode<TFile>,
|
|
166
145
|
element: TElement,
|
|
167
146
|
config: ErrorBoundaryWrapConfig<TFile, TElement>
|
|
@@ -176,7 +155,7 @@ export async function wrapSegmentWithErrorBoundaries<
|
|
|
176
155
|
if (isNaN(status)) continue;
|
|
177
156
|
const Component = await loadBoundaryComponent(file, loadModule);
|
|
178
157
|
if (Component) {
|
|
179
|
-
element = wrapInBoundary(element, Component,
|
|
158
|
+
element = wrapInBoundary(element, Component, status, config);
|
|
180
159
|
}
|
|
181
160
|
}
|
|
182
161
|
|
|
@@ -186,19 +165,16 @@ export async function wrapSegmentWithErrorBoundaries<
|
|
|
186
165
|
const Component = await loadBoundaryComponent(file, loadModule);
|
|
187
166
|
if (Component) {
|
|
188
167
|
const categoryStatus = key === '4xx' ? 400 : 500;
|
|
189
|
-
element = wrapInBoundary(element, Component,
|
|
168
|
+
element = wrapInBoundary(element, Component, categoryStatus, config);
|
|
190
169
|
}
|
|
191
170
|
}
|
|
192
171
|
}
|
|
193
172
|
|
|
194
173
|
// error.tsx (outermost — catches anything not matched by status files).
|
|
195
|
-
// error.tsx/error.mdx receives { error, digest, reset } props from the
|
|
196
|
-
// boundary at catch time; MDX error files are pre-rendered without them
|
|
197
|
-
// (they're static content).
|
|
198
174
|
if (segment.error) {
|
|
199
175
|
const Component = await loadBoundaryComponent(segment.error, loadModule);
|
|
200
176
|
if (Component) {
|
|
201
|
-
element = wrapInBoundary(element, Component,
|
|
177
|
+
element = wrapInBoundary(element, Component, undefined, config);
|
|
202
178
|
}
|
|
203
179
|
}
|
|
204
180
|
|
package/src/server/pipeline.ts
CHANGED
|
@@ -215,6 +215,15 @@ export interface PipelineConfig {
|
|
|
215
215
|
*/
|
|
216
216
|
devHmrOptions?: import('../dev-tools/dev-page-shell.ts').DevErrorHmrOptions;
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Dev debug channel transport — sends RSC Flight debug data to the
|
|
220
|
+
* browser via HMR WebSocket for owner stack resolution in console
|
|
221
|
+
* warnings. Set by the dev server. Undefined in production.
|
|
222
|
+
*
|
|
223
|
+
* See TIM-1507, design/13-security.md §7.
|
|
224
|
+
*/
|
|
225
|
+
devDebugRegistry?: import('../dev-tools/debug-channel.ts').DebugChannelRegistry;
|
|
226
|
+
|
|
218
227
|
/**
|
|
219
228
|
* Fallback error renderer — called when a catastrophic error escapes the
|
|
220
229
|
* render phase. Produces an HTML Response instead of a bare empty 500.
|
package/src/server/primitives.ts
CHANGED
|
@@ -228,14 +228,17 @@ export class DenySignal extends Error {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
/** Options for deny()
|
|
231
|
+
/** Options for deny(). */
|
|
232
232
|
export interface DenyOptions {
|
|
233
|
-
/** HTTP status code (4xx or 5xx). Default: 403. */
|
|
234
|
-
status?: number;
|
|
235
233
|
/** Human-readable message (logged server-side, not sent to client). */
|
|
236
234
|
message?: string;
|
|
237
|
-
/**
|
|
238
|
-
|
|
235
|
+
/**
|
|
236
|
+
* JSON-serializable data forwarded as the `data` prop to status-code files
|
|
237
|
+
* and `denied.tsx`. Named `dangerouslyPassData` because this data crosses
|
|
238
|
+
* the RSC → client serialization boundary — do not pass sensitive server
|
|
239
|
+
* state (tokens, internal IDs, database rows).
|
|
240
|
+
*/
|
|
241
|
+
dangerouslyPassData?: JsonSerializable;
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
/**
|
|
@@ -246,38 +249,29 @@ export interface DenyOptions {
|
|
|
246
249
|
* - Inside Suspense (hold window): promoted to pre-flush behavior
|
|
247
250
|
* - Inside Suspense (after flush): error boundary + noindex meta
|
|
248
251
|
*
|
|
249
|
-
* Supports both positional and object signatures:
|
|
250
252
|
* ```ts
|
|
251
|
-
* deny()
|
|
252
|
-
* deny(404)
|
|
253
|
-
* deny(503, {
|
|
254
|
-
* deny({
|
|
253
|
+
* deny() // 403 (default)
|
|
254
|
+
* deny(404) // 404
|
|
255
|
+
* deny(503, { message: 'Maintenance' }) // server-only log message
|
|
256
|
+
* deny(404, { dangerouslyPassData: { resourceId: params.id } }) // explicit client opt-in
|
|
255
257
|
* ```
|
|
256
258
|
*
|
|
257
259
|
* Accepts any 4xx or 5xx status code. This replaces the need for
|
|
258
260
|
* `throw new RenderError(...)` in user code — RenderError is now an
|
|
259
261
|
* internal pipeline detail.
|
|
260
262
|
*
|
|
261
|
-
* @param
|
|
262
|
-
* @param
|
|
263
|
+
* @param status - HTTP status code (4xx or 5xx). Default: 403.
|
|
264
|
+
* @param options - Optional message and/or data to pass to the client.
|
|
263
265
|
*/
|
|
264
|
-
export function deny(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (typeof statusOrOptions === 'object' && statusOrOptions !== null) {
|
|
269
|
-
status = statusOrOptions.status ?? 403;
|
|
270
|
-
resolvedData = statusOrOptions.data;
|
|
271
|
-
} else {
|
|
272
|
-
status = statusOrOptions ?? 403;
|
|
273
|
-
resolvedData = data;
|
|
274
|
-
}
|
|
266
|
+
export function deny(status?: number, options?: DenyOptions): never {
|
|
267
|
+
const resolvedStatus = status ?? 403;
|
|
268
|
+
const resolvedData = options?.dangerouslyPassData;
|
|
275
269
|
|
|
276
|
-
if (
|
|
277
|
-
throw new Error(`deny() requires a 4xx or 5xx status code, got ${
|
|
270
|
+
if (resolvedStatus < 400 || resolvedStatus > 599) {
|
|
271
|
+
throw new Error(`deny() requires a 4xx or 5xx status code, got ${resolvedStatus}.`);
|
|
278
272
|
}
|
|
279
273
|
warnIfNotSerializable(resolvedData, 'deny()');
|
|
280
|
-
throw new DenySignal(
|
|
274
|
+
throw new DenySignal(resolvedStatus, resolvedData);
|
|
281
275
|
}
|
|
282
276
|
|
|
283
277
|
// Single source of truth shared with the client next/navigation shim —
|