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.
Files changed (123) hide show
  1. package/LLM.md +1114 -0
  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 +22 -19
  122. package/dist/index.js +0 -298
  123. 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 &#124; false</code> | `src/fetch.ts` | main HTTP handler |
575
+ | `queue` | <code>string &#124; false</code> | `src/queue.ts` | queue consumer handler |
576
+ | `scheduled` | <code>string &#124; false</code> | `src/scheduled.ts` | scheduled handler |
577
+ | `email` | <code>string &#124; false</code> | `src/email.ts` | incoming email handler |
578
+ | `durableObjects` | <code>string &#124; false</code> | `**/do.*.{ts,js}` | Durable Object discovery glob |
579
+ | `entrypoints` | <code>string &#124; false</code> | `**/ep.*.{ts,js}` | WorkerEntrypoint discovery glob |
580
+ | `workflows` | <code>string &#124; false</code> | `**/wf.*.{ts,js}` | workflow discovery glob |
581
+ | `routes` | <code>{ dir, prefix? } &#124; 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.