Skip to content

Commit

Permalink
Respect package.json#exports when resolving plugins (#14110)
Browse files Browse the repository at this point in the history
* Respect `package.json#exports` when resolving plugins

* Use native import.meta.resolve when available

* Workaround V8 bug that makes Babel-Jest segfault.
  • Loading branch information
nicolo-ribaudo committed Jan 16, 2022
1 parent 693f2d2 commit e7c705a
Show file tree
Hide file tree
Showing 16 changed files with 282 additions and 67 deletions.
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(
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.

0 comments on commit e7c705a

Please sign in to comment.