import { mkdirSync, writeFileSync } from 'node:fs'; import { C as CoverageProviderMap } from './coverage.BcXK1DsQ.js'; import { L as Logger, k as VitestPackageInstaller, P as PluginHarness, w as resolveConfig, V as Vitest, A as stdout, y as prompt, g as FilesNotFoundError, G as GitNotFoundError, I as IncludeTaskLocationDisabledError, C as RangeLocationFilterProvidedError, E as LocationFilterFileNotFoundError } from './index.B89dZ0-N.js'; import readline from 'node:readline'; import { y } from './tinyrainbow.Ht9iggcq.js'; import { i as isWindows } from './env.DzFJjrmK.js'; import { stripVTControlCharacters } from 'node:util'; import { e as createDefer, a as relative, r as resolve, d as dirname, i as isAbsolute } from './pathe.M-eThtNZ.Clq7AXhv.js'; async function createVitest(modeOrOptions, optionsOrViteOverrides = {}, viteOverridesOrVitestOptions = {}, maybeVitestOptions = {}) { let options; let viteOverrides; let vitestOptions; if (typeof modeOrOptions === "string") { options = optionsOrViteOverrides; viteOverrides = viteOverridesOrVitestOptions; vitestOptions = maybeVitestOptions; } else { options = modeOrOptions; viteOverrides = optionsOrViteOverrides; vitestOptions = viteOverridesOrVitestOptions; } const logger = new Logger(vitestOptions.stdout, vitestOptions.stderr); const packageInstaller = vitestOptions.packageInstaller ?? new VitestPackageInstaller(); const pluginHarness = new PluginHarness(logger, packageInstaller); const config = await resolveConfig(options, viteOverrides, pluginHarness); const vitest = new Vitest(pluginHarness, config); try { await vitest._start(config); if (vitest.config.api.port && vitest.config.ui && vitest.config.open) vitest.vite.openBrowser(); return vitest; } // Vitest can fail at any point during setup or inside a custom plugin. // Make sure everything is properly closed (like the logger). catch (error) { await vitest.close(); throw error; } } const MAX_RESULT_COUNT = 10; const SELECTION_MAX_INDEX = 7; const ESC = "\x1B["; class WatchFilter { filterRL; currentKeyword = void 0; message; results = []; selectionIndex = -1; onKeyPress; stdin; stdout; constructor(message, stdin = process.stdin, stdout$1 = stdout()) { this.message = message; this.stdin = stdin; this.stdout = stdout$1; this.filterRL = readline.createInterface({ input: this.stdin, escapeCodeTimeout: 50 }); readline.emitKeypressEvents(this.stdin, this.filterRL); if (this.stdin.isTTY) this.stdin.setRawMode(true); } async filter(filterFunc) { this.write(this.promptLine()); const resultPromise = createDefer(); this.onKeyPress = this.filterHandler(filterFunc, (result) => { resultPromise.resolve(result); }); this.stdin.on("keypress", this.onKeyPress); try { return await resultPromise; } finally { this.close(); } } filterHandler(filterFunc, onSubmit) { return async (str, key) => { switch (true) { case key.sequence === "": if (this.currentKeyword && this.currentKeyword?.length > 1) this.currentKeyword = this.currentKeyword?.slice(0, -1); else this.currentKeyword = void 0; break; case key?.ctrl && key?.name === "c": case key?.name === "escape": this.write(`${ESC}1G${ESC}0J`); onSubmit(void 0); return; case key?.name === "enter": case key?.name === "return": { const selection = this.results[this.selectionIndex]; onSubmit((typeof selection === "string" ? selection : selection?.key) || this.currentKeyword || ""); this.currentKeyword = void 0; break; } case key?.name === "up": if (this.selectionIndex && this.selectionIndex > 0) this.selectionIndex--; else this.selectionIndex = -1; break; case key?.name === "down": if (this.selectionIndex < this.results.length - 1) this.selectionIndex++; else if (this.selectionIndex >= this.results.length - 1) this.selectionIndex = this.results.length - 1; break; case !key?.ctrl && !key?.meta: if (this.currentKeyword === void 0) this.currentKeyword = str; else this.currentKeyword += str || ""; } if (this.currentKeyword) this.results = await filterFunc(this.currentKeyword); this.render(); }; } render() { let printStr = this.promptLine(); if (!this.currentKeyword) printStr += "\nPlease input filter pattern"; else if (this.currentKeyword && this.results.length === 0) printStr += "\nPattern matches no results"; else { const resultCountLine = this.results.length === 1 ? `Pattern matches ${this.results.length} result` : `Pattern matches ${this.results.length} results`; let resultBody = ""; if (this.results.length > MAX_RESULT_COUNT) { const offset = this.selectionIndex > SELECTION_MAX_INDEX ? this.selectionIndex - SELECTION_MAX_INDEX : 0; const displayResults = this.results.slice(offset, MAX_RESULT_COUNT + offset); const remainingResultCount = this.results.length - offset - displayResults.length; resultBody = `${displayResults.map((result, index) => index + offset === this.selectionIndex ? y.green(` › ${result}`) : y.dim(` › ${result}`)).join("\n")}`; if (remainingResultCount > 0) resultBody += ` ${y.dim(` ...and ${remainingResultCount} more ${remainingResultCount === 1 ? "result" : "results"}`)}`; } else resultBody = this.results.map((result, index) => index === this.selectionIndex ? y.green(` › ${result}`) : y.dim(` › ${result}`)).join("\n"); printStr += `\n${resultCountLine}\n${resultBody}`; } this.eraseAndPrint(printStr); this.restoreCursor(); } keywordOffset() { return `? ${this.message} › `.length + 1; } promptLine() { return `${y.cyan("?")} ${y.bold(this.message)} › ${this.currentKeyword || ""}`; } eraseAndPrint(str) { let rows = 0; const lines = str.split(/\r?\n/); for (const line of lines) { const columns = "columns" in this.stdout ? this.stdout.columns : 80; // We have to take care of screen width in case of long lines rows += 1 + Math.floor(Math.max(stripVTControlCharacters(line).length - 1, 0) / columns); } this.write(`${ESC}1G`); this.write(`${ESC}J`); this.write(str); this.write(`${ESC}${rows - 1}A`); } close() { this.filterRL.close(); if (this.onKeyPress) this.stdin.removeListener("keypress", this.onKeyPress); if (this.stdin.isTTY) this.stdin.setRawMode(false); } restoreCursor() { const cursorPos = this.keywordOffset() + (this.currentKeyword?.length || 0); this.write(`${ESC}${cursorPos}G`); } write(data) { this.stdout.write(data); } getLastResults() { return this.results.map((r) => typeof r === "string" ? r : r.toString()); } } const keys = [ [["a", "return"], "rerun all tests"], ["r", "rerun current pattern tests"], ["f", "rerun only failed tests"], ["u", "update snapshot"], ["p", "filter by a filename"], ["t", "filter by a test name regex pattern"], ["w", "filter by a project name"], ["q", "quit"] ]; const cancelKeys = [ "space", "c", "h", ...keys.map((key) => key[0]).flat() ]; function printShortcutsHelp() { stdout().write(` ${y.bold(" Watch Usage")} ${keys.map((i) => y.dim(" press ") + y.reset([i[0]].flat().map(y.bold).join(", ")) + y.dim(` to ${i[1]}`)).join("\n")} `); } function* traverseFilteredTestNames(parentName, filter, t) { if (t.type === "test") { if (t.name.match(filter)) { const displayName = `${parentName} > ${t.name}`; yield { key: t.name, toString: () => displayName }; } } else { parentName = parentName.length ? `${parentName} > ${t.name}` : t.name; for (const task of t.tasks) yield* traverseFilteredTestNames(parentName, filter, task); } } function* getFilteredTestNames(pattern, suite) { try { const reg = new RegExp(pattern); // TODO: we cannot run tests per workspace yet: filtering files const files = /* @__PURE__ */ new Set(); for (const file of suite) if (!files.has(file.name)) { files.add(file.name); yield* traverseFilteredTestNames("", reg, file); } } catch {} } function registerConsoleShortcuts(ctx, stdin = process.stdin, stdout) { let latestFilename = ""; async function _keypressHandler(str, key) { // Cancel run and exit when ctrl-c or esc is pressed. // If cancelling takes long and key is pressed multiple times, exit forcefully. if (str === "" || str === "\x1B" || key && key.ctrl && key.name === "c") { if (!ctx.isCancelling) { ctx.logger.log(y.red("Cancelling test run. Press CTRL+c again to exit forcefully.\n")); process.exitCode = 130; // Unregister raw mode so that second CTRL+c is handled by Node.js as SIGINT off(); await ctx.cancelCurrentRun("keyboard-input"); } return ctx.exit(true); } // window not support suspend if (!isWindows && key && key.ctrl && key.name === "z") { process.kill(process.ppid, "SIGTSTP"); process.kill(process.pid, "SIGTSTP"); return; } const name = key?.name; if (ctx.runningPromise) { if (cancelKeys.includes(name)) await ctx.cancelCurrentRun("keyboard-input"); return; } // quit if (name === "q") return ctx.exit(true); // help if (name === "h") return printShortcutsHelp(); // update snapshot if (name === "u") return ctx.updateSnapshot(); // rerun all tests if (name === "a" || name === "return") { const files = await ctx._globTestFilepaths(); return ctx.changeNamePattern("", files, "rerun all tests"); } // rerun current pattern tests if (name === "r") return ctx.rerunFiles(); // rerun only failed tests if (name === "f") return ctx.rerunFailed(); // change project filter if (name === "w") return inputProjectName(); // change testNamePattern if (name === "t") return inputNamePattern(); // change fileNamePattern if (name === "p") return inputFilePattern(); } async function keypressHandler(str, key) { await _keypressHandler(str, key); } async function inputNamePattern() { off(); const filter = await new WatchFilter("Input test name pattern (RegExp)", stdin, stdout).filter((str) => { return [...getFilteredTestNames(str, ctx.state.getFiles())]; }); on(); if (typeof filter === "undefined") return; const files = ctx.state.getFilepaths(); // if running in standalone mode, Vitest instance doesn't know about any test file const cliFiles = ctx.config.standalone && !files.length ? await ctx._globTestFilepaths() : void 0; await ctx.changeNamePattern(filter?.trim() || "", cliFiles, "change pattern"); } async function inputProjectName() { off(); const { filter = "" } = await prompt([{ name: "filter", type: "text", message: "Input a single project name", initial: ctx.config.project[0] || "" }]); on(); await ctx.changeProjectName(filter.trim()); } async function inputFilePattern() { off(); const watchFilter = new WatchFilter("Input filename pattern", stdin, stdout); const filter = await watchFilter.filter(async (str) => { return (await ctx.globTestSpecifications([str])).map((specification) => relative(ctx.config.root, specification.moduleId)).filter((file, index, all) => all.indexOf(file) === index); }); on(); if (typeof filter === "undefined") return; latestFilename = filter?.trim() || ""; const lastResults = watchFilter.getLastResults(); await ctx.changeFilenamePattern(latestFilename, filter && lastResults.length ? lastResults.map((i) => resolve(ctx.config.root, i)) : void 0); } let rl; function on() { off(); rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 50 }); readline.emitKeypressEvents(stdin, rl); if (stdin.isTTY) stdin.setRawMode(true); stdin.on("keypress", keypressHandler); } function off() { rl?.close(); rl = void 0; stdin.removeListener("keypress", keypressHandler); if (stdin.isTTY) stdin.setRawMode(false); } on(); return function cleanup() { off(); }; } async function startVitest(modeOrCliFilters, cliFiltersOrOptions, optionsOrViteOverrides, viteOverridesOrVitestOptions, maybeVitestOptions) { let cliFilters; let options; let viteOverrides; let vitestOptions; if (typeof modeOrCliFilters === "string") { cliFilters = cliFiltersOrOptions ?? []; options = optionsOrViteOverrides ?? {}; viteOverrides = viteOverridesOrVitestOptions; vitestOptions = maybeVitestOptions; } else { cliFilters = modeOrCliFilters ?? []; options = cliFiltersOrOptions ?? {}; viteOverrides = optionsOrViteOverrides; vitestOptions = viteOverridesOrVitestOptions; } const root = resolve(options.root || process.cwd()); const ctx = await prepareVitest(options, viteOverrides, vitestOptions, cliFilters); if (ctx._coverageOptions.enabled) { const provider = ctx._coverageOptions.provider || "v8"; const requiredPackages = CoverageProviderMap[provider]; if (requiredPackages) { if (!await ctx.packageInstaller.ensureInstalled(requiredPackages, root, ctx.version)) { process.exitCode = 1; return ctx; } } } const stdin = vitestOptions?.stdin || process.stdin; const stdout = vitestOptions?.stdout || process.stdout; let stdinCleanup; if (stdin.isTTY && ctx.config.watch) stdinCleanup = registerConsoleShortcuts(ctx, stdin, stdout); ctx.onAfterSetServer(async () => { if (ctx.closingPromise) return; try { if (ctx.config.standalone) await ctx.standalone(); else await ctx.start(cliFilters); } catch (error) { reportStartError(ctx, error); } }); try { if (ctx.config.listTags) await ctx.listTags(); else if (ctx.config.clearCache) await ctx.clearCache(); else if (ctx.config.mergeReports) await ctx.mergeReports(); else if (ctx.config.standalone) await ctx.standalone(); else await ctx.start(cliFilters); return ctx; } catch (e) { reportStartError(ctx, e); return ctx; } finally { if (!ctx?.shouldKeepServer()) { stdinCleanup?.(); await ctx.close(); } } } function reportStartError(ctx, error) { if (error instanceof FilesNotFoundError) return; if (error instanceof GitNotFoundError) { ctx.logger.error(error.message); return; } if (error instanceof IncludeTaskLocationDisabledError || error instanceof RangeLocationFilterProvidedError || error instanceof LocationFilterFileNotFoundError) { ctx.logger.printError(error, { verbose: false }); return; } process.exitCode = 1; ctx.logger.printError(error, { fullStack: true, type: "Unhandled Error" }); ctx.logger.error("\n\n"); } async function prepareVitest(modeOrOptions, optionsOrViteOverrides, viteOverridesOrVitestOptions, vitestOptionsOrCliFilters, maybeCliFilters) { let options; let viteOverrides; let vitestOptions; let cliFilters; if (typeof modeOrOptions === "string") { options = optionsOrViteOverrides ?? {}; viteOverrides = viteOverridesOrVitestOptions; vitestOptions = vitestOptionsOrCliFilters; cliFilters = maybeCliFilters; } else { options = modeOrOptions ?? {}; viteOverrides = optionsOrViteOverrides; vitestOptions = viteOverridesOrVitestOptions; cliFilters = vitestOptionsOrCliFilters; } process.env.TEST = "true"; process.env.VITEST = "true"; process.env.NODE_ENV ??= "test"; if (options.run) options.watch = false; if (options.standalone && (cliFilters?.length || 0) > 0) options.standalone = false; // this shouldn't affect _application root_ that can be changed inside config const root = resolve(options.root || process.cwd()); const ctx = await createVitest(options, viteOverrides, vitestOptions); const environmentPackage = getEnvPackageName(ctx.config.environment); if (environmentPackage && !await ctx.packageInstaller.ensureInstalled(environmentPackage, root)) { process.exitCode = 1; return ctx; } return ctx; } function processCollected(ctx, files, options) { let errorsPrinted = false; forEachSuite(files, (suite) => { suite.errors().forEach((error) => { errorsPrinted = true; ctx.logger.printError(error, { project: suite.project }); }); }); if (errorsPrinted) return; if (typeof options.json !== "undefined") return processJsonOutput(files, options); return formatCollectedAsString(files).forEach((test) => console.log(test)); } function outputFileList(files, options) { if (typeof options.json !== "undefined") return outputJsonFileList(files, options); formatFilesAsString(files, options).map((file) => console.log(file)); } function outputJsonFileList(files, options) { if (typeof options.json === "boolean") return console.log(JSON.stringify(formatFilesAsJSON(files), null, 2)); if (typeof options.json === "string") { const jsonPath = resolve(options.root || process.cwd(), options.json); mkdirSync(dirname(jsonPath), { recursive: true }); writeFileSync(jsonPath, JSON.stringify(formatFilesAsJSON(files), null, 2)); } } function formatFilesAsJSON(files) { return files.map((file) => { const result = { file: file.moduleId }; if (file.project.name) result.projectName = file.project.name; return result; }); } function formatFilesAsString(files, options) { return files.map((file) => { let name = relative(options.root || process.cwd(), file.moduleId); if (file.project.name) name = `[${file.project.name}] ${name}`; return name; }); } function processJsonOutput(files, options) { if (typeof options.json === "boolean") return console.log(JSON.stringify(formatCollectedAsJSON(files), null, 2)); if (typeof options.json === "string") { const jsonPath = resolve(options.root || process.cwd(), options.json); mkdirSync(dirname(jsonPath), { recursive: true }); writeFileSync(jsonPath, JSON.stringify(formatCollectedAsJSON(files), null, 2)); } } function forEachSuite(modules, callback) { modules.forEach((testModule) => { callback(testModule); for (const suite of testModule.children.allSuites()) callback(suite); }); } function formatCollectedAsJSON(files) { const results = []; files.forEach((file) => { for (const test of file.children.allTests()) { if (test.result().state === "skipped") continue; const result = { name: test.fullName, file: test.module.moduleId }; if (test.project.name) result.projectName = test.project.name; if (test.location) result.location = test.location; results.push(result); } }); return results; } function formatCollectedAsString(testModules) { const results = []; testModules.forEach((testModule) => { for (const test of testModule.children.allTests()) { if (test.result().state === "skipped") continue; const fullName = `${test.module.task.name} > ${test.fullName}`; results.push((test.project.name ? `[${test.project.name}] ` : "") + fullName); } }); return results; } const envPackageNames = { "jsdom": "jsdom", "happy-dom": "happy-dom", "edge-runtime": "@edge-runtime/vm" }; function getEnvPackageName(env) { if (env === "node") return null; if (env in envPackageNames) return envPackageNames[env]; if (env[0] === "." || isAbsolute(env)) return null; return `vitest-environment-${env}`; } var cliApi = /*#__PURE__*/Object.freeze({ __proto__: null, outputFileList: outputFileList, prepareVitest: prepareVitest, processCollected: processCollected, startVitest: startVitest }); export { cliApi as a, createVitest as c, registerConsoleShortcuts as r, startVitest as s };