Skip to content

Commit 6508ea2

Browse files
authored
fix: ensure Vitest Pool Workers doesn't error with nodejs_compat_v2 flag (#7278)
* fix: ensure Vitest Pool Workers doesn't error with nodejs_compat_v2 flag * chore: remove spurious files * chore: add changeset * chore: consolidate two methods into one * chore: better naming * chore: remove unnecessary comment
1 parent 09e6e90 commit 6508ea2

File tree

5 files changed

+459
-158
lines changed

5 files changed

+459
-158
lines changed

.changeset/hot-dolphins-obey.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/vitest-pool-workers": patch
3+
---
4+
5+
fix: ensures Vitest Pool Workers doesn't error when using nodejs_compat_v2 flag
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* The `CompatibilityFlagAssertions` class provides methods to validate compatibility flags and dates
3+
* within a project's configuration. It ensures that specific flags are either present
4+
* or absent and that compatibility dates meet the required criteria.
5+
*/
6+
export class CompatibilityFlagAssertions {
7+
#compatibilityDate?: string;
8+
#compatibilityFlags: string[];
9+
#optionsPath: string;
10+
#relativeProjectPath: string;
11+
#relativeWranglerConfigPath?: string;
12+
13+
constructor(options: CommonOptions) {
14+
this.#compatibilityDate = options.compatibilityDate;
15+
this.#compatibilityFlags = options.compatibilityFlags;
16+
this.#optionsPath = options.optionsPath;
17+
this.#relativeProjectPath = options.relativeProjectPath;
18+
this.#relativeWranglerConfigPath = options.relativeWranglerConfigPath;
19+
}
20+
21+
/**
22+
* Checks if a specific flag is present in the compatibilityFlags array.
23+
*/
24+
#flagExists(flag: string): boolean {
25+
return this.#compatibilityFlags.includes(flag);
26+
}
27+
28+
/**
29+
* Constructs the base of the error message.
30+
*
31+
* @example
32+
* In project /path/to/project
33+
*
34+
* @example
35+
* In project /path/to/project's configuration file wrangler.toml
36+
*/
37+
#buildErrorMessageBase(): string {
38+
let message = `In project ${this.#relativeProjectPath}`;
39+
if (this.#relativeWranglerConfigPath) {
40+
message += `'s configuration file ${this.#relativeWranglerConfigPath}`;
41+
}
42+
return message;
43+
}
44+
45+
/**
46+
* Constructs the configuration path part of the error message.
47+
*/
48+
#buildConfigPath(setting: string): string {
49+
if (this.#relativeWranglerConfigPath) {
50+
return `\`${setting}\``;
51+
}
52+
53+
const camelCaseSetting = setting.replace(/_(\w)/g, (_, letter) =>
54+
letter.toUpperCase()
55+
);
56+
57+
return `\`${this.#optionsPath}.${camelCaseSetting}\``;
58+
}
59+
60+
/**
61+
* Ensures that a specific enable flag is present or that the compatibility date meets the required date.
62+
*/
63+
assertIsEnabled({
64+
enableFlag,
65+
disableFlag,
66+
defaultOnDate,
67+
}: {
68+
enableFlag: string;
69+
disableFlag: string;
70+
defaultOnDate?: string;
71+
}): AssertionResult {
72+
// If it's disabled by this flag, we can return early.
73+
if (this.#flagExists(disableFlag)) {
74+
const errorMessage = `${this.#buildErrorMessageBase()}, ${this.#buildConfigPath(
75+
"compatibility_flags"
76+
)} must not contain "${disableFlag}".\nThis flag is incompatible with \`@cloudflare/vitest-pool-workers\`.`;
77+
return { isValid: false, errorMessage };
78+
}
79+
80+
const enableFlagPresent = this.#flagExists(enableFlag);
81+
const dateSufficient = isDateSufficient(
82+
this.#compatibilityDate,
83+
defaultOnDate
84+
);
85+
86+
if (!enableFlagPresent && !dateSufficient) {
87+
let errorMessage = `${this.#buildErrorMessageBase()}, ${this.#buildConfigPath(
88+
"compatibility_flags"
89+
)} must contain "${enableFlag}"`;
90+
91+
if (defaultOnDate) {
92+
errorMessage += `, or ${this.#buildConfigPath(
93+
"compatibility_date"
94+
)} must be >= "${defaultOnDate}".`;
95+
}
96+
97+
errorMessage += `\nThis flag is required to use \`@cloudflare/vitest-pool-workers\`.`;
98+
99+
return { isValid: false, errorMessage };
100+
}
101+
102+
return { isValid: true };
103+
}
104+
105+
/**
106+
* Ensures that a any one of a given set of flags is present in the compatibility_flags array.
107+
*/
108+
assertAtLeastOneFlagExists(flags: string[]): AssertionResult {
109+
if (flags.length === 0 || flags.some((flag) => this.#flagExists(flag))) {
110+
return { isValid: true };
111+
}
112+
113+
const errorMessage = `${this.#buildErrorMessageBase()}, ${this.#buildConfigPath(
114+
"compatibility_flags"
115+
)} must contain one of ${flags.map((flag) => `"${flag}"`).join("/")}.\nEither one of these flags is required to use \`@cloudflare/vitest-pool-workers\`.`;
116+
117+
return { isValid: false, errorMessage };
118+
}
119+
}
120+
121+
/**
122+
* Common options used across all assertion methods.
123+
*/
124+
interface CommonOptions {
125+
compatibilityDate?: string;
126+
compatibilityFlags: string[];
127+
optionsPath: string;
128+
relativeProjectPath: string;
129+
relativeWranglerConfigPath?: string;
130+
}
131+
132+
/**
133+
* Result of an assertion method.
134+
*/
135+
interface AssertionResult {
136+
isValid: boolean;
137+
errorMessage?: string;
138+
}
139+
140+
/**
141+
* Parses a date string into a Date object.
142+
*/
143+
function parseDate(dateStr: string): Date {
144+
const date = new Date(dateStr);
145+
if (isNaN(date.getTime())) {
146+
throw new Error(`Invalid date format: "${dateStr}"`);
147+
}
148+
return date;
149+
}
150+
151+
/**
152+
* Checks if the compatibility date meets or exceeds the required date.
153+
*/
154+
function isDateSufficient(
155+
compatibilityDate?: string,
156+
defaultOnDate?: string
157+
): boolean {
158+
if (!compatibilityDate || !defaultOnDate) {
159+
return false;
160+
}
161+
const compDate = parseDate(compatibilityDate);
162+
const reqDate = parseDate(defaultOnDate);
163+
return compDate >= reqDate;
164+
}

packages/vitest-pool-workers/src/pool/index.ts

+26-75
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import semverSatisfies from "semver/functions/satisfies.js";
2424
import { createMethodsRPC } from "vitest/node";
2525
import { createChunkingSocket } from "../shared/chunking-socket";
26+
import { CompatibilityFlagAssertions } from "./compatibility-flag-assertions";
2627
import { OPTIONS_PATH, parseProjectOptions } from "./config";
2728
import {
2829
getProjectPath,
@@ -319,68 +320,6 @@ const SELF_SERVICE_BINDING = "__VITEST_POOL_WORKERS_SELF_SERVICE";
319320
const LOOPBACK_SERVICE_BINDING = "__VITEST_POOL_WORKERS_LOOPBACK_SERVICE";
320321
const RUNNER_OBJECT_BINDING = "__VITEST_POOL_WORKERS_RUNNER_OBJECT";
321322

322-
const numericCompare = new Intl.Collator("en", { numeric: true }).compare;
323-
324-
interface CompatibilityFlagCheckOptions {
325-
// Context to check against
326-
compatibilityFlags: string[];
327-
compatibilityDate?: string;
328-
relativeProjectPath: string | number;
329-
relativeWranglerConfigPath?: string;
330-
331-
// Details on flag to check
332-
enableFlag: string;
333-
disableFlag?: string;
334-
defaultOnDate?: string;
335-
}
336-
function assertCompatibilityFlagEnabled(opts: CompatibilityFlagCheckOptions) {
337-
const hasWranglerConfig = opts.relativeWranglerConfigPath !== undefined;
338-
339-
// Check disable flag (if any) not enabled
340-
if (
341-
opts.disableFlag !== undefined &&
342-
opts.compatibilityFlags.includes(opts.disableFlag)
343-
) {
344-
let message = `In project ${opts.relativeProjectPath}`;
345-
if (hasWranglerConfig) {
346-
message += `'s configuration file ${opts.relativeWranglerConfigPath}, \`compatibility_flags\` must not contain "${opts.disableFlag}".\nSimilarly`;
347-
// Since the config is merged by this point, we don't know where the
348-
// disable flag came from. So we include both possible locations in the
349-
// error message. Note the enable-flag case doesn't have this problem, as
350-
// we're asking the user to add something to *either* of their configs.
351-
}
352-
message +=
353-
`, \`${OPTIONS_PATH}.miniflare.compatibilityFlags\` must not contain "${opts.disableFlag}".\n` +
354-
"This flag is incompatible with `@cloudflare/vitest-pool-workers`.";
355-
throw new Error(message);
356-
}
357-
358-
// Check flag enabled or compatibility date enables flag by default
359-
const enabledByFlag = opts.compatibilityFlags.includes(opts.enableFlag);
360-
const enabledByDate =
361-
opts.compatibilityDate !== undefined &&
362-
opts.defaultOnDate !== undefined &&
363-
numericCompare(opts.compatibilityDate, opts.defaultOnDate) >= 0;
364-
if (!(enabledByFlag || enabledByDate)) {
365-
let message = `In project ${opts.relativeProjectPath}`;
366-
if (hasWranglerConfig) {
367-
message += `'s configuration file ${opts.relativeWranglerConfigPath}, \`compatibility_flags\` must contain "${opts.enableFlag}"`;
368-
} else {
369-
message += `, \`${OPTIONS_PATH}.miniflare.compatibilityFlags\` must contain "${opts.enableFlag}"`;
370-
}
371-
if (opts.defaultOnDate !== undefined) {
372-
if (hasWranglerConfig) {
373-
message += `, or \`compatibility_date\` must be >= "${opts.defaultOnDate}"`;
374-
} else {
375-
message += `, or \`${OPTIONS_PATH}.miniflare.compatibilityDate\` must be >= "${opts.defaultOnDate}"`;
376-
}
377-
}
378-
message +=
379-
".\nThis flag is required to use `@cloudflare/vitest-pool-workers`.";
380-
throw new Error(message);
381-
}
382-
}
383-
384323
function buildProjectWorkerOptions(
385324
project: Omit<Project, "testFiles">
386325
): ProjectWorkers {
@@ -400,24 +339,36 @@ function buildProjectWorkerOptions(
400339
// of the libraries it depends on expect `require()` to return
401340
// `module.exports` directly, rather than `{ default: module.exports }`.
402341
runnerWorker.compatibilityFlags ??= [];
403-
assertCompatibilityFlagEnabled({
404-
compatibilityFlags: runnerWorker.compatibilityFlags,
342+
343+
const flagAssertions = new CompatibilityFlagAssertions({
405344
compatibilityDate: runnerWorker.compatibilityDate,
406-
relativeProjectPath: project.relativePath,
407-
relativeWranglerConfigPath,
408-
// https://developers.cloudflare.com/workers/configuration/compatibility-dates/#commonjs-modules-do-not-export-a-module-namespace
409-
enableFlag: "export_commonjs_default",
410-
disableFlag: "export_commonjs_namespace",
411-
defaultOnDate: "2022-10-31",
412-
});
413-
assertCompatibilityFlagEnabled({
414345
compatibilityFlags: runnerWorker.compatibilityFlags,
415-
compatibilityDate: runnerWorker.compatibilityDate,
416-
relativeProjectPath: project.relativePath,
346+
optionsPath: `${OPTIONS_PATH}.miniflare`,
347+
relativeProjectPath: project.relativePath.toString(),
417348
relativeWranglerConfigPath,
418-
enableFlag: "nodejs_compat",
419349
});
420350

351+
const assertions = [
352+
() =>
353+
flagAssertions.assertIsEnabled({
354+
enableFlag: "export_commonjs_default",
355+
disableFlag: "export_commonjs_namespace",
356+
defaultOnDate: "2022-10-31",
357+
}),
358+
() =>
359+
flagAssertions.assertAtLeastOneFlagExists([
360+
"nodejs_compat",
361+
"nodejs_compat_v2",
362+
]),
363+
];
364+
365+
for (const assertion of assertions) {
366+
const result = assertion();
367+
if (!result.isValid) {
368+
throw new Error(result.errorMessage);
369+
}
370+
}
371+
421372
// Required for `workerd:unsafe` module. We don't require this flag to be set
422373
// as it's experimental, so couldn't be deployed by users.
423374
if (!runnerWorker.compatibilityFlags.includes("unsafe_module")) {

0 commit comments

Comments
 (0)