@timber-js/app 0.1.0 → 0.1.2
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/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/package.json +43 -58
- package/src/adapters/cloudflare.ts +325 -0
- package/src/adapters/nitro.ts +366 -0
- package/src/adapters/types.ts +63 -0
- package/src/cache/index.ts +91 -0
- package/src/cache/redis-handler.ts +91 -0
- package/src/cache/register-cached-function.ts +99 -0
- package/src/cache/singleflight.ts +26 -0
- package/src/cache/stable-stringify.ts +21 -0
- package/src/cache/timber-cache.ts +116 -0
- package/src/cli.ts +201 -0
- package/src/client/browser-entry.ts +663 -0
- package/src/client/error-boundary.tsx +209 -0
- package/src/client/form.tsx +200 -0
- package/src/client/head.ts +61 -0
- package/src/client/history.ts +46 -0
- package/src/client/index.ts +60 -0
- package/src/client/link-navigate-interceptor.tsx +62 -0
- package/src/client/link-status-provider.tsx +40 -0
- package/src/client/link.tsx +310 -0
- package/src/client/nuqs-adapter.tsx +117 -0
- package/src/client/router-ref.ts +25 -0
- package/src/client/router.ts +563 -0
- package/src/client/segment-cache.ts +194 -0
- package/src/client/segment-context.ts +57 -0
- package/src/client/ssr-data.ts +95 -0
- package/src/client/types.ts +4 -0
- package/src/client/unload-guard.ts +34 -0
- package/src/client/use-cookie.ts +122 -0
- package/src/client/use-link-status.ts +46 -0
- package/src/client/use-navigation-pending.ts +47 -0
- package/src/client/use-params.ts +71 -0
- package/src/client/use-pathname.ts +43 -0
- package/src/client/use-query-states.ts +133 -0
- package/src/client/use-router.ts +77 -0
- package/src/client/use-search-params.ts +74 -0
- package/src/client/use-selected-layout-segment.ts +110 -0
- package/src/content/index.ts +13 -0
- package/src/cookies/define-cookie.ts +137 -0
- package/src/cookies/index.ts +9 -0
- package/src/fonts/ast.ts +359 -0
- package/src/fonts/css.ts +68 -0
- package/src/fonts/fallbacks.ts +248 -0
- package/src/fonts/google.ts +332 -0
- package/src/fonts/local.ts +177 -0
- package/src/fonts/types.ts +88 -0
- package/src/index.ts +413 -0
- package/src/plugins/adapter-build.ts +118 -0
- package/src/plugins/build-manifest.ts +323 -0
- package/src/plugins/build-report.ts +353 -0
- package/src/plugins/cache-transform.ts +199 -0
- package/src/plugins/chunks.ts +90 -0
- package/src/plugins/content.ts +136 -0
- package/src/plugins/dev-error-overlay.ts +230 -0
- package/src/plugins/dev-logs.ts +280 -0
- package/src/plugins/dev-server.ts +389 -0
- package/src/plugins/dynamic-transform.ts +161 -0
- package/src/plugins/entries.ts +207 -0
- package/src/plugins/fonts.ts +581 -0
- package/src/plugins/mdx.ts +179 -0
- package/src/plugins/react-prod.ts +56 -0
- package/src/plugins/routing.ts +419 -0
- package/src/plugins/server-action-exports.ts +220 -0
- package/src/plugins/server-bundle.ts +113 -0
- package/src/plugins/shims.ts +168 -0
- package/src/plugins/static-build.ts +207 -0
- package/src/routing/codegen.ts +396 -0
- package/src/routing/index.ts +14 -0
- package/src/routing/interception.ts +173 -0
- package/src/routing/scanner.ts +487 -0
- package/src/routing/status-file-lint.ts +114 -0
- package/src/routing/types.ts +100 -0
- package/src/search-params/analyze.ts +192 -0
- package/src/search-params/codecs.ts +153 -0
- package/src/search-params/create.ts +314 -0
- package/src/search-params/index.ts +23 -0
- package/src/search-params/registry.ts +31 -0
- package/src/server/access-gate.tsx +142 -0
- package/src/server/action-client.ts +473 -0
- package/src/server/action-handler.ts +325 -0
- package/src/server/actions.ts +236 -0
- package/src/server/asset-headers.ts +81 -0
- package/src/server/body-limits.ts +102 -0
- package/src/server/build-manifest.ts +234 -0
- package/src/server/canonicalize.ts +90 -0
- package/src/server/client-module-map.ts +58 -0
- package/src/server/csrf.ts +79 -0
- package/src/server/deny-renderer.ts +302 -0
- package/src/server/dev-logger.ts +419 -0
- package/src/server/dev-span-processor.ts +78 -0
- package/src/server/dev-warnings.ts +282 -0
- package/src/server/early-hints-sender.ts +55 -0
- package/src/server/early-hints.ts +142 -0
- package/src/server/error-boundary-wrapper.ts +69 -0
- package/src/server/error-formatter.ts +184 -0
- package/src/server/flush.ts +182 -0
- package/src/server/form-data.ts +176 -0
- package/src/server/form-flash.ts +93 -0
- package/src/server/html-injectors.ts +445 -0
- package/src/server/index.ts +222 -0
- package/src/server/instrumentation.ts +136 -0
- package/src/server/logger.ts +145 -0
- package/src/server/manifest-status-resolver.ts +215 -0
- package/src/server/metadata-render.ts +527 -0
- package/src/server/metadata-routes.ts +189 -0
- package/src/server/metadata.ts +263 -0
- package/src/server/middleware-runner.ts +32 -0
- package/src/server/nuqs-ssr-provider.tsx +63 -0
- package/src/server/pipeline.ts +555 -0
- package/src/server/prerender.ts +139 -0
- package/src/server/primitives.ts +264 -0
- package/src/server/proxy.ts +43 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/route-element-builder.ts +395 -0
- package/src/server/route-handler.ts +153 -0
- package/src/server/route-matcher.ts +316 -0
- package/src/server/rsc-entry/api-handler.ts +112 -0
- package/src/server/rsc-entry/error-renderer.ts +177 -0
- package/src/server/rsc-entry/helpers.ts +147 -0
- package/src/server/rsc-entry/index.ts +688 -0
- package/src/server/rsc-entry/ssr-bridge.ts +18 -0
- package/src/server/slot-resolver.ts +359 -0
- package/src/server/ssr-entry.ts +161 -0
- package/src/server/ssr-render.ts +200 -0
- package/src/server/status-code-resolver.ts +282 -0
- package/src/server/tracing.ts +281 -0
- package/src/server/tree-builder.ts +354 -0
- package/src/server/types.ts +150 -0
- package/src/shims/font-google.ts +67 -0
- package/src/shims/headers.ts +11 -0
- package/src/shims/image.ts +48 -0
- package/src/shims/link.ts +9 -0
- package/src/shims/navigation-client.ts +52 -0
- package/src/shims/navigation.ts +31 -0
- package/src/shims/server-only-noop.js +5 -0
- package/src/utils/directive-parser.ts +529 -0
- package/src/utils/format.ts +10 -0
- package/src/utils/startup-timer.ts +102 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata resolution for timber.js.
|
|
3
|
+
*
|
|
4
|
+
* Resolves metadata from a segment chain (layouts + page), applies title
|
|
5
|
+
* templates, shallow-merges entries, and produces head element descriptors.
|
|
6
|
+
*
|
|
7
|
+
* Resolution happens inside the render pass — React.cache is active,
|
|
8
|
+
* metadata is outside Suspense, and the flush point guarantees completeness.
|
|
9
|
+
*
|
|
10
|
+
* Rendering (Metadata → HeadElement[]) is in metadata-render.ts.
|
|
11
|
+
*
|
|
12
|
+
* See design/16-metadata.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Metadata } from './types.js';
|
|
16
|
+
|
|
17
|
+
// Re-export renderMetadataToElements from the rendering module so existing
|
|
18
|
+
// consumers (route-element-builder, tests) can keep importing from here.
|
|
19
|
+
export { renderMetadataToElements } from './metadata-render.js';
|
|
20
|
+
|
|
21
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** A single metadata entry from a layout or page module. */
|
|
24
|
+
export interface SegmentMetadataEntry {
|
|
25
|
+
/** The resolved metadata object (from static or async `metadata` export). */
|
|
26
|
+
metadata: Metadata;
|
|
27
|
+
/** Whether this entry is from the page (leaf) module. */
|
|
28
|
+
isPage: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Options for resolveMetadata. */
|
|
32
|
+
export interface ResolveMetadataOptions {
|
|
33
|
+
/**
|
|
34
|
+
* When true, the page's metadata is discarded (simulating a render error)
|
|
35
|
+
* and `<meta name="robots" content="noindex">` is injected.
|
|
36
|
+
*/
|
|
37
|
+
errorState?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A rendered head element descriptor. */
|
|
41
|
+
export interface HeadElement {
|
|
42
|
+
tag: 'title' | 'meta' | 'link';
|
|
43
|
+
content?: string;
|
|
44
|
+
attrs?: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Title Resolution ────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a title value with an optional template.
|
|
51
|
+
*
|
|
52
|
+
* - string → apply template if present
|
|
53
|
+
* - { absolute: '...' } → use as-is, skip template
|
|
54
|
+
* - { default: '...' } → use as fallback (no template applied)
|
|
55
|
+
* - undefined → undefined
|
|
56
|
+
*/
|
|
57
|
+
export function resolveTitle(
|
|
58
|
+
title: Metadata['title'],
|
|
59
|
+
template: string | undefined
|
|
60
|
+
): string | undefined {
|
|
61
|
+
if (title === undefined || title === null) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof title === 'string') {
|
|
66
|
+
return template ? template.replace('%s', title) : title;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Object form
|
|
70
|
+
if (title.absolute !== undefined) {
|
|
71
|
+
return title.absolute;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (title.default !== undefined) {
|
|
75
|
+
return title.default;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Metadata Resolution ─────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve metadata from a segment chain.
|
|
85
|
+
*
|
|
86
|
+
* Processes entries from root layout to page (in segment order).
|
|
87
|
+
* The merge algorithm:
|
|
88
|
+
* 1. Shallow-merge all keys except title (later wins)
|
|
89
|
+
* 2. Track the most recent title template
|
|
90
|
+
* 3. Resolve the final title using the template
|
|
91
|
+
*
|
|
92
|
+
* In error state, the page entry is dropped and noindex is injected.
|
|
93
|
+
*
|
|
94
|
+
* See design/16-metadata.md §"Merge Algorithm"
|
|
95
|
+
*/
|
|
96
|
+
export function resolveMetadata(
|
|
97
|
+
entries: SegmentMetadataEntry[],
|
|
98
|
+
options: ResolveMetadataOptions = {}
|
|
99
|
+
): Metadata {
|
|
100
|
+
const { errorState = false } = options;
|
|
101
|
+
|
|
102
|
+
const merged: Metadata = {};
|
|
103
|
+
let titleTemplate: string | undefined;
|
|
104
|
+
let lastDefault: string | undefined;
|
|
105
|
+
let rawTitle: Metadata['title'];
|
|
106
|
+
|
|
107
|
+
for (const { metadata, isPage } of entries) {
|
|
108
|
+
// In error state, skip the page's metadata entirely
|
|
109
|
+
if (errorState && isPage) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Track title template
|
|
114
|
+
if (metadata.title !== undefined && typeof metadata.title === 'object') {
|
|
115
|
+
if (metadata.title.template !== undefined) {
|
|
116
|
+
titleTemplate = metadata.title.template;
|
|
117
|
+
}
|
|
118
|
+
if (metadata.title.default !== undefined) {
|
|
119
|
+
lastDefault = metadata.title.default;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Shallow-merge all keys except title
|
|
124
|
+
for (const key of Object.keys(metadata) as Array<keyof Metadata>) {
|
|
125
|
+
if (key === 'title') continue;
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
+
(merged as any)[key] = metadata[key];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Track raw title (will be resolved after the loop)
|
|
131
|
+
if (metadata.title !== undefined) {
|
|
132
|
+
rawTitle = metadata.title;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// In error state, we lost page title — use the most recent default
|
|
137
|
+
if (errorState) {
|
|
138
|
+
rawTitle = lastDefault !== undefined ? { default: lastDefault } : rawTitle;
|
|
139
|
+
// Don't apply template in error state
|
|
140
|
+
titleTemplate = undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Resolve the final title
|
|
144
|
+
const resolvedTitle = resolveTitle(rawTitle, titleTemplate);
|
|
145
|
+
if (resolvedTitle !== undefined) {
|
|
146
|
+
merged.title = resolvedTitle;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Error state: inject noindex, overriding any user robots
|
|
150
|
+
if (errorState) {
|
|
151
|
+
merged.robots = 'noindex';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return merged;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── URL Resolution ──────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a string is an absolute URL.
|
|
161
|
+
*/
|
|
162
|
+
function isAbsoluteUrl(url: string): boolean {
|
|
163
|
+
return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolve a relative URL against a base URL.
|
|
168
|
+
*/
|
|
169
|
+
function resolveUrl(url: string, base: URL): string {
|
|
170
|
+
if (isAbsoluteUrl(url)) return url;
|
|
171
|
+
return new URL(url, base).toString();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve relative URLs in metadata fields against metadataBase.
|
|
176
|
+
*
|
|
177
|
+
* Returns a new metadata object with URLs resolved. Absolute URLs are not modified.
|
|
178
|
+
* If metadataBase is not set, returns the metadata unchanged.
|
|
179
|
+
*/
|
|
180
|
+
export function resolveMetadataUrls(metadata: Metadata): Metadata {
|
|
181
|
+
const base = metadata.metadataBase;
|
|
182
|
+
if (!base) return metadata;
|
|
183
|
+
|
|
184
|
+
const result = { ...metadata };
|
|
185
|
+
|
|
186
|
+
// Resolve openGraph images
|
|
187
|
+
if (result.openGraph) {
|
|
188
|
+
result.openGraph = { ...result.openGraph };
|
|
189
|
+
if (typeof result.openGraph.images === 'string') {
|
|
190
|
+
result.openGraph.images = resolveUrl(result.openGraph.images, base);
|
|
191
|
+
} else if (Array.isArray(result.openGraph.images)) {
|
|
192
|
+
result.openGraph.images = result.openGraph.images.map((img) => ({
|
|
193
|
+
...img,
|
|
194
|
+
url: resolveUrl(img.url, base),
|
|
195
|
+
}));
|
|
196
|
+
} else if (result.openGraph.images) {
|
|
197
|
+
// Single object: { url, width?, height?, alt? }
|
|
198
|
+
result.openGraph.images = {
|
|
199
|
+
...result.openGraph.images,
|
|
200
|
+
url: resolveUrl(result.openGraph.images.url, base),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (result.openGraph.url && !isAbsoluteUrl(result.openGraph.url)) {
|
|
204
|
+
result.openGraph.url = resolveUrl(result.openGraph.url, base);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Resolve twitter images
|
|
209
|
+
if (result.twitter) {
|
|
210
|
+
result.twitter = { ...result.twitter };
|
|
211
|
+
if (typeof result.twitter.images === 'string') {
|
|
212
|
+
result.twitter.images = resolveUrl(result.twitter.images, base);
|
|
213
|
+
} else if (Array.isArray(result.twitter.images)) {
|
|
214
|
+
// Resolve each image URL, preserving the union type structure
|
|
215
|
+
const resolved = result.twitter.images.map((img) =>
|
|
216
|
+
typeof img === 'string' ? resolveUrl(img, base) : { ...img, url: resolveUrl(img.url, base) }
|
|
217
|
+
);
|
|
218
|
+
// If all entries are strings, assign as string[]; otherwise as object[]
|
|
219
|
+
const allStrings = resolved.every((r) => typeof r === 'string');
|
|
220
|
+
result.twitter.images = allStrings
|
|
221
|
+
? (resolved as string[])
|
|
222
|
+
: (resolved as Array<{ url: string; alt?: string; width?: number; height?: number }>);
|
|
223
|
+
} else if (result.twitter.images) {
|
|
224
|
+
// Single object: { url, alt?, width?, height? }
|
|
225
|
+
result.twitter.images = {
|
|
226
|
+
...result.twitter.images,
|
|
227
|
+
url: resolveUrl(result.twitter.images.url, base),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Resolve alternates
|
|
233
|
+
if (result.alternates) {
|
|
234
|
+
result.alternates = { ...result.alternates };
|
|
235
|
+
if (result.alternates.canonical && !isAbsoluteUrl(result.alternates.canonical)) {
|
|
236
|
+
result.alternates.canonical = resolveUrl(result.alternates.canonical, base);
|
|
237
|
+
}
|
|
238
|
+
if (result.alternates.languages) {
|
|
239
|
+
const langs: Record<string, string> = {};
|
|
240
|
+
for (const [lang, url] of Object.entries(result.alternates.languages)) {
|
|
241
|
+
langs[lang] = isAbsoluteUrl(url) ? url : resolveUrl(url, base);
|
|
242
|
+
}
|
|
243
|
+
result.alternates.languages = langs;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Resolve icon URLs
|
|
248
|
+
if (result.icons) {
|
|
249
|
+
result.icons = { ...result.icons };
|
|
250
|
+
if (typeof result.icons.icon === 'string') {
|
|
251
|
+
result.icons.icon = resolveUrl(result.icons.icon, base);
|
|
252
|
+
} else if (Array.isArray(result.icons.icon)) {
|
|
253
|
+
result.icons.icon = result.icons.icon.map((i) => ({ ...i, url: resolveUrl(i.url, base) }));
|
|
254
|
+
}
|
|
255
|
+
if (typeof result.icons.apple === 'string') {
|
|
256
|
+
result.icons.apple = resolveUrl(result.icons.apple, base);
|
|
257
|
+
} else if (Array.isArray(result.icons.apple)) {
|
|
258
|
+
result.icons.apple = result.icons.apple.map((i) => ({ ...i, url: resolveUrl(i.url, base) }));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware runner — executes a route's middleware.ts before rendering.
|
|
3
|
+
*
|
|
4
|
+
* Only the leaf route's middleware runs. There is no middleware chain.
|
|
5
|
+
* Middleware does NOT have next() — it either short-circuits with a Response
|
|
6
|
+
* or returns void to continue to access checks + render.
|
|
7
|
+
*
|
|
8
|
+
* See design/07-routing.md §"middleware.ts"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MiddlewareContext } from './types.js';
|
|
12
|
+
|
|
13
|
+
/** Signature of a middleware.ts default export. */
|
|
14
|
+
export type MiddlewareFn = (ctx: MiddlewareContext) => Response | void | Promise<Response | void>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run a route's middleware function.
|
|
18
|
+
*
|
|
19
|
+
* @param middlewareFn - The default export from the route's middleware.ts
|
|
20
|
+
* @param ctx - The middleware context (req, params, headers, requestHeaders, searchParams)
|
|
21
|
+
* @returns A Response if middleware short-circuited, or undefined to continue
|
|
22
|
+
*/
|
|
23
|
+
export async function runMiddleware(
|
|
24
|
+
middlewareFn: MiddlewareFn,
|
|
25
|
+
ctx: MiddlewareContext
|
|
26
|
+
): Promise<Response | undefined> {
|
|
27
|
+
const result = await middlewareFn(ctx);
|
|
28
|
+
if (result instanceof Response) {
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side nuqs adapter provider for SSR rendering.
|
|
3
|
+
*
|
|
4
|
+
* During SSR, `'use client'` components that call nuqs hooks (useQueryStates,
|
|
5
|
+
* useQueryState) need the nuqs adapter context. The client adapter
|
|
6
|
+
* (TimberNuqsAdapter) relies on `window` and React hooks that are
|
|
7
|
+
* client-only. This provider supplies a static, SSR-safe adapter
|
|
8
|
+
* that feeds the current request's search params into the nuqs context.
|
|
9
|
+
*
|
|
10
|
+
* The returned component wraps the React tree so that nuqs hooks render
|
|
11
|
+
* the correct initial values during server-side rendering. On the client,
|
|
12
|
+
* TimberNuqsAdapter (injected by browser-entry.ts) takes over.
|
|
13
|
+
*
|
|
14
|
+
* Design doc: design/23-search-params.md §"Custom Adapter"
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createElement, type ReactNode } from 'react';
|
|
18
|
+
import {
|
|
19
|
+
unstable_createAdapterProvider as createAdapterProvider,
|
|
20
|
+
type unstable_AdapterInterface as AdapterInterface,
|
|
21
|
+
} from 'nuqs/adapters/custom';
|
|
22
|
+
|
|
23
|
+
// ─── SSR Adapter ──────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a nuqs adapter provider for SSR that serves a static snapshot
|
|
27
|
+
* of search params. The `updateUrl` is a no-op because URL updates
|
|
28
|
+
* cannot happen on the server.
|
|
29
|
+
*/
|
|
30
|
+
function makeNuqsSsrAdapter(searchParams: URLSearchParams) {
|
|
31
|
+
function useNuqsSsrAdapter(_watchKeys: string[]): AdapterInterface {
|
|
32
|
+
return {
|
|
33
|
+
searchParams,
|
|
34
|
+
updateUrl: () => {},
|
|
35
|
+
getSearchParamsSnapshot: () => searchParams,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return createAdapterProvider(useNuqsSsrAdapter);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Provider Component ───────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Wrap the SSR element tree with a nuqs adapter context.
|
|
46
|
+
*
|
|
47
|
+
* Called by ssr-entry.ts before passing the element to renderSsrStream.
|
|
48
|
+
* Takes the NavContext search params and provides them to nuqs hooks
|
|
49
|
+
* running during SSR so they render with the correct initial values.
|
|
50
|
+
*
|
|
51
|
+
* @param searchParamsRecord - The request's search params as a plain record
|
|
52
|
+
* @param children - The React element tree to wrap
|
|
53
|
+
*/
|
|
54
|
+
export function withNuqsSsrAdapter(
|
|
55
|
+
searchParamsRecord: Record<string, string>,
|
|
56
|
+
children: ReactNode
|
|
57
|
+
): ReactNode {
|
|
58
|
+
const searchParams = new URLSearchParams(searchParamsRecord);
|
|
59
|
+
const Provider = makeNuqsSsrAdapter(searchParams);
|
|
60
|
+
// AdapterProvider types require children in the props object (not as 3rd arg)
|
|
61
|
+
// eslint-disable-next-line react/no-children-prop
|
|
62
|
+
return createElement(Provider, { defaultOptions: { shallow: false, scroll: true }, children });
|
|
63
|
+
}
|