diff --git a/.changeset/nodejs-compat-default-on.md b/.changeset/nodejs-compat-default-on.md new file mode 100644 index 00000000000..c07d9cb371e --- /dev/null +++ b/.changeset/nodejs-compat-default-on.md @@ -0,0 +1,18 @@ +--- +"miniflare": minor +"wrangler": minor +--- + +Enable Node.js compatibility by default for compatibility dates of 2026-08-04 or later + +Workers with a compatibility date of `2026-08-04` or later now have Node.js compatibility enabled automatically. Previously the `nodejs_compat` flag had to be set explicitly. + +Set the `no_nodejs_compat` compatibility flag to opt out. + +```jsonc +// wrangler.json +{ + "compatibility_date": "2026-08-04", + "compatibility_flags": ["no_nodejs_compat"] +} +``` diff --git a/fixtures/nodejs-compat-default/package.json b/fixtures/nodejs-compat-default/package.json new file mode 100644 index 00000000000..030c12936eb --- /dev/null +++ b/fixtures/nodejs-compat-default/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fixture/nodejs-compat-default", + "private": true, + "scripts": { + "build": "wrangler deploy --dry-run --outdir=./dist", + "dev": "wrangler dev", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/nodejs-compat-default/src/index.ts b/fixtures/nodejs-compat-default/src/index.ts new file mode 100644 index 00000000000..fa882ce1803 --- /dev/null +++ b/fixtures/nodejs-compat-default/src/index.ts @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import { Buffer } from "node:buffer"; +// Unprefixed import: only resolvable under Node.js compat v2, which is implied +// by the compatibility date without needing the `nodejs_compat` flag. +import { Stream } from "stream"; + +export default { + async fetch(req: Request, env: unknown, ctx: ExecutionContext) { + if (new URL(req.url).pathname !== "/") { + return new Response("Not Found", { status: 404 }); + } + + // `node:` prefixed builtin + const buffer = Buffer.from("nodejs", "utf8"); + assert.strictEqual(buffer.toString("base64"), "bm9kZWpz"); + + // Unprefixed builtin (v2 only) + const stream = new Stream(); + assert.ok(stream instanceof Stream); + + // `Buffer` global (v2 only) + assert.strictEqual(typeof Buffer, "function"); + + return new Response("OK"); + }, +}; diff --git a/fixtures/nodejs-compat-default/tests/index.test.ts b/fixtures/nodejs-compat-default/tests/index.test.ts new file mode 100644 index 00000000000..de7aea61ca0 --- /dev/null +++ b/fixtures/nodejs-compat-default/tests/index.test.ts @@ -0,0 +1,22 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { describe, it } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("nodejs compat default", () => { + it("imports node.js builtins without the nodejs_compat flag when the compatibility date implies it", async ({ + expect, + }) => { + const { ip, port, stop } = await runWranglerDev( + resolve(__dirname, "../src"), + ["--port=0", "--inspector-port=0"] + ); + try { + const response = await fetch(`http://${ip}:${port}/`); + const body = await response.text(); + expect(body).toMatchInlineSnapshot(`"OK"`); + } finally { + await stop(); + } + }); +}); diff --git a/fixtures/nodejs-compat-default/tests/tsconfig.json b/fixtures/nodejs-compat-default/tests/tsconfig.json new file mode 100644 index 00000000000..be0dabdb19b --- /dev/null +++ b/fixtures/nodejs-compat-default/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/nodejs-compat-default/tsconfig.json b/fixtures/nodejs-compat-default/tsconfig.json new file mode 100644 index 00000000000..3ffb885e58f --- /dev/null +++ b/fixtures/nodejs-compat-default/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "lib": ["esnext"], + "strict": true, + "isolatedModules": true, + "noEmit": true, + "types": ["@cloudflare/workers-types/experimental", "node"], + "allowJs": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src"] +} diff --git a/fixtures/nodejs-compat-default/turbo.json b/fixtures/nodejs-compat-default/turbo.json new file mode 100644 index 00000000000..6556dcf3e5e --- /dev/null +++ b/fixtures/nodejs-compat-default/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/nodejs-compat-default/vitest.config.mts b/fixtures/nodejs-compat-default/vitest.config.mts new file mode 100644 index 00000000000..846cddc4199 --- /dev/null +++ b/fixtures/nodejs-compat-default/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/nodejs-compat-default/wrangler.jsonc b/fixtures/nodejs-compat-default/wrangler.jsonc new file mode 100644 index 00000000000..bbc22ef0dfe --- /dev/null +++ b/fixtures/nodejs-compat-default/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "name": "nodejs-compat-default", + "main": "src/index.ts", + // From 2026-08-04 onwards, `nodejs_compat` is enabled by default, so we + // intentionally do NOT set the `nodejs_compat` compatibility flag here. + "compatibility_date": "2026-08-04", +} diff --git a/packages/deploy-helpers/src/deploy/helpers/node-compat.ts b/packages/deploy-helpers/src/deploy/helpers/node-compat.ts index 805fce613d3..1168fe30e44 100644 --- a/packages/deploy-helpers/src/deploy/helpers/node-compat.ts +++ b/packages/deploy-helpers/src/deploy/helpers/node-compat.ts @@ -7,7 +7,8 @@ import type { NodeJSCompatMode } from "miniflare"; * Computes and validates the Node.js compatibility mode we are running. * * NOTES: - * - The v2 mode is configured via `nodejs_compat_v2` compat flag or via `nodejs_compat` plus a compatibility date of Sept 23rd. 2024 or later. + * - Node.js compatibility is enabled by default for a compatibility date of Aug 4th. 2026 or later (unless the `no_nodejs_compat` flag is set), or explicitly via the `nodejs_compat`/`nodejs_compat_v2` flags. + * - The v2 mode is configured via `nodejs_compat_v2` compat flag or via `nodejs_compat` (explicit or implied by the compatibility date) plus a compatibility date of Sept 23rd. 2024 or later. * - See `EnvironmentInheritable` for `noBundle`. * * @param compatibilityDateStr The compatibility date diff --git a/packages/miniflare/src/plugins/core/node-compat.ts b/packages/miniflare/src/plugins/core/node-compat.ts index ed382485a94..e870410e57a 100644 --- a/packages/miniflare/src/plugins/core/node-compat.ts +++ b/packages/miniflare/src/plugins/core/node-compat.ts @@ -12,7 +12,11 @@ export type NodeJSCompatMode = "als" | "v1" | "v2" | null; * Computes the Node.js compatibility mode we are running. * * NOTES: - * - The v2 mode is configured via `nodejs_compat_v2` compat flag or via `nodejs_compat` plus a compatibility date of Sept 23rd. 2024 or later. + * - Node.js compatibility is enabled by default for a compatibility date of Aug 4th. 2026 or later, + * unless the `no_nodejs_compat` compat flag is set. It can also be enabled explicitly via the + * `nodejs_compat` or `nodejs_compat_v2` compat flags. + * - The v2 mode is configured via `nodejs_compat_v2` compat flag or via `nodejs_compat` (whether set + * explicitly or implied by the compatibility date) plus a compatibility date of Sept 23rd. 2024 or later. * - See `EnvironmentInheritable` for `nodeCompat` and `noBundle`. * * @param compatibilityDateStr The compatibility date @@ -26,21 +30,30 @@ export function getNodeCompat( const { hasNodejsAlsFlag, hasNodejsCompatFlag, + hasNoNodejsCompatFlag, hasNodejsCompatV2Flag, hasNoNodejsCompatV2Flag, hasExperimentalNodejsCompatV2Flag, } = parseNodeCompatibilityFlags(compatibilityFlags); const nodeCompatSwitchOverDate = "2024-09-23"; + const nodeCompatDefaultOnDate = "2026-08-04"; + + // From `nodeCompatDefaultOnDate` onwards, `nodejs_compat` is enabled by default (matching + // workerd's `$compatEnableDate` for the flag) unless explicitly disabled via `no_nodejs_compat`. + const nodejsCompatEnabled = + hasNodejsCompatFlag || + (compatibilityDate >= nodeCompatDefaultOnDate && !hasNoNodejsCompatFlag); + let mode: NodeJSCompatMode = null; if ( hasNodejsCompatV2Flag || - (hasNodejsCompatFlag && + (nodejsCompatEnabled && compatibilityDate >= nodeCompatSwitchOverDate && !hasNoNodejsCompatV2Flag) ) { mode = "v2"; - } else if (hasNodejsCompatFlag) { + } else if (nodejsCompatEnabled) { mode = "v1"; } else if (hasNodejsAlsFlag) { mode = "als"; @@ -50,6 +63,7 @@ export function getNodeCompat( mode, hasNodejsAlsFlag, hasNodejsCompatFlag, + hasNoNodejsCompatFlag, hasNodejsCompatV2Flag, hasNoNodejsCompatV2Flag, hasExperimentalNodejsCompatV2Flag, @@ -60,6 +74,7 @@ function parseNodeCompatibilityFlags(compatibilityFlags: string[]) { return { hasNodejsAlsFlag: compatibilityFlags.includes("nodejs_als"), hasNodejsCompatFlag: compatibilityFlags.includes("nodejs_compat"), + hasNoNodejsCompatFlag: compatibilityFlags.includes("no_nodejs_compat"), hasNodejsCompatV2Flag: compatibilityFlags.includes("nodejs_compat_v2"), hasNoNodejsCompatV2Flag: compatibilityFlags.includes("no_nodejs_compat_v2"), hasExperimentalNodejsCompatV2Flag: compatibilityFlags.includes( diff --git a/packages/miniflare/test/plugins/core/node-compat.spec.ts b/packages/miniflare/test/plugins/core/node-compat.spec.ts new file mode 100644 index 00000000000..039768003d9 --- /dev/null +++ b/packages/miniflare/test/plugins/core/node-compat.spec.ts @@ -0,0 +1,61 @@ +import { getNodeCompat } from "miniflare"; +import { test } from "vitest"; + +test("no flags and old date => no Node.js compat", ({ expect }) => { + expect(getNodeCompat("2000-01-01", []).mode).toBe(null); +}); + +test("nodejs_compat flag with old date => v1", ({ expect }) => { + expect(getNodeCompat("2000-01-01", ["nodejs_compat"]).mode).toBe("v1"); +}); + +test("nodejs_compat flag on/after the v2 switch-over date => v2", ({ + expect, +}) => { + expect(getNodeCompat("2024-09-23", ["nodejs_compat"]).mode).toBe("v2"); +}); + +test("nodejs_compat_v2 flag => v2 regardless of date", ({ expect }) => { + expect(getNodeCompat("2000-01-01", ["nodejs_compat_v2"]).mode).toBe("v2"); +}); + +test("nodejs_als flag => als", ({ expect }) => { + expect(getNodeCompat("2000-01-01", ["nodejs_als"]).mode).toBe("als"); +}); + +test("date on/after the default-on date implies Node.js compat (v2)", ({ + expect, +}) => { + expect(getNodeCompat("2026-08-04", []).mode).toBe("v2"); +}); + +test("date before the default-on date does not imply Node.js compat", ({ + expect, +}) => { + expect(getNodeCompat("2026-08-03", []).mode).toBe(null); +}); + +test("no_nodejs_compat opts out of the date-implied default", ({ expect }) => { + expect(getNodeCompat("2026-08-04", ["no_nodejs_compat"]).mode).toBe(null); +}); + +test("no_nodejs_compat_v2 opts out of v2 but keeps date-implied v1", ({ + expect, +}) => { + expect(getNodeCompat("2026-08-04", ["no_nodejs_compat_v2"]).mode).toBe("v1"); +}); + +test("no_nodejs_compat takes precedence over no_nodejs_compat_v2", ({ + expect, +}) => { + expect( + getNodeCompat("2026-08-04", ["no_nodejs_compat", "no_nodejs_compat_v2"]) + .mode + ).toBe(null); +}); + +test("explicit nodejs_compat flag enables compat before the default-on date", ({ + expect, +}) => { + expect(getNodeCompat("2025-01-01", ["nodejs_compat"]).mode).toBe("v2"); +}); diff --git a/packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-default-compat/default-compat.spec.ts b/packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-default-compat/default-compat.spec.ts new file mode 100644 index 00000000000..55939fda878 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-default-compat/default-compat.spec.ts @@ -0,0 +1,11 @@ +import { test, vi } from "vitest"; +import { getTextResponse, WAIT_FOR_OPTIONS } from "../../../__test-utils__"; + +test("imports node.js builtins without the nodejs_compat flag when the compatibility date implies it", async ({ + expect, +}) => { + await vi.waitFor( + async () => expect(await getTextResponse()).toEqual(`"OK!"`), + WAIT_FOR_OPTIONS + ); +}); diff --git a/packages/vite-plugin-cloudflare/playground/node-compat/package.json b/packages/vite-plugin-cloudflare/playground/node-compat/package.json index 0f488fda5b1..7ac14ba6629 100644 --- a/packages/vite-plugin-cloudflare/playground/node-compat/package.json +++ b/packages/vite-plugin-cloudflare/playground/node-compat/package.json @@ -8,7 +8,7 @@ "basic:dev": "vite dev -c vite.config.worker-basic.ts", "basic:preview": "vite preview -c vite.config.worker-basic.ts", "basic:test": "vitest run -c ../vitest.config.e2e.ts worker-basic", - "build:default": "pnpm run basic:build && pnpm run cross-env:build && pnpm run crypto:build && pnpm run postgres:build && pnpm run process:build && pnpm run random:build", + "build:default": "pnpm run basic:build && pnpm run cross-env:build && pnpm run crypto:build && pnpm run default-compat:build && pnpm run postgres:build && pnpm run process:build && pnpm run random:build", "check:type": "tsc --build", "cloudflare-node:build": "vite build -c vite.config.cloudflare-node.ts", "cloudflare-node:dev": "vite dev -c vite.config.cloudflare-node.ts", @@ -21,6 +21,10 @@ "crypto:dev": "vite dev -c vite.config.worker-crypto.ts", "crypto:preview": "vite preview -c vite.config.worker-crypto.ts", "crypto:test": "vitest run -c ../vitest.config.e2e.ts worker-crypto", + "default-compat:build": "vite build --app -c vite.config.worker-default-compat.ts", + "default-compat:dev": "vite dev -c vite.config.worker-default-compat.ts", + "default-compat:preview": "vite preview -c vite.config.worker-default-compat.ts", + "default-compat:test": "vitest run -c ../vitest.config.e2e.ts worker-default-compat", "debug:build": "vite build --app -c vite.config.worker-debug.ts", "debug:dev": "vite dev -c vite.config.worker-debug.ts", "debug:preview": "vite preview -c vite.config.worker-debug.ts", diff --git a/packages/vite-plugin-cloudflare/playground/node-compat/tsconfig.worker.json b/packages/vite-plugin-cloudflare/playground/node-compat/tsconfig.worker.json index d3a4c4652fa..5a28721e840 100644 --- a/packages/vite-plugin-cloudflare/playground/node-compat/tsconfig.worker.json +++ b/packages/vite-plugin-cloudflare/playground/node-compat/tsconfig.worker.json @@ -6,6 +6,7 @@ "worker-basic", "worker-cross-env", "worker-crypto", + "worker-default-compat", "worker-debug", "worker-https", "worker-postgres", diff --git a/packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-default-compat.ts b/packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-default-compat.ts new file mode 100644 index 00000000000..977bbc33d1e --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-default-compat.ts @@ -0,0 +1,15 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + outDir: "dist/worker-default-compat", + }, + plugins: [ + cloudflare({ + configPath: "./worker-default-compat/wrangler.jsonc", + inspectorPort: false, + persistState: false, + }), + ], +}); diff --git a/packages/vite-plugin-cloudflare/playground/node-compat/worker-default-compat/index.ts b/packages/vite-plugin-cloudflare/playground/node-compat/worker-default-compat/index.ts new file mode 100644 index 00000000000..d2ca289570c --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/node-compat/worker-default-compat/index.ts @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +// Unprefixed import: only resolvable under Node.js compat v2, which is implied +// by the compatibility date without needing the `nodejs_compat` flag. +import { join } from "path"; + +export default { + async fetch() { + return testNodejsCompatDefault(); + }, +} satisfies ExportedHandler; + +function testNodejsCompatDefault() { + assert(join("a", "b") === "a/b", "expected posix path joining"); + + const buffer = Buffer.of(1); + assert(buffer.toJSON().data[0] === 1, "Buffer global is broken"); + + return new Response(`"OK!"`); +} diff --git a/packages/vite-plugin-cloudflare/playground/node-compat/worker-default-compat/wrangler.jsonc b/packages/vite-plugin-cloudflare/playground/node-compat/worker-default-compat/wrangler.jsonc new file mode 100644 index 00000000000..f18259e7735 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/node-compat/worker-default-compat/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "name": "worker", + "main": "./index.ts", + // From 2026-08-04 onwards, `nodejs_compat` is enabled by default, so we + // intentionally do NOT set the `nodejs_compat` compatibility flag here. + "compatibility_date": "2026-08-04", +} diff --git a/packages/wrangler/src/__tests__/process-env-populated.test.ts b/packages/wrangler/src/__tests__/process-env-populated.test.ts index c9ae0091442..c64bb18a040 100644 --- a/packages/wrangler/src/__tests__/process-env-populated.test.ts +++ b/packages/wrangler/src/__tests__/process-env-populated.test.ts @@ -68,6 +68,26 @@ describe("isProcessEnvPopulated", () => { ).toBe(false); }); + test("nodejs_compat default-on date implies process.env population without a flag", ({ + expect, + }) => { + expect(isProcessEnvPopulated("2026-08-04")).toBe(true); + }); + + test("no_nodejs_compat opts out of date-implied process.env population", ({ + expect, + }) => { + expect(isProcessEnvPopulated("2026-08-04", ["no_nodejs_compat"])).toBe( + false + ); + }); + + test("date before nodejs_compat default-on date does not populate process.env without a flag", ({ + expect, + }) => { + expect(isProcessEnvPopulated("2026-01-01")).toBe(false); + }); + test("errors with disable and enable flags specified", ({ expect }) => { try { isProcessEnvPopulated("2024-01-01", [ diff --git a/packages/wrangler/src/process-env.ts b/packages/wrangler/src/process-env.ts index 6b139fedef0..c0474bac651 100644 --- a/packages/wrangler/src/process-env.ts +++ b/packages/wrangler/src/process-env.ts @@ -1,4 +1,5 @@ import { UserError } from "@cloudflare/workers-utils"; +import { getNodeCompat } from "miniflare"; export function isProcessEnvPopulated( compatibility_date: string | undefined, @@ -16,9 +17,19 @@ export function isProcessEnvPopulated( ); } + // Node.js compat can be enabled explicitly via a flag or implicitly by the compatibility date, + // so defer to `getNodeCompat` rather than checking for the `nodejs_compat` flag directly. + // The "als" mode (Async Local Storage only) does not count as full Node.js compat here. + const nodejsCompatMode = getNodeCompat( + compatibility_date, + compatibility_flags + ).mode; + const nodejsCompatEnabled = + nodejsCompatMode === "v1" || nodejsCompatMode === "v2"; + if ( compatibility_flags.includes("nodejs_compat_populate_process_env") && - compatibility_flags.includes("nodejs_compat") + nodejsCompatEnabled ) { return true; } @@ -28,7 +39,7 @@ export function isProcessEnvPopulated( return false; } return ( - compatibility_flags.includes("nodejs_compat") && + nodejsCompatEnabled && !!compatibility_date && compatibility_date >= "2025-04-01" ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a764c2d3f8..4823478ad40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -689,6 +689,24 @@ importers: specifier: workspace:* version: link:../../packages/wrangler + fixtures/nodejs-compat-default: + devDependencies: + '@cloudflare/workers-tsconfig': + specifier: workspace:* + version: link:../../packages/workers-tsconfig + '@cloudflare/workers-types': + specifier: catalog:default + version: 5.20260801.1 + undici: + specifier: catalog:default + version: 7.28.0 + vitest: + specifier: catalog:default + version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.1.5(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) + wrangler: + specifier: workspace:* + version: link:../../packages/wrangler + fixtures/nodejs-hybrid-app: devDependencies: '@cloudflare/workers-tsconfig':