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 +403 -15
- package/R2.md +170 -0
- package/dist/{dev-pa8dhm20.js → dev-b9dmrj7b.js} +318 -63
- package/dist/dev-server/server.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -2
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
|
-
|
|
1532
|
+
Usage:
|
|
1303
1533
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
- `--log`
|
|
1308
|
-
- `--log-temp`
|
|
1534
|
+
```text
|
|
1535
|
+
devflare dev [--config <path>] [--debug] [--port <port>] [--persist] [--verbose] [--log] [--log-temp]
|
|
1536
|
+
```
|
|
1309
1537
|
|
|
1310
|
-
|
|
1538
|
+
Flags:
|
|
1311
1539
|
|
|
1312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. **
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
const
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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 (
|
|
1756
|
-
const browserWorker = {
|
|
1912
|
+
if (needsBrowserWorker) {
|
|
1913
|
+
const browserWorker = createWorkerConfig({
|
|
1757
1914
|
name: browserWorkerName,
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1823
|
-
const
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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;
|
|
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-
|
|
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
|
+
"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 })\"",
|