devflare 1.0.0-next.0 → 1.0.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/LLM.md +1751 -0
  2. package/README.md +326 -458
  3. package/package.json +3 -2
package/LLM.md ADDED
@@ -0,0 +1,1751 @@
1
+ # Devflare
2
+
3
+ This file is for LLMs and developers **using** the published `devflare` package.
4
+
5
+ It documents the public package contract, the actual config/runtime model, and the places where Devflare adds behavior on top of Bun, Vite, Miniflare, Wrangler, and Cloudflare.
6
+
7
+ It does **not** document this repository’s internal implementation details, test fixtures, or example-case layout except when those details directly affect the public behavior of the package.
8
+
9
+ The most important thing to understand is that Devflare spans **multiple layers** that are easy to confuse:
10
+
11
+ 1. config-time `process.env`
12
+ 2. `devflare.config.ts`
13
+ 3. generated `wrangler.jsonc`
14
+ 4. runtime Worker `env` bindings
15
+ 5. local-only dev/test helpers
16
+
17
+ If two things have similar names but live in different layers, assume they are different until proven otherwise. The classic troublemakers are:
18
+
19
+ - `files.routes` vs top-level `routes`
20
+ - `vars` vs `secrets`
21
+ - Bun `.env` loading vs local runtime secret loading
22
+ - Devflare `env` overrides vs raw Wrangler environment sections
23
+
24
+ ---
25
+
26
+ ## What Devflare is
27
+
28
+ Devflare is a **developer platform layer for Cloudflare Workers**.
29
+
30
+ Miniflare gives you a local Workers runtime. Wrangler gives you deployment configuration and deployment workflows. Vite gives you a frontend/build pipeline. Bun gives you the CLI runtime and process environment behavior.
31
+
32
+ Devflare sits on top of those pieces and turns them into one coherent authoring model.
33
+
34
+ Concretely, Devflare provides:
35
+
36
+ - typed config via `defineConfig()`
37
+ - convention-first file discovery for worker surfaces
38
+ - compilation from Devflare config into Wrangler-compatible config
39
+ - local orchestration around Miniflare, Vite, and bridge-backed tooling
40
+ - typed multi-worker references via `ref()`
41
+ - test helpers for HTTP, queues, scheduled handlers, email, and tail events
42
+ - framework-aware integration for Vite and SvelteKit
43
+ - local features that are awkward or incomplete in raw Miniflare-only setups, including browser-rendering workflows, websocket-aware dev proxying, and unified test ergonomics
44
+
45
+ So Devflare is **not** “a different Workers runtime”.
46
+
47
+ It is a higher-level developer system that uses the Cloudflare ecosystem underneath.
48
+
49
+ ---
50
+
51
+ ## The authoring model Devflare wants
52
+
53
+ Devflare is intentionally opinionated.
54
+
55
+ It wants you to think in **surfaces** rather than “one giant worker file that does everything”.
56
+
57
+ Typical surfaces are:
58
+
59
+ - HTTP requests via `src/fetch.ts`
60
+ - queue consumers via `src/queue.ts`
61
+ - cron handlers via `src/scheduled.ts`
62
+ - incoming email via `src/email.ts`
63
+ - file-based HTTP routes via `src/routes/**`
64
+ - Durable Objects via `do.*.ts`
65
+ - WorkerEntrypoints via `ep.*.ts`
66
+ - workflows via `wf.*.ts`
67
+ - custom RPC serialization via `src/transport.ts`
68
+
69
+ This separation is one of Devflare’s biggest advantages:
70
+
71
+ - responsibility boundaries stay clear
72
+ - test helpers map naturally onto handler types
73
+ - multi-worker systems stay readable
74
+ - LLM-generated code stays composable instead of collapsing into a single blob
75
+
76
+ When generating or reviewing Devflare code, prefer **one responsibility per file** unless there is a very strong reason not to.
77
+
78
+ ---
79
+
80
+ ## Package entrypoints
81
+
82
+ Use package subpaths intentionally.
83
+
84
+ | Import | What it is for | What it exposes in practice |
85
+ |---|---|---|
86
+ | `devflare` | Main package entrypoint | `defineConfig`, config loading/compilation helpers, `ref()`, unified `env`, CLI helpers, bridge helpers, decorators, transform helpers, test helpers re-exported for convenience, `workerName` |
87
+ | `devflare/runtime` | Worker-safe runtime utilities | middleware helpers like `sequence()`, `resolve()`, `pipe()`, validation helpers, decorators |
88
+ | `devflare/test` | Testing API | `createTestContext`, `cf`, `email`, `queue`, `scheduled`, `worker`, `tail`, mock helpers, remote skip helpers, bridge test context |
89
+ | `devflare/vite` | Vite integration | `devflarePlugin`, plugin context helpers, config helpers, Vite-side types |
90
+ | `devflare/sveltekit` | SvelteKit integration | `handle`, `createDevflarePlatform`, `createHandle`, dev/reset helpers, integration types |
91
+ | `devflare/cloudflare` | Cloudflare account/auth/usage helpers | `account` helper object, auth/account/resource inspection, usage tracking, limits/preferences helpers, Cloudflare API errors |
92
+ | `devflare/decorators` | Decorators only | `durableObject()` and related decorator utilities |
93
+
94
+ ### Important runtime note
95
+
96
+ `devflare/runtime` is intentionally **smaller** than the main package.
97
+
98
+ Today it does **not** export the unified `env` proxy from the main `devflare` entrypoint. If you want `import { env } from 'devflare'`, import it from `devflare`, not `devflare/runtime`.
99
+
100
+ ### Common main-package exports
101
+
102
+ The most common exports from `devflare` are:
103
+
104
+ - `defineConfig`
105
+ - `loadConfig`
106
+ - `compileConfig`
107
+ - `stringifyConfig`
108
+ - `ref`
109
+ - `env`
110
+ - `workerName`
111
+
112
+ The main package also exports advanced tooling APIs such as CLI helpers, bridge helpers, DO transform helpers, WorkerEntrypoint transform helpers, decorators, and test helpers.
113
+
114
+ Two small but useful details:
115
+
116
+ - the package default export is also `defineConfig`
117
+ - `workerName` is a build-time-injected worker-name helper, with dev/test fallback behavior rather than being a dynamic Cloudflare metadata API
118
+
119
+ ### Deprecated and compatibility exports
120
+
121
+ The package still exposes a few compatibility surfaces so older code keeps working.
122
+
123
+ Prefer the newer APIs in fresh code:
124
+
125
+ - `resolveRef` and `serviceBinding` are legacy helpers around the newer `ref()` model
126
+ - test helpers are re-exported from the main package for convenience, but `devflare/test` is the clearer import path for test code
127
+ - `createMultiWorkerContext` and `createEntrypointScript` from `devflare/test` are deprecated in favor of `createTestContext()` and the worker-entrypoint pattern
128
+ - `isRemoteModeEnabled()` is a deprecated alias; treat the remote-mode flow and `shouldSkip` helpers as the primary public story
129
+ - `testEnv` belongs to the bridge-test-context API, not to the main unified `env` proxy
130
+
131
+ ---
132
+
133
+ ## Config lifecycle
134
+
135
+ This is the actual config lifecycle Devflare uses.
136
+
137
+ 1. You author `devflare.config.ts` with `defineConfig()`
138
+ 2. Devflare loads and validates it with the Zod-backed schema
139
+ 3. If an environment is selected, Devflare merges `config.env[environment]` into the base config
140
+ 4. Devflare compiles the resolved config into a Wrangler-compatible object
141
+ 5. Build/deploy flows write a generated `wrangler.jsonc`
142
+ 6. Vite/Wrangler/Miniflare then use the generated result in their respective phases
143
+
144
+ ### Config file names
145
+
146
+ `loadConfig()` looks for these filenames, in this order:
147
+
148
+ - `devflare.config.ts`
149
+ - `devflare.config.mts`
150
+ - `devflare.config.js`
151
+ - `devflare.config.mjs`
152
+
153
+ ### `defineConfig()` supported forms
154
+
155
+ `defineConfig()` accepts all of these forms:
156
+
157
+ ```ts
158
+ import { defineConfig } from 'devflare'
159
+
160
+ export default defineConfig({
161
+ name: 'my-worker'
162
+ })
163
+ ```
164
+
165
+ ```ts
166
+ import { defineConfig } from 'devflare'
167
+
168
+ export default defineConfig(() => ({
169
+ name: process.env.WORKER_NAME ?? 'my-worker'
170
+ }))
171
+ ```
172
+
173
+ ```ts
174
+ import { defineConfig } from 'devflare'
175
+
176
+ export default defineConfig(async () => ({
177
+ name: 'my-worker'
178
+ }))
179
+ ```
180
+
181
+ It also supports a generic entrypoint type parameter:
182
+
183
+ ```ts
184
+ import { defineConfig } from 'devflare'
185
+
186
+ export default defineConfig<import('./env').Entrypoints>({
187
+ name: 'my-worker'
188
+ })
189
+ ```
190
+
191
+ That generic exists to improve type inference for `ref().worker('SomeEntrypoint')` after `env.d.ts` has been generated.
192
+
193
+ ---
194
+
195
+ ## How env files, `vars`, and `secrets` actually work
196
+
197
+ This is the section people most often get wrong.
198
+
199
+ ### The short version
200
+
201
+ - `loadConfig()` itself does **not** run dotenv loading
202
+ - the Devflare CLI runs under **Bun**, and Bun **does** auto-load `.env` files into `process.env`
203
+ - `vars` are plain configuration values compiled into generated Wrangler config and exposed at runtime as normal bindings
204
+ - `secrets` are **declarations of expected runtime secrets**, not a place to store secret values
205
+ - actual secret values should come from Cloudflare secret storage or the local secret mechanisms used by Wrangler/Cloudflare tooling
206
+
207
+ ### The four sources you must distinguish
208
+
209
+ | Thing | Source of truth | Used when | Appears in generated `wrangler.jsonc` | Available as runtime binding | Safe for secrets |
210
+ |---|---|---|---|---|---|
211
+ | `process.env` | Bun process environment, shell env, or manually loaded env files | config evaluation time and build tooling | No, unless you copy values into config | Not automatically | Only if your own process handling is secure |
212
+ | `vars` | `devflare.config.ts` | compile time and runtime | **Yes** | **Yes** | **No** |
213
+ | `secrets` | secret declaration in `devflare.config.ts` plus actual runtime secret store | runtime | **No secret values are compiled** | **Yes**, if runtime values are provided | **Yes** |
214
+ | local runtime secret files such as `.dev.vars` or Wrangler-style local `.env` usage | local Cloudflare/Wrangler runtime layer | local runtime only | No | Yes | Yes |
215
+ | Cloudflare secret store | Cloudflare platform | deployed runtime | No | Yes | Yes |
216
+
217
+ ### What Devflare itself does and does not load
218
+
219
+ `loadConfig()` calls the underlying config loader with `dotenv: false`.
220
+
221
+ That means **Devflare’s config loader does not provide its own dotenv layer**.
222
+
223
+ However, the published CLI entrypoint runs via Bun, and Bun auto-loads env files such as:
224
+
225
+ - `.env`
226
+ - `.env.<NODE_ENV>`
227
+ - `.env.local`
228
+
229
+ So when you run the Devflare CLI normally, `process.env` is often already populated **before** `devflare.config.ts` is evaluated.
230
+
231
+ That gives you this effective behavior:
232
+
233
+ - **CLI usage under Bun**: `.env` values are usually available via `process.env`
234
+ - **programmatic `loadConfig()` in another runtime**: do **not** assume env files were loaded unless your process already loaded them
235
+
236
+ This distinction matters a lot. The env-file loading is coming from **Bun**, not from Devflare’s config loader.
237
+
238
+ ### Recommended mental model
239
+
240
+ Treat config-time env and runtime env as separate layers:
241
+
242
+ 1. read config-time values from `process.env`
243
+ 2. put **non-secret** values that should become Worker bindings in `vars`
244
+ 3. declare **secret names** in `secrets`
245
+ 4. provide actual secret values through local runtime secret files or Cloudflare secret storage
246
+
247
+ ### Example: non-secret build/runtime config vs runtime secret
248
+
249
+ ```ts
250
+ import { defineConfig } from 'devflare'
251
+
252
+ export default defineConfig(() => ({
253
+ name: 'billing-worker',
254
+ vars: {
255
+ PUBLIC_API_BASE: process.env.PUBLIC_API_BASE ?? 'http://localhost:5173',
256
+ LOG_LEVEL: process.env.LOG_LEVEL ?? 'info'
257
+ },
258
+ secrets: {
259
+ STRIPE_SECRET_KEY: { required: true },
260
+ POSTMARK_TOKEN: { required: true }
261
+ }
262
+ }))
263
+ ```
264
+
265
+ What this means:
266
+
267
+ - `PUBLIC_API_BASE` and `LOG_LEVEL` are not treated as secret and may be baked into generated config
268
+ - `STRIPE_SECRET_KEY` and `POSTMARK_TOKEN` are declared as required runtime secrets
269
+ - the **values** for those secrets should come from a runtime secret source, not from `vars`
270
+
271
+ ### Why `vars` are not secrets
272
+
273
+ Devflare’s compiler writes `vars` into the generated Wrangler config object.
274
+
275
+ That means they are part of the deployment configuration shape. In other words:
276
+
277
+ - they are configuration
278
+ - they are intentionally reproducible
279
+ - they are suitable for public base URLs, feature flags, modes, IDs, and other non-secret values
280
+ - they are **not** suitable for API keys, tokens, passwords, or signing secrets
281
+
282
+ ### What `secrets` actually mean in Devflare
283
+
284
+ The current `secrets` field is a declaration layer. It tells Devflare which secret names exist and whether they are required.
285
+
286
+ Today, each secret is configured as:
287
+
288
+ ```ts
289
+ secrets: {
290
+ MY_SECRET: { required: true }
291
+ }
292
+ ```
293
+
294
+ What this does **not** mean:
295
+
296
+ - it does not embed the secret value in `devflare.config.ts`
297
+ - it does not compile secret values into `wrangler.jsonc`
298
+ - it does not create a second custom secret-store format on top of Cloudflare/Wrangler
299
+
300
+ What it **does** mean:
301
+
302
+ - you are declaring an expected runtime binding
303
+ - generated types can treat that binding as a string-like secret binding
304
+ - your runtime still needs the actual value from a proper secret source
305
+
306
+ ### Local env-file overlap warning
307
+
308
+ The filename `.env` can be confusing because it may participate in **two different stories**:
309
+
310
+ 1. Bun may read it at process start and populate `process.env`
311
+ 2. local Cloudflare/Wrangler tooling may also use `.env`-style files for runtime bindings
312
+
313
+ So the same file can accidentally influence both config-time and runtime behavior.
314
+
315
+ If you want clearer separation, prefer this mental split:
316
+
317
+ - use Bun-loaded `.env` only for config-time inputs that you intentionally read via `process.env`
318
+ - use local secret files such as `.dev.vars` for runtime secrets
319
+
320
+ ### If you need strict control over env loading
321
+
322
+ Because the CLI runs under Bun, Bun’s own env-file controls still matter. If you invoke the CLI through Bun directly, Bun-level flags such as explicit env-file selection or env-file disabling control that part of the story.
323
+
324
+ ---
325
+
326
+ ## Top-level config reference
327
+
328
+ This table shows what each top-level config key means, which layer uses it, and whether it becomes part of compiled Wrangler output.
329
+
330
+ | Key | Meaning | Used by | Compiled to `wrangler.jsonc` | Notes |
331
+ |---|---|---|---|---|
332
+ | `name` | Worker name | all phases | Yes | required |
333
+ | `accountId` | Cloudflare account ID | compile, dev, remote integrations | Yes | especially relevant for remote services |
334
+ | `compatibilityDate` | Workers compatibility date | compile/runtime | Yes | defaults to current date |
335
+ | `compatibilityFlags` | Workers compatibility flags | compile/runtime | Yes | Devflare always includes `nodejs_compat` and `nodejs_als` |
336
+ | `files` | handler paths and discovery globs | Devflare discovery/dev/test/build tooling | Partly | `files.fetch` influences compiled `main`; most of `files` is Devflare-only metadata |
337
+ | `bindings` | Cloudflare bindings | compile/dev/test/runtime | Mostly | some bindings are stronger than others, see caveats below |
338
+ | `triggers` | cron triggers | compile/runtime | Yes | currently `crons` |
339
+ | `vars` | non-secret runtime bindings | compile/runtime | Yes | string values only in current schema |
340
+ | `secrets` | secret declarations | typing/validation/runtime expectations | No secret values | declaration layer only |
341
+ | `routes` | Cloudflare deployment routes | compile/deploy | Yes | this is edge routing, not file-based app routing |
342
+ | `wsRoutes` | dev-mode websocket-to-DO proxy rules | local dev | No | Devflare-only dev feature |
343
+ | `assets` | static assets config | compile/dev/runtime | Yes | current native shape is `{ directory, binding? }` |
344
+ | `limits` | worker runtime limits | compile/runtime | Yes | current native shape only models `cpu_ms` |
345
+ | `observability` | worker logs / sampling | compile/runtime | Yes | passes through directly |
346
+ | `migrations` | Durable Object migrations | compile/runtime | Yes | used for DO class evolution |
347
+ | `build` | build metadata/options | schema/dev/build tooling | No direct compiler output | see caveats below |
348
+ | `plugins` | Vite plugin list / build-time extension point | schema/build tooling | No direct compiler output | see caveats below |
349
+ | `env` | environment-specific overrides | Devflare pre-compilation merge layer | Not as nested env blocks | only a subset of top-level keys is supported inside each env block; see the dedicated section |
350
+ | `wrangler.passthrough` | raw Wrangler fields not modeled natively | compiler/deploy | Yes | merged at top level into compiled config |
351
+
352
+ ### Minimal config
353
+
354
+ ```ts
355
+ import { defineConfig } from 'devflare'
356
+
357
+ export default defineConfig({
358
+ name: 'hello-worker'
359
+ })
360
+ ```
361
+
362
+ ### Realistic starting config
363
+
364
+ ```ts
365
+ import { defineConfig } from 'devflare'
366
+
367
+ export default defineConfig({
368
+ name: 'my-app',
369
+ files: {
370
+ routes: {
371
+ dir: 'src/routes',
372
+ prefix: '/api'
373
+ }
374
+ },
375
+ bindings: {
376
+ kv: {
377
+ CACHE: 'cache-kv-id'
378
+ },
379
+ d1: {
380
+ DB: 'db-id'
381
+ },
382
+ r2: {
383
+ FILES: 'files-bucket'
384
+ },
385
+ durableObjects: {
386
+ COUNTER: 'Counter'
387
+ },
388
+ queues: {
389
+ producers: {
390
+ TASK_QUEUE: 'task-queue'
391
+ },
392
+ consumers: [
393
+ {
394
+ queue: 'task-queue',
395
+ maxBatchSize: 10,
396
+ maxRetries: 3
397
+ }
398
+ ]
399
+ },
400
+ browser: {
401
+ binding: 'BROWSER'
402
+ }
403
+ },
404
+ triggers: {
405
+ crons: ['0 */6 * * *']
406
+ },
407
+ vars: {
408
+ LOG_LEVEL: 'info'
409
+ }
410
+ })
411
+ ```
412
+
413
+ ---
414
+
415
+ ## `files`: conventions, discovery, and what each tool does with them
416
+
417
+ The `files` section tells Devflare where to find handlers and discovery targets.
418
+
419
+ | Key | Shape | Default convention | Meaning |
420
+ |---|---|---|---|
421
+ | `fetch` | `string | false` | `src/fetch.ts` or `src/fetch.js` | main HTTP handler |
422
+ | `queue` | `string | false` | `src/queue.ts` | queue consumer handler |
423
+ | `scheduled` | `string | false` | `src/scheduled.ts` | cron/scheduled handler |
424
+ | `email` | `string | false` | `src/email.ts` | incoming email handler |
425
+ | `durableObjects` | `string | false` | `**/do.*.{ts,js}` | DO class discovery glob |
426
+ | `entrypoints` | `string | false` | `**/ep.*.{ts,js}` | WorkerEntrypoint discovery glob |
427
+ | `workflows` | `string | false` | `**/wf.*.{ts,js}` | Workflow discovery glob |
428
+ | `routes` | `{ dir, prefix? } or false` | no implicit route dir unless configured | file-based HTTP routing |
429
+ | `transport` | `string` | `src/transport.ts` | custom encode/decode transport definitions |
430
+
431
+ ### Discovery rules
432
+
433
+ - discovery globs respect `.gitignore`
434
+ - ignored/generated directories such as `node_modules`, `dist`, and `.devflare` are excluded
435
+ - setting a handler/discovery key to `false` explicitly disables that surface
436
+
437
+ ### Test-context config resolution is more limited than `loadConfig()`
438
+
439
+ `loadConfig()` supports `.ts`, `.mts`, `.js`, and `.mjs` config filenames.
440
+
441
+ However, the current automatic nearest-config search used by `createTestContext()` only looks for:
442
+
443
+ - `devflare.config.ts`
444
+ - `devflare.config.js`
445
+
446
+ That means:
447
+
448
+ - if your project uses `devflare.config.mts` or `devflare.config.mjs`, pass the config path explicitly to `createTestContext()`
449
+ - explicit config paths in `createTestContext()` are resolved relative to the calling test file, not always relative to `process.cwd()`
450
+ - monorepos are safest when tests pass an explicit config path instead of relying on upward auto-discovery
451
+
452
+ ### `files.transport` must export a named `transport`
453
+
454
+ When `createTestContext()` loads `files.transport`, it expects a named `transport` export.
455
+
456
+ If the file exists but does not export `transport`, Devflare currently warns and falls back to testing without custom transport encoding/decoding.
457
+
458
+ ### Very important nuance: conventions are not used identically by every tool
459
+
460
+ Devflare is convention-friendly, but not every subsystem consumes conventions in the exact same way.
461
+
462
+ Current important behavior:
463
+
464
+ - local dev and `createTestContext()` can fall back to conventional handler files when present
465
+ - type generation and discovery use default globs for DOs, entrypoints, and workflows
466
+ - `compileConfig()` only writes Wrangler `main` when `files.fetch` is explicitly a string
467
+
468
+ So the safest rule is:
469
+
470
+ > If a surface matters to build/deploy output, prefer making it explicit in config even if Devflare can discover it conventionally in some other paths.
471
+
472
+ ### `files.routes` vs top-level `routes` vs `wsRoutes`
473
+
474
+ These three are different and should never be conflated.
475
+
476
+ | Key | What it does | Layer |
477
+ |---|---|---|
478
+ | `files.routes` | enables Devflare file-based HTTP route discovery inside your app | Devflare app structure |
479
+ | top-level `routes` | defines Cloudflare deployment route matching like `example.com/*` | Cloudflare deployment config |
480
+ | `wsRoutes` | defines local dev websocket upgrade forwarding to Durable Objects | Devflare dev server |
481
+
482
+ ### File-based routing example
483
+
484
+ ```ts
485
+ import { defineConfig } from 'devflare'
486
+
487
+ export default defineConfig({
488
+ name: 'my-api',
489
+ files: {
490
+ routes: {
491
+ dir: 'src/routes',
492
+ prefix: '/api'
493
+ }
494
+ }
495
+ })
496
+ ```
497
+
498
+ Typical route-tree mental model:
499
+
500
+ ```text
501
+ src/routes/
502
+ ├── index.ts
503
+ ├── users/
504
+ │ ├── index.ts
505
+ │ └── [id].ts
506
+ └── health.ts
507
+ ```
508
+
509
+ Use `files.routes` when the HTTP surface is large enough that a single `fetch.ts` would become a switch-statement graveyard.
510
+
511
+ ---
512
+
513
+ ## `bindings`: supported binding types and what they mean
514
+
515
+ Bindings are the heart of the runtime contract.
516
+
517
+ ### Binding reference
518
+
519
+ | Binding type | Shape | Compiles natively | Notes |
520
+ |---|---|---|---|
521
+ | `kv` | `Record<string, string>` | Yes | binding name → KV namespace ID |
522
+ | `d1` | `Record<string, string>` | Yes | binding name → D1 database ID |
523
+ | `r2` | `Record<string, string>` | Yes | binding name → bucket name |
524
+ | `durableObjects` | `Record<string, string | { className, scriptName? }>` | Yes | string shorthand or object form, including cross-worker refs |
525
+ | `queues.producers` | `Record<string, string>` | Yes | binding name → queue name |
526
+ | `queues.consumers` | array of consumer objects | Yes | batch size, retries, timeout, DLQ, concurrency, retry delay |
527
+ | `services` | `Record<string, { service, environment?, entrypoint? }>` or `ref().worker` | Mostly | see named-entrypoint caveat below |
528
+ | `ai` | `{ binding }` | Yes | remote-oriented service |
529
+ | `vectorize` | `Record<string, { indexName }>` | Yes | remote-oriented service |
530
+ | `hyperdrive` | `Record<string, { id }>` | Yes | PostgreSQL acceleration |
531
+ | `browser` | `{ binding }` | Yes | browser-rendering support with local shim behavior |
532
+ | `analyticsEngine` | `Record<string, { dataset }>` | Yes | Analytics Engine datasets |
533
+ | `sendEmail` | `Record<string, { destinationAddress? }>` | **Not fully** | schema exists, but support is lighter than core bindings |
534
+
535
+ ### Core storage bindings
536
+
537
+ These are straightforward binding-name maps:
538
+
539
+ ```ts
540
+ bindings: {
541
+ kv: {
542
+ CACHE: 'cache-kv-id'
543
+ },
544
+ d1: {
545
+ DB: 'db-id'
546
+ },
547
+ r2: {
548
+ FILES: 'files-bucket'
549
+ }
550
+ }
551
+ ```
552
+
553
+ ### Durable Objects
554
+
555
+ Durable Object bindings support both shorthand and object form.
556
+
557
+ Shorthand local form:
558
+
559
+ ```ts
560
+ bindings: {
561
+ durableObjects: {
562
+ COUNTER: 'Counter'
563
+ }
564
+ }
565
+ ```
566
+
567
+ Explicit form:
568
+
569
+ ```ts
570
+ bindings: {
571
+ durableObjects: {
572
+ COUNTER: {
573
+ className: 'Counter'
574
+ }
575
+ }
576
+ }
577
+ ```
578
+
579
+ Cross-worker form uses `scriptName` or a `ref()`-produced DO binding.
580
+
581
+ ### Queues
582
+
583
+ Producers and consumers are separated intentionally.
584
+
585
+ ```ts
586
+ bindings: {
587
+ queues: {
588
+ producers: {
589
+ TASK_QUEUE: 'task-queue'
590
+ },
591
+ consumers: [
592
+ {
593
+ queue: 'task-queue',
594
+ maxBatchSize: 10,
595
+ maxBatchTimeout: 5,
596
+ maxRetries: 3,
597
+ deadLetterQueue: 'task-queue-dlq'
598
+ }
599
+ ]
600
+ }
601
+ }
602
+ ```
603
+
604
+ Important distinction:
605
+
606
+ - `queues.producers` creates runtime bindings that your code can call
607
+ - `queues.consumers` configures delivery behavior for queue names and does not create extra env binding names by itself
608
+
609
+ ### Services and entrypoints
610
+
611
+ Service bindings can be written directly or via `ref()`.
612
+
613
+ Direct form:
614
+
615
+ ```ts
616
+ bindings: {
617
+ services: {
618
+ AUTH: {
619
+ service: 'auth-worker'
620
+ }
621
+ }
622
+ }
623
+ ```
624
+
625
+ Current compiler nuance:
626
+
627
+ - the service-binding schema and `ref().worker('EntrypointName')` surface can model named entrypoints
628
+ - the current config compiler emits `service` and optional `environment`
629
+ - it does **not** currently emit a native compiled `entrypoint` field into generated Wrangler config
630
+
631
+ So named-entrypoint service bindings should be treated as a typed/public surface with deployment-time caution.
632
+
633
+ `ref()` form:
634
+
635
+ ```ts
636
+ import { defineConfig, ref } from 'devflare'
637
+
638
+ const auth = ref(() => import('../auth/devflare.config'))
639
+
640
+ export default defineConfig({
641
+ name: 'gateway',
642
+ bindings: {
643
+ services: {
644
+ AUTH: auth.worker,
645
+ ADMIN: auth.worker('AdminEntrypoint')
646
+ }
647
+ }
648
+ })
649
+ ```
650
+
651
+ ### Browser rendering
652
+
653
+ Browser rendering is one of the clearest examples of Devflare adding meaningful orchestration on top of a local Workers runtime.
654
+
655
+ ```ts
656
+ bindings: {
657
+ browser: {
658
+ binding: 'BROWSER'
659
+ }
660
+ }
661
+ ```
662
+
663
+ In local dev, Devflare adds browser-shim orchestration so browser workflows can participate in the same system as the rest of your worker app.
664
+
665
+ ### AI and Vectorize
666
+
667
+ These services are fundamentally remote-oriented.
668
+
669
+ Devflare can type them, configure them, and help you test them intentionally, but it does **not** pretend they are fully local equivalents of KV or D1.
670
+
671
+ In the current high-level test setup, these are also the bindings that switch to Cloudflare-backed remote proxies when remote mode is enabled. They are the clearest “remote-only” part of the public testing story.
672
+
673
+ ### `sendEmail`
674
+
675
+ `sendEmail` is present in the schema and examples, with this current shape:
676
+
677
+ ```ts
678
+ bindings: {
679
+ sendEmail: {
680
+ ADMIN_EMAIL: {
681
+ destinationAddress: 'admin@example.com'
682
+ }
683
+ }
684
+ }
685
+ ```
686
+
687
+ However, this binding is currently **not wired through the compiler/types/runtime helpers as completely as core bindings like KV, D1, R2, DOs, queues, and services**. Treat it as a lighter/partial area and validate actual generated output before relying on it in deployment-critical paths.
688
+
689
+ ---
690
+
691
+ ## `vars`, `secrets`, and generated types
692
+
693
+ ### `vars`
694
+
695
+ Current Devflare schema models `vars` as:
696
+
697
+ ```ts
698
+ Record<string, string>
699
+ ```
700
+
701
+ That is stricter than the broadest possible Wrangler configuration story.
702
+
703
+ So in Devflare today:
704
+
705
+ - treat `vars` as **string-valued bindings**
706
+ - serialize more complex values yourself if needed
707
+ - do not assume arbitrary JSON-valued vars are a first-class Devflare config feature
708
+
709
+ ### `secrets`
710
+
711
+ Current Devflare schema models each secret as:
712
+
713
+ ```ts
714
+ { required?: boolean }
715
+ ```
716
+
717
+ That means the native configuration is about **declaring expected secret names and whether they are required**, not about storing the secret values themselves.
718
+
719
+ One subtle but important default: `required` defaults to `true`.
720
+
721
+ So this:
722
+
723
+ ```ts
724
+ secrets: {
725
+ API_KEY: {}
726
+ }
727
+ ```
728
+
729
+ means “`API_KEY` is a required runtime secret”, not “optional secret with no requirements”.
730
+
731
+ ### `devflare types`
732
+
733
+ `devflare types` generates `env.d.ts` and uses config/discovery information to generate:
734
+
735
+ - a global `DevflareEnv` interface
736
+ - binding members for `vars`
737
+ - binding members for `secrets`
738
+ - discovered WorkerEntrypoints
739
+ - entrypoint union types such as `Entrypoints`
740
+ - service/DO typing for cross-worker references
741
+
742
+ Recommended invocation:
743
+
744
+ ```text
745
+ bunx --bun devflare types
746
+ ```
747
+
748
+ After generation, project code can use the global `DevflareEnv` interface and typed `ref()` entrypoints.
749
+
750
+ ---
751
+
752
+ ## Environment overrides via `config.env`
753
+
754
+ Devflare’s `env` field is a **Devflare-level merge layer**.
755
+
756
+ It is not simply a verbatim mirror of raw Wrangler `[env.someName]` sections.
757
+
758
+ ### How it works
759
+
760
+ When you call `compileConfig(config, environment)` or run CLI commands with `--env <name>`, Devflare:
761
+
762
+ 1. takes the base config
763
+ 2. reads `config.env?.[name]`
764
+ 3. merges that override object into the base config with `defu`
765
+ 4. compiles the **resolved result**
766
+
767
+ That means omitted nested fields inherit from the base config.
768
+
769
+ ### Keys that can actually appear inside `env`
770
+
771
+ The current `env` schema supports overrides for these keys:
772
+
773
+ - `name`
774
+ - `compatibilityDate`
775
+ - `compatibilityFlags`
776
+ - `files`
777
+ - `bindings`
778
+ - `triggers`
779
+ - `vars`
780
+ - `secrets`
781
+ - `routes`
782
+ - `assets`
783
+ - `limits`
784
+ - `observability`
785
+ - `migrations`
786
+ - `build`
787
+ - `wrangler`
788
+
789
+ Not currently supported inside `config.env.<name>`:
790
+
791
+ - `accountId`
792
+ - `wsRoutes`
793
+ - `plugins`
794
+
795
+ So if you need environment-specific values for one of those unsupported keys, compute them in the outer `defineConfig()` function yourself instead of assuming `config.env` can hold them.
796
+
797
+ ### Merge semantics are `defu`, not “replace the base object”
798
+
799
+ Devflare currently resolves environments with:
800
+
801
+ ```ts
802
+ defu(config.env[environment], config)
803
+ ```
804
+
805
+ That means all of the following are true:
806
+
807
+ - leftmost values win when both sides define the same scalar field
808
+ - nested objects inherit omitted keys from the base config
809
+ - arrays are concatenated rather than replaced, with env-specific array items first and base items appended afterward
810
+ - `null` and `undefined` are skipped rather than used as a deletion mechanism
811
+
812
+ ### Example
813
+
814
+ ```ts
815
+ import { defineConfig } from 'devflare'
816
+
817
+ export default defineConfig({
818
+ name: 'app',
819
+ triggers: {
820
+ crons: ['0 0 * * *']
821
+ },
822
+ vars: {
823
+ API_URL: 'https://api.example.com',
824
+ DEBUG: 'false'
825
+ },
826
+ bindings: {
827
+ kv: {
828
+ CACHE: 'prod-cache-id'
829
+ }
830
+ },
831
+ env: {
832
+ dev: {
833
+ triggers: {
834
+ crons: ['*/5 * * * *']
835
+ },
836
+ vars: {
837
+ DEBUG: 'true'
838
+ },
839
+ bindings: {
840
+ kv: {
841
+ CACHE: 'dev-cache-id'
842
+ }
843
+ }
844
+ }
845
+ }
846
+ })
847
+ ```
848
+
849
+ Selecting `dev` gives you a merged result equivalent to:
850
+
851
+ - `vars.API_URL = 'https://api.example.com'`
852
+ - `vars.DEBUG = 'true'`
853
+ - `bindings.kv.CACHE = 'dev-cache-id'`
854
+ - `triggers.crons = ['*/5 * * * *', '0 0 * * *']`
855
+
856
+ That last point surprises people. If you expected the env-specific cron array to **replace** the base array, that is not how the current merge works.
857
+
858
+ ### Practical rule for arrays
859
+
860
+ If an array should be totally different per environment, do **not** rely on `config.env` to replace it.
861
+
862
+ Instead, compute the final value in `defineConfig(() => ...)` yourself and return the already-resolved array.
863
+
864
+ ### `wrangler.passthrough` is applied after compilation
865
+
866
+ Another important precedence rule: after Devflare compiles the resolved config, it applies `wrangler.passthrough` with a top-level `Object.assign()`.
867
+
868
+ That means passthrough values can overwrite fields that Devflare just compiled.
869
+
870
+ This is powerful, but it also means `wrangler.passthrough` is an escape hatch with override power, not just a harmless bag of extra settings.
871
+
872
+ ### Why this matters
873
+
874
+ Raw Wrangler environments have their own rules, and some sections in Wrangler are famously non-inheritable.
875
+
876
+ Devflare’s `config.env` is more ergonomic:
877
+
878
+ - you write only the differences
879
+ - Devflare resolves a single environment-specific config first
880
+ - then it compiles the resolved config
881
+
882
+ So the correct mental model is:
883
+
884
+ > `config.env` is a Devflare **pre-compilation override system**, not just a literal nested dump into Wrangler.
885
+
886
+ ---
887
+
888
+ ## `routes`, `assets`, `observability`, `limits`, `migrations`, and passthrough
889
+
890
+ ### Deployment `routes`
891
+
892
+ Top-level `routes` are Cloudflare deployment routes.
893
+
894
+ ```ts
895
+ routes: [
896
+ {
897
+ pattern: 'example.com/*',
898
+ zone_name: 'example.com'
899
+ }
900
+ ]
901
+ ```
902
+
903
+ These are about **which hostnames/paths invoke the worker in deployment**.
904
+
905
+ They are not related to file-based app routing.
906
+
907
+ ### `wsRoutes`
908
+
909
+ `wsRoutes` are for Devflare local development. They describe how websocket upgrade requests should be forwarded into Durable Objects during dev.
910
+
911
+ Current shape:
912
+
913
+ ```ts
914
+ wsRoutes: [
915
+ {
916
+ pattern: '/chat/api',
917
+ doNamespace: 'CHAT_ROOM',
918
+ idParam: 'roomId',
919
+ forwardPath: '/websocket'
920
+ }
921
+ ]
922
+ ```
923
+
924
+ These are **not** compiled into Wrangler config.
925
+
926
+ ### `assets`
927
+
928
+ Current native Devflare assets shape is:
929
+
930
+ ```ts
931
+ assets: {
932
+ directory: './public',
933
+ binding: 'ASSETS'
934
+ }
935
+ ```
936
+
937
+ The native modeled shape is currently focused on `directory` plus optional binding access.
938
+
939
+ If you need more advanced asset-related Wrangler fields, use `wrangler.passthrough`.
940
+
941
+ ### `observability`
942
+
943
+ Current native shape:
944
+
945
+ ```ts
946
+ observability: {
947
+ enabled: true,
948
+ head_sampling_rate: 0.1
949
+ }
950
+ ```
951
+
952
+ This compiles directly.
953
+
954
+ ### `limits`
955
+
956
+ Current native shape only models:
957
+
958
+ ```ts
959
+ limits: {
960
+ cpu_ms: 50
961
+ }
962
+ ```
963
+
964
+ If you need Wrangler/Cloudflare limit-related fields outside that native shape, use `wrangler.passthrough`.
965
+
966
+ ### `migrations`
967
+
968
+ Durable Object migrations compile directly and are required when evolving DO classes over time.
969
+
970
+ Current supported migration keys include:
971
+
972
+ - `tag`
973
+ - `new_classes`
974
+ - `renamed_classes`
975
+ - `deleted_classes`
976
+ - `new_sqlite_classes`
977
+
978
+ ### `wrangler.passthrough`
979
+
980
+ Use `wrangler.passthrough` for supported Wrangler fields that Devflare does not model natively.
981
+
982
+ Example:
983
+
984
+ ```ts
985
+ wrangler: {
986
+ passthrough: {
987
+ placement: { mode: 'smart' },
988
+ workers_dev: true
989
+ }
990
+ }
991
+ ```
992
+
993
+ The compiler merges these keys directly into the top level of the generated Wrangler config.
994
+
995
+ This is the correct escape hatch when native Devflare config is intentionally narrower than Wrangler’s full surface.
996
+
997
+ ---
998
+
999
+ ## `build` and `plugins`
1000
+
1001
+ Devflare’s schema includes:
1002
+
1003
+ - `build`
1004
+ - `plugins`
1005
+
1006
+ Current `build` shape includes:
1007
+
1008
+ - `target`
1009
+ - `minify`
1010
+ - `sourcemap`
1011
+ - `rolldownOptions`
1012
+
1013
+ However, it is important to understand the current maturity of these fields:
1014
+
1015
+ - they exist in the config schema
1016
+ - they are meaningful as project-level Devflare metadata and extension points
1017
+ - but the default CLI build/deploy path still primarily runs `bunx vite build`
1018
+ - they are **not** emitted directly by `compileConfig()` into `wrangler.jsonc`
1019
+
1020
+ So do not assume that every field in `build` or every item in `plugins` is already consumed by every CLI/build path.
1021
+
1022
+ When these fields matter, validate the actual Vite/build behavior in your project.
1023
+
1024
+ ---
1025
+
1026
+ ## `ref()`: multi-worker composition without stringly-typed glue
1027
+
1028
+ Use `ref()` when one worker depends on another worker config.
1029
+
1030
+ Supported forms include:
1031
+
1032
+ ```ts
1033
+ const auth = ref(() => import('../auth/devflare.config'))
1034
+ ```
1035
+
1036
+ ```ts
1037
+ const auth = ref('custom-auth-name', () => import('../auth/devflare.config'))
1038
+ ```
1039
+
1040
+ From a `ref()` result you can access:
1041
+
1042
+ - `.name`
1043
+ - `.config`
1044
+ - `.configPath`
1045
+ - `.worker`
1046
+ - `.worker('EntrypointName')`
1047
+ - uppercase DO binding access like `.COUNTER`
1048
+ - `.resolve()` for manual resolution
1049
+
1050
+ ### Service-binding example
1051
+
1052
+ ```ts
1053
+ import { defineConfig, ref } from 'devflare'
1054
+
1055
+ const auth = ref(() => import('../auth/devflare.config'))
1056
+
1057
+ export default defineConfig({
1058
+ name: 'gateway',
1059
+ bindings: {
1060
+ services: {
1061
+ AUTH: auth.worker,
1062
+ ADMIN: auth.worker('AdminEntrypoint')
1063
+ },
1064
+ durableObjects: {
1065
+ AUTH_CACHE: auth.AUTH_CACHE
1066
+ }
1067
+ }
1068
+ })
1069
+ ```
1070
+
1071
+ Why `ref()` matters:
1072
+
1073
+ - worker names stay centralized
1074
+ - cross-worker DO bindings stay typed
1075
+ - entrypoint names can be typed from generated `Entrypoints`
1076
+ - multi-worker systems avoid magic strings scattered everywhere
1077
+
1078
+ ---
1079
+
1080
+ ## Unified `env`
1081
+
1082
+ Importing `env` from the main package gives you a **unified environment proxy**.
1083
+
1084
+ ```ts
1085
+ import { env } from 'devflare'
1086
+ ```
1087
+
1088
+ Current resolution order is:
1089
+
1090
+ 1. request-scoped context
1091
+ 2. test context
1092
+ 3. bridge env
1093
+
1094
+ That means the same import can work across:
1095
+
1096
+ - real request handling
1097
+ - test contexts created by `devflare/test`
1098
+ - bridge-backed local flows outside the request callback itself
1099
+
1100
+ ### What `env.dispose()` is for
1101
+
1102
+ `env.dispose()` is primarily a **test cleanup hook**.
1103
+
1104
+ Typical test pattern:
1105
+
1106
+ ```ts
1107
+ import { beforeAll, afterAll, describe, expect, test } from 'bun:test'
1108
+ import { createTestContext, cf } from 'devflare/test'
1109
+ import { env } from 'devflare'
1110
+
1111
+ beforeAll(() => createTestContext())
1112
+ afterAll(() => env.dispose())
1113
+
1114
+ describe('worker', () => {
1115
+ test('GET /health', async () => {
1116
+ const response = await cf.worker.get('/health')
1117
+ expect(response.status).toBe(200)
1118
+ })
1119
+ })
1120
+ ```
1121
+
1122
+ ### When to use imported `env` vs handler parameters
1123
+
1124
+ Both are valid patterns.
1125
+
1126
+ - use handler parameters when you want explicit dependency flow
1127
+ - use `import { env } from 'devflare'` when it improves ergonomics, shared helper usage, or test symmetry
1128
+
1129
+ ---
1130
+
1131
+ ## Testing model
1132
+
1133
+ Devflare testing is one of the strongest parts of the package, but there are **two different layers** and the helpers are not all symmetric.
1134
+
1135
+ ### Main testing entrypoint
1136
+
1137
+ Use `devflare/test`.
1138
+
1139
+ The most important exports are:
1140
+
1141
+ - `createTestContext`
1142
+ - `cf`
1143
+ - `email`
1144
+ - `queue`
1145
+ - `scheduled`
1146
+ - `worker`
1147
+ - `tail`
1148
+ - `shouldSkip`
1149
+ - `isRemoteModeEnabled`
1150
+ - mock helpers such as `createMockKV`, `createMockD1`, `createMockR2`, `createMockQueue`, `createMockEnv`
1151
+ - bridge helpers such as `createBridgeTestContext` and `testEnv`
1152
+
1153
+ ### `createTestContext()` — the recommended high-level test harness
1154
+
1155
+ `createTestContext()` is the recommended default for most app and integration tests.
1156
+
1157
+ Important behavior:
1158
+
1159
+ - it resolves config relative to the **calling test file** when you pass an explicit config path
1160
+ - when you do not pass a config path, it searches upward from the caller directory for the nearest supported config file used by the test harness auto-search
1161
+ - it sets up the unified test env used by `import { env } from 'devflare'`
1162
+ - it resolves service bindings and cross-worker DO references
1163
+ - it can inject remote AI/Vectorize bindings when remote mode is enabled
1164
+ - it can fall back to conventional handler files for `fetch`, `queue`, and `scheduled` when those files exist
1165
+
1166
+ ### Handler helpers are intentionally asymmetric
1167
+
1168
+ Do **not** assume every `cf.*` helper runs handlers the same way.
1169
+
1170
+ | Helper | What it does | Waits for `waitUntil()`? | Important nuance |
1171
+ |---|---|---|---|
1172
+ | `cf.worker.fetch()` | imports the HTTP handler directly and calls default export or named `fetch` | **No** | returns as soon as the handler resolves; background work may still be pending |
1173
+ | `cf.queue.trigger()` | imports the queue handler directly and calls default export or named `queue` | **Yes** | waits for queued `waitUntil()` promises before returning the trigger result |
1174
+ | `cf.scheduled.trigger()` | imports the scheduled handler directly and calls default export or named `scheduled` | **Yes** | cron defaults to `* * * * *` if you do not provide one |
1175
+ | `email.send()` / `cf.email.send()` | posts a raw email to Miniflare’s local email endpoint | runtime-driven | this is how incoming email handlers are triggered locally |
1176
+ | `cf.tail.trigger()` | directly invokes a tail handler and waits for `waitUntil()` | **Yes, if configured** | standard `createTestContext()` currently does **not** auto-configure a tail handler path |
1177
+
1178
+ Two especially important consequences:
1179
+
1180
+ - `cf.worker.fetch()` is **not** a full “wait for all background work” helper
1181
+ - if you need to assert post-response side effects, queue/scheduled tests are more eager about draining `waitUntil()` than worker fetch tests
1182
+
1183
+ ### Email testing has two different directions
1184
+
1185
+ For email, Devflare exposes both an incoming-message trigger and outgoing-message observation.
1186
+
1187
+ - `email.send()` simulates an incoming email delivered to your worker’s `email()` handler
1188
+ - `email.onReceive()` and `email.getSentEmails()` observe outgoing emails produced by `env.EMAIL.send()`, `message.forward()`, or `message.reply()`
1189
+
1190
+ Those are related, but they are not the same action.
1191
+
1192
+ ### Remote mode inside `createTestContext()`
1193
+
1194
+ When remote mode is enabled, the current high-level test context only swaps in remote proxies for:
1195
+
1196
+ - AI
1197
+ - Vectorize
1198
+
1199
+ Other bindings remain local Miniflare-style or locally resolved bindings.
1200
+
1201
+ Also note:
1202
+
1203
+ - `config.vars` are still injected into the test env in remote mode
1204
+ - remote mode is not a blanket “make every binding remote” switch
1205
+
1206
+ ### `createBridgeTestContext()` — lower-level real Miniflare bindings
1207
+
1208
+ `createBridgeTestContext()` is the lower-level integration surface.
1209
+
1210
+ Use it when you want direct access to the real Miniflare bindings rather than the higher-level `cf.*` handler helpers.
1211
+
1212
+ It gives you:
1213
+
1214
+ - a running Miniflare instance
1215
+ - direct `env` bindings
1216
+ - `testEnv` proxy access from `devflare/test`
1217
+ - manual lifecycle control via `stop()` and `reset()`
1218
+
1219
+ Important caveat:
1220
+
1221
+ - `reset()` currently clears KV namespaces, but it does **not** fully reset every other binding type for you
1222
+ - if your test mutates D1, R2, Durable Objects, or similar resources, plan your own cleanup strategy
1223
+
1224
+ ### Transport loading in tests
1225
+
1226
+ If you rely on custom transport encoding/decoding, the test context loads `files.transport` and expects a named `transport` export.
1227
+
1228
+ If that export is missing, Devflare currently warns and continues without custom transport logic.
1229
+
1230
+ ### Tail support caveat
1231
+
1232
+ `tail` is publicly exported from `devflare/test`, but the standard `createTestContext()` currently configures the tail helper with no handler path.
1233
+
1234
+ So you should **not** assume `cf.tail.trigger()` works out of the box in the same way that `cf.worker`, `cf.queue`, or `cf.scheduled` do.
1235
+
1236
+ ### Mock/unit helpers
1237
+
1238
+ For logic tests that should not spin up the full local environment, use:
1239
+
1240
+ - `createMockTestContext`
1241
+ - `withTestContext`
1242
+ - mock binding factories such as `createMockKV` and `createMockD1`
1243
+
1244
+ ### Skip helpers and remote-only tests
1245
+
1246
+ Some services are intentionally treated as remote-only or cost-sensitive. Devflare includes skip helpers so tests do not accidentally become surprise invoice generators.
1247
+
1248
+ Important current behavior:
1249
+
1250
+ - AI and Vectorize are treated as remote-only services
1251
+ - `shouldSkip.<service>` checks remote-mode enablement when relevant, authentication, effective account selection, and usage-limit state
1252
+ - skip decisions are cached per service at module level
1253
+ - operational issues such as auth failures, timeouts, network errors, and rate limits are treated as skip conditions rather than hard test failures
1254
+
1255
+ Use:
1256
+
1257
+ - `shouldSkip`
1258
+ - `devflare/cloudflare` account usage helpers when needed
1259
+
1260
+ Treat `isRemoteModeEnabled()` as a compatibility alias, not the center of the modern test-guard story.
1261
+
1262
+ ---
1263
+
1264
+ ## Local dev, build, deploy, and generated artifacts
1265
+
1266
+ ### CLI commands
1267
+
1268
+ Current CLI commands are:
1269
+
1270
+ - `init`
1271
+ - `dev`
1272
+ - `build`
1273
+ - `deploy`
1274
+ - `types`
1275
+ - `doctor`
1276
+ - `account`
1277
+ - `ai`
1278
+ - `remote`
1279
+ - `help`
1280
+ - `version`
1281
+
1282
+ Recommended invocation style:
1283
+
1284
+ ```text
1285
+ bunx --bun devflare <command>
1286
+ ```
1287
+
1288
+ ### `dev`
1289
+
1290
+ `devflare dev` is not “just run Miniflare”.
1291
+
1292
+ It is a combined local development system that includes:
1293
+
1294
+ - Vite dev server behavior
1295
+ - Miniflare-backed Cloudflare bindings
1296
+ - bridge-backed communication between Bun/Node-side tooling and the worker runtime
1297
+ - DO-oriented local orchestration and bundling behavior
1298
+ - websocket-aware proxying for local DO workflows
1299
+ - browser-shim behavior for browser rendering
1300
+ - local migration handling when relevant resources are configured
1301
+
1302
+ Dev command flags include:
1303
+
1304
+ - `--port`
1305
+ - `--persist`
1306
+ - `--verbose`
1307
+ - `--log`
1308
+ - `--log-temp`
1309
+
1310
+ ### Config path and environment selection in CLI commands
1311
+
1312
+ For the current build and deploy flows:
1313
+
1314
+ - config is loaded from the current working directory unless you pass `--config <file>`
1315
+ - `--env <name>` selects `config.env.<name>` during Devflare compilation
1316
+ - on `deploy`, the same `--env <name>` is also forwarded to `wrangler deploy`
1317
+
1318
+ So `--env` on `deploy` affects both the Devflare-side resolution step **and** Wrangler’s deploy command.
1319
+
1320
+ ### `build`
1321
+
1322
+ `devflare build`:
1323
+
1324
+ 1. loads config
1325
+ 2. applies the selected Devflare environment override if `--env` is provided
1326
+ 3. compiles a resolved Wrangler-compatible config
1327
+ 4. writes `wrangler.jsonc`
1328
+ 5. runs `bunx vite build`
1329
+
1330
+ The build subprocess receives `DEVFLARE_BUILD=true` in its environment.
1331
+
1332
+ That means project build code can branch on “Devflare build mode” if it needs to.
1333
+
1334
+ ### `deploy`
1335
+
1336
+ `devflare deploy` performs the same config-resolution step, then builds and runs Wrangler deploy.
1337
+
1338
+ Important nuances:
1339
+
1340
+ - `--dry-run` prints the resolved compiled config and exits without deploying
1341
+ - `--env <name>` is forwarded to Wrangler after Devflare resolves the same environment locally
1342
+ - unlike `build`, the current deploy command does **not** inject `DEVFLARE_BUILD=true` into the Vite build subprocess
1343
+
1344
+ So do not assume build-time code sees identical environment flags under `build` and `deploy`.
1345
+
1346
+ ### `types`
1347
+
1348
+ `devflare types` generates `env.d.ts`, which is a key part of the typed Devflare workflow.
1349
+
1350
+ ### `doctor`
1351
+
1352
+ `devflare doctor` is a project diagnostics command.
1353
+
1354
+ It currently checks for things such as:
1355
+
1356
+ - a resolvable `devflare.config.*`
1357
+ - config validity
1358
+ - `package.json`
1359
+ - `vite` and `@cloudflare/vite-plugin` dependencies
1360
+ - a Vite config file
1361
+ - `tsconfig.json`
1362
+ - generated `wrangler.jsonc`
1363
+
1364
+ Use it as a quick environment sanity check, not as a proof that every runtime behavior is correct.
1365
+
1366
+ ### Generated artifacts
1367
+
1368
+ Treat these as generated output:
1369
+
1370
+ - project-root `wrangler.jsonc` written by build/deploy flows
1371
+ - `.devflare/` generated output used by the Vite/plugin side of the toolchain
1372
+ - `.devflare/wrangler.jsonc` generated by the Vite plugin integration
1373
+ - generated `env.d.ts`
1374
+
1375
+ Do not hand-edit generated outputs and then expect them to remain the source of truth. The source of truth is still `devflare.config.ts` plus your code files.
1376
+
1377
+ ---
1378
+
1379
+ ## Framework integration
1380
+
1381
+ ### `devflare/vite`
1382
+
1383
+ This subpath exposes:
1384
+
1385
+ - `devflarePlugin`
1386
+ - `getPluginContext`
1387
+ - `getCloudflareConfig`
1388
+ - `getDevflareConfigs`
1389
+
1390
+ Use it when you need Devflare-aware Vite integration, worker-aware transforms, or auxiliary worker configuration support.
1391
+
1392
+ ### Recommended Vite mental model
1393
+
1394
+ The Vite plugin does more than inject one helper.
1395
+
1396
+ It currently:
1397
+
1398
+ - loads and compiles `devflare.config.ts`
1399
+ - injects `__DEVFLARE_WORKER_NAME__` for `workerName`
1400
+ - writes `.devflare/wrangler.jsonc`
1401
+ - discovers Durable Object classes using `files.durableObjects` or the default DO glob
1402
+ - creates an auxiliary `${name}-do` worker config when DO classes are discovered
1403
+ - adds WebSocket proxy rules in dev mode from `wsRoutes` plus any explicit `wsProxyPatterns`
1404
+ - reloads the dev server when the Devflare config changes if config watching is enabled
1405
+
1406
+ If you want the least guesswork in `vite.config.ts`, use `getDevflareConfigs()` and pass the returned config into `@cloudflare/vite-plugin` explicitly.
1407
+
1408
+ ```ts
1409
+ import { defineConfig } from 'vite'
1410
+ import { cloudflare } from '@cloudflare/vite-plugin'
1411
+ import { devflarePlugin, getDevflareConfigs } from 'devflare/vite'
1412
+
1413
+ export default defineConfig(async () => {
1414
+ const { cloudflareConfig, auxiliaryWorkers } = await getDevflareConfigs()
1415
+
1416
+ return {
1417
+ plugins: [
1418
+ devflarePlugin(),
1419
+ cloudflare({
1420
+ config: cloudflareConfig,
1421
+ auxiliaryWorkers: auxiliaryWorkers.length ? auxiliaryWorkers : undefined
1422
+ })
1423
+ ]
1424
+ }
1425
+ })
1426
+ ```
1427
+
1428
+ ### `devflare/sveltekit`
1429
+
1430
+ This subpath exposes:
1431
+
1432
+ - `handle`
1433
+ - `createDevflarePlatform`
1434
+ - `createHandle`
1435
+ - `resetPlatform`
1436
+ - `resetConfigCache`
1437
+ - `isDevflareDev`
1438
+ - `getBridgePort`
1439
+
1440
+ Use it when SvelteKit should run against a Devflare-aware platform object and dev/runtime bridge.
1441
+
1442
+ ### Simplest SvelteKit setup
1443
+
1444
+ The simplest integration is:
1445
+
1446
+ ```ts
1447
+ export { handle } from 'devflare/sveltekit'
1448
+ ```
1449
+
1450
+ Current behavior of the prebuilt `handle`:
1451
+
1452
+ - it only activates when `NODE_ENV !== 'production'` and `DEVFLARE_DEV === 'true'`
1453
+ - it auto-loads binding hints from `devflare.config.ts` in the current working directory
1454
+ - it creates a bridge-backed `platform.env`
1455
+ - it supplies mocked `caches` and a synthetic development `cf` object for local use
1456
+
1457
+ If you need more control, use `createHandle()` or `createDevflarePlatform()` directly.
1458
+
1459
+ That is the better choice when:
1460
+
1461
+ - your project is in a monorepo
1462
+ - `process.cwd()` is not the same directory as the relevant config
1463
+ - you want explicit binding hints
1464
+ - you want a custom enable/disable condition
1465
+
1466
+ Also note:
1467
+
1468
+ - platform instances are cached by bridge URL
1469
+ - `resetPlatform()` and `resetConfigCache()` are mainly test/reset helpers, not everyday app APIs
1470
+
1471
+ ---
1472
+
1473
+ ## `devflare/cloudflare`
1474
+
1475
+ `devflare/cloudflare` is the Cloudflare-account helper module.
1476
+
1477
+ Its main export is the `account` helper object, which provides:
1478
+
1479
+ - authentication inspection
1480
+ - Wrangler-auth inspection
1481
+ - account selection and account summary helpers
1482
+ - resource listing for Workers, KV, D1, R2, Vectorize, and AI
1483
+ - service-status queries
1484
+ - usage tracking and per-service usage summaries
1485
+ - limit enforcement and test-guard helpers
1486
+ - preference helpers for default/global/workspace account selection
1487
+
1488
+ This module is especially useful when writing tooling, diagnostics, or guardrails around remote services.
1489
+
1490
+ ### Account selection precedence
1491
+
1492
+ The effective account selection order is currently:
1493
+
1494
+ 1. workspace account
1495
+ 2. global default account
1496
+ 3. primary Cloudflare account
1497
+
1498
+ ### Where account preferences are stored
1499
+
1500
+ Devflare stores account preferences in more than one place.
1501
+
1502
+ - workspace account: nearest `package.json` under `devflare.accountId`
1503
+ - global default account: local cache at `~/.devflare/preferences.json`
1504
+ - global default account sync: Cloudflare KV namespace titled `devflare-usage`
1505
+
1506
+ That means `devflare account workspace` edits project files, while `devflare account global` updates user-level state and may also sync to Cloudflare-managed storage.
1507
+
1508
+ ### Remote mode storage and precedence
1509
+
1510
+ Remote mode state is also persisted.
1511
+
1512
+ - stored remote-mode config lives at `~/.devflare/remote.json`
1513
+ - `devflare remote enable [minutes]` writes that file and clamps duration to the range `1` to `1440` minutes
1514
+ - `DEVFLARE_REMOTE=1`, `true`, or `yes` takes precedence over the stored file
1515
+
1516
+ This leads to one important practical detail:
1517
+
1518
+ - `devflare remote disable` removes the stored config file
1519
+ - it does **not** unset `DEVFLARE_REMOTE`
1520
+ - so remote mode can still remain active after “disable” if the environment variable is set
1521
+
1522
+ ### Usage guards and test skipping
1523
+
1524
+ The Cloudflare helper layer is also part of Devflare’s remote-test guardrail story.
1525
+
1526
+ Current public behavior includes:
1527
+
1528
+ - usage summaries and per-service limits
1529
+ - `account.canProceedWithTest()` and `account.shouldSkip()` helpers
1530
+ - best-effort behavior when auth/network/API issues occur
1531
+
1532
+ In practice, this means remote-sensitive tests often choose between:
1533
+
1534
+ - running normally
1535
+ - being skipped because remote mode is off
1536
+ - being skipped because the user is unauthenticated, over limits, or hitting operational API issues
1537
+
1538
+ ---
1539
+
1540
+ ## Current caveats and partial areas
1541
+
1542
+ This section exists on purpose. It is better to be accurate than accidentally over-promise.
1543
+
1544
+ ### Strong, well-defined areas
1545
+
1546
+ These are core Devflare strengths today:
1547
+
1548
+ - typed config via `defineConfig()`
1549
+ - config loading/validation/compilation
1550
+ - `vars`
1551
+ - file-based handler conventions
1552
+ - DO bindings and multi-worker references
1553
+ - queues
1554
+ - local test ergonomics
1555
+ - unified `env`
1556
+ - Vite/SvelteKit integration surfaces
1557
+ - browser binding orchestration
1558
+
1559
+ ### Important caveats
1560
+
1561
+ 1. **`secrets` are declaration-oriented, not a secret-value store**
1562
+ - Devflare currently models secret names and `required` status
1563
+ - `required` defaults to `true`
1564
+ - it does not compile secret values into `wrangler.jsonc`
1565
+ - actual secret values must come from runtime secret sources
1566
+
1567
+ 2. **`vars` are string-only in current Devflare config**
1568
+ - do not assume arbitrary JSON-valued vars are a native Devflare feature
1569
+
1570
+ 3. **`config.env` only supports a subset of top-level config keys**
1571
+ - it does not currently support environment-local `accountId`, `wsRoutes`, or `plugins`
1572
+
1573
+ 4. **`config.env` uses `defu` merge semantics**
1574
+ - nested objects inherit from the base config
1575
+ - arrays concatenate instead of replacing
1576
+ - `null` and `undefined` are skipped rather than deleting base values
1577
+
1578
+ 5. **`wrangler.passthrough` is merged last and can override compiled fields**
1579
+ - this is intentional escape-hatch power, but it means passthrough is not purely additive
1580
+
1581
+ 6. **`sendEmail` support is lighter than core bindings**
1582
+ - it exists in the schema and examples
1583
+ - but compiler/types/runtime support is not as complete as KV/D1/R2/DO/queues/services
1584
+
1585
+ 7. **named service entrypoints need caution**
1586
+ - the schema and `ref().worker('Entrypoint')` surface model named entrypoints
1587
+ - but the current config compiler writes `service` and optional `environment`, and does not currently emit a native `entrypoint` field into generated Wrangler config
1588
+ - validate generated output if named-entrypoint deployment wiring matters to your app
1589
+
1590
+ 8. **`createTestContext()` auto-discovery is narrower than `loadConfig()`**
1591
+ - the test-harness auto-search currently looks for `devflare.config.ts` and `devflare.config.js`
1592
+ - projects using `.mts` or `.mjs` should pass an explicit config path
1593
+
1594
+ 9. **test helpers do not all model background work the same way**
1595
+ - `cf.worker.fetch()` does not wait for `waitUntil()` promises
1596
+ - queue and scheduled helpers do wait for `waitUntil()`
1597
+
1598
+ 10. **`cf.tail.trigger()` is public, but standard `createTestContext()` does not auto-wire it today**
1599
+ - treat tail testing as a more manual/partial area than worker, queue, or scheduled testing
1600
+
1601
+ 11. **`createBridgeTestContext().reset()` is not a full universal reset**
1602
+ - KV is cleared
1603
+ - other binding types may still need test-specific cleanup
1604
+
1605
+ 12. **workflows are a lighter area than fetch/queue/DO/entrypoints**
1606
+ - workflow discovery naming is modeled
1607
+ - but the broader runtime/compiler/test story is less mature than the most central surfaces
1608
+
1609
+ 13. **`build` and `plugins` are schema-level extension points, not guaranteed end-to-end behavior in every path**
1610
+ - the default build/deploy commands still center on Vite output
1611
+ - verify actual project behavior when relying on these fields
1612
+
1613
+ 14. **native config intentionally models only part of Wrangler’s total surface**
1614
+ - for unsupported or not-yet-modeled Wrangler keys, use `wrangler.passthrough`
1615
+
1616
+ This is the right way to think about Devflare:
1617
+
1618
+ > Prefer native Devflare config where it exists, use passthrough for unsupported Wrangler features, and do not confuse schema presence with complete end-to-end implementation maturity.
1619
+
1620
+ ---
1621
+
1622
+ ## Recommended project shape
1623
+
1624
+ This is a strong default layout for many Devflare apps:
1625
+
1626
+ ```text
1627
+ .
1628
+ ├── devflare.config.ts
1629
+ ├── env.d.ts
1630
+ ├── src/
1631
+ │ ├── fetch.ts
1632
+ │ ├── queue.ts
1633
+ │ ├── scheduled.ts
1634
+ │ ├── email.ts
1635
+ │ ├── transport.ts
1636
+ │ ├── routes/
1637
+ │ ├── do.counter.ts
1638
+ │ ├── ep.admin.ts
1639
+ │ └── wf.sync.ts
1640
+ └── tests/
1641
+ └── worker.test.ts
1642
+ ```
1643
+
1644
+ You will not always need every file, but this shape matches the Devflare mental model well:
1645
+
1646
+ - config is declarative
1647
+ - each runtime surface has a natural home
1648
+ - tests target named surfaces
1649
+ - multi-worker concerns can be expressed explicitly with `ref()`
1650
+
1651
+ ---
1652
+
1653
+ ## Guidance for LLMs generating Devflare code
1654
+
1655
+ When generating Devflare code, follow these rules.
1656
+
1657
+ 1. **Prefer `defineConfig()`**
1658
+ - do not invent ad hoc config shapes when a normal Devflare config is appropriate
1659
+
1660
+ 2. **Prefer separation of concerns**
1661
+ - do not merge fetch, queue, scheduled, email, DOs, routes, and entrypoints into one file unless explicitly asked
1662
+
1663
+ 3. **Prefer explicit config for deploy-critical surfaces**
1664
+ - conventions are great, but explicit `files.fetch`, bindings, routes, and migrations are safer when deployment behavior matters
1665
+
1666
+ 4. **Use `vars` only for non-secret runtime configuration**
1667
+ - feature flags, public URLs, modes, IDs, log levels, and similar values belong here
1668
+
1669
+ 5. **Use `secrets` only to declare secret bindings**
1670
+ - actual secret values must come from runtime secret sources, not from committed config
1671
+
1672
+ 6. **Prefer `ref()` for cross-worker composition**
1673
+ - do not duplicate worker names and DO script names manually if a config reference is available
1674
+
1675
+ 7. **Prefer `devflare/test` for tests**
1676
+ - use `createTestContext()` and `cf.*` for integration-style tests
1677
+ - use mock helpers for pure unit tests
1678
+ - pass an explicit config path in monorepos or when using `.mts`/`.mjs` config files
1679
+
1680
+ 8. **Use imported `env` when it improves ergonomics**
1681
+ - especially in tests and shared helpers
1682
+
1683
+ 9. **Respect remote-only boundaries**
1684
+ - do not pretend AI or Vectorize is “just local”
1685
+
1686
+ 10. **Use `wrangler.passthrough` instead of inventing unsupported native fields**
1687
+ - if you need a Wrangler option Devflare does not model yet, passthrough is the supported escape hatch
1688
+
1689
+ 11. **Do not assume every test helper drains `waitUntil()` the same way**
1690
+ - `cf.worker.fetch()` and queue/scheduled helpers have importantly different behavior
1691
+
1692
+ ---
1693
+
1694
+ ## Minimal example
1695
+
1696
+ ```ts
1697
+ // devflare.config.ts
1698
+ import { defineConfig } from 'devflare'
1699
+
1700
+ export default defineConfig({
1701
+ name: 'hello-worker',
1702
+ files: {
1703
+ fetch: 'src/fetch.ts'
1704
+ }
1705
+ })
1706
+ ```
1707
+
1708
+ ```ts
1709
+ // src/fetch.ts
1710
+ export default async function fetch(): Promise<Response> {
1711
+ return new Response('Hello from Devflare')
1712
+ }
1713
+ ```
1714
+
1715
+ ```ts
1716
+ // tests/worker.test.ts
1717
+ import { beforeAll, afterAll, describe, expect, test } from 'bun:test'
1718
+ import { createTestContext, cf } from 'devflare/test'
1719
+ import { env } from 'devflare'
1720
+
1721
+ beforeAll(() => createTestContext())
1722
+ afterAll(() => env.dispose())
1723
+
1724
+ describe('hello-worker', () => {
1725
+ test('GET / returns text', async () => {
1726
+ const response = await cf.worker.get('/')
1727
+ expect(response.status).toBe(200)
1728
+ expect(await response.text()).toBe('Hello from Devflare')
1729
+ })
1730
+ })
1731
+ ```
1732
+
1733
+ ---
1734
+
1735
+ ## What to remember
1736
+
1737
+ If you only remember a few things, remember these:
1738
+
1739
+ 1. `defineConfig()` is the source of truth
1740
+ 2. Devflare extends Miniflare into a broader developer workflow rather than replacing Cloudflare’s runtime model
1741
+ 3. Bun env-file loading and Devflare config loading are related but not the same thing
1742
+ 4. `vars` are built into generated config and are not secrets
1743
+ 5. `secrets` declare runtime secret bindings, default to `required: true`, and still need real runtime secret sources
1744
+ 6. `config.env` is a Devflare merge layer applied before compilation, and it supports only part of the top-level config surface
1745
+ 7. `config.env` uses `defu`, so arrays concatenate and nullish values do not delete base config
1746
+ 8. `wrangler.passthrough` is merged last and can override compiled fields
1747
+ 9. `files.routes`, top-level `routes`, and `wsRoutes` are three different concepts
1748
+ 10. `createTestContext()` is caller-relative and not every test helper behaves the same way around `waitUntil()`
1749
+ 11. `devflare/test` plus unified `env` is a major part of the package’s value
1750
+ 12. use `ref()` for multi-worker composition instead of magic strings
1751
+ 13. when native config does not cover a Wrangler feature, use `wrangler.passthrough`