Skip to content

Commit 0b82b15

Browse files
authored
Strip redundant nodejs_compat flags (#15148)
1 parent 3b02915 commit 0b82b15

7 files changed

Lines changed: 157 additions & 24 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@cloudflare/vitest-pool-workers": patch
3+
"@cloudflare/vite-plugin": patch
4+
"miniflare": patch
5+
"wrangler": patch
6+
---
7+
8+
Ignore a `nodejs_compat` compatibility flag that the compatibility date already enables
9+
10+
workerd rejects a compatibility flag that its compatibility date enables by default, so a Worker configured with both a compatibility date of `2026-08-04` or later **and** `nodejs_compat` failed to start locally with "The compatibility flag nodejs_compat became the default as of 2026-08-04 so does not need to be specified anymore".
11+
12+
The redundant `nodejs_compat` and `nodejs_compat_v2` flags are now dropped when starting the runtime, which has no effect on the resulting Worker because the compatibility date enables both anyway. `no_nodejs_compat` and `no_nodejs_compat_v2` still switch Node.js compatibility off, and a flag specified alongside its own opt-out is left alone so that workerd still reports those as contradictory.

.changeset/nodejs-compat-default-on-date.md

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,6 @@ Detect Node.js compatibility from the compatibility date, now that `nodejs_compa
99

1010
As of compatibility date `2026-08-04`, workerd enables the `nodejs_compat` and `nodejs_compat_v2` compatibility flags by default. Previously these tools only treated Node.js compatibility as enabled when one of those flags was listed explicitly, so a Worker on a compatibility date of `2026-08-04` or later without the flag would get Node.js APIs from the runtime but no Node.js polyfills from the bundler, and `process.env` could be substituted with an empty object at build time. They now resolve these flags the same way workerd does, and honour `no_nodejs_compat` to opt out.
1111

12-
#### If you hit "does not need to be specified anymore"
13-
14-
workerd rejects a compatibility flag that its compatibility date already enables, so a Worker that sets both a compatibility date of `2026-08-04` or later **and** `nodejs_compat` now fails to start or deploy with:
15-
16-
```
17-
The compatibility flag nodejs_compat became the default as of 2026-08-04 so does not need to be specified anymore.
18-
```
19-
20-
This is expected: bumping a compatibility date is how you opt in to behaviour changes. To fix it, remove the flag — Node.js compatibility remains enabled via the compatibility date:
21-
22-
```jsonc
23-
// wrangler.json
24-
{
25-
"compatibility_date": "2026-08-04",
26-
// "compatibility_flags": ["nodejs_compat"] <-- remove this
27-
}
28-
```
29-
30-
Note that if you do not set a `compatibility_date` at all, Wrangler, the Vite plugin and the Vitest pool infer today's date on your behalf, so you can hit this without having changed your own configuration. Either remove the flag as above, or set an explicit `compatibility_date` earlier than `2026-08-04`.
31-
3212
To keep Node.js compatibility switched off on a newer compatibility date, specify both `no_nodejs_compat` and `no_nodejs_compat_v2`, since each flag has its own default.
3313

3414
`@cloudflare/vitest-pool-workers` needs `nodejs_compat_v2` for its own test runner, so it continues to override a project that opts out of it. On a compatibility date that enables the flag anyway, it now drops the opt-out rather than adding the flag back, which workerd would reject — previously this stopped such a project from running any tests at all.

packages/miniflare/src/plugins/core/index.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import path from "node:path";
1313
import tls from "node:tls";
1414
import { TextEncoder } from "node:util";
1515
import { DEFAULT_CONTAINER_EGRESS_INTERCEPTOR_IMAGE } from "@cloudflare/containers-shared";
16-
import { getTodaysCompatDate, removeDirSync } from "@cloudflare/workers-utils";
16+
import {
17+
getTodaysCompatDate,
18+
removeDirSync,
19+
stripRedundantNodejsCompatFlags,
20+
} from "@cloudflare/workers-utils";
1721
import SCRIPT_DEV_CONTROL from "worker:core/dev-control";
1822
import SCRIPT_ENTRY from "worker:core/entry";
1923
import OUTBOUND_WORKER from "worker:core/outbound";
@@ -563,18 +567,30 @@ export const CORE_PLUGIN: Plugin = {
563567
},
564568
]
565569
: (config.tailConsumers ?? []);
570+
// workerd rejects a compatibility flag that the compatibility date already
571+
// enables by default ("does not need to be specified anymore"), which
572+
// would stop the worker starting up. Strip them per service rather than on
573+
// the shared worker config: the Workflows plugin copies these flags into
574+
// its engine worker, which pairs them with an older hardcoded compatibility
575+
// date that still needs the flag.
576+
const userFlags = config.compatibilityFlags
577+
? stripRedundantNodejsCompatFlags(
578+
compatibilityDate,
579+
config.compatibilityFlags
580+
)
581+
: undefined;
566582
// Only add the flags the worker doesn't already declare. A worker that sets
567583
// e.g. `streaming_tail_worker` itself (some do) would otherwise have it
568584
// listed twice, which workerd rejects ("specified multiple times").
569-
const existingFlags = config.compatibilityFlags ?? [];
585+
const existingFlags = userFlags ?? [];
570586
const compatibilityFlags = observabilityEnabled
571587
? [
572588
...existingFlags,
573589
...OBSERVABILITY_COMPAT_FLAGS.filter(
574590
(flag) => !existingFlags.includes(flag)
575591
),
576592
]
577-
: config.compatibilityFlags;
593+
: userFlags;
578594

579595
services.push({
580596
name: serviceName,

packages/miniflare/test/index.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import os from "node:os";
1111
import path from "node:path";
1212
import { json, text } from "node:stream/consumers";
1313
import util from "node:util";
14-
import { _forceColour } from "@cloudflare/workers-utils";
14+
import {
15+
_forceColour,
16+
NODEJS_COMPAT_DEFAULT_ON_DATE,
17+
} from "@cloudflare/workers-utils";
1518
import {
1619
_transformsForContentEncodingAndContentType,
1720
DeferredPromise,
@@ -770,6 +773,36 @@ test("Miniflare: negotiates acceptable encoding", async ({ expect }) => {
770773
expect(await res.text()).toBe(testBody);
771774
});
772775

776+
test("Miniflare: ignores nodejs_compat flags the compatibility date enables", async ({
777+
expect,
778+
}) => {
779+
// workerd rejects a compatibility flag that its compatibility date already
780+
// enables, which `nodejs_compat` is as of `NODEJS_COMPAT_DEFAULT_ON_DATE`
781+
const mf = new Miniflare({
782+
workers: [
783+
{
784+
config: {
785+
type: "worker",
786+
name: "",
787+
compatibilityDate: NODEJS_COMPAT_DEFAULT_ON_DATE,
788+
compatibilityFlags: ["nodejs_compat", "nodejs_compat_v2"],
789+
manifest: singleModuleManifest(`
790+
import path from "node:path";
791+
792+
export default {
793+
fetch() { return new Response(path.join("a", "b")); },
794+
};
795+
`),
796+
},
797+
},
798+
],
799+
});
800+
useDispose(mf);
801+
802+
const res = await mf.dispatchFetch("http://placeholder");
803+
expect(await res.text()).toBe(path.posix.join("a", "b"));
804+
});
805+
773806
test("Miniflare: custom service using Set-Cookie header", async ({
774807
expect,
775808
}) => {

packages/workers-utils/src/compatibility-date.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,36 @@ export function resolveNodejsCompat(
105105

106106
return { isNodejsCompatEnabled, isNodejsCompatV2Enabled };
107107
}
108+
109+
// TODO: remove when workerd no longer errors
110+
/**
111+
* Removes the `nodejs_compat` and `nodejs_compat_v2` flags from a list of
112+
* compatibility flags when the compatibility date already enables them.
113+
*
114+
* workerd rejects a compatibility flag that its compatibility date enables by
115+
* default, so from {@link NODEJS_COMPAT_DEFAULT_ON_DATE} onwards specifying
116+
* either flag stops a Worker from starting up. Removing them is a no-op for
117+
* such a date, so tooling can accept the redundant flags rather than failing.
118+
*
119+
* A flag is kept when its matching opt-out is specified too, since workerd
120+
* reports those as contradictory and the enable flag wins there - dropping it
121+
* would silently switch Node.js compatibility off instead.
122+
*
123+
* @param compatibilityDate The compatibility date
124+
* @param compatibilityFlags The compatibility flags
125+
* @returns The flags, without the ones the date makes redundant
126+
*/
127+
export function stripRedundantNodejsCompatFlags(
128+
compatibilityDate: string | undefined,
129+
compatibilityFlags: string[]
130+
): string[] {
131+
if (!isNodejsCompatDefaultOn(compatibilityDate)) {
132+
return compatibilityFlags;
133+
}
134+
135+
const redundantFlags = ["nodejs_compat", "nodejs_compat_v2"].filter(
136+
(flag) => !compatibilityFlags.includes(`no_${flag}`)
137+
);
138+
139+
return compatibilityFlags.filter((flag) => !redundantFlags.includes(flag));
140+
}

packages/workers-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export {
120120
NODEJS_COMPAT_DEFAULT_ON_DATE,
121121
NODEJS_COMPAT_V2_SWITCH_OVER_DATE,
122122
resolveNodejsCompat,
123+
stripRedundantNodejsCompatFlags,
123124
} from "./compatibility-date";
124125
export type { CompatDate } from "./compatibility-date";
125126

packages/workers-utils/tests/compatibility-date.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
isNodejsCompatDefaultOn,
66
NODEJS_COMPAT_DEFAULT_ON_DATE,
77
resolveNodejsCompat,
8+
stripRedundantNodejsCompatFlags,
89
} from "../src/compatibility-date";
910

1011
describe("getTodaysCompatDate()", () => {
@@ -85,3 +86,60 @@ describe("resolveNodejsCompat", () => {
8586
}
8687
);
8788
});
89+
90+
describe("stripRedundantNodejsCompatFlags", () => {
91+
// [compatibilityDate, compatibilityFlags, expected]
92+
const cases: [string | undefined, string[], string[]][] = [
93+
// Before the default-on date the flags are the only way to enable this
94+
["2026-08-03", ["nodejs_compat"], ["nodejs_compat"]],
95+
["2024-09-22", ["nodejs_compat_v2"], ["nodejs_compat_v2"]],
96+
[undefined, ["nodejs_compat"], ["nodejs_compat"]],
97+
// From the default-on date the date enables them, so they're redundant
98+
["2026-08-04", ["nodejs_compat"], []],
99+
["2026-08-04", ["nodejs_compat_v2"], []],
100+
["2026-08-04", ["nodejs_compat", "nodejs_compat_v2"], []],
101+
["2030-01-01", ["nodejs_compat"], []],
102+
// A flag that contradicts its opt-out is kept, since workerd reports that
103+
// and treats the flag as the winner - dropping it would flip the behaviour
104+
[
105+
"2026-08-04",
106+
["nodejs_compat", "no_nodejs_compat"],
107+
["nodejs_compat", "no_nodejs_compat"],
108+
],
109+
[
110+
"2026-08-04",
111+
["nodejs_compat_v2", "no_nodejs_compat_v2"],
112+
["nodejs_compat_v2", "no_nodejs_compat_v2"],
113+
],
114+
// Each flag is considered separately
115+
[
116+
"2026-08-04",
117+
["nodejs_compat", "no_nodejs_compat_v2"],
118+
["no_nodejs_compat_v2"],
119+
],
120+
// Opt-outs are never redundant, they matter for later dates
121+
["2026-08-04", ["no_nodejs_compat"], ["no_nodejs_compat"]],
122+
// Other flags are left alone, in their original order
123+
[
124+
"2026-08-04",
125+
["nodejs_als", "nodejs_compat", "url_standard"],
126+
["nodejs_als", "url_standard"],
127+
],
128+
["2026-08-04", [], []],
129+
// Only exact matches are stripped
130+
[
131+
"2026-08-04",
132+
["experimental:nodejs_compat_v2"],
133+
["experimental:nodejs_compat_v2"],
134+
],
135+
];
136+
137+
test.for(cases)(
138+
"%s with %j",
139+
([compatibilityDate, flags, expected], { expect }) => {
140+
expect(stripRedundantNodejsCompatFlags(compatibilityDate, flags)).toEqual(
141+
expected
142+
);
143+
}
144+
);
145+
});

0 commit comments

Comments
 (0)