@timber-js/app 0.2.0-alpha.157 → 0.2.0-alpha.159
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-CqE7SW7E.js → actions-CBbP1hHO.js} +22 -6
- package/dist/_chunks/actions-CBbP1hHO.js.map +1 -0
- package/dist/_chunks/{build-output-helper-C8q4Zpf5.js → build-output-helper-DF5uScqS.js} +4 -1
- package/dist/_chunks/{build-output-helper-C8q4Zpf5.js.map → build-output-helper-DF5uScqS.js.map} +1 -1
- package/dist/_chunks/cache-api-DsWEYwVZ.js +1120 -0
- package/dist/_chunks/cache-api-DsWEYwVZ.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js → cli-schema-sync-Bivx4Rn7.js} +4 -3
- package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js.map → cli-schema-sync-Bivx4Rn7.js.map} +1 -1
- package/dist/_chunks/dist-BA3u1z3W.js +423 -0
- package/dist/_chunks/dist-BA3u1z3W.js.map +1 -0
- package/dist/_chunks/{logger-C5v1IMuh.js → logger-CrRmZMlo.js} +81 -56
- package/dist/_chunks/logger-CrRmZMlo.js.map +1 -0
- package/dist/_chunks/{plugin-context-DUl3dnCF.js → plugin-context-DeAxFRMq.js} +3 -36
- package/dist/_chunks/plugin-context-DeAxFRMq.js.map +1 -0
- package/dist/_chunks/sizeof-lXffQ1Vr.js +35 -0
- package/dist/_chunks/sizeof-lXffQ1Vr.js.map +1 -0
- package/dist/_chunks/{walkers-D5jCGPum.js → walkers-DXbJkeCY.js} +217 -19
- package/dist/_chunks/walkers-DXbJkeCY.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare.d.ts +28 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +143 -9
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/cache/cache-api.d.ts +2 -0
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/handler-store.d.ts +2 -0
- package/dist/cache/handler-store.d.ts.map +1 -1
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +3 -186
- package/dist/cache/redis-handler.d.ts +17 -5
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts +1 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/store.d.ts +53 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/stores/cloudflare-kv.d.ts +38 -0
- package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -0
- package/dist/cache/stores/cloudflare-kv.js +29 -0
- package/dist/cache/stores/cloudflare-kv.js.map +1 -0
- package/dist/cache/stores/memory.d.ts +42 -0
- package/dist/cache/stores/memory.d.ts.map +1 -0
- package/dist/cache/stores/memory.js +90 -0
- package/dist/cache/stores/memory.js.map +1 -0
- package/dist/cache/stores/redis.d.ts +69 -0
- package/dist/cache/stores/redis.d.ts.map +1 -0
- package/dist/cache/stores/redis.js +35 -0
- package/dist/cache/stores/redis.js.map +1 -0
- package/dist/cache/stores/vercel.d.ts +36 -0
- package/dist/cache/stores/vercel.d.ts.map +1 -0
- package/dist/cache/stores/vercel.js +33 -0
- package/dist/cache/stores/vercel.js.map +1 -0
- package/dist/cache/tag-aware-handler.d.ts +51 -0
- package/dist/cache/tag-aware-handler.d.ts.map +1 -0
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.d.ts +23 -0
- package/dist/cdn/cloudflare-purge.d.ts.map +1 -0
- package/dist/cdn/fastly-purge.d.ts +25 -0
- package/dist/cdn/fastly-purge.d.ts.map +1 -0
- package/dist/cdn/headers.d.ts +82 -0
- package/dist/cdn/headers.d.ts.map +1 -0
- package/dist/cdn/index.d.ts +5 -0
- package/dist/cdn/index.d.ts.map +1 -0
- package/dist/cdn/purge-store.d.ts +23 -0
- package/dist/cdn/purge-store.d.ts.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/history.d.ts +0 -3
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +0 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +11 -86
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +1 -3
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/router.d.ts +1 -4
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +1 -11
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -3
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/config-types.d.ts +3 -1
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/index.js +15 -11
- package/dist/cookies/index.js.map +1 -1
- package/dist/dev-tools/browser-logs.d.ts +28 -72
- package/dist/dev-tools/browser-logs.d.ts.map +1 -1
- package/dist/dev-tools/index.d.ts +1 -1
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/dev-tools/logger.d.ts.map +1 -1
- package/dist/dev-tools/terminal.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts +4 -0
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1269 -359
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +36 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +21 -0
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +5 -14
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/callsite-ast.d.ts +95 -0
- package/dist/plugins/callsite-ast.d.ts.map +1 -0
- package/dist/plugins/callsite-jsx.d.ts +35 -0
- package/dist/plugins/callsite-jsx.d.ts.map +1 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/prebuilt-capture.d.ts +60 -0
- package/dist/plugins/prebuilt-capture.d.ts.map +1 -0
- package/dist/plugins/prebuilt.d.ts +82 -0
- package/dist/plugins/prebuilt.d.ts.map +1 -0
- package/dist/plugins/prerender-sugar.d.ts +42 -0
- package/dist/plugins/prerender-sugar.d.ts.map +1 -0
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/export-detect.d.ts +56 -0
- package/dist/routing/export-detect.d.ts.map +1 -0
- package/dist/routing/index.js +2 -2
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +0 -3
- package/dist/server/actions.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/cookie-parsing.d.ts +15 -1
- package/dist/server/cookie-parsing.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +12 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +1 -0
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.d.ts +6 -1
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +563 -74
- package/dist/server/internal.js.map +1 -1
- package/dist/server/prebuilt/cache-key.d.ts +58 -0
- package/dist/server/prebuilt/cache-key.d.ts.map +1 -0
- package/dist/server/prebuilt/capture-state.d.ts +57 -0
- package/dist/server/prebuilt/capture-state.d.ts.map +1 -0
- package/dist/server/prebuilt/manifest-tag-index.d.ts +27 -0
- package/dist/server/prebuilt/manifest-tag-index.d.ts.map +1 -0
- package/dist/server/prebuilt/overlay.d.ts +100 -0
- package/dist/server/prebuilt/overlay.d.ts.map +1 -0
- package/dist/server/prebuilt/payload-source.d.ts +75 -0
- package/dist/server/prebuilt/payload-source.d.ts.map +1 -0
- package/dist/server/prebuilt/synthetic-store.d.ts +64 -0
- package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -0
- package/dist/server/prebuilt-builder.d.ts +110 -0
- package/dist/server/prebuilt-builder.d.ts.map +1 -0
- package/dist/server/prebuilt-runtime.d.ts +101 -0
- package/dist/server/prebuilt-runtime.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +10 -8
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -5
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +10 -0
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- 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 +3 -4
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts +6 -6
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +37 -3
- package/dist/server/tracing.d.ts.map +1 -1
- package/docs/api/32-api-cache.mdx +10 -4
- package/docs/learn/00-introduction.mdx +8 -13
- package/docs/learn/02-pages-and-layouts.mdx +9 -0
- package/docs/learn/06-forms-and-actions.mdx +0 -4
- package/docs/learn/07-typed-routes.mdx +127 -0
- package/docs/learn/09-caching.mdx +12 -1
- package/docs/learn/10-middleware.mdx +1 -0
- package/docs/learn/11-error-handling.mdx +9 -0
- package/docs/learn/13-configuration.mdx +28 -0
- package/docs/learn/14-deploying.mdx +52 -0
- package/docs/more/06-instrumentation.mdx +27 -1
- package/docs/more/07-security.mdx +8 -2
- package/docs/more/08-developer-experience.mdx +134 -0
- package/package.json +31 -2
- package/src/adapters/build-output-helper.ts +10 -0
- package/src/adapters/cloudflare.ts +178 -4
- package/src/cache/cache-api.ts +60 -0
- package/src/cache/handler-store.ts +8 -0
- package/src/cache/index.ts +4 -0
- package/src/cache/redis-handler.ts +25 -13
- package/src/cache/singleflight.ts +11 -11
- package/src/cache/store.ts +71 -0
- package/src/cache/stores/cloudflare-kv.ts +52 -0
- package/src/cache/stores/memory.ts +125 -0
- package/src/cache/stores/redis.ts +97 -0
- package/src/cache/stores/vercel.ts +59 -0
- package/src/cache/tag-aware-handler.ts +233 -0
- package/src/cache/timber-cache.ts +17 -7
- package/src/cdn/cloudflare-purge.ts +50 -0
- package/src/cdn/fastly-purge.ts +63 -0
- package/src/cdn/headers.ts +127 -0
- package/src/cdn/index.ts +16 -0
- package/src/cdn/purge-store.ts +34 -0
- package/src/client/browser-entry/action-dispatch.ts +1 -10
- package/src/client/browser-entry/post-hydration.ts +0 -1
- package/src/client/browser-entry/router-init.ts +0 -5
- package/src/client/history.ts +0 -4
- package/src/client/navigation-api.ts +5 -11
- package/src/client/router.ts +9 -43
- package/src/client/rsc-fetch.ts +5 -27
- package/src/client/segment-cache.ts +1 -4
- package/src/client/use-cookie.ts +18 -12
- package/src/config-types.ts +3 -1
- package/src/config-validation.ts +1 -0
- package/src/dev-tools/browser-logs.ts +37 -273
- package/src/dev-tools/index.ts +2 -2
- package/src/dev-tools/instrumentation.ts +1 -1
- package/src/dev-tools/logger.ts +33 -51
- package/src/dev-tools/terminal.ts +21 -22
- package/src/fonts/transform.ts +4 -43
- package/src/index.ts +16 -3
- package/src/plugin-context.ts +36 -0
- package/src/plugins/build-report.ts +133 -1
- package/src/plugins/cache.ts +32 -131
- package/src/plugins/callsite-ast.ts +167 -0
- package/src/plugins/callsite-jsx.ts +243 -0
- package/src/plugins/entries.ts +23 -4
- package/src/plugins/fonts.ts +16 -2
- package/src/plugins/prebuilt-capture.ts +208 -0
- package/src/plugins/prebuilt.ts +673 -0
- package/src/plugins/prerender-sugar.ts +329 -0
- package/src/plugins/server-bundle.ts +12 -0
- package/src/plugins/shims.ts +9 -0
- package/src/plugins/static-build.ts +0 -1
- package/src/routing/codegen.ts +1 -21
- package/src/routing/convention-lint.ts +85 -49
- package/src/routing/export-detect.ts +304 -0
- package/src/routing/manifest-codegen.ts +2 -11
- package/src/rsc-runtime/rsc.ts +7 -0
- package/src/server/action-handler.ts +11 -27
- package/src/server/actions.ts +3 -4
- package/src/server/als-registry.ts +7 -0
- package/src/server/cookie-parsing.ts +75 -59
- package/src/server/deny-renderer.ts +7 -15
- package/src/server/form-data.ts +23 -7
- package/src/server/html-injectors.ts +11 -3
- package/src/server/internal.ts +17 -1
- package/src/server/prebuilt/cache-key.ts +224 -0
- package/src/server/prebuilt/capture-state.ts +89 -0
- package/src/server/prebuilt/manifest-tag-index.ts +46 -0
- package/src/server/prebuilt/overlay.ts +256 -0
- package/src/server/prebuilt/payload-source.ts +192 -0
- package/src/server/prebuilt/synthetic-store.ts +108 -0
- package/src/server/prebuilt-builder.ts +640 -0
- package/src/server/prebuilt-runtime.ts +706 -0
- package/src/server/route-element-builder.ts +40 -12
- package/src/server/rsc-entry/helpers.ts +0 -17
- package/src/server/rsc-entry/index.ts +83 -0
- package/src/server/rsc-entry/render-route.ts +6 -33
- package/src/server/rsc-entry/revalidate-renderer.ts +16 -11
- package/src/server/rsc-entry/rsc-payload.ts +5 -11
- package/src/server/sitemap-generator.ts +16 -10
- package/src/server/tracing.ts +88 -3
- package/dist/_chunks/actions-CqE7SW7E.js.map +0 -1
- package/dist/_chunks/cache-api-Cbyh3RW4.js +0 -401
- package/dist/_chunks/cache-api-Cbyh3RW4.js.map +0 -1
- package/dist/_chunks/logger-C5v1IMuh.js.map +0 -1
- package/dist/_chunks/plugin-context-DUl3dnCF.js.map +0 -1
- package/dist/_chunks/validation-CnKhGJsp.js +0 -113
- package/dist/_chunks/validation-CnKhGJsp.js.map +0 -1
- package/dist/_chunks/walkers-D5jCGPum.js.map +0 -1
- package/dist/cache/index.js.map +0 -1
- package/dist/client/head.d.ts +0 -16
- package/dist/client/head.d.ts.map +0 -1
- package/dist/client/navigation-api-types.d.ts +0 -90
- package/dist/client/navigation-api-types.d.ts.map +0 -1
- package/dist/server/client-module-map.d.ts +0 -47
- package/dist/server/client-module-map.d.ts.map +0 -1
- package/dist/shared/head-header.d.ts +0 -54
- package/dist/shared/head-header.d.ts.map +0 -1
- package/src/client/head.ts +0 -61
- package/src/client/navigation-api-types.ts +0 -112
- package/src/server/client-module-map.ts +0 -58
- package/src/shared/head-header.ts +0 -69
|
@@ -196,6 +196,58 @@ export default {
|
|
|
196
196
|
|
|
197
197
|
Pre-renders every route to HTML at build time. Add `clientJavascript: false` for zero-JavaScript output. Deploy to any static file host — S3, CloudFront, Cloudflare Pages, GitHub Pages, Netlify, or any HTTP server.
|
|
198
198
|
|
|
199
|
+
## CDN Integration
|
|
200
|
+
|
|
201
|
+
timber.js provides typed helpers for CDN caching headers and tag-based purging. CDN caching is always developer-elected — the framework never sets cache headers automatically.
|
|
202
|
+
|
|
203
|
+
### Header Helpers
|
|
204
|
+
|
|
205
|
+
```ts title="app/proxy.ts"
|
|
206
|
+
import { cacheControl, surrogateKey } from '@timber-js/app/cdn';
|
|
207
|
+
|
|
208
|
+
export default async function proxy(ctx) {
|
|
209
|
+
if (ctx.pathname.startsWith('/products/')) {
|
|
210
|
+
ctx.headers.set('Cache-Control', cacheControl({
|
|
211
|
+
public: true,
|
|
212
|
+
maxAge: 600,
|
|
213
|
+
staleWhileRevalidate: 60,
|
|
214
|
+
}));
|
|
215
|
+
ctx.headers.set('Surrogate-Key', surrogateKey(['products']));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Purge on `revalidateTag`
|
|
221
|
+
|
|
222
|
+
Configure a CDN purge handler so `revalidateTag()` also purges the CDN:
|
|
223
|
+
|
|
224
|
+
```ts title="timber.config.ts"
|
|
225
|
+
import { CloudflarePurge } from '@timber-js/app/cdn/cloudflare';
|
|
226
|
+
|
|
227
|
+
export default {
|
|
228
|
+
cdnPurge: new CloudflarePurge({
|
|
229
|
+
zoneId: process.env.CF_ZONE_ID!,
|
|
230
|
+
apiToken: process.env.CF_API_TOKEN!,
|
|
231
|
+
}),
|
|
232
|
+
};
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Fastly:
|
|
236
|
+
|
|
237
|
+
```ts title="timber.config.ts"
|
|
238
|
+
import { FastlyPurge } from '@timber-js/app/cdn/fastly';
|
|
239
|
+
|
|
240
|
+
export default {
|
|
241
|
+
cdnPurge: new FastlyPurge({
|
|
242
|
+
serviceId: process.env.FASTLY_SERVICE_ID!,
|
|
243
|
+
apiToken: process.env.FASTLY_API_TOKEN!,
|
|
244
|
+
softPurge: true,
|
|
245
|
+
}),
|
|
246
|
+
};
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
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.
|
|
250
|
+
|
|
199
251
|
## Cache Handler Selection
|
|
200
252
|
|
|
201
253
|
| Deployment | Recommended Handler | Why |
|
|
@@ -115,7 +115,7 @@ const sid = getSpanId(); // "1234567890abcdef" or undefined
|
|
|
115
115
|
|
|
116
116
|
### `withSpan(name, attributes, fn)`
|
|
117
117
|
|
|
118
|
-
Run a function within an OTEL span
|
|
118
|
+
Run a function within a span. Emits an OTEL span when an SDK is active, and a native platform span on Cloudflare (see [Cloudflare Native Traces](#cloudflare-native-traces)). With neither, the function runs directly with zero overhead.
|
|
119
119
|
|
|
120
120
|
```ts
|
|
121
121
|
import { withSpan } from '@timber-js/app/server';
|
|
@@ -144,6 +144,32 @@ import { addSpanEvent } from '@timber-js/app/server';
|
|
|
144
144
|
await addSpanEvent('cache.hit', { key: 'user:123' });
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
## Cloudflare Native Traces
|
|
148
|
+
|
|
149
|
+
When you deploy with the Cloudflare adapter, timber's pipeline spans (`timber.proxy`, `timber.middleware`, `timber.access`, `timber.render`, `timber.action`, …) appear **natively in the Cloudflare Observability dashboard** — no OTEL SDK required. They nest alongside Cloudflare's auto-instrumented D1, KV, and `fetch` spans with the same attributes timber emits over OTEL (`http.request.method`, `url.path`, `http.route`, `http.response.status_code`, `timber.result`, …).
|
|
150
|
+
|
|
151
|
+
This works out of the box:
|
|
152
|
+
|
|
153
|
+
- The generated `_worker.js` registers timber's spans with the Workers custom spans API (`tracing.enterSpan()` from `cloudflare:workers`).
|
|
154
|
+
- The generated `wrangler.jsonc` enables the trace destination (`observability.traces.enabled`).
|
|
155
|
+
|
|
156
|
+
To turn native traces off, use the `wrangler` escape hatch:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// timber.config.ts
|
|
160
|
+
import { cloudflare } from '@timber-js/app/adapters/cloudflare';
|
|
161
|
+
|
|
162
|
+
export default {
|
|
163
|
+
adapter: cloudflare({
|
|
164
|
+
wrangler: { observability: { traces: { enabled: false } } },
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Native emission is additive — if you also initialize an OTEL SDK in `register()`, spans are emitted on both channels, so external collectors (Honeycomb, Jaeger, Datadog) keep working. `timber.cache` HIT/MISS **span events** are OTEL-only; the Workers span API has no event equivalent.
|
|
170
|
+
|
|
171
|
+
On runtimes without the custom spans API (older compatibility dates or an outdated local `wrangler`), the worker silently falls back to OTEL-only emission.
|
|
172
|
+
|
|
147
173
|
## Server-Timing
|
|
148
174
|
|
|
149
175
|
timber.js can emit `Server-Timing` headers for browser DevTools performance inspection.
|
|
@@ -60,6 +60,8 @@ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
|
|
60
60
|
|
|
61
61
|
## Redirect Safety
|
|
62
62
|
|
|
63
|
+
Open redirect vulnerabilities let an attacker craft a URL on your domain that redirects users to a malicious site (e.g., `/login?next=https://evil.com`). timber prevents this structurally — `redirect()` cannot produce an external redirect.
|
|
64
|
+
|
|
63
65
|
### `redirect()` is relative-only
|
|
64
66
|
|
|
65
67
|
The `redirect()` function only accepts relative paths. Absolute URLs and protocol-relative URLs are rejected:
|
|
@@ -72,11 +74,11 @@ redirect('https://evil.com'); // ❌ throws — absolute URL
|
|
|
72
74
|
redirect('//evil.com'); // ❌ throws — protocol-relative
|
|
73
75
|
```
|
|
74
76
|
|
|
75
|
-
This
|
|
77
|
+
This applies everywhere — middleware, access checks, server actions, and server components. There is no code path where `redirect()` can send users off-site.
|
|
76
78
|
|
|
77
79
|
### `redirectExternal()` with allow-list
|
|
78
80
|
|
|
79
|
-
When you need to redirect to an external URL, use `redirectExternal()` with an explicit origin allow-list. Only `http:` and `https:` schemes are permitted:
|
|
81
|
+
When you legitimately need to redirect to an external URL (e.g., an OAuth provider), use `redirectExternal()` with an explicit origin allow-list. Only `http:` and `https:` schemes are permitted:
|
|
80
82
|
|
|
81
83
|
```ts
|
|
82
84
|
import { redirectExternal } from '@timber-js/app/server';
|
|
@@ -89,6 +91,10 @@ redirectExternal('https://docs.example.com/guide', ['https://docs.example.com'],
|
|
|
89
91
|
|
|
90
92
|
If the target origin isn't in the allow-list, the scheme isn't http(s), or the URL is invalid, the call throws. This gives you external redirects without the open redirect risk.
|
|
91
93
|
|
|
94
|
+
<Callout variant="tip" title="vs. Next.js">
|
|
95
|
+
Next.js `redirect()` accepts absolute URLs by default. In timber, external redirects require an explicit allow-list — open redirect bugs are structurally impossible.
|
|
96
|
+
</Callout>
|
|
97
|
+
|
|
92
98
|
## Header Immutability
|
|
93
99
|
|
|
94
100
|
Request headers are immutable after the request enters the pipeline:
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Developer Experience'
|
|
3
|
+
description: 'Dev logging, hydration diffs, Server-Timing headers, build reports, and more.'
|
|
4
|
+
slug: 'developer-experience'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Developer Experience
|
|
8
|
+
|
|
9
|
+
timber.js includes a suite of development tools that surface what your app is doing — where time is spent, what's cached, where errors come from — without reaching for external profiling tools.
|
|
10
|
+
|
|
11
|
+
## Dev Logging
|
|
12
|
+
|
|
13
|
+
Every request in development emits a structured tree to `stderr` showing the full execution pipeline with timing:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
POST /dashboard/projects/123 trace_id: 4bf92f3577b34da6a3ce929d0e0e4736
|
|
17
|
+
├─ [proxy] proxy.ts 0ms → 2ms
|
|
18
|
+
├─ [rsc] middleware.ts 2ms → 4ms
|
|
19
|
+
│ ├── fired: requireUser() (timber.cache prefetch)
|
|
20
|
+
│ ├── fired: getProject("123") (timber.cache prefetch)
|
|
21
|
+
│ └── fired: getTaskCounts("123") (timber.cache prefetch)
|
|
22
|
+
├─ [rsc] render 4ms
|
|
23
|
+
│ ├─ [rsc] AccessGate (authenticated) 4ms → 5ms
|
|
24
|
+
│ │ └── requireUser() timber.cache HIT <1ms
|
|
25
|
+
│ ├─ [rsc] ProjectPage 8ms → 12ms
|
|
26
|
+
│ │ └── getTaskCounts("123") timber.cache HIT <1ms
|
|
27
|
+
│ └── onShellReady 12ms
|
|
28
|
+
├─ [ssr] hydration render 13ms → 18ms
|
|
29
|
+
└─ ✓ 200 OK total 18ms
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The tree mirrors the execution structure. `[rsc]`/`[ssr]`/`[client]` labels show which Vite environment each phase runs in. Cache hits and misses are annotated inline.
|
|
33
|
+
|
|
34
|
+
### Fetch Instrumentation
|
|
35
|
+
|
|
36
|
+
`fetch()` calls from server components appear as children of the component that made them:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
├─ [rsc] page / 6ms → 101ms
|
|
40
|
+
│ ├─ fetch GET https://api.example.com/products 12ms → 89ms (77ms)
|
|
41
|
+
│ └─ fetch GET https://api.example.com/user 12ms → 45ms (33ms) [cdn: HIT]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Start times reveal whether fetches ran concurrently or sequentially. Cache status from `X-Cache` or `CF-Cache-Status` headers is surfaced automatically. Fetch instrumentation is dev-only — `globalThis.fetch` is not patched in production.
|
|
45
|
+
|
|
46
|
+
### Log Modes
|
|
47
|
+
|
|
48
|
+
Control the verbosity with `TIMBER_DEV_LOG`:
|
|
49
|
+
|
|
50
|
+
| Mode | Output |
|
|
51
|
+
| --------- | ---------------------------------------------------------- |
|
|
52
|
+
| `tree` | Full indented tree per request (default) |
|
|
53
|
+
| `verbose` | Detailed tree showing every component render |
|
|
54
|
+
| `summary` | One line per request: `POST /path → 200 OK 18ms` |
|
|
55
|
+
| `json` | Chronological NDJSON of all spans with full attributes |
|
|
56
|
+
|
|
57
|
+
Suppress all dev logging with `TIMBER_DEV_QUIET=1`.
|
|
58
|
+
|
|
59
|
+
### Slow Phase Warnings
|
|
60
|
+
|
|
61
|
+
Phases that exceed a configurable threshold are highlighted in the tree output. The default is 200ms:
|
|
62
|
+
|
|
63
|
+
```ts title="timber.config.ts"
|
|
64
|
+
export default {
|
|
65
|
+
dev: {
|
|
66
|
+
slowPhaseMs: 200,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This surfaces performance bottlenecks during development without requiring explicit profiling.
|
|
72
|
+
|
|
73
|
+
## Server-Timing Header
|
|
74
|
+
|
|
75
|
+
timber.js emits a `Server-Timing` HTTP header for every response. The level of detail is configurable:
|
|
76
|
+
|
|
77
|
+
| Value | Output | Default in |
|
|
78
|
+
| ------------ | -------------------------------------------------------- | ----------- |
|
|
79
|
+
| `'detailed'` | Per-phase breakdown: proxy, middleware, render, SSR | Development |
|
|
80
|
+
| `'total'` | Single `total;dur=N` entry | Production |
|
|
81
|
+
| `false` | No header | — |
|
|
82
|
+
|
|
83
|
+
```ts title="timber.config.ts"
|
|
84
|
+
export default {
|
|
85
|
+
serverTiming: 'detailed',
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
In production, you can opt into `'detailed'` for APM integration — browser DevTools and monitoring tools parse `Server-Timing` automatically.
|
|
90
|
+
|
|
91
|
+
## Hydration Mismatch Diff
|
|
92
|
+
|
|
93
|
+
When React 19 detects a hydration mismatch, timber parses the diff and renders it with color-coded lines:
|
|
94
|
+
|
|
95
|
+
- Green (`+`) lines show the client-side markup
|
|
96
|
+
- Red (`-`) lines show the server-side markup
|
|
97
|
+
|
|
98
|
+
This replaces Vite's plain-text rendering of hydration errors, making mismatches easy to spot and fix. Multiple hydration errors accumulate in the same overlay.
|
|
99
|
+
|
|
100
|
+
## Error Overlay
|
|
101
|
+
|
|
102
|
+
Errors during development are shown in a browser overlay with:
|
|
103
|
+
|
|
104
|
+
- **Component stacks** — the React component hierarchy leading to the error
|
|
105
|
+
- **Phase labels** — whether the error occurred in middleware, access check, RSC render, SSR, or a client component
|
|
106
|
+
- **Source-mapped stack traces** — pointing to your original source files
|
|
107
|
+
|
|
108
|
+
Client-side errors are forwarded to the overlay via Vite's HMR channel, so they get the same treatment as server errors.
|
|
109
|
+
|
|
110
|
+
## Compiling Indicator
|
|
111
|
+
|
|
112
|
+
A small "Compiling…" indicator appears in the bottom-left corner during HMR updates. It has a 200ms debounce — fast updates never show it. It fades out when the update completes.
|
|
113
|
+
|
|
114
|
+
## Build Report
|
|
115
|
+
|
|
116
|
+
After `timber build`, a route table shows every route with its bundle size, route type, and first-load JS:
|
|
117
|
+
|
|
118
|
+
| Symbol | Meaning |
|
|
119
|
+
| ------ | -------------- |
|
|
120
|
+
| ○ | Static page |
|
|
121
|
+
| λ | Dynamic (SSR) |
|
|
122
|
+
| ƒ | API route |
|
|
123
|
+
|
|
124
|
+
This helps identify large bundles and verify route classification.
|
|
125
|
+
|
|
126
|
+
## HTTPS / HTTP-2 Dev Server
|
|
127
|
+
|
|
128
|
+
timber supports `server.https` in your Vite config for local HTTPS development. The request scheme is derived from the TLS socket (not hardcoded), so CSRF protection works correctly under HTTPS. HTTP/2 pseudo-headers are handled automatically.
|
|
129
|
+
|
|
130
|
+
## What's Next
|
|
131
|
+
|
|
132
|
+
- [Instrumentation](/docs/instrumentation) — production tracing with OpenTelemetry
|
|
133
|
+
- [Configuration](/docs/configuration) — all config options
|
|
134
|
+
- [Streaming](/docs/streaming) — Suspense placement and `deferSuspenseFor`
|
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.159",
|
|
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",
|
|
@@ -68,6 +68,34 @@
|
|
|
68
68
|
"types": "./dist/cache/index.d.ts",
|
|
69
69
|
"import": "./dist/cache/index.js"
|
|
70
70
|
},
|
|
71
|
+
"./cache/stores/memory": {
|
|
72
|
+
"types": "./dist/cache/stores/memory.d.ts",
|
|
73
|
+
"import": "./dist/cache/stores/memory.js"
|
|
74
|
+
},
|
|
75
|
+
"./cache/stores/redis": {
|
|
76
|
+
"types": "./dist/cache/stores/redis.d.ts",
|
|
77
|
+
"import": "./dist/cache/stores/redis.js"
|
|
78
|
+
},
|
|
79
|
+
"./cache/stores/cloudflare-kv": {
|
|
80
|
+
"types": "./dist/cache/stores/cloudflare-kv.d.ts",
|
|
81
|
+
"import": "./dist/cache/stores/cloudflare-kv.js"
|
|
82
|
+
},
|
|
83
|
+
"./cache/stores/vercel": {
|
|
84
|
+
"types": "./dist/cache/stores/vercel.d.ts",
|
|
85
|
+
"import": "./dist/cache/stores/vercel.js"
|
|
86
|
+
},
|
|
87
|
+
"./cdn": {
|
|
88
|
+
"types": "./dist/cdn/index.d.ts",
|
|
89
|
+
"import": "./dist/cdn/index.js"
|
|
90
|
+
},
|
|
91
|
+
"./cdn/cloudflare": {
|
|
92
|
+
"types": "./dist/cdn/cloudflare-purge.d.ts",
|
|
93
|
+
"import": "./dist/cdn/cloudflare-purge.js"
|
|
94
|
+
},
|
|
95
|
+
"./cdn/fastly": {
|
|
96
|
+
"types": "./dist/cdn/fastly-purge.d.ts",
|
|
97
|
+
"import": "./dist/cdn/fastly-purge.js"
|
|
98
|
+
},
|
|
71
99
|
"./codec": {
|
|
72
100
|
"types": "./dist/codec.d.ts",
|
|
73
101
|
"import": "./dist/codec.js"
|
|
@@ -121,6 +149,7 @@
|
|
|
121
149
|
"@opentelemetry/api": "^1.9.1",
|
|
122
150
|
"@opentelemetry/context-async-hooks": "^2.8.0",
|
|
123
151
|
"@opentelemetry/sdk-trace-base": "^2.8.0",
|
|
152
|
+
"cookie": "^1.1.1",
|
|
124
153
|
"magic-string": "^0.30.21",
|
|
125
154
|
"nitro": "3.0.260610-beta"
|
|
126
155
|
},
|
|
@@ -134,7 +163,7 @@
|
|
|
134
163
|
"nuqs": "^2.0.0",
|
|
135
164
|
"react": "19.2.7",
|
|
136
165
|
"react-dom": "19.2.7",
|
|
137
|
-
"vite": "^8.0
|
|
166
|
+
"vite": "^8.1.0",
|
|
138
167
|
"wrangler": "^4.0.0",
|
|
139
168
|
"zod": "^3.22.0 || ^4.0.0"
|
|
140
169
|
},
|
|
@@ -89,6 +89,16 @@ export async function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise
|
|
|
89
89
|
}
|
|
90
90
|
);
|
|
91
91
|
|
|
92
|
+
// 4b. Copy prebuilt/ flight payloads if present (cache.component artifacts).
|
|
93
|
+
// These are produced by the post-build capture pass (design/44) and read
|
|
94
|
+
// at request time via the adapter's PrebuiltPayloadSource.
|
|
95
|
+
await cp(join(buildDir, 'prebuilt'), join(outDir, 'prebuilt'), { recursive: true }).catch(
|
|
96
|
+
(e: NodeJS.ErrnoException) => {
|
|
97
|
+
// No prebuilt dir when the app has no cache.component calls
|
|
98
|
+
if (e.code !== 'ENOENT') throw e;
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
92
102
|
// 5. Write manifest-init module if present.
|
|
93
103
|
if (config.manifestInit) {
|
|
94
104
|
await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);
|
|
@@ -3,7 +3,7 @@
|
|
|
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 } from 'node:fs/promises';
|
|
6
|
+
import { writeFile, readFile, cp, rm, readdir } from 'node:fs/promises';
|
|
7
7
|
import { execFile } from 'node:child_process';
|
|
8
8
|
import { join, relative } from 'node:path';
|
|
9
9
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
@@ -228,6 +228,16 @@ export function cloudflare(options: CloudflareAdapterOptions = {}): TimberPlatfo
|
|
|
228
228
|
async buildOutput(config: TimberConfig, buildDir: string) {
|
|
229
229
|
const outDir = join(buildDir, 'cloudflare');
|
|
230
230
|
|
|
231
|
+
// Clean stale prebuilt artifacts BEFORE the shared copy step.
|
|
232
|
+
// The shared step uses cp (merge, not replace), so old .flight files
|
|
233
|
+
// from removed/renamed components would persist and get picked up by
|
|
234
|
+
// find_additional_modules. Also clean static/prebuilt/ from pre-TIM-1162
|
|
235
|
+
// builds where prebuilt was copied into static assets.
|
|
236
|
+
await rm(join(outDir, 'prebuilt'), { recursive: true, force: true }).catch(() => {});
|
|
237
|
+
await rm(join(outDir, 'static', 'prebuilt'), { recursive: true, force: true }).catch(
|
|
238
|
+
() => {}
|
|
239
|
+
);
|
|
240
|
+
|
|
231
241
|
// Steps 1–5: shared across all adapters (mkdir, copy client/rsc/ssr,
|
|
232
242
|
// write _headers, write manifest-init).
|
|
233
243
|
await runSharedBuildSteps({
|
|
@@ -256,13 +266,25 @@ export function cloudflare(options: CloudflareAdapterOptions = {}): TimberPlatfo
|
|
|
256
266
|
emptyOutDir: false,
|
|
257
267
|
minify: false,
|
|
258
268
|
rollupOptions: {
|
|
259
|
-
external: [/^node:/],
|
|
269
|
+
external: [/^node:/, /^cloudflare:/],
|
|
260
270
|
},
|
|
261
271
|
},
|
|
262
272
|
});
|
|
263
273
|
hasWorkerHandlers = true;
|
|
264
274
|
}
|
|
265
275
|
|
|
276
|
+
// Embed prebuilt flight payloads directly into the worker module
|
|
277
|
+
// (TIM-1162). Previously, prebuilt/ was copied into static assets and
|
|
278
|
+
// read via env.ASSETS.fetch() — but that made .flight files and
|
|
279
|
+
// manifest.json publicly fetchable, bypassing middleware and access
|
|
280
|
+
// gates. Now payloads are imported as native wrangler Data/Text modules
|
|
281
|
+
// from outDir/prebuilt/ (outside static/), keeping them server-side only.
|
|
282
|
+
const prebuiltData = await readPrebuiltData(buildDir);
|
|
283
|
+
await writeFile(
|
|
284
|
+
join(outDir, '_timber-prebuilt-source.js'),
|
|
285
|
+
generatePrebuiltSourceModule(prebuiltData)
|
|
286
|
+
);
|
|
287
|
+
|
|
266
288
|
// Generate the Workers entry point
|
|
267
289
|
const hasManifestInit = !!config.manifestInit;
|
|
268
290
|
const workerEntry = generateWorkerEntry(outDir, outDir, hasManifestInit, hasWorkerHandlers);
|
|
@@ -320,6 +342,10 @@ export function generateWorkerEntry(
|
|
|
320
342
|
// Do not edit — this file is regenerated on each build.
|
|
321
343
|
|
|
322
344
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
345
|
+
// Side-effect import: sets globalThis.__TIMBER_PREBUILT_SOURCE__ before the
|
|
346
|
+
// RSC handler evaluates. Must be a static import (not body code) so ESM
|
|
347
|
+
// evaluation order guarantees it runs before ./rsc/index.js.
|
|
348
|
+
import './_timber-prebuilt-source.js'
|
|
323
349
|
${manifestImport}import handler, { runWithWaitUntil } from '${rscEntryRelative}'
|
|
324
350
|
${handlersImport}
|
|
325
351
|
// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.
|
|
@@ -336,8 +362,35 @@ if (!bindingsAls) {
|
|
|
336
362
|
globalThis[ALS_KEY] = bindingsAls
|
|
337
363
|
}
|
|
338
364
|
|
|
365
|
+
// Register timber's platform tracer so pipeline phase spans (timber.proxy,
|
|
366
|
+
// timber.middleware, timber.render, timber.access, timber.action, …) appear
|
|
367
|
+
// as native Cloudflare spans in the Observability dashboard, nested under
|
|
368
|
+
// the auto-instrumented fetch/KV/D1 spans. Requires observability.traces
|
|
369
|
+
// enabled in wrangler.jsonc (generated by default).
|
|
370
|
+
//
|
|
371
|
+
// Lazily imported and guarded: runtimes without the custom spans API
|
|
372
|
+
// (older compatibility dates, older local workerd) fall back to OTEL-only
|
|
373
|
+
// emission. withSpan() in the framework composes both channels.
|
|
374
|
+
const TRACER_KEY = Symbol.for('timber:platform-tracer')
|
|
375
|
+
let tracerInit
|
|
376
|
+
function initPlatformTracer() {
|
|
377
|
+
tracerInit ??= import('cloudflare:workers')
|
|
378
|
+
.then(({ tracing }) => {
|
|
379
|
+
if (tracing && typeof tracing.enterSpan === 'function') {
|
|
380
|
+
globalThis[TRACER_KEY] ??= {
|
|
381
|
+
enterSpan: (name, fn) => tracing.enterSpan(name, fn),
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
.catch(() => {})
|
|
386
|
+
return tracerInit
|
|
387
|
+
}
|
|
388
|
+
|
|
339
389
|
export default {
|
|
340
390
|
${handlersSpread} async fetch(request, env, ctx) {
|
|
391
|
+
// One-time on first request; resolved-promise await thereafter.
|
|
392
|
+
await initPlatformTracer()
|
|
393
|
+
|
|
341
394
|
// Edge-cache GET/HEAD responses via the Cache API. RSC navigation
|
|
342
395
|
// requests (Accept: text/x-component) are excluded — they vary by
|
|
343
396
|
// X-Timber-State-Tree which the cache key doesn't capture.
|
|
@@ -379,6 +432,99 @@ ${handlersSpread} async fetch(request, env, ctx) {
|
|
|
379
432
|
`;
|
|
380
433
|
}
|
|
381
434
|
|
|
435
|
+
/** Build-time prebuilt file list read from `.timber/dist/prebuilt/`. */
|
|
436
|
+
export interface PrebuiltModuleData {
|
|
437
|
+
/** Payload file paths relative to prebuilt/ (e.g. "comp_abc/deadbeef.flight"). */
|
|
438
|
+
payloadFiles: string[];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Discover prebuilt flight payload files from the build directory.
|
|
443
|
+
* Returns null when no prebuilt artifacts exist (no cache.component usage).
|
|
444
|
+
*
|
|
445
|
+
* @internal Exported for testing.
|
|
446
|
+
*/
|
|
447
|
+
export async function readPrebuiltData(buildDir: string): Promise<PrebuiltModuleData | null> {
|
|
448
|
+
const prebuiltDir = join(buildDir, 'prebuilt');
|
|
449
|
+
try {
|
|
450
|
+
await readFile(join(prebuiltDir, 'manifest.json'));
|
|
451
|
+
} catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
const payloadFiles: string[] = [];
|
|
455
|
+
try {
|
|
456
|
+
const componentDirs = await readdir(prebuiltDir, { withFileTypes: true });
|
|
457
|
+
for (const dir of componentDirs) {
|
|
458
|
+
if (!dir.isDirectory()) continue;
|
|
459
|
+
const files = await readdir(join(prebuiltDir, dir.name));
|
|
460
|
+
for (const file of files) {
|
|
461
|
+
if (file.endsWith('.flight')) {
|
|
462
|
+
payloadFiles.push(`${dir.name}/${file}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
} catch {
|
|
467
|
+
// No subdirectories — manifest exists but no payloads
|
|
468
|
+
}
|
|
469
|
+
return { payloadFiles };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Generate the prebuilt payload source module for Cloudflare Workers.
|
|
474
|
+
*
|
|
475
|
+
* Imports prebuilt flight payloads as native wrangler Data/Text modules
|
|
476
|
+
* from the worker's module graph (NOT from static assets — TIM-1162).
|
|
477
|
+
* Files live at `outDir/prebuilt/` which is outside `static/` and thus
|
|
478
|
+
* not publicly fetchable. Wrangler discovers them via
|
|
479
|
+
* `find_additional_modules: true` with Data/Text rules.
|
|
480
|
+
*
|
|
481
|
+
* This is a separate ESM module so it evaluates before the RSC handler —
|
|
482
|
+
* static imports are hoisted, guaranteeing `globalThis.__TIMBER_PREBUILT_SOURCE__`
|
|
483
|
+
* is set before `createRequestHandler()` reads it.
|
|
484
|
+
*
|
|
485
|
+
* @internal Exported for testing.
|
|
486
|
+
*/
|
|
487
|
+
export function generatePrebuiltSourceModule(data: PrebuiltModuleData | null): string {
|
|
488
|
+
if (!data) {
|
|
489
|
+
return `// Generated by @timber-js/app/adapters/cloudflare
|
|
490
|
+
// No prebuilt artifacts — all cache.component wrappers render dynamically.
|
|
491
|
+
globalThis.__TIMBER_PREBUILT_SOURCE__ = {
|
|
492
|
+
async loadManifest() { return null },
|
|
493
|
+
async readPayload() { return null },
|
|
494
|
+
}
|
|
495
|
+
`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const imports: string[] = [];
|
|
499
|
+
const entries: string[] = [];
|
|
500
|
+
|
|
501
|
+
imports.push(`import _manifestText from './prebuilt/manifest.json'`);
|
|
502
|
+
|
|
503
|
+
for (let i = 0; i < data.payloadFiles.length; i++) {
|
|
504
|
+
const file = data.payloadFiles[i];
|
|
505
|
+
imports.push(`import _p${i} from './prebuilt/${file}'`);
|
|
506
|
+
entries.push(` ${JSON.stringify(file)}: new Uint8Array(_p${i})`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return `// Generated by @timber-js/app/adapters/cloudflare
|
|
510
|
+
// Prebuilt payload source — imports cache.component flight payloads as
|
|
511
|
+
// native wrangler modules from the worker bundle (TIM-1162). Files live
|
|
512
|
+
// at outDir/prebuilt/, outside static/, so they are not publicly fetchable.
|
|
513
|
+
${imports.join('\n')}
|
|
514
|
+
|
|
515
|
+
const _manifest = JSON.parse(_manifestText)
|
|
516
|
+
|
|
517
|
+
const _payloads = {
|
|
518
|
+
${entries.join(',\n')}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
globalThis.__TIMBER_PREBUILT_SOURCE__ = {
|
|
522
|
+
async loadManifest() { return _manifest },
|
|
523
|
+
async readPayload(file) { return _payloads[file] ?? null },
|
|
524
|
+
}
|
|
525
|
+
`;
|
|
526
|
+
}
|
|
527
|
+
|
|
382
528
|
/** Wrangler binding sections generated from `CloudflareBindings`. */
|
|
383
529
|
export interface WranglerBindingsConfig {
|
|
384
530
|
kv_namespaces?: Array<{ binding: string; id: string }>;
|
|
@@ -517,6 +663,11 @@ export function generateWranglerConfig(
|
|
|
517
663
|
|
|
518
664
|
const flags = options.compatibilityFlags ?? ['nodejs_compat'];
|
|
519
665
|
|
|
666
|
+
const PREBUILT_MODULE_RULES = [
|
|
667
|
+
{ type: 'Text' as const, globs: ['prebuilt/**/*.json'] },
|
|
668
|
+
{ type: 'Data' as const, globs: ['prebuilt/**/*.flight'] },
|
|
669
|
+
];
|
|
670
|
+
|
|
520
671
|
const base: Record<string, unknown> = {
|
|
521
672
|
name: 'timber-app',
|
|
522
673
|
main: '_worker.js',
|
|
@@ -526,9 +677,18 @@ export function generateWranglerConfig(
|
|
|
526
677
|
// esbuild pass to avoid issues with top-level await and module format.
|
|
527
678
|
no_bundle: true,
|
|
528
679
|
find_additional_modules: true,
|
|
529
|
-
rules: [{ type: 'ESModule', globs: ['**/*.js'] }],
|
|
680
|
+
rules: [{ type: 'ESModule', globs: ['**/*.js'] }, ...PREBUILT_MODULE_RULES],
|
|
530
681
|
assets: {
|
|
531
682
|
directory: './static',
|
|
683
|
+
binding: 'ASSETS',
|
|
684
|
+
},
|
|
685
|
+
// Native trace destination — required for timber's pipeline spans
|
|
686
|
+
// (emitted via tracing.enterSpan in the generated _worker.js) to appear
|
|
687
|
+
// in the Cloudflare Observability dashboard. Gates traces only; does
|
|
688
|
+
// not enable Workers Logs. Override via the `wrangler` escape hatch:
|
|
689
|
+
// wrangler: { observability: { traces: { enabled: false } } }.
|
|
690
|
+
observability: {
|
|
691
|
+
traces: { enabled: true },
|
|
532
692
|
},
|
|
533
693
|
};
|
|
534
694
|
|
|
@@ -542,7 +702,21 @@ export function generateWranglerConfig(
|
|
|
542
702
|
// durable_objects.bindings. Deep merge preserves generated keys while
|
|
543
703
|
// letting wrangler override on actual key-level conflicts.
|
|
544
704
|
if (options.wrangler) {
|
|
545
|
-
|
|
705
|
+
const result = shallowDeepMerge(merged, options.wrangler);
|
|
706
|
+
// Ensure required prebuilt module rules survive a user `rules` override.
|
|
707
|
+
// Without this, `wrangler: { rules: [...] }` replaces the entire array
|
|
708
|
+
// and drops the Text/Data rules that _timber-prebuilt-source.js needs.
|
|
709
|
+
if (options.wrangler.rules) {
|
|
710
|
+
const userRules = result.rules as Array<Record<string, unknown>>;
|
|
711
|
+
for (const required of PREBUILT_MODULE_RULES) {
|
|
712
|
+
const exists = userRules.some(
|
|
713
|
+
(r) =>
|
|
714
|
+
r.type === required.type && JSON.stringify(r.globs) === JSON.stringify(required.globs)
|
|
715
|
+
);
|
|
716
|
+
if (!exists) userRules.push(required);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return result;
|
|
546
720
|
}
|
|
547
721
|
|
|
548
722
|
return merged;
|
package/src/cache/cache-api.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { CacheOptions } from './index';
|
|
2
|
+
import type { PrebuiltComponentOptions } from '../server/prebuilt-runtime';
|
|
2
3
|
import { createCache } from './timber-cache';
|
|
3
4
|
import { getCacheHandler } from './handler-store';
|
|
4
5
|
import { recordInvalidation } from './invalidation-epoch';
|
|
6
|
+
import { writeTombstonesForTag } from '../server/prebuilt/overlay.js';
|
|
7
|
+
import { getCdnPurgeHandler } from '../cdn/purge-store.js';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Public caching API: `cache(fn, opts)`.
|
|
@@ -44,4 +47,61 @@ cache.invalidate = async function invalidate(opts: { key?: string; tag?: string
|
|
|
44
47
|
// post-resolve set instead of resurrecting the invalidated value (TIM-1028).
|
|
45
48
|
recordInvalidation(opts);
|
|
46
49
|
await getCacheHandler().invalidate(opts);
|
|
50
|
+
// Write tombstones for build-seed entries carrying the tag (design/45
|
|
51
|
+
// §ISR mechanics). Runs AFTER handler.invalidate so the tombstone
|
|
52
|
+
// overwrites the cleared overlay slot. This covers ALL invalidation
|
|
53
|
+
// paths — actions, webhooks, route handlers — not just executeAction.
|
|
54
|
+
if (opts.tag) {
|
|
55
|
+
await writeTombstonesForTag(opts.tag);
|
|
56
|
+
}
|
|
57
|
+
// Trigger CDN purge if a handler is configured (design/25 §Layer 2).
|
|
58
|
+
// Purge is best-effort: a CDN API failure or timeout must not block
|
|
59
|
+
// cache invalidation. 10s ceiling prevents a hung CDN API from stalling
|
|
60
|
+
// the entire revalidateTag/action flow.
|
|
61
|
+
if (opts.tag) {
|
|
62
|
+
const purgeHandler = getCdnPurgeHandler();
|
|
63
|
+
if (purgeHandler) {
|
|
64
|
+
try {
|
|
65
|
+
await Promise.race([
|
|
66
|
+
purgeHandler.purgeTags([opts.tag]),
|
|
67
|
+
new Promise<void>((_, reject) =>
|
|
68
|
+
setTimeout(() => reject(new Error('CDN purge timed out (10s)')), 10_000)
|
|
69
|
+
),
|
|
70
|
+
]);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error('[timber] CDN purge failed for tag:', err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const componentFallbackWarned = new WeakSet<object>();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Runtime fallback for `cache.component(...)` callsites the timber-prebuilt
|
|
82
|
+
* transform does not rewrite — nested/computed/argument-position forms (see
|
|
83
|
+
* plugins/prebuilt.ts). Only module-scope `const X = cache.component(...)`
|
|
84
|
+
* declarations are transformed and eligible for prebuilding; everything else
|
|
85
|
+
* lands here, warns once per component, and renders dynamically. Same
|
|
86
|
+
* safety-net pattern as the data cache's runtime fnId fallback (TIM-1054):
|
|
87
|
+
* an untraceable callsite degrades in performance, never in correctness.
|
|
88
|
+
*
|
|
89
|
+
* The full public surface (types, docs, `timber` namespace) is TIM-1121;
|
|
90
|
+
* design/45-cache-lifetimes.md specifies the options.
|
|
91
|
+
*/
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
cache.component = function component<C extends (props: any) => unknown>(
|
|
94
|
+
componentFn: C,
|
|
95
|
+
_options?: PrebuiltComponentOptions
|
|
96
|
+
): C {
|
|
97
|
+
if (!componentFallbackWarned.has(componentFn)) {
|
|
98
|
+
componentFallbackWarned.add(componentFn);
|
|
99
|
+
const name = componentFn.name || 'anonymous component';
|
|
100
|
+
console.warn(
|
|
101
|
+
`[timber] cache.component: "${name}" was not transformed at build time — ` +
|
|
102
|
+
`rendering dynamically. Only module-scope declarations ` +
|
|
103
|
+
`(const X = cache.component(...)) are prebuilt.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return componentFn;
|
|
47
107
|
};
|