@timber-js/app 0.1.10 → 0.1.11

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 (53) hide show
  1. package/dist/_chunks/request-context-BzES06i1.js.map +1 -1
  2. package/dist/_chunks/use-query-states-wEXY2JQB.js +109 -0
  3. package/dist/_chunks/use-query-states-wEXY2JQB.js.map +1 -0
  4. package/dist/client/index.js +1 -83
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/use-query-states.d.ts.map +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +5 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/plugins/entries.d.ts.map +1 -1
  11. package/dist/plugins/routing.d.ts.map +1 -1
  12. package/dist/plugins/server-bundle.d.ts.map +1 -1
  13. package/dist/routing/status-file-lint.d.ts.map +1 -1
  14. package/dist/search-params/create.d.ts.map +1 -1
  15. package/dist/search-params/index.js +13 -4
  16. package/dist/search-params/index.js.map +1 -1
  17. package/dist/server/fallback-error.d.ts +28 -0
  18. package/dist/server/fallback-error.d.ts.map +1 -0
  19. package/dist/server/html-injectors.d.ts.map +1 -1
  20. package/dist/server/index.js +4 -0
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/pipeline.d.ts +12 -0
  23. package/dist/server/pipeline.d.ts.map +1 -1
  24. package/dist/server/request-context.d.ts.map +1 -1
  25. package/dist/server/route-matcher.d.ts.map +1 -1
  26. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  27. package/dist/server/slot-resolver.d.ts +1 -1
  28. package/dist/server/slot-resolver.d.ts.map +1 -1
  29. package/dist/server/ssr-entry.d.ts.map +1 -1
  30. package/dist/server/tree-builder.d.ts.map +1 -1
  31. package/package.json +23 -23
  32. package/src/client/browser-entry.ts +1 -1
  33. package/src/client/use-query-states.ts +13 -1
  34. package/src/index.ts +16 -16
  35. package/src/plugins/dev-server.ts +3 -1
  36. package/src/plugins/entries.ts +2 -1
  37. package/src/plugins/routing.ts +5 -4
  38. package/src/plugins/server-bundle.ts +15 -6
  39. package/src/routing/status-file-lint.ts +1 -3
  40. package/src/search-params/create.ts +15 -8
  41. package/src/server/error-formatter.ts +12 -0
  42. package/src/server/fallback-error.ts +159 -0
  43. package/src/server/html-injectors.ts +9 -4
  44. package/src/server/pipeline.ts +24 -0
  45. package/src/server/request-context.ts +0 -1
  46. package/src/server/route-matcher.ts +1 -4
  47. package/src/server/rsc-entry/index.ts +88 -36
  48. package/src/server/slot-resolver.ts +38 -5
  49. package/src/server/ssr-entry.ts +4 -1
  50. package/src/server/tree-builder.ts +4 -1
  51. package/src/shims/server-only-noop.js +1 -0
  52. package/dist/_chunks/registry-BfPM41ri.js +0 -20
  53. package/dist/_chunks/registry-BfPM41ri.js.map +0 -1
@@ -53,7 +53,6 @@ export const requestContextAls = new AsyncLocalStorage<RequestContextStore>();
53
53
  // the ALS context persists for the entire request lifecycle including
54
54
  // async stream consumption by React's renderToReadableStream.
55
55
 
56
-
57
56
  // ─── Cookie Signing Secrets ──────────────────────────────────────────────
58
57
 
59
58
  /**
@@ -10,10 +10,7 @@
10
10
 
11
11
  import type { RouteMatch } from './pipeline.js';
12
12
  import type { MiddlewareFn } from './middleware-runner.js';
13
- import {
14
- METADATA_ROUTE_CONVENTIONS,
15
- type MetadataRouteType,
16
- } from './metadata-routes.js';
13
+ import { METADATA_ROUTE_CONVENTIONS, type MetadataRouteType } from './metadata-routes.js';
17
14
 
18
15
  // ─── Manifest Types ───────────────────────────────────────────────────────
19
16
  // The virtual module manifest has a slightly different shape than SegmentNode:
@@ -24,49 +24,49 @@ import buildManifest from 'virtual:timber-build-manifest';
24
24
 
25
25
  import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc';
26
26
 
27
- import React, { createElement } from 'react';
28
- import { createPipeline } from '#/server/pipeline.js';
29
- import { initDevTracing } from '#/server/tracing.js';
30
- import type { PipelineConfig, RouteMatch, InterceptionContext } from '#/server/pipeline.js';
31
- import { logRenderError } from '#/server/logger.js';
32
- import { resolveLogMode } from '#/server/dev-logger.js';
33
- import { createRouteMatcher, createMetadataRouteMatcher } from '#/server/route-matcher.js';
34
- import type { ManifestSegmentNode } from '#/server/route-matcher.js';
35
- import { DenySignal, RedirectSignal, RenderError, SsrStreamError } from '#/server/primitives.js';
36
- import { buildClientScripts } from '#/server/html-injectors.js';
37
- import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
38
- import { renderDenyPage, renderDenyPageAsRsc } from '#/server/deny-renderer.js';
39
- import type { LayoutEntry } from '#/server/deny-renderer.js';
27
+ import type { FormRerender } from '#/server/action-handler.js';
28
+ import { handleActionRequest, isActionRequest } from '#/server/action-handler.js';
29
+ import type { BodyLimitsConfig } from '#/server/body-limits.js';
30
+ import type { BuildManifest } from '#/server/build-manifest.js';
40
31
  import {
41
- collectRouteCss,
42
- collectRouteFonts,
43
- collectRouteModulepreloads,
44
32
  buildCssLinkTags,
45
33
  buildFontPreloadTags,
46
34
  buildModulepreloadTags,
35
+ collectRouteCss,
36
+ collectRouteFonts,
37
+ collectRouteModulepreloads,
47
38
  } from '#/server/build-manifest.js';
48
- import type { BuildManifest } from '#/server/build-manifest.js';
49
- import { collectEarlyHintHeaders } from '#/server/early-hints.js';
39
+ import type { LayoutEntry } from '#/server/deny-renderer.js';
40
+ import { renderDenyPage, renderDenyPageAsRsc } from '#/server/deny-renderer.js';
41
+ import { resolveLogMode } from '#/server/dev-logger.js';
50
42
  import { sendEarlyHints103 } from '#/server/early-hints-sender.js';
51
- import type { NavContext } from '#/server/ssr-entry.js';
52
- import { buildRouteElement, RouteSignalWithContext } from '#/server/route-element-builder.js';
53
- import { isActionRequest, handleActionRequest } from '#/server/action-handler.js';
54
- import type { FormRerender } from '#/server/action-handler.js';
55
- import type { BodyLimitsConfig } from '#/server/body-limits.js';
43
+ import { collectEarlyHintHeaders } from '#/server/early-hints.js';
56
44
  import { runWithFormFlash } from '#/server/form-flash.js';
45
+ import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
46
+ import { buildClientScripts } from '#/server/html-injectors.js';
47
+ import { logRenderError } from '#/server/logger.js';
48
+ import type { InterceptionContext, PipelineConfig, RouteMatch } from '#/server/pipeline.js';
49
+ import { createPipeline } from '#/server/pipeline.js';
50
+ import { DenySignal, RedirectSignal, RenderError, SsrStreamError } from '#/server/primitives.js';
51
+ import { buildRouteElement, RouteSignalWithContext } from '#/server/route-element-builder.js';
52
+ import type { ManifestSegmentNode } from '#/server/route-matcher.js';
53
+ import { createMetadataRouteMatcher, createRouteMatcher } from '#/server/route-matcher.js';
54
+ import type { NavContext } from '#/server/ssr-entry.js';
55
+ import { initDevTracing } from '#/server/tracing.js';
57
56
 
57
+ import { renderFallbackError as renderFallback } from '#/server/fallback-error.js';
58
+ import { handleApiRoute } from './api-handler.js';
59
+ import { renderErrorPage, renderNoMatchPage } from './error-renderer.js';
58
60
  import {
59
- createDebugChannelSink,
60
- buildSegmentInfo,
61
- isRscPayloadRequest,
62
61
  buildRedirectResponse,
62
+ buildSegmentInfo,
63
+ createDebugChannelSink,
63
64
  escapeHtml,
64
65
  isAbortError,
66
+ isRscPayloadRequest,
65
67
  parseCookiesFromHeader,
66
68
  RSC_CONTENT_TYPE,
67
69
  } from './helpers.js';
68
- import { handleApiRoute } from './api-handler.js';
69
- import { renderErrorPage, renderNoMatchPage } from './error-renderer.js';
70
70
  import { callSsr } from './ssr-bridge.js';
71
71
 
72
72
  // Dev-only pipeline error handler, set by the dev server after import.
@@ -183,6 +183,8 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
183
183
  if (_devPipelineErrorHandler) _devPipelineErrorHandler(error, phase);
184
184
  }
185
185
  : undefined,
186
+ renderFallbackError: (error, req, responseHeaders) =>
187
+ renderFallback(error, req, responseHeaders, isDev, manifest.root, clientBootstrap),
186
188
  };
187
189
 
188
190
  const pipeline = createPipeline(pipelineConfig);
@@ -409,6 +411,30 @@ async function renderRoute(
409
411
  status: error.status,
410
412
  });
411
413
  }
414
+ // Dev diagnostic: detect "Invalid hook call" errors which indicate
415
+ // a 'use client' component is being executed during RSC rendering
416
+ // instead of being serialized as a client reference. This happens when
417
+ // the RSC plugin's transform doesn't detect the directive — e.g., the
418
+ // directive isn't at the very top of the file, or the component is
419
+ // re-exported through a barrel file without 'use client'.
420
+ // See LOCAL-297.
421
+ if (
422
+ process.env.NODE_ENV !== 'production' &&
423
+ error instanceof Error &&
424
+ error.message.includes('Invalid hook call')
425
+ ) {
426
+ console.error(
427
+ '[timber] A React hook was called during RSC rendering. This usually means a ' +
428
+ "'use client' component is being executed as a server component instead of " +
429
+ 'being serialized as a client reference.\n\n' +
430
+ 'Common causes:\n' +
431
+ " 1. The 'use client' directive is not the FIRST statement in the file (before any imports)\n" +
432
+ " 2. The component is re-exported through a barrel file (index.ts) that lacks 'use client'\n" +
433
+ ' 3. @vitejs/plugin-rsc is not loaded or is misconfigured\n\n' +
434
+ `Request: ${_req.method} ${new URL(_req.url).pathname}`
435
+ );
436
+ }
437
+
412
438
  // Track unhandled errors for pre-flush handling (500 status)
413
439
  if (!renderError) {
414
440
  renderError = { error, status: 500 };
@@ -640,15 +666,28 @@ async function renderRoute(
640
666
  if (sig) return buildRedirectResponse(_req, sig, responseHeaders);
641
667
  if (denySignal) {
642
668
  return renderDenyPage(
643
- denySignal, segments, layoutComponents as LayoutEntry[],
644
- _req, match, responseHeaders, clientBootstrap, createDebugChannelSink, callSsr
669
+ denySignal,
670
+ segments,
671
+ layoutComponents as LayoutEntry[],
672
+ _req,
673
+ match,
674
+ responseHeaders,
675
+ clientBootstrap,
676
+ createDebugChannelSink,
677
+ callSsr
645
678
  );
646
679
  }
647
680
  const err = renderError as { error: unknown; status: number } | null;
648
681
  if (err) {
649
682
  return renderErrorPage(
650
- err.error, err.status, segments, layoutComponents as LayoutEntry[],
651
- _req, match, responseHeaders, clientBootstrap
683
+ err.error,
684
+ err.status,
685
+ segments,
686
+ layoutComponents as LayoutEntry[],
687
+ _req,
688
+ match,
689
+ responseHeaders,
690
+ clientBootstrap
652
691
  );
653
692
  }
654
693
  return null;
@@ -688,15 +727,28 @@ async function renderRoute(
688
727
  if (denySignal) {
689
728
  // Render deny page without layouts — pass empty layout list
690
729
  return renderDenyPage(
691
- denySignal, segments, [] as LayoutEntry[],
692
- _req, match, responseHeaders, clientBootstrap, createDebugChannelSink, callSsr
730
+ denySignal,
731
+ segments,
732
+ [] as LayoutEntry[],
733
+ _req,
734
+ match,
735
+ responseHeaders,
736
+ clientBootstrap,
737
+ createDebugChannelSink,
738
+ callSsr
693
739
  );
694
740
  }
695
741
  const err = renderError as { error: unknown; status: number } | null;
696
742
  if (err) {
697
743
  return renderErrorPage(
698
- err.error, err.status, segments, [] as LayoutEntry[],
699
- _req, match, responseHeaders, clientBootstrap
744
+ err.error,
745
+ err.status,
746
+ segments,
747
+ [] as LayoutEntry[],
748
+ _req,
749
+ match,
750
+ responseHeaders,
751
+ clientBootstrap
700
752
  );
701
753
  }
702
754
  // No captured signal — return bare 500
@@ -16,12 +16,13 @@
16
16
  * See design/02-rendering-pipeline.md §"Parallel Slots"
17
17
  */
18
18
 
19
- import type { ManifestSegmentNode } from './route-matcher.js';
20
- import type { RouteMatch, InterceptionContext } from './pipeline.js';
21
- import { SlotAccessGate } from './access-gate.js';
22
- import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
23
19
  import { TimberErrorBoundary } from '#/client/error-boundary.js';
24
20
  import SlotErrorFallback from '#/client/slot-error-fallback.js';
21
+ import { SlotAccessGate } from './access-gate.js';
22
+ import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
23
+ import type { InterceptionContext, RouteMatch } from './pipeline.js';
24
+ import { DenySignal } from './primitives.js';
25
+ import type { ManifestSegmentNode } from './route-matcher.js';
25
26
 
26
27
  type CreateElementFn = (...args: unknown[]) => React.ReactElement;
27
28
 
@@ -56,7 +57,39 @@ export async function resolveSlotElement(
56
57
  const mod = (await slotMatch.page.load()) as Record<string, unknown>;
57
58
  if (mod.default) {
58
59
  const SlotPage = mod.default as (...args: unknown[]) => unknown;
59
- let element: React.ReactElement = h(SlotPage, {
60
+
61
+ // Load default.tsx fallback for notFound() handling in the slot page.
62
+ // When a slot page calls notFound() or deny(), it should gracefully
63
+ // degrade to default.tsx or null — not crash the page. This matches
64
+ // Next.js behavior. See design/02-rendering-pipeline.md
65
+ // §"Slot Access Failure = Graceful Degradation"
66
+ let denyFallback: React.ReactElement | null = null;
67
+ if (slotNode.default) {
68
+ const defaultMod = (await slotNode.default.load()) as Record<string, unknown>;
69
+ const DefaultComp = defaultMod.default as ((...args: unknown[]) => unknown) | undefined;
70
+ if (DefaultComp) {
71
+ denyFallback = h(DefaultComp, { params: paramsPromise, searchParams: {} });
72
+ }
73
+ }
74
+
75
+ // Wrap the slot page to catch DenySignal (from notFound() or deny())
76
+ // at the component level. This prevents the signal from reaching the
77
+ // RSC onError callback and being tracked as a page-level denial, which
78
+ // would cause the pipeline to replace the entire successful SSR response
79
+ // with a deny page. Instead, the slot gracefully degrades.
80
+ const denyFallbackCapture = denyFallback;
81
+ const SafeSlotPage = async (props: Record<string, unknown>) => {
82
+ try {
83
+ return await (SlotPage as (props: Record<string, unknown>) => unknown)(props);
84
+ } catch (error) {
85
+ if (error instanceof DenySignal) {
86
+ return denyFallbackCapture;
87
+ }
88
+ throw error;
89
+ }
90
+ };
91
+
92
+ let element: React.ReactElement = h(SafeSlotPage, {
60
93
  params: paramsPromise,
61
94
  searchParams: {},
62
95
  });
@@ -156,7 +156,10 @@ export async function handleSsr(
156
156
  // Wrap in SsrStreamError so the RSC entry can handle it without
157
157
  // re-executing server components via renderDenyPage.
158
158
  // See LOCAL-293.
159
- console.error('[timber] SSR shell failed from RSC stream error:', formatSsrError(renderError));
159
+ console.error(
160
+ '[timber] SSR shell failed from RSC stream error:',
161
+ formatSsrError(renderError)
162
+ );
160
163
  throw new SsrStreamError(
161
164
  'SSR renderToReadableStream failed due to RSC stream error',
162
165
  renderError
@@ -79,7 +79,10 @@ export interface AccessGateProps {
79
79
  * - 'pass': render children
80
80
  * - DenySignal/RedirectSignal: throw synchronously
81
81
  */
82
- verdict?: 'pass' | import('./primitives.js').DenySignal | import('./primitives.js').RedirectSignal;
82
+ verdict?:
83
+ | 'pass'
84
+ | import('./primitives.js').DenySignal
85
+ | import('./primitives.js').RedirectSignal;
83
86
  children: ReactElement;
84
87
  }
85
88
 
@@ -1,3 +1,4 @@
1
+ // oxlint-disable
1
2
  // No-op shim for server-only/client-only packages.
2
3
  // In dev mode, Vite externalizes node_modules and loads them via Node's
3
4
  // require(). Deps like `bright` that import `server-only` would hit the
@@ -1,20 +0,0 @@
1
- //#region src/search-params/registry.ts
2
- var registry = /* @__PURE__ */ new Map();
3
- /**
4
- * Register a route's search params definition.
5
- * Called by the generated route manifest loader when a route's modules load.
6
- */
7
- function registerSearchParams(route, definition) {
8
- registry.set(route, definition);
9
- }
10
- /**
11
- * Look up a route's search params definition.
12
- * Returns undefined if the route hasn't been loaded yet.
13
- */
14
- function getSearchParams(route) {
15
- return registry.get(route);
16
- }
17
- //#endregion
18
- export { registerSearchParams as n, getSearchParams as t };
19
-
20
- //# sourceMappingURL=registry-BfPM41ri.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"registry-BfPM41ri.js","names":[],"sources":["../../src/search-params/registry.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"],"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"}