Skip to content

Commit e7c705a

Browse files
Respect package.json#exports when resolving plugins (#14110)
* Respect `package.json#exports` when resolving plugins * Use native import.meta.resolve when available * Workaround V8 bug that makes Babel-Jest segfault.
1 parent 693f2d2 commit e7c705a

16 files changed

Lines changed: 282 additions & 67 deletions

File tree

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ packages/babel-preset-env/test/debug-fixtures
2222
packages/babel-standalone/babel.js
2323
packages/babel-standalone/babel.min.js
2424
packages/babel-parser/test/expressions
25+
packages/babel-core/src/vendor
2526

2627
eslint/*/lib
2728
eslint/*/node_modules

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ package-lock.json
2525

2626
/packages/babel-compat-data/build
2727

28+
/packages/babel-core/src/vendor/*.js
29+
/packages/babel-core/src/vendor/*.ts
30+
2831
/packages/babel-runtime/helpers/*.js
2932
!/packages/babel-runtime/helpers/toArray.js
3033
!/packages/babel-runtime/helpers/iterableToArray.js

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package.json
22
packages/babel-preset-env/data
33
packages/babel-compat-data/data
44
packages/babel-compat-data/scripts/data/overlapping-plugins.js
5+
packages/babel-core/src/vendor
56
packages/*/test/fixtures/**/input.*
67
packages/*/test/fixtures/**/exec.*
78
packages/*/test/fixtures/**/output.*

Gulpfile.mjs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import _rollupDts from "rollup-plugin-dts";
2020
const { default: rollupDts } = _rollupDts;
2121
import { Worker as JestWorker } from "jest-worker";
2222
import glob from "glob";
23+
import { resolve as importMetaResolve } from "import-meta-resolve";
2324

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

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

531+
gulp.task("build-vendor", async () => {
532+
const input = fileURLToPath(
533+
await importMetaResolve("import-meta-resolve", import.meta.url)
534+
);
535+
const output = "./packages/babel-core/src/vendor/import-meta-resolve.js";
536+
537+
const bundle = await rollup({
538+
input,
539+
onwarn(warning, warn) {
540+
if (warning.code === "CIRCULAR_DEPENDENCY") return;
541+
warn(warning);
542+
},
543+
plugins: [
544+
rollupCommonJs({ defaultIsModuleExports: true }),
545+
rollupNodeResolve({
546+
extensions: [".js", ".mjs", ".cjs", ".json"],
547+
preferBuiltins: true,
548+
}),
549+
],
550+
});
551+
552+
await bundle.write({
553+
file: output,
554+
format: "es",
555+
sourcemap: false,
556+
exports: "named",
557+
banner: String.raw`
558+
/****************************************************************************\
559+
* NOTE FROM BABEL AUTHORS *
560+
* This file is inlined from https://github.com/wooorm/import-meta-resolve, *
561+
* because we need to compile it to CommonJS. *
562+
\****************************************************************************/
563+
564+
/*
565+
${fs.readFileSync(path.join(path.dirname(input), "license"), "utf8")}*/
566+
`,
567+
});
568+
569+
fs.writeFileSync(
570+
output.replace(".js", ".d.ts"),
571+
`export function resolve(specifier: stirng, parent: string): Promise<string>;`
572+
);
573+
});
574+
530575
gulp.task(
531576
"build",
532577
gulp.series(
578+
"build-vendor",
533579
gulp.parallel("build-rollup", "build-babel", "generate-runtime-helpers"),
534580
gulp.parallel(
535581
"generate-standalone",
@@ -552,6 +598,7 @@ gulp.task("build-no-bundle-watch", () => buildBabel(false));
552598
gulp.task(
553599
"build-dev",
554600
gulp.series(
601+
"build-vendor",
555602
"build-no-bundle",
556603
gulp.parallel(
557604
"generate-standalone",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"gulp-filter": "^7.0.0",
6363
"gulp-plumber": "^1.2.1",
6464
"husky": "^7.0.4",
65+
"import-meta-resolve": "^1.1.1",
6566
"jest": "^27.4.0",
6667
"jest-worker": "^27.4.0",
6768
"lint-staged": "^9.2.0",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { createRequire } from "module";
2+
import { resolve as polyfill } from "../../vendor/import-meta-resolve";
3+
4+
const require = createRequire(import.meta.url);
5+
6+
let import_;
7+
try {
8+
// Node < 13.3 doesn't support import() syntax.
9+
import_ = require("./import").default;
10+
} catch {}
11+
12+
// import.meta.resolve is only available in ESM, but this file is compiled to CJS.
13+
// We can extract ir using dynamic import.
14+
const resolveP =
15+
import_ &&
16+
// Due to a Node.js/V8 bug (https://github.com/nodejs/node/issues/35889), we cannot
17+
// use dynamic import when running in the default Jest environment because it
18+
// uses vm.SourceTextModule.
19+
// Jest defines globalThis["jest-symbol-do-not-touch"] in
20+
// https://github.com/facebook/jest/blob/11d79ec096a25851124356095d60352f6ca2824e/packages/jest-util/src/installCommonGlobals.ts#L49
21+
// which is called by
22+
// https://github.com/facebook/jest/blob/11d79ec096a25851124356095d60352f6ca2824e/packages/jest-environment-node/src/index.ts#L85
23+
//
24+
// Note that our Jest runner doesn't have this problem, because it runs ESM in the default
25+
// Node.js context rather than using the `vm` module.
26+
//
27+
// When V8 fixes this bug, we can remove this check. We usually don't have package-specific hacks,
28+
// but Jest is a big Babel consumer widely used in the community and they cannot workaround
29+
// this problem on their side.
30+
!Object.hasOwnProperty.call(global, "jest-symbol-do-not-touch")
31+
? import_("data:text/javascript,export default import.meta.resolve").then(
32+
// Since import.meta.resolve is unstable and only available when
33+
// using the --experimental-import-meta-resolve flag, we almost
34+
// always use the polyfill for now.
35+
m => m.default || polyfill,
36+
() => polyfill,
37+
)
38+
: Promise.resolve(polyfill);
39+
40+
export default function getImportMetaResolve(): Promise<ImportMeta["resolve"]> {
41+
return resolveP;
42+
}

packages/babel-core/src/config/files/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ export type {
2121
RelativeConfig,
2222
FilePackageData,
2323
} from "./types";
24-
export {
25-
resolvePlugin,
26-
resolvePreset,
27-
loadPlugin,
28-
loadPreset,
29-
} from "./plugins";
24+
export { loadPlugin, loadPreset } from "./plugins";
25+
26+
import gensync from "gensync";
27+
import * as plugins from "./plugins";
28+
29+
export const resolvePlugin = gensync(plugins.resolvePlugin).sync;
30+
export const resolvePreset = gensync(plugins.resolvePreset).sync;

packages/babel-core/src/config/files/module-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ try {
1212
import_ = require("./import").default;
1313
} catch {}
1414

15+
export const supportsESM = !!import_;
16+
1517
export default function* loadCjsOrMjsDefault(
1618
filepath: string,
1719
asyncError: string,

packages/babel-core/src/config/files/plugins.ts

Lines changed: 106 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
import buildDebug from "debug";
66
import path from "path";
7-
import type { Handler } from "gensync";
8-
import loadCjsOrMjsDefault from "./module-types";
7+
import gensync, { type Gensync, type Handler } from "gensync";
98
import { isAsync } from "../../gensync-utils/async";
9+
import loadCjsOrMjsDefault, { supportsESM } from "./module-types";
10+
import { fileURLToPath, pathToFileURL } from "url";
11+
12+
import getImportMetaResolve from "./import-meta-resolve";
1013

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

27-
export function resolvePlugin(name: string, dirname: string): string | null {
28-
return resolveStandardizedName("plugin", name, dirname);
30+
export function* resolvePlugin(name: string, dirname: string): Handler<string> {
31+
return yield* resolveStandardizedName("plugin", name, dirname);
2932
}
3033

31-
export function resolvePreset(name: string, dirname: string): string | null {
32-
return resolveStandardizedName("preset", name, dirname);
34+
export function* resolvePreset(name: string, dirname: string): Handler<string> {
35+
return yield* resolveStandardizedName("preset", name, dirname);
3336
}
3437

3538
export function* loadPlugin(
3639
name: string,
3740
dirname: string,
3841
): Handler<{ filepath: string; value: unknown }> {
39-
const filepath = resolvePlugin(name, dirname);
40-
if (!filepath) {
41-
throw new Error(`Plugin ${name} not found relative to ${dirname}`);
42-
}
42+
const filepath = yield* resolvePlugin(name, dirname);
4343

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

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

@@ -93,62 +90,111 @@ function standardizeName(type: "plugin" | "preset", name: string) {
9390
);
9491
}
9592

96-
function resolveStandardizedName(
93+
type Result<T> = { error: Error; value: null } | { error: null; value: T };
94+
95+
function* resolveAlternativesHelper(
9796
type: "plugin" | "preset",
9897
name: string,
99-
dirname: string = process.cwd(),
100-
) {
98+
): Iterator<string, string, Result<string>> {
10199
const standardizedName = standardizeName(type, name);
100+
const { error, value } = yield standardizedName;
101+
if (!error) return value;
102+
103+
// @ts-ignore
104+
if (error.code !== "MODULE_NOT_FOUND") throw error;
102105

106+
if (standardizedName !== name && !(yield name).error) {
107+
error.message += `\n- If you want to resolve "${name}", use "module:${name}"`;
108+
}
109+
110+
if (!(yield standardizeName(type, "@babel/" + name)).error) {
111+
error.message += `\n- Did you mean "@babel/${name}"?`;
112+
}
113+
114+
const oppositeType = type === "preset" ? "plugin" : "preset";
115+
if (!(yield standardizeName(oppositeType, name)).error) {
116+
error.message += `\n- Did you accidentally pass a ${oppositeType} as a ${type}?`;
117+
}
118+
119+
throw error;
120+
}
121+
122+
function tryRequireResolve(
123+
id: Parameters<RequireResolve>[0],
124+
{ paths: [dirname] }: Parameters<RequireResolve>[1],
125+
): Result<string> {
103126
try {
104-
return require.resolve(standardizedName, {
105-
paths: [dirname],
106-
});
107-
} catch (e) {
108-
if (e.code !== "MODULE_NOT_FOUND") throw e;
109-
110-
if (standardizedName !== name) {
111-
let resolvedOriginal = false;
112-
try {
113-
require.resolve(name, {
114-
paths: [dirname],
115-
});
116-
resolvedOriginal = true;
117-
} catch {}
118-
119-
if (resolvedOriginal) {
120-
e.message += `\n- If you want to resolve "${name}", use "module:${name}"`;
121-
}
122-
}
127+
return { error: null, value: require.resolve(id, { paths: [dirname] }) };
128+
} catch (error) {
129+
return { error, value: null };
130+
}
131+
}
123132

124-
let resolvedBabel = false;
125-
try {
126-
require.resolve(standardizeName(type, "@babel/" + name), {
127-
paths: [dirname],
128-
});
129-
resolvedBabel = true;
130-
} catch {}
131-
132-
if (resolvedBabel) {
133-
e.message += `\n- Did you mean "@babel/${name}"?`;
133+
async function tryImportMetaResolve(
134+
id: Parameters<ImportMeta["resolve"]>[0],
135+
options: Parameters<ImportMeta["resolve"]>[1],
136+
): Promise<Result<string>> {
137+
const importMetaResolve = await getImportMetaResolve();
138+
try {
139+
return { error: null, value: await importMetaResolve(id, options) };
140+
} catch (error) {
141+
return { error, value: null };
142+
}
143+
}
144+
145+
function resolveStandardizedNameForRequrie(
146+
type: "plugin" | "preset",
147+
name: string,
148+
dirname: string,
149+
) {
150+
const it = resolveAlternativesHelper(type, name);
151+
let res = it.next();
152+
while (!res.done) {
153+
res = it.next(tryRequireResolve(res.value, { paths: [dirname] }));
154+
}
155+
return res.value;
156+
}
157+
async function resolveStandardizedNameForImport(
158+
type: "plugin" | "preset",
159+
name: string,
160+
dirname: string,
161+
) {
162+
const parentUrl = pathToFileURL(
163+
path.join(dirname, "./babel-virtual-resolve-base.js"),
164+
).href;
165+
166+
const it = resolveAlternativesHelper(type, name);
167+
let res = it.next();
168+
while (!res.done) {
169+
res = it.next(await tryImportMetaResolve(res.value, parentUrl));
170+
}
171+
return fileURLToPath(res.value);
172+
}
173+
174+
const resolveStandardizedName: Gensync<
175+
(type: "plugin" | "preset", name: string, dirname?: string) => string
176+
> = gensync({
177+
sync(type, name, dirname = process.cwd()) {
178+
return resolveStandardizedNameForRequrie(type, name, dirname);
179+
},
180+
async async(type, name, dirname = process.cwd()) {
181+
if (!supportsESM) {
182+
return resolveStandardizedNameForRequrie(type, name, dirname);
134183
}
135184

136-
let resolvedOppositeType = false;
137-
const oppositeType = type === "preset" ? "plugin" : "preset";
138185
try {
139-
require.resolve(standardizeName(oppositeType, name), {
140-
paths: [dirname],
141-
});
142-
resolvedOppositeType = true;
143-
} catch {}
144-
145-
if (resolvedOppositeType) {
146-
e.message += `\n- Did you accidentally pass a ${oppositeType} as a ${type}?`;
186+
return await resolveStandardizedNameForImport(type, name, dirname);
187+
} catch (e) {
188+
try {
189+
return resolveStandardizedNameForRequrie(type, name, dirname);
190+
} catch (e2) {
191+
if (e.type === "MODULE_NOT_FOUND") throw e;
192+
if (e2.type === "MODULE_NOT_FOUND") throw e2;
193+
throw e;
194+
}
147195
}
148-
149-
throw e;
150-
}
151-
}
196+
},
197+
});
152198

153199
if (!process.env.BABEL_8_BREAKING) {
154200
// eslint-disable-next-line no-var

packages/babel-core/src/vendor/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)