diff --git a/.eslintignore b/.eslintignore index 46e7c9d1fd0e..f5e3671ee105 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 diff --git a/.gitignore b/.gitignore index fbb484db921e..8112138dfa1a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.prettierignore b/.prettierignore index 19e8316f03eb..a07d1de9cd44 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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.* diff --git a/Gulpfile.mjs b/Gulpfile.mjs index ba431b72cc35..6d15d7a8e27a 100644 --- a/Gulpfile.mjs +++ b/Gulpfile.mjs @@ -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"; @@ -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;` + ); +}); + gulp.task( "build", gulp.series( + "build-vendor", gulp.parallel("build-rollup", "build-babel", "generate-runtime-helpers"), gulp.parallel( "generate-standalone", @@ -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", diff --git a/package.json b/package.json index 6bf7b0d3e507..8272189498f8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/babel-core/src/config/files/import-meta-resolve.ts b/packages/babel-core/src/config/files/import-meta-resolve.ts new file mode 100644 index 000000000000..f44bf2c11cf1 --- /dev/null +++ b/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 { + return resolveP; +} diff --git a/packages/babel-core/src/config/files/index.ts b/packages/babel-core/src/config/files/index.ts index 5acd74166e61..31e856027e59 100644 --- a/packages/babel-core/src/config/files/index.ts +++ b/packages/babel-core/src/config/files/index.ts @@ -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; diff --git a/packages/babel-core/src/config/files/module-types.ts b/packages/babel-core/src/config/files/module-types.ts index 68c7049f30e3..cb19a02da21d 100644 --- a/packages/babel-core/src/config/files/module-types.ts +++ b/packages/babel-core/src/config/files/module-types.ts @@ -12,6 +12,8 @@ try { import_ = require("./import").default; } catch {} +export const supportsESM = !!import_; + export default function* loadCjsOrMjsDefault( filepath: string, asyncError: string, diff --git a/packages/babel-core/src/config/files/plugins.ts b/packages/babel-core/src/config/files/plugins.ts index cedcbadd4bae..4cc017d5b3f8 100644 --- a/packages/babel-core/src/config/files/plugins.ts +++ b/packages/babel-core/src/config/files/plugins.ts @@ -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); @@ -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 { + 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 { + 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); @@ -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); @@ -93,62 +90,111 @@ function standardizeName(type: "plugin" | "preset", name: string) { ); } -function resolveStandardizedName( +type Result = { error: Error; value: null } | { error: null; value: T }; + +function* resolveAlternativesHelper( type: "plugin" | "preset", name: string, - dirname: string = process.cwd(), -) { +): Iterator> { 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[0], + { paths: [dirname] }: Parameters[1], +): Result { 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[0], + options: Parameters[1], +): Promise> { + 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 diff --git a/packages/babel-core/src/vendor/.gitkeep b/packages/babel-core/src/vendor/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/babel-core/src/vendor/import-meta-resolve.d.ts b/packages/babel-core/src/vendor/import-meta-resolve.d.ts new file mode 100644 index 000000000000..ac3a5b55ffca --- /dev/null +++ b/packages/babel-core/src/vendor/import-meta-resolve.d.ts @@ -0,0 +1 @@ +export function resolve(specifier: stirng, parent: string): Promise; \ No newline at end of file diff --git a/packages/babel-core/test/fixtures/resolution/pkg-exports/node_modules/babel-plugin-dual/import.mjs b/packages/babel-core/test/fixtures/resolution/pkg-exports/node_modules/babel-plugin-dual/import.mjs new file mode 100644 index 000000000000..d1a9f08d5cf9 --- /dev/null +++ b/packages/babel-core/test/fixtures/resolution/pkg-exports/node_modules/babel-plugin-dual/import.mjs @@ -0,0 +1,9 @@ +export default function ({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("ESM")); + }, + }, + }; +} diff --git a/packages/babel-core/test/fixtures/resolution/pkg-exports/node_modules/babel-plugin-dual/package.json b/packages/babel-core/test/fixtures/resolution/pkg-exports/node_modules/babel-plugin-dual/package.json new file mode 100644 index 000000000000..555516994e16 --- /dev/null +++ b/packages/babel-core/test/fixtures/resolution/pkg-exports/node_modules/babel-plugin-dual/package.json @@ -0,0 +1,7 @@ +{ + "name": "babel-plugin-dual", + "exports": { + "import": "./import.mjs", + "require": "./require.cjs" + } +} diff --git a/packages/babel-core/test/fixtures/resolution/pkg-exports/node_modules/babel-plugin-dual/require.cjs b/packages/babel-core/test/fixtures/resolution/pkg-exports/node_modules/babel-plugin-dual/require.cjs new file mode 100644 index 000000000000..38d728eb033f --- /dev/null +++ b/packages/babel-core/test/fixtures/resolution/pkg-exports/node_modules/babel-plugin-dual/require.cjs @@ -0,0 +1,9 @@ +module.exports = function ({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("CJS")); + }, + }, + }; +}; diff --git a/packages/babel-core/test/resolution.js b/packages/babel-core/test/resolution.js index b8a204f9687c..09b5d1770b50 100644 --- a/packages/babel-core/test/resolution.js +++ b/packages/babel-core/test/resolution.js @@ -463,4 +463,30 @@ describe("addon resolution", function () { }); }).toThrow(/Cannot (?:find|resolve) module 'babel-plugin-foo'/); }); + + const nodeGte12 = parseInt(process.versions.node, 10) >= 12 ? it : it.skip; + + nodeGte12("should respect package.json#exports", async function () { + process.chdir("pkg-exports"); + + expect( + babel.transformSync("", { + filename: "filename.js", + babelrc: false, + configFile: false, + plugins: ["babel-plugin-dual"], + }).code, + ).toBe(`"CJS"`); + + expect( + ( + await babel.transformAsync("", { + filename: "filename.js", + babelrc: false, + configFile: false, + plugins: ["babel-plugin-dual"], + }) + ).code, + ).toBe(`"ESM"`); + }); }); diff --git a/yarn.lock b/yarn.lock index 066d1ef8c551..4f5bc94eba7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5866,6 +5866,7 @@ __metadata: 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 @@ -6281,6 +6282,15 @@ __metadata: languageName: node linkType: hard +"builtins@npm:^4.0.0": + version: 4.0.0 + resolution: "builtins@npm:4.0.0" + dependencies: + semver: ^7.0.0 + checksum: 3c8b3b96ed88dd8e21286a3590292862ad62a59085bbcd77a4470848fed0f59fcd67f366afdf9ca8d7e77abce7ccf336bf662c12ead949294aa03bc563a57a1c + languageName: node + linkType: hard + "cacache@npm:^12.0.2": version: 12.0.4 resolution: "cacache@npm:12.0.4" @@ -9627,6 +9637,15 @@ fsevents@^1.2.7: languageName: node linkType: hard +"import-meta-resolve@npm:^1.1.1": + version: 1.1.1 + resolution: "import-meta-resolve@npm:1.1.1" + dependencies: + builtins: ^4.0.0 + checksum: 2024161e169c45ed25a9f51d984a432a9cc342c35737f9410266bab237ca2f756c1f80c15e2297df83c92f585743d5105291f2ad24094a513f804c6023ea1472 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -13813,7 +13832,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5": +"semver@npm:^7.0.0, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5": version: 7.3.5 resolution: "semver@npm:7.3.5" dependencies: