@timber-js/app 0.2.0-alpha.34 → 0.2.0-alpha.36

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 (237) hide show
  1. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  2. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  3. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  4. package/dist/_chunks/{debug-B3Gypr3D.js → debug-ECi_61pb.js} +1 -1
  5. package/dist/_chunks/{debug-B3Gypr3D.js.map → debug-ECi_61pb.js.map} +1 -1
  6. package/dist/_chunks/define-cookie-w5GWm_bL.js +93 -0
  7. package/dist/_chunks/define-cookie-w5GWm_bL.js.map +1 -0
  8. package/dist/_chunks/error-boundary-TYEQJZ1-.js +211 -0
  9. package/dist/_chunks/error-boundary-TYEQJZ1-.js.map +1 -0
  10. package/dist/_chunks/{format-RyoGQL74.js → format-cX7wzEp2.js} +2 -2
  11. package/dist/_chunks/{format-RyoGQL74.js.map → format-cX7wzEp2.js.map} +1 -1
  12. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  13. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  14. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  15. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  16. package/dist/_chunks/{request-context-BQUC8PHn.js → request-context-CZz_T0Bc.js} +40 -71
  17. package/dist/_chunks/request-context-CZz_T0Bc.js.map +1 -0
  18. package/dist/_chunks/segment-context-Dpq2XOKg.js +34 -0
  19. package/dist/_chunks/segment-context-Dpq2XOKg.js.map +1 -0
  20. package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
  21. package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
  22. package/dist/_chunks/{tracing-CemImE6h.js → tracing-BPyIzIdu.js} +2 -2
  23. package/dist/_chunks/{tracing-CemImE6h.js.map → tracing-BPyIzIdu.js.map} +1 -1
  24. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
  25. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
  26. package/dist/_chunks/wrappers-C1SN725w.js +331 -0
  27. package/dist/_chunks/wrappers-C1SN725w.js.map +1 -0
  28. package/dist/cache/index.js +1 -1
  29. package/dist/client/error-boundary.d.ts +10 -1
  30. package/dist/client/error-boundary.d.ts.map +1 -1
  31. package/dist/client/error-boundary.js +1 -125
  32. package/dist/client/index.d.ts +2 -2
  33. package/dist/client/index.d.ts.map +1 -1
  34. package/dist/client/index.js +193 -90
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/link.d.ts +8 -8
  37. package/dist/client/link.d.ts.map +1 -1
  38. package/dist/client/navigation-context.d.ts +2 -2
  39. package/dist/client/router.d.ts +25 -3
  40. package/dist/client/router.d.ts.map +1 -1
  41. package/dist/client/rsc-fetch.d.ts +23 -2
  42. package/dist/client/rsc-fetch.d.ts.map +1 -1
  43. package/dist/client/segment-cache.d.ts +1 -1
  44. package/dist/client/segment-cache.d.ts.map +1 -1
  45. package/dist/client/stale-reload.d.ts +15 -0
  46. package/dist/client/stale-reload.d.ts.map +1 -1
  47. package/dist/client/top-loader.d.ts +1 -1
  48. package/dist/client/top-loader.d.ts.map +1 -1
  49. package/dist/client/use-params.d.ts +2 -2
  50. package/dist/client/use-params.d.ts.map +1 -1
  51. package/dist/client/use-query-states.d.ts +1 -1
  52. package/dist/codec.d.ts +21 -0
  53. package/dist/codec.d.ts.map +1 -0
  54. package/dist/cookies/define-cookie.d.ts +33 -12
  55. package/dist/cookies/define-cookie.d.ts.map +1 -1
  56. package/dist/cookies/index.js +1 -81
  57. package/dist/index.d.ts +87 -12
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +346 -210
  60. package/dist/index.js.map +1 -1
  61. package/dist/params/define.d.ts +76 -0
  62. package/dist/params/define.d.ts.map +1 -0
  63. package/dist/params/index.d.ts +8 -0
  64. package/dist/params/index.d.ts.map +1 -0
  65. package/dist/params/index.js +104 -0
  66. package/dist/params/index.js.map +1 -0
  67. package/dist/plugins/adapter-build.d.ts.map +1 -1
  68. package/dist/plugins/build-manifest.d.ts.map +1 -1
  69. package/dist/plugins/client-chunks.d.ts +32 -0
  70. package/dist/plugins/client-chunks.d.ts.map +1 -0
  71. package/dist/plugins/entries.d.ts.map +1 -1
  72. package/dist/plugins/routing.d.ts.map +1 -1
  73. package/dist/plugins/server-bundle.d.ts.map +1 -1
  74. package/dist/plugins/static-build.d.ts.map +1 -1
  75. package/dist/routing/codegen.d.ts +2 -2
  76. package/dist/routing/codegen.d.ts.map +1 -1
  77. package/dist/routing/index.js +1 -1
  78. package/dist/routing/scanner.d.ts.map +1 -1
  79. package/dist/routing/status-file-lint.d.ts +2 -1
  80. package/dist/routing/status-file-lint.d.ts.map +1 -1
  81. package/dist/routing/types.d.ts +6 -4
  82. package/dist/routing/types.d.ts.map +1 -1
  83. package/dist/rsc-runtime/rsc.d.ts +1 -1
  84. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  85. package/dist/search-params/codecs.d.ts +1 -1
  86. package/dist/search-params/define.d.ts +153 -0
  87. package/dist/search-params/define.d.ts.map +1 -0
  88. package/dist/search-params/index.d.ts +4 -5
  89. package/dist/search-params/index.d.ts.map +1 -1
  90. package/dist/search-params/index.js +3 -474
  91. package/dist/search-params/registry.d.ts +1 -1
  92. package/dist/search-params/wrappers.d.ts +53 -0
  93. package/dist/search-params/wrappers.d.ts.map +1 -0
  94. package/dist/server/access-gate.d.ts +4 -0
  95. package/dist/server/access-gate.d.ts.map +1 -1
  96. package/dist/server/action-encryption.d.ts +76 -0
  97. package/dist/server/action-encryption.d.ts.map +1 -0
  98. package/dist/server/action-handler.d.ts.map +1 -1
  99. package/dist/server/als-registry.d.ts +4 -4
  100. package/dist/server/als-registry.d.ts.map +1 -1
  101. package/dist/server/build-manifest.d.ts +2 -2
  102. package/dist/server/deny-renderer.d.ts.map +1 -1
  103. package/dist/server/early-hints.d.ts +13 -5
  104. package/dist/server/early-hints.d.ts.map +1 -1
  105. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  106. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  107. package/dist/server/flight-injection-state.d.ts +78 -0
  108. package/dist/server/flight-injection-state.d.ts.map +1 -0
  109. package/dist/server/flight-scripts.d.ts +39 -0
  110. package/dist/server/flight-scripts.d.ts.map +1 -0
  111. package/dist/server/form-data.d.ts +29 -0
  112. package/dist/server/form-data.d.ts.map +1 -1
  113. package/dist/server/html-injectors.d.ts +3 -9
  114. package/dist/server/html-injectors.d.ts.map +1 -1
  115. package/dist/server/index.d.ts +1 -1
  116. package/dist/server/index.d.ts.map +1 -1
  117. package/dist/server/index.js +1819 -1629
  118. package/dist/server/index.js.map +1 -1
  119. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  120. package/dist/server/pipeline.d.ts.map +1 -1
  121. package/dist/server/request-context.d.ts +28 -40
  122. package/dist/server/request-context.d.ts.map +1 -1
  123. package/dist/server/route-element-builder.d.ts +7 -0
  124. package/dist/server/route-element-builder.d.ts.map +1 -1
  125. package/dist/server/route-matcher.d.ts +2 -2
  126. package/dist/server/route-matcher.d.ts.map +1 -1
  127. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  128. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  129. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  130. package/dist/server/slot-resolver.d.ts.map +1 -1
  131. package/dist/server/ssr-entry.d.ts.map +1 -1
  132. package/dist/server/ssr-render.d.ts +3 -0
  133. package/dist/server/ssr-render.d.ts.map +1 -1
  134. package/dist/server/tree-builder.d.ts +12 -8
  135. package/dist/server/tree-builder.d.ts.map +1 -1
  136. package/dist/server/types.d.ts +1 -3
  137. package/dist/server/types.d.ts.map +1 -1
  138. package/dist/server/version-skew.d.ts +61 -0
  139. package/dist/server/version-skew.d.ts.map +1 -0
  140. package/dist/shims/navigation-client.d.ts +1 -1
  141. package/dist/shims/navigation-client.d.ts.map +1 -1
  142. package/dist/shims/navigation.d.ts +1 -1
  143. package/dist/shims/navigation.d.ts.map +1 -1
  144. package/dist/utils/state-machine.d.ts +80 -0
  145. package/dist/utils/state-machine.d.ts.map +1 -0
  146. package/package.json +12 -8
  147. package/src/client/browser-entry.ts +58 -25
  148. package/src/client/error-boundary.tsx +18 -1
  149. package/src/client/index.ts +9 -1
  150. package/src/client/link.tsx +9 -9
  151. package/src/client/navigation-context.ts +2 -2
  152. package/src/client/router.ts +102 -55
  153. package/src/client/rsc-fetch.ts +63 -2
  154. package/src/client/segment-cache.ts +1 -1
  155. package/src/client/stale-reload.ts +28 -0
  156. package/src/client/top-loader.tsx +2 -2
  157. package/src/client/use-params.ts +3 -3
  158. package/src/client/use-query-states.ts +1 -1
  159. package/src/codec.ts +21 -0
  160. package/src/cookies/define-cookie.ts +69 -18
  161. package/src/index.ts +255 -65
  162. package/src/params/define.ts +260 -0
  163. package/src/params/index.ts +28 -0
  164. package/src/plugins/adapter-build.ts +6 -0
  165. package/src/plugins/build-manifest.ts +11 -0
  166. package/src/plugins/client-chunks.ts +65 -0
  167. package/src/plugins/entries.ts +3 -6
  168. package/src/plugins/routing.ts +40 -14
  169. package/src/plugins/server-bundle.ts +32 -1
  170. package/src/plugins/shims.ts +1 -1
  171. package/src/plugins/static-build.ts +8 -4
  172. package/src/routing/codegen.ts +109 -88
  173. package/src/routing/scanner.ts +55 -6
  174. package/src/routing/status-file-lint.ts +2 -1
  175. package/src/routing/types.ts +7 -4
  176. package/src/rsc-runtime/rsc.ts +2 -0
  177. package/src/search-params/codecs.ts +1 -1
  178. package/src/search-params/define.ts +504 -0
  179. package/src/search-params/index.ts +12 -18
  180. package/src/search-params/registry.ts +1 -1
  181. package/src/search-params/wrappers.ts +85 -0
  182. package/src/server/access-gate.tsx +38 -8
  183. package/src/server/action-encryption.ts +144 -0
  184. package/src/server/action-handler.ts +16 -0
  185. package/src/server/als-registry.ts +4 -4
  186. package/src/server/build-manifest.ts +4 -4
  187. package/src/server/deny-renderer.ts +2 -1
  188. package/src/server/early-hints.ts +36 -15
  189. package/src/server/error-boundary-wrapper.ts +57 -14
  190. package/src/server/flight-injection-state.ts +152 -0
  191. package/src/server/flight-scripts.ts +59 -0
  192. package/src/server/form-data.ts +76 -0
  193. package/src/server/html-injectors.ts +50 -58
  194. package/src/server/index.ts +2 -4
  195. package/src/server/node-stream-transforms.ts +65 -54
  196. package/src/server/pipeline.ts +98 -26
  197. package/src/server/request-context.ts +49 -124
  198. package/src/server/route-element-builder.ts +102 -99
  199. package/src/server/route-matcher.ts +2 -2
  200. package/src/server/rsc-entry/error-renderer.ts +5 -3
  201. package/src/server/rsc-entry/index.ts +26 -11
  202. package/src/server/rsc-entry/rsc-payload.ts +2 -2
  203. package/src/server/rsc-entry/ssr-renderer.ts +13 -5
  204. package/src/server/slot-resolver.ts +204 -206
  205. package/src/server/ssr-entry.ts +3 -1
  206. package/src/server/ssr-render.ts +3 -0
  207. package/src/server/tree-builder.ts +84 -48
  208. package/src/server/types.ts +1 -3
  209. package/src/server/version-skew.ts +104 -0
  210. package/src/shims/navigation-client.ts +1 -1
  211. package/src/shims/navigation.ts +1 -1
  212. package/src/utils/state-machine.ts +111 -0
  213. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  214. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  215. package/dist/_chunks/request-context-BQUC8PHn.js.map +0 -1
  216. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  217. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  218. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  219. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  220. package/dist/client/error-boundary.js.map +0 -1
  221. package/dist/cookies/index.js.map +0 -1
  222. package/dist/plugins/dynamic-transform.d.ts +0 -72
  223. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  224. package/dist/search-params/analyze.d.ts +0 -54
  225. package/dist/search-params/analyze.d.ts.map +0 -1
  226. package/dist/search-params/builtin-codecs.d.ts +0 -105
  227. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  228. package/dist/search-params/create.d.ts +0 -106
  229. package/dist/search-params/create.d.ts.map +0 -1
  230. package/dist/search-params/index.js.map +0 -1
  231. package/dist/server/prerender.d.ts +0 -77
  232. package/dist/server/prerender.d.ts.map +0 -1
  233. package/src/plugins/dynamic-transform.ts +0 -161
  234. package/src/search-params/analyze.ts +0 -192
  235. package/src/search-params/builtin-codecs.ts +0 -228
  236. package/src/search-params/create.ts +0 -321
  237. package/src/server/prerender.ts +0 -139
package/src/index.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import type { Plugin, PluginOption } from 'vite';
2
2
  import { existsSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
3
+ import { join, resolve } from 'node:path';
5
4
  import { createRequire } from 'node:module';
6
- import react from '@vitejs/plugin-react';
5
+ import react, { reactCompilerPreset } from '@vitejs/plugin-react';
7
6
  import { cacheTransformPlugin } from './plugins/cache-transform';
8
7
  import { timberContent } from './plugins/content';
9
8
  import { timberDevServer } from './plugins/dev-server';
@@ -13,12 +12,12 @@ import { timberRouting } from './plugins/routing';
13
12
  import { timberShims } from './plugins/shims';
14
13
  import { timberFonts } from './plugins/fonts';
15
14
  import { timberStaticBuild } from './plugins/static-build';
16
- import { timberDynamicTransform } from './plugins/dynamic-transform';
17
15
  import { timberServerActionExports } from './plugins/server-action-exports';
18
16
  import { timberBuildManifest } from './plugins/build-manifest';
19
17
  import { timberDevLogs } from './plugins/dev-logs';
20
18
  import { timberReactProd } from './plugins/react-prod';
21
19
  import { timberChunks } from './plugins/chunks';
20
+ import { clientChunkGroup } from './plugins/client-chunks';
22
21
  import { timberServerBundle } from './plugins/server-bundle';
23
22
  import { timberAdapterBuild } from './plugins/adapter-build';
24
23
  import { timberBuildReport } from './plugins/build-report';
@@ -26,6 +25,7 @@ import type { RouteTree } from './routing/types';
26
25
  import type { BuildManifest } from './server/build-manifest';
27
26
  import type { StartupTimer } from './utils/startup-timer';
28
27
  import { createStartupTimer, createNoopTimer } from './utils/startup-timer';
28
+ import { resolveEncryptionKeyExpression, shouldEnableEncryption } from './server/action-encryption';
29
29
 
30
30
  /** Configuration for client-side JavaScript output. */
31
31
  export interface ClientJavascriptConfig {
@@ -95,18 +95,6 @@ export interface TimberUserConfig {
95
95
  /** Threshold in ms to highlight slow phases in dev logging output. Default: 200. */
96
96
  slowPhaseMs?: number;
97
97
  };
98
- /**
99
- * Cookie signing configuration. See design/29-cookies.md §"Signed Cookies".
100
- *
101
- * Provide `secret` for a single key, or `secrets` (array) for key rotation.
102
- * When `secrets` is used, index 0 is the signing key; all are tried for verification.
103
- */
104
- cookies?: {
105
- /** Single signing secret. Shorthand for `secrets: [secret]`. */
106
- secret?: string;
107
- /** Array of signing secrets for key rotation. Index 0 signs; all verify. */
108
- secrets?: string[];
109
- };
110
98
  /**
111
99
  * Control Server-Timing header output.
112
100
  *
@@ -148,6 +136,46 @@ export interface TimberUserConfig {
148
136
  *
149
137
  * See LOCAL-336 for design decisions.
150
138
  */
139
+ /**
140
+ * Server action bound args encryption configuration.
141
+ *
142
+ * The RSC plugin encrypts closure variables captured by 'use server' functions
143
+ * using AES-256-GCM so they are opaque and tamper-proof in the Flight payload.
144
+ * Encryption is always enabled in production.
145
+ *
146
+ * The encryption key is auto-generated at build time and embedded in the server bundle,
147
+ * so all instances running the same build share the same key automatically.
148
+ * For rolling/blue-green deployments where multiple builds coexist, set
149
+ * `TIMBER_ACTIONS_ENCRYPTION_KEY` env var to share a key across builds.
150
+ *
151
+ * See design/08-forms-and-actions.md §"Security"
152
+ */
153
+ actionEncryption?: {
154
+ /**
155
+ * Disable encryption in dev mode for easier debugging of bound args.
156
+ * Has no effect in production — encryption is always enabled.
157
+ * Default: false (encryption is on in dev too).
158
+ */
159
+ disableInDev?: boolean;
160
+ };
161
+ /**
162
+ * Enable the React Compiler (babel-plugin-react-compiler) for automatic
163
+ * memoization of components and hooks at build time.
164
+ *
165
+ * - `true` — enable with default options
166
+ * - `{ compilationMode, target }` — enable with custom options
167
+ * - `compilationMode: 'annotation'` — only compile files with `'use memo'`
168
+ * - `target: '18'` — target React 18 (uses react-compiler-runtime package)
169
+ * - `false` or omitted — disabled (default)
170
+ *
171
+ * Uses `@vitejs/plugin-react`'s built-in `reactCompilerPreset`, which:
172
+ * - Applies Babel only for the compiler pass (OXC handles JSX)
173
+ * - Automatically scopes to client environment only
174
+ * - Uses `react/compiler-runtime` built into React 19
175
+ *
176
+ * Requires `babel-plugin-react-compiler` as a peer dependency.
177
+ */
178
+ reactCompiler?: boolean | { compilationMode?: string; target?: string };
151
179
  topLoader?: {
152
180
  /** Whether the top-loader is enabled. Default: true. */
153
181
  enabled?: boolean;
@@ -209,6 +237,8 @@ export interface PluginContext {
209
237
  dev: boolean;
210
238
  /** CSS build manifest (populated by adapter after client build, null in dev) */
211
239
  buildManifest: BuildManifest | null;
240
+ /** Per-build deployment ID for version skew detection (null in dev) */
241
+ deploymentId: string | null;
212
242
  /** Startup timer for profiling cold start phases (active in dev, no-op in prod) */
213
243
  timer: StartupTimer;
214
244
  }
@@ -244,7 +274,10 @@ export function resolveAppDir(root: string, configAppDir?: string): string {
244
274
 
245
275
  function createPluginContext(config?: TimberUserConfig, root?: string): PluginContext {
246
276
  const projectRoot = root ?? process.cwd();
247
- const resolvedConfig: TimberUserConfig = { output: 'server', ...config };
277
+ // Don't apply defaults here they would override file-based config
278
+ // during mergeFileConfig (inline spreads over file). Defaults are
279
+ // applied after merge in timber(). See TIM-451.
280
+ const resolvedConfig: TimberUserConfig = { ...config };
248
281
  // Timer starts as active — swapped to noop in configResolved for production builds
249
282
  return {
250
283
  config: resolvedConfig,
@@ -254,6 +287,7 @@ function createPluginContext(config?: TimberUserConfig, root?: string): PluginCo
254
287
  root: projectRoot,
255
288
  dev: false,
256
289
  buildManifest: null,
290
+ deploymentId: null,
257
291
  timer: createStartupTimer(),
258
292
  };
259
293
  }
@@ -261,14 +295,18 @@ function createPluginContext(config?: TimberUserConfig, root?: string): PluginCo
261
295
  /**
262
296
  * Load timber.config.ts (or .js, .mjs) from the project root.
263
297
  * Returns the config object or null if no config file is found.
298
+ *
299
+ * Uses require() which works for ESM modules on Node 22.12+.
300
+ * This keeps timber() synchronous — no async config loading needed.
264
301
  */
265
- async function loadTimberConfigFile(root: string): Promise<TimberUserConfig | null> {
302
+ export function loadTimberConfigFile(root: string): TimberUserConfig | null {
266
303
  const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];
304
+ const req = createRequire(join(root, 'package.json'));
267
305
 
268
306
  for (const name of configNames) {
269
307
  const configPath = join(root, name);
270
308
  if (existsSync(configPath)) {
271
- const mod = await import(pathToFileURL(configPath).href);
309
+ const mod = req(configPath);
272
310
  return (mod.default ?? mod) as TimberUserConfig;
273
311
  }
274
312
  }
@@ -288,7 +326,6 @@ export function warnConfigConflicts(
288
326
  ): string[] {
289
327
  const conflicts: string[] = [];
290
328
  for (const key of Object.keys(fileConfig) as (keyof TimberUserConfig)[]) {
291
- if (key === 'output') continue;
292
329
  if (key in inline && inline[key] !== undefined) {
293
330
  conflicts.push(key);
294
331
  }
@@ -327,28 +364,155 @@ function mergeFileConfig(ctx: PluginContext, fileConfig: TimberUserConfig): void
327
364
  };
328
365
  }
329
366
 
367
+ /**
368
+ * Resolve the React Compiler plugin via @rolldown/plugin-babel.
369
+ *
370
+ * Uses the `reactCompilerPreset` from @vitejs/plugin-react, which:
371
+ * - Uses Babel ONLY for the compiler pass (OXC handles JSX)
372
+ * - Automatically scopes to client environment via applyToEnvironmentHook
373
+ * - Uses react/compiler-runtime built into React 19
374
+ *
375
+ * @rolldown/plugin-babel and babel-plugin-react-compiler are optional peer deps.
376
+ * If either is missing, require() fails with a clear error message.
377
+ */
378
+ function resolveReactCompilerPlugin(
379
+ config: true | { compilationMode?: string; target?: string },
380
+ req: NodeRequire
381
+ ): PluginOption {
382
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
383
+ let babel: any;
384
+ try {
385
+ babel = req('@rolldown/plugin-babel');
386
+ } catch {
387
+ throw new Error(
388
+ '[timber] reactCompiler requires @rolldown/plugin-babel. ' +
389
+ 'Install it: pnpm add -D @rolldown/plugin-babel babel-plugin-react-compiler'
390
+ );
391
+ }
392
+ const options = typeof config === 'object' ? config : {};
393
+ const babelPlugin = babel.default ?? babel;
394
+ return babelPlugin({
395
+ presets: [reactCompilerPreset(options as Parameters<typeof reactCompilerPreset>[0])],
396
+ }) as PluginOption;
397
+ }
398
+
399
+ /**
400
+ * Build the options object for @vitejs/plugin-rsc.
401
+ *
402
+ * Uses a getter for `enableActionEncryption` so the RSC plugin reads
403
+ * the value lazily — after ctx.dev is set in configResolved. This lets
404
+ * `actionEncryption.disableInDev` work correctly even though the RSC
405
+ * plugin is created before Vite resolves the command.
406
+ */
407
+ function createRscOptions(
408
+ ctx: PluginContext,
409
+ encryptionKeyExpr: string | undefined
410
+ ): Record<string, unknown> {
411
+ const options: Record<string, unknown> = {
412
+ serverHandler: false,
413
+ customClientEntry: true,
414
+ entries: {
415
+ rsc: 'virtual:timber-rsc-entry',
416
+ ssr: 'virtual:timber-ssr-entry',
417
+ client: 'virtual:timber-browser-entry',
418
+ },
419
+ // Group client references by layout boundary to balance route-scoped code
420
+ // splitting with HTTP request count. A constant group name ('client-refs')
421
+ // would collapse all routes into one chunk — any page downloads every
422
+ // client component. Per-serverChunk grouping creates many sub-500B files.
423
+ // Layout-boundary grouping is the middle ground: components under the same
424
+ // layout segment share a chunk. See design/27-chunking-strategy.md, TIM-440, TIM-499.
425
+ clientChunks: (meta: { id: string; normalizedId: string; serverChunk: string }) =>
426
+ clientChunkGroup(meta, ctx.appDir),
427
+ };
428
+
429
+ // Bound args encryption — AES-256-GCM authenticated encryption for
430
+ // closure variables in 'use server' functions. Always on in production,
431
+ // configurable in dev. See design/08-forms-and-actions.md §"Bound Args Encryption".
432
+ //
433
+ // Uses a getter so the RSC plugin reads the value lazily in its transform
434
+ // hooks, after ctx.dev is set in configResolved. This lets disableInDev
435
+ // work correctly — ctx.dev is false at construction time but true during
436
+ // dev server transforms.
437
+ Object.defineProperty(options, 'enableActionEncryption', {
438
+ get() {
439
+ return shouldEnableEncryption(ctx.dev, ctx.config.actionEncryption);
440
+ },
441
+ enumerable: true,
442
+ });
443
+
444
+ // When TIMBER_ACTIONS_ENCRYPTION_KEY is set, pass it as a runtime expression
445
+ // so the RSC plugin uses it instead of auto-generating a per-build key.
446
+ if (encryptionKeyExpr) {
447
+ options.defineEncryptionKey = encryptionKeyExpr;
448
+ }
449
+
450
+ return options;
451
+ }
452
+
330
453
  function timberCache(_ctx: PluginContext): Plugin {
331
454
  return cacheTransformPlugin();
332
455
  }
333
456
 
457
+ /**
458
+ * Create the timber Vite plugin array.
459
+ *
460
+ * Loads timber.config.ts and all dependencies synchronously before
461
+ * constructing the plugin array. This ensures ALL plugins — including
462
+ * the RSC plugin and React Compiler — see the fully merged config
463
+ * (inline + file-based). No async, no deferred config, no stale reads.
464
+ *
465
+ * Requires Node >= 22.12 for synchronous require() of ESM modules
466
+ * (@vitejs/plugin-rsc is ESM-only).
467
+ *
468
+ * Previous versions used async loading and deferred config merging,
469
+ * causing file-based config for reactCompiler, actionEncryption, and
470
+ * output mode to be silently ignored. See TIM-451.
471
+ */
334
472
  export function timber(config?: TimberUserConfig): PluginOption[] {
335
473
  const ctx = createPluginContext(config);
336
- // Sync ctx.root and ctx.appDir with Vite's resolved root, which may
337
- // differ from process.cwd() when --config points to a subdirectory.
338
- // Also loads timber.config.ts and merges it into ctx.config (inline config wins).
474
+
475
+ // Resolve dependencies from the consumer's project (process.cwd()),
476
+ // not from timber's own node_modules. This is critical for pnpm link:
477
+ // when linked, timber's node_modules has a separate vite instance, and
478
+ // the RSC plugin must use the same vite instance as the dev server.
479
+ const consumerRequire = createRequire(join(process.cwd(), 'package.json'));
480
+
481
+ // ── Step 1: Load @vitejs/plugin-rsc ─────────────────────────────────
482
+ // Synchronous require() works for ESM modules on Node 22.12+.
483
+ ctx.timer.start('rsc-plugin-import');
484
+ const rscMod = consumerRequire('@vitejs/plugin-rsc');
485
+ const vitePluginRsc = rscMod.default ?? rscMod;
486
+ ctx.timer.end('rsc-plugin-import');
487
+
488
+ // ── Step 2: Compute config-dependent options ────────────────────────
489
+ // encryptionKeyExpr is env-based and doesn't depend on file config.
490
+ const encryptionKeyExpr = resolveEncryptionKeyExpression();
491
+
492
+ // ── Step 3: Build rootSync plugin ───────────────────────────────────
493
+ // rootSync loads timber.config.ts and resolves the Vite root.
494
+ // Config file loading happens in the `config` hook so it uses Vite's
495
+ // resolved root (from userConfig.root) instead of process.cwd().
496
+ // This is critical when running from a workspace root with
497
+ // `vite --config subdir/vite.config.ts` or a custom `root` option.
498
+ // See TIM-498.
339
499
  const rootSync: Plugin = {
340
500
  name: 'timber-root-sync',
341
- async config(userConfig, { command }) {
342
- // Load timber.config.ts early before configResolved/buildStart — so
343
- // all plugins (including timber-mdx) see the merged config in their
344
- // buildStart hooks. The config hook runs once and supports async.
345
- const root = userConfig.root ?? process.cwd();
501
+ config(userConfig, { command }) {
502
+ // ── Load timber.config.ts from the correct root ───────────────
503
+ // Vite's `config` hook fires before `configResolved`. The user's
504
+ // `root` option (if set) tells us where the project lives.
505
+ // `resolve()` mirrors Vite's own root resolution logic.
506
+ const viteRoot = resolve(userConfig.root ?? process.cwd());
346
507
  ctx.timer.start('config-load');
347
- const fileConfig = await loadTimberConfigFile(root);
508
+ const fileConfig = loadTimberConfigFile(viteRoot);
348
509
  if (fileConfig) {
349
510
  mergeFileConfig(ctx, fileConfig);
350
511
  ctx.clientJavascript = resolveClientJavascript(ctx.config);
351
512
  }
513
+ // Apply defaults AFTER merge so file-based config isn't overridden
514
+ // by defaults that were baked into the inline config object.
515
+ ctx.config.output ??= 'server';
352
516
  ctx.timer.end('config-load');
353
517
 
354
518
  // Force production JSX transform for builds.
@@ -387,17 +551,49 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
387
551
  }
388
552
  },
389
553
  };
554
+
555
+ // ── Step 4: Resolve optional plugins ────────────────────────────────
556
+ // React Compiler — controlled by reactCompiler in config (inline or file).
557
+ // If set in inline config, resolve immediately. If it comes from
558
+ // timber.config.ts, it's picked up in rootSync's config hook and
559
+ // resolved lazily via the timber-react-compiler wrapper plugin.
560
+ const reactCompilerPlugins: PluginOption[] = [];
561
+ if (config?.reactCompiler) {
562
+ // Inline config — resolve eagerly (preserves sync throw on missing dep)
563
+ reactCompilerPlugins.push(resolveReactCompilerPlugin(config.reactCompiler, consumerRequire));
564
+ }
565
+ // Lazy wrapper for file-based reactCompiler config (timber.config.ts).
566
+ // After rootSync loads the file config in its `config` hook, this plugin's
567
+ // `configResolved` hook checks if reactCompiler was added by the file and
568
+ // not already resolved from inline config. If so, it resolves and copies
569
+ // the babel plugin's hooks onto itself so Vite invokes them.
570
+ const lazyReactCompiler: Plugin = {
571
+ name: 'timber-react-compiler',
572
+ configResolved() {
573
+ // Skip if already resolved from inline config or not configured
574
+ if (config?.reactCompiler || !ctx.config.reactCompiler) return;
575
+ // File config set reactCompiler — resolve and copy hooks
576
+ const resolved = resolveReactCompilerPlugin(
577
+ ctx.config.reactCompiler,
578
+ consumerRequire
579
+ ) as Plugin;
580
+ // Copy transform/resolveId/etc hooks from the babel plugin
581
+ for (const key of Object.keys(resolved) as (keyof Plugin)[]) {
582
+ if (key !== 'name') {
583
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
584
+ (lazyReactCompiler as any)[key] = resolved[key];
585
+ }
586
+ }
587
+ },
588
+ };
589
+
590
+ // ── Step 6: Assemble plugin array ───────────────────────────────────
390
591
  // @vitejs/plugin-rsc handles:
391
592
  // - RSC/SSR/client environment setup
392
593
  // - "use client" directive → client reference proxy transformation
393
594
  // - "use server" directive → server reference transformation
394
595
  // - Client reference tracking and module map generation
395
596
  //
396
- // Loaded via dynamic import() because @vitejs/plugin-rsc is ESM-only.
397
- // Vite's config loader uses esbuild to transpile to CJS, which breaks
398
- // static imports of ESM-only packages. The dynamic import() is preserved
399
- // by esbuild and runs natively in ESM at runtime.
400
- //
401
597
  // serverHandler: false — timber has its own dev server (timber-dev-server)
402
598
  // entries — tells the RSC plugin about timber's virtual entry modules so
403
599
  // it correctly wires up the browser entry (needed for React Fast Refresh
@@ -410,35 +606,6 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
410
606
  // We do NOT set customBuildApp — the RSC plugin's orchestration is correct
411
607
  // and handles bundle ordering, asset manifest generation, and environment
412
608
  // imports manifest. See @vitejs/plugin-rsc's buildApp implementation.
413
- // Resolve @vitejs/plugin-rsc from the consumer's project (process.cwd()),
414
- // not from timber's own node_modules. This is critical for pnpm link:
415
- // when linked, timber's node_modules has a separate vite instance, and
416
- // the RSC plugin must use the same vite instance as the dev server.
417
- const consumerRequire = createRequire(join(process.cwd(), 'package.json'));
418
- const rscPluginPath = consumerRequire.resolve('@vitejs/plugin-rsc');
419
- ctx.timer.start('rsc-plugin-import');
420
- const rscPluginsPromise = import(pathToFileURL(rscPluginPath).href).then(
421
- ({ default: vitePluginRsc }) => {
422
- ctx.timer.end('rsc-plugin-import');
423
- return vitePluginRsc({
424
- serverHandler: false,
425
- customClientEntry: true,
426
- entries: {
427
- rsc: 'virtual:timber-rsc-entry',
428
- ssr: 'virtual:timber-ssr-entry',
429
- client: 'virtual:timber-browser-entry',
430
- },
431
- // Group all client reference wrappers into a single chunk instead of
432
- // creating one tiny file per "use client" module. Without this, each
433
- // server chunk's client references become a separate entry point,
434
- // producing many sub-500B wrapper files (e.g., 30-byte re-exports).
435
- // A single group eliminates 10+ unnecessary HTTP requests.
436
- // See design/27-chunking-strategy.md and TIM-440.
437
- clientChunks: () => 'client-refs',
438
- });
439
- }
440
- );
441
-
442
609
  return [
443
610
  rootSync,
444
611
  timberReactProd(),
@@ -447,15 +614,16 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
447
614
  // following Vinext's convention — the RSC plugin's virtual browser entry
448
615
  // coordinates with plugin-react via __vite_plugin_react_preamble_installed__.
449
616
  react(),
617
+ ...reactCompilerPlugins,
618
+ lazyReactCompiler,
450
619
  timberServerActionExports(),
451
- rscPluginsPromise,
620
+ vitePluginRsc(createRscOptions(ctx, encryptionKeyExpr)),
452
621
  timberShims(ctx),
453
622
  timberRouting(ctx),
454
623
  timberEntries(ctx),
455
624
  timberBuildManifest(ctx),
456
625
  timberCache(ctx),
457
626
  timberStaticBuild(ctx),
458
- timberDynamicTransform(ctx),
459
627
  timberFonts(ctx),
460
628
  timberMdx(ctx),
461
629
  timberContent(ctx),
@@ -481,4 +649,26 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
481
649
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
482
650
  export interface Routes {}
483
651
 
652
+ /**
653
+ * Type-safe helper for timber.config.ts files.
654
+ *
655
+ * A pass-through identity function that provides autocomplete and
656
+ * type checking for timber configuration. No runtime validation —
657
+ * purely a DX convenience (same pattern as Vite's defineConfig).
658
+ *
659
+ * @example
660
+ * ```ts
661
+ * // timber.config.ts
662
+ * import { defineConfig } from '@timber-js/app';
663
+ *
664
+ * export default defineConfig({
665
+ * output: 'server',
666
+ * pageExtensions: ['tsx', 'ts', 'mdx'],
667
+ * });
668
+ * ```
669
+ */
670
+ export function defineConfig(config: TimberUserConfig): TimberUserConfig {
671
+ return config;
672
+ }
673
+
484
674
  export default timber;