diff --git a/packages/babel-register/src/node.js b/packages/babel-register/src/node.js index ed527cdac549..170a7fdbc391 100644 --- a/packages/babel-register/src/node.js +++ b/packages/babel-register/src/node.js @@ -6,7 +6,74 @@ 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 path, { extname } from "path"; + +// Technically we could use the "semver" package here, but (for exmaple) +// parseFloat("4.23.6") returns 4.23 so it's "good enough" +const BABEL_SUPPORTS_EXTENSIONS_OPTION = + true || parseFloat(babel.version) >= 7.12; + +// The "pirates" library, that we use to register require hooks, does +// not allow hooking into all the loaded files, but requries us to +// specify the extensions upfront. +// In order to hook into all of them, we need to do two things: +// 1) Add hooks for all the already registered extensions, defined in +// Module._extensions. By doing so, we can shadow the loaders already +// defined. +// 2) Node throws an error when requireing .mjs unless a hook has been +// defined. For compatibility reason (@babel/register can load .mjs files) +// we add the extension to the list. +// 3) Node fallbacks to the .js loader for unknown extensions, however +// pirates will only run our hook if it actually matches the extension +// (without a fallback mechanism). However, it checks if an extension +// has been registered by checking 'extensions.indexOf(...)'. +// And... we can make it always return true! ^-^ +// Since this is not technically part of the public API of "pirates", the +// version in package.json is fixed to avoid untested updates. +function generateExtensionsArray(exts) { + return Object.defineProperty(Array.from(exts), "indexOf", { + configurable: true, + writable: true, + enumerable: false, + value: () => true, + }); +} + +// Node.js's algorithm tries to automatically resolve the extension of the +// required file. This means that if you have, for example, require("./foo") +// it can load foo.js (even if ".js" is not specified). +// This doesn't only work with predefined extensions: whenever a new extension +// hook is registered, Node.js can resolve it. +// For this reason, we need to register new extensions as soon as we know about +// them. This means: +// 1) When we compile a file and see a new extension in its `extensions` option. +// 2) When a file with a new explicit extension is loaded +// +// In practice, this means that to load foo.ts you have to use +// node -r @babel/register ./foo.ts +// instead of just +// node -r @babel/register ./foo +const knownExtensions = new Set(); +function registerNewExtensions(extensions, filename) { + if (!BABEL_SUPPORTS_EXTENSIONS_OPTION) return; + + const prevSize = knownExtensions.size; + + if (filename) { + const ext = extname(filename); + if (ext) knownExtensions.add(ext); + } + + if (extensions) { + extensions.forEach(ext => { + if (ext && ext !== "*") knownExtensions.add(ext); + }); + } + + if (knownExtensions.size !== prevSize) { + hookExtensions(generateExtensionsArray(knownExtensions)); + } +} const maps = {}; let transformOpts = {}; @@ -47,9 +114,11 @@ function compile(code, filename) { }, ); - // Bail out ASAP if the file has been ignored. + // Bail out ASAP if the file has been ignored or has an unsupported extension if (opts === null) return code; + registerNewExtensions(opts.extensions, filename); + let cacheKey = `${JSON.stringify(opts)}:${babel.version}`; const env = babel.getEnv(false); @@ -107,10 +176,17 @@ register(); export default function register(opts?: Object = {}) { // Clone to avoid mutating the arguments object with the 'delete's below. - opts = { - ...opts, - }; - hookExtensions(opts.extensions || DEFAULT_EXTENSIONS); + opts = { ...opts }; + + if (BABEL_SUPPORTS_EXTENSIONS_OPTION) { + // TODO(Babel 8): At some point @babel/core will default to DEFAULT_EXTENSIONS + // instead of ["*"], and we can avoid setting it here. + opts.extensions ??= DEFAULT_EXTENSIONS; + + registerNewExtensions(opts.extensions); + } else { + hookExtensions(opts.extensions ?? DEFAULT_EXTENSIONS); + } if (opts.cache === false && cache) { registerCache.clear(); @@ -120,8 +196,10 @@ export default function register(opts?: Object = {}) { cache = registerCache.get(); } - delete opts.extensions; delete opts.cache; + if (!BABEL_SUPPORTS_EXTENSIONS_OPTION) { + delete opts.extensions; + } transformOpts = { ...opts, diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/babel.config.json b/packages/babel-register/test/fixtures/integration/default-extensions/babel.config.json new file mode 100644 index 000000000000..0aa5ca51a010 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/babel.config.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.cjs b/packages/babel-register/test/fixtures/integration/default-extensions/foo.cjs new file mode 100644 index 000000000000..330530fe1f41 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.cjs @@ -0,0 +1 @@ +console.log("DONE: foo.cjs"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.es b/packages/babel-register/test/fixtures/integration/default-extensions/foo.es new file mode 100644 index 000000000000..f30fa10263e7 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.es @@ -0,0 +1 @@ +console.log("DONE: foo.es"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.es6 b/packages/babel-register/test/fixtures/integration/default-extensions/foo.es6 new file mode 100644 index 000000000000..c616f9f06ef7 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.es6 @@ -0,0 +1 @@ +console.log("DONE: foo.es6"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.jsx b/packages/babel-register/test/fixtures/integration/default-extensions/foo.jsx new file mode 100644 index 000000000000..d575d566d47a --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.jsx @@ -0,0 +1 @@ +console.log("DONE: foo.jsx"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.mjs b/packages/babel-register/test/fixtures/integration/default-extensions/foo.mjs new file mode 100644 index 000000000000..cc2f2aeae020 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.mjs @@ -0,0 +1 @@ +console.log("DONE: foo.mjs"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.ts b/packages/babel-register/test/fixtures/integration/default-extensions/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.tsx b/packages/babel-register/test/fixtures/integration/default-extensions/foo.tsx new file mode 100644 index 000000000000..d39873b6909a --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.tsx @@ -0,0 +1 @@ +console.log("DONE: foo.tsx"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/index.js b/packages/babel-register/test/fixtures/integration/default-extensions/index.js new file mode 100644 index 000000000000..154e3b733489 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/index.js @@ -0,0 +1,11 @@ +require("./foo.jsx"); +require("./foo.es6"); +require("./foo.es"); +require("./foo.mjs"); + +// Not enabled by default +require("./foo.ts"); +require("./foo.tsx"); +require("./foo.cjs"); + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/fixtures/integration/load-ts/babel.config.json b/packages/babel-register/test/fixtures/integration/load-ts/babel.config.json new file mode 100644 index 000000000000..d1db72a50f40 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/load-ts/babel.config.json @@ -0,0 +1,4 @@ +{ + "extensions": [".js", ".ts"], + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/load-ts/foo.ts b/packages/babel-register/test/fixtures/integration/load-ts/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/load-ts/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/load-ts/index.js b/packages/babel-register/test/fixtures/integration/load-ts/index.js new file mode 100644 index 000000000000..83bf0994a783 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/load-ts/index.js @@ -0,0 +1,3 @@ +require("./foo.ts"); + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/fixtures/integration/logger.js b/packages/babel-register/test/fixtures/integration/logger.js new file mode 100644 index 000000000000..4ac49d441621 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/logger.js @@ -0,0 +1,9 @@ +module.exports = function () { + return { + pre() { + console.log( + `LOADED: ${JSON.stringify(this.filename.replace(__dirname, ""))}`, + ); + }, + }; +}; diff --git a/packages/babel-register/test/fixtures/integration/no-load-ts/babel.config.json b/packages/babel-register/test/fixtures/integration/no-load-ts/babel.config.json new file mode 100644 index 000000000000..43728ca07711 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/no-load-ts/babel.config.json @@ -0,0 +1,4 @@ +{ + "extensions": [".js"], + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/no-load-ts/foo.ts b/packages/babel-register/test/fixtures/integration/no-load-ts/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/no-load-ts/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/no-load-ts/index.js b/packages/babel-register/test/fixtures/integration/no-load-ts/index.js new file mode 100644 index 000000000000..83bf0994a783 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/no-load-ts/index.js @@ -0,0 +1,3 @@ +require("./foo.ts"); + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/babel.config.json b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/babel.config.json new file mode 100644 index 000000000000..d1db72a50f40 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/babel.config.json @@ -0,0 +1,4 @@ +{ + "extensions": [".js", ".ts"], + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/foo.ts b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/index.js b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/index.js new file mode 100644 index 000000000000..ffd1086b7c98 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/index.js @@ -0,0 +1,3 @@ +require("./foo"); // <-- No extensino here! + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/babel.config.json b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/babel.config.json new file mode 100644 index 000000000000..2cfc4d53c604 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/babel.config.json @@ -0,0 +1,4 @@ +{ + "extensions": ["*"], + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/foo.ts b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/index.ts b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/index.ts new file mode 100644 index 000000000000..c7b2c26c353e --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/index.ts @@ -0,0 +1,3 @@ +require("./foo"); // <-- No extensino here! + +console.log("DONE: index.ts"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/babel.config.json b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/babel.config.json new file mode 100644 index 000000000000..2cfc4d53c604 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/babel.config.json @@ -0,0 +1,4 @@ +{ + "extensions": ["*"], + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/bar.ts b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/bar.ts new file mode 100644 index 000000000000..1cb1cf93c6b6 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/bar.ts @@ -0,0 +1 @@ +console.log("DONE: bar.ts"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/foo.ts b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/foo.ts new file mode 100644 index 000000000000..021b83e292c6 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/foo.ts @@ -0,0 +1,3 @@ +require("./bar"); // <-- no extension! + +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/index.js b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/index.js new file mode 100644 index 000000000000..83bf0994a783 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/index.js @@ -0,0 +1,3 @@ +require("./foo.ts"); + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/index.js b/packages/babel-register/test/index.js index c83bb5e04c88..2a2bf2973be1 100644 --- a/packages/babel-register/test/index.js +++ b/packages/babel-register/test/index.js @@ -37,11 +37,6 @@ jest.mock("source-map-support", () => { }; }); -const defaultOptions = { - exts: [".js", ".jsx", ".es6", ".es", ".mjs"], - ignoreNodeModules: false, -}; - function cleanCache() { try { fs.unlinkSync(testCacheFilename); @@ -93,7 +88,8 @@ describe("@babel/register", function () { setupRegister(); expect(typeof currentHook).toBe("function"); - expect(currentOptions).toEqual(defaultOptions); + expect(currentOptions.exts).toContain(".js"); + expect(currentOptions.ignoreNodeModules).toBe(false); }); test("unregisters hook correctly", () => { diff --git a/packages/babel-register/test/integration.js b/packages/babel-register/test/integration.js new file mode 100644 index 000000000000..a9c0a6222fdd --- /dev/null +++ b/packages/babel-register/test/integration.js @@ -0,0 +1,113 @@ +import { exec as execCb } from "child_process"; + +// TODO(Babel 8): Use util.promisify(execCb) +const exec = (...args) => + new Promise((resolve, reject) => { + execCb(...args, (error, stdout, stderr) => { + if (error) reject(error); + else resolve({ stdout, stderr }); + }); + }); + +function fixture(name, file = "index.js") { + const cwd = `${__dirname}/fixtures/integration/${name}`; + return exec(`node -r ${__dirname}/.. ${cwd}/${file}`, { + cwd, + env: { ...process.env, BABEL_DISABLE_CACHE: true }, + }); +} + +describe("integration tests", function () { + it("can hook into extensions defined by the config", async () => { + const { stdout, stderr } = await fixture("load-ts"); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/load-ts/index.js\\" + LOADED: \\"/load-ts/foo.ts\\" + DONE: foo.ts + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("does not hook into unknown extensions", async () => { + const { stdout, stderr } = await fixture("no-load-ts"); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/no-load-ts/index.js\\" + DONE: foo.ts + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("hooks into the default extensions", async () => { + const { stdout, stderr } = await fixture("default-extensions"); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/default-extensions/index.js\\" + LOADED: \\"/default-extensions/foo.jsx\\" + DONE: foo.jsx + LOADED: \\"/default-extensions/foo.es6\\" + DONE: foo.es6 + LOADED: \\"/default-extensions/foo.es\\" + DONE: foo.es + LOADED: \\"/default-extensions/foo.mjs\\" + DONE: foo.mjs + DONE: foo.ts + DONE: foo.tsx + DONE: foo.cjs + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("node resolves extensions that it gets from the Babel config", async () => { + const { stdout, stderr } = await fixture("resolve-extension-from-config"); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/resolve-extension-from-config/index.js\\" + LOADED: \\"/resolve-extension-from-config/foo.ts\\" + DONE: foo.ts + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("node resolves extensions that it gets from the main file", async () => { + const { stdout, stderr } = await fixture( + "resolve-extension-from-main-file", + "index.ts", + ); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/resolve-extension-from-main-file/index.ts\\" + LOADED: \\"/resolve-extension-from-main-file/foo.ts\\" + DONE: foo.ts + DONE: index.ts + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("node resolves extensions that it gets from a required file", async () => { + const { stdout, stderr } = await fixture( + "resolve-extension-from-required-file", + ); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/resolve-extension-from-required-file/index.js\\" + LOADED: \\"/resolve-extension-from-required-file/foo.ts\\" + LOADED: \\"/resolve-extension-from-required-file/bar.ts\\" + DONE: bar.ts + DONE: foo.ts + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); +});