529 lines
19 KiB
JavaScript
529 lines
19 KiB
JavaScript
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 };
|