devflare 1.0.0-next.1 → 1.0.0-next.11

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 +1424 -610
  2. package/R2.md +200 -0
  3. package/README.md +302 -505
  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-ezksv2dd.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 +2594 -1234
  33. package/dist/config/schema.d.ts.map +1 -1
  34. package/dist/{deploy-nhceck39.js → deploy-jdpy21t6.js} +33 -15
  35. package/dist/{dev-qnxet3j9.js → dev-9mq7zhww.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-z4ffybce.js} +73 -50
  45. package/dist/{durable-object-t4kbb0yt.js → durable-object-yt8v1dyn.js} +1 -1
  46. package/dist/index-1p814k7s.js +227 -0
  47. package/dist/{index-tk6ej9dj.js → index-2q3pmzrx.js} +12 -16
  48. package/dist/{index-67qcae0f.js → index-51s1hkw4.js} +16 -1
  49. package/dist/{index-ep3445yc.js → index-53xcakh8.js} +414 -171
  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-8gtqgb3q.js +529 -0
  55. package/dist/{index-gz1gndna.js → index-9wt9x09k.js} +42 -62
  56. package/dist/index-dr6sbp8d.js +39 -0
  57. package/dist/index-fef08w43.js +231 -0
  58. package/dist/index-k7r18na8.js +0 -0
  59. package/dist/{index-m2q41jwa.js → index-n932ytmq.js} +9 -1
  60. package/dist/{index-07q6yxyc.js → index-v8vvsn9x.js} +1 -0
  61. package/dist/index-vky23txa.js +70 -0
  62. package/dist/{index-z14anrqp.js → index-wfbfz02q.js} +14 -15
  63. package/dist/index-ws68xvq2.js +311 -0
  64. package/dist/{index-hcex3rgh.js → index-wyf3s77s.js} +85 -8
  65. package/dist/index-xqfbd9fx.js +195 -0
  66. package/dist/index-xxwbb2nt.js +322 -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-nq5acrwh.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/LLM.md CHANGED
@@ -1,976 +1,1790 @@
1
1
  # Devflare
2
2
 
3
- This file is for LLMs and developers **using** the `devflare` library.
3
+ This file is the truth-first contract for `devflare` as implemented today.
4
4
 
5
- It explains the public mental model, the intended project structure, and the practical authoring patterns that make Devflare different from using raw Wrangler + Miniflare directly.
5
+ Use it when you are:
6
6
 
7
- It does **not** document the internal repository layout or example cases. Treat it as package-facing guidance.
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.
8
12
 
9
13
  ---
10
14
 
11
- ## What Devflare is
15
+ ## Quick start: safest supported path
12
16
 
13
- Devflare is a **developer platform for Cloudflare Workers built on top of Miniflare, Wrangler-compatible config, and framework-aware tooling**.
17
+ Prefer this shape unless you have a concrete reason not to:
14
18
 
15
- The short version:
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
16
26
 
17
- > Miniflare gives you a local Workers runtime. Devflare turns that runtime into a coherent developer system.
27
+ ```ts
28
+ import { defineConfig } from 'devflare'
18
29
 
19
- Devflare does this by combining:
30
+ export default defineConfig({
31
+ name: 'hello-worker',
32
+ compatibilityDate: '2026-03-17',
33
+ files: {
34
+ fetch: 'src/fetch.ts'
35
+ }
36
+ })
37
+ ```
20
38
 
21
- - typed config via `defineConfig()`
22
- - convention-driven file discovery
23
- - binding compilation to Wrangler-compatible config
24
- - local Miniflare orchestration
25
- - Node ↔ Worker bridging where needed
26
- - dedicated test helpers for each handler type
27
- - framework integration for Vite and SvelteKit
28
- - local development features that Miniflare alone does not provide cleanly, such as **browser rendering support**, multi-surface orchestration, and a consistent file-structured app model
39
+ ```ts
40
+ import type { FetchEvent } from 'devflare/runtime'
29
41
 
30
- If you are building Cloudflare applications and want more structure than a single `worker.ts` plus manual plumbing, Devflare is the layer that supplies that structure.
42
+ export async function fetch(event: FetchEvent): Promise<Response> {
43
+ return Response.json({
44
+ path: new URL(event.request.url).pathname
45
+ })
46
+ }
47
+ ```
31
48
 
32
49
  ---
33
50
 
34
- ## How Devflare extends Miniflare
51
+ ## Trust map
52
+
53
+ ### Layers that are easy to confuse
35
54
 
36
- This is the most important concept to understand.
55
+ Keep these layers separate until you have a reason to connect them:
37
56
 
38
- Devflare is **not** trying to replace Miniflare. It **extends** Miniflare so developers can work with a higher-level application model.
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
39
62
 
40
- ### Raw Miniflare gives you a runtime
63
+ The classic mixups are:
41
64
 
42
- Miniflare is excellent at local execution and local emulation of many Cloudflare bindings.
65
+ - `files.routes` vs top-level `routes`
66
+ - `vars` vs `secrets`
67
+ - Bun or host `.env*` loading vs runtime secret loading
68
+ - config-time `.env*` files vs local runtime `.dev.vars*` files
69
+ - Devflare `config.env` overrides vs Wrangler environment blocks
70
+ - main-entry `env` vs runtime `env`
43
71
 
44
- But by itself, it does not define:
72
+ ### Source of truth vs generated output
45
73
 
46
- - how your project should be structured
47
- - how `fetch`, `queue`, `scheduled`, `email`, routes, DOs, and entrypoints should relate
48
- - how cross-worker references should stay typed
49
- - how tests should address each handler surface consistently
50
- - how framework output and auxiliary workers should be wired together
51
- - how to simulate browser rendering locally in a coherent workflow
74
+ Treat these as generated output, not authoring input:
52
75
 
53
- ### Devflare adds the missing platform layer
76
+ - `.devflare/`
77
+ - `env.d.ts`
78
+ - generated `wrangler.jsonc`
54
79
 
55
- Devflare adds those higher-level capabilities on top of Miniflare.
80
+ The source of truth is still:
56
81
 
57
- Concretely, it provides:
82
+ - `devflare.config.ts`
83
+ - your source files under `src/`
84
+ - your tests
58
85
 
59
- 1. **Convention-first file structure**
60
- - `src/fetch.ts`
61
- - `src/queue.ts`
62
- - `src/scheduled.ts`
63
- - `src/email.ts`
64
- - `src/routes/**`
65
- - `do.*.ts`
66
- - `ep.*.ts`
67
- - `wf.*.ts`
68
- - `src/transport.ts`
86
+ If generated output looks wrong, fix the source and regenerate it. Do not hand-edit generated artifacts.
69
87
 
70
- 2. **Config compilation**
71
- - Devflare compiles your config into Wrangler-compatible config rather than making you hand-maintain the low-level representation yourself.
88
+ ---
72
89
 
73
- 3. **Responsibility isolation**
74
- - Instead of one giant worker file containing every concern, Devflare encourages one surface per responsibility: fetch, queue, cron, email, routes, Durable Objects, WorkerEntrypoints, workflows, and transport.
90
+ ## What Devflare is
75
91
 
76
- 4. **Unified testing surface**
77
- - `cf.worker`
78
- - `cf.queue`
79
- - `cf.scheduled`
80
- - `cf.email`
81
- - `cf.tail`
92
+ Devflare is a developer-first layer on top of Cloudflare Workers tooling.
82
93
 
83
- 5. **Bridge-backed local development**
84
- - Devflare handles Node-side access, env access, transport decoding, and Miniflare orchestration so tests and tooling feel like one system.
94
+ It composes existing tools instead of replacing them:
85
95
 
86
- 6. **Capabilities beyond raw Miniflare**
87
- - most notably **browser rendering support** through a local browser shim that lets browser-rendering workflows run locally in a way Miniflare does not provide directly
96
+ - **Miniflare** supplies the local Workers runtime
97
+ - **Wrangler** supplies deployment config and deploy workflows
98
+ - **Vite** participates when the current package opts into Vite-backed mode
99
+ - **Bun** is the CLI runtime and affects process-level `.env` loading
100
+ - **Devflare** ties those pieces into one authoring model
88
101
 
89
- 7. **Framework integration**
90
- - Vite and SvelteKit support with config generation, worker-aware transforms, auxiliary Durable Object workers, and websocket-aware local dev workflows
102
+ Core public capabilities today:
91
103
 
92
- So the design goal is not just “run a Worker locally”.
104
+ - typed config via `defineConfig()`
105
+ - compilation from Devflare config into Wrangler-compatible output
106
+ - event-first runtime helpers
107
+ - request-scoped proxies and per-surface getters
108
+ - typed multi-worker references via `ref()`
109
+ - local orchestration around Miniflare, Vite, and test helpers
110
+ - framework-aware Vite and SvelteKit integration
93
111
 
94
- The design goal is:
112
+ Devflare is not a replacement runtime. It is a higher-level developer system that sits on top of the Cloudflare ecosystem.
95
113
 
96
- > let developers build Cloudflare systems using coherent, isolated responsibilities while Devflare composes those responsibilities into a working local and testable whole
114
+ The shortest truthful mental model is:
97
115
 
98
- ---
116
+ - **Vite** is the optional outer app/framework host. Devflare plugs into it only when the current package has a local `vite.config.*`.
117
+ - **Rolldown** is the inner builder Devflare uses when Devflare itself needs to transform Worker source into runnable Worker modules. Today that shows up most clearly in the Durable Object bundler and its watch/rebuild loop.
99
118
 
100
- ## The authoring model Devflare wants you to use
119
+ ### Authoring model
101
120
 
102
- Devflare is opinionated in a useful way.
121
+ A **surface** is a distinct handler or entry file that Devflare treats as its own concern. The common surfaces are:
103
122
 
104
- It wants you to think in **surfaces** rather than in “one file that does everything”.
123
+ - HTTP via `src/fetch.ts`
124
+ - queue consumers via `src/queue.ts`
125
+ - scheduled handlers via `src/scheduled.ts`
126
+ - incoming email via `src/email.ts`
127
+ - Durable Objects via `do.*.ts`
128
+ - WorkerEntrypoints via `ep.*.ts`
129
+ - workflows via `wf.*.ts`
130
+ - custom transport definitions via `src/transport.ts`
105
131
 
106
- ### HTTP surface
132
+ 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.
107
133
 
108
- Use `src/fetch.ts` for normal request handling.
134
+ ---
109
135
 
110
- ### Queue surface
136
+ ## Package entrypoints
111
137
 
112
- Use `src/queue.ts` for queue consumers.
138
+ Use the narrowest import path that matches where the code runs.
113
139
 
114
- ### Scheduled surface
140
+ | Import | Use it for | Practical rule |
141
+ |---|---|---|
142
+ | `devflare` | main package entrypoint | config helpers, `ref()`, `workerName`, the main-entry `env`, and selected Node-side helpers |
143
+ | `devflare/runtime` | handler/runtime code | event types, middleware, strict `env` / `ctx` / `event` / `locals`, and per-surface getters |
144
+ | `devflare/test` | tests | `createTestContext()`, `cf.*`, and test helpers |
145
+ | `devflare/vite` | Vite integration | explicit Vite-side helpers |
146
+ | `devflare/sveltekit` | SvelteKit integration | SvelteKit-facing helpers |
147
+ | `devflare/cloudflare` | Cloudflare account and resource helpers | account/resource/usage helpers |
148
+ | `devflare/decorators` | decorators only | `durableObject()` and related decorator utilities |
115
149
 
116
- Use `src/scheduled.ts` for cron triggers.
150
+ ### `devflare` vs `devflare/runtime`
117
151
 
118
- ### Email surface
152
+ Default rule:
119
153
 
120
- Use `src/email.ts` for incoming email handling. Use `sendEmail` bindings in config for outgoing email.
154
+ 1. use handler parameters first
155
+ 2. use `devflare/runtime` in helpers that run inside a live handler trail
156
+ 3. use the main `devflare` entry when you specifically want the fallback-friendly `env`
121
157
 
122
- ### Routing surface
158
+ `import { env } from 'devflare/runtime'` is strict request-scoped access. It works only while Devflare has established an active handler context.
123
159
 
124
- Use `files.routes` with a directory like `src/routes` when you want file-based HTTP routing instead of one monolithic fetch file.
160
+ `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.
125
161
 
126
- ### Durable Object surface
162
+ Practical example:
127
163
 
128
- Use `do.*.ts` files for Durable Objects.
164
+ ```ts
165
+ // worker code
166
+ import { env, locals, type FetchEvent } from 'devflare/runtime'
167
+
168
+ export async function fetch(event: FetchEvent<DevflareEnv>): Promise<Response> {
169
+ const pathname = new URL(event.request.url).pathname
170
+ locals.startedAt = Date.now()
171
+
172
+ const cached = await env.CACHE.get(pathname)
173
+ if (cached) {
174
+ return new Response(cached, {
175
+ headers: {
176
+ 'x-cache': 'hit'
177
+ }
178
+ })
179
+ }
180
+
181
+ return new Response(`miss:${pathname}`)
182
+ }
183
+ ```
184
+
185
+ ```ts
186
+ // test or bridge code
187
+ import { env } from 'devflare'
188
+ import { createTestContext } from 'devflare/test'
129
189
 
130
- ### RPC / service-entry surface
190
+ await createTestContext()
191
+ await env.CACHE.put('health', 'ok')
192
+ ```
131
193
 
132
- Use `ep.*.ts` files for named WorkerEntrypoints.
194
+ ### Worker-safe caveat for the main entry
133
195
 
134
- ### Workflow surface
196
+ `devflare/runtime` is the explicit worker-safe runtime entry and should be the default teaching path for worker code.
135
197
 
136
- Use `wf.*.ts` files for Workflow classes.
198
+ 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.
137
199
 
138
- ### Transport surface
200
+ In that worker-safe main bundle:
139
201
 
140
- Use `src/transport.ts` when values crossing boundaries need custom serialization.
202
+ - the browser-safe subset of the main entry remains usable
203
+ - Node-side APIs such as config loading, CLI helpers, Miniflare orchestration, and test-context setup are not available and intentionally throw if called
141
204
 
142
- This is one of Devflare’s biggest advantages: it turns Cloudflare development from “single worker blob” into **separated, discoverable, testable responsibilities**.
205
+ If you need `ctx`, `event`, `locals`, middleware, or per-surface getters, import them from `devflare/runtime`.
143
206
 
144
207
  ---
145
208
 
146
- ## Public package entry points
209
+ ## Runtime and HTTP model
147
210
 
148
- Use the package subpaths intentionally.
211
+ ### Event-first handlers are the public story
149
212
 
150
- | Import | Use for |
151
- |---|---|
152
- | `devflare` | config, `ref()`, `env`, bridge/test convenience exports |
153
- | `devflare/runtime` | runtime-safe utilities for Worker code |
154
- | `devflare/test` | test context, mocks, `cf.*` helpers |
155
- | `devflare/vite` | Vite integration |
156
- | `devflare/sveltekit` | SvelteKit integration |
157
- | `devflare/cloudflare` | Cloudflare-specific helpers |
158
- | `devflare/decorators` | decorators such as `durableObject()` |
213
+ Fresh Devflare code should be event-first. Use shapes like:
159
214
 
160
- ---
215
+ - `fetch(event: FetchEvent)`
216
+ - `queue(event: QueueEvent)`
217
+ - `scheduled(event: ScheduledEvent)`
218
+ - `email(event: EmailEvent)`
219
+ - `tail(event: TailEvent)` when you are wiring a tail surface
220
+ - Durable Object handlers with their matching event types
221
+
222
+ These event types augment native Cloudflare inputs rather than replacing them with unrelated wrappers.
223
+
224
+ | Event type | Also behaves like | Convenience fields |
225
+ |---|---|---|
226
+ | `FetchEvent` | `Request` | `request`, `env`, `ctx`, `params`, `locals`, `type` |
227
+ | `QueueEvent` | `MessageBatch<T>` | `batch`, `env`, `ctx`, `locals`, `type` |
228
+ | `ScheduledEvent` | `ScheduledController` | `controller`, `env`, `ctx`, `locals`, `type` |
229
+ | `EmailEvent` | `ForwardableEmailMessage` | `message`, `env`, `ctx`, `locals`, `type` |
230
+ | `TailEvent` | `TraceItem[]` | `events`, `env`, `ctx`, `locals`, `type` |
231
+ | `DurableObjectFetchEvent` | `Request` | `request`, `env`, `ctx`, `state`, `locals`, `type` |
232
+ | `DurableObjectAlarmEvent` | event object only | `env`, `ctx`, `state`, `locals`, `type` |
233
+ | `DurableObjectWebSocketMessageEvent` | `WebSocket` | `ws`, `message`, `env`, `ctx`, `state`, `locals`, `type` |
234
+ | `DurableObjectWebSocketCloseEvent` | `WebSocket` | `ws`, `code`, `reason`, `wasClean`, `env`, `ctx`, `state`, `locals`, `type` |
235
+ | `DurableObjectWebSocketErrorEvent` | `WebSocket` | `ws`, `error`, `env`, `ctx`, `state`, `locals`, `type` |
236
+
237
+ On worker surfaces, `event.ctx` is the current `ExecutionContext`.
238
+
239
+ On Durable Object surfaces, `event.ctx` is the current `DurableObjectState`, and Devflare also exposes it as `event.state` for clarity.
240
+
241
+ ### Runtime access: parameters, getters, and proxies
161
242
 
162
- ## Core ideas you should internalize
243
+ Prefer runtime access in this order:
163
244
 
164
- ### `defineConfig()` is the center of the project
245
+ 1. handler parameters such as `fetch(event: FetchEvent)`
246
+ 2. per-surface getters when a deeper helper needs the current concrete surface
247
+ 3. generic runtime proxies when surface-agnostic access is enough
165
248
 
166
- Always define your worker through `defineConfig()`.
249
+ Devflare carries the active event through the current handler call trail, so deeper helpers can recover the current surface without manually threading arguments.
250
+
251
+ Proxy semantics:
252
+
253
+ - `env`, `ctx`, and `event` from `devflare/runtime` are readonly
254
+ - `locals` is the mutable request-scoped storage object
255
+ - `event.locals` and `locals` point at the same underlying object
256
+ - strict runtime helpers throw when there is no active Devflare-managed context
257
+
258
+ Per-surface getters:
259
+
260
+ - worker surfaces: `getFetchEvent()`, `getQueueEvent()`, `getScheduledEvent()`, `getEmailEvent()`, `getTailEvent()`
261
+ - Durable Object surfaces: `getDurableObjectEvent()`, `getDurableObjectFetchEvent()`, `getDurableObjectAlarmEvent()`
262
+ - Durable Object WebSocket surfaces: `getDurableObjectWebSocketMessageEvent()`, `getDurableObjectWebSocketCloseEvent()`, `getDurableObjectWebSocketErrorEvent()`
263
+
264
+ Every getter also exposes `.safe()`, which returns `null` instead of throwing.
265
+
266
+ Practical example:
167
267
 
168
268
  ```ts
169
- import { defineConfig } from 'devflare'
269
+ import { getFetchEvent, locals, type FetchEvent } from 'devflare/runtime'
170
270
 
171
- export default defineConfig({
172
- name: 'my-worker'
173
- })
271
+ function currentPath(): string {
272
+ return new URL(getFetchEvent().request.url).pathname
273
+ }
274
+
275
+ export async function fetch(event: FetchEvent): Promise<Response> {
276
+ locals.requestId = crypto.randomUUID()
277
+
278
+ return Response.json({
279
+ path: currentPath(),
280
+ requestId: String(locals.requestId),
281
+ requestUrl: getFetchEvent.safe()?.request.url ?? null,
282
+ method: event.request.method
283
+ })
284
+ }
174
285
  ```
175
286
 
176
- This is the source of truth Devflare uses to:
287
+ ### Manual context helpers are advanced/internal
177
288
 
178
- - understand your bindings
179
- - understand your file layout
180
- - generate Wrangler-compatible config
181
- - drive dev mode
182
- - drive test mode
289
+ Normal Devflare application code should **not** need `runWithEventContext()` or `runWithContext()`.
183
290
 
184
- ### Convention beats boilerplate
291
+ Devflare establishes the active event/context automatically before invoking user code in the flows developers normally use:
185
292
 
186
- If your filenames follow the defaults, you do not need to spell everything out.
293
+ - generated worker entrypoints for `fetch`, `queue`, `scheduled`, and `email`
294
+ - Durable Object wrappers
295
+ - HTTP middleware and route resolution
296
+ - `createTestContext()` helpers such as `cf.worker`, `cf.queue`, `cf.scheduled`, `cf.email`, and `cf.tail`
187
297
 
188
- Defaults include:
298
+ That means getters like `getQueueEvent()`, `getScheduledEvent()`, `getEmailEvent()`, and `getDurableObjectFetchEvent()` should work inside ordinary handlers without manual wrapping.
189
299
 
190
- - `src/fetch.ts`
191
- - `src/queue.ts`
192
- - `src/scheduled.ts`
193
- - `src/email.ts`
194
- - `src/transport.ts`
195
- - `**/do.*.{ts,js}`
196
- - `**/ep.*.{ts,js}`
197
- - `**/wf.*.{ts,js}`
300
+ Keep these helpers in the "advanced escape hatch" bucket:
198
301
 
199
- ### `ref()` is how cross-worker systems stay clean
302
+ - `runWithEventContext(event, fn)` preserves the exact event you provide
303
+ - `runWithContext(env, ctx, request, fn, type)` is a lower-level compatibility helper
200
304
 
201
- If one worker depends on another worker or on another worker’s Durable Objects, use `ref()` instead of manually duplicating names.
305
+ Important difference:
202
306
 
203
- That keeps cross-worker relationships typed and centralized.
307
+ - `runWithContext()` only synthesizes rich augmented events for `fetch` and `durable-object-fetch` when a `Request` is available
308
+ - 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
204
309
 
205
- ### `env` is the unified access point
310
+ ### HTTP entry, middleware, and method handlers
206
311
 
207
- Use:
312
+ Think of the built-in HTTP path today as:
313
+
314
+ `src/fetch.ts` request-wide entry → same-module method handlers → matched `src/routes/**` leaf module
315
+
316
+ When the HTTP surface matters to build or deploy output, prefer making it explicit in config:
208
317
 
209
318
  ```ts
210
- import { env } from 'devflare'
319
+ files: {
320
+ fetch: 'src/fetch.ts'
321
+ }
211
322
  ```
212
323
 
213
- Devflare makes `env` work coherently across request handling, tests, and bridge-backed local flows.
324
+ The file router is enabled automatically when a `src/routes` directory exists, unless you set `files.routes: false`.
325
+ Use `files.routes` when you want to change the route root or mount it under a prefix.
214
326
 
215
- ---
327
+ Supported primary entry shapes today:
216
328
 
217
- ## Why file structure matters so much in Devflare
329
+ - `export async function fetch(...) { ... }`
330
+ - `export const fetch = ...`
331
+ - `export const handle = ...`
332
+ - `export default async function (...) { ... }`
333
+ - `export default { fetch(...) { ... } }`
334
+ - `export default { handle(...) { ... } }`
218
335
 
219
- In plain Worker setups, it is common to end up with a single file that mixes:
336
+ `fetch` and `handle` are aliases for the same primary HTTP entry. Export one or the other, not both.
220
337
 
221
- - HTTP routing
222
- - queue handling
223
- - scheduled jobs
224
- - email handling
225
- - Durable Object exports
226
- - auxiliary RPC handlers
227
- - test-specific wiring
338
+ When this document says a `handle` export here, it means the primary fetch export name, not the legacy `handle(...handlers)` helper.
228
339
 
229
- Devflare pushes in the opposite direction.
340
+ Request-wide middleware belongs in `src/fetch.ts` and composes with `sequence(...)`.
230
341
 
231
- It gives each responsibility a natural home and then composes them together.
342
+ ```ts
343
+ import { sequence } from 'devflare/runtime'
344
+ import type { FetchEvent, ResolveFetch } from 'devflare/runtime'
232
345
 
233
- That has several benefits:
346
+ async function authHandle(event: FetchEvent, resolve: ResolveFetch): Promise<Response> {
347
+ if (!event.request.headers.get('authorization')) {
348
+ return new Response('Unauthorized', { status: 401 })
349
+ }
234
350
 
235
- - lower mental overhead
236
- - easier code generation
237
- - easier code review
238
- - easier testing
239
- - fewer accidental runtime coupling mistakes
240
- - clearer ownership boundaries within a team
351
+ return resolve(event)
352
+ }
241
353
 
242
- For LLM generation specifically, this matters a lot.
354
+ async function appFetch({ request }: FetchEvent): Promise<Response> {
355
+ return new Response(new URL(request.url).pathname)
356
+ }
243
357
 
244
- When generating Devflare code, prefer **separate files with clear responsibilities** over giant all-in-one worker files.
358
+ export const handle = sequence(authHandle, appFetch)
359
+ ```
245
360
 
246
- ---
361
+ `sequence(...)` uses the usual nested middleware flow:
362
+
363
+ 1. outer middleware before `resolve(event)`
364
+ 2. inner middleware before `resolve(event)`
365
+ 3. downstream leaf handler
366
+ 4. inner middleware after `resolve(event)`
367
+ 5. outer middleware after `resolve(event)`
368
+
369
+ Same-module method exports are real runtime behavior:
370
+
371
+ - method handlers such as `GET`, `POST`, and `ALL` are supported in the fetch module
372
+ - if `HEAD` is not exported, it falls back to `GET` with an empty body
373
+ - this is same-module dispatch, not file-system routing
374
+ - 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)`
375
+ - if no primary `fetch` or `handle` exists, Devflare can still dispatch directly to same-module method handlers
376
+
377
+ ### Routing today
378
+
379
+ `src/routes/**` is now a real built-in router.
380
+
381
+ Default behavior:
382
+
383
+ - if `src/routes` exists, Devflare discovers it automatically
384
+ - `files.routes.dir` changes the route root
385
+ - `files.routes.prefix` mounts the discovered routes under a fixed prefix such as `/api`
386
+ - `files.routes: false` disables file-route discovery
387
+
388
+ Filename conventions:
389
+
390
+ - `index.ts` → directory root
391
+ - `[id].ts` → single dynamic segment
392
+ - `[...slug].ts` → rest segment, one or more path parts
393
+ - `[[...slug]].ts` → optional rest segment, including the directory root
394
+ - files or directories beginning with `_` are ignored so route-local helpers can live beside handlers
395
+
396
+ Dispatch semantics:
397
+
398
+ - route params are populated on `event.params`
399
+ - route modules use the same handler forms as fetch modules: HTTP method exports, primary `fetch`, or primary `handle`
400
+ - if `src/fetch.ts` exports same-module `GET` / `POST` / `ALL` handlers, those run before the file router for matching methods
401
+ - for route-tree apps, keep `src/fetch.ts` focused on request-wide middleware and whole-app concerns
402
+ - `resolve(event)` from the primary fetch module falls through to the matched route module when no same-module method handler responded
247
403
 
248
- ## Config example
404
+ | Key | What it means today | What it does not do |
405
+ |---|---|---|
406
+ | `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` |
407
+ | top-level `routes` | Cloudflare deployment route patterns such as `example.com/*`, compiled into generated Wrangler config | it does not choose handlers inside your app |
408
+ | `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 |
249
409
 
250
- This is a good realistic starting point.
410
+ Practical route-tree example:
251
411
 
252
412
  ```ts
413
+ // devflare.config.ts
253
414
  import { defineConfig } from 'devflare'
254
415
 
255
416
  export default defineConfig({
256
- name: 'my-app',
417
+ name: 'api-worker',
257
418
  files: {
419
+ fetch: 'src/fetch.ts',
258
420
  routes: {
259
421
  dir: 'src/routes',
260
422
  prefix: '/api'
261
423
  }
262
- },
263
- bindings: {
264
- kv: {
265
- CACHE: 'cache-kv-id'
266
- },
267
- d1: {
268
- DB: 'db-id'
269
- },
270
- r2: {
271
- FILES: 'files-bucket'
272
- },
273
- durableObjects: {
274
- COUNTER: 'Counter'
275
- },
276
- queues: {
277
- producers: {
278
- TASK_QUEUE: 'task-queue'
279
- },
280
- consumers: [
281
- {
282
- queue: 'task-queue',
283
- maxBatchSize: 10,
284
- maxRetries: 3
285
- }
286
- ]
287
- },
288
- browser: {
289
- binding: 'BROWSER'
290
- }
291
- },
292
- triggers: {
293
- crons: ['0 */6 * * *']
294
- },
295
- vars: {
296
- LOG_LEVEL: 'info'
297
424
  }
298
425
  })
299
426
  ```
300
427
 
428
+ ```ts
429
+ // src/fetch.ts
430
+ import { sequence } from 'devflare/runtime'
431
+
432
+ export const handle = sequence(async (event, resolve) => {
433
+ const response = await resolve(event)
434
+ const next = new Response(response.body, response)
435
+ next.headers.set('x-user-id', event.params.id ?? 'none')
436
+ return next
437
+ })
438
+ ```
439
+
440
+ ```ts
441
+ // src/routes/users/[id].ts
442
+ import type { FetchEvent } from 'devflare/runtime'
443
+
444
+ export async function GET({ params }: FetchEvent): Promise<Response> {
445
+ return Response.json({ id: params.id })
446
+ }
447
+ ```
448
+
449
+ ```ts
450
+ // GET /api/users/123
451
+ // -> { "id": "123" }
452
+ // and x-user-id: 123
453
+ ```
454
+
301
455
  ---
302
456
 
303
- ## File-based routing
457
+ ## Configuration and compilation
304
458
 
305
- Devflare supports a file-structured routing model through `files.routes`.
459
+ ### Stable config flow
306
460
 
307
- That is important because it moves you away from hand-written request dispatch in one file and toward a more maintainable app structure.
461
+ This is the stable public flow for Devflare config:
308
462
 
309
- Example:
463
+ 1. write `devflare.config.*` and export it with `defineConfig()`
464
+ 2. Devflare loads the base config
465
+ 3. if you select an environment, Devflare merges `config.env[name]` into the base config
466
+ 4. Devflare compiles the resolved config into Wrangler-compatible output
467
+ 5. commands such as `dev`, `build`, `deploy`, and `types` use that resolved config for their own work
468
+
469
+ 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.
470
+
471
+ ### Supported config files and `defineConfig()`
472
+
473
+ Supported config filenames:
474
+
475
+ - `devflare.config.ts`
476
+ - `devflare.config.mts`
477
+ - `devflare.config.js`
478
+ - `devflare.config.mjs`
479
+
480
+ 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.
310
481
 
311
482
  ```ts
312
483
  import { defineConfig } from 'devflare'
313
484
 
314
485
  export default defineConfig({
315
- name: 'my-api',
486
+ name: 'api-worker',
487
+ compatibilityDate: '2026-03-17',
316
488
  files: {
317
- routes: {
318
- dir: 'src/routes',
319
- prefix: '/api'
320
- }
489
+ fetch: 'src/fetch.ts'
490
+ },
491
+ vars: {
492
+ APP_NAME: 'api-worker'
321
493
  }
322
494
  })
323
495
  ```
324
496
 
325
- Conceptually, this means:
497
+ 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.
326
498
 
327
- - route files map onto URL paths
328
- - route handlers can stay focused on their specific endpoint logic
329
- - your HTTP surface can scale without turning `fetch.ts` into a switch-statement graveyard
499
+ ### Top-level keys
330
500
 
331
- If you only need a small Worker, `src/fetch.ts` is perfect.
501
+ 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.
332
502
 
333
- If your HTTP surface is growing, `files.routes` is usually the better model.
503
+ Current defaults worth knowing:
334
504
 
335
- ---
505
+ - `compatibilityDate` defaults to the current date when omitted
506
+ - current default compatibility flags include `nodejs_compat` and `nodejs_als`
507
+
508
+ Common keys:
509
+
510
+ | Key | Use it for |
511
+ |---|---|
512
+ | `name` | Worker name |
513
+ | `compatibilityDate` | Workers compatibility date |
514
+ | `compatibilityFlags` | Workers compatibility flags |
515
+ | `files` | explicit handler paths and discovery globs |
516
+ | `bindings` | Cloudflare bindings |
517
+ | `triggers` | scheduled trigger config |
518
+ | `vars` | non-secret string bindings |
519
+ | `secrets` | secret binding declarations |
520
+ | `routes` | Cloudflare deployment routes |
521
+ | `env` | environment overrides merged before compile |
522
+
523
+ Secondary keys:
524
+
525
+ | Key | Use it for |
526
+ |---|---|
527
+ | `accountId` | account selection for remote and deploy flows |
528
+ | `observability` | Workers observability settings |
529
+ | `migrations` | Durable Object migrations |
530
+ | `assets` | static asset config |
531
+ | `limits` | worker limits |
532
+ | `wsRoutes` | local WebSocket proxy rules for dev |
533
+ | `vite` | Devflare Vite metadata |
534
+ | `rolldown` | Durable Object bundler options |
535
+ | `wrangler.passthrough` | raw Wrangler overrides after compile |
536
+
537
+ ### Complete config property reference
538
+
539
+ Treat this as the exhaustive property checklist for `defineConfig({...})`. The later sections explain behavior in narrative form; this section answers “what keys exist right now, what shape do they accept, and what do they control?”
540
+
541
+ #### Root properties
542
+
543
+ | Property | Shape | Required | Current behavior |
544
+ |---|---|---|---|
545
+ | `name` | `string` | yes | Worker name compiled to Wrangler `name`. It is also the base name used for generated auxiliary DO workers (`${name}-do`) when Devflare creates one. |
546
+ | `accountId` | `string` | no | Compiled to Wrangler `account_id`. Most relevant for deploy flows and remote-oriented bindings such as AI and Vectorize. Not currently supported inside `config.env`. |
547
+ | `compatibilityDate` | `string` (`YYYY-MM-DD`) | no | Compiled to Wrangler `compatibility_date`. Defaults to the current date when omitted. |
548
+ | `compatibilityFlags` | `string[]` | no | Additional Workers compatibility flags. Devflare also forces `nodejs_compat` and `nodejs_als`. |
549
+ | `files` | object | no | Explicit surface file paths and discovery globs. Use this when a surface matters to generated output. |
550
+ | `bindings` | object | no | Cloudflare binding declarations. These compile to Wrangler binding sections and also drive generated typing. |
551
+ | `triggers` | object | no | Scheduled trigger configuration such as cron expressions. |
552
+ | `vars` | `Record<string, string>` | no | Non-secret runtime bindings compiled into Wrangler `vars`. |
553
+ | `secrets` | `Record<string, { required?: boolean }>` | no | Secret declarations only. Values do not live here. |
554
+ | `routes` | `Array<{ pattern; zone_name?; zone_id?; custom_domain? }>` | no | Cloudflare deployment routes. This is separate from the built-in file router. |
555
+ | `wsRoutes` | `Array<{ pattern; doNamespace; idParam?; forwardPath? }>` | no | Dev-only WebSocket proxy rules that forward matching upgrade requests to Durable Objects. Not compiled into Wrangler config. |
556
+ | `assets` | `{ directory: string; binding?: string }` | no | Static asset directory config compiled into Wrangler `assets`. |
557
+ | `limits` | `{ cpu_ms?: number }` | no | Worker execution limits compiled into Wrangler `limits`. |
558
+ | `observability` | `{ enabled?: boolean; head_sampling_rate?: number }` | no | Workers observability and sampling config compiled into Wrangler `observability`. |
559
+ | `migrations` | `Migration[]` | no | Durable Object migration history compiled into Wrangler `migrations`. |
560
+ | `rolldown` | object | no | Rolldown builder configuration for Devflare's own code-transformation path, currently focused on Durable Object workers. This is not a replacement for `vite.config.*`. |
561
+ | `vite` | object | no | Devflare-owned Vite metadata namespace. Raw Vite server/build/resolve config still belongs in local `vite.config.*`. |
562
+ | `env` | `Record<string, EnvOverride>` | no | Named environment overlays merged into the base config before compile/build/deploy. |
563
+ | `wrangler` | `{ passthrough?: Record<string, unknown> }` | no | Escape hatch for unsupported Wrangler keys, merged after native Devflare compile. |
564
+ | `build` | object | legacy | Deprecated alias normalized into `rolldown`. Keep accepting it for compatibility; teach `rolldown` in new docs. |
565
+ | `plugins` | `unknown[]` | legacy | Deprecated alias normalized into `vite.plugins`. Raw Vite plugin wiring still belongs in `vite.config.*`. |
566
+
567
+ #### `files`
568
+
569
+ `files` is where Devflare discovers or pins the files that make up your worker surfaces.
570
+
571
+ | Key | Shape | Default or convention | Meaning |
572
+ |---|---|---|---|
573
+ | `fetch` | `string \| false` | `src/fetch.ts` when present | Main HTTP entry. Keep this explicit when build or deploy output depends on it. |
574
+ | `queue` | `string \| false` | `src/queue.ts` when present | Queue consumer surface. |
575
+ | `scheduled` | `string \| false` | `src/scheduled.ts` when present | Scheduled/cron surface. |
576
+ | `email` | `string \| false` | `src/email.ts` when present | Inbound email surface. |
577
+ | `durableObjects` | `string \| false` | `**/do.*.{ts,js}` | Discovery glob for Durable Object classes. Respects `.gitignore`. |
578
+ | `entrypoints` | `string \| false` | `**/ep.*.{ts,js}` | Discovery glob for `WorkerEntrypoint` classes. Respects `.gitignore`. |
579
+ | `workflows` | `string \| false` | `**/wf.*.{ts,js}` | Discovery glob for workflow classes. Respects `.gitignore`. |
580
+ | `routes` | `{ dir: string; prefix?: string } \| false` | `src/routes` when that directory exists | Built-in route-tree config. `dir` changes the route root; `prefix` mounts it under a static pathname prefix such as `/api`; `false` disables route discovery. |
581
+ | `transport` | `string \| null` | `src/transport.{ts,js,mts,mjs}` when present | Custom serialization transport file. The file must export a named `transport` object. Set `null` to disable autodiscovery explicitly. |
582
+
583
+ Current `files` rules worth keeping explicit:
584
+
585
+ - `compileConfig()` only writes Wrangler `main` when `files.fetch` is explicit
586
+ - higher-level `build`, `deploy`, and `devflare/vite` flows may still generate a composed `.devflare/worker-entrypoints/main.ts` when multiple surfaces must be stitched together
587
+ - `wrangler.passthrough.main` opts out of that composed-entry generation path
588
+ - `createTestContext()` and local dev can still auto-discover conventional files even when you omit them from config
589
+
590
+ #### `bindings`
591
+
592
+ `bindings` groups Cloudflare service bindings by kind.
593
+
594
+ | Key | Shape | Meaning |
595
+ |---|---|---|
596
+ | `kv` | `Record<string, string>` | KV namespace binding name → namespace id |
597
+ | `d1` | `Record<string, string>` | D1 binding name → database id |
598
+ | `r2` | `Record<string, string>` | R2 binding name → bucket name |
599
+ | `durableObjects` | `Record<string, string \| { className: string; scriptName?: string }>` | Durable Object namespace binding. String form is shorthand for `{ className }`. Object form also covers cross-worker DOs and `ref()`-driven bindings. |
600
+ | `queues` | `{ producers?: Record<string, string>; consumers?: QueueConsumer[] }` | Queue producer bindings plus consumer settings |
601
+ | `services` | `Record<string, { service: string; environment?: string; entrypoint?: string }>` | Worker service bindings. `ref().worker` and `ref().worker('Entrypoint')` normalize here. |
602
+ | `ai` | `{ binding: string }` | Workers AI binding |
603
+ | `vectorize` | `Record<string, { indexName: string }>` | Vectorize index bindings |
604
+ | `hyperdrive` | `Record<string, { id: string }>` | Hyperdrive bindings |
605
+ | `browser` | `{ binding: string }` | Browser Rendering binding |
606
+ | `analyticsEngine` | `Record<string, { dataset: string }>` | Analytics Engine dataset bindings |
607
+ | `sendEmail` | `Record<string, { destinationAddress?: string; allowedDestinationAddresses?: string[]; allowedSenderAddresses?: string[] }>` | Outbound email bindings |
608
+
609
+ Queue consumer objects currently support:
610
+
611
+ | Field | Shape | Meaning |
612
+ |---|---|---|
613
+ | `queue` | `string` | Queue name to consume |
614
+ | `maxBatchSize` | `number` | Max messages per batch |
615
+ | `maxBatchTimeout` | `number` | Max seconds to wait for a batch |
616
+ | `maxRetries` | `number` | Max retry attempts |
617
+ | `deadLetterQueue` | `string` | Queue for permanently failed messages |
618
+ | `maxConcurrency` | `number` | Max concurrent batch invocations |
619
+ | `retryDelay` | `number` | Delay between retries in seconds |
620
+
621
+ Two `bindings` details that matter in practice:
622
+
623
+ - `bindings.sendEmail` must use either `destinationAddress` or `allowedDestinationAddresses`, not both
624
+ - `bindings.durableObjects.*.scriptName` is how you point a binding at another worker when the class does not live in the main worker bundle
625
+
626
+ #### `triggers`, `routes`, and `wsRoutes`
627
+
628
+ | Property | Shape | Current behavior |
629
+ |---|---|---|
630
+ | `triggers.crons` | `string[]` | Cloudflare cron expressions compiled into Wrangler `triggers.crons` |
631
+ | `routes[].pattern` | `string` | Deployment route pattern such as `example.com/*` |
632
+ | `routes[].zone_name` | `string` | Optional zone association |
633
+ | `routes[].zone_id` | `string` | Optional zone association alternative to `zone_name` |
634
+ | `routes[].custom_domain` | `boolean` | Mark route as a custom domain |
635
+ | `wsRoutes[].pattern` | `string` | Local URL pattern to intercept for WebSocket upgrades |
636
+ | `wsRoutes[].doNamespace` | `string` | Target Durable Object namespace binding name |
637
+ | `wsRoutes[].idParam` | `string` | Query parameter used to pick the DO instance. Defaults to `'id'`. |
638
+ | `wsRoutes[].forwardPath` | `string` | Path forwarded inside the DO. Defaults to `'/websocket'`. |
639
+
640
+ Remember the split:
641
+
642
+ - `files.routes` is app routing
643
+ - top-level `routes` is Cloudflare deployment routing
644
+ - `wsRoutes` is local dev-time WebSocket proxy routing for Durable Objects
645
+
646
+ #### `vars` and `secrets`
647
+
648
+ | Property | Shape | Current behavior |
649
+ |---|---|---|
650
+ | `vars` | `Record<string, string>` | Non-secret runtime bindings compiled into Wrangler `vars` |
651
+ | `secrets` | `Record<string, { required?: boolean }>` | Secret declarations only. `required` defaults to `true`. Values must come from Cloudflare secrets, tests, or upstream local tooling. |
652
+
653
+ #### `assets`, `limits`, and `observability`
654
+
655
+ | Property | Shape | Current behavior |
656
+ |---|---|---|
657
+ | `assets.directory` | `string` | Required asset directory path |
658
+ | `assets.binding` | `string` | Optional asset binding name for programmatic access |
659
+ | `limits.cpu_ms` | `number` | Optional CPU limit for unbound workers |
660
+ | `observability.enabled` | `boolean` | Enable Worker Logs |
661
+ | `observability.head_sampling_rate` | `number` | Log sampling rate from `0` to `1` |
662
+
663
+ #### `migrations`
664
+
665
+ Each migration object has this shape:
666
+
667
+ | Field | Shape | Meaning |
668
+ |---|---|---|
669
+ | `tag` | `string` | Required migration version label |
670
+ | `new_classes` | `string[]` | Newly introduced DO classes |
671
+ | `renamed_classes` | `Array<{ from: string; to: string }>` | DO class renames with state preservation |
672
+ | `deleted_classes` | `string[]` | Deleted DO classes |
673
+ | `new_sqlite_classes` | `string[]` | DO classes migrating to SQLite storage |
674
+
675
+ #### `rolldown`
676
+
677
+ `rolldown` config applies to Devflare's Durable Object bundler.
678
+
679
+ | Key | Shape | Current behavior |
680
+ |---|---|---|
681
+ | `target` | `string` | Bundle target for emitted DO bundles |
682
+ | `minify` | `boolean` | Minify DO bundles |
683
+ | `sourcemap` | `boolean` | Emit source maps for DO bundles |
684
+ | `options` | `DevflareRolldownOptions` | Additional Rolldown input/output options and plugins, minus Devflare-owned fields |
685
+
686
+ Current `rolldown.options` ownership rules:
687
+
688
+ - Devflare owns `cwd`, `input`, `platform`, and `watch`
689
+ - Devflare also owns output `codeSplitting`, `dir`, `file`, `format`, and `inlineDynamicImports`
690
+ - output stays single-file ESM so the local DO bundling story remains worker-friendly
691
+ - `rolldown.options.plugins` is the intended extension point for custom transforms and Rollup-compatible plugins
692
+
693
+ #### `vite`
694
+
695
+ `vite` is the Devflare-side Vite metadata namespace, not the place for raw Vite app config.
696
+
697
+ | Key | Shape | Current behavior |
698
+ |---|---|---|
699
+ | `plugins` | `unknown[]` | Accepted by the schema and normalized from the legacy top-level `plugins` alias |
700
+ | any other key | `unknown` | Preserved by the schema as Devflare-level Vite metadata, but raw Vite server/build/resolve/plugin wiring still belongs in local `vite.config.*` |
701
+
702
+ That distinction is intentional:
703
+
704
+ - put real Vite config in `vite.config.ts`
705
+ - use `devflare/vite` helpers when Devflare needs to participate in the Vite pipeline
706
+ - treat `config.vite` as Devflare-owned metadata, not as a drop-in replacement for Vite's own config file
707
+
708
+ #### `env`
709
+
710
+ `env` is `Record<string, EnvOverride>`, where each environment can currently override these keys:
336
711
 
337
- ## Queues: produce in one place, consume in another
712
+ - `name`
713
+ - `compatibilityDate`
714
+ - `compatibilityFlags`
715
+ - `files`
716
+ - `bindings`
717
+ - `triggers`
718
+ - `vars`
719
+ - `secrets`
720
+ - `routes`
721
+ - `assets`
722
+ - `limits`
723
+ - `observability`
724
+ - `migrations`
725
+ - `rolldown`
726
+ - `vite`
727
+ - `wrangler`
728
+ - deprecated `build`
729
+ - deprecated `plugins`
338
730
 
339
- Devflare wants queue production and queue consumption to live in obvious places.
731
+ Current exclusions still matter:
340
732
 
341
- - produce messages from `fetch` or another handler via the bound queue producer
342
- - consume them in `src/queue.ts`
343
- - test them with `cf.queue`
733
+ - `accountId` is not supported inside `env`
734
+ - `wsRoutes` is not supported inside `env`
735
+ - nested `env` blocks are not part of the override shape
344
736
 
345
- ### Queue config example
737
+ Merge behavior is also part of the contract:
738
+
739
+ - scalars override base values
740
+ - nested objects merge
741
+ - arrays append instead of replacing
742
+ - `null` and `undefined` do not delete inherited values
743
+
744
+ #### `wrangler`
745
+
746
+ `wrangler` currently exposes one native child key:
747
+
748
+ | Key | Shape | Current behavior |
749
+ |---|---|---|
750
+ | `passthrough` | `Record<string, unknown>` | Shallow-merged on top of the compiled Wrangler config. Use this for unsupported Wrangler keys or to take full ownership of `main`. |
751
+
752
+ #### Deprecated aliases
753
+
754
+ | Legacy key | Current canonical key | Notes |
755
+ |---|---|---|
756
+ | `build.target` | `rolldown.target` | Deprecated but still normalized |
757
+ | `build.minify` | `rolldown.minify` | Deprecated but still normalized |
758
+ | `build.sourcemap` | `rolldown.sourcemap` | Deprecated but still normalized |
759
+ | `build.rolldownOptions` | `rolldown.options` | Deprecated but still normalized |
760
+ | `plugins` | `vite.plugins` | Deprecated top-level alias; raw Vite plugin wiring still belongs in `vite.config.*` |
761
+
762
+ ### Native config coverage vs `wrangler.passthrough`
763
+
764
+ 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.
765
+
766
+ Use `wrangler.passthrough` for unsupported Wrangler options.
767
+
768
+ Current merge order is:
769
+
770
+ 1. compile native Devflare config
771
+ 2. shallow-merge `wrangler.passthrough` on top
772
+ 3. if the same key exists in both places, the passthrough value wins
773
+
774
+ Practical example:
346
775
 
347
776
  ```ts
348
777
  import { defineConfig } from 'devflare'
349
778
 
350
779
  export default defineConfig({
351
- name: 'tasks-app',
352
- bindings: {
353
- queues: {
354
- producers: {
355
- TASK_QUEUE: 'task-queue'
356
- },
357
- consumers: [
358
- {
359
- queue: 'task-queue',
360
- maxBatchSize: 10,
361
- maxRetries: 3
362
- }
363
- ]
364
- },
365
- kv: {
366
- RESULTS: 'results-kv-id'
780
+ name: 'advanced-worker',
781
+ files: {
782
+ fetch: 'src/fetch.ts'
783
+ },
784
+ wrangler: {
785
+ passthrough: {
786
+ placement: {
787
+ mode: 'smart'
788
+ }
367
789
  }
368
790
  }
369
791
  })
370
792
  ```
371
793
 
372
- ### Producer example from `fetch`
794
+ Two practical rules:
373
795
 
374
- ```ts
375
- import { env } from 'devflare'
796
+ - if you want Devflare-managed entry composition, keep using `files.fetch`, `files.routes`, and related surface config, and do **not** set `wrangler.passthrough.main`
797
+ - if you want total control of the Worker `main` entry, set `wrangler.passthrough.main` and own that entry file yourself
376
798
 
377
- export default async function fetch(request: Request): Promise<Response> {
378
- const url = new URL(request.url)
799
+ ### `files`
379
800
 
380
- if (url.pathname === '/tasks') {
381
- const task = {
382
- id: crypto.randomUUID(),
383
- type: 'resize-image'
384
- }
801
+ `files` tells Devflare where your surfaces live. Use explicit paths for any surface that matters to build or deploy output, especially `files.fetch`.
385
802
 
386
- await env.TASK_QUEUE.send(task)
387
- return Response.json({ queued: true, task })
388
- }
803
+ | Key | Shape | Default convention | Meaning |
804
+ |---|---|---|---|
805
+ | `fetch` | <code>string &#124; false</code> | `src/fetch.ts` | main HTTP handler |
806
+ | `queue` | <code>string &#124; false</code> | `src/queue.ts` | queue consumer handler |
807
+ | `scheduled` | <code>string &#124; false</code> | `src/scheduled.ts` | scheduled handler |
808
+ | `email` | <code>string &#124; false</code> | `src/email.ts` | incoming email handler |
809
+ | `durableObjects` | <code>string &#124; false</code> | `**/do.*.{ts,js}` | Durable Object discovery glob |
810
+ | `entrypoints` | <code>string &#124; false</code> | `**/ep.*.{ts,js}` | WorkerEntrypoint discovery glob |
811
+ | `workflows` | <code>string &#124; false</code> | `**/wf.*.{ts,js}` | workflow discovery glob |
812
+ | `routes` | <code>{ dir, prefix? } &#124; false</code> | `src/routes` when that directory exists | built-in file router configuration |
813
+ | `transport` | <code>string &#124; null</code> | `src/transport.{ts,js,mts,mjs}` when one of those files exists | custom transport definition file |
389
814
 
390
- return new Response('Not found', { status: 404 })
391
- }
392
- ```
815
+ Discovery does not behave identically in every subsystem:
393
816
 
394
- ### Consumer example in `src/queue.ts`
817
+ - local dev and `createTestContext()` can fall back to conventional handler files when present
818
+ - local dev, `createTestContext()`, and higher-level worker-entry generation also auto-discover `src/routes/**` unless `files.routes` is `false`
819
+ - type generation and discovery use default globs for Durable Objects, entrypoints, and workflows
820
+ - `compileConfig()` only writes Wrangler `main` when `files.fetch` is explicit
821
+ - higher-level `build`, `deploy`, and `devflare/vite` flows may still generate a composed `.devflare/worker-entrypoints/main.ts`, including route trees
822
+ - `wrangler.passthrough.main` disables that composed-entry generation path
395
823
 
396
- ```ts
397
- import type { MessageBatch } from '@cloudflare/workers-types'
824
+ Safe rule: if a surface matters to build or deploy output, declare it explicitly even if another subsystem can discover it by convention.
398
825
 
399
- type Task = {
400
- id: string
401
- type: string
402
- }
826
+ Practical example:
403
827
 
404
- export default async function queue(
405
- batch: MessageBatch<Task>,
406
- env: DevflareEnv
407
- ): Promise<void> {
408
- for (const message of batch.messages) {
409
- try {
410
- await env.RESULTS.put(
411
- `result:${message.body.id}`,
412
- JSON.stringify({ status: 'completed', type: message.body.type })
413
- )
414
- message.ack()
415
- } catch {
416
- message.retry()
417
- }
828
+ ```ts
829
+ import { defineConfig } from 'devflare'
830
+
831
+ export default defineConfig({
832
+ name: 'multi-surface-worker',
833
+ files: {
834
+ fetch: 'src/fetch.ts',
835
+ queue: 'src/queue.ts',
836
+ scheduled: 'src/scheduled.ts',
837
+ email: 'src/email.ts',
838
+ routes: {
839
+ dir: 'src/routes',
840
+ prefix: '/api'
841
+ },
842
+ durableObjects: 'src/do/**/*.ts',
843
+ entrypoints: 'src/rpc/**/*.ts',
844
+ workflows: 'src/workflows/**/*.ts'
418
845
  }
419
- }
846
+ })
420
847
  ```
421
848
 
422
- Authoring guidance:
849
+ `files.transport` is convention-first. `createTestContext()` auto-loads `src/transport.{ts,js,mts,mjs}` when present, a string value points at a different transport file, and `files.transport: null` disables transport loading explicitly. The file must export a named `transport` object.
423
850
 
424
- - put queue consumers in `src/queue.ts` unless you have a strong reason not to
425
- - keep message bodies plain and serializable
426
- - use `message.ack()` on success and `message.retry()` on failure
427
- - bind persistence or downstream systems in config rather than hiding them in globals
851
+ There is no public `files.tail` config key today.
428
852
 
429
- ---
853
+ ### `.env`, `.dev.vars`, `vars`, `secrets`, and `config.env`
430
854
 
431
- ## Email: receiving and sending
855
+ Keep these layers separate:
432
856
 
433
- Devflare supports two sides of email: **receiving** incoming messages and **sending** outgoing ones.
857
+ | Layer | Holds values? | Compiled into generated config? | Use it for |
858
+ |---|---|---|---|
859
+ | `.env` / `process.env` | yes | indirectly, only when your config reads from it | local process-time inputs |
860
+ | `.env.dev` / `.env.<name>` | tool/runtime-dependent | indirectly at most, only if the surrounding tool has already populated `process.env` | local process-time variants, not a Devflare-native contract |
861
+ | `.dev.vars` / `.dev.vars.<name>` | yes | no | local runtime secret/value files in upstream Cloudflare tooling when applicable |
862
+ | `vars` | yes | yes | non-secret string bindings |
863
+ | `secrets` | no, declaration only | no | required/optional runtime secret bindings |
864
+ | `config.env` | yes, as config overlays | yes after merge | environment-specific config overrides |
434
865
 
435
- ### Receiving email
866
+ #### `.env`, `.env.dev`, and process env
436
867
 
437
- Export an `email` function from `src/email.ts`. Devflare wires it as the worker's incoming email handler.
868
+ `loadConfig()` does not do dotenv loading by itself. The Devflare CLI runs under Bun, and Bun may auto-load `.env` files into `process.env`.
438
869
 
439
- ```ts
440
- // src/email.ts
441
- import { env } from 'devflare'
442
- import type { ForwardableEmailMessage } from '@cloudflare/workers-types'
443
-
444
- export async function email(message: ForwardableEmailMessage): Promise<void> {
445
- const id = crypto.randomUUID()
446
-
447
- // Log the email to KV
448
- await env.EMAIL_LOG.put(
449
- `email:${id}`,
450
- JSON.stringify({
451
- from: message.from,
452
- to: message.to,
453
- receivedAt: new Date().toISOString()
454
- })
455
- )
870
+ Important boundary:
456
871
 
457
- // Forward every incoming message to admin
458
- await message.forward('admin@example.com')
459
- }
460
- ```
872
+ - Devflare sets `dotenv: false` in its config loader
873
+ - Devflare does **not** define special first-class semantics for `.env.dev` or `.env.<name>`
874
+ - if those files affect `process.env`, that comes from the surrounding host tool/runtime rather than a Devflare-native loader
875
+ - config-time/build-time code can still read `process.env` inside `defineConfig()` or other Node-side tooling
461
876
 
462
- The `ForwardableEmailMessage` object also supports `message.reply(replyMessage)` for auto-responses.
877
+ Treat `.env*` files as **config/build-time inputs**, not as Devflare's runtime secret system.
463
878
 
464
- ### Sending email (the `sendEmail` binding)
879
+ #### `.dev.vars` and local runtime secrets
465
880
 
466
- To send outgoing email, declare a `sendEmail` binding in config. Each key becomes a typed `env` property of type `SendEmail`.
881
+ Devflare does **not** currently implement its own first-class `.dev.vars` / `.dev.vars.<name>` loader for worker-only dev mode or `createTestContext()`.
467
882
 
468
- The value object accepts an optional `destinationAddress`:
883
+ That means:
469
884
 
470
- - **With `destinationAddress`** every email sent through that binding is locked to that recipient.
471
- - **Without it (`{}`)** you choose the recipient at send time.
885
+ - do not document `.dev.vars*` as a guaranteed Devflare-native feature across all modes
886
+ - `secrets` declares the names of expected runtime secrets, but does not provide values
887
+ - worker-only dev and `createTestContext()` should not be described as automatically materializing secret values from `.dev.vars*`
472
888
 
473
- ```ts
474
- import { defineConfig } from 'devflare'
889
+ In Vite-backed flows, some local runtime variable behavior may come from upstream Cloudflare/Vite tooling rather than from Devflare itself. Document that as inherited upstream behavior, not as a unified Devflare contract.
475
890
 
476
- export default defineConfig({
477
- name: 'support-mail',
478
- bindings: {
479
- kv: {
480
- EMAIL_LOG: 'email-log-kv-id'
481
- },
482
- sendEmail: {
483
- // Locked destination — always delivers to admin
484
- ADMIN_EMAIL: { destinationAddress: 'admin@example.com' },
891
+ #### `vars`
485
892
 
486
- // Open destination specify per message
487
- EMAIL: {}
488
- }
489
- }
490
- })
491
- ```
893
+ Use `vars` for non-secret runtime values that can safely appear in generated config, such as public URLs, modes, IDs, and feature flags.
492
894
 
493
- This produces:
895
+ In the current native Devflare schema, `vars` is:
494
896
 
495
- - `env.ADMIN_EMAIL: SendEmail` — hardcoded to `admin@example.com`
496
- - `env.EMAIL: SendEmail` — recipient provided at send time
897
+ ```ts
898
+ Record<string, string>
899
+ ```
497
900
 
498
- ### Testing email
901
+ #### `secrets`
499
902
 
500
- Use the `email` helper from `devflare/test`:
903
+ `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.
904
+
905
+ In the current schema, each secret has the shape:
501
906
 
502
907
  ```ts
503
- import { email } from 'devflare/test'
908
+ { required?: boolean }
909
+ ```
504
910
 
505
- const response = await email.send({
506
- from: 'sender@example.com',
507
- to: 'recipient@example.com',
508
- subject: 'Test message',
509
- body: 'Hello from the test suite.'
510
- })
911
+ `required` defaults to `true`, so this:
511
912
 
512
- expect(response.ok).toBe(true)
913
+ ```ts
914
+ secrets: {
915
+ API_KEY: {}
916
+ }
513
917
  ```
514
918
 
515
- Authoring guidance:
919
+ means “`API_KEY` is a required runtime secret,” not “optional secret with no requirements.”
516
920
 
517
- - use `src/email.ts` for incoming mail handling
518
- - use `message.forward(...)` for routing workflows
519
- - use `message.reply(...)` when implementing auto-replies
520
- - use `sendEmail` bindings with `destinationAddress` when the recipient is always the same
521
- - use `sendEmail` bindings with `{}` when you need flexible per-message recipients
522
- - log metadata to KV or D1 if you need observability or replay/debugging context
921
+ In practice, secret **values** come from outside Devflare config:
523
922
 
524
- ---
923
+ - Cloudflare-stored runtime secrets in deployed environments
924
+ - explicit test injection or lower-level mocks in tests
925
+ - upstream local-dev tooling when you intentionally rely on it
525
926
 
526
- ## Browser rendering is a real example of Devflare extending Miniflare
927
+ Do not describe `secrets` as a place that stores values.
527
928
 
528
- This is worth emphasizing.
929
+ #### Example files such as `.env.example` and `.dev.vars.example`
529
930
 
530
- Devflare supports **browser rendering** locally via a browser shim layer.
931
+ Example files are a **team convention**, not a Devflare feature.
531
932
 
532
- That matters because browser rendering is not something Miniflare gives you directly as a complete local developer workflow.
933
+ Current truthful guidance:
533
934
 
534
- With Devflare, browser rendering becomes part of the same system as the rest of your bindings and local development story:
935
+ - use `.env.example` to document required config-time/build-time variables that your config or Node-side tooling reads from `process.env`
936
+ - use `.dev.vars.example` to document expected local runtime secret names **if your project chooses to rely on upstream `.dev.vars` workflows**
937
+ - keep example files committed with placeholder or fake values only
938
+ - do not claim that Devflare auto-generates, auto-loads, or validates these example files today
535
939
 
536
- - declare the browser binding in config
537
- - run the app through Devflare’s orchestration
538
- - use the browser capability locally with the rest of your worker system
940
+ #### Canonical env and secrets layout
539
941
 
540
- This is exactly the kind of feature that demonstrates the Devflare philosophy:
942
+ If you want the lowest-confusion setup, use this split:
541
943
 
542
- > take a low-level runtime foundation, then add the higher-level orchestration developers actually need
944
+ - `.env.example` documents config-time/build-time inputs
945
+ - `.dev.vars.example` documents local runtime secret names **only if your project intentionally relies on upstream `.dev.vars` workflows**
946
+ - `devflare.config.ts` reads config-time values from `process.env`, puts safe runtime values in `vars`, and declares required runtime secret names in `secrets`
947
+ - deployed secret values live in Cloudflare, not in your repo
543
948
 
544
- ---
949
+ Recommended project shape:
545
950
 
546
- ## Durable Objects and multi-worker systems
951
+ ```text
952
+ my-worker/
953
+ ├─ .env.example
954
+ ├─ .dev.vars.example # optional; only if you intentionally use upstream .dev.vars flows
955
+ ├─ .gitignore
956
+ ├─ devflare.config.ts
957
+ └─ src/
958
+ └─ fetch.ts
959
+ ```
547
960
 
548
- Devflare is designed to keep multi-worker and DO-heavy systems manageable.
961
+ Example `.env.example`:
549
962
 
550
- ### Local Durable Objects
963
+ ```dotenv
964
+ WORKER_NAME=my-worker
965
+ API_ORIGIN=http://localhost:3000
966
+ ```
551
967
 
552
- Bind them in config:
968
+ Example `.dev.vars.example`:
553
969
 
554
- ```ts
555
- bindings: {
556
- durableObjects: {
557
- COUNTER: 'Counter'
558
- }
559
- }
970
+ ```dotenv
971
+ API_KEY=replace-me
972
+ SESSION_SECRET=replace-me
560
973
  ```
561
974
 
562
- Put their classes in `do.*.ts` files.
975
+ Typical git ignore pattern for user projects:
563
976
 
564
- ### Complete local Durable Object example
977
+ ```gitignore
978
+ .env
979
+ .env.*
980
+ !.env.example
981
+ .dev.vars
982
+ .dev.vars.*
983
+ !.dev.vars.example
984
+ ```
565
985
 
566
- If you need to generate a normal local Durable Object and call it from HTTP code, prefer this shape.
986
+ Example `devflare.config.ts`:
567
987
 
568
988
  ```ts
569
- // devflare.config.ts
570
989
  import { defineConfig } from 'devflare'
571
990
 
572
991
  export default defineConfig({
573
- name: 'sessions-app',
574
- bindings: {
575
- durableObjects: {
576
- SESSION: 'SessionStore'
992
+ name: process.env.WORKER_NAME ?? 'my-worker',
993
+ compatibilityDate: '2026-03-17',
994
+ files: {
995
+ fetch: 'src/fetch.ts'
996
+ },
997
+ vars: {
998
+ API_ORIGIN: process.env.API_ORIGIN ?? 'http://localhost:3000'
999
+ },
1000
+ secrets: {
1001
+ API_KEY: {},
1002
+ SESSION_SECRET: {}
1003
+ },
1004
+ env: {
1005
+ production: {
1006
+ vars: {
1007
+ API_ORIGIN: 'https://api.example.com'
1008
+ }
577
1009
  }
578
1010
  }
579
1011
  })
580
1012
  ```
581
1013
 
1014
+ Example `src/fetch.ts`:
1015
+
582
1016
  ```ts
583
- // src/do.session.ts
584
- import { DurableObject } from 'cloudflare:workers'
1017
+ import type { FetchEvent } from 'devflare/runtime'
585
1018
 
586
- export class SessionStore extends DurableObject<DevflareEnv> {
587
- private data = new Map<string, string>()
1019
+ export async function GET({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
1020
+ return Response.json({
1021
+ origin: env.API_ORIGIN,
1022
+ hasApiKey: Boolean(env.API_KEY),
1023
+ hasSessionSecret: Boolean(env.SESSION_SECRET)
1024
+ })
1025
+ }
1026
+ ```
588
1027
 
589
- getValue(key: string): string | null {
590
- return this.data.get(key) ?? null
591
- }
1028
+ Deployed runtime secrets should be created with Cloudflare/Wrangler tooling, not committed to config files or example files.
592
1029
 
593
- setValue(key: string, value: string): void {
594
- this.data.set(key, value)
595
- }
1030
+ Typical deployed secret flow:
596
1031
 
597
- clearAll(): void {
598
- this.data.clear()
599
- }
600
- }
1032
+ ```bash
1033
+ bunx --bun wrangler secret put API_KEY
1034
+ bunx --bun wrangler secret put SESSION_SECRET
601
1035
  ```
602
1036
 
603
- ```ts
604
- // src/fetch.ts
605
- import { env } from 'devflare'
1037
+ If you use named Cloudflare environments, set the secret in that environment explicitly:
606
1038
 
607
- export default async function fetch(request: Request): Promise<Response> {
608
- const url = new URL(request.url)
609
- const sessionId = url.searchParams.get('session') ?? 'default'
1039
+ ```bash
1040
+ bunx --bun wrangler secret put API_KEY --env production
1041
+ bunx --bun wrangler secret put SESSION_SECRET --env production
1042
+ ```
610
1043
 
611
- const id = env.SESSION.idFromName(sessionId)
612
- const session = env.SESSION.get(id)
1044
+ Practical rule of thumb:
613
1045
 
614
- if (url.pathname === '/set') {
615
- const key = url.searchParams.get('key') ?? 'name'
616
- const value = url.searchParams.get('value') ?? 'Arthur'
617
- await session.setValue(key, value)
618
- return Response.json({ ok: true })
619
- }
1046
+ - if a value is needed while evaluating config, put it in the `.env*` / `process.env` bucket
1047
+ - if a value should exist as a runtime binding but must not be committed, declare it in `secrets`
1048
+ - if a project wants local runtime secret files, treat `.dev.vars*` as an upstream convention and document it explicitly per project
620
1049
 
621
- if (url.pathname === '/get') {
622
- const key = url.searchParams.get('key') ?? 'name'
623
- const value = await session.getValue(key)
624
- return Response.json({ value })
625
- }
1050
+ #### `devflare types`
626
1051
 
627
- if (url.pathname === '/clear') {
628
- await session.clearAll()
629
- return Response.json({ ok: true })
630
- }
1052
+ `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.
631
1053
 
632
- return new Response('Not found', { status: 404 })
633
- }
634
- ```
1054
+ 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.
635
1055
 
636
- Important authoring notes:
1056
+ #### `config.env`
637
1057
 
638
- - extend `DurableObject` from `cloudflare:workers`
639
- - bind the namespace in `bindings.durableObjects`
640
- - put the class in a `do.*.ts` file so discovery works naturally
641
- - create a stub with `env.SESSION.get(env.SESSION.idFromName(...))`
642
- - keep RPC-style method inputs and outputs simple and serializable
1058
+ `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.
643
1059
 
644
- ### Transport layer example: Devflare encodes and decodes for you
1060
+ Current merge behavior uses deep merge semantics:
645
1061
 
646
- Use `src/transport.ts` when a Worker or Durable Object returns custom classes that should survive supported RPC/test boundaries as real instances.
1062
+ - scalar fields override base values
1063
+ - nested objects inherit omitted keys
1064
+ - arrays append instead of replacing
1065
+ - `null` and `undefined` do not delete base values
647
1066
 
648
- ```ts
649
- // src/DoubleableNumber.ts
650
- export class DoubleableNumber {
651
- value: number
1067
+ If you need full replacement behavior, compute the final value in `defineConfig()` instead of relying on `config.env`.
652
1068
 
653
- constructor(n: number) {
654
- this.value = n
655
- }
656
-
657
- get double() {
658
- return this.value * 2
659
- }
660
- }
661
- ```
1069
+ Example:
662
1070
 
663
1071
  ```ts
664
- // src/transport.ts
665
- import { DoubleableNumber } from './DoubleableNumber'
1072
+ import { defineConfig } from 'devflare'
666
1073
 
667
- export const transport = {
668
- DoubleableNumber: {
669
- encode: (v: unknown) => v instanceof DoubleableNumber && v.value,
670
- decode: (v: number) => new DoubleableNumber(v)
1074
+ export default defineConfig({
1075
+ name: 'api',
1076
+ files: {
1077
+ fetch: 'src/fetch.ts'
1078
+ },
1079
+ vars: {
1080
+ APP_ENV: 'development',
1081
+ API_ORIGIN: 'http://localhost:3000'
1082
+ },
1083
+ bindings: {
1084
+ kv: {
1085
+ CACHE: 'api-cache-dev'
1086
+ }
1087
+ },
1088
+ env: {
1089
+ production: {
1090
+ vars: {
1091
+ APP_ENV: 'production',
1092
+ API_ORIGIN: 'https://api.example.com'
1093
+ },
1094
+ bindings: {
1095
+ kv: {
1096
+ CACHE: 'api-cache'
1097
+ }
1098
+ }
1099
+ }
671
1100
  }
672
- }
1101
+ })
673
1102
  ```
674
1103
 
675
- ```ts
676
- // src/do.counter.ts
677
- import { DoubleableNumber } from './DoubleableNumber'
1104
+ With `--env production`, the resolved config keeps `files.fetch`, overrides the `vars`, and overrides `bindings.kv.CACHE`.
678
1105
 
679
- export class Counter {
680
- private count = 0
1106
+ Current limitation: `accountId` and `wsRoutes` are not supported inside `config.env`. Compute those in the outer `defineConfig()` function when you need environment-specific values.
681
1107
 
682
- getValue(): DoubleableNumber {
683
- return new DoubleableNumber(this.count)
684
- }
1108
+ ---
1109
+
1110
+ ## Bindings and multi-worker composition
1111
+
1112
+ ### Binding groups
1113
+
1114
+ | Group | Members | Coverage wording | Safe promise |
1115
+ |---|---|---|---|
1116
+ | 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 |
1117
+ | 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 |
1118
+ | Remote-oriented bindings | `ai`, `vectorize` | remote-dependent | supported and typed, but not full local equivalents of KV or D1 |
1119
+ | Narrower bindings | `hyperdrive`, `analyticsEngine`, `browser` | supported, narrower-scope | real public surfaces, but less central than the core storage/runtime path |
1120
+ | Outbound email binding | `sendEmail` | supported public surface | real outbound email binding; keep it separate from inbound email handling |
1121
+
1122
+ ### Core bindings
1123
+
1124
+ KV, D1, R2, Durable Objects, and queues have the clearest end-to-end story today across config, generated types, and local dev/test flows.
1125
+
1126
+ ### R2 browser access and public URLs
1127
+
1128
+ `bindings.r2` gives you real R2 binding access in local dev, tests, generated types, and compiled config.
685
1129
 
686
- increment(n: number = 1): DoubleableNumber {
687
- this.count += n
688
- return new DoubleableNumber(this.count)
1130
+ What Devflare does **not** currently expose as a public contract is a stable browser-facing local bucket URL.
1131
+
1132
+ If you need browser-visible local asset flows:
1133
+
1134
+ - serve them through your Worker routes or app endpoints
1135
+ - test real public/custom-domain URL behavior against an intentional staging bucket
1136
+ - do not hard-code frontend assumptions about a magic local bucket origin
1137
+
1138
+ For delivery patterns and production recommendations, see [`R2.md`](./R2.md).
1139
+
1140
+ Practical example:
1141
+
1142
+ ```ts
1143
+ import { defineConfig } from 'devflare'
1144
+
1145
+ export default defineConfig({
1146
+ name: 'orders-worker',
1147
+ bindings: {
1148
+ kv: {
1149
+ CACHE: 'orders-cache'
1150
+ },
1151
+ d1: {
1152
+ DB: 'orders-db'
1153
+ },
1154
+ durableObjects: {
1155
+ COUNTER: 'Counter'
1156
+ },
1157
+ queues: {
1158
+ producers: {
1159
+ TASK_QUEUE: 'orders-tasks'
1160
+ }
1161
+ }
689
1162
  }
690
- }
1163
+ })
691
1164
  ```
692
1165
 
693
1166
  ```ts
694
- // tests/counter.test.ts
695
- import { beforeAll, afterAll, describe, expect, test } from 'bun:test'
696
- import { createTestContext } from 'devflare/test'
697
- import { env } from 'devflare'
698
- import { DoubleableNumber } from '../src/DoubleableNumber'
1167
+ import type { FetchEvent } from 'devflare/runtime'
699
1168
 
700
- beforeAll(() => createTestContext())
701
- afterAll(() => env.dispose())
1169
+ export async function POST({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
1170
+ const status = await env.DB.prepare('select 1 as ok').first()
1171
+ await env.CACHE.put('health', JSON.stringify(status))
1172
+ await env.TASK_QUEUE.send({ type: 'reindex-orders' })
1173
+ return Response.json({ ok: true })
1174
+ }
1175
+ ```
702
1176
 
703
- describe('counter transport', () => {
704
- test('result is decoded back into a class instance', async () => {
705
- const id = env.COUNTER.idFromName('main')
706
- const counter = env.COUNTER.get(id)
707
- const result = await counter.increment(2)
1177
+ ### Services and named entrypoints
708
1178
 
709
- expect(result).toBeInstanceOf(DoubleableNumber)
710
- expect(result.double).toBe(4)
711
- })
712
- })
713
- ```
1179
+ Service bindings can be written directly or via `ref()`.
714
1180
 
715
- What Devflare is doing for the developer:
1181
+ ```ts
1182
+ import { defineConfig, ref } from 'devflare'
716
1183
 
717
- 1. your DO returns `new DoubleableNumber(...)`
718
- 2. Devflare finds a matching encoder in `src/transport.ts`
719
- 3. the value is serialized while crossing the boundary
720
- 4. Devflare applies the decoder on the receiving side
721
- 5. your code gets a real `DoubleableNumber` instance back
1184
+ const auth = ref(() => import('../auth/devflare.config'))
722
1185
 
723
- This means the developer writes the codec once and then works with real domain objects rather than manual serialization code.
1186
+ export default defineConfig({
1187
+ name: 'gateway',
1188
+ bindings: {
1189
+ services: {
1190
+ AUTH: auth.worker,
1191
+ ADMIN: auth.worker('AdminEntrypoint')
1192
+ }
1193
+ }
1194
+ })
1195
+ ```
724
1196
 
725
- ### Cross-worker references
1197
+ 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.
726
1198
 
727
- Use `ref()` when one worker depends on another worker or another worker’s DO bindings.
1199
+ Practical example:
728
1200
 
729
1201
  ```ts
1202
+ // gateway/devflare.config.ts
730
1203
  import { defineConfig, ref } from 'devflare'
731
1204
 
732
- const authWorker = ref(() => import('../auth/devflare.config'))
1205
+ const auth = ref(() => import('../auth/devflare.config'))
733
1206
 
734
1207
  export default defineConfig({
735
1208
  name: 'gateway',
736
1209
  bindings: {
737
1210
  services: {
738
- AUTH: authWorker.worker
1211
+ ADMIN: auth.worker('AdminEntrypoint')
739
1212
  }
740
1213
  }
741
1214
  })
742
1215
  ```
743
1216
 
744
- This is cleaner than scattering worker names and entrypoint names by hand throughout the codebase.
745
-
746
- ---
1217
+ ```ts
1218
+ // gateway/src/fetch.ts
1219
+ import type { FetchEvent } from 'devflare/runtime'
747
1220
 
748
- ## Runtime-safe helpers
1221
+ export async function GET({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
1222
+ const stats = await env.ADMIN.getStats()
1223
+ return Response.json(stats)
1224
+ }
1225
+ ```
749
1226
 
750
- Import runtime-only utilities from `devflare/runtime` when the code runs inside Workers.
1227
+ ### Remote-oriented and narrower bindings
751
1228
 
752
- Important helpers include:
1229
+ `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.
753
1230
 
754
- - `sequence()`
755
- - `resolve()`
756
- - `pipe()`
757
- - context-related utilities
1231
+ `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.
758
1232
 
759
- Use this path when you want runtime-safe middleware composition without pulling in Node-oriented orchestration code.
1233
+ ### `sendEmail` and inbound email
760
1234
 
761
- ---
1235
+ `sendEmail` is the outbound email binding surface. It is supported across config, generated types, and local development flows.
762
1236
 
763
- ## Testing model
1237
+ Incoming email is a separate worker surface:
764
1238
 
765
- Devflare testing is one of the library’s biggest strengths.
1239
+ - `files.email`
1240
+ - `src/email.ts`
1241
+ - `EmailEvent`
766
1242
 
767
- ### Integration-style local tests
1243
+ Keep outbound `sendEmail` and inbound email handling separate in docs and examples.
768
1244
 
769
- Use `createTestContext()` when you want a real local Devflare environment backed by Miniflare orchestration.
1245
+ Practical example:
770
1246
 
771
1247
  ```ts
772
- import { beforeAll, afterAll, describe, expect, test } from 'bun:test'
773
- import { createTestContext, cf } from 'devflare/test'
774
- import { env } from 'devflare'
1248
+ // devflare.config.ts
1249
+ import { defineConfig } from 'devflare'
775
1250
 
776
- beforeAll(() => createTestContext())
777
- afterAll(() => env.dispose())
1251
+ export default defineConfig({
1252
+ name: 'mail-worker',
1253
+ files: {
1254
+ fetch: 'src/fetch.ts',
1255
+ email: 'src/email.ts'
1256
+ },
1257
+ bindings: {
1258
+ sendEmail: {
1259
+ EMAIL: {
1260
+ destinationAddress: 'team@example.com'
1261
+ }
1262
+ }
1263
+ }
1264
+ })
1265
+ ```
778
1266
 
779
- describe('worker', () => {
780
- test('GET /health', async () => {
781
- const response = await cf.worker.get('/health')
782
- expect(response.status).toBe(200)
1267
+ ```ts
1268
+ // src/fetch.ts
1269
+ import type { FetchEvent } from 'devflare/runtime'
1270
+
1271
+ export async function POST({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
1272
+ await env.EMAIL.send({
1273
+ from: 'noreply@example.com',
1274
+ to: 'team@example.com',
1275
+ subject: 'Welcome',
1276
+ text: 'Hello from Devflare'
783
1277
  })
784
- })
1278
+
1279
+ return new Response('sent')
1280
+ }
785
1281
  ```
786
1282
 
787
- ### Mock/unit-style tests
1283
+ ```ts
1284
+ // src/email.ts
1285
+ import type { EmailEvent } from 'devflare/runtime'
788
1286
 
789
- Use `createMockTestContext()`, `withTestContext()`, and the mock binding helpers when you want pure logic tests.
1287
+ export async function email({ message }: EmailEvent<DevflareEnv>): Promise<void> {
1288
+ await message.forward('ops@example.com')
1289
+ }
1290
+ ```
790
1291
 
791
- ### Why this is better than ad hoc Miniflare setup
1292
+ ### `ref()` and multi-worker composition
792
1293
 
793
- Devflare gives you a single mental model for testing each handler surface:
1294
+ Use `ref()` when one worker depends on another worker config.
794
1295
 
795
- - HTTP via `cf.worker`
796
- - queues via `cf.queue`
797
- - cron via `cf.scheduled`
798
- - email via `cf.email`
799
- - tail events via `cf.tail`
1296
+ Main public story:
800
1297
 
801
- That consistency is part of the value. Instead of every project inventing its own test harness, Devflare gives you one coherent interface.
1298
+ - `ref(...).worker` for a default service binding backed by `src/worker.ts` or `src/worker.js`
1299
+ - `ref(...).worker('EntrypointName')` for a named service entrypoint
1300
+ - typed cross-worker Durable Object handles
1301
+
1302
+ ```ts
1303
+ const auth = ref(() => import('../auth/devflare.config'))
1304
+ ```
1305
+
1306
+ ```ts
1307
+ const auth = ref('custom-auth-name', () => import('../auth/devflare.config'))
1308
+ ```
1309
+
1310
+ Why `ref()` matters:
1311
+
1312
+ - worker names stay centralized
1313
+ - cross-worker Durable Object bindings stay typed
1314
+ - entrypoint names can be typed from generated `Entrypoints`
1315
+ - multi-worker systems avoid scattered magic strings
1316
+
1317
+ Advanced members such as `.name`, `.config`, `.configPath`, and `.resolve()` are real, but secondary to the main composition story.
802
1318
 
803
1319
  ---
804
1320
 
805
- ## Vite and SvelteKit integration
1321
+ ## Development workflows
806
1322
 
807
- Devflare includes dedicated framework-aware entry points.
1323
+ ### Vite vs Rolldown: the truthful mental model
808
1324
 
809
- ### `devflare/vite`
1325
+ They are both important, but they are not two names for the same job.
810
1326
 
811
- Use this when you want:
1327
+ | Tool | Role inside Devflare | When it matters most | What it is not |
1328
+ |---|---|---|---|
1329
+ | `Vite` | the optional outer dev/build host for packages that are already Vite apps or frameworks; Devflare plugs generated Worker config, config watching, auxiliary DO workers, and bridge behavior into that pipeline | packages with a local `vite.config.*`, SvelteKit, frontend HMR | not Devflare's own Worker bundler |
1330
+ | `Rolldown` | the inner code-transforming builder Devflare uses when Devflare itself bundles Worker code | Durable Object bundles, watch/rebuild, worker-side plugin transforms such as `.svelte` imported by a DO module | not the main app's Vite build |
812
1331
 
813
- - config compilation during dev and build
814
- - worker-aware transforms
815
- - auxiliary Durable Object worker generation
816
- - websocket-aware proxying for local development
1332
+ Three practical consequences fall straight out of the implementation:
817
1333
 
818
- ### `devflare/sveltekit`
1334
+ - remove `vite.config.*`, and Devflare drops back to worker-only mode instead of starting Vite
1335
+ - import `.svelte` from a Durable Object, and the compilation belongs to `rolldown.options.plugins`, not to the main Vite plugin chain
1336
+ - generated `.devflare/worker-entrypoints/main.ts` is separate glue code produced by Devflare when it needs to compose fetch, queue, scheduled, email, or route-tree surfaces into one Worker entry
819
1337
 
820
- Use this when your app needs:
1338
+ ### Operational decision rules
821
1339
 
822
- - a Devflare-aware platform object
823
- - integration between SvelteKit and Worker bindings
824
- - local dev behavior that still fits the Devflare model
1340
+ Use these rules in order:
825
1341
 
826
- This is another example of Devflare going beyond raw local runtime emulation. It helps compose framework tooling, worker bindings, auxiliary workers, and local dev behavior into one predictable setup.
1342
+ 1. a local `vite.config.*` in the current package decides whether `dev`, `build`, and `deploy` run in Vite-backed mode or worker-only mode
1343
+ 2. `devflare/vite` is an explicit helper layer, not a separate CLI mode
1344
+ 3. worker-only mode is a supported first-class path; no local `vite.config.*` means Devflare does not start Vite
1345
+ 4. raw Vite config belongs in `vite.config.*`; `config.vite` is Devflare metadata, not full Vite config
1346
+ 5. treat `.devflare/*`, `env.d.ts`, and generated Wrangler config as outputs, not authoring inputs
1347
+ 6. remote mode is mainly for remote-oriented services such as AI and Vectorize, not a blanket “make everything remote” switch
827
1348
 
828
- ---
1349
+ ### Vite-backed workflows
829
1350
 
830
- ## Remote-only services
1351
+ A package enters Vite-backed mode when it has a local `vite.config.*`. In that mode, Vite is the outer application pipeline: it owns the package's dev server and app build, while Devflare injects Worker-aware config, generated Wrangler output, auxiliary DO worker config, and bridge behavior into that Vite stack.
831
1352
 
832
- Some Cloudflare services are not meaningfully local.
1353
+ Current Vite-backed flow:
833
1354
 
834
- Most importantly:
1355
+ 1. Devflare loads and validates `devflare.config.*`
1356
+ 2. `devflarePlugin()` compiles that config into a generated `.devflare/wrangler.jsonc`
1357
+ 3. Devflare may generate `.devflare/worker-entrypoints/main.ts` when multiple surfaces must be composed into one Worker entry
1358
+ 4. if Durable Object files are discovered, Devflare builds an auxiliary DO worker config for Vite / Cloudflare interop
1359
+ 5. in serve mode, Devflare watches the resolved Devflare config file and triggers a full reload when it changes
1360
+ 6. if `wsRoutes` are configured, Devflare can proxy matching WebSocket upgrade paths to the Miniflare bridge
1361
+ 7. on build, Devflare runs `bunx vite build` only after it has prepared the Worker config for that package
835
1362
 
836
- - Workers AI
837
- - Vectorize
1363
+ Two ownership rules matter here:
838
1364
 
839
- Devflare treats those as explicit remote-mode concerns rather than pretending they are fully local.
1365
+ - setting `wrangler.passthrough.main` tells Devflare to preserve your explicit Worker `main` instead of generating a composed one
1366
+ - no local `vite.config.*` means none of this Vite-specific behavior runs; the package stays in worker-only mode
840
1367
 
841
- That is a good thing.
1368
+ #### `devflare/vite` helpers
842
1369
 
843
- It keeps the local story honest while still giving you an intentional path to test remote capabilities when needed.
1370
+ | Helper | Use it for | Timing |
1371
+ |---|---|---|
1372
+ | `devflarePlugin(options)` | generated `.devflare/wrangler.jsonc`, config watching, DO discovery, DO transforms, and WebSocket proxy wiring | include it in `vite.config.*` plugins |
1373
+ | `getCloudflareConfig(options)` | compiled programmatic config for `cloudflare({ config })` | call during Vite config creation |
1374
+ | `getDevflareConfigs(options)` | compiled config plus `auxiliaryWorkers` array for DO workers | call during Vite config creation |
1375
+ | `getPluginContext()` | read resolved plugin state such as `wranglerConfig`, `cloudflareConfig`, discovered DOs, and `projectRoot` | advanced use only, after Vite has resolved config |
844
1376
 
845
- Use the `devflare remote` workflow and guard remote-only tests appropriately.
1377
+ `devflarePlugin(options)` currently supports these options:
846
1378
 
847
- ---
1379
+ | Option | Default | What it changes |
1380
+ |---|---|---|
1381
+ | `configPath` | auto-resolve local supported config | point Vite at a specific `devflare.config.*` file |
1382
+ | `environment` | no explicit override | resolve `config.env[name]` before compilation |
1383
+ | `doTransforms` | `true` | enable or disable Devflare's DO code transforms |
1384
+ | `watchConfig` | `true` | watch the resolved config file and full-reload on change |
1385
+ | `bridgePort` | `process.env.DEVFLARE_BRIDGE_PORT`, then `8787` when proxying | choose the Miniflare bridge port for WebSocket proxying |
1386
+ | `wsProxyPatterns` | `[]` | add extra WebSocket proxy patterns beyond configured `wsRoutes` |
848
1387
 
849
- ## Generated artifacts
1388
+ Timing rule of thumb:
850
1389
 
851
- Devflare uses `.devflare/` for generated local artifacts.
1390
+ - if you need config while building the Vite config object, use `getCloudflareConfig()` or `getDevflareConfigs()`
1391
+ - if another Vite plugin needs to inspect the already-resolved Devflare state, `getPluginContext()` is the advanced hook
852
1392
 
853
- Treat that directory as generated output rather than hand-authored source.
1393
+ #### Minimal Vite wiring
854
1394
 
855
- ---
1395
+ ```ts
1396
+ import { defineConfig } from 'vite'
1397
+ import { devflarePlugin } from 'devflare/vite'
856
1398
 
857
- ## Recommended project structure
1399
+ export default defineConfig({
1400
+ plugins: [devflarePlugin()]
1401
+ })
1402
+ ```
858
1403
 
859
- Here is a strong default layout for most projects.
1404
+ #### Explicit `@cloudflare/vite-plugin` wiring
860
1405
 
861
- ```text
862
- .
863
- ├── devflare.config.ts
864
- ├── env.d.ts
865
- ├── src/
866
- │ ├── fetch.ts
867
- │ ├── queue.ts
868
- │ ├── scheduled.ts
869
- │ ├── email.ts
870
- │ ├── transport.ts
871
- │ ├── routes/
872
- │ ├── do.counter.ts
873
- │ ├── ep.admin.ts
874
- │ └── wf.sync.ts
875
- └── tests/
876
- └── worker.test.ts
877
- ```
878
-
879
- You will not always need every file.
880
-
881
- But this shape captures the Devflare philosophy well:
882
-
883
- - each capability gets an isolated home
884
- - config stays declarative
885
- - tests target named surfaces
886
- - local orchestration is handled by Devflare instead of bespoke glue
1406
+ ```ts
1407
+ import { defineConfig } from 'vite'
1408
+ import { cloudflare } from '@cloudflare/vite-plugin'
1409
+ import { devflarePlugin, getDevflareConfigs } from 'devflare/vite'
1410
+
1411
+ export default defineConfig(async () => {
1412
+ const { cloudflareConfig, auxiliaryWorkers } = await getDevflareConfigs()
1413
+
1414
+ return {
1415
+ plugins: [
1416
+ devflarePlugin(),
1417
+ cloudflare({
1418
+ config: cloudflareConfig,
1419
+ auxiliaryWorkers: auxiliaryWorkers.length > 0 ? auxiliaryWorkers : undefined
1420
+ })
1421
+ ]
1422
+ }
1423
+ })
1424
+ ```
887
1425
 
888
- ---
1426
+ That is the current high-signal pattern when you want Vite to stay the package's app/build host while Devflare owns Worker config compilation and Durable Object discovery.
889
1427
 
890
- ## Guidance for LLMs generating Devflare code
1428
+ #### SvelteKit-backed Worker example
891
1429
 
892
- When generating Devflare projects or edits, follow these rules.
1430
+ ```ts
1431
+ // devflare.config.ts
1432
+ import { defineConfig } from 'devflare'
893
1433
 
894
- ### Prefer conventions first
1434
+ export default defineConfig({
1435
+ name: 'notes-app',
1436
+ files: {
1437
+ fetch: '.svelte-kit/cloudflare/_worker.js',
1438
+ durableObjects: 'src/do/**/*.ts',
1439
+ transport: 'src/transport.ts'
1440
+ },
1441
+ bindings: {
1442
+ durableObjects: {
1443
+ CHAT_ROOM: 'ChatRoom'
1444
+ }
1445
+ }
1446
+ })
1447
+ ```
895
1448
 
896
- Only override paths when there is a real reason.
1449
+ ```ts
1450
+ // vite.config.ts
1451
+ import { defineConfig } from 'vite'
1452
+ import { sveltekit } from '@sveltejs/kit/vite'
1453
+ import { devflarePlugin } from 'devflare/vite'
897
1454
 
898
- ### Prefer separation of responsibilities
1455
+ export default defineConfig({
1456
+ plugins: [
1457
+ devflarePlugin(),
1458
+ sveltekit()
1459
+ ]
1460
+ })
1461
+ ```
899
1462
 
900
- Do **not** collapse fetch, queue, scheduled, email, DOs, and entrypoints into a single file unless the task explicitly demands that shape.
1463
+ ```ts
1464
+ // src/hooks.server.ts
1465
+ export { handle } from 'devflare/sveltekit'
1466
+ ```
901
1467
 
902
- ### Prefer `defineConfig()`
1468
+ Use `createHandle({...})` from `devflare/sveltekit` when you need custom binding hints or want to compose Devflare with other SvelteKit handles via `sequence(...)`.
903
1469
 
904
- Never emit untyped ad hoc config objects when a normal Devflare config is appropriate.
1470
+ ### Rolldown bundling and plugin workflows
905
1471
 
906
- ### Prefer `ref()` for cross-worker relationships
1472
+ `rolldown` is not just a namespace of knobs. Rolldown is the builder Devflare uses for the code Devflare actively bundles itself. Today that means the Durable Object path: Devflare discovers DO source files, applies its own transforms, lets user plugins transform imports, and emits runnable single-file ESM Worker modules that Miniflare can execute.
907
1473
 
908
- Avoid manually duplicating worker names and binding metadata.
1474
+ That is why `rolldown` is important but different from Vite:
909
1475
 
910
- ### Prefer `devflare/test` for tests
1476
+ - Vite may host the outer app or framework pipeline
1477
+ - Rolldown is the inner code-transform step that turns DO source into actual runnable worker code
1478
+ - if a Durable Object imports `.svelte`, that compilation belongs to the Rolldown plugin pipeline, not to the main Vite app plugin chain
911
1479
 
912
- Use `createTestContext()` for integration-style tests and mock helpers for unit tests.
1480
+ It is still not Vite config, not a replacement for your app's `vite.config.*`, and not the place to configure the main fetch build.
913
1481
 
914
- ### Prefer `env` for ergonomic binding access
1482
+ Current DO bundler behavior:
915
1483
 
916
- Use `import { env } from 'devflare'` when it improves clarity.
1484
+ - Devflare discovers DO files from `files.durableObjects`
1485
+ - discovered DO entries are bundled to worker-compatible ESM
1486
+ - code splitting is disabled so Devflare can emit a worker-friendly single-file bundle
1487
+ - user `rolldown.options.plugins` are merged into the bundle pipeline
1488
+ - internal externals cover `cloudflare:*`, `node:*`, and other worker/runtime modules that should stay external
1489
+ - Devflare also injects a `debug` alias shim so worker bundles do not accidentally drag in a Node-only debug dependency
1490
+ - this same DO bundling path still matters in unified Vite dev; Vite can host the app while Rolldown rebuilds DO worker code underneath it
917
1491
 
918
- ### Respect remote-only boundaries
1492
+ Rolldown's plugin API is almost fully compatible with Rollup's, which is why Rollup-style plugins can often be passed through in `rolldown.options.plugins`. That said, compatibility is high, not magical: keep integration tests around nontrivial plugin stacks.
919
1493
 
920
- Do not fake fully local AI or Vectorize support.
1494
+ #### Minimal custom transform example
921
1495
 
922
- ### Use routing when HTTP complexity grows
1496
+ ```ts
1497
+ import { defineConfig } from 'devflare'
1498
+ import type { Plugin as RolldownPlugin } from 'rolldown'
923
1499
 
924
- If the app has many endpoints, prefer `files.routes` and route files over a giant `fetch.ts` dispatcher.
1500
+ const inlineSvelteFixturePlugin: RolldownPlugin = {
1501
+ name: 'inline-svelte-fixture',
1502
+ transform(_code, id) {
1503
+ if (!id.endsWith('.svelte')) {
1504
+ return null
1505
+ }
925
1506
 
926
- ---
1507
+ return {
1508
+ code: 'export default { render() { return { html: "<h1>Hello from Svelte</h1>" } } }',
1509
+ map: null
1510
+ }
1511
+ }
1512
+ }
1513
+
1514
+ export default defineConfig({
1515
+ name: 'do-worker',
1516
+ files: {
1517
+ durableObjects: 'src/do/**/*.ts'
1518
+ },
1519
+ bindings: {
1520
+ durableObjects: {
1521
+ GREETER: 'Greeter'
1522
+ }
1523
+ },
1524
+ rolldown: {
1525
+ options: {
1526
+ plugins: [inlineSvelteFixturePlugin]
1527
+ }
1528
+ }
1529
+ })
1530
+ ```
1531
+
1532
+ That mirrors the kind of `.svelte` transform path the repo's own DO bundler tests exercise.
1533
+
1534
+ #### Svelte plugin example for Rolldown
927
1535
 
928
- ## Minimal example
1536
+ This example is intentionally about a `.svelte` import inside a Durable Object module. In that situation, Rolldown — not the main Vite app build — is the plugin pipeline doing the compilation.
1537
+
1538
+ For this pattern you typically install `svelte`, `rollup-plugin-svelte`, and `@rollup/plugin-node-resolve` in the package that owns the Durable Object code.
929
1539
 
930
1540
  ```ts
931
- // devflare.config.ts
932
1541
  import { defineConfig } from 'devflare'
1542
+ import resolve from '@rollup/plugin-node-resolve'
1543
+ import type { Plugin as RolldownPlugin } from 'rolldown'
1544
+ import svelte from 'rollup-plugin-svelte'
933
1545
 
934
1546
  export default defineConfig({
935
- name: 'hello-worker'
1547
+ name: 'chat-worker',
1548
+ files: {
1549
+ durableObjects: 'src/do/**/*.ts'
1550
+ },
1551
+ bindings: {
1552
+ durableObjects: {
1553
+ CHAT_ROOM: 'ChatRoom'
1554
+ }
1555
+ },
1556
+ rolldown: {
1557
+ target: 'es2022',
1558
+ sourcemap: true,
1559
+ options: {
1560
+ plugins: [
1561
+ svelte({
1562
+ emitCss: false,
1563
+ compilerOptions: {
1564
+ generate: 'ssr'
1565
+ }
1566
+ }) as unknown as RolldownPlugin,
1567
+ resolve({
1568
+ browser: true,
1569
+ exportConditions: ['svelte'],
1570
+ extensions: ['.svelte']
1571
+ }) as unknown as RolldownPlugin
1572
+ ]
1573
+ }
1574
+ }
936
1575
  })
937
1576
  ```
938
1577
 
1578
+ ```svelte
1579
+ <!-- src/do/Greeting.svelte -->
1580
+ <script lang='ts'>
1581
+ export let name: string
1582
+ </script>
1583
+
1584
+ <h1>Hello {name} from Svelte</h1>
1585
+ ```
1586
+
939
1587
  ```ts
940
- // src/fetch.ts
941
- export default async function fetch(): Promise<Response> {
942
- return new Response('Hello from Devflare')
1588
+ // src/do/chat-room.ts
1589
+ import { DurableObject } from 'cloudflare:workers'
1590
+ import Greeting from './Greeting.svelte'
1591
+
1592
+ export class ChatRoom extends DurableObject {
1593
+ async fetch(): Promise<Response> {
1594
+ return new Response(Greeting.render({ name: 'Devflare' }).html, {
1595
+ headers: {
1596
+ 'content-type': 'text/html; charset=utf-8'
1597
+ }
1598
+ })
1599
+ }
943
1600
  }
944
1601
  ```
945
1602
 
1603
+ What happens in this flow:
1604
+
1605
+ 1. Devflare discovers `src/do/**/*.ts` from `files.durableObjects`
1606
+ 2. the DO module imports `Greeting.svelte`
1607
+ 3. Rolldown runs the configured plugin pipeline, including `rollup-plugin-svelte`
1608
+ 4. the Svelte component becomes JavaScript before the DO bundle is written
1609
+ 5. Devflare writes a runnable single-file Worker bundle for that DO
1610
+
1611
+ Why this example is shaped that way:
1612
+
1613
+ - `emitCss: false` keeps the DO bundle single-file instead of emitting a separate CSS asset pipeline
1614
+ - `generate: 'ssr'` fits the Worker-side rendering story better than a browser DOM target
1615
+ - `@rollup/plugin-node-resolve` helps `.svelte` files and `exports.svelte` packages resolve cleanly
1616
+ - some Rollup plugins need a type cast to satisfy Rolldown's TypeScript types even when the runtime hooks work fine
1617
+ - this example is specifically about worker-side component compilation inside a DO; if your Svelte code lives in the main app or SvelteKit shell, that outer build is still Vite's job
1618
+
1619
+ If a plugin relies on Rollup-only hooks that Rolldown does not support yet, keep that plugin in your main Vite build instead of the DO bundler.
1620
+
1621
+ ### Daily development loop
1622
+
1623
+ Use the same CLI loop for both worker-only and Vite-backed packages. The presence of a local `vite.config.*` changes the mode automatically.
1624
+
1625
+ ```bash
1626
+ bunx --bun devflare dev
1627
+ bunx --bun devflare types
1628
+ bunx --bun devflare doctor
1629
+ bunx --bun devflare build --env staging
1630
+ bunx --bun devflare deploy --dry-run --env staging
1631
+ ```
1632
+
1633
+ Use `--env <name>` on build and deploy when you want Devflare to resolve a named config environment before compilation.
1634
+
1635
+ ### CLI reference
1636
+
1637
+ | Command | What it does | Important note |
1638
+ |---|---|---|
1639
+ | `devflare init` | scaffold a project | current templates generate `src/fetch.ts` and explicit `files.fetch` |
1640
+ | `devflare dev` | start local development | worker-only by default, Vite-backed only when the current package has a local `vite.config.*` |
1641
+ | `devflare build` | build from resolved config | skips Vite for worker-only packages |
1642
+ | `devflare deploy` | build and deploy | supports `--env` and `--dry-run` |
1643
+ | `devflare types` | generate `env.d.ts` | supports `--output` |
1644
+ | `devflare doctor` | check project configuration | supports `--config` |
1645
+ | `devflare account` | inspect accounts, resources, usage, and limits | `global` and `workspace` are account-selection flows, not resource-reporting subcommands |
1646
+ | `devflare ai` | show Workers AI model pricing | informational command |
1647
+ | `devflare remote` | manage remote test mode | helper flow for remote-oriented services |
1648
+ | `devflare help` | show help | direct command and flag form both work |
1649
+ | `devflare version` | show package version | sourced from installed package metadata |
1650
+
1651
+ ### `doctor`
1652
+
1653
+ Current `doctor` behavior:
1654
+
1655
+ - it checks for supported Devflare config filenames
1656
+ - it can inspect an explicit config path via `--config`
1657
+ - it accepts project-root `wrangler.jsonc`
1658
+ - Vite-backed flows may instead generate `.devflare/wrangler.jsonc`
1659
+
1660
+ ---
1661
+
1662
+ ## Testing model
1663
+
1664
+ Use `devflare/test`.
1665
+
1666
+ The default pairing is:
1667
+
946
1668
  ```ts
947
- // tests/worker.test.ts
948
- import { beforeAll, afterAll, describe, expect, test } from 'bun:test'
949
- import { createTestContext, cf } from 'devflare/test'
1669
+ import { beforeAll, afterAll } from 'bun:test'
1670
+ import { createTestContext } from 'devflare/test'
950
1671
  import { env } from 'devflare'
951
1672
 
952
1673
  beforeAll(() => createTestContext())
953
1674
  afterAll(() => env.dispose())
1675
+ ```
1676
+
1677
+ `createTestContext()` is the recommended default for most integration-style tests.
1678
+
1679
+ Current behavior:
1680
+
1681
+ - it can auto-discover the nearest supported config file by walking upward from the calling test file
1682
+ - it sets up the main-entry test env used by `import { env } from 'devflare'`
1683
+ - it resolves service bindings and cross-worker Durable Object references
1684
+ - it can infer conventional fetch, queue, scheduled, and email handler files when present
1685
+ - it also auto-detects `src/tail.ts` when present, even though there is no public `files.tail` config key
1686
+ - it auto-detects `src/transport.{ts,js,mts,mjs}` when present unless `files.transport` is `null`
1687
+ - it does not have a first-class `.dev.vars*` loader for populating declared secret values
954
1688
 
955
- describe('hello-worker', () => {
956
- test('GET / returns text', async () => {
957
- const response = await cf.worker.get('/')
1689
+ Practical example:
1690
+
1691
+ ```ts
1692
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
1693
+ import { cf, createTestContext } from 'devflare/test'
1694
+ import { env } from 'devflare'
1695
+
1696
+ beforeAll(() => createTestContext())
1697
+ afterAll(() => env.dispose())
1698
+
1699
+ describe('users routes', () => {
1700
+ test('GET /users/123 uses the built-in file router', async () => {
1701
+ const response = await cf.worker.get('/users/123')
958
1702
  expect(response.status).toBe(200)
959
- expect(await response.text()).toBe('Hello from Devflare')
1703
+ expect(await response.json()).toEqual({ id: '123' })
1704
+ })
1705
+
1706
+ test('queue side effects can be asserted with real bindings', async () => {
1707
+ await cf.queue.trigger([
1708
+ { type: 'reindex', id: 'job-1' }
1709
+ ])
1710
+
1711
+ expect(await env.RESULTS.get('job-1')).toBe('done')
960
1712
  })
961
1713
  })
962
1714
  ```
963
1715
 
1716
+ ### Helper behavior
1717
+
1718
+ Do not assume every `cf.*` helper behaves the same way.
1719
+
1720
+ | Helper | What it does | Waits for `waitUntil()`? | Important nuance |
1721
+ |---|---|---|---|
1722
+ | `cf.worker.fetch()` | invokes the configured HTTP surface, including built-in file routes | No | returns when the handler resolves |
1723
+ | `cf.queue.trigger()` | invokes the configured queue consumer | Yes | drains queued background work before returning |
1724
+ | `cf.scheduled.trigger()` | invokes the configured scheduled handler | Yes | drains queued background work before returning |
1725
+ | `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 |
1726
+ | `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 |
1727
+
1728
+ Two important consequences:
1729
+
1730
+ - `cf.worker.fetch()` is not a “wait for all background work” helper
1731
+ - helper availability does not mean every helper replays the full Cloudflare dispatch path in the same way
1732
+
1733
+ ### Email testing
1734
+
1735
+ Email testing has two directions:
1736
+
1737
+ - incoming email helper dispatch via `cf.email.send()` / `email.send()`
1738
+ - outgoing email observation via helper state
1739
+
1740
+ When `createTestContext()` has wired an email handler, the helper imports that handler directly, creates an `EmailEvent`, and waits for queued `waitUntil()` work.
1741
+
1742
+ If no email handler has been wired, the helper falls back to posting the raw email to the local email endpoint.
1743
+
1744
+ That makes the helper useful for handler-level tests, but ingress-fidelity-sensitive flows should still use higher-level integration testing.
1745
+
1746
+ ### Tail testing
1747
+
1748
+ Tail helpers are exported publicly, and `createTestContext()` auto-detects `src/tail.ts` when present.
1749
+
1750
+ So the truthful public statement is:
1751
+
1752
+ - `cf.tail.trigger()` is exported
1753
+ - `createTestContext()` wires it automatically when `src/tail.ts` exists
1754
+ - `cf.tail.trigger()` directly imports and invokes that handler and waits for `waitUntil()`
1755
+ - there is no public `files.tail` config key to advertise today
1756
+
1757
+ ### Remote mode in tests
1758
+
1759
+ 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.
1760
+
964
1761
  ---
965
1762
 
966
- ## What to remember
1763
+ ## Sharp edges
1764
+
1765
+ Keep these caveats explicit:
1766
+
1767
+ - `files.routes` configures the built-in file router; it is not the same thing as Cloudflare deployment `routes`
1768
+ - route trees are real now, but `src/fetch.ts` same-module method handlers still take precedence over file routes for matching methods
1769
+ - route files that normalize to the same pattern are rejected as conflicts
1770
+ - `_`-prefixed files and directories inside the route tree are ignored
1771
+ - `vars` values are strings in the current native schema
1772
+ - `secrets` declares runtime secret bindings; it does not store secret values
1773
+ - `.env*` and `.dev.vars*` are not a unified Devflare-native loading/validation system; any effect they have comes from Bun or upstream Cloudflare tooling, depending on the mode
1774
+ - `wrangler.passthrough` is the escape hatch for unsupported Wrangler keys and is merged after native compilation
1775
+ - named service entrypoints need deployment-time validation if they are critical to your app
1776
+ - local R2 binding support is real, but there is still no stable public/browser local bucket URL contract to document as public API
1777
+ - `sendEmail` is a supported outbound binding, while inbound email is a separate worker surface
1778
+ - email and tail helpers have real, useful test paths, but they should not be described as identical to full Cloudflare ingress or tail replay
1779
+ - Vite and Rolldown are different systems and should not be blurred together
1780
+ - `rolldown` config affects Durable Object bundling, not the main Vite app build
1781
+ - `wrangler.passthrough.main` suppresses Devflare's composed main-entry generation in higher-level build and Vite-backed flows
1782
+ - `getCloudflareConfig()` and `getDevflareConfigs()` are the safe config-time Vite helpers; `getPluginContext()` is advanced post-resolution state
1783
+ - Rollup-compatible plugins often work in `rolldown.options.plugins`, but compatibility is high-not-total and plugin-specific validation still matters
1784
+ - higher-level flows may generate composed main worker entries more aggressively than older docs implied
1785
+
1786
+ ---
967
1787
 
968
- If you remember only seven things, remember these:
1788
+ ## Summary
969
1789
 
970
- 1. `defineConfig()` is the source of truth
971
- 2. Devflare extends Miniflare into a full developer workflow
972
- 3. file structure is a feature, not a side detail
973
- 4. responsibilities should live in separate surfaces and files
974
- 5. browser rendering is a concrete example of Devflare adding capabilities above raw Miniflare
975
- 6. `devflare/test` gives you a coherent test API across handler types
976
- 7. use routing, refs, and framework integrations when the app grows beyond a tiny single-worker shape
1790
+ 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.