Skip to content

Commit

Permalink
Use the new "extensions" option in @babel/register
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Oct 9, 2020
1 parent d57bac4 commit c19499e
Show file tree
Hide file tree
Showing 29 changed files with 273 additions and 13 deletions.
92 changes: 85 additions & 7 deletions packages/babel-register/src/node.js
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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,
Expand Down
@@ -0,0 +1,3 @@
{
"plugins": ["../logger"]
}
@@ -0,0 +1 @@
console.log("DONE: foo.cjs");
@@ -0,0 +1 @@
console.log("DONE: foo.es");
@@ -0,0 +1 @@
console.log("DONE: foo.es6");
@@ -0,0 +1 @@
console.log("DONE: foo.jsx");
@@ -0,0 +1 @@
console.log("DONE: foo.mjs");
@@ -0,0 +1 @@
console.log("DONE: foo.ts");
@@ -0,0 +1 @@
console.log("DONE: foo.tsx");
@@ -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");
@@ -0,0 +1,4 @@
{
"extensions": [".js", ".ts"],
"plugins": ["../logger"]
}
@@ -0,0 +1 @@
console.log("DONE: foo.ts");
@@ -0,0 +1,3 @@
require("./foo.ts");

console.log("DONE: index.js");
9 changes: 9 additions & 0 deletions 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, "<ROOT>"))}`,
);
},
};
};
@@ -0,0 +1,4 @@
{
"extensions": [".js"],
"plugins": ["../logger"]
}
@@ -0,0 +1 @@
console.log("DONE: foo.ts");
@@ -0,0 +1,3 @@
require("./foo.ts");

console.log("DONE: index.js");
@@ -0,0 +1,4 @@
{
"extensions": [".js", ".ts"],
"plugins": ["../logger"]
}
@@ -0,0 +1 @@
console.log("DONE: foo.ts");
@@ -0,0 +1,3 @@
require("./foo"); // <-- No extensino here!

console.log("DONE: index.js");
@@ -0,0 +1,4 @@
{
"extensions": ["*"],
"plugins": ["../logger"]
}
@@ -0,0 +1 @@
console.log("DONE: foo.ts");
@@ -0,0 +1,3 @@
require("./foo"); // <-- No extensino here!

console.log("DONE: index.ts");
@@ -0,0 +1,4 @@
{
"extensions": ["*"],
"plugins": ["../logger"]
}
@@ -0,0 +1 @@
console.log("DONE: bar.ts");
@@ -0,0 +1,3 @@
require("./bar"); // <-- no extension!

console.log("DONE: foo.ts");
@@ -0,0 +1,3 @@
require("./foo.ts");

console.log("DONE: index.js");
8 changes: 2 additions & 6 deletions packages/babel-register/test/index.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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", () => {
Expand Down
113 changes: 113 additions & 0 deletions 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: \\"<ROOT>/load-ts/index.js\\"
LOADED: \\"<ROOT>/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: \\"<ROOT>/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: \\"<ROOT>/default-extensions/index.js\\"
LOADED: \\"<ROOT>/default-extensions/foo.jsx\\"
DONE: foo.jsx
LOADED: \\"<ROOT>/default-extensions/foo.es6\\"
DONE: foo.es6
LOADED: \\"<ROOT>/default-extensions/foo.es\\"
DONE: foo.es
LOADED: \\"<ROOT>/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: \\"<ROOT>/resolve-extension-from-config/index.js\\"
LOADED: \\"<ROOT>/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: \\"<ROOT>/resolve-extension-from-main-file/index.ts\\"
LOADED: \\"<ROOT>/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: \\"<ROOT>/resolve-extension-from-required-file/index.js\\"
LOADED: \\"<ROOT>/resolve-extension-from-required-file/foo.ts\\"
LOADED: \\"<ROOT>/resolve-extension-from-required-file/bar.ts\\"
DONE: bar.ts
DONE: foo.ts
DONE: index.js
"
`);
expect(stderr).toMatchInlineSnapshot(`""`);
});
});

0 comments on commit c19499e

Please sign in to comment.