@timber-js/app 0.1.5 → 0.1.7

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":"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"}
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 +1 @@
1
- {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA6BH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAwEnB;AAED,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAwCH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAwEnB;AAED,eAAe,SAAS,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",
@@ -38,6 +38,16 @@ import { TimberNuqsAdapter } from './nuqs-adapter.js';
38
38
  import { isPageUnloading } from './unload-guard.js';
39
39
  import { setCurrentParams } from './use-params.js';
40
40
  import { ON_NAVIGATE_KEY } from './link-navigate-interceptor.js';
41
+ import { registerUseQueryStatesHook } from '#/search-params/create.js';
42
+ import { useQueryStates as clientUseQueryStates } from './use-query-states.js';
43
+
44
+ // ─── useQueryStates Registration ─────────────────────────────────
45
+ // Register the client-side useQueryStates implementation so that
46
+ // definition.useQueryStates() works in 'use client' components.
47
+ // See LOCAL-297.
48
+ registerUseQueryStatesHook((codecs, options, urlKeys) => {
49
+ return clientUseQueryStates(codecs, options, urlKeys);
50
+ });
41
51
 
42
52
  // ─── Server Action Dispatch ──────────────────────────────────────
43
53
 
@@ -106,6 +106,33 @@ export interface SearchParamsDefinition<T extends Record<string, unknown>> {
106
106
  readonly _type?: T;
107
107
  }
108
108
 
109
+ // ---------------------------------------------------------------------------
110
+ // useQueryStates hook registration
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /**
114
+ * The registered useQueryStates implementation.
115
+ *
116
+ * null in the RSC environment (hooks can't run during server rendering).
117
+ * Set by:
118
+ * - browser-entry.ts → registers the nuqs-backed client implementation
119
+ * - ssr-entry.ts → registers a static SSR adapter
120
+ *
121
+ * The registration pattern avoids importing client-only code (nuqs, React hooks)
122
+ * into the shared search-params module, which is loaded in all three environments.
123
+ */
124
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
125
+ type UseQueryStatesHook = (codecs: any, options: any, urlKeys: any) => [any, any];
126
+ let _useQueryStatesHook: UseQueryStatesHook | null = null;
127
+
128
+ /**
129
+ * Register the runtime useQueryStates implementation.
130
+ * Called by browser-entry (client) and ssr-entry (SSR).
131
+ */
132
+ export function registerUseQueryStatesHook(hook: UseQueryStatesHook): void {
133
+ _useQueryStatesHook = hook;
134
+ }
135
+
109
136
  // ---------------------------------------------------------------------------
110
137
  // Internal helpers
111
138
  // ---------------------------------------------------------------------------
@@ -288,14 +315,19 @@ function buildDefinition<T extends Record<string, unknown>>(
288
315
  }
289
316
 
290
317
  // ---- useQueryStates ----
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
- );
318
+ // Delegates to the registered runtime hook (client or SSR).
319
+ // In the RSC environment, no hook is registered — calling this throws.
320
+ // On the client, browser-entry.ts registers the nuqs-backed implementation.
321
+ // In SSR, ssr-entry.ts registers a static adapter.
322
+ function useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>] {
323
+ const hook = _useQueryStatesHook;
324
+ if (!hook) {
325
+ throw new Error(
326
+ 'useQueryStates() can only be called in a client component. ' +
327
+ 'Import from @timber-js/app/client instead.'
328
+ );
329
+ }
330
+ return hook(codecMap, options, Object.freeze({ ...urlKeys })) as [T, SetParams<T>];
299
331
  }
300
332
 
301
333
  const definition: SearchParamsDefinition<T> = {
@@ -147,6 +147,18 @@ 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
+
150
162
  return null;
151
163
  }
152
164
 
@@ -409,6 +409,30 @@ 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
+
412
436
  // Track unhandled errors for pre-flush handling (500 status)
413
437
  if (!renderError) {
414
438
  renderError = { error, status: 500 };
@@ -25,6 +25,8 @@ import { withNuqsSsrAdapter } from './nuqs-ssr-provider.js';
25
25
  import { withSpan } from './tracing.js';
26
26
  import { setCurrentParams } from '#/client/use-params.js';
27
27
  import { registerSsrDataProvider, type SsrData } from '#/client/ssr-data.js';
28
+ import { registerUseQueryStatesHook } from '#/search-params/create.js';
29
+ import { useQueryStates as clientUseQueryStates } from '#/client/use-query-states.js';
28
30
 
29
31
  // ─── SSR Data ALS ─────────────────────────────────────────────────────────
30
32
  //
@@ -39,6 +41,15 @@ const ssrDataAls = new AsyncLocalStorage<SsrData>();
39
41
  // Register the ALS-backed provider so getSsrData() reads from ALS.
40
42
  registerSsrDataProvider(() => ssrDataAls.getStore());
41
43
 
44
+ // Register the useQueryStates hook for SSR.
45
+ // During SSR, 'use client' components that call definition.useQueryStates()
46
+ // need the real nuqs-backed implementation (which uses React hooks that work
47
+ // during SSR). Without this, the placeholder in create.ts throws.
48
+ // See LOCAL-297.
49
+ registerUseQueryStatesHook((codecs, options, urlKeys) => {
50
+ return clientUseQueryStates(codecs, options, urlKeys);
51
+ });
52
+
42
53
  /**
43
54
  * Navigation context passed from the RSC environment to SSR.
44
55
  *