2022-01-06 18:37:45 +01:00

423 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createRequire, Module } from "module";
import path from "path";
import fs from "fs";
import child from "child_process";
import { fileURLToPath } from "url";
const dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const testCacheFilename = path.join(dirname, ".index.babel");
const testFile = require.resolve("./fixtures/babelrc/es2015");
const testFileLog = require.resolve("./fixtures/babelrc/log");
const testFileMjs = require.resolve("./fixtures/mjs-babelrc/es2015");
const testFileContent = fs.readFileSync(testFile, "utf-8");
const testFileMjsContent = fs.readFileSync(testFileMjs, "utf-8");
const piratesPath = require.resolve("pirates");
const smsPath = require.resolve("source-map-support");
const defaultOptions = {
exts: [".js", ".jsx", ".es6", ".es", ".mjs", ".cjs"],
ignoreNodeModules: false,
};
function cleanCache() {
try {
fs.unlinkSync(testCacheFilename);
} catch (e) {
// It is convenient to always try to clear
}
}
function resetCache() {
process.env.BABEL_CACHE_PATH = null;
}
const OLD_JEST_MOCKS = !!jest.doMock;
describe("@babel/register", function () {
let currentHook, currentOptions, sourceMapSupport;
const mocks = {
["pirates"]: {
addHook(hook, opts) {
currentHook = hook;
currentOptions = opts;
return () => {
currentHook = null;
currentOptions = null;
};
},
},
["source-map-support"]: {
install() {
sourceMapSupport = true;
},
},
};
beforeEach(() => {
currentHook = null;
currentOptions = null;
sourceMapSupport = false;
});
let originalRequireCacheDescriptor;
if (OLD_JEST_MOCKS) {
jest.doMock("pirates", () => mocks["pirates"]);
jest.doMock("source-map-support", () => mocks["source-map-support"]);
afterEach(() => {
jest.resetModules();
});
} else {
beforeAll(() => {
originalRequireCacheDescriptor = Object.getOwnPropertyDescriptor(
Module,
"_cache",
);
});
afterAll(() => {
Object.defineProperty(Module, "_cache", originalRequireCacheDescriptor);
});
}
if (!process.env.BABEL_8_BREAKING) {
describe("babel 7", () => {
if (!OLD_JEST_MOCKS) {
beforeEach(() => {
const isEmptyObj = obj =>
Object.getPrototypeOf(obj) === null &&
Object.keys(obj).length === 0;
// This setter intercepts the Module._cache assignment in
// packages/babel-register/src/nodeWrapper.js to install in the
// internal isolated cache.
const emptyInitialCache = {};
Object.defineProperty(Module, "_cache", {
get: () => emptyInitialCache,
set(value) {
expect(isEmptyObj(value)).toBe(true);
Object.defineProperty(Module, "_cache", {
value,
enumerable: originalRequireCacheDescriptor.enumerable,
configurable: originalRequireCacheDescriptor.configurable,
writable: originalRequireCacheDescriptor.writable,
});
value[piratesPath] = { exports: mocks["pirates"] };
value[smsPath] = { exports: mocks["source-map-support"] };
},
enumerable: originalRequireCacheDescriptor.enumerable,
configurable: originalRequireCacheDescriptor.configurable,
});
});
}
const { setupRegister } = buildTests(require.resolve(".."));
it("does not mutate options", () => {
const proxyHandler = {
defineProperty: jest.fn(Reflect.defineProperty),
deleteProperty: jest.fn(Reflect.deleteProperty),
set: jest.fn(Reflect.set),
};
setupRegister(
new Proxy(
{
babelrc: true,
sourceMaps: false,
cwd: path.dirname(testFileMjs),
extensions: [".js"],
},
proxyHandler,
),
);
currentHook(testFileContent, testFile);
expect(proxyHandler.defineProperty).not.toHaveBeenCalled();
expect(proxyHandler.deleteProperty).not.toHaveBeenCalled();
expect(proxyHandler.set).not.toHaveBeenCalled();
});
});
}
const nodeGte12 = (fn, ...args) => {
// "minNodeVersion": "8.0.0" <-- For Ctrl+F when dropping node 6-8-10
const testFn = /v(?:6|8|10)\./.test(process.version) ? fn.skip : fn;
testFn(...args);
};
nodeGte12(describe, "worker", () => {
if (!OLD_JEST_MOCKS) {
beforeEach(() => {
Object.defineProperty(Module, "_cache", {
...originalRequireCacheDescriptor,
value: {
[piratesPath]: { exports: mocks["pirates"] },
[smsPath]: { exports: mocks["source-map-support"] },
},
});
});
}
const { setupRegister } = buildTests(
require.resolve("../experimental-worker"),
);
it("works with mjs config files", () => {
setupRegister({
babelrc: true,
sourceMaps: false,
cwd: path.dirname(testFileMjs),
});
const result = currentHook(testFileMjsContent, testFileMjs);
expect(result).toBe('"use strict";\n\nrequire("assert");');
});
});
function buildTests(registerFile) {
let babelRegister;
function setupRegister(config = { babelrc: false }) {
process.env.BABEL_CACHE_PATH = testCacheFilename;
config = {
cwd: path.dirname(testFile),
...config,
};
babelRegister = require(registerFile);
babelRegister.default(config);
}
function revertRegister() {
if (babelRegister) {
babelRegister.revert();
delete require.cache[registerFile];
babelRegister = null;
}
cleanCache();
}
afterEach(async () => {
// @babel/register saves the cache on process.nextTick.
// We need to wait for at least one tick so that when jest
// tears down the testing environment @babel/register has
// already finished.
await new Promise(setImmediate);
revertRegister();
});
afterAll(() => {
resetCache();
});
test("registers hook correctly", () => {
setupRegister();
expect(typeof currentHook).toBe("function");
expect(currentOptions).toEqual(defaultOptions);
});
test("unregisters hook correctly", () => {
setupRegister();
revertRegister();
expect(currentHook).toBeNull();
expect(currentOptions).toBeNull();
});
test("installs source map support by default", () => {
setupRegister();
currentHook("const a = 1;", testFile);
expect(sourceMapSupport).toBe(true);
});
test("installs source map support when requested", () => {
setupRegister({
babelrc: false,
sourceMaps: true,
});
currentHook("const a = 1;", testFile);
expect(sourceMapSupport).toBe(true);
});
test("does not install source map support if asked not to", () => {
setupRegister({
babelrc: false,
sourceMaps: false,
});
currentHook("const a = 1;", testFile);
expect(sourceMapSupport).toBe(false);
});
describe("node auto-require", () => {
it("works with the -r flag", async () => {
const output = await spawnNodeAsync(
["-r", registerFile, testFileLog],
path.dirname(testFileLog),
);
expect(output.trim()).toMatchInlineSnapshot(
`"It worked! function () {}"`,
);
});
it("works with the --require flag", async () => {
const output = await spawnNodeAsync(
["--require", registerFile, testFileLog],
path.dirname(testFileLog),
);
expect(output.trim()).toMatchInlineSnapshot(
`"It worked! function () {}"`,
);
});
it("works with the -r flag in NODE_OPTIONS", async () => {
const output = await spawnNodeAsync(
[testFileLog],
path.dirname(testFileLog),
{ NODE_OPTIONS: `-r ${registerFile}` },
);
expect(output.trim()).toMatchInlineSnapshot(
`"It worked! function () {}"`,
);
});
it("works with the --require flag in NODE_OPTIONS", async () => {
const output = await spawnNodeAsync(
[testFileLog],
path.dirname(testFileLog),
{ NODE_OPTIONS: `--require ${registerFile}` },
);
expect(output.trim()).toMatchInlineSnapshot(
`"It worked! function () {}"`,
);
});
});
it("returns concatenatable sourceRoot and sources", async () => {
// The Source Maps R3 standard https://sourcemaps.info/spec.html states
// that `sourceRoot` is “prepended to the individual entries in the
// source field.” If `sources` contains file names, and `sourceRoot`
// is intended to refer to a directory but doesnt end with a trailing
// slash, any consumers of the source map are in for a bad day.
//
// The underlying problem seems to only get triggered if one file
// requires() another with @babel/register active, and I couldnt get
// that working inside a test, possibly because of jests mocking
// hooks, so we spawn a separate process.
const output = await spawnNodeAsync([
"-r",
registerFile,
require.resolve("./fixtures/source-map/index"),
]);
const sourceMap = JSON.parse(output);
expect(sourceMap.map.sourceRoot + sourceMap.map.sources[0]).toBe(
require.resolve("./fixtures/source-map/foo/bar"),
);
});
test("hook transpiles with config", () => {
setupRegister({
babelrc: false,
sourceMaps: false,
plugins: ["@babel/transform-modules-commonjs"],
});
const result = currentHook(testFileContent, testFile);
expect(result).toBe('"use strict";\n\nrequire("assert");');
});
test("hook transpiles with babelrc", () => {
setupRegister({
babelrc: true,
sourceMaps: false,
});
const result = currentHook(testFileContent, testFile);
expect(result).toBe('"use strict";\n\nrequire("assert");');
});
test("transforms modules used within register", async () => {
// Need a clean environment without `convert-source-map`
// already in the require cache, so we spawn a separate process
const output = await spawnNodeAsync([
require.resolve("./fixtures/internal-modules/index.js"),
]);
const { convertSourceMap } = JSON.parse(output);
expect(convertSourceMap).toMatch("/* transformed */");
});
test("does not mutate options", () => {
const proxyHandler = {
defineProperty: jest.fn(Reflect.defineProperty),
deleteProperty: jest.fn(Reflect.deleteProperty),
set: jest.fn(Reflect.set),
};
setupRegister(
new Proxy(
{
babelrc: true,
sourceMaps: false,
cwd: path.dirname(testFileMjs),
extensions: [".js"],
},
proxyHandler,
),
);
currentHook(testFileContent, testFile);
expect(proxyHandler.defineProperty).not.toHaveBeenCalled();
expect(proxyHandler.deleteProperty).not.toHaveBeenCalled();
expect(proxyHandler.set).not.toHaveBeenCalled();
});
return { setupRegister, revertRegister };
}
});
function spawnNodeAsync(args, cwd = dirname, env) {
const spawn = child.spawn(process.execPath, args, { cwd, env });
let output = "";
let callback;
for (const stream of [spawn.stderr, spawn.stdout]) {
stream.on("data", chunk => {
output += chunk;
});
}
spawn.on("close", function () {
callback(output);
});
return new Promise(resolve => {
callback = resolve;
});
}