@timber-js/app 0.2.0-alpha.166 → 0.2.0-alpha.168
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/_chunks/{actions-CDPfMp_I.js → actions-O_LsyCE4.js} +3 -3
- package/dist/_chunks/{actions-CDPfMp_I.js.map → actions-O_LsyCE4.js.map} +1 -1
- package/dist/_chunks/{cache-api-DygSeKCB.js → cache-api-B-lhk9p4.js} +107 -58
- package/dist/_chunks/cache-api-B-lhk9p4.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-EXGYPhI2.js → cli-schema-sync-B73L6pMq.js} +5 -3
- package/dist/_chunks/cli-schema-sync-B73L6pMq.js.map +1 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js +1188 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js.map +1 -0
- package/dist/_chunks/json-lossy-check-ClNvBM_3.js +63 -0
- package/dist/_chunks/json-lossy-check-ClNvBM_3.js.map +1 -0
- package/dist/_chunks/{logger-t3uxAmbX.js → logger-AWfuX-KJ.js} +2 -19
- package/dist/_chunks/logger-AWfuX-KJ.js.map +1 -0
- package/dist/_chunks/{walkers-DBVzXuWc.js → walkers-CoOC8Hga.js} +2 -2
- package/dist/_chunks/{walkers-DBVzXuWc.js.map → walkers-CoOC8Hga.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +10 -3
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +12 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +2 -461
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/cache-api.d.ts +33 -11
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cache/json-lossy-check.d.ts +11 -0
- package/dist/cache/json-lossy-check.d.ts.map +1 -0
- package/dist/cache/redis-handler.d.ts +42 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -1
- package/dist/cache/stores/memory.d.ts +1 -1
- package/dist/cache/stores/memory.d.ts.map +1 -1
- package/dist/cache/stores/redis.d.ts +1 -1
- package/dist/cache/stores/redis.d.ts.map +1 -1
- package/dist/cache/stores/vercel.d.ts +1 -1
- package/dist/cache/stores/vercel.d.ts.map +1 -1
- package/dist/cache/tag-aware-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.d.ts.map +1 -1
- package/dist/cdn/fastly-purge.d.ts.map +1 -1
- package/dist/cdn/workers-cache-purge.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/child-segment-context.d.ts +2 -2
- package/dist/client/child-segment-context.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -0
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/internal.js +141 -116
- package/dist/client/internal.js.map +1 -1
- package/dist/client/router.d.ts +6 -0
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +3 -3
- package/dist/client/segment-update-context.d.ts.map +1 -1
- package/dist/client/slot-outlet.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +2 -1
- package/dist/codec.js.map +1 -1
- package/dist/config-types.d.ts +16 -37
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/dev-tools/instrumentation.d.ts.map +1 -1
- package/dist/fonts/pipeline.d.ts +19 -0
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +378 -109
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +8 -8
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +27 -3
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/action-client.d.ts +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +7 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/body-limits.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +3 -1
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/index.js +20 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +28 -41
- package/dist/server/internal.js.map +1 -1
- package/dist/server/middleware-runner.d.ts +0 -17
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +6 -3
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +18 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/prebuilt/capture-state.d.ts.map +1 -1
- package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -1
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +9 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +14 -14
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +1 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +8 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +44 -74
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
- package/dist/server/safe-load.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/shared/redirect-type.d.ts +2 -2
- package/dist/shared/redirect-type.d.ts.map +1 -1
- package/dist/shims/font-google.d.ts.map +1 -1
- package/dist/shims/image.d.ts +120 -120
- package/dist/shims/image.d.ts.map +1 -1
- package/docs/api/32-api-cache.mdx +116 -4
- package/docs/api/34-api-config.mdx +0 -26
- package/docs/learn/09-caching.mdx +126 -10
- package/docs/learn/12-client-navigation.mdx +9 -1
- package/docs/learn/13-configuration.mdx +6 -8
- package/docs/learn/14-deploying.mdx +18 -18
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/docs/more/50-ai-agent-instructions.mdx +5 -5
- package/package.json +6 -5
- package/src/adapters/cloudflare-kv-cache.ts +27 -6
- package/src/adapters/cloudflare.ts +63 -25
- package/src/adapters/types.ts +2 -0
- package/src/cache/cache-api.ts +84 -84
- package/src/cache/index.ts +1 -1
- package/src/cache/json-lossy-check.ts +75 -0
- package/src/cache/redis-handler.ts +65 -14
- package/src/cache/timber-cache.ts +10 -2
- package/src/client/browser-entry/index.ts +2 -0
- package/src/client/browser-entry/post-hydration.ts +16 -9
- package/src/client/browser-entry/router-init.ts +2 -0
- package/src/client/history.ts +11 -0
- package/src/client/router.ts +224 -208
- package/src/codec.ts +3 -1
- package/src/config-types.ts +16 -37
- package/src/config-validation.ts +7 -5
- package/src/fonts/pipeline.ts +32 -0
- package/src/fonts/transform.ts +30 -24
- package/src/plugins/adapter-build.ts +132 -13
- package/src/plugins/cache.ts +45 -30
- package/src/plugins/entries.ts +40 -43
- package/src/plugins/fonts.ts +30 -0
- package/src/plugins/server-bundle.ts +7 -15
- package/src/routing/scanner.ts +7 -0
- package/src/server/action-client.ts +1 -1
- package/src/server/als-registry.ts +7 -0
- package/src/server/deny-boundary.ts +6 -3
- package/src/server/middleware-runner.ts +0 -44
- package/src/server/param-coercion.ts +1 -0
- package/src/server/pipeline-phases.ts +11 -13
- package/src/server/pipeline.ts +51 -3
- package/src/server/route-element-builder.ts +59 -9
- package/src/server/rsc-entry/action-middleware-runner.ts +14 -14
- package/src/server/rsc-entry/index.ts +30 -40
- package/src/server/rsc-entry/render-route.ts +6 -2
- package/src/server/rsc-entry/rsc-payload.ts +21 -4
- package/src/server/rsc-entry/rsc-stream.ts +8 -0
- package/src/server/rsc-entry/wrap-action-dispatch.ts +109 -372
- package/dist/_chunks/cache-api-DygSeKCB.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-EXGYPhI2.js.map +0 -1
- package/dist/_chunks/logger-t3uxAmbX.js.map +0 -1
- package/dist/_chunks/tree-match-D2l830j2.js +0 -102
- package/dist/_chunks/tree-match-D2l830j2.js.map +0 -1
- package/dist/adapters/cloudflare.js.map +0 -1
|
@@ -36,7 +36,7 @@ export const getUser = cache(async (id: string) => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
// Caches across requests for 60 seconds
|
|
39
|
-
export const getPopularProducts = timberCache(
|
|
39
|
+
export const getPopularProducts = timberCache.data(
|
|
40
40
|
async () => {
|
|
41
41
|
return db.products.findPopular();
|
|
42
42
|
},
|
|
@@ -44,7 +44,7 @@ export const getPopularProducts = timberCache(
|
|
|
44
44
|
);
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
## `
|
|
47
|
+
## `cache.data()` Options
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
50
|
import { cache } from '@timber-js/app/cache';
|
|
@@ -53,7 +53,7 @@ declare const db: {
|
|
|
53
53
|
users: { findUnique(opts: { where: { id: string } }): Promise<{ id: string; name: string }> };
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
const getUser = cache(
|
|
56
|
+
const getUser = cache.data(
|
|
57
57
|
async (userId: string) => {
|
|
58
58
|
return db.users.findUnique({ where: { id: userId } });
|
|
59
59
|
},
|
|
@@ -74,7 +74,7 @@ Without an explicit `key`, the cache key is derived from the call site plus a de
|
|
|
74
74
|
|
|
75
75
|
### Content-Derived Cache Identities
|
|
76
76
|
|
|
77
|
-
The call-site identity is content-based — derived from the source code of the `cache()` call, not its position in the file. This means:
|
|
77
|
+
The call-site identity is content-based — derived from the source code of the `cache.data()` call, not its position in the file. This means:
|
|
78
78
|
|
|
79
79
|
- **Unchanged code = cache survives deploys.** If you redeploy without changing a cached function, its entries remain valid on persistent handlers like Redis or KV.
|
|
80
80
|
- **Changed code = automatic invalidation.** Editing the wrapped function or its options changes the cache identity, which invalidates old entries without manual cache busting.
|
|
@@ -86,7 +86,7 @@ Object key order doesn't matter — `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` produc
|
|
|
86
86
|
Arguments that can't be serialized faithfully — functions, symbols, or class instances whose data lives behind getters with no `toJSON()` — throw a `TypeError` instead of silently colliding. If you hit this, pass an explicit `key`:
|
|
87
87
|
|
|
88
88
|
```ts
|
|
89
|
-
cache(fn, { ttl: 60, key: (session) => `events:${session.userId}` });
|
|
89
|
+
cache.data(fn, { ttl: 60, key: (session) => `events:${session.userId}` });
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
## Invalidation
|
|
@@ -117,16 +117,132 @@ On serverless platforms (Cloudflare Workers, Lambda), the background refetch is
|
|
|
117
117
|
|
|
118
118
|
When multiple concurrent requests miss the cache for the same key, only one executes the underlying function. The rest wait for the result. The result is written to the cache exactly once, by the execution that ran the function. This is built in — no opt-in needed.
|
|
119
119
|
|
|
120
|
+
## Caching Components
|
|
121
|
+
|
|
122
|
+
`cache.data` caches a function's return value. `cache.component` caches a component's **rendered output** — the RSC flight payload — so re-renders skip the entire component tree:
|
|
123
|
+
|
|
124
|
+
```tsx title="app/blog/[slug]/page.tsx"
|
|
125
|
+
import { cache } from '@timber-js/app/cache';
|
|
126
|
+
import { getSegmentParams } from '@timber-js/app/server';
|
|
127
|
+
|
|
128
|
+
const PostBody = cache.component(
|
|
129
|
+
async function PostBody() {
|
|
130
|
+
const { slug } = getSegmentParams();
|
|
131
|
+
const post = await getPost(slug);
|
|
132
|
+
return <article><h1>{post.title}</h1><PostContent content={post.content} /></article>;
|
|
133
|
+
},
|
|
134
|
+
{ prerender: true }
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
export async function generateStaticSegmentParams() {
|
|
138
|
+
return allPosts.map((p) => ({ slug: p.slug }));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default async function BlogPost() {
|
|
142
|
+
return <PostBody />;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The same API gives you three tiers of freshness, controlled by two options:
|
|
147
|
+
|
|
148
|
+
### Static (build-time only)
|
|
149
|
+
|
|
150
|
+
Rendered at build for each set of params from `generateStaticSegmentParams`. Changes only on redeploy. Request context (`getCookieJar()`, `getHeaders()`) is a build error.
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
const BlogBody = cache.component(Body, { prerender: true });
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Cached (runtime only)
|
|
157
|
+
|
|
158
|
+
Rendered on first request, stored with a TTL. Invalidatable by tag. May read request context (with a warning — be careful not to leak per-user data into a shared cache).
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
const Pricing = cache.component(PricingTable, {
|
|
162
|
+
ttl: 300,
|
|
163
|
+
tags: ['pricing'],
|
|
164
|
+
staleWhileRevalidate: true,
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### ISR (build-seeded, runtime-refreshable)
|
|
169
|
+
|
|
170
|
+
Rendered at build, but refreshable via `revalidateTag` without a redeploy. Unenumerated params fill in on first request.
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
const DocsPage = cache.component(DocsBody, {
|
|
174
|
+
prerender: true,
|
|
175
|
+
ttl: 3600,
|
|
176
|
+
tags: ['docs'],
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Mixing Tiers on One Page
|
|
181
|
+
|
|
182
|
+
A page can combine all three — a dynamic shell with cached islands:
|
|
183
|
+
|
|
184
|
+
```tsx title="app/blog/[slug]/page.tsx"
|
|
185
|
+
import { getCookieJar } from '@timber-js/app/server';
|
|
186
|
+
|
|
187
|
+
export default async function BlogPost() {
|
|
188
|
+
const user = await getUser(getCookieJar());
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<Nav user={user} /> {/* dynamic — personalized */}
|
|
192
|
+
<PostBody /> {/* prerendered — from build artifact */}
|
|
193
|
+
<Suspense fallback={<CommentsSkeleton />}>
|
|
194
|
+
<Comments /> {/* dynamic — streams per-request */}
|
|
195
|
+
</Suspense>
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Route-Level Shorthand
|
|
202
|
+
|
|
203
|
+
For fully static routes, export `prerender` instead of wrapping the component manually:
|
|
204
|
+
|
|
205
|
+
```ts title="app/about/page.tsx"
|
|
206
|
+
export const prerender = true;
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
This desugars to `cache.component(...)` around the page's default export. For dynamic routes (`[slug]`), pair it with `generateStaticSegmentParams` so the build knows which params to prerender:
|
|
210
|
+
|
|
211
|
+
```ts title="app/docs/[slug]/page.tsx"
|
|
212
|
+
export const prerender = { ttl: 3600, tags: ['docs'] };
|
|
213
|
+
|
|
214
|
+
export async function generateStaticSegmentParams() {
|
|
215
|
+
return allDocs.map((d) => ({ slug: d.slug }));
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Layout `prerender` cascades to descendant pages unless a closer layout or page overrides it.
|
|
220
|
+
|
|
221
|
+
### Slots: Dynamic Children Through a Cached Shell
|
|
222
|
+
|
|
223
|
+
When the expensive part is a wrapper (nav, sidebar, layout chrome) but its children are dynamic, use `slots` to cache the shell with holes for live content:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
const Shell = cache.component(ExpensiveWrapper, {
|
|
227
|
+
ttl: 3600,
|
|
228
|
+
tags: ['marketing'],
|
|
229
|
+
slots: ['children'],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// The shell serves from cache; children render live per request.
|
|
233
|
+
<Shell theme="dark"><LiveUserFeed /></Shell>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Slot props are excluded from the cache key. Slots are runtime tier only — `prerender` is ignored when `slots` is active.
|
|
237
|
+
|
|
120
238
|
## Cache Handlers
|
|
121
239
|
|
|
122
|
-
The default handler is in-memory. Configure a different backend in `timber.
|
|
240
|
+
The default handler is in-memory. Configure a different backend in `timber.cache.ts`:
|
|
123
241
|
|
|
124
|
-
```ts title="timber.
|
|
242
|
+
```ts title="timber.cache.ts"
|
|
125
243
|
import { MemoryCacheHandler } from '@timber-js/app/cache';
|
|
126
244
|
|
|
127
|
-
export default
|
|
128
|
-
cacheHandler: new MemoryCacheHandler(),
|
|
129
|
-
};
|
|
245
|
+
export default new MemoryCacheHandler();
|
|
130
246
|
```
|
|
131
247
|
|
|
132
248
|
Implement the `CacheHandler` interface for Redis, Cloudflare KV, or any other backend. See the [Cache API Reference](/docs/api-cache).
|
|
@@ -108,7 +108,13 @@ export function NavItem({ href, children }: { href: string; children: React.Reac
|
|
|
108
108
|
|
|
109
109
|
## Segment Cache
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
Opt-in via `clientSegmentCache: true` in `timber.config.ts`. When enabled, the client maintains a mirror of the server's segment tree. On navigation, only changed segments are re-fetched:
|
|
112
|
+
|
|
113
|
+
```ts title="timber.config.ts"
|
|
114
|
+
export default {
|
|
115
|
+
clientSegmentCache: true,
|
|
116
|
+
};
|
|
117
|
+
```
|
|
112
118
|
|
|
113
119
|
```
|
|
114
120
|
/dashboard/settings → /dashboard/team
|
|
@@ -121,6 +127,8 @@ The client maintains a mirror of the server's segment tree. On navigation, only
|
|
|
121
127
|
|
|
122
128
|
Sync layouts stay cached while mounted. Async layouts always re-render. Pages always re-render. Client component state in shared layouts is preserved — counters, form inputs, scroll positions survive navigation.
|
|
123
129
|
|
|
130
|
+
When disabled (the default), every client navigation gets a full RSC payload from the server. This is simpler and avoids edge cases with stale cached layouts, at the cost of slightly larger payloads on navigation.
|
|
131
|
+
|
|
124
132
|
Back/forward navigation replays cached RSC payloads instantly — no server roundtrip.
|
|
125
133
|
|
|
126
134
|
## Scroll Restoration
|
|
@@ -81,16 +81,15 @@ export default {
|
|
|
81
81
|
|
|
82
82
|
## Cache Handler
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
Cache handler configuration lives in a separate `timber.cache.ts` file (not `timber.config.ts`). This keeps runtime cache instances separate from build-time config, preventing build dependencies from leaking into the server bundle.
|
|
85
|
+
|
|
86
|
+
```ts title="timber.cache.ts"
|
|
85
87
|
import { MemoryCacheHandler } from '@timber-js/app/cache';
|
|
86
88
|
|
|
87
|
-
export default
|
|
88
|
-
output: 'server',
|
|
89
|
-
cacheHandler: new MemoryCacheHandler(),
|
|
90
|
-
};
|
|
89
|
+
export default new MemoryCacheHandler();
|
|
91
90
|
```
|
|
92
91
|
|
|
93
|
-
The default is in-memory. See [Caching](/docs/caching) for Redis, KV, and custom handlers.
|
|
92
|
+
The default export is the `CacheHandler` instance. Export `cdnPurge` as a named export for CDN purge handlers. The default handler is in-memory. See [Caching](/docs/caching) for Redis, KV, and custom handlers.
|
|
94
93
|
|
|
95
94
|
## `clientJavascript`
|
|
96
95
|
|
|
@@ -129,8 +128,6 @@ Server actions still work — HTML forms submit natively via POST without JavaSc
|
|
|
129
128
|
| `buildDir` | `string` | `'.timber/dist'` | Build output directory |
|
|
130
129
|
| `clientJavascript` | `boolean \| object` | `true` | Control client-side JS |
|
|
131
130
|
| `adapter` | `TimberPlatformAdapter` | — | Deployment adapter |
|
|
132
|
-
| `cacheHandler` | `CacheHandler` | `MemoryCacheHandler` | Cache backend |
|
|
133
|
-
| `cdnPurge` | `CdnPurgeHandler` | — | CDN purge on revalidation |
|
|
134
131
|
| `serverTiming` | `'detailed' \| 'total' \| false` | `'detailed'` / `'total'` | Server-Timing header |
|
|
135
132
|
| `allowedOrigins` | `string[]` | — | CORS / CSRF allowed origins |
|
|
136
133
|
| `csrf` | `boolean` | `true` | CSRF protection |
|
|
@@ -148,6 +145,7 @@ Server actions still work — HTML forms submit natively via POST without JavaSc
|
|
|
148
145
|
| `actionEncryption` | `object` | — | Server action bound args encryption |
|
|
149
146
|
| `reactCompiler` | `boolean \| object` | `false` | React Compiler auto-memoization |
|
|
150
147
|
| `sitemap` | `object` | — | Auto-generated sitemap.xml |
|
|
148
|
+
| `clientSegmentCache`| `boolean` | `false` | Opt-in client segment cache for partial nav |
|
|
151
149
|
| `topLoader` | `object` | enabled | Navigation progress bar |
|
|
152
150
|
|
|
153
151
|
For the full type definition, see the [Config API Reference](/docs/api-config).
|
|
@@ -65,15 +65,19 @@ For caching, use `CloudflareKVCacheHandler`:
|
|
|
65
65
|
|
|
66
66
|
```ts title="timber.config.ts"
|
|
67
67
|
import { cloudflare } from '@timber-js/app/adapters/cloudflare';
|
|
68
|
-
import { CloudflareKVCacheHandler } from '@timber-js/app/adapters/cloudflare/cache';
|
|
69
68
|
|
|
70
69
|
export default {
|
|
71
70
|
output: 'server',
|
|
72
71
|
adapter: cloudflare(),
|
|
73
|
-
cacheHandler: new CloudflareKVCacheHandler({ bindingName: 'TIMBER_CACHE' }),
|
|
74
72
|
};
|
|
75
73
|
```
|
|
76
74
|
|
|
75
|
+
```ts title="timber.cache.ts"
|
|
76
|
+
import { CloudflareKVCacheHandler } from '@timber-js/app/adapters/cloudflare/cache';
|
|
77
|
+
|
|
78
|
+
export default new CloudflareKVCacheHandler({ bindingName: 'TIMBER_CACHE' });
|
|
79
|
+
```
|
|
80
|
+
|
|
77
81
|
**Constraints:** no file system, 10ms/30s CPU time limits, 10MB/25MB bundle size.
|
|
78
82
|
|
|
79
83
|
## Node.js & Docker
|
|
@@ -226,30 +230,26 @@ When using the Cloudflare adapter, CDN purge is **automatic** — no configurati
|
|
|
226
230
|
|
|
227
231
|
For non-Workers Cloudflare zones (Enterprise), or other CDNs, configure a purge handler manually:
|
|
228
232
|
|
|
229
|
-
```ts title="timber.
|
|
233
|
+
```ts title="timber.cache.ts"
|
|
230
234
|
import { CloudflarePurge } from '@timber-js/app/cdn/cloudflare';
|
|
231
235
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}),
|
|
238
|
-
};
|
|
236
|
+
// Only needed for non-Workers Cloudflare zones (Enterprise)
|
|
237
|
+
export const cdnPurge = new CloudflarePurge({
|
|
238
|
+
zoneId: process.env.CF_ZONE_ID!,
|
|
239
|
+
apiToken: process.env.CF_API_TOKEN!,
|
|
240
|
+
});
|
|
239
241
|
```
|
|
240
242
|
|
|
241
243
|
Fastly:
|
|
242
244
|
|
|
243
|
-
```ts title="timber.
|
|
245
|
+
```ts title="timber.cache.ts"
|
|
244
246
|
import { FastlyPurge } from '@timber-js/app/cdn/fastly';
|
|
245
247
|
|
|
246
|
-
export
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}),
|
|
252
|
-
};
|
|
248
|
+
export const cdnPurge = new FastlyPurge({
|
|
249
|
+
serviceId: process.env.FASTLY_SERVICE_ID!,
|
|
250
|
+
apiToken: process.env.FASTLY_API_TOKEN!,
|
|
251
|
+
softPurge: true,
|
|
252
|
+
});
|
|
253
253
|
```
|
|
254
254
|
|
|
255
255
|
When `revalidateTag('products')` fires, it clears the application cache **and** calls the CDN purge handler. CDN purge is best-effort — failures are logged but don't break cache invalidation.
|
|
@@ -80,14 +80,14 @@ export const searchParams = defineSearchParams({
|
|
|
80
80
|
|
|
81
81
|
**Next.js:** Implicit fetch caching, `unstable_cache`, ISR with `revalidate`.
|
|
82
82
|
|
|
83
|
-
**timber:** No implicit caching. No ISR. Explicit `
|
|
83
|
+
**timber:** No implicit caching. No ISR. Explicit `cache.data()` with TTL and tags:
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
86
|
import { cache } from '@timber-js/app/cache';
|
|
87
87
|
|
|
88
88
|
declare const db: { products: { findPopular(): Promise<{ id: string; name: string }[]> } };
|
|
89
89
|
|
|
90
|
-
const getProducts = cache(
|
|
90
|
+
const getProducts = cache.data(
|
|
91
91
|
async () => db.products.findPopular(),
|
|
92
92
|
{ ttl: 300, tags: ['products'] }
|
|
93
93
|
);
|
|
@@ -87,7 +87,7 @@ Special files in your route tree generate metadata endpoints:
|
|
|
87
87
|
|
|
88
88
|
When `opengraph-image.tsx` exists in a segment, timber automatically emits both `<meta property="og:image">` and `<meta name="twitter:image">` in `<head>` — no need to declare them in your `metadata` export. The auto-linked URL includes a cache-busting hash so social platforms pick up changes on redeploy.
|
|
89
89
|
|
|
90
|
-
If you declare `openGraph.images` in your metadata,
|
|
90
|
+
If you declare `openGraph.images` in your metadata, both auto-linked tags are suppressed. If you declare only `twitter.images`, the auto-linked `twitter:image` is suppressed but `og:image` is still emitted. User-declared metadata always wins.
|
|
91
91
|
|
|
92
92
|
`favicon.tsx` lets you generate favicons dynamically (e.g. tenant branding). The URL stays `/favicon.ico` for browser compatibility.
|
|
93
93
|
|
|
@@ -53,8 +53,8 @@ These are the things you will get wrong if you assume Next.js behavior.
|
|
|
53
53
|
| Concept | Next.js | timber.js |
|
|
54
54
|
| --- | --- | --- |
|
|
55
55
|
| Default component type | Client components need `'use client'` | **All components are server components** by default. Only add `'use client'` when you need browser APIs or hooks. |
|
|
56
|
-
| Fetch caching | `fetch()` is patched with implicit caching | `fetch()` is **never patched**. Use `cache()` from `@timber-js/app/cache` explicitly. |
|
|
57
|
-
| ISR | `revalidate` option | **Does not exist.** Use `cache()` with TTL and tags. |
|
|
56
|
+
| Fetch caching | `fetch()` is patched with implicit caching | `fetch()` is **never patched**. Use `cache.data()` from `@timber-js/app/cache` explicitly. |
|
|
57
|
+
| ISR | `revalidate` option | **Does not exist.** Use `cache.data()` with TTL and tags. |
|
|
58
58
|
| `loading.tsx` | Convention for auto-Suspense | **Does not exist.** Use `<Suspense>` explicitly. |
|
|
59
59
|
| Middleware | Single global `middleware.ts` with matchers | Per-segment `middleware.ts` files + global `proxy.ts`. One-arg signature: `middleware(ctx)`. |
|
|
60
60
|
| `notFound()` | `notFound()` from `next/navigation` | `deny(404)` from `@timber-js/app/server`. Sends a real HTTP 404. |
|
|
@@ -104,11 +104,11 @@ content-collections.ts # Collection definitions (optional)
|
|
|
104
104
|
|
|
105
105
|
2. **Adding `'use client'` to everything** — components are server components by default. Only add `'use client'` when you need browser APIs, event handlers, or React hooks like `useState`/`useEffect`.
|
|
106
106
|
|
|
107
|
-
3. **Using `fetch()` and expecting caching** — timber never patches `fetch`. Wrap data-fetching functions in `cache()` from `@timber-js/app/cache` when you want caching.
|
|
107
|
+
3. **Using `fetch()` and expecting caching** — timber never patches `fetch`. Wrap data-fetching functions in `cache.data()` from `@timber-js/app/cache` when you want caching.
|
|
108
108
|
|
|
109
109
|
4. **Creating `loading.tsx` files** — this convention does not exist. Use `<Suspense>` with an explicit fallback where you want streaming.
|
|
110
110
|
|
|
111
|
-
5. **Using ISR patterns** (`revalidate`, `unstable_cache`) — ISR does not exist. Use `cache()` with `{ ttl: seconds, tags: ['tag'] }`.
|
|
111
|
+
5. **Using ISR patterns** (`revalidate`, `unstable_cache`) — ISR does not exist. Use `cache.data()` with `{ ttl: seconds, tags: ['tag'] }`.
|
|
112
112
|
|
|
113
113
|
6. **Calling `notFound()`** — use `deny(404)` from `@timber-js/app/server` instead. It sends a real HTTP 404 status code.
|
|
114
114
|
|
|
@@ -123,7 +123,7 @@ content-collections.ts # Collection definitions (optional)
|
|
|
123
123
|
```tsx
|
|
124
124
|
import { cache } from '@timber-js/app/cache';
|
|
125
125
|
|
|
126
|
-
const getProducts = cache(
|
|
126
|
+
const getProducts = cache.data(
|
|
127
127
|
async () => db.products.findMany(),
|
|
128
128
|
{ ttl: 300, tags: ['products'] }
|
|
129
129
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.168",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -158,12 +158,13 @@
|
|
|
158
158
|
},
|
|
159
159
|
"dependencies": {
|
|
160
160
|
"@opentelemetry/api": "^1.9.1",
|
|
161
|
-
"@opentelemetry/context-async-hooks": "^2.
|
|
162
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
161
|
+
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
162
|
+
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
163
163
|
"cookie": "^1.1.1",
|
|
164
|
+
"jsonc-parser": "^3.3.1",
|
|
164
165
|
"magic-string": "^0.30.21",
|
|
165
166
|
"nitro": "3.0.260610-beta",
|
|
166
|
-
"srvx": "^0.11.
|
|
167
|
+
"srvx": "^0.11.22"
|
|
167
168
|
},
|
|
168
169
|
"peerDependencies": {
|
|
169
170
|
"@content-collections/core": "^0.14.0 || ^0.15.0",
|
|
@@ -175,7 +176,7 @@
|
|
|
175
176
|
"nuqs": "^2.0.0",
|
|
176
177
|
"react": "19.2.7",
|
|
177
178
|
"react-dom": "19.2.7",
|
|
178
|
-
"vite": "
|
|
179
|
+
"vite": "8.1.0",
|
|
179
180
|
"wrangler": "^4.0.0",
|
|
180
181
|
"zod": "^3.22.0 || ^4.0.0"
|
|
181
182
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CacheHandler } from '../cache/index';
|
|
2
2
|
import { getCloudflareBindings } from './cloudflare';
|
|
3
|
+
import { warnLossyJsonValue } from '../cache/json-lossy-check.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Cloudflare Workers KV interface — the subset of KVNamespace we depend on.
|
|
@@ -117,6 +118,7 @@ export class CloudflareKVCacheHandler implements CacheHandler {
|
|
|
117
118
|
value: unknown,
|
|
118
119
|
opts: { ttl: number; tags: string[]; generation?: number }
|
|
119
120
|
): Promise<void> {
|
|
121
|
+
warnLossyJsonValue(value, key);
|
|
120
122
|
const kv = this.getKV();
|
|
121
123
|
const entry: KVCacheEntry = {
|
|
122
124
|
value,
|
|
@@ -127,12 +129,16 @@ export class CloudflareKVCacheHandler implements CacheHandler {
|
|
|
127
129
|
// KV expirationTtl minimum is 60s. Use 2× TTL (min 60s) so stale entries
|
|
128
130
|
// survive for staleWhileRevalidate background refetches.
|
|
129
131
|
const kvTtl = Math.max(opts.ttl * 2, 60);
|
|
130
|
-
await kv.put(this.dataKey(key), JSON.stringify(entry), { expirationTtl: kvTtl });
|
|
131
132
|
|
|
132
|
-
//
|
|
133
|
-
// indexes
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
// Serialize upfront so BigInt/non-serializable errors throw before any
|
|
134
|
+
// writes — prevents tag indexes pointing at stale data from a prior set().
|
|
135
|
+
const payload = JSON.stringify(entry);
|
|
136
|
+
|
|
137
|
+
// Tag indexes are written BEFORE the data key so that a partial network
|
|
138
|
+
// failure on the data write never leaves an entry invisible to
|
|
139
|
+
// invalidate({tag}). A dangling tag-index reference to a non-existent
|
|
140
|
+
// data key is harmless — invalidate() re-checks stored tags and cleans
|
|
141
|
+
// up on the next pass.
|
|
136
142
|
const now = Date.now();
|
|
137
143
|
const kvExpiresAt = now + kvTtl * 1000;
|
|
138
144
|
await Promise.all(
|
|
@@ -145,6 +151,8 @@ export class CloudflareKVCacheHandler implements CacheHandler {
|
|
|
145
151
|
});
|
|
146
152
|
})
|
|
147
153
|
);
|
|
154
|
+
|
|
155
|
+
await kv.put(this.dataKey(key), payload, { expirationTtl: kvTtl });
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
async invalidate(opts: { key?: string; tag?: string }): Promise<void> {
|
|
@@ -175,7 +183,20 @@ export class CloudflareKVCacheHandler implements CacheHandler {
|
|
|
175
183
|
if (opts.tag) {
|
|
176
184
|
const entries = parseTagIndex(await kv.get(this.tagKey(opts.tag), { type: 'json' }));
|
|
177
185
|
if (entries.length > 0) {
|
|
178
|
-
|
|
186
|
+
// Re-check each member's stored tags before deleting — the tag index
|
|
187
|
+
// can contain stale references from partial writes or entries whose
|
|
188
|
+
// tags changed since they were indexed. Mirrors Redis handler behavior.
|
|
189
|
+
await Promise.all(
|
|
190
|
+
entries.map(async (e) => {
|
|
191
|
+
const raw = (await kv.get(this.dataKey(e.key), {
|
|
192
|
+
type: 'json',
|
|
193
|
+
})) as KVCacheEntry | null;
|
|
194
|
+
if (!raw) return;
|
|
195
|
+
if (raw.tags?.includes(opts.tag!)) {
|
|
196
|
+
await kv.delete(this.dataKey(e.key));
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
);
|
|
179
200
|
}
|
|
180
201
|
await kv.delete(this.tagKey(opts.tag));
|
|
181
202
|
}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// Primary deployment target. Generates a Workers-compatible entry point
|
|
4
4
|
// and wrangler.jsonc configuration. See design/11-platform.md §"Cloudflare Workers".
|
|
5
5
|
|
|
6
|
-
import { writeFile, readFile, cp, rm, readdir } from 'node:fs/promises';
|
|
6
|
+
import { writeFile, readFile, cp, rm, readdir, access } from 'node:fs/promises';
|
|
7
7
|
import { execFile } from 'node:child_process';
|
|
8
8
|
import { join, relative } from 'node:path';
|
|
9
|
+
import { parse as parseJsonc } from 'jsonc-parser';
|
|
9
10
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
10
11
|
import type { TimberPlatformAdapter, TimberConfig } from './types';
|
|
11
12
|
import { runSharedBuildSteps } from './build-output-helper.js';
|
|
@@ -158,6 +159,12 @@ export interface CloudflareBindings {
|
|
|
158
159
|
|
|
159
160
|
/** Options for the Cloudflare Workers adapter. */
|
|
160
161
|
export interface CloudflareAdapterOptions {
|
|
162
|
+
/**
|
|
163
|
+
* Worker name used in the generated wrangler.jsonc.
|
|
164
|
+
* @default 'timber-app'
|
|
165
|
+
*/
|
|
166
|
+
name?: string;
|
|
167
|
+
|
|
161
168
|
/**
|
|
162
169
|
* Cloudflare compatibility date.
|
|
163
170
|
* @default Current date in YYYY-MM-DD format at build time.
|
|
@@ -291,8 +298,10 @@ export function cloudflare(options: CloudflareAdapterOptions = {}): TimberPlatfo
|
|
|
291
298
|
const workerEntry = generateWorkerEntry(outDir, outDir, hasManifestInit, hasWorkerHandlers);
|
|
292
299
|
await writeFile(join(outDir, '_worker.js'), workerEntry);
|
|
293
300
|
|
|
294
|
-
// Generate wrangler.jsonc
|
|
295
|
-
const
|
|
301
|
+
// Generate wrangler.jsonc — merges user's wrangler config from disk if present
|
|
302
|
+
const root = config.root ?? process.cwd();
|
|
303
|
+
const userConfig = await readUserWranglerConfig(root);
|
|
304
|
+
const wranglerConfig = generateWranglerConfig(config, options, userConfig);
|
|
296
305
|
await writeFile(join(outDir, 'wrangler.jsonc'), JSON.stringify(wranglerConfig, null, 2));
|
|
297
306
|
},
|
|
298
307
|
|
|
@@ -682,22 +691,40 @@ function isPlainObject(val: unknown): val is Record<string, unknown> {
|
|
|
682
691
|
return val !== null && typeof val === 'object' && !Array.isArray(val);
|
|
683
692
|
}
|
|
684
693
|
|
|
694
|
+
const WRANGLER_CONFIG_FILES = ['wrangler.jsonc', 'wrangler.json'] as const;
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Read the user's wrangler config from the project root, if one exists.
|
|
698
|
+
* Checks wrangler.jsonc first, then wrangler.json.
|
|
699
|
+
* @internal Exported for testing.
|
|
700
|
+
*/
|
|
701
|
+
export async function readUserWranglerConfig(
|
|
702
|
+
root: string
|
|
703
|
+
): Promise<Record<string, unknown> | null> {
|
|
704
|
+
for (const filename of WRANGLER_CONFIG_FILES) {
|
|
705
|
+
const filepath = join(root, filename);
|
|
706
|
+
try {
|
|
707
|
+
await access(filepath);
|
|
708
|
+
} catch {
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const content = await readFile(filepath, 'utf-8');
|
|
712
|
+
return parseJsonc(content) as Record<string, unknown>;
|
|
713
|
+
}
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
|
|
685
717
|
/** @internal Exported for testing. */
|
|
686
718
|
export function generateWranglerConfig(
|
|
687
719
|
config: TimberConfig,
|
|
688
|
-
options: CloudflareAdapterOptions
|
|
720
|
+
options: CloudflareAdapterOptions,
|
|
721
|
+
userConfig?: Record<string, unknown> | null
|
|
689
722
|
): Record<string, unknown> {
|
|
690
|
-
const compatDate = options.compatibilityDate ?? new Date().toISOString().slice(0, 10);
|
|
691
|
-
|
|
692
|
-
const flags = options.compatibilityFlags ?? ['nodejs_compat'];
|
|
693
|
-
|
|
694
723
|
const base: Record<string, unknown> = {
|
|
695
724
|
name: 'timber-app',
|
|
696
725
|
main: '_worker.js',
|
|
697
|
-
compatibility_date:
|
|
698
|
-
compatibility_flags:
|
|
699
|
-
// The build output is already fully bundled by Vite — skip wrangler's
|
|
700
|
-
// esbuild pass to avoid issues with top-level await and module format.
|
|
726
|
+
compatibility_date: new Date().toISOString().slice(0, 10),
|
|
727
|
+
compatibility_flags: ['nodejs_compat'],
|
|
701
728
|
no_bundle: true,
|
|
702
729
|
find_additional_modules: true,
|
|
703
730
|
rules: [{ type: 'ESModule', globs: ['**/*.js'] }],
|
|
@@ -705,29 +732,40 @@ export function generateWranglerConfig(
|
|
|
705
732
|
directory: './static',
|
|
706
733
|
binding: 'ASSETS',
|
|
707
734
|
},
|
|
708
|
-
// Native trace destination — required for timber's pipeline spans
|
|
709
|
-
// (emitted via tracing.enterSpan in the generated _worker.js) to appear
|
|
710
|
-
// in the Cloudflare Observability dashboard. Gates traces only; does
|
|
711
|
-
// not enable Workers Logs. Override via the `wrangler` escape hatch:
|
|
712
|
-
// wrangler: { observability: { traces: { enabled: false } } }.
|
|
713
735
|
observability: {
|
|
714
736
|
traces: { enabled: true },
|
|
715
737
|
},
|
|
716
738
|
};
|
|
717
739
|
|
|
718
|
-
// Layer 1:
|
|
740
|
+
// Layer 1: user's wrangler.jsonc / wrangler.json from disk
|
|
741
|
+
let merged: Record<string, unknown> = userConfig ? shallowDeepMerge(base, userConfig) : base;
|
|
742
|
+
|
|
743
|
+
// Layer 2: explicit adapter options override disk config
|
|
744
|
+
const explicit: Record<string, unknown> = {};
|
|
745
|
+
if (options.name != null) explicit.name = options.name;
|
|
746
|
+
if (options.compatibilityDate != null) explicit.compatibility_date = options.compatibilityDate;
|
|
747
|
+
if (options.compatibilityFlags != null) explicit.compatibility_flags = options.compatibilityFlags;
|
|
748
|
+
if (Object.keys(explicit).length > 0) {
|
|
749
|
+
merged = { ...merged, ...explicit };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Layer 3: declarative bindings
|
|
719
753
|
const bindingsConfig = generateBindingsConfig(options.bindings);
|
|
720
|
-
|
|
754
|
+
merged = shallowDeepMerge(merged, bindingsConfig as Record<string, unknown>);
|
|
721
755
|
|
|
722
|
-
// Layer
|
|
723
|
-
// A shallow spread would replace entire sections like durable_objects and
|
|
724
|
-
// queues — e.g. adding migrations via wrangler would drop the generated
|
|
725
|
-
// durable_objects.bindings. Deep merge preserves generated keys while
|
|
726
|
-
// letting wrangler override on actual key-level conflicts.
|
|
756
|
+
// Layer 4: wrangler escape hatch — highest priority, deep merge.
|
|
727
757
|
if (options.wrangler) {
|
|
728
|
-
|
|
758
|
+
merged = shallowDeepMerge(merged, options.wrangler);
|
|
729
759
|
}
|
|
730
760
|
|
|
761
|
+
// Generated config targets a single environment — env blocks can override
|
|
762
|
+
// main/assets per-environment and break the build output.
|
|
763
|
+
delete merged.env;
|
|
764
|
+
|
|
765
|
+
// These fields are required for the build output to work correctly.
|
|
766
|
+
merged.main = '_worker.js';
|
|
767
|
+
merged.find_additional_modules = true;
|
|
768
|
+
|
|
731
769
|
return merged;
|
|
732
770
|
}
|
|
733
771
|
|
package/src/adapters/types.ts
CHANGED