devflare 1.0.0-next.0 → 1.0.0-next.10
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/LLM.md +1114 -0
- package/R2.md +200 -0
- package/README.md +285 -514
- package/bin/devflare.js +8 -8
- package/dist/{account-rvrj687w.js → account-8psavtg6.js} +27 -4
- package/dist/bridge/miniflare.d.ts +6 -0
- package/dist/bridge/miniflare.d.ts.map +1 -1
- package/dist/bridge/proxy.d.ts +5 -6
- package/dist/bridge/proxy.d.ts.map +1 -1
- package/dist/bridge/server.d.ts.map +1 -1
- package/dist/browser.d.ts +50 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/{build-mnf6v8gd.js → build-k36xrzvy.js} +26 -7
- package/dist/bundler/do-bundler.d.ts +7 -0
- package/dist/bundler/do-bundler.d.ts.map +1 -1
- package/dist/cli/commands/account.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/deploy.d.ts.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/types.d.ts.map +1 -1
- package/dist/cli/config-path.d.ts +5 -0
- package/dist/cli/config-path.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/package-metadata.d.ts +16 -0
- package/dist/cli/package-metadata.d.ts.map +1 -0
- package/dist/config/compiler.d.ts +7 -0
- package/dist/config/compiler.d.ts.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/schema.d.ts +2575 -1221
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/{deploy-nhceck39.js → deploy-dbvfq8vq.js} +33 -15
- package/dist/{dev-qnxet3j9.js → dev-rk8p6pse.js} +900 -234
- package/dist/dev-server/miniflare-log.d.ts +12 -0
- package/dist/dev-server/miniflare-log.d.ts.map +1 -0
- package/dist/dev-server/runtime-stdio.d.ts +8 -0
- package/dist/dev-server/runtime-stdio.d.ts.map +1 -0
- package/dist/dev-server/server.d.ts +2 -0
- package/dist/dev-server/server.d.ts.map +1 -1
- package/dist/dev-server/vite-utils.d.ts +37 -0
- package/dist/dev-server/vite-utils.d.ts.map +1 -0
- package/dist/{doctor-e8fy6fj5.js → doctor-06y8nxd4.js} +73 -50
- package/dist/{durable-object-t4kbb0yt.js → durable-object-yt8v1dyn.js} +1 -1
- package/dist/index-05fyzwne.js +195 -0
- package/dist/index-1p814k7s.js +227 -0
- package/dist/{index-hcex3rgh.js → index-1phx14av.js} +84 -7
- package/dist/{index-tk6ej9dj.js → index-2q3pmzrx.js} +12 -16
- package/dist/{index-pf5s73n9.js → index-59df49vn.js} +11 -281
- package/dist/index-5yxg30va.js +304 -0
- package/dist/index-62b3gt2g.js +12 -0
- package/dist/index-6h8xbs75.js +44 -0
- package/dist/{index-67qcae0f.js → index-6v3wjg1r.js} +16 -1
- package/dist/index-8gtqgb3q.js +529 -0
- package/dist/{index-gz1gndna.js → index-9wt9x09k.js} +42 -62
- package/dist/index-fef08w43.js +231 -0
- package/dist/{index-ep3445yc.js → index-jht2j546.js} +393 -170
- package/dist/index-k7r18na8.js +0 -0
- package/dist/{index-m2q41jwa.js → index-n932ytmq.js} +9 -1
- package/dist/index-pwgyy2q9.js +39 -0
- package/dist/{index-07q6yxyc.js → index-v8vvsn9x.js} +1 -0
- package/dist/index-vky23txa.js +70 -0
- package/dist/index-vs49yxn4.js +322 -0
- package/dist/{index-z14anrqp.js → index-wfbfz02q.js} +14 -15
- package/dist/index-ws68xvq2.js +311 -0
- package/dist/index-y1d8za14.js +196 -0
- package/dist/{init-f9mgmew3.js → init-na2atvz2.js} +42 -55
- package/dist/router/types.d.ts +24 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/runtime/context.d.ts +249 -8
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/exports.d.ts +50 -55
- package/dist/runtime/exports.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +8 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/middleware.d.ts +77 -60
- package/dist/runtime/middleware.d.ts.map +1 -1
- package/dist/runtime/router.d.ts +7 -0
- package/dist/runtime/router.d.ts.map +1 -0
- package/dist/runtime/validation.d.ts +1 -1
- package/dist/runtime/validation.d.ts.map +1 -1
- package/dist/src/browser.js +150 -0
- package/dist/src/cli/index.js +10 -0
- package/dist/{cloudflare → src/cloudflare}/index.js +3 -3
- package/dist/{decorators → src/decorators}/index.js +2 -2
- package/dist/src/index.js +132 -0
- package/dist/src/runtime/index.js +111 -0
- package/dist/{sveltekit → src/sveltekit}/index.js +14 -6
- package/dist/{test → src/test}/index.js +22 -13
- package/dist/{vite → src/vite}/index.js +128 -59
- package/dist/sveltekit/platform.d.ts.map +1 -1
- package/dist/test/bridge-context.d.ts +5 -2
- package/dist/test/bridge-context.d.ts.map +1 -1
- package/dist/test/cf.d.ts +25 -11
- package/dist/test/cf.d.ts.map +1 -1
- package/dist/test/email.d.ts +16 -7
- package/dist/test/email.d.ts.map +1 -1
- package/dist/test/queue.d.ts.map +1 -1
- package/dist/test/resolve-service-bindings.d.ts.map +1 -1
- package/dist/test/scheduled.d.ts.map +1 -1
- package/dist/test/simple-context.d.ts +1 -1
- package/dist/test/simple-context.d.ts.map +1 -1
- package/dist/test/tail.d.ts +2 -1
- package/dist/test/tail.d.ts.map +1 -1
- package/dist/test/worker.d.ts +6 -0
- package/dist/test/worker.d.ts.map +1 -1
- package/dist/transform/durable-object.d.ts.map +1 -1
- package/dist/transform/worker-entrypoint.d.ts.map +1 -1
- package/dist/{types-5nyrz1sz.js → types-x9q7t491.js} +30 -16
- package/dist/utils/entrypoint-discovery.d.ts +6 -3
- package/dist/utils/entrypoint-discovery.d.ts.map +1 -1
- package/dist/utils/send-email.d.ts +15 -0
- package/dist/utils/send-email.d.ts.map +1 -0
- package/dist/vite/plugin.d.ts.map +1 -1
- package/dist/worker-entry/composed-worker.d.ts +13 -0
- package/dist/worker-entry/composed-worker.d.ts.map +1 -0
- package/dist/worker-entry/routes.d.ts +22 -0
- package/dist/worker-entry/routes.d.ts.map +1 -0
- package/dist/{worker-entrypoint-m9th0rg0.js → worker-entrypoint-c259fmfs.js} +1 -1
- package/package.json +22 -19
- package/dist/index.js +0 -298
- package/dist/runtime/index.js +0 -111
package/LLM.md
ADDED
|
@@ -0,0 +1,1114 @@
|
|
|
1
|
+
# Devflare
|
|
2
|
+
|
|
3
|
+
This file is the truth-first contract for `devflare` as implemented today.
|
|
4
|
+
|
|
5
|
+
Use it when you are:
|
|
6
|
+
|
|
7
|
+
- generating code
|
|
8
|
+
- reviewing code
|
|
9
|
+
- updating docs
|
|
10
|
+
|
|
11
|
+
Use `Quick start` for the safest defaults, `Trust map` when similar layers are getting mixed together, and `Sharp edges` before documenting advanced behavior. If an example and the implementation disagree, trust the implementation and update the docs.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick start: safest supported path
|
|
16
|
+
|
|
17
|
+
Prefer this shape unless you have a concrete reason not to:
|
|
18
|
+
|
|
19
|
+
- explicit `files.fetch`
|
|
20
|
+
- `src/fetch.ts` as the main HTTP entry
|
|
21
|
+
- a named event-first `fetch(event)` or `handle(event)` export
|
|
22
|
+
- request-wide middleware via `sequence(...)`
|
|
23
|
+
- explicit bindings in config
|
|
24
|
+
- `createTestContext()` for core integration tests
|
|
25
|
+
- `ref()` for cross-worker composition
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { defineConfig } from 'devflare'
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
name: 'hello-worker',
|
|
32
|
+
compatibilityDate: '2026-03-17',
|
|
33
|
+
files: {
|
|
34
|
+
fetch: 'src/fetch.ts'
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import type { FetchEvent } from 'devflare/runtime'
|
|
41
|
+
|
|
42
|
+
export async function fetch(event: FetchEvent): Promise<Response> {
|
|
43
|
+
return Response.json({
|
|
44
|
+
path: new URL(event.request.url).pathname
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Trust map
|
|
52
|
+
|
|
53
|
+
### Layers that are easy to confuse
|
|
54
|
+
|
|
55
|
+
Keep these layers separate until you have a reason to connect them:
|
|
56
|
+
|
|
57
|
+
1. config-time `process.env`
|
|
58
|
+
2. `devflare.config.ts`
|
|
59
|
+
3. generated `wrangler.jsonc`
|
|
60
|
+
4. runtime Worker `env` bindings
|
|
61
|
+
5. local dev/test helpers
|
|
62
|
+
|
|
63
|
+
The classic mixups are:
|
|
64
|
+
|
|
65
|
+
- `files.routes` vs top-level `routes`
|
|
66
|
+
- `vars` vs `secrets`
|
|
67
|
+
- Bun `.env` loading vs runtime secret loading
|
|
68
|
+
- Devflare `config.env` overrides vs Wrangler environment blocks
|
|
69
|
+
- main-entry `env` vs runtime `env`
|
|
70
|
+
|
|
71
|
+
### Source of truth vs generated output
|
|
72
|
+
|
|
73
|
+
Treat these as generated output, not authoring input:
|
|
74
|
+
|
|
75
|
+
- `.devflare/`
|
|
76
|
+
- `env.d.ts`
|
|
77
|
+
- generated `wrangler.jsonc`
|
|
78
|
+
|
|
79
|
+
The source of truth is still:
|
|
80
|
+
|
|
81
|
+
- `devflare.config.ts`
|
|
82
|
+
- your source files under `src/`
|
|
83
|
+
- your tests
|
|
84
|
+
|
|
85
|
+
If generated output looks wrong, fix the source and regenerate it. Do not hand-edit generated artifacts.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## What Devflare is
|
|
90
|
+
|
|
91
|
+
Devflare is a developer-first layer on top of Cloudflare Workers tooling.
|
|
92
|
+
|
|
93
|
+
It composes existing tools instead of replacing them:
|
|
94
|
+
|
|
95
|
+
- **Miniflare** supplies the local Workers runtime
|
|
96
|
+
- **Wrangler** supplies deployment config and deploy workflows
|
|
97
|
+
- **Vite** participates when the current package opts into Vite-backed mode
|
|
98
|
+
- **Bun** is the CLI runtime and affects process-level `.env` loading
|
|
99
|
+
- **Devflare** ties those pieces into one authoring model
|
|
100
|
+
|
|
101
|
+
Core public capabilities today:
|
|
102
|
+
|
|
103
|
+
- typed config via `defineConfig()`
|
|
104
|
+
- compilation from Devflare config into Wrangler-compatible output
|
|
105
|
+
- event-first runtime helpers
|
|
106
|
+
- request-scoped proxies and per-surface getters
|
|
107
|
+
- typed multi-worker references via `ref()`
|
|
108
|
+
- local orchestration around Miniflare, Vite, and test helpers
|
|
109
|
+
- framework-aware Vite and SvelteKit integration
|
|
110
|
+
|
|
111
|
+
Devflare is not a replacement runtime. It is a higher-level developer system that sits on top of the Cloudflare ecosystem.
|
|
112
|
+
|
|
113
|
+
### Authoring model
|
|
114
|
+
|
|
115
|
+
A **surface** is a distinct handler or entry file that Devflare treats as its own concern. The common surfaces are:
|
|
116
|
+
|
|
117
|
+
- HTTP via `src/fetch.ts`
|
|
118
|
+
- queue consumers via `src/queue.ts`
|
|
119
|
+
- scheduled handlers via `src/scheduled.ts`
|
|
120
|
+
- incoming email via `src/email.ts`
|
|
121
|
+
- Durable Objects via `do.*.ts`
|
|
122
|
+
- WorkerEntrypoints via `ep.*.ts`
|
|
123
|
+
- workflows via `wf.*.ts`
|
|
124
|
+
- custom transport definitions via `src/transport.ts`
|
|
125
|
+
|
|
126
|
+
Prefer one responsibility per file unless you have a strong reason to combine surfaces. That keeps runtime behavior, testing, and multi-worker composition easier to reason about.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Package entrypoints
|
|
131
|
+
|
|
132
|
+
Use the narrowest import path that matches where the code runs.
|
|
133
|
+
|
|
134
|
+
| Import | Use it for | Practical rule |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `devflare` | main package entrypoint | config helpers, `ref()`, `workerName`, the main-entry `env`, and selected Node-side helpers |
|
|
137
|
+
| `devflare/runtime` | handler/runtime code | event types, middleware, strict `env` / `ctx` / `event` / `locals`, and per-surface getters |
|
|
138
|
+
| `devflare/test` | tests | `createTestContext()`, `cf.*`, and test helpers |
|
|
139
|
+
| `devflare/vite` | Vite integration | explicit Vite-side helpers |
|
|
140
|
+
| `devflare/sveltekit` | SvelteKit integration | SvelteKit-facing helpers |
|
|
141
|
+
| `devflare/cloudflare` | Cloudflare account and resource helpers | account/resource/usage helpers |
|
|
142
|
+
| `devflare/decorators` | decorators only | `durableObject()` and related decorator utilities |
|
|
143
|
+
|
|
144
|
+
### `devflare` vs `devflare/runtime`
|
|
145
|
+
|
|
146
|
+
Default rule:
|
|
147
|
+
|
|
148
|
+
1. use handler parameters first
|
|
149
|
+
2. use `devflare/runtime` in helpers that run inside a live handler trail
|
|
150
|
+
3. use the main `devflare` entry when you specifically want the fallback-friendly `env`
|
|
151
|
+
|
|
152
|
+
`import { env } from 'devflare/runtime'` is strict request-scoped access. It works only while Devflare has established an active handler context.
|
|
153
|
+
|
|
154
|
+
`import { env } from 'devflare'` is the main-entry proxy. It prefers active handler context and can also fall back to test or bridge-backed context when no live request is active.
|
|
155
|
+
|
|
156
|
+
Practical example:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// worker code
|
|
160
|
+
import { env, locals, type FetchEvent } from 'devflare/runtime'
|
|
161
|
+
|
|
162
|
+
export async function fetch(event: FetchEvent<DevflareEnv>): Promise<Response> {
|
|
163
|
+
const pathname = new URL(event.request.url).pathname
|
|
164
|
+
locals.startedAt = Date.now()
|
|
165
|
+
|
|
166
|
+
const cached = await env.CACHE.get(pathname)
|
|
167
|
+
if (cached) {
|
|
168
|
+
return new Response(cached, {
|
|
169
|
+
headers: {
|
|
170
|
+
'x-cache': 'hit'
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return new Response(`miss:${pathname}`)
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// test or bridge code
|
|
181
|
+
import { env } from 'devflare'
|
|
182
|
+
import { createTestContext } from 'devflare/test'
|
|
183
|
+
|
|
184
|
+
await createTestContext()
|
|
185
|
+
await env.CACHE.put('health', 'ok')
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Worker-safe caveat for the main entry
|
|
189
|
+
|
|
190
|
+
`devflare/runtime` is the explicit worker-safe runtime entry and should be the default teaching path for worker code.
|
|
191
|
+
|
|
192
|
+
The main `devflare` entry can also resolve to a worker-safe bundle when the resolver selects the package `browser` condition. Treat that as a compatibility detail, not as a signal that the main entry is the best place to import runtime helpers.
|
|
193
|
+
|
|
194
|
+
In that worker-safe main bundle:
|
|
195
|
+
|
|
196
|
+
- the browser-safe subset of the main entry remains usable
|
|
197
|
+
- Node-side APIs such as config loading, CLI helpers, Miniflare orchestration, and test-context setup are not available and intentionally throw if called
|
|
198
|
+
|
|
199
|
+
If you need `ctx`, `event`, `locals`, middleware, or per-surface getters, import them from `devflare/runtime`.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Runtime and HTTP model
|
|
204
|
+
|
|
205
|
+
### Event-first handlers are the public story
|
|
206
|
+
|
|
207
|
+
Fresh Devflare code should be event-first. Use shapes like:
|
|
208
|
+
|
|
209
|
+
- `fetch(event: FetchEvent)`
|
|
210
|
+
- `queue(event: QueueEvent)`
|
|
211
|
+
- `scheduled(event: ScheduledEvent)`
|
|
212
|
+
- `email(event: EmailEvent)`
|
|
213
|
+
- `tail(event: TailEvent)` when you are wiring a tail surface
|
|
214
|
+
- Durable Object handlers with their matching event types
|
|
215
|
+
|
|
216
|
+
These event types augment native Cloudflare inputs rather than replacing them with unrelated wrappers.
|
|
217
|
+
|
|
218
|
+
| Event type | Also behaves like | Convenience fields |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| `FetchEvent` | `Request` | `request`, `env`, `ctx`, `params`, `locals`, `type` |
|
|
221
|
+
| `QueueEvent` | `MessageBatch<T>` | `batch`, `env`, `ctx`, `locals`, `type` |
|
|
222
|
+
| `ScheduledEvent` | `ScheduledController` | `controller`, `env`, `ctx`, `locals`, `type` |
|
|
223
|
+
| `EmailEvent` | `ForwardableEmailMessage` | `message`, `env`, `ctx`, `locals`, `type` |
|
|
224
|
+
| `TailEvent` | `TraceItem[]` | `events`, `env`, `ctx`, `locals`, `type` |
|
|
225
|
+
| `DurableObjectFetchEvent` | `Request` | `request`, `env`, `ctx`, `state`, `locals`, `type` |
|
|
226
|
+
| `DurableObjectAlarmEvent` | event object only | `env`, `ctx`, `state`, `locals`, `type` |
|
|
227
|
+
| `DurableObjectWebSocketMessageEvent` | `WebSocket` | `ws`, `message`, `env`, `ctx`, `state`, `locals`, `type` |
|
|
228
|
+
| `DurableObjectWebSocketCloseEvent` | `WebSocket` | `ws`, `code`, `reason`, `wasClean`, `env`, `ctx`, `state`, `locals`, `type` |
|
|
229
|
+
| `DurableObjectWebSocketErrorEvent` | `WebSocket` | `ws`, `error`, `env`, `ctx`, `state`, `locals`, `type` |
|
|
230
|
+
|
|
231
|
+
On worker surfaces, `event.ctx` is the current `ExecutionContext`.
|
|
232
|
+
|
|
233
|
+
On Durable Object surfaces, `event.ctx` is the current `DurableObjectState`, and Devflare also exposes it as `event.state` for clarity.
|
|
234
|
+
|
|
235
|
+
### Runtime access: parameters, getters, and proxies
|
|
236
|
+
|
|
237
|
+
Prefer runtime access in this order:
|
|
238
|
+
|
|
239
|
+
1. handler parameters such as `fetch(event: FetchEvent)`
|
|
240
|
+
2. per-surface getters when a deeper helper needs the current concrete surface
|
|
241
|
+
3. generic runtime proxies when surface-agnostic access is enough
|
|
242
|
+
|
|
243
|
+
Devflare carries the active event through the current handler call trail, so deeper helpers can recover the current surface without manually threading arguments.
|
|
244
|
+
|
|
245
|
+
Proxy semantics:
|
|
246
|
+
|
|
247
|
+
- `env`, `ctx`, and `event` from `devflare/runtime` are readonly
|
|
248
|
+
- `locals` is the mutable request-scoped storage object
|
|
249
|
+
- `event.locals` and `locals` point at the same underlying object
|
|
250
|
+
- strict runtime helpers throw when there is no active Devflare-managed context
|
|
251
|
+
|
|
252
|
+
Per-surface getters:
|
|
253
|
+
|
|
254
|
+
- worker surfaces: `getFetchEvent()`, `getQueueEvent()`, `getScheduledEvent()`, `getEmailEvent()`, `getTailEvent()`
|
|
255
|
+
- Durable Object surfaces: `getDurableObjectEvent()`, `getDurableObjectFetchEvent()`, `getDurableObjectAlarmEvent()`
|
|
256
|
+
- Durable Object WebSocket surfaces: `getDurableObjectWebSocketMessageEvent()`, `getDurableObjectWebSocketCloseEvent()`, `getDurableObjectWebSocketErrorEvent()`
|
|
257
|
+
|
|
258
|
+
Every getter also exposes `.safe()`, which returns `null` instead of throwing.
|
|
259
|
+
|
|
260
|
+
Practical example:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { getFetchEvent, locals, type FetchEvent } from 'devflare/runtime'
|
|
264
|
+
|
|
265
|
+
function currentPath(): string {
|
|
266
|
+
return new URL(getFetchEvent().request.url).pathname
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function fetch(event: FetchEvent): Promise<Response> {
|
|
270
|
+
locals.requestId = crypto.randomUUID()
|
|
271
|
+
|
|
272
|
+
return Response.json({
|
|
273
|
+
path: currentPath(),
|
|
274
|
+
requestId: String(locals.requestId),
|
|
275
|
+
requestUrl: getFetchEvent.safe()?.request.url ?? null,
|
|
276
|
+
method: event.request.method
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Manual context helpers are advanced/internal
|
|
282
|
+
|
|
283
|
+
Normal Devflare application code should **not** need `runWithEventContext()` or `runWithContext()`.
|
|
284
|
+
|
|
285
|
+
Devflare establishes the active event/context automatically before invoking user code in the flows developers normally use:
|
|
286
|
+
|
|
287
|
+
- generated worker entrypoints for `fetch`, `queue`, `scheduled`, and `email`
|
|
288
|
+
- Durable Object wrappers
|
|
289
|
+
- HTTP middleware and route resolution
|
|
290
|
+
- `createTestContext()` helpers such as `cf.worker`, `cf.queue`, `cf.scheduled`, `cf.email`, and `cf.tail`
|
|
291
|
+
|
|
292
|
+
That means getters like `getQueueEvent()`, `getScheduledEvent()`, `getEmailEvent()`, and `getDurableObjectFetchEvent()` should work inside ordinary handlers without manual wrapping.
|
|
293
|
+
|
|
294
|
+
Keep these helpers in the "advanced escape hatch" bucket:
|
|
295
|
+
|
|
296
|
+
- `runWithEventContext(event, fn)` preserves the exact event you provide
|
|
297
|
+
- `runWithContext(env, ctx, request, fn, type)` is a lower-level compatibility helper
|
|
298
|
+
|
|
299
|
+
Important difference:
|
|
300
|
+
|
|
301
|
+
- `runWithContext()` only synthesizes rich augmented events for `fetch` and `durable-object-fetch` when a `Request` is available
|
|
302
|
+
- if you are manually constructing a non-fetch surface and truly need per-surface getters outside normal Devflare-managed entrypoints, `runWithEventContext()` is the correct low-level helper
|
|
303
|
+
|
|
304
|
+
### HTTP entry, middleware, and method handlers
|
|
305
|
+
|
|
306
|
+
Think of the built-in HTTP path today as:
|
|
307
|
+
|
|
308
|
+
`src/fetch.ts` request-wide entry → same-module method handlers → matched `src/routes/**` leaf module
|
|
309
|
+
|
|
310
|
+
When the HTTP surface matters to build or deploy output, prefer making it explicit in config:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
files: {
|
|
314
|
+
fetch: 'src/fetch.ts'
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The file router is enabled automatically when a `src/routes` directory exists, unless you set `files.routes: false`.
|
|
319
|
+
Use `files.routes` when you want to change the route root or mount it under a prefix.
|
|
320
|
+
|
|
321
|
+
Supported primary entry shapes today:
|
|
322
|
+
|
|
323
|
+
- `export async function fetch(...) { ... }`
|
|
324
|
+
- `export const fetch = ...`
|
|
325
|
+
- `export const handle = ...`
|
|
326
|
+
- `export default async function (...) { ... }`
|
|
327
|
+
- `export default { fetch(...) { ... } }`
|
|
328
|
+
- `export default { handle(...) { ... } }`
|
|
329
|
+
|
|
330
|
+
`fetch` and `handle` are aliases for the same primary HTTP entry. Export one or the other, not both.
|
|
331
|
+
|
|
332
|
+
When this document says a `handle` export here, it means the primary fetch export name, not the legacy `handle(...handlers)` helper.
|
|
333
|
+
|
|
334
|
+
Request-wide middleware belongs in `src/fetch.ts` and composes with `sequence(...)`.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { sequence } from 'devflare/runtime'
|
|
338
|
+
import type { FetchEvent, ResolveFetch } from 'devflare/runtime'
|
|
339
|
+
|
|
340
|
+
async function authHandle(event: FetchEvent, resolve: ResolveFetch): Promise<Response> {
|
|
341
|
+
if (!event.request.headers.get('authorization')) {
|
|
342
|
+
return new Response('Unauthorized', { status: 401 })
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return resolve(event)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function appFetch({ request }: FetchEvent): Promise<Response> {
|
|
349
|
+
return new Response(new URL(request.url).pathname)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export const handle = sequence(authHandle, appFetch)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`sequence(...)` uses the usual nested middleware flow:
|
|
356
|
+
|
|
357
|
+
1. outer middleware before `resolve(event)`
|
|
358
|
+
2. inner middleware before `resolve(event)`
|
|
359
|
+
3. downstream leaf handler
|
|
360
|
+
4. inner middleware after `resolve(event)`
|
|
361
|
+
5. outer middleware after `resolve(event)`
|
|
362
|
+
|
|
363
|
+
Same-module method exports are real runtime behavior:
|
|
364
|
+
|
|
365
|
+
- method handlers such as `GET`, `POST`, and `ALL` are supported in the fetch module
|
|
366
|
+
- if `HEAD` is not exported, it falls back to `GET` with an empty body
|
|
367
|
+
- this is same-module dispatch, not file-system routing
|
|
368
|
+
- if a module exports a primary `fetch` or `handle` entry and also exports method handlers, the method handlers are the downstream leaf handlers reached through `resolve(event)`
|
|
369
|
+
- if no primary `fetch` or `handle` exists, Devflare can still dispatch directly to same-module method handlers
|
|
370
|
+
|
|
371
|
+
### Routing today
|
|
372
|
+
|
|
373
|
+
`src/routes/**` is now a real built-in router.
|
|
374
|
+
|
|
375
|
+
Default behavior:
|
|
376
|
+
|
|
377
|
+
- if `src/routes` exists, Devflare discovers it automatically
|
|
378
|
+
- `files.routes.dir` changes the route root
|
|
379
|
+
- `files.routes.prefix` mounts the discovered routes under a fixed prefix such as `/api`
|
|
380
|
+
- `files.routes: false` disables file-route discovery
|
|
381
|
+
|
|
382
|
+
Filename conventions:
|
|
383
|
+
|
|
384
|
+
- `index.ts` → directory root
|
|
385
|
+
- `[id].ts` → single dynamic segment
|
|
386
|
+
- `[...slug].ts` → rest segment, one or more path parts
|
|
387
|
+
- `[[...slug]].ts` → optional rest segment, including the directory root
|
|
388
|
+
- files or directories beginning with `_` are ignored so route-local helpers can live beside handlers
|
|
389
|
+
|
|
390
|
+
Dispatch semantics:
|
|
391
|
+
|
|
392
|
+
- route params are populated on `event.params`
|
|
393
|
+
- route modules use the same handler forms as fetch modules: HTTP method exports, primary `fetch`, or primary `handle`
|
|
394
|
+
- if `src/fetch.ts` exports same-module `GET` / `POST` / `ALL` handlers, those run before the file router for matching methods
|
|
395
|
+
- for route-tree apps, keep `src/fetch.ts` focused on request-wide middleware and whole-app concerns
|
|
396
|
+
- `resolve(event)` from the primary fetch module falls through to the matched route module when no same-module method handler responded
|
|
397
|
+
|
|
398
|
+
| Key | What it means today | What it does not do |
|
|
399
|
+
|---|---|---|
|
|
400
|
+
| `files.routes` | built-in file router configuration for `src/routes/**` discovery, custom route roots, and optional prefixes | it does not replace top-level Cloudflare deployment `routes` |
|
|
401
|
+
| top-level `routes` | Cloudflare deployment route patterns such as `example.com/*`, compiled into generated Wrangler config | it does not choose handlers inside your app |
|
|
402
|
+
| `wsRoutes` | dev-only WebSocket proxy rules that forward matching local upgrade requests to Durable Objects | it does not replace deployment `routes` or act as general HTTP app routing |
|
|
403
|
+
|
|
404
|
+
Practical route-tree example:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
// devflare.config.ts
|
|
408
|
+
import { defineConfig } from 'devflare'
|
|
409
|
+
|
|
410
|
+
export default defineConfig({
|
|
411
|
+
name: 'api-worker',
|
|
412
|
+
files: {
|
|
413
|
+
fetch: 'src/fetch.ts',
|
|
414
|
+
routes: {
|
|
415
|
+
dir: 'src/routes',
|
|
416
|
+
prefix: '/api'
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
// src/fetch.ts
|
|
424
|
+
import { sequence } from 'devflare/runtime'
|
|
425
|
+
|
|
426
|
+
export const handle = sequence(async (event, resolve) => {
|
|
427
|
+
const response = await resolve(event)
|
|
428
|
+
const next = new Response(response.body, response)
|
|
429
|
+
next.headers.set('x-user-id', event.params.id ?? 'none')
|
|
430
|
+
return next
|
|
431
|
+
})
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
// src/routes/users/[id].ts
|
|
436
|
+
import type { FetchEvent } from 'devflare/runtime'
|
|
437
|
+
|
|
438
|
+
export async function GET({ params }: FetchEvent): Promise<Response> {
|
|
439
|
+
return Response.json({ id: params.id })
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
// GET /api/users/123
|
|
445
|
+
// -> { "id": "123" }
|
|
446
|
+
// and x-user-id: 123
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## Configuration and compilation
|
|
452
|
+
|
|
453
|
+
### Stable config flow
|
|
454
|
+
|
|
455
|
+
This is the stable public flow for Devflare config:
|
|
456
|
+
|
|
457
|
+
1. write `devflare.config.*` and export it with `defineConfig()`
|
|
458
|
+
2. Devflare loads the base config
|
|
459
|
+
3. if you select an environment, Devflare merges `config.env[name]` into the base config
|
|
460
|
+
4. Devflare compiles the resolved config into Wrangler-compatible output
|
|
461
|
+
5. commands such as `dev`, `build`, `deploy`, and `types` use that resolved config for their own work
|
|
462
|
+
|
|
463
|
+
Current implementation note: Devflare validates config against its schema before compilation, and some commands write generated `wrangler.jsonc` files as build artifacts. Treat generated Wrangler config as output, not source of truth.
|
|
464
|
+
|
|
465
|
+
### Supported config files and `defineConfig()`
|
|
466
|
+
|
|
467
|
+
Supported config filenames:
|
|
468
|
+
|
|
469
|
+
- `devflare.config.ts`
|
|
470
|
+
- `devflare.config.mts`
|
|
471
|
+
- `devflare.config.js`
|
|
472
|
+
- `devflare.config.mjs`
|
|
473
|
+
|
|
474
|
+
Use a plain object when your config is static. Use a sync or async function when you need to compute values from `process.env`, the selected environment, or project state.
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
import { defineConfig } from 'devflare'
|
|
478
|
+
|
|
479
|
+
export default defineConfig({
|
|
480
|
+
name: 'api-worker',
|
|
481
|
+
compatibilityDate: '2026-03-17',
|
|
482
|
+
files: {
|
|
483
|
+
fetch: 'src/fetch.ts'
|
|
484
|
+
},
|
|
485
|
+
vars: {
|
|
486
|
+
APP_NAME: 'api-worker'
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
TypeScript note: `defineConfig<T>()` is an advanced typing helper. It is mainly useful when you want stronger `ref()` and entrypoint inference after `env.d.ts` exists.
|
|
492
|
+
|
|
493
|
+
### Top-level keys
|
|
494
|
+
|
|
495
|
+
Most projects only need a small set of config keys at first. Start with `name`, `compatibilityDate`, `files`, and the `bindings`, `vars`, `secrets`, `triggers`, `routes`, or `env` your worker actually needs.
|
|
496
|
+
|
|
497
|
+
Current defaults worth knowing:
|
|
498
|
+
|
|
499
|
+
- `compatibilityDate` defaults to the current date when omitted
|
|
500
|
+
- current default compatibility flags include `nodejs_compat` and `nodejs_als`
|
|
501
|
+
|
|
502
|
+
Common keys:
|
|
503
|
+
|
|
504
|
+
| Key | Use it for |
|
|
505
|
+
|---|---|
|
|
506
|
+
| `name` | Worker name |
|
|
507
|
+
| `compatibilityDate` | Workers compatibility date |
|
|
508
|
+
| `compatibilityFlags` | Workers compatibility flags |
|
|
509
|
+
| `files` | explicit handler paths and discovery globs |
|
|
510
|
+
| `bindings` | Cloudflare bindings |
|
|
511
|
+
| `triggers` | scheduled trigger config |
|
|
512
|
+
| `vars` | non-secret string bindings |
|
|
513
|
+
| `secrets` | secret binding declarations |
|
|
514
|
+
| `routes` | Cloudflare deployment routes |
|
|
515
|
+
| `env` | environment overrides merged before compile |
|
|
516
|
+
|
|
517
|
+
Secondary keys:
|
|
518
|
+
|
|
519
|
+
| Key | Use it for |
|
|
520
|
+
|---|---|
|
|
521
|
+
| `accountId` | account selection for remote and deploy flows |
|
|
522
|
+
| `observability` | Workers observability settings |
|
|
523
|
+
| `migrations` | Durable Object migrations |
|
|
524
|
+
| `assets` | static asset config |
|
|
525
|
+
| `limits` | worker limits |
|
|
526
|
+
| `wsRoutes` | local WebSocket proxy rules for dev |
|
|
527
|
+
| `vite` | Devflare Vite metadata |
|
|
528
|
+
| `rolldown` | worker-only bundler options |
|
|
529
|
+
| `wrangler.passthrough` | raw Wrangler overrides after compile |
|
|
530
|
+
|
|
531
|
+
### Native config coverage vs `wrangler.passthrough`
|
|
532
|
+
|
|
533
|
+
Devflare natively models the common Worker config it actively composes around. It does **not** try to mirror every Wrangler field one-by-one as a first-class Devflare schema key.
|
|
534
|
+
|
|
535
|
+
Use `wrangler.passthrough` for unsupported Wrangler options.
|
|
536
|
+
|
|
537
|
+
Current merge order is:
|
|
538
|
+
|
|
539
|
+
1. compile native Devflare config
|
|
540
|
+
2. shallow-merge `wrangler.passthrough` on top
|
|
541
|
+
3. if the same key exists in both places, the passthrough value wins
|
|
542
|
+
|
|
543
|
+
Practical example:
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
import { defineConfig } from 'devflare'
|
|
547
|
+
|
|
548
|
+
export default defineConfig({
|
|
549
|
+
name: 'advanced-worker',
|
|
550
|
+
files: {
|
|
551
|
+
fetch: 'src/fetch.ts'
|
|
552
|
+
},
|
|
553
|
+
wrangler: {
|
|
554
|
+
passthrough: {
|
|
555
|
+
placement: {
|
|
556
|
+
mode: 'smart'
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Two practical rules:
|
|
564
|
+
|
|
565
|
+
- if you want Devflare-managed entry composition, keep using `files.fetch`, `files.routes`, and related surface config, and do **not** set `wrangler.passthrough.main`
|
|
566
|
+
- if you want total control of the Worker `main` entry, set `wrangler.passthrough.main` and own that entry file yourself
|
|
567
|
+
|
|
568
|
+
### `files`
|
|
569
|
+
|
|
570
|
+
`files` tells Devflare where your surfaces live. Use explicit paths for any surface that matters to build or deploy output, especially `files.fetch`.
|
|
571
|
+
|
|
572
|
+
| Key | Shape | Default convention | Meaning |
|
|
573
|
+
|---|---|---|---|
|
|
574
|
+
| `fetch` | <code>string | false</code> | `src/fetch.ts` | main HTTP handler |
|
|
575
|
+
| `queue` | <code>string | false</code> | `src/queue.ts` | queue consumer handler |
|
|
576
|
+
| `scheduled` | <code>string | false</code> | `src/scheduled.ts` | scheduled handler |
|
|
577
|
+
| `email` | <code>string | false</code> | `src/email.ts` | incoming email handler |
|
|
578
|
+
| `durableObjects` | <code>string | false</code> | `**/do.*.{ts,js}` | Durable Object discovery glob |
|
|
579
|
+
| `entrypoints` | <code>string | false</code> | `**/ep.*.{ts,js}` | WorkerEntrypoint discovery glob |
|
|
580
|
+
| `workflows` | <code>string | false</code> | `**/wf.*.{ts,js}` | workflow discovery glob |
|
|
581
|
+
| `routes` | <code>{ dir, prefix? } | false</code> | `src/routes` when that directory exists | built-in file router configuration |
|
|
582
|
+
| `transport` | `string` | none | custom transport definition file |
|
|
583
|
+
|
|
584
|
+
Discovery does not behave identically in every subsystem:
|
|
585
|
+
|
|
586
|
+
- local dev and `createTestContext()` can fall back to conventional handler files when present
|
|
587
|
+
- local dev, `createTestContext()`, and higher-level worker-entry generation also auto-discover `src/routes/**` unless `files.routes` is `false`
|
|
588
|
+
- type generation and discovery use default globs for Durable Objects, entrypoints, and workflows
|
|
589
|
+
- `compileConfig()` only writes Wrangler `main` when `files.fetch` is explicit
|
|
590
|
+
- higher-level `build`, `deploy`, and `devflare/vite` flows may still generate a composed `.devflare/worker-entrypoints/main.ts`, including route trees
|
|
591
|
+
- `wrangler.passthrough.main` disables that composed-entry generation path
|
|
592
|
+
|
|
593
|
+
Safe rule: if a surface matters to build or deploy output, declare it explicitly even if another subsystem can discover it by convention.
|
|
594
|
+
|
|
595
|
+
Practical example:
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
import { defineConfig } from 'devflare'
|
|
599
|
+
|
|
600
|
+
export default defineConfig({
|
|
601
|
+
name: 'multi-surface-worker',
|
|
602
|
+
files: {
|
|
603
|
+
fetch: 'src/fetch.ts',
|
|
604
|
+
queue: 'src/queue.ts',
|
|
605
|
+
scheduled: 'src/scheduled.ts',
|
|
606
|
+
email: 'src/email.ts',
|
|
607
|
+
routes: {
|
|
608
|
+
dir: 'src/routes',
|
|
609
|
+
prefix: '/api'
|
|
610
|
+
},
|
|
611
|
+
durableObjects: 'src/do/**/*.ts',
|
|
612
|
+
entrypoints: 'src/rpc/**/*.ts',
|
|
613
|
+
workflows: 'src/workflows/**/*.ts'
|
|
614
|
+
}
|
|
615
|
+
})
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
`files.transport` is opt-in. Devflare only loads it when `files.transport` is explicitly set, and the file must export a named `transport` object.
|
|
619
|
+
|
|
620
|
+
There is no public `files.tail` config key today.
|
|
621
|
+
|
|
622
|
+
### `.env`, `vars`, `secrets`, and `config.env`
|
|
623
|
+
|
|
624
|
+
Keep these layers separate:
|
|
625
|
+
|
|
626
|
+
| Layer | Holds values? | Compiled into generated config? | Use it for |
|
|
627
|
+
|---|---|---|---|
|
|
628
|
+
| `.env` / `process.env` | yes | indirectly, only when your config reads from it | local process-time inputs |
|
|
629
|
+
| `vars` | yes | yes | non-secret string bindings |
|
|
630
|
+
| `secrets` | no, declaration only | no | required/optional runtime secret bindings |
|
|
631
|
+
| `config.env` | yes, as config overlays | yes after merge | environment-specific config overrides |
|
|
632
|
+
|
|
633
|
+
#### `.env` and process env
|
|
634
|
+
|
|
635
|
+
`loadConfig()` does not do dotenv loading by itself. The Devflare CLI runs under Bun, and Bun may auto-load `.env` files into `process.env`.
|
|
636
|
+
|
|
637
|
+
#### `vars`
|
|
638
|
+
|
|
639
|
+
Use `vars` for non-secret runtime values that can safely appear in generated config, such as public URLs, modes, IDs, and feature flags.
|
|
640
|
+
|
|
641
|
+
In the current native Devflare schema, `vars` is:
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
Record<string, string>
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
#### `secrets`
|
|
648
|
+
|
|
649
|
+
`secrets` is a declaration layer. Use it to say which secret bindings your worker expects and whether they are required. Do not put secret values here.
|
|
650
|
+
|
|
651
|
+
In the current schema, each secret has the shape:
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
{ required?: boolean }
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
`required` defaults to `true`, so this:
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
secrets: {
|
|
661
|
+
API_KEY: {}
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
means “`API_KEY` is a required runtime secret,” not “optional secret with no requirements.”
|
|
666
|
+
|
|
667
|
+
#### `devflare types`
|
|
668
|
+
|
|
669
|
+
`devflare types` generates `env.d.ts` from the resolved config plus discovered surfaces. The stable public result is typed `DevflareEnv` coverage for bindings such as `vars`, `secrets`, services, Durable Objects, and discovered entrypoints.
|
|
670
|
+
|
|
671
|
+
Import-path behavior stays the same as described earlier: the main-entry `env` is the broader typed access story, while `devflare/runtime` is the strict request-scoped runtime helper.
|
|
672
|
+
|
|
673
|
+
#### `config.env`
|
|
674
|
+
|
|
675
|
+
`config.env` is Devflare’s environment override layer. When you pass `--env <name>` or call `compileConfig(config, name)`, Devflare starts from the base config, merges `config.env[name]` into it, and compiles the resolved result.
|
|
676
|
+
|
|
677
|
+
Current merge behavior uses deep merge semantics:
|
|
678
|
+
|
|
679
|
+
- scalar fields override base values
|
|
680
|
+
- nested objects inherit omitted keys
|
|
681
|
+
- arrays append instead of replacing
|
|
682
|
+
- `null` and `undefined` do not delete base values
|
|
683
|
+
|
|
684
|
+
If you need full replacement behavior, compute the final value in `defineConfig()` instead of relying on `config.env`.
|
|
685
|
+
|
|
686
|
+
Example:
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import { defineConfig } from 'devflare'
|
|
690
|
+
|
|
691
|
+
export default defineConfig({
|
|
692
|
+
name: 'api',
|
|
693
|
+
files: {
|
|
694
|
+
fetch: 'src/fetch.ts'
|
|
695
|
+
},
|
|
696
|
+
vars: {
|
|
697
|
+
APP_ENV: 'development',
|
|
698
|
+
API_ORIGIN: 'http://localhost:3000'
|
|
699
|
+
},
|
|
700
|
+
bindings: {
|
|
701
|
+
kv: {
|
|
702
|
+
CACHE: 'api-cache-dev'
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
env: {
|
|
706
|
+
production: {
|
|
707
|
+
vars: {
|
|
708
|
+
APP_ENV: 'production',
|
|
709
|
+
API_ORIGIN: 'https://api.example.com'
|
|
710
|
+
},
|
|
711
|
+
bindings: {
|
|
712
|
+
kv: {
|
|
713
|
+
CACHE: 'api-cache'
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
})
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
With `--env production`, the resolved config keeps `files.fetch`, overrides the `vars`, and overrides `bindings.kv.CACHE`.
|
|
722
|
+
|
|
723
|
+
Current limitation: `accountId` and `wsRoutes` are not supported inside `config.env`. Compute those in the outer `defineConfig()` function when you need environment-specific values.
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## Bindings and multi-worker composition
|
|
728
|
+
|
|
729
|
+
### Binding groups
|
|
730
|
+
|
|
731
|
+
| Group | Members | Coverage wording | Safe promise |
|
|
732
|
+
|---|---|---|---|
|
|
733
|
+
| Core bindings | `kv`, `d1`, `r2`, `durableObjects`, `queues.producers`, `queues.consumers` | well-covered | safest public binding path today across config, generated types, and local dev/test |
|
|
734
|
+
| Services and `ref()` | `services` | well-covered with a named-entrypoint caveat | worker-to-worker composition is solid; validate generated output when a named entrypoint matters |
|
|
735
|
+
| Remote-oriented bindings | `ai`, `vectorize` | remote-dependent | supported and typed, but not full local equivalents of KV or D1 |
|
|
736
|
+
| Narrower bindings | `hyperdrive`, `analyticsEngine`, `browser` | supported, narrower-scope | real public surfaces, but less central than the core storage/runtime path |
|
|
737
|
+
| Outbound email binding | `sendEmail` | supported public surface | real outbound email binding; keep it separate from inbound email handling |
|
|
738
|
+
|
|
739
|
+
### Core bindings
|
|
740
|
+
|
|
741
|
+
KV, D1, R2, Durable Objects, and queues have the clearest end-to-end story today across config, generated types, and local dev/test flows.
|
|
742
|
+
|
|
743
|
+
### R2 browser access and public URLs
|
|
744
|
+
|
|
745
|
+
`bindings.r2` gives you real R2 binding access in local dev, tests, generated types, and compiled config.
|
|
746
|
+
|
|
747
|
+
What Devflare does **not** currently expose as a public contract is a stable browser-facing local bucket URL.
|
|
748
|
+
|
|
749
|
+
If you need browser-visible local asset flows:
|
|
750
|
+
|
|
751
|
+
- serve them through your Worker routes or app endpoints
|
|
752
|
+
- test real public/custom-domain URL behavior against an intentional staging bucket
|
|
753
|
+
- do not hard-code frontend assumptions about a magic local bucket origin
|
|
754
|
+
|
|
755
|
+
For delivery patterns and production recommendations, see [`R2.md`](./R2.md).
|
|
756
|
+
|
|
757
|
+
Practical example:
|
|
758
|
+
|
|
759
|
+
```ts
|
|
760
|
+
import { defineConfig } from 'devflare'
|
|
761
|
+
|
|
762
|
+
export default defineConfig({
|
|
763
|
+
name: 'orders-worker',
|
|
764
|
+
bindings: {
|
|
765
|
+
kv: {
|
|
766
|
+
CACHE: 'orders-cache'
|
|
767
|
+
},
|
|
768
|
+
d1: {
|
|
769
|
+
DB: 'orders-db'
|
|
770
|
+
},
|
|
771
|
+
durableObjects: {
|
|
772
|
+
COUNTER: 'Counter'
|
|
773
|
+
},
|
|
774
|
+
queues: {
|
|
775
|
+
producers: {
|
|
776
|
+
TASK_QUEUE: 'orders-tasks'
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
})
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
```ts
|
|
784
|
+
import type { FetchEvent } from 'devflare/runtime'
|
|
785
|
+
|
|
786
|
+
export async function POST({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
|
|
787
|
+
const status = await env.DB.prepare('select 1 as ok').first()
|
|
788
|
+
await env.CACHE.put('health', JSON.stringify(status))
|
|
789
|
+
await env.TASK_QUEUE.send({ type: 'reindex-orders' })
|
|
790
|
+
return Response.json({ ok: true })
|
|
791
|
+
}
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Services and named entrypoints
|
|
795
|
+
|
|
796
|
+
Service bindings can be written directly or via `ref()`.
|
|
797
|
+
|
|
798
|
+
```ts
|
|
799
|
+
import { defineConfig, ref } from 'devflare'
|
|
800
|
+
|
|
801
|
+
const auth = ref(() => import('../auth/devflare.config'))
|
|
802
|
+
|
|
803
|
+
export default defineConfig({
|
|
804
|
+
name: 'gateway',
|
|
805
|
+
bindings: {
|
|
806
|
+
services: {
|
|
807
|
+
AUTH: auth.worker,
|
|
808
|
+
ADMIN: auth.worker('AdminEntrypoint')
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
})
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
Direct worker-to-worker service composition is a strong public path. Named service entrypoints are typed and configurable, but if a specific entrypoint matters at deployment time, validate the generated output in your project.
|
|
815
|
+
|
|
816
|
+
Practical example:
|
|
817
|
+
|
|
818
|
+
```ts
|
|
819
|
+
// gateway/devflare.config.ts
|
|
820
|
+
import { defineConfig, ref } from 'devflare'
|
|
821
|
+
|
|
822
|
+
const auth = ref(() => import('../auth/devflare.config'))
|
|
823
|
+
|
|
824
|
+
export default defineConfig({
|
|
825
|
+
name: 'gateway',
|
|
826
|
+
bindings: {
|
|
827
|
+
services: {
|
|
828
|
+
ADMIN: auth.worker('AdminEntrypoint')
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
})
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
```ts
|
|
835
|
+
// gateway/src/fetch.ts
|
|
836
|
+
import type { FetchEvent } from 'devflare/runtime'
|
|
837
|
+
|
|
838
|
+
export async function GET({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
|
|
839
|
+
const stats = await env.ADMIN.getStats()
|
|
840
|
+
return Response.json(stats)
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### Remote-oriented and narrower bindings
|
|
845
|
+
|
|
846
|
+
`ai` and `vectorize` are remote-dependent bindings. Devflare supports their config and typing, but they should not be documented as full local equivalents of KV or D1.
|
|
847
|
+
|
|
848
|
+
`hyperdrive`, `analyticsEngine`, and `browser` are supported narrower-scope surfaces. `browser` also sits closer to the dev-orchestration layer than to the core fetch/queue path.
|
|
849
|
+
|
|
850
|
+
### `sendEmail` and inbound email
|
|
851
|
+
|
|
852
|
+
`sendEmail` is the outbound email binding surface. It is supported across config, generated types, and local development flows.
|
|
853
|
+
|
|
854
|
+
Incoming email is a separate worker surface:
|
|
855
|
+
|
|
856
|
+
- `files.email`
|
|
857
|
+
- `src/email.ts`
|
|
858
|
+
- `EmailEvent`
|
|
859
|
+
|
|
860
|
+
Keep outbound `sendEmail` and inbound email handling separate in docs and examples.
|
|
861
|
+
|
|
862
|
+
Practical example:
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
// devflare.config.ts
|
|
866
|
+
import { defineConfig } from 'devflare'
|
|
867
|
+
|
|
868
|
+
export default defineConfig({
|
|
869
|
+
name: 'mail-worker',
|
|
870
|
+
files: {
|
|
871
|
+
fetch: 'src/fetch.ts',
|
|
872
|
+
email: 'src/email.ts'
|
|
873
|
+
},
|
|
874
|
+
bindings: {
|
|
875
|
+
sendEmail: {
|
|
876
|
+
EMAIL: {
|
|
877
|
+
destinationAddress: 'team@example.com'
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
})
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
```ts
|
|
885
|
+
// src/fetch.ts
|
|
886
|
+
import type { FetchEvent } from 'devflare/runtime'
|
|
887
|
+
|
|
888
|
+
export async function POST({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
|
|
889
|
+
await env.EMAIL.send({
|
|
890
|
+
from: 'noreply@example.com',
|
|
891
|
+
to: 'team@example.com',
|
|
892
|
+
subject: 'Welcome',
|
|
893
|
+
text: 'Hello from Devflare'
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
return new Response('sent')
|
|
897
|
+
}
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
```ts
|
|
901
|
+
// src/email.ts
|
|
902
|
+
import type { EmailEvent } from 'devflare/runtime'
|
|
903
|
+
|
|
904
|
+
export async function email({ message }: EmailEvent<DevflareEnv>): Promise<void> {
|
|
905
|
+
await message.forward('ops@example.com')
|
|
906
|
+
}
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### `ref()` and multi-worker composition
|
|
910
|
+
|
|
911
|
+
Use `ref()` when one worker depends on another worker config.
|
|
912
|
+
|
|
913
|
+
Main public story:
|
|
914
|
+
|
|
915
|
+
- `ref(...).worker` for a default service binding backed by `src/worker.ts` or `src/worker.js`
|
|
916
|
+
- `ref(...).worker('EntrypointName')` for a named service entrypoint
|
|
917
|
+
- typed cross-worker Durable Object handles
|
|
918
|
+
|
|
919
|
+
```ts
|
|
920
|
+
const auth = ref(() => import('../auth/devflare.config'))
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
```ts
|
|
924
|
+
const auth = ref('custom-auth-name', () => import('../auth/devflare.config'))
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
Why `ref()` matters:
|
|
928
|
+
|
|
929
|
+
- worker names stay centralized
|
|
930
|
+
- cross-worker Durable Object bindings stay typed
|
|
931
|
+
- entrypoint names can be typed from generated `Entrypoints`
|
|
932
|
+
- multi-worker systems avoid scattered magic strings
|
|
933
|
+
|
|
934
|
+
Advanced members such as `.name`, `.config`, `.configPath`, and `.resolve()` are real, but secondary to the main composition story.
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
## Development workflows
|
|
939
|
+
|
|
940
|
+
### Operational decision rules
|
|
941
|
+
|
|
942
|
+
Use these rules in order:
|
|
943
|
+
|
|
944
|
+
1. a local `vite.config.*` in the current package decides whether `dev`, `build`, and `deploy` run in Vite-backed mode or worker-only mode
|
|
945
|
+
2. `devflare/vite` is an explicit helper layer, not a separate CLI mode
|
|
946
|
+
3. worker-only mode is a supported first-class path; no local `vite.config.*` means Devflare does not start Vite
|
|
947
|
+
4. raw Vite config belongs in `vite.config.*`; `config.vite` is Devflare metadata, not full Vite config
|
|
948
|
+
5. treat `.devflare/*`, `env.d.ts`, and generated Wrangler config as outputs, not authoring inputs
|
|
949
|
+
6. remote mode is mainly for remote-oriented services such as AI and Vectorize, not a blanket “make everything remote” switch
|
|
950
|
+
|
|
951
|
+
### Daily development loop
|
|
952
|
+
|
|
953
|
+
Use the same CLI loop for both worker-only and Vite-backed packages. The presence of a local `vite.config.*` changes the mode automatically.
|
|
954
|
+
|
|
955
|
+
```bash
|
|
956
|
+
bunx --bun devflare dev
|
|
957
|
+
bunx --bun devflare types
|
|
958
|
+
bunx --bun devflare doctor
|
|
959
|
+
bunx --bun devflare build --env staging
|
|
960
|
+
bunx --bun devflare deploy --dry-run --env staging
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
Use `--env <name>` on build and deploy when you want Devflare to resolve a named config environment before compilation.
|
|
964
|
+
|
|
965
|
+
### CLI reference
|
|
966
|
+
|
|
967
|
+
| Command | What it does | Important note |
|
|
968
|
+
|---|---|---|
|
|
969
|
+
| `devflare init` | scaffold a project | current templates generate `src/fetch.ts` and explicit `files.fetch` |
|
|
970
|
+
| `devflare dev` | start local development | worker-only by default, Vite-backed only when the current package has a local `vite.config.*` |
|
|
971
|
+
| `devflare build` | build from resolved config | skips Vite for worker-only packages |
|
|
972
|
+
| `devflare deploy` | build and deploy | supports `--env` and `--dry-run` |
|
|
973
|
+
| `devflare types` | generate `env.d.ts` | supports `--output` |
|
|
974
|
+
| `devflare doctor` | check project configuration | supports `--config` |
|
|
975
|
+
| `devflare account` | inspect accounts, resources, usage, and limits | `global` and `workspace` are account-selection flows, not resource-reporting subcommands |
|
|
976
|
+
| `devflare ai` | show Workers AI model pricing | informational command |
|
|
977
|
+
| `devflare remote` | manage remote test mode | helper flow for remote-oriented services |
|
|
978
|
+
| `devflare help` | show help | direct command and flag form both work |
|
|
979
|
+
| `devflare version` | show package version | sourced from installed package metadata |
|
|
980
|
+
|
|
981
|
+
### `doctor`
|
|
982
|
+
|
|
983
|
+
Current `doctor` behavior:
|
|
984
|
+
|
|
985
|
+
- it checks for supported Devflare config filenames
|
|
986
|
+
- it can inspect an explicit config path via `--config`
|
|
987
|
+
- it accepts project-root `wrangler.jsonc`
|
|
988
|
+
- Vite-backed flows may instead generate `.devflare/wrangler.jsonc`
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
992
|
+
## Testing model
|
|
993
|
+
|
|
994
|
+
Use `devflare/test`.
|
|
995
|
+
|
|
996
|
+
The default pairing is:
|
|
997
|
+
|
|
998
|
+
```ts
|
|
999
|
+
import { beforeAll, afterAll } from 'bun:test'
|
|
1000
|
+
import { createTestContext } from 'devflare/test'
|
|
1001
|
+
import { env } from 'devflare'
|
|
1002
|
+
|
|
1003
|
+
beforeAll(() => createTestContext())
|
|
1004
|
+
afterAll(() => env.dispose())
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
`createTestContext()` is the recommended default for most integration-style tests.
|
|
1008
|
+
|
|
1009
|
+
Current behavior:
|
|
1010
|
+
|
|
1011
|
+
- it can auto-discover the nearest supported config file by walking upward from the calling test file
|
|
1012
|
+
- it sets up the main-entry test env used by `import { env } from 'devflare'`
|
|
1013
|
+
- it resolves service bindings and cross-worker Durable Object references
|
|
1014
|
+
- it can infer conventional fetch, queue, scheduled, and email handler files when present
|
|
1015
|
+
- it also auto-detects `src/tail.ts` when present, even though there is no public `files.tail` config key
|
|
1016
|
+
- `files.transport` is opt-in rather than auto-loaded by filename convention
|
|
1017
|
+
|
|
1018
|
+
Practical example:
|
|
1019
|
+
|
|
1020
|
+
```ts
|
|
1021
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
1022
|
+
import { cf, createTestContext } from 'devflare/test'
|
|
1023
|
+
import { env } from 'devflare'
|
|
1024
|
+
|
|
1025
|
+
beforeAll(() => createTestContext())
|
|
1026
|
+
afterAll(() => env.dispose())
|
|
1027
|
+
|
|
1028
|
+
describe('users routes', () => {
|
|
1029
|
+
test('GET /users/123 uses the built-in file router', async () => {
|
|
1030
|
+
const response = await cf.worker.get('/users/123')
|
|
1031
|
+
expect(response.status).toBe(200)
|
|
1032
|
+
expect(await response.json()).toEqual({ id: '123' })
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
test('queue side effects can be asserted with real bindings', async () => {
|
|
1036
|
+
await cf.queue.trigger([
|
|
1037
|
+
{ type: 'reindex', id: 'job-1' }
|
|
1038
|
+
])
|
|
1039
|
+
|
|
1040
|
+
expect(await env.RESULTS.get('job-1')).toBe('done')
|
|
1041
|
+
})
|
|
1042
|
+
})
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
### Helper behavior
|
|
1046
|
+
|
|
1047
|
+
Do not assume every `cf.*` helper behaves the same way.
|
|
1048
|
+
|
|
1049
|
+
| Helper | What it does | Waits for `waitUntil()`? | Important nuance |
|
|
1050
|
+
|---|---|---|---|
|
|
1051
|
+
| `cf.worker.fetch()` | invokes the configured HTTP surface, including built-in file routes | No | returns when the handler resolves |
|
|
1052
|
+
| `cf.queue.trigger()` | invokes the configured queue consumer | Yes | drains queued background work before returning |
|
|
1053
|
+
| `cf.scheduled.trigger()` | invokes the configured scheduled handler | Yes | drains queued background work before returning |
|
|
1054
|
+
| `cf.email.send()` | sends a raw email through the helper | Yes on the direct-handler path; fallback endpoint behavior is runtime-driven | when `createTestContext()` has wired an email handler, it imports and invokes that handler directly; otherwise it falls back to the local email endpoint |
|
|
1055
|
+
| `cf.tail.trigger()` | directly invokes a tail handler | Yes | `createTestContext()` auto-detects `src/tail.ts` when present, but there is still no public `files.tail` config key |
|
|
1056
|
+
|
|
1057
|
+
Two important consequences:
|
|
1058
|
+
|
|
1059
|
+
- `cf.worker.fetch()` is not a “wait for all background work” helper
|
|
1060
|
+
- helper availability does not mean every helper replays the full Cloudflare dispatch path in the same way
|
|
1061
|
+
|
|
1062
|
+
### Email testing
|
|
1063
|
+
|
|
1064
|
+
Email testing has two directions:
|
|
1065
|
+
|
|
1066
|
+
- incoming email helper dispatch via `cf.email.send()` / `email.send()`
|
|
1067
|
+
- outgoing email observation via helper state
|
|
1068
|
+
|
|
1069
|
+
When `createTestContext()` has wired an email handler, the helper imports that handler directly, creates an `EmailEvent`, and waits for queued `waitUntil()` work.
|
|
1070
|
+
|
|
1071
|
+
If no email handler has been wired, the helper falls back to posting the raw email to the local email endpoint.
|
|
1072
|
+
|
|
1073
|
+
That makes the helper useful for handler-level tests, but ingress-fidelity-sensitive flows should still use higher-level integration testing.
|
|
1074
|
+
|
|
1075
|
+
### Tail testing
|
|
1076
|
+
|
|
1077
|
+
Tail helpers are exported publicly, and `createTestContext()` auto-detects `src/tail.ts` when present.
|
|
1078
|
+
|
|
1079
|
+
So the truthful public statement is:
|
|
1080
|
+
|
|
1081
|
+
- `cf.tail.trigger()` is exported
|
|
1082
|
+
- `createTestContext()` wires it automatically when `src/tail.ts` exists
|
|
1083
|
+
- `cf.tail.trigger()` directly imports and invokes that handler and waits for `waitUntil()`
|
|
1084
|
+
- there is no public `files.tail` config key to advertise today
|
|
1085
|
+
|
|
1086
|
+
### Remote mode in tests
|
|
1087
|
+
|
|
1088
|
+
Remote mode is mainly about AI and Vectorize. Use `shouldSkip` for cost-sensitive or remote-only test gating. Do not treat remote mode as a blanket “make every binding remote” switch.
|
|
1089
|
+
|
|
1090
|
+
---
|
|
1091
|
+
|
|
1092
|
+
## Sharp edges
|
|
1093
|
+
|
|
1094
|
+
Keep these caveats explicit:
|
|
1095
|
+
|
|
1096
|
+
- `files.routes` configures the built-in file router; it is not the same thing as Cloudflare deployment `routes`
|
|
1097
|
+
- route trees are real now, but `src/fetch.ts` same-module method handlers still take precedence over file routes for matching methods
|
|
1098
|
+
- route files that normalize to the same pattern are rejected as conflicts
|
|
1099
|
+
- `_`-prefixed files and directories inside the route tree are ignored
|
|
1100
|
+
- `vars` values are strings in the current native schema
|
|
1101
|
+
- `secrets` declares runtime secret bindings; it does not store secret values
|
|
1102
|
+
- `wrangler.passthrough` is the escape hatch for unsupported Wrangler keys and is merged after native compilation
|
|
1103
|
+
- named service entrypoints need deployment-time validation if they are critical to your app
|
|
1104
|
+
- local R2 binding support is real, but there is still no stable public/browser local bucket URL contract to document as public API
|
|
1105
|
+
- `sendEmail` is a supported outbound binding, while inbound email is a separate worker surface
|
|
1106
|
+
- email and tail helpers have real, useful test paths, but they should not be described as identical to full Cloudflare ingress or tail replay
|
|
1107
|
+
- Vite and Rolldown are different systems and should not be blurred together
|
|
1108
|
+
- higher-level flows may generate composed main worker entries more aggressively than older docs implied
|
|
1109
|
+
|
|
1110
|
+
---
|
|
1111
|
+
|
|
1112
|
+
## Summary
|
|
1113
|
+
|
|
1114
|
+
Prefer explicit config over discovery, separated surfaces over a monolithic Worker file, and generated output as output rather than source. The safest public path today is explicit `files.fetch`, event-first handlers, explicit bindings, `createTestContext()` for core tests, and `ref()` for cross-worker composition.
|