diff --git a/.babelrc.js b/.babelrc.js index 9a6311a2c5..4a6d9dc764 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -46,7 +46,9 @@ module.exports = function(api) { "@babel/proposal-export-namespace-from", "@babel/proposal-numeric-separator", ["@babel/proposal-object-rest-spread", { useBuiltIns: true }], - convertESM ? "@babel/transform-modules-commonjs" : null, + + // Explicitly use the lazy version of CommonJS modules. + convertESM ? ["@babel/transform-modules-commonjs", { lazy: true }] : null, ].filter(Boolean), overrides: [ { @@ -56,6 +58,15 @@ module.exports = function(api) { ["@babel/transform-for-of", { assumeArray: true }], ], }, + { + test: "./packages/babel-register", + plugins: [ + // Override the root options to disable lazy imports for babel-register + // because otherwise the require hook will try to lazy-import things + // leading to dependency cycles. + convertESM ? "@babel/transform-modules-commonjs" : null, + ].filter(Boolean), + }, ], }; diff --git a/packages/babel-node/test/index.js b/packages/babel-node/test/index.js index 4643475255..eea0fbc410 100644 --- a/packages/babel-node/test/index.js +++ b/packages/babel-node/test/index.js @@ -64,7 +64,7 @@ const assertTest = function(stdout, stderr, opts) { chai.expect(stderr).to.equal(expectStderr, "stderr didn't match"); } } else if (stderr) { - throw new Error("stderr:\n" + stderr); + throw new Error("stderr:\n" + stderr + "\n\nstdout:\n" + stdout); } const expectStdout = opts.stdout.trim(); diff --git a/packages/babel-register/README.md b/packages/babel-register/README.md index a72e2fb003..dc8a475ef5 100644 --- a/packages/babel-register/README.md +++ b/packages/babel-register/README.md @@ -100,3 +100,34 @@ Disable the cache. ```sh BABEL_DISABLE_CACHE=1 babel-node script.js ``` + +## Compiling plugins and presets on the fly + +`@babel/register` uses Node's `require()` hook system to compile files +on the fly when they are loaded. While this is quite helpful overall, it means +that there can be confusing cases where code within a `require()` hook causes +_more_ calls to `require`, causing a dependency cycle. In Babel's case for +instance, this could mean that in the process of Babel trying to compile a +user's file, Babel could end up trying to compile itself _as it is loading_. + +To avoid this problem, this module explicitly disallows re-entrant compilation, +e.g. Babel's own compilation logic explicitly cannot trigger further compilation +of any other files on the fly. The downside of this is that if you want to +define a plugin or preset that is itself live-compiled, the process is +complicated. + +The crux of it is that your own code needs to load the plugin/preset first. +Assuming the plugin/preset loads all of its dependencies up front, what you'll +want to do is: + +``` +require("@babel/register")({ + // ... +}); + +require("./my-plugin"); +``` + +Because it is your own code that triggered the load, and not the logic within +`@babel/register` itself, this should successfully compile any plugin/preset +that that loads synchronously. diff --git a/packages/babel-register/src/node.js b/packages/babel-register/src/node.js index 9dbdf5d659..c3db5104cb 100644 --- a/packages/babel-register/src/node.js +++ b/packages/babel-register/src/node.js @@ -55,37 +55,47 @@ function compile(code, filename) { if (env) cacheKey += `:${env}`; - if (cache) { - const cached = cache[cacheKey]; - if (cached && cached.mtime === mtime(filename)) { - return cached.code; + let cached = cache && cache[cacheKey]; + + if (!cached || cached.mtime !== mtime(filename)) { + cached = babel.transform(code, { + ...opts, + sourceMaps: opts.sourceMaps === undefined ? "both" : opts.sourceMaps, + ast: false, + }); + + if (cache) { + cache[cacheKey] = cached; + cached.mtime = mtime(filename); } } - const result = babel.transform(code, { - ...opts, - sourceMaps: opts.sourceMaps === undefined ? "both" : opts.sourceMaps, - ast: false, - }); - - if (cache) { - cache[cacheKey] = result; - result.mtime = mtime(filename); - } - - if (result.map) { + if (cached.map) { if (Object.keys(maps).length === 0) { installSourceMapSupport(); } - maps[filename] = result.map; + maps[filename] = cached.map; } - return result.code; + return cached.code; +} + +let compiling = false; + +function compileHook(code, filename) { + if (compiling) return code; + + try { + compiling = true; + return compile(code, filename); + } finally { + compiling = false; + } } function hookExtensions(exts) { if (piratesRevert) piratesRevert(); - piratesRevert = addHook(compile, { exts, ignoreNodeModules: false }); + piratesRevert = addHook(compileHook, { exts, ignoreNodeModules: false }); } export function revert() {