mirror of
https://github.com/hanzoai/babel.git
synced 2026-09-24 16:56:45 +00:00
400 lines
11 KiB
JavaScript
400 lines
11 KiB
JavaScript
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;
|
||
});
|
||
}
|