@timber-js/app 0.1.9 → 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.
@@ -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;AAsKD,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
+ {"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;AAkpBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;8BA9hBpD,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAgiBhD,wBAAiE"}
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.9",
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",
@@ -26,20 +26,11 @@ export function timberServerBundle(): Plugin[] {
26
26
  // which throws in the SSR environment.
27
27
  if (command === 'serve') {
28
28
  // In dev, Vite externalizes node_modules and loads them via Node's
29
- // native require(). This causes two problems:
30
- //
31
- // 1. Poison-pill packages: deps that import `server-only` (like `bright`)
32
- // hit the real CJS package which throws in the SSR environment.
33
- //
34
- // 2. Dual React instances: deps with React hooks (nuqs, etc.) are
35
- // externalized and loaded via Node.js, getting their own copy of
36
- // React. Meanwhile, SSR's renderToReadableStream uses Vite's
37
- // dep-optimized React. Two React instances = dispatcher mismatch,
38
- // causing "Invalid hook call" / "Cannot read properties of null
39
- // (reading 'useId')" errors. See LOCAL-297.
40
- //
41
- // We force these packages to be non-external so they go through
42
- // Vite's module pipeline, which deduplicates React correctly.
29
+ // native require(). Deps that import `server-only` (like `bright`)
30
+ // hit the real CJS package which throws at runtime. We force these
31
+ // poison-pill packages to be non-external so they go through Vite's
32
+ // module pipeline, where the timber-shims plugin intercepts them
33
+ // and serves no-op virtual modules for server environments.
43
34
  return {
44
35
  environments: {
45
36
  rsc: {
@@ -49,7 +40,7 @@ export function timberServerBundle(): Plugin[] {
49
40
  },
50
41
  ssr: {
51
42
  resolve: {
52
- noExternal: ['server-only', 'client-only', 'nuqs'],
43
+ noExternal: ['server-only', 'client-only'],
53
44
  },
54
45
  },
55
46
  },
@@ -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
- // Delegates to the 'use client' implementation from use-query-states.ts.
294
- //
295
- // In the RSC environment: use-query-states.ts is transformed by the RSC
296
- // plugin into a client reference proxy. Calling it throws — correct,
297
- // because hooks can't run during server component rendering.
298
- // In SSR: use-query-states.ts is the real nuqs-backed function. Hooks
299
- // work during SSR's renderToReadableStream, so this works correctly.
300
- // On the client: same as SSR — the real function is available.
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 but their
140
- * children are checked as if they were direct children of the parent.
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 (they can match zero segments)
167
- for (const child of node.children) {
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
- // Try children in priority order
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 node.children) {
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. Group segments (transparent — recurse without consuming)
195
- for (const child of node.children) {
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
- // 4. Catch-all segments ([...param])
221
- for (const child of node.children) {
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
- // 5. Optional catch-all segments ([[...param]])
233
- for (const child of node.children) {
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"}