369 lines
11 KiB
Plaintext
369 lines
11 KiB
Plaintext
import fs from 'node:fs';
|
|
import module from 'node:module';
|
|
import path from 'node:path';
|
|
import isNodeCoreModule from '@nolyfill/is-core-module';
|
|
import debug from 'debug';
|
|
import { getTsconfig, createPathsMatcher } from 'get-tsconfig';
|
|
import { isBunModule } from 'is-bun-module';
|
|
import { stableHash } from 'stable-hash';
|
|
import { globSync, isDynamicPattern } from 'tinyglobby';
|
|
import { ResolverFactory } from 'unrs-resolver';
|
|
|
|
const IMPORTER_NAME = "eslint-import-resolver-typescript";
|
|
const log = debug(IMPORTER_NAME);
|
|
const defaultConditionNames = [
|
|
"types",
|
|
"import",
|
|
// APF: https://angular.io/guide/angular-package-format
|
|
"esm2020",
|
|
"es2020",
|
|
"es2015",
|
|
"require",
|
|
"node",
|
|
"node-addons",
|
|
"browser",
|
|
"default"
|
|
];
|
|
const defaultExtensions = [
|
|
".ts",
|
|
".tsx",
|
|
".d.ts",
|
|
".js",
|
|
".jsx",
|
|
".json",
|
|
".node"
|
|
];
|
|
const defaultExtensionAlias = {
|
|
".js": [
|
|
".ts",
|
|
// `.tsx` can also be compiled as `.js`
|
|
".tsx",
|
|
".d.ts",
|
|
".js"
|
|
],
|
|
".jsx": [".tsx", ".d.ts", ".jsx"],
|
|
".cjs": [".cts", ".d.cts", ".cjs"],
|
|
".mjs": [".mts", ".d.mts", ".mjs"]
|
|
};
|
|
const defaultMainFields = [
|
|
"types",
|
|
"typings",
|
|
// APF: https://angular.io/guide/angular-package-format
|
|
"fesm2020",
|
|
"fesm2015",
|
|
"esm2020",
|
|
"es2020",
|
|
"module",
|
|
"jsnext:main",
|
|
"main"
|
|
];
|
|
const interfaceVersion = 2;
|
|
const JS_EXT_PATTERN = /\.(?:[cm]js|jsx?)$/;
|
|
const RELATIVE_PATH_PATTERN = /^\.{1,2}(?:\/.*)?$/;
|
|
let previousOptionsHash;
|
|
let optionsHash;
|
|
let cachedOptions;
|
|
let cachedCwd;
|
|
let mappersCachedOptions;
|
|
let mappers = [];
|
|
let resolverCachedOptions;
|
|
let cachedResolver;
|
|
function resolve(source, file, options, resolver) {
|
|
if (!cachedOptions || previousOptionsHash !== (optionsHash = stableHash(options))) {
|
|
previousOptionsHash = optionsHash;
|
|
cachedOptions = {
|
|
...options,
|
|
conditionNames: options?.conditionNames ?? defaultConditionNames,
|
|
extensions: options?.extensions ?? defaultExtensions,
|
|
extensionAlias: options?.extensionAlias ?? defaultExtensionAlias,
|
|
mainFields: options?.mainFields ?? defaultMainFields
|
|
};
|
|
}
|
|
if (!resolver) {
|
|
if (!cachedResolver || resolverCachedOptions !== cachedOptions) {
|
|
cachedResolver = new ResolverFactory(cachedOptions);
|
|
resolverCachedOptions = cachedOptions;
|
|
}
|
|
resolver = cachedResolver;
|
|
}
|
|
log("looking for", source, "in", file);
|
|
source = removeQuerystring(source);
|
|
if (isNodeCoreModule(source) || isBunModule(source)) {
|
|
log("matched core:", source);
|
|
return {
|
|
found: true,
|
|
path: null
|
|
};
|
|
}
|
|
if (process.versions.pnp && source === "pnpapi") {
|
|
return {
|
|
found: true,
|
|
path: module.findPnpApi(file).resolveToUnqualified(source, file, {
|
|
considerBuiltins: false
|
|
})
|
|
};
|
|
}
|
|
initMappers(cachedOptions);
|
|
let mappedPaths = getMappedPaths(source, file, cachedOptions.extensions, true);
|
|
if (mappedPaths.length > 0) {
|
|
log("matched ts path:", ...mappedPaths);
|
|
} else {
|
|
mappedPaths = [source];
|
|
}
|
|
let foundNodePath;
|
|
for (const mappedPath of mappedPaths) {
|
|
try {
|
|
const resolved = resolver.sync(
|
|
path.dirname(path.resolve(file)),
|
|
mappedPath
|
|
);
|
|
if (resolved.path) {
|
|
foundNodePath = resolved.path;
|
|
break;
|
|
}
|
|
} catch {
|
|
log("failed to resolve with", mappedPath);
|
|
}
|
|
}
|
|
if ((JS_EXT_PATTERN.test(foundNodePath) || cachedOptions.alwaysTryTypes && !foundNodePath) && !/^@types[/\\]/.test(source) && !path.isAbsolute(source) && !source.startsWith(".")) {
|
|
const definitelyTyped = resolve(
|
|
"@types" + path.sep + mangleScopedPackage(source),
|
|
file,
|
|
options
|
|
);
|
|
if (definitelyTyped.found) {
|
|
return definitelyTyped;
|
|
}
|
|
}
|
|
if (foundNodePath) {
|
|
log("matched node path:", foundNodePath);
|
|
return {
|
|
found: true,
|
|
path: foundNodePath
|
|
};
|
|
}
|
|
log("didn't find ", source);
|
|
return {
|
|
found: false
|
|
};
|
|
}
|
|
function createTypeScriptImportResolver(options) {
|
|
const resolver = new ResolverFactory({
|
|
...options,
|
|
conditionNames: options?.conditionNames ?? defaultConditionNames,
|
|
extensions: options?.extensions ?? defaultExtensions,
|
|
extensionAlias: options?.extensionAlias ?? defaultExtensionAlias,
|
|
mainFields: options?.mainFields ?? defaultMainFields
|
|
});
|
|
return {
|
|
interfaceVersion: 3,
|
|
name: IMPORTER_NAME,
|
|
resolve(source, file) {
|
|
return resolve(source, file, options, resolver);
|
|
}
|
|
};
|
|
}
|
|
function removeQuerystring(id) {
|
|
const querystringIndex = id.lastIndexOf("?");
|
|
if (querystringIndex !== -1) {
|
|
return id.slice(0, querystringIndex);
|
|
}
|
|
return id;
|
|
}
|
|
const isFile = (path2) => {
|
|
try {
|
|
return !!(path2 && fs.statSync(path2, { throwIfNoEntry: false })?.isFile());
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
const isModule = (modulePath) => !!modulePath && isFile(path.resolve(modulePath, "package.json"));
|
|
function getMappedPaths(source, file, extensions = defaultExtensions, retry) {
|
|
const originalExtensions = extensions;
|
|
extensions = ["", ...extensions];
|
|
let paths = [];
|
|
if (RELATIVE_PATH_PATTERN.test(source)) {
|
|
const resolved = path.resolve(path.dirname(file), source);
|
|
if (isFile(resolved)) {
|
|
paths = [resolved];
|
|
}
|
|
} else {
|
|
let mapperFns = mappers.filter(({ files }) => files.has(file)).map(({ mapperFn }) => mapperFn);
|
|
if (mapperFns.length === 0) {
|
|
mapperFns = mappers.map((mapper) => ({
|
|
mapperFn: mapper.mapperFn,
|
|
counter: equalChars(path.dirname(file), path.dirname(mapper.path))
|
|
})).sort(
|
|
(a, b) => (
|
|
// Sort in descending order where the nearest one has the longest counter
|
|
b.counter - a.counter
|
|
)
|
|
).map(({ mapperFn }) => mapperFn);
|
|
}
|
|
paths = mapperFns.map(
|
|
(mapperFn) => mapperFn(source).map((item) => [
|
|
...extensions.map((ext) => `${item}${ext}`),
|
|
...originalExtensions.map((ext) => `${item}/index${ext}`)
|
|
])
|
|
).flat(
|
|
/* The depth is always 2 */
|
|
2
|
|
).map(toNativePathSeparator).filter((mappedPath) => {
|
|
try {
|
|
const stat = fs.statSync(mappedPath, { throwIfNoEntry: false });
|
|
if (stat === void 0) {
|
|
return false;
|
|
}
|
|
if (stat.isFile()) {
|
|
return true;
|
|
}
|
|
if (stat.isDirectory()) {
|
|
return isModule(mappedPath);
|
|
}
|
|
} catch {
|
|
return false;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
if (retry && paths.length === 0) {
|
|
const isJs = JS_EXT_PATTERN.test(source);
|
|
if (isJs) {
|
|
const jsExt = path.extname(source);
|
|
const tsExt = jsExt.replace("js", "ts");
|
|
const basename = source.replace(JS_EXT_PATTERN, "");
|
|
let resolved = getMappedPaths(basename + tsExt, file);
|
|
if (resolved.length === 0 && jsExt === ".js") {
|
|
const tsxExt = jsExt.replace("js", "tsx");
|
|
resolved = getMappedPaths(basename + tsxExt, file);
|
|
}
|
|
if (resolved.length === 0) {
|
|
resolved = getMappedPaths(
|
|
basename + ".d" + (tsExt === ".tsx" ? ".ts" : tsExt),
|
|
file
|
|
);
|
|
}
|
|
if (resolved.length > 0) {
|
|
return resolved;
|
|
}
|
|
}
|
|
for (const ext of extensions) {
|
|
const mappedPaths = isJs ? [] : getMappedPaths(source + ext, file);
|
|
const resolved = mappedPaths.length > 0 ? mappedPaths : getMappedPaths(source + `/index${ext}`, file);
|
|
if (resolved.length > 0) {
|
|
return resolved;
|
|
}
|
|
}
|
|
}
|
|
return paths;
|
|
}
|
|
function initMappers(options) {
|
|
if (mappers.length > 0 && mappersCachedOptions === options && cachedCwd === process.cwd()) {
|
|
return;
|
|
}
|
|
cachedCwd = process.cwd();
|
|
const configPaths = (typeof options.project === "string" ? [options.project] : (
|
|
// eslint-disable-next-line sonarjs/no-nested-conditional
|
|
Array.isArray(options.project) ? options.project : [cachedCwd]
|
|
)).map((config) => replacePathSeparator(config, path.sep, path.posix.sep));
|
|
const defaultInclude = ["**/*"];
|
|
const defaultIgnore = ["**/node_modules/**"];
|
|
const projectPaths = [
|
|
.../* @__PURE__ */ new Set([
|
|
...configPaths.filter((p) => !isDynamicPattern(p)).map((p) => path.resolve(process.cwd(), p)),
|
|
...globSync(
|
|
configPaths.filter((path2) => isDynamicPattern(path2)),
|
|
{
|
|
absolute: true,
|
|
dot: true,
|
|
expandDirectories: false,
|
|
ignore: defaultIgnore
|
|
}
|
|
)
|
|
])
|
|
];
|
|
mappers = projectPaths.map((projectPath) => {
|
|
let tsconfigResult;
|
|
if (isFile(projectPath)) {
|
|
const { dir, base } = path.parse(projectPath);
|
|
tsconfigResult = getTsconfig(dir, base);
|
|
} else {
|
|
tsconfigResult = getTsconfig(projectPath);
|
|
}
|
|
if (!tsconfigResult) {
|
|
return;
|
|
}
|
|
const mapperFn = createPathsMatcher(tsconfigResult);
|
|
if (!mapperFn) {
|
|
return;
|
|
}
|
|
const files = tsconfigResult.config.files == null && tsconfigResult.config.include == null ? (
|
|
// Include everything if no files or include options
|
|
globSync(defaultInclude, {
|
|
absolute: true,
|
|
cwd: path.dirname(tsconfigResult.path),
|
|
dot: true,
|
|
ignore: [
|
|
...tsconfigResult.config.exclude ?? [],
|
|
...defaultIgnore
|
|
]
|
|
})
|
|
) : [
|
|
// https://www.typescriptlang.org/tsconfig/#files
|
|
...tsconfigResult.config.files != null && tsconfigResult.config.files.length > 0 ? tsconfigResult.config.files.map(
|
|
(file) => path.normalize(
|
|
path.resolve(path.dirname(tsconfigResult.path), file)
|
|
)
|
|
) : [],
|
|
// https://www.typescriptlang.org/tsconfig/#include
|
|
...tsconfigResult.config.include != null && tsconfigResult.config.include.length > 0 ? globSync(tsconfigResult.config.include, {
|
|
absolute: true,
|
|
cwd: path.dirname(tsconfigResult.path),
|
|
dot: true,
|
|
ignore: [
|
|
...tsconfigResult.config.exclude ?? [],
|
|
...defaultIgnore
|
|
]
|
|
}) : []
|
|
];
|
|
return {
|
|
path: toNativePathSeparator(tsconfigResult.path),
|
|
files: new Set(files.map(toNativePathSeparator)),
|
|
mapperFn
|
|
};
|
|
}).filter(Boolean);
|
|
mappersCachedOptions = options;
|
|
}
|
|
function mangleScopedPackage(moduleName) {
|
|
if (moduleName.startsWith("@")) {
|
|
const replaceSlash = moduleName.replace(path.sep, "__");
|
|
if (replaceSlash !== moduleName) {
|
|
return replaceSlash.slice(1);
|
|
}
|
|
}
|
|
return moduleName;
|
|
}
|
|
function replacePathSeparator(p, from, to) {
|
|
return from === to ? p : p.replaceAll(from, to);
|
|
}
|
|
function toNativePathSeparator(p) {
|
|
return replacePathSeparator(
|
|
p,
|
|
path[process.platform === "win32" ? "posix" : "win32"].sep,
|
|
path[process.platform === "win32" ? "win32" : "posix"].sep
|
|
);
|
|
}
|
|
function equalChars(a, b) {
|
|
if (a.length === 0 || b.length === 0) {
|
|
return 0;
|
|
}
|
|
let i = 0;
|
|
const length = Math.min(a.length, b.length);
|
|
while (i < length && a.charAt(i) === b.charAt(i)) {
|
|
i += 1;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
export { createTypeScriptImportResolver, defaultConditionNames, defaultExtensionAlias, defaultExtensions, defaultMainFields, interfaceVersion, resolve };
|