@timber-js/app 0.1.8 → 0.1.10
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/registry-BfPM41ri.js +20 -0
- package/dist/_chunks/registry-BfPM41ri.js.map +1 -0
- package/dist/client/index.js +83 -1
- package/dist/client/index.js.map +1 -1
- package/dist/search-params/create.d.ts.map +1 -1
- package/dist/search-params/index.js +4 -13
- package/dist/search-params/index.js.map +1 -1
- package/dist/server/index.js +0 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/search-params/create.ts +8 -12
- package/src/server/error-formatter.ts +0 -12
- package/src/server/route-matcher.ts +62 -24
- package/src/server/rsc-entry/index.ts +0 -24
- package/dist/_chunks/use-query-states-Dd9PVu-L.js +0 -102
- package/dist/_chunks/use-query-states-Dd9PVu-L.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/server/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAM9B,6DAA6D;AAC7D,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EACP,QAAQ,GACR,SAAS,GACT,WAAW,GACX,oBAAoB,GACpB,OAAO,GACP,MAAM,GACN,cAAc,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC3D,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAE9C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC5C;AAED,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAEzC;
|
|
1
|
+
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/server/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAM9B,6DAA6D;AAC7D,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EACP,QAAQ,GACR,SAAS,GACT,WAAW,GACX,oBAAoB,GACpB,OAAO,GACP,MAAM,GACN,cAAc,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC3D,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAE9C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC5C;AAED,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAEzC;AA4MD,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,IAAI,EAAE,iBAAiB,CAAC;IACxB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,CAAC;IACnB,0DAA0D;IAC1D,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,kBAAkB,GAAG,IAAI,CAMjD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA2EA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA2EA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AA0nBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;8BAtgBpD,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAwgBhD,wBAAiE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
* Design doc: design/09-typescript.md §"Typed searchParams — search-params.ts"
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useQueryStates as clientUseQueryStates } from '#/client/use-query-states.js';
|
|
12
|
-
|
|
13
11
|
// ---------------------------------------------------------------------------
|
|
14
12
|
// Types
|
|
15
13
|
// ---------------------------------------------------------------------------
|
|
@@ -290,16 +288,14 @@ function buildDefinition<T extends Record<string, unknown>>(
|
|
|
290
288
|
}
|
|
291
289
|
|
|
292
290
|
// ---- useQueryStates ----
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
function useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>] {
|
|
302
|
-
return clientUseQueryStates(codecMap, options, Object.freeze({ ...urlKeys })) as [T, SetParams<T>];
|
|
291
|
+
// This is a placeholder that will be replaced by the client runtime.
|
|
292
|
+
// At import time in a server context, calling this throws.
|
|
293
|
+
// The actual implementation wraps nuqs and lives in @timber-js/app/client.
|
|
294
|
+
function useQueryStates(_options?: QueryStatesOptions): [T, SetParams<T>] {
|
|
295
|
+
throw new Error(
|
|
296
|
+
'useQueryStates() can only be called in a client component. ' +
|
|
297
|
+
'Import from @timber-js/app/client instead.'
|
|
298
|
+
);
|
|
303
299
|
}
|
|
304
300
|
|
|
305
301
|
const definition: SearchParamsDefinition<T> = {
|
|
@@ -147,18 +147,6 @@ function extractErrorHint(message: string): string | null {
|
|
|
147
147
|
return 'A component resolved to undefined/null — check default exports and import paths';
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
// "Invalid hook call" — hooks called outside React's render context.
|
|
151
|
-
// In RSC, this typically means a 'use client' component was executed as a
|
|
152
|
-
// server component instead of being serialized as a client reference.
|
|
153
|
-
if (message.includes('Invalid hook call')) {
|
|
154
|
-
return (
|
|
155
|
-
'A hook was called outside of a React component render. ' +
|
|
156
|
-
'If this is a \'use client\' component, ensure the directive is at the very top of the file ' +
|
|
157
|
-
'(before any imports) and that @vitejs/plugin-rsc is loaded correctly. ' +
|
|
158
|
-
'Barrel re-exports from non-\'use client\' files do not propagate the directive.'
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
150
|
return null;
|
|
163
151
|
}
|
|
164
152
|
|
|
@@ -127,6 +127,43 @@ function matchPathname(root: ManifestSegmentNode, pathname: string): RouteMatch
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* An effective child flattened through group segments.
|
|
132
|
+
* Includes the chain of group nodes that must be added to the segments
|
|
133
|
+
* array before the child itself (to preserve the group → child nesting
|
|
134
|
+
* that the renderer expects).
|
|
135
|
+
*/
|
|
136
|
+
interface EffectiveChild {
|
|
137
|
+
child: ManifestSegmentNode;
|
|
138
|
+
groupChain: ManifestSegmentNode[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Collect effective children by flattening through group segments.
|
|
143
|
+
*
|
|
144
|
+
* Groups are transparent for URL matching — their non-group descendants
|
|
145
|
+
* are returned with the chain of group nodes that lead to them. This
|
|
146
|
+
* allows the caller to apply priority ordering (static > dynamic > ...)
|
|
147
|
+
* across all groups uniformly instead of per-group.
|
|
148
|
+
*/
|
|
149
|
+
function collectEffectiveChildren(
|
|
150
|
+
node: ManifestSegmentNode,
|
|
151
|
+
groupChain: ManifestSegmentNode[] = []
|
|
152
|
+
): EffectiveChild[] {
|
|
153
|
+
const result: EffectiveChild[] = [];
|
|
154
|
+
for (const child of node.children) {
|
|
155
|
+
if (child.segmentType === 'group') {
|
|
156
|
+
// Look through the group — its children become effective children
|
|
157
|
+
// with this group prepended to their chain
|
|
158
|
+
const nested = collectEffectiveChildren(child, [...groupChain, child]);
|
|
159
|
+
result.push(...nested);
|
|
160
|
+
} else {
|
|
161
|
+
result.push({ child, groupChain });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
130
167
|
/**
|
|
131
168
|
* Recursively match URL segments against the segment tree.
|
|
132
169
|
*
|
|
@@ -136,8 +173,10 @@ function matchPathname(root: ManifestSegmentNode, pathname: string): RouteMatch
|
|
|
136
173
|
* 3. Catch-all segments ([...param])
|
|
137
174
|
* 4. Optional catch-all segments ([[...param]])
|
|
138
175
|
*
|
|
139
|
-
* Groups are transparent — they don't consume URL segments
|
|
140
|
-
*
|
|
176
|
+
* Groups are transparent — they don't consume URL segments. Children
|
|
177
|
+
* are flattened through groups so that priority ordering applies across
|
|
178
|
+
* all groups uniformly (a static in group A always beats a dynamic in
|
|
179
|
+
* group B, regardless of group ordering).
|
|
141
180
|
*/
|
|
142
181
|
function matchSegments(
|
|
143
182
|
node: ManifestSegmentNode,
|
|
@@ -163,11 +202,12 @@ function matchSegments(
|
|
|
163
202
|
}
|
|
164
203
|
}
|
|
165
204
|
|
|
166
|
-
// Check optional catch-all children (
|
|
167
|
-
|
|
205
|
+
// Check optional catch-all children (direct and through groups)
|
|
206
|
+
const effective = collectEffectiveChildren(node);
|
|
207
|
+
for (const { child, groupChain } of effective) {
|
|
168
208
|
if (child.segmentType === 'optional-catch-all') {
|
|
169
209
|
if (child.page || child.route) {
|
|
170
|
-
segments.push(child);
|
|
210
|
+
segments.push(...groupChain, child);
|
|
171
211
|
// Zero segments → param is undefined (not set), matching Next.js semantics
|
|
172
212
|
return true;
|
|
173
213
|
}
|
|
@@ -180,35 +220,33 @@ function matchSegments(
|
|
|
180
220
|
|
|
181
221
|
const part = parts[index];
|
|
182
222
|
|
|
183
|
-
//
|
|
223
|
+
// Flatten children through groups so priority ordering applies globally
|
|
224
|
+
// across all groups, not per-group.
|
|
225
|
+
const effective = collectEffectiveChildren(node);
|
|
184
226
|
|
|
185
227
|
// 1. Static segments
|
|
186
|
-
for (const child of
|
|
228
|
+
for (const { child, groupChain } of effective) {
|
|
187
229
|
if (child.segmentType === 'static' && child.segmentName === part) {
|
|
230
|
+
segments.push(...groupChain);
|
|
188
231
|
if (matchSegments(child, parts, index + 1, segments, params)) {
|
|
189
232
|
return true;
|
|
190
233
|
}
|
|
234
|
+
// Backtrack group chain
|
|
235
|
+
segments.length -= groupChain.length;
|
|
191
236
|
}
|
|
192
237
|
}
|
|
193
238
|
|
|
194
|
-
// 2.
|
|
195
|
-
for (const child of
|
|
196
|
-
if (child.segmentType === 'group') {
|
|
197
|
-
if (matchSegments(child, parts, index, segments, params)) {
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// 3. Dynamic segments ([param])
|
|
204
|
-
for (const child of node.children) {
|
|
239
|
+
// 2. Dynamic segments ([param])
|
|
240
|
+
for (const { child, groupChain } of effective) {
|
|
205
241
|
if (child.segmentType === 'dynamic' && child.paramName) {
|
|
242
|
+
segments.push(...groupChain);
|
|
206
243
|
const prevParam = params[child.paramName];
|
|
207
244
|
params[child.paramName] = part;
|
|
208
245
|
if (matchSegments(child, parts, index + 1, segments, params)) {
|
|
209
246
|
return true;
|
|
210
247
|
}
|
|
211
248
|
// Backtrack
|
|
249
|
+
segments.length -= groupChain.length;
|
|
212
250
|
if (prevParam !== undefined) {
|
|
213
251
|
params[child.paramName] = prevParam;
|
|
214
252
|
} else {
|
|
@@ -217,24 +255,24 @@ function matchSegments(
|
|
|
217
255
|
}
|
|
218
256
|
}
|
|
219
257
|
|
|
220
|
-
//
|
|
221
|
-
for (const child of
|
|
258
|
+
// 3. Catch-all segments ([...param])
|
|
259
|
+
for (const { child, groupChain } of effective) {
|
|
222
260
|
if (child.segmentType === 'catch-all' && child.paramName) {
|
|
223
261
|
if (child.page || child.route) {
|
|
224
262
|
const remaining = parts.slice(index);
|
|
225
|
-
segments.push(child);
|
|
263
|
+
segments.push(...groupChain, child);
|
|
226
264
|
params[child.paramName] = remaining;
|
|
227
265
|
return true;
|
|
228
266
|
}
|
|
229
267
|
}
|
|
230
268
|
}
|
|
231
269
|
|
|
232
|
-
//
|
|
233
|
-
for (const child of
|
|
270
|
+
// 4. Optional catch-all segments ([[...param]])
|
|
271
|
+
for (const { child, groupChain } of effective) {
|
|
234
272
|
if (child.segmentType === 'optional-catch-all' && child.paramName) {
|
|
235
273
|
if (child.page || child.route) {
|
|
236
274
|
const remaining = parts.slice(index);
|
|
237
|
-
segments.push(child);
|
|
275
|
+
segments.push(...groupChain, child);
|
|
238
276
|
params[child.paramName] = remaining;
|
|
239
277
|
return true;
|
|
240
278
|
}
|
|
@@ -409,30 +409,6 @@ async function renderRoute(
|
|
|
409
409
|
status: error.status,
|
|
410
410
|
});
|
|
411
411
|
}
|
|
412
|
-
// Dev diagnostic: detect "Invalid hook call" errors which indicate
|
|
413
|
-
// a 'use client' component is being executed during RSC rendering
|
|
414
|
-
// instead of being serialized as a client reference. This happens when
|
|
415
|
-
// the RSC plugin's transform doesn't detect the directive — e.g., the
|
|
416
|
-
// directive isn't at the very top of the file, or the component is
|
|
417
|
-
// re-exported through a barrel file without 'use client'.
|
|
418
|
-
// See LOCAL-297.
|
|
419
|
-
if (
|
|
420
|
-
process.env.NODE_ENV !== 'production' &&
|
|
421
|
-
error instanceof Error &&
|
|
422
|
-
error.message.includes('Invalid hook call')
|
|
423
|
-
) {
|
|
424
|
-
console.error(
|
|
425
|
-
'[timber] A React hook was called during RSC rendering. This usually means a ' +
|
|
426
|
-
"'use client' component is being executed as a server component instead of " +
|
|
427
|
-
'being serialized as a client reference.\n\n' +
|
|
428
|
-
'Common causes:\n' +
|
|
429
|
-
" 1. The 'use client' directive is not the FIRST statement in the file (before any imports)\n" +
|
|
430
|
-
" 2. The component is re-exported through a barrel file (index.ts) that lacks 'use client'\n" +
|
|
431
|
-
' 3. @vitejs/plugin-rsc is not loaded or is misconfigured\n\n' +
|
|
432
|
-
`Request: ${_req.method} ${new URL(_req.url).pathname}`
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
412
|
// Track unhandled errors for pre-flush handling (500 status)
|
|
437
413
|
if (!renderError) {
|
|
438
414
|
renderError = { error, status: 500 };
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { useQueryStates } from "nuqs";
|
|
2
|
-
//#region src/search-params/registry.ts
|
|
3
|
-
var registry = /* @__PURE__ */ new Map();
|
|
4
|
-
/**
|
|
5
|
-
* Register a route's search params definition.
|
|
6
|
-
* Called by the generated route manifest loader when a route's modules load.
|
|
7
|
-
*/
|
|
8
|
-
function registerSearchParams(route, definition) {
|
|
9
|
-
registry.set(route, definition);
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Look up a route's search params definition.
|
|
13
|
-
* Returns undefined if the route hasn't been loaded yet.
|
|
14
|
-
*/
|
|
15
|
-
function getSearchParams(route) {
|
|
16
|
-
return registry.get(route);
|
|
17
|
-
}
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/client/use-query-states.ts
|
|
20
|
-
/**
|
|
21
|
-
* useQueryStates — client-side hook for URL-synced search params.
|
|
22
|
-
*
|
|
23
|
-
* Delegates to nuqs for URL synchronization, batching, React 19 transitions,
|
|
24
|
-
* and throttled URL writes. Bridges timber's SearchParamCodec protocol to
|
|
25
|
-
* nuqs-compatible parsers.
|
|
26
|
-
*
|
|
27
|
-
* Design doc: design/23-search-params.md §"Codec Bridge"
|
|
28
|
-
*/
|
|
29
|
-
/**
|
|
30
|
-
* Bridge a timber SearchParamCodec to a nuqs-compatible SingleParser.
|
|
31
|
-
*
|
|
32
|
-
* nuqs parsers: { parse(string) → T|null, serialize?(T) → string, eq?, defaultValue? }
|
|
33
|
-
* timber codecs: { parse(string|string[]|undefined) → T, serialize(T) → string|null }
|
|
34
|
-
*/
|
|
35
|
-
function bridgeCodec(codec) {
|
|
36
|
-
return {
|
|
37
|
-
parse: (v) => codec.parse(v),
|
|
38
|
-
serialize: (v) => codec.serialize(v) ?? "",
|
|
39
|
-
defaultValue: codec.parse(void 0),
|
|
40
|
-
eq: (a, b) => codec.serialize(a) === codec.serialize(b)
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Bridge an entire codec map to nuqs-compatible parsers.
|
|
45
|
-
*/
|
|
46
|
-
function bridgeCodecs(codecs) {
|
|
47
|
-
const result = {};
|
|
48
|
-
for (const key of Object.keys(codecs)) result[key] = bridgeCodec(codecs[key]);
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Read and write typed search params from/to the URL.
|
|
53
|
-
*
|
|
54
|
-
* Delegates to nuqs internally. The timber nuqs adapter (auto-injected in
|
|
55
|
-
* browser-entry.ts) handles RSC navigation on non-shallow updates.
|
|
56
|
-
*
|
|
57
|
-
* Usage:
|
|
58
|
-
* ```ts
|
|
59
|
-
* // Via a SearchParamsDefinition
|
|
60
|
-
* const [params, setParams] = definition.useQueryStates()
|
|
61
|
-
*
|
|
62
|
-
* // Standalone with inline codecs
|
|
63
|
-
* const [params, setParams] = useQueryStates({
|
|
64
|
-
* page: fromSchema(z.coerce.number().int().min(1).default(1)),
|
|
65
|
-
* })
|
|
66
|
-
* ```
|
|
67
|
-
*/
|
|
68
|
-
function useQueryStates$1(codecsOrRoute, _options, urlKeys) {
|
|
69
|
-
let codecs;
|
|
70
|
-
let resolvedUrlKeys = urlKeys;
|
|
71
|
-
if (typeof codecsOrRoute === "string") {
|
|
72
|
-
const definition = getSearchParams(codecsOrRoute);
|
|
73
|
-
if (!definition) throw new Error(`useQueryStates('${codecsOrRoute}'): no search params registered for this route. Either the route has no search-params.ts file, or it hasn't been loaded yet. For cross-route usage, import the definition explicitly.`);
|
|
74
|
-
codecs = definition.codecs;
|
|
75
|
-
resolvedUrlKeys = definition.urlKeys;
|
|
76
|
-
} else codecs = codecsOrRoute;
|
|
77
|
-
const bridged = bridgeCodecs(codecs);
|
|
78
|
-
const nuqsOptions = {};
|
|
79
|
-
if (resolvedUrlKeys && Object.keys(resolvedUrlKeys).length > 0) nuqsOptions.urlKeys = resolvedUrlKeys;
|
|
80
|
-
const [values, setValues] = useQueryStates(bridged, nuqsOptions);
|
|
81
|
-
const setParams = (partial, setOptions) => {
|
|
82
|
-
const nuqsSetOptions = {};
|
|
83
|
-
if (setOptions?.shallow !== void 0) nuqsSetOptions.shallow = setOptions.shallow;
|
|
84
|
-
if (setOptions?.scroll !== void 0) nuqsSetOptions.scroll = setOptions.scroll;
|
|
85
|
-
if (setOptions?.history !== void 0) nuqsSetOptions.history = setOptions.history;
|
|
86
|
-
setValues(partial, nuqsSetOptions);
|
|
87
|
-
};
|
|
88
|
-
return [values, setParams];
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Create a useQueryStates binding for a SearchParamsDefinition.
|
|
92
|
-
* This is used internally by SearchParamsDefinition.useQueryStates().
|
|
93
|
-
*/
|
|
94
|
-
function bindUseQueryStates(definition) {
|
|
95
|
-
return (options) => {
|
|
96
|
-
return useQueryStates$1(definition.codecs, options, definition.urlKeys);
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
//#endregion
|
|
100
|
-
export { registerSearchParams as i, useQueryStates$1 as n, getSearchParams as r, bindUseQueryStates as t };
|
|
101
|
-
|
|
102
|
-
//# sourceMappingURL=use-query-states-Dd9PVu-L.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-query-states-Dd9PVu-L.js","names":[],"sources":["../../src/search-params/registry.ts","../../src/client/use-query-states.ts"],"sourcesContent":["/**\n * Runtime registry for route-scoped search params definitions.\n *\n * When a route's modules load, the framework registers its search-params\n * definition here. useQueryStates('/route') resolves codecs from this map.\n *\n * Design doc: design/23-search-params.md §\"Runtime: Registration at Route Load\"\n */\n\nimport type { SearchParamsDefinition } from './create.js';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst registry = new Map<string, SearchParamsDefinition<any>>();\n\n/**\n * Register a route's search params definition.\n * Called by the generated route manifest loader when a route's modules load.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function registerSearchParams(route: string, definition: SearchParamsDefinition<any>): void {\n registry.set(route, definition);\n}\n\n/**\n * Look up a route's search params definition.\n * Returns undefined if the route hasn't been loaded yet.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function getSearchParams(route: string): SearchParamsDefinition<any> | undefined {\n return registry.get(route);\n}\n","/**\n * useQueryStates — client-side hook for URL-synced search params.\n *\n * Delegates to nuqs for URL synchronization, batching, React 19 transitions,\n * and throttled URL writes. Bridges timber's SearchParamCodec protocol to\n * nuqs-compatible parsers.\n *\n * Design doc: design/23-search-params.md §\"Codec Bridge\"\n */\n\n'use client';\n\nimport { useQueryStates as nuqsUseQueryStates } from 'nuqs';\nimport type { SingleParser } from 'nuqs';\nimport type {\n SearchParamCodec,\n SearchParamsDefinition,\n SetParams,\n QueryStatesOptions,\n} from '#/search-params/create.js';\nimport { getSearchParams } from '#/search-params/registry.js';\n\n// ─── Codec Bridge ─────────────────────────────────────────────────\n\n/**\n * Bridge a timber SearchParamCodec to a nuqs-compatible SingleParser.\n *\n * nuqs parsers: { parse(string) → T|null, serialize?(T) → string, eq?, defaultValue? }\n * timber codecs: { parse(string|string[]|undefined) → T, serialize(T) → string|null }\n */\nfunction bridgeCodec<T>(codec: SearchParamCodec<T>): SingleParser<T> & { defaultValue: T } {\n return {\n parse: (v: string) => codec.parse(v),\n serialize: (v: T) => codec.serialize(v) ?? '',\n defaultValue: codec.parse(undefined) as T,\n eq: (a: T, b: T) => codec.serialize(a) === codec.serialize(b),\n };\n}\n\n/**\n * Bridge an entire codec map to nuqs-compatible parsers.\n */\nfunction bridgeCodecs<T extends Record<string, unknown>>(codecs: {\n [K in keyof T]: SearchParamCodec<T[K]>;\n}) {\n const result: Record<string, SingleParser<unknown> & { defaultValue: unknown }> = {};\n for (const key of Object.keys(codecs)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n result[key] = bridgeCodec(codecs[key as keyof T]) as any;\n }\n return result as { [K in keyof T]: SingleParser<T[K]> & { defaultValue: T[K] } };\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────\n\n/**\n * Read and write typed search params from/to the URL.\n *\n * Delegates to nuqs internally. The timber nuqs adapter (auto-injected in\n * browser-entry.ts) handles RSC navigation on non-shallow updates.\n *\n * Usage:\n * ```ts\n * // Via a SearchParamsDefinition\n * const [params, setParams] = definition.useQueryStates()\n *\n * // Standalone with inline codecs\n * const [params, setParams] = useQueryStates({\n * page: fromSchema(z.coerce.number().int().min(1).default(1)),\n * })\n * ```\n */\nexport function useQueryStates<T extends Record<string, unknown>>(\n codecsOrRoute: { [K in keyof T]: SearchParamCodec<T[K]> } | string,\n _options?: QueryStatesOptions,\n urlKeys?: Readonly<Record<string, string>>\n): [T, SetParams<T>] {\n // Route-string overload: resolve codecs from the registry\n let codecs: { [K in keyof T]: SearchParamCodec<T[K]> };\n let resolvedUrlKeys = urlKeys;\n if (typeof codecsOrRoute === 'string') {\n const definition = getSearchParams(codecsOrRoute);\n if (!definition) {\n throw new Error(\n `useQueryStates('${codecsOrRoute}'): no search params registered for this route. ` +\n `Either the route has no search-params.ts file, or it hasn't been loaded yet. ` +\n `For cross-route usage, import the definition explicitly.`\n );\n }\n codecs = definition.codecs as { [K in keyof T]: SearchParamCodec<T[K]> };\n resolvedUrlKeys = definition.urlKeys;\n } else {\n codecs = codecsOrRoute;\n }\n\n const bridged = bridgeCodecs(codecs);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const nuqsOptions: any = {};\n if (resolvedUrlKeys && Object.keys(resolvedUrlKeys).length > 0) {\n nuqsOptions.urlKeys = resolvedUrlKeys;\n }\n\n const [values, setValues] = nuqsUseQueryStates(bridged, nuqsOptions);\n\n // Wrap the nuqs setter to match timber's SetParams<T> signature.\n // nuqs's setter accepts Partial<Nullable<Values>> | UpdaterFn | null.\n // timber's setter accepts Partial<T> with optional SetParamsOptions.\n const setParams: SetParams<T> = (partial, setOptions?) => {\n const nuqsSetOptions: Record<string, unknown> = {};\n if (setOptions?.shallow !== undefined) nuqsSetOptions.shallow = setOptions.shallow;\n if (setOptions?.scroll !== undefined) nuqsSetOptions.scroll = setOptions.scroll;\n if (setOptions?.history !== undefined) nuqsSetOptions.history = setOptions.history;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n void setValues(partial as any, nuqsSetOptions);\n };\n\n return [values as T, setParams];\n}\n\n// ─── Definition binding ───────────────────────────────────────────\n\n/**\n * Create a useQueryStates binding for a SearchParamsDefinition.\n * This is used internally by SearchParamsDefinition.useQueryStates().\n */\nexport function bindUseQueryStates<T extends Record<string, unknown>>(\n definition: SearchParamsDefinition<T>\n): (options?: QueryStatesOptions) => [T, SetParams<T>] {\n return (options?: QueryStatesOptions) => {\n return useQueryStates<T>(definition.codecs, options, definition.urlKeys);\n };\n}\n"],"mappings":";;AAYA,IAAM,2BAAW,IAAI,KAA0C;;;;;AAO/D,SAAgB,qBAAqB,OAAe,YAA+C;AACjG,UAAS,IAAI,OAAO,WAAW;;;;;;AAQjC,SAAgB,gBAAgB,OAAwD;AACtF,QAAO,SAAS,IAAI,MAAM;;;;;;;;;;;;;;;;;;;ACC5B,SAAS,YAAe,OAAmE;AACzF,QAAO;EACL,QAAQ,MAAc,MAAM,MAAM,EAAE;EACpC,YAAY,MAAS,MAAM,UAAU,EAAE,IAAI;EAC3C,cAAc,MAAM,MAAM,KAAA,EAAU;EACpC,KAAK,GAAM,MAAS,MAAM,UAAU,EAAE,KAAK,MAAM,UAAU,EAAE;EAC9D;;;;;AAMH,SAAS,aAAgD,QAEtD;CACD,MAAM,SAA4E,EAAE;AACpF,MAAK,MAAM,OAAO,OAAO,KAAK,OAAO,CAEnC,QAAO,OAAO,YAAY,OAAO,KAAgB;AAEnD,QAAO;;;;;;;;;;;;;;;;;;;AAsBT,SAAgB,iBACd,eACA,UACA,SACmB;CAEnB,IAAI;CACJ,IAAI,kBAAkB;AACtB,KAAI,OAAO,kBAAkB,UAAU;EACrC,MAAM,aAAa,gBAAgB,cAAc;AACjD,MAAI,CAAC,WACH,OAAM,IAAI,MACR,mBAAmB,cAAc,uLAGlC;AAEH,WAAS,WAAW;AACpB,oBAAkB,WAAW;OAE7B,UAAS;CAGX,MAAM,UAAU,aAAa,OAAO;CAGpC,MAAM,cAAmB,EAAE;AAC3B,KAAI,mBAAmB,OAAO,KAAK,gBAAgB,CAAC,SAAS,EAC3D,aAAY,UAAU;CAGxB,MAAM,CAAC,QAAQ,aAAa,eAAmB,SAAS,YAAY;CAKpE,MAAM,aAA2B,SAAS,eAAgB;EACxD,MAAM,iBAA0C,EAAE;AAClD,MAAI,YAAY,YAAY,KAAA,EAAW,gBAAe,UAAU,WAAW;AAC3E,MAAI,YAAY,WAAW,KAAA,EAAW,gBAAe,SAAS,WAAW;AACzE,MAAI,YAAY,YAAY,KAAA,EAAW,gBAAe,UAAU,WAAW;AAEtE,YAAU,SAAgB,eAAe;;AAGhD,QAAO,CAAC,QAAa,UAAU;;;;;;AASjC,SAAgB,mBACd,YACqD;AACrD,SAAQ,YAAiC;AACvC,SAAO,iBAAkB,WAAW,QAAQ,SAAS,WAAW,QAAQ"}
|