[babel 8] Move @babel/register transform to a separate worker (#14025)

This commit is contained in:
Nicolò Ribaudo 2021-12-29 16:33:12 +01:00 committed by GitHub
parent d1cabf6bc8
commit e77e3de402
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 791 additions and 381 deletions

View File

@ -1 +1,3 @@
console.log("foo"); // See https://github.com/babel/babel/pull/14025#issuecomment-986296424
// for the reason behind using setImmediate.
setImmediate(() => console.log("foo"));

View File

@ -1 +1,3 @@
console.log("foo"); // See https://github.com/babel/babel/pull/14025#issuecomment-986296424
// for the reason behind using setImmediate.
setImmediate(() => console.log("foo"));

View File

@ -1 +1,3 @@
console.log("foo"); // See https://github.com/babel/babel/pull/14025#issuecomment-986296424
// for the reason behind using setImmediate.
setImmediate(() => console.log("foo"));

View File

@ -1 +1,3 @@
console.log("foo"); // See https://github.com/babel/babel/pull/14025#issuecomment-986296424
// for the reason behind using setImmediate.
setImmediate(() => console.log("foo"));

View File

@ -1,3 +1,7 @@
src src
test test
*.log *.log
experimental-worker.js
lib/experimental-worker.js
lib/is-in-register-worker.js

View File

@ -0,0 +1 @@
module.exports = require("./lib/experimental-worker");

View File

@ -12,9 +12,10 @@
"directory": "packages/babel-register" "directory": "packages/babel-register"
}, },
"author": "The Babel Team (https://babel.dev/team)", "author": "The Babel Team (https://babel.dev/team)",
"type": "commonjs",
"main": "./lib/index.js", "main": "./lib/index.js",
"browser": { "browser": {
"./lib/nodeWrapper.js": "./lib/browser.js" "./lib/index.js": "./lib/browser.js"
}, },
"dependencies": { "dependencies": {
"clone-deep": "^4.0.1", "clone-deep": "^4.0.1",
@ -28,6 +29,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "workspace:^", "@babel/core": "workspace:^",
"@babel/plugin-transform-arrow-functions": "workspace:^",
"@babel/plugin-transform-modules-commonjs": "workspace:^", "@babel/plugin-transform-modules-commonjs": "workspace:^",
"browserify": "^16.5.2" "browserify": "^16.5.2"
}, },

View File

@ -0,0 +1,10 @@
// required to safely use babel/register within a browserify codebase
function register() {}
module.exports = Object.assign(register, {
default: register,
register,
revert: function revert() {},
__esModule: true,
});

View File

@ -1,5 +0,0 @@
// required to safely use babel/register within a browserify codebase
export default function register() {}
export function revert() {}

View File

@ -0,0 +1,3 @@
// File moved to ./worker/cache.js
// TODO: Remove this backward-compat "proxy file" in Babel 8
module.exports = require("./worker/cache");

View File

@ -0,0 +1,26 @@
// TODO: Move this file to index.js in Babel 8
"use strict";
const [major, minor] = process.versions.node.split(".").map(Number);
if (major < 12 || (major === 12 && minor < 3)) {
throw new Error(
"@babel/register/experimental-worker requires Node.js >= 12.3.0",
);
}
const hook = require("./hook");
const { WorkerClient } = require("./worker-client");
const register = hook.register.bind(null, new WorkerClient());
module.exports = Object.assign(register, {
revert: hook.revert,
default: register,
__esModule: true,
});
if (!require("./is-in-register-worker").isInRegisterWorker) {
register();
}

View File

@ -0,0 +1,85 @@
"use strict";
const { addHook } = require("pirates");
const sourceMapSupport = require("source-map-support");
let piratesRevert;
const maps = Object.create(null);
function installSourceMapSupport() {
installSourceMapSupport = () => {}; // eslint-disable-line no-func-assign
sourceMapSupport.install({
handleUncaughtExceptions: false,
environment: "node",
retrieveSourceMap(filename) {
const map = maps?.[filename];
if (map) {
return { url: null, map: map };
} else {
return null;
}
},
});
}
if (!process.env.BABEL_8_BREAKING) {
// Babel 7 compiles files in the same thread where it hooks `require()`,
// so we must prevent mixing Babel plugin dependencies with the files
// to be compiled.
// All the `!process.env.BABEL_8_BREAKING` code in this file is for
// this purpose.
const Module = require("module");
let compiling = false;
const internalModuleCache = Module._cache;
// eslint-disable-next-line no-var
var compileBabel7 = function compileBabel7(client, code, filename) {
if (!client.isLocalClient) return compile(client, code, filename);
if (compiling) return code;
const globalModuleCache = Module._cache;
try {
compiling = true;
Module._cache = internalModuleCache;
return compile(client, code, filename);
} finally {
compiling = false;
Module._cache = globalModuleCache;
}
};
}
function compile(client, inputCode, filename) {
const result = client.transform(inputCode, filename);
if (result === null) return inputCode;
const { code, map } = result;
if (map) {
maps[filename] = map;
installSourceMapSupport();
}
return code;
}
exports.register = function register(client, opts = {}) {
if (piratesRevert) piratesRevert();
piratesRevert = addHook(
(process.env.BABEL_8_BREAKING ? compile : compileBabel7).bind(null, client),
{
exts: opts.extensions ?? client.getDefaultExtensions(),
ignoreNodeModules: false,
},
);
client.setOptions(opts);
};
exports.revert = function revert() {
if (piratesRevert) piratesRevert();
};

View File

@ -4,12 +4,16 @@
* from a compiled Babel import. * from a compiled Babel import.
*/ */
exports = module.exports = function (...args) { if (process.env.BABEL_8_BREAKING) {
return register(...args); module.exports = require("./experimental-worker");
}; } else {
exports.__esModule = true; exports = module.exports = function (...args) {
return register(...args);
};
exports.__esModule = true;
const node = require("./nodeWrapper"); const node = require("./nodeWrapper");
const register = node.default; const register = node.default;
Object.assign(exports, node); Object.assign(exports, node);
}

View File

@ -0,0 +1,20 @@
"use strict";
/**
* Since workers inherit the exec options from the parent thread, we
* must be careful to avoid infite "@babel/register" setup loops.
*
* If @babel/register is imported using the -r/--require flag, the worker
* will have the same flag and we must avoid registering the @babel/register
* hook again.
*
* - markInRegisterWorker() can be used to mark a set of env vars (that will
* be forwarded to a worker) as being in the @babel/register worker.
* - isInRegisterWorker will be true in @babel/register workers.
*/
const envVarName = "___INTERNAL___IS_INSIDE_BABEL_REGISTER_WORKER___";
const envVarValue = "yes_I_am";
exports.markInRegisterWorker = env => ({ ...env, [envVarName]: envVarValue });
exports.isInRegisterWorker = process.env[envVarName] === envVarValue;

View File

@ -0,0 +1,13 @@
// TODO: Remove this file in Babel 8
"use strict";
const hook = require("./hook");
const { LocalClient } = require("./worker-client");
const register = hook.register.bind(null, new LocalClient());
module.exports = Object.assign(register, {
revert: hook.revert,
default: register,
});

View File

@ -1,176 +0,0 @@
import cloneDeep from "clone-deep";
import sourceMapSupport from "source-map-support";
import * as registerCache from "./cache";
import * as babel from "@babel/core";
import { OptionManager, DEFAULT_EXTENSIONS } from "@babel/core";
import { addHook } from "pirates";
import fs from "fs";
import path from "path";
import Module from "module";
const maps = {};
let transformOpts: any = {};
let piratesRevert = null;
function installSourceMapSupport() {
sourceMapSupport.install({
handleUncaughtExceptions: false,
environment: "node",
retrieveSourceMap(source) {
const map = maps && maps[source];
if (map) {
return {
url: null,
map: map,
};
} else {
return null;
}
},
});
}
let cache;
function mtime(filename) {
return +fs.statSync(filename).mtime;
}
function compile(code, filename) {
// merge in base options and resolve all the plugins and presets relative to this file
const opts = new OptionManager().init(
// sourceRoot can be overwritten
{
sourceRoot: path.dirname(filename) + path.sep,
...cloneDeep(transformOpts),
filename,
},
);
// Bail out ASAP if the file has been ignored.
if (opts === null) return code;
let cacheKey = `${JSON.stringify(opts)}:${babel.version}`;
const env = babel.getEnv("");
if (env) cacheKey += `:${env}`;
let cached, fileMtime;
if (cache) {
cached = cache[cacheKey];
fileMtime = mtime(filename);
}
if (!cached || cached.mtime !== fileMtime) {
cached = babel.transform(code, {
...opts,
sourceMaps: opts.sourceMaps === undefined ? "both" : opts.sourceMaps,
ast: false,
});
if (cache) {
cache[cacheKey] = cached;
cached.mtime = fileMtime;
registerCache.setDirty();
}
}
if (cached.map) {
if (Object.keys(maps).length === 0) {
installSourceMapSupport();
}
maps[filename] = cached.map;
}
return cached.code;
}
let compiling = false;
// @ts-expect-error field is missing in type definitions
const internalModuleCache = Module._cache;
function compileHook(code, filename) {
if (compiling) return code;
// @ts-expect-error field is missing in type definitions
const globalModuleCache = Module._cache;
try {
compiling = true;
// @ts-expect-error field is missing in type definitions
Module._cache = internalModuleCache;
return compile(code, filename);
} finally {
compiling = false;
// @ts-expect-error field is missing in type definitions
Module._cache = globalModuleCache;
}
}
function hookExtensions(exts) {
if (piratesRevert) piratesRevert();
piratesRevert = addHook(compileHook, { exts, ignoreNodeModules: false });
}
export function revert() {
if (piratesRevert) piratesRevert();
}
function escapeRegExp(string) {
return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
}
export default function register(opts: any = {}) {
// Clone to avoid mutating the arguments object with the 'delete's below.
opts = {
...opts,
};
hookExtensions(opts.extensions || DEFAULT_EXTENSIONS);
if (opts.cache === false && cache) {
registerCache.clear();
cache = null;
} else if (opts.cache !== false && !cache) {
registerCache.load();
cache = registerCache.get();
}
delete opts.extensions;
delete opts.cache;
transformOpts = {
...opts,
caller: {
name: "@babel/register",
...(opts.caller || {}),
},
};
let { cwd = "." } = transformOpts;
// Ensure that the working directory is resolved up front so that
// things don't break if it changes later.
cwd = transformOpts.cwd = path.resolve(cwd);
if (transformOpts.ignore === undefined && transformOpts.only === undefined) {
transformOpts.only = [
// Only compile things inside the current working directory.
// $FlowIgnore
new RegExp("^" + escapeRegExp(cwd), "i"),
];
transformOpts.ignore = [
// Ignore any node_modules inside the current working directory.
new RegExp(
"^" +
// $FlowIgnore
escapeRegExp(cwd) +
"(?:" +
path.sep +
".*)?" +
// $FlowIgnore
escapeRegExp(path.sep + "node_modules" + path.sep),
"i",
),
];
}
}

View File

@ -5,7 +5,8 @@
* and allows register to transform these modules if they are loaded externally. * and allows register to transform these modules if they are loaded externally.
*/ */
// @ts-ignore todo(flow->ts) convert to esm // TODO: Remove this file in Babel 8
const Module = require("module"); const Module = require("module");
const globalModuleCache = Module._cache; const globalModuleCache = Module._cache;

View File

@ -0,0 +1,110 @@
const path = require("path");
const ACTIONS = {
GET_DEFAULT_EXTENSIONS: "GET_DEFAULT_EXTENSIONS",
SET_OPTIONS: "SET_OPTIONS",
TRANSFORM: "TRANSFORM",
TRANSFORM_SYNC: "TRANSFORM_SYNC",
};
class Client {
#send;
constructor(send) {
this.#send = send;
}
#eCache;
/** @return {string[]} */
getDefaultExtensions() {
return (this.#eCache ??= this.#send(
ACTIONS.GET_DEFAULT_EXTENSIONS,
undefined,
));
}
/**
* @param {object} options
* @return {void}
*/
setOptions(options) {
return this.#send(ACTIONS.SET_OPTIONS, options);
}
/**
* @param {string} code
* @param {string} filename
* @return {{ code: string, map: object } | null}
*/
transform(code, filename) {
return this.#send(ACTIONS.TRANSFORM, { code, filename });
}
}
// We need to run Babel in a worker because require hooks must
// run synchronously, but many steps of Babel's config loading
// (which is done for each file) can be asynchronous
exports.WorkerClient = class WorkerClient extends Client {
// These two require() calls are in deferred so that they are not imported in
// older Node.js versions (which don't support workers).
// TODO: Hoist them in Babel 8.
/** @type {typeof import("worker_threads")} */
static get #worker_threads() {
return require("worker_threads");
}
static get #markInRegisterWorker() {
return require("./is-in-register-worker").markInRegisterWorker;
}
#worker = new WorkerClient.#worker_threads.Worker(
path.resolve(__dirname, "./worker/index.js"),
{ env: WorkerClient.#markInRegisterWorker(process.env) },
);
#signal = new Int32Array(new SharedArrayBuffer(4));
constructor() {
super((action, payload) => {
this.#signal[0] = 0;
const subChannel = new WorkerClient.#worker_threads.MessageChannel();
this.#worker.postMessage(
{ signal: this.#signal, port: subChannel.port1, action, payload },
[subChannel.port1],
);
Atomics.wait(this.#signal, 0, 0);
const { message } = WorkerClient.#worker_threads.receiveMessageOnPort(
subChannel.port2,
);
if (message.error) throw Object.assign(message.error, message.errorData);
else return message.result;
});
// The worker will never exit by itself. Prevent it from keeping
// the main process alive.
this.#worker.unref();
}
};
if (!process.env.BABEL_8_BREAKING) {
exports.LocalClient = class LocalClient extends Client {
isLocalClient = true;
static #handleMessage;
constructor() {
LocalClient.#handleMessage ??= require("./worker/handle-message");
super((action, payload) => {
return LocalClient.#handleMessage(
action === ACTIONS.TRANSFORM ? ACTIONS.TRANSFORM_SYNC : action,
payload,
);
});
}
};
}

View File

@ -0,0 +1,20 @@
function initialize(babel) {
exports.init = null;
exports.version = babel.version;
exports.DEFAULT_EXTENSIONS = babel.DEFAULT_EXTENSIONS;
exports.loadOptionsAsync = babel.loadOptionsAsync;
exports.transformAsync = babel.transformAsync;
exports.getEnv = babel.getEnv;
if (!process.env.BABEL_8_BREAKING) {
exports.loadOptionsSync = babel.loadOptionsSync;
exports.transformSync = babel.transformSync;
}
}
if (process.env.BABEL_8_BREAKING) {
// @ts-expect-error CJS-ESM interop.
exports.init = import("@babel/core").then(ns => initialize(ns.default));
} else {
initialize(require("@babel/core"));
}

View File

@ -1,8 +1,10 @@
import path from "path"; "use strict";
import fs from "fs";
import os from "os"; const path = require("path");
import * as babel from "@babel/core"; const fs = require("fs");
import findCacheDir from "find-cache-dir"; const os = require("os");
const babel = require("@babel/core");
const findCacheDir = require("find-cache-dir");
const DEFAULT_CACHE_DIR = const DEFAULT_CACHE_DIR =
findCacheDir({ name: "@babel/register" }) || os.homedir() || os.tmpdir(); findCacheDir({ name: "@babel/register" }) || os.homedir() || os.tmpdir();
@ -10,8 +12,9 @@ const DEFAULT_FILENAME = path.join(
DEFAULT_CACHE_DIR, DEFAULT_CACHE_DIR,
`.babel.${babel.version}.${babel.getEnv()}.json`, `.babel.${babel.version}.${babel.getEnv()}.json`,
); );
const FILENAME: string = process.env.BABEL_CACHE_PATH || DEFAULT_FILENAME;
let data: any = {}; const FILENAME = process.env.BABEL_CACHE_PATH || DEFAULT_FILENAME;
let data = {};
let cacheDirty = false; let cacheDirty = false;
@ -20,15 +23,16 @@ let cacheDisabled = false;
function isCacheDisabled() { function isCacheDisabled() {
return process.env.BABEL_DISABLE_CACHE ?? cacheDisabled; return process.env.BABEL_DISABLE_CACHE ?? cacheDisabled;
} }
exports.save = save;
/** /**
* Write stringified cache to disk. * Write stringified cache to disk.
*/ */
function save() {
export function save() {
if (isCacheDisabled() || !cacheDirty) return; if (isCacheDisabled() || !cacheDirty) return;
cacheDirty = false; cacheDirty = false;
let serialised: string = "{}"; let serialised = "{}";
try { try {
serialised = JSON.stringify(data, null, " "); serialised = JSON.stringify(data, null, " ");
@ -74,7 +78,7 @@ because it resides in a readonly filesystem. Cache is disabled.`,
* Load cache from disk and parse. * Load cache from disk and parse.
*/ */
export function load() { exports.load = function load() {
if (isCacheDisabled()) { if (isCacheDisabled()) {
data = {}; data = {};
return; return;
@ -106,27 +110,25 @@ due to a permission issue. Cache is disabled.`,
try { try {
data = JSON.parse(cacheContent); data = JSON.parse(cacheContent);
} catch {} } catch {}
} };
/** /**
* Retrieve data from cache. * Retrieve data from cache.
*/ */
exports.get = function get() {
export function get(): any {
return data; return data;
} };
/** /**
* Set the cache dirty bit. * Set the cache dirty bit.
*/ */
export function setDirty() { exports.setDirty = function setDirty() {
cacheDirty = true; cacheDirty = true;
} };
/** /**
* Clear the cache object. * Clear the cache object.
*/ */
exports.clear = function clear() {
export function clear() {
data = {}; data = {};
} };

View File

@ -0,0 +1,20 @@
const babel = require("./babel-core");
const { setOptions, transform, transformSync } = require("./transform");
module.exports = function handleMessage(action, payload) {
switch (action) {
case "GET_DEFAULT_EXTENSIONS":
return babel.DEFAULT_EXTENSIONS;
case "SET_OPTIONS":
setOptions(payload);
return;
case "TRANSFORM":
return transform(payload.code, payload.filename);
case "TRANSFORM_SYNC":
if (!process.env.BABEL_8_BREAKING) {
return transformSync(payload.code, payload.filename);
}
}
throw new Error(`Unknown internal parser worker action: ${action}`);
};

View File

@ -0,0 +1,28 @@
const babel = require("./babel-core");
const handleMessage = require("./handle-message");
const { parentPort } = require("worker_threads");
parentPort.addListener("message", async ({ signal, port, action, payload }) => {
let response;
try {
if (babel.init) await babel.init;
response = { result: await handleMessage(action, payload) };
} catch (error) {
response = { error, errorData: { ...error } };
}
try {
port.postMessage(response);
} catch {
port.postMessage({
error: new Error("Cannot serialize worker response"),
});
} finally {
port.close();
Atomics.store(signal, 0, 1);
Atomics.notify(signal, 0);
}
});

View File

@ -0,0 +1,128 @@
"use strict";
const cloneDeep = require("clone-deep");
const path = require("path");
const fs = require("fs");
const babel = require("./babel-core");
const registerCache = require("../cache");
const nmRE = escapeRegExp(path.sep + "node_modules" + path.sep);
function escapeRegExp(string) {
return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
}
let cache;
let transformOpts;
exports.setOptions = function (opts) {
if (opts.cache === false && cache) {
registerCache.clear();
cache = null;
} else if (opts.cache !== false && !cache) {
registerCache.load();
cache = registerCache.get();
}
delete opts.cache;
delete opts.extensions;
transformOpts = {
...opts,
caller: {
name: "@babel/register",
...(opts.caller || {}),
},
};
let { cwd = "." } = transformOpts;
// Ensure that the working directory is resolved up front so that
// things don't break if it changes later.
cwd = transformOpts.cwd = path.resolve(cwd);
if (transformOpts.ignore === undefined && transformOpts.only === undefined) {
const cwdRE = escapeRegExp(cwd);
// Only compile things inside the current working directory.
transformOpts.only = [new RegExp("^" + cwdRE, "i")];
// Ignore any node_modules inside the current working directory.
transformOpts.ignore = [
new RegExp(`^${cwdRE}(?:${path.sep}.*)?${nmRE}`, "i"),
];
}
};
exports.transform = async function (input, filename) {
const opts = await babel.loadOptionsAsync({
// sourceRoot can be overwritten
sourceRoot: path.dirname(filename) + path.sep,
...cloneDeep(transformOpts),
filename,
});
// Bail out ASAP if the file has been ignored.
if (opts === null) return null;
const { cached, store } = cacheLookup(opts, filename);
if (cached) return cached;
const { code, map } = await babel.transformAsync(input, {
...opts,
sourceMaps: opts.sourceMaps === undefined ? "both" : opts.sourceMaps,
ast: false,
});
return store({ code, map });
};
if (!process.env.BABEL_8_BREAKING) {
exports.transformSync = function (input, filename) {
const opts = babel.loadOptionsSync({
// sourceRoot can be overwritten
sourceRoot: path.dirname(filename) + path.sep,
...cloneDeep(transformOpts),
filename,
});
// Bail out ASAP if the file has been ignored.
if (opts === null) return null;
const { cached, store } = cacheLookup(opts, filename);
if (cached) return cached;
const { code, map } = babel.transformSync(input, {
...opts,
sourceMaps: opts.sourceMaps === undefined ? "both" : opts.sourceMaps,
ast: false,
});
return store({ code, map });
};
}
const id = value => value;
function cacheLookup(opts, filename) {
if (!cache) return { cached: null, store: id };
let cacheKey = `${JSON.stringify(opts)}:${babel.version}`;
const env = babel.getEnv();
if (env) cacheKey += `:${env}`;
const cached = cache[cacheKey];
const fileMtime = +fs.statSync(filename).mtime;
if (cached && cached.mtime === fileMtime) {
return { cached: cached.value, store: id };
}
return {
cached: null,
store(value) {
cache[cacheKey] = { value, mtime: fileMtime };
return value;
},
};
}

View File

@ -1,5 +1,6 @@
{ {
"plugins": [ "plugins": [
"@babel/transform-modules-commonjs" "@babel/transform-modules-commonjs",
"@babel/plugin-transform-arrow-functions"
] ]
} }

View File

@ -0,0 +1 @@
console.log("It worked!", (() => {}).toString());

View File

@ -1,23 +1,10 @@
const register = require('../../..'); const register = require('../../..');
// Plugin to add '/* transformed */' comment to start of function bodies
const plugin = () => ( {
visitor: {
Function(path) {
const bodyNode = path.node.body;
(bodyNode.leadingComments || (bodyNode.leadingComments = [])).push( {
type: 'CommentBlock',
value: ' transformed '
} );
},
},
} );
register( { register( {
ignore: [], ignore: [],
babelrc: false, babelrc: false,
configFile: false, configFile: false,
plugins: [plugin] plugins: [require.resolve("./plugin")]
} ); } );
console.log( console.log(

View File

@ -0,0 +1,13 @@
// Plugin to add '/* transformed */' comment to start of function bodies
module.exports = () => ( {
visitor: {
Function(path) {
const bodyNode = path.node.body;
(bodyNode.leadingComments || (bodyNode.leadingComments = [])).push( {
type: 'CommentBlock',
value: ' transformed '
} );
},
},
} );

View File

@ -0,0 +1,3 @@
export default {
plugins: ["@babel/transform-modules-commonjs"],
};

View File

@ -0,0 +1 @@
import "assert";

View File

@ -7,10 +7,12 @@ import { fileURLToPath } from "url";
const dirname = path.dirname(fileURLToPath(import.meta.url)); const dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const registerFile = require.resolve("../lib/index");
const testCacheFilename = path.join(dirname, ".index.babel"); const testCacheFilename = path.join(dirname, ".index.babel");
const testFile = require.resolve("./fixtures/babelrc/es2015"); const testFile = require.resolve("./fixtures/babelrc/es2015");
const testFileContent = fs.readFileSync(testFile); 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 piratesPath = require.resolve("pirates");
const smsPath = require.resolve("source-map-support"); const smsPath = require.resolve("source-map-support");
@ -57,48 +59,13 @@ describe("@babel/register", function () {
}, },
}; };
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();
}
beforeEach(() => { beforeEach(() => {
currentHook = null; currentHook = null;
currentOptions = null; currentOptions = null;
sourceMapSupport = false; sourceMapSupport = false;
}); });
afterEach(async () => { let originalRequireCacheDescriptor;
// @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();
});
if (OLD_JEST_MOCKS) { if (OLD_JEST_MOCKS) {
jest.doMock("pirates", () => mocks["pirates"]); jest.doMock("pirates", () => mocks["pirates"]);
jest.doMock("source-map-support", () => mocks["source-map-support"]); jest.doMock("source-map-support", () => mocks["source-map-support"]);
@ -107,7 +74,6 @@ describe("@babel/register", function () {
jest.resetModules(); jest.resetModules();
}); });
} else { } else {
let originalRequireCacheDescriptor;
beforeAll(() => { beforeAll(() => {
originalRequireCacheDescriptor = Object.getOwnPropertyDescriptor( originalRequireCacheDescriptor = Object.getOwnPropertyDescriptor(
Module, Module,
@ -115,142 +81,275 @@ describe("@babel/register", function () {
); );
}); });
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,
});
});
afterAll(() => { afterAll(() => {
Object.defineProperty(Module, "_cache", originalRequireCacheDescriptor); Object.defineProperty(Module, "_cache", originalRequireCacheDescriptor);
}); });
} }
test("registers hook correctly", () => { if (!process.env.BABEL_8_BREAKING) {
setupRegister(); describe("babel 7", () => {
if (!OLD_JEST_MOCKS) {
beforeEach(() => {
const isEmptyObj = obj =>
Object.getPrototypeOf(obj) === null &&
Object.keys(obj).length === 0;
expect(typeof currentHook).toBe("function"); // This setter intercepts the Module._cache assignment in
expect(currentOptions).toEqual(defaultOptions); // 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);
test("unregisters hook correctly", () => { Object.defineProperty(Module, "_cache", {
setupRegister(); value,
revertRegister(); 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,
});
});
}
expect(currentHook).toBeNull(); buildTests(require.resolve(".."));
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); 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);
};
expect(sourceMapSupport).toBe(true); nodeGte12(describe, "worker", () => {
}); if (!OLD_JEST_MOCKS) {
beforeEach(() => {
Object.defineProperty(Module, "_cache", {
...originalRequireCacheDescriptor,
value: {
[piratesPath]: { exports: mocks["pirates"] },
[smsPath]: { exports: mocks["source-map-support"] },
},
});
});
}
test("does not install source map support if asked not to", () => { const { setupRegister } = buildTests(
setupRegister({ require.resolve("../experimental-worker"),
babelrc: false,
sourceMaps: false,
});
currentHook("const a = 1;", testFile);
expect(sourceMapSupport).toBe(false);
});
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"),
); );
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");');
});
}); });
test("hook transpiles with config", () => { function buildTests(registerFile) {
setupRegister({ let babelRegister;
babelrc: false,
sourceMaps: false, function setupRegister(config = { babelrc: false }) {
plugins: ["@babel/transform-modules-commonjs"], 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();
}); });
const result = currentHook(testFileContent, testFile); afterAll(() => {
resetCache();
expect(result).toBe('"use strict";\n\nrequire("assert");');
});
test("hook transpiles with babelrc", () => {
setupRegister({
babelrc: true,
sourceMaps: false,
}); });
const result = currentHook(testFileContent, testFile); test("registers hook correctly", () => {
setupRegister();
expect(result).toBe('"use strict";\n\nrequire("assert");'); expect(typeof currentHook).toBe("function");
}); expect(currentOptions).toEqual(defaultOptions);
});
test("transforms modules used within register", async () => { test("unregisters hook correctly", () => {
// Need a clean environment without `convert-source-map` setupRegister();
// already in the require cache, so we spawn a separate process revertRegister();
const output = await spawnNodeAsync([ expect(currentHook).toBeNull();
require.resolve("./fixtures/internal-modules/index.js"), expect(currentOptions).toBeNull();
]); });
const { convertSourceMap } = JSON.parse(output);
expect(convertSourceMap).toMatch("/* transformed */"); 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 */");
});
return { setupRegister, revertRegister };
}
}); });
function spawnNodeAsync(args) { function spawnNodeAsync(args, cwd = dirname, env) {
const spawn = child.spawn(process.execPath, args, { cwd: dirname }); const spawn = child.spawn(process.execPath, args, { cwd, env });
let output = ""; let output = "";
let callback; let callback;

View File

@ -3494,6 +3494,7 @@ __metadata:
resolution: "@babel/register@workspace:packages/babel-register" resolution: "@babel/register@workspace:packages/babel-register"
dependencies: dependencies:
"@babel/core": "workspace:^" "@babel/core": "workspace:^"
"@babel/plugin-transform-arrow-functions": "workspace:^"
"@babel/plugin-transform-modules-commonjs": "workspace:^" "@babel/plugin-transform-modules-commonjs": "workspace:^"
browserify: ^16.5.2 browserify: ^16.5.2
clone-deep: ^4.0.1 clone-deep: ^4.0.1