@timber-js/app 0.2.0-alpha.154 → 0.2.0-alpha.156

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 (70) hide show
  1. package/agent-skill.md +344 -0
  2. package/dist/_chunks/{build-output-helper-B20ZuTSA.js → build-output-helper-Rp9r7gSA.js} +62 -9
  3. package/dist/_chunks/build-output-helper-Rp9r7gSA.js.map +1 -0
  4. package/dist/adapters/build-output-helper.d.ts +1 -0
  5. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  6. package/dist/adapters/cloudflare.d.ts.map +1 -1
  7. package/dist/adapters/cloudflare.js +35 -5
  8. package/dist/adapters/cloudflare.js.map +1 -1
  9. package/dist/adapters/nitro.js +1 -1
  10. package/dist/adapters/shared.d.ts +1 -11
  11. package/dist/adapters/shared.d.ts.map +1 -1
  12. package/dist/cli.d.ts +9 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +41 -4
  15. package/dist/cli.js.map +1 -1
  16. package/dist/fonts/google.d.ts.map +1 -1
  17. package/dist/index.js +43 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugins/fonts.d.ts.map +1 -1
  20. package/dist/server/asset-headers.d.ts +7 -3
  21. package/dist/server/asset-headers.d.ts.map +1 -1
  22. package/dist/server/internal.js +12 -6
  23. package/dist/server/internal.js.map +1 -1
  24. package/dist/server/pipeline-phases.d.ts.map +1 -1
  25. package/docs/api/30-api-server.mdx +233 -0
  26. package/docs/api/31-api-client.mdx +161 -0
  27. package/docs/api/32-api-cache.mdx +193 -0
  28. package/docs/api/33-api-search-params.mdx +141 -0
  29. package/docs/api/34-api-config.mdx +189 -0
  30. package/docs/api/35-api-typescript.mdx +170 -0
  31. package/docs/api/36-cli.mdx +143 -0
  32. package/docs/learn/00-introduction.mdx +81 -0
  33. package/docs/learn/01-quick-start.mdx +94 -0
  34. package/docs/learn/02-pages-and-layouts.mdx +138 -0
  35. package/docs/learn/03-fetching-data.mdx +211 -0
  36. package/docs/learn/03b-access-control.mdx +130 -0
  37. package/docs/learn/04-loading-states.mdx +67 -0
  38. package/docs/learn/04b-the-flush-point.mdx +115 -0
  39. package/docs/learn/05-typed-params.mdx +275 -0
  40. package/docs/learn/06-forms-and-actions.mdx +150 -0
  41. package/docs/learn/08-streaming.mdx +146 -0
  42. package/docs/learn/09-caching.mdx +121 -0
  43. package/docs/learn/10-middleware.mdx +156 -0
  44. package/docs/learn/11-error-handling.mdx +147 -0
  45. package/docs/learn/12-client-navigation.mdx +168 -0
  46. package/docs/learn/13-configuration.mdx +117 -0
  47. package/docs/learn/14-deploying.mdx +206 -0
  48. package/docs/more/01-advanced-routing.mdx +139 -0
  49. package/docs/more/02-advanced-forms.mdx +137 -0
  50. package/docs/more/03-coming-from-nextjs.mdx +186 -0
  51. package/docs/more/04-metadata-and-fonts.mdx +193 -0
  52. package/docs/more/04b-mdx.mdx +209 -0
  53. package/docs/more/05-content-collections.mdx +90 -0
  54. package/docs/more/06-instrumentation.mdx +188 -0
  55. package/docs/more/07-security.mdx +123 -0
  56. package/docs/more/40-why-timber.mdx +50 -0
  57. package/docs/more/41-timber-vs-nextjs.mdx +81 -0
  58. package/docs/more/42-timber-vs-others.mdx +68 -0
  59. package/docs/more/50-ai-agent-instructions.mdx +169 -0
  60. package/llms.txt +55 -0
  61. package/package.json +8 -4
  62. package/src/adapters/build-output-helper.ts +52 -2
  63. package/src/adapters/cloudflare.ts +34 -4
  64. package/src/adapters/shared.ts +34 -6
  65. package/src/cli.ts +62 -3
  66. package/src/fonts/google.ts +56 -5
  67. package/src/plugins/fonts.ts +6 -0
  68. package/src/server/asset-headers.ts +7 -3
  69. package/src/server/pipeline-phases.ts +10 -2
  70. package/dist/_chunks/build-output-helper-B20ZuTSA.js.map +0 -1
package/agent-skill.md ADDED
@@ -0,0 +1,344 @@
1
+ # timber.js — AI Agent Skill
2
+
3
+ Use this skill when building, modifying, or debugging a timber.js application. timber.js is a Vite-native React framework with file-system routing, React Server Components, and real HTTP status codes.
4
+
5
+ timber.js is inspired by, but is NOT Next.js. Do not assume Next.js features, APIs, or conventions work in timber. The routing structure is similar, but the APIs, rendering model, and conventions diverge significantly. Always consult timber docs, not Next.js docs.
6
+
7
+ Full documentation is available in `node_modules/@timber-js/app/docs/` (MDX files). Read the relevant doc before implementing a feature. This skill covers the essential patterns — the docs go deeper.
8
+
9
+ ## Project structure
10
+
11
+ ```
12
+ app/
13
+ layout.tsx # Root layout (required) — wraps all pages
14
+ page.tsx # Home page (/)
15
+ globals.css # Global styles
16
+ about/
17
+ page.tsx # /about
18
+ blog/
19
+ layout.tsx # Blog layout — wraps blog pages
20
+ page.tsx # /blog
21
+ [slug]/
22
+ page.tsx # /blog/:slug
23
+ timber.config.ts # Framework config (output mode, adapter, cache)
24
+ vite.config.ts # Vite config — imports timber()
25
+ tsconfig.json
26
+ ```
27
+
28
+ ## Segment convention files
29
+
30
+ Each directory in `app/` is a **segment**. These files have special meaning:
31
+
32
+ | File | Purpose |
33
+ | --------------- | ------------------------------------------------------------- |
34
+ | `page.tsx` | Route leaf — renders at this URL |
35
+ | `layout.tsx` | Persistent shell — wraps children, survives client navigation |
36
+ | `access.ts` | Auth gate — runs inside React tree, supports slot degradation |
37
+ | `middleware.ts` | Pre-render hook — headers, redirects, cache warming |
38
+ | `schema.ts` | Segment & search param codecs |
39
+ | `error.tsx` | Error boundary for this subtree |
40
+ | `404.tsx` | Custom 404 page (must be a client component) |
41
+
42
+ There is NO `loading.tsx`. Use `<Suspense>` with the flush point model instead.
43
+
44
+ ## Key imports
45
+
46
+ ```ts
47
+ // Server (server components, middleware, access, actions)
48
+ import { deny, redirect, getHeaders, getCookieJar, getSegmentParams,
49
+ createActionClient, ActionError, revalidatePath, revalidateTag,
50
+ waitUntil, getTraceId, withSpan } from '@timber-js/app/server';
51
+
52
+ // Client (client components)
53
+ import { Link, useRouter, usePathname, useActionState,
54
+ useSegmentParams, usePendingNavigation, useLinkStatus,
55
+ useSelectedLayoutSegment } from '@timber-js/app/client';
56
+
57
+ // Typed params
58
+ import { defineSchema } from '@timber-js/app/params';
59
+ import { codec } from '@timber-js/app/codec';
60
+ import { defineSearchParams } from '@timber-js/app/search-params';
61
+ import { defineCookie } from '@timber-js/app/cookies';
62
+
63
+ // Caching
64
+ import { cache } from '@timber-js/app/cache';
65
+
66
+ // Generated segment module (typed params, segment path)
67
+ import { SEGMENT_PATH } from './$segment';
68
+ ```
69
+
70
+ ## Pages and layouts
71
+
72
+ Pages are server components by default — async, server-only, zero client JS:
73
+
74
+ ```tsx
75
+ // app/products/[id]/page.tsx
76
+ import { SEGMENT_PATH } from './$segment';
77
+ import { deny, getSegmentParams } from '@timber-js/app/server';
78
+
79
+ export default async function ProductPage() {
80
+ const { id } = getSegmentParams(SEGMENT_PATH);
81
+ const product = await db.products.find(id);
82
+ if (!product) return deny(404); // Real HTTP 404
83
+ return <h1>{product.name}</h1>;
84
+ }
85
+ ```
86
+
87
+ Layouts receive `{ children }` and persist across navigation:
88
+
89
+ ```tsx
90
+ // app/layout.tsx — root layout (required)
91
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
92
+ return (
93
+ <html lang="en">
94
+ <body>{children}</body>
95
+ </html>
96
+ );
97
+ }
98
+ ```
99
+
100
+ ## Server and client components
101
+
102
+ **Server components** (default): async, access secrets/DB, zero client JS cost.
103
+ **Client components** (`'use client'`): `useState`, `useEffect`, event handlers, browser APIs.
104
+
105
+ ```tsx
106
+ // app/counter.tsx
107
+ 'use client';
108
+ import { useState } from 'react';
109
+ export function Counter() {
110
+ const [count, setCount] = useState(0);
111
+ return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
112
+ }
113
+ ```
114
+
115
+ Server components can render client components as children. Client components cannot import server components (pass them as `children` or props instead).
116
+
117
+ ## deny() and redirect()
118
+
119
+ `deny()` produces real HTTP status codes. `redirect()` sends HTTP redirects.
120
+
121
+ ```ts
122
+ deny(); // 403 (default)
123
+ deny(404); // 404 Not Found
124
+ deny(503, data); // 503 with data for error boundary
125
+
126
+ redirect('/login'); // 307 temporary
127
+ redirect('/new-page', { permanent: true }); // 308 permanent
128
+ ```
129
+
130
+ ## Forms and server actions
131
+
132
+ Actions use `createActionClient` for reusable middleware:
133
+
134
+ ```ts
135
+ // lib/action.ts
136
+ 'use server';
137
+ import { createActionClient, ActionError } from '@timber-js/app/server';
138
+
139
+ export const actionClient = createActionClient({
140
+ middleware: async () => {
141
+ const user = await getUser();
142
+ if (!user) throw new ActionError('UNAUTHORIZED');
143
+ return { user };
144
+ },
145
+ });
146
+ ```
147
+
148
+ ```ts
149
+ // app/todos/actions.ts
150
+ 'use server';
151
+ import { z } from 'zod/v4';
152
+ import { action } from '@/lib/action';
153
+ import { revalidatePath } from '@timber-js/app/server';
154
+
155
+ export const createTodo = actionClient
156
+ .schema(z.object({ title: z.string().min(1) }))
157
+ .action(async ({ input, ctx }) => {
158
+ await db.todos.create({ ...input, userId: ctx.user.id });
159
+ return revalidatePath('/todos');
160
+ });
161
+ ```
162
+
163
+ ```tsx
164
+ // app/todos/todo-form.tsx
165
+ 'use client';
166
+ import { useActionState } from '@timber-js/app/client';
167
+ import { createTodo } from './actions';
168
+
169
+ export function TodoForm() {
170
+ const [state, formAction, pending, errors] = useActionState(createTodo, null);
171
+ return (
172
+ <form action={formAction}>
173
+ <input name="title" required />
174
+ {errors.getFieldError('title') && <p>{errors.getFieldError('title')}</p>}
175
+ <button disabled={pending}>{pending ? 'Adding...' : 'Add'}</button>
176
+ </form>
177
+ );
178
+ }
179
+ ```
180
+
181
+ Forms work without JavaScript — they submit as standard POST. With JS, `useActionState` enhances with pending state.
182
+
183
+ ## Access control (access.ts)
184
+
185
+ ```ts
186
+ // app/(authenticated)/access.ts
187
+ import { getCookieJar, redirect } from '@timber-js/app/server';
188
+
189
+ export default async function access() {
190
+ const session = getSession(getCookieJar());
191
+ if (!session) return redirect('/login');
192
+ }
193
+ ```
194
+
195
+ Access runs inside the React tree. It shares `React.cache` scope with layouts. For parallel routes (slots), denied slots degrade gracefully while the rest of the page renders.
196
+
197
+ ## Middleware
198
+
199
+ Two layers: `proxy.ts` (global, has `next()`) and `middleware.ts` (per-segment, no `next()`).
200
+
201
+ ```ts
202
+ // app/dashboard/middleware.ts
203
+ import { redirect } from '@timber-js/app/server';
204
+ import type { MiddlewareContext } from '@timber-js/app/server';
205
+
206
+ export default async function middleware(ctx: MiddlewareContext) {
207
+ const session = await getSession(ctx.req);
208
+ if (!session) redirect('/login');
209
+ ctx.headers.set('x-custom', 'value'); // Set response headers
210
+ }
211
+ ```
212
+
213
+ Use middleware for headers, redirects, cache warming. Use `access.ts` for auth.
214
+
215
+ ## Typed params
216
+
217
+ Define codecs in `app/schema.ts`:
218
+
219
+ ```ts
220
+ // app/schema.ts
221
+ import { defineSchema } from '@timber-js/app/params';
222
+ import { codec } from '@timber-js/app/codec';
223
+
224
+ export default defineSchema({
225
+ segmentParams: {
226
+ '[id]': codec.integer,
227
+ '[slug]': codec.string,
228
+ },
229
+ });
230
+ ```
231
+
232
+ ## Search params
233
+
234
+ ```ts
235
+ // app/products/search-params.ts
236
+ import { defineSearchParams } from '@timber-js/app/search-params';
237
+ import { codec } from '@timber-js/app/codec';
238
+
239
+ export const { searchParams } = defineSearchParams({
240
+ q: codec.string.optional,
241
+ page: codec.integer.default(1),
242
+ });
243
+ ```
244
+
245
+ Server: `searchParams.get()`. Client: `searchParams.useQueryStates()`.
246
+
247
+ ## Caching
248
+
249
+ No implicit caching. `fetch()` is never patched. Use `timber.cache()` for cross-request caching:
250
+
251
+ ```ts
252
+ import { cache } from '@timber-js/app/cache';
253
+
254
+ const getProducts = cache(
255
+ async () => db.products.findMany(),
256
+ { ttl: 60, tags: ['products'] }
257
+ );
258
+ ```
259
+
260
+ Use `React.cache` for single-request deduplication.
261
+
262
+ ## The flush point
263
+
264
+ timber holds the HTTP response until the shell (everything outside `<Suspense>`) resolves. This means:
265
+
266
+ - Data fetched outside `<Suspense>` can affect the status code (`deny(404)` works)
267
+ - Data inside `<Suspense>` streams after the shell flushes
268
+ - There is no `loading.tsx` — use `<Suspense>` with a fallback explicitly
269
+
270
+ ## Configuration
271
+
272
+ ```ts
273
+ // timber.config.ts
274
+ import { cloudflare } from '@timber-js/app/adapters/cloudflare';
275
+
276
+ export default {
277
+ output: 'server', // or 'static'
278
+ adapter: cloudflare(), // or nitro({ preset: 'node-server' })
279
+ };
280
+ ```
281
+
282
+ ```ts
283
+ // vite.config.ts
284
+ import { defineConfig } from 'vite';
285
+ import { timber } from '@timber-js/app';
286
+
287
+ export default defineConfig({
288
+ plugins: [timber()],
289
+ });
290
+ ```
291
+
292
+ ## Client navigation
293
+
294
+ ```tsx
295
+ import { Link } from '@timber-js/app/client';
296
+
297
+ <Link href="/about">About</Link>
298
+ <Link href="/products/123" prefetch>Product</Link>
299
+ ```
300
+
301
+ ```tsx
302
+ const router = useRouter();
303
+ router.push('/dashboard');
304
+ router.replace('/settings');
305
+ router.refresh();
306
+ ```
307
+
308
+ ## Route patterns
309
+
310
+ - `(group)/` — route group (no URL segment)
311
+ - `_private/` — excluded from routing
312
+ - `[param]/` — dynamic segment
313
+ - `[...catchAll]/` — catch-all segment
314
+ - `@slot/` — parallel route (named slot)
315
+ - `(.)path/` — intercepting route
316
+
317
+ ## Common mistakes to avoid
318
+
319
+ 1. **No `loading.tsx`** — timber doesn't have it. Use `<Suspense>` explicitly.
320
+ 2. **Don't patch fetch** — `fetch()` is never cached. Use `timber.cache()` or `React.cache`.
321
+ 3. **`deny()` not `throw new Error`** — use `deny(404)` for proper status codes, not error throwing.
322
+ 4. **Server actions need `'use server'`** — at the top of the file or the function.
323
+ 5. **`access.ts` exports a default function** — not a named export.
324
+ 6. **Root layout must include `<html>` and `<body>`** — timber doesn't add these.
325
+ 7. **`getSegmentParams` uses ALS** — works in server components, middleware, access, actions. No prop drilling needed.
326
+ 8. **`useActionState` returns 4 values** — `[state, formAction, isPending, errors]`. The 4th is timber-specific with `errors.getFieldError()`, `errors.fieldErrors`, `errors.serverError`.
327
+ 9. **Middleware has one arg** — `middleware(ctx: MiddlewareContext)`, not `(req, res)`.
328
+ 10. **Don't use `next/` imports** — use `@timber-js/app/server`, `@timber-js/app/client`, etc.
329
+
330
+ ## Commands
331
+
332
+ ```bash
333
+ pnpm dev # Start dev server
334
+ pnpm build # Production build
335
+ pnpm preview # Preview production build
336
+ ```
337
+
338
+ ## Further reading
339
+
340
+ Full docs are in `node_modules/@timber-js/app/docs/`. Key files:
341
+
342
+ - `docs/learn/` — Core concepts (pages, data fetching, forms, caching, middleware)
343
+ - `docs/more/` — Advanced topics (routing, metadata, MDX, content collections, security)
344
+ - `docs/api/` — API reference for every export
@@ -1,4 +1,4 @@
1
- import { basename, join } from "node:path";
1
+ import { basename, join, relative } from "node:path";
2
2
  import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
3
  //#region src/adapters/shared.ts
4
4
  /** Cache-Control value for hashed (immutable) assets. */
@@ -12,19 +12,34 @@ var STATIC_CACHE = "public, max-age=3600, must-revalidate";
12
12
  * Static Assets, Cloudflare Pages, and Netlify. It maps URL patterns to
13
13
  * HTTP response headers.
14
14
  *
15
- * Vite places all hashed chunks under `/assets/` — these get immutable caching.
16
- * Everything else (favicon.ico, robots.txt, etc.) gets a shorter cache.
15
+ * Only hashed (immutable) asset paths are listed here by default. When
16
+ * `unhashedPaths` is provided, per-file rules are appended for unhashed
17
+ * static assets (favicon.ico, robots.txt, etc.) so they get Cache-Control
18
+ * on platforms like Cloudflare Workers Static Assets where the server
19
+ * handler never sees static file requests.
20
+ *
21
+ * A `/*` catch-all is NOT used — it would be additive on Cloudflare,
22
+ * producing duplicate Cache-Control directives on hashed assets
23
+ * (invalid per RFC 9111).
17
24
  */
18
- function generateHeadersFile() {
19
- return `# Auto-generated by @timber-js/app — static asset cache headers.
25
+ var MAX_UNHASHED_RULES = 98;
26
+ function generateHeadersFile(unhashedPaths) {
27
+ let content = `# Auto-generated by @timber-js/app — static asset cache headers.
20
28
  # See design/25-production-deployments.md §"CDN / Edge Cache"
21
29
 
22
30
  /assets/*
23
31
  Cache-Control: ${IMMUTABLE_CACHE}
24
32
 
25
- /*
26
- Cache-Control: ${STATIC_CACHE}
33
+ /_timber/fonts/*
34
+ Cache-Control: ${IMMUTABLE_CACHE}
27
35
  `;
36
+ if (unhashedPaths && unhashedPaths.length > 0) {
37
+ const capped = unhashedPaths.slice(0, MAX_UNHASHED_RULES);
38
+ if (unhashedPaths.length > MAX_UNHASHED_RULES) console.warn(`[timber] _headers: ${unhashedPaths.length} unhashed static files found, but Cloudflare limits _headers to 100 rules. Only the first ${MAX_UNHASHED_RULES} unhashed files will get Cache-Control headers.`);
39
+ content += `\n# Unhashed static assets — short-lived cache with revalidation\n`;
40
+ for (const p of capped) content += `\n${p}\n Cache-Control: ${STATIC_CACHE}\n`;
41
+ }
42
+ return content;
28
43
  }
29
44
  //#endregion
30
45
  //#region src/adapters/build-output-helper.ts
@@ -56,7 +71,8 @@ async function runSharedBuildSteps(opts) {
56
71
  }).catch((e) => {
57
72
  if (e.code !== "ENOENT") throw e;
58
73
  });
59
- await writeFile(join(publicDir, "_headers"), generateHeadersFile());
74
+ const unhashedPaths = await findUnhashedStaticFiles(publicDir);
75
+ await writeFile(join(publicDir, "_headers"), generateHeadersFile(unhashedPaths));
60
76
  await cp(join(buildDir, "rsc"), join(outDir, "rsc"), { recursive: true });
61
77
  await cp(join(buildDir, "ssr"), join(outDir, "ssr"), { recursive: true }).catch((e) => {
62
78
  if (e.code !== "ENOENT") throw e;
@@ -65,6 +81,43 @@ async function runSharedBuildSteps(opts) {
65
81
  return publicDir;
66
82
  }
67
83
  /**
84
+ * Find unhashed static files in the public directory that need per-file
85
+ * Cache-Control rules in the `_headers` file.
86
+ *
87
+ * Excludes:
88
+ * - Files under `assets/` and `_timber/` (covered by wildcard rules)
89
+ * - Underscore-prefixed files (`_headers`, `_worker.js`, etc.)
90
+ *
91
+ * @internal Exported for testing.
92
+ */
93
+ var PRIORITY_FILES = new Set([
94
+ "/favicon.ico",
95
+ "/robots.txt",
96
+ "/sitemap.xml",
97
+ "/manifest.json",
98
+ "/manifest.webmanifest",
99
+ "/apple-touch-icon.png"
100
+ ]);
101
+ async function findUnhashedStaticFiles(publicDir) {
102
+ let files;
103
+ try {
104
+ files = await collectFiles(publicDir);
105
+ } catch {
106
+ return [];
107
+ }
108
+ const priority = [];
109
+ const rest = [];
110
+ for (const file of files) {
111
+ const rel = relative(publicDir, file).replace(/\\/g, "/");
112
+ if (rel.startsWith("assets/") || rel.startsWith("_timber/")) continue;
113
+ if (basename(rel).startsWith("_")) continue;
114
+ const urlPath = "/" + rel;
115
+ if (PRIORITY_FILES.has(urlPath)) priority.push(urlPath);
116
+ else rest.push(urlPath);
117
+ }
118
+ return [...priority.sort(), ...rest.sort()];
119
+ }
120
+ /**
68
121
  * Scan client JS files for references to .wasm filenames.
69
122
  * Returns the set of WASM basenames that are actually imported/referenced
70
123
  * by client code and must be preserved in the static output.
@@ -99,4 +152,4 @@ async function collectFiles(dir) {
99
152
  //#endregion
100
153
  export { IMMUTABLE_CACHE as n, runSharedBuildSteps as t };
101
154
 
102
- //# sourceMappingURL=build-output-helper-B20ZuTSA.js.map
155
+ //# sourceMappingURL=build-output-helper-Rp9r7gSA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-output-helper-Rp9r7gSA.js","names":[],"sources":["../../src/adapters/shared.ts","../../src/adapters/build-output-helper.ts"],"sourcesContent":["// Shared adapter utilities — leaf module with zero deps on server/, client/,\n// or any Vite-dependent code.\n//\n// Adapters are loaded by Node at Vite startup time, before Vite's module\n// resolver is available. This module uses only Node built-ins and can be\n// imported by both adapters via a relative path.\n//\n// IMPORTANT: Do NOT add imports from ../server/, ../client/, ../plugins/,\n// or any module that transitively depends on Vite. A test in\n// tests/adapters/shared-no-forbidden-imports.test.ts enforces this.\n\n// ─── Static asset cache headers ──────────────────────────────────────────\n\n/** Cache-Control value for hashed (immutable) assets. */\nexport const IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\n\n/** Cache-Control value for unhashed static assets. */\nexport const STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\n/**\n * Generate a `_headers` file for static asset cache control.\n *\n * The `_headers` file is a platform convention supported by Cloudflare Workers\n * Static Assets, Cloudflare Pages, and Netlify. It maps URL patterns to\n * HTTP response headers.\n *\n * Only hashed (immutable) asset paths are listed here by default. When\n * `unhashedPaths` is provided, per-file rules are appended for unhashed\n * static assets (favicon.ico, robots.txt, etc.) so they get Cache-Control\n * on platforms like Cloudflare Workers Static Assets where the server\n * handler never sees static file requests.\n *\n * A `/*` catch-all is NOT used — it would be additive on Cloudflare,\n * producing duplicate Cache-Control directives on hashed assets\n * (invalid per RFC 9111).\n */\n// Cloudflare Workers Static Assets caps _headers at 100 rules.\n// Reserve 2 for the wildcard hashed-asset rules.\nconst MAX_UNHASHED_RULES = 98;\n\nexport function generateHeadersFile(unhashedPaths?: string[]): string {\n let content = `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n\n/assets/*\n Cache-Control: ${IMMUTABLE_CACHE}\n\n/_timber/fonts/*\n Cache-Control: ${IMMUTABLE_CACHE}\n`;\n\n if (unhashedPaths && unhashedPaths.length > 0) {\n const capped = unhashedPaths.slice(0, MAX_UNHASHED_RULES);\n if (unhashedPaths.length > MAX_UNHASHED_RULES) {\n console.warn(\n `[timber] _headers: ${unhashedPaths.length} unhashed static files found, ` +\n `but Cloudflare limits _headers to 100 rules. Only the first ${MAX_UNHASHED_RULES} ` +\n `unhashed files will get Cache-Control headers.`\n );\n }\n content += `\\n# Unhashed static assets — short-lived cache with revalidation\\n`;\n for (const p of capped) {\n content += `\\n${p}\\n Cache-Control: ${STATIC_CACHE}\\n`;\n }\n }\n\n return content;\n}\n","// Shared build-output helper for adapter buildOutput() methods.\n//\n// Steps 1–5 of every adapter's buildOutput are platform-agnostic:\n// 1. Copy client assets to the public/static directory\n// 2. Write _headers file for static asset cache control\n// 3. Copy RSC + SSR server bundles\n// 4. Write manifest-init module (if present)\n//\n// Only the final steps (entry codegen, platform config) vary per adapter.\n//\n// IMPORTANT: This module must remain a leaf — no imports from ../server/,\n// ../client/, or Vite-dependent code. See adapters/shared.ts header.\n\nimport { writeFile, mkdir, cp, readdir, readFile } from 'node:fs/promises';\nimport { join, basename, relative } from 'node:path';\nimport { generateHeadersFile } from './shared.js';\nimport type { TimberConfig } from './types';\n\n/** Options for the shared build output steps. */\nexport interface BuildOutputBaseOptions {\n /** Resolved timber config. */\n config: TimberConfig;\n /** Root build directory (e.g. `.timber/build`). */\n buildDir: string;\n /** Adapter-specific output directory (e.g. `.timber/build/nitro`). */\n outDir: string;\n /**\n * Name of the public/static directory within outDir.\n * Nitro uses 'public', Cloudflare uses 'static'.\n */\n publicDirName: string;\n}\n\n/**\n * Run the platform-agnostic build output steps shared by all adapters:\n *\n * 1. Create the output directory\n * 2. Copy client assets to the public/static directory\n * 3. Write the `_headers` file for static asset cache control\n * 4. Copy RSC and SSR server bundles into the output directory\n * 5. Write the manifest-init module if present\n *\n * Returns the resolved public directory path for further adapter-specific use.\n */\nexport async function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise<string> {\n const { config, buildDir, outDir, publicDirName } = opts;\n\n // 1. Create the output directory.\n await mkdir(outDir, { recursive: true });\n\n // 2. Copy client assets to public/static directory.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const publicDir = join(outDir, publicDirName);\n await mkdir(publicDir, { recursive: true });\n // Find WASM files that are referenced by client JS (legitimate client WASM)\n // vs orphaned server-only WASM (e.g. @takumi-rs for OG image generation).\n // Only strip orphaned WASM to avoid breaking client-side WASM usage.\n const referencedWasm = await findReferencedWasm(clientDir);\n\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: (src: string) => {\n if (src.endsWith('.wasm') && !referencedWasm.has(basename(src))) {\n return false;\n }\n if (config.clientJavascriptDisabled && src.endsWith('.js')) return false;\n return true;\n },\n }).catch((e: NodeJS.ErrnoException) => {\n // Client dir may not exist when client JavaScript is disabled\n if (e.code !== 'ENOENT') throw e;\n });\n\n // 3. Write _headers file for static asset cache control.\n // Scan for unhashed files so they get per-file Cache-Control rules in the\n // _headers file. On platforms like Cloudflare Workers Static Assets, the\n // worker never sees static file requests — _headers is the only mechanism.\n const unhashedPaths = await findUnhashedStaticFiles(publicDir);\n await writeFile(join(publicDir, '_headers'), generateHeadersFile(unhashedPaths));\n\n // 4. Copy RSC + SSR server bundles into the output directory.\n await cp(join(buildDir, 'rsc'), join(outDir, 'rsc'), { recursive: true });\n await cp(join(buildDir, 'ssr'), join(outDir, 'ssr'), { recursive: true }).catch(\n (e: NodeJS.ErrnoException) => {\n // SSR dir may not exist for static-only builds\n if (e.code !== 'ENOENT') throw e;\n }\n );\n\n // 5. Write manifest-init module if present.\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\n }\n\n return publicDir;\n}\n\n/**\n * Find unhashed static files in the public directory that need per-file\n * Cache-Control rules in the `_headers` file.\n *\n * Excludes:\n * - Files under `assets/` and `_timber/` (covered by wildcard rules)\n * - Underscore-prefixed files (`_headers`, `_worker.js`, etc.)\n *\n * @internal Exported for testing.\n */\n// Well-known metadata files that should be prioritized when capping\n// _headers rules, so they're never dropped in favor of less important files.\nconst PRIORITY_FILES = new Set([\n '/favicon.ico',\n '/robots.txt',\n '/sitemap.xml',\n '/manifest.json',\n '/manifest.webmanifest',\n '/apple-touch-icon.png',\n]);\n\nexport async function findUnhashedStaticFiles(publicDir: string): Promise<string[]> {\n let files: string[];\n try {\n files = await collectFiles(publicDir);\n } catch {\n return [];\n }\n\n const priority: string[] = [];\n const rest: string[] = [];\n for (const file of files) {\n const rel = relative(publicDir, file).replace(/\\\\/g, '/');\n if (rel.startsWith('assets/') || rel.startsWith('_timber/')) continue;\n if (basename(rel).startsWith('_')) continue;\n const urlPath = '/' + rel;\n if (PRIORITY_FILES.has(urlPath)) {\n priority.push(urlPath);\n } else {\n rest.push(urlPath);\n }\n }\n\n return [...priority.sort(), ...rest.sort()];\n}\n\n/**\n * Scan client JS files for references to .wasm filenames.\n * Returns the set of WASM basenames that are actually imported/referenced\n * by client code and must be preserved in the static output.\n */\nasync function findReferencedWasm(clientDir: string): Promise<Set<string>> {\n const referenced = new Set<string>();\n\n let files: string[];\n try {\n files = await collectFiles(clientDir);\n } catch {\n return referenced;\n }\n\n const jsFiles = files.filter((f) => f.endsWith('.js'));\n const wasmFiles = files.filter((f) => f.endsWith('.wasm')).map((f) => basename(f));\n if (wasmFiles.length === 0) return referenced;\n\n for (const jsFile of jsFiles) {\n const content = await readFile(jsFile, 'utf-8');\n for (const wasmName of wasmFiles) {\n if (content.includes(wasmName)) {\n referenced.add(wasmName);\n }\n }\n }\n\n return referenced;\n}\n\nasync function collectFiles(dir: string): Promise<string[]> {\n const result: string[] = [];\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n result.push(...(await collectFiles(fullPath)));\n } else {\n result.push(fullPath);\n }\n }\n return result;\n}\n"],"mappings":"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"}
@@ -25,4 +25,5 @@ export interface BuildOutputBaseOptions {
25
25
  * Returns the resolved public directory path for further adapter-specific use.
26
26
  */
27
27
  export declare function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise<string>;
28
+ export declare function findUnhashedStaticFiles(publicDir: string): Promise<string[]>;
28
29
  //# sourceMappingURL=build-output-helper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-output-helper.d.ts","sourceRoot":"","sources":["../../src/adapters/build-output-helper.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,iDAAiD;AACjD,MAAM,WAAW,sBAAsB;IACrC,8BAA8B;IAC9B,MAAM,EAAE,YAAY,CAAC;IACrB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAiDvF"}
1
+ {"version":3,"file":"build-output-helper.d.ts","sourceRoot":"","sources":["../../src/adapters/build-output-helper.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,iDAAiD;AACjD,MAAM,WAAW,sBAAsB;IACrC,8BAA8B;IAC9B,MAAM,EAAE,YAAY,CAAC;IACrB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqDvF;AAuBD,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAuBlF"}
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AA8BnE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAUtE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAE/E;AAMD,wFAAwF;AACxF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,kFAAkF;AAClF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,oFAAoF;AACpF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,uEAAuE;AACvE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,8EAA8E;AAC9E,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,2EAA2E;AAC3E,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC3B,EAAE,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC3B,EAAE,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,uBAAuB,EAAE,CAAC;QACtC,SAAS,CAAC,EAAE,uBAAuB,EAAE,CAAC;KACvC,CAAC;IACF,cAAc,CAAC,EAAE,8BAA8B,EAAE,CAAC;CACnD;AAED,kDAAkD;AAClD,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE9B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAE9B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEnC;;;;;;;;;;;;;;;;;;OAkBG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,wBAA6B,GAAG,qBAAqB,CA+DxF;AAID,sCAAsC;AACtC,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,eAAe,UAAQ,EACvB,iBAAiB,UAAQ,GACxB,MAAM,CAmFR;AAED,qEAAqE;AACrE,MAAM,WAAW,sBAAsB;IACrC,aAAa,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACtD,SAAS,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;KAC5C,CAAC;IACF,eAAe,CAAC,EAAE;QAChB,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;KACzC,CAAC;CACH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,kBAAkB,GAAG,SAAS,GACvC,sBAAsB,CAmExB;AAgCD,sCAAsC;AACtC,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,wBAAwB,GAChC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkCzB;AAID,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,sCAAsC;AACtC,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAOvE;AAuBD,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3C,sBAAsB,IAAI,IAAI,CAAC;CAChC"}
@@ -1,4 +1,4 @@
1
- import { t as runSharedBuildSteps } from "../_chunks/build-output-helper-B20ZuTSA.js";
1
+ import { t as runSharedBuildSteps } from "../_chunks/build-output-helper-Rp9r7gSA.js";
2
2
  import { join, relative } from "node:path";
3
3
  import { writeFile } from "node:fs/promises";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -128,12 +128,42 @@ if (!bindingsAls) {
128
128
 
129
129
  export default {
130
130
  ${hasWorkerHandlers ? " ...workerHandlers,\n" : ""} async fetch(request, env, ctx) {
131
- // Bridge ctx.waitUntil into the per-request ALS so waitUntil() calls in
132
- // user code keep the isolate alive after the response is sent.
133
- // See design/11-platform.md §"waitUntil()".
134
- return bindingsAls.run(env, () =>
131
+ // Edge-cache GET/HEAD responses via the Cache API. RSC navigation
132
+ // requests (Accept: text/x-component) are excluded — they vary by
133
+ // X-Timber-State-Tree which the cache key doesn't capture.
134
+ const isRsc = request.headers.get('Accept')?.includes('text/x-component')
135
+ const reqCc = request.headers.get('Cache-Control') || ''
136
+ const bypassCache = reqCc.includes('no-cache') || reqCc.includes('max-age=0')
137
+ || request.headers.has('Pragma')
138
+ const cacheable = !isRsc && !bypassCache
139
+ && (request.method === 'GET' || request.method === 'HEAD')
140
+ const cache = cacheable && typeof caches !== 'undefined' ? caches.default : null
141
+ if (cache) {
142
+ const cached = await cache.match(request)
143
+ if (cached) return cached
144
+ }
145
+
146
+ const response = await bindingsAls.run(env, () =>
135
147
  runWithWaitUntil((promise) => ctx.waitUntil(promise), () => handler(request))
136
148
  )
149
+
150
+ // Cache public responses at the edge. A response is edge-cacheable if
151
+ // it has 'public' or 's-maxage' (a shared-cache TTL directive) and no
152
+ // 'no-store' or 'private'. Cloudflare's cache.put() only accepts GET
153
+ // requests and rejects 206, so only store GET 200s. HEAD requests
154
+ // benefit from cache.match() which handles HEAD→GET matching automatically.
155
+ // See design/25-production-deployments.md §"CDN / Edge Cache".
156
+ if (cache && request.method === 'GET' && response.status === 200) {
157
+ const cc = response.headers.get('Cache-Control')
158
+ const vary = response.headers.get('Vary')
159
+ if (cc && !cc.includes('no-store') && !cc.includes('private')
160
+ && (cc.includes('public') || cc.includes('s-maxage'))
161
+ && vary !== '*' && !response.headers.has('Set-Cookie')) {
162
+ ctx.waitUntil(cache.put(request, response.clone()))
163
+ }
164
+ }
165
+
166
+ return response
137
167
  }
138
168
  }
139
169
  `;