@babel/helper-transform-fixture-test-runner
Version:
Transform test runner for @babel/helper-fixtures module
744 lines (738 loc) • 25.2 kB
JavaScript
import * as babel from '@babel/core';
import { buildExternalHelpers } from '@babel/core';
import getFixtures, { readFile, resolveOptionPluginOrPreset } from '@babel/helper-fixtures';
import { codeFrameColumns } from '@babel/code-frame';
import { eachMapping, TraceMap } from '@jridgewell/trace-mapping';
import assert from 'node:assert';
import fs, { realpathSync, readFileSync } from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
import { LRUCache } from 'lru-cache';
import { fileURLToPath } from 'node:url';
import { diff } from 'jest-diff';
import { spawn } from 'node:child_process';
import os from 'node:os';
import * as resolve from 'resolve';
import { createRequire } from 'node:module';
import checkDuplicateNodes from '@babel/helper-check-duplicate-nodes';
import { createHash } from 'node:crypto';
function assertNoOwnProperties(obj) {
expect(Object.getOwnPropertyNames(obj)).toHaveLength(0);
}
function multiline(arr) {
return arr.join("\n");
}
const helpers = /*#__PURE__*/Object.defineProperty({
__proto__: null,
assertNoOwnProperties,
multiline
}, Symbol.toStringTag, { value: 'Module' });
const CONTEXT_SIZE = 4;
const LOC_SIZE = 10;
const CONTENT_SIZE = 15;
function simpleCodeFramePoint(lines, line, col) {
const start = Math.max(col - CONTEXT_SIZE, 0);
const end = Math.min(col + 1 + CONTEXT_SIZE, lines[line - 1].length);
const code = lines[line - 1].slice(start, end);
const loc = `(${line}:${col}) `.padStart(LOC_SIZE, " ");
return loc + code + "\n" + " ".repeat(col - start + loc.length) + "^";
}
function joinMultiline(left, right, leftLen) {
const leftLines = left.split("\n");
const rightLines = right.split("\n");
leftLen ??= leftLines.reduce((len, line) => Math.max(len, line.length), 0);
const linesCount = Math.max(leftLines.length, rightLines.length);
let res = "";
for (let i = 0; i < linesCount; i++) {
if (res !== "") res += "\n";
if (i < leftLines.length) res += leftLines[i].padEnd(leftLen, " ");else res += " ".repeat(leftLen);
if (i < rightLines.length) res += rightLines[i];
}
return res;
}
function visualize(output, map) {
const sourcesLines = new Map(map.sources.map((source, index) => [source, map.sourcesContent[index].split("\n")]));
const outputLines = output.split("\n");
const ranges = [];
let prev = null;
eachMapping(new TraceMap(map), mapping => {
if (prev === null) {
prev = mapping;
return;
}
const original = {
from: {
line: prev.originalLine,
column: prev.originalColumn
},
to: {
line: prev.originalLine,
column: prev.originalColumn + 1
}
};
const generated = {
from: {
line: prev.generatedLine,
column: prev.generatedColumn
},
to: {
line: prev.generatedLine,
column: prev.generatedColumn + 1
}
};
if (original.from.line !== original.to.line) {
original.to.line = original.from.line;
original.to.column = Infinity;
} else if (original.to.column < original.from.column) {
original.to.column = original.from.column;
}
if (generated.from.line !== generated.to.line) {
generated.to.line = generated.from.line;
generated.to.column = Infinity;
} else if (generated.to.column < generated.from.column) {
generated.to.column = generated.from.column;
}
ranges.push({
original,
generated,
source: prev.source
});
prev = mapping;
});
if (prev.originalLine) {
ranges.push({
original: {
from: {
line: prev.originalLine,
column: prev.originalColumn
},
to: {
line: prev.originalLine,
column: prev.originalColumn + 1
}
},
generated: {
from: {
line: prev.generatedLine,
column: prev.generatedColumn
},
to: {
line: prev.generatedLine,
column: prev.generatedColumn + 1
}
},
source: prev.source
});
}
const res = ranges.map(({
original,
generated,
source
}) => {
const input = simpleCodeFramePoint(sourcesLines.get(source), original.from.line, original.from.column);
const output = simpleCodeFramePoint(outputLines, generated.from.line, generated.from.column);
return joinMultiline(joinMultiline(input, " <-- ", LOC_SIZE + CONTEXT_SIZE * 2 + CONTENT_SIZE), output);
});
return res.join("\n\n");
}
const require$1 = createRequire(import.meta.url);
const dirname = path.dirname(fileURLToPath(import.meta.url));
const EXTERNAL_HELPERS_VERSION = "7.100.0";
const cachedScripts = new LRUCache({
max: 10
});
const contextModuleCache = new WeakMap();
function transformWithoutConfigFile(code, opts) {
return babel.transformSync(code, {
browserslistConfigFile: false,
configFile: false,
babelrc: false,
caller: {
name: "babel-helper-transform-fixture-test-runner/sync",
supportsStaticESM: false,
supportsDynamicImport: false,
supportsExportNamespaceFrom: false
},
...opts
});
}
function transformAsyncWithoutConfigFile(code, opts) {
return babel.transformAsync(code, {
browserslistConfigFile: false,
configFile: false,
babelrc: false,
caller: {
name: "babel-helper-transform-fixture-test-runner/async",
supportsStaticESM: false,
supportsDynamicImport: false,
supportsExportNamespaceFrom: false
},
...opts
});
}
function createTestContext() {
const context = vm.createContext({
...helpers,
process: process,
transform: transformWithoutConfigFile,
transformAsync: transformAsyncWithoutConfigFile,
setTimeout: setTimeout,
setImmediate: setImmediate,
expect
});
context.global = context;
const moduleCache = Object.create(null);
contextModuleCache.set(context, moduleCache);
runCacheableScriptInTestContext(path.join(path.dirname(fileURLToPath(import.meta.url)), "babel-helpers-in-memory.js"), buildExternalHelpers, context, moduleCache);
return context;
}
function runCacheableScriptInTestContext(filename, srcFn, context, moduleCache) {
let cached = cachedScripts.get(filename);
if (!cached) {
const code = `(function (exports, require, module, __filename, __dirname) {\n${srcFn()}\n});`;
cached = {
code,
cachedData: undefined
};
cachedScripts.set(filename, cached);
}
const script = new vm.Script(cached.code, {
filename,
lineOffset: -1,
cachedData: cached.cachedData
});
cached.cachedData = script.createCachedData();
const module = {
id: filename,
exports: {}
};
moduleCache[filename] = module;
const req = id => runModuleInTestContext(id, filename, context, moduleCache);
const dirname = path.dirname(filename);
script.runInContext(context).call(module.exports, module.exports, req, module, filename, dirname);
return module;
}
function runModuleInTestContext(id, relativeFilename, context, moduleCache) {
const filename = resolve.sync(id, {
basedir: path.dirname(relativeFilename)
});
if (filename === id) return require$1(id);
if (moduleCache[filename]) return moduleCache[filename].exports;
return runCacheableScriptInTestContext(filename, () => fs.readFileSync(filename, "utf8"), context, moduleCache).exports;
}
let sharedTestContext;
function runCodeInTestContext(code, opts, context = sharedTestContext ??= createTestContext()) {
const filename = opts.filename instanceof URL ? fileURLToPath(opts.filename) : opts.filename;
const dirname = path.dirname(filename);
const moduleCache = contextModuleCache.get(context);
const req = id => runModuleInTestContext(id, filename, context, moduleCache);
const module = {
id: filename,
exports: {}
};
const oldCwd = process.cwd();
try {
if (filename) process.chdir(path.dirname(filename));
const src = `((function(exports, require, module, __filename, __dirname, opts) {\n${code}\n})).apply(global, global.__callArgs);`;
context.__callArgs = [module.exports, req, module, filename, dirname, opts];
return vm.runInContext(src, context, {
filename,
displayErrors: true,
lineOffset: -1,
timeout: opts.timeout ?? 10000
});
} finally {
context.__callArgs = undefined;
process.chdir(oldCwd);
}
}
async function maybeMockConsole(validateLogs, run) {
const actualLogs = {
stdout: "",
stderr: ""
};
if (!validateLogs) return {
result: await run(),
actualLogs
};
const spy1 = jest.spyOn(console, "log").mockImplementation(msg => {
actualLogs.stdout += `${msg}\n`;
});
const spy2 = jest.spyOn(console, "warn").mockImplementation(msg => {
actualLogs.stderr += `${msg}\n`;
});
try {
return {
result: await run(),
actualLogs
};
} finally {
spy1.mockRestore();
spy2.mockRestore();
}
}
async function run(task) {
const {
actual,
expect: expected,
exec,
options: opts,
doNotSetSourceType,
optionsDir,
validateLogs,
ignoreOutput,
stdout,
stderr
} = task;
function getOpts(self) {
const newOpts = {
ast: true,
cwd: path.dirname(self.loc),
filename: self.loc,
filenameRelative: self.filename,
sourceFileName: self.filename,
...(doNotSetSourceType ? {} : {
sourceType: "script"
}),
babelrc: false,
configFile: false,
inputSourceMap: task.inputSourceMap || undefined,
...opts
};
return resolveOptionPluginOrPreset(newOpts, optionsDir);
}
let execCode = exec.code;
let result;
let resultExec;
let execErr;
if (execCode) {
const context = createTestContext();
const execOpts = getOpts(exec);
result = (await maybeMockConsole(validateLogs, async () => babel.transformAsync(execCode, execOpts))).result;
checkDuplicateNodes(result.ast);
execCode = result.code;
try {
resultExec = runCodeInTestContext(execCode, execOpts, context);
} catch (err) {
if (typeof err === "object" && err.message) {
err.message = `${exec.loc}: ${err.message}\n` + codeFrameColumns(execCode, {});
}
execErr = err;
}
}
const inputCode = actual.code;
const expectedCode = expected.code;
if (!execCode || inputCode) {
const res = await maybeMockConsole(validateLogs, () => babel.transformAsync(inputCode, getOpts(actual)));
result = res.result;
const outputCode = normalizeOutput(result.code, {
normalizePathSeparator: true
});
checkDuplicateNodes(result.ast);
if (!ignoreOutput) {
if (!expectedCode && outputCode && !opts.throws && fs.statSync(path.dirname(expected.loc)).isDirectory() && !process.env.CI) {
const expectedFile = expected.loc.replace(/\.m?js$/, result.sourceType === "module" ? ".mjs" : ".js");
console.log(`New test file created: ${expectedFile}`);
fs.writeFileSync(expectedFile, `${outputCode}\n`);
if (expected.loc !== expectedFile) {
try {
fs.unlinkSync(expected.loc);
} catch (_) {}
}
} else {
validateFile(outputCode, expected.loc, expectedCode);
if (inputCode) {
expect(expected.loc).toMatch(result.sourceType === "module" ? /\.mjs$/ : /\.js$/);
}
}
}
if (validateLogs) {
const normalizationOpts = {
normalizePathSeparator: true,
normalizePresetEnvDebug: task.taskDir.includes("babel-preset-env")
};
validateFile(normalizeOutput(res.actualLogs.stdout, normalizationOpts), stdout.loc, stdout.code);
validateFile(normalizeOutput(res.actualLogs.stderr, normalizationOpts), stderr.loc, stderr.code);
}
}
if (execErr) {
throw execErr;
}
if (task.validateSourceMapVisual === true) {
const visual = visualize(result.code, result.map);
try {
expect(visual).toEqual(task.sourceMapVisual.code);
} catch (e) {
if (!process.env.OVERWRITE && task.sourceMapVisual.code) throw e;
console.log(`Updated test file: ${task.sourceMapVisual.loc}`);
fs.writeFileSync(task.sourceMapVisual.loc ?? task.taskDir + "/source-map-visual.txt", visual + "\n");
}
}
if (opts.sourceMaps === true) {
try {
expect(result.map).toEqual(task.sourceMap);
} catch (e) {
if (!process.env.OVERWRITE && task.sourceMap) throw e;
task.sourceMapFile.loc ??= task.taskDir + "/source-map.json";
console.log(`Updated test file: ${task.sourceMapFile.loc}`);
fs.writeFileSync(task.sourceMapFile.loc, JSON.stringify(result.map, null, 2));
}
}
if (execCode && resultExec) {
return resultExec;
}
}
function validateFile(actualCode, expectedLoc, expectedCode) {
if (actualCode !== expectedCode) {
if (process.env.OVERWRITE) {
console.log(`Updated test file: ${expectedLoc}`);
fs.writeFileSync(expectedLoc, `${actualCode}\n`);
return;
}
throw new Error(`Expected ${expectedLoc} to match transform output.\n` + `To autogenerate a passing version of this file, delete ` + ` the file and re-run the tests.\n\n` + `Diff:\n\n${diff(expectedCode, actualCode, {
expand: false
})}`);
}
}
function normalizeOutput(code, {
normalizePathSeparator = false
} = {}) {
const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../");
const symbol = "<CWD>";
let result = code.trim().replaceAll(dir, symbol);
if (process.platform === "win32") {
result = result.replaceAll(dir.replaceAll("\\", "/"), symbol).replaceAll(dir.replaceAll("\\", "\\\\"), symbol);
if (normalizePathSeparator) {
result = result.replaceAll(/<CWD>[\w\\/.-]+/g, path => path.replaceAll(/\\\\?/g, "/"));
}
}
return result;
}
function index (fixturesLoc, name, suiteOpts = {}, taskOpts = {}, dynamicOpts) {
const suites = getFixtures(fixturesLoc);
for (const testSuite of suites) {
if (suiteOpts.ignoreSuites?.includes(testSuite.title)) continue;
describe(name + "/" + testSuite.title, function () {
for (const task of testSuite.tests) {
if (suiteOpts.ignoreTasks?.includes(task.title) || suiteOpts.ignoreTasks?.includes(testSuite.title + "/" + task.title)) {
continue;
}
const testFn = task.disabled ? it.skip : it;
const testTitle = typeof task.disabled === "string" ? `(SKIP: ${task.disabled}) ${task.title}` : task.title;
testFn(testTitle, async function () {
const runTask = () => run(task);
if ("sourceMap" in task.options) {
throw new Error("`sourceMap` option is deprecated. Use `sourceMaps` instead.");
}
if ("sourceMaps" in task.options === false) {
task.options.sourceMaps = !!task.sourceMap;
}
Object.assign(task.options, taskOpts);
if (dynamicOpts) dynamicOpts(task.options, task);
if (task.externalHelpers) {
(task.options.plugins ??= []).push(["external-helpers", {
helperVersion: EXTERNAL_HELPERS_VERSION
}]);
}
const throwMsg = task.options.throws;
if (throwMsg) {
delete task.options.throws;
await assert.rejects(runTask, function (err) {
assert.ok(throwMsg === true || err.message.includes(throwMsg), `
Expected Error: ${throwMsg}
Actual Error: ${err.message}`);
return true;
});
} else {
return runTask();
}
});
}
});
}
}
const tmpDir = realpathSync.native(os.tmpdir());
const readDir = function (loc, pathFilter) {
const files = {};
if (fs.existsSync(loc)) {
fs.readdirSync(loc, {
withFileTypes: true,
recursive: true
}).filter(dirent => dirent.isFile() && pathFilter(dirent.name)).forEach(dirent => {
const fullpath = path.join(dirent.parentPath, dirent.name);
files[path.relative(loc, fullpath)] = readFile(fullpath);
});
}
return files;
};
const outputFileSync = function (filePath, data) {
fs.mkdirSync(path.dirname(filePath), {
recursive: true
});
fs.writeFileSync(filePath, data);
};
function deleteDir(path) {
fs.rmSync(path, {
force: true,
recursive: true
});
}
const pathFilter = function (x) {
return path.basename(x) !== ".DS_Store";
};
const assertTest = function (stdout, stderr, ipcMessage, opts, tmpDir) {
const expectStderr = opts.stderr.trim();
stderr = stderr.trim();
try {
if (opts.stderr) {
if (opts.stderrContains) {
expect(stderr).toContain(expectStderr);
} else {
expect(stderr).toBe(expectStderr);
}
} else if (stderr) {
throw new Error("stderr:\n" + stderr);
}
} catch (e) {
if (!process.env.OVERWRITE) throw e;
console.log(`Updated test file: ${opts.stderrPath}`);
outputFileSync(opts.stderrPath, stderr + "\n");
}
const expectStdout = opts.stdout.trim();
stdout = stdout.trim();
stdout = stdout.replace(/\\/g, "/");
try {
if (opts.stdout) {
if (opts.stdoutContains) {
expect(stdout).toContain(expectStdout);
} else {
expect(stdout).toBe(expectStdout);
}
} else if (stdout) {
throw new Error("stdout:\n" + stdout);
}
} catch (e) {
if (!process.env.OVERWRITE) throw e;
console.log(`Updated test file: ${opts.stdoutPath}`);
outputFileSync(opts.stdoutPath, stdout + "\n");
}
if (opts.ipc) {
expect(ipcMessage).toEqual(opts.ipcMessage);
}
if (opts.outFiles) {
const actualFiles = readDir(tmpDir, pathFilter);
Object.keys(actualFiles).forEach(function (filename) {
try {
if (filename !== ".babelrc" && filename !== ".babelignore" && !Object.hasOwn(opts.inFiles, filename)) {
const expected = opts.outFiles[filename];
const actual = actualFiles[filename];
expect(actual).toBe(expected || "");
}
} catch (e) {
if (!process.env.OVERWRITE) {
e.message += "\n at " + filename;
throw e;
}
const expectedLoc = path.join(opts.testLoc, "out-files", filename);
console.log(`Updated test file: ${expectedLoc}`);
outputFileSync(expectedLoc, actualFiles[filename]);
}
});
Object.keys(opts.outFiles).forEach(function (filename) {
expect(actualFiles).toHaveProperty([filename]);
});
}
};
function buildParallelProcessTests(name, tests) {
return function (curr, total) {
const sliceLength = Math.ceil(tests.length / total);
const sliceStart = curr * sliceLength;
const sliceEnd = sliceStart + sliceLength;
const testsSlice = tests.slice(sliceStart, sliceEnd);
describe(`${name} [${curr}/${total}]`, function () {
it("dummy", () => {});
for (const test of testsSlice) {
(test.skip ? it.skip : it)(test.suiteName + " " + test.testName, test.fn);
}
});
};
}
function buildProcessTests(dir, beforeHook, afterHook) {
if (dir instanceof URL) dir = fileURLToPath(dir);
const tests = [];
fs.readdirSync(dir).forEach(function (suiteName) {
if (suiteName.startsWith(".") || suiteName === "package.json") return;
const suiteLoc = path.join(dir, suiteName);
fs.readdirSync(suiteLoc).forEach(function (testName) {
if (testName.startsWith(".")) return;
const testLoc = path.join(suiteLoc, testName);
let opts = {
args: [],
stdout: "",
stderr: "",
stdin: "",
stdoutPath: "",
stderrPath: "",
testLoc: "",
outFiles: {},
inFiles: {}
};
const optionsLoc = path.join(testLoc, "options.json");
if (fs.existsSync(optionsLoc)) {
const taskOpts = JSON.parse(readFileSync(optionsLoc, "utf8"));
if (taskOpts.os) {
let os = taskOpts.os;
if (!Array.isArray(os) && typeof os !== "string") {
throw new Error(`'os' should be either string or string array: ${taskOpts.os}`);
}
if (typeof os === "string") {
os = [os];
}
if (!os.includes(process.platform)) {
return;
}
delete taskOpts.os;
}
opts = {
args: [],
...taskOpts
};
}
const executorLoc = path.join(testLoc, "executor.js");
if (fs.existsSync(executorLoc)) {
opts.executor = executorLoc;
}
opts.stderrPath = path.join(testLoc, "stderr.txt");
opts.stdoutPath = path.join(testLoc, "stdout.txt");
for (const key of ["stdout", "stdin", "stderr"]) {
const loc = path.join(testLoc, key + ".txt");
if (fs.existsSync(loc)) {
opts[key] = readFile(loc);
} else {
opts[key] = opts[key] || "";
}
}
opts.testLoc = testLoc;
opts.outFiles = readDir(path.join(testLoc, "out-files"), pathFilter);
opts.inFiles = readDir(path.join(testLoc, "in-files"), pathFilter);
const babelrcLoc = path.join(testLoc, ".babelrc");
const babelIgnoreLoc = path.join(testLoc, ".babelignore");
if (fs.existsSync(babelrcLoc)) {
opts.inFiles[".babelrc"] = readFile(babelrcLoc);
} else if (!opts.noBabelrc) {
opts.inFiles[".babelrc"] = "{}";
}
if (fs.existsSync(babelIgnoreLoc)) {
opts.inFiles[".babelignore"] = readFile(babelIgnoreLoc);
}
const skip = opts.minNodeVersion && parseInt(process.versions.node, 10) < opts.minNodeVersion || opts.BABEL_8_BREAKING === false;
const test = {
suiteName,
testName,
skip,
opts,
fn: function (callback) {
const tmpLoc = path.join(tmpDir, "babel-process-test", createHash("sha1").update(testLoc).digest("hex"));
deleteDir(tmpLoc);
fs.mkdirSync(tmpLoc, {
recursive: true
});
const {
inFiles
} = opts;
for (const filename of Object.keys(inFiles)) {
outputFileSync(path.join(tmpLoc, filename), inFiles[filename]);
}
try {
beforeHook(test, tmpLoc);
if (test.binLoc === undefined) {
throw new Error("test.binLoc is undefined");
}
let args = opts.executor ? ["--require", path.join(dirname, "./exit-loader.cjs"), test.binLoc] : [test.binLoc];
args = args.concat(opts.args);
const env = {
...process.env,
FORCE_COLOR: "false",
...(parseInt(process.versions.node) >= 22 && {
NODE_OPTIONS: "--disable-warning=ExperimentalWarning"
}),
...opts.env
};
const child = spawn(process.execPath, args, {
env,
cwd: tmpLoc,
stdio: opts.executor || opts.ipc ? ["pipe", "pipe", "pipe", "ipc"] : "pipe"
});
let stderr = "";
let stdout = "";
let ipcMessage;
child.on("close", function () {
let err;
try {
const result = afterHook ? afterHook(test, tmpLoc, stdout, stderr) : {
stdout,
stderr
};
assertTest(result.stdout, result.stderr, ipcMessage, opts, tmpLoc);
} catch (e) {
err = e;
} finally {
try {
deleteDir(tmpLoc);
} catch (error) {
console.error(error);
}
}
if (err) {
err.message = args.map(arg => `"${arg}"`).join(" ") + ": " + err.message;
}
callback(err);
});
if (opts.ipc) {
child.on("message", function (message) {
ipcMessage = message;
});
}
if (opts.stdin) {
child.stdin.write(opts.stdin);
child.stdin.end();
}
const captureOutput = proc => {
proc.stderr.on("data", function (chunk) {
stderr += chunk;
});
proc.stdout.on("data", function (chunk) {
stdout += chunk;
});
};
if (opts.executor) {
const executor = spawn(process.execPath, [opts.executor], {
cwd: tmpLoc
});
child.stdout.pipe(executor.stdin);
child.stderr.pipe(executor.stdin);
executor.on("close", function () {
child.send("exit");
});
captureOutput(executor);
} else {
captureOutput(child);
}
} catch (e) {
deleteDir(tmpLoc);
throw e;
}
}
};
tests.push(test);
});
});
tests.sort(function (testA, testB) {
const nameA = testA.suiteName + "/" + testA.testName;
const nameB = testB.suiteName + "/" + testB.testName;
return nameA.localeCompare(nameB);
});
return tests;
}
export { buildParallelProcessTests, buildProcessTests, createTestContext, index as default, runCodeInTestContext };
//# sourceMappingURL=index.js.map