Files
metabase/frontend/lint/oxlint-import-resolver.mjs
2026-10-05 22:41:31 +13:00

226 lines
7.8 KiB
JavaScript

import { existsSync } from "node:fs";
import { isBuiltin, createRequire } from "node:module";
import path from "node:path";
const require = createRequire(import.meta.url);
const legacyNode = require("eslint-import-resolver-node");
function aliases(values = {}) {
return Object.fromEntries(
Object.entries(values).map(([key, value]) => {
const entries = Array.isArray(value) ? value : [value];
if (
entries.some(
(entry) => typeof entry !== "string" || !path.isAbsolute(entry),
)
) {
throw new Error(
`Unsupported resolver alias: ${key} must contain absolute paths for reuse across package directories`,
);
}
return [key, entries];
}),
);
}
const isBare = (source) => /^[\w@]/.test(source) && !/[?#!]/.test(source);
function memoize(resolve, packageRoot) {
const cache = new Map();
return (source, file) => {
const directory = path.dirname(file);
const key = `${isBare(source) ? packageRoot(directory) : directory}\0${source}`;
if (!cache.has(key)) {
if (cache.size >= 50_000) {
cache.clear();
}
cache.set(key, resolve(source, file));
}
return cache.get(key);
};
}
// Construct a fresh service after filesystem, dependency or configuration changes.
export function createImportResolverService({ ResolverFactory }) {
const settingsCache = new WeakMap();
const resolverSettingsCache = new WeakMap();
const webpackResolvers = new Map();
const resolvers = new Map();
const packageRoots = new Map();
// Absolute aliases and a shared node_modules ancestor make bare imports independent of subdirectory.
function packageRoot(directory) {
if (!packageRoots.has(directory)) {
const parent = path.dirname(directory);
const isRoot =
parent === directory ||
existsSync(path.join(directory, "package.json")) ||
existsSync(path.join(directory, "node_modules"));
packageRoots.set(directory, isRoot ? directory : packageRoot(parent));
}
return packageRoots.get(directory);
}
const node = new ResolverFactory({
extensions: [".mjs", ".cjs", ".js", ".json", ".node"],
conditionNames: ["import", "require", "default"],
mainFields: ["module", "main"],
symlinks: false,
});
// eslint-import-resolver-node ignores exports and prioritizes module, jsnext:main, then main.
const legacyNative = new ResolverFactory({
extensions: [".mjs", ".js", ".json", ".node"],
exportsFields: [],
importsFields: [],
mainFields: ["module", "jsnext:main", "main"],
symlinks: false,
});
const resolveLegacyNode = memoize((source, file) => {
// eslint-import-resolver-node treats queries and fragments as filename characters.
if (/[?#!]/.test(source)) {
return legacyNode.resolve(source, file);
}
if (isBuiltin(source)) return { found: true, path: null };
const result = legacyNative.sync(path.dirname(file), source);
return result.path ? { found: true, path: result.path } : { found: false };
}, packageRoot);
function webpackResolver(configPath) {
if (webpackResolvers.has(configPath)) {
return webpackResolvers.get(configPath);
}
const config = require(configPath);
if (!config || typeof config !== "object" || Array.isArray(config)) {
throw new Error("The resolver adapter needs an object Rspack config");
}
const supported = new Set(["extensions", "alias", "fallback"]);
for (const key of Object.keys(config.resolve ?? {})) {
if (!supported.has(key)) {
throw new Error(`Unsupported Rspack resolve option: ${key}`);
}
}
const externals = config.externals ?? {};
if (
typeof externals !== "object" ||
Array.isArray(externals) ||
Object.getPrototypeOf(externals) !== Object.prototype
) {
throw new Error("The resolver adapter needs an object externals map");
}
const resolver = new ResolverFactory({
// Match eslint-import-resolver-webpack's enhanced-resolve defaults.
aliasFields: ["browser"],
mainFields: ["browser", "module", "main"],
extensions: config.resolve?.extensions ?? [".js", ".json"],
alias: aliases(config.resolve?.alias),
fallback: aliases(config.resolve?.fallback),
});
const resolve = (source, file) => {
source = source.slice(source.lastIndexOf("!") + 1);
const query = source.lastIndexOf("?");
if (query >= 0) {
source = source.slice(0, query);
}
if (Object.hasOwn(externals, source)) {
return { found: true, path: null };
}
const result = resolver.sync(path.dirname(file), source);
if (result.path) {
return { found: true, path: result.path };
}
if (isBuiltin(source)) {
return { found: true, path: null };
}
return { found: false };
};
webpackResolvers.set(configPath, resolve);
return resolve;
}
function forSettings(settings, mode = "import-x") {
if (mode !== "import-x" && mode !== "import") {
throw new Error(`Unsupported resolver mode: ${mode}`);
}
const cached = resolverSettingsCache.get(settings)?.get(mode);
if (cached) {
return cached;
}
for (const key of ["resolver-next", "resolver-legacy", "resolve"]) {
if (`${mode}/${key}` in settings) {
throw new Error(`Unsupported resolver setting: ${mode}/${key}`);
}
}
const config = settings[`${mode}/resolver`];
if (
config !== undefined &&
(config?.node !== true ||
Object.keys(config)[0] !== "node" ||
Object.keys(config).some(
(key) => key !== "node" && key !== "webpack",
) ||
(config.webpack &&
(typeof config.webpack.config !== "string" ||
!path.isAbsolute(config.webpack.config) ||
Object.keys(config.webpack).some(
(key) => key !== "config" && key !== "typescript",
))))
) {
throw new Error("Unsupported resolver configuration in adapter");
}
const configPath = config?.webpack?.config;
const key = `${mode}\0${configPath ?? ""}`;
if (!resolvers.has(key)) {
const webpack = configPath ? webpackResolver(configPath) : undefined;
const resolveFallback = (source, file) => {
const result = resolveLegacyNode(source, file);
return result.found ? result : (webpack?.(source, file) ?? result);
};
const resolveImportX = (source, file) => {
if (isBuiltin(source) || source.startsWith("data:")) {
return { found: true, path: null };
}
const result = node.sync(path.dirname(file), source);
return result.path
? { found: true, path: result.path }
: resolveFallback(source, file);
};
// import-x tries the exports-aware resolver before the node/webpack chain.
const resolve = memoize(
mode === "import-x" ? resolveImportX : resolveFallback,
packageRoot,
);
resolvers.set(key, {
interfaceVersion: 3,
name: `metabase-oxc-${mode}`,
resolve,
});
}
const resolver = resolvers.get(key);
if (!resolverSettingsCache.has(settings)) {
resolverSettingsCache.set(settings, new Map());
}
resolverSettingsCache.get(settings).set(mode, resolver);
return resolver;
}
function importSettings(settings) {
if (!settingsCache.has(settings)) {
settingsCache.set(settings, {
...settings,
"import-x/resolver-next": [forSettings(settings)],
});
}
return settingsCache.get(settings);
}
function resolveImport(source, context) {
if (context.settings["import/core-modules"]?.includes(source)) {
return null;
}
return forSettings(context.settings, "import").resolve(
source,
context.physicalFilename ?? context.filename,
).path;
}
return { forSettings, importSettings, resolveImport };
}