Files

400 lines
11 KiB
JavaScript
Raw Permalink 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 { Module } from "node:module";
import path from "node:path";
import fs from "node:fs";
import child from "node:child_process";
import { commonJS } from "$repo-utils";
const { __dirname, require } = commonJS(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("@cspotcode/source-map-support");
function cleanCache() {
try {
fs.rmSync(testCacheFilename, { recursive: true, force: true });
} catch (e) {
// It is convenient to always try to clean
}
}
function resetCache() {
process.env.BABEL_CACHE_PATH = null;
}
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;
},
uninstall() {
sourceMapSupport = false;
},
},
};
beforeEach(() => {
currentHook = null;
currentOptions = null;
});
let originalRequireCacheDescriptor;
beforeAll(() => {
originalRequireCacheDescriptor = Object.getOwnPropertyDescriptor(
Module,
"_cache",
);
});
afterAll(() => {
Object.defineProperty(Module, "_cache", originalRequireCacheDescriptor);
});
describe("worker", () => {
beforeEach(() => {
Object.defineProperty(Module, "_cache", {
...originalRequireCacheDescriptor,
value: {
[piratesPath]: { exports: mocks.pirates },
[smsPath]: { exports: mocks["source-map-support"] },
},
});
});
const { setupRegister } = buildTests(require.resolve("../lib/index.js"));
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();
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).toMatchInlineSnapshot(`
{
"exts": [
".js",
".jsx",
".es6",
".es",
".mjs",
".cjs",
],
"ignoreNodeModules": false,
"matcher": [Function],
}
`);
});
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("uninstalls source map support when reverting", () => {
setupRegister();
currentHook("const a = 1;", testFile);
revertRegister();
expect(sourceMapSupport).toBe(false);
});
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),
{
...process.env,
NODE_OPTIONS:
`-r ${registerFile} ` + (process.env.NODE_OPTIONS || ""),
},
);
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),
{
...process.env,
NODE_OPTIONS:
`--require ${registerFile} ` + (process.env.NODE_OPTIONS || ""),
},
);
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 doesn’t 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 couldn’t get
// that working inside a test, possibly because of jest’s 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();
});
describe("hook skip ignored files", () => {
// We can't use the test runner because the mocked `pirates`
// does not support the `matcher` option.
test("via programmatic options ignored", async () => {
const entryFile =
require.resolve("./fixtures/ignore-programmatic/index.ts");
const output = await spawnNodeAsync(
[entryFile],
path.dirname(entryFile),
);
expect(output).toBe(
"SyntaxError is thrown as expected from ignored-throw.ts",
);
});
test("via programmatic options only", async () => {
const entryFile =
require.resolve("./fixtures/only-programmatic/index.ts");
const output = await spawnNodeAsync(
[entryFile],
path.dirname(entryFile),
);
expect(output).toBe(
"SyntaxError is thrown as expected from ignored-throw.ts",
);
});
});
return { setupRegister, revertRegister };
}
});
function spawnNodeAsync(args, cwd = __dirname, env = process.env) {
const spawn = child.spawn(process.execPath, args, {
cwd,
env: {
...env,
...(parseInt(process.versions.node) >= 22 && {
NODE_OPTIONS:
"--disable-warning=ExperimentalWarning " + (env.NODE_OPTIONS || ""),
}),
},
});
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;
});
}