@timber-js/app 0.2.0-alpha.98 → 0.2.0-alpha.99
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/LICENSE +8 -0
- package/dist/_chunks/actions-CQ8Z8VGL.js +1061 -0
- package/dist/_chunks/actions-CQ8Z8VGL.js.map +1 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js +61 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js.map +1 -0
- package/dist/_chunks/{define-Itxvcd7F.js → define-B-Q_UMOD.js} +19 -23
- package/dist/_chunks/define-B-Q_UMOD.js.map +1 -0
- package/dist/_chunks/{define-C77ScO0m.js → define-CfBPoJb0.js} +24 -7
- package/dist/_chunks/define-CfBPoJb0.js.map +1 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js +194 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js.map +1 -0
- package/dist/_chunks/{format-CYBGxKtc.js → format-Bcn-Iv1x.js} +1 -1
- package/dist/_chunks/{format-CYBGxKtc.js.map → format-Bcn-Iv1x.js.map} +1 -1
- package/dist/_chunks/handler-store-B-lqaGyh.js +54 -0
- package/dist/_chunks/handler-store-B-lqaGyh.js.map +1 -0
- package/dist/_chunks/logger-0m8MsKdc.js +291 -0
- package/dist/_chunks/logger-0m8MsKdc.js.map +1 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js +122 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js.map +1 -0
- package/dist/_chunks/navigation-root-BCYczjml.js +96 -0
- package/dist/_chunks/navigation-root-BCYczjml.js.map +1 -0
- package/dist/_chunks/registry-I2ss-lvy.js +20 -0
- package/dist/_chunks/registry-I2ss-lvy.js.map +1 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js +28 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js.map +1 -0
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js → schema-bridge-Cxu4l-7p.js} +1 -1
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js.map → schema-bridge-Cxu4l-7p.js.map} +1 -1
- package/dist/_chunks/{segment-context-fHFLF1PE.js → segment-context-Dx_OizxD.js} +1 -1
- package/dist/_chunks/{segment-context-fHFLF1PE.js.map → segment-context-Dx_OizxD.js.map} +1 -1
- package/dist/_chunks/{router-ref-C8OCm7g7.js → ssr-data-B4CdH7rE.js} +2 -26
- package/dist/_chunks/ssr-data-B4CdH7rE.js.map +1 -0
- package/dist/_chunks/{stale-reload-BX5gL1r-.js → stale-reload-Bab885FO.js} +1 -1
- package/dist/_chunks/{stale-reload-BX5gL1r-.js.map → stale-reload-Bab885FO.js.map} +1 -1
- package/dist/_chunks/tracing-C8V-YGsP.js +329 -0
- package/dist/_chunks/tracing-C8V-YGsP.js.map +1 -0
- package/dist/_chunks/{use-query-states-BiV5GJgm.js → use-query-states-B2XTqxDR.js} +3 -19
- package/dist/_chunks/use-query-states-B2XTqxDR.js.map +1 -0
- package/dist/_chunks/{use-params-IOPu7E8t.js → use-segment-params-BkpKAQ7D.js} +9 -95
- package/dist/_chunks/use-segment-params-BkpKAQ7D.js.map +1 -0
- package/dist/_chunks/{walkers-VOXgavMF.js → walkers-Tg0Alwcg.js} +6 -3
- package/dist/_chunks/walkers-Tg0Alwcg.js.map +1 -0
- package/dist/_chunks/{dev-warnings-DpGRGoDi.js → warnings-Cg47l5sk.js} +3 -3
- package/dist/_chunks/warnings-Cg47l5sk.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +28 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +8 -28
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +8 -26
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +16 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/cache/index.js +9 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -1
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/form.d.ts +10 -24
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +40 -90
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +2 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +81 -7
- package/dist/client/internal.js.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/state.d.ts +1 -1
- package/dist/client/use-cookie.d.ts +8 -0
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/{use-params.d.ts → use-segment-params.d.ts} +1 -1
- package/dist/client/use-segment-params.d.ts.map +1 -0
- package/dist/codec.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +2 -2
- package/dist/config-types.d.ts +28 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts +87 -35
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.d.ts +2 -1
- package/dist/cookies/index.d.ts.map +1 -1
- package/dist/cookies/index.js +48 -2
- package/dist/cookies/index.js.map +1 -0
- package/dist/cookies/json-cookie.d.ts +64 -0
- package/dist/cookies/json-cookie.d.ts.map +1 -0
- package/dist/cookies/validation.d.ts +46 -0
- package/dist/cookies/validation.d.ts.map +1 -0
- package/dist/{plugins/dev-404-page.d.ts → dev-tools/404-page.d.ts} +1 -1
- package/dist/dev-tools/404-page.d.ts.map +1 -0
- package/dist/{plugins/dev-browser-logs.d.ts → dev-tools/browser-logs.d.ts} +1 -1
- package/dist/dev-tools/browser-logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-page.d.ts → dev-tools/error-page.d.ts} +2 -2
- package/dist/dev-tools/error-page.d.ts.map +1 -0
- package/dist/{server/dev-holding-server.d.ts → dev-tools/holding-server.d.ts} +1 -1
- package/dist/dev-tools/holding-server.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +31 -0
- package/dist/dev-tools/index.d.ts.map +1 -0
- package/dist/{server/dev-span-processor.d.ts → dev-tools/instrumentation.d.ts} +26 -6
- package/dist/dev-tools/instrumentation.d.ts.map +1 -0
- package/dist/{server/dev-logger.d.ts → dev-tools/logger.d.ts} +1 -1
- package/dist/dev-tools/logger.d.ts.map +1 -0
- package/dist/{plugins/dev-logs.d.ts → dev-tools/logs.d.ts} +1 -1
- package/dist/dev-tools/logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-overlay.d.ts → dev-tools/overlay.d.ts} +3 -12
- package/dist/dev-tools/overlay.d.ts.map +1 -0
- package/dist/dev-tools/stack-classifier.d.ts +34 -0
- package/dist/dev-tools/stack-classifier.d.ts.map +1 -0
- package/dist/{plugins/dev-terminal-error.d.ts → dev-tools/terminal.d.ts} +2 -2
- package/dist/dev-tools/terminal.d.ts.map +1 -0
- package/dist/{server/dev-warnings.d.ts → dev-tools/warnings.d.ts} +1 -1
- package/dist/dev-tools/warnings.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -72
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +1 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +25 -7
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +5 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +2 -2
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts +23 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts +4 -3
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +15 -6
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +5 -5
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/asset-headers.d.ts +1 -15
- package/dist/server/asset-headers.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts +170 -0
- package/dist/server/cookie-context.d.ts.map +1 -0
- package/dist/server/cookie-parsing.d.ts +51 -0
- package/dist/server/cookie-parsing.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +90 -0
- package/dist/server/deny-boundary.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints-sender.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -149
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +6 -4
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +261 -408
- package/dist/server/internal.js.map +1 -1
- package/dist/server/logger.d.ts +14 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware-runner.d.ts +17 -0
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +26 -0
- package/dist/server/param-coercion.d.ts.map +1 -0
- package/dist/server/pipeline-helpers.d.ts +14 -7
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts +49 -0
- package/dist/server/pipeline-outcome.d.ts.map +1 -0
- package/dist/server/pipeline-phases.d.ts +4 -49
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +0 -2
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +22 -159
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +66 -0
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -0
- package/dist/server/rsc-entry/helpers.d.ts +1 -1
- 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 +50 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +59 -14
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +1 -1
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +45 -16
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +48 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/escape-html.d.ts +14 -0
- package/dist/server/utils/escape-html.d.ts.map +1 -0
- package/dist/shims/headers.d.ts +2 -2
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +3 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +9 -4
- package/dist/shims/navigation.d.ts.map +1 -1
- package/package.json +6 -7
- package/src/adapters/build-output-helper.ts +77 -0
- package/src/adapters/cloudflare.ts +10 -50
- package/src/adapters/nitro.ts +11 -45
- package/src/adapters/shared.ts +40 -0
- package/src/cache/timber-cache.ts +3 -2
- package/src/cli.ts +0 -0
- package/src/client/form.tsx +17 -25
- package/src/client/index.ts +16 -9
- package/src/client/internal.ts +3 -2
- package/src/client/router.ts +1 -1
- package/src/client/rsc-fetch.ts +15 -0
- package/src/client/state.ts +2 -2
- package/src/client/use-cookie.ts +29 -0
- package/src/codec.ts +3 -7
- package/src/config-types.ts +28 -0
- package/src/cookies/define-cookie.ts +271 -78
- package/src/cookies/index.ts +11 -8
- package/src/cookies/json-cookie.ts +105 -0
- package/src/cookies/validation.ts +134 -0
- package/src/{plugins/dev-404-page.ts → dev-tools/404-page.ts} +2 -7
- package/src/{plugins/dev-error-page.ts → dev-tools/error-page.ts} +5 -32
- package/src/dev-tools/index.ts +90 -0
- package/src/dev-tools/instrumentation.ts +176 -0
- package/src/{plugins/dev-logs.ts → dev-tools/logs.ts} +2 -2
- package/src/{plugins/dev-error-overlay.ts → dev-tools/overlay.ts} +5 -23
- package/src/dev-tools/stack-classifier.ts +75 -0
- package/src/{plugins/dev-terminal-error.ts → dev-tools/terminal.ts} +4 -38
- package/src/{server/dev-warnings.ts → dev-tools/warnings.ts} +1 -1
- package/src/index.ts +11 -3
- package/src/plugin-context.ts +1 -1
- package/src/plugins/adapter-build.ts +3 -1
- package/src/plugins/dev-server.ts +3 -3
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +1 -1
- package/src/routing/convention-lint.ts +5 -4
- package/src/routing/scanner.ts +5 -2
- package/src/routing/status-file-lint.ts +4 -2
- package/src/search-params/define.ts +71 -15
- package/src/search-params/wrappers.ts +9 -2
- package/src/segment-params/define.ts +66 -13
- package/src/server/access-gate.tsx +9 -8
- package/src/server/action-handler.ts +28 -38
- package/src/server/als-registry.ts +5 -5
- package/src/server/asset-headers.ts +8 -34
- package/src/server/cookie-context.ts +468 -0
- package/src/server/cookie-parsing.ts +135 -0
- package/src/server/{deny-page-resolver.ts → deny-boundary.ts} +78 -14
- package/src/server/deny-renderer.ts +2 -7
- package/src/server/early-hints-sender.ts +3 -2
- package/src/server/fallback-error.ts +1 -1
- package/src/server/index.ts +13 -14
- package/src/server/internal.ts +10 -3
- package/src/server/logger.ts +23 -0
- package/src/server/middleware-runner.ts +44 -0
- package/src/server/param-coercion.ts +76 -0
- package/src/server/pipeline-helpers.ts +37 -13
- package/src/server/pipeline-outcome.ts +167 -0
- package/src/server/pipeline-phases.ts +27 -209
- package/src/server/pipeline.ts +2 -9
- package/src/server/request-context.ts +46 -451
- package/src/server/route-element-builder.ts +7 -3
- package/src/server/rsc-entry/action-middleware-runner.ts +167 -0
- package/src/server/rsc-entry/error-renderer.ts +1 -1
- package/src/server/rsc-entry/helpers.ts +2 -7
- package/src/server/rsc-entry/index.ts +34 -273
- package/src/server/rsc-entry/render-route.ts +304 -0
- package/src/server/rsc-entry/rsc-payload.ts +1 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/rsc-entry/wrap-action-dispatch.ts +316 -23
- package/src/server/ssr-entry.ts +1 -1
- package/src/server/state-tree-diff.ts +4 -1
- package/src/server/tracing.ts +3 -3
- package/src/server/tree-builder.ts +128 -52
- package/src/server/types.ts +52 -0
- package/src/server/utils/escape-html.ts +20 -0
- package/src/shims/headers.ts +3 -3
- package/src/shims/navigation-client.ts +4 -3
- package/src/shims/navigation.ts +9 -7
- package/dist/_chunks/actions-DLnUaR65.js +0 -421
- package/dist/_chunks/actions-DLnUaR65.js.map +0 -1
- package/dist/_chunks/als-registry-HS0LGUl2.js +0 -41
- package/dist/_chunks/als-registry-HS0LGUl2.js.map +0 -1
- package/dist/_chunks/debug-ECi_61pb.js +0 -108
- package/dist/_chunks/debug-ECi_61pb.js.map +0 -1
- package/dist/_chunks/define-C77ScO0m.js.map +0 -1
- package/dist/_chunks/define-Itxvcd7F.js.map +0 -1
- package/dist/_chunks/define-cookie-BowvzoP0.js +0 -94
- package/dist/_chunks/define-cookie-BowvzoP0.js.map +0 -1
- package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +0 -1
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js +0 -41
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +0 -1
- package/dist/_chunks/request-context-CK5tZqIP.js +0 -478
- package/dist/_chunks/request-context-CK5tZqIP.js.map +0 -1
- package/dist/_chunks/router-ref-C8OCm7g7.js.map +0 -1
- package/dist/_chunks/tracing-CCYbKn5n.js +0 -238
- package/dist/_chunks/tracing-CCYbKn5n.js.map +0 -1
- package/dist/_chunks/use-params-IOPu7E8t.js.map +0 -1
- package/dist/_chunks/use-query-states-BiV5GJgm.js.map +0 -1
- package/dist/_chunks/walkers-VOXgavMF.js.map +0 -1
- package/dist/client/use-params.d.ts.map +0 -1
- package/dist/plugins/dev-404-page.d.ts.map +0 -1
- package/dist/plugins/dev-browser-logs.d.ts.map +0 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +0 -1
- package/dist/plugins/dev-error-page.d.ts.map +0 -1
- package/dist/plugins/dev-logs.d.ts.map +0 -1
- package/dist/plugins/dev-terminal-error.d.ts.map +0 -1
- package/dist/server/deny-page-resolver.d.ts +0 -52
- package/dist/server/deny-page-resolver.d.ts.map +0 -1
- package/dist/server/dev-fetch-instrumentation.d.ts +0 -22
- package/dist/server/dev-fetch-instrumentation.d.ts.map +0 -1
- package/dist/server/dev-holding-server.d.ts.map +0 -1
- package/dist/server/dev-logger.d.ts.map +0 -1
- package/dist/server/dev-span-processor.d.ts.map +0 -1
- package/dist/server/dev-warnings.d.ts.map +0 -1
- package/dist/server/page-deny-boundary.d.ts +0 -31
- package/dist/server/page-deny-boundary.d.ts.map +0 -1
- package/src/server/dev-fetch-instrumentation.ts +0 -96
- package/src/server/dev-span-processor.ts +0 -78
- package/src/server/page-deny-boundary.tsx +0 -56
- /package/src/client/{use-params.ts → use-segment-params.ts} +0 -0
- /package/src/{plugins/dev-browser-logs.ts → dev-tools/browser-logs.ts} +0 -0
- /package/src/{server/dev-holding-server.ts → dev-tools/holding-server.ts} +0 -0
- /package/src/{server/dev-logger.ts → dev-tools/logger.ts} +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware-on-actions runner — runs the matched route's `middleware.ts`
|
|
3
|
+
* chain for an action POST inside its own `runWithRequestContext` scope
|
|
4
|
+
* and returns a `MiddlewareOutcome` value the dispatcher can translate.
|
|
5
|
+
*
|
|
6
|
+
* Lifted out of `wrap-action-dispatch.ts` (TIM-853) so the dispatcher
|
|
7
|
+
* can be read top-to-bottom without scrolling past the chain runner. The
|
|
8
|
+
* function is intentionally a free function (not a closure over deps)
|
|
9
|
+
* so it can be unit-tested in isolation. It does not import virtual
|
|
10
|
+
* modules and does not call into the action handler — it owns exactly
|
|
11
|
+
* the "run middleware once" step and nothing else.
|
|
12
|
+
*
|
|
13
|
+
* Failure modes are encoded as outcome variants rather than thrown so the
|
|
14
|
+
* caller never has to wrap this in try/catch. The only thrown errors are
|
|
15
|
+
* truly unexpected (programmer bugs) and propagate naturally.
|
|
16
|
+
*
|
|
17
|
+
* See TIM-871 for the full design rationale (closing the
|
|
18
|
+
* CVE-2025-29927-class bug where actions silently bypass middleware).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getCookiesForSsr, getSetCookieHeaders } from '../cookie-context.js';
|
|
22
|
+
import {
|
|
23
|
+
applyRequestHeaderOverlay,
|
|
24
|
+
runWithRequestContext,
|
|
25
|
+
setMutableCookieContext,
|
|
26
|
+
setSegmentParams,
|
|
27
|
+
} from '../request-context.js';
|
|
28
|
+
import { runMiddlewareChain } from '../middleware-runner.js';
|
|
29
|
+
import { DenySignal, RedirectSignal } from '../primitives.js';
|
|
30
|
+
import { ParamCoercionError } from '../route-element-builder.js';
|
|
31
|
+
import type { RouteMatch } from '../pipeline.js';
|
|
32
|
+
import type { MiddlewareContext } from '../types.js';
|
|
33
|
+
|
|
34
|
+
/** Coerce a matched route's segment params via its `params.ts` codecs. */
|
|
35
|
+
export type CoerceSegmentParamsFn = (match: RouteMatch) => Promise<void>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Outcome of running middleware on an action request, surfaced from the
|
|
39
|
+
* inner `runWithRequestContext` scope so the outer dispatcher can convert
|
|
40
|
+
* it to a Response (short-circuit) or continue to the action handler.
|
|
41
|
+
*
|
|
42
|
+
* Each variant carries the post-middleware Set-Cookie snapshot so the
|
|
43
|
+
* outer scope can apply cookies to whichever Response it ultimately
|
|
44
|
+
* returns. The middleware ALS scope ends before the action ALS scope
|
|
45
|
+
* begins, so cookie state must be threaded explicitly across the boundary
|
|
46
|
+
* via `seedRequestCookies` (cookie reads) and `setCookieHeaders` (cookie
|
|
47
|
+
* writes).
|
|
48
|
+
*/
|
|
49
|
+
export type MiddlewareOutcome =
|
|
50
|
+
| {
|
|
51
|
+
kind: 'continue';
|
|
52
|
+
overlay: Headers;
|
|
53
|
+
cookies: Map<string, string>;
|
|
54
|
+
setCookieHeaders: string[];
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
kind: 'short-circuit';
|
|
58
|
+
response: Response;
|
|
59
|
+
setCookieHeaders: string[];
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
kind: 'redirect';
|
|
63
|
+
signal: RedirectSignal;
|
|
64
|
+
setCookieHeaders: string[];
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
kind: 'deny';
|
|
68
|
+
signal: DenySignal;
|
|
69
|
+
match: RouteMatch;
|
|
70
|
+
setCookieHeaders: string[];
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
kind: 'param-coercion-error';
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
kind: 'error';
|
|
77
|
+
error: unknown;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Run the matched route's middleware chain inside a fresh request-context
|
|
82
|
+
* scope, captured as a `MiddlewareOutcome` value the outer dispatcher can
|
|
83
|
+
* translate to a Response or use as the seed for the action handler.
|
|
84
|
+
*/
|
|
85
|
+
export async function runMiddlewareForAction(
|
|
86
|
+
req: Request,
|
|
87
|
+
match: RouteMatch,
|
|
88
|
+
coerceSegmentParams: CoerceSegmentParamsFn
|
|
89
|
+
): Promise<MiddlewareOutcome> {
|
|
90
|
+
return runWithRequestContext(req, async (): Promise<MiddlewareOutcome> => {
|
|
91
|
+
// ─── Coerce segment params (mirrors page pipeline) ──────────────
|
|
92
|
+
try {
|
|
93
|
+
await coerceSegmentParams(match);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (err instanceof ParamCoercionError) {
|
|
96
|
+
return { kind: 'param-coercion-error' };
|
|
97
|
+
}
|
|
98
|
+
return { kind: 'error', error: err };
|
|
99
|
+
}
|
|
100
|
+
setSegmentParams(match.segmentParams);
|
|
101
|
+
|
|
102
|
+
// ─── Build the middleware context ───────────────────────────────
|
|
103
|
+
const responseHeaders = new Headers();
|
|
104
|
+
const requestHeaderOverlay = new Headers();
|
|
105
|
+
const ctx: MiddlewareContext = {
|
|
106
|
+
req,
|
|
107
|
+
requestHeaders: requestHeaderOverlay,
|
|
108
|
+
headers: responseHeaders,
|
|
109
|
+
segmentParams: match.segmentParams,
|
|
110
|
+
// 103 Early Hints are a page-render concern — server actions are
|
|
111
|
+
// RPC and never produce HTML, so there is nothing to hint about.
|
|
112
|
+
// Provide a no-op so user middleware can call `ctx.earlyHints()`
|
|
113
|
+
// unconditionally without branching on request kind.
|
|
114
|
+
earlyHints: () => {},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ─── Run the chain ──────────────────────────────────────────────
|
|
118
|
+
setMutableCookieContext(true);
|
|
119
|
+
try {
|
|
120
|
+
let middlewareResponse: Response | undefined;
|
|
121
|
+
try {
|
|
122
|
+
middlewareResponse = await runMiddlewareChain(match.middlewareChain, ctx);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err instanceof RedirectSignal) {
|
|
125
|
+
return {
|
|
126
|
+
kind: 'redirect',
|
|
127
|
+
signal: err,
|
|
128
|
+
setCookieHeaders: getSetCookieHeaders(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (err instanceof DenySignal) {
|
|
132
|
+
return {
|
|
133
|
+
kind: 'deny',
|
|
134
|
+
signal: err,
|
|
135
|
+
match,
|
|
136
|
+
setCookieHeaders: getSetCookieHeaders(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return { kind: 'error', error: err };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (middlewareResponse) {
|
|
143
|
+
return {
|
|
144
|
+
kind: 'short-circuit',
|
|
145
|
+
response: middlewareResponse,
|
|
146
|
+
setCookieHeaders: getSetCookieHeaders(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Middleware passed — apply the request-header overlay to the
|
|
151
|
+
// current scope so any code that might still read it within
|
|
152
|
+
// this scope sees the merged view. The overlay is also captured
|
|
153
|
+
// in the outcome so the caller can re-apply it inside the
|
|
154
|
+
// action's request-context scope (which is a separate ALS run).
|
|
155
|
+
applyRequestHeaderOverlay(requestHeaderOverlay);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
kind: 'continue',
|
|
159
|
+
overlay: requestHeaderOverlay,
|
|
160
|
+
cookies: getCookiesForSsr(),
|
|
161
|
+
setCookieHeaders: getSetCookieHeaders(),
|
|
162
|
+
};
|
|
163
|
+
} finally {
|
|
164
|
+
setMutableCookieContext(false);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -25,7 +25,7 @@ import { renderDenyPage } from '../deny-renderer.js';
|
|
|
25
25
|
import type { LayoutEntry } from '../deny-renderer.js';
|
|
26
26
|
import type { NavContext } from '../ssr-entry.js';
|
|
27
27
|
import { createDebugChannelSink } from './helpers.js';
|
|
28
|
-
import { getCookiesForSsr } from '../
|
|
28
|
+
import { getCookiesForSsr } from '../cookie-context.js';
|
|
29
29
|
import { callSsr } from './ssr-bridge.js';
|
|
30
30
|
import { teeWithErrorPropagation } from '../stream-utils.js';
|
|
31
31
|
import { isDevMode } from '../debug.js';
|
|
@@ -250,13 +250,8 @@ export function isAbortError(error: unknown): boolean {
|
|
|
250
250
|
return false;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.replace(/&/g, '&')
|
|
256
|
-
.replace(/</g, '<')
|
|
257
|
-
.replace(/>/g, '>')
|
|
258
|
-
.replace(/"/g, '"');
|
|
259
|
-
}
|
|
253
|
+
// escapeHtml is now imported from server/utils/escape-html.ts
|
|
254
|
+
export { escapeHtml } from '../utils/escape-html.js';
|
|
260
255
|
|
|
261
256
|
/**
|
|
262
257
|
* Parse a Cookie header string into a name→value Map.
|
|
@@ -34,23 +34,17 @@ import loadCacheHandler from 'virtual:timber-cache-handler';
|
|
|
34
34
|
import { wrapPipelineWithActionDispatch } from './wrap-action-dispatch.js';
|
|
35
35
|
import type { BodyLimitsConfig } from '../body-limits.js';
|
|
36
36
|
import type { BuildManifest } from '../build-manifest.js';
|
|
37
|
-
import {
|
|
38
|
-
buildFontPreloadTags,
|
|
39
|
-
buildModulepreloadTags,
|
|
40
|
-
collectRouteFonts,
|
|
41
|
-
collectRouteModulepreloads,
|
|
42
|
-
} from '../build-manifest.js';
|
|
43
37
|
import type { LayoutEntry } from '../deny-renderer.js';
|
|
44
38
|
import { renderDenyPage, renderDenyPageAsRsc } from '../deny-renderer.js';
|
|
45
|
-
import { resolveLogMode } from '
|
|
39
|
+
import { resolveLogMode } from '../../dev-tools/logger.js';
|
|
46
40
|
import { sendEarlyHints103 } from '../early-hints-sender.js';
|
|
47
41
|
import { collectEarlyHintHeaders } from '../early-hints.js';
|
|
48
|
-
import type { ClientBootstrapConfig } from '../html-injectors.js';
|
|
49
42
|
import { buildClientScripts } from '../html-injectors.js';
|
|
50
43
|
import type { InterceptionContext, PipelineConfig, RouteMatch } from '../pipeline.js';
|
|
51
|
-
import { createPipeline
|
|
44
|
+
import { createPipeline } from '../pipeline.js';
|
|
45
|
+
import { coerceSegmentParams } from '../param-coercion.js';
|
|
52
46
|
import { setSegmentParams } from '../request-context.js';
|
|
53
|
-
import { buildRouteElement
|
|
47
|
+
import { buildRouteElement } from '../route-element-builder.js';
|
|
54
48
|
import type { ManifestSegmentNode } from '../route-matcher.js';
|
|
55
49
|
import { createMetadataRouteMatcher, createRouteMatcher } from '../route-matcher.js';
|
|
56
50
|
import { initDevTracing } from '../tracing.js';
|
|
@@ -59,23 +53,16 @@ import { renderFallbackError as renderFallback } from '../fallback-error.js';
|
|
|
59
53
|
import { loadInstrumentation } from '../instrumentation.js';
|
|
60
54
|
import { loadModule } from '../safe-load.js';
|
|
61
55
|
import { logRenderError } from '../logger.js';
|
|
62
|
-
import {
|
|
63
|
-
import { renderErrorPage, renderNoMatchPage } from './error-renderer.js';
|
|
56
|
+
import { renderNoMatchPage } from './error-renderer.js';
|
|
64
57
|
import {
|
|
65
|
-
buildRedirectResponse,
|
|
66
58
|
createDebugChannelSink,
|
|
67
|
-
escapeHtml,
|
|
68
59
|
isRscPayloadRequest,
|
|
69
60
|
type DebugComponentEntry,
|
|
70
61
|
} from './helpers.js';
|
|
71
|
-
import {
|
|
72
|
-
import { buildRscPayloadResponse } from './rsc-payload.js';
|
|
73
|
-
import { renderRscStream } from './rsc-stream.js';
|
|
74
|
-
import { renderSsrResponse } from './ssr-renderer.js';
|
|
62
|
+
import { renderRoute } from './render-route.js';
|
|
75
63
|
import { callSsr } from './ssr-bridge.js';
|
|
76
64
|
import { isDebug, isDevMode, setDebugFromConfig } from '../debug.js';
|
|
77
65
|
import { setSourceMapCallback } from '../dev-source-map.js';
|
|
78
|
-
import { recordTiming } from '../server-timing.js';
|
|
79
66
|
import { requestContextAls } from '../als-registry.js';
|
|
80
67
|
import { createAutoSitemapHandler } from '../sitemap-handler.js';
|
|
81
68
|
|
|
@@ -230,7 +217,7 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
230
217
|
await initDevTracing({ mode: devLogMode, slowPhaseMs });
|
|
231
218
|
// Patch globalThis.fetch to create OTEL spans for fetch calls.
|
|
232
219
|
// Spans appear as children of the active component span in the dev log tree.
|
|
233
|
-
const { instrumentDevFetch } = await import('
|
|
220
|
+
const { instrumentDevFetch } = await import('../../dev-tools/instrumentation.js');
|
|
234
221
|
instrumentDevFetch();
|
|
235
222
|
}
|
|
236
223
|
}
|
|
@@ -274,11 +261,14 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
274
261
|
req,
|
|
275
262
|
match,
|
|
276
263
|
responseHeaders,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
264
|
+
{
|
|
265
|
+
clientBootstrap,
|
|
266
|
+
clientJsDisabled,
|
|
267
|
+
rootSegment: manifest.root,
|
|
268
|
+
buildManifest: typedBuildManifest,
|
|
269
|
+
globalError: manifest.globalError,
|
|
270
|
+
},
|
|
271
|
+
interception
|
|
282
272
|
);
|
|
283
273
|
},
|
|
284
274
|
renderNoMatch: async (req: Request, responseHeaders: Headers) => {
|
|
@@ -296,7 +286,7 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
296
286
|
// should receive the bare 404 without an HTML body (TIM-793).
|
|
297
287
|
const acceptsHtml = (req.headers.get('accept') ?? '').includes('text/html');
|
|
298
288
|
if (isDev && !response.body && req.method === 'GET' && acceptsHtml) {
|
|
299
|
-
const { generateDev404Page, collectRoutes } = await import('../../
|
|
289
|
+
const { generateDev404Page, collectRoutes } = await import('../../dev-tools/404-page.js');
|
|
300
290
|
const routes = collectRoutes(manifest.root);
|
|
301
291
|
const pathname = new URL(req.url).pathname;
|
|
302
292
|
const html = generateDev404Page(pathname, routes);
|
|
@@ -420,8 +410,10 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
420
410
|
const pipeline = createPipeline(pipelineConfig);
|
|
421
411
|
|
|
422
412
|
// Wrap the pipeline to enforce CSRF at the request boundary and intercept
|
|
423
|
-
// server action requests
|
|
424
|
-
//
|
|
413
|
+
// server action requests. By default, the wrapper also runs the matched
|
|
414
|
+
// route's `middleware.ts` chain on action POSTs, so authentication, rate
|
|
415
|
+
// limiting, tenant isolation, and request-header injection apply to
|
|
416
|
+
// actions just like they do to page renders. See TIM-871 and
|
|
425
417
|
// design/08-forms-and-actions.md §"Middleware for Server Actions".
|
|
426
418
|
//
|
|
427
419
|
// CSRF validation lives in the wrapper (not inside the action handler) so
|
|
@@ -433,10 +425,23 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
433
425
|
| undefined,
|
|
434
426
|
};
|
|
435
427
|
|
|
428
|
+
const actionsConfig = (runtimeConfig as Record<string, unknown>).actions as
|
|
429
|
+
| { runMiddleware?: boolean }
|
|
430
|
+
| undefined;
|
|
431
|
+
|
|
436
432
|
return wrapPipelineWithActionDispatch(pipeline, {
|
|
437
433
|
csrfConfig,
|
|
438
434
|
bodyLimits: (runtimeConfig as Record<string, unknown>).limits as BodyLimitsConfig['limits'],
|
|
439
435
|
sensitiveFields: formsConfig?.stripSensitiveFields,
|
|
436
|
+
matchRoute,
|
|
437
|
+
coerceSegmentParams,
|
|
438
|
+
renderDenyFallback: pipelineConfig.renderDenyFallback,
|
|
439
|
+
runMiddleware: actionsConfig?.runMiddleware,
|
|
440
|
+
// Thread the pipeline's `stripTrailingSlash` setting into the wrapper
|
|
441
|
+
// so its canonicalization step (applied before `matchRoute` for
|
|
442
|
+
// action POSTs) matches the page pipeline exactly. Defaults to `true`
|
|
443
|
+
// both here and inside the wrapper.
|
|
444
|
+
stripTrailingSlash: pipelineConfig.stripTrailingSlash,
|
|
440
445
|
buildRevalidateRenderer: (req) => async (path: string) => {
|
|
441
446
|
// Build the React element tree for the route at `path`.
|
|
442
447
|
// Returns the element tree (not serialized) so the action handler can
|
|
@@ -456,7 +461,7 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
456
461
|
await coerceSegmentParams(revalidateMatch);
|
|
457
462
|
// Set coerced params in ALS so getSegmentParams() works during the
|
|
458
463
|
// revalidation render (AccessGate reads params via ALS). Without this,
|
|
459
|
-
// AccessGate → getSegmentParams() throws because
|
|
464
|
+
// AccessGate → getSegmentParams() throws because segmentParams
|
|
460
465
|
// is never set. See TIM-667.
|
|
461
466
|
setSegmentParams(revalidateMatch.segmentParams);
|
|
462
467
|
const routeResult = await buildRouteElement(revalidateReq, revalidateMatch);
|
|
@@ -468,250 +473,6 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
468
473
|
});
|
|
469
474
|
}
|
|
470
475
|
|
|
471
|
-
/**
|
|
472
|
-
* Render a matched route to an HTML Response via RSC → SSR pipeline,
|
|
473
|
-
* or return a raw RSC Flight stream for client-side navigation requests.
|
|
474
|
-
*
|
|
475
|
-
* 1. Load page/layout components from the segment chain
|
|
476
|
-
* 2. Resolve metadata
|
|
477
|
-
* 3. Render to RSC Flight stream (serializes "use client" as references)
|
|
478
|
-
* 4. If Accept: text/x-component → return RSC stream directly
|
|
479
|
-
* Otherwise → pass RSC stream to SSR entry for HTML rendering
|
|
480
|
-
*/
|
|
481
|
-
async function renderRoute(
|
|
482
|
-
_req: Request,
|
|
483
|
-
match: RouteMatch,
|
|
484
|
-
responseHeaders: Headers,
|
|
485
|
-
clientBootstrap: ClientBootstrapConfig,
|
|
486
|
-
clientJsDisabled: boolean,
|
|
487
|
-
interception?: InterceptionContext,
|
|
488
|
-
rootSegment?: ManifestSegmentNode,
|
|
489
|
-
globalError?: { load: () => Promise<unknown>; filePath: string }
|
|
490
|
-
): Promise<Response> {
|
|
491
|
-
const segments = match.segments;
|
|
492
|
-
const leaf = segments[segments.length - 1];
|
|
493
|
-
|
|
494
|
-
// API routes (route.ts) — run access.ts standalone then dispatch to handler.
|
|
495
|
-
// No React render pass — AccessGate is not used, React.cache is not active.
|
|
496
|
-
// See design/04-authorization.md §"Auth in API Routes".
|
|
497
|
-
if (leaf.route && !leaf.page) {
|
|
498
|
-
return handleApiRoute(_req, match, segments, responseHeaders);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Parse X-Timber-State-Tree for RSC payload requests (client navigation).
|
|
502
|
-
// The state tree lists sync segments the client has cached — the server
|
|
503
|
-
// skips re-rendering those layouts for a smaller, faster RSC payload.
|
|
504
|
-
// Only used for RSC requests — HTML requests always get a full render.
|
|
505
|
-
// See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
506
|
-
const clientStateTree = isRscPayloadRequest(_req) ? parseClientStateTree(_req) : null;
|
|
507
|
-
|
|
508
|
-
// Build the React element tree — loads modules, collects access checks,
|
|
509
|
-
// resolves metadata. Access checks are NOT run here — they run inside
|
|
510
|
-
// AccessPreRunner during renderToReadableStream so that access.ts and
|
|
511
|
-
// render components share the same React.cache scope (TIM-662).
|
|
512
|
-
//
|
|
513
|
-
// DenySignal/RedirectSignal from access checks are caught by onError
|
|
514
|
-
// during stream consumption and handled by renderSsrResponse /
|
|
515
|
-
// buildRscPayloadResponse.
|
|
516
|
-
let routeResult;
|
|
517
|
-
const _buildStart = performance.now();
|
|
518
|
-
try {
|
|
519
|
-
routeResult = await buildRouteElement(_req, match, interception, clientStateTree);
|
|
520
|
-
} catch (error) {
|
|
521
|
-
// Param coercion failed — render the custom 404 page (status files / not-found).
|
|
522
|
-
// Previously returned a bare Response(null, { status: 404 }) which bypassed
|
|
523
|
-
// custom not-found pages. Now routes through renderNoMatchPage so apps with
|
|
524
|
-
// 404.tsx / not-found status files render their custom page.
|
|
525
|
-
if (error instanceof ParamCoercionError) {
|
|
526
|
-
return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
|
|
527
|
-
}
|
|
528
|
-
// No PageComponent found — same treatment as param coercion: render custom 404.
|
|
529
|
-
if (error instanceof Error && error.message.startsWith('No page component')) {
|
|
530
|
-
return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
|
|
531
|
-
}
|
|
532
|
-
throw error;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const _buildEnd = performance.now();
|
|
536
|
-
recordTiming({
|
|
537
|
-
name: 'build',
|
|
538
|
-
dur: Math.round(_buildEnd - _buildStart),
|
|
539
|
-
desc: 'build element tree',
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
const { element, headElements, layoutComponents, deferSuspenseFor, skippedSegments } =
|
|
543
|
-
routeResult;
|
|
544
|
-
|
|
545
|
-
// Build head HTML for injection into the SSR output.
|
|
546
|
-
// Collects CSS, fonts, and modulepreload from the build manifest for matched segments.
|
|
547
|
-
// In dev mode the manifest is empty — Vite HMR handles CSS/JS.
|
|
548
|
-
//
|
|
549
|
-
// Link headers (for 103 Early Hints) are emitted by the earlyHints pipeline
|
|
550
|
-
// stage before middleware runs. Here we only emit the <head> HTML fallback tags
|
|
551
|
-
// — these ensure resources load even on platforms without Early Hints support.
|
|
552
|
-
const typedManifest = buildManifest as BuildManifest;
|
|
553
|
-
let headHtml = '';
|
|
554
|
-
|
|
555
|
-
// CSS is handled by the RSC plugin via ReactDOM.preinit() with
|
|
556
|
-
// data-precedence attributes. This injects <link rel="stylesheet">
|
|
557
|
-
// tags during the RSC render phase — before our headHtml injection.
|
|
558
|
-
// We do NOT emit additional <link rel="stylesheet"> tags here because:
|
|
559
|
-
// 1. React's Float system deduplicates them, making ours redundant
|
|
560
|
-
// 2. The duplicate reference confuses React's client-side preload
|
|
561
|
-
// deduplication, causing "preload ignored" browser warnings
|
|
562
|
-
//
|
|
563
|
-
// CSS URLs are still collected for Early Hints (Link headers) in
|
|
564
|
-
// buildEarlyHintsHeaders() — those are HTTP headers, not DOM elements,
|
|
565
|
-
// so they don't conflict with Float.
|
|
566
|
-
|
|
567
|
-
// Font CSS is NOT inlined here anymore (TIM-828). The transform hook
|
|
568
|
-
// injects a side-effect `import 'virtual:timber-font-css/<hash>.css'`
|
|
569
|
-
// into each file that calls a font function. @vitejs/plugin-rsc's
|
|
570
|
-
// `collectCss` walks the RSC module graph, finds the virtual CSS
|
|
571
|
-
// module, and React Float `preinit()`s it as
|
|
572
|
-
// `<link rel="stylesheet" data-rsc-css-href=...>` — the same path
|
|
573
|
-
// component CSS rides on. Dev and production share a single path,
|
|
574
|
-
// with no `globalThis` channel and no dev/prod skew.
|
|
575
|
-
//
|
|
576
|
-
// Font preload hints (distinct from the @font-face CSS) still flow
|
|
577
|
-
// through `BuildManifest.fonts[importer]` and are emitted below plus
|
|
578
|
-
// as 103 Early Hints via `buildEarlyHintsHeaders()`.
|
|
579
|
-
const fontEntries = collectRouteFonts(segments, typedManifest);
|
|
580
|
-
if (fontEntries.length > 0) {
|
|
581
|
-
headHtml += buildFontPreloadTags(fontEntries);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Skip modulepreload tags when client JavaScript is disabled — no JS to preload.
|
|
585
|
-
if (!clientJsDisabled) {
|
|
586
|
-
const preloadUrls = collectRouteModulepreloads(segments, typedManifest);
|
|
587
|
-
if (preloadUrls.length > 0) {
|
|
588
|
-
headHtml += buildModulepreloadTags(preloadUrls);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
for (const el of headElements) {
|
|
593
|
-
if (el.tag === 'title' && el.content) {
|
|
594
|
-
headHtml += `<title>${escapeHtml(el.content)}</title>`;
|
|
595
|
-
} else if (el.attrs) {
|
|
596
|
-
const attrs = Object.entries(el.attrs)
|
|
597
|
-
.filter(([, v]) => v != null)
|
|
598
|
-
.map(([k, v]) => `${k}="${escapeHtml(v as string)}"`)
|
|
599
|
-
.join(' ');
|
|
600
|
-
headHtml += `<${el.tag} ${attrs}>`;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// In dev mode, inject the browser log forwarding script so console
|
|
605
|
-
// errors/warnings from the browser appear in the server terminal.
|
|
606
|
-
// Set by timber-dev-browser-logs plugin via globalThis (TIM-575).
|
|
607
|
-
if (isDevMode()) {
|
|
608
|
-
const devLogScript = (globalThis as Record<string, unknown>).__timber_dev_browser_log_script as
|
|
609
|
-
| string
|
|
610
|
-
| undefined;
|
|
611
|
-
if (devLogScript) {
|
|
612
|
-
headHtml += devLogScript;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// Render to RSC Flight stream with signal tracking.
|
|
617
|
-
const _rscStart = performance.now();
|
|
618
|
-
const { rscStream, signals, getDebugComponents } = renderRscStream(element, _req);
|
|
619
|
-
|
|
620
|
-
// Store the debug components getter in ALS so onPipelineError can
|
|
621
|
-
// include component tree context for render-phase errors (dev mode only).
|
|
622
|
-
// Per-request via ALS — no cross-request race. See TIM-557.
|
|
623
|
-
const alsStore = requestContextAls.getStore();
|
|
624
|
-
if (alsStore) {
|
|
625
|
-
alsStore.debugComponentsGetter = getDebugComponents;
|
|
626
|
-
}
|
|
627
|
-
recordTiming({
|
|
628
|
-
name: 'rsc-init',
|
|
629
|
-
dur: Math.round(performance.now() - _rscStart),
|
|
630
|
-
desc: 'RSC stream init',
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
// Synchronous redirect — redirect() in access.ts or a non-async component
|
|
634
|
-
// throws during renderToReadableStream creation. Return HTTP redirect.
|
|
635
|
-
if (signals.redirectSignal) {
|
|
636
|
-
return buildRedirectResponse(_req, signals.redirectSignal, responseHeaders);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Synchronous deny — deny() in a non-async component throws during
|
|
640
|
-
// renderToReadableStream creation, caught in the try/catch above.
|
|
641
|
-
if (signals.denySignal) {
|
|
642
|
-
if (isRscPayloadRequest(_req)) {
|
|
643
|
-
return renderDenyPageAsRsc(
|
|
644
|
-
signals.denySignal,
|
|
645
|
-
segments,
|
|
646
|
-
layoutComponents as LayoutEntry[],
|
|
647
|
-
responseHeaders,
|
|
648
|
-
createDebugChannelSink
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
return renderDenyPage(
|
|
652
|
-
signals.denySignal,
|
|
653
|
-
segments,
|
|
654
|
-
layoutComponents as LayoutEntry[],
|
|
655
|
-
_req,
|
|
656
|
-
match,
|
|
657
|
-
responseHeaders,
|
|
658
|
-
clientBootstrap,
|
|
659
|
-
createDebugChannelSink,
|
|
660
|
-
callSsr
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Synchronous render error — renderToReadableStream threw before
|
|
665
|
-
// creating the stream. Render the error page with correct 5xx status.
|
|
666
|
-
// (Async render errors are tracked in onError and handled after SSR.)
|
|
667
|
-
if (signals.renderError && !rscStream) {
|
|
668
|
-
return renderErrorPage(
|
|
669
|
-
signals.renderError.error,
|
|
670
|
-
signals.renderError.status,
|
|
671
|
-
segments,
|
|
672
|
-
layoutComponents as LayoutEntry[],
|
|
673
|
-
_req,
|
|
674
|
-
match,
|
|
675
|
-
responseHeaders,
|
|
676
|
-
clientBootstrap,
|
|
677
|
-
globalError
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// For RSC payload requests (client navigation), return the RSC Flight
|
|
682
|
-
// stream directly — skip SSR HTML rendering entirely.
|
|
683
|
-
// See design/19-client-navigation.md §"RSC Payload Handling"
|
|
684
|
-
if (isRscPayloadRequest(_req)) {
|
|
685
|
-
return buildRscPayloadResponse(
|
|
686
|
-
_req,
|
|
687
|
-
rscStream!,
|
|
688
|
-
signals,
|
|
689
|
-
segments,
|
|
690
|
-
layoutComponents,
|
|
691
|
-
headElements,
|
|
692
|
-
match,
|
|
693
|
-
responseHeaders,
|
|
694
|
-
skippedSegments
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Pipe through SSR for HTML rendering with streaming Suspense support.
|
|
699
|
-
return renderSsrResponse({
|
|
700
|
-
req: _req,
|
|
701
|
-
rscStream: rscStream!,
|
|
702
|
-
signals,
|
|
703
|
-
segments,
|
|
704
|
-
layoutComponents,
|
|
705
|
-
match,
|
|
706
|
-
responseHeaders,
|
|
707
|
-
clientBootstrap,
|
|
708
|
-
clientJsDisabled,
|
|
709
|
-
headHtml,
|
|
710
|
-
deferSuspenseFor,
|
|
711
|
-
globalError,
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
|
|
715
476
|
// Re-export for generated entry points (e.g., Nitro node-server/bun) to wrap
|
|
716
477
|
// the handler with per-request 103 Early Hints sender via ALS.
|
|
717
478
|
export { runWithEarlyHintsSender } from '../early-hints-sender.js';
|