8000 feat(cloudflare): miniflare dev server (#396) · sam-goodwin/alchemy@3d21941 · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Commit 3d21941

Browse files
authored
feat(cloudflare): miniflare dev server (#396)
1 parent 19f166e commit 3d21941

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2160
-74
lines changed

alchemy-web/docs/guides/dev.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
order: 7
3+
title: Development Mode
4+
description: Learn how to use Alchemy's development mode to run you F438 r application locally.
5+
---
6+
7+
# Development Mode (Beta)
8+
9+
Alchemy's development mode provides a powerful local development experience for Cloudflare Workers, featuring hot reloading, local resource emulation, and seamless integration with remote Cloudflare services.
10+
11+
> **Note:** Development mode is currently in beta. Some features may not work as expected.
12+
13+
## Overview
14+
15+
To run Alchemy in development mode, use the `--dev` flag when running your `alchemy.run.ts` script:
16+
17+
```bash
18+
bun run alchemy.run.ts --dev
19+
npx tsx alchemy.run.ts --dev
20+
```
21+
22+
This starts Alchemy in development mode, which will:
23+
24+
- Emulate Cloudflare Workers and associated resources locally using Miniflare
25+
- Hot reload Workers when you make changes to your code
26+
27+
### Watching Your Alchemy Configuration
28+
29+
Alchemy does not watch your `alchemy.run.ts` file for changes. To automatically apply changes to your configuration, you can the watch mode associated with your runtime environment. For example:
30+
31+
```bash
32+
# Using bun's watch mode
33+
bun run --watch alchemy.run.ts
34+
35+
# Using Node.js watch mode
36+
npx tsx --watch alchemy.run.ts
37+
```
38+
39+
Development mode is enabled automatically when the `--watch` flag is detected.
40+
41+
### Programmatic Configuration
42+
43+
You can also enable dev mode programmatically by setting the `dev` option:
44+
45+
```typescript
46+
const app = await alchemy("my-app", {
47+
dev: true
48+
});
49+
```
50+
51+
## Configuration
52+
53+
When running in dev mode, Alchemy runs your Cloudflare Workers locally using Miniflare, and will be available on a randomly selected port. You can specify the port by setting the `port` property on the `Worker` resource:
54+
55+
```typescript
56+
const worker = await Worker("my-worker", {
57+
entrypoint: "worker.ts",
58+
dev: {
59+
port: 3000
60+
}
61+
});
62+
63+
console.log(worker.url); // http://localhost:3000
64+
```
65+
66+
## Website Development
67+
68+
When using the `Website` resource in development mode, you can specify a custom development command that Alchemy will run locally:
69+
70+
```typescript
71+
const website = await Website("my-website", {
72+
dev: {
73+
command: "npm run dev",
74+
url: "http://localhost:5173",
75+
}
76+
});
77+
```
78+
79+
If no command is specified, Alchemy will automatically detect and run the appropriate dev command based on your project's package manager:
80+
81+
- **bun**: `bun vite dev`
82+
- **npm**: `npx vite dev`
83+
- **pnpm**: `pnpm vite dev`
84+
- **yarn**: `yarn vite dev`
85+
86+
### Vite Integration
87+
88+
For projects using Vite, Alchemy integrates with the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/development-testing/vite/) to provide enhanced local development capabilities. This integration enables better support for certain binding types when running locally.
89+
90+
To enable Vite integration, configure your `vite.config.ts` with the Cloudflare plugin:
91+
92+
```typescript
93+
import { cloudflare } from "@cloudflare/vite-plugin";
94+
import { defineConfig } from "vite";
95+
96+
export default defineConfig({
97+
plugins: [
98+
cloudflare({
99+
persistState: process.env.ALCHEMY_CLOUDFLARE_PERSIST_PATH
100+
? {
101+
path: process.env.ALCHEMY_CLOUDFLARE_PERSIST_PATH,
102+
}
103+
: undefined,
104+
}),
105+
],
106+
});
107+
```
108+
109+
The Vite integration provides improved support for the following binding types (marked with ✅ in the "Vite" column of the supported resources table below).
110+
111+
## Bindings
112+
113+
By default, Alchemy emulates resources such as [D1 Databases](../providers/cloudflare/d1-database.md), [KV Namespaces](../providers/cloudflare/kv-namespace.md), and [R2 Buckets](../providers/cloudflare/bucket.md) locally.
114+
115+
Alchemy also supports [remote bindings](https://developers.cloudflare.com/workers/development-testing/#remote-bindings) for select resources. For resources that allow either local or remote execution, you can set the `dev` property on the resource to `{ remote: true }`:
116+
117+
```typescript
118+
const db = await D1Database("my-db", {
119+
dev: { remote: true }
120+
});
121+
122+
const kv = await KVNamespace("my-kv", {
123+
dev: { remote: true }
124+
});
125+
126+
const r2 = await R2Bucket("my-r2", {
127+
dev: { remote: true }
128+
});
129+
```
130+
131+
Some resources only support remote execution, such as [AI Gateways](../providers/cloudflare/ai-gateway.md). These resources will automatically be run remotely, so usage will be billed the same as if you were running them in production.
132+
133+
### Supported Resources
134+
135+
The following bindings are supported in dev mode:
136+
137+
| Resource | Local | Remote | Vite |
138+
|----------|-------|--------|------|
139+
| AI ||||
140+
| Analytics Engine ||||
141+
| Assets ||||
142+
| Browser Rendering ||||
143+
| D1 Database ||||
144+
| Dispatch Namespace ||||
145+
| Durable Object Namespace ||||
146+
| Hyperdrive ||||
147+
| Images ||||
148+
| JSON ||||
149+
| KV Namespace ||||
150+
| Pipeline ||||
151+
| Queue ||||
152+
| R2 Bucket ||||
153+
| Secret ||||
154+
| Secret Key ||||
155+
| Service ||||
156+
| Vectorize Index ||||
157+
| Version Metadata ||||
158+
| Workflow ||||
159+
| Text ||||
160+
161+
## Limitations
162+
163+
- Hot reloading for Workers is only supported when the `entrypoint` property is set. To hot reload an inline script, you must use an external watcher to monitor your `alchemy.run.ts` file.
164+
- Local Workers can push to remote queues, but cannot consume from them.
165+
- Hyperdrive support is experimental. Hyperdrive configurations that use Cloudflare Access are not supported, and only configurations provisioned in the same `alchemy.run.ts` file will work. This is a [limitation from Cloudflare that is actively being worked on](https://developers.cloudflare.com/workers/development-testing/#unsupported-remote-bindings).
166+
167+
## Best Practices
168+
169+
1. **Use local resources for development** - Faster iteration and no API costs
170+
2. **Test with remote resources** - Validate integration before deployment
171+
3. **Leverage hot reloading** - Use entrypoint files for automatic rebuilds
172+
4. **Monitor build output** - Watch for compilation errors and warnings
173+
5. **Configure Worker ports explicitly** - Avoid conflicts in multi-worker setups
174+
6. **Use external watchers** - For automatic restarts when configuration changes

alchemy/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"bin",
2727
"lib",
2828
"src",
29-
"templates"
29+
"templates",
30+
"workers"
3031
],
3132
"exports": {
3233
".": {
@@ -189,7 +190,7 @@
189190
"fs-extra": "^11.3.0",
190191
"globby": "^14.1.0",
191192
"libsodium-wrappers": "^0.7.15",
192-
"miniflare": "^4.0.0",
193+
"miniflare": "^4.20250617.3",
193194
"nitro-cloudflare-dev": "^0.2.2",
194195
"openpgp": "^6.1.0",
195196
"picocolors": "^1.1.1",

alchemy/src/alchemy.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ function parseCliArgs(): Partial<AlchemyOptions> {
3434
options.phase = "read";
3535
}
3636

37+
if (
38+
args.includes("--dev") ||
39+
args.includes("--watch") ||
40+
process.execArgv.includes("--watch")
41+
) {
42+
options.dev = true;
43+
}
44+
3745
// Parse quiet flag
3846
if (args.includes("--quiet")) {
3947
options.quiet = true;
@@ -337,6 +345,12 @@ export interface AlchemyOptions {
337345
* @default "up"
338346
*/
339347
phase?: Phase;
348+
/**
349+
* Determines whether Alchemy will run in dev mode.
350+
*
351+
* @default - `true` if `--dev` or `--watch` is passed as a CLI argument, `false` otherwise
352+
*/
353+
dev?: boolean;
340354
/**
341355
* Name to scope the resource state under (e.g. `.alchemy/{stage}/..`).
342356
*

alchemy/src/cloudflare/bucket.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ export interface BucketProps {
8787
* Whether to adopt an existing bucket
8888
*/
8989
adopt?: boolean;
90+
91+
/**
92+
* Whether to emulate the bucket locally when Alchemy is running in watch mode.
93+
*/
94+
dev?: {
95+
/**
96+
* Whether to run the bucket remotely instead of locally
97+
* @default false
98+
*/
99+
remote?: boolean;
100+
};
90101
}
91102

92103
/**
@@ -248,6 +259,7 @@ const R2BucketResource = Resource(
248259
jurisdiction: props.jurisdiction || "default",
249260
type: "r2_bucket",
250261
accountId: api.accountId,
262+
dev: props.dev,
251263
});
252264
},
253265
);
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type esbuild from "esbuild";
2+
import type { Bindings } from "../bindings.ts";
3+
import type { WorkerProps } from "../worker.ts";
4+
import { createAliasPlugin } from "./alias-plugin.ts";
5+
import { external, external_als } from "./external.ts";
6+
import { getNodeJSCompatMode } from "./nodejs-compat-mode.ts";
7+
import { nodeJsCompatPlugin } from "./nodejs-compat.ts";
8+
import { wasmPlugin } from "./wasm-plugin.ts";
9+
10+
interface DevWorkerContext {
11+
context: esbuild.BuildContext;
12+
dispose: () => Promise<void>;
13+
}
14+
15+
declare global {
16+
var _ALCHEMY_DEV_WORKER_CONTEXTS: Map<string, DevWorkerContext> | undefined;
17+
}
18+
19+
const activeContexts = () =>
20+
(globalThis._ALCHEMY_DEV_WORKER_CONTEXTS ??= new Map());
21+
22+
/**
23+
* Creates an esbuild context for watching and hot-reloading a worker
24+
*/
25+
export async function createWorkerDevContext<B extends Bindings>(
26+
workerName: string,
27+
props: WorkerProps<B> & {
28+
entrypoint: string;
29+
compatibilityDate: string;
30+
compatibilityFlags: string[];
31+
},
32+
hooks: HotReloadHooks,
33+
) {
34+
// Clean up any existing context for this worker
35+
const existing = activeContexts().get(workerName);
36+
if (existing) {
37+
await existing.dispose();
38+
}
39+
40+
if (!props.entrypoint) {
41+
throw new Error(
42+
"A worker dev context was created, but no entry point was provided.",
43+
);
44+
}
45+
46+
const esbuild = await import("esbuild");
47+
const nodeJsCompatMode = await getNodeJSCompatMode(
48+
props.compatibilityDate,
49+
props.compatibilityFlags,
50+
);
51+
52+
const projectRoot = props.projectRoot ?? process.cwd();
53+
54+
const context = await esbuild.context({
55+
entryPoints: [props.entrypoint],
56+
format: props.format === "cjs" ? "cjs" : "esm",
57+
target: "esnext",
58+
platform: "node",
59+
minify: false,
60+
bundle: true,
61+
...props.bundle,
62+
write: false, // We want the result in memory for hot reloading
63+
conditions: ["workerd", "worker", "browser"],
64+
absWorkingDir: projectRoot,
65+
keepNames: true,
66+
loader: {
67+
".sql": "text",
68+
".json": "json",
69+
...props.bundle?.loader,
70+
},
71+
plugins: [
72+
wasmPlugin,
73+
...(props.bundle?.plugins ?? []),
74+
...(nodeJsCompatMode === "v2" ? [await nodeJsCompatPlugin()] : []),
75+
...(props.bundle?.alias
76+
? [
77+
createAliasPlugin({
78+
alias: props.bundle?.alias,
79+
projectRoot,
80+
}),
81+
]
82+
: []),
83+
hotReloadPlugin(hooks),
84+
],
85+
external: [
86+
...(nodeJsCompatMode === "als" ? external_als : external),
87+
...(props.bundle?.external ?? []),
88+
],
89+
});
90+
91+
await context.watch();
92+
93+
activeContexts().set(workerName, {
94+
context,
95+
dispose: async () => {
96+
await context.dispose();
97+
activeContexts().delete(workerName);
98+
},
99+
});
100+
}
101+
102+
interface HotReloadHooks {
103+
onBuildStart: () => void | Promise<void>;
104+
onBuildEnd: (script: string) => void | Promise<void>;
105+
onBuildError: (errors: esbuild.Message[]) => void | Promise<void>;
106+
}
107+
108+
function hotReloadPlugin(hooks: HotReloadHooks): esbuild.Plugin {
109+
return {
110+
name: "alchemy-hot-reload",
111+
setup(build) {
112+
build.onStart(hooks.onBuildStart);
113+
build.onEnd(async (result) => {
114+
if (result.errors.length > 0) {
115+
await hooks.onBuildError(result.errors);
116+
return;
117+
}
118+
119+
if (result.outputFiles && result.outputFiles.length > 0) {
120+
const newScript = result.outputFiles[0].text;
121+
await hooks.onBuildEnd(newScript);
122+
}
123+
});
124+
},
125+
};
126+
}

alchemy/src/cloudflare/bundle/bundle-worker.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ export async function bundleWorkerScript<B extends Bindings>(
3434
props.compatibilityFlags,
3535
);
3636

37-
if (nodeJsCompatMode === "v1") {
38-
throw new Error(
39-
"You must set your compatibilty date >= 2024-09-23 when using 'nodejs_compat' compatibility flag",
40-
);
41-
}
4237
const main = props.entrypoint;
4338

4439
if (props.noBundle) {

0 commit comments

Comments
 (0)
0