Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve CJS compat with ESM-based @babel/core #15137

Merged
merged 6 commits into from Feb 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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