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