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