@cloudflare/vitest-pool-workers
Version:
Workers Vitest integration for writing Vitest unit and integration tests that run inside the Workers runtime
1,378 lines (1,367 loc) • 65.7 kB
JavaScript
// <define:VITEST_POOL_WORKERS_DEFINE_BUILTIN_MODULES>
var define_VITEST_POOL_WORKERS_DEFINE_BUILTIN_MODULES_default = ["workerd:compatibility-flags", "node-internal:async_hooks", "node-internal:buffer", "node-internal:crypto", "node-internal:module", "node-internal:util", "node-internal:diagnostics_channel", "node-internal:zlib", "node-internal:url", "node-internal:dns", "node-internal:timers", "node:_stream_duplex", "node:_stream_passthrough", "node:_stream_readable", "node:_stream_transform", "node:_stream_writable", "node:_tls_common", "node:_tls_wrap", "node:assert", "node:assert/strict", "node:async_hooks", "node:buffer", "node:crypto", "node:diagnostics_channel", "node:dns", "node:dns/promises", "node:events", "node:module", "node:net", "node:path", "node:path/posix", "node:path/win32", "node:process", "node:querystring", "node:stream", "node:stream/consumers", "node:stream/promises", "node:stream/web", "node:string_decoder", "node:test", "node:timers", "node:timers/promises", "node:tls", "node:url", "node:util", "node:util/types", "node:zlib", "node-internal:constants", "node-internal:crypto_cipher", "node-internal:crypto_dh", "node-internal:crypto_hash", "node-internal:crypto_hkdf", "node-internal:crypto_keys", "node-internal:crypto_pbkdf2", "node-internal:crypto_random", "node-internal:crypto_scrypt", "node-internal:crypto_sign", "node-internal:crypto_spkac", "node-internal:crypto_util", "node-internal:crypto_x509", "node-internal:debuglog", "node-internal:events", "node-internal:internal_assert", "node-internal:internal_assertionerror", "node-internal:internal_buffer", "node-internal:internal_comparisons", "node-internal:internal_diffs", "node-internal:internal_dns", "node-internal:internal_dns_client", "node-internal:internal_dns_constants", "node-internal:internal_dns_promises", "node-internal:internal_errors", "node-internal:internal_inspect", "node-internal:internal_net", "node-internal:internal_path", "node-internal:internal_querystring", "node-internal:internal_stringdecoder", "node-internal:internal_timers", "node-internal:internal_timers_promises", "node-internal:internal_tls", "node-internal:internal_tls_common", "node-internal:internal_tls_wrap", "node-internal:internal_types", "node-internal:internal_url", "node-internal:internal_utils", "node-internal:internal_zlib", "node-internal:internal_zlib_base", "node-internal:internal_zlib_constants", "node-internal:legacy_url", "node-internal:mock", "node-internal:process", "node-internal:streams_adapters", "node-internal:streams_compose", "node-internal:streams_duplex", "node-internal:streams_legacy", "node-internal:streams_pipeline", "node-internal:streams_promises", "node-internal:streams_readable", "node-internal:streams_transform", "node-internal:streams_util", "node-internal:streams_writable", "node-internal:validators", "internal:unsafe-eval", "cloudflare-internal:sockets", "cloudflare:ai", "cloudflare:br", "cloudflare:email", "cloudflare:pipelines", "cloudflare:sockets", "cloudflare:vectorize", "cloudflare:workers", "cloudflare:workflows", "cloudflare-internal:ai-api", "cloudflare-internal:aig-api", "cloudflare-internal:autorag-api", "cloudflare-internal:br-api", "cloudflare-internal:d1-api", "cloudflare-internal:images-api", "cloudflare-internal:pipeline-transform", "cloudflare-internal:vectorize-api", "cloudflare-internal:workflows-api", "cloudflare-internal:workers", "cloudflare-internal:env", "workerd:unsafe"];
// src/pool/index.ts
import assert4 from "node:assert";
import crypto from "node:crypto";
import events from "node:events";
import fs3 from "node:fs";
import path4 from "node:path";
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "node:url";
import util2 from "node:util";
import { createBirpc } from "birpc";
import * as devalue from "devalue";
import {
compileModuleRules,
getNodeCompat,
kCurrentWorker,
kUnsafeEphemeralUniqueKey,
Log as Log2,
LogLevel as LogLevel2,
maybeApply,
Miniflare,
structuredSerializableReducers,
structuredSerializableRevivers,
testRegExps,
WebSocket
} from "miniflare";
import semverSatisfies from "semver/functions/satisfies.js";
import { createMethodsRPC } from "vitest/node";
// src/shared/builtin-modules.ts
var workerdBuiltinModules = /* @__PURE__ */ new Set([
...define_VITEST_POOL_WORKERS_DEFINE_BUILTIN_MODULES_default,
"__STATIC_CONTENT_MANIFEST"
]);
// src/shared/chunking-socket.ts
import assert from "node:assert";
import { Buffer } from "node:buffer";
function createChunkingSocket(socket, maxChunkByteLength = 1048576) {
const listeners = [];
const decoder = new TextDecoder();
let chunks;
socket.on((message) => {
if (typeof message === "string") {
if (chunks !== void 0) {
assert.strictEqual(message, "", "Expected end-of-chunks");
message = chunks + decoder.decode();
chunks = void 0;
}
for (const listener of listeners) {
listener(message);
}
} else {
chunks ??= "";
chunks += decoder.decode(message, { stream: true });
}
});
return {
post(value) {
if (Buffer.byteLength(value) > maxChunkByteLength) {
const encoded = Buffer.from(value);
for (let i = 0; i < encoded.byteLength; i += maxChunkByteLength) {
socket.post(encoded.subarray(i, i + maxChunkByteLength));
}
socket.post("");
} else {
socket.post(value);
}
},
on(listener) {
listeners.push(listener);
}
};
}
// src/pool/compatibility-flag-assertions.ts
var CompatibilityFlagAssertions = class {
#compatibilityDate;
#compatibilityFlags;
#optionsPath;
#relativeProjectPath;
#relativeWranglerConfigPath;
constructor(options) {
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) {
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() {
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) {
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
}) {
if (this.#flagExists(disableFlag)) {
const errorMessage = `${this.#buildErrorMessageBase()}, ${this.#buildConfigPath(
"compatibility_flags"
)} must not contain "${disableFlag}".
This 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 += `
This 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) {
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("/")}.
Either one of these flags is required to use \`@cloudflare/vitest-pool-workers\`.`;
return { isValid: false, errorMessage };
}
};
function parseDate(dateStr) {
const date = new Date(dateStr);
if (isNaN(date.getTime())) {
throw new Error(`Invalid date format: "${dateStr}"`);
}
return date;
}
function isDateSufficient(compatibilityDate, defaultOnDate) {
if (!compatibilityDate || !defaultOnDate) {
return false;
}
const compDate = parseDate(compatibilityDate);
const reqDate = parseDate(defaultOnDate);
return compDate >= reqDate;
}
// src/pool/config.ts
import path2 from "node:path";
import {
formatZodError,
getRootPath,
Log,
LogLevel,
mergeWorkerOptions,
parseWithRootPath,
PLUGINS
} from "miniflare";
import { z } from "zod";
// src/pool/helpers.ts
import path from "node:path";
var WORKER_NAME_PREFIX = "vitest-pool-workers-";
function isFileNotFoundError(e) {
return typeof e === "object" && e !== null && "code" in e && e.code === "ENOENT";
}
function getProjectPath(project) {
return project.config.config ?? project.path;
}
function getRelativeProjectPath(project) {
const projectPath = getProjectPath(project);
if (typeof projectPath === "number") {
return projectPath;
} else {
return path.relative("", projectPath);
}
}
// src/pool/config.ts
var PLUGIN_VALUES = Object.values(PLUGINS);
var OPTIONS_PATH_ARRAY = ["test", "poolOptions", "workers"];
var OPTIONS_PATH = OPTIONS_PATH_ARRAY.join(".");
var WorkersPoolOptionsSchema = z.object({
/**
* Entrypoint to Worker run in the same isolate/context as tests. This is
* required to use `import { SELF } from "cloudflare:test"`, or Durable
* Objects without an explicit `scriptName`. Note this goes through Vite
* transforms and can be a TypeScript file. Note also
* `import module from "<path-to-main>"` inside tests gives exactly the same
* `module` instance as is used internally for the `SELF` and Durable Object
* bindings.
*/
main: z.ostring(),
/**
* Enables per-test isolated storage. If enabled, any writes to storage
* performed in a test will be undone at the end of the test. The test storage
* environment is copied from the containing suite, meaning `beforeAll()`
* hooks can be used to seed data. If this is disabled, all tests will share
* the same storage.
*/
isolatedStorage: z.boolean().default(true),
/**
* Runs all tests in this project serially in the same worker, using the same
* module cache. This can significantly speed up tests if you've got lots of
* small test files.
*/
singleWorker: z.boolean().default(false),
miniflare: z.object({
workers: z.array(z.object({}).passthrough()).optional()
}).passthrough().optional(),
wrangler: z.object({ configPath: z.ostring(), environment: z.ostring() }).optional()
});
function isZodErrorLike(value) {
return typeof value === "object" && value !== null && "issues" in value && Array.isArray(value.issues);
}
function coalesceZodErrors(ref, thrown) {
if (!isZodErrorLike(thrown)) {
throw thrown;
}
if (ref.value === void 0) {
ref.value = thrown;
} else {
ref.value.issues.push(...thrown.issues);
}
}
function parseWorkerOptions(rootPath, value, withoutScript, opts) {
if (withoutScript) {
value["script"] = "";
delete value["scriptPath"];
delete value["modules"];
delete value["modulesRoot"];
}
const result = {};
const errorRef = {};
for (const plugin of PLUGIN_VALUES) {
try {
const parsed = parseWithRootPath(rootPath, plugin.options, value, opts);
Object.assign(result, parsed);
} catch (e) {
coalesceZodErrors(errorRef, e);
}
}
if (errorRef.value !== void 0) {
throw errorRef.value;
}
if (withoutScript) {
delete value["script"];
}
return result;
}
var log = new Log(LogLevel.WARN, { prefix: "vpw" });
async function parseCustomPoolOptions(rootPath, value, opts) {
const options = WorkersPoolOptionsSchema.parse(
value,
opts
);
options.miniflare ??= {};
const errorRef = {};
const workers = options.miniflare?.workers;
const rootPathOption = getRootPath(options.miniflare);
rootPath = path2.resolve(rootPath, rootPathOption);
try {
options.miniflare = parseWorkerOptions(
rootPath,
options.miniflare,
/* withoutScript */
true,
// (script provided by runner)
{ path: [...opts.path, "miniflare"] }
);
} catch (e) {
coalesceZodErrors(errorRef, e);
}
options.miniflare.workers = [];
if (workers !== void 0) {
options.miniflare.workers = workers.map((worker, i) => {
try {
const workerRootPathOption = getRootPath(worker);
const workerRootPath = path2.resolve(rootPath, workerRootPathOption);
return parseWorkerOptions(
workerRootPath,
worker,
/* withoutScript */
false,
{
path: [...opts.path, "miniflare", "workers", i]
}
);
} catch (e) {
coalesceZodErrors(errorRef, e);
return { script: "" };
}
});
}
if (errorRef.value !== void 0) {
throw errorRef.value;
}
if (options.wrangler?.configPath !== void 0) {
const configPath = path2.resolve(rootPath, options.wrangler.configPath);
options.wrangler.configPath = configPath;
const wrangler = await import("wrangler");
const { workerOptions, externalWorkers, define, main } = wrangler.unstable_getMiniflareWorkerOptions(
configPath,
options.wrangler.environment,
{ imagesLocalMode: true }
);
const wrappedBindings = Object.values(workerOptions.wrappedBindings ?? {});
const hasAIOrVectorizeBindings = wrappedBindings.some((binding) => {
return typeof binding === "object" && (binding.scriptName.includes("__WRANGLER_EXTERNAL_VECTORIZE_WORKER") || binding.scriptName.includes("__WRANGLER_EXTERNAL_AI_WORKER"));
});
if (hasAIOrVectorizeBindings) {
log.warn(
"Workers AI and Vectorize bindings will access your Cloudflare account and incur usage charges even in testing. We recommend mocking any usage of these bindings in your tests."
);
}
options.main ??= main;
options.miniflare.workers = [
...options.miniflare.workers,
...externalWorkers
];
options.miniflare = mergeWorkerOptions(
workerOptions,
options.miniflare
);
options.defines = define;
}
if (options.miniflare?.assets) {
options.miniflare.hasAssetsAndIsVitest = true;
options.miniflare.assets.routerConfig ??= {};
options.miniflare.assets.routerConfig.has_user_worker = Boolean(
options.main
);
}
return options;
}
async function parseProjectOptions(project) {
const environment = project.config.environment;
if (environment !== void 0 && environment !== "node") {
const quotedEnvironment = JSON.stringify(environment);
let migrationGuide = ".";
if (environment === "miniflare") {
migrationGuide = ", and refer to the migration guide if upgrading from `vitest-environment-miniflare`:\nhttps://developers.cloudflare.com/workers/testing/vitest-integration/get-started/migrate-from-miniflare-2/";
}
const relativePath = getRelativeProjectPath(project);
const message = [
`Unexpected custom \`environment\` ${quotedEnvironment} in project ${relativePath}.`,
"The Workers pool always runs your tests inside of an environment providing Workers runtime APIs.",
`Please remove the \`environment\` configuration${migrationGuide}`,
"Use `poolMatchGlobs`/`environmentMatchGlobs` to run a subset of your tests in a different pool/environment."
].join("\n");
throw new TypeError(message);
}
const projectPath = getProjectPath(project);
const rootPath = typeof projectPath === "string" ? path2.dirname(projectPath) : "";
const poolOptions = project.config.poolOptions;
let workersPoolOptions = poolOptions?.workers ?? {};
try {
if (typeof workersPoolOptions === "function") {
const inject = (key) => {
return project.getProvidedContext()[key];
};
workersPoolOptions = await workersPoolOptions({ inject });
}
return await parseCustomPoolOptions(rootPath, workersPoolOptions, {
path: OPTIONS_PATH_ARRAY
});
} catch (e) {
if (!isZodErrorLike(e)) {
throw e;
}
let formatted;
try {
formatted = formatZodError(e, {
test: { poolOptions: { workers: workersPoolOptions } }
});
} catch (error) {
throw e;
}
const relativePath = getRelativeProjectPath(project);
throw new TypeError(
`Unexpected pool options in project ${relativePath}:
${formatted}`
);
}
}
// src/pool/loopback.ts
import assert2 from "node:assert";
import fs from "node:fs/promises";
import path3 from "node:path";
import {
CACHE_PLUGIN_NAME,
D1_PLUGIN_NAME,
DURABLE_OBJECTS_PLUGIN_NAME,
KV_PLUGIN_NAME,
Mutex,
R2_PLUGIN_NAME,
Response
} from "miniflare";
async function handleSnapshotRequest(request, url) {
const filePath = url.searchParams.get("path");
if (filePath === null) {
return new Response(null, { status: 400 });
}
if (request.method === "POST") {
await fs.mkdir(filePath, { recursive: true });
return new Response(null, { status: 204 });
}
if (request.method === "PUT") {
const snapshot = await request.arrayBuffer();
await fs.mkdir(path3.posix.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, new Uint8Array(snapshot));
return new Response(null, { status: 204 });
}
if (request.method === "GET") {
try {
return new Response(await fs.readFile(filePath));
} catch (e) {
if (!isFileNotFoundError(e)) {
throw e;
}
return new Response(null, { status: 404 });
}
}
if (request.method === "DELETE") {
try {
await fs.unlink(filePath);
} catch (e) {
if (!isFileNotFoundError(e)) {
throw e;
}
}
return new Response(null, { status: 204 });
}
return new Response(null, { status: 405 });
}
async function emptyDir(dirPath) {
let names;
try {
names = await fs.readdir(dirPath);
} catch (e) {
if (isFileNotFoundError(e)) {
return;
}
throw e;
}
for (const name of names) {
await fs.rm(path3.join(dirPath, name), { recursive: true, force: true });
}
}
var stackStates = /* @__PURE__ */ new WeakMap();
function getState(mf) {
let state = stackStates.get(mf);
if (state === void 0) {
const persistPaths = mf.unsafeGetPersistPaths();
const durableObjectPersistPath = persistPaths.get("do");
assert2(
durableObjectPersistPath !== void 0,
"Expected Durable Object persist path"
);
state = {
mutex: new Mutex(),
depth: 0,
broken: false,
persistPaths: Array.from(new Set(persistPaths.values())),
durableObjectPersistPath
};
stackStates.set(mf, state);
}
return state;
}
var ABORT_ALL_WORKER_NAME = `${WORKER_NAME_PREFIX}abort-all`;
var ABORT_ALL_WORKER = {
name: ABORT_ALL_WORKER_NAME,
compatibilityFlags: ["unsafe_module"],
modules: [
{
type: "ESModule",
path: "index.mjs",
contents: `
import workerdUnsafe from "workerd:unsafe";
export default {
async fetch(request) {
if (request.method !== "DELETE") return new Response(null, { status: 405 });
await workerdUnsafe.abortAllDurableObjects();
return new Response(null, { status: 204 });
}
};
`
}
]
};
function scheduleStorageReset(mf) {
const state = getState(mf);
assert2(state.storageResetPromise === void 0);
state.storageResetPromise = state.mutex.runWith(async () => {
const abortAllWorker = await mf.getWorker(ABORT_ALL_WORKER_NAME);
await abortAllWorker.fetch("http://placeholder", { method: "DELETE" });
for (const persistPath of state.persistPaths) {
await emptyDir(persistPath);
}
state.depth = 0;
state.storageResetPromise = void 0;
});
}
async function waitForStorageReset(mf) {
await getState(mf).storageResetPromise;
}
var BLOBS_DIR_NAME = "blobs";
var STACK_DIR_NAME = "__vitest_pool_workers_stack";
async function pushStackedStorage(intoDepth, persistPath) {
const stackFramePath = path3.join(
persistPath,
STACK_DIR_NAME,
intoDepth.toString()
);
await fs.mkdir(stackFramePath, { recursive: true });
for (const key of await fs.readdir(persistPath, { withFileTypes: true })) {
if (key.name === STACK_DIR_NAME) {
continue;
}
const keyPath = path3.join(persistPath, key.name);
const stackFrameKeyPath = path3.join(stackFramePath, key.name);
assert2(key.isDirectory(), `Expected ${keyPath} to be a directory`);
let createdStackFrameKeyPath = false;
for (const name of await fs.readdir(keyPath)) {
if (name === BLOBS_DIR_NAME) {
break;
}
if (!createdStackFrameKeyPath) {
createdStackFrameKeyPath = true;
await fs.mkdir(stackFrameKeyPath);
}
const namePath = path3.join(keyPath, name);
const stackFrameNamePath = path3.join(stackFrameKeyPath, name);
assert2(name.endsWith(".sqlite"), `Expected .sqlite, got ${namePath}`);
await fs.copyFile(namePath, stackFrameNamePath);
}
}
}
async function popStackedStorage(fromDepth, persistPath) {
for (const key of await fs.readdir(persistPath, { withFileTypes: true })) {
if (key.name === STACK_DIR_NAME) {
continue;
}
const keyPath = path3.join(persistPath, key.name);
for (const name of await fs.readdir(keyPath)) {
if (name === BLOBS_DIR_NAME) {
break;
}
const namePath = path3.join(keyPath, name);
assert2(name.endsWith(".sqlite"), `Expected .sqlite, got ${namePath}`);
await fs.unlink(namePath);
}
}
const stackFramePath = path3.join(
persistPath,
STACK_DIR_NAME,
fromDepth.toString()
);
await fs.cp(stackFramePath, persistPath, { recursive: true });
await fs.rm(stackFramePath, { recursive: true, force: true });
}
var PLUGIN_PRODUCT_NAMES = {
[CACHE_PLUGIN_NAME]: "Cache",
[D1_PLUGIN_NAME]: "D1",
[DURABLE_OBJECTS_PLUGIN_NAME]: "Durable Objects",
[KV_PLUGIN_NAME]: "KV",
[R2_PLUGIN_NAME]: "R2"
};
var LIST_FORMAT = new Intl.ListFormat("en-US");
function checkAllStorageOperationsResolved(action, source, persistPaths, results) {
const failedProducts = [];
const lines = [];
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.status === "rejected") {
const pluginName = path3.basename(persistPaths[i]);
const productName = PLUGIN_PRODUCT_NAMES[pluginName] ?? pluginName;
failedProducts.push(productName);
lines.push(`- ${result.reason}`);
}
}
if (failedProducts.length > 0) {
const separator = "=".repeat(80);
lines.unshift(
"",
separator,
`Failed to ${action} isolated storage stack frame in ${source}.`,
`In particular, we were unable to ${action} ${LIST_FORMAT.format(failedProducts)} storage.`,
"This usually means your Worker tried to access storage outside of a test, or some resources have not been disposed of properly.",
`Ensure you "await" all Promises that read or write to these services, and make sure you use the "using" keyword when passing data across JSRPC.`,
`See https://developers.cloudflare.com/workers/testing/vitest-integration/known-issues/#isolated-storage for more details.`,
"\x1B[2m"
);
lines.push("\x1B[22m" + separator, "");
console.error(lines.join("\n"));
return false;
}
return true;
}
async function handleStorageRequest(request, mf) {
const state = getState(mf);
if (state.broken) {
return new Response(
"Isolated storage failed. There should be additional logs above.",
{ status: 500 }
);
}
const source = request.headers.get("MF-Vitest-Source") ?? "an unknown location";
let success;
if (request.method === "POST") {
success = await state.mutex.runWith(async () => {
state.depth++;
const results = await Promise.allSettled(
state.persistPaths.map(
(persistPath) => pushStackedStorage(state.depth, persistPath)
)
);
return checkAllStorageOperationsResolved(
"push",
source,
state.persistPaths,
results
);
});
} else if (request.method === "DELETE") {
success = await state.mutex.runWith(async () => {
assert2(state.depth > 0, "Stack underflow");
const results = await Promise.allSettled(
state.persistPaths.map(
(persistPath) => popStackedStorage(state.depth, persistPath)
)
);
state.depth--;
return checkAllStorageOperationsResolved(
"pop",
source,
state.persistPaths,
results
);
});
} else {
return new Response(null, { status: 405 });
}
if (success) {
return new Response(null, { status: 204 });
} else {
state.broken = true;
return new Response(
"Isolated storage failed. There should be additional logs above.",
{ status: 500 }
);
}
}
async function handleDurableObjectsRequest(request, mf, url) {
if (request.method !== "GET") {
return new Response(null, { status: 405 });
}
const { durableObjectPersistPath } = getState(mf);
const uniqueKey = url.searchParams.get("unique_key");
if (uniqueKey === null) {
return new Response(null, { status: 400 });
}
const namespacePath = path3.join(durableObjectPersistPath, uniqueKey);
const ids = [];
try {
const names = await fs.readdir(namespacePath);
for (const name of names) {
if (name.endsWith(".sqlite")) {
ids.push(name.substring(
0,
name.length - 7
/* ".sqlite".length */
));
}
}
} catch (e) {
if (!isFileNotFoundError(e)) {
throw e;
}
}
return Response.json(ids);
}
function handleLoopbackRequest(request, mf) {
const url = new URL(request.url);
if (url.pathname === "/snapshot") {
return handleSnapshotRequest(request, url);
}
if (url.pathname === "/storage") {
return handleStorageRequest(request, mf);
}
if (url.pathname === "/durable-objects") {
return handleDurableObjectsRequest(request, mf, url);
}
return new Response(null, { status: 404 });
}
// src/pool/module-fallback.ts
import assert3 from "node:assert";
import fs2 from "node:fs";
import { createRequire } from "node:module";
import platformPath from "node:path";
import posixPath from "node:path/posix";
import { fileURLToPath, pathToFileURL } from "node:url";
import util from "node:util";
import * as cjsModuleLexer from "cjs-module-lexer";
import { ModuleRuleTypeSchema, Response as Response2 } from "miniflare";
var debuglog = util.debuglog(
"vitest-pool-workers:module-fallback",
(log3) => debuglog = log3
);
var isWindows = process.platform === "win32";
function ensurePosixLikePath(filePath) {
return isWindows ? filePath.replaceAll("\\", "/") : filePath;
}
var __filename = fileURLToPath(import.meta.url);
var __dirname = platformPath.dirname(__filename);
var require2 = createRequire(__filename);
var distPath = ensurePosixLikePath(platformPath.resolve(__dirname, ".."));
var libPath = posixPath.join(distPath, "worker", "lib");
var emptyLibPath = posixPath.join(libPath, "cloudflare/empty-internal.cjs");
var disableCjsEsmShimSuffix = "?mf_vitest_no_cjs_esm_shim";
function trimSuffix(suffix, value) {
assert3(value.endsWith(suffix));
return value.substring(0, value.length - suffix.length);
}
var versionHashRegExp = /\?v=[0-9a-f]+$/;
function trimViteVersionHash(filePath) {
return filePath.replace(versionHashRegExp, "");
}
var forceModuleTypeRegexp = new RegExp(
`\\?mf_vitest_force=(${ModuleRuleTypeSchema.options.join("|")})$`
);
function isFile(filePath) {
try {
return fs2.statSync(filePath).isFile();
} catch (e) {
if (isFileNotFoundError(e)) {
return false;
}
throw e;
}
}
function isDirectory(filePath) {
try {
return fs2.statSync(filePath).isDirectory();
} catch (e) {
if (isFileNotFoundError(e)) {
return false;
}
throw e;
}
}
function getParentPaths(filePath) {
const parentPaths = [];
while (true) {
const parentPath = posixPath.dirname(filePath);
if (parentPath === filePath) {
return parentPaths;
}
parentPaths.push(parentPath);
filePath = parentPath;
}
}
var dirPathTypeModuleCache = /* @__PURE__ */ new Map();
function isWithinTypeModuleContext(filePath) {
const parentPaths = getParentPaths(filePath);
for (const parentPath of parentPaths) {
const cache = dirPathTypeModuleCache.get(parentPath);
if (cache !== void 0) {
return cache;
}
}
for (const parentPath of parentPaths) {
try {
const pkgPath = posixPath.join(parentPath, "package.json");
const pkgJson = fs2.readFileSync(pkgPath, "utf8");
const pkg = JSON.parse(pkgJson);
const maybeModulePath = pkg.module ? posixPath.join(parentPath, pkg.module) : "";
const cache = pkg.type === "module" || maybeModulePath === filePath;
dirPathTypeModuleCache.set(parentPath, cache);
return cache;
} catch (e) {
if (!isFileNotFoundError(e)) {
throw e;
}
}
}
return false;
}
await cjsModuleLexer.init();
async function getCjsNamedExports(vite, filePath, contents, seen = /* @__PURE__ */ new Set()) {
const { exports, reexports } = cjsModuleLexer.parse(contents);
const result = new Set(exports);
for (const reexport of reexports) {
const resolved = await viteResolve(
vite,
reexport,
filePath,
/* isRequire */
true
);
if (seen.has(resolved)) {
continue;
}
try {
const resolvedContents = fs2.readFileSync(resolved, "utf8");
seen.add(filePath);
const resolvedNames = await getCjsNamedExports(
vite,
resolved,
resolvedContents,
seen
);
seen.delete(filePath);
for (const name of resolvedNames) {
result.add(name);
}
} catch (e) {
if (!isFileNotFoundError(e)) {
throw e;
}
}
}
result.delete("default");
result.delete("__esModule");
return result;
}
function withSourceUrl(contents, url) {
if (contents.lastIndexOf("//# sourceURL=") !== -1) {
return contents;
}
const sourceURL = `
//# sourceURL=${url.toString()}
`;
return contents + sourceURL;
}
function withImportMetaUrl(contents, url) {
return contents.replaceAll("import.meta.url", JSON.stringify(url.toString()));
}
var jsExtensions = [".js", ".mjs", ".cjs"];
function maybeGetTargetFilePath(target) {
if (isFile(target)) {
return target;
}
for (const extension of jsExtensions) {
const targetWithExtension = target + extension;
if (fs2.existsSync(targetWithExtension)) {
return targetWithExtension;
}
}
if (target.endsWith(disableCjsEsmShimSuffix)) {
return target;
}
if (isDirectory(target)) {
return maybeGetTargetFilePath(target + "/index");
}
}
function getApproximateSpecifier(target, referrerDir) {
if (/^(node|cloudflare|workerd):/.test(target)) {
return target;
}
return posixPath.relative(referrerDir, target);
}
async function viteResolve(vite, specifier, referrer, isRequire) {
const resolved = await vite.pluginContainer.resolveId(specifier, referrer, {
ssr: true,
// https://github.com/vitejs/vite/blob/v5.1.4/packages/vite/src/node/plugins/resolve.ts#L178-L179
custom: { "node-resolve": { isRequire } }
});
if (resolved === null) {
if (isRequire && specifier[0] === ".") {
return require2.resolve(specifier, { paths: [referrer] });
}
throw new Error("Not found");
}
if (resolved.id === "__vite-browser-external") {
return emptyLibPath;
}
if (resolved.external) {
let { id } = resolved;
if (workerdBuiltinModules.has(id)) {
return `/${id}`;
}
if (id.startsWith("node:")) {
throw new Error("Not found");
}
id = `node:${id}`;
if (workerdBuiltinModules.has(id)) {
return `/${id}`;
}
return id;
}
return trimViteVersionHash(resolved.id);
}
async function resolve(vite, method, target, specifier, referrer) {
const referrerDir = posixPath.dirname(referrer);
let filePath = maybeGetTargetFilePath(target);
if (filePath !== void 0) {
return filePath;
}
if (referrerDir !== "/" && workerdBuiltinModules.has(specifier)) {
return `/${specifier}`;
}
const specifierLibPath = posixPath.join(
libPath,
specifier.replaceAll(":", "/")
);
filePath = maybeGetTargetFilePath(specifierLibPath);
if (filePath !== void 0) {
return filePath;
}
return viteResolve(vite, specifier, referrer, method === "require");
}
function buildRedirectResponse(filePath) {
if (isWindows && filePath[0] !== "/") {
filePath = `/${filePath}`;
}
return new Response2(null, { status: 301, headers: { Location: filePath } });
}
function maybeGetForceTypeModuleContents(filePath) {
const match = forceModuleTypeRegexp.exec(filePath);
if (match === null) {
return;
}
filePath = trimSuffix(match[0], filePath);
const type = match[1];
const contents = fs2.readFileSync(filePath);
switch (type) {
case "ESModule":
return { esModule: contents.toString() };
case "CommonJS":
return { commonJsModule: contents.toString() };
case "Text":
return { text: contents.toString() };
case "Data":
return { data: contents };
case "CompiledWasm":
return { wasm: contents };
case "PythonModule":
return { pythonModule: contents.toString() };
case "PythonRequirement":
return { pythonRequirement: contents.toString() };
default: {
const exhaustive = type;
assert3.fail(`Unreachable: ${exhaustive} modules are unsupported`);
}
}
}
function buildModuleResponse(target, contents) {
let name = target;
if (!isWindows) {
name = posixPath.relative("/", target);
}
assert3(name[0] !== "/");
const result = { name };
for (const key in contents) {
const value = contents[key];
result[key] = value instanceof Uint8Array ? Array.from(value) : value;
}
return Response2.json(result);
}
async function load(vite, logBase, method, target, specifier, filePath) {
if (target !== filePath) {
if (method === "require" && !specifier.startsWith("node:")) {
filePath += disableCjsEsmShimSuffix;
}
debuglog(logBase, "redirect:", filePath);
return buildRedirectResponse(filePath);
}
if (filePath.endsWith(".wasm")) {
filePath += `?mf_vitest_force=CompiledWasm`;
}
const maybeContents = maybeGetForceTypeModuleContents(filePath);
if (maybeContents !== void 0) {
debuglog(logBase, "forced:", filePath);
return buildModuleResponse(target, maybeContents);
}
const disableCjsEsmShim = filePath.endsWith(disableCjsEsmShimSuffix);
if (disableCjsEsmShim) {
filePath = trimSuffix(disableCjsEsmShimSuffix, filePath);
}
const isEsm = filePath.endsWith(".mjs") || filePath.endsWith(".js") && isWithinTypeModuleContext(filePath);
let contents = fs2.readFileSync(filePath, "utf8");
const targetUrl = pathToFileURL(target);
contents = withSourceUrl(contents, targetUrl);
if (isEsm) {
contents = withImportMetaUrl(contents, targetUrl);
debuglog(logBase, "esm:", filePath);
return buildModuleResponse(target, { esModule: contents });
}
const insertCjsEsmShim = method === "import" || specifier.startsWith("node:");
if (insertCjsEsmShim && !disableCjsEsmShim) {
const fileName = posixPath.basename(filePath);
const disableShimSpecifier = `./${fileName}${disableCjsEsmShimSuffix}`;
const quotedDisableShimSpecifier = JSON.stringify(disableShimSpecifier);
let esModule = `import mod from ${quotedDisableShimSpecifier}; export default mod;`;
for (const name of await getCjsNamedExports(vite, filePath, contents)) {
esModule += ` export const ${name} = mod.${name};`;
}
debuglog(logBase, "cjs-esm-shim:", filePath);
return buildModuleResponse(target, { esModule });
}
debuglog(logBase, "cjs:", filePath);
return buildModuleResponse(target, { commonJsModule: contents });
}
async function handleModuleFallbackRequest(vite, request) {
const method = request.headers.get("X-Resolve-Method");
assert3(method === "import" || method === "require");
const url = new URL(request.url);
let target = url.searchParams.get("specifier");
let referrer = url.searchParams.get("referrer");
assert3(target !== null, "Expected specifier search param");
assert3(referrer !== null, "Expected referrer search param");
const referrerDir = posixPath.dirname(referrer);
let specifier = getApproximateSpecifier(target, referrerDir);
if (specifier.startsWith("file:")) {
specifier = fileURLToPath(specifier);
}
if (isWindows) {
if (target[0] === "/") {
target = target.substring(1);
}
if (referrer[0] === "/") {
referrer = referrer.substring(1);
}
}
const quotedTarget = JSON.stringify(target);
const logBase = `${method}(${quotedTarget}) relative to ${referrer}:`;
try {
const filePath = await resolve(vite, method, target, specifier, referrer);
return await load(vite, logBase, method, target, specifier, filePath);
} catch (e) {
debuglog(logBase, "error:", e);
console.error(
`[vitest-pool-workers] Failed to ${method} ${JSON.stringify(target)} from ${JSON.stringify(referrer)}.`,
"To resolve this, try bundling the relevant dependency with Vite.",
"For more details, refer to https://developers.cloudflare.com/workers/testing/vitest-integration/known-issues/#module-resolution"
);
}
return new Response2(null, { status: 404 });
}
// src/pool/index.ts
assert4(
typeof __vite_ssr_import__ === "undefined",
"Expected `@cloudflare/vitest-pool-workers` not to be transformed by Vite"
);
function structuredSerializableStringify(value) {
if (value && typeof value === "object" && "r" in value && value.r && typeof value.r === "object" && "map" in value.r && value.r.map) {
delete value.r.map;
}
return devalue.stringify(value, structuredSerializableReducers);
}
function structuredSerializableParse(value) {
return devalue.parse(value, structuredSerializableRevivers);
}
var debuglog2 = util2.debuglog(
"vitest-pool-workers:index",
(fn) => debuglog2 = fn
);
var log2 = new Log2(LogLevel2.VERBOSE, { prefix: "vpw" });
var mfLog = new Log2(LogLevel2.WARN);
var __filename2 = fileURLToPath2(import.meta.url);
var __dirname2 = path4.dirname(__filename2);
var DIST_PATH = path4.resolve(__dirname2, "..");
var POOL_WORKER_PATH = path4.join(DIST_PATH, "worker", "index.mjs");
var NODE_URL_PATH = path4.join(DIST_PATH, "worker", "lib", "node", "url.mjs");
var symbolizerWarning = "warning: Not symbolizing stack traces because $LLVM_SYMBOLIZER is not set.";
var ignoreMessages = [
// Not user actionable
// TODO(someday): this is normal operation and really shouldn't error
"disconnected: operation canceled",
"disconnected: worker_do_not_log; Request failed due to internal error",
"disconnected: WebSocket was aborted"
];
function trimSymbolizerWarning(chunk) {
return chunk.includes(symbolizerWarning) ? chunk.substring(chunk.indexOf("\n") + 1) : chunk;
}
function handleRuntimeStdio(stdout, stderr) {
stdout.on("data", (chunk) => {
process.stdout.write(chunk);
});
stderr.on("data", (chunk) => {
const str = trimSymbolizerWarning(chunk.toString());
if (ignoreMessages.some((message) => str.includes(message))) {
return;
}
process.stderr.write(str);
});
}
function forEachMiniflare(mfs, callback) {
if (mfs instanceof Miniflare) {
return callback(mfs);
}
const promises = [];
for (const mf of mfs.values()) {
promises.push(callback(mf));
}
return Promise.all(promises);
}
var allProjects = /* @__PURE__ */ new Map();
function getRunnerName(project, testFile) {
const name = `${WORKER_NAME_PREFIX}runner-${project.getName().replace(/[^a-z0-9-]/gi, "_")}`;
if (testFile === void 0) {
return name;
}
const testFileHash = crypto.createHash("sha1").update(testFile).digest("hex");
testFile = testFile.replace(/[^a-z0-9-]/gi, "_");
return `${name}-${testFileHash}-${testFile}`;
}
function isDurableObjectDesignatorToSelf(value) {
if (typeof value === "string") {
return true;
}
return typeof value === "object" && value !== null && "className" in value && typeof value.className === "string" && (!("scriptName" in value) || value.scriptName === void 0);
}
function isWorkflowDesignatorToSelf(value, currentScriptName) {
return typeof value === "object" && value !== null && "className" in value && typeof value.className === "string" && (!("scriptName" in value) || value.scriptName === void 0 || value.scriptName === currentScriptName);
}
function getDurableObjectDesignators(options) {
const result = /* @__PURE__ */ new Map();
const durableObjects = options.miniflare?.durableObjects ?? {};
for (const [key, designator] of Object.entries(durableObjects)) {
if (typeof designator === "string") {
result.set(key, { className: USER_OBJECT_MODULE_NAME + designator });
} else if (typeof designator.unsafeUniqueKey !== "symbol") {
let className = designator.className;
if (designator.scriptName === void 0) {
className = USER_OBJECT_MODULE_NAME + className;
}
result.set(key, {
className,
scriptName: designator.scriptName,
unsafeUniqueKey: designator.unsafeUniqueKey
});
}
}
return result;
}
var POOL_WORKER_DIR = path4.dirname(POOL_WORKER_PATH);
var USER_OBJECT_MODULE_NAME = "__VITEST_POOL_WORKERS_USER_OBJECT";
var USER_OBJECT_MODULE_PATH = path4.join(
POOL_WORKER_DIR,
USER_OBJECT_MODULE_NAME
);
var DEFINES_MODULE_PATH = path4.join(
POOL_WORKER_DIR,
"__VITEST_POOL_WORKERS_DEFINES"
);
function fixupServiceBindingsToSelf(worker) {
const result = /* @__PURE__ */ new Set();
if (worker.serviceBindings === void 0) {
return result;
}
for (const value of Object.values(worker.serviceBindings)) {
if (typeof value === "object" && "name" in value && value.name === kCurrentWorker && value.entrypoint !== void 0 && value.entrypoint !== "default") {
result.add(value.entrypoint);
value.entrypoint = USER_OBJECT_MODULE_NAME + value.entrypoint;
}
}
return result;
}
function fixupDurableObjectBindingsToSelf(worker) {
const result = /* @__PURE__ */ new Set();
if (worker.durableObjects === void 0) {
return result;
}
for (const key of Object.keys(worker.durableObjects)) {
const designator = worker.durableObjects[key];
if (typeof designator === "string") {
result.add(designator);
worker.durableObjects[key] = USER_OBJECT_MODULE_NAME + designator;
} else if (isDurableObjectDesignatorToSelf(designator)) {
result.add(designator.className);
worker.durableObjects[key] = {
...designator,
className: USER_OBJECT_MODULE_NAME + designator.className
};
}
}
return result;
}
function fixupWorkflowBindingsToSelf(worker) {
const result = /* @__PURE__ */ new Set();
if (worker.workflows === void 0) {
return result;
}
for (const key of Object.keys(worker.workflows)) {
const designator = worker.workflows[key];
if (isWorkflowDesignatorToSelf(designator, worker.name)) {
result.add(designator.className);
worker.workflows[key] = {
...designator,
className: USER_OBJECT_MODULE_NAME + designator.className
};
}
}
return result;
}
var SELF_NAME_BINDING = "__VITEST_POOL_WORKERS_SELF_NAME";
var SELF_SERVICE_BINDING = "__VITEST_POOL_WORKERS_SELF_SERVICE";
var LOOPBACK_SERVICE_BINDING = "__VITEST_POOL_WORKERS_LOOPBACK_SERVICE";
var RUNNER_OBJECT_BINDING = "__VITEST_POOL_WORKERS_RUNNER_OBJECT";
function buildProjectWorkerOptions(project) {
const relativeWranglerConfigPath = maybeApply(
(v) => path4.relative("", v),
project.options.wrangler?.configPath
);
const runnerWorker = project.options.miniflare ?? {};
runnerWorker.name = getRunnerName(project.project);
runnerWorker.bindings ??= {};
runnerWorker.bindings[SELF_NAME_BINDING] = runnerWorker.name;
runnerWorker.compatibilityFlags ??= [];
const flagAssertions = new CompatibilityFlagAssertions({
compatibilityDate: runnerWorker.compatibilityDate,
compatibilityFlags: runnerWorker.compatibilityFlags,
optionsPath: `${OPTIONS_PATH}.miniflare`,
relativeProjectPath: project.relativePath.toString(),
relativeWranglerConfigPath
});
const assertions = [
() => flagAssertions.assertIsEnabled({
enableFlag: "export_commonjs_default",
disableFlag: "export_commonjs_namespace",
defaultOnDate: "2022-10-31"
})
];
for (const assertion of assertions) {
const result = assertion();
if (!result.isValid) {
throw new Error(result.errorMessage);
}
}
const { mode } = getNodeCompat(
runnerWorker.compatibilityDate,
runnerWorker.compatibilityFlags
);
if (mode !== "v2") {
runnerWorker.compatibilityFlags.push("nodejs_compat_v2");
}
if (!runnerWorker.compatibilityFlags.includes("unsafe_module")) {
runnerWorker.compatibilityFlags.push("unsafe_module");
}
runnerWorker.unsafeEvalBinding = "__VITEST_POOL_WORKERS_UNSAFE_EVAL";
runnerWorker.unsafeUseModuleFallbackService = true;
runnerWorker.serviceBindings ??= {};
runnerWorker.serviceBindings[SELF_SERVICE_BINDING] = kCurrentWorker;
runnerWorker.serviceBindings[LOOPBACK_SERVICE_BINDING] = handleLoopbackRequest;
runnerWorker.durableObjects ??= {};
const serviceBindingEntrypointNames = Array.from(
fixupServiceBindingsToSelf(runnerWorker)
).sort();
const durableObjectClassNames = Array.from(
fixupDurableObjectBindingsToSelf(runnerWorker)
).sort();
const workflowClassNames = Array.from(
fixupWorkflowBindingsToSelf(runnerWorker)
).sort();
if (workflowClassNames.length !== 0 && project.options.isolatedStorage === true) {
throw new Error(`Project ${project.relativePath} has Workflows defined and \`isolatedStorage\` set to true.
Please set \`isolatedStorage\` to false in order to run projects with Workflows.
Workflows defined in project: ${workflowClassNames.join(", ")}`);
}
const wrappers = [
'import { createWorkerEntrypointWrapper, createDurableObjectWrapper, createWorkflowEntrypointWrapper } from "cloudflare:test-internal";'
];
for (const entrypointName of serviceBindingEntrypointNames) {
const quotedEntrypointName = JSON.stringify(entrypointName);
const wrapper = `export const ${USER_OBJECT_MODULE_NAME}${entrypointName} = createWorkerEntrypointWrapper(${quotedEntrypointName});`;
wrappers.push(wrapper);
}
for (const className of durableObjectClassNames) {
const quotedClassName = JSON.stringify(className);
const wrapper = `export const ${USER_OBJECT_MODULE_NAME}${className} = createDurableObjectWrapper(${quotedClassName});`;
wrappers.push(wrapper);
}
for (const className of workflowClassNames) {
const quotedClassName = JSON.stringify(className);
const wrapper = `export const ${USER_OBJECT_MODULE_NAME}${className} = createWorkflowEntrypointWrapper(${quotedClassName});`;
wrappers.push(wrapper);
}
runnerWorker.durableObjects[RUNNER_OBJECT_BINDING] = {
className: "RunnerObject",
// Make the runner object ephemeral, so it doesn't write any `.sqlite` files
// that would disrupt stacked storage because we prevent eviction
unsafeUniqueKey: kUnsafeEphemeralUniqueKey,
unsafePreventEviction: true
};
const defines = `export default {
${Object.entries(project.options.defines ?? {}).map(([key, value]) => `${JSON.stringify(key)}: ${value}`).join(",\n")}
};
`;
if ("script" in runnerWorker) {
delete runnerWorker.script;
}
if ("scriptPath" in runnerWorker) {
delete runnerWorker.scriptPath;
}
const modulesRoot = process.platform === "win32" ? "Z:\\" : "/";
runnerWorker.modulesRoot = modulesRoot;
runnerWorker.modules = [
{
type: "ESModule",
path: path4.join(modulesRoot, POOL_WORKER_PATH),
contents: fs3.readFileSync(POOL_WORKER_PATH)
},
{
type: "ESModule",
path: path4.join(modulesRoot, USER_OBJECT_MODULE_PATH),
contents: wrappers.join("\n")
},
{
type: "ESModule",
path: path4.join(modulesRoot, DEFINES_MODULE_PATH),
contents: defines
},
// The workerd provided `node:url` module doesn't support everything Vitest needs.
// As a short-term fix, inject a `node:url` polyfill into the worker bundle
{
type: "ESModule",
path: path4.join(modulesRoot, "node:url"),
contents: fs3.readFileSync(NODE_URL_PATH)
}
];
const workers = [runnerWorker];
if (runnerWorker.workers !== void 0) {
for (let i = 0; i < runnerWorker.workers.length; i++) {
const worker = runnerWorker.workers[i];
if (typeof worker !== "object" || worker === null || !("name" in worker) || typeof worker.name !== "string" || worker.name === "") {
throw new Error(
`In project ${project.relativePath}, \`${OPTIONS_PATH}.miniflare.workers[${i}].name\` must be non-empty`
);