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

Respect package.json#exports when resolving plugins #14110

Merged
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
1 change: 1 addition & 0 deletions .eslintignore
Expand Up @@ -22,6 +22,7 @@ packages/babel-preset-env/test/debug-fixtures
packages/babel-standalone/babel.js
packages/babel-standalone/babel.min.js
packages/babel-parser/test/expressions
packages/babel-core/src/vendor

eslint/*/lib
eslint/*/node_modules
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -25,6 +25,9 @@ package-lock.json

/packages/babel-compat-data/build

/packages/babel-core/src/vendor/*.js
/packages/babel-core/src/vendor/*.ts

/packages/babel-runtime/helpers/*.js
!/packages/babel-runtime/helpers/toArray.js
!/packages/babel-runtime/helpers/iterableToArray.js
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Expand Up @@ -2,6 +2,7 @@ package.json
packages/babel-preset-env/data
packages/babel-compat-data/data
packages/babel-compat-data/scripts/data/overlapping-plugins.js
packages/babel-core/src/vendor
packages/*/test/fixtures/**/input.*
packages/*/test/fixtures/**/exec.*
packages/*/test/fixtures/**/output.*
47 changes: 47 additions & 0 deletions Gulpfile.mjs
Expand Up @@ -20,6 +20,7 @@ import _rollupDts from "rollup-plugin-dts";
const { default: rollupDts } = _rollupDts;
import { Worker as JestWorker } from "jest-worker";
import glob from "glob";
import { resolve as importMetaResolve } from "import-meta-resolve";

import rollupBabelSource from "./scripts/rollup-plugin-babel-source.js";
import formatCode from "./scripts/utils/formatCode.js";
Expand Down Expand Up @@ -527,9 +528,54 @@ gulp.task(

gulp.task("build-babel", () => buildBabel(true, /* exclude */ libBundles));

gulp.task("build-vendor", async () => {
const input = fileURLToPath(
await importMetaResolve("import-meta-resolve", import.meta.url)
);
const output = "./packages/babel-core/src/vendor/import-meta-resolve.js";

const bundle = await rollup({
input,
onwarn(warning, warn) {
if (warning.code === "CIRCULAR_DEPENDENCY") return;
warn(warning);
},
plugins: [
rollupCommonJs({ defaultIsModuleExports: true }),
rollupNodeResolve({
extensions: [".js", ".mjs", ".cjs", ".json"],
preferBuiltins: true,
}),
],
});

await bundle.write({
file: output,
format: "es",
sourcemap: false,
exports: "named",
banner: String.raw`
/****************************************************************************\
* NOTE FROM BABEL AUTHORS *
* This file is inlined from https://github.com/wooorm/import-meta-resolve, *
* because we need to compile it to CommonJS. *
\****************************************************************************/

/*
${fs.readFileSync(path.join(path.dirname(input), "license"), "utf8")}*/
`,
});

fs.writeFileSync(
output.replace(".js", ".d.ts"),
`export function resolve(specifier: stirng, parent: string): Promise<string>;`
);
});

gulp.task(
"build",
gulp.series(
"build-vendor",
gulp.parallel("build-rollup", "build-babel", "generate-runtime-helpers"),
gulp.parallel(
"generate-standalone",
Expand All @@ -552,6 +598,7 @@ gulp.task("build-no-bundle-watch", () => buildBabel(false));
gulp.task(
"build-dev",
gulp.series(
"build-vendor",
"build-no-bundle",
gulp.parallel(
"generate-standalone",
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -62,6 +62,7 @@
"gulp-filter": "^7.0.0",
"gulp-plumber": "^1.2.1",
"husky": "^7.0.4",
"import-meta-resolve": "^1.1.1",
"jest": "^27.4.0",
"jest-worker": "^27.4.0",
"lint-staged": "^9.2.0",
Expand Down
42 changes: 42 additions & 0 deletions packages/babel-core/src/config/files/import-meta-resolve.ts
@@ -0,0 +1,42 @@
import { createRequire } from "module";
import { resolve as polyfill } from "../../vendor/import-meta-resolve";

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

let import_;
try {
// Node < 13.3 doesn't support import() syntax.
import_ = require("./import").default;
} catch {}

// import.meta.resolve is only available in ESM, but this file is compiled to CJS.
// We can extract ir using dynamic import.
const resolveP =
import_ &&
// Due to a Node.js/V8 bug (https://github.com/nodejs/node/issues/35889), we cannot
// use dynamic import when running in the default Jest environment because it
// uses vm.SourceTextModule.
// Jest defines globalThis["jest-symbol-do-not-touch"] in
// https://github.com/facebook/jest/blob/11d79ec096a25851124356095d60352f6ca2824e/packages/jest-util/src/installCommonGlobals.ts#L49
// which is called by
// https://github.com/facebook/jest/blob/11d79ec096a25851124356095d60352f6ca2824e/packages/jest-environment-node/src/index.ts#L85
//
// Note that our Jest runner doesn't have this problem, because it runs ESM in the default
// Node.js context rather than using the `vm` module.
//
// When V8 fixes this bug, we can remove this check. We usually don't have package-specific hacks,
// but Jest is a big Babel consumer widely used in the community and they cannot workaround
// this problem on their side.
!Object.hasOwnProperty.call(global, "jest-symbol-do-not-touch")
? import_("data:text/javascript,export default import.meta.resolve").then(
// Since import.meta.resolve is unstable and only available when
// using the --experimental-import-meta-resolve flag, we almost
// always use the polyfill for now.
m => m.default || polyfill,
() => polyfill,
)
: Promise.resolve(polyfill);

export default function getImportMetaResolve(): Promise<ImportMeta["resolve"]> {
return resolveP;
}
13 changes: 7 additions & 6 deletions packages/babel-core/src/config/files/index.ts
Expand Up @@ -21,9 +21,10 @@ export type {
RelativeConfig,
FilePackageData,
} from "./types";
export {
resolvePlugin,
resolvePreset,
loadPlugin,
loadPreset,
} from "./plugins";
export { loadPlugin, loadPreset } from "./plugins";

import gensync from "gensync";
import * as plugins from "./plugins";

export const resolvePlugin = gensync(plugins.resolvePlugin).sync;
export const resolvePreset = gensync(plugins.resolvePreset).sync;
2 changes: 2 additions & 0 deletions packages/babel-core/src/config/files/module-types.ts
Expand Up @@ -12,6 +12,8 @@ try {
import_ = require("./import").default;
} catch {}

export const supportsESM = !!import_;

export default function* loadCjsOrMjsDefault(
filepath: string,
asyncError: string,
Expand Down
166 changes: 106 additions & 60 deletions packages/babel-core/src/config/files/plugins.ts
Expand Up @@ -4,9 +4,12 @@

import buildDebug from "debug";
import path from "path";
import type { Handler } from "gensync";
import loadCjsOrMjsDefault from "./module-types";
import gensync, { type Gensync, type Handler } from "gensync";
import { isAsync } from "../../gensync-utils/async";
import loadCjsOrMjsDefault, { supportsESM } from "./module-types";
import { fileURLToPath, pathToFileURL } from "url";

import getImportMetaResolve from "./import-meta-resolve";

import { createRequire } from "module";
const require = createRequire(import.meta.url);
Expand All @@ -24,22 +27,19 @@ const OTHER_PRESET_ORG_RE =
/^(@(?!babel\/)[^/]+\/)(?![^/]*babel-preset(?:-|\/|$)|[^/]+\/)/;
const OTHER_ORG_DEFAULT_RE = /^(@(?!babel$)[^/]+)$/;

export function resolvePlugin(name: string, dirname: string): string | null {
return resolveStandardizedName("plugin", name, dirname);
export function* resolvePlugin(name: string, dirname: string): Handler<string> {
return yield* resolveStandardizedName("plugin", name, dirname);
}

export function resolvePreset(name: string, dirname: string): string | null {
return resolveStandardizedName("preset", name, dirname);
export function* resolvePreset(name: string, dirname: string): Handler<string> {
return yield* resolveStandardizedName("preset", name, dirname);
}

export function* loadPlugin(
name: string,
dirname: string,
): Handler<{ filepath: string; value: unknown }> {
const filepath = resolvePlugin(name, dirname);
if (!filepath) {
throw new Error(`Plugin ${name} not found relative to ${dirname}`);
}
const filepath = yield* resolvePlugin(name, dirname);

const value = yield* requireModule("plugin", filepath);
debug("Loaded plugin %o from %o.", name, dirname);
Expand All @@ -51,10 +51,7 @@ export function* loadPreset(
name: string,
dirname: string,
): Handler<{ filepath: string; value: unknown }> {
const filepath = resolvePreset(name, dirname);
if (!filepath) {
throw new Error(`Preset ${name} not found relative to ${dirname}`);
}
const filepath = yield* resolvePreset(name, dirname);

const value = yield* requireModule("preset", filepath);

Expand Down Expand Up @@ -93,62 +90,111 @@ function standardizeName(type: "plugin" | "preset", name: string) {
);
}

function resolveStandardizedName(
type Result<T> = { error: Error; value: null } | { error: null; value: T };

function* resolveAlternativesHelper(
type: "plugin" | "preset",
name: string,
dirname: string = process.cwd(),
) {
): Iterator<string, string, Result<string>> {
const standardizedName = standardizeName(type, name);
const { error, value } = yield standardizedName;
if (!error) return value;

// @ts-ignore
if (error.code !== "MODULE_NOT_FOUND") throw error;

if (standardizedName !== name && !(yield name).error) {
error.message += `\n- If you want to resolve "${name}", use "module:${name}"`;
}

if (!(yield standardizeName(type, "@babel/" + name)).error) {
error.message += `\n- Did you mean "@babel/${name}"?`;
}

const oppositeType = type === "preset" ? "plugin" : "preset";
if (!(yield standardizeName(oppositeType, name)).error) {
error.message += `\n- Did you accidentally pass a ${oppositeType} as a ${type}?`;
}

throw error;
}

function tryRequireResolve(
id: Parameters<RequireResolve>[0],
{ paths: [dirname] }: Parameters<RequireResolve>[1],
): Result<string> {
try {
return require.resolve(standardizedName, {
paths: [dirname],
});
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") throw e;

if (standardizedName !== name) {
let resolvedOriginal = false;
try {
require.resolve(name, {
paths: [dirname],
});
resolvedOriginal = true;
} catch {}

if (resolvedOriginal) {
e.message += `\n- If you want to resolve "${name}", use "module:${name}"`;
}
}
return { error: null, value: require.resolve(id, { paths: [dirname] }) };
} catch (error) {
return { error, value: null };
}
}

let resolvedBabel = false;
try {
require.resolve(standardizeName(type, "@babel/" + name), {
paths: [dirname],
});
resolvedBabel = true;
} catch {}

if (resolvedBabel) {
e.message += `\n- Did you mean "@babel/${name}"?`;
async function tryImportMetaResolve(
id: Parameters<ImportMeta["resolve"]>[0],
options: Parameters<ImportMeta["resolve"]>[1],
): Promise<Result<string>> {
const importMetaResolve = await getImportMetaResolve();
try {
return { error: null, value: await importMetaResolve(id, options) };
} catch (error) {
return { error, value: null };
}
}

function resolveStandardizedNameForRequrie(
type: "plugin" | "preset",
name: string,
dirname: string,
) {
const it = resolveAlternativesHelper(type, name);
let res = it.next();
while (!res.done) {
res = it.next(tryRequireResolve(res.value, { paths: [dirname] }));
}
return res.value;
}
async function resolveStandardizedNameForImport(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept these two functions separated (rather than creating a gensync) because they are not simply a sync and an async version: the sync one is also used when Babel is called asynchronously.

type: "plugin" | "preset",
name: string,
dirname: string,
) {
const parentUrl = pathToFileURL(
path.join(dirname, "./babel-virtual-resolve-base.js"),
).href;

const it = resolveAlternativesHelper(type, name);
let res = it.next();
while (!res.done) {
res = it.next(await tryImportMetaResolve(res.value, parentUrl));
}
return fileURLToPath(res.value);
}

const resolveStandardizedName: Gensync<
(type: "plugin" | "preset", name: string, dirname?: string) => string
> = gensync({
sync(type, name, dirname = process.cwd()) {
return resolveStandardizedNameForRequrie(type, name, dirname);
},
async async(type, name, dirname = process.cwd()) {
if (!supportsESM) {
return resolveStandardizedNameForRequrie(type, name, dirname);
}

let resolvedOppositeType = false;
const oppositeType = type === "preset" ? "plugin" : "preset";
try {
require.resolve(standardizeName(oppositeType, name), {
paths: [dirname],
});
resolvedOppositeType = true;
} catch {}

if (resolvedOppositeType) {
e.message += `\n- Did you accidentally pass a ${oppositeType} as a ${type}?`;
return await resolveStandardizedNameForImport(type, name, dirname);
} catch (e) {
try {
return resolveStandardizedNameForRequrie(type, name, dirname);
} catch (e2) {
if (e.type === "MODULE_NOT_FOUND") throw e;
if (e2.type === "MODULE_NOT_FOUND") throw e2;
throw e;
}
}

throw e;
}
}
},
});

if (!process.env.BABEL_8_BREAKING) {
// eslint-disable-next-line no-var
Expand Down
Empty file.
1 change: 1 addition & 0 deletions packages/babel-core/src/vendor/import-meta-resolve.d.ts
@@ -0,0 +1 @@
export function resolve(specifier: stirng, parent: string): Promise<string>;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.