@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.
- package/dist/search-params/create.d.ts +18 -0
- package/dist/search-params/create.d.ts.map +1 -1
- package/dist/search-params/index.js +5 -2
- package/dist/search-params/index.js.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/browser-entry.ts +10 -0
- package/src/search-params/create.ts +40 -8
- package/src/server/error-formatter.ts +12 -0
- package/src/server/rsc-entry/index.ts +24 -0
- package/src/server/ssr-entry.ts +11 -0
|
@@ -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;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;
|
|
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.
|
|
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
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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 };
|
package/src/server/ssr-entry.ts
CHANGED
|
@@ -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
|
*
|