@timber-js/app 0.2.0-alpha.158 → 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 (286) hide show
  1. package/dist/_chunks/{actions-BA5tIpVu.js → actions-CBbP1hHO.js} +3 -3
  2. package/dist/_chunks/{actions-BA5tIpVu.js.map → actions-CBbP1hHO.js.map} +1 -1
  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/html-injectors.d.ts +1 -0
  139. package/dist/server/html-injectors.d.ts.map +1 -1
  140. package/dist/server/index.js +2 -2
  141. package/dist/server/internal.d.ts +6 -1
  142. package/dist/server/internal.d.ts.map +1 -1
  143. package/dist/server/internal.js +563 -74
  144. package/dist/server/internal.js.map +1 -1
  145. package/dist/server/prebuilt/cache-key.d.ts +58 -0
  146. package/dist/server/prebuilt/cache-key.d.ts.map +1 -0
  147. package/dist/server/prebuilt/capture-state.d.ts +57 -0
  148. package/dist/server/prebuilt/capture-state.d.ts.map +1 -0
  149. package/dist/server/prebuilt/manifest-tag-index.d.ts +27 -0
  150. package/dist/server/prebuilt/manifest-tag-index.d.ts.map +1 -0
  151. package/dist/server/prebuilt/overlay.d.ts +100 -0
  152. package/dist/server/prebuilt/overlay.d.ts.map +1 -0
  153. package/dist/server/prebuilt/payload-source.d.ts +75 -0
  154. package/dist/server/prebuilt/payload-source.d.ts.map +1 -0
  155. package/dist/server/prebuilt/synthetic-store.d.ts +64 -0
  156. package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -0
  157. package/dist/server/prebuilt-builder.d.ts +110 -0
  158. package/dist/server/prebuilt-builder.d.ts.map +1 -0
  159. package/dist/server/prebuilt-runtime.d.ts +101 -0
  160. package/dist/server/prebuilt-runtime.d.ts.map +1 -0
  161. package/dist/server/route-element-builder.d.ts +10 -8
  162. package/dist/server/route-element-builder.d.ts.map +1 -1
  163. package/dist/server/rsc-entry/helpers.d.ts +0 -5
  164. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  165. package/dist/server/rsc-entry/index.d.ts +10 -0
  166. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  167. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  168. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  169. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -4
  170. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  171. package/dist/server/sitemap-generator.d.ts +6 -6
  172. package/dist/server/sitemap-generator.d.ts.map +1 -1
  173. package/dist/server/tracing.d.ts +37 -3
  174. package/dist/server/tracing.d.ts.map +1 -1
  175. package/docs/api/32-api-cache.mdx +10 -4
  176. package/docs/learn/00-introduction.mdx +8 -13
  177. package/docs/learn/02-pages-and-layouts.mdx +9 -0
  178. package/docs/learn/06-forms-and-actions.mdx +0 -4
  179. package/docs/learn/07-typed-routes.mdx +127 -0
  180. package/docs/learn/09-caching.mdx +12 -1
  181. package/docs/learn/10-middleware.mdx +1 -0
  182. package/docs/learn/11-error-handling.mdx +9 -0
  183. package/docs/learn/13-configuration.mdx +28 -0
  184. package/docs/learn/14-deploying.mdx +52 -0
  185. package/docs/more/06-instrumentation.mdx +27 -1
  186. package/docs/more/07-security.mdx +8 -2
  187. package/docs/more/08-developer-experience.mdx +134 -0
  188. package/package.json +31 -2
  189. package/src/adapters/build-output-helper.ts +10 -0
  190. package/src/adapters/cloudflare.ts +178 -4
  191. package/src/cache/cache-api.ts +60 -0
  192. package/src/cache/handler-store.ts +8 -0
  193. package/src/cache/index.ts +4 -0
  194. package/src/cache/redis-handler.ts +25 -13
  195. package/src/cache/singleflight.ts +11 -11
  196. package/src/cache/store.ts +71 -0
  197. package/src/cache/stores/cloudflare-kv.ts +52 -0
  198. package/src/cache/stores/memory.ts +125 -0
  199. package/src/cache/stores/redis.ts +97 -0
  200. package/src/cache/stores/vercel.ts +59 -0
  201. package/src/cache/tag-aware-handler.ts +233 -0
  202. package/src/cache/timber-cache.ts +17 -7
  203. package/src/cdn/cloudflare-purge.ts +50 -0
  204. package/src/cdn/fastly-purge.ts +63 -0
  205. package/src/cdn/headers.ts +127 -0
  206. package/src/cdn/index.ts +16 -0
  207. package/src/cdn/purge-store.ts +34 -0
  208. package/src/client/browser-entry/action-dispatch.ts +1 -10
  209. package/src/client/browser-entry/post-hydration.ts +0 -1
  210. package/src/client/browser-entry/router-init.ts +0 -5
  211. package/src/client/history.ts +0 -4
  212. package/src/client/navigation-api.ts +5 -11
  213. package/src/client/router.ts +9 -43
  214. package/src/client/rsc-fetch.ts +5 -27
  215. package/src/client/segment-cache.ts +1 -4
  216. package/src/client/use-cookie.ts +18 -12
  217. package/src/config-types.ts +3 -1
  218. package/src/config-validation.ts +1 -0
  219. package/src/dev-tools/browser-logs.ts +37 -273
  220. package/src/dev-tools/index.ts +2 -2
  221. package/src/dev-tools/instrumentation.ts +1 -1
  222. package/src/dev-tools/logger.ts +33 -51
  223. package/src/dev-tools/terminal.ts +21 -22
  224. package/src/fonts/transform.ts +4 -43
  225. package/src/index.ts +16 -3
  226. package/src/plugin-context.ts +36 -0
  227. package/src/plugins/build-report.ts +133 -1
  228. package/src/plugins/cache.ts +32 -131
  229. package/src/plugins/callsite-ast.ts +167 -0
  230. package/src/plugins/callsite-jsx.ts +243 -0
  231. package/src/plugins/entries.ts +23 -4
  232. package/src/plugins/fonts.ts +16 -2
  233. package/src/plugins/prebuilt-capture.ts +208 -0
  234. package/src/plugins/prebuilt.ts +673 -0
  235. package/src/plugins/prerender-sugar.ts +329 -0
  236. package/src/plugins/server-bundle.ts +12 -0
  237. package/src/plugins/shims.ts +9 -0
  238. package/src/plugins/static-build.ts +0 -1
  239. package/src/routing/codegen.ts +1 -21
  240. package/src/routing/convention-lint.ts +85 -49
  241. package/src/routing/export-detect.ts +304 -0
  242. package/src/routing/manifest-codegen.ts +2 -11
  243. package/src/rsc-runtime/rsc.ts +7 -0
  244. package/src/server/action-handler.ts +11 -27
  245. package/src/server/actions.ts +3 -4
  246. package/src/server/als-registry.ts +7 -0
  247. package/src/server/cookie-parsing.ts +75 -59
  248. package/src/server/deny-renderer.ts +7 -15
  249. package/src/server/html-injectors.ts +11 -3
  250. package/src/server/internal.ts +17 -1
  251. package/src/server/prebuilt/cache-key.ts +224 -0
  252. package/src/server/prebuilt/capture-state.ts +89 -0
  253. package/src/server/prebuilt/manifest-tag-index.ts +46 -0
  254. package/src/server/prebuilt/overlay.ts +256 -0
  255. package/src/server/prebuilt/payload-source.ts +192 -0
  256. package/src/server/prebuilt/synthetic-store.ts +108 -0
  257. package/src/server/prebuilt-builder.ts +640 -0
  258. package/src/server/prebuilt-runtime.ts +706 -0
  259. package/src/server/route-element-builder.ts +40 -12
  260. package/src/server/rsc-entry/helpers.ts +0 -17
  261. package/src/server/rsc-entry/index.ts +83 -0
  262. package/src/server/rsc-entry/render-route.ts +6 -33
  263. package/src/server/rsc-entry/revalidate-renderer.ts +16 -11
  264. package/src/server/rsc-entry/rsc-payload.ts +5 -11
  265. package/src/server/sitemap-generator.ts +16 -10
  266. package/src/server/tracing.ts +88 -3
  267. package/dist/_chunks/cache-api-Cbyh3RW4.js +0 -401
  268. package/dist/_chunks/cache-api-Cbyh3RW4.js.map +0 -1
  269. package/dist/_chunks/logger-C5v1IMuh.js.map +0 -1
  270. package/dist/_chunks/plugin-context-DUl3dnCF.js.map +0 -1
  271. package/dist/_chunks/validation-CnKhGJsp.js +0 -113
  272. package/dist/_chunks/validation-CnKhGJsp.js.map +0 -1
  273. package/dist/_chunks/walkers-D5jCGPum.js.map +0 -1
  274. package/dist/cache/index.js.map +0 -1
  275. package/dist/client/head.d.ts +0 -16
  276. package/dist/client/head.d.ts.map +0 -1
  277. package/dist/client/navigation-api-types.d.ts +0 -90
  278. package/dist/client/navigation-api-types.d.ts.map +0 -1
  279. package/dist/server/client-module-map.d.ts +0 -47
  280. package/dist/server/client-module-map.d.ts.map +0 -1
  281. package/dist/shared/head-header.d.ts +0 -54
  282. package/dist/shared/head-header.d.ts.map +0 -1
  283. package/src/client/head.ts +0 -61
  284. package/src/client/navigation-api-types.ts +0 -112
  285. package/src/server/client-module-map.ts +0 -58
  286. package/src/shared/head-header.ts +0 -69
@@ -139,32 +139,6 @@ function buildFontResultLiteral(extracted: ExtractedFont): string {
139
139
  /** Type for the Vite plugin's `this.error` callback. */
140
140
  type EmitError = (msg: string) => never;
141
141
 
142
- /**
143
- * File extensions that may contain real font imports. Only JS/TS module
144
- * variants are scanned. Excluding non-JS extensions (.mdx, .md, .css,
145
- * .json, etc.) prevents false-positive matches on text that mentions a
146
- * font specifier inside documentation, code fences, or asset metadata.
147
- *
148
- * See TIM-840.
149
- */
150
- const FONT_IMPORT_SCANNABLE_EXTENSIONS = new Set([
151
- '.js',
152
- '.jsx',
153
- '.ts',
154
- '.tsx',
155
- '.mjs',
156
- '.cjs',
157
- '.mts',
158
- '.cts',
159
- ]);
160
-
161
- function hasScannableExtension(id: string): boolean {
162
- const path = id.split('?', 1)[0];
163
- const dot = path.lastIndexOf('.');
164
- if (dot === -1) return false;
165
- return FONT_IMPORT_SCANNABLE_EXTENSIONS.has(path.slice(dot).toLowerCase());
166
- }
167
-
168
142
  /**
169
143
  * Run the timber-fonts transform pass on a single source file.
170
144
  *
@@ -174,6 +148,10 @@ function hasScannableExtension(id: string): boolean {
174
148
  * source map alongside the transformed code.
175
149
  *
176
150
  * Returns `null` if the file does not import from any font virtual module.
151
+ *
152
+ * Pre-filtering (extension allow-list, node_modules exclusion, and code
153
+ * substring checks) is handled by Vite 8's rolldown-native `transform.filter`
154
+ * in the plugin hook definition (plugins/fonts.ts). See TIM-840, TIM-1147.
177
155
  */
178
156
  export function runFontsTransform(
179
157
  code: string,
@@ -181,23 +159,6 @@ export function runFontsTransform(
181
159
  pipeline: FontPipeline,
182
160
  emitError: EmitError
183
161
  ): { code: string; map: ReturnType<MagicString['generateMap']> } | null {
184
- // Skip virtual modules and node_modules
185
- if (id.startsWith('\0') || id.includes('node_modules')) return null;
186
-
187
- // Only scan JS/TS module variants (see TIM-840).
188
- if (!hasScannableExtension(id)) return null;
189
-
190
- // Fast pre-filter: skip files that don't mention any font module.
191
- const hasGoogleImport =
192
- code.includes('@timber/fonts/google') ||
193
- code.includes('@timber-js/app/fonts/google') ||
194
- code.includes('next/font/google');
195
- const hasLocalImport =
196
- code.includes('@timber/fonts/local') ||
197
- code.includes('@timber-js/app/fonts/local') ||
198
- code.includes('next/font/local');
199
- if (!hasGoogleImport && !hasLocalImport) return null;
200
-
201
162
  // Single parse + AST walk.
202
163
  const analysis = findFontCallSites(code);
203
164
  if (!analysis) return null;
package/src/index.ts CHANGED
@@ -24,12 +24,14 @@ import { timberFonts } from './plugins/fonts';
24
24
  import { timberStaticBuild } from './plugins/static-build';
25
25
  import { timberBuildManifest } from './plugins/build-manifest';
26
26
  import { timberDevLogs } from './dev-tools/logs';
27
- import { timberDevBrowserLogs } from './dev-tools/browser-logs';
27
+ import { resolveForwardConsole } from './dev-tools/browser-logs';
28
28
  import { timberReactProd } from './plugins/react-prod';
29
29
  import { timberChunks } from './plugins/chunks';
30
30
  import { clientChunkGroup } from './plugins/client-chunks';
31
31
  import { timberServerBundle } from './plugins/server-bundle';
32
32
  import { timberCacheTransform } from './plugins/cache';
33
+ import { timberPrebuilt } from './plugins/prebuilt';
34
+ import { timberPrerenderSugar } from './plugins/prerender-sugar';
33
35
  import { timberCloudflareWasm } from './plugins/cloudflare-wasm';
34
36
  import { timberAdapterBuild } from './plugins/adapter-build';
35
37
  import { timberBuildReport } from './plugins/build-report';
@@ -416,7 +418,11 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
416
418
  // auto-bump from 3000. Explicit preview uses strictPort: true
417
419
  // so conflicts fail loudly, matching the dev behavior and the
418
420
  // TIM-842 spec.
419
- const serverConfig: { port?: number; strictPort?: boolean } = {};
421
+ const serverConfig: {
422
+ port?: number;
423
+ strictPort?: boolean;
424
+ forwardConsole?: boolean | { unhandledErrors?: boolean; logLevels?: string[] };
425
+ } = {};
420
426
  const previewConfig: { port?: number; strictPort?: boolean } = {};
421
427
  if (resolvedDevPort != null) {
422
428
  if (!isPreview) {
@@ -426,6 +432,12 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
426
432
  previewConfig.port = resolvedDevPort;
427
433
  previewConfig.strictPort = resolvedDevPortExplicit;
428
434
  }
435
+
436
+ // Enable Vite 8's built-in browser→terminal console forwarding.
437
+ // Vite defaults this OFF for non-agent dev; timber enables it based
438
+ // on the devBrowserLogs config (default: 'warn'). See TIM-1152.
439
+ serverConfig.forwardConsole = resolveForwardConsole(ctx.config.devBrowserLogs);
440
+
429
441
  return {
430
442
  build: { outDir: buildOutDir },
431
443
  environments: envOutDirs,
@@ -535,6 +547,8 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
535
547
  timberFonts(ctx),
536
548
  timberMdx(ctx),
537
549
  timberCacheTransform(ctx),
550
+ timberPrerenderSugar(ctx), // export const prerender → cache.component() desugaring
551
+ timberPrebuilt(ctx), // cache.component() → __prebuilt() callsite rewrite
538
552
  timberContent(ctx),
539
553
  timberServerBundle(), // Bundle all deps in server environments for prod
540
554
  timberCloudflareWasm(ctx), // Externalize WASM for Cloudflare Workers
@@ -542,7 +556,6 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
542
556
  timberBuildReport(ctx), // Post-build: route table with bundle sizes
543
557
  timberAdapterBuild(ctx), // Post-build: invoke adapter.buildOutput()
544
558
  timberDevLogs(ctx), // Dev-only: forward server console.* to browser console
545
- timberDevBrowserLogs(ctx), // Dev-only: forward browser console.* to server terminal
546
559
  timberDevServer(ctx), // Must be last — configureServer post-hook runs after all watchers
547
560
  ];
548
561
  }
@@ -13,6 +13,7 @@ import { join, resolve } from 'node:path';
13
13
  import { createRequire } from 'node:module';
14
14
  import type { RouteTree } from './routing/types';
15
15
  import type { BuildManifest } from './server/build-manifest';
16
+ import type { CaptureSummary } from './server/prebuilt-builder';
16
17
  import type { StartupTimer } from './utils/startup-timer';
17
18
  import { createStartupTimer } from './utils/startup-timer';
18
19
  import type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';
@@ -103,6 +104,41 @@ export interface PluginContext {
103
104
  * See design/27-chunking-strategy.md, TIM-1107.
104
105
  */
105
106
  sharedModuleFacades?: Map<string, string[]>;
107
+ /**
108
+ * Module ID → prebuilt component IDs declared in it. Recorded by the
109
+ * timber-prebuilt transform (RSC environment, production builds only),
110
+ * consumed by the RSC `generateBundle` module-graph walk that computes
111
+ * `routePrebuiltComponents`. See design/44-render-at-build-time.md.
112
+ */
113
+ prebuiltModuleComponents?: Map<string, string[]>;
114
+ /**
115
+ * Module ID → mapping of variable name to component ID for the prebuilt
116
+ * components declared in that module. Used at `generateBundle` time to
117
+ * trace imports from other modules back to specific component IDs.
118
+ */
119
+ prebuiltBindingNames?: Map<string, Map<string, string>>;
120
+ /**
121
+ * Component ID → distinct static call-site prop objects captured from
122
+ * JSX usage sites during the transform and generateBundle phases.
123
+ * Consumed by the post-build capture pass to render with actual props
124
+ * instead of empty `{}`. See design/44 §Props Derivation.
125
+ */
126
+ prebuiltCallSiteProps?: Map<string, Record<string, unknown>[]>;
127
+ /**
128
+ * Route file path (page or layout, absolute) → prebuilt component IDs
129
+ * transitively reachable from it. Computed during RSC `generateBundle`,
130
+ * consumed by the post-build capture pass (TIM-1119) to decide which
131
+ * components to render under which route's param combos.
132
+ */
133
+ routePrebuiltComponents?: Map<string, string[]>;
134
+ /**
135
+ * Page file paths that were desugared from `export const prerender` by
136
+ * timber-prerender-sugar. The prebuilt transform uses this to verify
137
+ * the generated `cache.component(...)` call was picked up.
138
+ */
139
+ prerenderSugarPages?: Set<string>;
140
+ /** Post-build prebuilt capture summary (populated by timber-prebuilt, consumed by build-report). */
141
+ captureSummary?: CaptureSummary;
106
142
  }
107
143
 
108
144
  // ── App directory resolution ──────────────────────────────────────────────
@@ -13,11 +13,14 @@
13
13
  * Task: TIM-287
14
14
  */
15
15
 
16
- import { relative } from 'node:path';
16
+ import { readFileSync } from 'node:fs';
17
+ import { join, relative } from 'node:path';
17
18
  import { gzipSync } from 'node:zlib';
18
19
  import type { Plugin, Logger } from 'vite';
19
20
  import type { PluginContext } from '../plugin-context.js';
20
21
  import type { SegmentNode, RouteTree } from '../routing/types.js';
22
+ import type { CaptureSummary } from '../server/prebuilt-builder.js';
23
+ import type { PrebuiltManifest } from '../server/prebuilt/payload-source.js';
21
24
  import { collectLeafRoutes } from '../routing/walkers.js';
22
25
  import { formatSize } from '../utils/format.js';
23
26
 
@@ -186,6 +189,120 @@ function stripAnsi(str: string): string {
186
189
  return str.replace(/\u001b\[\d+m/g, '');
187
190
  }
188
191
 
192
+ // ─── Prebuilt component stats ────────────────────────────────────────────
193
+
194
+ export interface PrebuiltComponentStats {
195
+ componentId: string;
196
+ entryCount: number;
197
+ totalBytes: number;
198
+ }
199
+
200
+ export interface PrebuiltReportData {
201
+ components: PrebuiltComponentStats[];
202
+ totalEntries: number;
203
+ totalBytes: number;
204
+ skippedCount: number;
205
+ skippedReasons: Array<{ componentId: string; route: string; message: string }>;
206
+ }
207
+
208
+ export function loadPrebuiltManifest(buildDir: string): PrebuiltManifest | null {
209
+ try {
210
+ const raw = readFileSync(join(buildDir, 'prebuilt', 'manifest.json'), 'utf-8');
211
+ return JSON.parse(raw) as PrebuiltManifest;
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+
217
+ export function buildPrebuiltReportData(
218
+ manifest: PrebuiltManifest | null,
219
+ summary: CaptureSummary | undefined
220
+ ): PrebuiltReportData | null {
221
+ const hasCaptures = summary && (summary.captured > 0 || summary.skipped.length > 0);
222
+ if (!manifest && !hasCaptures) return null;
223
+
224
+ const components: PrebuiltComponentStats[] = [];
225
+ let totalEntries = 0;
226
+ let totalBytes = 0;
227
+
228
+ if (manifest) {
229
+ for (const [componentId, component] of Object.entries(manifest)) {
230
+ let entryCount = 0;
231
+ let componentBytes = 0;
232
+ for (const entry of Object.values(component.entries)) {
233
+ entryCount++;
234
+ componentBytes += entry.bytes ?? 0;
235
+ }
236
+ components.push({ componentId, entryCount, totalBytes: componentBytes });
237
+ totalEntries += entryCount;
238
+ totalBytes += componentBytes;
239
+ }
240
+ components.sort((a, b) => a.componentId.localeCompare(b.componentId));
241
+ }
242
+
243
+ const skippedCount = summary?.skipped.length ?? 0;
244
+ const skippedReasons = (summary?.skipped ?? []).map((s) => ({
245
+ componentId: s.componentId,
246
+ route: s.routeUrlPath,
247
+ message: s.message,
248
+ }));
249
+
250
+ return { components, totalEntries, totalBytes, skippedCount, skippedReasons };
251
+ }
252
+
253
+ export function buildPrebuiltReport(data: PrebuiltReportData): string[] {
254
+ const lines: string[] = [];
255
+
256
+ const total = data.totalEntries + data.skippedCount;
257
+ lines.push(
258
+ `Prebuilt ${data.totalEntries}/${total} entries across ` +
259
+ `${data.components.length} component${data.components.length !== 1 ? 's' : ''}` +
260
+ (data.totalBytes > 0 ? ` (${formatSize(data.totalBytes)} total)` : '') +
261
+ '.' +
262
+ (data.skippedCount > 0 ? ` ${data.skippedCount} skipped due to errors.` : '')
263
+ );
264
+
265
+ if (data.components.length > 0) {
266
+ lines.push('');
267
+ const nameHeader = 'Component';
268
+ const entriesHeader = 'Entries';
269
+ const sizeHeader = 'Size';
270
+
271
+ const nameW = Math.max(
272
+ nameHeader.length + 2,
273
+ ...data.components.map((c) => c.componentId.length + 4)
274
+ );
275
+ const entriesW = Math.max(entriesHeader.length, 8);
276
+ const sizeW = Math.max(sizeHeader.length, 10);
277
+ const totalW = nameW + entriesW + sizeW + 4;
278
+ const sep = '─'.repeat(totalW);
279
+
280
+ lines.push(
281
+ `${pad(nameHeader, nameW)} ${pad(entriesHeader, entriesW, 'left')} ${pad(sizeHeader, sizeW, 'left')}`
282
+ );
283
+ lines.push(sep);
284
+
285
+ for (const comp of data.components) {
286
+ const nameStr = ` ◆ ${comp.componentId}`;
287
+ const entriesStr = String(comp.entryCount);
288
+ const sizeStr = formatSize(comp.totalBytes);
289
+ lines.push(
290
+ `${pad(nameStr, nameW)} ${pad(entriesStr, entriesW, 'left')} ${pad(sizeStr, sizeW, 'left')}`
291
+ );
292
+ }
293
+ }
294
+
295
+ if (data.skippedReasons.length > 0) {
296
+ lines.push('');
297
+ lines.push('Skipped entries:');
298
+ for (const s of data.skippedReasons) {
299
+ lines.push(` ⚠ ${s.componentId} (${s.route}) — ${s.message}`);
300
+ }
301
+ }
302
+
303
+ return lines;
304
+ }
305
+
189
306
  // ─── Bundle analysis ──────────────────────────────────────────────────────
190
307
 
191
308
  interface ChunkSize {
@@ -455,5 +572,20 @@ export function timberBuildReport(ctx: PluginContext): Plugin {
455
572
  );
456
573
  logger.info('');
457
574
  },
575
+
576
+ buildApp: {
577
+ order: 'post' as const,
578
+ async handler() {
579
+ if (ctx.dev || !logger) return;
580
+ const manifest = loadPrebuiltManifest(ctx.buildDir);
581
+ const prebuiltData = buildPrebuiltReportData(manifest, ctx.captureSummary);
582
+ if (!prebuiltData) return;
583
+
584
+ logger.info('');
585
+ const prebuiltLines = buildPrebuiltReport(prebuiltData);
586
+ for (const line of prebuiltLines) logger.info(line);
587
+ logger.info('');
588
+ },
589
+ },
458
590
  };
459
591
  }
@@ -47,6 +47,9 @@
47
47
  * referencing the same bindings — the same function, where an ID swap is
48
48
  * harmless.
49
49
  *
50
+ * AST helpers and the ID derivation are shared with timber-prebuilt
51
+ * (plugins/prebuilt.ts) via plugins/callsite-ast.ts.
52
+ *
50
53
  * See TIM-997, TIM-1053, design/06-caching.md §"Cache Key Security".
51
54
  */
52
55
 
@@ -55,84 +58,20 @@ import MagicString from 'magic-string';
55
58
  import { parseAst } from 'vite';
56
59
  import type { Plugin } from 'vite';
57
60
  import type { PluginContext } from '../plugin-context.js';
58
- import { fnv1aHash } from '../cache/fast-hash.js';
59
-
60
- const CACHE_MODULE_SOURCES = new Set(['@timber-js/app/cache', 'timber/cache']);
61
-
62
- interface PositionedNode {
63
- type: string;
64
- start: number;
65
- end: number;
66
- [key: string]: unknown;
67
- }
68
-
69
- interface IdentifierNode extends PositionedNode {
70
- type: 'Identifier';
71
- name: string;
72
- }
73
-
74
- interface ImportDeclarationNode extends PositionedNode {
75
- type: 'ImportDeclaration';
76
- source: { value: string };
77
- specifiers: Array<
78
- PositionedNode & {
79
- type: string;
80
- local: IdentifierNode;
81
- imported?: IdentifierNode;
82
- }
83
- >;
84
- }
85
-
86
- interface CallExpressionNode extends PositionedNode {
87
- type: 'CallExpression';
88
- callee: PositionedNode;
89
- arguments: PositionedNode[];
90
- }
91
-
92
- interface MemberExpressionNode extends PositionedNode {
93
- type: 'MemberExpression';
94
- object: PositionedNode;
95
- property: PositionedNode;
96
- computed: boolean;
97
- }
98
-
99
- interface ProgramNode extends PositionedNode {
100
- type: 'Program';
101
- body: PositionedNode[];
102
- }
103
-
104
- interface CacheBindings {
105
- /** Local names bound to `cache` via named imports (`import { cache as c }`). */
106
- named: Set<string>;
107
- /** Local names of namespace imports (`import * as ns`) of a cache module. */
108
- namespaces: Set<string>;
109
- }
110
-
111
- /**
112
- * Find the local binding name(s) for `cache` imported from the cache module —
113
- * both named imports and namespace imports (TIM-1054).
114
- */
115
- function findCacheBindings(program: ProgramNode): CacheBindings {
116
- const named = new Set<string>();
117
- const namespaces = new Set<string>();
118
- for (const stmt of program.body) {
119
- if (stmt.type !== 'ImportDeclaration') continue;
120
- const decl = stmt as unknown as ImportDeclarationNode;
121
- if (!CACHE_MODULE_SOURCES.has(decl.source.value)) continue;
122
- for (const spec of decl.specifiers) {
123
- if (spec.type === 'ImportNamespaceSpecifier') {
124
- namespaces.add(spec.local.name);
125
- continue;
126
- }
127
- if (spec.type !== 'ImportSpecifier') continue;
128
- const importedName = spec.imported?.name ?? spec.local.name;
129
- if (importedName === 'cache') {
130
- named.add(spec.local.name);
131
- }
132
- }
133
- }
134
- return { named, namespaces };
135
- }
61
+ import {
62
+ CACHE_MODULE_SOURCES,
63
+ findCallSites,
64
+ findImportBindings,
65
+ generateStableCallsiteId,
66
+ } from './callsite-ast.js';
67
+ import type {
68
+ CallExpressionNode,
69
+ IdentifierNode,
70
+ ImportBindings,
71
+ MemberExpressionNode,
72
+ PositionedNode,
73
+ ProgramNode,
74
+ } from './callsite-ast.js';
136
75
 
137
76
  /**
138
77
  * Is this callee a reference to the imported `cache` function — either a bare
@@ -140,7 +79,7 @@ function findCacheBindings(program: ProgramNode): CacheBindings {
140
79
  * Computed access (`ns['cache']`) is deliberately not matched; it falls back
141
80
  * to the runtime fnId derivation.
142
81
  */
143
- function isCacheCallee(callee: PositionedNode, bindings: CacheBindings): boolean {
82
+ function isCacheCallee(callee: PositionedNode, bindings: ImportBindings): boolean {
144
83
  if (callee.type === 'Identifier') {
145
84
  return bindings.named.has((callee as IdentifierNode).name);
146
85
  }
@@ -158,54 +97,8 @@ function isCacheCallee(callee: PositionedNode, bindings: CacheBindings): boolean
158
97
  }
159
98
 
160
99
  /**
161
- * Recursively walk AST nodes to find cache() call expressions.
162
- *
163
- * cache() is typically called at module scope, but we also handle calls
164
- * inside functions/blocks for completeness.
165
- */
166
- function findCacheCallSites(
167
- node: PositionedNode,
168
- cacheBindings: CacheBindings,
169
- results: CallExpressionNode[]
170
- ): void {
171
- if (node.type === 'CallExpression') {
172
- const call = node as unknown as CallExpressionNode;
173
- if (call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings)) {
174
- results.push(call);
175
- }
176
- }
177
- for (const key of Object.keys(node)) {
178
- if (key === 'type' || key === 'start' || key === 'end') continue;
179
- const val = node[key];
180
- if (val && typeof val === 'object') {
181
- if (Array.isArray(val)) {
182
- for (const item of val) {
183
- if (item && typeof item === 'object' && typeof item.type === 'string') {
184
- findCacheCallSites(item as PositionedNode, cacheBindings, results);
185
- }
186
- }
187
- } else if (typeof (val as PositionedNode).type === 'string') {
188
- findCacheCallSites(val as PositionedNode, cacheBindings, results);
189
- }
190
- }
191
- }
192
- }
193
-
194
- /**
195
- * Generate a stable callsite ID from a module path, the source text of the
196
- * enclosing top-level statement, the source text of the cache(...) call
197
- * expression, and an occurrence index among fully byte-identical
198
- * (statement, call) pairs in the same file.
199
- *
200
- * The ID is deterministic and:
201
- * - Stable across cold starts AND across builds/deploys for unchanged calls —
202
- * inserting, deleting, or reordering other cache() calls does not affect it
203
- * - Unique across different modules (relPath component)
204
- * - Unique across distinct cache() calls within a module (statement + span
205
- * hashes), with fully identical duplicates disambiguated by source-order
206
- * occurrence
207
- * - Fresh when the wrapped fn, opts, or enclosing statement change, which
208
- * intentionally invalidates the old persisted entries
100
+ * Generate a stable callsite ID for a cache() call. See callsite-ast.ts for
101
+ * the derivation properties.
209
102
  */
210
103
  function generateStableId(
211
104
  relativeModulePath: string,
@@ -213,8 +106,12 @@ function generateStableId(
213
106
  callSpanText: string,
214
107
  occurrence: number
215
108
  ): string {
216
- return fnv1aHash(
217
- `${relativeModulePath}:cache:${fnv1aHash(statementText)}:${fnv1aHash(callSpanText)}:${occurrence}`
109
+ return generateStableCallsiteId(
110
+ relativeModulePath,
111
+ 'cache',
112
+ statementText,
113
+ callSpanText,
114
+ occurrence
218
115
  );
219
116
  }
220
117
 
@@ -234,7 +131,7 @@ export function timberCacheTransform(ctx: PluginContext): Plugin {
234
131
  return;
235
132
  }
236
133
 
237
- const cacheBindings = findCacheBindings(program);
134
+ const cacheBindings = findImportBindings(program, CACHE_MODULE_SOURCES, 'cache');
238
135
  if (cacheBindings.named.size === 0 && cacheBindings.namespaces.size === 0) return;
239
136
 
240
137
  // Collect call sites per top-level statement so each call carries its
@@ -242,7 +139,11 @@ export function timberCacheTransform(ctx: PluginContext): Plugin {
242
139
  const callSites: Array<{ call: CallExpressionNode; stmt: PositionedNode }> = [];
243
140
  for (const stmt of program.body) {
244
141
  const calls: CallExpressionNode[] = [];
245
- findCacheCallSites(stmt, cacheBindings, calls);
142
+ findCallSites(
143
+ stmt,
144
+ (call) => call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings),
145
+ calls
146
+ );
246
147
  for (const call of calls) callSites.push({ call, stmt });
247
148
  }
248
149
  if (callSites.length === 0) return;