@timber-js/app 0.2.0-alpha.71 → 0.2.0-alpha.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/actions-Dg-ANYHb.js +421 -0
- package/dist/_chunks/actions-Dg-ANYHb.js.map +1 -0
- package/dist/_chunks/{als-registry-BJARkOcu.js → als-registry-HS0LGUl2.js} +1 -1
- package/dist/_chunks/als-registry-HS0LGUl2.js.map +1 -0
- package/dist/_chunks/{define-Dz1bqwaS.js → define-C77ScO0m.js} +14 -14
- package/dist/_chunks/define-C77ScO0m.js.map +1 -0
- package/dist/_chunks/{define-CGuYoRHU.js → define-CZqDwhSu.js} +15 -15
- package/dist/_chunks/define-CZqDwhSu.js.map +1 -0
- package/dist/_chunks/{define-cookie-B5mewxwM.js → define-cookie-C2IkoFGN.js} +9 -8
- package/dist/_chunks/{define-cookie-B5mewxwM.js.map → define-cookie-C2IkoFGN.js.map} +1 -1
- package/dist/_chunks/{format-Rn922VH2.js → dev-warnings-DpGRGoDi.js} +4 -26
- package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +1 -0
- package/dist/_chunks/format-CYBGxKtc.js +14 -0
- package/dist/_chunks/format-CYBGxKtc.js.map +1 -0
- package/dist/_chunks/{interception-CEdHHviP.js → interception-Dpn_UfAD.js} +2 -2
- package/dist/_chunks/{interception-CEdHHviP.js.map → interception-Dpn_UfAD.js.map} +1 -1
- package/dist/_chunks/{segment-context-hzuJ048X.js → merge-search-params-Cm_KIWDX.js} +2 -33
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +1 -0
- package/dist/_chunks/{request-context-CywiO4jV.js → request-context-qMsWgy9C.js} +72 -36
- package/dist/_chunks/request-context-qMsWgy9C.js.map +1 -0
- package/dist/_chunks/{schema-bridge-C4SwjCQD.js → schema-bridge-C3xl_vfb.js} +1 -1
- package/dist/_chunks/{schema-bridge-C4SwjCQD.js.map → schema-bridge-C3xl_vfb.js.map} +1 -1
- package/dist/_chunks/segment-context-fHFLF1PE.js +34 -0
- package/dist/_chunks/segment-context-fHFLF1PE.js.map +1 -0
- package/dist/_chunks/ssr-data-DzuI0bIV.js +88 -0
- package/dist/_chunks/ssr-data-DzuI0bIV.js.map +1 -0
- package/dist/_chunks/{stale-reload-BLUC_Pl_.js → stale-reload-C2plcNtG.js} +1 -1
- package/dist/_chunks/{stale-reload-BLUC_Pl_.js.map → stale-reload-C2plcNtG.js.map} +1 -1
- package/dist/_chunks/{handler-store-BVePM7hp.js → tracing-CCYbKn5n.js} +60 -60
- package/dist/_chunks/tracing-CCYbKn5n.js.map +1 -0
- package/dist/_chunks/use-params-B1AuhI1p.js +307 -0
- package/dist/_chunks/use-params-B1AuhI1p.js.map +1 -0
- package/dist/_chunks/{use-query-states-DAhgj8Gx.js → use-query-states-Lo_s_pw2.js} +4 -4
- package/dist/_chunks/use-query-states-Lo_s_pw2.js.map +1 -0
- package/dist/_chunks/{wrappers-LZbghvn0.js → wrappers-_DTmImGt.js} +1 -1
- package/dist/_chunks/{wrappers-LZbghvn0.js.map → wrappers-_DTmImGt.js.map} +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts +64 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -0
- package/dist/adapters/cloudflare-kv-cache.js +95 -0
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -0
- package/dist/cache/index.d.ts +18 -4
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +78 -12
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/sizeof.d.ts +22 -0
- package/dist/cache/sizeof.d.ts.map +1 -0
- package/dist/cli.d.ts +6 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-dev.d.ts +27 -1
- package/dist/client/browser-dev.d.ts.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts +17 -0
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -0
- package/dist/client/browser-entry/hmr.d.ts +21 -0
- package/dist/client/browser-entry/hmr.d.ts.map +1 -0
- package/dist/client/browser-entry/hydrate.d.ts +46 -0
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -0
- package/dist/client/browser-entry/index.d.ts +30 -0
- package/dist/client/browser-entry/index.d.ts.map +1 -0
- package/dist/client/browser-entry/post-hydration.d.ts +26 -0
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -0
- package/dist/client/browser-entry/router-init.d.ts +23 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -0
- package/dist/client/browser-entry/rsc-stream.d.ts +24 -0
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -0
- package/dist/client/browser-entry/scroll.d.ts +19 -0
- package/dist/client/browser-entry/scroll.d.ts.map +1 -0
- package/dist/client/error-boundary.js +131 -1
- package/dist/client/error-boundary.js.map +1 -0
- package/dist/client/index.d.ts +4 -19
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +14 -1191
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +18 -0
- package/dist/client/internal.d.ts.map +1 -0
- package/dist/client/internal.js +890 -0
- package/dist/client/internal.js.map +1 -0
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/top-loader.d.ts +2 -2
- package/dist/client/use-link-status.d.ts +1 -1
- package/dist/client/{use-navigation-pending.d.ts → use-pending-navigation.d.ts} +4 -4
- package/dist/client/use-pending-navigation.d.ts.map +1 -0
- package/dist/client/use-router.d.ts +1 -1
- package/dist/codec.d.ts +10 -0
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/config-types.d.ts +210 -0
- package/dist/config-types.d.ts.map +1 -0
- package/dist/content/index.d.ts +1 -10
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +0 -2
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.d.ts +0 -2
- package/dist/cookies/index.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -3
- package/dist/index.d.ts +25 -288
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +261 -43
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +84 -0
- package/dist/plugin-context.d.ts.map +1 -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 +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +1 -1
- package/dist/plugins/build-report.d.ts.map +1 -1
- 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 +1 -1
- package/dist/plugins/dev-browser-logs.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 +1 -1
- 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/shims.d.ts +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +4 -4
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/search-params/define.d.ts +6 -6
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.d.ts +1 -2
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +4 -4
- package/dist/search-params/registry.d.ts +1 -1
- package/dist/search-params/registry.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts +6 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.d.ts +0 -1
- package/dist/segment-params/index.d.ts.map +1 -1
- package/dist/segment-params/index.js +3 -3
- package/dist/server/als-registry.d.ts +1 -1
- package/dist/server/dev-holding-server.d.ts +52 -0
- package/dist/server/dev-holding-server.d.ts.map +1 -0
- package/dist/server/dev-warnings.d.ts +1 -7
- package/dist/server/dev-warnings.d.ts.map +1 -1
- package/dist/server/index.d.ts +6 -45
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -3272
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +46 -0
- package/dist/server/internal.d.ts.map +1 -0
- package/dist/server/internal.js +2865 -0
- package/dist/server/internal.js.map +1 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +41 -17
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +45 -15
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +4 -4
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/shims/headers.d.ts +2 -1
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +2 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/package.json +19 -13
- package/src/adapters/cloudflare-kv-cache.ts +142 -0
- package/src/cache/handler-store.ts +2 -2
- package/src/cache/index.ts +74 -15
- package/src/cache/sizeof.ts +31 -0
- package/src/cli.ts +6 -1
- package/src/client/browser-dev.ts +128 -1
- package/src/client/browser-entry/action-dispatch.ts +116 -0
- package/src/client/browser-entry/hmr.ts +81 -0
- package/src/client/browser-entry/hydrate.ts +145 -0
- package/src/client/browser-entry/index.ts +138 -0
- package/src/client/browser-entry/post-hydration.ts +119 -0
- package/src/client/browser-entry/router-init.ts +184 -0
- package/src/client/browser-entry/rsc-stream.ts +157 -0
- package/src/client/browser-entry/scroll.ts +27 -0
- package/src/client/index.ts +10 -38
- package/src/client/internal.ts +57 -0
- package/src/client/navigation-context.ts +6 -2
- package/src/client/navigation-root.tsx +1 -1
- package/src/client/router-ref.ts +1 -1
- package/src/client/top-loader.tsx +2 -2
- package/src/client/use-link-status.ts +1 -1
- package/src/client/{use-navigation-pending.ts → use-pending-navigation.ts} +5 -5
- package/src/client/use-query-states.ts +2 -2
- package/src/client/use-router.ts +1 -1
- package/src/codec.ts +15 -0
- package/src/config-types.ts +208 -0
- package/src/content/index.ts +5 -13
- package/src/cookies/define-cookie.ts +9 -7
- package/src/cookies/index.ts +6 -5
- package/src/index.ts +84 -416
- package/src/plugin-context.ts +200 -0
- package/src/plugins/adapter-build.ts +1 -1
- package/src/plugins/build-manifest.ts +1 -1
- package/src/plugins/build-report.ts +1 -1
- package/src/plugins/content.ts +1 -1
- package/src/plugins/dev-browser-logs.ts +1 -1
- package/src/plugins/dev-logs.ts +1 -1
- package/src/plugins/dev-server.ts +16 -1
- package/src/plugins/entries.ts +2 -2
- package/src/plugins/fonts.ts +4 -3
- package/src/plugins/mdx.ts +1 -1
- package/src/plugins/routing.ts +1 -1
- package/src/plugins/shims.ts +53 -5
- package/src/plugins/static-build.ts +8 -6
- package/src/search-params/define.ts +22 -22
- package/src/search-params/index.ts +3 -3
- package/src/search-params/registry.ts +1 -1
- package/src/segment-params/define.ts +18 -18
- package/src/segment-params/index.ts +2 -1
- package/src/server/action-handler.ts +1 -1
- package/src/server/als-registry.ts +3 -3
- package/src/server/dev-holding-server.ts +185 -0
- package/src/server/dev-warnings.ts +2 -21
- package/src/server/html-injectors.ts +3 -3
- package/src/server/index.ts +25 -180
- package/src/server/internal.ts +169 -0
- package/src/server/pipeline.ts +12 -7
- package/src/server/primitives.ts +71 -30
- package/src/server/request-context.ts +77 -39
- package/src/server/route-element-builder.ts +1 -1
- package/src/server/rsc-entry/index.ts +2 -2
- package/src/server/rsc-entry/ssr-renderer.ts +1 -1
- package/src/server/slot-resolver.ts +1 -1
- package/src/server/tracing.ts +6 -6
- package/src/server/tree-builder.ts +1 -1
- package/src/shims/headers.ts +5 -1
- package/src/shims/navigation.ts +5 -1
- package/dist/_chunks/als-registry-BJARkOcu.js.map +0 -1
- package/dist/_chunks/define-CGuYoRHU.js.map +0 -1
- package/dist/_chunks/define-Dz1bqwaS.js.map +0 -1
- package/dist/_chunks/error-boundary-D9hzsveV.js +0 -216
- package/dist/_chunks/error-boundary-D9hzsveV.js.map +0 -1
- package/dist/_chunks/format-Rn922VH2.js.map +0 -1
- package/dist/_chunks/handler-store-BVePM7hp.js.map +0 -1
- package/dist/_chunks/request-context-CywiO4jV.js.map +0 -1
- package/dist/_chunks/segment-context-hzuJ048X.js.map +0 -1
- package/dist/_chunks/use-query-states-DAhgj8Gx.js.map +0 -1
- package/dist/client/browser-entry.d.ts +0 -21
- package/dist/client/browser-entry.d.ts.map +0 -1
- package/dist/client/use-navigation-pending.d.ts.map +0 -1
- package/src/client/browser-entry.ts +0 -846
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Entry — Client-side hydration and navigation bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* This is the thin orchestrator that coordinates the bootstrap sequence.
|
|
5
|
+
* Each responsibility is extracted into a focused module:
|
|
6
|
+
*
|
|
7
|
+
* action-dispatch.ts — server action callServer callback
|
|
8
|
+
* rsc-stream.ts — __timber_f chunk handling + ReadableStream
|
|
9
|
+
* router-init.ts — createRouter + Navigation API setup
|
|
10
|
+
* hydrate.ts — pre-hydration sequence + hydrateRoot/createRoot
|
|
11
|
+
* post-hydration.ts — history stack, segment cache, popstate, scroll
|
|
12
|
+
* hmr.ts — dev-only HMR + error forwarding
|
|
13
|
+
* scroll.ts — getScrollY helper
|
|
14
|
+
*
|
|
15
|
+
* Bootstrap call order contract:
|
|
16
|
+
*
|
|
17
|
+
* 1. setupServerActions() — register callServer (independent)
|
|
18
|
+
* 2. createRscPayloadStream() — decode inlined RSC payload
|
|
19
|
+
* 3. createTimberRouter() — create router + Navigation API
|
|
20
|
+
* 4. runPreHydration() — set params + navigation state
|
|
21
|
+
* 5. hydrateApp() — hydrateRoot or deferred createRoot
|
|
22
|
+
* 6. setupPostHydration() — history stack, popstate, scroll
|
|
23
|
+
* 7. setupHmr() — dev-only HMR wiring
|
|
24
|
+
* 8. stale reload handlers — global error listeners
|
|
25
|
+
* 9. timber-ready signal — E2E test readiness indicator
|
|
26
|
+
*
|
|
27
|
+
* Design docs: 18-build-system.md §"Entry Files", 19-client-navigation.md
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// @ts-expect-error — virtual module provided by timber-entries plugin
|
|
31
|
+
import config from 'virtual:timber-config';
|
|
32
|
+
|
|
33
|
+
import { setClientDeploymentId } from '../rsc-fetch.js';
|
|
34
|
+
import {
|
|
35
|
+
isStaleClientReference,
|
|
36
|
+
isChunkLoadError,
|
|
37
|
+
triggerStaleReload,
|
|
38
|
+
clearStaleReloadFlag,
|
|
39
|
+
} from '../stale-reload.js';
|
|
40
|
+
|
|
41
|
+
import { setupServerActions } from './action-dispatch.js';
|
|
42
|
+
import { createRscPayloadStream } from './rsc-stream.js';
|
|
43
|
+
import { createTimberRouter } from './router-init.js';
|
|
44
|
+
import { runPreHydration, hydrateApp } from './hydrate.js';
|
|
45
|
+
import { setupPostHydration } from './post-hydration.js';
|
|
46
|
+
import { setupHmr } from './hmr.js';
|
|
47
|
+
|
|
48
|
+
// ─── 1. Server Action Dispatch (independent) ────────────────────
|
|
49
|
+
|
|
50
|
+
setupServerActions();
|
|
51
|
+
|
|
52
|
+
// ─── 2–7. Bootstrap ─────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function bootstrap(runtimeConfig: typeof config): void {
|
|
55
|
+
// Initialize deployment ID for version skew detection (TIM-446).
|
|
56
|
+
// In dev mode this is null — skew checks are skipped.
|
|
57
|
+
const deploymentId = (runtimeConfig as Record<string, unknown>).deploymentId as string | null;
|
|
58
|
+
if (deploymentId) {
|
|
59
|
+
setClientDeploymentId(deploymentId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Take manual control of scroll restoration. Even though segment tree
|
|
63
|
+
// merging preserves shared layout DOM via cloneElement (so React doesn't
|
|
64
|
+
// reset scroll on those elements), the root-level reactRoot.render() with
|
|
65
|
+
// a new element tree can still cause scroll resets on the document during
|
|
66
|
+
// reconciliation. Manual control ensures consistent behavior.
|
|
67
|
+
window.history.scrollRestoration = 'manual';
|
|
68
|
+
|
|
69
|
+
// Step 2: Decode inlined RSC payload (may be null for JS-only clients)
|
|
70
|
+
const rscResult = createRscPayloadStream();
|
|
71
|
+
|
|
72
|
+
// Step 3: Create router + Navigation API integration
|
|
73
|
+
const { router, navApiController } = createTimberRouter();
|
|
74
|
+
|
|
75
|
+
// Step 4: Pre-hydration — set params + navigation state (MUST run after router init)
|
|
76
|
+
runPreHydration();
|
|
77
|
+
|
|
78
|
+
// Step 5: Hydrate or set up deferred root creation
|
|
79
|
+
hydrateApp({ rscResult, config: runtimeConfig });
|
|
80
|
+
|
|
81
|
+
// Step 6: Post-hydration wiring
|
|
82
|
+
setupPostHydration({
|
|
83
|
+
router,
|
|
84
|
+
navApiController,
|
|
85
|
+
initialElement: rscResult?.element ?? null,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Step 7: HMR (dev-only, no-op in production)
|
|
89
|
+
setupHmr(router);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
bootstrap(config);
|
|
93
|
+
|
|
94
|
+
// ─── 8. Stale Reload Handlers ───────────────────────────────────
|
|
95
|
+
|
|
96
|
+
// Clear the stale reload flag on successful bootstrap. If the page
|
|
97
|
+
// loaded and bootstrapped without hitting a stale reference error,
|
|
98
|
+
// the loop guard should reset so the next stale error gets a fresh
|
|
99
|
+
// reload attempt.
|
|
100
|
+
clearStaleReloadFlag();
|
|
101
|
+
|
|
102
|
+
// Global error handler for stale client reference errors during hydration.
|
|
103
|
+
// The initial RSC payload is decoded lazily by React via createFromReadableStream.
|
|
104
|
+
// If the payload references a module ID from a newer deployment, the error
|
|
105
|
+
// surfaces as an unhandled rejection during React's render/hydration cycle.
|
|
106
|
+
// This handler catches those errors and triggers a full page reload.
|
|
107
|
+
//
|
|
108
|
+
// Also catches chunk load failures (dynamic import of missing assets after
|
|
109
|
+
// a deployment) — these surface as "Failed to fetch dynamically imported module"
|
|
110
|
+
// or "Loading chunk <name> failed" errors. See TIM-446.
|
|
111
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
112
|
+
if (isStaleClientReference(event.reason) || isChunkLoadError(event.reason)) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
triggerStaleReload();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Also catch synchronous errors from chunk loads (some browsers throw
|
|
119
|
+
// TypeError synchronously instead of via unhandled rejection).
|
|
120
|
+
window.addEventListener('error', (event) => {
|
|
121
|
+
if (isChunkLoadError(event.error)) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
triggerStaleReload();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── 9. Ready Signal ────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
// Signal that the client runtime has been initialized.
|
|
130
|
+
// Used by E2E tests to wait for hydration before interacting.
|
|
131
|
+
// We append a <meta name="timber-ready"> tag rather than setting a
|
|
132
|
+
// data attribute on <html>. Since React owns the entire document
|
|
133
|
+
// via hydrateRoot(document, ...), mutating <html> attributes causes
|
|
134
|
+
// hydration mismatch warnings. Dynamically-added <meta> tags don't
|
|
135
|
+
// conflict because React doesn't reconcile them.
|
|
136
|
+
const readyMeta = document.createElement('meta');
|
|
137
|
+
readyMeta.name = 'timber-ready';
|
|
138
|
+
document.head.appendChild(readyMeta);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-Hydration Wiring — history stack, segment cache, popstate, scroll.
|
|
3
|
+
*
|
|
4
|
+
* Sets up everything that needs to happen after the React root exists:
|
|
5
|
+
* - Stores initial page in history stack for instant back navigation
|
|
6
|
+
* - Initializes scroll state for the initial entry
|
|
7
|
+
* - Populates segment cache from server-embedded metadata
|
|
8
|
+
* - Registers popstate handler for back/forward navigation
|
|
9
|
+
* - Sets up debounced scroll position saving
|
|
10
|
+
*
|
|
11
|
+
* See design/19-client-navigation.md §"History Stack"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { setCurrentParams } from '#client-internal';
|
|
15
|
+
import type { RouterInstance } from '#client-internal';
|
|
16
|
+
import { setNavigationState } from '../navigation-context.js';
|
|
17
|
+
import type { NavigationApiController } from '../navigation-api.js';
|
|
18
|
+
import { getScrollY } from './scroll.js';
|
|
19
|
+
|
|
20
|
+
interface PostHydrationOptions {
|
|
21
|
+
router: RouterInstance;
|
|
22
|
+
navApiController: NavigationApiController | null;
|
|
23
|
+
/** Decoded RSC element from initial SSR (null if no RSC payload) */
|
|
24
|
+
initialElement: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wire up post-hydration event handlers and state.
|
|
29
|
+
*/
|
|
30
|
+
export function setupPostHydration({
|
|
31
|
+
router,
|
|
32
|
+
navApiController,
|
|
33
|
+
initialElement,
|
|
34
|
+
}: PostHydrationOptions): void {
|
|
35
|
+
// Store the initial page in the history stack so back-button works
|
|
36
|
+
// after the first navigation. We store the decoded RSC element so
|
|
37
|
+
// back navigation can replay it instantly without a server fetch.
|
|
38
|
+
router.historyStack.push(window.location.pathname + window.location.search, {
|
|
39
|
+
payload: initialElement,
|
|
40
|
+
headElements: null, // SSR already set the correct head
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Initialize scroll state for the initial entry.
|
|
44
|
+
// When Navigation API is available, use per-entry state.
|
|
45
|
+
// Otherwise fall back to history.state.
|
|
46
|
+
if (navApiController) {
|
|
47
|
+
navApiController.saveScrollPosition(0);
|
|
48
|
+
} else {
|
|
49
|
+
window.history.replaceState({ timber: true, scrollY: 0 }, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Populate the segment cache from server-embedded segment metadata.
|
|
53
|
+
// This enables state tree diffing from the very first client navigation.
|
|
54
|
+
// See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
55
|
+
const timberSegments = (self as unknown as Record<string, unknown>).__timber_segments;
|
|
56
|
+
if (Array.isArray(timberSegments)) {
|
|
57
|
+
router.initSegmentCache(timberSegments);
|
|
58
|
+
delete (self as unknown as Record<string, unknown>).__timber_segments;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// NOTE: We do NOT cache segment elements from the initial RSC payload here.
|
|
62
|
+
// The decoded element from createFromReadableStream is a thenable/lazy
|
|
63
|
+
// element that React resolves during render — the segment walker can't
|
|
64
|
+
// traverse it. The element cache is populated lazily after the first SPA
|
|
65
|
+
// navigation (via mergeAndCachePayload in renderViaTransition), when
|
|
66
|
+
// the decoded payload is a fully resolved React element tree.
|
|
67
|
+
|
|
68
|
+
// If the hydration path was skipped (no RSC payload), populate the
|
|
69
|
+
// fallback params from server embed here.
|
|
70
|
+
const lateTimberParams = (self as unknown as Record<string, unknown>).__timber_params;
|
|
71
|
+
if (lateTimberParams && typeof lateTimberParams === 'object') {
|
|
72
|
+
setCurrentParams(lateTimberParams as Record<string, string | string[]>);
|
|
73
|
+
setNavigationState({
|
|
74
|
+
params: lateTimberParams as Record<string, string | string[]>,
|
|
75
|
+
pathname: window.location.pathname,
|
|
76
|
+
});
|
|
77
|
+
delete (self as unknown as Record<string, unknown>).__timber_params;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Register popstate handler for back/forward navigation.
|
|
81
|
+
// When Navigation API is active, the navigate event covers traversals —
|
|
82
|
+
// popstate is a no-op. When unavailable, popstate handles back/forward.
|
|
83
|
+
//
|
|
84
|
+
// Use pathname+search (not full href) to match the URL format used by
|
|
85
|
+
// navigate() — Link hrefs are relative paths like "/scroll-test/page-a".
|
|
86
|
+
// Read scrollY from history.state — the browser maintains per-entry state
|
|
87
|
+
// so duplicate URLs in history each have their own scroll position.
|
|
88
|
+
window.addEventListener('popstate', () => {
|
|
89
|
+
// Navigation API handles traversals via the navigate event.
|
|
90
|
+
if (navApiController) return;
|
|
91
|
+
|
|
92
|
+
const state = window.history.state;
|
|
93
|
+
const scrollY = state && typeof state.scrollY === 'number' ? state.scrollY : 0;
|
|
94
|
+
void router.handlePopState(window.location.pathname + window.location.search, scrollY);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Keep scroll position up to date as the user scrolls.
|
|
98
|
+
// This ensures that when the user presses back/forward, the departing
|
|
99
|
+
// page's scroll position is already saved in its history entry.
|
|
100
|
+
// When Navigation API is available, uses per-entry state via
|
|
101
|
+
// navigation.updateCurrentEntry(). Otherwise falls back to history.state.
|
|
102
|
+
// Debounced to avoid excessive state updates during smooth scrolling.
|
|
103
|
+
let scrollTimer: ReturnType<typeof setTimeout>;
|
|
104
|
+
function saveScrollPosition(): void {
|
|
105
|
+
clearTimeout(scrollTimer);
|
|
106
|
+
scrollTimer = setTimeout(() => {
|
|
107
|
+
const y = getScrollY();
|
|
108
|
+
if (navApiController) {
|
|
109
|
+
navApiController.saveScrollPosition(y);
|
|
110
|
+
} else {
|
|
111
|
+
const state = window.history.state;
|
|
112
|
+
if (state && typeof state === 'object') {
|
|
113
|
+
window.history.replaceState({ ...state, scrollY: y }, '');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}, 100);
|
|
117
|
+
}
|
|
118
|
+
window.addEventListener('scroll', saveScrollPosition, { passive: true });
|
|
119
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Initialization — creates the timber router with all dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Wires up RouterDeps (fetch, history, scroll, RSC decoding, render
|
|
5
|
+
* callbacks) and optionally sets up Navigation API integration.
|
|
6
|
+
*
|
|
7
|
+
* See design/19-client-navigation.md §"Navigation API Integration"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createElement } from 'react';
|
|
11
|
+
import { createFromFetch } from '../../rsc-runtime/browser.js';
|
|
12
|
+
import { createRouter, setGlobalRouter } from '#client-internal';
|
|
13
|
+
import type { RouterDeps, RouterInstance } from '#client-internal';
|
|
14
|
+
import type { NavigationState } from '#client-internal';
|
|
15
|
+
import { applyHeadElements } from '../head.js';
|
|
16
|
+
import { TimberNuqsAdapter } from '../nuqs-adapter.js';
|
|
17
|
+
import { NavigationProvider } from '../navigation-context.js';
|
|
18
|
+
import { transitionRender, navigateTransition } from '../navigation-root.js';
|
|
19
|
+
import { isStaleClientReference, triggerStaleReload } from '../stale-reload.js';
|
|
20
|
+
import {
|
|
21
|
+
hasNavigationApi,
|
|
22
|
+
setupNavigationApi,
|
|
23
|
+
type NavigationApiController,
|
|
24
|
+
} from '../navigation-api.js';
|
|
25
|
+
import { getScrollY } from './scroll.js';
|
|
26
|
+
|
|
27
|
+
export interface RouterInitResult {
|
|
28
|
+
router: RouterInstance;
|
|
29
|
+
navApiController: NavigationApiController | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create and register the global timber router.
|
|
34
|
+
*
|
|
35
|
+
* Must be called before hydrateRoot so `useRouter()` works during
|
|
36
|
+
* the initial render (methods lazily resolve the router at invocation,
|
|
37
|
+
* not render time, but initRouter must still run first).
|
|
38
|
+
*/
|
|
39
|
+
export function createTimberRouter(): RouterInitResult {
|
|
40
|
+
// Feature-detect Navigation API. When available, the navigate event
|
|
41
|
+
// replaces popstate for back/forward and catches external navigations.
|
|
42
|
+
// See design/19-client-navigation.md §"Navigation API Integration"
|
|
43
|
+
const useNavApi = hasNavigationApi();
|
|
44
|
+
|
|
45
|
+
const deps: RouterDeps = {
|
|
46
|
+
fetch: (url, init) => window.fetch(url, init),
|
|
47
|
+
pushState: (data, unused, url) => window.history.pushState(data, unused, url),
|
|
48
|
+
replaceState: (data, unused, url) => window.history.replaceState(data, unused, url),
|
|
49
|
+
navigationApiActive: useNavApi,
|
|
50
|
+
scrollTo: (x, y) => {
|
|
51
|
+
// Scroll the document viewport.
|
|
52
|
+
window.scrollTo(x, y);
|
|
53
|
+
document.documentElement.scrollTop = y;
|
|
54
|
+
document.body.scrollTop = y;
|
|
55
|
+
// Scroll any element explicitly marked as a scroll container.
|
|
56
|
+
// With segment tree merging, shared layouts (sidebars, nav bars)
|
|
57
|
+
// are reconciled in place via cloneElement — React preserves their
|
|
58
|
+
// DOM and scroll state naturally. We no longer auto-detect overflow
|
|
59
|
+
// containers, which previously found the wrong element (e.g.,
|
|
60
|
+
// scrolling a sidebar instead of the main content area).
|
|
61
|
+
// Use `data-timber-scroll-restoration` to opt in specific containers.
|
|
62
|
+
for (const el of document.querySelectorAll('[data-timber-scroll-restoration]')) {
|
|
63
|
+
(el as HTMLElement).scrollTop = y;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
getCurrentUrl: () => window.location.pathname + window.location.search,
|
|
67
|
+
getScrollY,
|
|
68
|
+
|
|
69
|
+
// Decode RSC Flight stream using createFromFetch.
|
|
70
|
+
// createFromFetch takes a Promise<Response> and progressively
|
|
71
|
+
// parses the RSC stream as chunks arrive.
|
|
72
|
+
//
|
|
73
|
+
// Wrapped with stale client reference detection: if the server
|
|
74
|
+
// has been redeployed with new bundles, the RSC payload may
|
|
75
|
+
// reference module IDs that don't exist in the old client bundle.
|
|
76
|
+
// We catch "Could not find the module" errors and trigger a full
|
|
77
|
+
// page reload so the browser fetches the new bundle.
|
|
78
|
+
decodeRsc: async (fetchPromise: Promise<Response>) => {
|
|
79
|
+
try {
|
|
80
|
+
return await createFromFetch(fetchPromise);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (isStaleClientReference(error)) {
|
|
83
|
+
triggerStaleReload();
|
|
84
|
+
// Return a never-resolving promise to prevent further processing
|
|
85
|
+
// while the page is reloading.
|
|
86
|
+
return new Promise(() => {});
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Render decoded RSC tree via NavigationRoot's state-based mechanism.
|
|
93
|
+
// Used for non-navigation renders (popstate cached replay, applyRevalidation).
|
|
94
|
+
// Wraps with NavigationProvider + TimberNuqsAdapter.
|
|
95
|
+
//
|
|
96
|
+
// For navigation renders (navigate, refresh, popstate-with-fetch),
|
|
97
|
+
// navigateTransition is used instead — it wraps the entire navigation
|
|
98
|
+
// in a React transition with useOptimistic for the pending URL.
|
|
99
|
+
//
|
|
100
|
+
// navState is passed explicitly by the router — no temporal coupling
|
|
101
|
+
// with getNavigationState().
|
|
102
|
+
renderRoot: (element: unknown, navState: NavigationState) => {
|
|
103
|
+
const withNav = createElement(
|
|
104
|
+
NavigationProvider,
|
|
105
|
+
{ value: navState },
|
|
106
|
+
element as React.ReactNode
|
|
107
|
+
);
|
|
108
|
+
const wrapped = createElement(TimberNuqsAdapter, null, withNav);
|
|
109
|
+
transitionRender(wrapped);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// Run a navigation inside a React transition with optimistic pending URL.
|
|
113
|
+
// The entire fetch + state update runs inside startTransition. useOptimistic
|
|
114
|
+
// shows the pending URL immediately and reverts to null when the transition
|
|
115
|
+
// commits (atomic with the new tree + params).
|
|
116
|
+
//
|
|
117
|
+
// The perform callback receives a wrapPayload function that wraps the
|
|
118
|
+
// decoded RSC payload with NavigationProvider + NuqsAdapter. navState
|
|
119
|
+
// is passed explicitly by the router — no getNavigationState() needed.
|
|
120
|
+
navigateTransition: (pendingUrl: string, perform) => {
|
|
121
|
+
return navigateTransition(pendingUrl, async () => {
|
|
122
|
+
const payload = await perform((rawPayload: unknown, navState: NavigationState) => {
|
|
123
|
+
const withNav = createElement(
|
|
124
|
+
NavigationProvider,
|
|
125
|
+
{ value: navState },
|
|
126
|
+
rawPayload as React.ReactNode
|
|
127
|
+
);
|
|
128
|
+
return createElement(TimberNuqsAdapter, null, withNav);
|
|
129
|
+
});
|
|
130
|
+
return payload as React.ReactNode;
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// Schedule a callback after the next paint so scroll operations
|
|
135
|
+
// happen after React commits the new content to the DOM.
|
|
136
|
+
// Double-rAF ensures the browser has painted the new frame.
|
|
137
|
+
afterPaint: (callback: () => void) => {
|
|
138
|
+
requestAnimationFrame(() => {
|
|
139
|
+
requestAnimationFrame(callback);
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// Apply resolved head elements (title, meta tags) to the DOM after
|
|
144
|
+
// SPA navigation. See design/16-metadata.md.
|
|
145
|
+
applyHead: applyHeadElements,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const router = createRouter(deps);
|
|
149
|
+
setGlobalRouter(router);
|
|
150
|
+
|
|
151
|
+
// Set up Navigation API integration after router is created.
|
|
152
|
+
// The navigate event listener delegates to router.navigate and
|
|
153
|
+
// router.handlePopState for external navigations and traversals.
|
|
154
|
+
let navApiController: NavigationApiController | null = null;
|
|
155
|
+
if (useNavApi) {
|
|
156
|
+
navApiController = setupNavigationApi({
|
|
157
|
+
onExternalNavigate: async (url, { replace, signal, scroll }) => {
|
|
158
|
+
// Navigation intercepted by the Navigation API. Covers both
|
|
159
|
+
// Link <a> clicks (user-initiated) and external navigations.
|
|
160
|
+
// The Navigation API handles the URL update via intercept(),
|
|
161
|
+
// so pass _skipHistory to avoid double pushState.
|
|
162
|
+
await router.navigate(url, {
|
|
163
|
+
replace,
|
|
164
|
+
scroll,
|
|
165
|
+
_signal: signal,
|
|
166
|
+
_skipHistory: true,
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
onTraverse: async (url, scrollY, signal) => {
|
|
170
|
+
// Back/forward — delegate to the router's popstate handler.
|
|
171
|
+
await router.handlePopState(url, scrollY, signal);
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Wire the router-navigating flag into RouterDeps.
|
|
176
|
+
// This must be done after setupNavigationApi returns the controller.
|
|
177
|
+
deps.setRouterNavigating = (v) => navApiController!.setRouterNavigating(v);
|
|
178
|
+
deps.saveNavigationEntryScroll = (y) => navApiController!.saveScrollPosition(y);
|
|
179
|
+
deps.completeRouterNavigation = () => navApiController!.completeRouterNavigation();
|
|
180
|
+
deps.navigationNavigate = (url, replace) => navApiController!.navigate(url, replace);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { router, navApiController };
|
|
184
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RSC Stream Bootstrap — decodes the server-inlined RSC payload.
|
|
3
|
+
*
|
|
4
|
+
* The RSC payload is embedded in the HTML as progressive inline script
|
|
5
|
+
* tags that call `self.__timber_f.push([type, data])` as RSC chunks arrive.
|
|
6
|
+
* Typed tuples: [0] = bootstrap signal, [1, string] = Flight data chunk.
|
|
7
|
+
*
|
|
8
|
+
* This module sets up a ReadableStream fed by those push() calls so
|
|
9
|
+
* `createFromReadableStream` can decode the Flight protocol progressively.
|
|
10
|
+
*
|
|
11
|
+
* See design/18-build-system.md §"Entry Files"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createFromReadableStream } from '../../rsc-runtime/browser.js';
|
|
15
|
+
import { isPageUnloading } from '../unload-guard.js';
|
|
16
|
+
|
|
17
|
+
type FlightSegment = [isBootstrap: 0] | [isData: 1, data: string];
|
|
18
|
+
|
|
19
|
+
export interface RscStreamResult {
|
|
20
|
+
/** The decoded RSC element (thenable/lazy — resolved by React during render) */
|
|
21
|
+
element: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create the RSC payload stream from server-inlined `__timber_f` chunks.
|
|
26
|
+
*
|
|
27
|
+
* Returns null if no RSC payload was inlined (e.g., JS-only client).
|
|
28
|
+
* When a payload exists, returns the decoded element for hydration.
|
|
29
|
+
*/
|
|
30
|
+
export function createRscPayloadStream(): RscStreamResult | null {
|
|
31
|
+
// __timber_f is initialized in <head> via flightInitScript() (see
|
|
32
|
+
// flight-scripts.ts). If it doesn't exist, skip Flight decoding
|
|
33
|
+
// entirely and fall through to the createRoot branch.
|
|
34
|
+
// Do NOT defensively create it here: that would cause
|
|
35
|
+
// createFromReadableStream to be called on an empty stream, producing
|
|
36
|
+
// a "Connection closed" error on hydration. See TIM-552.
|
|
37
|
+
const timberChunks = (self as unknown as Record<string, FlightSegment[] | undefined>).__timber_f;
|
|
38
|
+
if (!timberChunks) return null;
|
|
39
|
+
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
|
|
42
|
+
// Buffer to hold string data until the stream writer is ready.
|
|
43
|
+
// Scripts that execute before hydration starts push data here.
|
|
44
|
+
let dataBuffer: string[] | undefined = [];
|
|
45
|
+
let streamWriter: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
46
|
+
let streamFlushed = false;
|
|
47
|
+
|
|
48
|
+
/** Process a typed tuple from __timber_f. */
|
|
49
|
+
function handleSegment(seg: FlightSegment): void {
|
|
50
|
+
if (seg[0] === 0) {
|
|
51
|
+
// Bootstrap signal — initialize buffer (already done above)
|
|
52
|
+
if (!dataBuffer) dataBuffer = [];
|
|
53
|
+
} else if (seg[0] === 1) {
|
|
54
|
+
// Flight data chunk
|
|
55
|
+
if (streamWriter) {
|
|
56
|
+
streamWriter.enqueue(encoder.encode(seg[1]));
|
|
57
|
+
} else if (dataBuffer) {
|
|
58
|
+
dataBuffer.push(seg[1]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Process any chunks that arrived before this script executed.
|
|
64
|
+
for (const seg of timberChunks) {
|
|
65
|
+
handleSegment(seg);
|
|
66
|
+
}
|
|
67
|
+
// Clear the array to release memory.
|
|
68
|
+
timberChunks.length = 0;
|
|
69
|
+
|
|
70
|
+
// Patch push() so subsequent script tags feed data in real time.
|
|
71
|
+
(timberChunks as unknown as { push: (seg: FlightSegment) => void }).push = handleSegment;
|
|
72
|
+
|
|
73
|
+
const rscPayload = new ReadableStream<Uint8Array>({
|
|
74
|
+
start(controller) {
|
|
75
|
+
streamWriter = controller;
|
|
76
|
+
// Flush buffered data into the stream.
|
|
77
|
+
if (dataBuffer) {
|
|
78
|
+
for (const data of dataBuffer) {
|
|
79
|
+
controller.enqueue(encoder.encode(data));
|
|
80
|
+
}
|
|
81
|
+
dataBuffer = undefined;
|
|
82
|
+
}
|
|
83
|
+
// If DOM already loaded (non-streaming or fast page), close now.
|
|
84
|
+
if (streamFlushed) {
|
|
85
|
+
controller.close();
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Close the stream when the document finishes loading.
|
|
91
|
+
// DOMContentLoaded fires after the HTML parser has processed all
|
|
92
|
+
// inline scripts (including streamed Suspense replacements and
|
|
93
|
+
// RSC data), so all push() calls have completed by this point.
|
|
94
|
+
//
|
|
95
|
+
// If the page is unloading (user refreshed or navigated away),
|
|
96
|
+
// do NOT close the stream. When the connection drops mid-stream,
|
|
97
|
+
// DOMContentLoaded fires because the parser finishes. Closing an
|
|
98
|
+
// incomplete RSC stream causes React's Flight client to throw
|
|
99
|
+
// "Connection closed." — a jarring error on a page being replaced.
|
|
100
|
+
// Leaving the stream open is harmless: the page is being torn down.
|
|
101
|
+
function onDOMContentLoaded(): void {
|
|
102
|
+
if (isPageUnloading()) return;
|
|
103
|
+
|
|
104
|
+
// In dev mode, do NOT close the stream. React's RSC renderer
|
|
105
|
+
// includes debug owner/stack references ($1, $14, etc.) in the
|
|
106
|
+
// Flight payload that point to rows delivered through the debug
|
|
107
|
+
// channel, not the main Flight stream. The browser Flight client
|
|
108
|
+
// tracks these as pending chunks. Closing the stream with
|
|
109
|
+
// unresolved chunks triggers reportGlobalError("Connection closed")
|
|
110
|
+
// which kills the entire React tree.
|
|
111
|
+
//
|
|
112
|
+
// Leaving the stream open is harmless: React has already received
|
|
113
|
+
// all data rows and can hydrate fully. The pending debug chunks
|
|
114
|
+
// just remain unresolved (they're only used for React DevTools
|
|
115
|
+
// component stacks, not rendering).
|
|
116
|
+
//
|
|
117
|
+
// In production, debug rows are not emitted, so closing is safe.
|
|
118
|
+
if (process.env.NODE_ENV === 'development') {
|
|
119
|
+
// Mark as flushed so no more data is buffered, but don't close.
|
|
120
|
+
streamFlushed = true;
|
|
121
|
+
dataBuffer = undefined;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (streamWriter && !streamFlushed) {
|
|
126
|
+
streamWriter.close();
|
|
127
|
+
streamFlushed = true;
|
|
128
|
+
dataBuffer = undefined;
|
|
129
|
+
}
|
|
130
|
+
streamFlushed = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (document.readyState === 'loading') {
|
|
134
|
+
document.addEventListener('DOMContentLoaded', onDOMContentLoaded, false);
|
|
135
|
+
} else {
|
|
136
|
+
// DOM already parsed. All inline RSC <script> tags have already
|
|
137
|
+
// executed and pushed their data into the buffer. The buffer was
|
|
138
|
+
// flushed into the stream during start() above.
|
|
139
|
+
//
|
|
140
|
+
// Close via queueMicrotask rather than setTimeout. setTimeout
|
|
141
|
+
// defers to the next macrotask, which can race with React's
|
|
142
|
+
// Flight client read loop — if React finishes reading all queued
|
|
143
|
+
// chunks and issues a reader.read() that pends, the stream is
|
|
144
|
+
// NOT closed yet (setTimeout hasn't fired), so React sees an
|
|
145
|
+
// open stream and waits. Then setTimeout fires and closes it.
|
|
146
|
+
// This works in theory but some React Flight builds interpret
|
|
147
|
+
// a mid-read close as "Connection closed" rather than clean EOF.
|
|
148
|
+
// queueMicrotask fires at the end of the current microtask
|
|
149
|
+
// checkpoint — after start() and createFromReadableStream
|
|
150
|
+
// initialization but before any macrotask, giving React a
|
|
151
|
+
// consistent close signal. See TIM-524.
|
|
152
|
+
queueMicrotask(onDOMContentLoaded);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const element = createFromReadableStream(rscPayload);
|
|
156
|
+
return { element };
|
|
157
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll position helpers for the browser entry.
|
|
3
|
+
*
|
|
4
|
+
* Reads scroll position from the document viewport or explicitly marked
|
|
5
|
+
* `data-timber-scroll-restoration` containers.
|
|
6
|
+
*
|
|
7
|
+
* See design/19-client-navigation.md §"Overflow Scroll Containers".
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read the current scroll position.
|
|
12
|
+
*
|
|
13
|
+
* Checks window scroll first, then explicit `data-timber-scroll-restoration`
|
|
14
|
+
* containers. With segment tree merging, shared layouts are reconciled in
|
|
15
|
+
* place via `cloneElement` — React preserves their DOM and scroll state
|
|
16
|
+
* naturally. We don't need to auto-detect overflow containers; only
|
|
17
|
+
* explicitly marked containers are tracked.
|
|
18
|
+
*/
|
|
19
|
+
export function getScrollY(): number {
|
|
20
|
+
if (window.scrollY || document.documentElement.scrollTop || document.body.scrollTop) {
|
|
21
|
+
return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop;
|
|
22
|
+
}
|
|
23
|
+
for (const el of document.querySelectorAll('[data-timber-scroll-restoration]')) {
|
|
24
|
+
if ((el as HTMLElement).scrollTop > 0) return (el as HTMLElement).scrollTop;
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
}
|