@timber-js/app 0.1.21 → 0.1.23
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/als-registry-c0AGnbqS.js +39 -0
- package/dist/_chunks/als-registry-c0AGnbqS.js.map +1 -0
- package/dist/_chunks/{interception-c-a3uODY.js → interception-DGDIjDbR.js} +10 -3
- package/dist/_chunks/interception-DGDIjDbR.js.map +1 -0
- package/dist/_chunks/{metadata-routes-BDnswgRO.js → metadata-routes-CQCnF4VK.js} +14 -2
- package/dist/_chunks/metadata-routes-CQCnF4VK.js.map +1 -0
- package/dist/_chunks/{request-context-BzES06i1.js → request-context-C69VW4xS.js} +2 -4
- package/dist/_chunks/request-context-C69VW4xS.js.map +1 -0
- package/dist/_chunks/ssr-data-B2yikEEB.js +90 -0
- package/dist/_chunks/ssr-data-B2yikEEB.js.map +1 -0
- package/dist/_chunks/{tracing-BtOwb8O6.js → tracing-tIvqStk8.js} +2 -3
- package/dist/_chunks/tracing-tIvqStk8.js.map +1 -0
- package/dist/_chunks/{use-cookie-D2cZu0jK.js → use-cookie-D5aS4slY.js} +2 -2
- package/dist/_chunks/{use-cookie-D2cZu0jK.js.map → use-cookie-D5aS4slY.js.map} +1 -1
- package/dist/_chunks/{use-query-states-wEXY2JQB.js → use-query-states-DAhgj8Gx.js} +1 -1
- package/dist/_chunks/{use-query-states-wEXY2JQB.js.map → use-query-states-DAhgj8Gx.js.map} +1 -1
- package/dist/cache/index.js +2 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +35 -25
- package/dist/client/index.js.map +1 -1
- package/dist/client/router-ref.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +3 -0
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/state.d.ts +47 -0
- package/dist/client/state.d.ts.map +1 -0
- package/dist/client/types.d.ts +10 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/unload-guard.d.ts +3 -0
- package/dist/client/unload-guard.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +3 -0
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +3 -0
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/cookies/index.js +4 -2
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/rsc-runtime/browser.d.ts +13 -0
- package/dist/rsc-runtime/browser.d.ts.map +1 -0
- package/dist/rsc-runtime/rsc.d.ts +14 -0
- package/dist/rsc-runtime/rsc.d.ts.map +1 -0
- package/dist/rsc-runtime/ssr.d.ts +13 -0
- package/dist/rsc-runtime/ssr.d.ts.map +1 -0
- package/dist/search-params/builtin-codecs.d.ts +105 -0
- package/dist/search-params/builtin-codecs.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +1 -0
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +167 -2
- package/dist/search-params/index.js.map +1 -1
- package/dist/server/actions.d.ts +2 -7
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +80 -0
- package/dist/server/als-registry.d.ts.map +1 -0
- package/dist/server/early-hints-sender.d.ts.map +1 -1
- package/dist/server/form-flash.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +242 -76
- package/dist/server/index.js.map +1 -1
- package/dist/server/metadata-routes.d.ts +27 -0
- package/dist/server/metadata-routes.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +7 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +14 -6
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +2 -32
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +5 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +25 -0
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts +43 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts +52 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -0
- package/dist/server/rsc-prop-warnings.d.ts +53 -0
- package/dist/server/rsc-prop-warnings.d.ts.map +1 -0
- package/dist/server/server-timing.d.ts +49 -0
- package/dist/server/server-timing.d.ts.map +1 -0
- package/dist/server/tracing.d.ts +2 -6
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/types.d.ts +11 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/browser-entry.ts +1 -1
- package/src/client/index.ts +1 -1
- package/src/client/router-ref.ts +6 -12
- package/src/client/router.ts +25 -20
- package/src/client/ssr-data.ts +25 -9
- package/src/client/state.ts +83 -0
- package/src/client/types.ts +18 -1
- package/src/client/unload-guard.ts +6 -3
- package/src/client/use-params.ts +10 -13
- package/src/client/use-search-params.ts +9 -5
- package/src/plugins/shims.ts +26 -2
- package/src/routing/scanner.ts +18 -2
- package/src/rsc-runtime/browser.ts +18 -0
- package/src/rsc-runtime/rsc.ts +19 -0
- package/src/rsc-runtime/ssr.ts +13 -0
- package/src/search-params/builtin-codecs.ts +228 -0
- package/src/search-params/index.ts +11 -0
- package/src/server/action-handler.ts +1 -1
- package/src/server/actions.ts +4 -10
- package/src/server/als-registry.ts +116 -0
- package/src/server/deny-renderer.ts +1 -1
- package/src/server/early-hints-sender.ts +1 -3
- package/src/server/form-flash.ts +1 -5
- package/src/server/index.ts +1 -0
- package/src/server/metadata-routes.ts +61 -0
- package/src/server/pipeline.ts +164 -38
- package/src/server/primitives.ts +110 -6
- package/src/server/request-context.ts +8 -36
- package/src/server/route-matcher.ts +25 -2
- package/src/server/rsc-entry/error-renderer.ts +1 -1
- package/src/server/rsc-entry/index.ts +42 -380
- package/src/server/rsc-entry/rsc-payload.ts +126 -0
- package/src/server/rsc-entry/rsc-stream.ts +162 -0
- package/src/server/rsc-entry/ssr-renderer.ts +228 -0
- package/src/server/rsc-prop-warnings.ts +187 -0
- package/src/server/server-timing.ts +132 -0
- package/src/server/ssr-entry.ts +1 -1
- package/src/server/tracing.ts +3 -11
- package/src/server/types.ts +16 -0
- package/dist/_chunks/interception-c-a3uODY.js.map +0 -1
- package/dist/_chunks/metadata-routes-BDnswgRO.js.map +0 -1
- package/dist/_chunks/request-context-BzES06i1.js.map +0 -1
- package/dist/_chunks/ssr-data-BgSwMbN9.js +0 -38
- package/dist/_chunks/ssr-data-BgSwMbN9.js.map +0 -1
- package/dist/_chunks/tracing-BtOwb8O6.js.map +0 -1
package/src/client/router.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { SegmentCache, PrefetchCache, buildSegmentTree } from './segment-cache';
|
|
|
5
5
|
import type { SegmentInfo } from './segment-cache';
|
|
6
6
|
import { HistoryStack } from './history';
|
|
7
7
|
import type { HeadElement } from './head';
|
|
8
|
+
import { flushSync } from 'react-dom';
|
|
8
9
|
import { setCurrentParams, notifyParamsListeners } from './use-params.js';
|
|
9
10
|
|
|
10
11
|
// ─── Types ───────────────────────────────────────────────────────
|
|
@@ -393,18 +394,18 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
393
394
|
// header reflects the currently mounted segments.
|
|
394
395
|
updateSegmentCache(result.segmentInfo);
|
|
395
396
|
|
|
396
|
-
// Update
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
397
|
+
// Update params, render the new tree, and notify params subscribers
|
|
398
|
+
// in a single synchronous flush. Without flushSync, renderPayload()
|
|
399
|
+
// (reactRoot.render) and notifyParamsListeners() are separate update
|
|
400
|
+
// mechanisms that React may commit in different frames — causing a
|
|
401
|
+
// flash where both the old and new active rows show simultaneously
|
|
402
|
+
// in preserved layouts (the new tree commits before the external
|
|
403
|
+
// store re-render deactivates the old row).
|
|
400
404
|
updateParams(result.params);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// Now notify useParams() subscribers — preserved layout components
|
|
406
|
-
// re-render after the new tree is committed, seeing {new tree, new params}.
|
|
407
|
-
notifyParamsListeners();
|
|
405
|
+
flushSync(() => {
|
|
406
|
+
renderPayload(result.payload);
|
|
407
|
+
notifyParamsListeners();
|
|
408
|
+
});
|
|
408
409
|
|
|
409
410
|
// Update document.title and <meta> tags with the new page's metadata
|
|
410
411
|
applyHead(result.headElements);
|
|
@@ -464,12 +465,12 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
464
465
|
// Update segment cache with fresh segment info from full render
|
|
465
466
|
updateSegmentCache(result.segmentInfo);
|
|
466
467
|
|
|
467
|
-
//
|
|
468
|
+
// Atomic update — see navigate() for rationale on flushSync.
|
|
468
469
|
updateParams(result.params);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
470
|
+
flushSync(() => {
|
|
471
|
+
renderPayload(result.payload);
|
|
472
|
+
notifyParamsListeners();
|
|
473
|
+
});
|
|
473
474
|
applyHead(result.headElements);
|
|
474
475
|
} finally {
|
|
475
476
|
setPending(false);
|
|
@@ -485,8 +486,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
485
486
|
if (entry && entry.payload !== null) {
|
|
486
487
|
// Replay cached payload — no server roundtrip
|
|
487
488
|
updateParams(entry.params);
|
|
488
|
-
|
|
489
|
-
|
|
489
|
+
flushSync(() => {
|
|
490
|
+
renderPayload(entry.payload);
|
|
491
|
+
notifyParamsListeners();
|
|
492
|
+
});
|
|
490
493
|
applyHead(entry.headElements);
|
|
491
494
|
afterPaint(() => {
|
|
492
495
|
deps.scrollTo(0, scrollY);
|
|
@@ -508,8 +511,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
508
511
|
headElements: result.headElements,
|
|
509
512
|
params: result.params,
|
|
510
513
|
});
|
|
511
|
-
|
|
512
|
-
|
|
514
|
+
flushSync(() => {
|
|
515
|
+
renderPayload(result.payload);
|
|
516
|
+
notifyParamsListeners();
|
|
517
|
+
});
|
|
513
518
|
applyHead(result.headElements);
|
|
514
519
|
afterPaint(() => {
|
|
515
520
|
deps.scrollTo(0, scrollY);
|
package/src/client/ssr-data.ts
CHANGED
|
@@ -17,8 +17,18 @@
|
|
|
17
17
|
* APIs, as it's imported by 'use client' hooks that are bundled for the browser.
|
|
18
18
|
* The ALS instance lives in ssr-entry.ts (server-only); this module only holds
|
|
19
19
|
* a reference to the provider function.
|
|
20
|
+
*
|
|
21
|
+
* All mutable state is delegated to client/state.ts for singleton guarantees.
|
|
22
|
+
* See design/18-build-system.md §"Singleton State Registry"
|
|
20
23
|
*/
|
|
21
24
|
|
|
25
|
+
import {
|
|
26
|
+
ssrDataProvider,
|
|
27
|
+
currentSsrData,
|
|
28
|
+
_setSsrDataProvider,
|
|
29
|
+
_setCurrentSsrData,
|
|
30
|
+
} from './state.js';
|
|
31
|
+
|
|
22
32
|
// ─── Types ────────────────────────────────────────────────────────
|
|
23
33
|
|
|
24
34
|
export interface SsrData {
|
|
@@ -44,8 +54,16 @@ export interface SsrData {
|
|
|
44
54
|
// Server-side code (ssr-entry.ts) registers a provider that reads
|
|
45
55
|
// from AsyncLocalStorage. This avoids importing node:async_hooks
|
|
46
56
|
// in this browser-bundled module.
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
//
|
|
58
|
+
// Module singleton guarantee: In Vite's SSR environment, both
|
|
59
|
+
// ssr-entry.ts (via #/client/ssr-data.js) and client component hooks
|
|
60
|
+
// (via @timber-js/app/client) must resolve to the SAME module instance
|
|
61
|
+
// of this file. The timber-shims plugin ensures this by remapping
|
|
62
|
+
// @timber-js/app/client → src/client/index.ts in the SSR environment.
|
|
63
|
+
// Without this remap, @timber-js/app/client resolves to dist/ (via
|
|
64
|
+
// package.json exports), creating a split where registerSsrDataProvider
|
|
65
|
+
// writes to one instance but getSsrData reads from another.
|
|
66
|
+
// See timber-shims plugin resolveId for details.
|
|
49
67
|
|
|
50
68
|
/**
|
|
51
69
|
* Register an ALS-backed SSR data provider. Called once at module load
|
|
@@ -56,15 +74,13 @@ let _ssrDataProvider: (() => SsrData | undefined) | undefined;
|
|
|
56
74
|
* concurrent requests with streaming Suspense.
|
|
57
75
|
*/
|
|
58
76
|
export function registerSsrDataProvider(provider: () => SsrData | undefined): void {
|
|
59
|
-
|
|
77
|
+
_setSsrDataProvider(provider);
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
// ─── Module-Level Fallback ────────────────────────────────────────
|
|
63
81
|
//
|
|
64
82
|
// Used by tests and as a fallback when no ALS provider is registered.
|
|
65
83
|
|
|
66
|
-
let currentSsrData: SsrData | undefined;
|
|
67
|
-
|
|
68
84
|
/**
|
|
69
85
|
* Set the SSR data for the current request via module-level state.
|
|
70
86
|
*
|
|
@@ -72,7 +88,7 @@ let currentSsrData: SsrData | undefined;
|
|
|
72
88
|
* This function is retained for tests and as a fallback.
|
|
73
89
|
*/
|
|
74
90
|
export function setSsrData(data: SsrData): void {
|
|
75
|
-
|
|
91
|
+
_setCurrentSsrData(data);
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
/**
|
|
@@ -82,7 +98,7 @@ export function setSsrData(data: SsrData): void {
|
|
|
82
98
|
* This function is retained for tests and as a fallback.
|
|
83
99
|
*/
|
|
84
100
|
export function clearSsrData(): void {
|
|
85
|
-
|
|
101
|
+
_setCurrentSsrData(undefined);
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
/**
|
|
@@ -95,8 +111,8 @@ export function clearSsrData(): void {
|
|
|
95
111
|
* Used by client hooks' server snapshot functions.
|
|
96
112
|
*/
|
|
97
113
|
export function getSsrData(): SsrData | undefined {
|
|
98
|
-
if (
|
|
99
|
-
return
|
|
114
|
+
if (ssrDataProvider) {
|
|
115
|
+
return ssrDataProvider();
|
|
100
116
|
}
|
|
101
117
|
return currentSsrData;
|
|
102
118
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized client singleton state registry.
|
|
3
|
+
*
|
|
4
|
+
* ALL mutable module-level state that must have singleton semantics across
|
|
5
|
+
* the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,
|
|
6
|
+
* use-params.ts, use-search-params.ts, unload-guard.ts) import from this file
|
|
7
|
+
* and re-export thin wrapper functions.
|
|
8
|
+
*
|
|
9
|
+
* Why: In Vite dev, a module is instantiated separately if reached via different
|
|
10
|
+
* import paths (e.g., relative `./foo.js` vs barrel `@timber-js/app/client`).
|
|
11
|
+
* By centralizing all mutable state in a single module that is always reached
|
|
12
|
+
* through the same dependency chain (barrel → wrapper → state.ts), we guarantee
|
|
13
|
+
* a single instance of every piece of shared state.
|
|
14
|
+
*
|
|
15
|
+
* DO NOT import this file from outside client/. Server code must never depend
|
|
16
|
+
* on client state. The barrel (client/index.ts) is the public entry point.
|
|
17
|
+
*
|
|
18
|
+
* See design/18-build-system.md §"Module Singleton Strategy" and
|
|
19
|
+
* §"Singleton State Registry".
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { RouterInstance } from './router.js';
|
|
23
|
+
import type { SsrData } from './ssr-data.js';
|
|
24
|
+
|
|
25
|
+
// ─── Router (from router-ref.ts) ──────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/** The global router singleton — set once during bootstrap. */
|
|
28
|
+
export let globalRouter: RouterInstance | null = null;
|
|
29
|
+
|
|
30
|
+
export function _setGlobalRouter(router: RouterInstance | null): void {
|
|
31
|
+
globalRouter = router;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── SSR Data Provider (from ssr-data.ts) ──────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ALS-backed SSR data provider. When registered, getSsrData() reads from
|
|
38
|
+
* this function (ALS store) instead of module-level currentSsrData.
|
|
39
|
+
*/
|
|
40
|
+
export let ssrDataProvider: (() => SsrData | undefined) | undefined;
|
|
41
|
+
|
|
42
|
+
export function _setSsrDataProvider(provider: (() => SsrData | undefined) | undefined): void {
|
|
43
|
+
ssrDataProvider = provider;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fallback SSR data for tests and environments without ALS. */
|
|
47
|
+
export let currentSsrData: SsrData | undefined;
|
|
48
|
+
|
|
49
|
+
export function _setCurrentSsrData(data: SsrData | undefined): void {
|
|
50
|
+
currentSsrData = data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Route Params (from use-params.ts) ──────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/** Current route params snapshot — replaced (not mutated) on each navigation. */
|
|
56
|
+
export let currentParams: Record<string, string | string[]> = {};
|
|
57
|
+
|
|
58
|
+
export function _setCurrentParams(params: Record<string, string | string[]>): void {
|
|
59
|
+
currentParams = params;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Listeners notified when currentParams changes. */
|
|
63
|
+
export const paramsListeners = new Set<() => void>();
|
|
64
|
+
|
|
65
|
+
// ─── Search Params Cache (from use-search-params.ts) ────────────────────────
|
|
66
|
+
|
|
67
|
+
/** Cached search string — avoids reparsing when URL hasn't changed. */
|
|
68
|
+
export let cachedSearch = '';
|
|
69
|
+
export let cachedSearchParams = new URLSearchParams();
|
|
70
|
+
|
|
71
|
+
export function _setCachedSearch(search: string, params: URLSearchParams): void {
|
|
72
|
+
cachedSearch = search;
|
|
73
|
+
cachedSearchParams = params;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Unload Guard (from unload-guard.ts) ─────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/** Whether the page is currently being unloaded. */
|
|
79
|
+
export let unloading = false;
|
|
80
|
+
|
|
81
|
+
export function _setUnloading(value: boolean): void {
|
|
82
|
+
unloading = value;
|
|
83
|
+
}
|
package/src/client/types.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A value that is safe to pass through `JSON.stringify` without data loss.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the server-side type. Defined separately to avoid importing server
|
|
5
|
+
* modules into the client bundle.
|
|
6
|
+
*/
|
|
7
|
+
export type JsonSerializable =
|
|
8
|
+
| string
|
|
9
|
+
| number
|
|
10
|
+
| boolean
|
|
11
|
+
| null
|
|
12
|
+
| JsonSerializable[]
|
|
13
|
+
| { [key: string]: JsonSerializable };
|
|
14
|
+
|
|
15
|
+
export interface RenderErrorDigest<
|
|
16
|
+
Code extends string = string,
|
|
17
|
+
Data extends JsonSerializable = JsonSerializable,
|
|
18
|
+
> {
|
|
2
19
|
code: Code;
|
|
3
20
|
data: Data;
|
|
4
21
|
}
|
|
@@ -8,20 +8,23 @@
|
|
|
8
8
|
* unloaded so error boundaries and error handlers can suppress abort-related
|
|
9
9
|
* errors during the unload window.
|
|
10
10
|
*
|
|
11
|
+
* Mutable state is delegated to client/state.ts for singleton guarantees.
|
|
12
|
+
* See design/18-build-system.md §"Singleton State Registry"
|
|
13
|
+
*
|
|
11
14
|
* See design/10-error-handling.md §"Known limitation: deny() inside Suspense and hydration"
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
import { unloading, _setUnloading } from './state.js';
|
|
15
18
|
|
|
16
19
|
if (typeof window !== 'undefined') {
|
|
17
20
|
window.addEventListener('beforeunload', () => {
|
|
18
|
-
|
|
21
|
+
_setUnloading(true);
|
|
19
22
|
});
|
|
20
23
|
|
|
21
24
|
// Also detect pagehide for bfcache-aware browsers (Safari).
|
|
22
25
|
// pagehide fires for both navigations and page hide events.
|
|
23
26
|
window.addEventListener('pagehide', () => {
|
|
24
|
-
|
|
27
|
+
_setUnloading(true);
|
|
25
28
|
});
|
|
26
29
|
}
|
|
27
30
|
|
package/src/client/use-params.ts
CHANGED
|
@@ -23,31 +23,28 @@
|
|
|
23
23
|
* params change during client-side navigation. This matches the pattern
|
|
24
24
|
* used by usePathname() and useSearchParams().
|
|
25
25
|
*
|
|
26
|
+
* All mutable state is delegated to client/state.ts for singleton guarantees.
|
|
27
|
+
* See design/18-build-system.md §"Singleton State Registry"
|
|
28
|
+
*
|
|
26
29
|
* Design doc: design/09-typescript.md §"Typed Routes"
|
|
27
30
|
*/
|
|
28
31
|
|
|
29
32
|
import { useSyncExternalStore } from 'react';
|
|
30
33
|
import type { Routes } from '#/index.js';
|
|
31
34
|
import { getSsrData } from './ssr-data.js';
|
|
35
|
+
import { currentParams, _setCurrentParams, paramsListeners } from './state.js';
|
|
32
36
|
|
|
33
37
|
// ---------------------------------------------------------------------------
|
|
34
|
-
// Module-level
|
|
38
|
+
// Module-level subscribe/notify pattern — state lives in state.ts
|
|
35
39
|
// ---------------------------------------------------------------------------
|
|
36
40
|
|
|
37
|
-
// The current params snapshot. Replaced (not mutated) on each navigation
|
|
38
|
-
// so that React's Object.is check on the snapshot detects changes.
|
|
39
|
-
let currentParams: Record<string, string | string[]> = {};
|
|
40
|
-
|
|
41
|
-
// Listeners notified when currentParams changes.
|
|
42
|
-
const listeners = new Set<() => void>();
|
|
43
|
-
|
|
44
41
|
/**
|
|
45
42
|
* Subscribe to params changes. Called by useSyncExternalStore.
|
|
46
43
|
* Exported for testing — not intended for direct use by app code.
|
|
47
44
|
*/
|
|
48
45
|
export function subscribe(callback: () => void): () => void {
|
|
49
|
-
|
|
50
|
-
return () =>
|
|
46
|
+
paramsListeners.add(callback);
|
|
47
|
+
return () => paramsListeners.delete(callback);
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
/**
|
|
@@ -87,7 +84,7 @@ function getServerSnapshot(): Record<string, string | string[]> {
|
|
|
87
84
|
* module-level fallback path.
|
|
88
85
|
*/
|
|
89
86
|
export function setCurrentParams(params: Record<string, string | string[]>): void {
|
|
90
|
-
|
|
87
|
+
_setCurrentParams(params);
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
/**
|
|
@@ -98,7 +95,7 @@ export function setCurrentParams(params: Record<string, string | string[]>): voi
|
|
|
98
95
|
* {old tree, new params} intermediate state.
|
|
99
96
|
*/
|
|
100
97
|
export function notifyParamsListeners(): void {
|
|
101
|
-
for (const listener of
|
|
98
|
+
for (const listener of paramsListeners) {
|
|
102
99
|
listener();
|
|
103
100
|
}
|
|
104
101
|
}
|
|
@@ -125,7 +122,7 @@ export function useParams<R extends keyof Routes>(route: R): Routes[R]['params']
|
|
|
125
122
|
export function useParams(route?: string): Record<string, string | string[]>;
|
|
126
123
|
export function useParams(_route?: string): Record<string, string | string[]> {
|
|
127
124
|
// useSyncExternalStore handles both client and SSR:
|
|
128
|
-
// - Client: calls getSnapshot() → reads
|
|
125
|
+
// - Client: calls getSnapshot() → reads currentParams from state.ts
|
|
129
126
|
// - SSR: calls getServerSnapshot() → reads from ALS-backed getSsrData()
|
|
130
127
|
//
|
|
131
128
|
// We must always call the hook (Rules of Hooks — no conditional hook calls).
|
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
*
|
|
15
15
|
* During SSR, reads the request search params from the SSR ALS context
|
|
16
16
|
* (populated by ssr-entry.ts) instead of window.location.
|
|
17
|
+
*
|
|
18
|
+
* All mutable state is delegated to client/state.ts for singleton guarantees.
|
|
19
|
+
* See design/18-build-system.md §"Singleton State Registry"
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
22
|
import { useSyncExternalStore } from 'react';
|
|
20
23
|
import { getSsrData } from './ssr-data.js';
|
|
24
|
+
import { cachedSearch, cachedSearchParams, _setCachedSearch } from './state.js';
|
|
21
25
|
|
|
22
26
|
function getSearch(): string {
|
|
23
27
|
if (typeof window !== 'undefined') return window.location.search;
|
|
@@ -43,16 +47,16 @@ function subscribe(callback: () => void): () => void {
|
|
|
43
47
|
|
|
44
48
|
// Cache the last search string and its parsed URLSearchParams to avoid
|
|
45
49
|
// creating a new object on every render when the URL hasn't changed.
|
|
46
|
-
|
|
47
|
-
let cachedParams = new URLSearchParams();
|
|
50
|
+
// State lives in client/state.ts for singleton guarantees.
|
|
48
51
|
|
|
49
52
|
function getSearchParams(): URLSearchParams {
|
|
50
53
|
const search = getSearch();
|
|
51
54
|
if (search !== cachedSearch) {
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
const params = new URLSearchParams(search);
|
|
56
|
+
_setCachedSearch(search, params);
|
|
57
|
+
return params;
|
|
54
58
|
}
|
|
55
|
-
return
|
|
59
|
+
return cachedSearchParams;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function getServerSearchParams(): URLSearchParams {
|
package/src/plugins/shims.ts
CHANGED
|
@@ -92,8 +92,10 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
92
92
|
* instance as framework internals (which import via #/). This ensures
|
|
93
93
|
* a single requestContextAls and _getRscFallback variable.
|
|
94
94
|
*
|
|
95
|
-
* @timber-js/app/client is
|
|
96
|
-
*
|
|
95
|
+
* @timber-js/app/client is resolved to src/ in the SSR environment so
|
|
96
|
+
* client hooks share the same module instance as ssr-entry.ts internals.
|
|
97
|
+
* In RSC it resolves to dist/ (via package.json exports) where 'use client'
|
|
98
|
+
* is preserved on the entry for client boundary detection.
|
|
97
99
|
*/
|
|
98
100
|
resolveId(id: string) {
|
|
99
101
|
// Poison pill packages — resolve to virtual modules handled by load()
|
|
@@ -127,6 +129,28 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
127
129
|
return resolve(PKG_ROOT, 'src', 'server', 'index.ts');
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
// @timber-js/app/client → src/ in the SSR environment so client hooks
|
|
133
|
+
// (useParams, usePathname, etc.) share the same module instance as
|
|
134
|
+
// ssr-entry.ts's internal imports (via #/client/...).
|
|
135
|
+
//
|
|
136
|
+
// Without this remap, @timber-js/app/client resolves to dist/ (via
|
|
137
|
+
// package.json exports), creating a module instance split: ssr-entry.ts
|
|
138
|
+
// registers the ALS-backed SSR data provider on the src/ instance of
|
|
139
|
+
// ssr-data.ts, but client component hooks read getSsrData() from the
|
|
140
|
+
// dist/ instance — which has no provider. Result: hooks like useParams()
|
|
141
|
+
// return empty defaults during SSR.
|
|
142
|
+
//
|
|
143
|
+
// This remap is SSR-only. The RSC environment still resolves to dist/
|
|
144
|
+
// where 'use client' is preserved on the entry (needed for client
|
|
145
|
+
// boundary detection). The client (browser) environment uses dist/
|
|
146
|
+
// for bundling.
|
|
147
|
+
if (cleanId === '@timber-js/app/client') {
|
|
148
|
+
const envName = (this as unknown as { environment?: { name?: string } }).environment?.name;
|
|
149
|
+
if (envName === 'ssr') {
|
|
150
|
+
return resolve(PKG_ROOT, 'src', 'client', 'index.ts');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
return null;
|
|
131
155
|
},
|
|
132
156
|
|
package/src/routing/scanner.ts
CHANGED
|
@@ -19,7 +19,10 @@ import type {
|
|
|
19
19
|
InterceptionMarker,
|
|
20
20
|
} from './types.js';
|
|
21
21
|
import { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS } from './types.js';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
classifyMetadataRoute,
|
|
24
|
+
isDynamicMetadataExtension,
|
|
25
|
+
} from '#/server/metadata-routes.js';
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* Pattern matching encoded path delimiters that must be rejected during route discovery.
|
|
@@ -317,13 +320,26 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
|
|
|
317
320
|
}
|
|
318
321
|
|
|
319
322
|
// Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)
|
|
323
|
+
// Both static (.xml, .txt, .png, .ico, etc.) and dynamic (.ts, .tsx) files are recognized.
|
|
324
|
+
// When both exist for the same base name, dynamic takes precedence.
|
|
320
325
|
// See design/16-metadata.md §"Metadata Routes"
|
|
321
326
|
const metaInfo = classifyMetadataRoute(entry);
|
|
322
327
|
if (metaInfo) {
|
|
323
328
|
if (!node.metadataRoutes) {
|
|
324
329
|
node.metadataRoutes = new Map();
|
|
325
330
|
}
|
|
326
|
-
node.metadataRoutes.
|
|
331
|
+
const existing = node.metadataRoutes.get(name);
|
|
332
|
+
if (existing) {
|
|
333
|
+
// Dynamic > static precedence: only overwrite if the new file is dynamic
|
|
334
|
+
// or the existing file is static (dynamic always wins).
|
|
335
|
+
const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);
|
|
336
|
+
const newIsDynamic = isDynamicMetadataExtension(name, ext);
|
|
337
|
+
if (newIsDynamic || !existingIsDynamic) {
|
|
338
|
+
node.metadataRoutes.set(name, { filePath: fullPath, extension: ext });
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
node.metadataRoutes.set(name, { filePath: fullPath, extension: ext });
|
|
342
|
+
}
|
|
327
343
|
}
|
|
328
344
|
}
|
|
329
345
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Runtime Adapter — Re-exports from @vitejs/plugin-rsc/browser.
|
|
3
|
+
*
|
|
4
|
+
* This module insulates the rest of the framework from direct imports of
|
|
5
|
+
* @vitejs/plugin-rsc. The plugin is pre-1.0 and its API surface will change.
|
|
6
|
+
* By routing all browser-environment imports through this single file, a
|
|
7
|
+
* breaking upstream change only requires updating one place.
|
|
8
|
+
*
|
|
9
|
+
* Keep this as thin pass-through re-exports — the value is the single choke
|
|
10
|
+
* point, not abstraction.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
createFromReadableStream,
|
|
15
|
+
createFromFetch,
|
|
16
|
+
setServerCallback,
|
|
17
|
+
encodeReply,
|
|
18
|
+
} from '@vitejs/plugin-rsc/browser';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RSC Runtime Adapter — Re-exports from @vitejs/plugin-rsc/rsc.
|
|
3
|
+
*
|
|
4
|
+
* This module insulates the rest of the framework from direct imports of
|
|
5
|
+
* @vitejs/plugin-rsc. The plugin is pre-1.0 and its API surface will change.
|
|
6
|
+
* By routing all RSC-environment imports through this single file, a breaking
|
|
7
|
+
* upstream change only requires updating one place instead of every file that
|
|
8
|
+
* touches the RSC runtime.
|
|
9
|
+
*
|
|
10
|
+
* Keep this as thin pass-through re-exports — the value is the single choke
|
|
11
|
+
* point, not abstraction.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
renderToReadableStream,
|
|
16
|
+
loadServerAction,
|
|
17
|
+
decodeReply,
|
|
18
|
+
decodeAction,
|
|
19
|
+
} from '@vitejs/plugin-rsc/rsc';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR Runtime Adapter — Re-exports from @vitejs/plugin-rsc/ssr.
|
|
3
|
+
*
|
|
4
|
+
* This module insulates the rest of the framework from direct imports of
|
|
5
|
+
* @vitejs/plugin-rsc. The plugin is pre-1.0 and its API surface will change.
|
|
6
|
+
* By routing all SSR-environment imports through this single file, a breaking
|
|
7
|
+
* upstream change only requires updating one place.
|
|
8
|
+
*
|
|
9
|
+
* Keep this as thin pass-through re-exports — the value is the single choke
|
|
10
|
+
* point, not abstraction.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { createFromReadableStream } from '@vitejs/plugin-rsc/ssr';
|