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

Only perform config loading re-entrancy check for cjs #15923

Merged
merged 2 commits into from
Sep 4, 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
26 changes: 5 additions & 21 deletions packages/babel-core/src/config/files/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ const RELATIVE_CONFIG_FILENAMES = [

const BABELIGNORE_FILENAME = ".babelignore";

const LOADING_CONFIGS = new Set();

type ConfigCacheData = {
envName: string;
caller: CallerMetadata | undefined;
Expand Down Expand Up @@ -72,25 +70,11 @@ function* readConfigCode(
): Handler<ConfigFile | null> {
if (!nodeFs.existsSync(filepath)) return null;

// The `require()` call below can make this code reentrant if a require hook like @babel/register has been
// loaded into the system. That would cause Babel to attempt to compile the `.babelrc.js` file as it loads
// below. To cover this case, we auto-ignore re-entrant config processing.
if (LOADING_CONFIGS.has(filepath)) {
debug("Auto-ignoring usage of config %o.", filepath);
return buildConfigFileObject({}, filepath);
}

let options: unknown;
try {
LOADING_CONFIGS.add(filepath);
options = yield* loadCodeDefault(
filepath,
"You appear to be using a native ECMAScript module configuration " +
"file, which is only supported when running Babel asynchronously.",
);
} finally {
LOADING_CONFIGS.delete(filepath);
}
let options = yield* loadCodeDefault(
filepath,
"You appear to be using a native ECMAScript module configuration " +
"file, which is only supported when running Babel asynchronously.",
);

let cacheNeedsConfiguration = false;
if (typeof options === "function") {
Expand Down
27 changes: 24 additions & 3 deletions packages/babel-core/src/config/files/module-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import path from "path";
import { pathToFileURL } from "url";
import { createRequire } from "module";
import semver from "semver";
import buildDebug from "debug";

import { endHiddenCallStack } from "../../errors/rewrite-stack-trace.ts";
import ConfigError from "../../errors/config-error.ts";

import type { InputOptions } from "../index.ts";
import { transformFileSync } from "../../transform-file.ts";

const debug = buildDebug("babel:config:loading:files:module-types");

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

let import_: ((specifier: string | URL) => any) | undefined;
Expand Down Expand Up @@ -126,8 +129,7 @@ function loadCtsDefault(filepath: string) {
require.extensions[ext] = handler;
}
try {
const module = endHiddenCallStack(require)(filepath);
return module?.__esModule ? module.default : module;
return loadCjsDefault(filepath);
} finally {
if (!hasTsSupport) {
if (require.extensions[ext] === handler) delete require.extensions[ext];
Expand All @@ -136,8 +138,27 @@ function loadCtsDefault(filepath: string) {
}
}

const LOADING_CJS_FILES = new Set();

function loadCjsDefault(filepath: string) {
const module = endHiddenCallStack(require)(filepath);
// The `require()` call below can make this code reentrant if a require hook
// like @babel/register has been loaded into the system. That would cause
// Babel to attempt to compile the `.babelrc.js` file as it loads below. To
// cover this case, we auto-ignore re-entrant config processing. ESM loaders
// do not have this problem, because loaders do not apply to themselves.
if (LOADING_CJS_FILES.has(filepath)) {
debug("Auto-ignoring usage of config %o.", filepath);
return {};
}

let module;
try {
LOADING_CJS_FILES.add(filepath);
module = endHiddenCallStack(require)(filepath);
} finally {
LOADING_CJS_FILES.delete(filepath);
}

if (process.env.BABEL_8_BREAKING) {
return module?.__esModule ? module.default : module;
} else {
Expand Down
32 changes: 32 additions & 0 deletions packages/babel-core/test/config-loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import {
loadOptionsSync,
loadOptionsAsync,
loadPartialConfig,
loadPartialConfigAsync,
loadPartialConfigSync,
createConfigItem,
createConfigItemSync,
} from "../lib/index.js";
import path from "path";
import { itNoWin32, itBabel8, commonJS } from "$repo-utils";
import { supportsESM } from "./helpers/esm.js";

const { require, __dirname } = commonJS(import.meta.url);

Expand Down Expand Up @@ -166,6 +168,36 @@ describe("@babel/core config loading", () => {
});
});

describe("loadPartialConfigAsync", () => {
// https://github.com/babel/babel/issues/15916
(supportsESM ? it : it.skip)(
JLHwung marked this conversation as resolved.
Show resolved Hide resolved
"two calls in parallel loading the same ESM config",
async () => {
const cwd = path.join(
__dirname,
"fixtures",
"config",
"config-files",
"babel-config-mjs-object",
);

const [config1, config2] = await Promise.all([
loadPartialConfigAsync({
cwd,
filename: path.join(cwd, "./a.js"),
}),
loadPartialConfigAsync({
cwd,
filename: path.join(cwd, "./b.js"),
}),
]);

// eslint-disable-next-line jest/no-standalone-expect
expect(config1.options.plugins).toEqual(config2.options.plugins);
},
);
});

describe("loadOptions", () => {
itBabel8("throws on undefined callback", () => {
const opts = makeOpts();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
plugins: [() => ({})],
};