Skip to content

Commit

Permalink
Respect package.json#exports when resolving plugins
Browse files Browse the repository at this point in the history
* Use native import.meta.resolve when available
  • Loading branch information
nicolo-ribaudo committed Jan 7, 2022
1 parent 53ac687 commit ced9928
Show file tree
Hide file tree
Showing 16 changed files with 266 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
26 changes: 26 additions & 0 deletions packages/babel-core/src/config/files/import-meta-resolve.ts
@@ -0,0 +1,26 @@
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_
? 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,8 +4,11 @@

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 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 @@ -23,22 +26,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 @@ -50,10 +50,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 @@ -92,62 +89,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;
}
}
},
});

const LOADING_MODULES = new Set();
function* requireModule(type: string, name: string): Handler<unknown> {
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.

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

0 comments on commit ced9928

Please sign in to comment.