resolve.js 8.1 KB

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