Skip to content

Commit

Permalink
Improve CJS compat with ESM-based @babel/core (#15137)
Browse files Browse the repository at this point in the history
Co-authored-by: liuxingbaoyu <30521560+liuxingbaoyu@users.noreply.github.com>
  • Loading branch information
nicolo-ribaudo and liuxingbaoyu committed Feb 18, 2023
1 parent 43dce19 commit ca52e08
Show file tree
Hide file tree
Showing 16 changed files with 286 additions and 5 deletions.
5 changes: 5 additions & 0 deletions babel.config.js
Expand Up @@ -231,6 +231,11 @@ module.exports = function (api) {
{ name: "USE_ESM", value: outputType === "module" },
"flag-USE_ESM",
],
[
pluginToggleBooleanFlag,
{ name: "IS_STANDALONE", value: env === "standalone" },
"flag-IS_STANDALONE",
],

process.env.STRIP_BABEL_8_FLAG && [
pluginToggleBooleanFlag,
Expand Down
32 changes: 27 additions & 5 deletions packages/babel-core/cjs-proxy.cjs
@@ -1,6 +1,12 @@
"use strict";

const babelP = import("./lib/index.js");
let babel = null;
Object.defineProperty(exports, "__ initialize @babel/core cjs proxy __", {
set(val) {
babel = val;
},
});

const functionNames = [
"createConfigItem",
Expand All @@ -11,13 +17,9 @@ const functionNames = [
"transformFromAst",
"parse",
];
const propertyNames = ["types", "tokTypes", "traverse", "template", "version"];

for (const name of functionNames) {
exports[`${name}Sync`] = function () {
throw new Error(
`"${name}Sync" is not supported when loading @babel/core using require()`
);
};
exports[name] = function (...args) {
babelP.then(babel => {
babel[name](...args);
Expand All @@ -26,4 +28,24 @@ for (const name of functionNames) {
exports[`${name}Async`] = function (...args) {
return babelP.then(babel => babel[`${name}Async`](...args));
};
exports[`${name}Sync`] = function (...args) {
if (!babel) throw notLoadedError(`${name}Sync`, "callable");
return babel[`${name}Sync`](...args);
};
}

for (const name of propertyNames) {
Object.defineProperty(exports, name, {
get() {
if (!babel) throw notLoadedError(name, "accessible");
return babel[name];
},
});
}

function notLoadedError(name, keyword) {
return new Error(
`The \`${name}\` export of @babel/core is only ${keyword}` +
` from the CommonJS version after that the ESM version is loaded.`
);
}
14 changes: 14 additions & 0 deletions packages/babel-core/src/index.ts
@@ -1,4 +1,6 @@
declare const PACKAGE_JSON: { name: string; version: string };
declare const USE_ESM: boolean, IS_STANDALONE: boolean;

export const version = PACKAGE_JSON.version;

export { default as File } from "./transformation/file/file";
Expand Down Expand Up @@ -72,15 +74,27 @@ export const DEFAULT_EXTENSIONS = Object.freeze([
] as const);

// For easier backward-compatibility, provide an API like the one we exposed in Babel 6.
// TODO(Babel 8): Remove this.
import { loadOptionsSync } from "./config";
export class OptionManager {
init(opts: {}) {
return loadOptionsSync(opts);
}
}

// TODO(Babel 8): Remove this.
export function Plugin(alias: string) {
throw new Error(
`The (${alias}) Babel 5 plugin is being run with an unsupported Babel version.`,
);
}

import Module from "module";
import * as thisFile from "./index";
if (USE_ESM) {
if (!IS_STANDALONE) {
// Pass this module to the CJS proxy, so that it can be synchronously accessed.
const cjsProxy = Module.createRequire(import.meta.url)("../cjs-proxy.cjs");
cjsProxy["__ initialize @babel/core cjs proxy __"] = thisFile;
}
}
109 changes: 109 additions & 0 deletions packages/babel-core/test/esm-cjs-integration.js
@@ -0,0 +1,109 @@
import { execFile } from "child_process";
import { createRequire } from "module";
import { outputType } from "./helpers/esm.js";

const require = createRequire(import.meta.url);

async function run(name) {
return new Promise((res, rej) => {
execFile(
process.execPath,
[require.resolve(`./fixtures/esm-cjs-integration/${name}`)],
{ env: process.env },
(error, stdout, stderr) => {
if (error) rej(error);
res({ stdout: stdout.toString(), stderr: stderr.toString() });
},
);
});
}

(outputType === "module" ? describe : describe.skip)("usage from cjs", () => {
it("lazy plugin required", async () => {
expect(await run("lazy-plugin-required.cjs")).toMatchInlineSnapshot(`
Object {
"stderr": "",
"stdout": "\\"Replaced!\\";
",
}
`);
});

it("lazy plugin as config string", async () => {
expect(await run("lazy-plugin-as-string.cjs")).toMatchInlineSnapshot(`
Object {
"stderr": "",
"stdout": "\\"Replaced!\\";
",
}
`);
});

it("eager plugin required", async () => {
await expect(run("eager-plugin-required.cjs")).rejects.toThrow(
"The `types` export of @babel/core is only accessible from" +
" the CommonJS version after that the ESM version is loaded.",
);
});

it("eager plugin required after dynamic esm import", async () => {
expect(await run("eager-plugin-required-after-dynamic-esm-import.cjs"))
.toMatchInlineSnapshot(`
Object {
"stderr": "",
"stdout": "\\"Replaced!\\";
",
}
`);
});

it("eager plugin required after static esm import", async () => {
expect(await run("eager-plugin-required-after-static-esm-import.mjs"))
.toMatchInlineSnapshot(`
Object {
"stderr": "",
"stdout": "\\"Replaced!\\";
",
}
`);
});

it("eager plugin as config string", async () => {
expect(await run("eager-plugin-as-string.cjs")).toMatchInlineSnapshot(`
Object {
"stderr": "",
"stdout": "\\"Replaced!\\";
",
}
`);
});

it("transformSync", async () => {
await expect(run("transform-sync.cjs")).rejects.toThrow(
"The `transformSync` export of @babel/core is only callable from" +
" the CommonJS version after that the ESM version is loaded.",
);
});

it("transformSync after dynamic esm import", async () => {
expect(await run("transform-sync-after-dynamic-esm-import.cjs"))
.toMatchInlineSnapshot(`
Object {
"stderr": "",
"stdout": "REPLACE_ME;
",
}
`);
});

it("transformSync after static esm import", async () => {
expect(await run("transform-sync-after-static-esm-import.mjs"))
.toMatchInlineSnapshot(`
Object {
"stderr": "",
"stdout": "REPLACE_ME;
",
}
`);
});
});
@@ -0,0 +1,8 @@
const babel = require("../../../cjs-proxy.cjs");

babel
.transformAsync("REPLACE_ME;", {
configFile: false,
plugins: [__dirname + "/plugins/eager.cjs"],
})
.then(out => console.log(out.code), console.error);
@@ -0,0 +1,8 @@
import("../../../lib/index.js")
.then(babel =>
babel.transformAsync("REPLACE_ME;", {
configFile: false,
plugins: [require("./plugins/eager.cjs")],
}),
)
.then(out => console.log(out.code), console.error);
@@ -0,0 +1,8 @@
import * as babel from "../../../lib/index.js";

babel
.transformAsync("REPLACE_ME;", {
configFile: false,
plugins: [(await import("./plugins/eager.cjs")).default],
})
.then(out => console.log(out.code), console.error);
@@ -0,0 +1,14 @@
/*
* This test throws an error, because the plugin accesses
* @babel/core's CJS .types export before that the ESM
* version is loaded.
*/

const babel = require("../../../cjs-proxy.cjs");

babel
.transformAsync("REPLACE_ME;", {
configFile: false,
plugins: [require("./plugins/eager.cjs")],
})
.then(out => console.log(out.code), console.error);
@@ -0,0 +1,8 @@
const babel = require("../../../cjs-proxy.cjs");

babel
.transformAsync("REPLACE_ME;", {
configFile: false,
plugins: [__dirname + "/plugins/lazy.cjs"],
})
.then(out => console.log(out.code), console.error);
@@ -0,0 +1,8 @@
const babel = require("../../../cjs-proxy.cjs");

babel
.transformAsync("REPLACE_ME;", {
configFile: false,
plugins: [require("./plugins/lazy.cjs")],
})
.then(out => console.log(out.code), console.error);
@@ -0,0 +1,13 @@
const { types: t } = require("../../../../cjs-proxy.cjs");

module.exports = function () {
return {
visitor: {
Identifier(path) {
if (path.node.name === "REPLACE_ME") {
path.replaceWith(t.stringLiteral("Replaced!"));
}
},
},
};
};
@@ -0,0 +1,34 @@
/*
import { types as t } from "../../../../cjs-proxy.cjs";
export default function () {
return {
visitor: {
Identifier(path) {
if (path.node.name === "REPLACE_ME") {
path.replaceWith(t.stringLiteral("Replaced!"));
}
}
}
}
}
*/

"use strict";

Object.defineProperty(exports, "__esModule", {
value: true,
});
exports["default"] = _default;
var _core = require("../../../../cjs-proxy.cjs");
function _default() {
return {
visitor: {
Identifier: function Identifier(path) {
if (path.node.name === "REPLACE_ME") {
path.replaceWith(_core.types.stringLiteral("Replaced!"));
}
},
},
};
}
@@ -0,0 +1,3 @@
import("../../../lib/index.js")
.then(babel => babel.transformSync("REPLACE_ME;", { configFile: false }))
.then(out => console.log(out.code), console.error);
@@ -0,0 +1,4 @@
import * as babel from "../../../lib/index.js";

const out = babel.transformSync("REPLACE_ME;", { configFile: false });
console.log(out.code);
@@ -0,0 +1,9 @@
/*
* This test throws an error, because the CJS .transformSync
* is called before that the ESM version is loaded.
*/

const babel = require("../../../cjs-proxy.cjs");

const out = babel.transformSync("REPLACE_ME;", { configFile: false });
console.log(out.code);
14 changes: 14 additions & 0 deletions packages/babel-core/test/helpers/esm.js
@@ -1,6 +1,7 @@
import cp from "child_process";
import util from "util";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
import { createRequire } from "module";

Expand All @@ -12,6 +13,19 @@ const dirname = path.dirname(fileURLToPath(import.meta.url));
// "minNodeVersion": "10.0.0" <-- For Ctrl+F when dropping node 10
export const supportsESM = parseInt(process.versions.node) >= 12;

export const outputType = (() => {
try {
return fs
.readFileSync(
new URL("../../../../.module-type", import.meta.url),
"utf-8",
)
.trim();
} catch (_) {
return "script";
}
})();

export const isMJS = file => path.extname(file) === ".mjs";

export const itESM = supportsESM ? it : it.skip;
Expand Down

0 comments on commit ca52e08

Please sign in to comment.