Skip to content

feat(cloudflare): add compatibility presets to Cloudflare Worker #526

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions alchemy-web/src/content/docs/guides/cloudflare-worker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,40 @@ const worker = await Worker("frontend", {
});
```

## Compatibility Presets

Compatibility presets provide common sets of compatibility flags to avoid having to remember which flags are needed for specific use cases:

```ts
import { Worker } from "alchemy/cloudflare";

const worker = await Worker("nodejs-worker", {
name: "nodejs-worker",
entrypoint: "./src/api.ts",
compatibility: "node", // Automatically includes nodejs_compat flag
});
```

### Available Presets

- **`"node"`**: Enables Node.js compatibility by including the `nodejs_compat` flag

### Combining with Custom Flags

Preset flags are automatically combined with any custom `compatibilityFlags` you provide:

```ts
import { Worker } from "alchemy/cloudflare";

const worker = await Worker("advanced-worker", {
name: "advanced-worker",
entrypoint: "./src/api.ts",
compatibility: "node", // Adds nodejs_compat
compatibilityFlags: ["experimental_feature"], // Additional flags
// Result: ["nodejs_compat", "experimental_feature"]
});
```

## Cron Triggers

Schedule recurring tasks:
Expand Down
34 changes: 34 additions & 0 deletions alchemy-web/src/content/docs/providers/cloudflare/worker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,40 @@ const worker = await Worker("frontend", {
});
```

## Compatibility Presets

Compatibility presets provide common sets of compatibility flags to avoid having to remember which flags are needed for specific use cases:

```ts
import { Worker } from "alchemy/cloudflare";

const worker = await Worker("nodejs-worker", {
name: "nodejs-worker",
entrypoint: "./src/api.ts",
compatibility: "node", // Automatically includes nodejs_compat flag
});
```

### Available Presets

- **`"node"`**: Enables Node.js compatibility by including the `nodejs_compat` flag

### Combining with Custom Flags

Preset flags are automatically combined with any custom `compatibilityFlags` you provide:

```ts
import { Worker } from "alchemy/cloudflare";

const worker = await Worker("advanced-worker", {
name: "advanced-worker",
entrypoint: "./src/api.ts",
compatibility: "node", // Adds nodejs_compat
compatibilityFlags: ["experimental_feature"], // Additional flags
// Result: ["nodejs_compat", "experimental_feature"]
});
```

## Cron Triggers

Schedule recurring tasks:
Expand Down
34 changes: 34 additions & 0 deletions alchemy/src/cloudflare/compatibility-presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Compatibility presets for Cloudflare Workers
*
* These presets provide common sets of compatibility flags to avoid
* users having to remember which flags they need for common use cases.
*/

/**
* Mapping of compatibility presets to their respective compatibility flags
*/
export const COMPATIBILITY_PRESETS = {
/**
* Node.js compatibility preset
* Enables Node.js APIs and runtime compatibility
*/
node: ["nodejs_compat"],
} as const;

export type CompatibilityPreset = keyof typeof COMPATIBILITY_PRESETS;

/**
* Union preset compatibility flags with user-provided flags
*/
export function unionCompatibilityFlags(
preset: CompatibilityPreset | undefined,
userFlags: string[] = []
): string[] {
if (!preset) {
return userFlags;
}

const presetFlags = COMPATIBILITY_PRESETS[preset];
return Array.from(new Set([...presetFlags, ...userFlags]));
}
26 changes: 24 additions & 2 deletions alchemy/src/cloudflare/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ import {
normalizeWorkerBundle,
} from "./bundle/index.ts";
import { wrap } from "./bundle/normalize.ts";
import {
type CompatibilityPreset,
unionCompatibilityFlags,
} from "./compatibility-presets.ts";
import { type Container, ContainerApplication } from "./container.ts";
import { CustomDomain } from "./custom-domain.ts";
import { isD1Database } from "./d1-database.ts";
Expand Down Expand Up @@ -215,6 +219,15 @@ export interface BaseWorkerProps<
*/
compatibilityFlags?: string[];

/**
* Compatibility preset to automatically include common compatibility flags
*
* - "node": Includes nodejs_compat flag for Node.js compatibility
*
* @default undefined (no preset)
*/
compatibility?: CompatibilityPreset;

/**
* Configuration for static assets
*/
Expand Down Expand Up @@ -834,7 +847,13 @@ export function Worker<const B extends Bindings>(
...(props as any),
url: true,
compatibilityFlags: Array.from(
new Set(["nodejs_compat", ...(props.compatibilityFlags ?? [])]),
new Set([
"nodejs_compat",
...unionCompatibilityFlags(
props.compatibility,
props.compatibilityFlags,
),
]),
),
entrypoint: meta!.filename,
name: workerName,
Expand Down Expand Up @@ -987,7 +1006,10 @@ export const _Worker = Resource(
const workerName = props.name ?? id;
const compatibilityDate =
props.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE;
const compatibilityFlags = props.compatibilityFlags ?? [];
const compatibilityFlags = unionCompatibilityFlags(
props.compatibility,
props.compatibilityFlags,
);
const dispatchNamespace =
typeof props.namespace === "string"
? props.namespace
Expand Down
62 changes: 62 additions & 0 deletions alchemy/test/cloudflare/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1876,4 +1876,66 @@ describe("Worker Resource", () => {
await assertWorkerDoesNotExist(api, workerName);
}
});

test("create worker with compatibility preset", async (scope) => {
const workerName = `${BRANCH_PREFIX}-test-worker-compatibility-preset`;

let worker: Worker | undefined;
try {
// Create a worker with the "node" compatibility preset
worker = await Worker(workerName, {
name: workerName,
adopt: true,
script: `
export default {
async fetch(request, env, ctx) {
return new Response('Hello from Node.js compatible worker!', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
});
}
};
`,
format: "esm",
url: true,
compatibility: "node", // Use the "node" preset
});

expect(worker.id).toBeTruthy();
expect(worker.name).toEqual(workerName);
expect(worker.url).toBeTruthy();

// Verify that the "node" preset automatically includes nodejs_compat flag
expect(worker.compatibilityFlags).toContain("nodejs_compat");

// Test that preset flags are combined with user-provided flags
worker = await Worker(workerName, {
name: workerName,
adopt: true,
script: `
import crypto from 'node:crypto';
export default {
async fetch(request, env, ctx) {
return new Response('Hello from Node.js compatible worker with additional flags!', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
});
}
};
`,
format: "esm",
url: true,
compatibility: "node",
compatibilityFlags: ["nodejs_als"], // Add valid compatibility flag in addition to preset
});

// Verify that both preset flags and user-provided flags are present
expect(worker.compatibilityFlags).toContain("nodejs_compat"); // From preset
expect(worker.compatibilityFlags).toContain("nodejs_als"); // From user
} finally {
await destroy(scope);
await assertWorkerDoesNotExist(api, workerName);
}
});
});
6 changes: 6 additions & 0 deletions vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
import { config } from "dotenv";
config({ path: ".env" });

// Polyfill File constructor for Node.js if not available
if (typeof globalThis.File === "undefined") {
const { File } = require("node:buffer");
globalThis.File = File;
}
Loading