@timber-js/app 0.2.0-alpha.4 → 0.2.0-alpha.41

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 (336) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  3. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  4. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  5. package/dist/_chunks/debug-ECi_61pb.js +108 -0
  6. package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
  7. package/dist/_chunks/define-cookie-BmKbSyp0.js +93 -0
  8. package/dist/_chunks/define-cookie-BmKbSyp0.js.map +1 -0
  9. package/dist/_chunks/error-boundary-BAN3751q.js +211 -0
  10. package/dist/_chunks/error-boundary-BAN3751q.js.map +1 -0
  11. package/dist/_chunks/{format-CwdaB0_2.js → format-cX7wzEp2.js} +2 -2
  12. package/dist/_chunks/{format-CwdaB0_2.js.map → format-cX7wzEp2.js.map} +1 -1
  13. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  14. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  15. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  16. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  17. package/dist/_chunks/{request-context-CZJi4CuK.js → request-context-BxYIJM24.js} +93 -69
  18. package/dist/_chunks/request-context-BxYIJM24.js.map +1 -0
  19. package/dist/_chunks/segment-context-C6byCyZU.js +69 -0
  20. package/dist/_chunks/segment-context-C6byCyZU.js.map +1 -0
  21. package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
  22. package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
  23. package/dist/_chunks/{tracing-Cwn7697K.js → tracing-CuXiCP5p.js} +17 -3
  24. package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-CuXiCP5p.js.map} +1 -1
  25. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
  26. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
  27. package/dist/_chunks/wrappers-C6J0nNji.js +331 -0
  28. package/dist/_chunks/wrappers-C6J0nNji.js.map +1 -0
  29. package/dist/adapters/compress-module.d.ts.map +1 -1
  30. package/dist/adapters/nitro.d.ts +17 -1
  31. package/dist/adapters/nitro.d.ts.map +1 -1
  32. package/dist/adapters/nitro.js +56 -13
  33. package/dist/adapters/nitro.js.map +1 -1
  34. package/dist/cache/fast-hash.d.ts +22 -0
  35. package/dist/cache/fast-hash.d.ts.map +1 -0
  36. package/dist/cache/index.d.ts +5 -2
  37. package/dist/cache/index.d.ts.map +1 -1
  38. package/dist/cache/index.js +88 -18
  39. package/dist/cache/index.js.map +1 -1
  40. package/dist/cache/register-cached-function.d.ts.map +1 -1
  41. package/dist/cache/singleflight.d.ts +18 -1
  42. package/dist/cache/singleflight.d.ts.map +1 -1
  43. package/dist/cache/timber-cache.d.ts.map +1 -1
  44. package/dist/client/error-boundary.d.ts +10 -1
  45. package/dist/client/error-boundary.d.ts.map +1 -1
  46. package/dist/client/error-boundary.js +1 -125
  47. package/dist/client/index.d.ts +3 -2
  48. package/dist/client/index.d.ts.map +1 -1
  49. package/dist/client/index.js +213 -93
  50. package/dist/client/index.js.map +1 -1
  51. package/dist/client/link.d.ts +22 -8
  52. package/dist/client/link.d.ts.map +1 -1
  53. package/dist/client/navigation-context.d.ts +2 -2
  54. package/dist/client/router.d.ts +25 -3
  55. package/dist/client/router.d.ts.map +1 -1
  56. package/dist/client/rsc-fetch.d.ts +23 -2
  57. package/dist/client/rsc-fetch.d.ts.map +1 -1
  58. package/dist/client/segment-cache.d.ts +1 -1
  59. package/dist/client/segment-cache.d.ts.map +1 -1
  60. package/dist/client/segment-context.d.ts +1 -1
  61. package/dist/client/segment-context.d.ts.map +1 -1
  62. package/dist/client/segment-merger.d.ts.map +1 -1
  63. package/dist/client/stale-reload.d.ts +15 -0
  64. package/dist/client/stale-reload.d.ts.map +1 -1
  65. package/dist/client/top-loader.d.ts +1 -1
  66. package/dist/client/top-loader.d.ts.map +1 -1
  67. package/dist/client/transition-root.d.ts +1 -1
  68. package/dist/client/transition-root.d.ts.map +1 -1
  69. package/dist/client/use-params.d.ts +2 -2
  70. package/dist/client/use-params.d.ts.map +1 -1
  71. package/dist/client/use-query-states.d.ts +1 -1
  72. package/dist/codec.d.ts +21 -0
  73. package/dist/codec.d.ts.map +1 -0
  74. package/dist/cookies/define-cookie.d.ts +33 -12
  75. package/dist/cookies/define-cookie.d.ts.map +1 -1
  76. package/dist/cookies/index.js +1 -83
  77. package/dist/fonts/css.d.ts +1 -0
  78. package/dist/fonts/css.d.ts.map +1 -1
  79. package/dist/fonts/local.d.ts +4 -2
  80. package/dist/fonts/local.d.ts.map +1 -1
  81. package/dist/index.d.ts +112 -35
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +635 -233
  84. package/dist/index.js.map +1 -1
  85. package/dist/params/define.d.ts +76 -0
  86. package/dist/params/define.d.ts.map +1 -0
  87. package/dist/params/index.d.ts +8 -0
  88. package/dist/params/index.d.ts.map +1 -0
  89. package/dist/params/index.js +104 -0
  90. package/dist/params/index.js.map +1 -0
  91. package/dist/plugins/adapter-build.d.ts.map +1 -1
  92. package/dist/plugins/build-manifest.d.ts.map +1 -1
  93. package/dist/plugins/client-chunks.d.ts +32 -0
  94. package/dist/plugins/client-chunks.d.ts.map +1 -0
  95. package/dist/plugins/dev-error-overlay.d.ts +26 -1
  96. package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
  97. package/dist/plugins/entries.d.ts +7 -0
  98. package/dist/plugins/entries.d.ts.map +1 -1
  99. package/dist/plugins/fonts.d.ts +9 -1
  100. package/dist/plugins/fonts.d.ts.map +1 -1
  101. package/dist/plugins/mdx.d.ts +6 -0
  102. package/dist/plugins/mdx.d.ts.map +1 -1
  103. package/dist/plugins/routing.d.ts.map +1 -1
  104. package/dist/plugins/server-bundle.d.ts.map +1 -1
  105. package/dist/plugins/static-build.d.ts.map +1 -1
  106. package/dist/routing/codegen.d.ts +2 -2
  107. package/dist/routing/codegen.d.ts.map +1 -1
  108. package/dist/routing/index.js +1 -1
  109. package/dist/routing/scanner.d.ts.map +1 -1
  110. package/dist/routing/status-file-lint.d.ts +2 -1
  111. package/dist/routing/status-file-lint.d.ts.map +1 -1
  112. package/dist/routing/types.d.ts +6 -4
  113. package/dist/routing/types.d.ts.map +1 -1
  114. package/dist/rsc-runtime/rsc.d.ts +1 -1
  115. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  116. package/dist/rsc-runtime/ssr.d.ts +12 -0
  117. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  118. package/dist/search-params/codecs.d.ts +1 -1
  119. package/dist/search-params/define.d.ts +153 -0
  120. package/dist/search-params/define.d.ts.map +1 -0
  121. package/dist/search-params/index.d.ts +4 -5
  122. package/dist/search-params/index.d.ts.map +1 -1
  123. package/dist/search-params/index.js +3 -474
  124. package/dist/search-params/registry.d.ts +1 -1
  125. package/dist/search-params/wrappers.d.ts +53 -0
  126. package/dist/search-params/wrappers.d.ts.map +1 -0
  127. package/dist/server/access-gate.d.ts +4 -0
  128. package/dist/server/access-gate.d.ts.map +1 -1
  129. package/dist/server/action-client.d.ts.map +1 -1
  130. package/dist/server/action-encryption.d.ts +76 -0
  131. package/dist/server/action-encryption.d.ts.map +1 -0
  132. package/dist/server/action-handler.d.ts.map +1 -1
  133. package/dist/server/als-registry.d.ts +18 -4
  134. package/dist/server/als-registry.d.ts.map +1 -1
  135. package/dist/server/build-manifest.d.ts +2 -2
  136. package/dist/server/debug.d.ts +46 -15
  137. package/dist/server/debug.d.ts.map +1 -1
  138. package/dist/server/default-logger.d.ts +22 -0
  139. package/dist/server/default-logger.d.ts.map +1 -0
  140. package/dist/server/deny-renderer.d.ts.map +1 -1
  141. package/dist/server/early-hints.d.ts +13 -5
  142. package/dist/server/early-hints.d.ts.map +1 -1
  143. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  144. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  145. package/dist/server/flight-injection-state.d.ts +78 -0
  146. package/dist/server/flight-injection-state.d.ts.map +1 -0
  147. package/dist/server/flight-scripts.d.ts +39 -0
  148. package/dist/server/flight-scripts.d.ts.map +1 -0
  149. package/dist/server/flush.d.ts.map +1 -1
  150. package/dist/server/form-data.d.ts +29 -0
  151. package/dist/server/form-data.d.ts.map +1 -1
  152. package/dist/server/html-injectors.d.ts +5 -11
  153. package/dist/server/html-injectors.d.ts.map +1 -1
  154. package/dist/server/index.d.ts +4 -2
  155. package/dist/server/index.d.ts.map +1 -1
  156. package/dist/server/index.js +1975 -1649
  157. package/dist/server/index.js.map +1 -1
  158. package/dist/server/logger.d.ts +24 -7
  159. package/dist/server/logger.d.ts.map +1 -1
  160. package/dist/server/node-stream-transforms.d.ts +77 -0
  161. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  162. package/dist/server/pipeline.d.ts +7 -4
  163. package/dist/server/pipeline.d.ts.map +1 -1
  164. package/dist/server/primitives.d.ts +30 -3
  165. package/dist/server/primitives.d.ts.map +1 -1
  166. package/dist/server/render-timeout.d.ts +51 -0
  167. package/dist/server/render-timeout.d.ts.map +1 -0
  168. package/dist/server/request-context.d.ts +65 -38
  169. package/dist/server/request-context.d.ts.map +1 -1
  170. package/dist/server/route-element-builder.d.ts +7 -0
  171. package/dist/server/route-element-builder.d.ts.map +1 -1
  172. package/dist/server/route-handler.d.ts.map +1 -1
  173. package/dist/server/route-matcher.d.ts +2 -2
  174. package/dist/server/route-matcher.d.ts.map +1 -1
  175. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/helpers.d.ts +46 -3
  177. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/index.d.ts +6 -1
  179. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  180. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  181. package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
  182. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  183. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  184. package/dist/server/slot-resolver.d.ts +1 -1
  185. package/dist/server/slot-resolver.d.ts.map +1 -1
  186. package/dist/server/ssr-entry.d.ts +22 -0
  187. package/dist/server/ssr-entry.d.ts.map +1 -1
  188. package/dist/server/ssr-render.d.ts +39 -21
  189. package/dist/server/ssr-render.d.ts.map +1 -1
  190. package/dist/server/tracing.d.ts +10 -0
  191. package/dist/server/tracing.d.ts.map +1 -1
  192. package/dist/server/tree-builder.d.ts +19 -12
  193. package/dist/server/tree-builder.d.ts.map +1 -1
  194. package/dist/server/types.d.ts +1 -3
  195. package/dist/server/types.d.ts.map +1 -1
  196. package/dist/server/version-skew.d.ts +61 -0
  197. package/dist/server/version-skew.d.ts.map +1 -0
  198. package/dist/server/waituntil-bridge.d.ts.map +1 -1
  199. package/dist/shared/merge-search-params.d.ts +22 -0
  200. package/dist/shared/merge-search-params.d.ts.map +1 -0
  201. package/dist/shims/navigation-client.d.ts +1 -1
  202. package/dist/shims/navigation-client.d.ts.map +1 -1
  203. package/dist/shims/navigation.d.ts +1 -1
  204. package/dist/shims/navigation.d.ts.map +1 -1
  205. package/dist/utils/state-machine.d.ts +80 -0
  206. package/dist/utils/state-machine.d.ts.map +1 -0
  207. package/package.json +17 -14
  208. package/src/adapters/compress-module.ts +24 -4
  209. package/src/adapters/nitro.ts +58 -9
  210. package/src/cache/fast-hash.ts +34 -0
  211. package/src/cache/index.ts +5 -2
  212. package/src/cache/register-cached-function.ts +7 -3
  213. package/src/cache/singleflight.ts +62 -4
  214. package/src/cache/timber-cache.ts +34 -26
  215. package/src/cli.ts +0 -0
  216. package/src/client/browser-entry.ts +94 -90
  217. package/src/client/error-boundary.tsx +18 -1
  218. package/src/client/index.ts +10 -1
  219. package/src/client/link.tsx +78 -19
  220. package/src/client/navigation-context.ts +2 -2
  221. package/src/client/router.ts +105 -60
  222. package/src/client/rsc-fetch.ts +63 -2
  223. package/src/client/segment-cache.ts +1 -1
  224. package/src/client/segment-context.ts +6 -1
  225. package/src/client/segment-merger.ts +2 -8
  226. package/src/client/stale-reload.ts +32 -6
  227. package/src/client/top-loader.tsx +10 -9
  228. package/src/client/transition-root.tsx +7 -1
  229. package/src/client/use-params.ts +3 -3
  230. package/src/client/use-query-states.ts +1 -1
  231. package/src/codec.ts +21 -0
  232. package/src/cookies/define-cookie.ts +69 -18
  233. package/src/fonts/css.ts +2 -1
  234. package/src/fonts/local.ts +7 -3
  235. package/src/index.ts +280 -85
  236. package/src/params/define.ts +260 -0
  237. package/src/params/index.ts +28 -0
  238. package/src/plugins/adapter-build.ts +6 -0
  239. package/src/plugins/build-manifest.ts +11 -0
  240. package/src/plugins/client-chunks.ts +65 -0
  241. package/src/plugins/dev-error-overlay.ts +70 -1
  242. package/src/plugins/dev-server.ts +38 -4
  243. package/src/plugins/entries.ts +12 -11
  244. package/src/plugins/fonts.ts +171 -19
  245. package/src/plugins/mdx.ts +9 -5
  246. package/src/plugins/routing.ts +40 -14
  247. package/src/plugins/server-bundle.ts +32 -1
  248. package/src/plugins/shims.ts +1 -1
  249. package/src/plugins/static-build.ts +8 -4
  250. package/src/routing/codegen.ts +109 -88
  251. package/src/routing/scanner.ts +55 -6
  252. package/src/routing/status-file-lint.ts +2 -1
  253. package/src/routing/types.ts +7 -4
  254. package/src/rsc-runtime/rsc.ts +2 -0
  255. package/src/rsc-runtime/ssr.ts +50 -0
  256. package/src/rsc-runtime/vendor-types.d.ts +7 -0
  257. package/src/search-params/codecs.ts +1 -1
  258. package/src/search-params/define.ts +504 -0
  259. package/src/search-params/index.ts +12 -18
  260. package/src/search-params/registry.ts +1 -1
  261. package/src/search-params/wrappers.ts +85 -0
  262. package/src/server/access-gate.tsx +40 -9
  263. package/src/server/action-client.ts +14 -5
  264. package/src/server/action-encryption.ts +144 -0
  265. package/src/server/action-handler.ts +19 -2
  266. package/src/server/als-registry.ts +18 -4
  267. package/src/server/build-manifest.ts +4 -4
  268. package/src/server/compress.ts +25 -7
  269. package/src/server/debug.ts +55 -17
  270. package/src/server/default-logger.ts +98 -0
  271. package/src/server/deny-renderer.ts +2 -1
  272. package/src/server/early-hints.ts +36 -15
  273. package/src/server/error-boundary-wrapper.ts +57 -14
  274. package/src/server/flight-injection-state.ts +152 -0
  275. package/src/server/flight-scripts.ts +59 -0
  276. package/src/server/flush.ts +2 -1
  277. package/src/server/form-data.ts +76 -0
  278. package/src/server/html-injectors.ts +103 -66
  279. package/src/server/index.ts +9 -4
  280. package/src/server/logger.ts +38 -35
  281. package/src/server/node-stream-transforms.ts +381 -0
  282. package/src/server/pipeline.ts +131 -39
  283. package/src/server/primitives.ts +47 -5
  284. package/src/server/render-timeout.ts +108 -0
  285. package/src/server/request-context.ts +112 -119
  286. package/src/server/route-element-builder.ts +106 -114
  287. package/src/server/route-handler.ts +2 -1
  288. package/src/server/route-matcher.ts +2 -2
  289. package/src/server/rsc-entry/error-renderer.ts +5 -3
  290. package/src/server/rsc-entry/helpers.ts +122 -3
  291. package/src/server/rsc-entry/index.ts +125 -49
  292. package/src/server/rsc-entry/rsc-payload.ts +52 -12
  293. package/src/server/rsc-entry/rsc-stream.ts +33 -8
  294. package/src/server/rsc-entry/ssr-renderer.ts +40 -13
  295. package/src/server/slot-resolver.ts +199 -210
  296. package/src/server/ssr-entry.ts +168 -22
  297. package/src/server/ssr-render.ts +289 -67
  298. package/src/server/tracing.ts +23 -0
  299. package/src/server/tree-builder.ts +91 -57
  300. package/src/server/types.ts +1 -3
  301. package/src/server/version-skew.ts +104 -0
  302. package/src/server/waituntil-bridge.ts +4 -1
  303. package/src/shared/merge-search-params.ts +48 -0
  304. package/src/shims/navigation-client.ts +1 -1
  305. package/src/shims/navigation.ts +1 -1
  306. package/src/utils/state-machine.ts +111 -0
  307. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  308. package/dist/_chunks/debug-B4WUeqJ-.js +0 -75
  309. package/dist/_chunks/debug-B4WUeqJ-.js.map +0 -1
  310. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  311. package/dist/_chunks/request-context-CZJi4CuK.js.map +0 -1
  312. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  313. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  314. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  315. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  316. package/dist/client/error-boundary.js.map +0 -1
  317. package/dist/cookies/index.js.map +0 -1
  318. package/dist/plugins/dynamic-transform.d.ts +0 -72
  319. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  320. package/dist/search-params/analyze.d.ts +0 -54
  321. package/dist/search-params/analyze.d.ts.map +0 -1
  322. package/dist/search-params/builtin-codecs.d.ts +0 -105
  323. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  324. package/dist/search-params/create.d.ts +0 -106
  325. package/dist/search-params/create.d.ts.map +0 -1
  326. package/dist/search-params/index.js.map +0 -1
  327. package/dist/server/prerender.d.ts +0 -77
  328. package/dist/server/prerender.d.ts.map +0 -1
  329. package/dist/server/response-cache.d.ts +0 -53
  330. package/dist/server/response-cache.d.ts.map +0 -1
  331. package/src/plugins/dynamic-transform.ts +0 -161
  332. package/src/search-params/analyze.ts +0 -192
  333. package/src/search-params/builtin-codecs.ts +0 -228
  334. package/src/search-params/create.ts +0 -321
  335. package/src/server/prerender.ts +0 -139
  336. package/src/server/response-cache.ts +0 -277
@@ -14,13 +14,15 @@
14
14
  * Design doc: 24-fonts.md
15
15
  */
16
16
 
17
- import type { Plugin } from 'vite';
17
+ import type { Plugin, ViteDevServer } from 'vite';
18
+ import { readFileSync, existsSync } from 'node:fs';
19
+ import { resolve, normalize } from 'node:path';
18
20
  import type { PluginContext } from '#/index.js';
19
21
  import type { ExtractedFont, GoogleFontConfig } from '#/fonts/types.js';
20
22
  import type { ManifestFontEntry } from '#/server/build-manifest.js';
21
- import { generateVariableClass, generateFontFamilyClass } from '#/fonts/css.js';
23
+ import { generateVariableClass, generateFontFamilyClass, generateFontFaces } from '#/fonts/css.js';
22
24
  import { generateFallbackCss, buildFontStack } from '#/fonts/fallbacks.js';
23
- import { processLocalFont } from '#/fonts/local.js';
25
+ import { processLocalFont, generateLocalFontFaces } from '#/fonts/local.js';
24
26
  import { inferFontFormat } from '#/fonts/local.js';
25
27
  import { downloadAndCacheFonts, type CachedFont } from '#/fonts/google.js';
26
28
  import {
@@ -34,6 +36,23 @@ const VIRTUAL_LOCAL = '@timber/fonts/local';
34
36
  const RESOLVED_GOOGLE = '\0@timber/fonts/google';
35
37
  const RESOLVED_LOCAL = '\0@timber/fonts/local';
36
38
 
39
+ /**
40
+ * Virtual side-effect module that registers font CSS on globalThis.
41
+ *
42
+ * When a file calls localFont() or a Google font function, the transform
43
+ * hook injects `import 'virtual:timber-font-css-register'` into that file.
44
+ * This virtual module sets `globalThis.__timber_font_css` with the combined
45
+ * @font-face CSS. The RSC entry reads it at render time to inline a <style> tag.
46
+ *
47
+ * This approach avoids timing issues because:
48
+ * 1. The font file is in the RSC module graph (imported by layout.tsx)
49
+ * 2. The side-effect import is added to the font file during transform
50
+ * 3. When layout.tsx is loaded, fonts.ts runs → side-effect module runs → globalThis is set
51
+ * 4. RSC entry renders → reads globalThis → inlines <style>
52
+ */
53
+ const VIRTUAL_FONT_CSS_REGISTER = 'virtual:timber-font-css-register';
54
+ const RESOLVED_FONT_CSS_REGISTER = '\0virtual:timber-font-css-register';
55
+
37
56
  /**
38
57
  * Registry of fonts extracted during transform.
39
58
  * Keyed by a unique font ID derived from family + config.
@@ -242,27 +261,44 @@ function generateLocalVirtualModule(): string {
242
261
  ].join('\n');
243
262
  }
244
263
 
264
+ /**
265
+ * Generate CSS for a single extracted font.
266
+ *
267
+ * Includes @font-face rules (for local fonts), fallback @font-face,
268
+ * and the scoped class rule.
269
+ */
270
+ export function generateFontCss(font: ExtractedFont): string {
271
+ const cssParts: string[] = [];
272
+
273
+ if (font.provider === 'local' && font.localSources) {
274
+ const faces = generateLocalFontFaces(font.family, font.localSources, font.display);
275
+ const faceCss = generateFontFaces(faces);
276
+ if (faceCss) cssParts.push(faceCss);
277
+ }
278
+
279
+ const fallbackCss = generateFallbackCss(font.family);
280
+ if (fallbackCss) cssParts.push(fallbackCss);
281
+
282
+ if (font.variable) {
283
+ cssParts.push(generateVariableClass(font.className, font.variable, font.fontFamily));
284
+ } else {
285
+ cssParts.push(generateFontFamilyClass(font.className, font.fontFamily));
286
+ }
287
+
288
+ return cssParts.join('\n\n');
289
+ }
290
+
245
291
  /**
246
292
  * Generate the CSS output for all extracted fonts.
247
293
  *
248
- * Includes @font-face rules, fallback @font-face rules, and scoped classes.
294
+ * Includes @font-face rules for local fonts, fallback @font-face rules,
295
+ * and scoped classes.
249
296
  */
250
297
  export function generateAllFontCss(registry: FontRegistry): string {
251
298
  const cssParts: string[] = [];
252
-
253
299
  for (const font of registry.values()) {
254
- // Generate fallback @font-face if metrics are available
255
- const fallbackCss = generateFallbackCss(font.family);
256
- if (fallbackCss) cssParts.push(fallbackCss);
257
-
258
- // Generate scoped class
259
- if (font.variable) {
260
- cssParts.push(generateVariableClass(font.className, font.variable, font.fontFamily));
261
- } else {
262
- cssParts.push(generateFontFamilyClass(font.className, font.fontFamily));
263
- }
300
+ cssParts.push(generateFontCss(font));
264
301
  }
265
-
266
302
  return cssParts.join('\n\n');
267
303
  }
268
304
 
@@ -359,23 +395,112 @@ export function timberFonts(ctx: PluginContext): Plugin {
359
395
  name: 'timber-fonts',
360
396
 
361
397
  /**
362
- * Resolve `@timber/fonts/google` and `@timber/fonts/local` to virtual modules.
398
+ * Resolve `@timber/fonts/google`, `@timber/fonts/local`,
399
+ * and `virtual:timber-font-css` virtual modules.
400
+ *
401
+ * Handles \0 prefix and root prefix stripping for RSC/SSR
402
+ * environments where the RSC plugin re-imports virtual modules
403
+ * with additional prefixes.
363
404
  */
364
405
  resolveId(id: string) {
365
- if (id === VIRTUAL_GOOGLE) return RESOLVED_GOOGLE;
366
- if (id === VIRTUAL_LOCAL) return RESOLVED_LOCAL;
406
+ // Strip \0 prefix (RSC plugin re-imports)
407
+ let cleanId = id.startsWith('\0') ? id.slice(1) : id;
408
+ // Strip root prefix (SSR build entries)
409
+ if (cleanId.startsWith(ctx.root)) {
410
+ const stripped = cleanId.slice(ctx.root.length);
411
+ if (stripped.startsWith('/') || stripped.startsWith('\\')) {
412
+ cleanId = stripped.slice(1);
413
+ } else {
414
+ cleanId = stripped;
415
+ }
416
+ }
417
+
418
+ if (cleanId === VIRTUAL_GOOGLE) return RESOLVED_GOOGLE;
419
+ if (cleanId === VIRTUAL_LOCAL) return RESOLVED_LOCAL;
420
+ if (cleanId === VIRTUAL_FONT_CSS_REGISTER) return RESOLVED_FONT_CSS_REGISTER;
367
421
  return null;
368
422
  },
369
423
 
370
424
  /**
371
425
  * Return generated source for font virtual modules.
426
+ *
427
+ * `virtual:timber-font-css` exports the combined @font-face CSS
428
+ * as a string. The RSC entry imports it and inlines a <style> tag.
429
+ * Because this is loaded lazily (on first request), the font
430
+ * registry is always populated by the time it's needed.
372
431
  */
373
432
  load(id: string) {
374
433
  if (id === RESOLVED_GOOGLE) return generateGoogleVirtualModule(registry);
375
434
  if (id === RESOLVED_LOCAL) return generateLocalVirtualModule();
435
+
436
+ if (id === RESOLVED_FONT_CSS_REGISTER) {
437
+ const css = generateAllFontCss(registry);
438
+ // Side-effect module: sets font CSS on globalThis for the RSC entry to read.
439
+ return `globalThis.__timber_font_css = ${JSON.stringify(css)};`;
440
+ }
376
441
  return null;
377
442
  },
378
443
 
444
+ /**
445
+ * Serve local font files and font CSS in dev mode under `/_timber/fonts/`.
446
+ *
447
+ * Serves:
448
+ * - `/_timber/fonts/fonts.css` — combined @font-face + scoped class CSS
449
+ * - `/_timber/fonts/<filename>` — individual font files from the registry
450
+ *
451
+ * Only files registered in the font registry are served.
452
+ * Paths are validated to prevent directory traversal.
453
+ */
454
+ configureServer(server: ViteDevServer) {
455
+ server.middlewares.use((req, res, next) => {
456
+ const url = req.url;
457
+ if (!url || !url.startsWith('/_timber/fonts/')) return next();
458
+
459
+ const requestedFilename = url.slice('/_timber/fonts/'.length);
460
+ // Reject path traversal attempts
461
+ if (requestedFilename.includes('..') || requestedFilename.includes('/')) {
462
+ res.statusCode = 400;
463
+ res.end('Bad request');
464
+ return;
465
+ }
466
+
467
+ // Font CSS is now injected via Vite's CSS pipeline (virtual:timber-font-css modules).
468
+ // This middleware only serves font binary files (woff2, etc.).
469
+
470
+ // Find the matching font file in the registry
471
+ for (const font of registry.values()) {
472
+ if (font.provider !== 'local' || !font.localSources) continue;
473
+ for (const src of font.localSources) {
474
+ const basename = src.path.split('/').pop() ?? '';
475
+ if (basename === requestedFilename) {
476
+ const absolutePath = normalize(resolve(src.path));
477
+ if (!existsSync(absolutePath)) {
478
+ res.statusCode = 404;
479
+ res.end('Not found');
480
+ return;
481
+ }
482
+ const data = readFileSync(absolutePath);
483
+ const ext = absolutePath.split('.').pop()?.toLowerCase();
484
+ const mimeMap: Record<string, string> = {
485
+ woff2: 'font/woff2',
486
+ woff: 'font/woff',
487
+ ttf: 'font/ttf',
488
+ otf: 'font/otf',
489
+ eot: 'application/vnd.ms-fontopen',
490
+ };
491
+ res.setHeader('Content-Type', mimeMap[ext ?? ''] ?? 'application/octet-stream');
492
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
493
+ res.setHeader('Access-Control-Allow-Origin', '*');
494
+ res.end(data);
495
+ return;
496
+ }
497
+ }
498
+ }
499
+
500
+ next();
501
+ });
502
+ },
503
+
379
504
  /**
380
505
  * Download and cache Google Fonts during production builds.
381
506
  *
@@ -499,6 +624,11 @@ export function timberFonts(ctx: PluginContext): Plugin {
499
624
  }
500
625
 
501
626
  if (transformedCode !== code) {
627
+ // Inject side-effect import that registers font CSS on globalThis.
628
+ // The RSC entry reads globalThis.__timber_font_css to inline a <style> tag.
629
+ if (registry.size > 0) {
630
+ transformedCode = `import '${VIRTUAL_FONT_CSS_REGISTER}';\n` + transformedCode;
631
+ }
502
632
  return { code: transformedCode, map: null };
503
633
  }
504
634
 
@@ -525,6 +655,28 @@ export function timberFonts(ctx: PluginContext): Plugin {
525
655
  });
526
656
  }
527
657
 
658
+ // Emit local font files as assets
659
+ for (const font of registry.values()) {
660
+ if (font.provider !== 'local' || !font.localSources) continue;
661
+ for (const src of font.localSources) {
662
+ const absolutePath = normalize(resolve(src.path));
663
+ if (!existsSync(absolutePath)) {
664
+ this.warn(`Local font file not found: ${absolutePath}`);
665
+ continue;
666
+ }
667
+ const basename = src.path.split('/').pop() ?? src.path;
668
+ const data = readFileSync(absolutePath);
669
+ this.emitFile({
670
+ type: 'asset',
671
+ fileName: `_timber/fonts/${basename}`,
672
+ source: data,
673
+ });
674
+ }
675
+ }
676
+
677
+ // Font CSS is emitted by Vite's CSS pipeline via virtual:timber-font-css modules.
678
+ // We only need to emit font binary files and update the build manifest here.
679
+
528
680
  if (!ctx.buildManifest) return;
529
681
 
530
682
  // Build a lookup from font family → cached files for manifest entries
@@ -16,19 +16,23 @@ import type { PluginContext } from '#/index.js';
16
16
  const MDX_EXTENSIONS = ['mdx', 'md'];
17
17
 
18
18
  /**
19
- * Check if mdx-components.tsx (or .ts, .jsx, .js) exists at the project root.
19
+ * Check if mdx-components.tsx (or .ts, .jsx, .js) exists at the project root
20
+ * or in src/. Root takes precedence, matching Next.js behavior.
20
21
  * Returns the absolute path if found, otherwise undefined.
21
22
  */
22
- function findMdxComponents(root: string): string | undefined {
23
+ export function findMdxComponents(root: string): string | undefined {
23
24
  const candidates = [
24
25
  'mdx-components.tsx',
25
26
  'mdx-components.ts',
26
27
  'mdx-components.jsx',
27
28
  'mdx-components.js',
28
29
  ];
29
- for (const name of candidates) {
30
- const p = join(root, name);
31
- if (existsSync(p)) return p;
30
+ const dirs = [root, join(root, 'src')];
31
+ for (const dir of dirs) {
32
+ for (const name of candidates) {
33
+ const p = join(dir, name);
34
+ if (existsSync(p)) return p;
35
+ }
32
36
  }
33
37
  return undefined;
34
38
  }
@@ -28,7 +28,7 @@ const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_MODULE_ID}`;
28
28
  * File convention names we track for changes that require manifest regeneration.
29
29
  */
30
30
  const ROUTE_FILE_PATTERNS =
31
- /\/(page|layout|middleware|access|route|error|default|denied|search-params|\d{3}|[45]xx|not-found|forbidden|unauthorized|sitemap|robots|manifest|favicon|icon|opengraph-image|twitter-image|apple-icon)\./;
31
+ /\/(page|layout|middleware|access|route|error|default|denied|\d{3}|[45]xx|not-found|forbidden|unauthorized|sitemap|robots|manifest|favicon|icon|opengraph-image|twitter-image|apple-icon)\./;
32
32
 
33
33
  /**
34
34
  * Create the timber-routing Vite plugin.
@@ -168,20 +168,49 @@ export function timberRouting(ctx: PluginContext): Plugin {
168
168
  // Watch the app directory
169
169
  devServer.watcher.add(ctx.appDir);
170
170
 
171
- const handleFileChange = (filePath: string) => {
172
- // Only react to route-significant files in the app directory
171
+ /** Snapshot of the last generated manifest, used to detect structural changes. */
172
+ let lastManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree) : '';
173
+
174
+ /**
175
+ * Handle a route-significant file being added or removed.
176
+ * Always triggers a full-reload since the route tree structure changed.
177
+ */
178
+ const handleStructuralChange = (filePath: string) => {
173
179
  if (!filePath.startsWith(ctx.appDir)) return;
174
180
  if (!ROUTE_FILE_PATTERNS.test(filePath)) return;
175
181
 
176
- // Rescan the route tree
177
182
  rescan();
178
-
179
- // Invalidate the virtual module in all environments
183
+ lastManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree) : '';
180
184
  invalidateManifest(devServer);
181
185
  };
182
186
 
183
- devServer.watcher.on('add', handleFileChange);
184
- devServer.watcher.on('unlink', handleFileChange);
187
+ /**
188
+ * Handle a route file's content changing.
189
+ *
190
+ * Most content edits (JSX changes, fixing typos) don't affect route
191
+ * metadata — Vite's React Fast Refresh handles those via normal HMR.
192
+ * Only rescan and full-reload when route metadata actually changed
193
+ * (e.g., searchParams export added/removed, metadata export changed).
194
+ */
195
+ const handleContentChange = (filePath: string) => {
196
+ if (!filePath.startsWith(ctx.appDir)) return;
197
+ if (!ROUTE_FILE_PATTERNS.test(filePath)) return;
198
+
199
+ rescan();
200
+ const newManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree) : '';
201
+ if (newManifest !== lastManifest) {
202
+ lastManifest = newManifest;
203
+ invalidateManifest(devServer);
204
+ }
205
+ // Otherwise: content edit didn't change route metadata — let Vite HMR handle it
206
+ };
207
+
208
+ devServer.watcher.on('add', handleStructuralChange);
209
+ devServer.watcher.on('unlink', handleStructuralChange);
210
+ // Watch content changes to page files — searchParams detection depends
211
+ // on file contents (export const searchParams), not just file presence.
212
+ // But only full-reload when route metadata actually changes.
213
+ devServer.watcher.on('change', handleContentChange);
185
214
  // Also watch renames (which are add+unlink) — handled by the above
186
215
  },
187
216
  };
@@ -301,12 +330,9 @@ function generateManifestModule(tree: RouteTree): string {
301
330
  `${nextIndent}denied: { load: ${v}, filePath: ${JSON.stringify(node.denied.filePath)} },`
302
331
  );
303
332
  }
304
- if (node.searchParams) {
305
- const v = addImport(node.searchParams);
306
- parts.push(
307
- `${nextIndent}searchParams: { load: ${v}, filePath: ${JSON.stringify(node.searchParams.filePath)} },`
308
- );
309
- }
333
+ // searchParams is now a named export from page.tsx, not a separate file.
334
+ // The page module's searchParams export is loaded via the page's lazy import.
335
+ // Runtime registration happens in the route loader using the page module.
310
336
 
311
337
  // Status-code files
312
338
  if (node.statusFiles && node.statusFiles.size > 0) {
@@ -137,5 +137,36 @@ export function timberServerBundle(): Plugin[] {
137
137
  },
138
138
  };
139
139
 
140
- return [bundlePlugin, esmInitFixPlugin];
140
+ // Fix Rolldown's `createRequire(import.meta.url)` CJS interop shim for
141
+ // Cloudflare Workers. Rolldown emits this for CJS dependencies (e.g.
142
+ // @opentelemetry/context-async-hooks) that use `require()`. On Workers,
143
+ // `import.meta.url` is `undefined` for non-entry modules, causing:
144
+ // TypeError: The argument 'path' must be a file URL object, a file URL
145
+ // string, or an absolute path string. Received 'undefined'
146
+ //
147
+ // The fix: provide a fallback URL when `import.meta.url` is undefined.
148
+ // The actual URL doesn't matter — `createRequire` only needs it for
149
+ // resolving relative paths, but the only `__require()` calls are for
150
+ // Node built-ins (events, async_hooks) which resolve from any base.
151
+ //
152
+ // The top-level `ssr: { target: 'webworker' }` was supposed to prevent
153
+ // this, but it doesn't propagate to custom environments (rsc) in Vite's
154
+ // Environment API. See LOCAL-405.
155
+ const createRequireFixPlugin: Plugin = {
156
+ name: 'timber-create-require-fix',
157
+ applyToEnvironment(environment) {
158
+ return environment.name === 'rsc' || environment.name === 'ssr';
159
+ },
160
+ renderChunk(code) {
161
+ const pattern = 'createRequire(import.meta.url)';
162
+ if (!code.includes(pattern)) return null;
163
+
164
+ return {
165
+ code: code.replace(pattern, 'createRequire(import.meta.url || "file:///app")'),
166
+ map: null,
167
+ };
168
+ },
169
+ };
170
+
171
+ return [bundlePlugin, esmInitFixPlugin, createRequireFixPlugin];
141
172
  }
@@ -137,7 +137,7 @@ export function timberShims(_ctx: PluginContext): Plugin {
137
137
  // package.json exports), creating a module instance split: ssr-entry.ts
138
138
  // registers the ALS-backed SSR data provider on the src/ instance of
139
139
  // ssr-data.ts, but client component hooks read getSsrData() from the
140
- // dist/ instance — which has no provider. Result: hooks like useParams()
140
+ // dist/ instance — which has no provider. Result: hooks like useSegmentParams()
141
141
  // return empty defaults during SSR.
142
142
  //
143
143
  // This remap is SSR-only. The RSC environment still resolves to dist/
@@ -163,9 +163,6 @@ export function validateStaticMode(
163
163
  * - transform: Validates source files for static mode violations
164
164
  */
165
165
  export function timberStaticBuild(ctx: PluginContext): Plugin {
166
- const isStatic = ctx.config.output === 'static';
167
- const clientJavascriptDisabled = ctx.clientJavascript.disabled;
168
-
169
166
  return {
170
167
  name: 'timber-static-build',
171
168
 
@@ -177,6 +174,11 @@ export function timberStaticBuild(ctx: PluginContext): Plugin {
177
174
  * - When client JS disabled: 'use client' / 'use server' directives → build error
178
175
  */
179
176
  transform(code: string, id: string) {
177
+ // Read ctx.config lazily inside the hook — not at plugin construction
178
+ // time — so file-based config from timber.config.ts is respected.
179
+ // See TIM-451.
180
+ const isStatic = ctx.config.output === 'static';
181
+
180
182
  // Only active in static mode
181
183
  if (!isStatic) return null;
182
184
 
@@ -189,7 +191,9 @@ export function timberStaticBuild(ctx: PluginContext): Plugin {
189
191
  // Only check JS/TS files
190
192
  if (!/\.[jt]sx?$/.test(id)) return null;
191
193
 
192
- const errors = validateStaticMode(code, id, { clientJavascriptDisabled });
194
+ const errors = validateStaticMode(code, id, {
195
+ clientJavascriptDisabled: ctx.clientJavascript.disabled,
196
+ });
193
197
 
194
198
  if (errors.length > 0) {
195
199
  // Format all errors into a single build error message