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.
- package/LLM.md +1424 -610
- package/R2.md +200 -0
- package/README.md +302 -505
- package/bin/devflare.js +8 -8
- package/dist/{account-rvrj687w.js → account-8psavtg6.js} +27 -4
- package/dist/bridge/miniflare.d.ts +6 -0
- package/dist/bridge/miniflare.d.ts.map +1 -1
- package/dist/bridge/proxy.d.ts +5 -6
- package/dist/bridge/proxy.d.ts.map +1 -1
- package/dist/bridge/server.d.ts.map +1 -1
- package/dist/browser.d.ts +50 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/{build-mnf6v8gd.js → build-ezksv2dd.js} +26 -7
- package/dist/bundler/do-bundler.d.ts +7 -0
- package/dist/bundler/do-bundler.d.ts.map +1 -1
- package/dist/cli/commands/account.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/deploy.d.ts.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/types.d.ts.map +1 -1
- package/dist/cli/config-path.d.ts +5 -0
- package/dist/cli/config-path.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/package-metadata.d.ts +16 -0
- package/dist/cli/package-metadata.d.ts.map +1 -0
- package/dist/config/compiler.d.ts +7 -0
- package/dist/config/compiler.d.ts.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/schema.d.ts +2594 -1234
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/{deploy-nhceck39.js → deploy-jdpy21t6.js} +33 -15
- package/dist/{dev-qnxet3j9.js → dev-9mq7zhww.js} +900 -234
- package/dist/dev-server/miniflare-log.d.ts +12 -0
- package/dist/dev-server/miniflare-log.d.ts.map +1 -0
- package/dist/dev-server/runtime-stdio.d.ts +8 -0
- package/dist/dev-server/runtime-stdio.d.ts.map +1 -0
- package/dist/dev-server/server.d.ts +2 -0
- package/dist/dev-server/server.d.ts.map +1 -1
- package/dist/dev-server/vite-utils.d.ts +37 -0
- package/dist/dev-server/vite-utils.d.ts.map +1 -0
- package/dist/{doctor-e8fy6fj5.js → doctor-z4ffybce.js} +73 -50
- package/dist/{durable-object-t4kbb0yt.js → durable-object-yt8v1dyn.js} +1 -1
- package/dist/index-1p814k7s.js +227 -0
- package/dist/{index-tk6ej9dj.js → index-2q3pmzrx.js} +12 -16
- package/dist/{index-67qcae0f.js → index-51s1hkw4.js} +16 -1
- package/dist/{index-ep3445yc.js → index-53xcakh8.js} +414 -171
- package/dist/{index-pf5s73n9.js → index-59df49vn.js} +11 -281
- package/dist/index-5yxg30va.js +304 -0
- package/dist/index-62b3gt2g.js +12 -0
- package/dist/index-6h8xbs75.js +44 -0
- package/dist/index-8gtqgb3q.js +529 -0
- package/dist/{index-gz1gndna.js → index-9wt9x09k.js} +42 -62
- package/dist/index-dr6sbp8d.js +39 -0
- package/dist/index-fef08w43.js +231 -0
- package/dist/index-k7r18na8.js +0 -0
- package/dist/{index-m2q41jwa.js → index-n932ytmq.js} +9 -1
- package/dist/{index-07q6yxyc.js → index-v8vvsn9x.js} +1 -0
- package/dist/index-vky23txa.js +70 -0
- package/dist/{index-z14anrqp.js → index-wfbfz02q.js} +14 -15
- package/dist/index-ws68xvq2.js +311 -0
- package/dist/{index-hcex3rgh.js → index-wyf3s77s.js} +85 -8
- package/dist/index-xqfbd9fx.js +195 -0
- package/dist/index-xxwbb2nt.js +322 -0
- package/dist/index-y1d8za14.js +196 -0
- package/dist/{init-f9mgmew3.js → init-na2atvz2.js} +42 -55
- package/dist/router/types.d.ts +24 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/runtime/context.d.ts +249 -8
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/exports.d.ts +50 -55
- package/dist/runtime/exports.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +8 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/middleware.d.ts +77 -60
- package/dist/runtime/middleware.d.ts.map +1 -1
- package/dist/runtime/router.d.ts +7 -0
- package/dist/runtime/router.d.ts.map +1 -0
- package/dist/runtime/validation.d.ts +1 -1
- package/dist/runtime/validation.d.ts.map +1 -1
- package/dist/src/browser.js +150 -0
- package/dist/src/cli/index.js +10 -0
- package/dist/{cloudflare → src/cloudflare}/index.js +3 -3
- package/dist/{decorators → src/decorators}/index.js +2 -2
- package/dist/src/index.js +132 -0
- package/dist/src/runtime/index.js +111 -0
- package/dist/{sveltekit → src/sveltekit}/index.js +14 -6
- package/dist/{test → src/test}/index.js +22 -13
- package/dist/{vite → src/vite}/index.js +128 -59
- package/dist/sveltekit/platform.d.ts.map +1 -1
- package/dist/test/bridge-context.d.ts +5 -2
- package/dist/test/bridge-context.d.ts.map +1 -1
- package/dist/test/cf.d.ts +25 -11
- package/dist/test/cf.d.ts.map +1 -1
- package/dist/test/email.d.ts +16 -7
- package/dist/test/email.d.ts.map +1 -1
- package/dist/test/queue.d.ts.map +1 -1
- package/dist/test/resolve-service-bindings.d.ts.map +1 -1
- package/dist/test/scheduled.d.ts.map +1 -1
- package/dist/test/simple-context.d.ts +1 -1
- package/dist/test/simple-context.d.ts.map +1 -1
- package/dist/test/tail.d.ts +2 -1
- package/dist/test/tail.d.ts.map +1 -1
- package/dist/test/worker.d.ts +6 -0
- package/dist/test/worker.d.ts.map +1 -1
- package/dist/transform/durable-object.d.ts.map +1 -1
- package/dist/transform/worker-entrypoint.d.ts.map +1 -1
- package/dist/{types-5nyrz1sz.js → types-nq5acrwh.js} +30 -16
- package/dist/utils/entrypoint-discovery.d.ts +6 -3
- package/dist/utils/entrypoint-discovery.d.ts.map +1 -1
- package/dist/utils/send-email.d.ts +15 -0
- package/dist/utils/send-email.d.ts.map +1 -0
- package/dist/vite/plugin.d.ts.map +1 -1
- package/dist/worker-entry/composed-worker.d.ts +13 -0
- package/dist/worker-entry/composed-worker.d.ts.map +1 -0
- package/dist/worker-entry/routes.d.ts +22 -0
- package/dist/worker-entry/routes.d.ts.map +1 -0
- package/dist/{worker-entrypoint-m9th0rg0.js → worker-entrypoint-c259fmfs.js} +1 -1
- package/package.json +21 -19
- package/dist/index.js +0 -298
- package/dist/runtime/index.js +0 -111
package/LLM.md
CHANGED
|
@@ -1,976 +1,1790 @@
|
|
|
1
1
|
# Devflare
|
|
2
2
|
|
|
3
|
-
This file is
|
|
3
|
+
This file is the truth-first contract for `devflare` as implemented today.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use it when you are:
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
##
|
|
15
|
+
## Quick start: safest supported path
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
Prefer this shape unless you have a concrete reason not to:
|
|
14
18
|
|
|
15
|
-
|
|
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
|
-
|
|
27
|
+
```ts
|
|
28
|
+
import { defineConfig } from 'devflare'
|
|
18
29
|
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
51
|
+
## Trust map
|
|
52
|
+
|
|
53
|
+
### Layers that are easy to confuse
|
|
35
54
|
|
|
36
|
-
|
|
55
|
+
Keep these layers separate until you have a reason to connect them:
|
|
37
56
|
|
|
38
|
-
|
|
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
|
-
|
|
63
|
+
The classic mixups are:
|
|
41
64
|
|
|
42
|
-
|
|
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
|
-
|
|
72
|
+
### Source of truth vs generated output
|
|
45
73
|
|
|
46
|
-
|
|
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
|
-
|
|
76
|
+
- `.devflare/`
|
|
77
|
+
- `env.d.ts`
|
|
78
|
+
- generated `wrangler.jsonc`
|
|
54
79
|
|
|
55
|
-
|
|
80
|
+
The source of truth is still:
|
|
56
81
|
|
|
57
|
-
|
|
82
|
+
- `devflare.config.ts`
|
|
83
|
+
- your source files under `src/`
|
|
84
|
+
- your tests
|
|
58
85
|
|
|
59
|
-
|
|
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
|
-
|
|
71
|
-
- Devflare compiles your config into Wrangler-compatible config rather than making you hand-maintain the low-level representation yourself.
|
|
88
|
+
---
|
|
72
89
|
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
+
### Authoring model
|
|
101
120
|
|
|
102
|
-
|
|
121
|
+
A **surface** is a distinct handler or entry file that Devflare treats as its own concern. The common surfaces are:
|
|
103
122
|
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
+
---
|
|
109
135
|
|
|
110
|
-
|
|
136
|
+
## Package entrypoints
|
|
111
137
|
|
|
112
|
-
Use
|
|
138
|
+
Use the narrowest import path that matches where the code runs.
|
|
113
139
|
|
|
114
|
-
|
|
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
|
-
|
|
150
|
+
### `devflare` vs `devflare/runtime`
|
|
117
151
|
|
|
118
|
-
|
|
152
|
+
Default rule:
|
|
119
153
|
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
Practical example:
|
|
127
163
|
|
|
128
|
-
|
|
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
|
-
|
|
190
|
+
await createTestContext()
|
|
191
|
+
await env.CACHE.put('health', 'ok')
|
|
192
|
+
```
|
|
131
193
|
|
|
132
|
-
|
|
194
|
+
### Worker-safe caveat for the main entry
|
|
133
195
|
|
|
134
|
-
|
|
196
|
+
`devflare/runtime` is the explicit worker-safe runtime entry and should be the default teaching path for worker code.
|
|
135
197
|
|
|
136
|
-
|
|
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
|
-
|
|
200
|
+
In that worker-safe main bundle:
|
|
139
201
|
|
|
140
|
-
|
|
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
|
-
|
|
205
|
+
If you need `ctx`, `event`, `locals`, middleware, or per-surface getters, import them from `devflare/runtime`.
|
|
143
206
|
|
|
144
207
|
---
|
|
145
208
|
|
|
146
|
-
##
|
|
209
|
+
## Runtime and HTTP model
|
|
147
210
|
|
|
148
|
-
|
|
211
|
+
### Event-first handlers are the public story
|
|
149
212
|
|
|
150
|
-
|
|
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
|
-
|
|
243
|
+
Prefer runtime access in this order:
|
|
163
244
|
|
|
164
|
-
|
|
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
|
-
|
|
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 {
|
|
269
|
+
import { getFetchEvent, locals, type FetchEvent } from 'devflare/runtime'
|
|
170
270
|
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
287
|
+
### Manual context helpers are advanced/internal
|
|
177
288
|
|
|
178
|
-
|
|
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
|
-
|
|
291
|
+
Devflare establishes the active event/context automatically before invoking user code in the flows developers normally use:
|
|
185
292
|
|
|
186
|
-
|
|
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
|
-
|
|
298
|
+
That means getters like `getQueueEvent()`, `getScheduledEvent()`, `getEmailEvent()`, and `getDurableObjectFetchEvent()` should work inside ordinary handlers without manual wrapping.
|
|
189
299
|
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
+
Important difference:
|
|
202
306
|
|
|
203
|
-
|
|
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
|
-
###
|
|
310
|
+
### HTTP entry, middleware, and method handlers
|
|
206
311
|
|
|
207
|
-
|
|
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
|
-
|
|
319
|
+
files: {
|
|
320
|
+
fetch: 'src/fetch.ts'
|
|
321
|
+
}
|
|
211
322
|
```
|
|
212
323
|
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
+
`fetch` and `handle` are aliases for the same primary HTTP entry. Export one or the other, not both.
|
|
220
337
|
|
|
221
|
-
|
|
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
|
-
|
|
340
|
+
Request-wide middleware belongs in `src/fetch.ts` and composes with `sequence(...)`.
|
|
230
341
|
|
|
231
|
-
|
|
342
|
+
```ts
|
|
343
|
+
import { sequence } from 'devflare/runtime'
|
|
344
|
+
import type { FetchEvent, ResolveFetch } from 'devflare/runtime'
|
|
232
345
|
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
354
|
+
async function appFetch({ request }: FetchEvent): Promise<Response> {
|
|
355
|
+
return new Response(new URL(request.url).pathname)
|
|
356
|
+
}
|
|
243
357
|
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
##
|
|
457
|
+
## Configuration and compilation
|
|
304
458
|
|
|
305
|
-
|
|
459
|
+
### Stable config flow
|
|
306
460
|
|
|
307
|
-
|
|
461
|
+
This is the stable public flow for Devflare config:
|
|
308
462
|
|
|
309
|
-
|
|
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: '
|
|
486
|
+
name: 'api-worker',
|
|
487
|
+
compatibilityDate: '2026-03-17',
|
|
316
488
|
files: {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
489
|
+
fetch: 'src/fetch.ts'
|
|
490
|
+
},
|
|
491
|
+
vars: {
|
|
492
|
+
APP_NAME: 'api-worker'
|
|
321
493
|
}
|
|
322
494
|
})
|
|
323
495
|
```
|
|
324
496
|
|
|
325
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
731
|
+
Current exclusions still matter:
|
|
340
732
|
|
|
341
|
-
-
|
|
342
|
-
-
|
|
343
|
-
-
|
|
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
|
-
|
|
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: '
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
794
|
+
Two practical rules:
|
|
373
795
|
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
const url = new URL(request.url)
|
|
799
|
+
### `files`
|
|
379
800
|
|
|
380
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
803
|
+
| Key | Shape | Default convention | Meaning |
|
|
804
|
+
|---|---|---|---|
|
|
805
|
+
| `fetch` | <code>string | false</code> | `src/fetch.ts` | main HTTP handler |
|
|
806
|
+
| `queue` | <code>string | false</code> | `src/queue.ts` | queue consumer handler |
|
|
807
|
+
| `scheduled` | <code>string | false</code> | `src/scheduled.ts` | scheduled handler |
|
|
808
|
+
| `email` | <code>string | false</code> | `src/email.ts` | incoming email handler |
|
|
809
|
+
| `durableObjects` | <code>string | false</code> | `**/do.*.{ts,js}` | Durable Object discovery glob |
|
|
810
|
+
| `entrypoints` | <code>string | false</code> | `**/ep.*.{ts,js}` | WorkerEntrypoint discovery glob |
|
|
811
|
+
| `workflows` | <code>string | false</code> | `**/wf.*.{ts,js}` | workflow discovery glob |
|
|
812
|
+
| `routes` | <code>{ dir, prefix? } | false</code> | `src/routes` when that directory exists | built-in file router configuration |
|
|
813
|
+
| `transport` | <code>string | null</code> | `src/transport.{ts,js,mts,mjs}` when one of those files exists | custom transport definition file |
|
|
389
814
|
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
```
|
|
815
|
+
Discovery does not behave identically in every subsystem:
|
|
393
816
|
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
id: string
|
|
401
|
-
type: string
|
|
402
|
-
}
|
|
826
|
+
Practical example:
|
|
403
827
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
855
|
+
Keep these layers separate:
|
|
432
856
|
|
|
433
|
-
|
|
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
|
-
|
|
866
|
+
#### `.env`, `.env.dev`, and process env
|
|
436
867
|
|
|
437
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
877
|
+
Treat `.env*` files as **config/build-time inputs**, not as Devflare's runtime secret system.
|
|
463
878
|
|
|
464
|
-
|
|
879
|
+
#### `.dev.vars` and local runtime secrets
|
|
465
880
|
|
|
466
|
-
|
|
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
|
-
|
|
883
|
+
That means:
|
|
469
884
|
|
|
470
|
-
-
|
|
471
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
895
|
+
In the current native Devflare schema, `vars` is:
|
|
494
896
|
|
|
495
|
-
|
|
496
|
-
|
|
897
|
+
```ts
|
|
898
|
+
Record<string, string>
|
|
899
|
+
```
|
|
497
900
|
|
|
498
|
-
|
|
901
|
+
#### `secrets`
|
|
499
902
|
|
|
500
|
-
Use
|
|
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
|
-
|
|
908
|
+
{ required?: boolean }
|
|
909
|
+
```
|
|
504
910
|
|
|
505
|
-
|
|
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
|
-
|
|
913
|
+
```ts
|
|
914
|
+
secrets: {
|
|
915
|
+
API_KEY: {}
|
|
916
|
+
}
|
|
513
917
|
```
|
|
514
918
|
|
|
515
|
-
|
|
919
|
+
means “`API_KEY` is a required runtime secret,” not “optional secret with no requirements.”
|
|
516
920
|
|
|
517
|
-
|
|
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
|
-
|
|
927
|
+
Do not describe `secrets` as a place that stores values.
|
|
527
928
|
|
|
528
|
-
|
|
929
|
+
#### Example files such as `.env.example` and `.dev.vars.example`
|
|
529
930
|
|
|
530
|
-
|
|
931
|
+
Example files are a **team convention**, not a Devflare feature.
|
|
531
932
|
|
|
532
|
-
|
|
933
|
+
Current truthful guidance:
|
|
533
934
|
|
|
534
|
-
|
|
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
|
-
|
|
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
|
-
|
|
942
|
+
If you want the lowest-confusion setup, use this split:
|
|
541
943
|
|
|
542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
961
|
+
Example `.env.example`:
|
|
549
962
|
|
|
550
|
-
|
|
963
|
+
```dotenv
|
|
964
|
+
WORKER_NAME=my-worker
|
|
965
|
+
API_ORIGIN=http://localhost:3000
|
|
966
|
+
```
|
|
551
967
|
|
|
552
|
-
|
|
968
|
+
Example `.dev.vars.example`:
|
|
553
969
|
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
COUNTER: 'Counter'
|
|
558
|
-
}
|
|
559
|
-
}
|
|
970
|
+
```dotenv
|
|
971
|
+
API_KEY=replace-me
|
|
972
|
+
SESSION_SECRET=replace-me
|
|
560
973
|
```
|
|
561
974
|
|
|
562
|
-
|
|
975
|
+
Typical git ignore pattern for user projects:
|
|
563
976
|
|
|
564
|
-
|
|
977
|
+
```gitignore
|
|
978
|
+
.env
|
|
979
|
+
.env.*
|
|
980
|
+
!.env.example
|
|
981
|
+
.dev.vars
|
|
982
|
+
.dev.vars.*
|
|
983
|
+
!.dev.vars.example
|
|
984
|
+
```
|
|
565
985
|
|
|
566
|
-
|
|
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: '
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
584
|
-
import { DurableObject } from 'cloudflare:workers'
|
|
1017
|
+
import type { FetchEvent } from 'devflare/runtime'
|
|
585
1018
|
|
|
586
|
-
export
|
|
587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
594
|
-
this.data.set(key, value)
|
|
595
|
-
}
|
|
1030
|
+
Typical deployed secret flow:
|
|
596
1031
|
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
612
|
-
const session = env.SESSION.get(id)
|
|
1044
|
+
Practical rule of thumb:
|
|
613
1045
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1056
|
+
#### `config.env`
|
|
637
1057
|
|
|
638
|
-
|
|
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
|
-
|
|
1060
|
+
Current merge behavior uses deep merge semantics:
|
|
645
1061
|
|
|
646
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
665
|
-
import { DoubleableNumber } from './DoubleableNumber'
|
|
1072
|
+
import { defineConfig } from 'devflare'
|
|
666
1073
|
|
|
667
|
-
export
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
683
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
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
|
-
|
|
701
|
-
|
|
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
|
-
|
|
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
|
-
|
|
710
|
-
expect(result.double).toBe(4)
|
|
711
|
-
})
|
|
712
|
-
})
|
|
713
|
-
```
|
|
1179
|
+
Service bindings can be written directly or via `ref()`.
|
|
714
1180
|
|
|
715
|
-
|
|
1181
|
+
```ts
|
|
1182
|
+
import { defineConfig, ref } from 'devflare'
|
|
716
1183
|
|
|
717
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1199
|
+
Practical example:
|
|
728
1200
|
|
|
729
1201
|
```ts
|
|
1202
|
+
// gateway/devflare.config.ts
|
|
730
1203
|
import { defineConfig, ref } from 'devflare'
|
|
731
1204
|
|
|
732
|
-
const
|
|
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
|
-
|
|
1211
|
+
ADMIN: auth.worker('AdminEntrypoint')
|
|
739
1212
|
}
|
|
740
1213
|
}
|
|
741
1214
|
})
|
|
742
1215
|
```
|
|
743
1216
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1217
|
+
```ts
|
|
1218
|
+
// gateway/src/fetch.ts
|
|
1219
|
+
import type { FetchEvent } from 'devflare/runtime'
|
|
747
1220
|
|
|
748
|
-
|
|
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
|
-
|
|
1227
|
+
### Remote-oriented and narrower bindings
|
|
751
1228
|
|
|
752
|
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
1237
|
+
Incoming email is a separate worker surface:
|
|
764
1238
|
|
|
765
|
-
|
|
1239
|
+
- `files.email`
|
|
1240
|
+
- `src/email.ts`
|
|
1241
|
+
- `EmailEvent`
|
|
766
1242
|
|
|
767
|
-
|
|
1243
|
+
Keep outbound `sendEmail` and inbound email handling separate in docs and examples.
|
|
768
1244
|
|
|
769
|
-
|
|
1245
|
+
Practical example:
|
|
770
1246
|
|
|
771
1247
|
```ts
|
|
772
|
-
|
|
773
|
-
import {
|
|
774
|
-
import { env } from 'devflare'
|
|
1248
|
+
// devflare.config.ts
|
|
1249
|
+
import { defineConfig } from 'devflare'
|
|
775
1250
|
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
1283
|
+
```ts
|
|
1284
|
+
// src/email.ts
|
|
1285
|
+
import type { EmailEvent } from 'devflare/runtime'
|
|
788
1286
|
|
|
789
|
-
|
|
1287
|
+
export async function email({ message }: EmailEvent<DevflareEnv>): Promise<void> {
|
|
1288
|
+
await message.forward('ops@example.com')
|
|
1289
|
+
}
|
|
1290
|
+
```
|
|
790
1291
|
|
|
791
|
-
###
|
|
1292
|
+
### `ref()` and multi-worker composition
|
|
792
1293
|
|
|
793
|
-
|
|
1294
|
+
Use `ref()` when one worker depends on another worker config.
|
|
794
1295
|
|
|
795
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
1321
|
+
## Development workflows
|
|
806
1322
|
|
|
807
|
-
|
|
1323
|
+
### Vite vs Rolldown: the truthful mental model
|
|
808
1324
|
|
|
809
|
-
|
|
1325
|
+
They are both important, but they are not two names for the same job.
|
|
810
1326
|
|
|
811
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1338
|
+
### Operational decision rules
|
|
821
1339
|
|
|
822
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1353
|
+
Current Vite-backed flow:
|
|
833
1354
|
|
|
834
|
-
|
|
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
|
-
|
|
837
|
-
- Vectorize
|
|
1363
|
+
Two ownership rules matter here:
|
|
838
1364
|
|
|
839
|
-
Devflare
|
|
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
|
-
|
|
1368
|
+
#### `devflare/vite` helpers
|
|
842
1369
|
|
|
843
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1388
|
+
Timing rule of thumb:
|
|
850
1389
|
|
|
851
|
-
|
|
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
|
-
|
|
1393
|
+
#### Minimal Vite wiring
|
|
854
1394
|
|
|
855
|
-
|
|
1395
|
+
```ts
|
|
1396
|
+
import { defineConfig } from 'vite'
|
|
1397
|
+
import { devflarePlugin } from 'devflare/vite'
|
|
856
1398
|
|
|
857
|
-
|
|
1399
|
+
export default defineConfig({
|
|
1400
|
+
plugins: [devflarePlugin()]
|
|
1401
|
+
})
|
|
1402
|
+
```
|
|
858
1403
|
|
|
859
|
-
|
|
1404
|
+
#### Explicit `@cloudflare/vite-plugin` wiring
|
|
860
1405
|
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
|
|
1428
|
+
#### SvelteKit-backed Worker example
|
|
891
1429
|
|
|
892
|
-
|
|
1430
|
+
```ts
|
|
1431
|
+
// devflare.config.ts
|
|
1432
|
+
import { defineConfig } from 'devflare'
|
|
893
1433
|
|
|
894
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1455
|
+
export default defineConfig({
|
|
1456
|
+
plugins: [
|
|
1457
|
+
devflarePlugin(),
|
|
1458
|
+
sveltekit()
|
|
1459
|
+
]
|
|
1460
|
+
})
|
|
1461
|
+
```
|
|
899
1462
|
|
|
900
|
-
|
|
1463
|
+
```ts
|
|
1464
|
+
// src/hooks.server.ts
|
|
1465
|
+
export { handle } from 'devflare/sveltekit'
|
|
1466
|
+
```
|
|
901
1467
|
|
|
902
|
-
|
|
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
|
-
|
|
1470
|
+
### Rolldown bundling and plugin workflows
|
|
905
1471
|
|
|
906
|
-
|
|
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
|
-
|
|
1474
|
+
That is why `rolldown` is important but different from Vite:
|
|
909
1475
|
|
|
910
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1482
|
+
Current DO bundler behavior:
|
|
915
1483
|
|
|
916
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1494
|
+
#### Minimal custom transform example
|
|
921
1495
|
|
|
922
|
-
|
|
1496
|
+
```ts
|
|
1497
|
+
import { defineConfig } from 'devflare'
|
|
1498
|
+
import type { Plugin as RolldownPlugin } from 'rolldown'
|
|
923
1499
|
|
|
924
|
-
|
|
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
|
-
|
|
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: '
|
|
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/
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
|
|
948
|
-
import {
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
|
|
1788
|
+
## Summary
|
|
969
1789
|
|
|
970
|
-
|
|
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.
|