@timber-js/app 0.1.20 → 0.1.22
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 +40 -26
- 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 +19 -6
- 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 +14 -4
- 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 +42 -32
- 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,7 +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 { setCurrentParams } from './use-params.js';
|
|
8
|
+
import { setCurrentParams, notifyParamsListeners } from './use-params.js';
|
|
9
9
|
|
|
10
10
|
// ─── Types ───────────────────────────────────────────────────────
|
|
11
11
|
|
|
@@ -393,12 +393,19 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
393
393
|
// header reflects the currently mounted segments.
|
|
394
394
|
updateSegmentCache(result.segmentInfo);
|
|
395
395
|
|
|
396
|
-
// Update
|
|
396
|
+
// Update the params snapshot before rendering so new components in the
|
|
397
|
+
// RSC tree see the correct params via getSnapshot(). Subscriber
|
|
398
|
+
// notification is deferred until after renderPayload() so preserved
|
|
399
|
+
// layouts don't re-render with {old tree, new params}.
|
|
397
400
|
updateParams(result.params);
|
|
398
401
|
|
|
399
402
|
// Render the decoded RSC tree into the DOM.
|
|
400
403
|
renderPayload(result.payload);
|
|
401
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();
|
|
408
|
+
|
|
402
409
|
// Update document.title and <meta> tags with the new page's metadata
|
|
403
410
|
applyHead(result.headElements);
|
|
404
411
|
|
|
@@ -457,11 +464,12 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
457
464
|
// Update segment cache with fresh segment info from full render
|
|
458
465
|
updateSegmentCache(result.segmentInfo);
|
|
459
466
|
|
|
460
|
-
// Update
|
|
467
|
+
// Update params snapshot before rendering (see navigate() for rationale)
|
|
461
468
|
updateParams(result.params);
|
|
462
469
|
|
|
463
|
-
// Render the fresh RSC tree
|
|
470
|
+
// Render the fresh RSC tree, then notify params subscribers
|
|
464
471
|
renderPayload(result.payload);
|
|
472
|
+
notifyParamsListeners();
|
|
465
473
|
applyHead(result.headElements);
|
|
466
474
|
} finally {
|
|
467
475
|
setPending(false);
|
|
@@ -478,6 +486,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
478
486
|
// Replay cached payload — no server roundtrip
|
|
479
487
|
updateParams(entry.params);
|
|
480
488
|
renderPayload(entry.payload);
|
|
489
|
+
notifyParamsListeners();
|
|
481
490
|
applyHead(entry.headElements);
|
|
482
491
|
afterPaint(() => {
|
|
483
492
|
deps.scrollTo(0, scrollY);
|
|
@@ -500,6 +509,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
500
509
|
params: result.params,
|
|
501
510
|
});
|
|
502
511
|
renderPayload(result.payload);
|
|
512
|
+
notifyParamsListeners();
|
|
503
513
|
applyHead(result.headElements);
|
|
504
514
|
afterPaint(() => {
|
|
505
515
|
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
|
/**
|
|
@@ -72,21 +69,33 @@ function getServerSnapshot(): Record<string, string | string[]> {
|
|
|
72
69
|
// ---------------------------------------------------------------------------
|
|
73
70
|
|
|
74
71
|
/**
|
|
75
|
-
* Set the current route params
|
|
76
|
-
*
|
|
72
|
+
* Set the current route params WITHOUT notifying subscribers.
|
|
73
|
+
* Called by the router before renderPayload() so that new components
|
|
74
|
+
* in the RSC tree see the updated params via getSnapshot(), but
|
|
75
|
+
* preserved layout components don't re-render prematurely with
|
|
76
|
+
* {old tree, new params}.
|
|
77
|
+
*
|
|
78
|
+
* After the React render commits, the router calls notifyParamsListeners()
|
|
79
|
+
* to trigger re-renders in preserved layouts that read useParams().
|
|
77
80
|
*
|
|
78
81
|
* On the client, the segment router calls this on each navigation.
|
|
79
82
|
* During SSR, params are also available via getSsrData().params
|
|
80
83
|
* (ALS-backed), but setCurrentParams is still called for the
|
|
81
84
|
* module-level fallback path.
|
|
82
|
-
*
|
|
83
|
-
* After mutation, all useSyncExternalStore subscribers are notified
|
|
84
|
-
* so that every mounted useParams() consumer re-renders in the same
|
|
85
|
-
* React commit — even components in unchanged layouts.
|
|
86
85
|
*/
|
|
87
86
|
export function setCurrentParams(params: Record<string, string | string[]>): void {
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
_setCurrentParams(params);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Notify all useSyncExternalStore subscribers that params have changed.
|
|
92
|
+
* Called by the router AFTER renderPayload() so that preserved layout
|
|
93
|
+
* components re-render only after the new tree is committed — producing
|
|
94
|
+
* an atomic {new tree, new params} update instead of a stale
|
|
95
|
+
* {old tree, new params} intermediate state.
|
|
96
|
+
*/
|
|
97
|
+
export function notifyParamsListeners(): void {
|
|
98
|
+
for (const listener of paramsListeners) {
|
|
90
99
|
listener();
|
|
91
100
|
}
|
|
92
101
|
}
|
|
@@ -112,24 +121,25 @@ export function setCurrentParams(params: Record<string, string | string[]>): voi
|
|
|
112
121
|
export function useParams<R extends keyof Routes>(route: R): Routes[R]['params'];
|
|
113
122
|
export function useParams(route?: string): Record<string, string | string[]>;
|
|
114
123
|
export function useParams(_route?: string): Record<string, string | string[]> {
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// useSyncExternalStore
|
|
124
|
-
//
|
|
125
|
-
// tests or setup code), fall back to reading the snapshot directly.
|
|
126
|
-
// This mirrors React's own behavior — hooks only work during rendering.
|
|
124
|
+
// useSyncExternalStore handles both client and SSR:
|
|
125
|
+
// - Client: calls getSnapshot() → reads currentParams from state.ts
|
|
126
|
+
// - SSR: calls getServerSnapshot() → reads from ALS-backed getSsrData()
|
|
127
|
+
//
|
|
128
|
+
// We must always call the hook (Rules of Hooks — no conditional hook calls).
|
|
129
|
+
// React picks the right snapshot function based on the environment.
|
|
130
|
+
//
|
|
131
|
+
// When called outside a React component (e.g., in test assertions),
|
|
132
|
+
// useSyncExternalStore throws because there's no dispatcher. In that case,
|
|
133
|
+
// fall back to reading the snapshot directly.
|
|
127
134
|
try {
|
|
128
135
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
129
136
|
} catch {
|
|
130
|
-
// No React dispatcher available — return the snapshot
|
|
137
|
+
// No React dispatcher available — return the best available snapshot.
|
|
131
138
|
// This path is hit when useParams() is called outside a component,
|
|
132
139
|
// e.g. in test assertions that verify the current params value.
|
|
133
|
-
|
|
140
|
+
// Use getServerSnapshot() because it checks the ALS-backed SSR context
|
|
141
|
+
// first (request-isolated), falling back to module-level currentParams
|
|
142
|
+
// only when no SSR context exists (client-side / tests).
|
|
143
|
+
return getServerSnapshot();
|
|
134
144
|
}
|
|
135
145
|
}
|
|
@@ -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';
|