| 1 | 'use strict';
|
|---|
| 2 |
|
|---|
| 3 | exports.__esModule = true;
|
|---|
| 4 |
|
|---|
| 5 | const fs = require('fs');
|
|---|
| 6 | const Module = require('module');
|
|---|
| 7 | const path = require('path');
|
|---|
| 8 | const { getPhysicalFilename } = require('./contextCompat');
|
|---|
| 9 |
|
|---|
| 10 | const hashObject = require('./hash').hashObject;
|
|---|
| 11 | const ModuleCache = require('./ModuleCache').default;
|
|---|
| 12 | const pkgDir = require('./pkgDir').default;
|
|---|
| 13 |
|
|---|
| 14 | const CASE_SENSITIVE_FS = !fs.existsSync(path.join(__dirname.toUpperCase(), 'reSOLVE.js'));
|
|---|
| 15 | exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS;
|
|---|
| 16 |
|
|---|
| 17 | const ERROR_NAME = 'EslintPluginImportResolveError';
|
|---|
| 18 |
|
|---|
| 19 | const fileExistsCache = new ModuleCache();
|
|---|
| 20 |
|
|---|
| 21 | // Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0)
|
|---|
| 22 | // Use `Module.createRequire` if available (added in Node v12.2.0)
|
|---|
| 23 | const createRequire = Module.createRequire
|
|---|
| 24 | // @ts-expect-error this only exists in older node
|
|---|
| 25 | || Module.createRequireFromPath
|
|---|
| 26 | || /** @type {(filename: string) => unknown} */ function (filename) {
|
|---|
| 27 | const mod = new Module(filename, void null);
|
|---|
| 28 | mod.filename = filename;
|
|---|
| 29 | // @ts-expect-error _nodeModulePaths is undocumented
|
|---|
| 30 | mod.paths = Module._nodeModulePaths(path.dirname(filename));
|
|---|
| 31 |
|
|---|
| 32 | // @ts-expect-error _compile is undocumented
|
|---|
| 33 | mod._compile(`module.exports = require;`, filename);
|
|---|
| 34 |
|
|---|
| 35 | return mod.exports;
|
|---|
| 36 | };
|
|---|
| 37 |
|
|---|
| 38 | /** @type {(resolver: object) => resolver is import('./resolve').Resolver} */
|
|---|
| 39 | function isResolverValid(resolver) {
|
|---|
| 40 | if ('interfaceVersion' in resolver && resolver.interfaceVersion === 2) {
|
|---|
| 41 | return 'resolve' in resolver && !!resolver.resolve && typeof resolver.resolve === 'function';
|
|---|
| 42 | }
|
|---|
| 43 | return 'resolveImport' in resolver && !!resolver.resolveImport && typeof resolver.resolveImport === 'function';
|
|---|
| 44 | }
|
|---|
| 45 |
|
|---|
| 46 | /** @type {<T extends string>(target: T, sourceFile?: string | null | undefined) => undefined | ReturnType<typeof require>} */
|
|---|
| 47 | function tryRequire(target, sourceFile) {
|
|---|
| 48 | let resolved;
|
|---|
| 49 | try {
|
|---|
| 50 | // Check if the target exists
|
|---|
| 51 | if (sourceFile != null) {
|
|---|
| 52 | try {
|
|---|
| 53 | resolved = createRequire(path.resolve(sourceFile)).resolve(target);
|
|---|
| 54 | } catch (e) {
|
|---|
| 55 | resolved = require.resolve(target);
|
|---|
| 56 | }
|
|---|
| 57 | } else {
|
|---|
| 58 | resolved = require.resolve(target);
|
|---|
| 59 | }
|
|---|
| 60 | } catch (e) {
|
|---|
| 61 | // If the target does not exist then just return undefined
|
|---|
| 62 | return undefined;
|
|---|
| 63 | }
|
|---|
| 64 |
|
|---|
| 65 | // If the target exists then return the loaded module
|
|---|
| 66 | return require(resolved);
|
|---|
| 67 | }
|
|---|
| 68 |
|
|---|
| 69 | /** @type {<T extends Map<string, unknown>>(resolvers: string[] | string | { [k: string]: string }, map: T) => T} */
|
|---|
| 70 | function resolverReducer(resolvers, map) {
|
|---|
| 71 | if (Array.isArray(resolvers)) {
|
|---|
| 72 | resolvers.forEach((r) => resolverReducer(r, map));
|
|---|
| 73 | return map;
|
|---|
| 74 | }
|
|---|
| 75 |
|
|---|
| 76 | if (typeof resolvers === 'string') {
|
|---|
| 77 | map.set(resolvers, null);
|
|---|
| 78 | return map;
|
|---|
| 79 | }
|
|---|
| 80 |
|
|---|
| 81 | if (typeof resolvers === 'object') {
|
|---|
| 82 | for (const key in resolvers) {
|
|---|
| 83 | map.set(key, resolvers[key]);
|
|---|
| 84 | }
|
|---|
| 85 | return map;
|
|---|
| 86 | }
|
|---|
| 87 |
|
|---|
| 88 | const err = new Error('invalid resolver config');
|
|---|
| 89 | err.name = ERROR_NAME;
|
|---|
| 90 | throw err;
|
|---|
| 91 | }
|
|---|
| 92 |
|
|---|
| 93 | /** @type {(sourceFile: string) => string} */
|
|---|
| 94 | function getBaseDir(sourceFile) {
|
|---|
| 95 | return pkgDir(sourceFile) || process.cwd();
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | /** @type {(name: string, sourceFile: string) => import('./resolve').Resolver} */
|
|---|
| 99 | function requireResolver(name, sourceFile) {
|
|---|
| 100 | // Try to resolve package with conventional name
|
|---|
| 101 | const resolver = tryRequire(`eslint-import-resolver-${name}`, sourceFile)
|
|---|
| 102 | || tryRequire(name, sourceFile)
|
|---|
| 103 | || tryRequire(path.resolve(getBaseDir(sourceFile), name));
|
|---|
| 104 |
|
|---|
| 105 | if (!resolver) {
|
|---|
| 106 | const err = new Error(`unable to load resolver "${name}".`);
|
|---|
| 107 | err.name = ERROR_NAME;
|
|---|
| 108 | throw err;
|
|---|
| 109 | }
|
|---|
| 110 | if (!isResolverValid(resolver)) {
|
|---|
| 111 | const err = new Error(`${name} with invalid interface loaded as resolver`);
|
|---|
| 112 | err.name = ERROR_NAME;
|
|---|
| 113 | throw err;
|
|---|
| 114 | }
|
|---|
| 115 |
|
|---|
| 116 | return resolver;
|
|---|
| 117 | }
|
|---|
| 118 |
|
|---|
| 119 | // https://stackoverflow.com/a/27382838
|
|---|
| 120 | /** @type {import('./resolve').fileExistsWithCaseSync} */
|
|---|
| 121 | exports.fileExistsWithCaseSync = function fileExistsWithCaseSync(filepath, cacheSettings, strict) {
|
|---|
| 122 | // don't care if the FS is case-sensitive
|
|---|
| 123 | if (CASE_SENSITIVE_FS) { return true; }
|
|---|
| 124 |
|
|---|
| 125 | // null means it resolved to a builtin
|
|---|
| 126 | if (filepath === null) { return true; }
|
|---|
| 127 | if (filepath.toLowerCase() === process.cwd().toLowerCase() && !strict) { return true; }
|
|---|
| 128 | const parsedPath = path.parse(filepath);
|
|---|
| 129 | const dir = parsedPath.dir;
|
|---|
| 130 |
|
|---|
| 131 | let result = fileExistsCache.get(filepath, cacheSettings);
|
|---|
| 132 | if (result != null) { return result; }
|
|---|
| 133 |
|
|---|
| 134 | // base case
|
|---|
| 135 | if (dir === '' || parsedPath.root === filepath) {
|
|---|
| 136 | result = true;
|
|---|
| 137 | } else {
|
|---|
| 138 | const filenames = fs.readdirSync(dir);
|
|---|
| 139 | if (filenames.indexOf(parsedPath.base) === -1) {
|
|---|
| 140 | result = false;
|
|---|
| 141 | } else {
|
|---|
| 142 | result = fileExistsWithCaseSync(dir, cacheSettings, strict);
|
|---|
| 143 | }
|
|---|
| 144 | }
|
|---|
| 145 | fileExistsCache.set(filepath, result);
|
|---|
| 146 | return result;
|
|---|
| 147 | };
|
|---|
| 148 |
|
|---|
| 149 | /** @type {import('./types').ESLintSettings | null} */
|
|---|
| 150 | let prevSettings = null;
|
|---|
| 151 | let memoizedHash = '';
|
|---|
| 152 | /** @type {(modulePath: string, sourceFile: string, settings: import('./types').ESLintSettings) => import('./resolve').ResolvedResult} */
|
|---|
| 153 | function fullResolve(modulePath, sourceFile, settings) {
|
|---|
| 154 | // check if this is a bonus core module
|
|---|
| 155 | const coreSet = new Set(settings['import/core-modules']);
|
|---|
| 156 | if (coreSet.has(modulePath)) { return { found: true, path: null }; }
|
|---|
| 157 |
|
|---|
| 158 | const sourceDir = path.dirname(sourceFile);
|
|---|
| 159 |
|
|---|
| 160 | if (prevSettings !== settings) {
|
|---|
| 161 | memoizedHash = hashObject(settings).digest('hex');
|
|---|
| 162 | prevSettings = settings;
|
|---|
| 163 | }
|
|---|
| 164 |
|
|---|
| 165 | const cacheKey = sourceDir + memoizedHash + modulePath;
|
|---|
| 166 |
|
|---|
| 167 | const cacheSettings = ModuleCache.getSettings(settings);
|
|---|
| 168 |
|
|---|
| 169 | const cachedPath = fileExistsCache.get(cacheKey, cacheSettings);
|
|---|
| 170 | if (cachedPath !== undefined) { return { found: true, path: cachedPath }; }
|
|---|
| 171 |
|
|---|
| 172 | /** @type {(resolvedPath: string | null) => void} */
|
|---|
| 173 | function cache(resolvedPath) {
|
|---|
| 174 | fileExistsCache.set(cacheKey, resolvedPath);
|
|---|
| 175 | }
|
|---|
| 176 |
|
|---|
| 177 | /** @type {(resolver: import('./resolve').Resolver, config: unknown) => import('./resolve').ResolvedResult} */
|
|---|
| 178 | function withResolver(resolver, config) {
|
|---|
| 179 | if (resolver.interfaceVersion === 2) {
|
|---|
| 180 | return resolver.resolve(modulePath, sourceFile, config);
|
|---|
| 181 | }
|
|---|
| 182 |
|
|---|
| 183 | try {
|
|---|
| 184 | const resolved = resolver.resolveImport(modulePath, sourceFile, config);
|
|---|
| 185 | if (resolved === undefined) { return { found: false }; }
|
|---|
| 186 | return { found: true, path: resolved };
|
|---|
| 187 | } catch (err) {
|
|---|
| 188 | return { found: false };
|
|---|
| 189 | }
|
|---|
| 190 | }
|
|---|
| 191 |
|
|---|
| 192 | const configResolvers = settings['import/resolver']
|
|---|
| 193 | || { node: settings['import/resolve'] }; // backward compatibility
|
|---|
| 194 |
|
|---|
| 195 | const resolvers = resolverReducer(configResolvers, new Map());
|
|---|
| 196 |
|
|---|
| 197 | for (const pair of resolvers) {
|
|---|
| 198 | const name = pair[0];
|
|---|
| 199 | const config = pair[1];
|
|---|
| 200 | const resolver = requireResolver(name, sourceFile);
|
|---|
| 201 | const resolved = withResolver(resolver, config);
|
|---|
| 202 |
|
|---|
| 203 | if (!resolved.found) { continue; }
|
|---|
| 204 |
|
|---|
| 205 | // else, counts
|
|---|
| 206 | cache(resolved.path);
|
|---|
| 207 | return resolved;
|
|---|
| 208 | }
|
|---|
| 209 |
|
|---|
| 210 | // failed
|
|---|
| 211 | // cache(undefined)
|
|---|
| 212 | return { found: false };
|
|---|
| 213 | }
|
|---|
| 214 |
|
|---|
| 215 | /** @type {import('./resolve').relative} */
|
|---|
| 216 | function relative(modulePath, sourceFile, settings) {
|
|---|
| 217 | return fullResolve(modulePath, sourceFile, settings).path;
|
|---|
| 218 | }
|
|---|
| 219 | exports.relative = relative;
|
|---|
| 220 |
|
|---|
| 221 | /** @type {Set<import('eslint').Rule.RuleContext>} */
|
|---|
| 222 | const erroredContexts = new Set();
|
|---|
| 223 |
|
|---|
| 224 | /**
|
|---|
| 225 | * Given
|
|---|
| 226 | * @param p - module path
|
|---|
| 227 | * @param context - ESLint context
|
|---|
| 228 | * @return - the full module filesystem path; null if package is core; undefined if not found
|
|---|
| 229 | * @type {import('./resolve').default}
|
|---|
| 230 | */
|
|---|
| 231 | function resolve(p, context) {
|
|---|
| 232 | try {
|
|---|
| 233 | return relative(p, getPhysicalFilename(context), context.settings);
|
|---|
| 234 | } catch (err) {
|
|---|
| 235 | if (!erroredContexts.has(context)) {
|
|---|
| 236 | // The `err.stack` string starts with `err.name` followed by colon and `err.message`.
|
|---|
| 237 | // We're filtering out the default `err.name` because it adds little value to the message.
|
|---|
| 238 | // @ts-expect-error this might be an Error
|
|---|
| 239 | let errMessage = err.message;
|
|---|
| 240 | // @ts-expect-error this might be an Error
|
|---|
| 241 | if (err.name !== ERROR_NAME && err.stack) {
|
|---|
| 242 | // @ts-expect-error this might be an Error
|
|---|
| 243 | errMessage = err.stack.replace(/^Error: /, '');
|
|---|
| 244 | }
|
|---|
| 245 | context.report({
|
|---|
| 246 | message: `Resolve error: ${errMessage}`,
|
|---|
| 247 | loc: { line: 1, column: 0 },
|
|---|
| 248 | });
|
|---|
| 249 | erroredContexts.add(context);
|
|---|
| 250 | }
|
|---|
| 251 | }
|
|---|
| 252 | }
|
|---|
| 253 | resolve.relative = relative;
|
|---|
| 254 | exports.default = resolve;
|
|---|