@timber-js/app 0.1.1 → 0.1.2

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 (141) hide show
  1. package/dist/index.js +5 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/plugins/entries.d.ts.map +1 -1
  4. package/package.json +2 -1
  5. package/src/adapters/cloudflare.ts +325 -0
  6. package/src/adapters/nitro.ts +366 -0
  7. package/src/adapters/types.ts +63 -0
  8. package/src/cache/index.ts +91 -0
  9. package/src/cache/redis-handler.ts +91 -0
  10. package/src/cache/register-cached-function.ts +99 -0
  11. package/src/cache/singleflight.ts +26 -0
  12. package/src/cache/stable-stringify.ts +21 -0
  13. package/src/cache/timber-cache.ts +116 -0
  14. package/src/cli.ts +201 -0
  15. package/src/client/browser-entry.ts +663 -0
  16. package/src/client/error-boundary.tsx +209 -0
  17. package/src/client/form.tsx +200 -0
  18. package/src/client/head.ts +61 -0
  19. package/src/client/history.ts +46 -0
  20. package/src/client/index.ts +60 -0
  21. package/src/client/link-navigate-interceptor.tsx +62 -0
  22. package/src/client/link-status-provider.tsx +40 -0
  23. package/src/client/link.tsx +310 -0
  24. package/src/client/nuqs-adapter.tsx +117 -0
  25. package/src/client/router-ref.ts +25 -0
  26. package/src/client/router.ts +563 -0
  27. package/src/client/segment-cache.ts +194 -0
  28. package/src/client/segment-context.ts +57 -0
  29. package/src/client/ssr-data.ts +95 -0
  30. package/src/client/types.ts +4 -0
  31. package/src/client/unload-guard.ts +34 -0
  32. package/src/client/use-cookie.ts +122 -0
  33. package/src/client/use-link-status.ts +46 -0
  34. package/src/client/use-navigation-pending.ts +47 -0
  35. package/src/client/use-params.ts +71 -0
  36. package/src/client/use-pathname.ts +43 -0
  37. package/src/client/use-query-states.ts +133 -0
  38. package/src/client/use-router.ts +77 -0
  39. package/src/client/use-search-params.ts +74 -0
  40. package/src/client/use-selected-layout-segment.ts +110 -0
  41. package/src/content/index.ts +13 -0
  42. package/src/cookies/define-cookie.ts +137 -0
  43. package/src/cookies/index.ts +9 -0
  44. package/src/fonts/ast.ts +359 -0
  45. package/src/fonts/css.ts +68 -0
  46. package/src/fonts/fallbacks.ts +248 -0
  47. package/src/fonts/google.ts +332 -0
  48. package/src/fonts/local.ts +177 -0
  49. package/src/fonts/types.ts +88 -0
  50. package/src/index.ts +413 -0
  51. package/src/plugins/adapter-build.ts +118 -0
  52. package/src/plugins/build-manifest.ts +323 -0
  53. package/src/plugins/build-report.ts +353 -0
  54. package/src/plugins/cache-transform.ts +199 -0
  55. package/src/plugins/chunks.ts +90 -0
  56. package/src/plugins/content.ts +136 -0
  57. package/src/plugins/dev-error-overlay.ts +230 -0
  58. package/src/plugins/dev-logs.ts +280 -0
  59. package/src/plugins/dev-server.ts +389 -0
  60. package/src/plugins/dynamic-transform.ts +161 -0
  61. package/src/plugins/entries.ts +207 -0
  62. package/src/plugins/fonts.ts +581 -0
  63. package/src/plugins/mdx.ts +179 -0
  64. package/src/plugins/react-prod.ts +56 -0
  65. package/src/plugins/routing.ts +419 -0
  66. package/src/plugins/server-action-exports.ts +220 -0
  67. package/src/plugins/server-bundle.ts +113 -0
  68. package/src/plugins/shims.ts +168 -0
  69. package/src/plugins/static-build.ts +207 -0
  70. package/src/routing/codegen.ts +396 -0
  71. package/src/routing/index.ts +14 -0
  72. package/src/routing/interception.ts +173 -0
  73. package/src/routing/scanner.ts +487 -0
  74. package/src/routing/status-file-lint.ts +114 -0
  75. package/src/routing/types.ts +100 -0
  76. package/src/search-params/analyze.ts +192 -0
  77. package/src/search-params/codecs.ts +153 -0
  78. package/src/search-params/create.ts +314 -0
  79. package/src/search-params/index.ts +23 -0
  80. package/src/search-params/registry.ts +31 -0
  81. package/src/server/access-gate.tsx +142 -0
  82. package/src/server/action-client.ts +473 -0
  83. package/src/server/action-handler.ts +325 -0
  84. package/src/server/actions.ts +236 -0
  85. package/src/server/asset-headers.ts +81 -0
  86. package/src/server/body-limits.ts +102 -0
  87. package/src/server/build-manifest.ts +234 -0
  88. package/src/server/canonicalize.ts +90 -0
  89. package/src/server/client-module-map.ts +58 -0
  90. package/src/server/csrf.ts +79 -0
  91. package/src/server/deny-renderer.ts +302 -0
  92. package/src/server/dev-logger.ts +419 -0
  93. package/src/server/dev-span-processor.ts +78 -0
  94. package/src/server/dev-warnings.ts +282 -0
  95. package/src/server/early-hints-sender.ts +55 -0
  96. package/src/server/early-hints.ts +142 -0
  97. package/src/server/error-boundary-wrapper.ts +69 -0
  98. package/src/server/error-formatter.ts +184 -0
  99. package/src/server/flush.ts +182 -0
  100. package/src/server/form-data.ts +176 -0
  101. package/src/server/form-flash.ts +93 -0
  102. package/src/server/html-injectors.ts +445 -0
  103. package/src/server/index.ts +222 -0
  104. package/src/server/instrumentation.ts +136 -0
  105. package/src/server/logger.ts +145 -0
  106. package/src/server/manifest-status-resolver.ts +215 -0
  107. package/src/server/metadata-render.ts +527 -0
  108. package/src/server/metadata-routes.ts +189 -0
  109. package/src/server/metadata.ts +263 -0
  110. package/src/server/middleware-runner.ts +32 -0
  111. package/src/server/nuqs-ssr-provider.tsx +63 -0
  112. package/src/server/pipeline.ts +555 -0
  113. package/src/server/prerender.ts +139 -0
  114. package/src/server/primitives.ts +264 -0
  115. package/src/server/proxy.ts +43 -0
  116. package/src/server/request-context.ts +554 -0
  117. package/src/server/route-element-builder.ts +395 -0
  118. package/src/server/route-handler.ts +153 -0
  119. package/src/server/route-matcher.ts +316 -0
  120. package/src/server/rsc-entry/api-handler.ts +112 -0
  121. package/src/server/rsc-entry/error-renderer.ts +177 -0
  122. package/src/server/rsc-entry/helpers.ts +147 -0
  123. package/src/server/rsc-entry/index.ts +688 -0
  124. package/src/server/rsc-entry/ssr-bridge.ts +18 -0
  125. package/src/server/slot-resolver.ts +359 -0
  126. package/src/server/ssr-entry.ts +161 -0
  127. package/src/server/ssr-render.ts +200 -0
  128. package/src/server/status-code-resolver.ts +282 -0
  129. package/src/server/tracing.ts +281 -0
  130. package/src/server/tree-builder.ts +354 -0
  131. package/src/server/types.ts +150 -0
  132. package/src/shims/font-google.ts +67 -0
  133. package/src/shims/headers.ts +11 -0
  134. package/src/shims/image.ts +48 -0
  135. package/src/shims/link.ts +9 -0
  136. package/src/shims/navigation-client.ts +52 -0
  137. package/src/shims/navigation.ts +31 -0
  138. package/src/shims/server-only-noop.js +5 -0
  139. package/src/utils/directive-parser.ts +529 -0
  140. package/src/utils/format.ts +10 -0
  141. package/src/utils/startup-timer.ts +102 -0
package/src/index.ts ADDED
@@ -0,0 +1,413 @@
1
+ import type { Plugin, PluginOption } from 'vite';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import react from '@vitejs/plugin-react';
6
+ import { cacheTransformPlugin } from './plugins/cache-transform';
7
+ import { timberContent } from './plugins/content';
8
+ import { timberDevServer } from './plugins/dev-server';
9
+ import { timberEntries } from './plugins/entries';
10
+ import { timberMdx } from './plugins/mdx';
11
+ import { timberRouting } from './plugins/routing';
12
+ import { timberShims } from './plugins/shims';
13
+ import { timberFonts } from './plugins/fonts';
14
+ import { timberStaticBuild } from './plugins/static-build';
15
+ import { timberDynamicTransform } from './plugins/dynamic-transform';
16
+ import { timberServerActionExports } from './plugins/server-action-exports';
17
+ import { timberBuildManifest } from './plugins/build-manifest';
18
+ import { timberDevLogs } from './plugins/dev-logs';
19
+ import { timberReactProd } from './plugins/react-prod';
20
+ import { timberChunks, assignClientChunk } from './plugins/chunks';
21
+ import { timberServerBundle } from './plugins/server-bundle';
22
+ import { timberAdapterBuild } from './plugins/adapter-build';
23
+ import { timberBuildReport } from './plugins/build-report';
24
+ import type { RouteTree } from './routing/types';
25
+ import type { BuildManifest } from './server/build-manifest';
26
+ import type { StartupTimer } from './utils/startup-timer';
27
+ import { createStartupTimer, createNoopTimer } from './utils/startup-timer';
28
+
29
+ /** Configuration for client-side JavaScript output. */
30
+ export interface ClientJavascriptConfig {
31
+ /** When true, no client JS bundles are emitted or referenced in HTML. */
32
+ disabled: boolean;
33
+ /**
34
+ * When `disabled` is true, still inject the Vite HMR client in dev mode
35
+ * so hot reloading works during development. Default: true.
36
+ */
37
+ enableHMRInDev?: boolean;
38
+ }
39
+
40
+ /** Fully resolved client JavaScript configuration (no optionals). */
41
+ export interface ResolvedClientJavascript {
42
+ disabled: boolean;
43
+ enableHMRInDev: boolean;
44
+ }
45
+
46
+ export interface TimberUserConfig {
47
+ output?: 'server' | 'static';
48
+ /**
49
+ * Control client-side JavaScript output.
50
+ *
51
+ * Boolean shorthand:
52
+ * `clientJavascript: false` disables all client JS (equivalent to `{ disabled: true }`).
53
+ * `clientJavascript: true` enables client JS (the default).
54
+ *
55
+ * Object form:
56
+ * `clientJavascript: { disabled: true, enableHMRInDev: true }` disables client JS
57
+ * in production but preserves Vite HMR in dev mode.
58
+ *
59
+ * When `disabled` is true, `enableHMRInDev` defaults to `true`.
60
+ * Server-side JS still runs — this only affects what is sent to the browser.
61
+ */
62
+ clientJavascript?: boolean | ClientJavascriptConfig;
63
+ /**
64
+ * @deprecated Use `clientJavascript: false` or `clientJavascript: { disabled: true }` instead.
65
+ *
66
+ * Disable all client-side JavaScript output. When true, no client JS
67
+ * bundles are emitted or referenced in HTML. Pages work entirely via
68
+ * server-rendered HTML. Works in both 'server' and 'static' modes.
69
+ *
70
+ * Server-side JS still runs — this only affects what is sent to the browser.
71
+ */
72
+ noClientJavascript?: boolean;
73
+ adapter?: unknown;
74
+ cacheHandler?: unknown;
75
+ allowedOrigins?: string[];
76
+ csrf?: boolean;
77
+ limits?: {
78
+ actionBodySize?: string;
79
+ uploadBodySize?: string;
80
+ maxFields?: number;
81
+ };
82
+ pageExtensions?: string[];
83
+ /** Dev-mode options. These have no effect in production builds. */
84
+ dev?: {
85
+ /** Threshold in ms to highlight slow phases in dev logging output. Default: 200. */
86
+ slowPhaseMs?: number;
87
+ };
88
+ /**
89
+ * Cookie signing configuration. See design/29-cookies.md §"Signed Cookies".
90
+ *
91
+ * Provide `secret` for a single key, or `secrets` (array) for key rotation.
92
+ * When `secrets` is used, index 0 is the signing key; all are tried for verification.
93
+ */
94
+ cookies?: {
95
+ /** Single signing secret. Shorthand for `secrets: [secret]`. */
96
+ secret?: string;
97
+ /** Array of signing secrets for key rotation. Index 0 signs; all verify. */
98
+ secrets?: string[];
99
+ };
100
+ /**
101
+ * Override the app directory location. By default, timber auto-detects
102
+ * `app/` at the project root, falling back to `src/app/`.
103
+ *
104
+ * Set this to a relative path from the project root (e.g. `'src/app'`)
105
+ * to use a custom location.
106
+ */
107
+ appDir?: string;
108
+ /** MDX compilation options passed to @mdx-js/rollup. See design/20-content-collections.md. */
109
+ mdx?: {
110
+ remarkPlugins?: unknown[];
111
+ rehypePlugins?: unknown[];
112
+ recmaPlugins?: unknown[];
113
+ remarkRehypeOptions?: Record<string, unknown>;
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Resolve `clientJavascript` (new) and `noClientJavascript` (deprecated) into
119
+ * a fully resolved config. Emits a deprecation warning for the old option.
120
+ */
121
+ export function resolveClientJavascript(config: TimberUserConfig): ResolvedClientJavascript {
122
+ // New option takes precedence over deprecated option
123
+ if (config.clientJavascript !== undefined) {
124
+ if (typeof config.clientJavascript === 'boolean') {
125
+ // `clientJavascript: false` → disabled
126
+ // `clientJavascript: true` → enabled (default)
127
+ return {
128
+ disabled: !config.clientJavascript,
129
+ enableHMRInDev: !config.clientJavascript, // default true when disabled
130
+ };
131
+ }
132
+ // Object form
133
+ return {
134
+ disabled: config.clientJavascript.disabled,
135
+ enableHMRInDev: config.clientJavascript.enableHMRInDev ?? config.clientJavascript.disabled,
136
+ };
137
+ }
138
+
139
+ // Fall back to deprecated noClientJavascript
140
+ if (config.noClientJavascript !== undefined) {
141
+ console.warn(
142
+ '[timber] `noClientJavascript` is deprecated. ' +
143
+ 'Use `clientJavascript: false` or `clientJavascript: { disabled: true, enableHMRInDev: true }` instead.'
144
+ );
145
+ return {
146
+ disabled: config.noClientJavascript,
147
+ enableHMRInDev: config.noClientJavascript, // default true when disabled
148
+ };
149
+ }
150
+
151
+ // Default: client JS enabled
152
+ return { disabled: false, enableHMRInDev: false };
153
+ }
154
+
155
+ /**
156
+ * Shared context object passed to all sub-plugins via closure.
157
+ *
158
+ * Sub-plugins communicate through this context — not through Vite's
159
+ * plugin API or global state.
160
+ * See design/18-build-system.md §"Shared Plugin Context".
161
+ */
162
+ export interface PluginContext {
163
+ config: TimberUserConfig;
164
+ /** Resolved client JavaScript configuration */
165
+ clientJavascript: ResolvedClientJavascript;
166
+ /** The scanned route tree (populated by timber-routing, consumed by timber-entries) */
167
+ routeTree: RouteTree | null;
168
+ /** Absolute path to the app/ directory */
169
+ appDir: string;
170
+ /** Absolute path to the project root */
171
+ root: string;
172
+ /** Whether the dev server is running (set by timber-root-sync in configResolved) */
173
+ dev: boolean;
174
+ /** CSS build manifest (populated by adapter after client build, null in dev) */
175
+ buildManifest: BuildManifest | null;
176
+ /** Startup timer for profiling cold start phases (active in dev, no-op in prod) */
177
+ timer: StartupTimer;
178
+ }
179
+
180
+ /**
181
+ * Resolve the app directory. Checks (in order):
182
+ * 1. Explicit `configAppDir` from timber.config.ts
183
+ * 2. `<root>/app`
184
+ * 3. `<root>/src/app`
185
+ *
186
+ * Throws if none exist.
187
+ */
188
+ export function resolveAppDir(root: string, configAppDir?: string): string {
189
+ if (configAppDir) {
190
+ const explicit = join(root, configAppDir);
191
+ if (!existsSync(explicit)) {
192
+ throw new Error(
193
+ `[timber] Configured appDir "${configAppDir}" does not exist at ${explicit}`
194
+ );
195
+ }
196
+ return explicit;
197
+ }
198
+
199
+ const rootApp = join(root, 'app');
200
+ if (existsSync(rootApp)) return rootApp;
201
+
202
+ const srcApp = join(root, 'src', 'app');
203
+ if (existsSync(srcApp)) return srcApp;
204
+
205
+ throw new Error(
206
+ `[timber] Could not find app directory. Expected "app/" or "src/app/" in ${root}. ` +
207
+ `You can set appDir in timber.config.ts to specify a custom location.`
208
+ );
209
+ }
210
+
211
+ function createPluginContext(config?: TimberUserConfig, root?: string): PluginContext {
212
+ const projectRoot = root ?? process.cwd();
213
+ const resolvedConfig: TimberUserConfig = { output: 'server', ...config };
214
+ // Timer starts as active — swapped to noop in configResolved for production builds
215
+ return {
216
+ config: resolvedConfig,
217
+ clientJavascript: resolveClientJavascript(resolvedConfig),
218
+ routeTree: null,
219
+ appDir: join(projectRoot, 'app'),
220
+ root: projectRoot,
221
+ dev: false,
222
+ buildManifest: null,
223
+ timer: createStartupTimer(),
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Load timber.config.ts (or .js, .mjs) from the project root.
229
+ * Returns the config object or null if no config file is found.
230
+ */
231
+ async function loadTimberConfigFile(root: string): Promise<TimberUserConfig | null> {
232
+ const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];
233
+
234
+ for (const name of configNames) {
235
+ const configPath = join(root, name);
236
+ if (existsSync(configPath)) {
237
+ const mod = await import(pathToFileURL(configPath).href);
238
+ return (mod.default ?? mod) as TimberUserConfig;
239
+ }
240
+ }
241
+ return null;
242
+ }
243
+
244
+ /**
245
+ * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)
246
+ * and warn the user. The `output` key is excluded because it defaults to 'server'
247
+ * in createPluginContext and would always appear as an inline key.
248
+ *
249
+ * Returns the list of conflicting key names (for testing).
250
+ */
251
+ export function warnConfigConflicts(
252
+ inline: TimberUserConfig,
253
+ fileConfig: TimberUserConfig
254
+ ): string[] {
255
+ const conflicts: string[] = [];
256
+ for (const key of Object.keys(fileConfig) as (keyof TimberUserConfig)[]) {
257
+ if (key === 'output') continue;
258
+ if (key in inline && inline[key] !== undefined) {
259
+ conflicts.push(key);
260
+ }
261
+ }
262
+ if (conflicts.length > 0) {
263
+ console.warn(
264
+ `[timber] Config conflict: ${conflicts.map((k) => `"${k}"`).join(', ')} set in both ` +
265
+ `vite.config.ts (inline) and timber.config.ts. ` +
266
+ `Move all config to timber.config.ts to avoid confusion. ` +
267
+ `The inline value from vite.config.ts will be used.`
268
+ );
269
+ }
270
+ return conflicts;
271
+ }
272
+
273
+ /**
274
+ * Merge file-based config into ctx.config. Inline config (already in ctx.config)
275
+ * takes precedence — file config only fills in missing fields.
276
+ */
277
+ function mergeFileConfig(ctx: PluginContext, fileConfig: TimberUserConfig): void {
278
+ const inline = ctx.config;
279
+
280
+ // Warn if the same key is set in both places
281
+ warnConfigConflicts(inline, fileConfig);
282
+
283
+ // For each top-level key, use inline value if present, otherwise file value
284
+ ctx.config = {
285
+ ...fileConfig,
286
+ ...inline,
287
+ // Deep merge for nested objects where both exist
288
+ ...(fileConfig.limits && inline.limits
289
+ ? { limits: { ...fileConfig.limits, ...inline.limits } }
290
+ : {}),
291
+ ...(fileConfig.dev && inline.dev ? { dev: { ...fileConfig.dev, ...inline.dev } } : {}),
292
+ ...(fileConfig.mdx && inline.mdx ? { mdx: { ...fileConfig.mdx, ...inline.mdx } } : {}),
293
+ };
294
+ }
295
+
296
+ function timberCache(_ctx: PluginContext): Plugin {
297
+ return cacheTransformPlugin();
298
+ }
299
+
300
+ export function timber(config?: TimberUserConfig): PluginOption[] {
301
+ const ctx = createPluginContext(config);
302
+ // Sync ctx.root and ctx.appDir with Vite's resolved root, which may
303
+ // differ from process.cwd() when --config points to a subdirectory.
304
+ // Also loads timber.config.ts and merges it into ctx.config (inline config wins).
305
+ const rootSync: Plugin = {
306
+ name: 'timber-root-sync',
307
+ async config(userConfig) {
308
+ // Load timber.config.ts early — before configResolved/buildStart — so
309
+ // all plugins (including timber-mdx) see the merged config in their
310
+ // buildStart hooks. The config hook runs once and supports async.
311
+ const root = userConfig.root ?? process.cwd();
312
+ ctx.timer.start('config-load');
313
+ const fileConfig = await loadTimberConfigFile(root);
314
+ if (fileConfig) {
315
+ mergeFileConfig(ctx, fileConfig);
316
+ ctx.clientJavascript = resolveClientJavascript(ctx.config);
317
+ }
318
+ ctx.timer.end('config-load');
319
+ },
320
+ configResolved(resolved) {
321
+ ctx.root = resolved.root;
322
+ ctx.appDir = resolveAppDir(resolved.root, ctx.config.appDir);
323
+ ctx.dev = resolved.command === 'serve';
324
+ // In production builds, swap to a no-op timer to avoid overhead
325
+ if (!ctx.dev) {
326
+ ctx.timer = createNoopTimer();
327
+ } else {
328
+ // Start the overall dev server setup timer — ends in timber-dev-server
329
+ ctx.timer.start('dev-server-setup');
330
+ }
331
+ },
332
+ };
333
+ // @vitejs/plugin-rsc handles:
334
+ // - RSC/SSR/client environment setup
335
+ // - "use client" directive → client reference proxy transformation
336
+ // - "use server" directive → server reference transformation
337
+ // - Client reference tracking and module map generation
338
+ //
339
+ // Loaded via dynamic import() because @vitejs/plugin-rsc is ESM-only.
340
+ // Vite's config loader uses esbuild to transpile to CJS, which breaks
341
+ // static imports of ESM-only packages. The dynamic import() is preserved
342
+ // by esbuild and runs natively in ESM at runtime.
343
+ //
344
+ // serverHandler: false — timber has its own dev server (timber-dev-server)
345
+ // entries — tells the RSC plugin about timber's virtual entry modules so
346
+ // it correctly wires up the browser entry (needed for React Fast Refresh
347
+ // preamble coordination with @vitejs/plugin-react)
348
+ // customClientEntry: true — timber manages its own browser entry and
349
+ // preloading; skips RSC plugin's default "index" client entry convention
350
+ //
351
+ // The RSC plugin's built-in buildApp handles the 5-step multi-environment
352
+ // build sequence (analyze references → build RSC → build client → build SSR).
353
+ // We do NOT set customBuildApp — the RSC plugin's orchestration is correct
354
+ // and handles bundle ordering, asset manifest generation, and environment
355
+ // imports manifest. See @vitejs/plugin-rsc's buildApp implementation.
356
+ ctx.timer.start('rsc-plugin-import');
357
+ const rscPluginsPromise = import('@vitejs/plugin-rsc').then(({ default: vitePluginRsc }) => {
358
+ ctx.timer.end('rsc-plugin-import');
359
+ return vitePluginRsc({
360
+ serverHandler: false,
361
+ customClientEntry: true,
362
+ entries: {
363
+ rsc: 'virtual:timber-rsc-entry',
364
+ ssr: 'virtual:timber-ssr-entry',
365
+ client: 'virtual:timber-browser-entry',
366
+ },
367
+ clientChunks: assignClientChunk,
368
+ });
369
+ });
370
+
371
+ return [
372
+ rootSync,
373
+ timberReactProd(),
374
+ // @vitejs/plugin-react provides React Fast Refresh (state-preserving HMR)
375
+ // for client components via Babel transform. Placed before @vitejs/plugin-rsc
376
+ // following Vinext's convention — the RSC plugin's virtual browser entry
377
+ // coordinates with plugin-react via __vite_plugin_react_preamble_installed__.
378
+ react(),
379
+ timberServerActionExports(),
380
+ rscPluginsPromise,
381
+ timberShims(ctx),
382
+ timberRouting(ctx),
383
+ timberEntries(ctx),
384
+ timberBuildManifest(ctx),
385
+ timberCache(ctx),
386
+ timberStaticBuild(ctx),
387
+ timberDynamicTransform(ctx),
388
+ timberFonts(ctx),
389
+ timberMdx(ctx),
390
+ timberContent(ctx),
391
+ timberServerBundle(), // Bundle all deps in server environments for prod
392
+ timberChunks(),
393
+ timberBuildReport(ctx), // Post-build: route table with bundle sizes
394
+ timberAdapterBuild(ctx), // Post-build: invoke adapter.buildOutput()
395
+ timberDevLogs(ctx), // Dev-only: forward server console.* to browser console
396
+ timberDevServer(ctx), // Must be last — configureServer post-hook runs after all watchers
397
+ ];
398
+ }
399
+
400
+ /**
401
+ * Route map interface — augmented by the generated timber-routes.d.ts.
402
+ *
403
+ * Each key is a route path pattern. Values have:
404
+ * params: shape of URL params (e.g. { id: string })
405
+ * searchParams: parsed type from search-params.ts, or {} if none
406
+ *
407
+ * This interface is empty by default and populated via codegen.
408
+ * See design/09-typescript.md §"Typed Routes".
409
+ */
410
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
411
+ export interface Routes {}
412
+
413
+ export default timber;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * timber-adapter-build — Invoke the adapter's buildOutput after vite build.
3
+ *
4
+ * After all environments are built and the RSC plugin has written its
5
+ * asset manifests, calls `adapter.buildOutput()` to transform the output
6
+ * into a deployable artifact (e.g., Cloudflare Workers entry + wrangler.jsonc).
7
+ *
8
+ * Uses a `buildApp` hook with `order: 'post'` so that Vite calls the
9
+ * RSC plugin's buildApp (which orchestrates all environment builds and
10
+ * writes asset manifests) first, then runs this handler after everything
11
+ * is complete.
12
+ *
13
+ * Design docs: design/11-platform.md, design/25-production-deployments.md
14
+ */
15
+
16
+ import type { Plugin } from 'vite';
17
+ import { join } from 'node:path';
18
+ import { readFile, writeFile } from 'node:fs/promises';
19
+ import type { PluginContext } from '#/index.js';
20
+ import type { TimberPlatformAdapter, TimberConfig } from '#/adapters/types.js';
21
+
22
+ export function timberAdapterBuild(ctx: PluginContext): Plugin {
23
+ return {
24
+ name: 'timber-adapter-build',
25
+
26
+ // order: 'post' causes Vite to run configBuilder.buildApp() (which
27
+ // includes the RSC plugin's buildApp) before calling this handler.
28
+ // By the time we run, all environments are built and asset manifests
29
+ // are written — safe to copy the output.
30
+ buildApp: {
31
+ order: 'post' as const,
32
+ async handler() {
33
+ if (ctx.dev) return;
34
+
35
+ const adapter = ctx.config.adapter as TimberPlatformAdapter | undefined;
36
+ if (!adapter || typeof adapter.buildOutput !== 'function') return;
37
+
38
+ const buildDir = join(ctx.root, 'dist');
39
+
40
+ // Serialize the build manifest as a JS module that sets the global.
41
+ // The adapter writes this as _timber-manifest-init.mjs and imports it
42
+ // before the RSC handler, so globalThis.__TIMBER_BUILD_MANIFEST__ is
43
+ // available when virtual:timber-build-manifest evaluates.
44
+ let manifestInit: string | undefined;
45
+ if (ctx.buildManifest) {
46
+ // Strip JS/modulepreload from manifest when client JS is disabled —
47
+ // those files aren't served, so hints for them are useless.
48
+ const manifest = ctx.clientJavascript.disabled
49
+ ? { ...ctx.buildManifest, js: {}, modulepreload: {} }
50
+ : ctx.buildManifest;
51
+ const json = JSON.stringify(manifest);
52
+ manifestInit = `globalThis.__TIMBER_BUILD_MANIFEST__ = ${json};\n`;
53
+ }
54
+
55
+ // Strip JS from the RSC plugin's assets manifest when client JS
56
+ // is disabled. The RSC plugin writes __vite_rsc_assets_manifest.js
57
+ // with clientReferenceDeps containing JS URLs — used to inject
58
+ // <link rel="modulepreload"> tags. Must happen before the adapter
59
+ // copies files to the output directory.
60
+ if (ctx.clientJavascript.disabled) {
61
+ await stripJsFromRscAssetsManifests(buildDir);
62
+ }
63
+
64
+ const adapterConfig: TimberConfig = {
65
+ output: ctx.config.output ?? 'server',
66
+ clientJavascriptDisabled: ctx.clientJavascript.disabled,
67
+ manifestInit,
68
+ };
69
+
70
+ await adapter.buildOutput(adapterConfig, buildDir);
71
+ },
72
+ },
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Strip JS references from the RSC plugin's assets manifest files.
78
+ *
79
+ * The RSC plugin writes `__vite_rsc_assets_manifest.js` to rsc/ and ssr/
80
+ * as standalone files (not Rollup chunks), so generateBundle can't
81
+ * intercept them. This rewrites the files on disk after all builds
82
+ * complete but before the adapter copies them to the output directory.
83
+ */
84
+ async function stripJsFromRscAssetsManifests(buildDir: string): Promise<void> {
85
+ const manifestName = '__vite_rsc_assets_manifest.js';
86
+ const paths = [join(buildDir, 'rsc', manifestName), join(buildDir, 'ssr', manifestName)];
87
+
88
+ for (const path of paths) {
89
+ let content: string;
90
+ try {
91
+ content = await readFile(path, 'utf-8');
92
+ } catch {
93
+ continue;
94
+ }
95
+
96
+ const jsonStr = content.replace(/^export default\s*/, '').replace(/;?\s*$/, '');
97
+ let manifest: Record<string, unknown>;
98
+ try {
99
+ manifest = JSON.parse(jsonStr);
100
+ } catch {
101
+ continue;
102
+ }
103
+
104
+ // Clear JS from clientReferenceDeps — preserves CSS
105
+ const deps = manifest.clientReferenceDeps as
106
+ | Record<string, { js: string[]; css: string[] }>
107
+ | undefined;
108
+ if (deps) {
109
+ for (const entry of Object.values(deps)) {
110
+ entry.js = [];
111
+ }
112
+ }
113
+
114
+ manifest.bootstrapScriptContent = '';
115
+
116
+ await writeFile(path, `export default ${JSON.stringify(manifest, null, 2)};\n`);
117
+ }
118
+ }