devflare 1.0.0-next.1 → 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.
Files changed (123) hide show
  1. package/LLM.md +775 -637
  2. package/R2.md +200 -0
  3. package/README.md +285 -514
  4. package/bin/devflare.js +8 -8
  5. package/dist/{account-rvrj687w.js → account-8psavtg6.js} +27 -4
  6. package/dist/bridge/miniflare.d.ts +6 -0
  7. package/dist/bridge/miniflare.d.ts.map +1 -1
  8. package/dist/bridge/proxy.d.ts +5 -6
  9. package/dist/bridge/proxy.d.ts.map +1 -1
  10. package/dist/bridge/server.d.ts.map +1 -1
  11. package/dist/browser.d.ts +50 -0
  12. package/dist/browser.d.ts.map +1 -0
  13. package/dist/{build-mnf6v8gd.js → build-k36xrzvy.js} +26 -7
  14. package/dist/bundler/do-bundler.d.ts +7 -0
  15. package/dist/bundler/do-bundler.d.ts.map +1 -1
  16. package/dist/cli/commands/account.d.ts.map +1 -1
  17. package/dist/cli/commands/build.d.ts.map +1 -1
  18. package/dist/cli/commands/deploy.d.ts.map +1 -1
  19. package/dist/cli/commands/dev.d.ts.map +1 -1
  20. package/dist/cli/commands/doctor.d.ts.map +1 -1
  21. package/dist/cli/commands/init.d.ts.map +1 -1
  22. package/dist/cli/commands/types.d.ts.map +1 -1
  23. package/dist/cli/config-path.d.ts +5 -0
  24. package/dist/cli/config-path.d.ts.map +1 -0
  25. package/dist/cli/index.d.ts.map +1 -1
  26. package/dist/cli/package-metadata.d.ts +16 -0
  27. package/dist/cli/package-metadata.d.ts.map +1 -0
  28. package/dist/config/compiler.d.ts +7 -0
  29. package/dist/config/compiler.d.ts.map +1 -1
  30. package/dist/config/index.d.ts +1 -1
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/schema.d.ts +2575 -1221
  33. package/dist/config/schema.d.ts.map +1 -1
  34. package/dist/{deploy-nhceck39.js → deploy-dbvfq8vq.js} +33 -15
  35. package/dist/{dev-qnxet3j9.js → dev-rk8p6pse.js} +900 -234
  36. package/dist/dev-server/miniflare-log.d.ts +12 -0
  37. package/dist/dev-server/miniflare-log.d.ts.map +1 -0
  38. package/dist/dev-server/runtime-stdio.d.ts +8 -0
  39. package/dist/dev-server/runtime-stdio.d.ts.map +1 -0
  40. package/dist/dev-server/server.d.ts +2 -0
  41. package/dist/dev-server/server.d.ts.map +1 -1
  42. package/dist/dev-server/vite-utils.d.ts +37 -0
  43. package/dist/dev-server/vite-utils.d.ts.map +1 -0
  44. package/dist/{doctor-e8fy6fj5.js → doctor-06y8nxd4.js} +73 -50
  45. package/dist/{durable-object-t4kbb0yt.js → durable-object-yt8v1dyn.js} +1 -1
  46. package/dist/index-05fyzwne.js +195 -0
  47. package/dist/index-1p814k7s.js +227 -0
  48. package/dist/{index-hcex3rgh.js → index-1phx14av.js} +84 -7
  49. package/dist/{index-tk6ej9dj.js → index-2q3pmzrx.js} +12 -16
  50. package/dist/{index-pf5s73n9.js → index-59df49vn.js} +11 -281
  51. package/dist/index-5yxg30va.js +304 -0
  52. package/dist/index-62b3gt2g.js +12 -0
  53. package/dist/index-6h8xbs75.js +44 -0
  54. package/dist/{index-67qcae0f.js → index-6v3wjg1r.js} +16 -1
  55. package/dist/index-8gtqgb3q.js +529 -0
  56. package/dist/{index-gz1gndna.js → index-9wt9x09k.js} +42 -62
  57. package/dist/index-fef08w43.js +231 -0
  58. package/dist/{index-ep3445yc.js → index-jht2j546.js} +393 -170
  59. package/dist/index-k7r18na8.js +0 -0
  60. package/dist/{index-m2q41jwa.js → index-n932ytmq.js} +9 -1
  61. package/dist/index-pwgyy2q9.js +39 -0
  62. package/dist/{index-07q6yxyc.js → index-v8vvsn9x.js} +1 -0
  63. package/dist/index-vky23txa.js +70 -0
  64. package/dist/index-vs49yxn4.js +322 -0
  65. package/dist/{index-z14anrqp.js → index-wfbfz02q.js} +14 -15
  66. package/dist/index-ws68xvq2.js +311 -0
  67. package/dist/index-y1d8za14.js +196 -0
  68. package/dist/{init-f9mgmew3.js → init-na2atvz2.js} +42 -55
  69. package/dist/router/types.d.ts +24 -0
  70. package/dist/router/types.d.ts.map +1 -0
  71. package/dist/runtime/context.d.ts +249 -8
  72. package/dist/runtime/context.d.ts.map +1 -1
  73. package/dist/runtime/exports.d.ts +50 -55
  74. package/dist/runtime/exports.d.ts.map +1 -1
  75. package/dist/runtime/index.d.ts +8 -1
  76. package/dist/runtime/index.d.ts.map +1 -1
  77. package/dist/runtime/middleware.d.ts +77 -60
  78. package/dist/runtime/middleware.d.ts.map +1 -1
  79. package/dist/runtime/router.d.ts +7 -0
  80. package/dist/runtime/router.d.ts.map +1 -0
  81. package/dist/runtime/validation.d.ts +1 -1
  82. package/dist/runtime/validation.d.ts.map +1 -1
  83. package/dist/src/browser.js +150 -0
  84. package/dist/src/cli/index.js +10 -0
  85. package/dist/{cloudflare → src/cloudflare}/index.js +3 -3
  86. package/dist/{decorators → src/decorators}/index.js +2 -2
  87. package/dist/src/index.js +132 -0
  88. package/dist/src/runtime/index.js +111 -0
  89. package/dist/{sveltekit → src/sveltekit}/index.js +14 -6
  90. package/dist/{test → src/test}/index.js +22 -13
  91. package/dist/{vite → src/vite}/index.js +128 -59
  92. package/dist/sveltekit/platform.d.ts.map +1 -1
  93. package/dist/test/bridge-context.d.ts +5 -2
  94. package/dist/test/bridge-context.d.ts.map +1 -1
  95. package/dist/test/cf.d.ts +25 -11
  96. package/dist/test/cf.d.ts.map +1 -1
  97. package/dist/test/email.d.ts +16 -7
  98. package/dist/test/email.d.ts.map +1 -1
  99. package/dist/test/queue.d.ts.map +1 -1
  100. package/dist/test/resolve-service-bindings.d.ts.map +1 -1
  101. package/dist/test/scheduled.d.ts.map +1 -1
  102. package/dist/test/simple-context.d.ts +1 -1
  103. package/dist/test/simple-context.d.ts.map +1 -1
  104. package/dist/test/tail.d.ts +2 -1
  105. package/dist/test/tail.d.ts.map +1 -1
  106. package/dist/test/worker.d.ts +6 -0
  107. package/dist/test/worker.d.ts.map +1 -1
  108. package/dist/transform/durable-object.d.ts.map +1 -1
  109. package/dist/transform/worker-entrypoint.d.ts.map +1 -1
  110. package/dist/{types-5nyrz1sz.js → types-x9q7t491.js} +30 -16
  111. package/dist/utils/entrypoint-discovery.d.ts +6 -3
  112. package/dist/utils/entrypoint-discovery.d.ts.map +1 -1
  113. package/dist/utils/send-email.d.ts +15 -0
  114. package/dist/utils/send-email.d.ts.map +1 -0
  115. package/dist/vite/plugin.d.ts.map +1 -1
  116. package/dist/worker-entry/composed-worker.d.ts +13 -0
  117. package/dist/worker-entry/composed-worker.d.ts.map +1 -0
  118. package/dist/worker-entry/routes.d.ts +22 -0
  119. package/dist/worker-entry/routes.d.ts.map +1 -0
  120. package/dist/{worker-entrypoint-m9th0rg0.js → worker-entrypoint-c259fmfs.js} +1 -1
  121. package/package.json +21 -19
  122. package/dist/index.js +0 -298
  123. package/dist/runtime/index.js +0 -111
package/README.md CHANGED
@@ -2,101 +2,42 @@
2
2
 
3
3
  **Build Cloudflare Workers like an application, not a pile of glue.**
4
4
 
5
- Devflare is a developer-first toolkit for Cloudflare Workers that sits on top of Miniflare and Wrangler-compatible config.
5
+ Devflare is a developer-first layer on top of Cloudflare Workers, Miniflare, Bun, Vite, and Wrangler-compatible config.
6
6
 
7
7
  It gives you:
8
8
 
9
- - a clean file structure
10
9
  - typed config with `defineConfig()`
11
- - easier local development
12
- - great testing ergonomics
10
+ - convention-friendly worker surfaces
11
+ - local orchestration for multi-surface Worker apps
13
12
  - first-class Durable Object and multi-worker workflows
14
- - browser rendering support in local development
15
- - Vite and SvelteKit integration when your app grows up
13
+ - test helpers that mirror real handler surfaces
14
+ - worker-safe runtime helpers under `devflare/runtime`
15
+ - optional Vite and SvelteKit integration when your package actually uses them
16
16
 
17
17
  Miniflare gives you a local runtime.
18
18
 
19
19
  Devflare turns that runtime into a **coherent development system**.
20
20
 
21
- ---
22
-
23
- ## Why Devflare?
24
-
25
- Cloudflare development gets messy fast.
26
-
27
- You start with one Worker, then suddenly you have:
28
-
29
- - HTTP routes
30
- - queues
31
- - scheduled jobs
32
- - email handlers
33
- - Durable Objects
34
- - service bindings
35
- - framework output
36
- - test setup that no longer resembles runtime
37
-
38
- Devflare keeps those responsibilities isolated and composable.
39
-
40
- Instead of one giant worker file, you get a structure like this:
41
-
42
- ```text
43
- .
44
- ├── devflare.config.ts
45
- ├── env.d.ts
46
- ├── src/
47
- │ ├── fetch.ts
48
- │ ├── queue.ts
49
- │ ├── scheduled.ts
50
- │ ├── email.ts
51
- │ ├── routes/
52
- │ ├── do.counter.ts
53
- │ ├── ep.admin.ts
54
- │ └── transport.ts
55
- └── tests/
56
- └── worker.test.ts
57
- ```
58
-
59
- That structure is one of Devflare’s biggest strengths: **separate responsibilities, fewer surprises**.
60
-
61
- ---
62
-
63
- ## What makes it different from plain Miniflare?
64
-
65
- Devflare does not replace Miniflare.
66
-
67
- It **extends** it.
68
-
69
- Miniflare gives you local execution.
70
- Devflare adds the developer workflow around it:
71
-
72
- - convention-first file discovery
73
- - config compilation to Wrangler-compatible output
74
- - unified test helpers for `fetch`, `queue`, `scheduled`, `email`, and `tail`
75
- - typed cross-worker references with `ref()`
76
- - local orchestration for multi-surface Worker apps
77
- - browser rendering support in local development
78
- - framework-aware integration for Vite and SvelteKit
79
-
80
- In short:
81
-
82
- > Devflare helps you build Cloudflare systems with clear boundaries and smooth local development.
21
+ For the deeper public contract, caveats, and current feature boundaries, see [`LLM.md`](./LLM.md).
83
22
 
84
23
  ---
85
24
 
86
25
  ## Install
87
26
 
88
- For a typical project, install Devflare and Wrangler as dev dependencies:
27
+ For a worker-only project, Devflare works fine with just the Worker toolchain:
89
28
 
90
29
  ```bash
91
- bun add -d devflare wrangler
30
+ bun add -d devflare wrangler @cloudflare/workers-types
92
31
  ```
93
32
 
94
- If you are using Vite integration, also install the Vite plugin dependencies:
33
+ If the current package also uses Vite, add Vite and the Cloudflare Vite plugin too:
95
34
 
96
35
  ```bash
97
- bun add -d vite @cloudflare/vite-plugin
36
+ bun add -d devflare wrangler @cloudflare/workers-types vite @cloudflare/vite-plugin
98
37
  ```
99
38
 
39
+ A local `vite.config.*` opts that package into Vite-backed flows. Without one, Devflare stays in worker-only mode.
40
+
100
41
  ---
101
42
 
102
43
  ## Quick start
@@ -108,7 +49,11 @@ bun add -d vite @cloudflare/vite-plugin
108
49
  import { defineConfig } from 'devflare'
109
50
 
110
51
  export default defineConfig({
111
- name: 'hello-worker'
52
+ name: 'hello-worker',
53
+ compatibilityDate: '2026-03-17',
54
+ files: {
55
+ fetch: 'src/fetch.ts'
56
+ }
112
57
  })
113
58
  ```
114
59
 
@@ -116,8 +61,15 @@ export default defineConfig({
116
61
 
117
62
  ```ts
118
63
  // src/fetch.ts
119
- export default async function fetch(): Promise<Response> {
120
- return new Response('Hello from Devflare')
64
+ import type { FetchEvent } from 'devflare/runtime'
65
+
66
+ export async function fetch({ request }: FetchEvent): Promise<Response> {
67
+ const url = new URL(request.url)
68
+ return new Response(
69
+ url.pathname === '/'
70
+ ? 'Hello from Devflare'
71
+ : `Hello from Devflare: ${url.pathname}`
72
+ )
121
73
  }
122
74
  ```
123
75
 
@@ -127,7 +79,7 @@ export default async function fetch(): Promise<Response> {
127
79
  bunx --bun devflare types
128
80
  ```
129
81
 
130
- This creates `env.d.ts` so your bindings and entrypoints stay typed.
82
+ This generates `env.d.ts` so bindings, secrets, and discovered entrypoints stay typed.
131
83
 
132
84
  ### 4. Start development
133
85
 
@@ -157,477 +109,322 @@ describe('hello-worker', () => {
157
109
 
158
110
  ---
159
111
 
160
- ## The Devflare way to structure an app
112
+ ## Package entrypoints
161
113
 
162
- Devflare works best when each concern lives in its own file.
114
+ Use subpaths intentionally.
163
115
 
164
- ### Common files
165
-
166
- | File | Purpose |
116
+ | Import | Use for |
167
117
  |---|---|
168
- | `src/fetch.ts` | HTTP requests |
169
- | `src/queue.ts` | queue consumers |
170
- | `src/scheduled.ts` | cron handlers |
171
- | `src/email.ts` | email handlers |
172
- | `src/routes/**` | file-based routing |
173
- | `do.*.ts` | Durable Objects |
174
- | `ep.*.ts` | named WorkerEntrypoints |
175
- | `wf.*.ts` | workflows |
176
- | `src/transport.ts` | custom serialization across boundaries |
177
-
178
- This is more than style.
179
-
180
- It is how Devflare helps you keep local development, testing, and runtime behavior aligned.
118
+ | `devflare` | main package entrypoint: config helpers, `ref()`, unified `env`, bridge helpers, CLI helpers, decorators |
119
+ | `devflare/runtime` | worker-safe runtime helpers like `env`, `ctx`, `event`, `locals`, event types/getters, middleware helpers |
120
+ | `devflare/test` | `createTestContext`, `cf.*`, mock helpers, bridge test context, skip helpers |
121
+ | `devflare/vite` | Vite integration |
122
+ | `devflare/sveltekit` | SvelteKit integration |
123
+ | `devflare/cloudflare` | Cloudflare account/auth/usage/limits/preferences helpers |
124
+ | `devflare/decorators` | decorators only |
181
125
 
182
- ---
183
-
184
- ## Example config
126
+ ### Runtime import rule of thumb
185
127
 
186
- ```ts
187
- import { defineConfig } from 'devflare'
128
+ - use `import { env } from 'devflare/runtime'` for **strict request-scoped runtime access**
129
+ - use `import { env } from 'devflare'` when you want the **unified proxy** that can fall back to test or bridge context outside a live request
188
130
 
189
- export default defineConfig({
190
- name: 'my-app',
191
- files: {
192
- routes: {
193
- dir: 'src/routes',
194
- prefix: '/api'
195
- }
196
- },
197
- bindings: {
198
- kv: {
199
- CACHE: 'cache-kv-id'
200
- },
201
- d1: {
202
- DB: 'db-id'
203
- },
204
- durableObjects: {
205
- COUNTER: 'Counter'
206
- },
207
- queues: {
208
- producers: {
209
- TASK_QUEUE: 'task-queue'
210
- }
211
- },
212
- browser: {
213
- binding: 'BROWSER'
214
- }
215
- },
216
- triggers: {
217
- crons: ['0 */6 * * *']
218
- }
219
- })
220
- ```
131
+ The reduced worker-safe main entry for `devflare` is selected through the package `browser` export condition. `devflare/runtime` is the direct worker-safe runtime subpath.
221
132
 
222
133
  ---
223
134
 
224
- ## Routing without the giant switch statement
135
+ ## Event-first handlers
225
136
 
226
- As your HTTP surface grows, you do not need to keep stuffing logic into `fetch.ts`.
137
+ Fresh Devflare code should be event-first:
227
138
 
228
- Use file-based routing instead:
139
+ - `fetch(event: FetchEvent)`
140
+ - `queue(event: QueueEvent)`
141
+ - `scheduled(event: ScheduledEvent)`
142
+ - `email(event: EmailEvent)`
143
+ - Durable Object lifecycle handlers with their matching event types
229
144
 
230
- ```ts
231
- import { defineConfig } from 'devflare'
145
+ Devflare stores the active event in `AsyncLocalStorage`, and Devflare-managed entrypoints establish that context for you before your handler runs. That includes generated worker wrappers, the local dev server, and `createTestContext()` helper surfaces.
232
146
 
233
- export default defineConfig({
234
- name: 'my-api',
235
- files: {
236
- routes: {
237
- dir: 'src/routes',
238
- prefix: '/api'
239
- }
240
- }
241
- })
242
- ```
147
+ Helpers deeper in the same call trail can recover the current surface with getters like:
243
148
 
244
- This lets your app grow naturally while keeping each endpoint focused and easy to maintain.
149
+ - `getFetchEvent()`
150
+ - `getQueueEvent()`
151
+ - `getScheduledEvent()`
152
+ - `getEmailEvent()`
153
+ - `getDurableObjectFetchEvent()`
245
154
 
246
- ---
155
+ Every getter also exposes `.safe()`, which returns `null` instead of throwing.
247
156
 
248
- ## Queues without extra glue
157
+ In normal app code you should not need to call `runWithEventContext()` or `runWithContext()` yourself.
249
158
 
250
- Devflare gives queues a natural place in your app:
159
+ ---
251
160
 
252
- - produce messages from `fetch` or other handlers
253
- - consume them in `src/queue.ts`
254
- - test them with `cf.queue`
161
+ ## HTTP structure that matches the runtime today
255
162
 
256
- ```ts
257
- // devflare.config.ts
258
- import { defineConfig } from 'devflare'
163
+ The built-in HTTP story now has **two cooperating layers**:
259
164
 
260
- export default defineConfig({
261
- name: 'tasks-app',
262
- bindings: {
263
- queues: {
264
- producers: {
265
- TASK_QUEUE: 'task-queue'
266
- },
267
- consumers: [
268
- {
269
- queue: 'task-queue',
270
- maxBatchSize: 10,
271
- maxRetries: 3
272
- }
273
- ]
274
- },
275
- kv: {
276
- RESULTS: 'results-kv-id'
277
- }
278
- }
279
- })
280
- ```
165
+ 1. an optional global fetch module at `src/fetch.ts`
166
+ 2. a built-in file router rooted at `src/routes/**`
281
167
 
282
- ```ts
283
- // src/fetch.ts
284
- import { env } from 'devflare'
168
+ Use `src/fetch.ts` for request-wide middleware and whole-app HTTP concerns.
169
+ Use `src/routes/**` for leaf handlers.
285
170
 
286
- export default async function fetch(request: Request): Promise<Response> {
287
- const url = new URL(request.url)
171
+ ### Request-wide middleware
288
172
 
289
- if (url.pathname === '/tasks') {
290
- const task = {
291
- id: crypto.randomUUID(),
292
- type: 'resize-image'
293
- }
173
+ Use `sequence(...)` with a single exported primary fetch entry:
294
174
 
295
- await env.TASK_QUEUE.send(task)
296
- return Response.json({ queued: true, task })
175
+ ```ts
176
+ import { sequence } from 'devflare/runtime'
177
+ import type { FetchEvent, ResolveFetch } from 'devflare/runtime'
178
+
179
+ async function corsHandle(event: FetchEvent, resolve: ResolveFetch): Promise<Response> {
180
+ if (event.request.method === 'OPTIONS') {
181
+ return new Response(null, {
182
+ headers: {
183
+ 'Access-Control-Allow-Origin': '*',
184
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
185
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization'
186
+ }
187
+ })
297
188
  }
298
189
 
299
- return new Response('Not found', { status: 404 })
190
+ const response = await resolve(event)
191
+ const next = new Response(response.body, response)
192
+ next.headers.set('Access-Control-Allow-Origin', '*')
193
+ return next
300
194
  }
301
- ```
302
-
303
- ```ts
304
- // src/queue.ts
305
- import type { MessageBatch } from '@cloudflare/workers-types'
306
195
 
307
- type Task = {
308
- id: string
309
- type: string
196
+ async function appFetch({ request }: FetchEvent): Promise<Response> {
197
+ return Response.json({ path: new URL(request.url).pathname })
310
198
  }
311
199
 
312
- export default async function queue(
313
- batch: MessageBatch<Task>,
314
- env: DevflareEnv
315
- ): Promise<void> {
316
- for (const message of batch.messages) {
317
- try {
318
- await env.RESULTS.put(
319
- `result:${message.body.id}`,
320
- JSON.stringify({ status: 'completed', type: message.body.type })
321
- )
322
- message.ack()
323
- } catch {
324
- message.retry()
325
- }
326
- }
327
- }
200
+ export const handle = sequence(corsHandle, appFetch)
328
201
  ```
329
202
 
330
- ---
331
-
332
- ## Email
333
-
334
- Devflare supports two sides of email: **receiving** incoming messages and **sending** outgoing ones.
335
-
336
- ### Receiving email
337
-
338
- Export an `email` function from `src/email.ts`. Devflare wires it as the worker's incoming email handler automatically.
203
+ Important rules:
339
204
 
340
- ```ts
341
- // src/email.ts
342
- import { env } from 'devflare'
343
- import type { ForwardableEmailMessage } from '@cloudflare/workers-types'
344
-
345
- export async function email(message: ForwardableEmailMessage): Promise<void> {
346
- const id = crypto.randomUUID()
347
-
348
- // Log the email to KV for later inspection
349
- await env.EMAIL_LOG.put(
350
- `email:${id}`,
351
- JSON.stringify({
352
- from: message.from,
353
- to: message.to,
354
- receivedAt: new Date().toISOString()
355
- })
356
- )
357
-
358
- // Forward to an admin address
359
- await message.forward(env.FORWARD_ADDRESS)
360
- }
361
- ```
205
+ - `fetch` and `handle` are two names for the same primary HTTP entry
206
+ - export **one** of them from a given module, never both
207
+ - if `src/fetch.ts` exports same-module `GET()` / `POST()` handlers, those run before file routes for matching methods
208
+ - for route-tree apps, keep `src/fetch.ts` focused on request-wide middleware and put leaf handlers in `src/routes/**`
362
209
 
363
- You can also auto-reply with `message.reply(replyMessage)` — useful for sending confirmation receipts.
210
+ ### File router
364
211
 
365
- ### Sending email
212
+ `src/routes/**` is now a real built-in router.
366
213
 
367
- To send outgoing email, declare a `sendEmail` binding. Each key becomes an `env` property of type `SendEmail`.
214
+ By default, Devflare discovers `src/routes/**` automatically when that directory exists.
368
215
 
369
- The value is a config object that accepts an optional `destinationAddress`. When set, every email sent through that binding goes to that address. When omitted (`{}`), you specify the recipient at send time.
216
+ You can customize or disable it with `files.routes`:
370
217
 
371
218
  ```ts
372
- // devflare.config.ts
373
- import { defineConfig } from 'devflare'
374
-
375
- export default defineConfig({
376
- name: 'support-mail',
377
- bindings: {
378
- kv: {
379
- EMAIL_LOG: 'email-log-kv-id'
380
- },
381
- sendEmail: {
382
- // Locked to a specific recipient — every send goes to admin
383
- ADMIN_EMAIL: { destinationAddress: 'admin@example.com' },
384
-
385
- // Open — you choose the recipient per message
386
- EMAIL: {}
219
+ export default {
220
+ name: 'api-worker',
221
+ files: {
222
+ fetch: 'src/fetch.ts',
223
+ routes: {
224
+ dir: 'src/routes',
225
+ prefix: '/api'
387
226
  }
388
227
  }
389
- })
228
+ }
390
229
  ```
391
230
 
392
- This generates two typed bindings on `env`:
393
-
394
- - `env.ADMIN_EMAIL` always delivers to `admin@example.com`
395
- - `env.EMAIL` — you provide the destination when composing the message
396
-
397
- ### Testing email
398
-
399
- Import the `email` helper from `devflare/test` to send test messages to your running dev server:
400
-
401
- ```ts
402
- import { email } from 'devflare/test'
403
-
404
- const response = await email.send({
405
- from: 'sender@example.com',
406
- to: 'recipient@example.com',
407
- subject: 'Hello from devflare',
408
- body: 'This is a test email.'
409
- })
231
+ - `files.routes.dir` changes the route root
232
+ - `files.routes.prefix` mounts the route tree under a fixed prefix
233
+ - `files.routes: false` disables automatic route discovery entirely
410
234
 
411
- expect(response.ok).toBe(true)
412
- ```
235
+ Route conventions:
413
236
 
414
- ---
237
+ - `src/routes/index.ts` → `/`
238
+ - `src/routes/users/index.ts` → `/users`
239
+ - `src/routes/users/[id].ts` → `/users/:id`
240
+ - `src/routes/blog/[...slug].ts` → `/blog/*` with `params.slug = 'a/b'`
241
+ - `src/routes/docs/[[...slug]].ts` → optional catch-all, including `/docs`
242
+ - files or directories that start with `_` are ignored so you can keep route-local helpers nearby
415
243
 
416
- ## Durable Objects and multi-worker setups
244
+ Route module exports use the same fetch-module rules as `src/fetch.ts`:
417
245
 
418
- Devflare makes advanced Worker architecture feel much less dramatic.
246
+ - `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `ALL`
247
+ - route-local `fetch(event)` / `handle(event, resolve)` if you want per-route middleware
248
+ - `HEAD` falls back to `GET` when no explicit `HEAD` export exists
419
249
 
420
- ### Local Durable Objects
250
+ Route params are available on `event.params`.
421
251
 
422
252
  ```ts
423
- bindings: {
424
- durableObjects: {
425
- COUNTER: 'Counter'
426
- }
253
+ // src/routes/users/[id].ts
254
+ export async function GET(event): Promise<Response> {
255
+ return Response.json({ id: event.params.id })
427
256
  }
428
257
  ```
429
258
 
430
- Place the class in a `do.*.ts` file and Devflare handles the discovery flow.
259
+ ### Resolution order
431
260
 
432
- ### Write a Durable Object and call it from `fetch`
261
+ When a request comes in, Devflare resolves HTTP in this order:
433
262
 
434
- This is the most common pattern:
263
+ 1. create the initial fetch event and populate `event.params` from the matched file route, if any
264
+ 2. run the primary `src/fetch.ts` `fetch` / `handle` export if present
265
+ 3. inside `resolve(event)`, try same-module method handlers from `src/fetch.ts`
266
+ 4. if no same-module handler matched, dispatch to the matched route module from `src/routes/**`
267
+ 5. return `404 Not Found` if nothing matched
268
+
269
+ That means global middleware can see `event.params`, while route files remain the main leaf-handler story.
270
+
271
+ For example:
435
272
 
436
273
  ```ts
437
- // devflare.config.ts
438
- import { defineConfig } from 'devflare'
274
+ // src/fetch.ts
275
+ import { sequence } from 'devflare/runtime'
439
276
 
440
- export default defineConfig({
441
- name: 'sessions-app',
442
- bindings: {
443
- durableObjects: {
444
- SESSION: 'SessionStore'
445
- }
446
- }
277
+ export const handle = sequence(async (event, resolve) => {
278
+ const response = await resolve(event)
279
+ const next = new Response(response.body, response)
280
+ next.headers.set('x-user-id', event.params.id ?? 'none')
281
+ return next
447
282
  })
448
283
  ```
449
284
 
450
- ```ts
451
- // src/do.session.ts
452
- import { DurableObject } from 'cloudflare:workers'
285
+ ---
453
286
 
454
- export class SessionStore extends DurableObject<DevflareEnv> {
455
- private data = new Map<string, string>()
287
+ ## Config highlights
456
288
 
457
- getValue(key: string): string | null {
458
- return this.data.get(key) ?? null
459
- }
289
+ Devflare looks for these config filenames:
460
290
 
461
- setValue(key: string, value: string): void {
462
- this.data.set(key, value)
463
- }
291
+ - `devflare.config.ts`
292
+ - `devflare.config.mts`
293
+ - `devflare.config.js`
294
+ - `devflare.config.mjs`
464
295
 
465
- clearAll(): void {
466
- this.data.clear()
467
- }
468
- }
469
- ```
296
+ The most important top-level keys are:
470
297
 
471
- ```ts
472
- // src/fetch.ts
473
- import { env } from 'devflare'
298
+ - `name`
299
+ - `accountId`
300
+ - `compatibilityDate`
301
+ - `compatibilityFlags`
302
+ - `files`
303
+ - `bindings`
304
+ - `triggers`
305
+ - `vars`
306
+ - `secrets`
307
+ - `routes`
308
+ - `wsRoutes`
309
+ - `assets`
310
+ - `limits`
311
+ - `observability`
312
+ - `migrations`
313
+ - `rolldown`
314
+ - `vite`
315
+ - `env`
316
+ - `wrangler.passthrough`
474
317
 
475
- export default async function fetch(request: Request): Promise<Response> {
476
- const url = new URL(request.url)
477
- const sessionId = url.searchParams.get('session') ?? 'default'
318
+ ### `vars` vs `secrets`
478
319
 
479
- const id = env.SESSION.idFromName(sessionId)
480
- const session = env.SESSION.get(id)
320
+ Keep these separate:
481
321
 
482
- if (url.pathname === '/set') {
483
- const key = url.searchParams.get('key') ?? 'name'
484
- const value = url.searchParams.get('value') ?? 'Arthur'
485
- await session.setValue(key, value)
486
- return Response.json({ ok: true })
487
- }
322
+ - `vars` are **string-valued config bindings** that compile into generated Wrangler config
323
+ - `secrets` are **declarations of expected runtime secret bindings**
488
324
 
489
- if (url.pathname === '/get') {
490
- const key = url.searchParams.get('key') ?? 'name'
491
- const value = await session.getValue(key)
492
- return Response.json({ value })
493
- }
325
+ `loadConfig()` does **not** do dotenv loading by itself.
494
326
 
495
- if (url.pathname === '/clear') {
496
- await session.clearAll()
497
- return Response.json({ ok: true })
498
- }
327
+ When you run the CLI under Bun, Bun may already have loaded `.env` values into `process.env` before your config executes.
499
328
 
500
- return new Response('Not found', { status: 404 })
501
- }
502
- ```
329
+ ### `config.env`
503
330
 
504
- The flow is simple:
331
+ `config.env` is a Devflare merge layer, not a raw Wrangler env mirror.
505
332
 
506
- 1. bind the Durable Object in `devflare.config.ts`
507
- 2. implement the class in a `do.*.ts` file
508
- 3. get a stub with `env.SESSION.get(env.SESSION.idFromName(...))`
509
- 4. call your methods from `fetch`
333
+ When you select `--env name`, Devflare merges `config.env[name]` into the base config before compiling.
510
334
 
511
- ### Cross-worker references
335
+ ### Native config vs Wrangler coverage
512
336
 
513
- Use `ref()` when one worker depends on another worker or its entrypoints/DO bindings:
337
+ Devflare natively models the Worker config it actively composes around:
514
338
 
515
- ```ts
516
- import { defineConfig, ref } from 'devflare'
339
+ - handler/file surfaces
340
+ - core bindings and service composition
341
+ - routes, assets, migrations, observability, and limits
342
+ - Vite and Rolldown metadata
343
+ - environment overlays
344
+
345
+ It does **not** try to re-model every Wrangler key as a first-class Devflare schema field.
346
+ For unsupported Wrangler options, use `wrangler.passthrough`.
347
+
348
+ Current compile order is:
517
349
 
518
- const authWorker = ref(() => import('../auth/devflare.config'))
350
+ 1. Devflare compiles native config into Wrangler-compatible output
351
+ 2. `wrangler.passthrough` is shallow-merged on top
352
+ 3. if the same key exists in both places, the passthrough value wins
519
353
 
354
+ ```ts
520
355
  export default defineConfig({
521
- name: 'gateway',
522
- bindings: {
523
- services: {
524
- AUTH: authWorker.worker
356
+ name: 'advanced-worker',
357
+ files: {
358
+ fetch: 'src/fetch.ts'
359
+ },
360
+ wrangler: {
361
+ passthrough: {
362
+ placement: {
363
+ mode: 'smart'
364
+ }
525
365
  }
526
366
  }
527
367
  })
528
368
  ```
529
369
 
530
- Typed relationships, less duplication, fewer paper cuts.
370
+ Special case: `wrangler.passthrough.main` tells higher-level `build`, `deploy`, and `devflare/vite` flows to stop generating a composed `.devflare/worker-entrypoints/main.ts` and use your explicit main entry instead.
531
371
 
532
372
  ---
533
373
 
534
- ## Transport for custom types, without manual serialization
535
-
536
- Sometimes your Worker or Durable Object wants to return a real class instance, not just plain JSON.
537
-
538
- That is what `src/transport.ts` is for.
374
+ ## Bindings
539
375
 
540
- You define how to encode and decode a custom type **once**, and Devflare applies it for you across supported boundaries.
376
+ Devflare natively models:
541
377
 
542
- ```ts
543
- // src/DoubleableNumber.ts
544
- export class DoubleableNumber {
545
- value: number
546
-
547
- constructor(n: number) {
548
- this.value = n
549
- }
550
-
551
- get double() {
552
- return this.value * 2
553
- }
554
- }
555
- ```
556
-
557
- ```ts
558
- // src/transport.ts
559
- import { DoubleableNumber } from './DoubleableNumber'
378
+ - KV
379
+ - D1
380
+ - R2
381
+ - Durable Objects
382
+ - Queues
383
+ - Services
384
+ - AI
385
+ - Vectorize
386
+ - Hyperdrive
387
+ - Browser Rendering
388
+ - Analytics Engine
389
+ - `sendEmail`
560
390
 
561
- export const transport = {
562
- DoubleableNumber: {
563
- encode: (v: unknown) => v instanceof DoubleableNumber && v.value,
564
- decode: (v: number) => new DoubleableNumber(v)
565
- }
566
- }
567
- ```
391
+ ### Caveats worth knowing up front
568
392
 
569
- ```ts
570
- // src/do.counter.ts
571
- import { DoubleableNumber } from './DoubleableNumber'
393
+ - KV, D1, R2, Durable Objects, queues, and the core test/runtime flow are the strongest surfaces
394
+ - AI and Vectorize are remote-oriented bindings
395
+ - named service entrypoints are modeled at the Devflare layer, but validate generated deployment output if they are critical to your app
396
+ - `sendEmail` is modeled through config compilation, generated env types, and local runtime/test flows
397
+ - R2 bindings are real in local dev/test/runtime flows, but Devflare does **not** publish a stable browser-facing local bucket URL contract; browser-visible local asset flows should go through your Worker routes
572
398
 
573
- export class Counter {
574
- private count = 0
399
+ For R2 delivery strategy guidance, see [`R2.md`](./R2.md).
575
400
 
576
- increment(n: number = 1): DoubleableNumber {
577
- this.count += n
578
- return new DoubleableNumber(this.count)
579
- }
580
- }
581
- ```
401
+ ---
582
402
 
583
- ```ts
584
- // tests/counter.test.ts
585
- import { beforeAll, afterAll, expect, test } from 'bun:test'
586
- import { createTestContext } from 'devflare/test'
587
- import { env } from 'devflare'
588
- import { DoubleableNumber } from '../src/DoubleableNumber'
403
+ ## Dev, build, deploy, Vite, and Rolldown
589
404
 
590
- beforeAll(() => createTestContext())
591
- afterAll(() => env.dispose())
405
+ Devflare is worker-only first.
592
406
 
593
- test('transport restores my custom type', async () => {
594
- const id = env.COUNTER.idFromName('main')
595
- const counter = env.COUNTER.get(id)
596
- const result = await counter.increment(2)
407
+ ### Mode selection
597
408
 
598
- expect(result).toBeInstanceOf(DoubleableNumber)
599
- expect(result.double).toBe(4)
600
- })
601
- ```
409
+ - a local `vite.config.*` opts the current package into **Vite-backed** flows
410
+ - Vite-related dependencies without a local config do **not** switch the package into Vite mode
411
+ - without a local `vite.config.*`, `dev`, `build`, and `deploy` stay in **worker-only** mode
602
412
 
603
- The important bit: **you do not manually encode before returning or manually decode after receiving**.
413
+ ### Current behavior that matters
604
414
 
605
- Devflare handles the transport step so the developer works with the real type again.
415
+ - worker-only `dev` is a real first-class path
416
+ - `build` and `deploy` skip Vite when the current package has no local `vite.config.*`
417
+ - higher-level `build`, `deploy`, and `devflare/vite` flows currently synthesize `.devflare/worker-entrypoints/main.ts` whenever a fetch, route tree, queue, scheduled, or email surface is discovered
418
+ - `wrangler.passthrough.main` disables that composed-entry generation path
419
+ - Rolldown currently matters most in the worker-only Durable Object bundler path
606
420
 
607
421
  ---
608
422
 
609
- ## Testing that feels like the real app
423
+ ## Testing
610
424
 
611
- Devflare ships a clean testing API through `devflare/test`.
425
+ Use `devflare/test`.
612
426
 
613
- ### Integration-style tests
614
-
615
- Use `createTestContext()` for real local behavior backed by Devflare’s Miniflare orchestration.
616
-
617
- ### Mock-style tests
618
-
619
- Use:
620
-
621
- - `createMockTestContext()`
622
- - `withTestContext()`
623
- - `createMockKV()`
624
- - `createMockD1()`
625
- - `createMockR2()`
626
- - `createMockQueue()`
627
-
628
- ### Unified handler helpers
629
-
630
- You can test each surface directly:
427
+ The high-level entrypoint is `createTestContext()`, and the unified helper surface is `cf`:
631
428
 
632
429
  - `cf.worker`
633
430
  - `cf.queue`
@@ -635,56 +432,52 @@ You can test each surface directly:
635
432
  - `cf.email`
636
433
  - `cf.tail`
637
434
 
638
- That consistency is a huge quality-of-life improvement.
639
-
640
- ---
641
-
642
- ## Browser rendering, locally
643
-
644
- Browser rendering is a perfect example of Devflare adding capability above raw Miniflare.
645
-
646
- With Devflare, browser rendering becomes part of the same local development story as the rest of your Worker bindings.
647
-
648
- That means you can build richer Worker applications without stitching together a separate custom local browser setup by hand.
435
+ ### `createTestContext()` autodiscovery
649
436
 
650
- ---
651
-
652
- ## Framework support
653
-
654
- ### `devflare/vite`
437
+ If you omit the config path, `createTestContext()` walks upward from the calling test file and looks for the nearest supported config filename:
655
438
 
656
- Use this when you want:
439
+ - `devflare.config.ts`
440
+ - `devflare.config.mts`
441
+ - `devflare.config.js`
442
+ - `devflare.config.mjs`
657
443
 
658
- - config compilation during dev and build
659
- - worker-aware transforms
660
- - auxiliary Durable Object worker generation
661
- - websocket-aware local development
444
+ It also auto-detects conventional `src/fetch.ts`, `src/queue.ts`, `src/scheduled.ts`, `src/email.ts`, built-in `src/routes/**`, and `src/tail.ts` when they are present.
662
445
 
663
- ### `devflare/sveltekit`
446
+ ### Important current testing truth
664
447
 
665
- Use this when your project needs:
666
-
667
- - a Devflare-aware platform layer
668
- - smooth SvelteKit + Worker binding integration
669
- - local development that still behaves like a coherent Worker system
448
+ - `cf.worker.fetch()` returns when the handler resolves and does **not** eagerly wait for all `waitUntil()` work
449
+ - `cf.worker.fetch()` and the shorthand helpers dispatch through both `src/fetch.ts` and built-in `src/routes/**` file routes when present
450
+ - `cf.queue.trigger()` and `cf.scheduled.trigger()` do wait for queued background work before they return
451
+ - `cf.tail.trigger()` is exported, and `createTestContext()` auto-detects `src/tail.ts` when present; there is still no public `files.tail` config key
452
+ - `cf.email.send()` invokes the configured email handler in `createTestContext()`-backed tests and otherwise falls back to the local email endpoint; for ingress-fidelity-sensitive flows, validate with a higher-level integration test
453
+ - remote mode is mainly about AI and Vectorize, not “make every binding remote”
670
454
 
671
455
  ---
672
456
 
673
457
  ## CLI
674
458
 
675
- Devflare includes a CLI for the full development loop.
676
-
677
- ```text
678
- devflare init
679
- devflare dev
680
- devflare build
681
- devflare deploy
682
- devflare types
683
- devflare doctor
684
- devflare remote
685
- ```
686
-
687
- Most projects will mainly use:
459
+ | Command | What it does |
460
+ |---|---|
461
+ | `devflare init` | scaffold a project using `src/fetch.ts` and explicit `files.fetch` |
462
+ | `devflare dev` | start the worker-only dev server, enabling Vite only when the current package has a local `vite.config.*` |
463
+ | `devflare build` | resolve config, generate `wrangler.jsonc`, and run `vite build` only for Vite-backed packages |
464
+ | `devflare deploy` | build and deploy with Wrangler |
465
+ | `devflare types` | generate `env.d.ts` |
466
+ | `devflare doctor` | check project configuration and generated artifacts |
467
+ | `devflare account` | inspect accounts, resources, usage, and limits |
468
+ | `devflare ai` | show Workers AI model pricing info |
469
+ | `devflare remote` | manage remote test mode |
470
+
471
+ Useful flags:
472
+
473
+ - `build --env <name>`
474
+ - `deploy --env <name>`
475
+ - `deploy --dry-run`
476
+ - `types --output <path>`
477
+ - `doctor --config <path>`
478
+ - `account --account <id>`
479
+
480
+ Recommended invocation style:
688
481
 
689
482
  ```bash
690
483
  bunx --bun devflare dev
@@ -694,44 +487,22 @@ bunx --bun devflare build
694
487
 
695
488
  ---
696
489
 
697
- ## A good starter layout
698
-
699
- ```text
700
- .
701
- ├── devflare.config.ts
702
- ├── env.d.ts
703
- ├── src/
704
- │ ├── fetch.ts
705
- │ ├── routes/
706
- │ ├── queue.ts
707
- │ ├── scheduled.ts
708
- │ ├── email.ts
709
- │ ├── do.counter.ts
710
- │ ├── ep.admin.ts
711
- │ └── transport.ts
712
- └── tests/
713
- └── worker.test.ts
714
- ```
715
-
716
- Not every project needs every file.
717
-
718
- But if you follow this shape, Devflare will feel very natural.
490
+ ## Generated artifacts
719
491
 
720
- ---
492
+ Treat these as generated output, not source of truth:
721
493
 
722
- ## When to use Devflare
494
+ - `.devflare/`
495
+ - `env.d.ts`
496
+ - generated `wrangler.jsonc`
723
497
 
724
- Devflare is especially nice when you want:
498
+ The source of truth is still:
725
499
 
726
- - Cloudflare Workers with clean project structure
727
- - Durable Object-heavy apps
728
- - multi-worker systems with service bindings
729
- - local-first development with realistic tests
730
- - browser rendering in your local workflow
731
- - Vite or SvelteKit apps that still want a clean Worker model
500
+ - `devflare.config.ts`
501
+ - your source files under `src/`
502
+ - your tests
732
503
 
733
504
  ---
734
505
 
735
506
  ## In one sentence
736
507
 
737
- **Devflare helps you build Cloudflare Workers with clearer structure, better local tooling, and a development experience that scales with your app.**
508
+ **Devflare helps you build Cloudflare Workers with clearer structure, better local tooling, and a development workflow that stays coherent as the app grows.**