@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.
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/adapters/cloudflare.ts +325 -0
- package/src/adapters/nitro.ts +366 -0
- package/src/adapters/types.ts +63 -0
- package/src/cache/index.ts +91 -0
- package/src/cache/redis-handler.ts +91 -0
- package/src/cache/register-cached-function.ts +99 -0
- package/src/cache/singleflight.ts +26 -0
- package/src/cache/stable-stringify.ts +21 -0
- package/src/cache/timber-cache.ts +116 -0
- package/src/cli.ts +201 -0
- package/src/client/browser-entry.ts +663 -0
- package/src/client/error-boundary.tsx +209 -0
- package/src/client/form.tsx +200 -0
- package/src/client/head.ts +61 -0
- package/src/client/history.ts +46 -0
- package/src/client/index.ts +60 -0
- package/src/client/link-navigate-interceptor.tsx +62 -0
- package/src/client/link-status-provider.tsx +40 -0
- package/src/client/link.tsx +310 -0
- package/src/client/nuqs-adapter.tsx +117 -0
- package/src/client/router-ref.ts +25 -0
- package/src/client/router.ts +563 -0
- package/src/client/segment-cache.ts +194 -0
- package/src/client/segment-context.ts +57 -0
- package/src/client/ssr-data.ts +95 -0
- package/src/client/types.ts +4 -0
- package/src/client/unload-guard.ts +34 -0
- package/src/client/use-cookie.ts +122 -0
- package/src/client/use-link-status.ts +46 -0
- package/src/client/use-navigation-pending.ts +47 -0
- package/src/client/use-params.ts +71 -0
- package/src/client/use-pathname.ts +43 -0
- package/src/client/use-query-states.ts +133 -0
- package/src/client/use-router.ts +77 -0
- package/src/client/use-search-params.ts +74 -0
- package/src/client/use-selected-layout-segment.ts +110 -0
- package/src/content/index.ts +13 -0
- package/src/cookies/define-cookie.ts +137 -0
- package/src/cookies/index.ts +9 -0
- package/src/fonts/ast.ts +359 -0
- package/src/fonts/css.ts +68 -0
- package/src/fonts/fallbacks.ts +248 -0
- package/src/fonts/google.ts +332 -0
- package/src/fonts/local.ts +177 -0
- package/src/fonts/types.ts +88 -0
- package/src/index.ts +413 -0
- package/src/plugins/adapter-build.ts +118 -0
- package/src/plugins/build-manifest.ts +323 -0
- package/src/plugins/build-report.ts +353 -0
- package/src/plugins/cache-transform.ts +199 -0
- package/src/plugins/chunks.ts +90 -0
- package/src/plugins/content.ts +136 -0
- package/src/plugins/dev-error-overlay.ts +230 -0
- package/src/plugins/dev-logs.ts +280 -0
- package/src/plugins/dev-server.ts +389 -0
- package/src/plugins/dynamic-transform.ts +161 -0
- package/src/plugins/entries.ts +207 -0
- package/src/plugins/fonts.ts +581 -0
- package/src/plugins/mdx.ts +179 -0
- package/src/plugins/react-prod.ts +56 -0
- package/src/plugins/routing.ts +419 -0
- package/src/plugins/server-action-exports.ts +220 -0
- package/src/plugins/server-bundle.ts +113 -0
- package/src/plugins/shims.ts +168 -0
- package/src/plugins/static-build.ts +207 -0
- package/src/routing/codegen.ts +396 -0
- package/src/routing/index.ts +14 -0
- package/src/routing/interception.ts +173 -0
- package/src/routing/scanner.ts +487 -0
- package/src/routing/status-file-lint.ts +114 -0
- package/src/routing/types.ts +100 -0
- package/src/search-params/analyze.ts +192 -0
- package/src/search-params/codecs.ts +153 -0
- package/src/search-params/create.ts +314 -0
- package/src/search-params/index.ts +23 -0
- package/src/search-params/registry.ts +31 -0
- package/src/server/access-gate.tsx +142 -0
- package/src/server/action-client.ts +473 -0
- package/src/server/action-handler.ts +325 -0
- package/src/server/actions.ts +236 -0
- package/src/server/asset-headers.ts +81 -0
- package/src/server/body-limits.ts +102 -0
- package/src/server/build-manifest.ts +234 -0
- package/src/server/canonicalize.ts +90 -0
- package/src/server/client-module-map.ts +58 -0
- package/src/server/csrf.ts +79 -0
- package/src/server/deny-renderer.ts +302 -0
- package/src/server/dev-logger.ts +419 -0
- package/src/server/dev-span-processor.ts +78 -0
- package/src/server/dev-warnings.ts +282 -0
- package/src/server/early-hints-sender.ts +55 -0
- package/src/server/early-hints.ts +142 -0
- package/src/server/error-boundary-wrapper.ts +69 -0
- package/src/server/error-formatter.ts +184 -0
- package/src/server/flush.ts +182 -0
- package/src/server/form-data.ts +176 -0
- package/src/server/form-flash.ts +93 -0
- package/src/server/html-injectors.ts +445 -0
- package/src/server/index.ts +222 -0
- package/src/server/instrumentation.ts +136 -0
- package/src/server/logger.ts +145 -0
- package/src/server/manifest-status-resolver.ts +215 -0
- package/src/server/metadata-render.ts +527 -0
- package/src/server/metadata-routes.ts +189 -0
- package/src/server/metadata.ts +263 -0
- package/src/server/middleware-runner.ts +32 -0
- package/src/server/nuqs-ssr-provider.tsx +63 -0
- package/src/server/pipeline.ts +555 -0
- package/src/server/prerender.ts +139 -0
- package/src/server/primitives.ts +264 -0
- package/src/server/proxy.ts +43 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/route-element-builder.ts +395 -0
- package/src/server/route-handler.ts +153 -0
- package/src/server/route-matcher.ts +316 -0
- package/src/server/rsc-entry/api-handler.ts +112 -0
- package/src/server/rsc-entry/error-renderer.ts +177 -0
- package/src/server/rsc-entry/helpers.ts +147 -0
- package/src/server/rsc-entry/index.ts +688 -0
- package/src/server/rsc-entry/ssr-bridge.ts +18 -0
- package/src/server/slot-resolver.ts +359 -0
- package/src/server/ssr-entry.ts +161 -0
- package/src/server/ssr-render.ts +200 -0
- package/src/server/status-code-resolver.ts +282 -0
- package/src/server/tracing.ts +281 -0
- package/src/server/tree-builder.ts +354 -0
- package/src/server/types.ts +150 -0
- package/src/shims/font-google.ts +67 -0
- package/src/shims/headers.ts +11 -0
- package/src/shims/image.ts +48 -0
- package/src/shims/link.ts +9 -0
- package/src/shims/navigation-client.ts +52 -0
- package/src/shims/navigation.ts +31 -0
- package/src/shims/server-only-noop.js +5 -0
- package/src/utils/directive-parser.ts +529 -0
- package/src/utils/format.ts +10 -0
- 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
|
+
}
|