1314 lines
52 KiB
JavaScript
1314 lines
52 KiB
JavaScript
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import v8 from 'node:v8';
|
|
import vm, { isContext, runInContext } from 'node:vm';
|
|
import { l as loadEnvironment, a as listenForErrors, e as emitModuleRunner } from './init.BS957yFf.js';
|
|
import { distDir } from '../path.js';
|
|
import { createCustomConsole } from './console.CHi0lDgQ.js';
|
|
import fs__default from 'node:fs';
|
|
import { createRequire, Module, isBuiltin } from 'node:module';
|
|
import { d as dirname, f as extname, b as basename, C as CSS_LANGS_RE, h as KNOWN_ASSET_RE, t as toArray, k as splitFileAndPostfix, n as normalize, l as isBareImport, r as resolve } from './pathe.M-eThtNZ.Clq7AXhv.js';
|
|
import { l as lookupPackageScopeType } from './resolver.CBCHRZ7j.js';
|
|
import { initSync, parse } from 'es-module-lexer';
|
|
import { dirname as dirname$1 } from 'node:path';
|
|
import { V as VITEST_VM_CONTEXT_SYMBOL, s as startVitestModuleRunner, c as createNodeImportMeta } from './index.1_nbEjJY.js';
|
|
import { getDefaultRequestStubs } from '../module-evaluator.js';
|
|
import { b as setupEnv } from './setup-common.D5-2VgqG.js';
|
|
import { p as provideWorkerState } from './utils.DYj33du9.js';
|
|
|
|
/**
|
|
* Worker-wide cache of V8 code cache buffers for externalized modules.
|
|
*
|
|
* vm pools create a fresh executor per test file, so every externalized
|
|
* module is compiled and evaluated again in each fresh context. The compiled
|
|
* code has no per-context state — reusing its V8 code cache skips the
|
|
* re-parse/re-compile while the evaluation still happens per context.
|
|
*
|
|
* Entries are keyed by the module identifier and guarded by the exact source
|
|
* text, so an invalidated module that produces different code simply replaces
|
|
* its entry.
|
|
*/
|
|
class CodeCache {
|
|
entries = /* @__PURE__ */ new Map();
|
|
get(identifier, source) {
|
|
const entry = this.entries.get(identifier);
|
|
if (entry && entry.source === source) return entry.data;
|
|
}
|
|
/**
|
|
* Stores the code cache produced by `produce` unless an entry for the same
|
|
* source already exists. A `produce` failure is recorded as an empty entry,
|
|
* so it is not retried on every fresh context.
|
|
*/
|
|
store(identifier, source, produce) {
|
|
const entry = this.entries.get(identifier);
|
|
if (entry && entry.source === source) return;
|
|
let data;
|
|
try {
|
|
data = produce();
|
|
} catch {
|
|
data = void 0;
|
|
}
|
|
this.entries.set(identifier, {
|
|
source,
|
|
data
|
|
});
|
|
}
|
|
delete(identifier) {
|
|
this.entries.delete(identifier);
|
|
}
|
|
clear() {
|
|
this.entries.clear();
|
|
}
|
|
}
|
|
/**
|
|
* `node:v8` as seen inside the vm context: changing V8 flags invalidates every
|
|
* code cache produced so far, so `setFlagsFromString` also empties ours.
|
|
*/
|
|
function createV8ModuleWithCacheReset(v8, codeCache) {
|
|
const patched = Object.create(Object.getPrototypeOf(v8), Object.getOwnPropertyDescriptors(v8));
|
|
patched.setFlagsFromString = function setFlagsFromString(flags) {
|
|
v8.setFlagsFromString(flags);
|
|
codeCache.clear();
|
|
};
|
|
return patched;
|
|
}
|
|
|
|
function interopCommonJsModule(interopDefault, mod) {
|
|
if (isPrimitive(mod) || Array.isArray(mod) || mod instanceof Promise) return {
|
|
keys: [],
|
|
moduleExports: {},
|
|
defaultExport: mod
|
|
};
|
|
if (interopDefault !== false && "__esModule" in mod && !isPrimitive(mod.default)) {
|
|
const defaultKets = Object.keys(mod.default);
|
|
const moduleKeys = Object.keys(mod);
|
|
const allKeys = /* @__PURE__ */ new Set([...defaultKets, ...moduleKeys]);
|
|
allKeys.delete("default");
|
|
// the namespace always provides its own synthetic 'module.exports'
|
|
// export, shadowing a real property of that name (Node parity)
|
|
allKeys.delete("module.exports");
|
|
return {
|
|
keys: Array.from(allKeys),
|
|
moduleExports: new Proxy(mod, { get(mod, prop) {
|
|
return mod[prop] ?? mod.default?.[prop];
|
|
} }),
|
|
defaultExport: mod
|
|
};
|
|
}
|
|
return {
|
|
keys: Object.keys(mod).filter((key) => key !== "default" && key !== "module.exports"),
|
|
moduleExports: mod,
|
|
defaultExport: mod
|
|
};
|
|
}
|
|
function isPrimitive(obj) {
|
|
return !(obj != null && (typeof obj === "object" || typeof obj === "function"));
|
|
}
|
|
const SyntheticModule = vm.SyntheticModule;
|
|
const SourceTextModule = vm.SourceTextModule;
|
|
// `SourceTextModule#hasAsyncGraph` marks the Node 24.9+ vm APIs required to
|
|
// load an ES module graph synchronously (`moduleRequests`, `linkRequests`,
|
|
// `instantiate`, synchronously-completing `evaluate`) — the same APIs Node
|
|
// itself uses for require(esm)
|
|
const supportsSyncEsmEvaluate = typeof SourceTextModule?.prototype.hasAsyncGraph === "function";
|
|
let lexerInitialized = false;
|
|
// Returns true when `source` contains ESM syntax: static import/export
|
|
// statements or `import.meta` (dynamic import is allowed in CJS and does not
|
|
// count). Returns false when the lexer cannot parse the source at all —
|
|
// native ESM would fail on it as well, so the CJS error should surface.
|
|
function hasEsmSyntax(source) {
|
|
if (!lexerInitialized) {
|
|
initSync();
|
|
lexerInitialized = true;
|
|
}
|
|
try {
|
|
return parse(source)[3];
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
// mirrors Node's require(esm) error codes so user-side catches work uniformly
|
|
function createRequireAsyncModuleError(identifier, detail) {
|
|
const error = /* @__PURE__ */ new Error(`require() cannot be used to load ES Module ${identifier}: ${detail}. Use import() instead.`);
|
|
error.code = "ERR_REQUIRE_ASYNC_MODULE";
|
|
return error;
|
|
}
|
|
function createConcurrentRequireError(identifier) {
|
|
const error = /* @__PURE__ */ new Error(`Cannot require() ES Module ${identifier} synchronously: it is currently being loaded by a concurrent import(). Await that import before calling require(), or import this module instead of requiring it.`);
|
|
error.code = "ERR_REQUIRE_ESM";
|
|
return error;
|
|
}
|
|
let activeVmExecutor;
|
|
function setActiveVmExecutor(executor) {
|
|
activeVmExecutor = executor;
|
|
}
|
|
async function activeImportModuleDynamically(specifier, referencer) {
|
|
if (!activeVmExecutor) throw new Error(`Cannot import "${specifier}": the test context was torn down.`);
|
|
return activeVmExecutor.importModuleDynamically(specifier, referencer);
|
|
}
|
|
// Node never collects a vm context in which multiple scripts installed
|
|
// closures, and `vm.SourceTextModule`s are pinned by the realm's base object
|
|
// list: the ContextifyContext/ModuleWrap wrappers keep the whole context
|
|
// reachable even through forced full GCs, so a long-lived vm worker
|
|
// accumulates every test file's world until it hits `vmMemoryLimit` and gets
|
|
// recycled, destroying the worker's compile caches with it. Clearing what the
|
|
// test file added to the global object (and the DOM) caps what a pinned
|
|
// context retains. Pristine globals are kept so that work queued before the
|
|
// teardown (jsdom events, worker-scoped fixture cleanups) can still run.
|
|
const captureKeysScript = new vm.Script(`Object.getOwnPropertyNames(globalThis).concat(Object.getOwnPropertySymbols(globalThis))`, { filename: "virtual:vitest-capture-context-keys.js" });
|
|
function captureContextKeys(context) {
|
|
try {
|
|
return new Set(captureKeysScript.runInContext(context));
|
|
} catch {
|
|
return /* @__PURE__ */ new Set();
|
|
}
|
|
}
|
|
const stripScript = new vm.Script(`(initialKeys) => {
|
|
const g = globalThis
|
|
try { g.document.body.textContent = '' } catch {}
|
|
try { g.document.head.textContent = '' } catch {}
|
|
let keys = []
|
|
try { keys = Object.getOwnPropertyNames(g).concat(Object.getOwnPropertySymbols(g)) } catch {}
|
|
for (const key of keys) {
|
|
if (initialKeys.has(key)) continue
|
|
try { delete g[key] } catch {}
|
|
}
|
|
}`, { filename: "virtual:vitest-strip-context.js" });
|
|
function stripDisposedContext(context, initialKeys) {
|
|
try {
|
|
stripScript.runInContext(context)(initialKeys);
|
|
} catch {}
|
|
}
|
|
|
|
const _require = createRequire(import.meta.url);
|
|
// Thrown when the CJS parser rejects a .js file that may contain ESM syntax.
|
|
// `loadCommonJSModule` catches it and retries the file as an ES module,
|
|
// mirroring Node's own require() ESM-syntax fallback for .js files without
|
|
// an ESM package scope. The original error is in `cause`.
|
|
class CjsParseError extends SyntaxError {
|
|
name = "CjsParseError";
|
|
constructor(cause) {
|
|
super(cause.message, { cause });
|
|
}
|
|
}
|
|
const requiresCache = /* @__PURE__ */ new WeakMap();
|
|
// Compiled scripts of commonjs modules, shared across vm contexts: only the
|
|
// evaluation has to happen per context. No invalidation is needed because
|
|
// watch mode reruns destroy the worker.
|
|
const cjsScriptCache = /* @__PURE__ */ new Map();
|
|
class CommonjsExecutor {
|
|
context;
|
|
requireCache = /* @__PURE__ */ new Map();
|
|
publicRequireCache = this.createProxyCache();
|
|
moduleCache = /* @__PURE__ */ new Map();
|
|
builtinCache = Object.create(null);
|
|
extensions = Object.create(null);
|
|
fs;
|
|
codeCache;
|
|
Module;
|
|
interopDefault;
|
|
shouldRequireAsEsm;
|
|
requireEsm;
|
|
// .js files that the ESM-syntax fallback already loaded as ES modules,
|
|
// so later require() calls skip the guaranteed-to-fail CJS parse
|
|
esmSyntaxFallbackFiles = /* @__PURE__ */ new Set();
|
|
constructor(options) {
|
|
this.context = options.context;
|
|
this.fs = options.fileMap;
|
|
this.codeCache = options.codeCache;
|
|
this.interopDefault = options.interopDefault;
|
|
this.shouldRequireAsEsm = options.shouldRequireAsEsm;
|
|
this.requireEsm = options.requireEsm;
|
|
const primitives = vm.runInContext("({ Object, Array, Error })", this.context);
|
|
// eslint-disable-next-line ts/no-this-alias
|
|
const executor = this;
|
|
this.Module = class Module$1 {
|
|
exports;
|
|
isPreloading = false;
|
|
id;
|
|
filename;
|
|
loaded;
|
|
parent;
|
|
children = [];
|
|
path;
|
|
paths = [];
|
|
constructor(id = "", parent) {
|
|
this.exports = primitives.Object.create(primitives.Object.prototype);
|
|
// in our case the path should always be resolved already
|
|
this.path = dirname(id);
|
|
this.id = id;
|
|
this.filename = id;
|
|
this.loaded = false;
|
|
this.parent = parent;
|
|
}
|
|
get require() {
|
|
const require = requiresCache.get(this);
|
|
if (require) return require;
|
|
const _require = Module$1.createRequire(this.id);
|
|
requiresCache.set(this, _require);
|
|
return _require;
|
|
}
|
|
static getSourceMapsSupport = () => ({
|
|
enabled: false,
|
|
nodeModules: false,
|
|
generatedCode: false
|
|
});
|
|
static setSourceMapsSupport = () => {
|
|
// noop
|
|
};
|
|
static register = () => {
|
|
throw new Error(`[vitest] "register" is not available when running in Vitest.`);
|
|
};
|
|
static registerHooks = () => {
|
|
throw new Error(`[vitest] "registerHooks" is not available when running in Vitest.`);
|
|
};
|
|
_compile(code, filename) {
|
|
const cjsModule = Module$1.wrap(code);
|
|
const codeCache = executor.codeCache;
|
|
let script = cjsScriptCache.get(filename);
|
|
if (!script) {
|
|
const cachedData = codeCache?.get(filename, cjsModule);
|
|
// the dynamic import callback is a static function (the executor is
|
|
// resolved when it is called), so the compiled script holds no
|
|
// per-context state and can be reused by every vm context
|
|
try {
|
|
script = new vm.Script(cjsModule, {
|
|
filename,
|
|
cachedData,
|
|
importModuleDynamically: activeImportModuleDynamically
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof SyntaxError && executor.canFallbackToEsm(filename)) throw new CjsParseError(error);
|
|
throw error;
|
|
}
|
|
if (cachedData && script.cachedDataRejected) codeCache.delete(filename);
|
|
// @ts-expect-error mark script with current identifier
|
|
script.identifier = filename;
|
|
cjsScriptCache.set(filename, script);
|
|
}
|
|
const fn = script.runInContext(executor.context);
|
|
const __dirname = dirname(filename);
|
|
executor.requireCache.set(filename, this);
|
|
try {
|
|
fn(this.exports, this.require, this, filename, __dirname);
|
|
return this.exports;
|
|
} finally {
|
|
this.loaded = true;
|
|
// store after execution so the code cache carries the compiled
|
|
// module body, not only the lazily-parsed wrapper
|
|
codeCache?.store(filename, cjsModule, () => script.createCachedData());
|
|
}
|
|
}
|
|
// exposed for external use, Node.js does the opposite
|
|
static _load = (request, parent, _isMain) => {
|
|
return Module$1.createRequire(parent?.filename ?? request)(request);
|
|
};
|
|
static wrap = (script) => {
|
|
return Module$1.wrapper[0] + script + Module$1.wrapper[1];
|
|
};
|
|
static wrapper = new primitives.Array("(function (exports, require, module, __filename, __dirname) { ", "\n});");
|
|
static builtinModules = Module.builtinModules;
|
|
static findSourceMap = Module.findSourceMap;
|
|
static SourceMap = Module.SourceMap;
|
|
static syncBuiltinESMExports = Module.syncBuiltinESMExports;
|
|
static _cache = executor.publicRequireCache;
|
|
static _extensions = executor.extensions;
|
|
static createRequire = (filename) => {
|
|
return executor.createRequire(filename);
|
|
};
|
|
static runMain = () => {
|
|
throw new primitives.Error("[vitest] \"runMain\" is not implemented.");
|
|
};
|
|
// @ts-expect-error not typed
|
|
static _resolveFilename = Module._resolveFilename;
|
|
// @ts-expect-error not typed
|
|
static _findPath = Module._findPath;
|
|
// @ts-expect-error not typed
|
|
static _initPaths = Module._initPaths;
|
|
// @ts-expect-error not typed
|
|
static _preloadModules = Module._preloadModules;
|
|
// @ts-expect-error not typed
|
|
static _resolveLookupPaths = Module._resolveLookupPaths;
|
|
// @ts-expect-error not typed
|
|
static globalPaths = Module.globalPaths;
|
|
static isBuiltin = Module.isBuiltin;
|
|
static constants = Module.constants;
|
|
static enableCompileCache = Module.enableCompileCache;
|
|
static getCompileCacheDir = Module.getCompileCacheDir;
|
|
static flushCompileCache = Module.flushCompileCache;
|
|
static stripTypeScriptTypes = Module.stripTypeScriptTypes;
|
|
static findPackageJSON = Module.findPackageJSON;
|
|
static Module = Module$1;
|
|
};
|
|
this.extensions[".js"] = this.requireJs;
|
|
this.extensions[".json"] = this.requireJson;
|
|
}
|
|
requireJs = (m, filename) => {
|
|
const content = this.fs.readFile(filename);
|
|
m._compile(content, filename);
|
|
};
|
|
requireJson = (m, filename) => {
|
|
const code = this.fs.readFile(filename);
|
|
m.exports = JSON.parse(code);
|
|
};
|
|
static cjsConditions;
|
|
static getCjsConditions() {
|
|
if (!CommonjsExecutor.cjsConditions) CommonjsExecutor.cjsConditions = parseCjsConditions(process.execArgv, process.env.NODE_OPTIONS, supportsSyncEsmEvaluate);
|
|
return CommonjsExecutor.cjsConditions;
|
|
}
|
|
createRequire = (filename) => {
|
|
const _require = createRequire(filename);
|
|
const resolve = (id, options) => {
|
|
return _require.resolve(id, {
|
|
...options,
|
|
// Works on Node 22.12+ where _resolveFilename supports conditions.
|
|
// Silently ignored on older Node versions.
|
|
conditions: CommonjsExecutor.getCjsConditions()
|
|
});
|
|
};
|
|
const require = ((id) => {
|
|
const resolved = resolve(id);
|
|
if (extname(resolved) === ".node" || isBuiltin(resolved)) return this.requireCoreModule(resolved);
|
|
if (this.shouldRequireAsEsm(resolved)) return this.requireEsm(resolved);
|
|
const module = new this.Module(resolved);
|
|
return this.loadCommonJSModule(module, resolved);
|
|
});
|
|
require.resolve = resolve;
|
|
require.resolve.paths = _require.resolve.paths;
|
|
Object.defineProperty(require, "extensions", {
|
|
get: () => this.extensions,
|
|
set: () => {},
|
|
configurable: true
|
|
});
|
|
require.main = void 0;
|
|
require.cache = this.publicRequireCache;
|
|
return require;
|
|
};
|
|
createProxyCache() {
|
|
return new Proxy(Object.create(null), {
|
|
defineProperty: () => true,
|
|
deleteProperty: () => true,
|
|
set: () => true,
|
|
get: (_, key) => this.requireCache.get(key),
|
|
has: (_, key) => this.requireCache.has(key),
|
|
ownKeys: () => Array.from(this.requireCache.keys()),
|
|
getOwnPropertyDescriptor() {
|
|
return {
|
|
configurable: true,
|
|
enumerable: true
|
|
};
|
|
}
|
|
});
|
|
}
|
|
// very naive implementation for Node.js require
|
|
loadCommonJSModule(module, filename) {
|
|
const cached = this.requireCache.get(filename);
|
|
if (cached) return cached.exports;
|
|
if (this.esmSyntaxFallbackFiles.has(filename)) return this.requireEsm(filename);
|
|
const extension = this.findLongestRegisteredExtension(filename);
|
|
const loader = this.extensions[extension] || this.extensions[".js"];
|
|
try {
|
|
loader(module, filename);
|
|
} catch (error) {
|
|
if (error instanceof CjsParseError) return this.fallbackRequireEsm(filename, error);
|
|
throw error;
|
|
}
|
|
return module.exports;
|
|
}
|
|
canFallbackToEsm(filename) {
|
|
return supportsSyncEsmEvaluate && extname(filename) === ".js";
|
|
}
|
|
fallbackRequireEsm(filename, parseError) {
|
|
let exports;
|
|
try {
|
|
exports = this.requireEsm(filename);
|
|
} catch (esmError) {
|
|
// both parsers rejected the file — surface the original CJS error
|
|
if (esmError instanceof SyntaxError) throw parseError.cause;
|
|
throw esmError;
|
|
}
|
|
this.esmSyntaxFallbackFiles.add(filename);
|
|
return exports;
|
|
}
|
|
findLongestRegisteredExtension(filename) {
|
|
const name = basename(filename);
|
|
let currentExtension;
|
|
let index;
|
|
let startIndex = 0;
|
|
// eslint-disable-next-line no-cond-assign
|
|
while ((index = name.indexOf(".", startIndex)) !== -1) {
|
|
startIndex = index + 1;
|
|
if (index === 0) continue;
|
|
currentExtension = name.slice(index);
|
|
if (this.extensions[currentExtension]) return currentExtension;
|
|
}
|
|
return ".js";
|
|
}
|
|
getCoreSyntheticModule(identifier) {
|
|
if (this.moduleCache.has(identifier)) return this.moduleCache.get(identifier);
|
|
const exports = this.require(identifier);
|
|
const keys = Object.keys(exports);
|
|
const module = new SyntheticModule([...keys, "default"], () => {
|
|
for (const key of keys) module.setExport(key, exports[key]);
|
|
module.setExport("default", exports);
|
|
}, {
|
|
context: this.context,
|
|
identifier
|
|
});
|
|
this.moduleCache.set(identifier, module);
|
|
return module;
|
|
}
|
|
getCjsSyntheticModule(path, identifier) {
|
|
if (this.moduleCache.has(identifier)) return this.moduleCache.get(identifier);
|
|
const exports = this.require(path);
|
|
// TODO: technically module should be parsed to find static exports, implement for strict mode in #2854
|
|
const { keys, moduleExports, defaultExport } = interopCommonJsModule(this.interopDefault, exports);
|
|
const module = new SyntheticModule([
|
|
...keys,
|
|
"default",
|
|
"module.exports"
|
|
], function() {
|
|
for (const key of keys) this.setExport(key, moduleExports[key]);
|
|
this.setExport("default", defaultExport);
|
|
// the raw module.exports value, mirroring the handling of the
|
|
// 'module.exports' export name in require(esm) interop:
|
|
// https://nodejs.org/api/esm.html#commonjs-namespaces
|
|
this.setExport("module.exports", exports);
|
|
}, {
|
|
context: this.context,
|
|
identifier
|
|
});
|
|
this.moduleCache.set(identifier, module);
|
|
return module;
|
|
}
|
|
// TODO: use this in strict mode, when available in #2854
|
|
// private _getNamedCjsExports(path: string): Set<string> {
|
|
// const cachedNamedExports = this.cjsNamedExportsMap.get(path)
|
|
// if (cachedNamedExports) {
|
|
// return cachedNamedExports
|
|
// }
|
|
// if (extname(path) === '.node') {
|
|
// const moduleExports = this.require(path)
|
|
// const namedExports = new Set(Object.keys(moduleExports))
|
|
// this.cjsNamedExportsMap.set(path, namedExports)
|
|
// return namedExports
|
|
// }
|
|
// const code = this.fs.readFile(path)
|
|
// const { exports, reexports } = parseCjs(code, path)
|
|
// const namedExports = new Set(exports)
|
|
// this.cjsNamedExportsMap.set(path, namedExports)
|
|
// for (const reexport of reexports) {
|
|
// if (isNodeBuiltin(reexport)) {
|
|
// const exports = this.require(reexport)
|
|
// if (exports !== null && typeof exports === 'object') {
|
|
// for (const e of Object.keys(exports)) {
|
|
// namedExports.add(e)
|
|
// }
|
|
// }
|
|
// }
|
|
// else {
|
|
// const require = this.createRequire(path)
|
|
// const resolved = require.resolve(reexport)
|
|
// const exports = this._getNamedCjsExports(resolved)
|
|
// for (const e of exports) {
|
|
// namedExports.add(e)
|
|
// }
|
|
// }
|
|
// }
|
|
// return namedExports
|
|
// }
|
|
require(identifier) {
|
|
if (extname(identifier) === ".node" || isBuiltin(identifier)) return this.requireCoreModule(identifier);
|
|
if (this.shouldRequireAsEsm(identifier)) return this.requireEsm(identifier);
|
|
const module = new this.Module(identifier);
|
|
return this.loadCommonJSModule(module, identifier);
|
|
}
|
|
requireCoreModule(identifier) {
|
|
const normalized = identifier.replace(/^node:/, "");
|
|
if (this.builtinCache[normalized]) return this.builtinCache[normalized].exports;
|
|
const moduleExports = _require(identifier);
|
|
if (identifier === "node:module" || identifier === "module") {
|
|
const module = new this.Module("/module.js");
|
|
module.exports = this.Module;
|
|
this.builtinCache[normalized] = module;
|
|
return module.exports;
|
|
}
|
|
if (normalized === "v8" && this.codeCache) {
|
|
const module = new this.Module("/v8.js");
|
|
module.exports = createV8ModuleWithCacheReset(moduleExports, this.codeCache);
|
|
this.builtinCache[normalized] = module;
|
|
return module.exports;
|
|
}
|
|
this.builtinCache[normalized] = _require.cache[normalized];
|
|
// TODO: should we wrap module to rethrow context errors?
|
|
return moduleExports;
|
|
}
|
|
}
|
|
// The "module-sync" exports condition (added in Node 22.12/20.19 when
|
|
// require(esm) was unflagged) can resolve to ESM files. When require(esm)
|
|
// is supported (Node 24.9+), the condition is included, matching Node.
|
|
// Otherwise our CJS vm.Script executor cannot handle the resolved ESM
|
|
// files, so the condition is excluded by passing explicit CJS conditions
|
|
// to require.resolve (Node 22.12+).
|
|
// Must be a Set because Node's internal resolver calls conditions.has().
|
|
function parseCjsConditions(execArgv, nodeOptions, requireEsmSupported = false) {
|
|
const conditions = [
|
|
"node",
|
|
"require",
|
|
"node-addons"
|
|
];
|
|
if (requireEsmSupported) conditions.push("module-sync");
|
|
const args = [...execArgv, ...nodeOptions?.split(/\s+/) ?? []];
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
const eqMatch = arg.match(/^(?:--conditions|-C)=(.+)$/);
|
|
if (eqMatch) conditions.push(eqMatch[1]);
|
|
else if ((arg === "--conditions" || arg === "-C") && i + 1 < args.length) conditions.push(args[++i]);
|
|
}
|
|
if (!requireEsmSupported) return new Set(conditions.filter((c) => c !== "module-sync"));
|
|
return new Set(conditions);
|
|
}
|
|
|
|
// `hasAsyncGraph` only exists on SourceTextModule — a SyntheticModule is
|
|
// synchronous by definition (its evaluation callback is sync)
|
|
function moduleHasAsyncGraph(module) {
|
|
return module instanceof SourceTextModule && module.hasAsyncGraph();
|
|
}
|
|
const dataURIRegex = /^data:(?<mime>text\/javascript|application\/json|application\/wasm)(?:;(?<encoding>charset=utf-8|base64))?,(?<code>.*)$/;
|
|
function parseDataUri(identifier) {
|
|
const match = identifier.match(dataURIRegex);
|
|
if (!match || !match.groups) throw new Error("Invalid data URI");
|
|
const { mime, encoding } = match.groups;
|
|
let code = match.groups.code;
|
|
if (mime === "application/wasm") {
|
|
if (!encoding) throw new Error("Missing data URI encoding");
|
|
if (encoding !== "base64") throw new Error(`Invalid data URI encoding: ${encoding}`);
|
|
return {
|
|
mime,
|
|
code: Buffer.from(code, "base64")
|
|
};
|
|
}
|
|
if (!encoding || encoding === "charset=utf-8") code = decodeURIComponent(code);
|
|
else if (encoding === "base64") code = Buffer.from(code, "base64").toString();
|
|
else throw new Error(`Invalid data URI encoding: ${encoding}`);
|
|
return {
|
|
mime,
|
|
code
|
|
};
|
|
}
|
|
function getContextExecutor(mod) {
|
|
const vmContext = mod.context?.[VITEST_VM_CONTEXT_SYMBOL];
|
|
if (!vmContext) throw new Error(`Cannot import "${mod.identifier}": its vm context was torn down.`);
|
|
return vmContext.externalModulesExecutor;
|
|
}
|
|
async function staticImportModuleDynamically(specifier, referencer) {
|
|
return getContextExecutor(referencer).importModuleDynamically(specifier, referencer);
|
|
}
|
|
function staticInitializeImportMeta(meta, mod) {
|
|
meta.url = mod.identifier;
|
|
if (mod.identifier.startsWith("file:")) {
|
|
const filename = fileURLToPath(mod.identifier);
|
|
meta.filename = filename;
|
|
meta.dirname = dirname$1(filename);
|
|
}
|
|
meta.resolve = (specifier, importer) => {
|
|
return getContextExecutor(mod).resolve(specifier, importer != null ? importer.toString() : mod.identifier);
|
|
};
|
|
}
|
|
class EsmExecutor {
|
|
executor;
|
|
moduleCache = /* @__PURE__ */ new Map();
|
|
esmLinkMap = /* @__PURE__ */ new WeakMap();
|
|
linkQueue = Promise.resolve();
|
|
context;
|
|
#httpIp = IPnumber("127.0.0.0");
|
|
constructor(executor, options) {
|
|
this.executor = executor;
|
|
this.context = options.context;
|
|
}
|
|
async evaluateModule(m) {
|
|
// a module that failed to evaluate keeps living in the cache — rethrow
|
|
// its error instead of silently returning an unusable namespace
|
|
if (m.status === "errored") throw m.error;
|
|
await this.linkModule(m);
|
|
if (m.status === "linked") await m.evaluate();
|
|
return m;
|
|
}
|
|
// Roots are linked one at a time: Node's link() does not wait for a
|
|
// dependency that another root is still linking, and instantiate() then
|
|
// fails on it. Sharing the queue with all roots keeps cycle handling to
|
|
// Node's own single-root linker, which never has to wait.
|
|
linkModule(m) {
|
|
const pending = this.esmLinkMap.get(m);
|
|
if (pending) return pending;
|
|
if (m.status !== "unlinked" && m.status !== "linking") return Promise.resolve();
|
|
const linking = this.linkQueue.then(() => {
|
|
if (m.status === "unlinked") return m.link(this.linker);
|
|
});
|
|
this.esmLinkMap.set(m, linking);
|
|
this.linkQueue = linking.catch(() => {});
|
|
return linking;
|
|
}
|
|
linker = (identifier, referencer) => this.executor.resolveModule(identifier, referencer.identifier);
|
|
async createEsModule(fileURL, getCode) {
|
|
const cached = this.moduleCache.get(fileURL);
|
|
if (cached) return cached;
|
|
const promise = this.loadEsModule(fileURL, getCode);
|
|
this.moduleCache.set(fileURL, promise);
|
|
return promise;
|
|
}
|
|
loadEsModule(fileURL, getCode) {
|
|
const code = getCode();
|
|
if (code instanceof Promise) return code.then((content) => this.createModule(fileURL, content));
|
|
return this.createModule(fileURL, code);
|
|
}
|
|
createModule(fileURL, code) {
|
|
const module = fileURL.endsWith(".json") ? this.createJsonModule(fileURL, code) : this.createSourceTextModule(fileURL, code);
|
|
this.moduleCache.set(fileURL, module);
|
|
return module;
|
|
}
|
|
createSourceTextModule(fileURL, code) {
|
|
const codeCache = this.executor.codeCache;
|
|
let cachedData = codeCache?.get(fileURL, code);
|
|
const options = {
|
|
identifier: fileURL,
|
|
context: this.context,
|
|
// static callbacks: Node keeps them registered for as long as the
|
|
// module's host-defined-options symbol is alive, so a closure here would
|
|
// retain this executor (and the whole test file's world) beyond the
|
|
// file's lifetime. The executor is recovered from the module's context
|
|
// at call time instead.
|
|
importModuleDynamically: staticImportModuleDynamically,
|
|
initializeImportMeta: staticInitializeImportMeta
|
|
};
|
|
let m;
|
|
if (cachedData) try {
|
|
m = new SourceTextModule(code, {
|
|
...options,
|
|
cachedData
|
|
});
|
|
} catch (error) {
|
|
// unlike vm.Script, a module throws when V8 rejects the cache (e.g. the
|
|
// V8 flags changed at runtime): compile from source instead
|
|
if (error?.code !== "ERR_VM_MODULE_CACHED_DATA_REJECTED") throw error;
|
|
codeCache.delete(fileURL);
|
|
cachedData = void 0;
|
|
}
|
|
m ??= new SourceTextModule(code, options);
|
|
// the code cache of a SourceTextModule must be created before evaluation
|
|
if (!cachedData) {
|
|
const created = m;
|
|
codeCache?.store(fileURL, code, () => created.createCachedData());
|
|
}
|
|
return m;
|
|
}
|
|
// Loads an ES module graph synchronously for require(esm), mirroring Node's
|
|
// own behaviour on Node 24.9+. The graph is collected into a local scratch
|
|
// map first and committed to the module cache only after the whole graph is
|
|
// proven to be synchronously evaluable, so a failed require() does not
|
|
// poison the cache for a later import() of the same file.
|
|
requireEsModuleSync(rootIdentifier) {
|
|
const cachedRoot = this.moduleCache.get(rootIdentifier);
|
|
if (cachedRoot) return this.reuseSyncModule(rootIdentifier, cachedRoot);
|
|
const scratch = /* @__PURE__ */ new Map();
|
|
const worklist = [rootIdentifier];
|
|
while (worklist.length > 0) {
|
|
const identifier = worklist.pop();
|
|
if (scratch.has(identifier)) continue;
|
|
const cached = this.moduleCache.get(identifier);
|
|
if (cached) {
|
|
scratch.set(identifier, {
|
|
module: this.reuseSyncModule(identifier, cached),
|
|
commit: false
|
|
});
|
|
continue;
|
|
}
|
|
const disposition = identifier.startsWith("data:") ? this.materializeSyncDataModule(identifier) : this.executor.materializeSyncModule(identifier, identifier === rootIdentifier);
|
|
if (disposition.kind === "ready") {
|
|
scratch.set(identifier, {
|
|
module: disposition.module,
|
|
commit: false
|
|
});
|
|
continue;
|
|
}
|
|
if (disposition.kind === "json") {
|
|
scratch.set(identifier, {
|
|
module: this.createJsonModule(identifier, disposition.code),
|
|
commit: true
|
|
});
|
|
continue;
|
|
}
|
|
const module = this.createSourceTextModule(identifier, disposition.code);
|
|
if (module.hasTopLevelAwait()) throw createRequireAsyncModuleError(identifier, "the module uses top-level await");
|
|
const deps = [];
|
|
for (const request of module.moduleRequests) {
|
|
const depIdentifier = this.executor.resolveSyncSpecifier(request.specifier, identifier);
|
|
deps.push(depIdentifier);
|
|
if (!scratch.has(depIdentifier)) worklist.push(depIdentifier);
|
|
}
|
|
scratch.set(identifier, {
|
|
module,
|
|
deps,
|
|
commit: true
|
|
});
|
|
}
|
|
for (const entry of scratch.values()) if (entry.deps) entry.module.linkRequests(entry.deps.map((dep) => scratch.get(dep).module));
|
|
const root = scratch.get(rootIdentifier);
|
|
if (root.deps) root.module.instantiate();
|
|
if (moduleHasAsyncGraph(root.module)) {
|
|
// top-level await is rejected per module during the walk, so this is a
|
|
// defensive check that an async graph never reaches the sync evaluate
|
|
let culprit = rootIdentifier;
|
|
for (const [identifier, entry] of scratch) if (entry.deps && entry.module.hasTopLevelAwait()) {
|
|
culprit = identifier;
|
|
break;
|
|
}
|
|
throw createRequireAsyncModuleError(rootIdentifier, culprit === rootIdentifier ? "the module uses top-level await" : `its dependency uses top-level await (${culprit})`);
|
|
}
|
|
for (const [identifier, entry] of scratch) if (entry.commit && !this.moduleCache.has(identifier)) this.moduleCache.set(identifier, entry.module);
|
|
// with no top-level await in the graph, evaluate() fulfills synchronously
|
|
// and an evaluation error lands on `status`/`error`, not on the promise
|
|
root.module.evaluate().catch(() => {});
|
|
if (root.module.status === "errored") throw root.module.error;
|
|
if (root.module.status !== "evaluated") throw new Error(`[vitest] Expected synchronous evaluation to complete for ${rootIdentifier}, but module status is "${root.module.status}". This is a bug in Vitest.`);
|
|
return root.module;
|
|
}
|
|
// A cached module is reusable by the sync walker only when it is settled:
|
|
// anything else (a pending Promise or a module in 'unlinked' → 'evaluating')
|
|
// is a concurrent import() mid-flight that a synchronous require() can
|
|
// neither await nor safely link against.
|
|
reuseSyncModule(identifier, cached) {
|
|
if (cached instanceof Promise) throw createConcurrentRequireError(identifier);
|
|
if (cached.status === "errored") throw cached.error;
|
|
if (cached.status !== "evaluated") throw createConcurrentRequireError(identifier);
|
|
// a module with top-level await reports 'evaluated' as soon as evaluate()
|
|
// is called, while its async evaluation may still be pending — and even a
|
|
// settled async graph is never allowed in require() (Node parity)
|
|
if (moduleHasAsyncGraph(cached)) throw createRequireAsyncModuleError(identifier, "the module uses top-level await");
|
|
return cached;
|
|
}
|
|
materializeSyncDataModule(identifier) {
|
|
const { mime, code } = parseDataUri(identifier);
|
|
if (mime === "application/wasm") throw createRequireAsyncModuleError(identifier, "WebAssembly modules cannot be loaded synchronously");
|
|
if (mime === "application/json") return {
|
|
kind: "json",
|
|
code
|
|
};
|
|
return {
|
|
kind: "source",
|
|
code
|
|
};
|
|
}
|
|
createJsonModule(identifier, code) {
|
|
return new SyntheticModule(["default"], function() {
|
|
this.setExport("default", JSON.parse(code));
|
|
}, {
|
|
context: this.context,
|
|
identifier
|
|
});
|
|
}
|
|
async createWebAssemblyModule(fileUrl, getCode) {
|
|
const cached = this.moduleCache.get(fileUrl);
|
|
if (cached) return cached;
|
|
const m = this.loadWebAssemblyModule(getCode(), fileUrl);
|
|
this.moduleCache.set(fileUrl, m);
|
|
return m;
|
|
}
|
|
async createNetworkModule(fileUrl) {
|
|
// https://nodejs.org/api/esm.html#https-and-http-imports
|
|
if (fileUrl.startsWith("http:")) {
|
|
const url = new URL(fileUrl);
|
|
if (url.hostname !== "localhost" && url.hostname !== "::1" && (IPnumber(url.hostname) & IPmask(8)) !== this.#httpIp) throw new Error(
|
|
// we don't know the importer, so it's undefined (the same happens in --pool=threads)
|
|
`import of '${fileUrl}' by undefined is not supported: http can only be used to load local resources (use https instead).`
|
|
);
|
|
}
|
|
return this.createEsModule(fileUrl, () => fetch(fileUrl).then((r) => r.text()));
|
|
}
|
|
async loadWebAssemblyModule(source, identifier) {
|
|
const cached = this.moduleCache.get(identifier);
|
|
if (cached) return cached;
|
|
const wasmModule = await WebAssembly.compile(source);
|
|
const exports = WebAssembly.Module.exports(wasmModule);
|
|
const imports = WebAssembly.Module.imports(wasmModule);
|
|
const moduleLookup = {};
|
|
for (const { module } of imports) if (moduleLookup[module] === void 0) moduleLookup[module] = await this.executor.resolveModule(module, identifier);
|
|
const evaluateModule = (module) => this.evaluateModule(module);
|
|
return new SyntheticModule(exports.map(({ name }) => name), async function() {
|
|
const importsObject = {};
|
|
for (const { module, name } of imports) {
|
|
if (!importsObject[module]) importsObject[module] = {};
|
|
await evaluateModule(moduleLookup[module]);
|
|
importsObject[module][name] = moduleLookup[module].namespace[name];
|
|
}
|
|
const wasmInstance = new WebAssembly.Instance(wasmModule, importsObject);
|
|
for (const { name } of exports) this.setExport(name, wasmInstance.exports[name]);
|
|
}, {
|
|
context: this.context,
|
|
identifier
|
|
});
|
|
}
|
|
cacheModule(identifier, module) {
|
|
this.moduleCache.set(identifier, module);
|
|
}
|
|
resolveCachedModule(identifier) {
|
|
return this.moduleCache.get(identifier);
|
|
}
|
|
async createDataModule(identifier) {
|
|
const cached = this.moduleCache.get(identifier);
|
|
if (cached) return cached;
|
|
const { mime, code } = parseDataUri(identifier);
|
|
if (mime === "application/wasm") {
|
|
const module = this.loadWebAssemblyModule(code, identifier);
|
|
this.moduleCache.set(identifier, module);
|
|
return module;
|
|
}
|
|
if (mime === "application/json") {
|
|
const module = this.createJsonModule(identifier, code);
|
|
this.moduleCache.set(identifier, module);
|
|
return module;
|
|
}
|
|
return this.createEsModule(identifier, () => code);
|
|
}
|
|
}
|
|
function IPnumber(address) {
|
|
const ip = address.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
if (ip) return (+ip[1] << 24) + (+ip[2] << 16) + (+ip[3] << 8) + +ip[4];
|
|
throw new Error(`Expected IP address, received ${address}`);
|
|
}
|
|
function IPmask(maskSize) {
|
|
return -1 << 32 - maskSize;
|
|
}
|
|
|
|
const CLIENT_ID = "/@vite/client";
|
|
const CLIENT_FILE = pathToFileURL(CLIENT_ID).href;
|
|
class ViteExecutor {
|
|
options;
|
|
esm;
|
|
constructor(options) {
|
|
this.options = options;
|
|
this.esm = options.esmExecutor;
|
|
}
|
|
resolve = (identifier) => {
|
|
if (identifier === CLIENT_ID) return identifier;
|
|
};
|
|
get workerState() {
|
|
return this.options.context.__vitest_worker__;
|
|
}
|
|
async createViteModule(fileUrl) {
|
|
if (fileUrl === CLIENT_FILE || fileUrl === CLIENT_ID) return this.createViteClientModule();
|
|
const cached = this.esm.resolveCachedModule(fileUrl);
|
|
if (cached) return cached;
|
|
return this.esm.createEsModule(fileUrl, async () => {
|
|
try {
|
|
const result = await this.options.transform(fileUrl);
|
|
if (result.code) return result.code;
|
|
} catch (cause) {
|
|
// rethrow vite error if it cannot load the module because it's not resolved
|
|
if (typeof cause === "object" && cause.code === "ERR_LOAD_URL" || typeof cause?.message === "string" && cause.message.includes("Failed to load url")) {
|
|
const error = new Error(`Cannot find module '${fileUrl}'`, { cause });
|
|
error.code = "ERR_MODULE_NOT_FOUND";
|
|
throw error;
|
|
}
|
|
}
|
|
throw new Error(`[vitest] Failed to transform ${fileUrl}. Does the file exist?`);
|
|
});
|
|
}
|
|
createViteClientModule() {
|
|
const identifier = CLIENT_ID;
|
|
const cached = this.esm.resolveCachedModule(identifier);
|
|
if (cached) return cached;
|
|
const stub = this.options.viteClientModule;
|
|
const moduleKeys = Object.keys(stub);
|
|
const module = new SyntheticModule(moduleKeys, function() {
|
|
moduleKeys.forEach((key) => {
|
|
this.setExport(key, stub[key]);
|
|
});
|
|
}, {
|
|
context: this.options.context,
|
|
identifier
|
|
});
|
|
this.esm.cacheModule(identifier, module);
|
|
return module;
|
|
}
|
|
canResolve = (fileUrl) => {
|
|
if (fileUrl === CLIENT_FILE) return true;
|
|
const config = this.workerState.config.deps?.web || {};
|
|
const [modulePath] = fileUrl.split("?");
|
|
if (config.transformCss && CSS_LANGS_RE.test(modulePath)) return true;
|
|
if (config.transformAssets && KNOWN_ASSET_RE.test(modulePath)) return true;
|
|
if (toArray(config.transformGlobPattern).some((pattern) => pattern.test(modulePath))) return true;
|
|
return false;
|
|
};
|
|
}
|
|
|
|
const { existsSync } = fs__default;
|
|
// always defined when we use vm pool
|
|
const nativeResolve = import.meta.resolve;
|
|
// a relative ESM specifier resolves by plain URL join — Node's resolver adds
|
|
// no information for these (it does not check existence and relative
|
|
// specifiers never consult package.json), but it re-derives the package
|
|
// scope on every uncached call, re-parsing large `exports` maps. Restricted
|
|
// to a conservative charset so anything URL-special falls back to Node.
|
|
const SIMPLE_RELATIVE_SPECIFIER_RE = /^\.{1,2}\/[\w\-./]+$/;
|
|
// TODO: improve Node.js strict mode support in #2854
|
|
class ExternalModulesExecutor {
|
|
options;
|
|
cjs;
|
|
esm;
|
|
vite;
|
|
context;
|
|
fs;
|
|
codeCache;
|
|
resolvers = [];
|
|
#networkSupported = null;
|
|
constructor(options) {
|
|
this.options = options;
|
|
this.context = options.context;
|
|
this.fs = options.fileMap;
|
|
this.codeCache = options.codeCache;
|
|
this.esm = new EsmExecutor(this, { context: this.context });
|
|
setActiveVmExecutor(this);
|
|
this.cjs = new CommonjsExecutor({
|
|
context: this.context,
|
|
fileMap: options.fileMap,
|
|
codeCache: options.codeCache,
|
|
interopDefault: options.interopDefault,
|
|
shouldRequireAsEsm: this.shouldRequireAsEsm,
|
|
requireEsm: this.requireEsm
|
|
});
|
|
this.vite = new ViteExecutor({
|
|
esmExecutor: this.esm,
|
|
context: this.context,
|
|
transform: options.transform,
|
|
viteClientModule: options.viteClientModule
|
|
});
|
|
this.resolvers = [this.vite.resolve];
|
|
}
|
|
async import(identifier) {
|
|
const module = await this.createModule(identifier);
|
|
await this.esm.evaluateModule(module);
|
|
return module.namespace;
|
|
}
|
|
require(identifier) {
|
|
return this.cjs.require(identifier);
|
|
}
|
|
createRequire(identifier) {
|
|
return this.cjs.createRequire(identifier);
|
|
}
|
|
#esmSyntaxCache = /* @__PURE__ */ new Map();
|
|
// require() dispatches to the sync ESM loader only for files that are
|
|
// explicitly marked as ESM (.mjs or a "type": "module" package scope).
|
|
// JSON files keep the CJS json loader for Node require() parity — the
|
|
// extension wins over the package scope.
|
|
shouldRequireAsEsm = (resolvedPath) => {
|
|
if (!supportsSyncEsmEvaluate) return false;
|
|
const information = this.getModuleInformation(resolvedPath);
|
|
if (information.type !== "module" || information.path.endsWith(".json")) return false;
|
|
if (information.path.endsWith(".mjs")) return true;
|
|
// A .js file in an ESM package scope may still contain plain CJS code —
|
|
// Node evaluates it as ESM with injected CJS module variables (module,
|
|
// require, __filename), which a vm SourceTextModule cannot emulate.
|
|
// Files without ESM syntax keep loading through the CJS executor; a
|
|
// false negative here is corrected by its ESM-syntax fallback.
|
|
let syntax = this.#esmSyntaxCache.get(information.path);
|
|
if (syntax == null) {
|
|
syntax = hasEsmSyntax(this.fs.readFile(information.path));
|
|
this.#esmSyntaxCache.set(information.path, syntax);
|
|
}
|
|
return syntax;
|
|
};
|
|
requireEsm = (resolvedPath) => {
|
|
const { url } = this.getModuleInformation(resolvedPath);
|
|
const namespace = this.esm.requireEsModuleSync(url).namespace;
|
|
// Node parity: an ES module can define its own require() result with an
|
|
// export named "module.exports"
|
|
return "module.exports" in namespace ? namespace["module.exports"] : namespace;
|
|
};
|
|
resolveSyncSpecifier = (specifier, referencer) => {
|
|
const resolved = this.resolve(specifier, referencer);
|
|
if (resolved instanceof Promise) throw createRequireAsyncModuleError(referencer, `"${specifier}" cannot be resolved synchronously`);
|
|
return resolved;
|
|
};
|
|
// the sync counterpart of `createModule`, used by the require(esm) graph
|
|
// walker. `forceEsmSource` loads a 'commonjs'-typed file as ES module
|
|
// source — the CJS executor requests this after its parser rejected a .js
|
|
// file that contains ESM syntax.
|
|
materializeSyncModule = (identifier, forceEsmSource) => {
|
|
const information = this.getModuleInformation(identifier);
|
|
const { type, path } = information;
|
|
this.assertModuleExists(information);
|
|
switch (type) {
|
|
case "builtin": return {
|
|
kind: "ready",
|
|
module: this.cjs.getCoreSyntheticModule(identifier)
|
|
};
|
|
case "module":
|
|
case "commonjs":
|
|
if (type === "commonjs" && !forceEsmSource) return {
|
|
kind: "ready",
|
|
module: this.cjs.getCjsSyntheticModule(path, identifier)
|
|
};
|
|
if (path.endsWith(".json")) return {
|
|
kind: "json",
|
|
code: this.fs.readFile(path)
|
|
};
|
|
return {
|
|
kind: "source",
|
|
code: this.fs.readFile(path)
|
|
};
|
|
case "data":
|
|
// data: URIs are materialized by the ESM executor before it consults
|
|
// the external executor
|
|
throw new Error(`[vitest] Unexpected data: module ${identifier} in the sync module walker. This is a bug in Vitest.`);
|
|
case "vite": throw createRequireAsyncModuleError(identifier, "the module is transformed by Vite, which is asynchronous");
|
|
case "wasm": throw createRequireAsyncModuleError(identifier, "WebAssembly modules cannot be loaded synchronously");
|
|
case "network": throw createRequireAsyncModuleError(identifier, "network modules cannot be loaded synchronously");
|
|
default: return type;
|
|
}
|
|
};
|
|
// dynamic import can be used in both ESM and CJS, so we have it in the executor
|
|
importModuleDynamically = async (specifier, referencer) => {
|
|
const module = await this.resolveModule(specifier, referencer.identifier);
|
|
return await this.esm.evaluateModule(module);
|
|
};
|
|
resolveModule = async (specifier, referencer) => {
|
|
let identifier = this.resolve(specifier, referencer);
|
|
if (identifier instanceof Promise) identifier = await identifier;
|
|
return await this.createModule(identifier);
|
|
};
|
|
resolve(specifier, parent) {
|
|
for (const resolver of this.resolvers) {
|
|
const id = resolver(specifier, parent);
|
|
if (id) return id;
|
|
}
|
|
if (SIMPLE_RELATIVE_SPECIFIER_RE.test(specifier) && parent.startsWith("file://")) return new URL(specifier, parent).href;
|
|
// resolution of externalized modules is stable for the lifetime of the
|
|
// worker (like fileMap/packageCache), while fresh vm contexts re-resolve
|
|
// every import edge
|
|
const cache = this.options.resolveCache;
|
|
const key = cache ? `${parent}\n${specifier}` : void 0;
|
|
if (cache) {
|
|
const cached = cache.get(key);
|
|
if (cached !== void 0) return cached;
|
|
}
|
|
// import.meta.resolve can be asynchronous in older +18 Node versions
|
|
const resolved = nativeResolve(specifier, parent);
|
|
if (cache && typeof resolved === "string") cache.set(key, resolved);
|
|
return resolved;
|
|
}
|
|
getModuleInformation(identifier) {
|
|
const cached = this.options.moduleInfoCache?.get(identifier);
|
|
if (cached) return cached;
|
|
const info = this.resolveModuleInformation(identifier);
|
|
this.options.moduleInfoCache?.set(identifier, info);
|
|
return info;
|
|
}
|
|
resolveModuleInformation(identifier) {
|
|
if (identifier.startsWith("data:")) return {
|
|
type: "data",
|
|
url: identifier,
|
|
path: identifier
|
|
};
|
|
const { file, postfix } = splitFileAndPostfix(identifier);
|
|
const extension = extname(file);
|
|
if (extension === ".node" || isBuiltin(identifier)) return {
|
|
type: "builtin",
|
|
url: identifier,
|
|
path: identifier
|
|
};
|
|
if (this.isNetworkSupported && (identifier.startsWith("http:") || identifier.startsWith("https:"))) return {
|
|
type: "network",
|
|
url: identifier,
|
|
path: identifier
|
|
};
|
|
const isFileUrl = identifier.startsWith("file://");
|
|
const pathUrl = isFileUrl ? fileURLToPath(file) : file;
|
|
const fileUrl = isFileUrl ? identifier : `${pathToFileURL(file)}${postfix}`;
|
|
let type;
|
|
if (this.vite.canResolve(fileUrl)) type = "vite";
|
|
else if (extension === ".mjs") type = "module";
|
|
else if (extension === ".cjs") type = "commonjs";
|
|
else if (extension === ".wasm")
|
|
// still experimental on NodeJS --experimental-wasm-modules
|
|
// cf. ESM_FILE_FORMAT(url) in https://nodejs.org/docs/latest-v20.x/api/esm.html#resolution-algorithm
|
|
type = "wasm";
|
|
else type = lookupPackageScopeType(normalize(pathUrl)) === "esm" ? "module" : "commonjs";
|
|
return {
|
|
type,
|
|
path: pathUrl,
|
|
url: fileUrl
|
|
};
|
|
}
|
|
// create ERR_MODULE_NOT_FOUND on our own since latest NodeJS's import.meta.resolve doesn't throw on non-existing namespace or path
|
|
// https://github.com/nodejs/node/pull/49038
|
|
assertModuleExists(information) {
|
|
const { type, path } = information;
|
|
if (type === "module" || type === "commonjs" || type === "wasm") information.exists ??= existsSync(path);
|
|
if (information.exists === false) {
|
|
const error = /* @__PURE__ */ new Error(`Cannot find ${isBareImport(path) ? "package" : "module"} '${path}'`);
|
|
error.code = "ERR_MODULE_NOT_FOUND";
|
|
throw error;
|
|
}
|
|
}
|
|
createModule(identifier) {
|
|
const information = this.getModuleInformation(identifier);
|
|
const { type, url, path } = information;
|
|
this.assertModuleExists(information);
|
|
switch (type) {
|
|
case "data": return this.esm.createDataModule(identifier);
|
|
case "builtin": return this.cjs.getCoreSyntheticModule(identifier);
|
|
case "vite": return this.vite.createViteModule(url);
|
|
case "wasm": return this.esm.createWebAssemblyModule(url, () => this.fs.readBuffer(path));
|
|
case "module": return this.esm.createEsModule(url, () => this.fs.readFile(path));
|
|
case "commonjs": return this.cjs.getCjsSyntheticModule(path, identifier);
|
|
case "network": return this.esm.createNetworkModule(url);
|
|
default: return type;
|
|
}
|
|
}
|
|
get isNetworkSupported() {
|
|
if (this.#networkSupported == null) if (process.execArgv.includes("--experimental-network-imports")) this.#networkSupported = true;
|
|
else if (process.env.NODE_OPTIONS?.includes("--experimental-network-imports")) this.#networkSupported = true;
|
|
else this.#networkSupported = false;
|
|
return this.#networkSupported;
|
|
}
|
|
}
|
|
|
|
const { promises, readFileSync } = fs__default;
|
|
class FileMap {
|
|
fsCache = /* @__PURE__ */ new Map();
|
|
fsBufferCache = /* @__PURE__ */ new Map();
|
|
async readFileAsync(path) {
|
|
const cached = this.fsCache.get(path);
|
|
if (cached != null) return cached;
|
|
const source = await promises.readFile(path, "utf-8");
|
|
this.fsCache.set(path, source);
|
|
return source;
|
|
}
|
|
readFile(path) {
|
|
const cached = this.fsCache.get(path);
|
|
if (cached != null) return cached;
|
|
const source = readFileSync(path, "utf-8");
|
|
this.fsCache.set(path, source);
|
|
return source;
|
|
}
|
|
readBuffer(path) {
|
|
const cached = this.fsBufferCache.get(path);
|
|
if (cached != null) return cached;
|
|
const buffer = readFileSync(path);
|
|
this.fsBufferCache.set(path, buffer);
|
|
return buffer;
|
|
}
|
|
}
|
|
|
|
const entryFile = pathToFileURL(resolve(distDir, "workers/runVmTests.js")).href;
|
|
const fileMap = new FileMap();
|
|
const packageCache = /* @__PURE__ */ new Map();
|
|
const codeCache = new CodeCache();
|
|
const resolveCache = /* @__PURE__ */ new Map();
|
|
const moduleInfoCache = /* @__PURE__ */ new Map();
|
|
async function runVmTests(method, state, traces) {
|
|
const { ctx, rpc } = state;
|
|
const beforeEnvironmentTime = performance.now();
|
|
const { environment } = await loadEnvironment(ctx.environment.name, ctx.config.root, rpc, traces, true);
|
|
state.environment = environment;
|
|
// let the server transform this file's import graph while this worker is
|
|
// busy setting up the environment (jsdom takes ~0.5s per worker) —
|
|
// the server is otherwise idle during that window on a cold start.
|
|
if (environment.prewarmModules !== false) rpc.prewarmModuleGraph(environment.viteEnvironment || environment.name, ctx.files.map((file) => file.filepath)).catch(() => {});
|
|
if (!environment.setupVM) {
|
|
const envName = ctx.environment.name;
|
|
const packageId = envName[0] === "." ? envName : `vitest-environment-${envName}`;
|
|
throw new TypeError(`Environment "${ctx.environment.name}" is not a valid environment. Path "${packageId}" doesn't support vm environment because it doesn't provide "setupVM" method.`);
|
|
}
|
|
const vm = await traces.$("vitest.runtime.environment.setup", { attributes: {
|
|
"vitest.environment": environment.name,
|
|
"vitest.environment.vite_environment": environment.viteEnvironment || environment.name
|
|
} }, () => environment.setupVM(ctx.environment.options || ctx.config.environmentOptions || {}));
|
|
state.durations.environment = performance.now() - beforeEnvironmentTime;
|
|
process.env.VITEST_VM_POOL = "1";
|
|
if (!vm.getVmContext) throw new TypeError(`Environment ${environment.name} doesn't provide "getVmContext" method. It should return a context created by "vm.createContext" method.`);
|
|
const context = vm.getVmContext();
|
|
if (!isContext(context)) throw new TypeError(`Environment ${environment.name} doesn't provide a valid context. It should be created by "vm.createContext" method.`);
|
|
// captured before vitest installs its own globals (worker state, console,
|
|
// mocker, executor symbol): they reference the test file's module graph, so
|
|
// the teardown strip must treat them as removable, not as pristine
|
|
const initialContextKeys = captureContextKeys(context);
|
|
provideWorkerState(context, state);
|
|
// this is unfortunately needed for our own dependencies
|
|
// we need to find a way to not rely on this by default
|
|
// because browser doesn't provide these globals
|
|
context.process = process;
|
|
context.global = context;
|
|
context.console = state.config.disableConsoleIntercept ? console : createCustomConsole(state);
|
|
// TODO: don't hardcode setImmediate in fake timers defaults
|
|
context.setImmediate = setImmediate;
|
|
context.clearImmediate = clearImmediate;
|
|
const stubs = getDefaultRequestStubs(context);
|
|
const externalModulesExecutor = new ExternalModulesExecutor({
|
|
context,
|
|
fileMap,
|
|
codeCache,
|
|
resolveCache,
|
|
moduleInfoCache,
|
|
packageCache,
|
|
transform: rpc.transform,
|
|
viteClientModule: stubs["/@vite/client"]
|
|
});
|
|
process.exit = (code = process.exitCode || 0) => {
|
|
const filepath = state.filepath;
|
|
throw new Error(`process.exit unexpectedly called with "${code}"${filepath ? ` (test file: ${filepath})` : ""}`);
|
|
};
|
|
listenForErrors(() => state);
|
|
const moduleRunner = startVitestModuleRunner({
|
|
context,
|
|
evaluatedModules: state.evaluatedModules,
|
|
state,
|
|
externalModulesExecutor,
|
|
createImportMeta: createNodeImportMeta,
|
|
traces
|
|
});
|
|
emitModuleRunner(moduleRunner);
|
|
Object.defineProperty(context, VITEST_VM_CONTEXT_SYMBOL, {
|
|
value: {
|
|
context,
|
|
externalModulesExecutor
|
|
},
|
|
configurable: true,
|
|
enumerable: false,
|
|
writable: false
|
|
});
|
|
context.__vitest_mocker__ = moduleRunner.mocker;
|
|
setupEnv(ctx.config.env, state.metaEnv);
|
|
if (ctx.config.serializedDefines) try {
|
|
runInContext(ctx.config.serializedDefines, context, { filename: "virtual:load-defines.js" });
|
|
} catch (error) {
|
|
throw new Error(`Failed to load custom "defines": ${error.message}`);
|
|
}
|
|
await moduleRunner.mocker.initializeSpyModule();
|
|
const { run } = await moduleRunner.import(entryFile);
|
|
try {
|
|
await run(method, ctx.files, ctx.config, moduleRunner, traces);
|
|
} finally {
|
|
await traces.$("vitest.runtime.environment.teardown", () => vm.teardown?.());
|
|
// unregisters the runner from Vite's `Error.prepareStackTrace` interceptor:
|
|
// its module-level cache holds `evaluatedModules` of every runner it has
|
|
// seen, which would otherwise keep each test file's entire module graph
|
|
// (and with it the vm context) alive for the lifetime of the worker
|
|
await moduleRunner.close();
|
|
stripDisposedContext(context, initialContextKeys);
|
|
setActiveVmExecutor(void 0);
|
|
}
|
|
}
|
|
function setupVmWorker(context) {
|
|
if (context.config.experimental.viteModuleRunner === false) throw new Error(`Pool "${context.pool}" cannot run with "experimental.viteModuleRunner: false". Please, use "threads" or "forks" instead.`);
|
|
// V8's isolate-level compilation cache keeps evaluated `vm.SourceTextModule`s
|
|
// (and everything their module state references) alive until a
|
|
// memory-pressure GC clears the cache, which in practice means every test
|
|
// file's world accumulates until the worker hits `vmMemoryLimit`. The
|
|
// compiled-code caching the flag disables is already covered by the
|
|
// worker's own script and code caches.
|
|
v8.setFlagsFromString("--no-compilation-cache");
|
|
}
|
|
|
|
export { runVmTests as r, setupVmWorker as s };
|