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

Use synchronous import.meta.resolve #15575

Merged
merged 5 commits into from Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion Gulpfile.mjs
Expand Up @@ -763,7 +763,7 @@ gulp.task("build-babel", () => buildBabel(true, /* exclude */ libBundles));

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

Expand Down
7 changes: 6 additions & 1 deletion babel.config.js
Expand Up @@ -182,7 +182,6 @@ module.exports = function (api) {
["@babel/proposal-object-rest-spread", { useBuiltIns: true }],

convertESM ? "@babel/proposal-export-namespace-from" : null,
convertESM ? pluginImportMetaUrl : null,

pluginPackageJsonMacro,

Expand Down Expand Up @@ -276,6 +275,12 @@ module.exports = function (api) {
],
],
},
convertESM && {
exclude: [
"./packages/babel-core/src/config/files/import-meta-resolve.ts",
].map(normalize),
plugins: [pluginImportMetaUrl],
},
{
test: sources.map(source => normalize(source.replace("/src", "/test"))),
plugins: [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -58,7 +58,7 @@
"gulp-filter": "^7.0.0",
"gulp-plumber": "^1.2.1",
"husky": "^7.0.4",
"import-meta-resolve": "^1.1.1",
"import-meta-resolve": "^3.0.0",
"jest": "^29.0.1",
"jest-light-runner": "^0.4.0",
"jest-worker": "^29.0.1",
Expand Down
64 changes: 22 additions & 42 deletions packages/babel-core/src/config/files/import-meta-resolve.ts
@@ -1,48 +1,28 @@
import { createRequire } from "module";
import { resolve as polyfill } from "../../vendor/import-meta-resolve";

const require = createRequire(import.meta.url);
declare const USE_ESM: boolean;

let import_;
try {
// Node < 13.3 doesn't support import() syntax.
import_ = require("./import.cjs");
} catch {}
let importMetaResolve: ImportMeta["resolve"];

// import.meta.resolve is only available in ESM, but this file is compiled to CJS.
// We can extract it using dynamic import.
const importMetaResolveP: Promise<ImportMeta["resolve"]> =
import_ &&
// Due to a Node.js/V8 bug (https://github.com/nodejs/node/issues/35889), we cannot
// use always dynamic import because it segfaults when running in a Node.js `vm` context,
// which is used by the default Jest environment and by webpack-cli.
//
// However, import.meta.resolve is experimental and only enabled when Node.js is run
// with the `--experimental-import-meta-resolve` flag: we can avoid calling import()
// when that flag is not enabled, so that the default behavior never segfaults.
//
// Hopefully, before Node.js unflags import.meta.resolve, either:
// - we will move to ESM, so that we have direct access to import.meta.resolve, or
// - the V8 bug will be fixed so that we can safely use dynamic import by default.
//
// I (@nicolo-ribaudo) am really annoyed by this bug, because there is no known
// work-around other than "don't use dynamic import if you are running in a `vm` context",
// but there is no reliable way to detect it (you cannot try/catch segfaults).
//
// This is the only place where we *need* to use dynamic import because we need to access
// an ES module. All the other places will first try using require() and *then*, if
// it throws because it's a module, will fallback to import().
process.execArgv.includes("--experimental-import-meta-resolve")
? import_("data:text/javascript,export default import.meta.resolve").then(
(m: { default: ImportMeta["resolve"] | undefined }) =>
m.default || polyfill,
() => polyfill,
)
: Promise.resolve(polyfill);
if (USE_ESM) {
// Node.js < 20, when using the `--experimental-import-meta-resolve` flag,
// have an asynchronous implementation of import.meta.resolve.
if (
typeof import.meta.resolve === "function" &&
typeof import.meta.resolve(import.meta.url) === "string"
) {
importMetaResolve = import.meta.resolve;
} else {
importMetaResolve = polyfill;
}
} else {
importMetaResolve = polyfill;
}

export default async function resolve(
specifier: Parameters<ImportMeta["resolve"]>[0],
parent?: Parameters<ImportMeta["resolve"]>[1],
): ReturnType<ImportMeta["resolve"]> {
return (await importMetaResolveP)(specifier, parent);
export default function resolve(
specifier: string,
parent?: string | URL,
): string {
// @ts-expect-error: TS defines import.meta.resolve as returning a promises
return importMetaResolve(specifier, parent);
}
13 changes: 6 additions & 7 deletions packages/babel-core/src/config/files/index.ts
Expand Up @@ -21,10 +21,9 @@ export type {
RelativeConfig,
FilePackageData,
} from "./types";
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;
export {
loadPlugin,
loadPreset,
resolvePlugin,
resolvePreset,
} from "./plugins";
78 changes: 36 additions & 42 deletions packages/babel-core/src/config/files/plugins.ts
Expand Up @@ -4,7 +4,7 @@

import buildDebug from "debug";
import path from "path";
import gensync, { type Handler } from "gensync";
import type { Handler } from "gensync";
import { isAsync } from "../../gensync-utils/async";
import loadCodeDefault, { supportsESM } from "./module-types";
import { fileURLToPath, pathToFileURL } from "url";
Expand All @@ -27,21 +27,14 @@ const OTHER_PRESET_ORG_RE =
/^(@(?!babel\/)[^/]+\/)(?![^/]*babel-preset(?:-|\/|$)|[^/]+\/)/;
const OTHER_ORG_DEFAULT_RE = /^(@(?!babel$)[^/]+)$/;

export function* resolvePlugin(name: string, dirname: string): Handler<string> {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return yield* resolveStandardizedName("plugin", name, dirname);
}

export function* resolvePreset(name: string, dirname: string): Handler<string> {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return yield* resolveStandardizedName("preset", name, dirname);
}
export const resolvePlugin = resolveStandardizedName.bind(null, "plugin");
export const resolvePreset = resolveStandardizedName.bind(null, "preset");

export function* loadPlugin(
name: string,
dirname: string,
): Handler<{ filepath: string; value: unknown }> {
const filepath = yield* resolvePlugin(name, dirname);
const filepath = resolvePlugin(name, dirname, yield* isAsync());

const value = yield* requireModule("plugin", filepath);
debug("Loaded plugin %o from %o.", name, dirname);
Expand All @@ -53,7 +46,7 @@ export function* loadPreset(
name: string,
dirname: string,
): Handler<{ filepath: string; value: unknown }> {
const filepath = yield* resolvePreset(name, dirname);
const filepath = resolvePreset(name, dirname, yield* isAsync());

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

Expand Down Expand Up @@ -122,22 +115,26 @@ function* resolveAlternativesHelper(
}

function tryRequireResolve(
id: Parameters<RequireResolve>[0],
{ paths: [dirname] }: Parameters<RequireResolve>[1],
id: string,
dirname: string | undefined,
): Result<string> {
try {
return { error: null, value: require.resolve(id, { paths: [dirname] }) };
if (dirname) {
return { error: null, value: require.resolve(id, { paths: [dirname] }) };
} else {
return { error: null, value: require.resolve(id) };
Copy link
Member Author

Choose a reason for hiding this comment

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

This change is necessary because sometimes dirname is undefined here, and Node.js 20 as stricter validation.

}
} catch (error) {
return { error, value: null };
}
}

async function tryImportMetaResolve(
function tryImportMetaResolve(
id: Parameters<ImportMeta["resolve"]>[0],
options: Parameters<ImportMeta["resolve"]>[1],
): Promise<Result<string>> {
): Result<string> {
try {
return { error: null, value: await importMetaResolve(id, options) };
return { error: null, value: importMetaResolve(id, options) };
} catch (error) {
return { error, value: null };
}
Expand All @@ -151,11 +148,11 @@ function resolveStandardizedNameForRequire(
const it = resolveAlternativesHelper(type, name);
let res = it.next();
while (!res.done) {
res = it.next(tryRequireResolve(res.value, { paths: [dirname] }));
res = it.next(tryRequireResolve(res.value, dirname));
}
return res.value;
}
async function resolveStandardizedNameForImport(
function resolveStandardizedNameForImport(
type: "plugin" | "preset",
name: string,
dirname: string,
Expand All @@ -167,36 +164,33 @@ async function resolveStandardizedNameForImport(
const it = resolveAlternativesHelper(type, name);
let res = it.next();
while (!res.done) {
res = it.next(await tryImportMetaResolve(res.value, parentUrl));
res = it.next(tryImportMetaResolve(res.value, parentUrl));
}
return fileURLToPath(res.value);
}

const resolveStandardizedName = gensync<
[type: "plugin" | "preset", name: string, dirname?: string],
string
>({
sync(type, name, dirname = process.cwd()) {
function resolveStandardizedName(
type: "plugin" | "preset",
name: string,
dirname: string,
resolveESM: boolean,
) {
if (!supportsESM || !resolveESM) {
return resolveStandardizedNameForRequire(type, name, dirname);
},
async async(type, name, dirname = process.cwd()) {
if (!supportsESM) {
return resolveStandardizedNameForRequire(type, name, dirname);
}
}

try {
return resolveStandardizedNameForImport(type, name, dirname);
} catch (e) {
try {
return await resolveStandardizedNameForImport(type, name, dirname);
} catch (e) {
try {
return resolveStandardizedNameForRequire(type, name, dirname);
} catch (e2) {
if (e.type === "MODULE_NOT_FOUND") throw e;
if (e2.type === "MODULE_NOT_FOUND") throw e2;
throw e;
}
return resolveStandardizedNameForRequire(type, name, dirname);
} catch (e2) {
if (e.type === "MODULE_NOT_FOUND") throw e;
if (e2.type === "MODULE_NOT_FOUND") throw e2;
throw e;
}
},
});
}
}

if (!process.env.BABEL_8_BREAKING) {
// eslint-disable-next-line no-var
Expand Down
23 changes: 6 additions & 17 deletions yarn.lock
Expand Up @@ -6087,7 +6087,7 @@ __metadata:
gulp-filter: ^7.0.0
gulp-plumber: ^1.2.1
husky: ^7.0.4
import-meta-resolve: ^1.1.1
import-meta-resolve: ^3.0.0
jest: ^29.0.1
jest-light-runner: ^0.4.0
jest-worker: ^29.0.1
Expand Down Expand Up @@ -6506,15 +6506,6 @@ __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

"c8@npm:^7.12.0":
version: 7.12.0
resolution: "c8@npm:7.12.0"
Expand Down Expand Up @@ -9606,12 +9597,10 @@ 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
"import-meta-resolve@npm:^3.0.0":
version: 3.0.0
resolution: "import-meta-resolve@npm:3.0.0"
checksum: d0428cd14915ee0093b995dc5bbc70bd01cc668822f52b62af98f728e5d6a08724f07e6aa9f5fae002d5eecbf6ec2cdcd379bf4869dd1b353bd080693f91e394
languageName: node
linkType: hard

Expand Down Expand Up @@ -13611,7 +13600,7 @@ fsevents@^1.2.7:
languageName: node
linkType: hard

"semver@npm:^7.0.0, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7":
"semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7":
version: 7.3.7
resolution: "semver@npm:7.3.7"
dependencies:
Expand Down