@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.
- package/agent-skill.md +344 -0
- package/dist/_chunks/{build-output-helper-B20ZuTSA.js → build-output-helper-Rp9r7gSA.js} +62 -9
- package/dist/_chunks/build-output-helper-Rp9r7gSA.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +1 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +35 -5
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/adapters/shared.d.ts +1 -11
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/cli.d.ts +9 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +41 -4
- package/dist/cli.js.map +1 -1
- package/dist/fonts/google.d.ts.map +1 -1
- package/dist/index.js +43 -3
- package/dist/index.js.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/server/asset-headers.d.ts +7 -3
- package/dist/server/asset-headers.d.ts.map +1 -1
- package/dist/server/internal.js +12 -6
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/docs/api/30-api-server.mdx +233 -0
- package/docs/api/31-api-client.mdx +161 -0
- package/docs/api/32-api-cache.mdx +193 -0
- package/docs/api/33-api-search-params.mdx +141 -0
- package/docs/api/34-api-config.mdx +189 -0
- package/docs/api/35-api-typescript.mdx +170 -0
- package/docs/api/36-cli.mdx +143 -0
- package/docs/learn/00-introduction.mdx +81 -0
- package/docs/learn/01-quick-start.mdx +94 -0
- package/docs/learn/02-pages-and-layouts.mdx +138 -0
- package/docs/learn/03-fetching-data.mdx +211 -0
- package/docs/learn/03b-access-control.mdx +130 -0
- package/docs/learn/04-loading-states.mdx +67 -0
- package/docs/learn/04b-the-flush-point.mdx +115 -0
- package/docs/learn/05-typed-params.mdx +275 -0
- package/docs/learn/06-forms-and-actions.mdx +150 -0
- package/docs/learn/08-streaming.mdx +146 -0
- package/docs/learn/09-caching.mdx +121 -0
- package/docs/learn/10-middleware.mdx +156 -0
- package/docs/learn/11-error-handling.mdx +147 -0
- package/docs/learn/12-client-navigation.mdx +168 -0
- package/docs/learn/13-configuration.mdx +117 -0
- package/docs/learn/14-deploying.mdx +206 -0
- package/docs/more/01-advanced-routing.mdx +139 -0
- package/docs/more/02-advanced-forms.mdx +137 -0
- package/docs/more/03-coming-from-nextjs.mdx +186 -0
- package/docs/more/04-metadata-and-fonts.mdx +193 -0
- package/docs/more/04b-mdx.mdx +209 -0
- package/docs/more/05-content-collections.mdx +90 -0
- package/docs/more/06-instrumentation.mdx +188 -0
- package/docs/more/07-security.mdx +123 -0
- package/docs/more/40-why-timber.mdx +50 -0
- package/docs/more/41-timber-vs-nextjs.mdx +81 -0
- package/docs/more/42-timber-vs-others.mdx +68 -0
- package/docs/more/50-ai-agent-instructions.mdx +169 -0
- package/llms.txt +55 -0
- package/package.json +8 -4
- package/src/adapters/build-output-helper.ts +52 -2
- package/src/adapters/cloudflare.ts +34 -4
- package/src/adapters/shared.ts +34 -6
- package/src/cli.ts +62 -3
- package/src/fonts/google.ts +56 -5
- package/src/plugins/fonts.ts +6 -0
- package/src/server/asset-headers.ts +7 -3
- package/src/server/pipeline-phases.ts +10 -2
- 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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
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: ${
|
|
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
|
|
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-
|
|
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,
|
|
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":"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,
|
|
1
|
+
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as runSharedBuildSteps } from "../_chunks/build-output-helper-
|
|
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
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
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
|
`;
|