UNPKG

@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
// <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` );