Skip to content

Commit

Permalink
fix: ensure Vitest Pool Workers doesn't error with nodejs_compat_v2 f…
Browse files Browse the repository at this point in the history
…lag (#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
  • Loading branch information
andyjessop authored Nov 19, 2024
1 parent 09e6e90 commit 6508ea2
Show file tree
Hide file tree
Showing 5 changed files with 459 additions and 158 deletions.
5 changes: 5 additions & 0 deletions .changeset/hot-dolphins-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/vitest-pool-workers": patch
---

fix: ensures Vitest Pool Workers doesn't error when using nodejs_compat_v2 flag
164 changes: 164 additions & 0 deletions packages/vitest-pool-workers/src/pool/compatibility-flag-assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* The `CompatibilityFlagAssertions` class provides methods to validate compatibility flags and dates
* within a project's configuration. It ensures that specific flags are either present
* or absent and that compatibility dates meet the required criteria.
*/
export class CompatibilityFlagAssertions {
#compatibilityDate?: string;
#compatibilityFlags: string[];
#optionsPath: string;
#relativeProjectPath: string;
#relativeWranglerConfigPath?: string;

constructor(options: CommonOptions) {
this.#compatibilityDate = options.compatibilityDate;
this.#compatibilityFlags = options.compatibilityFlags;
this.#optionsPath = options.optionsPath;
this.#relativeProjectPath = options.relativeProjectPath;
this.#relativeWranglerConfigPath = options.relativeWranglerConfigPath;
}

/**
* Checks if a specific flag is present in the compatibilityFlags array.
*/
#flagExists(flag: string): boolean {
return this.#compatibilityFlags.includes(flag);
}

/**
* Constructs the base of the error message.
*
* @example
* In project /path/to/project
*
* @example
* In project /path/to/project's configuration file wrangler.toml
*/
#buildErrorMessageBase(): string {
let message = `In project ${this.#relativeProjectPath}`;
if (this.#relativeWranglerConfigPath) {
message += `'s configuration file ${this.#relativeWranglerConfigPath}`;
}
return message;
}

/**
* Constructs the configuration path part of the error message.
*/
#buildConfigPath(setting: string): string {
if (this.#relativeWranglerConfigPath) {
return `\`${setting}\``;
}

const camelCaseSetting = setting.replace(/_(\w)/g, (_, letter) =>
letter.toUpperCase()
);

return `\`${this.#optionsPath}.${camelCaseSetting}\``;
}

/**
* Ensures that a specific enable flag is present or that the compatibility date meets the required date.
*/
assertIsEnabled({
enableFlag,
disableFlag,
defaultOnDate,
}: {
enableFlag: string;
disableFlag: string;
defaultOnDate?: string;
}): AssertionResult {
// If it's disabled by this flag, we can return early.
if (this.#flagExists(disableFlag)) {
const errorMessage = `${this.#buildErrorMessageBase()}, ${this.#buildConfigPath(
"compatibility_flags"
)} must not contain "${disableFlag}".\nThis flag is incompatible with \`@cloudflare/vitest-pool-workers\`.`;
return { isValid: false, errorMessage };
}

const enableFlagPresent = this.#flagExists(enableFlag);
const dateSufficient = isDateSufficient(
this.#compatibilityDate,
defaultOnDate
);

if (!enableFlagPresent && !dateSufficient) {
let errorMessage = `${this.#buildErrorMessageBase()}, ${this.#buildConfigPath(
"compatibility_flags"
)} must contain "${enableFlag}"`;

if (defaultOnDate) {
errorMessage += `, or ${this.#buildConfigPath(
"compatibility_date"
)} must be >= "${defaultOnDate}".`;
}

errorMessage += `\nThis flag is required to use \`@cloudflare/vitest-pool-workers\`.`;

return { isValid: false, errorMessage };
}

return { isValid: true };
}

/**
* Ensures that a any one of a given set of flags is present in the compatibility_flags array.
*/
assertAtLeastOneFlagExists(flags: string[]): AssertionResult {
if (flags.length === 0 || flags.some((flag) => this.#flagExists(flag))) {
return { isValid: true };
}

const errorMessage = `${this.#buildErrorMessageBase()}, ${this.#buildConfigPath(
"compatibility_flags"
)} must contain one of ${flags.map((flag) => `"${flag}"`).join("/")}.\nEither one of these flags is required to use \`@cloudflare/vitest-pool-workers\`.`;

return { isValid: false, errorMessage };
}
}

/**
* Common options used across all assertion methods.
*/
interface CommonOptions {
compatibilityDate?: string;
compatibilityFlags: string[];
optionsPath: string;
relativeProjectPath: string;
relativeWranglerConfigPath?: string;
}

/**
* Result of an assertion method.
*/
interface AssertionResult {
isValid: boolean;
errorMessage?: string;
}

/**
* Parses a date string into a Date object.
*/
function parseDate(dateStr: string): Date {
const date = new Date(dateStr);
if (isNaN(date.getTime())) {
throw new Error(`Invalid date format: "${dateStr}"`);
}
return date;
}

/**
* Checks if the compatibility date meets or exceeds the required date.
*/
function isDateSufficient(
compatibilityDate?: string,
defaultOnDate?: string
): boolean {
if (!compatibilityDate || !defaultOnDate) {
return false;
}
const compDate = parseDate(compatibilityDate);
const reqDate = parseDate(defaultOnDate);
return compDate >= reqDate;
}
101 changes: 26 additions & 75 deletions packages/vitest-pool-workers/src/pool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import semverSatisfies from "semver/functions/satisfies.js";
import { createMethodsRPC } from "vitest/node";
import { createChunkingSocket } from "../shared/chunking-socket";
import { CompatibilityFlagAssertions } from "./compatibility-flag-assertions";
import { OPTIONS_PATH, parseProjectOptions } from "./config";
import {
getProjectPath,
Expand Down Expand Up @@ -319,68 +320,6 @@ const SELF_SERVICE_BINDING = "__VITEST_POOL_WORKERS_SELF_SERVICE";
const LOOPBACK_SERVICE_BINDING = "__VITEST_POOL_WORKERS_LOOPBACK_SERVICE";
const RUNNER_OBJECT_BINDING = "__VITEST_POOL_WORKERS_RUNNER_OBJECT";

const numericCompare = new Intl.Collator("en", { numeric: true }).compare;

interface CompatibilityFlagCheckOptions {
// Context to check against
compatibilityFlags: string[];
compatibilityDate?: string;
relativeProjectPath: string | number;
relativeWranglerConfigPath?: string;

// Details on flag to check
enableFlag: string;
disableFlag?: string;
defaultOnDate?: string;
}
function assertCompatibilityFlagEnabled(opts: CompatibilityFlagCheckOptions) {
const hasWranglerConfig = opts.relativeWranglerConfigPath !== undefined;

// Check disable flag (if any) not enabled
if (
opts.disableFlag !== undefined &&
opts.compatibilityFlags.includes(opts.disableFlag)
) {
let message = `In project ${opts.relativeProjectPath}`;
if (hasWranglerConfig) {
message += `'s configuration file ${opts.relativeWranglerConfigPath}, \`compatibility_flags\` must not contain "${opts.disableFlag}".\nSimilarly`;
// Since the config is merged by this point, we don't know where the
// disable flag came from. So we include both possible locations in the
// error message. Note the enable-flag case doesn't have this problem, as
// we're asking the user to add something to *either* of their configs.
}
message +=
`, \`${OPTIONS_PATH}.miniflare.compatibilityFlags\` must not contain "${opts.disableFlag}".\n` +
"This flag is incompatible with `@cloudflare/vitest-pool-workers`.";
throw new Error(message);
}

// Check flag enabled or compatibility date enables flag by default
const enabledByFlag = opts.compatibilityFlags.includes(opts.enableFlag);
const enabledByDate =
opts.compatibilityDate !== undefined &&
opts.defaultOnDate !== undefined &&
numericCompare(opts.compatibilityDate, opts.defaultOnDate) >= 0;
if (!(enabledByFlag || enabledByDate)) {
let message = `In project ${opts.relativeProjectPath}`;
if (hasWranglerConfig) {
message += `'s configuration file ${opts.relativeWranglerConfigPath}, \`compatibility_flags\` must contain "${opts.enableFlag}"`;
} else {
message += `, \`${OPTIONS_PATH}.miniflare.compatibilityFlags\` must contain "${opts.enableFlag}"`;
}
if (opts.defaultOnDate !== undefined) {
if (hasWranglerConfig) {
message += `, or \`compatibility_date\` must be >= "${opts.defaultOnDate}"`;
} else {
message += `, or \`${OPTIONS_PATH}.miniflare.compatibilityDate\` must be >= "${opts.defaultOnDate}"`;
}
}
message +=
".\nThis flag is required to use `@cloudflare/vitest-pool-workers`.";
throw new Error(message);
}
}

function buildProjectWorkerOptions(
project: Omit<Project, "testFiles">
): ProjectWorkers {
Expand All @@ -400,24 +339,36 @@ function buildProjectWorkerOptions(
// of the libraries it depends on expect `require()` to return
// `module.exports` directly, rather than `{ default: module.exports }`.
runnerWorker.compatibilityFlags ??= [];
assertCompatibilityFlagEnabled({
compatibilityFlags: runnerWorker.compatibilityFlags,

const flagAssertions = new CompatibilityFlagAssertions({
compatibilityDate: runnerWorker.compatibilityDate,
relativeProjectPath: project.relativePath,
relativeWranglerConfigPath,
// https://developers.cloudflare.com/workers/configuration/compatibility-dates/#commonjs-modules-do-not-export-a-module-namespace
enableFlag: "export_commonjs_default",
disableFlag: "export_commonjs_namespace",
defaultOnDate: "2022-10-31",
});
assertCompatibilityFlagEnabled({
compatibilityFlags: runnerWorker.compatibilityFlags,
compatibilityDate: runnerWorker.compatibilityDate,
relativeProjectPath: project.relativePath,
optionsPath: `${OPTIONS_PATH}.miniflare`,
relativeProjectPath: project.relativePath.toString(),
relativeWranglerConfigPath,
enableFlag: "nodejs_compat",
});

const assertions = [
() =>
flagAssertions.assertIsEnabled({
enableFlag: "export_commonjs_default",
disableFlag: "export_commonjs_namespace",
defaultOnDate: "2022-10-31",
}),
() =>
flagAssertions.assertAtLeastOneFlagExists([
"nodejs_compat",
"nodejs_compat_v2",
]),
];

for (const assertion of assertions) {
const result = assertion();
if (!result.isValid) {
throw new Error(result.errorMessage);
}
}

// Required for `workerd:unsafe` module. We don't require this flag to be set
// as it's experimental, so couldn't be deployed by users.
if (!runnerWorker.compatibilityFlags.includes("unsafe_module")) {
Expand Down
Loading

0 comments on commit 6508ea2

Please sign in to comment.