@timber-js/app 0.2.0-alpha.5 → 0.2.0-alpha.50
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/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
- package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
- package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
- package/dist/_chunks/{debug-gwlJkDuf.js → debug-ECi_61pb.js} +2 -2
- package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
- package/dist/_chunks/define-TK8C1M3x.js +279 -0
- package/dist/_chunks/define-TK8C1M3x.js.map +1 -0
- package/dist/_chunks/define-cookie-k9btcEfI.js +93 -0
- package/dist/_chunks/define-cookie-k9btcEfI.js.map +1 -0
- package/dist/_chunks/error-boundary-B9vT_YK_.js +211 -0
- package/dist/_chunks/error-boundary-B9vT_YK_.js.map +1 -0
- package/dist/_chunks/{format-DviM89f0.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-DviM89f0.js.map → format-cX7wzEp2.js.map} +1 -1
- package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
- package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/{request-context-DIkVh_jG.js → request-context-0h-6Voad.js} +95 -69
- package/dist/_chunks/request-context-0h-6Voad.js.map +1 -0
- package/dist/_chunks/segment-context-DBn-nrMN.js +69 -0
- package/dist/_chunks/segment-context-DBn-nrMN.js.map +1 -0
- package/dist/_chunks/stale-reload-4L-_skC7.js +47 -0
- package/dist/_chunks/stale-reload-4L-_skC7.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-JI4cYUdz.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-JI4cYUdz.js.map} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-wEXY2JQB.js} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-wEXY2JQB.js.map} +1 -1
- package/dist/_chunks/wrappers-C9XPg7-U.js +63 -0
- package/dist/_chunks/wrappers-C9XPg7-U.js.map +1 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +17 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +56 -13
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/fast-hash.d.ts +22 -0
- package/dist/cache/fast-hash.d.ts.map +1 -0
- package/dist/cache/index.d.ts +5 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +90 -20
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/register-cached-function.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts +18 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +10 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -125
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +213 -93
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +22 -8
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +2 -2
- package/dist/client/router.d.ts +25 -3
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +23 -2
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-merger.d.ts.map +1 -1
- package/dist/client/stale-reload.d.ts +15 -0
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +1 -1
- package/dist/client/transition-root.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +2 -2
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/codec.d.ts +21 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/cookies/define-cookie.d.ts +33 -12
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -83
- package/dist/fonts/css.d.ts +1 -0
- package/dist/fonts/css.d.ts.map +1 -1
- package/dist/index.d.ts +112 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +467 -246
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +76 -0
- package/dist/params/define.d.ts.map +1 -0
- package/dist/params/index.d.ts +8 -0
- package/dist/params/index.d.ts.map +1 -0
- package/dist/params/index.js +105 -0
- package/dist/params/index.js.map +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/client-chunks.d.ts +32 -0
- package/dist/plugins/client-chunks.d.ts.map +1 -0
- package/dist/plugins/dev-error-overlay.d.ts +26 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +7 -0
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts +2 -2
- package/dist/routing/codegen.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 +2 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +6 -4
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +12 -0
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/search-params/codecs.d.ts +1 -1
- package/dist/search-params/define.d.ts +159 -0
- package/dist/search-params/define.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +4 -5
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +4 -474
- package/dist/search-params/registry.d.ts +1 -1
- package/dist/search-params/wrappers.d.ts +53 -0
- package/dist/search-params/wrappers.d.ts.map +1 -0
- package/dist/server/access-gate.d.ts +4 -0
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/action-encryption.d.ts +76 -0
- package/dist/server/action-encryption.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +18 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- package/dist/server/debug.d.ts +1 -1
- package/dist/server/default-logger.d.ts +22 -0
- package/dist/server/default-logger.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints.d.ts +13 -5
- package/dist/server/early-hints.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +4 -0
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/flight-injection-state.d.ts +66 -0
- package/dist/server/flight-injection-state.d.ts.map +1 -0
- package/dist/server/flight-scripts.d.ts +39 -0
- package/dist/server/flight-scripts.d.ts.map +1 -0
- package/dist/server/flush.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +29 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +51 -11
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1974 -1648
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +24 -7
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +113 -0
- package/dist/server/node-stream-transforms.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +7 -4
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +30 -3
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts +51 -0
- package/dist/server/render-timeout.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +65 -38
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +7 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +2 -2
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +46 -3
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +6 -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/rsc-entry/rsc-stream.d.ts +9 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +22 -0
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +39 -21
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +50 -0
- package/dist/server/ssr-wrappers.d.ts.map +1 -0
- package/dist/server/tracing.d.ts +10 -0
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +19 -12
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -3
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/version-skew.d.ts +61 -0
- package/dist/server/version-skew.d.ts.map +1 -0
- package/dist/server/waituntil-bridge.d.ts.map +1 -1
- package/dist/shared/merge-search-params.d.ts +22 -0
- package/dist/shared/merge-search-params.d.ts.map +1 -0
- package/dist/shims/navigation-client.d.ts +1 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +1 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/state-machine.d.ts +80 -0
- package/dist/utils/state-machine.d.ts.map +1 -0
- package/package.json +17 -14
- package/src/adapters/compress-module.ts +24 -4
- package/src/adapters/nitro.ts +58 -9
- package/src/cache/fast-hash.ts +34 -0
- package/src/cache/index.ts +5 -2
- package/src/cache/register-cached-function.ts +7 -3
- package/src/cache/singleflight.ts +62 -4
- package/src/cache/timber-cache.ts +40 -29
- package/src/cli.ts +0 -0
- package/src/client/browser-entry.ts +133 -93
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/index.ts +10 -1
- package/src/client/link.tsx +78 -19
- package/src/client/navigation-context.ts +2 -2
- package/src/client/router.ts +105 -60
- package/src/client/rsc-fetch.ts +63 -2
- package/src/client/segment-cache.ts +1 -1
- package/src/client/segment-context.ts +6 -1
- package/src/client/segment-merger.ts +2 -8
- package/src/client/stale-reload.ts +32 -6
- package/src/client/top-loader.tsx +10 -9
- package/src/client/transition-root.tsx +7 -1
- package/src/client/use-params.ts +3 -3
- package/src/client/use-query-states.ts +1 -1
- package/src/codec.ts +21 -0
- package/src/cookies/define-cookie.ts +69 -18
- package/src/fonts/css.ts +2 -1
- package/src/index.ts +280 -85
- package/src/params/define.ts +260 -0
- package/src/params/index.ts +28 -0
- package/src/plugins/adapter-build.ts +6 -0
- package/src/plugins/build-manifest.ts +11 -0
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-server.ts +38 -4
- package/src/plugins/entries.ts +5 -7
- package/src/plugins/fonts.ts +93 -42
- package/src/plugins/routing.ts +40 -14
- package/src/plugins/server-bundle.ts +32 -1
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +8 -4
- package/src/routing/codegen.ts +109 -88
- package/src/routing/scanner.ts +55 -6
- package/src/routing/status-file-lint.ts +2 -1
- package/src/routing/types.ts +7 -4
- package/src/rsc-runtime/rsc.ts +2 -0
- package/src/rsc-runtime/ssr.ts +50 -0
- package/src/rsc-runtime/vendor-types.d.ts +7 -0
- package/src/search-params/codecs.ts +1 -1
- package/src/search-params/define.ts +518 -0
- package/src/search-params/index.ts +12 -18
- package/src/search-params/registry.ts +1 -1
- package/src/search-params/wrappers.ts +85 -0
- package/src/server/access-gate.tsx +40 -9
- package/src/server/action-client.ts +7 -1
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +19 -2
- package/src/server/als-registry.ts +18 -4
- package/src/server/build-manifest.ts +4 -4
- package/src/server/compress.ts +25 -7
- package/src/server/debug.ts +1 -1
- package/src/server/default-logger.ts +98 -0
- package/src/server/deny-renderer.ts +2 -1
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +57 -14
- package/src/server/flight-injection-state.ts +113 -0
- package/src/server/flight-scripts.ts +59 -0
- package/src/server/flush.ts +2 -1
- package/src/server/form-data.ts +76 -0
- package/src/server/html-injectors.ts +261 -117
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +38 -35
- package/src/server/node-stream-transforms.ts +504 -0
- package/src/server/pipeline.ts +131 -39
- package/src/server/primitives.ts +47 -5
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +119 -119
- package/src/server/route-element-builder.ts +106 -114
- package/src/server/route-handler.ts +2 -1
- package/src/server/route-matcher.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +5 -3
- package/src/server/rsc-entry/helpers.ts +122 -3
- package/src/server/rsc-entry/index.ts +108 -43
- package/src/server/rsc-entry/rsc-payload.ts +52 -12
- package/src/server/rsc-entry/rsc-stream.ts +49 -12
- package/src/server/rsc-entry/ssr-renderer.ts +40 -13
- package/src/server/slot-resolver.ts +222 -217
- package/src/server/ssr-entry.ts +209 -30
- package/src/server/ssr-render.ts +289 -67
- package/src/server/ssr-wrappers.tsx +139 -0
- package/src/server/tracing.ts +23 -0
- package/src/server/tree-builder.ts +91 -57
- package/src/server/types.ts +1 -3
- package/src/server/version-skew.ts +104 -0
- package/src/server/waituntil-bridge.ts +4 -1
- package/src/shared/merge-search-params.ts +48 -0
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +1 -1
- package/src/utils/state-machine.ts +111 -0
- package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
- package/dist/_chunks/debug-gwlJkDuf.js.map +0 -1
- package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-DIkVh_jG.js.map +0 -1
- package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
- package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
- package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
- package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/cookies/index.js.map +0 -1
- package/dist/plugins/dynamic-transform.d.ts +0 -72
- package/dist/plugins/dynamic-transform.d.ts.map +0 -1
- package/dist/search-params/analyze.d.ts +0 -54
- package/dist/search-params/analyze.d.ts.map +0 -1
- package/dist/search-params/builtin-codecs.d.ts +0 -105
- package/dist/search-params/builtin-codecs.d.ts.map +0 -1
- package/dist/search-params/create.d.ts +0 -106
- package/dist/search-params/create.d.ts.map +0 -1
- package/dist/search-params/index.js.map +0 -1
- package/dist/server/prerender.d.ts +0 -77
- package/dist/server/prerender.d.ts.map +0 -1
- package/dist/server/response-cache.d.ts +0 -53
- package/dist/server/response-cache.d.ts.map +0 -1
- package/src/plugins/dynamic-transform.ts +0 -161
- package/src/search-params/analyze.ts +0 -192
- package/src/search-params/builtin-codecs.ts +0 -228
- package/src/search-params/create.ts +0 -321
- package/src/server/prerender.ts +0 -139
- package/src/server/response-cache.ts +0 -277
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
|
|
16
|
+
import { flightInitScript } from '#/server/flight-scripts.js';
|
|
16
17
|
import type { LayoutEntry } from '#/server/deny-renderer.js';
|
|
17
18
|
import { renderDenyPage } from '#/server/deny-renderer.js';
|
|
18
19
|
import type { RouteMatch } from '#/server/pipeline.js';
|
|
@@ -26,11 +27,12 @@ import {
|
|
|
26
27
|
buildSegmentInfo,
|
|
27
28
|
createDebugChannelSink,
|
|
28
29
|
isAbortError,
|
|
29
|
-
parseCookiesFromHeader,
|
|
30
30
|
} from './helpers.js';
|
|
31
|
+
import { getCookiesForSsr } from '#/server/request-context.js';
|
|
31
32
|
import { renderErrorPage } from './error-renderer.js';
|
|
32
33
|
import { callSsr } from './ssr-bridge.js';
|
|
33
34
|
import type { RenderSignals } from './rsc-stream.js';
|
|
35
|
+
import { recordTiming } from '#/server/server-timing.js';
|
|
34
36
|
|
|
35
37
|
interface SsrRenderOptions {
|
|
36
38
|
req: Request;
|
|
@@ -91,8 +93,8 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
91
93
|
? ''
|
|
92
94
|
: `<script>self.__timber_segments=${JSON.stringify(buildSegmentInfo(segments, layoutComponents))}</script>`;
|
|
93
95
|
|
|
94
|
-
// Embed route params in HTML so
|
|
95
|
-
// Without this,
|
|
96
|
+
// Embed route params in HTML so useSegmentParams() works on initial hydration.
|
|
97
|
+
// Without this, useSegmentParams() returns {} until the first client navigation.
|
|
96
98
|
const paramsScript =
|
|
97
99
|
clientJsDisabled || Object.keys(match.params).length === 0
|
|
98
100
|
? ''
|
|
@@ -104,13 +106,20 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
104
106
|
searchParams: Object.fromEntries(new URL(req.url).searchParams),
|
|
105
107
|
statusCode: 200,
|
|
106
108
|
responseHeaders,
|
|
107
|
-
headHtml:
|
|
109
|
+
headHtml:
|
|
110
|
+
headHtml +
|
|
111
|
+
clientBootstrap.preloadLinks +
|
|
112
|
+
segmentScript +
|
|
113
|
+
paramsScript +
|
|
114
|
+
// Initialize __timber_f in <head> so it exists before any streaming
|
|
115
|
+
// chunk scripts arrive in <body>. See flight-scripts.ts, LOCAL-415.
|
|
116
|
+
(clientJsDisabled ? '' : flightInitScript()),
|
|
108
117
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
109
118
|
// Skip RSC inline stream when client JS is disabled — no client to hydrate.
|
|
110
119
|
rscStream: clientJsDisabled ? undefined : inlineStream,
|
|
111
120
|
deferSuspenseFor: deferSuspenseFor > 0 ? deferSuspenseFor : undefined,
|
|
112
121
|
signal: req.signal,
|
|
113
|
-
cookies:
|
|
122
|
+
cookies: getCookiesForSsr(),
|
|
114
123
|
};
|
|
115
124
|
|
|
116
125
|
// Helper: check if render-phase signals were captured and return the
|
|
@@ -156,16 +165,34 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
156
165
|
try {
|
|
157
166
|
const ssrResponse = await callSsr(ssrStream, navContext);
|
|
158
167
|
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
168
|
+
// Record SSR sub-phase timings for Server-Timing header (detailed mode).
|
|
169
|
+
// These are populated by handleSsr() in the SSR environment and passed
|
|
170
|
+
// back via navContext._ssrTimings across the RSC→SSR boundary.
|
|
171
|
+
if (navContext._ssrTimings) {
|
|
172
|
+
const t = navContext._ssrTimings;
|
|
173
|
+
recordTiming({ name: 'ssr-decode', dur: t.decodeMs, desc: 'RSC Flight decode' });
|
|
174
|
+
recordTiming({ name: 'ssr-shell', dur: t.shellMs, desc: 'Fizz onShellReady' });
|
|
175
|
+
recordTiming({ name: 'ssr-pipeline', dur: t.pipelineMs, desc: 'stream transforms' });
|
|
176
|
+
recordTiming({
|
|
177
|
+
name: 'ssr-total',
|
|
178
|
+
dur: t.totalMs,
|
|
179
|
+
desc: t.nodeStreams ? 'SSR (Node streams)' : 'SSR (Web Streams)',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Signal promotion: check if any signals were captured during rendering
|
|
184
|
+
// inside Suspense boundaries. If no signals are present yet, yield one
|
|
185
|
+
// microtask so async component rejections propagate to the RSC onError
|
|
186
|
+
// callback before we commit the response.
|
|
163
187
|
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
188
|
+
// When signals are already captured (onSignal already fired), skip the
|
|
189
|
+
// yield entirely — react immediately. Uses queueMicrotask instead of
|
|
190
|
+
// setTimeout(0) for the fallback to avoid yielding to the full event
|
|
191
|
+
// loop (timers phase).
|
|
167
192
|
// See design/05-streaming.md §"deferSuspenseFor and the Hold Window"
|
|
168
|
-
|
|
193
|
+
if (!signals.redirectSignal && !signals.denySignal && !signals.renderError) {
|
|
194
|
+
await new Promise<void>((r) => queueMicrotask(r));
|
|
195
|
+
}
|
|
169
196
|
|
|
170
197
|
const promoted = checkCapturedSignals(/* skipHandledDeny */ true);
|
|
171
198
|
if (promoted) {
|
|
@@ -22,10 +22,123 @@ import { SlotAccessGate } from './access-gate.js';
|
|
|
22
22
|
import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
|
|
23
23
|
import type { InterceptionContext, RouteMatch } from './pipeline.js';
|
|
24
24
|
import { DenySignal } from './primitives.js';
|
|
25
|
+
import { logRenderError } from './logger.js';
|
|
25
26
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
26
27
|
|
|
27
28
|
type CreateElementFn = (...args: unknown[]) => React.ReactElement;
|
|
28
29
|
|
|
30
|
+
// ─── Module Loading Helpers ─────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load a module and extract its default export as a component function.
|
|
34
|
+
* Returns undefined if no default export exists.
|
|
35
|
+
*/
|
|
36
|
+
async function loadComponent(loader: {
|
|
37
|
+
load: () => Promise<unknown>;
|
|
38
|
+
}): Promise<((...args: unknown[]) => unknown) | undefined> {
|
|
39
|
+
const mod = (await loader.load()) as Record<string, unknown>;
|
|
40
|
+
return mod.default as ((...args: unknown[]) => unknown) | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load and render the default.tsx fallback for a slot node.
|
|
45
|
+
* Returns null if the slot has no default.tsx or it has no default export.
|
|
46
|
+
*/
|
|
47
|
+
async function renderDefaultFallback(
|
|
48
|
+
slotNode: ManifestSegmentNode,
|
|
49
|
+
h: CreateElementFn
|
|
50
|
+
): Promise<React.ReactElement | null> {
|
|
51
|
+
if (!slotNode.default) return null;
|
|
52
|
+
const DefaultComp = await loadComponent(slotNode.default);
|
|
53
|
+
if (!DefaultComp) return null;
|
|
54
|
+
return h(DefaultComp, {});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Segment Tree Matching ──────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find a matching child node for a URL segment name.
|
|
61
|
+
*
|
|
62
|
+
* Tries matches in priority order:
|
|
63
|
+
* 1. Static segment (exact name match)
|
|
64
|
+
* 2. Dynamic segment ([param])
|
|
65
|
+
* 3. Catch-all or optional-catch-all ([...param] / [[...param]])
|
|
66
|
+
* 4. Group children (transparent wrappers)
|
|
67
|
+
*
|
|
68
|
+
* Returns `{ node, consumesRest }` where `consumesRest` is true for catch-all
|
|
69
|
+
* segments that consume all remaining URL parts.
|
|
70
|
+
*/
|
|
71
|
+
function findMatchingChild(
|
|
72
|
+
children: ManifestSegmentNode[],
|
|
73
|
+
segmentName: string
|
|
74
|
+
): { node: ManifestSegmentNode; consumesRest: boolean } | null {
|
|
75
|
+
// 1. Static match
|
|
76
|
+
for (const child of children) {
|
|
77
|
+
if (child.segmentType === 'static' && child.segmentName === segmentName) {
|
|
78
|
+
return { node: child, consumesRest: false };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. Dynamic match
|
|
83
|
+
for (const child of children) {
|
|
84
|
+
if (child.segmentType === 'dynamic') {
|
|
85
|
+
return { node: child, consumesRest: false };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 3. Catch-all match — consumes all remaining segments
|
|
90
|
+
for (const child of children) {
|
|
91
|
+
if (child.segmentType === 'catch-all' || child.segmentType === 'optional-catch-all') {
|
|
92
|
+
return { node: child, consumesRest: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 4. Group children (transparent)
|
|
97
|
+
for (const child of children) {
|
|
98
|
+
if (child.segmentType === 'group') {
|
|
99
|
+
for (const groupChild of child.children ?? []) {
|
|
100
|
+
if (groupChild.segmentName === segmentName) {
|
|
101
|
+
return { node: groupChild, consumesRest: false };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Walk a segment tree from `startNode`, matching each part in `parts` against
|
|
112
|
+
* child nodes. Returns the chain of matched nodes (including startNode) and the
|
|
113
|
+
* final node, or null if any part fails to match.
|
|
114
|
+
*/
|
|
115
|
+
function walkSegmentTree(
|
|
116
|
+
startNode: ManifestSegmentNode,
|
|
117
|
+
parts: { segmentName: string }[] | string[],
|
|
118
|
+
initialChain: ManifestSegmentNode[] = [startNode]
|
|
119
|
+
): { chain: ManifestSegmentNode[]; leaf: ManifestSegmentNode } | null {
|
|
120
|
+
const chain = [...initialChain];
|
|
121
|
+
let currentNode = startNode;
|
|
122
|
+
|
|
123
|
+
for (const part of parts) {
|
|
124
|
+
const segName = typeof part === 'string' ? part : part.segmentName;
|
|
125
|
+
const directChildren = currentNode.children ?? [];
|
|
126
|
+
const match = findMatchingChild(directChildren, segName);
|
|
127
|
+
|
|
128
|
+
if (!match) return null;
|
|
129
|
+
|
|
130
|
+
chain.push(match.node);
|
|
131
|
+
currentNode = match.node;
|
|
132
|
+
|
|
133
|
+
// Catch-all segments consume all remaining parts
|
|
134
|
+
if (match.consumesRest) break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { chain, leaf: currentNode };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Slot Element Resolution ────────────────────────────────────────────────
|
|
141
|
+
|
|
29
142
|
/**
|
|
30
143
|
* Resolve the element for a parallel slot.
|
|
31
144
|
*
|
|
@@ -40,7 +153,6 @@ type CreateElementFn = (...args: unknown[]) => React.ReactElement;
|
|
|
40
153
|
export async function resolveSlotElement(
|
|
41
154
|
slotNode: ManifestSegmentNode,
|
|
42
155
|
match: RouteMatch,
|
|
43
|
-
paramsPromise: Promise<Record<string, string | string[]>>,
|
|
44
156
|
h: CreateElementFn,
|
|
45
157
|
interception?: InterceptionContext
|
|
46
158
|
): Promise<React.ReactElement | null> {
|
|
@@ -54,125 +166,61 @@ export async function resolveSlotElement(
|
|
|
54
166
|
: findSlotMatch(slotNode, match);
|
|
55
167
|
|
|
56
168
|
if (slotMatch) {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
const SlotPage = mod.default as (...args: unknown[]) => unknown;
|
|
60
|
-
|
|
169
|
+
const SlotPage = await loadComponent(slotMatch.page);
|
|
170
|
+
if (SlotPage) {
|
|
61
171
|
// Load default.tsx fallback for notFound() handling in the slot page.
|
|
62
172
|
// When a slot page calls notFound() or deny(), it should gracefully
|
|
63
173
|
// degrade to default.tsx or null — not crash the page. This matches
|
|
64
174
|
// Next.js behavior. See design/02-rendering-pipeline.md
|
|
65
175
|
// §"Slot Access Failure = Graceful Degradation"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
176
|
+
const denyFallback = await renderDefaultFallback(slotNode, h);
|
|
177
|
+
|
|
178
|
+
// Wrap the slot page to catch ALL errors at the component level.
|
|
179
|
+
// This prevents errors from leaving unresolved Flight rows in the
|
|
180
|
+
// RSC stream — when a slot component throws and the error propagates
|
|
181
|
+
// to React's Flight renderer, it may not emit a resolution row for
|
|
182
|
+
// the slot's lazy reference. The client's createFromReadableStream
|
|
183
|
+
// then throws "Connection closed" when the stream ends with pending
|
|
184
|
+
// references. By catching all errors here and returning a fallback,
|
|
185
|
+
// React sees a resolved component and emits a proper Flight row.
|
|
186
|
+
//
|
|
187
|
+
// DenySignal (from notFound() or deny()) returns the deny fallback.
|
|
188
|
+
// All other errors return the deny fallback or null — the slot
|
|
189
|
+
// gracefully degrades rather than breaking the entire page.
|
|
190
|
+
// See TIM-524.
|
|
81
191
|
const SafeSlotPage = async (props: Record<string, unknown>) => {
|
|
82
192
|
try {
|
|
83
193
|
return await (SlotPage as (props: Record<string, unknown>) => unknown)(props);
|
|
84
194
|
} catch (error) {
|
|
85
195
|
if (error instanceof DenySignal) {
|
|
86
|
-
return
|
|
196
|
+
return denyFallback;
|
|
87
197
|
}
|
|
88
|
-
throw
|
|
198
|
+
// Log the error but don't re-throw — returning fallback ensures
|
|
199
|
+
// the Flight row is resolved and the page hydrates correctly.
|
|
200
|
+
logRenderError({
|
|
201
|
+
method: '',
|
|
202
|
+
path: '',
|
|
203
|
+
error,
|
|
204
|
+
});
|
|
205
|
+
return denyFallback;
|
|
89
206
|
}
|
|
90
207
|
};
|
|
91
208
|
|
|
92
|
-
let element: React.ReactElement = h(SafeSlotPage, {
|
|
93
|
-
params: paramsPromise,
|
|
94
|
-
searchParams: {},
|
|
95
|
-
});
|
|
209
|
+
let element: React.ReactElement = h(SafeSlotPage, {});
|
|
96
210
|
|
|
97
211
|
// Wrap with error boundaries and layouts from intermediate slot segments
|
|
98
212
|
// (everything between slot root and leaf). Process innermost-first, same
|
|
99
213
|
// order as route-element-builder.ts handles main segments. The slot root
|
|
100
214
|
// (index 0) is handled separately after the access gate below.
|
|
101
|
-
|
|
102
|
-
const seg = slotMatch.chain[i];
|
|
103
|
-
|
|
104
|
-
// Error boundaries from this segment
|
|
105
|
-
element = await wrapSegmentWithErrorBoundaries(seg, element, h);
|
|
106
|
-
|
|
107
|
-
// Layout from this segment
|
|
108
|
-
if (seg.layout) {
|
|
109
|
-
const layoutMod = (await seg.layout.load()) as Record<string, unknown>;
|
|
110
|
-
if (layoutMod.default) {
|
|
111
|
-
const Layout = layoutMod.default as (...args: unknown[]) => unknown;
|
|
112
|
-
element = h(Layout, {
|
|
113
|
-
params: paramsPromise,
|
|
114
|
-
searchParams: {},
|
|
115
|
-
children: element,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
215
|
+
element = await wrapWithIntermediateSegments(slotMatch.chain, element, h);
|
|
120
216
|
|
|
121
217
|
// Wrap in SlotAccessGate if slot root has access.ts.
|
|
122
218
|
// On denial: denied.tsx → default.tsx → null (graceful degradation).
|
|
123
219
|
// See design/04-authorization.md §"Slot-Level Auth".
|
|
124
|
-
|
|
125
|
-
const accessMod = (await slotNode.access.load()) as Record<string, unknown>;
|
|
126
|
-
const accessFn = accessMod.default as
|
|
127
|
-
| ((ctx: { params: Record<string, string | string[]>; searchParams: unknown }) => unknown)
|
|
128
|
-
| undefined;
|
|
129
|
-
if (accessFn) {
|
|
130
|
-
// Load denied.tsx fallback
|
|
131
|
-
let deniedFallback: React.ReactElement | null = null;
|
|
132
|
-
if (slotNode.denied) {
|
|
133
|
-
const deniedMod = (await slotNode.denied.load()) as Record<string, unknown>;
|
|
134
|
-
const DeniedComponent = deniedMod.default as
|
|
135
|
-
| ((...args: unknown[]) => unknown)
|
|
136
|
-
| undefined;
|
|
137
|
-
if (DeniedComponent) {
|
|
138
|
-
deniedFallback = h(DeniedComponent, {});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Load default.tsx fallback
|
|
143
|
-
let defaultFallback: React.ReactElement | null = null;
|
|
144
|
-
if (slotNode.default) {
|
|
145
|
-
const defaultMod = (await slotNode.default.load()) as Record<string, unknown>;
|
|
146
|
-
const DefaultComp = defaultMod.default as ((...args: unknown[]) => unknown) | undefined;
|
|
147
|
-
if (DefaultComp) {
|
|
148
|
-
defaultFallback = h(DefaultComp, { params: paramsPromise, searchParams: {} });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const params = await paramsPromise;
|
|
153
|
-
element = h(SlotAccessGate, {
|
|
154
|
-
accessFn,
|
|
155
|
-
params,
|
|
156
|
-
searchParams: {},
|
|
157
|
-
deniedFallback,
|
|
158
|
-
defaultFallback,
|
|
159
|
-
children: element,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
220
|
+
element = await wrapWithAccessGate(slotNode, element, h);
|
|
163
221
|
|
|
164
222
|
// Wrap with slot root's layout (outermost, outside access gate)
|
|
165
|
-
|
|
166
|
-
const layoutMod = (await slotNode.layout.load()) as Record<string, unknown>;
|
|
167
|
-
if (layoutMod.default) {
|
|
168
|
-
const Layout = layoutMod.default as (...args: unknown[]) => unknown;
|
|
169
|
-
element = h(Layout, {
|
|
170
|
-
params: paramsPromise,
|
|
171
|
-
searchParams: {},
|
|
172
|
-
children: element,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
223
|
+
element = await wrapWithLayout(slotNode, element, h);
|
|
176
224
|
|
|
177
225
|
// Wrap with slot root's error boundaries (outermost)
|
|
178
226
|
element = await wrapSegmentWithErrorBoundaries(slotNode, element, h);
|
|
@@ -195,18 +243,81 @@ export async function resolveSlotElement(
|
|
|
195
243
|
}
|
|
196
244
|
|
|
197
245
|
// No matching page — render default.tsx fallback
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
246
|
+
return renderDefaultFallback(slotNode, h);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─── Element Wrapping Helpers ───────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Wrap an element with error boundaries and layouts from intermediate
|
|
253
|
+
* slot segments (indices 1..n, skipping the slot root at index 0).
|
|
254
|
+
* Processes innermost-first to match route-element-builder.ts ordering.
|
|
255
|
+
*/
|
|
256
|
+
async function wrapWithIntermediateSegments(
|
|
257
|
+
chain: ManifestSegmentNode[],
|
|
258
|
+
element: React.ReactElement,
|
|
259
|
+
h: CreateElementFn
|
|
260
|
+
): Promise<React.ReactElement> {
|
|
261
|
+
for (let i = chain.length - 1; i > 0; i--) {
|
|
262
|
+
const seg = chain[i];
|
|
263
|
+
element = await wrapSegmentWithErrorBoundaries(seg, element, h);
|
|
264
|
+
element = await wrapWithLayout(seg, element, h);
|
|
204
265
|
}
|
|
266
|
+
return element;
|
|
267
|
+
}
|
|
205
268
|
|
|
206
|
-
|
|
207
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Wrap an element with a segment's layout component, if present.
|
|
271
|
+
*/
|
|
272
|
+
async function wrapWithLayout(
|
|
273
|
+
node: ManifestSegmentNode,
|
|
274
|
+
element: React.ReactElement,
|
|
275
|
+
h: CreateElementFn
|
|
276
|
+
): Promise<React.ReactElement> {
|
|
277
|
+
if (!node.layout) return element;
|
|
278
|
+
const Layout = await loadComponent(node.layout);
|
|
279
|
+
if (!Layout) return element;
|
|
280
|
+
return h(Layout, { children: element });
|
|
208
281
|
}
|
|
209
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Wrap an element with a SlotAccessGate if the node has access.ts.
|
|
285
|
+
* On denial: denied.tsx → default.tsx → null (graceful degradation).
|
|
286
|
+
*/
|
|
287
|
+
async function wrapWithAccessGate(
|
|
288
|
+
slotNode: ManifestSegmentNode,
|
|
289
|
+
element: React.ReactElement,
|
|
290
|
+
h: CreateElementFn
|
|
291
|
+
): Promise<React.ReactElement> {
|
|
292
|
+
if (!slotNode.access) return element;
|
|
293
|
+
|
|
294
|
+
const accessFn = await loadComponent(slotNode.access);
|
|
295
|
+
if (!accessFn) return element;
|
|
296
|
+
|
|
297
|
+
// Pass the component (not pre-built element) so SlotAccessGate can
|
|
298
|
+
// forward DenySignal.data as dangerouslyPassData dynamically. See TIM-488.
|
|
299
|
+
let DeniedComponent: ((...args: unknown[]) => unknown) | null = null;
|
|
300
|
+
if (slotNode.denied) {
|
|
301
|
+
DeniedComponent = (await loadComponent(slotNode.denied)) ?? null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Extract slot name from the directory name (strip @ prefix)
|
|
305
|
+
const slotName = slotNode.segmentName?.replace(/^@/, '') ?? '';
|
|
306
|
+
|
|
307
|
+
const defaultFallback = await renderDefaultFallback(slotNode, h);
|
|
308
|
+
|
|
309
|
+
return h(SlotAccessGate, {
|
|
310
|
+
accessFn,
|
|
311
|
+
DeniedComponent,
|
|
312
|
+
slotName,
|
|
313
|
+
createElement: h,
|
|
314
|
+
defaultFallback,
|
|
315
|
+
children: element,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─── Slot Matching ──────────────────────────────────────────────────────────
|
|
320
|
+
|
|
210
321
|
/** Result of matching a slot's sub-tree against the current route. */
|
|
211
322
|
interface SlotMatchResult {
|
|
212
323
|
/** The page file at the matched leaf. */
|
|
@@ -267,74 +378,10 @@ function findSlotMatch(slotNode: ManifestSegmentNode, match: RouteMatch): SlotMa
|
|
|
267
378
|
return null;
|
|
268
379
|
}
|
|
269
380
|
|
|
270
|
-
// Walk the slot's children to match remaining URL segments
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
for (const seg of remainingSegments) {
|
|
275
|
-
const childName = seg.segmentName;
|
|
276
|
-
const directChildren = currentNode.children ?? [];
|
|
277
|
-
|
|
278
|
-
let found: ManifestSegmentNode | null = null;
|
|
279
|
-
for (const child of directChildren) {
|
|
280
|
-
// Exact static match
|
|
281
|
-
if (child.segmentType === 'static' && child.segmentName === childName) {
|
|
282
|
-
found = child;
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Try dynamic segments if no static match
|
|
288
|
-
if (!found) {
|
|
289
|
-
for (const child of directChildren) {
|
|
290
|
-
if (child.segmentType === 'dynamic') {
|
|
291
|
-
found = child;
|
|
292
|
-
break;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Try catch-all segments — these consume ALL remaining URL segments,
|
|
298
|
-
// so we break out of the outer loop immediately.
|
|
299
|
-
if (!found) {
|
|
300
|
-
for (const child of directChildren) {
|
|
301
|
-
if (child.segmentType === 'catch-all' || child.segmentType === 'optional-catch-all') {
|
|
302
|
-
found = child;
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (found) {
|
|
307
|
-
chain.push(found);
|
|
308
|
-
currentNode = found;
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Try group children (transparent)
|
|
314
|
-
if (!found) {
|
|
315
|
-
for (const child of directChildren) {
|
|
316
|
-
if (child.segmentType === 'group') {
|
|
317
|
-
for (const groupChild of child.children ?? []) {
|
|
318
|
-
if (groupChild.segmentName === childName) {
|
|
319
|
-
found = groupChild;
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (found) break;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (!found) {
|
|
329
|
-
// No matching child in slot tree — slot doesn't match this URL
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
chain.push(found);
|
|
333
|
-
currentNode = found;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (currentNode.page) {
|
|
337
|
-
return { page: currentNode.page, chain };
|
|
381
|
+
// Walk the slot's children to match remaining URL segments
|
|
382
|
+
const result = walkSegmentTree(slotNode, remainingSegments);
|
|
383
|
+
if (result && result.leaf.page) {
|
|
384
|
+
return { page: result.leaf.page, chain: result.chain };
|
|
338
385
|
}
|
|
339
386
|
return null;
|
|
340
387
|
}
|
|
@@ -374,59 +421,17 @@ function findInterceptingMatch(
|
|
|
374
421
|
|
|
375
422
|
// Walk the intercepting child's sub-tree to match remaining target parts
|
|
376
423
|
const remaining = targetParts.slice(matchIdx + 1);
|
|
377
|
-
const chain: ManifestSegmentNode[] = [slotNode, child];
|
|
378
424
|
|
|
379
425
|
if (remaining.length === 0) {
|
|
380
426
|
if (child.page) {
|
|
381
|
-
return { page: child.page, chain };
|
|
427
|
+
return { page: child.page, chain: [slotNode, child] };
|
|
382
428
|
}
|
|
383
429
|
continue;
|
|
384
430
|
}
|
|
385
431
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const children = currentNode.children ?? [];
|
|
390
|
-
let found: ManifestSegmentNode | null = null;
|
|
391
|
-
|
|
392
|
-
// Static match
|
|
393
|
-
for (const c of children) {
|
|
394
|
-
if (c.segmentType === 'static' && c.segmentName === part) {
|
|
395
|
-
found = c;
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Dynamic match
|
|
401
|
-
if (!found) {
|
|
402
|
-
for (const c of children) {
|
|
403
|
-
if (c.segmentType === 'dynamic') {
|
|
404
|
-
found = c;
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Catch-all match
|
|
411
|
-
if (!found) {
|
|
412
|
-
for (const c of children) {
|
|
413
|
-
if (c.segmentType === 'catch-all' || c.segmentType === 'optional-catch-all') {
|
|
414
|
-
found = c;
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (!found) {
|
|
421
|
-
matched = false;
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
chain.push(found);
|
|
425
|
-
currentNode = found;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (matched && currentNode.page) {
|
|
429
|
-
return { page: currentNode.page, chain };
|
|
432
|
+
const result = walkSegmentTree(child, remaining, [slotNode, child]);
|
|
433
|
+
if (result && result.leaf.page) {
|
|
434
|
+
return { page: result.leaf.page, chain: result.chain };
|
|
430
435
|
}
|
|
431
436
|
}
|
|
432
437
|
|