@timber-js/app 0.2.0-alpha.157 → 0.2.0-alpha.159

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 (290) hide show
  1. package/dist/_chunks/{actions-CqE7SW7E.js → actions-CBbP1hHO.js} +22 -6
  2. package/dist/_chunks/actions-CBbP1hHO.js.map +1 -0
  3. package/dist/_chunks/{build-output-helper-C8q4Zpf5.js → build-output-helper-DF5uScqS.js} +4 -1
  4. package/dist/_chunks/{build-output-helper-C8q4Zpf5.js.map → build-output-helper-DF5uScqS.js.map} +1 -1
  5. package/dist/_chunks/cache-api-DsWEYwVZ.js +1120 -0
  6. package/dist/_chunks/cache-api-DsWEYwVZ.js.map +1 -0
  7. package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js → cli-schema-sync-Bivx4Rn7.js} +4 -3
  8. package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js.map → cli-schema-sync-Bivx4Rn7.js.map} +1 -1
  9. package/dist/_chunks/dist-BA3u1z3W.js +423 -0
  10. package/dist/_chunks/dist-BA3u1z3W.js.map +1 -0
  11. package/dist/_chunks/{logger-C5v1IMuh.js → logger-CrRmZMlo.js} +81 -56
  12. package/dist/_chunks/logger-CrRmZMlo.js.map +1 -0
  13. package/dist/_chunks/{plugin-context-DUl3dnCF.js → plugin-context-DeAxFRMq.js} +3 -36
  14. package/dist/_chunks/plugin-context-DeAxFRMq.js.map +1 -0
  15. package/dist/_chunks/sizeof-lXffQ1Vr.js +35 -0
  16. package/dist/_chunks/sizeof-lXffQ1Vr.js.map +1 -0
  17. package/dist/_chunks/{walkers-D5jCGPum.js → walkers-DXbJkeCY.js} +217 -19
  18. package/dist/_chunks/walkers-DXbJkeCY.js.map +1 -0
  19. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  20. package/dist/adapters/cloudflare.d.ts +28 -0
  21. package/dist/adapters/cloudflare.d.ts.map +1 -1
  22. package/dist/adapters/cloudflare.js +143 -9
  23. package/dist/adapters/cloudflare.js.map +1 -1
  24. package/dist/adapters/nitro.js +1 -1
  25. package/dist/cache/cache-api.d.ts +2 -0
  26. package/dist/cache/cache-api.d.ts.map +1 -1
  27. package/dist/cache/handler-store.d.ts +2 -0
  28. package/dist/cache/handler-store.d.ts.map +1 -1
  29. package/dist/cache/index.d.ts +2 -0
  30. package/dist/cache/index.d.ts.map +1 -1
  31. package/dist/cache/index.js +3 -186
  32. package/dist/cache/redis-handler.d.ts +17 -5
  33. package/dist/cache/redis-handler.d.ts.map +1 -1
  34. package/dist/cache/singleflight.d.ts +1 -1
  35. package/dist/cache/singleflight.d.ts.map +1 -1
  36. package/dist/cache/store.d.ts +53 -0
  37. package/dist/cache/store.d.ts.map +1 -0
  38. package/dist/cache/stores/cloudflare-kv.d.ts +38 -0
  39. package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -0
  40. package/dist/cache/stores/cloudflare-kv.js +29 -0
  41. package/dist/cache/stores/cloudflare-kv.js.map +1 -0
  42. package/dist/cache/stores/memory.d.ts +42 -0
  43. package/dist/cache/stores/memory.d.ts.map +1 -0
  44. package/dist/cache/stores/memory.js +90 -0
  45. package/dist/cache/stores/memory.js.map +1 -0
  46. package/dist/cache/stores/redis.d.ts +69 -0
  47. package/dist/cache/stores/redis.d.ts.map +1 -0
  48. package/dist/cache/stores/redis.js +35 -0
  49. package/dist/cache/stores/redis.js.map +1 -0
  50. package/dist/cache/stores/vercel.d.ts +36 -0
  51. package/dist/cache/stores/vercel.d.ts.map +1 -0
  52. package/dist/cache/stores/vercel.js +33 -0
  53. package/dist/cache/stores/vercel.js.map +1 -0
  54. package/dist/cache/tag-aware-handler.d.ts +51 -0
  55. package/dist/cache/tag-aware-handler.d.ts.map +1 -0
  56. package/dist/cache/timber-cache.d.ts.map +1 -1
  57. package/dist/cdn/cloudflare-purge.d.ts +23 -0
  58. package/dist/cdn/cloudflare-purge.d.ts.map +1 -0
  59. package/dist/cdn/fastly-purge.d.ts +25 -0
  60. package/dist/cdn/fastly-purge.d.ts.map +1 -0
  61. package/dist/cdn/headers.d.ts +82 -0
  62. package/dist/cdn/headers.d.ts.map +1 -0
  63. package/dist/cdn/index.d.ts +5 -0
  64. package/dist/cdn/index.d.ts.map +1 -0
  65. package/dist/cdn/purge-store.d.ts +23 -0
  66. package/dist/cdn/purge-store.d.ts.map +1 -0
  67. package/dist/cli.js +2 -2
  68. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  69. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  70. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  71. package/dist/client/history.d.ts +0 -3
  72. package/dist/client/history.d.ts.map +1 -1
  73. package/dist/client/index.js +0 -1
  74. package/dist/client/index.js.map +1 -1
  75. package/dist/client/internal.js +11 -86
  76. package/dist/client/internal.js.map +1 -1
  77. package/dist/client/navigation-api.d.ts +1 -3
  78. package/dist/client/navigation-api.d.ts.map +1 -1
  79. package/dist/client/router.d.ts +1 -4
  80. package/dist/client/router.d.ts.map +1 -1
  81. package/dist/client/rsc-fetch.d.ts +1 -11
  82. package/dist/client/rsc-fetch.d.ts.map +1 -1
  83. package/dist/client/segment-cache.d.ts +1 -3
  84. package/dist/client/segment-cache.d.ts.map +1 -1
  85. package/dist/client/use-cookie.d.ts.map +1 -1
  86. package/dist/config-types.d.ts +3 -1
  87. package/dist/config-types.d.ts.map +1 -1
  88. package/dist/config-validation.d.ts.map +1 -1
  89. package/dist/cookies/index.js +15 -11
  90. package/dist/cookies/index.js.map +1 -1
  91. package/dist/dev-tools/browser-logs.d.ts +28 -72
  92. package/dist/dev-tools/browser-logs.d.ts.map +1 -1
  93. package/dist/dev-tools/index.d.ts +1 -1
  94. package/dist/dev-tools/index.d.ts.map +1 -1
  95. package/dist/dev-tools/logger.d.ts.map +1 -1
  96. package/dist/dev-tools/terminal.d.ts.map +1 -1
  97. package/dist/fonts/transform.d.ts +4 -0
  98. package/dist/fonts/transform.d.ts.map +1 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +1269 -359
  101. package/dist/index.js.map +1 -1
  102. package/dist/plugin-context.d.ts +36 -0
  103. package/dist/plugin-context.d.ts.map +1 -1
  104. package/dist/plugins/build-report.d.ts +21 -0
  105. package/dist/plugins/build-report.d.ts.map +1 -1
  106. package/dist/plugins/cache.d.ts +5 -14
  107. package/dist/plugins/cache.d.ts.map +1 -1
  108. package/dist/plugins/callsite-ast.d.ts +95 -0
  109. package/dist/plugins/callsite-ast.d.ts.map +1 -0
  110. package/dist/plugins/callsite-jsx.d.ts +35 -0
  111. package/dist/plugins/callsite-jsx.d.ts.map +1 -0
  112. package/dist/plugins/entries.d.ts.map +1 -1
  113. package/dist/plugins/fonts.d.ts.map +1 -1
  114. package/dist/plugins/prebuilt-capture.d.ts +60 -0
  115. package/dist/plugins/prebuilt-capture.d.ts.map +1 -0
  116. package/dist/plugins/prebuilt.d.ts +82 -0
  117. package/dist/plugins/prebuilt.d.ts.map +1 -0
  118. package/dist/plugins/prerender-sugar.d.ts +42 -0
  119. package/dist/plugins/prerender-sugar.d.ts.map +1 -0
  120. package/dist/plugins/server-bundle.d.ts.map +1 -1
  121. package/dist/plugins/shims.d.ts.map +1 -1
  122. package/dist/plugins/static-build.d.ts.map +1 -1
  123. package/dist/routing/convention-lint.d.ts.map +1 -1
  124. package/dist/routing/export-detect.d.ts +56 -0
  125. package/dist/routing/export-detect.d.ts.map +1 -0
  126. package/dist/routing/index.js +2 -2
  127. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  128. package/dist/rsc-runtime/rsc.d.ts +1 -1
  129. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  130. package/dist/server/action-handler.d.ts.map +1 -1
  131. package/dist/server/actions.d.ts +0 -3
  132. package/dist/server/actions.d.ts.map +1 -1
  133. package/dist/server/als-registry.d.ts +7 -0
  134. package/dist/server/als-registry.d.ts.map +1 -1
  135. package/dist/server/cookie-parsing.d.ts +15 -1
  136. package/dist/server/cookie-parsing.d.ts.map +1 -1
  137. package/dist/server/deny-renderer.d.ts.map +1 -1
  138. package/dist/server/form-data.d.ts +12 -0
  139. package/dist/server/form-data.d.ts.map +1 -1
  140. package/dist/server/html-injectors.d.ts +1 -0
  141. package/dist/server/html-injectors.d.ts.map +1 -1
  142. package/dist/server/index.js +2 -2
  143. package/dist/server/internal.d.ts +6 -1
  144. package/dist/server/internal.d.ts.map +1 -1
  145. package/dist/server/internal.js +563 -74
  146. package/dist/server/internal.js.map +1 -1
  147. package/dist/server/prebuilt/cache-key.d.ts +58 -0
  148. package/dist/server/prebuilt/cache-key.d.ts.map +1 -0
  149. package/dist/server/prebuilt/capture-state.d.ts +57 -0
  150. package/dist/server/prebuilt/capture-state.d.ts.map +1 -0
  151. package/dist/server/prebuilt/manifest-tag-index.d.ts +27 -0
  152. package/dist/server/prebuilt/manifest-tag-index.d.ts.map +1 -0
  153. package/dist/server/prebuilt/overlay.d.ts +100 -0
  154. package/dist/server/prebuilt/overlay.d.ts.map +1 -0
  155. package/dist/server/prebuilt/payload-source.d.ts +75 -0
  156. package/dist/server/prebuilt/payload-source.d.ts.map +1 -0
  157. package/dist/server/prebuilt/synthetic-store.d.ts +64 -0
  158. package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -0
  159. package/dist/server/prebuilt-builder.d.ts +110 -0
  160. package/dist/server/prebuilt-builder.d.ts.map +1 -0
  161. package/dist/server/prebuilt-runtime.d.ts +101 -0
  162. package/dist/server/prebuilt-runtime.d.ts.map +1 -0
  163. package/dist/server/route-element-builder.d.ts +10 -8
  164. package/dist/server/route-element-builder.d.ts.map +1 -1
  165. package/dist/server/rsc-entry/helpers.d.ts +0 -5
  166. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  167. package/dist/server/rsc-entry/index.d.ts +10 -0
  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/revalidate-renderer.d.ts.map +1 -1
  171. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -4
  172. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  173. package/dist/server/sitemap-generator.d.ts +6 -6
  174. package/dist/server/sitemap-generator.d.ts.map +1 -1
  175. package/dist/server/tracing.d.ts +37 -3
  176. package/dist/server/tracing.d.ts.map +1 -1
  177. package/docs/api/32-api-cache.mdx +10 -4
  178. package/docs/learn/00-introduction.mdx +8 -13
  179. package/docs/learn/02-pages-and-layouts.mdx +9 -0
  180. package/docs/learn/06-forms-and-actions.mdx +0 -4
  181. package/docs/learn/07-typed-routes.mdx +127 -0
  182. package/docs/learn/09-caching.mdx +12 -1
  183. package/docs/learn/10-middleware.mdx +1 -0
  184. package/docs/learn/11-error-handling.mdx +9 -0
  185. package/docs/learn/13-configuration.mdx +28 -0
  186. package/docs/learn/14-deploying.mdx +52 -0
  187. package/docs/more/06-instrumentation.mdx +27 -1
  188. package/docs/more/07-security.mdx +8 -2
  189. package/docs/more/08-developer-experience.mdx +134 -0
  190. package/package.json +31 -2
  191. package/src/adapters/build-output-helper.ts +10 -0
  192. package/src/adapters/cloudflare.ts +178 -4
  193. package/src/cache/cache-api.ts +60 -0
  194. package/src/cache/handler-store.ts +8 -0
  195. package/src/cache/index.ts +4 -0
  196. package/src/cache/redis-handler.ts +25 -13
  197. package/src/cache/singleflight.ts +11 -11
  198. package/src/cache/store.ts +71 -0
  199. package/src/cache/stores/cloudflare-kv.ts +52 -0
  200. package/src/cache/stores/memory.ts +125 -0
  201. package/src/cache/stores/redis.ts +97 -0
  202. package/src/cache/stores/vercel.ts +59 -0
  203. package/src/cache/tag-aware-handler.ts +233 -0
  204. package/src/cache/timber-cache.ts +17 -7
  205. package/src/cdn/cloudflare-purge.ts +50 -0
  206. package/src/cdn/fastly-purge.ts +63 -0
  207. package/src/cdn/headers.ts +127 -0
  208. package/src/cdn/index.ts +16 -0
  209. package/src/cdn/purge-store.ts +34 -0
  210. package/src/client/browser-entry/action-dispatch.ts +1 -10
  211. package/src/client/browser-entry/post-hydration.ts +0 -1
  212. package/src/client/browser-entry/router-init.ts +0 -5
  213. package/src/client/history.ts +0 -4
  214. package/src/client/navigation-api.ts +5 -11
  215. package/src/client/router.ts +9 -43
  216. package/src/client/rsc-fetch.ts +5 -27
  217. package/src/client/segment-cache.ts +1 -4
  218. package/src/client/use-cookie.ts +18 -12
  219. package/src/config-types.ts +3 -1
  220. package/src/config-validation.ts +1 -0
  221. package/src/dev-tools/browser-logs.ts +37 -273
  222. package/src/dev-tools/index.ts +2 -2
  223. package/src/dev-tools/instrumentation.ts +1 -1
  224. package/src/dev-tools/logger.ts +33 -51
  225. package/src/dev-tools/terminal.ts +21 -22
  226. package/src/fonts/transform.ts +4 -43
  227. package/src/index.ts +16 -3
  228. package/src/plugin-context.ts +36 -0
  229. package/src/plugins/build-report.ts +133 -1
  230. package/src/plugins/cache.ts +32 -131
  231. package/src/plugins/callsite-ast.ts +167 -0
  232. package/src/plugins/callsite-jsx.ts +243 -0
  233. package/src/plugins/entries.ts +23 -4
  234. package/src/plugins/fonts.ts +16 -2
  235. package/src/plugins/prebuilt-capture.ts +208 -0
  236. package/src/plugins/prebuilt.ts +673 -0
  237. package/src/plugins/prerender-sugar.ts +329 -0
  238. package/src/plugins/server-bundle.ts +12 -0
  239. package/src/plugins/shims.ts +9 -0
  240. package/src/plugins/static-build.ts +0 -1
  241. package/src/routing/codegen.ts +1 -21
  242. package/src/routing/convention-lint.ts +85 -49
  243. package/src/routing/export-detect.ts +304 -0
  244. package/src/routing/manifest-codegen.ts +2 -11
  245. package/src/rsc-runtime/rsc.ts +7 -0
  246. package/src/server/action-handler.ts +11 -27
  247. package/src/server/actions.ts +3 -4
  248. package/src/server/als-registry.ts +7 -0
  249. package/src/server/cookie-parsing.ts +75 -59
  250. package/src/server/deny-renderer.ts +7 -15
  251. package/src/server/form-data.ts +23 -7
  252. package/src/server/html-injectors.ts +11 -3
  253. package/src/server/internal.ts +17 -1
  254. package/src/server/prebuilt/cache-key.ts +224 -0
  255. package/src/server/prebuilt/capture-state.ts +89 -0
  256. package/src/server/prebuilt/manifest-tag-index.ts +46 -0
  257. package/src/server/prebuilt/overlay.ts +256 -0
  258. package/src/server/prebuilt/payload-source.ts +192 -0
  259. package/src/server/prebuilt/synthetic-store.ts +108 -0
  260. package/src/server/prebuilt-builder.ts +640 -0
  261. package/src/server/prebuilt-runtime.ts +706 -0
  262. package/src/server/route-element-builder.ts +40 -12
  263. package/src/server/rsc-entry/helpers.ts +0 -17
  264. package/src/server/rsc-entry/index.ts +83 -0
  265. package/src/server/rsc-entry/render-route.ts +6 -33
  266. package/src/server/rsc-entry/revalidate-renderer.ts +16 -11
  267. package/src/server/rsc-entry/rsc-payload.ts +5 -11
  268. package/src/server/sitemap-generator.ts +16 -10
  269. package/src/server/tracing.ts +88 -3
  270. package/dist/_chunks/actions-CqE7SW7E.js.map +0 -1
  271. package/dist/_chunks/cache-api-Cbyh3RW4.js +0 -401
  272. package/dist/_chunks/cache-api-Cbyh3RW4.js.map +0 -1
  273. package/dist/_chunks/logger-C5v1IMuh.js.map +0 -1
  274. package/dist/_chunks/plugin-context-DUl3dnCF.js.map +0 -1
  275. package/dist/_chunks/validation-CnKhGJsp.js +0 -113
  276. package/dist/_chunks/validation-CnKhGJsp.js.map +0 -1
  277. package/dist/_chunks/walkers-D5jCGPum.js.map +0 -1
  278. package/dist/cache/index.js.map +0 -1
  279. package/dist/client/head.d.ts +0 -16
  280. package/dist/client/head.d.ts.map +0 -1
  281. package/dist/client/navigation-api-types.d.ts +0 -90
  282. package/dist/client/navigation-api-types.d.ts.map +0 -1
  283. package/dist/server/client-module-map.d.ts +0 -47
  284. package/dist/server/client-module-map.d.ts.map +0 -1
  285. package/dist/shared/head-header.d.ts +0 -54
  286. package/dist/shared/head-header.d.ts.map +0 -1
  287. package/src/client/head.ts +0 -61
  288. package/src/client/navigation-api-types.ts +0 -112
  289. package/src/server/client-module-map.ts +0 -58
  290. package/src/shared/head-header.ts +0 -69
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
- import { a as resolveAppDir, c as createNoopTimer, d as __toCommonJS, l as __esmMin, n as loadTimberConfigFile, o as resolveBuildDir, r as mergeFileConfig, s as resolveClientJavascript, t as createPluginContext, u as __exportAll } from "./_chunks/plugin-context-DUl3dnCF.js";
1
+ import { a as __esmMin, o as __exportAll, s as __toCommonJS } from "./_chunks/dist-BA3u1z3W.js";
2
2
  import { c as isDynamicMetadataExtension, l as isMetadataRouteServePath, r as canonicalize } from "./_chunks/canonicalize-P41GR6tY.js";
3
3
  import { t as matchUrlParts } from "./_chunks/tree-match-D2l830j2.js";
4
- import { h as swallow } from "./_chunks/logger-C5v1IMuh.js";
5
- import { i as scanRoutes } from "./_chunks/cli-schema-sync-DcmQrzJG.js";
6
- import { a as generateRouteMap, i as validateSchemaAgainstRoutes, n as collectInterceptionRewrites, o as readFileCached, t as collectLeafRoutes } from "./_chunks/walkers-D5jCGPum.js";
4
+ import { h as swallow } from "./_chunks/logger-CrRmZMlo.js";
5
+ import { i as scanRoutes } from "./_chunks/cli-schema-sync-Bivx4Rn7.js";
6
+ import { a as generateRouteMap, c as fileHasDirective, d as getPrerenderExport, i as validateSchemaAgainstRoutes, l as fileHasExport, n as collectInterceptionRewrites, o as fileHasAnyExport, s as fileHasDefaultExport, t as collectLeafRoutes, u as fileHasStarExport } from "./_chunks/walkers-DXbJkeCY.js";
7
+ import { a as resolveAppDir, c as createNoopTimer, n as loadTimberConfigFile, o as resolveBuildDir, r as mergeFileConfig, s as resolveClientJavascript, t as createPluginContext } from "./_chunks/plugin-context-DeAxFRMq.js";
7
8
  import { t as fnv1aHash } from "./_chunks/fast-hash-D6hIVt1Y.js";
8
9
  import { t as formatSize } from "./_chunks/format-CfwjgPz9.js";
9
10
  import { basename, dirname, extname, join, normalize, relative, resolve, win32 } from "node:path";
@@ -11,9 +12,10 @@ import { builtinModules, createRequire } from "node:module";
11
12
  import react, { reactCompilerPreset } from "@vitejs/plugin-react";
12
13
  import { existsSync, readFileSync, statSync } from "node:fs";
13
14
  import { fileURLToPath, pathToFileURL } from "node:url";
15
+ import { styleText } from "node:util";
14
16
  import { constants, createGzip, gzipSync } from "node:zlib";
15
17
  import { Readable, pipeline } from "node:stream";
16
- import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
18
+ import { copyFile, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
17
19
  import { createHash, randomUUID } from "node:crypto";
18
20
  import { parseAst } from "vite";
19
21
  import http from "node:http";
@@ -171,12 +173,8 @@ function classifyStack(stack, projectRoot) {
171
173
  *
172
174
  * Design doc: 21-dev-server.md §"Error Overlay"
173
175
  */
174
- var RED$2 = "\x1B[31m";
175
- var CYAN = "\x1B[36m";
176
- var DIM$2 = "\x1B[2m";
177
- var RESET$2 = "\x1B[0m";
178
- var BOLD$2 = "\x1B[1m";
179
- var UNDERLINE = "\x1B[4m";
176
+ var noValidate$1 = { validateStream: false };
177
+ var style$1 = (fmt, text) => styleText(fmt, text, noValidate$1);
180
178
  /**
181
179
  * Wrap text in an OSC 8 hyperlink escape sequence.
182
180
  *
@@ -198,7 +196,7 @@ function hyperlink(text, url) {
198
196
  function fileLink(filePath, line, col) {
199
197
  const display = line ? `${filePath}:${line}${col ? `:${col}` : ""}` : filePath;
200
198
  const base = pathToFileURL(filePath).href;
201
- return `${CYAN}${UNDERLINE}${hyperlink(display, line ? `${base}:${line}${col ? `:${col}` : ""}` : base)}${RESET$2}`;
199
+ return style$1(["cyan", "underline"], hyperlink(display, line ? `${base}:${line}${col ? `:${col}` : ""}` : base));
202
200
  }
203
201
  var BOX = {
204
202
  topLeft: "╭",
@@ -214,12 +212,12 @@ var BOX = {
214
212
  * @param lines - Content lines (no ANSI length calculation — keeps it simple)
215
213
  * @param width - Box width (characters). Lines longer than this are not truncated.
216
214
  */
217
- function box(lines, borderColor, width = 80) {
215
+ function box(lines, borderFormat, width = 80) {
218
216
  const bar = BOX.horizontal.repeat(width - 2);
219
217
  const output = [];
220
- output.push(`${borderColor}${BOX.topLeft}${bar}${BOX.topRight}${RESET$2}`);
221
- for (const line of lines) output.push(`${borderColor}${BOX.vertical}${RESET$2} ${line}`);
222
- output.push(`${borderColor}${BOX.bottomLeft}${bar}${BOX.bottomRight}${RESET$2}`);
218
+ output.push(style$1(borderFormat, `${BOX.topLeft}${bar}${BOX.topRight}`));
219
+ for (const line of lines) output.push(`${style$1(borderFormat, BOX.vertical)} ${line}`);
220
+ output.push(style$1(borderFormat, `${BOX.bottomLeft}${bar}${BOX.bottomRight}`));
223
221
  return output.join("\n");
224
222
  }
225
223
  /**
@@ -240,32 +238,32 @@ function formatTerminalError$1(error, phase, projectRoot) {
240
238
  const appFrames = frames.filter((f) => f.type === "app");
241
239
  const internalCount = frames.filter((f) => f.type !== "app").length;
242
240
  const boxLines = [];
243
- boxLines.push(`${RED$2}${BOLD$2}${PHASE_LABELS$1[phase]} Error${RESET$2}`);
241
+ boxLines.push(style$1(["red", "bold"], `${PHASE_LABELS$1[phase]} Error`));
244
242
  boxLines.push("");
245
- for (const msgLine of error.message.split("\n")) boxLines.push(`${RED$2}${msgLine}${RESET$2}`);
243
+ for (const msgLine of error.message.split("\n")) boxLines.push(style$1("red", msgLine));
246
244
  if (loc) {
247
245
  boxLines.push("");
248
246
  const relPath = loc.file.startsWith(projectRoot) ? loc.file.slice(projectRoot.length + 1) : loc.file;
249
- boxLines.push(`${BOLD$2}→${RESET$2} ${fileLink(loc.file, loc.line, loc.column)} ${DIM$2}(${relPath})${RESET$2}`);
247
+ boxLines.push(`${style$1("bold", "→")} ${fileLink(loc.file, loc.line, loc.column)} ${style$1("dim", `(${relPath})`)}`);
250
248
  }
251
- sections.push(box(boxLines, RED$2));
249
+ sections.push(box(boxLines, "red"));
252
250
  if (componentStack) {
253
251
  sections.push("");
254
- sections.push(` ${BOLD$2}Component Stack:${RESET$2}`);
255
- for (const csLine of componentStack.trim().split("\n")) sections.push(` ${DIM$2}${csLine.trim()}${RESET$2}`);
252
+ sections.push(` ${style$1("bold", "Component Stack:")}`);
253
+ for (const csLine of componentStack.trim().split("\n")) sections.push(` ${style$1("dim", csLine.trim())}`);
256
254
  }
257
255
  if (appFrames.length > 0) {
258
256
  sections.push("");
259
- sections.push(` ${BOLD$2}Application Frames:${RESET$2}`);
257
+ sections.push(` ${style$1("bold", "Application Frames:")}`);
260
258
  for (let i = 0; i < appFrames.length; i++) {
261
259
  const f = appFrames[i];
262
260
  if (f.file && f.line) {
263
- const prefix = i === 0 ? `${BOLD$2}▸${RESET$2}` : " ";
264
- sections.push(` ${prefix} ${fileLink(f.file, f.line, f.col)} ${DIM$2}${extractFnName(f.raw)}${RESET$2}`);
261
+ const prefix = i === 0 ? style$1("bold", "▸") : " ";
262
+ sections.push(` ${prefix} ${fileLink(f.file, f.line, f.col)} ${style$1("dim", extractFnName(f.raw))}`);
265
263
  } else sections.push(` ${f.raw}`);
266
264
  }
267
265
  }
268
- if (internalCount > 0) sections.push(` ${DIM$2}… ${internalCount} internal frame${internalCount !== 1 ? "s" : ""} hidden${RESET$2}`);
266
+ if (internalCount > 0) sections.push(` ${style$1("dim", `… ${internalCount} internal frame${internalCount !== 1 ? "s" : ""} hidden`)}`);
269
267
  sections.push("");
270
268
  return sections.join("\n");
271
269
  }
@@ -1147,11 +1145,11 @@ function validateConfig(config) {
1147
1145
  function formatConfigErrors(errors) {
1148
1146
  if (errors.length === 0) return "";
1149
1147
  const lines = [];
1150
- lines.push(`${RED$1}${BOLD$1}[timber]${RESET$1} ${RED$1}${errors.length} config error${errors.length !== 1 ? "s" : ""} in timber.config.ts:${RESET$1}`);
1148
+ lines.push(`${RED}${BOLD}[timber]${RESET} ${RED}${errors.length} config error${errors.length !== 1 ? "s" : ""} in timber.config.ts:${RESET}`);
1151
1149
  for (const err of errors) {
1152
1150
  lines.push("");
1153
- lines.push(` ${RED$1}✗${RESET$1} ${BOLD$1}${err.field}${RESET$1}: ${err.message}`);
1154
- if (err.suggestion) lines.push(` ${DIM$1}${err.suggestion}${RESET$1}`);
1151
+ lines.push(` ${RED}✗${RESET} ${BOLD}${err.field}${RESET}: ${err.message}`);
1152
+ if (err.suggestion) lines.push(` ${DIM}${err.suggestion}${RESET}`);
1155
1153
  }
1156
1154
  return lines.join("\n");
1157
1155
  }
@@ -1196,15 +1194,15 @@ function formatMissingPeers(results) {
1196
1194
  if (missing.length === 0) return "";
1197
1195
  const names = missing.map((r) => r.name);
1198
1196
  const lines = [];
1199
- lines.push(`${RED$1}${BOLD$1}[timber]${RESET$1} ${RED$1}Missing required dependencies:${RESET$1}`);
1197
+ lines.push(`${RED}${BOLD}[timber]${RESET} ${RED}Missing required dependencies:${RESET}`);
1200
1198
  lines.push("");
1201
- for (const name of names) lines.push(` ${RED$1}✗${RESET$1} ${name}`);
1199
+ for (const name of names) lines.push(` ${RED}✗${RESET} ${name}`);
1202
1200
  lines.push("");
1203
- lines.push(` ${DIM$1}Install with:${RESET$1}`);
1204
- lines.push(` ${BOLD$1}pnpm add ${names.join(" ")}${RESET$1}`);
1201
+ lines.push(` ${DIM}Install with:${RESET}`);
1202
+ lines.push(` ${BOLD}pnpm add ${names.join(" ")}${RESET}`);
1205
1203
  return lines.join("\n");
1206
1204
  }
1207
- var knownKeys, RED$1, BOLD$1, DIM$1, RESET$1, VIRTUAL_MODULE_NAMES, REQUIRED_PEERS;
1205
+ var knownKeys, RED, BOLD, DIM, RESET, VIRTUAL_MODULE_NAMES, REQUIRED_PEERS;
1208
1206
  var init_config_validation = __esmMin((() => {
1209
1207
  knownKeys = new Set(Object.keys({
1210
1208
  output: true,
@@ -1212,6 +1210,7 @@ var init_config_validation = __esmMin((() => {
1212
1210
  clientJavascript: true,
1213
1211
  adapter: true,
1214
1212
  cacheHandler: true,
1213
+ cdnPurge: true,
1215
1214
  allowedOrigins: true,
1216
1215
  csrf: true,
1217
1216
  limits: true,
@@ -1232,10 +1231,10 @@ var init_config_validation = __esmMin((() => {
1232
1231
  topLoader: true,
1233
1232
  budget: true
1234
1233
  }));
1235
- RED$1 = "\x1B[31m";
1236
- BOLD$1 = "\x1B[1m";
1237
- DIM$1 = "\x1B[2m";
1238
- RESET$1 = "\x1B[0m";
1234
+ RED = "\x1B[31m";
1235
+ BOLD = "\x1B[1m";
1236
+ DIM = "\x1B[2m";
1237
+ RESET = "\x1B[0m";
1239
1238
  VIRTUAL_MODULE_NAMES = {
1240
1239
  "virtual:timber-rsc-entry": "RSC entry (server component handler)",
1241
1240
  "virtual:timber-ssr-entry": "SSR entry (HTML renderer)",
@@ -1832,7 +1831,8 @@ function generateConfigModule(ctx) {
1832
1831
  renderTimeoutMs: ctx.config.renderTimeoutMs ?? 3e4,
1833
1832
  sitemap: ctx.config.sitemap,
1834
1833
  forms: serializeFormsConfig(ctx.config.forms),
1835
- deploymentId: ctx.deploymentId ?? null
1834
+ deploymentId: ctx.deploymentId ?? null,
1835
+ devBrowserLogs: ctx.config.devBrowserLogs
1836
1836
  };
1837
1837
  return [
1838
1838
  "// Auto-generated runtime config — do not edit.",
@@ -1914,8 +1914,8 @@ function detectConfigFile(root) {
1914
1914
  * cannot be JSON-serialized into virtual:timber-config, so it must
1915
1915
  * be loaded at runtime via dynamic import. See TIM-599.
1916
1916
  */
1917
- function generateCacheHandlerModule(configPath, hasCacheHandler) {
1918
- if (configPath && hasCacheHandler) return [
1917
+ function generateCacheHandlerModule(configPath, hasCacheHandler, hasCdnPurge) {
1918
+ if (configPath && (hasCacheHandler || hasCdnPurge)) return [
1919
1919
  "// Auto-generated cache handler loader — do not edit.",
1920
1920
  "// Generated by timber-entries plugin.",
1921
1921
  "",
@@ -1923,15 +1923,25 @@ function generateCacheHandlerModule(configPath, hasCacheHandler) {
1923
1923
  ` const mod = await import(${JSON.stringify(configPath)});`,
1924
1924
  ` const config = mod.default ?? mod;`,
1925
1925
  ` return config.cacheHandler ?? null;`,
1926
+ `}`,
1927
+ "",
1928
+ `export async function loadCdnPurgeHandler() {`,
1929
+ ` const mod = await import(${JSON.stringify(configPath)});`,
1930
+ ` const config = mod.default ?? mod;`,
1931
+ ` return config.cdnPurge ?? null;`,
1926
1932
  `}`
1927
1933
  ].join("\n");
1928
1934
  return [
1929
1935
  "// Auto-generated cache handler loader — do not edit.",
1930
1936
  "// Generated by timber-entries plugin.",
1931
- "// No cacheHandler configured in timber.config.",
1937
+ "// No cacheHandler or cdnPurge configured in timber.config.",
1932
1938
  "",
1933
1939
  `export default async function loadCacheHandler() {`,
1934
1940
  ` return null;`,
1941
+ `}`,
1942
+ "",
1943
+ `export async function loadCdnPurgeHandler() {`,
1944
+ ` return null;`,
1935
1945
  `}`
1936
1946
  ].join("\n");
1937
1947
  }
@@ -1972,7 +1982,7 @@ function timberEntries(ctx) {
1972
1982
  load(id) {
1973
1983
  if (id === RESOLVED_CONFIG_ID) return generateConfigModule(ctx);
1974
1984
  if (id === RESOLVED_INSTRUMENTATION_ID) return generateInstrumentationModule(detectInstrumentationFile(ctx.root));
1975
- if (id === RESOLVED_CACHE_HANDLER_ID) return generateCacheHandlerModule(detectConfigFile(ctx.root), ctx.config.cacheHandler != null);
1985
+ if (id === RESOLVED_CACHE_HANDLER_ID) return generateCacheHandlerModule(detectConfigFile(ctx.root), ctx.config.cacheHandler != null, ctx.config.cdnPurge != null);
1976
1986
  return null;
1977
1987
  }
1978
1988
  };
@@ -2335,13 +2345,7 @@ function generateSearchParamsRegistryModule(tree) {
2335
2345
  return lines.join("\n");
2336
2346
  }
2337
2347
  function fileHasSearchParamsExport(filePath) {
2338
- try {
2339
- if (!existsSync(filePath)) return false;
2340
- const content = readFileCached(filePath);
2341
- return /export\s+(const|let|var)\s+searchParams\b/.test(content) || /export\s*\{[^}]*\bsearchParams\b[^}]*\}/.test(content);
2342
- } catch {
2343
- return false;
2344
- }
2348
+ return fileHasExport(filePath, "searchParams");
2345
2349
  }
2346
2350
  /**
2347
2351
  * Generate the virtual:timber-schema module.
@@ -2399,6 +2403,8 @@ function lintConventions(tree, appDir) {
2399
2403
  checkRouteExports(tree.root, warnings);
2400
2404
  checkRootLayout(tree.root, warnings);
2401
2405
  checkDefaultExports(tree.root, warnings);
2406
+ checkErrorDirectives(tree.root, warnings);
2407
+ if (tree.globalError && isScriptExtension(tree.globalError.extension)) checkFileUseClientDirective(tree.globalError.filePath, "global-error", warnings);
2402
2408
  return warnings;
2403
2409
  }
2404
2410
  /**
@@ -2455,28 +2461,18 @@ var HTTP_METHODS = [
2455
2461
  "OPTIONS"
2456
2462
  ];
2457
2463
  /**
2458
- * Pattern to detect named exports that look like HTTP method handlers.
2459
- * Matches: export function GET, export const GET, export async function POST, etc.
2460
- * Also matches: export { GET } or re-exports like export { GET } from './handler'.
2461
- *
2462
- * The re-export branch uses word boundaries (\b) around method names to avoid
2463
- * matching substrings (e.g. TARGET should not match GET). See TIM-795.
2464
- */
2465
- var EXPORT_PATTERN = new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var)\\s+(${HTTP_METHODS.join("|")})\\b|export\\s*\\{[^}]*\\b(${HTTP_METHODS.join("|")})\\b[^}]*\\}`);
2466
- /**
2467
2464
  * Warn when a route.ts file doesn't appear to export any recognized HTTP methods.
2468
2465
  *
2469
- * Uses static analysis (regex on source text) — not module loading.
2470
- * This is intentionally conservative: it may miss complex re-exports but
2471
- * catches the common case of an empty route.ts or one with wrong export names.
2466
+ * Uses AST-based detection via parseAst for precise matching.
2467
+ * Falls back silently when the file can't be parsed (syntax errors produce
2468
+ * no spurious warning — the TS compiler will catch those).
2472
2469
  */
2473
2470
  function checkRouteExports(node, warnings) {
2474
2471
  if (node.route) {
2475
2472
  const filePath = node.route.filePath;
2476
2473
  try {
2477
- const source = readFileCached(filePath);
2478
- if (!EXPORT_PATTERN.test(source)) {
2479
- const hint = /export\s+default\b/.test(source) ? " It looks like you have a default export. route.ts uses named exports\n for HTTP methods (GET, POST, etc.), not a default export.\n" : "";
2474
+ if (fileHasAnyExport(filePath, HTTP_METHODS).size === 0 && !fileHasStarExport(filePath)) {
2475
+ const hint = fileHasDefaultExport(filePath) ? " It looks like you have a default export. route.ts uses named exports\n for HTTP methods (GET, POST, etc.), not a default export.\n" : "";
2480
2476
  warnings.push({
2481
2477
  id: "ROUTE_NO_METHODS",
2482
2478
  summary: `route.ts has no HTTP method exports: ${filePath}`,
@@ -2511,23 +2507,14 @@ function checkRootLayout(root, warnings) {
2511
2507
  });
2512
2508
  }
2513
2509
  /**
2514
- * Pattern to detect a default export in source code.
2515
- * Matches: export default function, export default class, export default
2516
- * Also matches: export { X as default }, export { default } from './Impl'
2517
- *
2518
- * The third alternative catches bare `default` re-exports (TIM-790).
2519
- * The negative lookahead (?!\s+as\b) prevents matching `export { default as X }`
2520
- * which is a named export, not a default export (TIM-806).
2521
- */
2522
- var DEFAULT_EXPORT_PATTERN = /export\s+default\b|export\s*\{[^}]*\bas\s+default\b|export\s*\{[^}]*\bdefault\b(?!\s+as\b)[^}]*\}/;
2523
- /**
2524
2510
  * Warn when page.tsx or layout.tsx files don't have a default export.
2525
2511
  *
2526
2512
  * Without a default export:
2527
2513
  * - page.tsx: the page renders nothing (empty content)
2528
2514
  * - layout.tsx: the layout module loads but has no component to render
2529
2515
  *
2530
- * Uses static analysis (regex on source text) — intentionally conservative.
2516
+ * Uses AST-based detection via parseAst for precise matching. Falls back
2517
+ * silently when the file can't be parsed (no spurious warning on syntax errors).
2531
2518
  */
2532
2519
  function checkDefaultExports(node, warnings) {
2533
2520
  if (node.page && isScriptExtension(node.page.extension)) checkFileDefaultExport(node.page.filePath, "page", warnings);
@@ -2540,8 +2527,7 @@ function isScriptExtension(ext) {
2540
2527
  }
2541
2528
  function checkFileDefaultExport(filePath, fileType, warnings) {
2542
2529
  try {
2543
- const source = readFileCached(filePath);
2544
- if (!DEFAULT_EXPORT_PATTERN.test(source)) warnings.push({
2530
+ if (!fileHasDefaultExport(filePath)) warnings.push({
2545
2531
  id: `NO_DEFAULT_EXPORT:${filePath}`,
2546
2532
  summary: `${fileType}.tsx has no default export: ${filePath}`,
2547
2533
  details: ` File: ${filePath}\n\n ${fileType}.tsx files must export a default React component.\n Without a default export, the ${fileType === "page" ? "page renders nothing" : "layout has no component to wrap children"}.\n\n To fix: Add a default export:\n\n export default function My${fileType === "page" ? "Page" : "Layout"}(${fileType === "layout" ? "{ children }" : ""}) {\n return ${fileType === "layout" ? "<div>{children}</div>" : "<h1>Hello</h1>"};\n }\n`,
@@ -2552,6 +2538,39 @@ function checkFileDefaultExport(filePath, fileType, warnings) {
2552
2538
  }
2553
2539
  }
2554
2540
  /**
2541
+ * Warn when error.tsx or global-error.tsx is missing 'use client'.
2542
+ *
2543
+ * React's error boundary API requires client components. Without the directive,
2544
+ * the error boundary silently fails — the component renders as a server component
2545
+ * with no error catching. Users discover this only via cryptic runtime errors.
2546
+ */
2547
+ function checkErrorDirectives(node, warnings) {
2548
+ if (node.error && isScriptExtension(node.error.extension)) checkFileUseClientDirective(node.error.filePath, "error", warnings);
2549
+ for (const child of node.children) checkErrorDirectives(child, warnings);
2550
+ for (const slot of Object.values(node.slots)) checkErrorDirectives(slot, warnings);
2551
+ }
2552
+ function checkFileUseClientDirective(filePath, fileType, warnings) {
2553
+ try {
2554
+ if (!fileHasDirective(filePath, "use client")) warnings.push({
2555
+ id: `ERROR_NO_USE_CLIENT:${filePath}`,
2556
+ summary: `${fileType}.tsx is missing 'use client': ${filePath}`,
2557
+ details: ` File: ${filePath}\n\n ${fileType}.tsx must start with 'use client' because React error\n boundaries require client components. Without it, the error boundary
2558
+ silently fails and errors are uncaught.
2559
+
2560
+ To fix: Add 'use client' as the first line of the file:
2561
+
2562
+ 'use client';
2563
+
2564
+ export default function ${fileType === "global-error" ? "GlobalError" : "ErrorBoundary"}({ error, reset }) {\n return <div><h2>Something went wrong</h2><button onClick={reset}>Try again</button></div>;
2565
+ }
2566
+ `,
2567
+ level: "warn"
2568
+ });
2569
+ } catch (err) {
2570
+ swallow(err, `convention-lint: unreadable ${fileType} file ${filePath}`);
2571
+ }
2572
+ }
2573
+ /**
2555
2574
  * Check if the app/ directory exists. Called before scanning.
2556
2575
  * Returns a warning if missing, or null if the directory exists.
2557
2576
  */
@@ -2571,11 +2590,8 @@ function checkAppDirExists(appDir) {
2571
2590
  level: "error"
2572
2591
  };
2573
2592
  }
2574
- var YELLOW = "\x1B[33m";
2575
- var RED = "\x1B[31m";
2576
- var BOLD = "\x1B[1m";
2577
- var DIM = "\x1B[2m";
2578
- var RESET = "\x1B[0m";
2593
+ var noValidate = { validateStream: false };
2594
+ var style = (fmt, text) => styleText(fmt, text, noValidate);
2579
2595
  /**
2580
2596
  * Format warnings for terminal output.
2581
2597
  *
@@ -2587,20 +2603,20 @@ function formatConventionWarnings(warnings) {
2587
2603
  const warns = warnings.filter((w) => w.level === "warn");
2588
2604
  const lines = [];
2589
2605
  if (errors.length > 0) {
2590
- lines.push(`${RED}${BOLD}[timber]${RESET} ${RED}${errors.length} configuration error${errors.length !== 1 ? "s" : ""}:${RESET}`);
2606
+ lines.push(`${style(["red", "bold"], "[timber]")} ${style("red", `${errors.length} configuration error${errors.length !== 1 ? "s" : ""}:`)}`);
2591
2607
  for (const e of errors) {
2592
2608
  lines.push("");
2593
- lines.push(` ${RED}✗${RESET} ${e.summary}`);
2594
- lines.push(`${DIM}${e.details}${RESET}`);
2609
+ lines.push(` ${style("red", "✗")} ${e.summary}`);
2610
+ lines.push(style("dim", e.details));
2595
2611
  }
2596
2612
  }
2597
2613
  if (warns.length > 0) {
2598
2614
  if (errors.length > 0) lines.push("");
2599
- lines.push(`${YELLOW}${BOLD}[timber]${RESET} ${YELLOW}${warns.length} configuration warning${warns.length !== 1 ? "s" : ""}:${RESET}`);
2615
+ lines.push(`${style(["yellow", "bold"], "[timber]")} ${style("yellow", `${warns.length} configuration warning${warns.length !== 1 ? "s" : ""}:`)}`);
2600
2616
  for (const w of warns) {
2601
2617
  lines.push("");
2602
- lines.push(` ${YELLOW}⚠${RESET} ${w.summary}`);
2603
- lines.push(`${DIM}${w.details}${RESET}`);
2618
+ lines.push(` ${style("yellow", "⚠")} ${w.summary}`);
2619
+ lines.push(style("dim", w.details));
2604
2620
  }
2605
2621
  }
2606
2622
  return lines.join("\n");
@@ -2894,6 +2910,13 @@ var SUBPATH_SRC_MAP = {
2894
2910
  "params": "params/index.ts",
2895
2911
  "search-params": "search-params/index.ts",
2896
2912
  "segment-params": "segment-params/index.ts",
2913
+ "cache/stores/memory": "cache/stores/memory.ts",
2914
+ "cache/stores/redis": "cache/stores/redis.ts",
2915
+ "cache/stores/cloudflare-kv": "cache/stores/cloudflare-kv.ts",
2916
+ "cache/stores/vercel": "cache/stores/vercel.ts",
2917
+ "cdn": "cdn/index.ts",
2918
+ "cdn/cloudflare": "cdn/cloudflare-purge.ts",
2919
+ "cdn/fastly": "cdn/fastly-purge.ts",
2897
2920
  "adapters/cloudflare": "adapters/cloudflare.ts",
2898
2921
  "adapters/cloudflare/cache": "adapters/cloudflare-kv-cache.ts",
2899
2922
  "adapters/cloudflare/dev": "adapters/cloudflare-dev.ts",
@@ -5666,30 +5689,6 @@ function buildFontResultLiteral(extracted) {
5666
5689
  return extracted.variable ? `{ className: "${extracted.className}", style: { fontFamily: "${extracted.fontFamily}" }, variable: "${extracted.variable}" }` : `{ className: "${extracted.className}", style: { fontFamily: "${extracted.fontFamily}" } }`;
5667
5690
  }
5668
5691
  /**
5669
- * File extensions that may contain real font imports. Only JS/TS module
5670
- * variants are scanned. Excluding non-JS extensions (.mdx, .md, .css,
5671
- * .json, etc.) prevents false-positive matches on text that mentions a
5672
- * font specifier inside documentation, code fences, or asset metadata.
5673
- *
5674
- * See TIM-840.
5675
- */
5676
- var FONT_IMPORT_SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
5677
- ".js",
5678
- ".jsx",
5679
- ".ts",
5680
- ".tsx",
5681
- ".mjs",
5682
- ".cjs",
5683
- ".mts",
5684
- ".cts"
5685
- ]);
5686
- function hasScannableExtension(id) {
5687
- const path = id.split("?", 1)[0];
5688
- const dot = path.lastIndexOf(".");
5689
- if (dot === -1) return false;
5690
- return FONT_IMPORT_SCANNABLE_EXTENSIONS.has(path.slice(dot).toLowerCase());
5691
- }
5692
- /**
5693
5692
  * Run the timber-fonts transform pass on a single source file.
5694
5693
  *
5695
5694
  * Parses the source once with OXC (via vite.parseAst), walks the AST to find font imports
@@ -5698,13 +5697,12 @@ function hasScannableExtension(id) {
5698
5697
  * source map alongside the transformed code.
5699
5698
  *
5700
5699
  * Returns `null` if the file does not import from any font virtual module.
5700
+ *
5701
+ * Pre-filtering (extension allow-list, node_modules exclusion, and code
5702
+ * substring checks) is handled by Vite 8's rolldown-native `transform.filter`
5703
+ * in the plugin hook definition (plugins/fonts.ts). See TIM-840, TIM-1147.
5701
5704
  */
5702
5705
  function runFontsTransform(code, id, pipeline, emitError) {
5703
- if (id.startsWith("\0") || id.includes("node_modules")) return null;
5704
- if (!hasScannableExtension(id)) return null;
5705
- const hasGoogleImport = code.includes("@timber/fonts/google") || code.includes("@timber-js/app/fonts/google") || code.includes("next/font/google");
5706
- const hasLocalImport = code.includes("@timber/fonts/local") || code.includes("@timber-js/app/fonts/local") || code.includes("next/font/local");
5707
- if (!hasGoogleImport && !hasLocalImport) return null;
5708
5706
  const analysis = findFontCallSites(code);
5709
5707
  if (!analysis) return null;
5710
5708
  if (analysis.dynamicCall) emitError(`Font function calls must be statically analyzable. Found dynamic call: ${analysis.dynamicCall}. Pass a literal object with string/array values instead.`);
@@ -6052,9 +6050,27 @@ function timberFonts(ctx) {
6052
6050
  *
6053
6051
  * Delegates to `runFontsTransform`. Errors raised through the
6054
6052
  * `this.error` callback are surfaced as Vite plugin errors.
6053
+ *
6054
+ * The `filter` option uses Vite 8's rolldown-native hook filters to
6055
+ * skip non-JS/TS files and files that don't mention any font module
6056
+ * before crossing into JS — replacing the hand-rolled pre-filter that
6057
+ * was previously inside `runFontsTransform` (TIM-840, TIM-1147).
6055
6058
  */
6056
- transform(code, id) {
6057
- return runFontsTransform(code, id, pipeline, (msg) => this.error(msg));
6059
+ transform: {
6060
+ filter: {
6061
+ id: {
6062
+ include: /^[^?]*\.(?:js|jsx|ts|tsx|mjs|cjs|mts|cts)(?:\?|$)/,
6063
+ exclude: /(?:^\0|(?:^|\/)node_modules\/)/
6064
+ },
6065
+ code: [
6066
+ "@timber/fonts/",
6067
+ "@timber-js/app/fonts/",
6068
+ "next/font/"
6069
+ ]
6070
+ },
6071
+ handler(code, id) {
6072
+ return runFontsTransform(code, id, pipeline, (msg) => this.error(msg));
6073
+ }
6058
6074
  },
6059
6075
  /**
6060
6076
  * Emit font binaries and build-manifest entries.
@@ -6136,7 +6152,6 @@ function timberStaticBuild(ctx) {
6136
6152
  *
6137
6153
  * In static mode, we check every app/ file for:
6138
6154
  * - Dynamic API usage (getCookies(), getHeaders()) → build error
6139
- * - When client JS disabled: 'use client' / 'use server' directives → build error
6140
6155
  */
6141
6156
  transform(code, id) {
6142
6157
  if (!(ctx.config.output === "static")) return null;
@@ -6532,180 +6547,32 @@ function timberDevLogs(_ctx) {
6532
6547
  }
6533
6548
  //#endregion
6534
6549
  //#region src/dev-tools/browser-logs.ts
6535
- /** Max message size in bytes before truncation. */
6536
- var MAX_MESSAGE_BYTES = 2048;
6537
- /** HMR event name for browser→server log forwarding. */
6538
- var HMR_EVENT = "timber:browser-log";
6539
- /** ANSI color codes for terminal output. */
6540
- var COLORS = {
6541
- red: "\x1B[31m",
6542
- yellow: "\x1B[33m",
6543
- blue: "\x1B[34m",
6544
- dim: "\x1B[2m",
6545
- reset: "\x1B[0m"
6546
- };
6547
- /** Level severity ordering for threshold comparison. */
6548
- var LEVEL_SEVERITY = {
6549
- none: 0,
6550
- info: 1,
6551
- warn: 2,
6552
- error: 3
6553
- };
6554
- /**
6555
- * Format a browser log payload for server terminal output.
6556
- *
6557
- * Produces color-coded output like:
6558
- * [browser:error] Uncaught TypeError: x is not a function
6559
- * at App (app.tsx:10:5)
6560
- * [browser:warn] Deprecation warning (app/page.tsx:42:12)
6561
- */
6562
- function formatBrowserLog(payload) {
6563
- const { level, message, stack, source } = payload;
6564
- let output = `${`${level === "error" ? COLORS.red : level === "warn" ? COLORS.yellow : COLORS.blue}[browser:${level}]${COLORS.reset}`} ${message}${source ? ` ${COLORS.dim}(${source})${COLORS.reset}` : ""}`;
6565
- if (stack) {
6566
- const indented = stack.split("\n").map((line) => ` ${COLORS.dim}${line}${COLORS.reset}`).join("\n");
6567
- output += `\n${indented}`;
6568
- }
6569
- return output;
6570
- }
6571
- /**
6572
- * Check if a log at `level` should be forwarded given the configured threshold.
6573
- *
6574
- * The threshold acts as a minimum severity:
6575
- * - 'error' → only errors
6576
- * - 'warn' → errors + warnings
6577
- * - 'info' → errors + warnings + info
6578
- * - 'none' → nothing
6579
- */
6580
- function shouldForwardLevel(level, threshold) {
6581
- if (threshold === "none") return false;
6582
- return LEVEL_SEVERITY[level] >= LEVEL_SEVERITY[threshold];
6583
- }
6584
6550
  /**
6585
- * Truncate a message to `maxBytes` to avoid flooding the terminal.
6586
- * Appends a suffix indicating how many bytes were dropped.
6587
- */
6588
- function truncateMessage(message, maxBytes) {
6589
- if (message.length <= maxBytes) return message;
6590
- return `${message.slice(0, maxBytes)}… [truncated ${message.length - maxBytes} bytes]`;
6591
- }
6592
- /**
6593
- * Generate the inline script injected into the browser in dev mode.
6594
- *
6595
- * This script:
6596
- * 1. Saves references to the original console methods
6597
- * 2. Wraps `console.error`, `console.warn`, `console.info`
6598
- * 3. Calls the original first (browser devtools still work)
6599
- * 4. Serializes and forwards via `import.meta.hot.send()`
6600
- * 5. Truncates messages to MAX_MESSAGE_BYTES
6601
- *
6602
- * The script is minimal and self-contained — no imports, no dependencies.
6603
- */
6604
- function generateClientScript(threshold) {
6605
- if (threshold === "none") return "";
6606
- const levels = [
6607
- "error",
6608
- "warn",
6609
- "info"
6610
- ].filter((l) => shouldForwardLevel(l, threshold));
6611
- if (levels.length === 0) return "";
6612
- return `
6613
- (function() {
6614
- if (!import.meta.hot) return;
6615
- var MAX_BYTES = ${MAX_MESSAGE_BYTES};
6616
- var levels = ${JSON.stringify(levels)};
6617
- var originals = {};
6618
-
6619
- function serialize(arg) {
6620
- if (arg === null) return 'null';
6621
- if (arg === undefined) return 'undefined';
6622
- if (arg instanceof Error) return arg.stack || arg.message || String(arg);
6623
- if (typeof arg === 'object') {
6624
- try { return JSON.stringify(arg); } catch(e) { return String(arg); }
6625
- }
6626
- return String(arg);
6627
- }
6628
-
6629
- function truncate(s) {
6630
- if (s.length <= MAX_BYTES) return s;
6631
- return s.slice(0, MAX_BYTES) + '... [truncated ' + (s.length - MAX_BYTES) + ' bytes]';
6632
- }
6633
-
6634
- levels.forEach(function(level) {
6635
- originals[level] = console[level];
6636
- console[level] = function() {
6637
- originals[level].apply(console, arguments);
6638
- try {
6639
- var args = Array.prototype.slice.call(arguments);
6640
- var firstArg = args[0];
6641
- var stack = null;
6642
- var source = null;
6643
-
6644
- if (firstArg instanceof Error) {
6645
- stack = firstArg.stack || null;
6646
- source = null;
6647
- }
6648
-
6649
- var message = args.map(serialize).join(' ');
6650
- message = truncate(message);
6651
-
6652
- import.meta.hot.send('${HMR_EVENT}', {
6653
- level: level,
6654
- message: message,
6655
- stack: stack,
6656
- source: source,
6657
- timestamp: Date.now()
6658
- });
6659
- } catch(e) {
6660
- // Never let log forwarding break the page
6661
- }
6662
- };
6663
- });
6664
- })();
6665
- `.trim();
6666
- }
6667
- /**
6668
- * Create the timber-dev-browser-logs Vite plugin.
6669
- *
6670
- * - `configureServer`: Listens for HMR messages and prints them to the terminal
6671
- * - `transformIndexHtml`: Injects the client-side interception script
6672
- *
6673
- * Only active during `vite dev` (apply: 'serve').
6674
- */
6675
- function timberDevBrowserLogs(ctx) {
6551
+ * Map timber's devBrowserLogs threshold to Vite 8's server.forwardConsole.
6552
+ *
6553
+ * Vite defaults forwardConsole OFF for non-agent dev; timber enables it
6554
+ * based on the configured threshold (default: 'warn'). The threshold acts
6555
+ * as a minimum severity — 'warn' forwards errors + warnings, 'info'
6556
+ * forwards everything, 'error' forwards only errors, 'none' disables.
6557
+ *
6558
+ * Vite's unhandledErrors is explicitly disabled — timber's own
6559
+ * setupClientErrorForwarding (browser-dev.ts) already catches window
6560
+ * error/unhandledrejection, filters framework control-flow signals
6561
+ * (redirect/deny digests) and page-unload aborts (TIM-838), and
6562
+ * echoes errors to the Vite overlay. Enabling Vite's unhandledErrors
6563
+ * would duplicate terminal output for normal errors and reintroduce
6564
+ * unfiltered noise for framework signals.
6565
+ */
6566
+ function resolveForwardConsole(threshold) {
6567
+ const resolved = threshold ?? "warn";
6568
+ if (resolved === "none") return false;
6569
+ const logLevels = [];
6570
+ if (resolved === "info") logLevels.push("info", "warn", "error");
6571
+ else if (resolved === "warn") logLevels.push("warn", "error");
6572
+ else logLevels.push("error");
6676
6573
  return {
6677
- name: "timber-dev-browser-logs",
6678
- apply: "serve",
6679
- configureServer(server) {
6680
- const threshold = ctx.config.devBrowserLogs ?? "warn";
6681
- if (threshold === "none") {
6682
- delete globalThis.__timber_dev_browser_log_script;
6683
- return;
6684
- }
6685
- const script = generateClientScript(threshold);
6686
- if (script) globalThis.__timber_dev_browser_log_script = `<script type="module">${script}<\/script>`;
6687
- server.hot.on(HMR_EVENT, (payload) => {
6688
- try {
6689
- if (!shouldForwardLevel(payload.level, threshold)) return;
6690
- payload.message = truncateMessage(payload.message, MAX_MESSAGE_BYTES);
6691
- const formatted = formatBrowserLog(payload);
6692
- if (payload.level === "error") process.stderr.write(formatted + "\n");
6693
- else process.stdout.write(formatted + "\n");
6694
- } catch (e) {
6695
- process.stderr.write(`[timber] log forwarding failed: ${e instanceof Error ? e.message : e}\n`);
6696
- }
6697
- });
6698
- },
6699
- transformIndexHtml() {
6700
- const script = generateClientScript(ctx.config.devBrowserLogs ?? "warn");
6701
- if (!script) return [];
6702
- return [{
6703
- tag: "script",
6704
- attrs: { type: "module" },
6705
- children: script,
6706
- injectTo: "head"
6707
- }];
6708
- }
6574
+ unhandledErrors: false,
6575
+ logLevels
6709
6576
  };
6710
6577
  }
6711
6578
  //#endregion
@@ -6939,8 +6806,16 @@ function clientChunkGroup(meta, appDir, root, sharedModuleFacades) {
6939
6806
  *
6940
6807
  * Design docs: design/11-platform.md, design/25-production-deployments.md
6941
6808
  */
6809
+ var NODE_BUILTINS = builtinModules.flatMap((m) => [m, `node:${m}`]);
6810
+ var CLOUDFLARE_BUILTINS = [
6811
+ "cloudflare:workers",
6812
+ "cloudflare:sockets",
6813
+ "cloudflare:email",
6814
+ "cloudflare:workflows"
6815
+ ];
6942
6816
  var BUILD_ONLY_EXTERNALS = [
6943
- ...builtinModules.flatMap((m) => [m, `node:${m}`]),
6817
+ ...NODE_BUILTINS,
6818
+ ...CLOUDFLARE_BUILTINS,
6944
6819
  "vite",
6945
6820
  "nitro",
6946
6821
  "rollup",
@@ -6970,6 +6845,7 @@ function timberServerBundle() {
6970
6845
  noExternal: true,
6971
6846
  external: BUILD_ONLY_EXTERNALS
6972
6847
  },
6848
+ build: { rolldownOptions: { external: [/^cloudflare:/] } },
6973
6849
  define: serverDefine
6974
6850
  },
6975
6851
  ssr: {
@@ -6977,6 +6853,7 @@ function timberServerBundle() {
6977
6853
  noExternal: true,
6978
6854
  external: BUILD_ONLY_EXTERNALS
6979
6855
  },
6856
+ build: { rolldownOptions: { external: [/^cloudflare:/] } },
6980
6857
  define: serverDefine
6981
6858
  }
6982
6859
  }
@@ -7026,6 +6903,96 @@ function timberServerBundle() {
7026
6903
  ];
7027
6904
  }
7028
6905
  //#endregion
6906
+ //#region src/plugins/callsite-ast.ts
6907
+ /**
6908
+ * Shared AST helpers for callsite-ID transforms.
6909
+ *
6910
+ * Both timber-cache-transform (plugins/cache.ts) and timber-prebuilt
6911
+ * (plugins/prebuilt.ts) detect calls on bindings imported from the cache
6912
+ * module and stamp them with content-derived stable IDs. The node shapes,
6913
+ * import-binding resolution, recursive call walk, and ID derivation are
6914
+ * identical — only the callee shape and rewrite differ per plugin.
6915
+ *
6916
+ * See TIM-997, TIM-1053, design/06-caching.md §"Cache Key Security".
6917
+ */
6918
+ /** Module specifiers whose bindings the transforms track. */
6919
+ var CACHE_MODULE_SOURCES = /* @__PURE__ */ new Set(["@timber-js/app/cache", "timber/cache"]);
6920
+ /**
6921
+ * Find the local binding name(s) for `importedName` imported from one of
6922
+ * `sources` — both named imports (including aliases) and namespace imports
6923
+ * (TIM-1054).
6924
+ */
6925
+ function findImportBindings(program, sources, importedName) {
6926
+ const named = /* @__PURE__ */ new Set();
6927
+ const namespaces = /* @__PURE__ */ new Set();
6928
+ for (const stmt of program.body) {
6929
+ if (stmt.type !== "ImportDeclaration") continue;
6930
+ const decl = stmt;
6931
+ if (!sources.has(decl.source.value)) continue;
6932
+ for (const spec of decl.specifiers) {
6933
+ if (spec.type === "ImportNamespaceSpecifier") {
6934
+ namespaces.add(spec.local.name);
6935
+ continue;
6936
+ }
6937
+ if (spec.type !== "ImportSpecifier") continue;
6938
+ if ((spec.imported?.name ?? spec.local.name) === importedName) named.add(spec.local.name);
6939
+ }
6940
+ }
6941
+ return {
6942
+ named,
6943
+ namespaces
6944
+ };
6945
+ }
6946
+ /**
6947
+ * Recursively walk AST nodes collecting call expressions accepted by
6948
+ * `matches`. Calls are typically at module scope, but calls inside
6949
+ * functions/blocks are handled for completeness.
6950
+ *
6951
+ * NOTE: matching is by binding NAME, so nested scopes that shadow a tracked
6952
+ * import can produce false positives for consumers of this walker (see
6953
+ * TIM-1141 for the cache transform). The prebuilt transform avoids the
6954
+ * class entirely by not using this walker — it allowlists top-level
6955
+ * declarator positions instead (plugins/prebuilt.ts).
6956
+ */
6957
+ function findCallSites(node, matches, results) {
6958
+ if (node.type === "CallExpression") {
6959
+ const call = node;
6960
+ if (matches(call)) results.push(call);
6961
+ }
6962
+ for (const key of Object.keys(node)) {
6963
+ if (key === "type" || key === "start" || key === "end") continue;
6964
+ const val = node[key];
6965
+ if (val && typeof val === "object") {
6966
+ if (Array.isArray(val)) {
6967
+ for (const item of val) if (item && typeof item === "object" && typeof item.type === "string") findCallSites(item, matches, results);
6968
+ } else if (typeof val.type === "string") findCallSites(val, matches, results);
6969
+ }
6970
+ }
6971
+ }
6972
+ /**
6973
+ * Generate a stable callsite ID from a module path, a kind discriminator
6974
+ * ('cache' | 'component'), the source text of the enclosing top-level
6975
+ * statement, the source text of the call expression, and an occurrence index
6976
+ * among fully byte-identical (statement, call) pairs in the same file.
6977
+ *
6978
+ * The ID is deterministic and:
6979
+ * - Stable across cold starts AND across builds/deploys for unchanged calls —
6980
+ * inserting, deleting, or reordering other calls does not affect it
6981
+ * - Unique across different modules (relPath component)
6982
+ * - Unique across distinct calls within a module (statement + span hashes),
6983
+ * with fully identical duplicates disambiguated by source-order occurrence
6984
+ * - Fresh when the wrapped fn, opts, or enclosing statement change, which
6985
+ * intentionally invalidates the old persisted entries
6986
+ *
6987
+ * The enclosing-statement text is included so that byte-identical calls in
6988
+ * different scopes — e.g. two factory helpers with identical call spans —
6989
+ * get distinct, insertion-stable IDs. See plugins/cache.ts header for the
6990
+ * full derivation rationale (TIM-1053).
6991
+ */
6992
+ function generateStableCallsiteId(relativeModulePath, kind, statementText, callSpanText, occurrence) {
6993
+ return fnv1aHash(`${relativeModulePath}:${kind}:${fnv1aHash(statementText)}:${fnv1aHash(callSpanText)}:${occurrence}`);
6994
+ }
6995
+ //#endregion
7029
6996
  //#region src/plugins/cache.ts
7030
6997
  /**
7031
6998
  * timber-cache-transform — Vite plugin that injects stable callsite IDs
@@ -7076,34 +7043,11 @@ function timberServerBundle() {
7076
7043
  * referencing the same bindings — the same function, where an ID swap is
7077
7044
  * harmless.
7078
7045
  *
7046
+ * AST helpers and the ID derivation are shared with timber-prebuilt
7047
+ * (plugins/prebuilt.ts) via plugins/callsite-ast.ts.
7048
+ *
7079
7049
  * See TIM-997, TIM-1053, design/06-caching.md §"Cache Key Security".
7080
7050
  */
7081
- var CACHE_MODULE_SOURCES = /* @__PURE__ */ new Set(["@timber-js/app/cache", "timber/cache"]);
7082
- /**
7083
- * Find the local binding name(s) for `cache` imported from the cache module —
7084
- * both named imports and namespace imports (TIM-1054).
7085
- */
7086
- function findCacheBindings(program) {
7087
- const named = /* @__PURE__ */ new Set();
7088
- const namespaces = /* @__PURE__ */ new Set();
7089
- for (const stmt of program.body) {
7090
- if (stmt.type !== "ImportDeclaration") continue;
7091
- const decl = stmt;
7092
- if (!CACHE_MODULE_SOURCES.has(decl.source.value)) continue;
7093
- for (const spec of decl.specifiers) {
7094
- if (spec.type === "ImportNamespaceSpecifier") {
7095
- namespaces.add(spec.local.name);
7096
- continue;
7097
- }
7098
- if (spec.type !== "ImportSpecifier") continue;
7099
- if ((spec.imported?.name ?? spec.local.name) === "cache") named.add(spec.local.name);
7100
- }
7101
- }
7102
- return {
7103
- named,
7104
- namespaces
7105
- };
7106
- }
7107
7051
  /**
7108
7052
  * Is this callee a reference to the imported `cache` function — either a bare
7109
7053
  * identifier bound by a named import, or `ns.cache` on a namespace import?
@@ -7119,44 +7063,11 @@ function isCacheCallee(callee, bindings) {
7119
7063
  return false;
7120
7064
  }
7121
7065
  /**
7122
- * Recursively walk AST nodes to find cache() call expressions.
7123
- *
7124
- * cache() is typically called at module scope, but we also handle calls
7125
- * inside functions/blocks for completeness.
7126
- */
7127
- function findCacheCallSites(node, cacheBindings, results) {
7128
- if (node.type === "CallExpression") {
7129
- const call = node;
7130
- if (call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings)) results.push(call);
7131
- }
7132
- for (const key of Object.keys(node)) {
7133
- if (key === "type" || key === "start" || key === "end") continue;
7134
- const val = node[key];
7135
- if (val && typeof val === "object") {
7136
- if (Array.isArray(val)) {
7137
- for (const item of val) if (item && typeof item === "object" && typeof item.type === "string") findCacheCallSites(item, cacheBindings, results);
7138
- } else if (typeof val.type === "string") findCacheCallSites(val, cacheBindings, results);
7139
- }
7140
- }
7141
- }
7142
- /**
7143
- * Generate a stable callsite ID from a module path, the source text of the
7144
- * enclosing top-level statement, the source text of the cache(...) call
7145
- * expression, and an occurrence index among fully byte-identical
7146
- * (statement, call) pairs in the same file.
7147
- *
7148
- * The ID is deterministic and:
7149
- * - Stable across cold starts AND across builds/deploys for unchanged calls —
7150
- * inserting, deleting, or reordering other cache() calls does not affect it
7151
- * - Unique across different modules (relPath component)
7152
- * - Unique across distinct cache() calls within a module (statement + span
7153
- * hashes), with fully identical duplicates disambiguated by source-order
7154
- * occurrence
7155
- * - Fresh when the wrapped fn, opts, or enclosing statement change, which
7156
- * intentionally invalidates the old persisted entries
7066
+ * Generate a stable callsite ID for a cache() call. See callsite-ast.ts for
7067
+ * the derivation properties.
7157
7068
  */
7158
7069
  function generateStableId(relativeModulePath, statementText, callSpanText, occurrence) {
7159
- return fnv1aHash(`${relativeModulePath}:cache:${fnv1aHash(statementText)}:${fnv1aHash(callSpanText)}:${occurrence}`);
7070
+ return generateStableCallsiteId(relativeModulePath, "cache", statementText, callSpanText, occurrence);
7160
7071
  }
7161
7072
  function timberCacheTransform(ctx) {
7162
7073
  return {
@@ -7170,12 +7081,12 @@ function timberCacheTransform(ctx) {
7170
7081
  } catch {
7171
7082
  return;
7172
7083
  }
7173
- const cacheBindings = findCacheBindings(program);
7084
+ const cacheBindings = findImportBindings(program, CACHE_MODULE_SOURCES, "cache");
7174
7085
  if (cacheBindings.named.size === 0 && cacheBindings.namespaces.size === 0) return;
7175
7086
  const callSites = [];
7176
7087
  for (const stmt of program.body) {
7177
7088
  const calls = [];
7178
- findCacheCallSites(stmt, cacheBindings, calls);
7089
+ findCallSites(stmt, (call) => call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings), calls);
7179
7090
  for (const call of calls) callSites.push({
7180
7091
  call,
7181
7092
  stmt
@@ -7204,6 +7115,915 @@ function timberCacheTransform(ctx) {
7204
7115
  };
7205
7116
  }
7206
7117
  //#endregion
7118
+ //#region src/plugins/callsite-jsx.ts
7119
+ /**
7120
+ * Try to extract a static JSON-safe value from an AST expression node.
7121
+ * Returns `undefined` (not null — null is a valid JSON value) when the
7122
+ * expression is dynamic/computed and cannot be captured statically.
7123
+ */
7124
+ function tryExtractStaticValue(node) {
7125
+ if (node.type === "Literal") {
7126
+ const val = node.value;
7127
+ if (val === null || typeof val === "string" || typeof val === "number" || typeof val === "boolean") return val;
7128
+ return;
7129
+ }
7130
+ if (node.type === "UnaryExpression") {
7131
+ const unary = node;
7132
+ if (unary.operator === "-" && unary.argument.type === "Literal") {
7133
+ const val = unary.argument.value;
7134
+ if (typeof val === "number") return -val;
7135
+ }
7136
+ return;
7137
+ }
7138
+ if (node.type === "TemplateLiteral") {
7139
+ const tpl = node;
7140
+ if (tpl.expressions.length === 0) return tpl.quasis[0]?.value.cooked ?? void 0;
7141
+ return;
7142
+ }
7143
+ if (node.type === "ArrayExpression") {
7144
+ const elements = node.elements;
7145
+ const arr = [];
7146
+ for (const el of elements) {
7147
+ if (el === null) return void 0;
7148
+ const val = tryExtractStaticValue(el);
7149
+ if (val === void 0) return void 0;
7150
+ arr.push(val);
7151
+ }
7152
+ return arr;
7153
+ }
7154
+ if (node.type === "ObjectExpression") {
7155
+ const properties = node.properties;
7156
+ const obj = {};
7157
+ for (const prop of properties) {
7158
+ if (prop.type === "SpreadElement") return void 0;
7159
+ if (prop.type !== "Property") return void 0;
7160
+ const p = prop;
7161
+ if (p.computed) return void 0;
7162
+ const keyName = p.key.type === "Identifier" ? p.key.name : p.key.type === "Literal" ? String(p.key.value) : void 0;
7163
+ if (keyName === void 0) return void 0;
7164
+ const val = tryExtractStaticValue(p.value);
7165
+ if (val === void 0) return void 0;
7166
+ obj[keyName] = val;
7167
+ }
7168
+ return obj;
7169
+ }
7170
+ }
7171
+ /**
7172
+ * Extract static props from a JSX element's attributes. Returns null if
7173
+ * any attribute is dynamic (spread, computed expression, JSX element, etc.)
7174
+ * — the component must fall back to dynamic rendering for that call site.
7175
+ */
7176
+ function extractStaticJsxProps(attributes) {
7177
+ const props = {};
7178
+ for (const attr of attributes) {
7179
+ if (attr.type === "JSXSpreadAttribute") return null;
7180
+ if (attr.type !== "JSXAttribute") return null;
7181
+ const jsxAttr = attr;
7182
+ const name = jsxAttr.name?.name;
7183
+ if (!name) return null;
7184
+ if (name === "key" || name === "ref") continue;
7185
+ if (jsxAttr.value === null) {
7186
+ props[name] = true;
7187
+ continue;
7188
+ }
7189
+ if (jsxAttr.value.type === "Literal") {
7190
+ const val = tryExtractStaticValue(jsxAttr.value);
7191
+ if (val === void 0) return null;
7192
+ props[name] = val;
7193
+ continue;
7194
+ }
7195
+ if (jsxAttr.value.type === "JSXExpressionContainer") {
7196
+ const container = jsxAttr.value;
7197
+ const val = tryExtractStaticValue(container.expression);
7198
+ if (val === void 0) return null;
7199
+ props[name] = val;
7200
+ continue;
7201
+ }
7202
+ return null;
7203
+ }
7204
+ return props;
7205
+ }
7206
+ /**
7207
+ * Walk an AST tree and collect JSX usages of the given binding names,
7208
+ * returning the static props for each usage. Returns a map of
7209
+ * bindingName → array of static prop objects (or null for dynamic props).
7210
+ */
7211
+ function collectJsxUsages(program, bindingNames) {
7212
+ const result = /* @__PURE__ */ new Map();
7213
+ const walk = (node) => {
7214
+ if (!node || typeof node !== "object") return;
7215
+ const n = node;
7216
+ if (n.type === "JSXElement") {
7217
+ const jsx = n;
7218
+ const tagName = jsx.openingElement.name;
7219
+ if (tagName.type === "JSXIdentifier") {
7220
+ const name = tagName.name;
7221
+ if (bindingNames.has(name)) {
7222
+ const children = jsx.children ?? [];
7223
+ const props = children.length > 0 && children.some((c) => {
7224
+ const child = c;
7225
+ if (child.type === "JSXText") return child.value.trim().length > 0;
7226
+ return true;
7227
+ }) ? null : extractStaticJsxProps(jsx.openingElement.attributes);
7228
+ const usages = result.get(name) ?? [];
7229
+ usages.push(props);
7230
+ result.set(name, usages);
7231
+ }
7232
+ }
7233
+ }
7234
+ for (const value of Object.values(n)) if (Array.isArray(value)) for (const item of value) walk(item);
7235
+ else if (value && typeof value === "object" && "type" in value) walk(value);
7236
+ };
7237
+ for (const stmt of program.body) walk(stmt);
7238
+ return result;
7239
+ }
7240
+ /**
7241
+ * Deep stable JSON stringification with recursively sorted keys.
7242
+ * `JSON.stringify` with an array replacer only includes listed keys at
7243
+ * ALL nesting levels, so `{config:{theme:'dark'}}` and
7244
+ * `{config:{theme:'light'}}` would both serialize as `{"config":{}}`.
7245
+ */
7246
+ function stableStringify(value) {
7247
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
7248
+ if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]";
7249
+ const obj = value;
7250
+ return "{" + Object.keys(obj).sort().map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
7251
+ }
7252
+ /**
7253
+ * Add a distinct props object to the call-site-props map for a component,
7254
+ * deduplicating by deep JSON value (recursively sorted keys).
7255
+ */
7256
+ function recordCallSiteProps(ctx, componentId, props) {
7257
+ ctx.prebuiltCallSiteProps ??= /* @__PURE__ */ new Map();
7258
+ const existing = ctx.prebuiltCallSiteProps.get(componentId) ?? [];
7259
+ const canonical = stableStringify(props);
7260
+ if (!existing.some((p) => stableStringify(p) === canonical)) {
7261
+ existing.push(props);
7262
+ ctx.prebuiltCallSiteProps.set(componentId, existing);
7263
+ }
7264
+ }
7265
+ //#endregion
7266
+ //#region src/plugins/prebuilt-capture.ts
7267
+ /**
7268
+ * Post-build prebuilt capture — Node-side orchestration (TIM-1119).
7269
+ *
7270
+ * Runs from timber-prebuilt's `buildApp` post hook, after Vite's 5-step
7271
+ * build sequence. Imports the BUILT `rsc/index.js` (the bundle carries
7272
+ * the react-server condition resolved at build time, so a plain dynamic
7273
+ * import works) and calls its `__timber_capturePrebuilt` export — the
7274
+ * capture itself must run inside the bundle's module graph, where the
7275
+ * component registry and flight renderer live. See
7276
+ * server/prebuilt-builder.ts and design/44-render-at-build-time.md
7277
+ * §Build Phase.
7278
+ *
7279
+ * This side owns PERSISTENCE: each captured payload is written to
7280
+ * `.timber/dist/prebuilt/<componentId>/<cacheKey>.flight` atomically
7281
+ * (temp file + rename — a crashed build never leaves a torn artifact
7282
+ * under its final name), and `manifest.json` is written last, only after
7283
+ * every payload it references is fully on disk. Keeping node:fs on the
7284
+ * plugin side keeps it out of the server bundle, which must also run on
7285
+ * Cloudflare Workers.
7286
+ *
7287
+ * NOTE: importing the built entry runs the app's production startup —
7288
+ * instrumentation.ts register() and the configured cache handler load
7289
+ * inside the build process. That is by design (prebuilt components may
7290
+ * call timber.cache), and it only happens when the app actually declares
7291
+ * prebuilt components.
7292
+ */
7293
+ /**
7294
+ * Both patterns are framework-generated (plugins/prebuilt.ts assigns IDs,
7295
+ * server/prebuilt/cache-key.ts hashes keys), so a mismatch is a framework
7296
+ * bug or a tampered registry — either way it must never reach a path join.
7297
+ */
7298
+ var COMPONENT_ID_RE = /^[a-z0-9]+_[0-9a-f]{16}$/;
7299
+ var CACHE_KEY_RE = /^[0-9a-f]{16}$/;
7300
+ /** Write a file atomically: temp sibling + rename. */
7301
+ async function writeFileAtomic(path, data) {
7302
+ const tmp = `${path}.tmp`;
7303
+ await writeFile(tmp, data);
7304
+ await rename(tmp, path);
7305
+ }
7306
+ /**
7307
+ * Create the writer for `.timber/dist/prebuilt/`. Storage layout per
7308
+ * design/44 §Storage: one directory per component ID, one `.flight` file
7309
+ * per cache key, one manifest.json mapping componentId + key → file +
7310
+ * metadata.
7311
+ */
7312
+ function createPrebuiltArtifactWriter(prebuiltDir) {
7313
+ const manifest = {};
7314
+ const buildTimestamp = Date.now();
7315
+ let count = 0;
7316
+ return {
7317
+ async write(entry) {
7318
+ if (!COMPONENT_ID_RE.test(entry.componentId) || !CACHE_KEY_RE.test(entry.cacheKey)) throw new Error(`[timber] prebuilt artifact writer received a malformed identity (componentId: ${JSON.stringify(entry.componentId)}, cacheKey: ${JSON.stringify(entry.cacheKey)}) — refusing to write`);
7319
+ const relativeFile = `${entry.componentId}/${entry.cacheKey}.flight`;
7320
+ const path = join(prebuiltDir, relativeFile);
7321
+ await mkdir(dirname(path), { recursive: true });
7322
+ await writeFileAtomic(path, entry.bytes);
7323
+ manifest[entry.componentId] ??= { entries: {} };
7324
+ manifest[entry.componentId].entries[entry.cacheKey] = {
7325
+ params: entry.params,
7326
+ file: relativeFile,
7327
+ bytes: entry.bytes.byteLength,
7328
+ buildTimestamp,
7329
+ ...entry.tags ? { tags: entry.tags } : {}
7330
+ };
7331
+ count++;
7332
+ },
7333
+ async finalize() {
7334
+ if (count === 0) return 0;
7335
+ await writeFileAtomic(join(prebuiltDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
7336
+ return count;
7337
+ }
7338
+ };
7339
+ }
7340
+ /**
7341
+ * Remove any `prebuilt/` output left by a previous build in this
7342
+ * directory (watch mode, `emptyOutDir: false`). A stale manifest would
7343
+ * make adapters ship payloads from an OLD build — reviving them serves
7344
+ * outdated content with that build's chunk references instead of the
7345
+ * intended miss → live render. Runs unconditionally at the start of the
7346
+ * post-build hook, BEFORE the capture decision: a build whose app no
7347
+ * longer declares any prebuilt components must also shed the artifacts
7348
+ * (codex P2 on PR #836).
7349
+ */
7350
+ async function cleanPrebuiltDir(buildDir) {
7351
+ await rm(join(buildDir, "prebuilt"), {
7352
+ recursive: true,
7353
+ force: true
7354
+ });
7355
+ }
7356
+ /**
7357
+ * Load the built RSC entry and run the capture pass. Throws (failing the
7358
+ * build) on infrastructure errors — missing/unloadable entry — and on
7359
+ * dynamic-context hard errors (cookies()/headers() in a prerendered
7360
+ * component). Per-entry data errors skip-and-report per design/44
7361
+ * §Build-Phase Error Handling.
7362
+ */
7363
+ async function runPrebuiltCapture(ctx) {
7364
+ const routeComponents = ctx.routePrebuiltComponents;
7365
+ if (!routeComponents || routeComponents.size === 0) return {
7366
+ captured: 0,
7367
+ skipped: [],
7368
+ hardErrors: [],
7369
+ nestingWarnings: 0
7370
+ };
7371
+ const entryPath = join(ctx.buildDir, "rsc", "index.js");
7372
+ if (!existsSync(entryPath)) throw new Error(`[timber] prebuilt capture: built RSC entry not found at ${entryPath} — the app declares cache.component({ prerender: true }) call sites but the RSC build output is missing`);
7373
+ const g = globalThis;
7374
+ if (ctx.buildManifest) g.__TIMBER_BUILD_MANIFEST__ ??= ctx.buildManifest;
7375
+ if (ctx.deploymentId) g.__TIMBER_DEPLOYMENT_ID__ ??= ctx.deploymentId;
7376
+ console.log("[timber] cache.component: capturing prebuilt flight payloads…");
7377
+ let mod;
7378
+ try {
7379
+ mod = await import(
7380
+ /* @vite-ignore */
7381
+ `${pathToFileURL(entryPath).href}?t=${Date.now()}`
7382
+ );
7383
+ } catch (error) {
7384
+ throw new Error(`[timber] prebuilt capture: failed to load the built RSC entry (${entryPath}). The capture pass runs the production server module inside the build process — its startup must succeed for prerendering to work.`, { cause: error });
7385
+ }
7386
+ const capture = mod.__timber_capturePrebuilt;
7387
+ if (typeof capture !== "function") throw new Error("[timber] prebuilt capture: built RSC entry does not export __timber_capturePrebuilt — the build output predates this framework version; rebuild from a clean .timber/dist");
7388
+ const writer = createPrebuiltArtifactWriter(join(ctx.buildDir, "prebuilt"));
7389
+ const summary = await capture({
7390
+ fileComponentMap: Object.fromEntries(routeComponents),
7391
+ callSiteProps: ctx.prebuiltCallSiteProps ? Object.fromEntries(ctx.prebuiltCallSiteProps) : void 0,
7392
+ onEntry: (entry) => writer.write(entry)
7393
+ });
7394
+ if (summary.hardErrors.length > 0) {
7395
+ const details = summary.hardErrors.map((issue) => ` - ${issue.message}`).join("\n");
7396
+ throw new Error(`[timber] prebuilt capture failed: ${summary.hardErrors.length} component(s) read per-request context at build time:\n${details}`);
7397
+ }
7398
+ await writer.finalize();
7399
+ const total = summary.captured + summary.skipped.length;
7400
+ console.log(`[timber] Prebuilt ${summary.captured}/${total} entries.` + (summary.skipped.length > 0 ? ` ${summary.skipped.length} skipped due to errors.` : ""));
7401
+ return summary;
7402
+ }
7403
+ //#endregion
7404
+ //#region src/plugins/prebuilt.ts
7405
+ /**
7406
+ * timber-prebuilt — Vite plugin that detects `cache.component(...)` call
7407
+ * sites and rewrites them to `__prebuilt(stableId, component, options)`.
7408
+ *
7409
+ * `timber.cache.component(Component, { prerender: true })` is a build-time
7410
+ * transform, not a runtime function (design/44-render-at-build-time.md
7411
+ * §Registration, surface API renamed by design/45-cache-lifetimes.md). The
7412
+ * rewrite registers the component in the server-side prebuilt registry under
7413
+ * a stable ID that keys its cached flight payloads.
7414
+ *
7415
+ * Detected callsite forms (mirrors timber-cache-transform, TIM-1054):
7416
+ * - named imports, including aliases:
7417
+ * `import { cache } from '@timber-js/app/cache'` + `cache.component(fn, opts)`
7418
+ * - namespace imports:
7419
+ * `import * as timber` + `timber.cache.component(fn, opts)`
7420
+ * Computed access (`cache['component']`) is deliberately not matched — the
7421
+ * untransformed call reaches the runtime `cache.component` fallback
7422
+ * (cache/cache-api.ts), which warns once and renders dynamically.
7423
+ *
7424
+ * Matching is an ALLOWLIST of module-scope positions: the direct initializer
7425
+ * of a top-level variable declarator (including exported ones) and a
7426
+ * top-level `export default cache.component(...)`, with TS expression
7427
+ * wrappers (`as`, `satisfies`, `!`) unwrapped. Nothing else is walked — no
7428
+ * function/class/loop bodies, no params, no call arguments. Two reasons:
7429
+ * (1) `__prebuilt` must register exactly once at module load time; the
7430
+ * post-build capture pass walks the registry after loading modules, so a
7431
+ * call inside a function registers too late to ever be prebuilt, and a call
7432
+ * in a loop would re-register per iteration. (2) Name-based callee matching
7433
+ * is only shadow-proof in these positions — nested scopes can rebind `cache`
7434
+ * (`function render(cache) {...}`, `for (const cache of caches) ...`), and
7435
+ * rewriting those would corrupt user code; a top-level declarator initializer
7436
+ * cannot see a shadow of an import binding (redeclaration error). Calls in
7437
+ * other positions fall back to the runtime path like other untraceable forms.
7438
+ *
7439
+ * The rewrite per mode:
7440
+ * production: cache.component(fn, opts) → __prebuilt("<id>", fn, opts)
7441
+ * dev: cache.component(fn, opts) → __prebuiltDev("<name>", fn, opts)
7442
+ *
7443
+ * with `import { __prebuilt } from '#server-internal'` prepended. In dev the
7444
+ * wrapper is a no-op that logs once per component and renders dynamically —
7445
+ * no caching, no markers (design/44 §Dev Mode).
7446
+ *
7447
+ * Component identity: `<name>_<hash16>`, e.g. `blogbody_a1b2c3d4e5f6a7b8`.
7448
+ * The name prefix is the declaring variable's name (lowercased, for legible
7449
+ * manifest keys and log lines); the hash is the same content-derived callsite
7450
+ * scheme as the data cache transform (callsite-ast.ts) — deterministic across
7451
+ * builds for unchanged calls, immune to inserting/reordering other calls, and
7452
+ * fresh when the component or its options change. Component cache entries are
7453
+ * per-deploy (design/45 §Per-deploy namespacing) so cross-deploy ID reuse is
7454
+ * not a poisoning vector, but content-derived IDs keep prebuilt artifacts
7455
+ * addressable across incremental edits for free.
7456
+ *
7457
+ * The transform is skipped in the client environment: server components never
7458
+ * execute there, and `#server-internal` resolves to an empty stub in the
7459
+ * client bundle (plugins/shims.ts).
7460
+ *
7461
+ * See TIM-1118, design/44-render-at-build-time.md, design/45-cache-lifetimes.md.
7462
+ */
7463
+ /**
7464
+ * Is this callee `cache.component` (named import, aliases included) or
7465
+ * `ns.cache.component` (namespace import)? Computed access is not matched.
7466
+ */
7467
+ function isCacheComponentCallee(callee, bindings) {
7468
+ if (callee.type !== "MemberExpression") return false;
7469
+ const member = callee;
7470
+ if (member.computed || member.property.type !== "Identifier" || member.property.name !== "component") return false;
7471
+ if (member.object.type === "Identifier") return bindings.named.has(member.object.name);
7472
+ if (member.object.type === "MemberExpression") {
7473
+ const inner = member.object;
7474
+ return !inner.computed && inner.object.type === "Identifier" && bindings.namespaces.has(inner.object.name) && inner.property.type === "Identifier" && inner.property.name === "cache";
7475
+ }
7476
+ return false;
7477
+ }
7478
+ /**
7479
+ * Offset where an injected import may be inserted: after the directive
7480
+ * prologue ('use server', 'use strict', ...). Prepending at offset 0 would
7481
+ * demote a leading directive to a plain expression statement — silently
7482
+ * disabling it.
7483
+ */
7484
+ function importInsertionOffset(program) {
7485
+ for (const stmt of program.body) if (!(stmt.type === "ExpressionStatement" && typeof stmt.directive === "string")) return stmt.start;
7486
+ return program.body.length > 0 ? program.body[program.body.length - 1].end : 0;
7487
+ }
7488
+ /** TS/paren wrappers that may sit between a declarator and the call. */
7489
+ var EXPRESSION_WRAPPER_TYPES = /* @__PURE__ */ new Set([
7490
+ "TSAsExpression",
7491
+ "TSSatisfiesExpression",
7492
+ "TSNonNullExpression",
7493
+ "ParenthesizedExpression"
7494
+ ]);
7495
+ function unwrapExpression(node) {
7496
+ let current = node;
7497
+ while (EXPRESSION_WRAPPER_TYPES.has(current.type) && current.expression) current = current.expression;
7498
+ return current;
7499
+ }
7500
+ /**
7501
+ * Can this call site's options put the component in the prerender tier?
7502
+ *
7503
+ * Runtime-tier call sites (`ttl`/`tags` without `prerender`) must do NO
7504
+ * build work — recording them would make the post-build hook import the
7505
+ * built RSC entry (running production startup) and load route modules for
7506
+ * routes that have nothing to prerender (codex P2 on PR #836). The answer
7507
+ * is static only when the options argument is an object literal:
7508
+ *
7509
+ * - literal with `prerender: true` → yes
7510
+ * - literal with `prerender: false`/absent → no (spread-free)
7511
+ * - anything else (identifier, spread, non-literal `prerender` value)
7512
+ * → conservatively yes — the capture pass re-checks the real options
7513
+ * at runtime and skips non-prerender registrations; over-inclusion
7514
+ * costs a wasted entry load, under-inclusion would silently skip
7515
+ * prerendering.
7516
+ */
7517
+ function mayPrerender(call) {
7518
+ if (call.arguments.length < 2) return false;
7519
+ const options = unwrapExpression(call.arguments[1]);
7520
+ if (options.type !== "ObjectExpression") return true;
7521
+ const properties = options.properties ?? [];
7522
+ let verdict = "absent";
7523
+ for (const prop of properties) {
7524
+ if (prop.type === "SpreadElement") {
7525
+ verdict = "unknown";
7526
+ continue;
7527
+ }
7528
+ if (prop.type !== "Property") continue;
7529
+ const property = prop;
7530
+ if (property.computed) {
7531
+ verdict = "unknown";
7532
+ continue;
7533
+ }
7534
+ if ((property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : void 0) !== "prerender") continue;
7535
+ const value = unwrapExpression(property.value);
7536
+ if (value.type === "Literal") verdict = value.value === true ? "true" : "false";
7537
+ else verdict = "unknown";
7538
+ }
7539
+ return verdict === "true" || verdict === "unknown";
7540
+ }
7541
+ /**
7542
+ * Collect matching call sites from the allowlisted module-scope positions:
7543
+ * top-level (possibly exported) variable declarator initializers and
7544
+ * `export default` expressions. Returned in source order. The display name
7545
+ * is the declaring variable's name, or 'Component' for default exports.
7546
+ */
7547
+ function collectModuleScopeSites(program, bindings) {
7548
+ const isMatch = (node) => node.type === "CallExpression" && node.arguments.length >= 1 && node.arguments.length <= 2 && isCacheComponentCallee(node.callee, bindings);
7549
+ const sites = [];
7550
+ for (const stmt of program.body) {
7551
+ if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration) {
7552
+ const call = unwrapExpression(stmt.declaration);
7553
+ if (isMatch(call)) sites.push({
7554
+ call,
7555
+ stmt,
7556
+ displayName: "Component"
7557
+ });
7558
+ continue;
7559
+ }
7560
+ let decl = stmt;
7561
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) decl = stmt.declaration;
7562
+ if (decl.type !== "VariableDeclaration" || !Array.isArray(decl.declarations)) continue;
7563
+ for (const d of decl.declarations) {
7564
+ if (!d.init) continue;
7565
+ const call = unwrapExpression(d.init);
7566
+ if (!isMatch(call)) continue;
7567
+ const displayName = d.id.type === "Identifier" ? d.id.name : "Component";
7568
+ sites.push({
7569
+ call,
7570
+ stmt,
7571
+ displayName
7572
+ });
7573
+ }
7574
+ }
7575
+ return sites;
7576
+ }
7577
+ /**
7578
+ * Generate the stable component ID for a cache.component() call site:
7579
+ * `<sanitized-name>_<content-hash16>`. Exported for testing.
7580
+ */
7581
+ function generatePrebuiltId(relativeModulePath, statementText, callSpanText, occurrence, displayName) {
7582
+ return `${displayName.toLowerCase().replace(/[^a-z0-9]/g, "") || "component"}_${generateStableCallsiteId(relativeModulePath, "component", statementText, callSpanText, occurrence)}`;
7583
+ }
7584
+ /**
7585
+ * Collect the prebuilt component IDs transitively reachable from a route
7586
+ * file, following static AND dynamic imports. Same pattern as the
7587
+ * client-reference walk in build-manifest.ts; dynamic imports are included
7588
+ * because a dynamically-imported module still registers its components
7589
+ * when it eventually loads — attribution over-approximating registration
7590
+ * is handled at capture time (an attributed-but-unregistered component is
7591
+ * reported and skipped).
7592
+ */
7593
+ function collectReachablePrebuiltComponents(entryModuleId, moduleComponents, getModuleInfo) {
7594
+ const visited = /* @__PURE__ */ new Set();
7595
+ const result = /* @__PURE__ */ new Set();
7596
+ const stack = [entryModuleId];
7597
+ while (stack.length > 0) {
7598
+ const moduleId = stack.pop();
7599
+ if (visited.has(moduleId)) continue;
7600
+ visited.add(moduleId);
7601
+ const ids = moduleComponents.get(moduleId);
7602
+ if (ids) for (const id of ids) result.add(id);
7603
+ const info = getModuleInfo(moduleId);
7604
+ if (!info) continue;
7605
+ for (const imported of info.importedIds) stack.push(imported);
7606
+ for (const imported of info.dynamicallyImportedIds ?? []) stack.push(imported);
7607
+ }
7608
+ return Array.from(result);
7609
+ }
7610
+ /**
7611
+ * Collect page and layout file paths from the scanned route tree — the
7612
+ * walk roots for attribution. Slots and intercepting/private segments are
7613
+ * excluded, mirroring the capture pass's route walk
7614
+ * (server/prebuilt-builder.ts §collectCandidateRoutes).
7615
+ */
7616
+ function collectPageAndLayoutFiles(root) {
7617
+ const files = [];
7618
+ const walk = (node) => {
7619
+ if (node.segmentType === "intercepting" || node.segmentType === "private") return;
7620
+ if (node.page) files.push(node.page.filePath);
7621
+ if (node.layout) files.push(node.layout.filePath);
7622
+ for (const child of node.children) walk(child);
7623
+ };
7624
+ walk(root);
7625
+ return files;
7626
+ }
7627
+ /**
7628
+ * For a route file (page/layout), check if it imports from any prebuilt-
7629
+ * declaring module and extract JSX call-site props for those bindings.
7630
+ * Uses `getModuleInfo` to get the RESOLVED import targets and the module's
7631
+ * transformed code (available on Rollup's ModuleInfo at generateBundle).
7632
+ */
7633
+ function collectCrossModuleProps(ctx, moduleId, getInfo) {
7634
+ const bindingNames = ctx.prebuiltBindingNames;
7635
+ const info = getInfo(moduleId);
7636
+ if (!info) return;
7637
+ const declaringImports = info.importedIds.filter((id) => bindingNames.has(id) && id !== moduleId);
7638
+ if (declaringImports.length === 0) return;
7639
+ const exportNameToId = /* @__PURE__ */ new Map();
7640
+ const collisions = /* @__PURE__ */ new Set();
7641
+ for (const declId of declaringImports) {
7642
+ const map = bindingNames.get(declId);
7643
+ for (const [name, compId] of map) {
7644
+ if (exportNameToId.has(name)) collisions.add(name);
7645
+ exportNameToId.set(name, compId);
7646
+ }
7647
+ }
7648
+ for (const name of collisions) exportNameToId.delete(name);
7649
+ let source = null;
7650
+ const extInfo = getInfo(moduleId);
7651
+ if (extInfo && typeof extInfo.code === "string") source = extInfo.code;
7652
+ else try {
7653
+ source = readFileSync(moduleId, "utf8");
7654
+ } catch {
7655
+ return;
7656
+ }
7657
+ if (!source) return;
7658
+ let program;
7659
+ try {
7660
+ program = parseAst(source, { lang: "tsx" });
7661
+ } catch {
7662
+ return;
7663
+ }
7664
+ const localToComponentId = /* @__PURE__ */ new Map();
7665
+ for (const stmt of program.body) {
7666
+ if (stmt.type !== "ImportDeclaration") continue;
7667
+ const importDecl = stmt;
7668
+ for (const spec of importDecl.specifiers) if (spec.type === "ImportSpecifier") {
7669
+ const exportedName = spec.imported?.name ?? spec.local.name;
7670
+ const compId = exportNameToId.get(exportedName);
7671
+ if (compId) localToComponentId.set(spec.local.name, compId);
7672
+ } else if (spec.type === "ImportDefaultSpecifier") {
7673
+ const compId = exportNameToId.get("Component") ?? exportNameToId.get("default");
7674
+ if (compId) localToComponentId.set(spec.local.name, compId);
7675
+ else if (exportNameToId.size === 1) {
7676
+ const [, soleId] = [...exportNameToId.entries()][0];
7677
+ localToComponentId.set(spec.local.name, soleId);
7678
+ }
7679
+ }
7680
+ }
7681
+ if (localToComponentId.size === 0) return;
7682
+ const usages = collectJsxUsages(program, new Set(localToComponentId.keys()));
7683
+ for (const [localName, propsList] of usages) {
7684
+ const componentId = localToComponentId.get(localName);
7685
+ if (!componentId) continue;
7686
+ for (const props of propsList) {
7687
+ if (props === null) continue;
7688
+ recordCallSiteProps(ctx, componentId, props);
7689
+ }
7690
+ }
7691
+ }
7692
+ function timberPrebuilt(ctx) {
7693
+ return {
7694
+ name: "timber-prebuilt",
7695
+ transform(code, id) {
7696
+ if (!ctx.dev && this?.environment?.name === "rsc") ctx.prebuiltModuleComponents?.delete(id);
7697
+ if (!code.includes("@timber-js/app/cache") && !code.includes("timber/cache")) return;
7698
+ if (id.includes("node_modules")) return;
7699
+ if (this?.environment?.name === "client") return;
7700
+ let program;
7701
+ try {
7702
+ program = parseAst(code, { lang: "tsx" });
7703
+ } catch {
7704
+ return;
7705
+ }
7706
+ const bindings = findImportBindings(program, CACHE_MODULE_SOURCES, "cache");
7707
+ if (bindings.named.size === 0 && bindings.namespaces.size === 0) return;
7708
+ const callSites = collectModuleScopeSites(program, bindings);
7709
+ if (callSites.length === 0) return;
7710
+ const relPath = relative(ctx.root || process.cwd(), id);
7711
+ const s = new MagicString(code);
7712
+ const occurrences = /* @__PURE__ */ new Map();
7713
+ const wrapperName = ctx.dev ? "__prebuiltDev" : "__prebuilt";
7714
+ const componentIds = [];
7715
+ for (const { call, stmt, displayName } of callSites) {
7716
+ const stmtText = code.slice(stmt.start, stmt.end);
7717
+ const spanText = code.slice(call.start, call.end);
7718
+ const dupKey = stmtText + String.fromCharCode(0) + spanText;
7719
+ const occurrence = occurrences.get(dupKey) ?? 0;
7720
+ occurrences.set(dupKey, occurrence + 1);
7721
+ const firstArg = ctx.dev ? displayName : generatePrebuiltId(relPath, stmtText, spanText, occurrence, displayName);
7722
+ if (!ctx.dev && mayPrerender(call)) componentIds.push(firstArg);
7723
+ s.overwrite(call.callee.start, call.callee.end, wrapperName);
7724
+ s.appendLeft(call.arguments[0].start, `${JSON.stringify(firstArg)}, `);
7725
+ }
7726
+ if (!ctx.dev && componentIds.length > 0 && this?.environment?.name === "rsc") {
7727
+ ctx.prebuiltModuleComponents ??= /* @__PURE__ */ new Map();
7728
+ ctx.prebuiltModuleComponents.set(id, componentIds);
7729
+ const bindingMap = /* @__PURE__ */ new Map();
7730
+ let componentIdIdx = 0;
7731
+ for (const { call, displayName } of callSites) if (!ctx.dev && mayPrerender(call)) bindingMap.set(displayName, componentIds[componentIdIdx++]);
7732
+ for (const stmt of program.body) if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration && stmt.declaration.type === "Identifier") {
7733
+ const name = stmt.declaration.name;
7734
+ if (bindingMap.has(name) && !bindingMap.has("default")) bindingMap.set("default", bindingMap.get(name));
7735
+ }
7736
+ if (bindingMap.size > 0) {
7737
+ ctx.prebuiltBindingNames ??= /* @__PURE__ */ new Map();
7738
+ ctx.prebuiltBindingNames.set(id, bindingMap);
7739
+ }
7740
+ const declaredBindings = new Set(bindingMap.keys());
7741
+ if (declaredBindings.size > 0) {
7742
+ const usages = collectJsxUsages(program, declaredBindings);
7743
+ for (const [bindingName, propsList] of usages) {
7744
+ const compId = bindingMap.get(bindingName);
7745
+ if (!compId) continue;
7746
+ for (const props of propsList) {
7747
+ if (props === null) continue;
7748
+ recordCallSiteProps(ctx, compId, props);
7749
+ }
7750
+ }
7751
+ }
7752
+ }
7753
+ s.appendLeft(importInsertionOffset(program), `import { ${wrapperName} } from '#server-internal';\n`);
7754
+ return {
7755
+ code: s.toString(),
7756
+ map: s.generateMap({ hires: "boundary" })
7757
+ };
7758
+ },
7759
+ /**
7760
+ * RSC environment, production: walk each route file's module graph to
7761
+ * attribute prebuilt components to the routes that can render them.
7762
+ * The graph walk (rather than load-order registry deltas) is what makes
7763
+ * SHARED components work — a component imported by several routes is
7764
+ * captured under every importing route's param combos.
7765
+ */
7766
+ generateBundle() {
7767
+ if (ctx.dev) return;
7768
+ if (this?.environment?.name !== "rsc") return;
7769
+ ctx.routePrebuiltComponents = void 0;
7770
+ const moduleComponents = ctx.prebuiltModuleComponents;
7771
+ if (!moduleComponents || moduleComponents.size === 0 || !ctx.routeTree) return;
7772
+ const self = this;
7773
+ const getInfo = (id) => self.getModuleInfo?.(id) ?? null;
7774
+ const routeComponents = /* @__PURE__ */ new Map();
7775
+ for (const filePath of collectPageAndLayoutFiles(ctx.routeTree.root)) {
7776
+ const ids = collectReachablePrebuiltComponents(filePath, moduleComponents, getInfo);
7777
+ if (ids.length > 0) routeComponents.set(filePath, ids);
7778
+ }
7779
+ if (routeComponents.size > 0) {
7780
+ ctx.routePrebuiltComponents = routeComponents;
7781
+ if (ctx.prebuiltBindingNames && ctx.prebuiltBindingNames.size > 0) {
7782
+ const scannedModules = /* @__PURE__ */ new Set();
7783
+ for (const filePath of routeComponents.keys()) {
7784
+ const visited = /* @__PURE__ */ new Set();
7785
+ const stack = [filePath];
7786
+ while (stack.length > 0) {
7787
+ const modId = stack.pop();
7788
+ if (visited.has(modId)) continue;
7789
+ visited.add(modId);
7790
+ if (!scannedModules.has(modId)) {
7791
+ scannedModules.add(modId);
7792
+ collectCrossModuleProps(ctx, modId, getInfo);
7793
+ }
7794
+ const modInfo = getInfo(modId);
7795
+ if (!modInfo) continue;
7796
+ for (const imp of modInfo.importedIds) stack.push(imp);
7797
+ for (const imp of modInfo.dynamicallyImportedIds ?? []) stack.push(imp);
7798
+ }
7799
+ }
7800
+ }
7801
+ } else console.warn(`[timber] cache.component: ${moduleComponents.size} module(s) declare prebuilt components, but none are reachable from any page or layout — no payloads will be prerendered`);
7802
+ },
7803
+ /**
7804
+ * Post-build capture (design/44 §Build Phase). `order: 'post'` runs
7805
+ * after the RSC plugin's buildApp — all five build steps are complete —
7806
+ * and BEFORE timber-adapter-build's post hook (plugin array order), so
7807
+ * `prebuilt/` exists when the adapter assembles its output (TIM-1122).
7808
+ */
7809
+ buildApp: {
7810
+ order: "post",
7811
+ async handler() {
7812
+ if (ctx.dev) return;
7813
+ ctx.captureSummary = void 0;
7814
+ await cleanPrebuiltDir(ctx.buildDir);
7815
+ if (!ctx.routePrebuiltComponents || ctx.routePrebuiltComponents.size === 0) return;
7816
+ if (ctx.config.output === "static") {
7817
+ console.log(`[timber] cache.component: skipped prerender capture (output: 'static' renders all components at build time)`);
7818
+ return;
7819
+ }
7820
+ ctx.captureSummary = await runPrebuiltCapture(ctx);
7821
+ }
7822
+ }
7823
+ };
7824
+ }
7825
+ //#endregion
7826
+ //#region src/plugins/prerender-sugar.ts
7827
+ /**
7828
+ * timber-prerender-sugar — Vite plugin that desugars `export const prerender`
7829
+ * on page and layout files into `cache.component(...)` wrappers.
7830
+ *
7831
+ * Design doc: design/45-cache-lifetimes.md §"Route-Level Static" and
7832
+ * §"Layout Propagation".
7833
+ *
7834
+ * `export const prerender = true` on a page is pure sugar: it desugars to
7835
+ * wrapping the page's default export in `timber.cache.component(Component,
7836
+ * { prerender: true })`. Layout propagation cascades a layout's
7837
+ * `export const prerender` to descendant pages via most-specific-wins:
7838
+ * walk the segment tree from leaf to root, first explicit value wins.
7839
+ *
7840
+ * A layout that declares `export const prerender` is ALSO wrapped — the
7841
+ * layout's flight payload is its own static shell (`children` is a
7842
+ * separate component boundary), so caching it is natural (nav, sidebar,
7843
+ * footer benefit from build-time rendering). See design/45 §Layout
7844
+ * Propagation.
7845
+ *
7846
+ * This transform runs BEFORE timber-prebuilt so the generated
7847
+ * `cache.component(...)` call is picked up by the callsite rewrite.
7848
+ */
7849
+ /**
7850
+ * Walk the segment tree and resolve the effective prerender value for
7851
+ * every page and layout. Returns a map from file path to resolved value.
7852
+ *
7853
+ * **Pages:** walk from leaf toward root; first explicit `export const prerender`
7854
+ * wins (page > nearest layout > ... > root layout). If none, dynamic.
7855
+ *
7856
+ * **Layouts:** a layout that declares `export const prerender` is wrapped
7857
+ * directly (its own shell is a separate component boundary from `children`).
7858
+ * Layouts without their own declaration are NOT wrapped — they don't
7859
+ * inherit from ancestors, because wrapping a layout's shell without the
7860
+ * author's explicit opt-in would cache content they may expect to be dynamic.
7861
+ */
7862
+ function resolvePagePrerender(root) {
7863
+ const result = /* @__PURE__ */ new Map();
7864
+ function walk(node, ancestorStack) {
7865
+ if (node.segmentType === "intercepting" || node.segmentType === "private") return;
7866
+ const layoutValue = node.layout ? getPrerenderExport(node.layout.filePath) : void 0;
7867
+ const stack = layoutValue !== void 0 ? [layoutValue, ...ancestorStack] : ancestorStack;
7868
+ if (node.layout && layoutValue !== void 0 && layoutValue !== false) result.set(node.layout.filePath, layoutValue);
7869
+ if (node.page) {
7870
+ const pageValue = getPrerenderExport(node.page.filePath);
7871
+ const resolved = pageValue !== void 0 ? pageValue : stack[0];
7872
+ if (resolved !== void 0 && resolved !== false) result.set(node.page.filePath, resolved);
7873
+ }
7874
+ for (const child of node.children) walk(child, stack);
7875
+ for (const slot of Object.values(node.slots)) walk(slot, stack);
7876
+ }
7877
+ walk(root, []);
7878
+ return result;
7879
+ }
7880
+ /**
7881
+ * Build the `cache.component` options source text from a resolved value.
7882
+ */
7883
+ function buildOptionsSource(value) {
7884
+ if (value === true) return "{ prerender: true }";
7885
+ if (typeof value === "object") {
7886
+ const parts = ["prerender: true"];
7887
+ if (value.ttl !== void 0) parts.push(`ttl: ${value.ttl}`);
7888
+ if (value.tags !== void 0) parts.push(`tags: ${JSON.stringify(value.tags)}`);
7889
+ return `{ ${parts.join(", ")} }`;
7890
+ }
7891
+ return "{ prerender: true }";
7892
+ }
7893
+ function findDefaultExport(program, source) {
7894
+ for (const stmt of program.body) {
7895
+ if (stmt.type !== "ExportDefaultDeclaration" || !stmt.declaration) continue;
7896
+ const decl = stmt.declaration;
7897
+ if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") {
7898
+ const name = decl.id?.name;
7899
+ if (name) return {
7900
+ kind: "declaration",
7901
+ node: stmt,
7902
+ name
7903
+ };
7904
+ return {
7905
+ kind: "expression",
7906
+ node: stmt
7907
+ };
7908
+ }
7909
+ if (decl.type === "Identifier") return {
7910
+ kind: "identifier",
7911
+ node: stmt,
7912
+ name: decl.name
7913
+ };
7914
+ return {
7915
+ kind: "expression",
7916
+ node: stmt
7917
+ };
7918
+ }
7919
+ for (const stmt of program.body) {
7920
+ if (stmt.type !== "ExportNamedDeclaration") continue;
7921
+ if (stmt.source) continue;
7922
+ const specifiers = stmt.specifiers;
7923
+ if (!specifiers) continue;
7924
+ for (let i = 0; i < specifiers.length; i++) {
7925
+ const spec = specifiers[i];
7926
+ if (spec.exported?.name === "default" && spec.local?.name && spec.local.name !== "default") return {
7927
+ kind: "identifier",
7928
+ node: stmt,
7929
+ name: spec.local.name,
7930
+ specifierIndex: i,
7931
+ specifierCount: specifiers.length,
7932
+ specifierStart: spec.start,
7933
+ specifierEnd: spec.end
7934
+ };
7935
+ }
7936
+ }
7937
+ return null;
7938
+ }
7939
+ function timberPrerenderSugar(ctx) {
7940
+ let resolvedPages = null;
7941
+ function ensureResolved() {
7942
+ if (resolvedPages) return;
7943
+ if (!ctx.routeTree) {
7944
+ resolvedPages = /* @__PURE__ */ new Map();
7945
+ return;
7946
+ }
7947
+ resolvedPages = resolvePagePrerender(ctx.routeTree.root);
7948
+ }
7949
+ return {
7950
+ name: "timber-prerender-sugar",
7951
+ buildStart() {
7952
+ resolvedPages = null;
7953
+ },
7954
+ transform(code, id) {
7955
+ if (this?.environment?.name === "client") return;
7956
+ if (id.includes("node_modules")) return;
7957
+ ensureResolved();
7958
+ if (!resolvedPages || resolvedPages.size === 0) return;
7959
+ const value = resolvedPages.get(id);
7960
+ if (!value) return;
7961
+ let program;
7962
+ try {
7963
+ program = parseAst(code, { lang: "tsx" });
7964
+ } catch {
7965
+ return;
7966
+ }
7967
+ const defaultExport = findDefaultExport(program, code);
7968
+ if (!defaultExport) {
7969
+ if (value) {
7970
+ const relPath = id.replace(ctx.root + "/", "");
7971
+ console.warn(`[timber] ${relPath}: export const prerender is declared but the default export cannot be wrapped (re-export or missing). The prerender declaration will be ignored for this file.`);
7972
+ }
7973
+ return;
7974
+ }
7975
+ if (defaultExport.kind === "expression" && defaultExport.node.declaration) {
7976
+ if (code.slice(defaultExport.node.declaration.start, defaultExport.node.declaration.end).includes(".component(")) return;
7977
+ }
7978
+ ctx.prerenderSugarPages ??= /* @__PURE__ */ new Set();
7979
+ ctx.prerenderSugarPages.add(id);
7980
+ const s = new MagicString(code);
7981
+ const optionsSource = buildOptionsSource(value);
7982
+ const importAlias = "__timber_prerender_cache__";
7983
+ let insertOffset = 0;
7984
+ for (const stmt of program.body) if (!(stmt.type === "ExpressionStatement" && typeof stmt.directive === "string")) {
7985
+ insertOffset = stmt.start;
7986
+ break;
7987
+ }
7988
+ s.appendLeft(insertOffset, `import { cache as ${importAlias} } from '@timber-js/app/cache';\n`);
7989
+ switch (defaultExport.kind) {
7990
+ case "declaration": {
7991
+ const declStart = defaultExport.node.declaration.start;
7992
+ s.overwrite(defaultExport.node.start, declStart, "");
7993
+ s.append(`\nexport default ${importAlias}.component(${defaultExport.name}, ${optionsSource});\n`);
7994
+ break;
7995
+ }
7996
+ case "identifier":
7997
+ if (defaultExport.specifierCount !== void 0 && defaultExport.specifierCount > 1) {
7998
+ const specStart = defaultExport.specifierStart;
7999
+ const specEnd = defaultExport.specifierEnd;
8000
+ const idx = defaultExport.specifierIndex;
8001
+ if (idx === 0) {
8002
+ const nextSpec = defaultExport.node.specifiers?.[1];
8003
+ if (nextSpec) s.remove(specStart, nextSpec.start);
8004
+ } else {
8005
+ const prevSpec = defaultExport.node.specifiers?.[idx - 1];
8006
+ if (prevSpec) s.remove(prevSpec.end, specEnd);
8007
+ }
8008
+ s.appendLeft(defaultExport.node.end, `\nexport default ${importAlias}.component(${defaultExport.name}, ${optionsSource});`);
8009
+ } else s.overwrite(defaultExport.node.start, defaultExport.node.end, `export default ${importAlias}.component(${defaultExport.name}, ${optionsSource});`);
8010
+ break;
8011
+ case "expression": {
8012
+ const decl = defaultExport.node.declaration;
8013
+ s.overwrite(defaultExport.node.start, decl.start, "const __timber_default_page__ = ");
8014
+ const afterExpr = code.charAt(defaultExport.node.end - 1) === ";" ? "" : ";";
8015
+ s.appendLeft(defaultExport.node.end, `${afterExpr}\nexport default ${importAlias}.component(__timber_default_page__, ${optionsSource});`);
8016
+ break;
8017
+ }
8018
+ }
8019
+ return {
8020
+ code: s.toString(),
8021
+ map: s.generateMap({ hires: "boundary" })
8022
+ };
8023
+ }
8024
+ };
8025
+ }
8026
+ //#endregion
7207
8027
  //#region src/plugins/cloudflare-wasm.ts
7208
8028
  /**
7209
8029
  * timber-cloudflare-wasm — Handle WASM modules for Cloudflare Workers builds.
@@ -7504,6 +8324,82 @@ function pad(str, width, align = "right") {
7504
8324
  function stripAnsi(str) {
7505
8325
  return str.replace(/\u001b\[\d+m/g, "");
7506
8326
  }
8327
+ function loadPrebuiltManifest(buildDir) {
8328
+ try {
8329
+ const raw = readFileSync(join(buildDir, "prebuilt", "manifest.json"), "utf-8");
8330
+ return JSON.parse(raw);
8331
+ } catch {
8332
+ return null;
8333
+ }
8334
+ }
8335
+ function buildPrebuiltReportData(manifest, summary) {
8336
+ const hasCaptures = summary && (summary.captured > 0 || summary.skipped.length > 0);
8337
+ if (!manifest && !hasCaptures) return null;
8338
+ const components = [];
8339
+ let totalEntries = 0;
8340
+ let totalBytes = 0;
8341
+ if (manifest) {
8342
+ for (const [componentId, component] of Object.entries(manifest)) {
8343
+ let entryCount = 0;
8344
+ let componentBytes = 0;
8345
+ for (const entry of Object.values(component.entries)) {
8346
+ entryCount++;
8347
+ componentBytes += entry.bytes ?? 0;
8348
+ }
8349
+ components.push({
8350
+ componentId,
8351
+ entryCount,
8352
+ totalBytes: componentBytes
8353
+ });
8354
+ totalEntries += entryCount;
8355
+ totalBytes += componentBytes;
8356
+ }
8357
+ components.sort((a, b) => a.componentId.localeCompare(b.componentId));
8358
+ }
8359
+ const skippedCount = summary?.skipped.length ?? 0;
8360
+ const skippedReasons = (summary?.skipped ?? []).map((s) => ({
8361
+ componentId: s.componentId,
8362
+ route: s.routeUrlPath,
8363
+ message: s.message
8364
+ }));
8365
+ return {
8366
+ components,
8367
+ totalEntries,
8368
+ totalBytes,
8369
+ skippedCount,
8370
+ skippedReasons
8371
+ };
8372
+ }
8373
+ function buildPrebuiltReport(data) {
8374
+ const lines = [];
8375
+ const total = data.totalEntries + data.skippedCount;
8376
+ lines.push(`Prebuilt ${data.totalEntries}/${total} entries across ${data.components.length} component${data.components.length !== 1 ? "s" : ""}` + (data.totalBytes > 0 ? ` (${formatSize(data.totalBytes)} total)` : "") + "." + (data.skippedCount > 0 ? ` ${data.skippedCount} skipped due to errors.` : ""));
8377
+ if (data.components.length > 0) {
8378
+ lines.push("");
8379
+ const nameHeader = "Component";
8380
+ const entriesHeader = "Entries";
8381
+ const sizeHeader = "Size";
8382
+ const nameW = Math.max(11, ...data.components.map((c) => c.componentId.length + 4));
8383
+ const entriesW = Math.max(7, 8);
8384
+ const sizeW = Math.max(4, 10);
8385
+ const totalW = nameW + entriesW + sizeW + 4;
8386
+ const sep = "─".repeat(totalW);
8387
+ lines.push(`${pad(nameHeader, nameW)} ${pad(entriesHeader, entriesW, "left")} ${pad(sizeHeader, sizeW, "left")}`);
8388
+ lines.push(sep);
8389
+ for (const comp of data.components) {
8390
+ const nameStr = ` ◆ ${comp.componentId}`;
8391
+ const entriesStr = String(comp.entryCount);
8392
+ const sizeStr = formatSize(comp.totalBytes);
8393
+ lines.push(`${pad(nameStr, nameW)} ${pad(entriesStr, entriesW, "left")} ${pad(sizeStr, sizeW, "left")}`);
8394
+ }
8395
+ }
8396
+ if (data.skippedReasons.length > 0) {
8397
+ lines.push("");
8398
+ lines.push("Skipped entries:");
8399
+ for (const s of data.skippedReasons) lines.push(` ⚠ ${s.componentId} (${s.route}) — ${s.message}`);
8400
+ }
8401
+ return lines;
8402
+ }
7507
8403
  /** Measure raw + gzip sizes for all JS chunks and CSS assets in a bundle. */
7508
8404
  function collectChunkSizes(bundle) {
7509
8405
  const sizes = /* @__PURE__ */ new Map();
@@ -7691,6 +8587,18 @@ function timberBuildReport(ctx) {
7691
8587
  logger.info("");
7692
8588
  logger.info(`✓ built ${entries.length} routes for all three environments (rsc, ssr, client) in ${elapsed}s`);
7693
8589
  logger.info("");
8590
+ },
8591
+ buildApp: {
8592
+ order: "post",
8593
+ async handler() {
8594
+ if (ctx.dev || !logger) return;
8595
+ const prebuiltData = buildPrebuiltReportData(loadPrebuiltManifest(ctx.buildDir), ctx.captureSummary);
8596
+ if (!prebuiltData) return;
8597
+ logger.info("");
8598
+ const prebuiltLines = buildPrebuiltReport(prebuiltData);
8599
+ for (const line of prebuiltLines) logger.info(line);
8600
+ logger.info("");
8601
+ }
7694
8602
  }
7695
8603
  };
7696
8604
  }
@@ -8202,6 +9110,7 @@ function timber(config) {
8202
9110
  previewConfig.port = resolvedDevPort;
8203
9111
  previewConfig.strictPort = resolvedDevPortExplicit;
8204
9112
  }
9113
+ serverConfig.forwardConsole = resolveForwardConsole(ctx.config.devBrowserLogs);
8205
9114
  return {
8206
9115
  build: { outDir: buildOutDir },
8207
9116
  environments: envOutDirs,
@@ -8247,6 +9156,8 @@ function timber(config) {
8247
9156
  timberFonts(ctx),
8248
9157
  timberMdx(ctx),
8249
9158
  timberCacheTransform(ctx),
9159
+ timberPrerenderSugar(ctx),
9160
+ timberPrebuilt(ctx),
8250
9161
  timberContent(ctx),
8251
9162
  timberServerBundle(),
8252
9163
  timberCloudflareWasm(ctx),
@@ -8254,7 +9165,6 @@ function timber(config) {
8254
9165
  timberBuildReport(ctx),
8255
9166
  timberAdapterBuild(ctx),
8256
9167
  timberDevLogs(ctx),
8257
- timberDevBrowserLogs(ctx),
8258
9168
  timberDevServer(ctx)
8259
9169
  ];
8260
9170
  }