index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. var findRoot = require('find-root')
  2. , path = require('path')
  3. , get = require('lodash/get')
  4. , isEqual = require('lodash/isEqual')
  5. , find = require('array-find')
  6. , interpret = require('interpret')
  7. , fs = require('fs')
  8. , coreLibs = require('node-libs-browser')
  9. , resolve = require('resolve')
  10. , semver = require('semver')
  11. , has = require('has')
  12. var log = require('debug')('eslint-plugin-import:resolver:webpack')
  13. exports.interfaceVersion = 2
  14. /**
  15. * Find the full path to 'source', given 'file' as a full reference path.
  16. *
  17. * resolveImport('./foo', '/Users/ben/bar.js') => '/Users/ben/foo.js'
  18. * @param {string} source - the module to resolve; i.e './some-module'
  19. * @param {string} file - the importing file's full path; i.e. '/usr/local/bin/file.js'
  20. * @param {object} settings - the webpack config file name, as well as cwd
  21. * @example
  22. * options: {
  23. * // Path to the webpack config
  24. * config: 'webpack.config.js',
  25. * // Path to be used to determine where to resolve webpack from
  26. * // (may differ from the cwd in some cases)
  27. * cwd: process.cwd()
  28. * }
  29. * @return {string?} the resolved path to source, undefined if not resolved, or null
  30. * if resolved to a non-FS resource (i.e. script tag at page load)
  31. */
  32. exports.resolve = function (source, file, settings) {
  33. // strip loaders
  34. var finalBang = source.lastIndexOf('!')
  35. if (finalBang >= 0) {
  36. source = source.slice(finalBang + 1)
  37. }
  38. // strip resource query
  39. var finalQuestionMark = source.lastIndexOf('?')
  40. if (finalQuestionMark >= 0) {
  41. source = source.slice(0, finalQuestionMark)
  42. }
  43. var webpackConfig
  44. var configPath = get(settings, 'config')
  45. /**
  46. * Attempt to set the current working directory.
  47. * If none is passed, default to the `cwd` where the config is located.
  48. */
  49. , cwd = get(settings, 'cwd')
  50. , configIndex = get(settings, 'config-index')
  51. , env = get(settings, 'env')
  52. , argv = get(settings, 'argv', {})
  53. , packageDir
  54. log('Config path from settings:', configPath)
  55. // see if we've got a config path, a config object, an array of config objects or a config function
  56. if (!configPath || typeof configPath === 'string') {
  57. // see if we've got an absolute path
  58. if (!configPath || !path.isAbsolute(configPath)) {
  59. // if not, find ancestral package.json and use its directory as base for the path
  60. packageDir = findRoot(path.resolve(file))
  61. if (!packageDir) throw new Error('package not found above ' + file)
  62. }
  63. configPath = findConfigPath(configPath, packageDir)
  64. log('Config path resolved to:', configPath)
  65. if (configPath) {
  66. try {
  67. webpackConfig = require(configPath)
  68. } catch(e) {
  69. console.log('Error resolving webpackConfig', e)
  70. throw e
  71. }
  72. } else {
  73. log('No config path found relative to', file, '; using {}')
  74. webpackConfig = {}
  75. }
  76. if (webpackConfig && webpackConfig.default) {
  77. log('Using ES6 module "default" key instead of module.exports.')
  78. webpackConfig = webpackConfig.default
  79. }
  80. } else {
  81. webpackConfig = configPath
  82. configPath = null
  83. }
  84. if (typeof webpackConfig === 'function') {
  85. webpackConfig = webpackConfig(env, argv)
  86. }
  87. if (Array.isArray(webpackConfig)) {
  88. webpackConfig = webpackConfig.map(cfg => {
  89. if (typeof cfg === 'function') {
  90. return cfg(env, argv)
  91. }
  92. return cfg
  93. })
  94. if (typeof configIndex !== 'undefined' && webpackConfig.length > configIndex) {
  95. webpackConfig = webpackConfig[configIndex]
  96. }
  97. else {
  98. webpackConfig = find(webpackConfig, function findFirstWithResolve(config) {
  99. return !!config.resolve
  100. })
  101. }
  102. }
  103. if (webpackConfig == null) {
  104. webpackConfig = {}
  105. console.warn('No webpack configuration with a "resolve" field found. Using empty object instead')
  106. }
  107. log('Using config: ', webpackConfig)
  108. // externals
  109. if (findExternal(source, webpackConfig.externals, path.dirname(file))) {
  110. return { found: true, path: null }
  111. }
  112. // otherwise, resolve "normally"
  113. var resolveSync = getResolveSync(configPath, webpackConfig, cwd)
  114. try {
  115. return { found: true, path: resolveSync(path.dirname(file), source) }
  116. } catch (err) {
  117. if (source in coreLibs) {
  118. return { found: true, path: coreLibs[source] }
  119. }
  120. log('Error during module resolution:', err)
  121. return { found: false }
  122. }
  123. }
  124. var MAX_CACHE = 10
  125. var _cache = []
  126. function getResolveSync(configPath, webpackConfig, cwd) {
  127. var cacheKey = { configPath: configPath, webpackConfig: webpackConfig }
  128. var cached = find(_cache, function (entry) { return isEqual(entry.key, cacheKey) })
  129. if (!cached) {
  130. cached = {
  131. key: cacheKey,
  132. value: createResolveSync(configPath, webpackConfig, cwd),
  133. }
  134. // put in front and pop last item
  135. if (_cache.unshift(cached) > MAX_CACHE) {
  136. _cache.pop()
  137. }
  138. }
  139. return cached.value
  140. }
  141. function createResolveSync(configPath, webpackConfig, cwd) {
  142. var webpackRequire
  143. , basedir = null
  144. if (typeof configPath === 'string') {
  145. // This can be changed via the settings passed in when defining the resolver
  146. basedir = cwd || configPath
  147. log(`Attempting to load webpack path from ${basedir}`)
  148. }
  149. try {
  150. // Attempt to resolve webpack from the given `basedir`
  151. var webpackFilename = resolve.sync('webpack', { basedir, preserveSymlinks: false })
  152. var webpackResolveOpts = { basedir: path.dirname(webpackFilename), preserveSymlinks: false }
  153. webpackRequire = function (id) {
  154. return require(resolve.sync(id, webpackResolveOpts))
  155. }
  156. } catch (e) {
  157. // Something has gone wrong (or we're in a test). Use our own bundled
  158. // enhanced-resolve.
  159. log('Using bundled enhanced-resolve.')
  160. webpackRequire = require
  161. }
  162. var enhancedResolvePackage = webpackRequire('enhanced-resolve/package.json')
  163. var enhancedResolveVersion = enhancedResolvePackage.version
  164. log('enhanced-resolve version:', enhancedResolveVersion)
  165. var resolveConfig = webpackConfig.resolve || {}
  166. if (semver.major(enhancedResolveVersion) >= 2) {
  167. return createWebpack2ResolveSync(webpackRequire, resolveConfig)
  168. }
  169. return createWebpack1ResolveSync(webpackRequire, resolveConfig, webpackConfig.plugins)
  170. }
  171. function createWebpack2ResolveSync(webpackRequire, resolveConfig) {
  172. var EnhancedResolve = webpackRequire('enhanced-resolve')
  173. return EnhancedResolve.create.sync(Object.assign({}, webpack2DefaultResolveConfig, resolveConfig))
  174. }
  175. /**
  176. * webpack 2 defaults:
  177. * https://github.com/webpack/webpack/blob/v2.1.0-beta.20/lib/WebpackOptionsDefaulter.js#L72-L87
  178. * @type {Object}
  179. */
  180. var webpack2DefaultResolveConfig = {
  181. unsafeCache: true, // Probably a no-op, since how can we cache anything at all here?
  182. modules: ['node_modules'],
  183. extensions: ['.js', '.json'],
  184. aliasFields: ['browser'],
  185. mainFields: ['browser', 'module', 'main'],
  186. }
  187. // adapted from tests &
  188. // https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L322
  189. function createWebpack1ResolveSync(webpackRequire, resolveConfig, plugins) {
  190. var Resolver = webpackRequire('enhanced-resolve/lib/Resolver')
  191. var SyncNodeJsInputFileSystem = webpackRequire('enhanced-resolve/lib/SyncNodeJsInputFileSystem')
  192. var ModuleAliasPlugin = webpackRequire('enhanced-resolve/lib/ModuleAliasPlugin')
  193. var ModulesInDirectoriesPlugin =
  194. webpackRequire('enhanced-resolve/lib/ModulesInDirectoriesPlugin')
  195. var ModulesInRootPlugin = webpackRequire('enhanced-resolve/lib/ModulesInRootPlugin')
  196. var ModuleAsFilePlugin = webpackRequire('enhanced-resolve/lib/ModuleAsFilePlugin')
  197. var ModuleAsDirectoryPlugin = webpackRequire('enhanced-resolve/lib/ModuleAsDirectoryPlugin')
  198. var DirectoryDescriptionFilePlugin =
  199. webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFilePlugin')
  200. var DirectoryDefaultFilePlugin =
  201. webpackRequire('enhanced-resolve/lib/DirectoryDefaultFilePlugin')
  202. var FileAppendPlugin = webpackRequire('enhanced-resolve/lib/FileAppendPlugin')
  203. var ResultSymlinkPlugin = webpackRequire('enhanced-resolve/lib/ResultSymlinkPlugin')
  204. var DirectoryDescriptionFileFieldAliasPlugin =
  205. webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFileFieldAliasPlugin')
  206. var resolver = new Resolver(new SyncNodeJsInputFileSystem())
  207. resolver.apply(
  208. resolveConfig.packageAlias
  209. ? new DirectoryDescriptionFileFieldAliasPlugin('package.json', resolveConfig.packageAlias)
  210. : function() {},
  211. new ModuleAliasPlugin(resolveConfig.alias || {}),
  212. makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.root),
  213. new ModulesInDirectoriesPlugin(
  214. 'module',
  215. resolveConfig.modulesDirectories || resolveConfig.modules || ['web_modules', 'node_modules']
  216. ),
  217. makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.fallback),
  218. new ModuleAsFilePlugin('module'),
  219. new ModuleAsDirectoryPlugin('module'),
  220. new DirectoryDescriptionFilePlugin(
  221. 'package.json',
  222. ['module', 'jsnext:main'].concat(resolveConfig.packageMains || webpack1DefaultMains)
  223. ),
  224. new DirectoryDefaultFilePlugin(['index']),
  225. new FileAppendPlugin(resolveConfig.extensions || ['', '.webpack.js', '.web.js', '.js']),
  226. new ResultSymlinkPlugin()
  227. )
  228. var resolvePlugins = []
  229. // support webpack.ResolverPlugin
  230. if (plugins) {
  231. plugins.forEach(function (plugin) {
  232. if (
  233. plugin.constructor &&
  234. plugin.constructor.name === 'ResolverPlugin' &&
  235. Array.isArray(plugin.plugins)
  236. ) {
  237. resolvePlugins.push.apply(resolvePlugins, plugin.plugins)
  238. }
  239. })
  240. }
  241. resolver.apply.apply(resolver, resolvePlugins)
  242. return function() {
  243. return resolver.resolveSync.apply(resolver, arguments)
  244. }
  245. }
  246. /* eslint-disable */
  247. // from https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L365
  248. function makeRootPlugin(ModulesInRootPlugin, name, root) {
  249. if(typeof root === "string")
  250. return new ModulesInRootPlugin(name, root);
  251. else if(Array.isArray(root)) {
  252. return function() {
  253. root.forEach(function(root) {
  254. this.apply(new ModulesInRootPlugin(name, root));
  255. }, this);
  256. };
  257. }
  258. return function() {};
  259. }
  260. /* eslint-enable */
  261. function findExternal(source, externals, context) {
  262. if (!externals) return false
  263. // string match
  264. if (typeof externals === 'string') return (source === externals)
  265. // array: recurse
  266. if (externals instanceof Array) {
  267. return externals.some(function (e) { return findExternal(source, e, context) })
  268. }
  269. if (externals instanceof RegExp) {
  270. return externals.test(source)
  271. }
  272. if (typeof externals === 'function') {
  273. var functionExternalFound = false
  274. externals.call(null, context, source, function(err, value) {
  275. if (err) {
  276. functionExternalFound = false
  277. } else {
  278. functionExternalFound = findExternal(source, value, context)
  279. }
  280. })
  281. return functionExternalFound
  282. }
  283. // else, vanilla object
  284. for (var key in externals) {
  285. if (!has(externals, key)) continue
  286. if (source === key) return true
  287. }
  288. return false
  289. }
  290. /**
  291. * webpack 1 defaults: http://webpack.github.io/docs/configuration.html#resolve-packagemains
  292. * @type {Array}
  293. */
  294. var webpack1DefaultMains = [
  295. 'webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main',
  296. ]
  297. function findConfigPath(configPath, packageDir) {
  298. var extensions = Object.keys(interpret.extensions).sort(function(a, b) {
  299. return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length
  300. })
  301. , extension
  302. if (configPath) {
  303. // extensions is not reused below, so safe to mutate it here.
  304. extensions.reverse()
  305. extensions.forEach(function (maybeExtension) {
  306. if (extension) {
  307. return
  308. }
  309. if (configPath.substr(-maybeExtension.length) === maybeExtension) {
  310. extension = maybeExtension
  311. }
  312. })
  313. // see if we've got an absolute path
  314. if (!path.isAbsolute(configPath)) {
  315. configPath = path.join(packageDir, configPath)
  316. }
  317. } else {
  318. extensions.forEach(function (maybeExtension) {
  319. if (extension) {
  320. return
  321. }
  322. var maybePath = path.resolve(
  323. path.join(packageDir, 'webpack.config' + maybeExtension)
  324. )
  325. if (fs.existsSync(maybePath)) {
  326. configPath = maybePath
  327. extension = maybeExtension
  328. }
  329. })
  330. }
  331. registerCompiler(interpret.extensions[extension])
  332. return configPath
  333. }
  334. function registerCompiler(moduleDescriptor) {
  335. if(moduleDescriptor) {
  336. if(typeof moduleDescriptor === 'string') {
  337. require(moduleDescriptor)
  338. } else if(!Array.isArray(moduleDescriptor)) {
  339. moduleDescriptor.register(require(moduleDescriptor.module))
  340. } else {
  341. for(var i = 0; i < moduleDescriptor.length; i++) {
  342. try {
  343. registerCompiler(moduleDescriptor[i])
  344. break
  345. } catch(e) {
  346. log('Failed to register compiler for moduleDescriptor[]:', i, moduleDescriptor)
  347. }
  348. }
  349. }
  350. }
  351. }