devflare 1.0.0-next.3 → 1.0.0-next.5

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 CHANGED
@@ -520,7 +520,7 @@ Bindings are the heart of the runtime contract.
520
520
  |---|---|---|---|
521
521
  | `kv` | `Record<string, string>` | Yes | binding name → KV namespace ID |
522
522
  | `d1` | `Record<string, string>` | Yes | binding name → D1 database ID |
523
- | `r2` | `Record<string, string>` | Yes | binding name → bucket name |
523
+ | `r2` | `Record<string, string>` | Yes | binding name → bucket name, not a delivery/auth strategy |
524
524
  | `durableObjects` | `Record<string, string | { className, scriptName? }>` | Yes | string shorthand or object form, including cross-worker refs |
525
525
  | `queues.producers` | `Record<string, string>` | Yes | binding name → queue name |
526
526
  | `queues.consumers` | array of consumer objects | Yes | batch size, retries, timeout, DLQ, concurrency, retry delay |
@@ -550,6 +550,15 @@ bindings: {
550
550
  }
551
551
  ```
552
552
 
553
+ An R2 binding declaration only makes the bucket available as a runtime `env` binding.
554
+
555
+ It does **not** by itself decide:
556
+
557
+ - how uploads should be authorized
558
+ - whether objects are public or private
559
+ - which URL shape should be stored in your database
560
+ - whether files should be served through a custom domain, presigned URL, Access, WAF token auth, or a Worker
561
+
553
562
  ### Durable Objects
554
563
 
555
564
  Durable Object bindings support both shorthand and object form.
@@ -688,6 +697,199 @@ However, this binding is currently **not wired through the compiler/types/runtim
688
697
 
689
698
  ---
690
699
 
700
+ ## R2 workflow: uploads, delivery, local development, and preview mindset
701
+
702
+ For a shorter strategy guide focused specifically on Cloudflare R2 delivery patterns, see `R2.md`.
703
+
704
+ This section explains how R2 fits into the **public Devflare mental model**.
705
+
706
+ ### `bindings.r2` gives you storage access, not a complete delivery policy
707
+
708
+ When you configure:
709
+
710
+ ```ts
711
+ bindings: {
712
+ r2: {
713
+ FILES: 'files-bucket'
714
+ }
715
+ }
716
+ ```
717
+
718
+ Devflare is saying:
719
+
720
+ - your Worker should receive an `env.FILES` R2 binding
721
+ - local development can simulate that binding
722
+ - build/deploy compilation can emit the corresponding Wrangler binding declaration
723
+
724
+ Devflare is **not** automatically saying:
725
+
726
+ - uploads should come from the browser directly
727
+ - objects should be public
728
+ - objects should be private
729
+ - your app should store full URLs instead of object keys
730
+ - local dev URLs should look like production URLs
731
+
732
+ Those choices still belong to your application architecture.
733
+
734
+ ### Store object keys, not full URLs
735
+
736
+ This is the safest default rule for R2-backed applications:
737
+
738
+ - store the **object key** as the durable identifier
739
+ - optionally store metadata such as content type, size, owner, visibility, or logical folder/prefix
740
+ - derive the actual viewing or download URL per environment and per access policy
741
+
742
+ Example durable record shape:
743
+
744
+ ```ts
745
+ {
746
+ key: 'users/42/avatar/8d8b7f2a.png',
747
+ contentType: 'image/png',
748
+ visibility: 'private'
749
+ }
750
+ ```
751
+
752
+ Why this is better than storing full URLs:
753
+
754
+ - local preview URLs can change without breaking your data model
755
+ - staging and production can use different delivery strategies for the same object key
756
+ - you do not couple persistent data to `localhost`, temporary signed URLs, or a specific CDN hostname
757
+ - you can move a file from private delivery to public delivery without rewriting the canonical identifier
758
+
759
+ ### Recommended upload and serving models
760
+
761
+ These are the recommended patterns to pair with a Devflare R2 binding.
762
+
763
+ #### Direct user uploads
764
+
765
+ For normal browser uploads, the safest default is:
766
+
767
+ 1. your frontend asks your app for upload permission
768
+ 2. your Worker authenticates the user and validates file type, size, and target key
769
+ 3. your Worker returns a short-lived presigned `PUT` URL
770
+ 4. the browser uploads directly to R2
771
+ 5. your app stores the resulting object key and metadata
772
+
773
+ This keeps large uploads out of your application request path while preserving control over authorization and naming.
774
+
775
+ #### Worker-generated or server-generated content
776
+
777
+ If the file is produced by your Worker, by Workers AI, or by another trusted backend process, it is normal for the Worker itself to write directly to R2.
778
+
779
+ That is different from browser-driven uploads, where presigned URLs are usually the cleaner fit.
780
+
781
+ #### Public files
782
+
783
+ For avatars, blog images, product images, and other content that anyone may read:
784
+
785
+ - use a public bucket or public custom-domain access pattern
786
+ - prefer a custom domain for stable production URLs and caching behavior
787
+ - do not treat `r2.dev` as the production answer
788
+
789
+ #### Private or authenticated files
790
+
791
+ For invoices, receipts, paid media, tenant-scoped files, or other user-specific objects:
792
+
793
+ - keep the bucket private
794
+ - store only object keys in durable data
795
+ - serve reads through a Worker that checks session, tenant, or authorization state before reading from R2
796
+
797
+ #### Temporary direct reads
798
+
799
+ Presigned `GET` URLs are useful when you need short-lived direct access.
800
+
801
+ Important caveats:
802
+
803
+ - they are bearer tokens
804
+ - they work on the R2 S3 endpoint, not on custom domains
805
+ - they are great for temporary access, but not the right long-term answer for polished custom-domain delivery
806
+
807
+ #### Team-only or org-only buckets
808
+
809
+ If the audience is your teammates or internal users, Cloudflare Access on a custom domain is often a better fit than inventing your own mini auth protocol.
810
+
811
+ #### Custom-domain expiring links
812
+
813
+ If you want URLs like `https://cdn.example.com/files/...` with time-limited access, use:
814
+
815
+ - a Worker-controlled token/signature layer, or
816
+ - Cloudflare WAF token authentication / HMAC validation
817
+
818
+ Do **not** confuse that with R2 presigned URLs. They solve different problems.
819
+
820
+ ### Local development vs production
821
+
822
+ #### Local development
823
+
824
+ In local development, the correct default assumption is:
825
+
826
+ - your Worker code runs locally
827
+ - your R2 binding is simulated locally unless you intentionally opt into remote resources
828
+
829
+ This is good. It gives you fast feedback, zero surprise cloud writes, and predictable local iteration.
830
+
831
+ When you run `devflare dev --persist`, local Miniflare-backed state is persisted under `.devflare/data/`, including R2 object state under `.devflare/data/r2/`.
832
+
833
+ #### Remote bindings change the risk profile
834
+
835
+ If you connect development to remote resources, remember:
836
+
837
+ - reads and writes touch **real buckets**
838
+ - operations can incur **real cost**
839
+ - local experiments can now interact with non-local data
840
+
841
+ So the recommended pattern is:
842
+
843
+ - use local simulated R2 for normal development
844
+ - use separate dev or staging buckets when you need higher-fidelity integration testing
845
+ - avoid pointing local development at production user uploads unless you truly mean to
846
+
847
+ #### Production
848
+
849
+ Production is where you make the real delivery choice:
850
+
851
+ - public custom-domain access for public files
852
+ - Worker-gated reads for private/authenticated files
853
+ - Access for org-only content
854
+ - Worker/WAF token validation for custom-domain expiring links
855
+
856
+ That is why `R2.md` exists. It is the short operational guide for those production choices.
857
+
858
+ ### Local preview and internal route guidance
859
+
860
+ Devflare already uses internal `/_devflare/*` routes during local development for gateway and bridge behavior.
861
+
862
+ Treat those routes as **internal dev-server implementation details**, not as part of your application’s public URL contract.
863
+
864
+ Important current rule:
865
+
866
+ - do **not** assume there is a stable public `/_devflare/r2/...` contract for browser-facing local file delivery
867
+ - do **not** persist `/_devflare/*` URLs in your database
868
+ - do **not** design production delivery around the hope that local preview URLs and production URLs will be the same
869
+
870
+ If you build custom local preview tooling or if Devflare gains a future R2 inspector surface, the safest mental model is:
871
+
872
+ - it is a **dev-only convenience or inspector URL**
873
+ - it should be treated as **read-only**
874
+ - it should help you inspect objects, not redefine your production delivery architecture
875
+ - the canonical identifier should still be the object key, not the preview URL
876
+
877
+ Also remember that R2 is object storage, not a real filesystem. Prefix browsing can be useful in tooling, but those “folders” are naming conventions, not first-class directories.
878
+
879
+ ### Benefits of this model
880
+
881
+ This local-first but production-explicit R2 model has real benefits.
882
+
883
+ - **Faster local iteration:** you can develop upload and media flows against local simulated R2 without waiting on cloud round-trips
884
+ - **Safer production posture:** public, private, org-only, and expiring-link delivery can be chosen intentionally instead of collapsing into one accidental URL model
885
+ - **Better data durability:** storing keys instead of URLs keeps your database portable across local, staging, and production environments
886
+ - **Cleaner security boundaries:** auth policy stays where it belongs, in Workers, Access, or token validation, rather than being smuggled into a fragile client-side convention
887
+ - **Better debugging ergonomics:** local preview or inspector tooling can exist as a convenience layer without becoming the source of truth
888
+ - **Less environment coupling:** your app does not become dependent on `localhost`, temporary presigned URLs, or an implementation-specific dev route shape
889
+ - **Better media realism:** you can use local development for quick feedback, then use staging for production-like checks such as custom-domain caching, token auth, and large-media behavior
890
+
891
+ ---
892
+
691
893
  ## `vars`, `secrets`, and generated types
692
894
 
693
895
  ### `vars`
@@ -1285,6 +1487,34 @@ Recommended invocation style:
1285
1487
  bunx --bun devflare <command>
1286
1488
  ```
1287
1489
 
1490
+ ### Global flags
1491
+
1492
+ These flags are parsed at the CLI entrypoint level.
1493
+
1494
+ | Flag | Meaning | Where it actually matters | Notes |
1495
+ |---|---|---|---|
1496
+ | `-h`, `--help` | show help text and exit | all invocations | short-circuits immediately to the help command |
1497
+ | `-v`, `--version` | show the CLI/package version and exit | all invocations | short-circuits immediately to the version command |
1498
+ | `--config <path>` | choose a config file instead of nearest default discovery | `dev`, `build`, `deploy`, `types` | currently not consumed by `doctor`, `account`, `ai`, or `remote` |
1499
+ | `--debug` | enable debug-oriented output | `dev`, `build`, `deploy`, `types` | in `dev`, this also implies verbose logging; in `build`/`deploy`/`types`, it mainly controls stack-trace output on failure |
1500
+
1501
+ There is also a current internal/testing-oriented `--cwd` path override used by the `dev` command implementation. Treat that as an unsupported internal hook rather than part of the stable public CLI contract.
1502
+
1503
+ ### `init`
1504
+
1505
+ Usage:
1506
+
1507
+ ```text
1508
+ devflare init [name] [--template <template>]
1509
+ ```
1510
+
1511
+ Arguments and flags:
1512
+
1513
+ | Argument or flag | Meaning | Notes |
1514
+ |---|---|---|
1515
+ | `[name]` | project directory name | defaults to `my-devflare-app` |
1516
+ | `--template <template>` | choose a starter template | current built-in templates are `minimal` and `api`; default is `minimal` |
1517
+
1288
1518
  ### `dev`
1289
1519
 
1290
1520
  `devflare dev` is not “just run Miniflare”.
@@ -1299,23 +1529,30 @@ It is a combined local development system that includes:
1299
1529
  - browser-shim behavior for browser rendering
1300
1530
  - local migration handling when relevant resources are configured
1301
1531
 
1302
- Dev command flags include:
1532
+ Usage:
1303
1533
 
1304
- - `--port`
1305
- - `--persist`
1306
- - `--verbose`
1307
- - `--log`
1308
- - `--log-temp`
1534
+ ```text
1535
+ devflare dev [--config <path>] [--debug] [--port <port>] [--persist] [--verbose] [--log] [--log-temp]
1536
+ ```
1309
1537
 
1310
- ### Config path and environment selection in CLI commands
1538
+ Flags:
1311
1539
 
1312
- For the current build and deploy flows:
1540
+ | Flag | Meaning | Notes |
1541
+ |---|---|---|
1542
+ | `--config <path>` | load a specific `devflare.config.*` file | otherwise config is loaded from the current working directory |
1543
+ | `--debug` | enable debug mode | equivalent effect to setting `DEVFLARE_DEBUG=true` for the dev command and also implies verbose logging |
1544
+ | `--port <port>` | set the preferred Vite port | only matters when the current package has a local `vite.config.*` and Devflare actually starts Vite |
1545
+ | `--persist` | persist Miniflare storage across restarts | opt-in in the current CLI; persists KV, R2, D1, and Durable Object state under `.devflare/data/` |
1546
+ | `--verbose` | increase logging verbosity | debug mode also turns this on |
1547
+ | `--log` | tee log output to both terminal and a timestamped file | file name shape is `.log-YYYY-MM-DD_HH-MM-SS` in the project root |
1548
+ | `--log-temp` | tee log output to both terminal and `.log` | `.log` is overwritten each run; if both `--log` and `--log-temp` are passed, temp-log mode wins |
1313
1549
 
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`
1550
+ Important current CLI behavior:
1317
1551
 
1318
- So `--env` on `deploy` affects both the Devflare-side resolution step **and** Wrangler’s deploy command.
1552
+ - Devflare starts in **worker-only mode** unless the current package has a local `vite.config.ts`, `vite.config.js`, `vite.config.mts`, or `vite.config.mjs`
1553
+ - `--port` does not force Vite on by itself; it only affects startup if Vite is already enabled by local package structure
1554
+ - there is no public `--env` flag on the current `dev` command
1555
+ - without `--persist`, local Miniflare-backed state is ephemeral for that dev session
1319
1556
 
1320
1557
  ### `build`
1321
1558
 
@@ -1331,6 +1568,20 @@ The build subprocess receives `DEVFLARE_BUILD=true` in its environment.
1331
1568
 
1332
1569
  That means project build code can branch on “Devflare build mode” if it needs to.
1333
1570
 
1571
+ Usage:
1572
+
1573
+ ```text
1574
+ devflare build [--config <path>] [--env <name>] [--debug]
1575
+ ```
1576
+
1577
+ Flags:
1578
+
1579
+ | Flag | Meaning | Notes |
1580
+ |---|---|---|
1581
+ | `--config <path>` | load a specific config file | otherwise uses config discovery from the current working directory |
1582
+ | `--env <name>` | select `config.env.<name>` before compilation | this affects the resolved config written to generated output |
1583
+ | `--debug` | show stack traces on failure | mainly useful when config loading, compilation, or the Vite build subprocess fails |
1584
+
1334
1585
  ### `deploy`
1335
1586
 
1336
1587
  `devflare deploy` performs the same config-resolution step, then builds and runs Wrangler deploy.
@@ -1343,10 +1594,39 @@ Important nuances:
1343
1594
 
1344
1595
  So do not assume build-time code sees identical environment flags under `build` and `deploy`.
1345
1596
 
1597
+ Usage:
1598
+
1599
+ ```text
1600
+ devflare deploy [--config <path>] [--env <name>] [--dry-run] [--debug]
1601
+ ```
1602
+
1603
+ Flags:
1604
+
1605
+ | Flag | Meaning | Notes |
1606
+ |---|---|---|
1607
+ | `--config <path>` | load a specific config file | otherwise uses config discovery from the current working directory |
1608
+ | `--env <name>` | select `config.env.<name>` during Devflare compilation and forward the same env to `wrangler deploy` | this is the most important CLI env flag to understand |
1609
+ | `--dry-run` | print the resolved compiled Wrangler config and exit | current implementation exits before writing `wrangler.jsonc`, running `vite build`, or calling Wrangler |
1610
+ | `--debug` | show stack traces on failure | useful for config/build/deploy debugging |
1611
+
1346
1612
  ### `types`
1347
1613
 
1348
1614
  `devflare types` generates `env.d.ts`, which is a key part of the typed Devflare workflow.
1349
1615
 
1616
+ Usage:
1617
+
1618
+ ```text
1619
+ devflare types [--config <path>] [--output <path>] [--debug]
1620
+ ```
1621
+
1622
+ Flags:
1623
+
1624
+ | Flag | Meaning | Notes |
1625
+ |---|---|---|
1626
+ | `--config <path>` | load a specific config file | otherwise defaults to the current project’s normal config discovery |
1627
+ | `--output <path>` | choose the output path for generated types | defaults to `env.d.ts` |
1628
+ | `--debug` | show stack traces on failure | useful when discovery or generation fails |
1629
+
1350
1630
  ### `doctor`
1351
1631
 
1352
1632
  `devflare doctor` is a project diagnostics command.
@@ -1363,12 +1643,113 @@ It currently checks for things such as:
1363
1643
 
1364
1644
  Use it as a quick environment sanity check, not as a proof that every runtime behavior is correct.
1365
1645
 
1646
+ Usage:
1647
+
1648
+ ```text
1649
+ devflare doctor
1650
+ ```
1651
+
1652
+ Current flag behavior:
1653
+
1654
+ - there are no command-specific public flags documented for `doctor` right now
1655
+ - current implementation checks Vite-related files only when the current package appears to opt into Vite integration
1656
+ - do not assume `--config <path>` currently redirects `doctor` in the same way it does for `build`, `deploy`, `dev`, or `types`
1657
+
1658
+ ### `account`
1659
+
1660
+ Usage:
1661
+
1662
+ ```text
1663
+ devflare account [subcommand] [--account <accountId>]
1664
+ ```
1665
+
1666
+ Flags:
1667
+
1668
+ | Flag | Meaning | Notes |
1669
+ |---|---|---|
1670
+ | `--account <accountId>` | inspect or mutate a specific Cloudflare account instead of the primary account | most useful for `info`, `workers`, `kv`, `d1`, `r2`, `vectorize`, `limits`, and `usage`; the interactive `global` and `workspace` selectors do not depend on it |
1671
+
1672
+ Subcommands:
1673
+
1674
+ | Subcommand | Meaning | Notes |
1675
+ |---|---|---|
1676
+ | `info` | show account overview | default when no subcommand is provided |
1677
+ | `workers` | list Workers scripts | requires Cloudflare auth |
1678
+ | `kv` | list KV namespaces | requires Cloudflare auth |
1679
+ | `d1` | list D1 databases | requires Cloudflare auth |
1680
+ | `r2` | list R2 buckets | requires Cloudflare auth |
1681
+ | `vectorize` | list Vectorize indexes | requires Cloudflare auth |
1682
+ | `limits` | show current Devflare-managed usage limits | supports nested actions described below |
1683
+ | `usage` | show current usage summary | requires Cloudflare auth |
1684
+ | `global` | interactively choose the global default account | stores selection under `~/.devflare/preferences.json` and may sync to cloud KV |
1685
+ | `workspace` | interactively choose the workspace account | writes nearest `package.json` |
1686
+
1687
+ Nested `limits` actions:
1688
+
1689
+ ```text
1690
+ devflare account limits
1691
+ devflare account limits enable
1692
+ devflare account limits disable
1693
+ devflare account limits set <limit-name> <value>
1694
+ ```
1695
+
1696
+ Valid current limit names are:
1697
+
1698
+ - `ai-requests`
1699
+ - `ai-tokens`
1700
+ - `vectorize-ops`
1701
+
1702
+ ### `ai`
1703
+
1704
+ Usage:
1705
+
1706
+ ```text
1707
+ devflare ai
1708
+ ```
1709
+
1710
+ This command currently has no command-specific public flags. It prints Workers AI pricing/reference data.
1711
+
1712
+ ### `remote`
1713
+
1714
+ Usage:
1715
+
1716
+ ```text
1717
+ devflare remote [status]
1718
+ devflare remote enable [minutes]
1719
+ devflare remote disable
1720
+ ```
1721
+
1722
+ This command currently has no command-specific flags. It is driven by subcommands and an optional positional minutes value.
1723
+
1724
+ Important current behavior:
1725
+
1726
+ - `status` is the default when you run `devflare remote` with no subcommand
1727
+ - `enable [minutes]` defaults to `30` when minutes are omitted or invalid
1728
+ - CLI enablement clamps duration into the current supported range of `1` to `1440` minutes
1729
+ - `disable` removes stored CLI remote-mode state, but does **not** unset `DEVFLARE_REMOTE`
1730
+
1731
+ ### `help` and `version`
1732
+
1733
+ Usage:
1734
+
1735
+ ```text
1736
+ devflare help
1737
+ devflare version
1738
+ devflare --help
1739
+ devflare --version
1740
+ devflare -h
1741
+ devflare -v
1742
+ ```
1743
+
1744
+ The command forms and flag forms are equivalent public entrypoints.
1745
+
1366
1746
  ### Generated artifacts
1367
1747
 
1368
1748
  Treat these as generated output:
1369
1749
 
1370
1750
  - project-root `wrangler.jsonc` written by build/deploy flows
1371
1751
  - `.devflare/` generated output used by the Vite/plugin side of the toolchain
1752
+ - `.devflare/data/` local persisted Miniflare state when you run `devflare dev --persist`
1372
1753
  - `.devflare/wrangler.jsonc` generated by the Vite plugin integration
1373
1754
  - generated `env.d.ts`
1374
1755
 
@@ -1610,7 +1991,12 @@ These are core Devflare strengths today:
1610
1991
  - the default build/deploy commands still center on Vite output
1611
1992
  - verify actual project behavior when relying on these fields
1612
1993
 
1613
- 14. **native config intentionally models only part of Wrangler’s total surface**
1994
+ 14. **there is no stable public local-R2 browser URL contract today**
1995
+ - Devflare uses internal `/_devflare/*` routes for dev-server behavior
1996
+ - treat those as implementation details rather than app-facing URLs
1997
+ - if you build local preview tooling, keep object keys as the durable identifier and treat preview URLs as disposable
1998
+
1999
+ 15. **native config intentionally models only part of Wrangler’s total surface**
1614
2000
  - for unsupported or not-yet-modeled Wrangler keys, use `wrangler.passthrough`
1615
2001
 
1616
2002
  This is the right way to think about Devflare:
@@ -1748,4 +2134,6 @@ If you only remember a few things, remember these:
1748
2134
  10. `createTestContext()` is caller-relative and not every test helper behaves the same way around `waitUntil()`
1749
2135
  11. `devflare/test` plus unified `env` is a major part of the package’s value
1750
2136
  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`
2137
+ 13. store R2 object keys as the durable identifier instead of storing full delivery URLs
2138
+ 14. treat local preview or inspector URLs as dev conveniences, not as the production file-delivery contract
2139
+ 15. when native config does not cover a Wrangler feature, use `wrangler.passthrough`
package/R2.md ADDED
@@ -0,0 +1,170 @@
1
+ # R2
2
+
3
+ A short guide for handling uploads and file delivery with Cloudflare R2.
4
+
5
+ ---
6
+
7
+ ## Quick rules
8
+
9
+ - Use **presigned `PUT` URLs** for direct user uploads to R2
10
+ - Use a **public bucket on a custom domain** for truly public assets
11
+ - Use a **private bucket + Worker authorization** for authenticated/private assets
12
+ - Use **Cloudflare Access** for teammate/org-only buckets
13
+ - Use **WAF token auth / HMAC validation** or a **Worker** for expiring custom-domain media links
14
+ - Do **not** use `r2.dev` for production delivery
15
+ - If you protect a custom-domain bucket with Access or WAF, **disable `r2.dev`** or the bucket may still be reachable there
16
+
17
+ ---
18
+
19
+ ## Uploads
20
+
21
+ The usual safe upload flow is:
22
+
23
+ 1. frontend asks your app for upload permission
24
+ 2. your Worker/app authenticates the user and validates file type, size, and target key
25
+ 3. your backend returns a short-lived **presigned `PUT` URL**
26
+ 4. the browser uploads **directly to R2**
27
+ 5. your app stores the **object key + metadata**, not the presigned URL
28
+
29
+ Good practice:
30
+
31
+ - generate keys server-side, for example `users/<userId>/<uuid>.jpg`
32
+ - restrict `Content-Type` when signing uploads
33
+ - keep upload URLs short-lived
34
+ - configure bucket **CORS** if the browser uploads directly
35
+
36
+ Cloudflare docs:
37
+
38
+ - [Presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/)
39
+ - [Configure CORS](https://developers.cloudflare.com/r2/buckets/cors/)
40
+ - [Storing user generated content](https://developers.cloudflare.com/reference-architecture/diagrams/storage/storing-user-generated-content/)
41
+
42
+ ---
43
+
44
+ ## Viewing / serving files
45
+
46
+ ### Public files
47
+
48
+ For public images, media, and assets:
49
+
50
+ - use a **public bucket**
51
+ - attach a **custom domain**
52
+ - serve stable URLs from that domain
53
+ - let Cloudflare cache them
54
+
55
+ This is the best fit for avatars, product images, blog images, and other content that anyone may view.
56
+
57
+ Cloudflare docs:
58
+
59
+ - [Public buckets](https://developers.cloudflare.com/r2/buckets/public-buckets/)
60
+
61
+ ### Private or authenticated files
62
+
63
+ For invoices, receipts, private user uploads, paid content, or tenant-scoped assets:
64
+
65
+ - keep the bucket **private**
66
+ - store only the object key in your database
67
+ - serve through a **Worker** that checks session/JWT/permissions before reading from R2
68
+
69
+ This is usually the best default when access depends on the current user.
70
+
71
+ Cloudflare docs:
72
+
73
+ - [Use R2 from Workers](https://developers.cloudflare.com/r2/api/workers/workers-api-usage/)
74
+
75
+ ### Time-limited direct access
76
+
77
+ You can also mint a **presigned `GET` URL** for temporary direct viewing or download.
78
+
79
+ Important caveat:
80
+
81
+ - presigned URLs work on the **R2 S3 endpoint**
82
+ - they **do not work with custom domains**
83
+ - treat them as **bearer tokens**
84
+
85
+ So they are good for short-lived direct access, but not for polished custom-domain media delivery.
86
+
87
+ Cloudflare docs:
88
+
89
+ - [Presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/)
90
+
91
+ ### Team-only / org-only files
92
+
93
+ If access should be limited to employees or teammates, protect the R2 custom domain with **Cloudflare Access**.
94
+
95
+ Cloudflare docs:
96
+
97
+ - [Protect an R2 Bucket with Cloudflare Access](https://developers.cloudflare.com/r2/tutorials/cloudflare-access/)
98
+
99
+ ### Signed links on a custom domain
100
+
101
+ If you want expiring links on `https://cdn.example.com/...`, R2 presigned URLs are not the right tool.
102
+
103
+ Instead use:
104
+
105
+ - a **Worker** that signs and verifies access tokens, or
106
+ - **Cloudflare WAF token authentication / HMAC validation** on the custom domain
107
+
108
+ Cloudflare docs:
109
+
110
+ - [Configure token authentication](https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/)
111
+ - [HMAC validation function](https://developers.cloudflare.com/ruleset-engine/rules-language/functions/#hmac-validation)
112
+ - [Workers request signing example](https://developers.cloudflare.com/workers/examples/signing-requests/)
113
+
114
+ ---
115
+
116
+ ## Development vs production
117
+
118
+ ### Development
119
+
120
+ By default, local Worker development uses **local simulated bindings**, including local R2-style storage.
121
+
122
+ Use this for normal development.
123
+
124
+ Only connect to real remote buckets when you intentionally need integration testing, and prefer **separate dev/staging buckets** instead of production buckets.
125
+
126
+ Important remote-dev reality:
127
+
128
+ - remote bindings touch **real data**
129
+ - remote bindings incur **real costs**
130
+ - avoid pointing local development at production uploads unless absolutely necessary
131
+
132
+ Cloudflare docs:
133
+
134
+ - [Workers development & testing](https://developers.cloudflare.com/workers/development-testing/)
135
+ - [Remote bindings](https://developers.cloudflare.com/workers/development-testing/#remote-bindings)
136
+
137
+ ### Production
138
+
139
+ For production:
140
+
141
+ - use a **custom domain**, not `r2.dev`
142
+ - choose public vs private intentionally per bucket or per content class
143
+ - keep sensitive content private behind a Worker, Access, or token validation
144
+ - configure **CORS** intentionally for browser upload/download flows
145
+ - use separate **dev**, **staging**, and **prod** buckets
146
+
147
+ Optional performance feature:
148
+
149
+ - if users upload from many regions, consider **Local Uploads** for better upload performance
150
+
151
+ Cloudflare docs:
152
+
153
+ - [Public buckets](https://developers.cloudflare.com/r2/buckets/public-buckets/)
154
+ - [Local uploads](https://developers.cloudflare.com/r2/buckets/local-uploads/)
155
+
156
+ ---
157
+
158
+ ## Recommended defaults
159
+
160
+ If you need a sane default architecture:
161
+
162
+ - **public assets** → public bucket + custom domain
163
+ - **user uploads** → presigned `PUT` upload + object key stored in DB
164
+ - **private assets** → private bucket + Worker-gated reads
165
+ - **internal assets** → custom domain + Cloudflare Access
166
+ - **custom-domain expiring links** → Worker token auth or WAF HMAC validation
167
+
168
+ If you only remember one rule, remember this:
169
+
170
+ > Use **presigned URLs** for short-lived direct R2 access, but use a **Worker/custom domain auth layer** for polished private media delivery.
@@ -21,7 +21,7 @@ import { createConsola } from "consola";
21
21
  import { resolve as resolve3 } from "pathe";
22
22
 
23
23
  // src/dev-server/server.ts
24
- import { resolve as resolve2 } from "pathe";
24
+ import { dirname as dirname2, resolve as resolve2 } from "pathe";
25
25
 
26
26
  // src/bundler/do-bundler.ts
27
27
  import { resolve, dirname, relative } from "pathe";
@@ -1093,8 +1093,60 @@ async function checkRemoteBindingRequirements(config) {
1093
1093
  }
1094
1094
 
1095
1095
  // src/dev-server/server.ts
1096
- function getGatewayScript(wsRoutes = [], debug = false) {
1096
+ var DEFAULT_FETCH_ENTRY_FILES = [
1097
+ "src/fetch.ts",
1098
+ "src/fetch.js",
1099
+ "src/fetch.mts",
1100
+ "src/fetch.mjs"
1101
+ ];
1102
+ var INTERNAL_APP_SERVICE_BINDING = "__DEVFLARE_APP";
1103
+ async function resolveMainWorkerScriptPath(cwd, config) {
1104
+ if (config.files?.fetch === false) {
1105
+ return null;
1106
+ }
1107
+ const fs = await import("node:fs/promises");
1108
+ const candidates = new Set;
1109
+ if (typeof config.files?.fetch === "string" && config.files.fetch) {
1110
+ candidates.add(config.files.fetch);
1111
+ }
1112
+ for (const defaultEntry of DEFAULT_FETCH_ENTRY_FILES) {
1113
+ candidates.add(defaultEntry);
1114
+ }
1115
+ for (const candidate of candidates) {
1116
+ const absolutePath = resolve2(cwd, candidate);
1117
+ try {
1118
+ await fs.access(absolutePath);
1119
+ return absolutePath;
1120
+ } catch {
1121
+ continue;
1122
+ }
1123
+ }
1124
+ return null;
1125
+ }
1126
+ function collectWorkerWatchRoots(cwd, config, mainWorkerScriptPath) {
1127
+ const roots = new Set;
1128
+ const addFileParent = (filePath) => {
1129
+ if (typeof filePath !== "string" || !filePath) {
1130
+ return;
1131
+ }
1132
+ roots.add(dirname2(resolve2(cwd, filePath)));
1133
+ };
1134
+ if (mainWorkerScriptPath) {
1135
+ roots.add(dirname2(mainWorkerScriptPath));
1136
+ }
1137
+ addFileParent(config.files?.fetch);
1138
+ addFileParent(config.files?.queue);
1139
+ addFileParent(config.files?.scheduled);
1140
+ addFileParent(config.files?.email);
1141
+ addFileParent(config.files?.transport);
1142
+ if (config.files?.routes && typeof config.files.routes === "object") {
1143
+ roots.add(resolve2(cwd, config.files.routes.dir));
1144
+ }
1145
+ return [...roots];
1146
+ }
1147
+ function getGatewayScript(wsRoutes = [], debug = false, appServiceBindingName = null) {
1097
1148
  const wsRoutesJson = JSON.stringify(wsRoutes);
1149
+ const appServiceBindingJson = JSON.stringify(appServiceBindingName);
1098
1150
  return `
1099
1151
  // Bridge Gateway Worker — RPC Handler
1100
1152
  // Handles all binding operations via WebSocket RPC
@@ -1109,6 +1161,7 @@ const incomingStreams = new Map()
1109
1161
 
1110
1162
  // WebSocket routes configuration (injected at build time)
1111
1163
  const WS_ROUTES = ${wsRoutesJson}
1164
+ const APP_SERVICE_BINDING = ${appServiceBindingJson}
1112
1165
 
1113
1166
  export default {
1114
1167
  async fetch(request, env, ctx) {
@@ -1149,6 +1202,13 @@ export default {
1149
1202
  }), { headers: { 'Content-Type': 'application/json' } })
1150
1203
  }
1151
1204
 
1205
+ if (APP_SERVICE_BINDING) {
1206
+ const appWorker = env[APP_SERVICE_BINDING]
1207
+ if (appWorker && typeof appWorker.fetch === 'function') {
1208
+ return appWorker.fetch(request)
1209
+ }
1210
+ }
1211
+
1152
1212
  return new Response('Devflare Bridge Gateway', { status: 200 })
1153
1213
  }
1154
1214
  }
@@ -1676,15 +1736,63 @@ function createDevServer(options) {
1676
1736
  } = options;
1677
1737
  let miniflare = null;
1678
1738
  let doBundler = null;
1739
+ let workerSourceWatcher = null;
1679
1740
  let viteProcess = null;
1680
1741
  let config = null;
1681
1742
  let browserShim = null;
1682
1743
  let browserShimPort = 8788;
1744
+ let mainWorkerScriptPath = null;
1745
+ let bundledMainWorkerScriptPath = null;
1746
+ let currentDoResult = null;
1747
+ let reloadChain = Promise.resolve();
1748
+ async function bundleMainWorker() {
1749
+ if (!mainWorkerScriptPath) {
1750
+ bundledMainWorkerScriptPath = null;
1751
+ return;
1752
+ }
1753
+ if (typeof Bun === "undefined" || typeof Bun.build !== "function") {
1754
+ throw new Error("Worker-only dev mode requires the Bun runtime for main worker bundling");
1755
+ }
1756
+ const fs = await import("node:fs/promises");
1757
+ const outDir = resolve2(cwd, ".devflare", "worker-bundles");
1758
+ await fs.rm(outDir, { recursive: true, force: true });
1759
+ await fs.mkdir(outDir, { recursive: true });
1760
+ const result = await Bun.build({
1761
+ entrypoints: [mainWorkerScriptPath],
1762
+ outdir: outDir,
1763
+ target: "browser",
1764
+ format: "esm",
1765
+ minify: false,
1766
+ splitting: false,
1767
+ external: ["cloudflare:workers", "cloudflare:*"]
1768
+ });
1769
+ if (!result.success || result.outputs.length === 0) {
1770
+ const logs = result.logs.map((log) => ("message" in log) ? log.message : String(log)).join(`
1771
+ `);
1772
+ throw new Error(`Failed to bundle main worker:
1773
+ ${logs}`);
1774
+ }
1775
+ bundledMainWorkerScriptPath = result.outputs[0].path;
1776
+ logger?.debug(`Bundled main worker → ${bundledMainWorkerScriptPath}`);
1777
+ }
1683
1778
  function buildMiniflareConfig(doResult) {
1684
1779
  if (!config)
1685
1780
  throw new Error("Config not loaded");
1686
- const bindings = config.bindings ?? {};
1781
+ const loadedConfig = config;
1782
+ const bindings = loadedConfig.bindings ?? {};
1687
1783
  const persistPath = resolve2(cwd, ".devflare/data");
1784
+ const appWorkerName = loadedConfig.name;
1785
+ const shouldRunMainWorker = !enableVite && !!mainWorkerScriptPath;
1786
+ const queueProducers = (() => {
1787
+ if (!bindings.queues?.producers) {
1788
+ return;
1789
+ }
1790
+ const producers = {};
1791
+ for (const [bindingName, queueName] of Object.entries(bindings.queues.producers)) {
1792
+ producers[bindingName] = { queueName };
1793
+ }
1794
+ return producers;
1795
+ })();
1688
1796
  const sharedOptions = {
1689
1797
  port: miniflarePort,
1690
1798
  host: "127.0.0.1",
@@ -1693,19 +1801,67 @@ function createDevServer(options) {
1693
1801
  d1Persist: persist ? `${persistPath}/d1` : undefined,
1694
1802
  durableObjectsPersist: persist ? `${persistPath}/do` : undefined
1695
1803
  };
1696
- const gatewayWorker = {
1697
- name: "gateway",
1698
- modules: true,
1699
- script: getGatewayScript(config.wsRoutes, debug),
1700
- compatibilityDate: config.compatibilityDate,
1701
- compatibilityFlags: config.compatibilityFlags ?? [],
1702
- routes: ["*"],
1703
- kvNamespaces: bindings.kv ? bindings.kv : undefined,
1704
- r2Buckets: bindings.r2 ? bindings.r2 : undefined,
1705
- d1Databases: bindings.d1 ? bindings.d1 : undefined,
1706
- bindings: config.vars
1804
+ const createServiceBindings = (extraBindings = {}) => {
1805
+ const serviceBindings = {};
1806
+ if (bindings.services) {
1807
+ for (const [bindingName, serviceConfig] of Object.entries(bindings.services)) {
1808
+ serviceBindings[bindingName] = {
1809
+ name: serviceConfig.service,
1810
+ ...serviceConfig.entrypoint && { entrypoint: serviceConfig.entrypoint }
1811
+ };
1812
+ }
1813
+ }
1814
+ for (const [bindingName, target] of Object.entries(extraBindings)) {
1815
+ serviceBindings[bindingName] = target;
1816
+ }
1817
+ return Object.keys(serviceBindings).length > 0 ? serviceBindings : undefined;
1818
+ };
1819
+ const createWorkerConfig = (options2) => {
1820
+ const baseFlags = loadedConfig.compatibilityFlags ?? [];
1821
+ const compatFlags = baseFlags.includes("nodejs_compat") ? baseFlags : [...baseFlags, "nodejs_compat"];
1822
+ const workerConfig = {
1823
+ name: options2.name,
1824
+ modules: true,
1825
+ compatibilityDate: loadedConfig.compatibilityDate,
1826
+ compatibilityFlags: compatFlags,
1827
+ ...bindings.kv && { kvNamespaces: bindings.kv },
1828
+ ...bindings.r2 && { r2Buckets: bindings.r2 },
1829
+ ...bindings.d1 && { d1Databases: bindings.d1 },
1830
+ ...loadedConfig.vars && Object.keys(loadedConfig.vars).length > 0 && { bindings: loadedConfig.vars },
1831
+ ...queueProducers && { queueProducers }
1832
+ };
1833
+ if (options2.scriptPath) {
1834
+ workerConfig.scriptPath = options2.scriptPath;
1835
+ workerConfig.modulesRoot = cwd;
1836
+ workerConfig.modulesRules = [
1837
+ { type: "ESModule", include: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.mjs"] },
1838
+ { type: "CommonJS", include: ["**/*.js", "**/*.cjs"] },
1839
+ { type: "ESModule", include: ["**/*.jsx"] }
1840
+ ];
1841
+ }
1842
+ if (options2.script) {
1843
+ workerConfig.script = options2.script;
1844
+ }
1845
+ if (options2.durableObjects && Object.keys(options2.durableObjects).length > 0) {
1846
+ workerConfig.durableObjects = options2.durableObjects;
1847
+ }
1848
+ if (options2.serviceBindings && Object.keys(options2.serviceBindings).length > 0) {
1849
+ workerConfig.serviceBindings = options2.serviceBindings;
1850
+ }
1851
+ return workerConfig;
1707
1852
  };
1708
- if (!doResult || doResult.bundles.size === 0) {
1853
+ const gatewayWorker = createWorkerConfig({
1854
+ name: "gateway",
1855
+ script: getGatewayScript(loadedConfig.wsRoutes, debug, shouldRunMainWorker ? INTERNAL_APP_SERVICE_BINDING : null),
1856
+ serviceBindings: shouldRunMainWorker ? createServiceBindings({
1857
+ [INTERNAL_APP_SERVICE_BINDING]: { name: appWorkerName }
1858
+ }) : createServiceBindings()
1859
+ });
1860
+ gatewayWorker.routes = ["*"];
1861
+ const hasDurableObjectBundles = !!doResult && doResult.bundles.size > 0;
1862
+ const browserBindingName = bindings.browser?.binding;
1863
+ const needsBrowserWorker = Boolean(browserBindingName && (hasDurableObjectBundles || shouldRunMainWorker));
1864
+ if (!shouldRunMainWorker && !hasDurableObjectBundles && !needsBrowserWorker) {
1709
1865
  return {
1710
1866
  ...sharedOptions,
1711
1867
  ...gatewayWorker
@@ -1714,56 +1870,62 @@ function createDevServer(options) {
1714
1870
  const workers = [];
1715
1871
  const durableObjects = {};
1716
1872
  const browserShimUrl = `http://127.0.0.1:${browserShimPort}`;
1717
- const browserBindingName = bindings.browser?.binding;
1718
1873
  const browserWorkerName = "browser-binding";
1719
- for (const [bindingName, bundlePath] of doResult.bundles) {
1720
- const className = doResult.classes.get(bindingName);
1721
- if (!className)
1722
- continue;
1723
- const workerName = `do-${bindingName.toLowerCase()}`;
1724
- const baseFlags = config.compatibilityFlags ?? [];
1725
- const compatFlags = baseFlags.includes("nodejs_compat") ? baseFlags : [...baseFlags, "nodejs_compat"];
1726
- const workerConfig = {
1727
- name: workerName,
1728
- modules: true,
1729
- modulesRoot: cwd,
1730
- modulesRules: [
1731
- { type: "CommonJS", include: ["**/*.js", "**/*.cjs"] },
1732
- { type: "ESModule", include: ["**/*.mjs"] }
1733
- ],
1734
- scriptPath: bundlePath,
1735
- compatibilityDate: config.compatibilityDate,
1736
- compatibilityFlags: compatFlags,
1737
- durableObjects: {
1738
- [bindingName]: className
1874
+ if (shouldRunMainWorker && mainWorkerScriptPath) {
1875
+ const mainWorkerServiceBindings = createServiceBindings(browserBindingName ? {
1876
+ [browserBindingName]: { name: browserWorkerName }
1877
+ } : {});
1878
+ const mainWorkerConfig = createWorkerConfig({
1879
+ name: appWorkerName,
1880
+ scriptPath: bundledMainWorkerScriptPath ?? mainWorkerScriptPath,
1881
+ serviceBindings: mainWorkerServiceBindings
1882
+ });
1883
+ workers.push(mainWorkerConfig);
1884
+ }
1885
+ if (doResult) {
1886
+ for (const [bindingName, bundlePath] of doResult.bundles) {
1887
+ const className = doResult.classes.get(bindingName);
1888
+ if (!className)
1889
+ continue;
1890
+ const workerName = `do-${bindingName.toLowerCase()}`;
1891
+ const workerConfig = createWorkerConfig({
1892
+ name: workerName,
1893
+ scriptPath: bundlePath,
1894
+ durableObjects: {
1895
+ [bindingName]: className
1896
+ },
1897
+ serviceBindings: createServiceBindings(browserBindingName ? {
1898
+ [browserBindingName]: { name: browserWorkerName }
1899
+ } : {})
1900
+ });
1901
+ if (browserBindingName) {
1902
+ logger?.debug(`DO ${workerName} has browser service binding: ${browserBindingName} → ${browserWorkerName}`);
1739
1903
  }
1740
- };
1741
- if (browserBindingName) {
1742
- workerConfig.serviceBindings = {
1743
- ...workerConfig.serviceBindings,
1744
- [browserBindingName]: browserWorkerName
1904
+ logger?.debug(`DO ${workerName} config:`, JSON.stringify(workerConfig, null, 2));
1905
+ workers.push(workerConfig);
1906
+ durableObjects[bindingName] = {
1907
+ className,
1908
+ scriptName: workerName
1745
1909
  };
1746
- logger?.debug(`DO ${workerName} has browser service binding: ${browserBindingName} → ${browserWorkerName}`);
1747
1910
  }
1748
- logger?.debug(`DO ${workerName} config:`, JSON.stringify(workerConfig, null, 2));
1749
- workers.push(workerConfig);
1750
- durableObjects[bindingName] = {
1751
- className,
1752
- scriptName: workerName
1753
- };
1754
1911
  }
1755
- if (browserBindingName) {
1756
- const browserWorker = {
1912
+ if (needsBrowserWorker) {
1913
+ const browserWorker = createWorkerConfig({
1757
1914
  name: browserWorkerName,
1758
- modules: true,
1759
- script: getBrowserBindingScript(browserShimUrl, debug),
1760
- compatibilityDate: config.compatibilityDate,
1761
- compatibilityFlags: config.compatibilityFlags ?? []
1762
- };
1915
+ script: getBrowserBindingScript(browserShimUrl, debug)
1916
+ });
1763
1917
  workers.push(browserWorker);
1764
1918
  logger?.info(`Browser binding worker configured: ${browserBindingName} → ${browserShimUrl}`);
1765
1919
  }
1766
- gatewayWorker.durableObjects = durableObjects;
1920
+ if (Object.keys(durableObjects).length > 0) {
1921
+ gatewayWorker.durableObjects = durableObjects;
1922
+ if (shouldRunMainWorker) {
1923
+ const mainWorker = workers.find((worker) => worker.name === appWorkerName);
1924
+ if (mainWorker) {
1925
+ mainWorker.durableObjects = durableObjects;
1926
+ }
1927
+ }
1928
+ }
1767
1929
  return {
1768
1930
  ...sharedOptions,
1769
1931
  workers: [gatewayWorker, ...workers]
@@ -1817,14 +1979,93 @@ function createDevServer(options) {
1817
1979
  }
1818
1980
  }
1819
1981
  async function reloadMiniflare(doResult) {
1820
- if (!miniflare)
1982
+ currentDoResult = doResult;
1983
+ const queuedReload = reloadChain.then(async () => {
1984
+ if (!miniflare)
1985
+ return;
1986
+ const { Log, LogLevel } = await import("miniflare");
1987
+ const mfConfig = buildMiniflareConfig(currentDoResult);
1988
+ mfConfig.log = new Log(LogLevel.DEBUG);
1989
+ logger?.info("Reloading Miniflare...");
1990
+ await miniflare.setOptions(mfConfig);
1991
+ logger?.success("Miniflare reloaded");
1992
+ });
1993
+ reloadChain = queuedReload.catch(() => {});
1994
+ await queuedReload;
1995
+ }
1996
+ async function startWorkerSourceWatcher() {
1997
+ if (enableVite || !config || !mainWorkerScriptPath) {
1821
1998
  return;
1822
- const { Log, LogLevel } = await import("miniflare");
1823
- const mfConfig = buildMiniflareConfig(doResult);
1824
- mfConfig.log = new Log(LogLevel.DEBUG);
1825
- logger?.info("Reloading Miniflare with updated DOs...");
1826
- await miniflare.setOptions(mfConfig);
1827
- logger?.success("Miniflare reloaded");
1999
+ }
2000
+ const watchRoots = collectWorkerWatchRoots(cwd, config, mainWorkerScriptPath);
2001
+ if (watchRoots.length === 0) {
2002
+ return;
2003
+ }
2004
+ const chokidar = await import("chokidar");
2005
+ const isWindows = process.platform === "win32";
2006
+ const ignoredSegments = ["/node_modules/", "/.git/", "/.devflare/", "/dist/"];
2007
+ const normalizePath = (filePath) => filePath.replace(/\\/g, "/");
2008
+ const isIgnoredPath = (filePath) => {
2009
+ const normalizedPath = normalizePath(filePath);
2010
+ return ignoredSegments.some((segment) => normalizedPath.includes(segment));
2011
+ };
2012
+ let reloadTimeout = null;
2013
+ let reloadInProgress = false;
2014
+ let pendingReloadPath = null;
2015
+ const triggerReload = async (filePath) => {
2016
+ if (reloadInProgress) {
2017
+ pendingReloadPath = filePath;
2018
+ return;
2019
+ }
2020
+ reloadInProgress = true;
2021
+ try {
2022
+ logger?.info(`Worker source changed: ${filePath}`);
2023
+ await bundleMainWorker();
2024
+ await reloadMiniflare(currentDoResult);
2025
+ } catch (error) {
2026
+ logger?.error("Worker source reload failed:", error);
2027
+ } finally {
2028
+ reloadInProgress = false;
2029
+ if (pendingReloadPath) {
2030
+ const nextPath = pendingReloadPath;
2031
+ pendingReloadPath = null;
2032
+ await triggerReload(nextPath);
2033
+ }
2034
+ }
2035
+ };
2036
+ const scheduleReload = (filePath) => {
2037
+ if (reloadTimeout) {
2038
+ clearTimeout(reloadTimeout);
2039
+ }
2040
+ reloadTimeout = setTimeout(() => {
2041
+ triggerReload(filePath);
2042
+ }, 150);
2043
+ };
2044
+ workerSourceWatcher = chokidar.watch(watchRoots, {
2045
+ ignoreInitial: true,
2046
+ usePolling: isWindows,
2047
+ interval: isWindows ? 300 : undefined,
2048
+ awaitWriteFinish: {
2049
+ stabilityThreshold: 100,
2050
+ pollInterval: 50
2051
+ },
2052
+ ignored: (filePath) => isIgnoredPath(filePath)
2053
+ });
2054
+ const onFileEvent = (filePath) => {
2055
+ if (isIgnoredPath(filePath)) {
2056
+ return;
2057
+ }
2058
+ scheduleReload(filePath);
2059
+ };
2060
+ workerSourceWatcher.on("change", onFileEvent);
2061
+ workerSourceWatcher.on("add", onFileEvent);
2062
+ workerSourceWatcher.on("unlink", onFileEvent);
2063
+ workerSourceWatcher.on("ready", () => {
2064
+ logger?.info(`Worker source watcher ready (${watchRoots.length} root(s))`);
2065
+ });
2066
+ workerSourceWatcher.on("error", (error) => {
2067
+ logger?.error("Worker source watcher error:", error);
2068
+ });
1828
2069
  }
1829
2070
  async function runD1Migrations() {
1830
2071
  if (!miniflare || !config?.bindings?.d1)
@@ -1911,6 +2152,13 @@ function createDevServer(options) {
1911
2152
  logger?.info("Starting unified dev server...");
1912
2153
  config = await loadConfig({ cwd, configFile: configPath });
1913
2154
  logger?.debug("Loaded config:", config.name);
2155
+ mainWorkerScriptPath = await resolveMainWorkerScriptPath(cwd, config);
2156
+ if (!enableVite && mainWorkerScriptPath) {
2157
+ logger?.info(`Worker entry detected: ${mainWorkerScriptPath}`);
2158
+ await bundleMainWorker();
2159
+ } else if (!enableVite) {
2160
+ logger?.warn("No local fetch worker entry was found for worker-only mode");
2161
+ }
1914
2162
  const remoteCheck = await checkRemoteBindingRequirements(config);
1915
2163
  if (remoteCheck.hasRemoteBindings) {
1916
2164
  logger?.info("");
@@ -1961,9 +2209,12 @@ function createDevServer(options) {
1961
2209
  }
1962
2210
  });
1963
2211
  doResult = await doBundler.build();
2212
+ currentDoResult = doResult;
1964
2213
  await doBundler.watch();
1965
2214
  }
2215
+ currentDoResult = doResult;
1966
2216
  await startMiniflare(doResult);
2217
+ await startWorkerSourceWatcher();
1967
2218
  if (enableVite) {
1968
2219
  await startVite();
1969
2220
  } else {
@@ -1977,6 +2228,10 @@ function createDevServer(options) {
1977
2228
  await doBundler.close();
1978
2229
  doBundler = null;
1979
2230
  }
2231
+ if (workerSourceWatcher) {
2232
+ await workerSourceWatcher.close();
2233
+ workerSourceWatcher = null;
2234
+ }
1980
2235
  if (miniflare) {
1981
2236
  await miniflare.dispose();
1982
2237
  miniflare = null;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dev-server/server.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,WAAW,CAAA;AAc3D,MAAM,WAAW,gBAAgB;IAChC,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,sBAAsB;IACtB,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,6BAA6B;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,SAAS;IACzB,2BAA2B;IAC3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,0BAA0B;IAC1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,yCAAyC;IACzC,YAAY,IAAI,aAAa,GAAG,IAAI,CAAA;CACpC;AA2kBD,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CA4epE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dev-server/server.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,WAAW,CAAA;AAc3D,MAAM,WAAW,gBAAgB;IAChC,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,sBAAsB;IACtB,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,6BAA6B;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,SAAS;IACzB,2BAA2B;IAC3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,0BAA0B;IAC1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,yCAAyC;IACzC,YAAY,IAAI,aAAa,GAAG,IAAI,CAAA;CACpC;AAmqBD,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAkuBpE"}
package/dist/index.js CHANGED
@@ -216,7 +216,7 @@ async function runInit(parsed, logger, options) {
216
216
  return runInitCommand(parsed, logger, options);
217
217
  }
218
218
  async function runDev(parsed, logger, options) {
219
- const { runDevCommand } = await import("./dev-pa8dhm20.js");
219
+ const { runDevCommand } = await import("./dev-b9dmrj7b.js");
220
220
  return runDevCommand(parsed, logger, options);
221
221
  }
222
222
  async function runBuild(parsed, logger, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devflare",
3
- "version": "1.0.0-next.3",
3
+ "version": "1.0.0-next.5",
4
4
  "description": "Devflare is a developer-first toolkit for Cloudflare Workers that sits on top of Miniflare and Wrangler-compatible config",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -48,7 +48,8 @@
48
48
  "files": [
49
49
  "dist",
50
50
  "bin",
51
- "LLM.md"
51
+ "LLM.md",
52
+ "R2.md"
52
53
  ],
53
54
  "scripts": {
54
55
  "prebuild": "node -e \"require('fs').rmSync('./dist', { recursive: true, force: true })\"",