UNPKG

8.27 kBJavaScriptView Raw
1'use strict';
2
3exports.__esModule = true;
4
5const fs = require('fs');
6const Module = require('module');
7const path = require('path');
8const { getPhysicalFilename } = require('./contextCompat');
9
10const hashObject = require('./hash').hashObject;
11const ModuleCache = require('./ModuleCache').default;
12const pkgDir = require('./pkgDir').default;
13
14const CASE_SENSITIVE_FS = !fs.existsSync(path.join(__dirname.toUpperCase(), 'reSOLVE.js'));
15exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS;
16
17const ERROR_NAME = 'EslintPluginImportResolveError';
18
19const 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)
23const 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} */
39function 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>} */
47function 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} */
70function 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} */
94function getBaseDir(sourceFile) {
95 return pkgDir(sourceFile) || process.cwd();
96}
97
98/** @type {(name: string, sourceFile: string) => import('./resolve').Resolver} */
99function 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} */
121exports.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} */
150let prevSettings = null;
151let memoizedHash = '';
152/** @type {(modulePath: string, sourceFile: string, settings: import('./types').ESLintSettings) => import('./resolve').ResolvedResult} */
153function 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} */
216function relative(modulePath, sourceFile, settings) {
217 return fullResolve(modulePath, sourceFile, settings).path;
218}
219exports.relative = relative;
220
221/** @type {Set<import('eslint').Rule.RuleContext>} */
222const 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 */
231function 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}
253resolve.relative = relative;
254exports.default = resolve;