From 76cb9c2a39d477a64824a985ade40507e3bbade1 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Fri, 13 Feb 2026 21:34:48 -0800 Subject: feat(vanilla): add testing infrastructure and tests (NK-wjnczv) --- .../@vitest/mocker/dist/chunk-mocker.js | 521 +++++++++++++++++++++ 1 file changed, 521 insertions(+) create mode 100644 vanilla/node_modules/@vitest/mocker/dist/chunk-mocker.js (limited to 'vanilla/node_modules/@vitest/mocker/dist/chunk-mocker.js') diff --git a/vanilla/node_modules/@vitest/mocker/dist/chunk-mocker.js b/vanilla/node_modules/@vitest/mocker/dist/chunk-mocker.js new file mode 100644 index 0000000..d86b758 --- /dev/null +++ b/vanilla/node_modules/@vitest/mocker/dist/chunk-mocker.js @@ -0,0 +1,521 @@ +import { mockObject } from './index.js'; +import { M as MockerRegistry, R as RedirectedModule, A as AutomockedModule } from './chunk-registry.js'; +import { e as extname, j as join } from './chunk-pathe.M-eThtNZ.js'; + +/** +* Get original stacktrace without source map support the most performant way. +* - Create only 1 stack frame. +* - Rewrite prepareStackTrace to bypass "support-stack-trace" (usually takes ~250ms). +*/ +function createSimpleStackTrace(options) { + const { message = "$$stack trace error", stackTraceLimit = 1 } = options || {}; + const limit = Error.stackTraceLimit; + const prepareStackTrace = Error.prepareStackTrace; + Error.stackTraceLimit = stackTraceLimit; + Error.prepareStackTrace = (e) => e.stack; + const err = new Error(message); + const stackTrace = err.stack || ""; + Error.prepareStackTrace = prepareStackTrace; + Error.stackTraceLimit = limit; + return stackTrace; +} + +const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//; +function normalizeWindowsPath(input = "") { + if (!input) { + return input; + } + return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase()); +} +const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/; +function cwd() { + if (typeof process !== "undefined" && typeof process.cwd === "function") { + return process.cwd().replace(/\\/g, "/"); + } + return "/"; +} +const resolve = function(...arguments_) { + arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument)); + let resolvedPath = ""; + let resolvedAbsolute = false; + for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) { + const path = index >= 0 ? arguments_[index] : cwd(); + if (!path || path.length === 0) { + continue; + } + resolvedPath = `${path}/${resolvedPath}`; + resolvedAbsolute = isAbsolute(path); + } + resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute); + if (resolvedAbsolute && !isAbsolute(resolvedPath)) { + return `/${resolvedPath}`; + } + return resolvedPath.length > 0 ? resolvedPath : "."; +}; +function normalizeString(path, allowAboveRoot) { + let res = ""; + let lastSegmentLength = 0; + let lastSlash = -1; + let dots = 0; + let char = null; + for (let index = 0; index <= path.length; ++index) { + if (index < path.length) { + char = path[index]; + } else if (char === "/") { + break; + } else { + char = "/"; + } + if (char === "/") { + if (lastSlash === index - 1 || dots === 1); + else if (dots === 2) { + if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") { + if (res.length > 2) { + const lastSlashIndex = res.lastIndexOf("/"); + if (lastSlashIndex === -1) { + res = ""; + lastSegmentLength = 0; + } else { + res = res.slice(0, lastSlashIndex); + lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); + } + lastSlash = index; + dots = 0; + continue; + } else if (res.length > 0) { + res = ""; + lastSegmentLength = 0; + lastSlash = index; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + res += res.length > 0 ? "/.." : ".."; + lastSegmentLength = 2; + } + } else { + if (res.length > 0) { + res += `/${path.slice(lastSlash + 1, index)}`; + } else { + res = path.slice(lastSlash + 1, index); + } + lastSegmentLength = index - lastSlash - 1; + } + lastSlash = index; + dots = 0; + } else if (char === "." && dots !== -1) { + ++dots; + } else { + dots = -1; + } + } + return res; +} +const isAbsolute = function(p) { + return _IS_ABSOLUTE_RE.test(p); +}; + +var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +var intToChar = new Uint8Array(64); +var charToInt = new Uint8Array(128); +for (let i = 0; i < chars.length; i++) { + const c = chars.charCodeAt(i); + intToChar[i] = c; + charToInt[c] = i; +} +const CHROME_IE_STACK_REGEXP = /^\s*at .*(?:\S:\d+|\(native\))/m; +const SAFARI_NATIVE_CODE_REGEXP = /^(?:eval@)?(?:\[native code\])?$/; +function extractLocation(urlLike) { + // Fail-fast but return locations like "(native)" + if (!urlLike.includes(":")) { + return [urlLike]; + } + const regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/; + const parts = regExp.exec(urlLike.replace(/^\(|\)$/g, "")); + if (!parts) { + return [urlLike]; + } + let url = parts[1]; + if (url.startsWith("async ")) { + url = url.slice(6); + } + if (url.startsWith("http:") || url.startsWith("https:")) { + const urlObj = new URL(url); + urlObj.searchParams.delete("import"); + urlObj.searchParams.delete("browserv"); + url = urlObj.pathname + urlObj.hash + urlObj.search; + } + if (url.startsWith("/@fs/")) { + const isWindows = /^\/@fs\/[a-zA-Z]:\//.test(url); + url = url.slice(isWindows ? 5 : 4); + } + return [ + url, + parts[2] || undefined, + parts[3] || undefined + ]; +} +function parseSingleFFOrSafariStack(raw) { + let line = raw.trim(); + if (SAFARI_NATIVE_CODE_REGEXP.test(line)) { + return null; + } + if (line.includes(" > eval")) { + line = line.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ":$1"); + } + // Early return for lines that don't look like Firefox/Safari stack traces + // Firefox/Safari stack traces must contain '@' and should have location info after it + if (!line.includes("@")) { + return null; + } + // Find the correct @ that separates function name from location + // For cases like '@https://@fs/path' or 'functionName@https://@fs/path' + // we need to find the first @ that precedes a valid location (containing :) + let atIndex = -1; + let locationPart = ""; + let functionName; + // Try each @ from left to right to find the one that gives us a valid location + for (let i = 0; i < line.length; i++) { + if (line[i] === "@") { + const candidateLocation = line.slice(i + 1); + // Minimum length 3 for valid location: 1 for filename + 1 for colon + 1 for line number (e.g., "a:1") + if (candidateLocation.includes(":") && candidateLocation.length >= 3) { + atIndex = i; + locationPart = candidateLocation; + functionName = i > 0 ? line.slice(0, i) : undefined; + break; + } + } + } + // Validate we found a valid location with minimum length (filename:line format) + if (atIndex === -1 || !locationPart.includes(":") || locationPart.length < 3) { + return null; + } + const [url, lineNumber, columnNumber] = extractLocation(locationPart); + if (!url || !lineNumber || !columnNumber) { + return null; + } + return { + file: url, + method: functionName || "", + line: Number.parseInt(lineNumber), + column: Number.parseInt(columnNumber) + }; +} +function parseSingleStack(raw) { + const line = raw.trim(); + if (!CHROME_IE_STACK_REGEXP.test(line)) { + return parseSingleFFOrSafariStack(line); + } + return parseSingleV8Stack(line); +} +// Based on https://github.com/stacktracejs/error-stack-parser +// Credit to stacktracejs +function parseSingleV8Stack(raw) { + let line = raw.trim(); + if (!CHROME_IE_STACK_REGEXP.test(line)) { + return null; + } + if (line.includes("(eval ")) { + line = line.replace(/eval code/g, "eval").replace(/(\(eval at [^()]*)|(,.*$)/g, ""); + } + let sanitizedLine = line.replace(/^\s+/, "").replace(/\(eval code/g, "(").replace(/^.*?\s+/, ""); + // capture and preserve the parenthesized location "(/foo/my bar.js:12:87)" in + // case it has spaces in it, as the string is split on \s+ later on + const location = sanitizedLine.match(/ (\(.+\)$)/); + // remove the parenthesized location from the line, if it was matched + sanitizedLine = location ? sanitizedLine.replace(location[0], "") : sanitizedLine; + // if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine + // because this line doesn't have function name + const [url, lineNumber, columnNumber] = extractLocation(location ? location[1] : sanitizedLine); + let method = location && sanitizedLine || ""; + let file = url && ["eval", ""].includes(url) ? undefined : url; + if (!file || !lineNumber || !columnNumber) { + return null; + } + if (method.startsWith("async ")) { + method = method.slice(6); + } + if (file.startsWith("file://")) { + file = file.slice(7); + } + // normalize Windows path (\ -> /) + file = file.startsWith("node:") || file.startsWith("internal:") ? file : resolve(file); + if (method) { + method = method.replace(/__vite_ssr_import_\d+__\./g, "").replace(/(Object\.)?__vite_ssr_export_default__\s?/g, ""); + } + return { + method, + file, + line: Number.parseInt(lineNumber), + column: Number.parseInt(columnNumber) + }; +} + +function createCompilerHints(options) { + const globalThisAccessor = (options === null || options === void 0 ? void 0 : options.globalThisKey) || "__vitest_mocker__"; + function _mocker() { + // @ts-expect-error injected by the plugin + return typeof globalThis[globalThisAccessor] !== "undefined" ? globalThis[globalThisAccessor] : new Proxy({}, { get(_, name) { + throw new Error("Vitest mocker was not initialized in this environment. " + `vi.${String(name)}() is forbidden.`); + } }); + } + return { + hoisted(factory) { + if (typeof factory !== "function") { + throw new TypeError(`vi.hoisted() expects a function, but received a ${typeof factory}`); + } + return factory(); + }, + mock(path, factory) { + if (typeof path !== "string") { + throw new TypeError(`vi.mock() expects a string path, but received a ${typeof path}`); + } + const importer = getImporter("mock"); + _mocker().queueMock(path, importer, typeof factory === "function" ? () => factory(() => _mocker().importActual(path, importer)) : factory); + }, + unmock(path) { + if (typeof path !== "string") { + throw new TypeError(`vi.unmock() expects a string path, but received a ${typeof path}`); + } + _mocker().queueUnmock(path, getImporter("unmock")); + }, + doMock(path, factory) { + if (typeof path !== "string") { + throw new TypeError(`vi.doMock() expects a string path, but received a ${typeof path}`); + } + const importer = getImporter("doMock"); + _mocker().queueMock(path, importer, typeof factory === "function" ? () => factory(() => _mocker().importActual(path, importer)) : factory); + }, + doUnmock(path) { + if (typeof path !== "string") { + throw new TypeError(`vi.doUnmock() expects a string path, but received a ${typeof path}`); + } + _mocker().queueUnmock(path, getImporter("doUnmock")); + }, + async importActual(path) { + return _mocker().importActual(path, getImporter("importActual")); + }, + async importMock(path) { + return _mocker().importMock(path, getImporter("importMock")); + } + }; +} +function getImporter(name) { + const stackTrace = /* @__PURE__ */ createSimpleStackTrace({ stackTraceLimit: 5 }); + const stackArray = stackTrace.split("\n"); + // if there is no message in a stack trace, use the item - 1 + const importerStackIndex = stackArray.findIndex((stack) => { + return stack.includes(` at Object.${name}`) || stack.includes(`${name}@`); + }); + const stack = /* @__PURE__ */ parseSingleStack(stackArray[importerStackIndex + 1]); + return (stack === null || stack === void 0 ? void 0 : stack.file) || ""; +} + +const hot = import.meta.hot || { + on: warn, + off: warn, + send: warn +}; +function warn() { + console.warn("Vitest mocker cannot work if Vite didn't establish WS connection."); +} +function rpc(event, data) { + hot.send(event, data); + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error(`Failed to resolve ${event} in time`)); + }, 5e3); + hot.on(`${event}:result`, function r(data) { + resolve(data); + clearTimeout(timeout); + hot.off(`${event}:result`, r); + }); + }); +} + +const { now } = Date; +class ModuleMocker { + registry = new MockerRegistry(); + queue = new Set(); + mockedIds = new Set(); + constructor(interceptor, rpc, createMockInstance, config) { + this.interceptor = interceptor; + this.rpc = rpc; + this.createMockInstance = createMockInstance; + this.config = config; + } + async prepare() { + if (!this.queue.size) { + return; + } + await Promise.all([...this.queue.values()]); + } + async resolveFactoryModule(id) { + const mock = this.registry.get(id); + if (!mock || mock.type !== "manual") { + throw new Error(`Mock ${id} wasn't registered. This is probably a Vitest error. Please, open a new issue with reproduction.`); + } + const result = await mock.resolve(); + return result; + } + getFactoryModule(id) { + const mock = this.registry.get(id); + if (!mock || mock.type !== "manual") { + throw new Error(`Mock ${id} wasn't registered. This is probably a Vitest error. Please, open a new issue with reproduction.`); + } + if (!mock.cache) { + throw new Error(`Mock ${id} wasn't resolved. This is probably a Vitest error. Please, open a new issue with reproduction.`); + } + return mock.cache; + } + async invalidate() { + const ids = Array.from(this.mockedIds); + if (!ids.length) { + return; + } + await this.rpc.invalidate(ids); + await this.interceptor.invalidate(); + this.registry.clear(); + } + async importActual(id, importer) { + const resolved = await this.rpc.resolveId(id, importer); + if (resolved == null) { + throw new Error(`[vitest] Cannot resolve "${id}" imported from "${importer}"`); + } + const ext = extname(resolved.id); + const url = new URL(resolved.url, location.href); + const query = `_vitest_original&ext${ext}`; + const actualUrl = `${url.pathname}${url.search ? `${url.search}&${query}` : `?${query}`}${url.hash}`; + return this.wrapDynamicImport(() => import( + /* @vite-ignore */ + actualUrl +)).then((mod) => { + if (!resolved.optimized || typeof mod.default === "undefined") { + return mod; + } + // vite injects this helper for optimized modules, so we try to follow the same behavior + const m = mod.default; + return (m === null || m === void 0 ? void 0 : m.__esModule) ? m : { + ...typeof m === "object" && !Array.isArray(m) || typeof m === "function" ? m : {}, + default: m + }; + }); + } + async importMock(rawId, importer) { + await this.prepare(); + const { resolvedId, resolvedUrl, redirectUrl } = await this.rpc.resolveMock(rawId, importer, { mock: "auto" }); + const mockUrl = this.resolveMockPath(cleanVersion(resolvedUrl)); + let mock = this.registry.get(mockUrl); + if (!mock) { + if (redirectUrl) { + const resolvedRedirect = new URL(this.resolveMockPath(cleanVersion(redirectUrl)), location.href).toString(); + mock = new RedirectedModule(rawId, resolvedId, mockUrl, resolvedRedirect); + } else { + mock = new AutomockedModule(rawId, resolvedId, mockUrl); + } + } + if (mock.type === "manual") { + return await mock.resolve(); + } + if (mock.type === "automock" || mock.type === "autospy") { + const url = new URL(`/@id/${resolvedId}`, location.href); + const query = url.search ? `${url.search}&t=${now()}` : `?t=${now()}`; + const moduleObject = await import( + /* @vite-ignore */ + `${url.pathname}${query}&mock=${mock.type}${url.hash}` +); + return this.mockObject(moduleObject, mock.type); + } + return import( + /* @vite-ignore */ + mock.redirect +); + } + mockObject(object, moduleType = "automock") { + return mockObject({ + globalConstructors: { + Object, + Function, + Array, + Map, + RegExp + }, + createMockInstance: this.createMockInstance, + type: moduleType + }, object); + } + queueMock(rawId, importer, factoryOrOptions) { + const promise = this.rpc.resolveMock(rawId, importer, { mock: typeof factoryOrOptions === "function" ? "factory" : (factoryOrOptions === null || factoryOrOptions === void 0 ? void 0 : factoryOrOptions.spy) ? "spy" : "auto" }).then(async ({ redirectUrl, resolvedId, resolvedUrl, needsInterop, mockType }) => { + const mockUrl = this.resolveMockPath(cleanVersion(resolvedUrl)); + this.mockedIds.add(resolvedId); + const factory = typeof factoryOrOptions === "function" ? async () => { + const data = await factoryOrOptions(); + // vite wraps all external modules that have "needsInterop" in a function that + // merges all exports from default into the module object + return needsInterop ? { default: data } : data; + } : undefined; + const mockRedirect = typeof redirectUrl === "string" ? new URL(this.resolveMockPath(cleanVersion(redirectUrl)), location.href).toString() : null; + let module; + if (mockType === "manual") { + module = this.registry.register("manual", rawId, resolvedId, mockUrl, factory); + } else if (mockType === "autospy") { + module = this.registry.register("autospy", rawId, resolvedId, mockUrl); + } else if (mockType === "redirect") { + module = this.registry.register("redirect", rawId, resolvedId, mockUrl, mockRedirect); + } else { + module = this.registry.register("automock", rawId, resolvedId, mockUrl); + } + await this.interceptor.register(module); + }).finally(() => { + this.queue.delete(promise); + }); + this.queue.add(promise); + } + queueUnmock(id, importer) { + const promise = this.rpc.resolveId(id, importer).then(async (resolved) => { + if (!resolved) { + return; + } + const mockUrl = this.resolveMockPath(cleanVersion(resolved.url)); + this.mockedIds.add(resolved.id); + this.registry.delete(mockUrl); + await this.interceptor.delete(mockUrl); + }).finally(() => { + this.queue.delete(promise); + }); + this.queue.add(promise); + } + // We need to await mock registration before importing the actual module + // In case there is a mocked module in the import chain + wrapDynamicImport(moduleFactory) { + if (typeof moduleFactory === "function") { + const promise = new Promise((resolve, reject) => { + this.prepare().finally(() => { + moduleFactory().then(resolve, reject); + }); + }); + return promise; + } + return moduleFactory; + } + resolveMockPath(path) { + const config = this.config; + const fsRoot = join("/@fs/", config.root); + // URL can be /file/path.js, but path is resolved to /file/path + if (path.startsWith(config.root)) { + return path.slice(config.root.length); + } + if (path.startsWith(fsRoot)) { + return path.slice(fsRoot.length); + } + return path; + } +} +const versionRegexp = /(\?|&)v=\w{8}/; +function cleanVersion(url) { + return url.replace(versionRegexp, ""); +} + +export { ModuleMocker as M, createCompilerHints as c, hot as h, rpc as r }; -- cgit v1.2.3