@timber-js/app 0.2.0-alpha.147 → 0.2.0-alpha.149

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 (277) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
  3. package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
  4. package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
  5. package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
  6. package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
  7. package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
  8. package/dist/_chunks/{cli-schema-sync-CQaxupHv.js → cli-schema-sync-DJDdyEae.js} +111 -260
  9. package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
  10. package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
  11. package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
  12. package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
  13. package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
  14. package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
  15. package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
  16. package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
  17. package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
  18. package/dist/_chunks/logger-CM0clugc.js.map +1 -0
  19. package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
  20. package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
  21. package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
  22. package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
  23. package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
  24. package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
  25. package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
  26. package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
  27. package/dist/_chunks/tree-match-D2l830j2.js +102 -0
  28. package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
  29. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
  30. package/dist/_chunks/{walkers-Ck4Y45XN.js → walkers-DvWDj453.js} +107 -82
  31. package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
  32. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  33. package/dist/adapters/cloudflare-kv-cache.js +36 -14
  34. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  35. package/dist/adapters/compress-module.d.ts +5 -4
  36. package/dist/adapters/compress-module.d.ts.map +1 -1
  37. package/dist/adapters/nitro.d.ts +1 -3
  38. package/dist/adapters/nitro.d.ts.map +1 -1
  39. package/dist/adapters/nitro.js +14 -134
  40. package/dist/adapters/nitro.js.map +1 -1
  41. package/dist/cache/cache-api.d.ts.map +1 -1
  42. package/dist/cache/index.d.ts +3 -1
  43. package/dist/cache/index.d.ts.map +1 -1
  44. package/dist/cache/index.js +20 -235
  45. package/dist/cache/index.js.map +1 -1
  46. package/dist/cache/invalidation-epoch.d.ts +43 -0
  47. package/dist/cache/invalidation-epoch.d.ts.map +1 -0
  48. package/dist/cache/redis-handler.d.ts +13 -0
  49. package/dist/cache/redis-handler.d.ts.map +1 -1
  50. package/dist/cache/timber-cache.d.ts +17 -9
  51. package/dist/cache/timber-cache.d.ts.map +1 -1
  52. package/dist/cli-schema-sync.d.ts +21 -1
  53. package/dist/cli-schema-sync.d.ts.map +1 -1
  54. package/dist/cli.d.ts +12 -0
  55. package/dist/cli.d.ts.map +1 -1
  56. package/dist/cli.js +26 -31
  57. package/dist/cli.js.map +1 -1
  58. package/dist/client/browser-entry/hmr.d.ts.map +1 -1
  59. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  60. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  61. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  62. package/dist/client/history.d.ts +10 -17
  63. package/dist/client/history.d.ts.map +1 -1
  64. package/dist/client/index.js +1 -1
  65. package/dist/client/index.js.map +1 -1
  66. package/dist/client/internal.js +30 -33
  67. package/dist/client/internal.js.map +1 -1
  68. package/dist/client/navigation-api.d.ts +9 -2
  69. package/dist/client/navigation-api.d.ts.map +1 -1
  70. package/dist/client/navigation-context.d.ts +0 -11
  71. package/dist/client/navigation-context.d.ts.map +1 -1
  72. package/dist/client/navigation-root.d.ts +0 -5
  73. package/dist/client/navigation-root.d.ts.map +1 -1
  74. package/dist/client/router.d.ts.map +1 -1
  75. package/dist/client/rsc-fetch.d.ts.map +1 -1
  76. package/dist/client/use-search-params.d.ts +10 -5
  77. package/dist/client/use-search-params.d.ts.map +1 -1
  78. package/dist/codec.js +1 -1
  79. package/dist/config-validation.d.ts.map +1 -1
  80. package/dist/cookies/define-cookie.d.ts.map +1 -1
  81. package/dist/cookies/index.js +2 -4
  82. package/dist/cookies/index.js.map +1 -1
  83. package/dist/dev-tools/dev-page-shell.d.ts +1 -0
  84. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
  85. package/dist/dev-tools/hydration-overlay.d.ts +11 -0
  86. package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
  87. package/dist/dev-tools/index.d.ts +1 -2
  88. package/dist/dev-tools/index.d.ts.map +1 -1
  89. package/dist/fonts/ast-walk.d.ts +68 -0
  90. package/dist/fonts/ast-walk.d.ts.map +1 -0
  91. package/dist/fonts/ast.d.ts +23 -11
  92. package/dist/fonts/ast.d.ts.map +1 -1
  93. package/dist/fonts/bundle.d.ts +3 -1
  94. package/dist/fonts/bundle.d.ts.map +1 -1
  95. package/dist/fonts/dev-middleware.d.ts +1 -1
  96. package/dist/fonts/dev-middleware.d.ts.map +1 -1
  97. package/dist/fonts/fallbacks.d.ts +0 -7
  98. package/dist/fonts/fallbacks.d.ts.map +1 -1
  99. package/dist/fonts/google.d.ts +4 -2
  100. package/dist/fonts/google.d.ts.map +1 -1
  101. package/dist/fonts/local.d.ts +3 -3
  102. package/dist/fonts/local.d.ts.map +1 -1
  103. package/dist/fonts/paths.d.ts +43 -0
  104. package/dist/fonts/paths.d.ts.map +1 -0
  105. package/dist/fonts/pipeline.d.ts +7 -21
  106. package/dist/fonts/pipeline.d.ts.map +1 -1
  107. package/dist/fonts/transform.d.ts.map +1 -1
  108. package/dist/fonts/virtual-modules.d.ts +1 -18
  109. package/dist/fonts/virtual-modules.d.ts.map +1 -1
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +610 -406
  112. package/dist/index.js.map +1 -1
  113. package/dist/params/index.js +1 -1
  114. package/dist/params/resolve-schema.d.ts.map +1 -1
  115. package/dist/plugin-context.d.ts +28 -5
  116. package/dist/plugin-context.d.ts.map +1 -1
  117. package/dist/plugins/cache.d.ts +13 -5
  118. package/dist/plugins/cache.d.ts.map +1 -1
  119. package/dist/plugins/dev-server.d.ts.map +1 -1
  120. package/dist/plugins/fonts.d.ts +1 -2
  121. package/dist/plugins/fonts.d.ts.map +1 -1
  122. package/dist/plugins/routing.d.ts.map +1 -1
  123. package/dist/plugins/server-bundle.d.ts.map +1 -1
  124. package/dist/routing/codegen.d.ts.map +1 -1
  125. package/dist/routing/convention-lint.d.ts.map +1 -1
  126. package/dist/routing/file-cache.d.ts +8 -0
  127. package/dist/routing/file-cache.d.ts.map +1 -0
  128. package/dist/routing/index.js +2 -2
  129. package/dist/routing/interception.d.ts +0 -5
  130. package/dist/routing/interception.d.ts.map +1 -1
  131. package/dist/routing/manifest-codegen.d.ts +32 -0
  132. package/dist/routing/manifest-codegen.d.ts.map +1 -0
  133. package/dist/routing/scanner.d.ts.map +1 -1
  134. package/dist/schema-bridge.d.ts +15 -0
  135. package/dist/schema-bridge.d.ts.map +1 -1
  136. package/dist/search-params/define.d.ts.map +1 -1
  137. package/dist/search-params/index.js +7 -3
  138. package/dist/search-params/index.js.map +1 -1
  139. package/dist/search-params/wrappers.d.ts.map +1 -1
  140. package/dist/segment-params/define.d.ts.map +1 -1
  141. package/dist/segment-params/index.js +1 -1
  142. package/dist/server/access-gate.d.ts.map +1 -1
  143. package/dist/server/actions.d.ts +0 -14
  144. package/dist/server/actions.d.ts.map +1 -1
  145. package/dist/server/build-manifest.d.ts +0 -29
  146. package/dist/server/build-manifest.d.ts.map +1 -1
  147. package/dist/server/canonicalize.d.ts +10 -0
  148. package/dist/server/canonicalize.d.ts.map +1 -1
  149. package/dist/server/deny-renderer.d.ts +1 -1
  150. package/dist/server/deny-renderer.d.ts.map +1 -1
  151. package/dist/server/html-injector-core.d.ts +8 -0
  152. package/dist/server/html-injector-core.d.ts.map +1 -1
  153. package/dist/server/html-injectors.d.ts.map +1 -1
  154. package/dist/server/index.js +2 -2
  155. package/dist/server/internal.d.ts +1 -5
  156. package/dist/server/internal.d.ts.map +1 -1
  157. package/dist/server/internal.js +12 -171
  158. package/dist/server/internal.js.map +1 -1
  159. package/dist/server/node-stream-transforms.d.ts +0 -11
  160. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  161. package/dist/server/render-utils.d.ts +25 -0
  162. package/dist/server/render-utils.d.ts.map +1 -0
  163. package/dist/server/route-element-builder.d.ts +11 -25
  164. package/dist/server/route-element-builder.d.ts.map +1 -1
  165. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  166. package/dist/server/rsc-entry/helpers.d.ts +2 -18
  167. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  168. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  169. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  170. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  171. package/dist/server/ssr-render.d.ts.map +1 -1
  172. package/dist/shared/href-validation.d.ts.map +1 -1
  173. package/dist/shims/image.d.ts +15 -15
  174. package/package.json +7 -8
  175. package/src/adapters/cloudflare-kv-cache.ts +56 -19
  176. package/src/adapters/compress-module.ts +27 -118
  177. package/src/adapters/nitro.ts +3 -41
  178. package/src/adapters/raw-imports.d.ts +5 -0
  179. package/src/cache/cache-api.ts +9 -1
  180. package/src/cache/index.ts +5 -2
  181. package/src/cache/invalidation-epoch.ts +93 -0
  182. package/src/cache/redis-handler.ts +33 -2
  183. package/src/cache/timber-cache.ts +109 -53
  184. package/src/cli-schema-sync.ts +123 -30
  185. package/src/cli.ts +33 -37
  186. package/src/client/browser-entry/hmr.ts +0 -11
  187. package/src/client/browser-entry/hydrate.ts +39 -5
  188. package/src/client/browser-entry/post-hydration.ts +6 -15
  189. package/src/client/browser-entry/router-init.ts +98 -3
  190. package/src/client/history.ts +10 -24
  191. package/src/client/navigation-api.ts +14 -9
  192. package/src/client/navigation-context.ts +1 -21
  193. package/src/client/navigation-root.tsx +0 -8
  194. package/src/client/router.ts +18 -8
  195. package/src/client/rsc-fetch.ts +7 -2
  196. package/src/client/use-search-params.ts +10 -5
  197. package/src/config-validation.ts +33 -25
  198. package/src/cookies/define-cookie.ts +2 -18
  199. package/src/dev-tools/dev-page-shell.ts +6 -2
  200. package/src/dev-tools/hydration-overlay.ts +158 -0
  201. package/src/dev-tools/index.ts +3 -16
  202. package/src/fonts/ast-walk.ts +235 -0
  203. package/src/fonts/ast.ts +91 -249
  204. package/src/fonts/bundle.ts +16 -9
  205. package/src/fonts/dev-middleware.ts +16 -5
  206. package/src/fonts/fallbacks.ts +0 -23
  207. package/src/fonts/google.ts +7 -3
  208. package/src/fonts/local.ts +7 -6
  209. package/src/fonts/paths.ts +58 -0
  210. package/src/fonts/pipeline.ts +27 -48
  211. package/src/fonts/transform.ts +27 -0
  212. package/src/fonts/virtual-modules.ts +1 -57
  213. package/src/index.ts +5 -11
  214. package/src/params/resolve-schema.ts +2 -34
  215. package/src/plugin-context.ts +95 -17
  216. package/src/plugins/cache.ts +64 -17
  217. package/src/plugins/dev-server.ts +61 -9
  218. package/src/plugins/fonts.ts +21 -6
  219. package/src/plugins/routing.ts +5 -348
  220. package/src/plugins/server-bundle.ts +1 -0
  221. package/src/routing/codegen.ts +3 -2
  222. package/src/routing/convention-lint.ts +4 -3
  223. package/src/routing/file-cache.ts +23 -0
  224. package/src/routing/interception.ts +2 -21
  225. package/src/routing/manifest-codegen.ts +290 -0
  226. package/src/routing/scanner.ts +5 -17
  227. package/src/routing/schema-validation.ts +2 -2
  228. package/src/schema-bridge.ts +63 -2
  229. package/src/search-params/define.ts +34 -2
  230. package/src/search-params/wrappers.ts +21 -3
  231. package/src/segment-params/define.ts +2 -62
  232. package/src/server/access-gate.tsx +8 -0
  233. package/src/server/actions.ts +8 -31
  234. package/src/server/build-manifest.ts +0 -56
  235. package/src/server/canonicalize.ts +6 -3
  236. package/src/server/deny-renderer.ts +6 -0
  237. package/src/server/html-injector-core.ts +19 -0
  238. package/src/server/html-injectors.ts +14 -4
  239. package/src/server/internal.ts +1 -18
  240. package/src/server/node-stream-transforms.ts +5 -72
  241. package/src/server/render-utils.ts +31 -0
  242. package/src/server/route-element-builder.ts +77 -47
  243. package/src/server/rsc-entry/error-renderer.ts +1 -0
  244. package/src/server/rsc-entry/helpers.ts +6 -34
  245. package/src/server/rsc-entry/index.ts +11 -0
  246. package/src/server/rsc-entry/render-route.ts +7 -9
  247. package/src/server/rsc-entry/rsc-payload.ts +2 -1
  248. package/src/server/rsc-entry/ssr-renderer.ts +2 -2
  249. package/src/server/ssr-render.ts +1 -11
  250. package/src/shared/href-validation.ts +8 -0
  251. package/dist/_chunks/cli-schema-sync-CBiMEtqa.js +0 -2
  252. package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +0 -1
  253. package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
  254. package/dist/_chunks/define-DMVhsipD.js.map +0 -1
  255. package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
  256. package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
  257. package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
  258. package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
  259. package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
  260. package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
  261. package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
  262. package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
  263. package/dist/_chunks/walkers-Ck4Y45XN.js.map +0 -1
  264. package/dist/_chunks/warnings-CNPXaypM.js +0 -125
  265. package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
  266. package/dist/client/link-pending-store.d.ts +0 -65
  267. package/dist/client/link-pending-store.d.ts.map +0 -1
  268. package/dist/client/nav-link-store.d.ts +0 -27
  269. package/dist/client/nav-link-store.d.ts.map +0 -1
  270. package/dist/dev-tools/warnings.d.ts +0 -109
  271. package/dist/dev-tools/warnings.d.ts.map +0 -1
  272. package/dist/server/flush.d.ts +0 -74
  273. package/dist/server/flush.d.ts.map +0 -1
  274. package/src/client/link-pending-store.ts +0 -116
  275. package/src/client/nav-link-store.ts +0 -37
  276. package/src/dev-tools/warnings.ts +0 -238
  277. package/src/server/flush.ts +0 -183
@@ -1,116 +0,0 @@
1
- /**
2
- * Link Pending Store — per-link pending state.
3
- *
4
- * Tracks which link instance is currently navigating so that only the
5
- * clicked link shows pending. Uses useState in Link — the pending
6
- * state is set as an urgent update (outside startTransition) so it
7
- * shows immediately, then reset inside the transition so it commits
8
- * atomically with the new tree.
9
- *
10
- * Flow:
11
- * 1. Link click → setLinkForCurrentNavigation(instance) stores the ref
12
- * 2. NavigationRoot → activateLinkPending() calls
13
- * instance.setIsPending(LINK_PENDING) as an urgent update
14
- * 3. Inside startTransition → resetLinkPending() commits with new tree
15
- *
16
- * SINGLETON GUARANTEE: Uses `globalThis` via `Symbol.for` keys because
17
- * the RSC client bundler can duplicate this module across chunks.
18
- *
19
- * See design/19-client-navigation.md §"Per-Link Pending State"
20
- */
21
-
22
- import type { LinkStatus } from './use-link-status.js';
23
-
24
- // ─── Types ───────────────────────────────────────────────────────
25
-
26
- export interface LinkPendingInstance {
27
- setIsPending: (status: LinkStatus) => void;
28
- }
29
-
30
- // ─── Constants ───────────────────────────────────────────────────
31
-
32
- const LINK_PENDING_KEY = Symbol.for('__timber_link_pending');
33
-
34
- /** Status object: link navigation in flight */
35
- export const LINK_PENDING: LinkStatus = { isPending: true };
36
-
37
- /** Status object: link idle */
38
- export const LINK_IDLE: LinkStatus = { isPending: false };
39
-
40
- // ─── Singleton Storage ───────────────────────────────────────────
41
-
42
- function getStore(): { current: LinkPendingInstance | null } {
43
- const g = globalThis as Record<symbol, unknown>;
44
- if (!g[LINK_PENDING_KEY]) {
45
- g[LINK_PENDING_KEY] = { current: null };
46
- }
47
- return g[LINK_PENDING_KEY] as { current: LinkPendingInstance | null };
48
- }
49
-
50
- // ─── Public API ──────────────────────────────────────────────────
51
-
52
- /**
53
- * Register the link instance that initiated the current navigation.
54
- * Called from Link's click handler. Does NOT call setIsPending —
55
- * that happens via activateLinkPending() called from NavigationRoot.
56
- *
57
- * Resets the previous link to idle immediately if a different link
58
- * is being registered (handles overlapping link clicks).
59
- */
60
- export function setLinkForCurrentNavigation(link: LinkPendingInstance | null): void {
61
- const store = getStore();
62
- if (store.current && store.current !== link) {
63
- store.current.setIsPending(LINK_IDLE);
64
- }
65
- store.current = link;
66
- }
67
-
68
- /**
69
- * Activate pending state on the current link instance.
70
- * Called as an urgent update (outside startTransition) so the link
71
- * shows pending immediately. The corresponding resetLinkPending()
72
- * is called inside the transition so it commits atomically with
73
- * the new tree.
74
- */
75
- export function activateLinkPending(): void {
76
- const store = getStore();
77
- store.current?.setIsPending(LINK_PENDING);
78
- }
79
-
80
- /**
81
- * Reset the current link's pending state and clear the store.
82
- * Call inside startTransition so the reset commits atomically
83
- * with the new tree (no frame where pending=false but old content
84
- * is still visible).
85
- */
86
- export function resetLinkPending(): void {
87
- const store = getStore();
88
- if (store.current) {
89
- store.current.setIsPending(LINK_IDLE);
90
- store.current = null;
91
- }
92
- }
93
-
94
- /**
95
- * @deprecated No longer needed with useOptimistic. Kept for callers.
96
- */
97
- export function getCurrentNavId(): number {
98
- return 0;
99
- }
100
-
101
- /**
102
- * Clean up the link pending store entirely.
103
- */
104
- export function clearLinkPendingSetter(): void {
105
- getStore().current = null;
106
- }
107
-
108
- /**
109
- * Unmount a link instance from navigation tracking.
110
- */
111
- export function unmountLinkForCurrentNavigation(link: LinkPendingInstance): void {
112
- const store = getStore();
113
- if (store.current === link) {
114
- store.current = null;
115
- }
116
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * Navigation Link Store — passes per-link metadata from Link's onClick
3
- * to the Navigation API's navigate event handler.
4
- *
5
- * Used for external navigations (plain <a> tags intercepted by the
6
- * Navigation API). Link components drive navigation directly via
7
- * router.navigate() and don't use this store.
8
- *
9
- * See design/19-client-navigation.md §"Navigation API Integration"
10
- */
11
-
12
- export interface NavLinkMetadata {
13
- /** Whether to scroll to top after navigation. Default: true. */
14
- scroll: boolean;
15
- }
16
-
17
- let pendingMetadata: NavLinkMetadata | null = null;
18
-
19
- /**
20
- * Store metadata from Link's onClick for the next navigate event.
21
- * Called synchronously in the click handler — the navigate event
22
- * fires synchronously after onClick returns.
23
- */
24
- export function setNavLinkMetadata(metadata: NavLinkMetadata): void {
25
- pendingMetadata = metadata;
26
- }
27
-
28
- /**
29
- * Consume the stored metadata. Returns null if no Link onClick
30
- * preceded this navigation (e.g., plain <a> tag, programmatic nav).
31
- * Clears the store after reading.
32
- */
33
- export function consumeNavLinkMetadata(): NavLinkMetadata | null {
34
- const metadata = pendingMetadata;
35
- pendingMetadata = null;
36
- return metadata;
37
- }
@@ -1,238 +0,0 @@
1
- /**
2
- * Dev-mode warnings for common timber.js misuse patterns.
3
- *
4
- * These fire in development only and are stripped from production builds.
5
- * Each warning targets a specific misuse identified during design review.
6
- *
7
- * Warnings are deduplicated by warningId:filePath:line so the same warning
8
- * is only emitted once per dev session (per unique source location).
9
- *
10
- * Warnings are written to stderr and, when a Vite dev server is available,
11
- * forwarded to the browser console via Vite's WebSocket.
12
- *
13
- * See design/21-dev-server.md §"Dev-Mode Warnings"
14
- * See design/11-platform.md §"Dev Mode"
15
- */
16
-
17
- import type { ViteDevServer } from 'vite';
18
- import { isDebug } from '../server/debug.js';
19
-
20
- // ─── Warning IDs ───────────────────────────────────────────────────────────
21
-
22
- export const WarningId = {
23
- SUSPENSE_WRAPS_CHILDREN: 'SUSPENSE_WRAPS_CHILDREN',
24
- DENY_IN_SUSPENSE: 'DENY_IN_SUSPENSE',
25
- REDIRECT_IN_SUSPENSE: 'REDIRECT_IN_SUSPENSE',
26
- REDIRECT_IN_ACCESS: 'REDIRECT_IN_ACCESS',
27
- STATIC_REQUEST_API: 'STATIC_REQUEST_API',
28
- SLOW_SLOT_NO_SUSPENSE: 'SLOW_SLOT_NO_SUSPENSE',
29
- } as const;
30
-
31
- export type WarningId = (typeof WarningId)[keyof typeof WarningId];
32
-
33
- // ─── Configuration ──────────────────────────────────────────────────────────
34
-
35
- /** Configuration for dev warning behavior. */
36
- export interface DevWarningConfig {
37
- /** Threshold in ms for "slow slot" warnings. Default: 200. */
38
- slowSlotThresholdMs?: number;
39
- }
40
-
41
- // ─── Deduplication & Server ─────────────────────────────────────────────────
42
-
43
- const _emitted = new Set<string>();
44
-
45
- /** Vite dev server for forwarding warnings to browser console. */
46
- let _viteServer: ViteDevServer | null = null;
47
-
48
- /**
49
- * Register the Vite dev server for browser console forwarding.
50
- * Called by timber-dev-server during configureServer.
51
- */
52
- export function setViteServer(server: ViteDevServer | null): void {
53
- _viteServer = server;
54
- }
55
-
56
- function isDev(): boolean {
57
- return isDebug();
58
- }
59
-
60
- /**
61
- * Emit a warning only once per dedup key.
62
- *
63
- * Writes to stderr and forwards to browser console via Vite WebSocket.
64
- * Returns true if emitted (not deduplicated).
65
- */
66
- function emitOnce(
67
- warningId: WarningId,
68
- location: string,
69
- level: 'warn' | 'error',
70
- message: string
71
- ): boolean {
72
- if (!isDev()) return false;
73
-
74
- const dedupKey = `${warningId}:${location}`;
75
- if (_emitted.has(dedupKey)) return false;
76
- _emitted.add(dedupKey);
77
-
78
- // Write to stderr
79
- const prefix = level === 'error' ? '\x1b[31m[timber]\x1b[0m' : '\x1b[33m[timber]\x1b[0m';
80
- process.stderr.write(`${prefix} ${message}\n`);
81
-
82
- // Forward to browser console via Vite WebSocket
83
- if (_viteServer?.hot) {
84
- _viteServer.hot.send('timber:dev-warning', {
85
- warningId,
86
- level,
87
- message: `[timber] ${message}`,
88
- });
89
- }
90
-
91
- return true;
92
- }
93
-
94
- // ─── Warning Functions ──────────────────────────────────────────────────────
95
-
96
- /**
97
- * Warn when a layout wraps {children} in <Suspense>.
98
- *
99
- * This defers the page content — the primary resource — behind a fallback.
100
- * The page's data fetches won't affect the HTTP status code because they
101
- * resolve after onShellReady. If the page calls deny(404), the status code
102
- * is already committed as 200.
103
- *
104
- * @param layoutFile - Relative path to the layout file (e.g., "app/(dashboard)/layout.tsx")
105
- */
106
- export function warnSuspenseWrappingChildren(layoutFile: string): void {
107
- emitOnce(
108
- WarningId.SUSPENSE_WRAPS_CHILDREN,
109
- layoutFile,
110
- 'warn',
111
- `Layout at ${layoutFile} wraps {children} in <Suspense>. ` +
112
- 'This prevents child pages from setting HTTP status codes. ' +
113
- 'Use usePendingNavigation() for loading states instead.'
114
- );
115
- }
116
-
117
- /**
118
- * Warn when deny() is called inside a Suspense boundary.
119
- *
120
- * After the shell has flushed and the status code is committed, deny()
121
- * cannot change the HTTP response. The signal will be caught by the nearest
122
- * error boundary instead of producing a correct status code.
123
- *
124
- * @param file - Relative path to the file
125
- * @param line - Line number where deny() was called
126
- */
127
- export function warnDenyInSuspense(file: string, line?: number): void {
128
- const location = line ? `${file}:${line}` : file;
129
- emitOnce(
130
- WarningId.DENY_IN_SUSPENSE,
131
- location,
132
- 'error',
133
- `deny() called inside <Suspense> at ${location}. ` +
134
- 'The HTTP status is already committed — this will trigger an error boundary with a 200 status. ' +
135
- 'Move deny() outside <Suspense> for correct HTTP semantics.'
136
- );
137
- }
138
-
139
- /**
140
- * Warn when redirect() is called inside a Suspense boundary.
141
- *
142
- * This will perform a client-side navigation instead of an HTTP redirect.
143
- *
144
- * @param file - Relative path to the file
145
- * @param line - Line number where redirect() was called
146
- */
147
- export function warnRedirectInSuspense(file: string, line?: number): void {
148
- const location = line ? `${file}:${line}` : file;
149
- emitOnce(
150
- WarningId.REDIRECT_IN_SUSPENSE,
151
- location,
152
- 'error',
153
- `redirect() called inside <Suspense> at ${location}. ` +
154
- 'This will perform a client-side navigation instead of an HTTP redirect.'
155
- );
156
- }
157
-
158
- /**
159
- * Warn when redirect() is called in a slot's access.ts.
160
- *
161
- * Slots use deny() for graceful degradation. Redirecting from a slot would
162
- * redirect the entire page, breaking the contract that slot failure is
163
- * isolated to the slot.
164
- *
165
- * @param accessFile - Relative path to the access.ts file
166
- * @param line - Line number where redirect() was called
167
- */
168
- export function warnRedirectInAccess(accessFile: string, line?: number): void {
169
- const location = line ? `${accessFile}:${line}` : accessFile;
170
- emitOnce(
171
- WarningId.REDIRECT_IN_ACCESS,
172
- location,
173
- 'error',
174
- `redirect() called in access.ts at ${location}. ` +
175
- 'Only deny() is valid in slot access checks. ' +
176
- 'Use deny() to block access or move redirect() to middleware.ts.'
177
- );
178
- }
179
-
180
- /**
181
- * Warn when getCookies() or getHeaders() is called during a static build.
182
- *
183
- * In output: 'static' mode, there is no per-request context — these APIs
184
- * read build-time values only. This is almost always a mistake.
185
- *
186
- * @param api - The dynamic API name ("cookies" or "headers")
187
- * @param file - Relative path to the file calling the API
188
- */
189
- export function warnStaticRequestApi(api: 'cookies' | 'headers', file: string): void {
190
- emitOnce(
191
- WarningId.STATIC_REQUEST_API,
192
- `${api}:${file}`,
193
- 'error',
194
- `${api}() called during static generation of ${file}. ` +
195
- 'Dynamic request APIs are not available during prerendering.'
196
- );
197
- }
198
-
199
- // NOTE: warnCacheRequestProps removed — 'use cache' directive is a future feature.
200
- // See design/06-caching.md.
201
-
202
- /**
203
- * Warn when a parallel slot resolves slowly without a <Suspense> wrapper.
204
- *
205
- * A slow slot without Suspense blocks onShellReady — and therefore the
206
- * status code commit — for the entire page. Wrapping it in <Suspense>
207
- * lets the shell flush without waiting for the slot.
208
- *
209
- * @param slotName - The slot name (e.g., "@admin")
210
- * @param durationMs - How long the slot took to resolve
211
- */
212
- export function warnSlowSlotWithoutSuspense(slotName: string, durationMs: number): void {
213
- emitOnce(
214
- WarningId.SLOW_SLOT_NO_SUSPENSE,
215
- slotName,
216
- 'warn',
217
- `Slot ${slotName} resolved in ${durationMs}ms and is not wrapped in <Suspense>. ` +
218
- 'Consider wrapping to avoid blocking the flush.'
219
- );
220
- }
221
-
222
- // ─── Testing ────────────────────────────────────────────────────────────────
223
-
224
- /**
225
- * Reset emitted warnings. For testing only.
226
- * @internal
227
- */
228
- export function _resetWarnings(): void {
229
- _emitted.clear();
230
- }
231
-
232
- /**
233
- * Get the set of emitted dedup keys. For testing only.
234
- * @internal
235
- */
236
- export function _getEmitted(): ReadonlySet<string> {
237
- return _emitted;
238
- }
@@ -1,183 +0,0 @@
1
- /**
2
- * Flush controller for timber.js rendering.
3
- *
4
- * Holds the response until `onShellReady` fires, then commits the HTTP status
5
- * code and flushes the shell. Render-phase signals (deny, redirect, unhandled
6
- * throws) caught before flush produce correct HTTP status codes.
7
- *
8
- * See design/02-rendering-pipeline.md §"The Flush Point" and §"The Hold Window"
9
- */
10
-
11
- import { DenySignal, RedirectSignal, RenderError } from './primitives.js';
12
- import { logRenderError } from './logger.js';
13
-
14
- // ─── Types ───────────────────────────────────────────────────────────────────
15
-
16
- /** The readable stream from React's renderToReadableStream. */
17
- export interface ReactRenderStream {
18
- /** The underlying ReadableStream of HTML bytes. */
19
- readable: ReadableStream<Uint8Array>;
20
- /** Resolves when the shell has finished rendering (all non-Suspense content). */
21
- allReady?: Promise<void>;
22
- }
23
-
24
- /** Options for the flush controller. */
25
- export interface FlushOptions {
26
- /** Response headers to include (from middleware.ts, proxy.ts, etc.). */
27
- responseHeaders?: Headers;
28
- /** Default status code when rendering succeeds. Default: 200. */
29
- defaultStatus?: number;
30
- }
31
-
32
- /** Result of the flush process. */
33
- export interface FlushResult {
34
- /** The final HTTP Response. */
35
- response: Response;
36
- /** The status code committed. */
37
- status: number;
38
- /** Whether the response was a redirect. */
39
- isRedirect: boolean;
40
- /** Whether the response was a denial. */
41
- isDenial: boolean;
42
- }
43
-
44
- // ─── Render Function Type ────────────────────────────────────────────────────
45
-
46
- /**
47
- * A function that performs the React render.
48
- *
49
- * The flush controller calls this, catches any signals thrown during the
50
- * synchronous shell render (before onShellReady), and produces the
51
- * correct HTTP response.
52
- *
53
- * Must return an object with:
54
- * - `stream`: The ReadableStream from renderToReadableStream
55
- * - `shellReady`: A Promise that resolves when onShellReady fires
56
- */
57
- export interface RenderResult {
58
- /** The HTML byte stream. */
59
- stream: ReadableStream<Uint8Array>;
60
- /** Resolves when the shell is ready (all non-Suspense content rendered). */
61
- shellReady: Promise<void>;
62
- }
63
-
64
- export type RenderFn = () => RenderResult | Promise<RenderResult>;
65
-
66
- // ─── Flush Controller ────────────────────────────────────────────────────────
67
-
68
- /**
69
- * Execute a render and hold the response until the shell is ready.
70
- *
71
- * The flush controller:
72
- * 1. Calls the render function to start renderToReadableStream
73
- * 2. Waits for shellReady (onShellReady)
74
- * 3. If a render-phase signal was thrown (deny, redirect, error), produces
75
- * the correct HTTP status code
76
- * 4. If the shell rendered successfully, commits the status and streams
77
- *
78
- * Render-phase signals caught before flush:
79
- * - `DenySignal` → HTTP 4xx with appropriate status code
80
- * - `RedirectSignal` → HTTP 3xx with Location header
81
- * - `RenderError` → HTTP status from error (default 500)
82
- * - Unhandled error → HTTP 500
83
- *
84
- * @param renderFn - Function that starts the React render.
85
- * @param options - Flush configuration.
86
- * @returns The committed HTTP Response.
87
- */
88
- export async function flushResponse(
89
- renderFn: RenderFn,
90
- options: FlushOptions = {}
91
- ): Promise<FlushResult> {
92
- const { responseHeaders = new Headers(), defaultStatus = 200 } = options;
93
-
94
- let renderResult: RenderResult;
95
-
96
- // Phase 1: Start the render. The render function may throw synchronously
97
- // if there's an immediate error before React even starts.
98
- try {
99
- renderResult = await renderFn();
100
- } catch (error) {
101
- return handleSignal(error, responseHeaders);
102
- }
103
-
104
- // Phase 2: Wait for onShellReady. Render-phase signals (deny, redirect,
105
- // throws outside Suspense) are caught here.
106
- try {
107
- await renderResult.shellReady;
108
- } catch (error) {
109
- return handleSignal(error, responseHeaders);
110
- }
111
-
112
- // Phase 3: Shell rendered successfully. Commit status and stream.
113
- responseHeaders.set('Content-Type', 'text/html; charset=utf-8');
114
-
115
- return {
116
- response: new Response(renderResult.stream, {
117
- status: defaultStatus,
118
- headers: responseHeaders,
119
- }),
120
- status: defaultStatus,
121
- isRedirect: false,
122
- isDenial: false,
123
- };
124
- }
125
-
126
- // ─── Signal Handling ─────────────────────────────────────────────────────────
127
-
128
- /**
129
- * Handle a render-phase signal and produce the correct HTTP response.
130
- */
131
- function handleSignal(error: unknown, responseHeaders: Headers): FlushResult {
132
- // Redirect signal → HTTP 3xx
133
- if (error instanceof RedirectSignal) {
134
- responseHeaders.set('Location', error.location);
135
- return {
136
- response: new Response(null, {
137
- status: error.status,
138
- headers: responseHeaders,
139
- }),
140
- status: error.status,
141
- isRedirect: true,
142
- isDenial: false,
143
- };
144
- }
145
-
146
- // Deny signal → HTTP 4xx
147
- if (error instanceof DenySignal) {
148
- return {
149
- response: new Response(null, {
150
- status: error.status,
151
- headers: responseHeaders,
152
- }),
153
- status: error.status,
154
- isRedirect: false,
155
- isDenial: true,
156
- };
157
- }
158
-
159
- // RenderError → HTTP status from error
160
- if (error instanceof RenderError) {
161
- return {
162
- response: new Response(null, {
163
- status: error.status,
164
- headers: responseHeaders,
165
- }),
166
- status: error.status,
167
- isRedirect: false,
168
- isDenial: false,
169
- };
170
- }
171
-
172
- // Unknown error → HTTP 500
173
- logRenderError({ method: '', path: '', error });
174
- return {
175
- response: new Response(null, {
176
- status: 500,
177
- headers: responseHeaders,
178
- }),
179
- status: 500,
180
- isRedirect: false,
181
- isDenial: false,
182
- };
183
- }