@timber-js/app 0.2.0-alpha.6 → 0.2.0-alpha.61
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-CT98cU9c.js +121 -0
- package/dist/_chunks/define-CT98cU9c.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-BWr_52kY.js +93 -0
- package/dist/_chunks/define-cookie-BWr_52kY.js.map +1 -0
- package/dist/_chunks/error-boundary-DpZJBCqh.js +211 -0
- package/dist/_chunks/error-boundary-DpZJBCqh.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-Cey5DCGr.js} +129 -77
- package/dist/_chunks/interception-Cey5DCGr.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-rju2rbga.js} +97 -69
- package/dist/_chunks/request-context-rju2rbga.js.map +1 -0
- package/dist/_chunks/segment-context-CyaM1mrD.js +72 -0
- package/dist/_chunks/segment-context-CyaM1mrD.js.map +1 -0
- package/dist/_chunks/stale-reload-BSSym1MJ.js +64 -0
- package/dist/_chunks/stale-reload-BSSym1MJ.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-VYETCQsg.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-VYETCQsg.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-wEXY2JQB.js.map +1 -0
- package/dist/_chunks/wrappers-BaG1bnM3.js +63 -0
- package/dist/_chunks/wrappers-BaG1bnM3.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/error-reconstituter.d.ts +54 -0
- package/dist/client/error-reconstituter.d.ts.map +1 -0
- package/dist/client/form.d.ts +2 -2
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +433 -252
- package/dist/client/index.js.map +1 -1
- package/dist/client/link-pending-store.d.ts +78 -0
- package/dist/client/link-pending-store.d.ts.map +1 -0
- package/dist/client/link.d.ts +23 -9
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +2 -2
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/router.d.ts +25 -3
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +36 -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/segment-outlet.d.ts +63 -0
- package/dist/client/segment-outlet.d.ts.map +1 -0
- 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 +3 -3
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.d.ts +21 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/cookies/define-cookie.d.ts +34 -13
- 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 +127 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -242
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +100 -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 +4 -0
- package/dist/plugins/adapter-build.d.ts +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts +2 -2
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +3 -3
- package/dist/plugins/build-report.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/content.d.ts +1 -1
- package/dist/plugins/content.d.ts.map +1 -1
- package/dist/plugins/dev-browser-logs.d.ts +84 -0
- package/dist/plugins/dev-browser-logs.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/dev-logs.d.ts +1 -1
- package/dist/plugins/dev-logs.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +9 -2
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts +1 -1
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts +6 -5
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +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 +16 -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/actions.d.ts +1 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +25 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- package/dist/server/build-manifest.d.ts.map +1 -1
- 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/fallback-error.d.ts +4 -3
- package/dist/server/fallback-error.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 +42 -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 +1977 -1648
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +25 -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-interception.d.ts +1 -1
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +20 -6
- 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 +9 -2
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/api-handler.d.ts +2 -2
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +26 -13
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +48 -5
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +8 -3
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +3 -3
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +10 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts +1 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +19 -4
- 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/status-code-resolver.d.ts +1 -1
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/stream-utils.d.ts +36 -0
- package/dist/server/stream-utils.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 +20 -13
- 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/font-google.d.ts +1 -1
- package/dist/shims/font-google.d.ts.map +1 -1
- 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 -17
- 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 +151 -99
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/error-reconstituter.tsx +65 -0
- package/src/client/form.tsx +2 -2
- package/src/client/index.ts +10 -1
- package/src/client/link-pending-store.ts +136 -0
- package/src/client/link.tsx +137 -22
- package/src/client/navigation-context.ts +6 -5
- package/src/client/router.ts +117 -60
- package/src/client/rsc-fetch.ts +90 -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/segment-outlet.tsx +86 -0
- package/src/client/stale-reload.ts +73 -6
- package/src/client/top-loader.tsx +10 -9
- package/src/client/transition-root.tsx +20 -2
- package/src/client/use-params.ts +4 -4
- package/src/client/use-query-states.ts +2 -2
- package/src/codec.ts +21 -0
- package/src/cookies/define-cookie.ts +71 -20
- package/src/fonts/css.ts +2 -1
- package/src/index.ts +297 -85
- package/src/params/define.ts +327 -0
- package/src/params/index.ts +28 -0
- package/src/plugins/adapter-build.ts +8 -2
- package/src/plugins/build-manifest.ts +13 -2
- package/src/plugins/build-report.ts +3 -3
- package/src/plugins/cache-transform.ts +1 -1
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/content.ts +1 -1
- package/src/plugins/dev-browser-logs.ts +284 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-logs.ts +1 -1
- package/src/plugins/dev-server.ts +41 -7
- package/src/plugins/entries.ts +6 -8
- package/src/plugins/fonts.ts +102 -55
- package/src/plugins/mdx.ts +1 -1
- package/src/plugins/routing.ts +57 -17
- package/src/plugins/server-action-exports.ts +1 -1
- package/src/plugins/server-bundle.ts +32 -1
- package/src/plugins/shims.ts +69 -31
- package/src/plugins/static-build.ts +10 -6
- package/src/routing/codegen.ts +109 -88
- package/src/routing/scanner.ts +86 -7
- package/src/routing/status-file-lint.ts +3 -2
- package/src/routing/types.ts +17 -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 +8 -2
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +20 -3
- package/src/server/actions.ts +1 -1
- package/src/server/als-registry.ts +25 -4
- package/src/server/build-manifest.ts +10 -4
- package/src/server/compress.ts +25 -7
- package/src/server/debug.ts +1 -1
- package/src/server/default-logger.ts +99 -0
- package/src/server/deny-renderer.ts +5 -3
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +58 -15
- package/src/server/fallback-error.ts +29 -14
- package/src/server/flight-injection-state.ts +113 -0
- package/src/server/flight-scripts.ts +62 -0
- package/src/server/flush.ts +2 -1
- package/src/server/form-data.ts +76 -0
- package/src/server/html-injectors.ts +277 -117
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +44 -36
- package/src/server/node-stream-transforms.ts +509 -0
- package/src/server/pipeline-interception.ts +1 -1
- package/src/server/pipeline.ts +148 -41
- package/src/server/primitives.ts +47 -5
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +125 -119
- package/src/server/route-element-builder.ts +107 -115
- package/src/server/route-handler.ts +2 -1
- package/src/server/route-matcher.ts +9 -2
- package/src/server/rsc-entry/api-handler.ts +8 -8
- package/src/server/rsc-entry/error-renderer.ts +286 -81
- package/src/server/rsc-entry/helpers.ts +134 -5
- package/src/server/rsc-entry/index.ts +177 -76
- package/src/server/rsc-entry/rsc-payload.ts +91 -18
- package/src/server/rsc-entry/rsc-stream.ts +74 -18
- package/src/server/rsc-entry/ssr-bridge.ts +2 -2
- package/src/server/rsc-entry/ssr-renderer.ts +152 -34
- package/src/server/slot-resolver.ts +231 -220
- package/src/server/ssr-entry.ts +211 -32
- package/src/server/ssr-render.ts +289 -67
- package/src/server/ssr-wrappers.tsx +139 -0
- package/src/server/status-code-resolver.ts +1 -1
- package/src/server/stream-utils.ts +213 -0
- package/src/server/tracing.ts +23 -0
- package/src/server/tree-builder.ts +92 -58
- 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 +55 -0
- package/src/shims/font-google.ts +1 -1
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +2 -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/_chunks/use-query-states-D5KaffOK.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/client/link-status-provider.d.ts +0 -11
- package/dist/client/link-status-provider.d.ts.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/client/link-status-provider.tsx +0 -30
- 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
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
createFromFetch,
|
|
29
29
|
setServerCallback,
|
|
30
30
|
encodeReply,
|
|
31
|
-
} from '
|
|
31
|
+
} from '../rsc-runtime/browser.js';
|
|
32
32
|
// Shared-state modules MUST be imported from @timber-js/app/client (the public
|
|
33
33
|
// barrel) so they resolve to the same module instances as user code. In Vite dev,
|
|
34
34
|
// user code imports @timber-js/app/client from dist/ via package.json exports.
|
|
@@ -47,12 +47,24 @@ import {
|
|
|
47
47
|
NavigationProvider,
|
|
48
48
|
getNavigationState,
|
|
49
49
|
setNavigationState,
|
|
50
|
+
type NavigationState,
|
|
50
51
|
} from './navigation-context.js';
|
|
51
52
|
import { setupServerLogReplay, setupClientErrorForwarding } from './browser-dev.js';
|
|
52
53
|
// browser-links.ts removed — Link components own their click/hover handlers directly.
|
|
53
54
|
// See LOCAL-340.
|
|
54
55
|
import { TransitionRoot, transitionRender, navigateTransition } from './transition-root.js';
|
|
55
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
isStaleClientReference,
|
|
58
|
+
isChunkLoadError,
|
|
59
|
+
triggerStaleReload,
|
|
60
|
+
clearStaleReloadFlag,
|
|
61
|
+
} from './stale-reload.js';
|
|
62
|
+
import {
|
|
63
|
+
setClientDeploymentId,
|
|
64
|
+
getClientDeploymentId,
|
|
65
|
+
DEPLOYMENT_ID_HEADER,
|
|
66
|
+
RELOAD_HEADER,
|
|
67
|
+
} from './rsc-fetch.js';
|
|
56
68
|
|
|
57
69
|
// ─── Server Action Dispatch ──────────────────────────────────────
|
|
58
70
|
|
|
@@ -81,14 +93,28 @@ setServerCallback(async (id: string, args: unknown[]) => {
|
|
|
81
93
|
let hasRedirect = false;
|
|
82
94
|
let headElementsJson: string | null = null;
|
|
83
95
|
|
|
96
|
+
// Build action request headers. Include deployment ID for version
|
|
97
|
+
// skew detection (TIM-446) — the server rejects stale actions gracefully.
|
|
98
|
+
const actionHeaders: Record<string, string> = {
|
|
99
|
+
'Accept': 'text/x-component',
|
|
100
|
+
'x-rsc-action': id,
|
|
101
|
+
};
|
|
102
|
+
const actionDeploymentId = getClientDeploymentId();
|
|
103
|
+
if (actionDeploymentId) {
|
|
104
|
+
actionHeaders[DEPLOYMENT_ID_HEADER] = actionDeploymentId;
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
const response = fetch(window.location.href, {
|
|
85
108
|
method: 'POST',
|
|
86
|
-
headers:
|
|
87
|
-
'Accept': 'text/x-component',
|
|
88
|
-
'x-rsc-action': id,
|
|
89
|
-
},
|
|
109
|
+
headers: actionHeaders,
|
|
90
110
|
body,
|
|
91
111
|
}).then((res) => {
|
|
112
|
+
// Version skew detection (TIM-446): if the server signals a reload,
|
|
113
|
+
// trigger a full page load to pick up the new deployment.
|
|
114
|
+
if (res.headers.get(RELOAD_HEADER) === '1') {
|
|
115
|
+
window.location.reload();
|
|
116
|
+
throw new Error('Version skew detected — reloading page');
|
|
117
|
+
}
|
|
92
118
|
hasRevalidation = res.headers.get('X-Timber-Revalidation') === '1';
|
|
93
119
|
hasRedirect = res.headers.get('X-Timber-Redirect') != null;
|
|
94
120
|
headElementsJson = res.headers.get('X-Timber-Head');
|
|
@@ -155,7 +181,17 @@ setServerCallback(async (id: string, args: unknown[]) => {
|
|
|
155
181
|
* Hydrates the server-rendered HTML with React, then initializes
|
|
156
182
|
* client-side navigation for SPA transitions.
|
|
157
183
|
*/
|
|
158
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Read the current scroll position.
|
|
186
|
+
*
|
|
187
|
+
* Checks window scroll first, then explicit `data-timber-scroll-restoration`
|
|
188
|
+
* containers. With segment tree merging, shared layouts are reconciled in
|
|
189
|
+
* place via `cloneElement` — React preserves their DOM and scroll state
|
|
190
|
+
* naturally. We don't need to auto-detect overflow containers; only
|
|
191
|
+
* explicitly marked containers are tracked.
|
|
192
|
+
*
|
|
193
|
+
* See design/19-client-navigation.md §"Overflow Scroll Containers".
|
|
194
|
+
*/
|
|
159
195
|
function getScrollY(): number {
|
|
160
196
|
if (window.scrollY || document.documentElement.scrollTop || document.body.scrollTop) {
|
|
161
197
|
return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop;
|
|
@@ -163,74 +199,24 @@ function getScrollY(): number {
|
|
|
163
199
|
for (const el of document.querySelectorAll('[data-timber-scroll-restoration]')) {
|
|
164
200
|
if ((el as HTMLElement).scrollTop > 0) return (el as HTMLElement).scrollTop;
|
|
165
201
|
}
|
|
166
|
-
// Auto-detect: if window isn't scrolled, check for overflow containers.
|
|
167
|
-
// Common pattern: layouts use a scrollable div (overflow-y: auto/scroll)
|
|
168
|
-
// inside a fixed-height parent (h-screen). In this case window.scrollY is
|
|
169
|
-
// always 0 and the real scroll position lives on the overflow container.
|
|
170
|
-
const container = findOverflowContainer();
|
|
171
|
-
if (container && container.scrollTop > 0) return container.scrollTop;
|
|
172
202
|
return 0;
|
|
173
203
|
}
|
|
174
204
|
|
|
175
|
-
/**
|
|
176
|
-
* Find the primary overflow scroll container in the document.
|
|
177
|
-
*
|
|
178
|
-
* Walks direct children of body and their immediate children looking for
|
|
179
|
-
* an element with overflow-y: auto|scroll that is actually scrollable
|
|
180
|
-
* (scrollHeight > clientHeight). Returns the first match, or null.
|
|
181
|
-
*
|
|
182
|
-
* This heuristic covers the common layout patterns:
|
|
183
|
-
* <body> → <root-layout> → <div class="overflow-y-auto">
|
|
184
|
-
* <body> → <root-layout> → <main> → <nested-layout overflow-y-auto>
|
|
185
|
-
*
|
|
186
|
-
* We limit depth to 3 to avoid expensive full-tree traversals while still
|
|
187
|
-
* reaching nested layout scroll containers (e.g., parallel route layouts
|
|
188
|
-
* inside a root layout's <main> element).
|
|
189
|
-
*
|
|
190
|
-
* DIVERGENCE FROM NEXT.JS: Next.js's ScrollAndFocusHandler scrolls only
|
|
191
|
-
* document.documentElement.scrollTop — it does NOT handle overflow containers.
|
|
192
|
-
* Layouts using h-screen + overflow-y-auto have the same scroll bug in Next.js.
|
|
193
|
-
* This heuristic is a deliberate improvement. The tradeoff is fragility: depth-3
|
|
194
|
-
* traversal may miss deeply nested containers or match the wrong element.
|
|
195
|
-
* See design/19-client-navigation.md §"Overflow Scroll Containers".
|
|
196
|
-
*/
|
|
197
|
-
function findOverflowContainer(): HTMLElement | null {
|
|
198
|
-
const candidates: HTMLElement[] = [];
|
|
199
|
-
// Check body's descendants up to depth 3. Depth 3 covers the common case:
|
|
200
|
-
// <body> → <root-layout-div> → <main> → <overflow-container>
|
|
201
|
-
// React context providers (SegmentProvider, NavigationProvider) don't add
|
|
202
|
-
// DOM elements, so depth 3 from body reaches nested layout scroll containers.
|
|
203
|
-
for (const child of document.body.children) {
|
|
204
|
-
candidates.push(child as HTMLElement);
|
|
205
|
-
for (const grandchild of child.children) {
|
|
206
|
-
candidates.push(grandchild as HTMLElement);
|
|
207
|
-
for (const greatGrandchild of grandchild.children) {
|
|
208
|
-
candidates.push(greatGrandchild as HTMLElement);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
for (const el of candidates) {
|
|
213
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
214
|
-
const style = getComputedStyle(el);
|
|
215
|
-
const overflowY = style.overflowY;
|
|
216
|
-
if (
|
|
217
|
-
(overflowY === 'auto' || overflowY === 'scroll') &&
|
|
218
|
-
el.scrollHeight > el.clientHeight
|
|
219
|
-
) {
|
|
220
|
-
return el;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
205
|
function bootstrap(runtimeConfig: typeof config): void {
|
|
227
206
|
const _config = runtimeConfig;
|
|
228
207
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
208
|
+
// Initialize deployment ID for version skew detection (TIM-446).
|
|
209
|
+
// In dev mode this is null — skew checks are skipped.
|
|
210
|
+
const deploymentId = (_config as Record<string, unknown>).deploymentId as string | null;
|
|
211
|
+
if (deploymentId) {
|
|
212
|
+
setClientDeploymentId(deploymentId);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Take manual control of scroll restoration. Even though segment tree
|
|
216
|
+
// merging preserves shared layout DOM via cloneElement (so React doesn't
|
|
217
|
+
// reset scroll on those elements), the root-level reactRoot.render() with
|
|
218
|
+
// a new element tree can still cause scroll resets on the document during
|
|
219
|
+
// reconciliation. Manual control ensures consistent behavior.
|
|
234
220
|
window.history.scrollRestoration = 'manual';
|
|
235
221
|
|
|
236
222
|
// Hydrate the React tree from the RSC payload.
|
|
@@ -246,7 +232,13 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
246
232
|
// For subsequent navigations, it's fetched from the server.
|
|
247
233
|
type FlightSegment = [isBootstrap: 0] | [isData: 1, data: string];
|
|
248
234
|
|
|
249
|
-
|
|
235
|
+
// __timber_f is initialized in <head> via flightInitScript() (see
|
|
236
|
+
// flight-scripts.ts). If it doesn't exist, skip Flight decoding
|
|
237
|
+
// entirely and fall through to the createRoot branch.
|
|
238
|
+
// Do NOT defensively create it here: that would cause
|
|
239
|
+
// createFromReadableStream to be called on an empty stream, producing
|
|
240
|
+
// a "Connection closed" error on hydration. See TIM-552.
|
|
241
|
+
const timberChunks = (self as unknown as Record<string, FlightSegment[] | undefined>).__timber_f;
|
|
250
242
|
|
|
251
243
|
let _reactRoot: Root | null = null;
|
|
252
244
|
let initialElement: unknown = null;
|
|
@@ -318,6 +310,28 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
318
310
|
// Leaving the stream open is harmless: the page is being torn down.
|
|
319
311
|
function onDOMContentLoaded(): void {
|
|
320
312
|
if (isPageUnloading()) return;
|
|
313
|
+
|
|
314
|
+
// In dev mode, do NOT close the stream. React's RSC renderer
|
|
315
|
+
// includes debug owner/stack references ($1, $14, etc.) in the
|
|
316
|
+
// Flight payload that point to rows delivered through the debug
|
|
317
|
+
// channel, not the main Flight stream. The browser Flight client
|
|
318
|
+
// tracks these as pending chunks. Closing the stream with
|
|
319
|
+
// unresolved chunks triggers reportGlobalError("Connection closed")
|
|
320
|
+
// which kills the entire React tree.
|
|
321
|
+
//
|
|
322
|
+
// Leaving the stream open is harmless: React has already received
|
|
323
|
+
// all data rows and can hydrate fully. The pending debug chunks
|
|
324
|
+
// just remain unresolved (they're only used for React DevTools
|
|
325
|
+
// component stacks, not rendering).
|
|
326
|
+
//
|
|
327
|
+
// In production, debug rows are not emitted, so closing is safe.
|
|
328
|
+
if (process.env.NODE_ENV === 'development') {
|
|
329
|
+
// Mark as flushed so no more data is buffered, but don't close.
|
|
330
|
+
streamFlushed = true;
|
|
331
|
+
dataBuffer = undefined;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
321
335
|
if (streamWriter && !streamFlushed) {
|
|
322
336
|
streamWriter.close();
|
|
323
337
|
streamFlushed = true;
|
|
@@ -329,9 +343,23 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
329
343
|
if (document.readyState === 'loading') {
|
|
330
344
|
document.addEventListener('DOMContentLoaded', onDOMContentLoaded, false);
|
|
331
345
|
} else {
|
|
332
|
-
// DOM already parsed
|
|
333
|
-
//
|
|
334
|
-
|
|
346
|
+
// DOM already parsed. All inline RSC <script> tags have already
|
|
347
|
+
// executed and pushed their data into the buffer. The buffer was
|
|
348
|
+
// flushed into the stream during start() above.
|
|
349
|
+
//
|
|
350
|
+
// Close via queueMicrotask rather than setTimeout. setTimeout
|
|
351
|
+
// defers to the next macrotask, which can race with React's
|
|
352
|
+
// Flight client read loop — if React finishes reading all queued
|
|
353
|
+
// chunks and issues a reader.read() that pends, the stream is
|
|
354
|
+
// NOT closed yet (setTimeout hasn't fired), so React sees an
|
|
355
|
+
// open stream and waits. Then setTimeout fires and closes it.
|
|
356
|
+
// This works in theory but some React Flight builds interpret
|
|
357
|
+
// a mid-read close as "Connection closed" rather than clean EOF.
|
|
358
|
+
// queueMicrotask fires at the end of the current microtask
|
|
359
|
+
// checkpoint — after start() and createFromReadableStream
|
|
360
|
+
// initialization but before any macrotask, giving React a
|
|
361
|
+
// consistent close signal. See TIM-524.
|
|
362
|
+
queueMicrotask(onDOMContentLoaded);
|
|
335
363
|
}
|
|
336
364
|
|
|
337
365
|
const element = createFromReadableStream(rscPayload);
|
|
@@ -347,7 +375,7 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
347
375
|
|
|
348
376
|
// ── Initialize navigation state BEFORE hydration ───────────────────
|
|
349
377
|
// Read server-embedded params and set navigation state so that
|
|
350
|
-
//
|
|
378
|
+
// useSegmentParams() and usePathname() return correct values during hydration.
|
|
351
379
|
// This must happen before hydrateRoot so the NavigationProvider
|
|
352
380
|
// wrapping the element has the right values on the initial render.
|
|
353
381
|
const earlyParams = (self as unknown as Record<string, unknown>).__timber_params;
|
|
@@ -381,7 +409,10 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
381
409
|
element as React.ReactNode
|
|
382
410
|
);
|
|
383
411
|
const wrapped = createElement(TimberNuqsAdapter, null, withNav);
|
|
384
|
-
const rootElement = createElement(TransitionRoot, {
|
|
412
|
+
const rootElement = createElement(TransitionRoot, {
|
|
413
|
+
initial: wrapped,
|
|
414
|
+
topLoaderConfig: _config.topLoader,
|
|
415
|
+
});
|
|
385
416
|
_reactRoot = hydrateRoot(document, rootElement, {
|
|
386
417
|
// Suppress recoverable hydration errors from deny/error signals
|
|
387
418
|
// inside Suspense boundaries. The server already handled these
|
|
@@ -401,12 +432,24 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
401
432
|
},
|
|
402
433
|
});
|
|
403
434
|
} else {
|
|
404
|
-
// No RSC payload available
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
435
|
+
// No RSC payload available — create a non-hydrated root so client
|
|
436
|
+
// navigation can still render RSC payloads. The initial SSR HTML
|
|
437
|
+
// remains as-is; the first client navigation will replace it with
|
|
438
|
+
// a React-managed tree.
|
|
408
439
|
initRouter();
|
|
440
|
+
// Mount TransitionRoot so navigateTransition is wired up for client
|
|
441
|
+
// navigation. Without this, navigation from SSR-only/shell-less pages
|
|
442
|
+
// updates URL/history but leaves stale page content (TIM-580).
|
|
443
|
+
const navState = getNavigationState();
|
|
444
|
+
const placeholder = createElement('div') as unknown as React.ReactNode;
|
|
445
|
+
const withNav = createElement(NavigationProvider, { value: navState }, placeholder);
|
|
446
|
+
const wrapped = createElement(TimberNuqsAdapter, null, withNav);
|
|
447
|
+
const rootElement = createElement(TransitionRoot, {
|
|
448
|
+
initial: wrapped,
|
|
449
|
+
topLoaderConfig: _config.topLoader,
|
|
450
|
+
});
|
|
409
451
|
_reactRoot = createRoot(document);
|
|
452
|
+
_reactRoot.render(rootElement);
|
|
410
453
|
}
|
|
411
454
|
|
|
412
455
|
// ── Router initialization (hoisted above hydrateRoot) ────────────────
|
|
@@ -420,23 +463,20 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
420
463
|
pushState: (data, unused, url) => window.history.pushState(data, unused, url),
|
|
421
464
|
replaceState: (data, unused, url) => window.history.replaceState(data, unused, url),
|
|
422
465
|
scrollTo: (x, y) => {
|
|
466
|
+
// Scroll the document viewport.
|
|
423
467
|
window.scrollTo(x, y);
|
|
424
468
|
document.documentElement.scrollTop = y;
|
|
425
469
|
document.body.scrollTop = y;
|
|
426
|
-
//
|
|
470
|
+
// Scroll any element explicitly marked as a scroll container.
|
|
471
|
+
// With segment tree merging, shared layouts (sidebars, nav bars)
|
|
472
|
+
// are reconciled in place via cloneElement — React preserves their
|
|
473
|
+
// DOM and scroll state naturally. We no longer auto-detect overflow
|
|
474
|
+
// containers, which previously found the wrong element (e.g.,
|
|
475
|
+
// scrolling a sidebar instead of the main content area).
|
|
476
|
+
// Use `data-timber-scroll-restoration` to opt in specific containers.
|
|
427
477
|
for (const el of document.querySelectorAll('[data-timber-scroll-restoration]')) {
|
|
428
478
|
(el as HTMLElement).scrollTop = y;
|
|
429
479
|
}
|
|
430
|
-
// Auto-detect overflow containers for layouts that scroll inside
|
|
431
|
-
// a fixed-height wrapper (e.g., h-screen + overflow-y-auto).
|
|
432
|
-
// In these layouts, window.scrollY is always 0 and the real scroll
|
|
433
|
-
// lives on the overflow container. Without this, forward navigation
|
|
434
|
-
// between pages that share a layout with parallel route slots won't
|
|
435
|
-
// scroll to top — the router's window.scrollTo(0,0) is a no-op.
|
|
436
|
-
const container = findOverflowContainer();
|
|
437
|
-
if (container) {
|
|
438
|
-
container.scrollTop = y;
|
|
439
|
-
}
|
|
440
480
|
},
|
|
441
481
|
getCurrentUrl: () => window.location.pathname + window.location.search,
|
|
442
482
|
getScrollY,
|
|
@@ -471,8 +511,10 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
471
511
|
// For navigation renders (navigate, refresh, popstate-with-fetch),
|
|
472
512
|
// navigateTransition is used instead — it wraps the entire navigation
|
|
473
513
|
// in a React transition with useOptimistic for the pending URL.
|
|
474
|
-
|
|
475
|
-
|
|
514
|
+
//
|
|
515
|
+
// navState is passed explicitly by the router — no temporal coupling
|
|
516
|
+
// with getNavigationState().
|
|
517
|
+
renderRoot: (element: unknown, navState: NavigationState) => {
|
|
476
518
|
const withNav = createElement(
|
|
477
519
|
NavigationProvider,
|
|
478
520
|
{ value: navState },
|
|
@@ -488,13 +530,11 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
488
530
|
// commits (atomic with the new tree + params).
|
|
489
531
|
//
|
|
490
532
|
// The perform callback receives a wrapPayload function that wraps the
|
|
491
|
-
// decoded RSC payload with NavigationProvider + NuqsAdapter
|
|
492
|
-
//
|
|
493
|
-
// UPDATED navigation state (set by the router inside perform).
|
|
533
|
+
// decoded RSC payload with NavigationProvider + NuqsAdapter. navState
|
|
534
|
+
// is passed explicitly by the router — no getNavigationState() needed.
|
|
494
535
|
navigateTransition: (pendingUrl: string, perform) => {
|
|
495
536
|
return navigateTransition(pendingUrl, async () => {
|
|
496
|
-
const payload = await perform((rawPayload: unknown) => {
|
|
497
|
-
const navState = getNavigationState();
|
|
537
|
+
const payload = await perform((rawPayload: unknown, navState: NavigationState) => {
|
|
498
538
|
const withNav = createElement(
|
|
499
539
|
NavigationProvider,
|
|
500
540
|
{ value: navState },
|
|
@@ -586,7 +626,6 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
586
626
|
scrollTimer = setTimeout(() => {
|
|
587
627
|
const state = window.history.state;
|
|
588
628
|
if (state && typeof state === 'object') {
|
|
589
|
-
// Use getScrollY to capture scroll from overflow containers too.
|
|
590
629
|
window.history.replaceState({ ...state, scrollY: getScrollY() }, '');
|
|
591
630
|
}
|
|
592
631
|
}, 100);
|
|
@@ -655,8 +694,21 @@ clearStaleReloadFlag();
|
|
|
655
694
|
// If the payload references a module ID from a newer deployment, the error
|
|
656
695
|
// surfaces as an unhandled rejection during React's render/hydration cycle.
|
|
657
696
|
// This handler catches those errors and triggers a full page reload.
|
|
697
|
+
//
|
|
698
|
+
// Also catches chunk load failures (dynamic import of missing assets after
|
|
699
|
+
// a deployment) — these surface as "Failed to fetch dynamically imported module"
|
|
700
|
+
// or "Loading chunk <name> failed" errors. See TIM-446.
|
|
658
701
|
window.addEventListener('unhandledrejection', (event) => {
|
|
659
|
-
if (isStaleClientReference(event.reason)) {
|
|
702
|
+
if (isStaleClientReference(event.reason) || isChunkLoadError(event.reason)) {
|
|
703
|
+
event.preventDefault();
|
|
704
|
+
triggerStaleReload();
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// Also catch synchronous errors from chunk loads (some browsers throw
|
|
709
|
+
// TypeError synchronously instead of via unhandled rejection).
|
|
710
|
+
window.addEventListener('error', (event) => {
|
|
711
|
+
if (isChunkLoadError(event.error)) {
|
|
660
712
|
event.preventDefault();
|
|
661
713
|
triggerStaleReload();
|
|
662
714
|
}
|
|
@@ -65,7 +65,16 @@ type ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;
|
|
|
65
65
|
|
|
66
66
|
export interface TimberErrorBoundaryProps {
|
|
67
67
|
/** The component to render when an error is caught. */
|
|
68
|
-
fallbackComponent
|
|
68
|
+
fallbackComponent?: (...args: unknown[]) => ReactNode;
|
|
69
|
+
/**
|
|
70
|
+
* Pre-rendered fallback element. Used for MDX status files which are server
|
|
71
|
+
* components and cannot be passed as function props across the RSC→client
|
|
72
|
+
* boundary. When set, rendered directly instead of calling fallbackComponent.
|
|
73
|
+
*
|
|
74
|
+
* See design/10-error-handling.md §"Status-Code File Variants" — MDX status
|
|
75
|
+
* files are server components by default (zero client JS).
|
|
76
|
+
*/
|
|
77
|
+
fallbackElement?: ReactNode;
|
|
69
78
|
/**
|
|
70
79
|
* Status code filter. If set, only catches errors matching this status.
|
|
71
80
|
* 400 = any 4xx, 500 = any 5xx, specific number = exact match.
|
|
@@ -162,6 +171,14 @@ export class TimberErrorBoundary extends Component<
|
|
|
162
171
|
}
|
|
163
172
|
}
|
|
164
173
|
|
|
174
|
+
// Pre-rendered fallback element (MDX status files) — render directly.
|
|
175
|
+
// MDX components are server components that cannot be passed as function
|
|
176
|
+
// props across the RSC→client boundary. Instead, they are pre-rendered
|
|
177
|
+
// as elements in the RSC environment and passed here as fallbackElement.
|
|
178
|
+
if (this.props.fallbackElement != null) {
|
|
179
|
+
return this.props.fallbackElement;
|
|
180
|
+
}
|
|
181
|
+
|
|
165
182
|
// Render the fallback component with the right props shape.
|
|
166
183
|
if (parsed?.type === 'deny') {
|
|
167
184
|
return createElement(this.props.fallbackComponent as never, {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reconstitutes a SerializableError into a real Error instance before
|
|
5
|
+
* passing to the user's error component.
|
|
6
|
+
*
|
|
7
|
+
* TSX error pages are 'use client' components that receive { error: Error, digest, reset }.
|
|
8
|
+
* Error objects are not RSC-serializable (React Flight throws "Only plain objects
|
|
9
|
+
* can be passed to Client Components"). This wrapper receives the error as a plain
|
|
10
|
+
* SerializableError object, reconstitutes a real Error instance, and passes it
|
|
11
|
+
* to the user's error component — ensuring error instanceof Error works correctly.
|
|
12
|
+
*
|
|
13
|
+
* See design/spike-TIM-565-unify-error-pages.md §"Edge Case B"
|
|
14
|
+
* See design/10-error-handling.md §"RSC → SSR for Error Pages via SerializableError"
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createElement, type ReactNode, type ComponentType } from 'react';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Plain-object representation of an Error that can cross the RSC → client boundary.
|
|
21
|
+
* Stack is only included in dev mode (gated by isDevMode() on the server).
|
|
22
|
+
*/
|
|
23
|
+
export interface SerializableError {
|
|
24
|
+
message: string;
|
|
25
|
+
name: string;
|
|
26
|
+
stack?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Props for the ErrorReconstituter wrapper component.
|
|
31
|
+
* All props are RSC-serializable:
|
|
32
|
+
* - error: plain object (SerializableError)
|
|
33
|
+
* - digest: plain JSON or null
|
|
34
|
+
* - reset: undefined (only meaningful on client after boundary catch)
|
|
35
|
+
* - component: client module reference (RSC Flight serializes as opaque ref)
|
|
36
|
+
*/
|
|
37
|
+
interface ErrorReconstituterProps {
|
|
38
|
+
error: SerializableError;
|
|
39
|
+
digest: { code: string; data: unknown } | null;
|
|
40
|
+
reset: undefined;
|
|
41
|
+
component: ComponentType<{
|
|
42
|
+
error: Error;
|
|
43
|
+
digest: { code: string; data: unknown } | null;
|
|
44
|
+
reset: (() => void) | undefined;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Reconstitute a SerializableError into a real Error instance and render
|
|
50
|
+
* the user's error component with the proper props.
|
|
51
|
+
*/
|
|
52
|
+
export function ErrorReconstituter({
|
|
53
|
+
error: serialized,
|
|
54
|
+
digest,
|
|
55
|
+
reset,
|
|
56
|
+
component,
|
|
57
|
+
}: ErrorReconstituterProps): ReactNode {
|
|
58
|
+
// Reconstitute a real Error so instanceof checks work in user code
|
|
59
|
+
const error = Object.assign(new Error(serialized.message), {
|
|
60
|
+
name: serialized.name,
|
|
61
|
+
...(serialized.stack != null ? { stack: serialized.stack } : {}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return createElement(component, { error, digest, reset });
|
|
65
|
+
}
|
package/src/client/form.tsx
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { useActionState as reactUseActionState, useTransition } from 'react';
|
|
15
|
-
import type { ActionResult, ValidationErrors } from '
|
|
16
|
-
import type { FormFlashData } from '
|
|
15
|
+
import type { ActionResult, ValidationErrors } from '../server/action-client';
|
|
16
|
+
import type { FormFlashData } from '../server/form-flash';
|
|
17
17
|
|
|
18
18
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
19
19
|
|
package/src/client/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type { JsonSerializable, RenderErrorDigest } from './types';
|
|
|
5
5
|
|
|
6
6
|
// Navigation
|
|
7
7
|
export { Link, interpolateParams, resolveHref, validateLinkHref, buildLinkProps } from './link';
|
|
8
|
+
export { mergePreservedSearchParams } from '../shared/merge-search-params.js';
|
|
8
9
|
export type { LinkProps, LinkPropsWithHref, LinkPropsWithParams } from './link';
|
|
9
10
|
export type { OnNavigateHandler, OnNavigateEvent } from './link';
|
|
10
11
|
export { createRouter } from './router';
|
|
@@ -12,6 +13,7 @@ export type {
|
|
|
12
13
|
RouterInstance,
|
|
13
14
|
NavigationOptions,
|
|
14
15
|
RouterDeps,
|
|
16
|
+
RouterPhase,
|
|
15
17
|
RscDecoder,
|
|
16
18
|
RootRenderer,
|
|
17
19
|
} from './router';
|
|
@@ -42,7 +44,7 @@ export { useActionState, useFormAction, useFormErrors } from './form';
|
|
|
42
44
|
export type { UseActionStateFn, UseActionStateReturn, FormErrorsResult } from './form';
|
|
43
45
|
|
|
44
46
|
// Params
|
|
45
|
-
export {
|
|
47
|
+
export { useSegmentParams, setCurrentParams } from './use-params';
|
|
46
48
|
|
|
47
49
|
// Navigation context (framework-internal, used by browser-entry for atomic updates)
|
|
48
50
|
export { NavigationProvider, getNavigationState, setNavigationState } from './navigation-context';
|
|
@@ -55,6 +57,13 @@ export { useQueryStates, bindUseQueryStates } from './use-query-states';
|
|
|
55
57
|
export { useCookie } from './use-cookie';
|
|
56
58
|
export type { ClientCookieOptions, CookieSetter } from './use-cookie';
|
|
57
59
|
|
|
60
|
+
// Register the client cookie module with defineCookie's lazy reference.
|
|
61
|
+
// This runs at module load time in the client/SSR environment, wiring up
|
|
62
|
+
// the useCookie hook without a top-level import in define-cookie.ts.
|
|
63
|
+
import * as _useCookieMod from './use-cookie.js';
|
|
64
|
+
import { _registerUseCookieModule } from '../cookies/define-cookie.js';
|
|
65
|
+
_registerUseCookieModule(_useCookieMod);
|
|
66
|
+
|
|
58
67
|
// SSR data (framework-internal, used by ssr-entry to provide request data to hooks)
|
|
59
68
|
export { setSsrData, clearSsrData, getSsrData } from './ssr-data';
|
|
60
69
|
export type { SsrData } from './ssr-data';
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Pending Store — module-level singleton for per-link pending state.
|
|
3
|
+
*
|
|
4
|
+
* Tracks which link instance is currently navigating so that only the
|
|
5
|
+
* clicked link shows pending. All other links remain unaffected — zero
|
|
6
|
+
* wasted re-renders.
|
|
7
|
+
*
|
|
8
|
+
* The store holds:
|
|
9
|
+
* - A reference to the currently navigating link's state setter
|
|
10
|
+
* - A navigation counter to guard against stale clears (race condition
|
|
11
|
+
* when the user clicks Link B before Link A's navigation completes)
|
|
12
|
+
*
|
|
13
|
+
* Flow:
|
|
14
|
+
* 1. Link click handler: setLinkForCurrentNavigation(instance) →
|
|
15
|
+
* resets previous link (urgent), sets new link pending (urgent),
|
|
16
|
+
* stores setter + increments navId
|
|
17
|
+
* 2. TransitionRoot startTransition: captures navId, does async work
|
|
18
|
+
* 3. TransitionRoot commit: resetLinkPending(capturedNavId) →
|
|
19
|
+
* calls setter(IDLE) inside the transition (batched, atomic with tree)
|
|
20
|
+
* Only clears if navId matches (prevents stale T1 from clearing T2's link)
|
|
21
|
+
*
|
|
22
|
+
* SINGLETON GUARANTEE: Uses `globalThis` via `Symbol.for` keys (same
|
|
23
|
+
* pattern as NavigationContext) because the RSC client bundler can
|
|
24
|
+
* duplicate this module across chunks. Module-level variables would
|
|
25
|
+
* create separate instances per chunk.
|
|
26
|
+
*
|
|
27
|
+
* See design/19-client-navigation.md §"Per-Link Pending State"
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { LinkStatus } from './use-link-status.js';
|
|
31
|
+
|
|
32
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export interface LinkPendingInstance {
|
|
35
|
+
setLinkStatus: (status: LinkStatus) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Singleton Storage ───────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const LINK_PENDING_KEY = Symbol.for('__timber_link_pending');
|
|
41
|
+
|
|
42
|
+
/** Status object indicating link is pending — shared reference */
|
|
43
|
+
export const PENDING_LINK_STATUS: LinkStatus = { pending: true };
|
|
44
|
+
|
|
45
|
+
/** Status object indicating link is idle — shared reference */
|
|
46
|
+
export const IDLE_LINK_STATUS: LinkStatus = { pending: false };
|
|
47
|
+
|
|
48
|
+
interface LinkPendingState {
|
|
49
|
+
/** The link instance that initiated the current navigation, or null */
|
|
50
|
+
current: LinkPendingInstance | null;
|
|
51
|
+
/** Monotonically increasing counter — guards against stale clears */
|
|
52
|
+
navId: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getStore(): LinkPendingState {
|
|
56
|
+
const g = globalThis as Record<symbol, unknown>;
|
|
57
|
+
if (!g[LINK_PENDING_KEY]) {
|
|
58
|
+
g[LINK_PENDING_KEY] = { current: null, navId: 0 };
|
|
59
|
+
}
|
|
60
|
+
return g[LINK_PENDING_KEY] as LinkPendingState;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Public API ──────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register the link instance that initiated the current navigation.
|
|
67
|
+
*
|
|
68
|
+
* Called from <Link>'s click handler before router.navigate().
|
|
69
|
+
* - Resets the previous pending link to IDLE (urgent update, immediate)
|
|
70
|
+
* - Does NOT set the new link to PENDING here — the Link's click handler
|
|
71
|
+
* calls setLinkStatus(PENDING) directly for the eager show
|
|
72
|
+
* - Increments the navId counter for stale-clear protection
|
|
73
|
+
*
|
|
74
|
+
* Pass `null` to clear (e.g., for programmatic navigations).
|
|
75
|
+
*/
|
|
76
|
+
export function setLinkForCurrentNavigation(link: LinkPendingInstance | null): void {
|
|
77
|
+
const store = getStore();
|
|
78
|
+
const prev = store.current;
|
|
79
|
+
|
|
80
|
+
// Reset previous pending link to idle (urgent update — shows immediately)
|
|
81
|
+
if (prev && prev !== link) {
|
|
82
|
+
prev.setLinkStatus(IDLE_LINK_STATUS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
store.current = link;
|
|
86
|
+
store.navId++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the current navigation ID. Called at the start of the transition
|
|
91
|
+
* to capture the ID before async work begins.
|
|
92
|
+
*/
|
|
93
|
+
export function getCurrentNavId(): number {
|
|
94
|
+
return getStore().navId;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reset the current link's pending state to IDLE, but only if the navId
|
|
99
|
+
* matches. Called inside TransitionRoot's startTransition after the async
|
|
100
|
+
* work completes — the setter call is a transition update, so it commits
|
|
101
|
+
* atomically with the new tree.
|
|
102
|
+
*
|
|
103
|
+
* The navId guard prevents stale transitions from clearing a newer
|
|
104
|
+
* navigation's pending state. Scenario:
|
|
105
|
+
* Click A → navId=1, T1 starts
|
|
106
|
+
* Click B → navId=2, A reset to IDLE, T2 starts
|
|
107
|
+
* T1 completes → resetLinkPending(1) → navId is 2, skip ✓
|
|
108
|
+
* T2 completes → resetLinkPending(2) → navId is 2, clear ✓
|
|
109
|
+
*/
|
|
110
|
+
export function resetLinkPending(forNavId: number): void {
|
|
111
|
+
const store = getStore();
|
|
112
|
+
if (store.navId === forNavId && store.current) {
|
|
113
|
+
store.current.setLinkStatus(IDLE_LINK_STATUS);
|
|
114
|
+
store.current = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clean up the link pending store entirely. Safety net for error paths.
|
|
120
|
+
*/
|
|
121
|
+
export function clearLinkPendingSetter(): void {
|
|
122
|
+
const store = getStore();
|
|
123
|
+
store.current = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Unmount a link instance from navigation tracking. Called when a Link
|
|
128
|
+
* component unmounts while it is the current navigation link. Prevents
|
|
129
|
+
* calling setState on an unmounted component.
|
|
130
|
+
*/
|
|
131
|
+
export function unmountLinkForCurrentNavigation(link: LinkPendingInstance): void {
|
|
132
|
+
const store = getStore();
|
|
133
|
+
if (store.current === link) {
|
|
134
|
+
store.current = null;
|
|
135
|
+
}
|
|
136
|
+
}
|