@timber-js/app 0.2.0-alpha.123 → 0.2.0-alpha.124

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.
Files changed (44) hide show
  1. package/dist/_chunks/{actions-ZlcKx0H9.js → actions-418ygYKC.js} +2 -2
  2. package/dist/_chunks/{actions-ZlcKx0H9.js.map → actions-418ygYKC.js.map} +1 -1
  3. package/dist/_chunks/{cli-schema-sync-VcCf1TFY.js → cli-schema-sync-DFHBj6yZ.js} +59 -12
  4. package/dist/_chunks/{cli-schema-sync-VcCf1TFY.js.map → cli-schema-sync-DFHBj6yZ.js.map} +1 -1
  5. package/dist/_chunks/cli-schema-sync-vdobDWwX.js +2 -0
  6. package/dist/_chunks/{define-C0V_eKiW.js → define-BJMwm0c4.js} +2 -2
  7. package/dist/_chunks/{define-C0V_eKiW.js.map → define-BJMwm0c4.js.map} +1 -1
  8. package/dist/_chunks/{navigation-root-Ds_hDtRW.js → navigation-root-DaERBEGV.js} +2 -2
  9. package/dist/_chunks/{navigation-root-Ds_hDtRW.js.map → navigation-root-DaERBEGV.js.map} +1 -1
  10. package/dist/_chunks/{use-segment-params-Du7u25cK.js → use-segment-params-ByGqwUuO.js} +3 -2
  11. package/dist/_chunks/use-segment-params-ByGqwUuO.js.map +1 -0
  12. package/dist/_chunks/{walkers-tkw7sDlj.js → walkers-DMSGZeCk.js} +2 -2
  13. package/dist/_chunks/{walkers-tkw7sDlj.js.map → walkers-DMSGZeCk.js.map} +1 -1
  14. package/dist/cli-schema-sync.d.ts +6 -4
  15. package/dist/cli-schema-sync.d.ts.map +1 -1
  16. package/dist/cli.d.ts +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +12 -6
  19. package/dist/cli.js.map +1 -1
  20. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  21. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  22. package/dist/client/index.js +2 -2
  23. package/dist/client/internal.js +22 -34
  24. package/dist/client/internal.js.map +1 -1
  25. package/dist/client/navigation-context.d.ts +1 -0
  26. package/dist/client/navigation-context.d.ts.map +1 -1
  27. package/dist/client/router.d.ts.map +1 -1
  28. package/dist/client/use-search-params.d.ts +7 -5
  29. package/dist/client/use-search-params.d.ts.map +1 -1
  30. package/dist/index.js +2 -2
  31. package/dist/routing/index.js +2 -2
  32. package/dist/segment-params/index.js +1 -1
  33. package/dist/server/index.js +1 -1
  34. package/dist/server/internal.js +1 -1
  35. package/package.json +1 -1
  36. package/src/cli-schema-sync.ts +80 -13
  37. package/src/cli.ts +18 -7
  38. package/src/client/browser-entry/hydrate.ts +2 -0
  39. package/src/client/browser-entry/post-hydration.ts +1 -0
  40. package/src/client/navigation-context.ts +2 -1
  41. package/src/client/router.ts +4 -2
  42. package/src/client/use-search-params.ts +25 -44
  43. package/dist/_chunks/cli-schema-sync-4HcQIziG.js +0 -2
  44. package/dist/_chunks/use-segment-params-Du7u25cK.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { n as setViteServer } from "./_chunks/warnings-B30wpfaS.js";
2
2
  import { a as isDynamicMetadataExtension, o as isMetadataRouteServePath } from "./_chunks/metadata-routes-DESG8uk5.js";
3
3
  import { h as swallow } from "./_chunks/logger-DDmK5skc.js";
4
- import { a as DEFAULT_BUILD_DIR, c as mergeFileConfig, d as resolveClientJavascript, f as createNoopTimer, l as resolveAppDir, o as createPluginContext, p as scanRoutes, s as loadTimberConfigFile, u as resolveBuildDir } from "./_chunks/cli-schema-sync-VcCf1TFY.js";
5
- import { a as generateRouteMap, i as collectInterceptionRewrites, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-tkw7sDlj.js";
4
+ import { c as loadTimberConfigFile, d as resolveBuildDir, f as resolveClientJavascript, l as mergeFileConfig, m as scanRoutes, o as DEFAULT_BUILD_DIR, p as createNoopTimer, s as createPluginContext, u as resolveAppDir } from "./_chunks/cli-schema-sync-DFHBj6yZ.js";
5
+ import { a as generateRouteMap, i as collectInterceptionRewrites, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-DMSGZeCk.js";
6
6
  import { t as formatSize } from "./_chunks/format-BjN9HLu9.js";
7
7
  import { basename, dirname, extname, join, normalize, resolve } from "node:path";
8
8
  import { createRequire } from "node:module";
@@ -1,4 +1,4 @@
1
1
  import { i as INTERCEPTION_MARKERS, n as classifyUrlSegment, r as DEFAULT_PAGE_EXTENSIONS, t as classifySegment } from "../_chunks/segment-classify-BjfuctV2.js";
2
- import { p as scanRoutes } from "../_chunks/cli-schema-sync-VcCf1TFY.js";
3
- import { a as generateRouteMap, i as collectInterceptionRewrites, n as collectDynamicSegmentsFromTree, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "../_chunks/walkers-tkw7sDlj.js";
2
+ import { m as scanRoutes } from "../_chunks/cli-schema-sync-DFHBj6yZ.js";
3
+ import { a as generateRouteMap, i as collectInterceptionRewrites, n as collectDynamicSegmentsFromTree, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "../_chunks/walkers-DMSGZeCk.js";
4
4
  export { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS, classifySegment, classifyUrlSegment, collectDynamicSegmentsFromTree, collectInterceptionRewrites, collectLeafRoutes, generateRouteMap, scanRoutes, validateSchemaAgainstRoutes };
@@ -1,2 +1,2 @@
1
- import { n as defineSegmentParams } from "../_chunks/define-C0V_eKiW.js";
1
+ import { n as defineSegmentParams } from "../_chunks/define-BJMwm0c4.js";
2
2
  export { defineSegmentParams };
@@ -1,6 +1,6 @@
1
1
  import { a as getSpanId, d as withSpan, o as getTraceId, p as formFlashAls, t as addSpanEvent, v as isDebug, y as isDevMode } from "../_chunks/tracing-DvKLzCFa.js";
2
2
  import { t as formatSize } from "../_chunks/format-BjN9HLu9.js";
3
- import { D as getCookieJar, a as revalidateTag, b as getSegmentParams, c as DenySignal, f as deny, h as waitUntil, i as revalidatePath, l as RedirectSignal, m as redirectExternal, o as coerce, p as redirect, s as parseFormData, u as RedirectType, v as getHeaders } from "../_chunks/actions-ZlcKx0H9.js";
3
+ import { D as getCookieJar, a as revalidateTag, b as getSegmentParams, c as DenySignal, f as deny, h as waitUntil, i as revalidatePath, l as RedirectSignal, m as redirectExternal, o as coerce, p as redirect, s as parseFormData, u as RedirectType, v as getHeaders } from "../_chunks/actions-418ygYKC.js";
4
4
  //#region src/server/sensitive-fields.ts
5
5
  /**
6
6
  * Sensitive field stripping — removes password/token/CVV-style fields
@@ -3,7 +3,7 @@ import { a as warnRedirectInSuspense, c as warnSuspenseWrappingChildren, i as wa
3
3
  import { i as getMetadataRouteServePath, n as classifyMetadataRoute, r as getMetadataRouteAutoLink, t as METADATA_ROUTE_CONVENTIONS } from "../_chunks/metadata-routes-DESG8uk5.js";
4
4
  import { n as classifyUrlSegment } from "../_chunks/segment-classify-BjfuctV2.js";
5
5
  import { a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, h as swallow, i as logMiddlewareShortCircuit, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest } from "../_chunks/logger-DDmK5skc.js";
6
- import { C as setMatchedSegmentPath, E as getCookie, O as getSetCookieHeaders, S as runWithRequestContext, T as setSegmentParams, _ as getHeader, b as getSegmentParams, c as DenySignal, d as RenderError, g as applyRequestHeaderOverlay, l as RedirectSignal, n as executeAction, o as coerce, r as isRscActionRequest, t as buildNoJsResponse, w as setMutableCookieContext, x as markResponseFlushed, y as getSearchParams } from "../_chunks/actions-ZlcKx0H9.js";
6
+ import { C as setMatchedSegmentPath, E as getCookie, O as getSetCookieHeaders, S as runWithRequestContext, T as setSegmentParams, _ as getHeader, b as getSegmentParams, c as DenySignal, d as RenderError, g as applyRequestHeaderOverlay, l as RedirectSignal, n as executeAction, o as coerce, r as isRscActionRequest, t as buildNoJsResponse, w as setMutableCookieContext, x as markResponseFlushed, y as getSearchParams } from "../_chunks/actions-418ygYKC.js";
7
7
  import "../client/error-boundary.js";
8
8
  import { n as toBracketKey } from "../_chunks/resolve-schema-DU2nbVOi.js";
9
9
  import "../_chunks/segment-context-Bg0pFbLT.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.123",
3
+ "version": "0.2.0-alpha.124",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -1,8 +1,7 @@
1
1
  /**
2
- * `timber schema sync` — scan filesystem for dynamic segments and append
3
- * missing entries to app/schema.ts with `codec.string` defaults.
4
- *
5
- * Only adds, never removes or modifies existing entries.
2
+ * `timber schema sync` — scan filesystem for dynamic segments, append
3
+ * missing entries with `codec.string` defaults, and remove stale entries
4
+ * whose segments no longer exist in the filesystem.
6
5
  *
7
6
  * Design doc: design/41-global-params.md §Auto-Scaffolding
8
7
  */
@@ -66,6 +65,12 @@ export function findSchemaFile(appDir: string): string | undefined {
66
65
  return undefined;
67
66
  }
68
67
 
68
+ export function defaultCodecForSegment(seg: string): string {
69
+ if (seg.startsWith('[[...')) return 'codec.optionalCatchAll(codec.string)';
70
+ if (seg.startsWith('[...')) return 'codec.stringArray';
71
+ return 'codec.string';
72
+ }
73
+
69
74
  /**
70
75
  * Generate the content for a new app/schema.ts with the given segment keys.
71
76
  */
@@ -77,7 +82,7 @@ function generateNewSchema(segments: string[]): string {
77
82
  lines.push('export default defineSchema({');
78
83
  lines.push(' segmentParams: {');
79
84
  for (const seg of segments) {
80
- lines.push(` '${seg}': codec.string,`);
85
+ lines.push(` '${seg}': ${defaultCodecForSegment(seg)},`);
81
86
  }
82
87
  lines.push(' },');
83
88
  lines.push('});');
@@ -147,7 +152,9 @@ function appendToSchema(content: string, newSegments: string[]): string {
147
152
  const indent = existingLine ? existingLine[1] : ' ';
148
153
 
149
154
  // Build insertion text
150
- const insertion = newSegments.map((seg) => `${indent}'${seg}': codec.string,`).join('\n');
155
+ const insertion = newSegments
156
+ .map((seg) => `${indent}'${seg}': ${defaultCodecForSegment(seg)},`)
157
+ .join('\n');
151
158
 
152
159
  // Insert before the closing brace
153
160
  const before = content.slice(0, closeBrace);
@@ -159,6 +166,54 @@ function appendToSchema(content: string, newSegments: string[]): string {
159
166
  return before + separator + insertion + '\n' + after;
160
167
  }
161
168
 
169
+ /**
170
+ * Remove entries from the segmentParams block whose keys match `staleSegments`.
171
+ * Removes the entire line including trailing comma/whitespace.
172
+ */
173
+ function removeFromSchema(content: string, staleSegments: string[]): string {
174
+ for (const seg of staleSegments) {
175
+ const escaped = seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
176
+ const keyRe = new RegExp(`^[ \t]*['"]${escaped}['"][ \t]*:`, 'gm');
177
+ let m: RegExpExecArray | null;
178
+ while ((m = keyRe.exec(content)) !== null) {
179
+ const start = m.index;
180
+ let i = m.index + m[0].length;
181
+ let depth = 0;
182
+ let inString: string | null = null;
183
+
184
+ while (i < content.length) {
185
+ const ch = content[i];
186
+ if (inString) {
187
+ if (ch === '\\') {
188
+ i++;
189
+ } else if (ch === inString) {
190
+ inString = null;
191
+ }
192
+ } else if (ch === '"' || ch === "'" || ch === '`') {
193
+ inString = ch;
194
+ } else if (ch === '(' || ch === '{' || ch === '[') {
195
+ depth++;
196
+ } else if (ch === ')' || ch === '}' || ch === ']') {
197
+ depth--;
198
+ } else if (depth === 0 && ch === ',') {
199
+ i++;
200
+ break;
201
+ } else if (depth === 0 && (ch === '\n' || ch === '}')) {
202
+ break;
203
+ }
204
+ i++;
205
+ }
206
+
207
+ // Consume trailing newline
208
+ if (i < content.length && content[i] === '\n') i++;
209
+
210
+ content = content.slice(0, start) + content.slice(i);
211
+ keyRe.lastIndex = start;
212
+ }
213
+ }
214
+ return content;
215
+ }
216
+
162
217
  export interface SchemaSyncResult {
163
218
  /** Segments found in the filesystem. */
164
219
  filesystemSegments: string[];
@@ -166,6 +221,8 @@ export interface SchemaSyncResult {
166
221
  existingSegments: string[];
167
222
  /** Segments that were added. */
168
223
  addedSegments: string[];
224
+ /** Segments that were removed (no longer in filesystem). */
225
+ removedSegments: string[];
169
226
  /** Whether a new schema file was created. */
170
227
  created: boolean;
171
228
  }
@@ -178,17 +235,18 @@ export function runSchemaSync(projectRoot: string): SchemaSyncResult {
178
235
 
179
236
  const filesystemSegments = collectDynamicSegments(appDir);
180
237
 
181
- if (filesystemSegments.length === 0) {
238
+ const schemaPath = findSchemaFile(appDir);
239
+
240
+ if (filesystemSegments.length === 0 && !schemaPath) {
182
241
  return {
183
242
  filesystemSegments: [],
184
243
  existingSegments: [],
185
244
  addedSegments: [],
245
+ removedSegments: [],
186
246
  created: false,
187
247
  };
188
248
  }
189
249
 
190
- const schemaPath = findSchemaFile(appDir);
191
-
192
250
  if (!schemaPath) {
193
251
  // Create new schema file
194
252
  const newPath = join(appDir, 'schema.ts');
@@ -197,32 +255,41 @@ export function runSchemaSync(projectRoot: string): SchemaSyncResult {
197
255
  filesystemSegments,
198
256
  existingSegments: [],
199
257
  addedSegments: filesystemSegments,
258
+ removedSegments: [],
200
259
  created: true,
201
260
  };
202
261
  }
203
262
 
204
- // Read existing schema and find missing segments
263
+ // Read existing schema and diff
205
264
  const content = readFileSync(schemaPath, 'utf-8');
206
265
  const existingSegments = parseExistingSchemaKeys(content);
207
266
  const missing = filesystemSegments.filter((seg) => !existingSegments.includes(seg));
267
+ const stale = existingSegments.filter((seg) => !filesystemSegments.includes(seg));
208
268
 
209
- if (missing.length === 0) {
269
+ if (missing.length === 0 && stale.length === 0) {
210
270
  return {
211
271
  filesystemSegments,
212
272
  existingSegments,
213
273
  addedSegments: [],
274
+ removedSegments: [],
214
275
  created: false,
215
276
  };
216
277
  }
217
278
 
218
- // Append missing segments
219
- const updated = appendToSchema(content, missing);
279
+ let updated = content;
280
+ if (stale.length > 0) {
281
+ updated = removeFromSchema(updated, stale);
282
+ }
283
+ if (missing.length > 0) {
284
+ updated = appendToSchema(updated, missing);
285
+ }
220
286
  writeFileSync(schemaPath, updated, 'utf-8');
221
287
 
222
288
  return {
223
289
  filesystemSegments,
224
290
  existingSegments,
225
291
  addedSegments: missing,
292
+ removedSegments: stale,
226
293
  created: false,
227
294
  };
228
295
  }
package/src/cli.ts CHANGED
@@ -188,7 +188,7 @@ export async function runCheck(options: CommandOptions): Promise<void> {
188
188
 
189
189
  /**
190
190
  * `timber schema sync` — scan filesystem for dynamic segments, diff against
191
- * app/schema.ts, and append missing entries with codec.string default.
191
+ * app/schema.ts, and append missing entries with type-appropriate codec defaults.
192
192
  *
193
193
  * See design/41-global-params.md §Auto-Scaffolding
194
194
  */
@@ -201,7 +201,7 @@ export async function runSchema(args: string[]): Promise<void> {
201
201
  );
202
202
  }
203
203
 
204
- const { runSchemaSync } = await import('./cli-schema-sync.js');
204
+ const { runSchemaSync, defaultCodecForSegment } = await import('./cli-schema-sync.js');
205
205
  const result = runSchemaSync(process.cwd());
206
206
 
207
207
  if (result.filesystemSegments.length === 0) {
@@ -212,19 +212,30 @@ export async function runSchema(args: string[]): Promise<void> {
212
212
  if (result.created) {
213
213
  console.log(`[timber] Created app/schema.ts with ${result.addedSegments.length} segment(s):`);
214
214
  for (const seg of result.addedSegments) {
215
- console.log(` + ${seg}: codec.string`);
215
+ console.log(` + ${seg}: ${defaultCodecForSegment(seg)}`);
216
216
  }
217
217
  return;
218
218
  }
219
219
 
220
- if (result.addedSegments.length === 0) {
220
+ if (result.addedSegments.length === 0 && result.removedSegments.length === 0) {
221
221
  console.log('[timber] Schema is up to date — all segments are registered.');
222
222
  return;
223
223
  }
224
224
 
225
- console.log(`[timber] Added ${result.addedSegments.length} segment(s) to app/schema.ts:`);
226
- for (const seg of result.addedSegments) {
227
- console.log(` + ${seg}: codec.string`);
225
+ if (result.addedSegments.length > 0) {
226
+ console.log(`[timber] Added ${result.addedSegments.length} segment(s) to app/schema.ts:`);
227
+ for (const seg of result.addedSegments) {
228
+ console.log(` + ${seg}: ${defaultCodecForSegment(seg)}`);
229
+ }
230
+ }
231
+
232
+ if (result.removedSegments.length > 0) {
233
+ console.log(
234
+ `[timber] Removed ${result.removedSegments.length} stale segment(s) from app/schema.ts:`
235
+ );
236
+ for (const seg of result.removedSegments) {
237
+ console.log(` - ${seg}`);
238
+ }
228
239
  }
229
240
  }
230
241
 
@@ -50,12 +50,14 @@ export function runPreHydration(): void {
50
50
  setNavigationState({
51
51
  params: earlyParams as Record<string, string | string[]>,
52
52
  pathname: window.location.pathname,
53
+ search: window.location.search,
53
54
  });
54
55
  delete (self as unknown as Record<string, unknown>).__timber_params;
55
56
  } else {
56
57
  setNavigationState({
57
58
  params: {},
58
59
  pathname: window.location.pathname,
60
+ search: window.location.search,
59
61
  });
60
62
  }
61
63
  }
@@ -79,6 +79,7 @@ export function setupPostHydration({
79
79
  setNavigationState({
80
80
  params: lateTimberParams as Record<string, string | string[]>,
81
81
  pathname: window.location.pathname,
82
+ search: window.location.search,
82
83
  });
83
84
  delete (self as unknown as Record<string, unknown>).__timber_params;
84
85
  }
@@ -48,6 +48,7 @@ import React, { createElement, type ReactNode } from 'react';
48
48
  export interface NavigationState {
49
49
  params: Record<string, string | string[]>;
50
50
  pathname: string;
51
+ search: string;
51
52
  }
52
53
 
53
54
  // ---------------------------------------------------------------------------
@@ -153,7 +154,7 @@ const NAV_STATE_KEY = Symbol.for('__timber_nav_state');
153
154
  function _getNavStateStore(): { current: NavigationState } {
154
155
  const g = globalThis as Record<symbol, unknown>;
155
156
  if (!g[NAV_STATE_KEY]) {
156
- g[NAV_STATE_KEY] = { current: { params: {}, pathname: '/' } };
157
+ g[NAV_STATE_KEY] = { current: { params: {}, pathname: '/', search: '' } };
157
158
  }
158
159
  return g[NAV_STATE_KEY] as { current: NavigationState };
159
160
  }
@@ -346,8 +346,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
346
346
  // Module-level fallback for tests (no NavigationProvider) and SSR
347
347
  setCurrentParams(resolvedParams);
348
348
  // globalThis bridge — kept for backward compat
349
- const pathname = url.startsWith('http') ? new URL(url).pathname : url.split('?')[0] || '/';
350
- const navState: NavigationState = { params: resolvedParams, pathname };
349
+ const parsed = new URL(url, 'http://localhost');
350
+ const pathname = parsed.pathname || '/';
351
+ const search = parsed.search;
352
+ const navState: NavigationState = { params: resolvedParams, pathname, search };
351
353
  setNavigationState(navState);
352
354
  return navState;
353
355
  }
@@ -4,9 +4,12 @@
4
4
  * Returns a read-only URLSearchParams instance reflecting the current
5
5
  * URL's query string. Updates when client-side navigation changes the URL.
6
6
  *
7
- * This is a thin wrapper over window.location.search, provided for
8
- * Next.js API compatibility (libraries like nuqs import useSearchParams
9
- * from next/navigation).
7
+ * On the client, reads from NavigationContext which is updated atomically
8
+ * with the RSC tree render. This replaces the previous useSyncExternalStore
9
+ * approach which read window.location.search directly — causing React to
10
+ * detect external store tearing during transitions and fall back to
11
+ * synchronous rendering (renderRootSync instead of renderRootConcurrent),
12
+ * blocking the main thread and freezing animations.
10
13
  *
11
14
  * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard
12
15
  * URLSearchParams. Mutation methods (set, delete, append) work on the
@@ -15,42 +18,14 @@
15
18
  * During SSR, reads the request search params from the SSR ALS context
16
19
  * (populated by ssr-entry.ts) instead of window.location.
17
20
  *
18
- * All mutable state is delegated to client/state.ts for singleton guarantees.
19
- * See design/18-build-system.md §"Singleton State Registry"
21
+ * Compatible with Next.js's `useSearchParams()` from `next/navigation`.
20
22
  */
21
23
 
22
- import { useSyncExternalStore } from 'react';
23
24
  import { getSsrData } from './ssr-data.js';
25
+ import { useNavigationContext } from './navigation-context.js';
24
26
  import { cachedSearch, cachedSearchParams, _setCachedSearch } from './state.js';
25
27
 
26
- function getSearch(): string {
27
- if (typeof window !== 'undefined') return window.location.search;
28
- const data = getSsrData();
29
- if (!data) return '';
30
- const sp = new URLSearchParams(data.searchParams);
31
- const str = sp.toString();
32
- return str ? `?${str}` : '';
33
- }
34
-
35
- function getServerSearch(): string {
36
- const data = getSsrData();
37
- if (!data) return '';
38
- const sp = new URLSearchParams(data.searchParams);
39
- const str = sp.toString();
40
- return str ? `?${str}` : '';
41
- }
42
-
43
- function subscribe(callback: () => void): () => void {
44
- window.addEventListener('popstate', callback);
45
- return () => window.removeEventListener('popstate', callback);
46
- }
47
-
48
- // Cache the last search string and its parsed URLSearchParams to avoid
49
- // creating a new object on every render when the URL hasn't changed.
50
- // State lives in client/state.ts for singleton guarantees.
51
-
52
- function getSearchParams(): URLSearchParams {
53
- const search = getSearch();
28
+ function getSearchParams(search: string): URLSearchParams {
54
29
  if (search !== cachedSearch) {
55
30
  const params = new URLSearchParams(search);
56
31
  _setCachedSearch(search, params);
@@ -59,20 +34,26 @@ function getSearchParams(): URLSearchParams {
59
34
  return cachedSearchParams;
60
35
  }
61
36
 
62
- function getServerSearchParams(): URLSearchParams {
63
- const data = getSsrData();
64
- return data ? new URLSearchParams(data.searchParams) : new URLSearchParams();
65
- }
66
-
67
37
  /**
68
38
  * Read the current URL search params.
69
39
  *
70
40
  * Compatible with Next.js's `useSearchParams()` from `next/navigation`.
71
41
  */
72
42
  export function useSearchParams(): URLSearchParams {
73
- // useSyncExternalStore needs a primitive snapshot for comparison.
74
- // We use the raw search string as the snapshot, then return the
75
- // parsed URLSearchParams.
76
- useSyncExternalStore(subscribe, getSearch, getServerSearch);
77
- return typeof window !== 'undefined' ? getSearchParams() : getServerSearchParams();
43
+ try {
44
+ const navContext = useNavigationContext();
45
+ if (navContext !== null) {
46
+ return getSearchParams(navContext.search);
47
+ }
48
+ } catch {
49
+ // No React dispatcher available (called outside a component).
50
+ }
51
+
52
+ // SSR path: read from ALS-backed SSR data context.
53
+ const ssrData = getSsrData();
54
+ if (ssrData) return new URLSearchParams(ssrData.searchParams);
55
+
56
+ // Final fallback: window.location (tests, edge cases).
57
+ if (typeof window !== 'undefined') return getSearchParams(window.location.search);
58
+ return new URLSearchParams();
78
59
  }
@@ -1,2 +0,0 @@
1
- import { i as runSchemaSync } from "./cli-schema-sync-VcCf1TFY.js";
2
- export { runSchemaSync };
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-segment-params-Du7u25cK.js","names":[],"sources":["../../src/client/navigation-context.ts","../../src/client/use-segment-params.ts"],"sourcesContent":["'use client';\n\n/**\n * NavigationContext — React context for navigation state.\n *\n * Holds the current route params and pathname, updated atomically\n * with the RSC tree on each navigation. This replaces the previous\n * useSyncExternalStore approach for useSegmentParams() and usePathname(),\n * which suffered from a timing gap: the new tree could commit before\n * the external store re-renders fired, causing a frame where both\n * old and new active states were visible simultaneously.\n *\n * By wrapping the RSC payload element in NavigationProvider inside\n * renderRoot(), the context value and the element tree are passed to\n * reactRoot.render() in the same call — atomic by construction.\n * All consumers (useParams, usePathname) see the new values in the\n * same render pass as the new tree.\n *\n * During SSR, no NavigationProvider is mounted. Hooks fall back to\n * the ALS-backed getSsrData() for per-request isolation.\n *\n * IMPORTANT: createContext and useContext are NOT available in the RSC\n * environment (React Server Components use a stripped-down React).\n * The context is lazily initialized on first access, and all functions\n * that depend on these APIs are safe to call from any environment —\n * they return null or no-op when the APIs aren't available.\n *\n * SINGLETON GUARANTEE: All shared mutable state uses globalThis via\n * Symbol.for keys. The RSC client bundler can duplicate this module\n * across chunks (browser-entry graph + client-reference graph). With\n * ESM output, each chunk gets its own module scope — module-level\n * variables would create separate singleton instances per chunk.\n * globalThis guarantees a single instance regardless of duplication.\n *\n * This workaround will be removed when Rolldown ships `format: 'app'`\n * (module registry format that deduplicates like webpack/Turbopack).\n * See design/27-chunking-strategy.md.\n *\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport React, { createElement, type ReactNode } from 'react';\n\n// ---------------------------------------------------------------------------\n// Context type\n// ---------------------------------------------------------------------------\n\nexport interface NavigationState {\n params: Record<string, string | string[]>;\n pathname: string;\n}\n\n// ---------------------------------------------------------------------------\n// Lazy context initialization\n// ---------------------------------------------------------------------------\n\n/**\n * The context is created lazily to avoid calling createContext at module\n * level. In the RSC environment, React.createContext doesn't exist —\n * calling it at import time would crash the server.\n *\n * Context instances are stored on globalThis (NOT in module-level\n * variables) because the ESM bundler can duplicate this module across\n * chunks. Module-level variables would create separate instances per\n * chunk — the provider in NavigationRoot (index chunk) would use\n * context A while the consumer in usePendingNavigation (shared chunk)\n * reads from context B. globalThis guarantees a single instance.\n *\n * See design/27-chunking-strategy.md §\"Singleton Safety\"\n *\n * NOTE: Despite similar naming, `usePendingNavigationUrl()` here is an\n * internal helper — the public hook is `usePendingNavigation()` in\n * use-pending-navigation.ts.\n */\n\n// Symbol keys for globalThis storage — prevents collisions with user code\nconst NAV_CTX_KEY = Symbol.for('__timber_nav_ctx');\nconst PENDING_CTX_KEY = Symbol.for('__timber_pending_nav_ctx');\n\nfunction getOrCreateContext(): React.Context<NavigationState | null> | undefined {\n const existing = (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] as\n | React.Context<NavigationState | null>\n | undefined;\n if (existing !== undefined) return existing;\n // createContext may not exist in the RSC environment\n if (typeof React.createContext === 'function') {\n const ctx = React.createContext<NavigationState | null>(null);\n (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;\n return ctx;\n }\n return undefined;\n}\n\n/**\n * Read the navigation context. Returns null during SSR (no provider)\n * or in the RSC environment (no context available).\n * Internal — used by useSegmentParams() and usePathname().\n */\nexport function useNavigationContext(): NavigationState | null {\n const ctx = getOrCreateContext();\n if (!ctx) return null;\n // useContext may not exist in the RSC environment — caller wraps in try/catch\n if (typeof React.useContext !== 'function') return null;\n return React.useContext(ctx);\n}\n\n// ---------------------------------------------------------------------------\n// Provider component\n// ---------------------------------------------------------------------------\n\nexport interface NavigationProviderProps {\n value: NavigationState;\n children?: ReactNode;\n}\n\n/**\n * Wraps children with NavigationContext.Provider.\n *\n * Used in browser-entry.ts renderRoot to wrap the RSC payload element\n * so that navigation state updates atomically with the tree render.\n */\nexport function NavigationProvider({\n value,\n children,\n}: NavigationProviderProps): React.ReactElement {\n const ctx = getOrCreateContext();\n if (!ctx) {\n // RSC environment — no context available. Return children as-is.\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state for renderRoot to read\n// ---------------------------------------------------------------------------\n\n/**\n * Navigation state communicated between the router and renderRoot.\n *\n * The router calls setNavigationState() before renderRoot(). The\n * renderRoot callback reads via getNavigationState() to create the\n * NavigationProvider with the correct params/pathname.\n *\n * This is NOT used by hooks directly — hooks read from React context.\n *\n * Stored on globalThis (like the context instances above) because the\n * router lives in one chunk while renderRoot lives in another. Module-\n * level variables would be separate per chunk.\n */\nconst NAV_STATE_KEY = Symbol.for('__timber_nav_state');\n\nfunction _getNavStateStore(): { current: NavigationState } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[NAV_STATE_KEY]) {\n g[NAV_STATE_KEY] = { current: { params: {}, pathname: '/' } };\n }\n return g[NAV_STATE_KEY] as { current: NavigationState };\n}\n\nexport function setNavigationState(state: NavigationState): void {\n _getNavStateStore().current = state;\n}\n\nexport function getNavigationState(): NavigationState {\n return _getNavStateStore().current;\n}\n\n// ---------------------------------------------------------------------------\n// Pending Navigation Context (same module for singleton guarantee)\n// ---------------------------------------------------------------------------\n\n/**\n * Separate context for the in-flight navigation URL. Provided by\n * NavigationRoot (urgent useState), consumed by usePendingNavigation\n * and TopLoader. Per-link pending state uses useOptimistic instead\n * (see link-pending-store.ts).\n *\n * Uses globalThis via Symbol.for for the same reason as NavigationContext\n * above — the bundler may duplicate this module across chunks, and module-\n * level variables would create separate context instances.\n */\n\nfunction getOrCreatePendingContext(): React.Context<string | null> | undefined {\n const existing = (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] as\n | React.Context<string | null>\n | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext === 'function') {\n const ctx = React.createContext<string | null>(null);\n (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] = ctx;\n return ctx;\n }\n return undefined;\n}\n\n/**\n * Read the pending navigation URL from context.\n * Returns null during SSR (no provider) or in the RSC environment.\n */\nexport function usePendingNavigationUrl(): string | null {\n const ctx = getOrCreatePendingContext();\n if (!ctx) return null;\n if (typeof React.useContext !== 'function') return null;\n return React.useContext(ctx);\n}\n\n/**\n * Provider for the pending navigation URL. Wraps children with\n * the pending context Provider.\n */\nexport function PendingNavigationProvider({\n value,\n children,\n}: {\n value: string | null;\n children?: ReactNode;\n}): React.ReactElement {\n const ctx = getOrCreatePendingContext();\n if (!ctx) {\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Navigation API transition state (optional progressive enhancement)\n// ---------------------------------------------------------------------------\n\n/**\n * Check if the browser's Navigation API has an active transition.\n *\n * When the Navigation API is available and a navigation has been intercepted\n * via event.intercept(), `navigation.transition` is non-null until the\n * handler resolves. This provides browser-native progress tracking that\n * can be used alongside the existing pendingUrl mechanism.\n *\n * Returns false when Navigation API is unavailable or no transition is active.\n */\nexport function hasNativeNavigationTransition(): boolean {\n if (typeof window === 'undefined') return false;\n const nav = (window as unknown as { navigation?: { transition?: unknown } }).navigation;\n return nav?.transition != null;\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['segmentParams']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * During SSR, params are read from the ALS-backed SSR data context\n * (populated by ssr-entry.ts) to ensure correct per-request isolation\n * across concurrent requests with streaming Suspense.\n *\n * Reactivity: On the client, useParams() reads from NavigationContext\n * which is updated atomically with the RSC tree render. This replaces\n * the previous useSyncExternalStore approach that suffered from a\n * timing gap between tree render and store notification — causing\n * preserved layout components to briefly show stale active state.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { Routes } from '../index.js';\nimport { getSsrData } from './ssr-data.js';\nimport { currentParams, _setCurrentParams, paramsListeners } from './state.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n// ---------------------------------------------------------------------------\n// Module-level subscribe/notify pattern — kept for backward compat and tests\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to params changes.\n * Retained for backward compatibility with tests that verify the\n * subscribe/notify contract. On the client, useParams() reads from\n * NavigationContext instead.\n */\nexport function subscribe(callback: () => void): () => void {\n paramsListeners.add(callback);\n return () => paramsListeners.delete(callback);\n}\n\n/**\n * Get the current params snapshot (module-level fallback).\n * Used by tests and by the hook when called outside a React component.\n */\nexport function getSnapshot(): Record<string, string | string[]> {\n return currentParams;\n}\n\n// ---------------------------------------------------------------------------\n// Framework API — called by the segment router on each navigation\n// ---------------------------------------------------------------------------\n\n/**\n * Set the current route params in the module-level store.\n *\n * Called by the router on each navigation. This updates the fallback\n * snapshot used by tests and by the hook when called outside a React\n * component (no NavigationContext available).\n *\n * On the client, the primary reactivity path is NavigationContext —\n * the router calls setNavigationState() then renderRoot() which wraps\n * the element in NavigationProvider. setCurrentParams is still called\n * for the module-level fallback.\n *\n * During SSR, params are also available via getSsrData().params\n * (ALS-backed).\n */\nexport function setCurrentParams(params: Record<string, string | string[]>): void {\n _setCurrentParams(params);\n}\n\n/**\n * Notify all legacy subscribers that params have changed.\n *\n * Retained for backward compatibility with tests. On the client,\n * the NavigationContext + renderRoot pattern replaces this — params\n * update atomically with the tree render, so explicit notification\n * is no longer needed.\n */\nexport function notifyParamsListeners(): void {\n for (const listener of paramsListeners) {\n listener();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public hook\n// ---------------------------------------------------------------------------\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). This ensures params update\n * atomically with the RSC tree — no timing gap.\n *\n * During SSR, reads from the ALS-backed SSR data context to ensure\n * per-request isolation across concurrent requests with streaming Suspense.\n *\n * When called outside a React component (e.g., in test assertions),\n * falls back to the module-level snapshot.\n *\n * @overload Typed — when a known segment path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useSegmentParams<R extends keyof Routes>(\n segmentPath: R\n): Routes[R] extends { segmentParams: infer P } ? P : Record<string, string | string[]>;\nexport function useSegmentParams(segmentPath?: string): Record<string, string | string[]>;\nexport function useSegmentParams(_segmentPath?: string): Record<string, string | string[]> {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n // When called outside a React component, useContext throws — caught below.\n try {\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.params;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to module-level snapshot below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n // Falls back to module-level currentParams for tests.\n return getSsrData()?.params ?? currentParams;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,IAAM,cAAc,OAAO,IAAI,mBAAmB;AAClD,IAAM,kBAAkB,OAAO,IAAI,2BAA2B;AAE9D,SAAS,qBAAwE;CAC/E,MAAM,WAAY,WAAuC;AAGzD,KAAI,aAAa,KAAA,EAAW,QAAO;AAEnC,KAAI,OAAO,MAAM,kBAAkB,YAAY;EAC7C,MAAM,MAAM,MAAM,cAAsC,KAAK;AAC5D,aAAuC,eAAe;AACvD,SAAO;;;;;;;;AAUX,SAAgB,uBAA+C;CAC7D,MAAM,MAAM,oBAAoB;AAChC,KAAI,CAAC,IAAK,QAAO;AAEjB,KAAI,OAAO,MAAM,eAAe,WAAY,QAAO;AACnD,QAAO,MAAM,WAAW,IAAI;;;;;;;;AAkB9B,SAAgB,mBAAmB,EACjC,OACA,YAC8C;CAC9C,MAAM,MAAM,oBAAoB;AAChC,KAAI,CAAC,IAEH,QAAO;AAET,QAAO,cAAc,IAAI,UAAU,EAAE,OAAO,EAAE,SAAS;;;;;;;;;;;;;;;AAoBzD,IAAM,gBAAgB,OAAO,IAAI,qBAAqB;AAEtD,SAAS,oBAAkD;CACzD,MAAM,IAAI;AACV,KAAI,CAAC,EAAE,eACL,GAAE,iBAAiB,EAAE,SAAS;EAAE,QAAQ,EAAE;EAAE,UAAU;EAAK,EAAE;AAE/D,QAAO,EAAE;;AAGX,SAAgB,mBAAmB,OAA8B;AAC/D,oBAAmB,CAAC,UAAU;;AAGhC,SAAgB,qBAAsC;AACpD,QAAO,mBAAmB,CAAC;;;;;;;;;;;;AAkB7B,SAAS,4BAAsE;CAC7E,MAAM,WAAY,WAAuC;AAGzD,KAAI,aAAa,KAAA,EAAW,QAAO;AACnC,KAAI,OAAO,MAAM,kBAAkB,YAAY;EAC7C,MAAM,MAAM,MAAM,cAA6B,KAAK;AACnD,aAAuC,mBAAmB;AAC3D,SAAO;;;;;;;AASX,SAAgB,0BAAyC;CACvD,MAAM,MAAM,2BAA2B;AACvC,KAAI,CAAC,IAAK,QAAO;AACjB,KAAI,OAAO,MAAM,eAAe,WAAY,QAAO;AACnD,QAAO,MAAM,WAAW,IAAI;;;;;;;;;;;;;;;;;;;AC7H9B,SAAgB,iBAAiB,QAAiD;AAChF,mBAAkB,OAAO;;AA6C3B,SAAgB,iBAAiB,cAA0D;AAIzF,KAAI;EACF,MAAM,aAAa,sBAAsB;AACzC,MAAI,eAAe,KACjB,QAAO,WAAW;SAEd;AAOR,QAAO,YAAY,EAAE,UAAU"}