UNPKG

eslint-import-resolver-webpack

Version:

Resolve paths to dependencies, given a webpack.config.js. Plugin for eslint-plugin-import.

478 lines (411 loc) 15.4 kB
'use strict'; const findRoot = require('find-root'); const path = require('path'); const isEqual = require('lodash/isEqual'); const interpret = require('interpret'); const existsSync = require('fs').existsSync; const isCore = require('is-core-module'); const resolve = require('resolve/sync'); const semver = require('semver'); const hasOwn = require('hasown'); const isRegex = require('is-regex'); const isArray = Array.isArray; const keys = Object.keys; const assign = Object.assign; const log = require('debug')('eslint-plugin-import:resolver:webpack'); exports.interfaceVersion = 2; function registerCompiler(moduleDescriptor) { if (moduleDescriptor) { if (typeof moduleDescriptor === 'string') { require(moduleDescriptor); } else if (!isArray(moduleDescriptor)) { moduleDescriptor.register(require(moduleDescriptor.module)); } else { for (let i = 0; i < moduleDescriptor.length; i++) { try { registerCompiler(moduleDescriptor[i]); break; } catch (e) { log('Failed to register compiler for moduleDescriptor[]:', i, moduleDescriptor); } } } } } function findConfigPath(configPath, packageDir) { const extensions = keys(interpret.extensions).sort(function (a, b) { return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length; }); let extension; if (configPath) { for (let i = extensions.length - 1; i >= 0 && !extension; i--) { const maybeExtension = extensions[i]; if (configPath.slice(-maybeExtension.length) === maybeExtension) { extension = maybeExtension; } } // see if we've got an absolute path if (!path.isAbsolute(configPath)) { configPath = path.join(packageDir, configPath); } } else { for (let i = 0; i < extensions.length && !extension; i++) { const maybeExtension = extensions[i]; const maybePath = path.resolve( path.join(packageDir, 'webpack.config' + maybeExtension) ); if (existsSync(maybePath)) { configPath = maybePath; extension = maybeExtension; } } } registerCompiler(interpret.extensions[extension]); return configPath; } function findExternal(source, externals, context, resolveSync) { if (!externals) { return false; } // string match if (typeof externals === 'string') { return source === externals; } // array: recurse if (isArray(externals)) { return externals.some(function (e) { return findExternal(source, e, context, resolveSync); }); } if (isRegex(externals)) { return externals.test(source); } if (typeof externals === 'function') { let functionExternalFound = false; const callback = function (err, value) { if (err) { functionExternalFound = false; } else { functionExternalFound = findExternal(source, value, context, resolveSync); } }; // - for prior webpack 5, 'externals function' uses 3 arguments // - for webpack 5, the count of arguments is less than 3 if (externals.length === 3) { externals.call(null, context, source, callback); } else { const ctx = { context, request: source, contextInfo: { issuer: '', issuerLayer: null, compiler: '', }, getResolve: () => (resolveContext, requestToResolve, cb) => { if (cb) { try { cb(null, resolveSync(resolveContext, requestToResolve)); } catch (e) { cb(e); } } else { log('getResolve without callback not supported'); return Promise.reject(new Error('Not supported')); } }, }; const result = externals.call(null, ctx, callback); // todo handling Promise object (using synchronous-promise package?) if (result && typeof result.then === 'function') { log('Asynchronous functions for externals not supported'); } } return functionExternalFound; } // else, vanilla object for (const key in externals) { if (hasOwn(externals, key) && source === key) { return true; } } return false; } /** * webpack 2 defaults: * https://github.com/webpack/webpack/blob/v2.1.0-beta.20/lib/WebpackOptionsDefaulter.js#L72-L87 * @type {Object} */ const webpack2DefaultResolveConfig = { unsafeCache: true, // Probably a no-op, since how can we cache anything at all here? modules: ['node_modules'], extensions: ['.js', '.json'], aliasFields: ['browser'], mainFields: ['browser', 'module', 'main'], }; function createWebpack2ResolveSync(webpackRequire, resolveConfig) { const EnhancedResolve = webpackRequire('enhanced-resolve'); return EnhancedResolve.create.sync(assign({}, webpack2DefaultResolveConfig, resolveConfig)); } /** * webpack 1 defaults: https://webpack.github.io/docs/configuration.html#resolve-packagemains * @type {string[]} */ const webpack1DefaultMains = [ 'webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main', ]; /* eslint-disable */ // from https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L365 function makeRootPlugin(ModulesInRootPlugin, name, root) { if (typeof root === 'string') { return new ModulesInRootPlugin(name, root); } if (isArray(root)) { return function () { root.forEach(function (root) { this.apply(new ModulesInRootPlugin(name, root)); }, this); }; } return function () {}; } /* eslint-enable */ // adapted from tests & // https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L322 function createWebpack1ResolveSync(webpackRequire, resolveConfig, plugins) { const Resolver = webpackRequire('enhanced-resolve/lib/Resolver'); const SyncNodeJsInputFileSystem = webpackRequire('enhanced-resolve/lib/SyncNodeJsInputFileSystem'); const ModuleAliasPlugin = webpackRequire('enhanced-resolve/lib/ModuleAliasPlugin'); const ModulesInDirectoriesPlugin = webpackRequire('enhanced-resolve/lib/ModulesInDirectoriesPlugin'); const ModulesInRootPlugin = webpackRequire('enhanced-resolve/lib/ModulesInRootPlugin'); const ModuleAsFilePlugin = webpackRequire('enhanced-resolve/lib/ModuleAsFilePlugin'); const ModuleAsDirectoryPlugin = webpackRequire('enhanced-resolve/lib/ModuleAsDirectoryPlugin'); const DirectoryDescriptionFilePlugin = webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFilePlugin'); const DirectoryDefaultFilePlugin = webpackRequire('enhanced-resolve/lib/DirectoryDefaultFilePlugin'); const FileAppendPlugin = webpackRequire('enhanced-resolve/lib/FileAppendPlugin'); const ResultSymlinkPlugin = webpackRequire('enhanced-resolve/lib/ResultSymlinkPlugin'); const DirectoryDescriptionFileFieldAliasPlugin = webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFileFieldAliasPlugin'); const resolver = new Resolver(new SyncNodeJsInputFileSystem()); resolver.apply( resolveConfig.packageAlias ? new DirectoryDescriptionFileFieldAliasPlugin('package.json', resolveConfig.packageAlias) : function () {}, new ModuleAliasPlugin(resolveConfig.alias || {}), makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.root), new ModulesInDirectoriesPlugin( 'module', resolveConfig.modulesDirectories || resolveConfig.modules || ['web_modules', 'node_modules'] ), makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.fallback), new ModuleAsFilePlugin('module'), new ModuleAsDirectoryPlugin('module'), new DirectoryDescriptionFilePlugin( 'package.json', ['module', 'jsnext:main'].concat(resolveConfig.packageMains || webpack1DefaultMains) ), new DirectoryDefaultFilePlugin(['index']), new FileAppendPlugin(resolveConfig.extensions || ['', '.webpack.js', '.web.js', '.js']), new ResultSymlinkPlugin() ); const resolvePlugins = []; // support webpack.ResolverPlugin if (plugins) { plugins.forEach(function (plugin) { if ( plugin.constructor && plugin.constructor.name === 'ResolverPlugin' && isArray(plugin.plugins) ) { resolvePlugins.push.apply(resolvePlugins, plugin.plugins); } }); } resolver.apply.apply(resolver, resolvePlugins); return function () { return resolver.resolveSync.apply(resolver, arguments); }; } function createResolveSync(configPath, webpackConfig, cwd) { let webpackRequire; let basedir = null; if (typeof configPath === 'string') { // This can be changed via the settings passed in when defining the resolver basedir = cwd || path.dirname(configPath); log(`Attempting to load webpack path from ${basedir}`); } try { // Attempt to resolve webpack from the given `basedir` const webpackFilename = resolve('webpack', { basedir, preserveSymlinks: false }); const webpackResolveOpts = { basedir: path.dirname(webpackFilename), preserveSymlinks: false }; webpackRequire = function (id) { return require(resolve(id, webpackResolveOpts)); }; } catch (e) { // Something has gone wrong (or we're in a test). Use our own bundled // enhanced-resolve. log('Using bundled enhanced-resolve.'); webpackRequire = require; } const enhancedResolvePackage = webpackRequire('enhanced-resolve/package.json'); const enhancedResolveVersion = enhancedResolvePackage.version; log('enhanced-resolve version:', enhancedResolveVersion); const resolveConfig = webpackConfig.resolve || {}; if (semver.major(enhancedResolveVersion) >= 2) { return createWebpack2ResolveSync(webpackRequire, resolveConfig); } return createWebpack1ResolveSync(webpackRequire, resolveConfig, webpackConfig.plugins); } const MAX_CACHE = 10; const _cache = []; function getResolveSync(configPath, webpackConfig, cwd) { const cacheKey = { configPath, webpackConfig }; for (let i = 0; i < _cache.length; i++) { if (isEqual(_cache[i].key, cacheKey)) { return _cache[i].value; } } const cached = { key: cacheKey, value: createResolveSync(configPath, webpackConfig, cwd), }; // put in front and pop last item if (_cache.unshift(cached) > MAX_CACHE) { _cache.pop(); } return cached.value; } const _evalCache = new Map(); function evaluateFunctionConfigCached(configPath, webpackConfig, env, argv) { const cacheKey = JSON.stringify({ configPath, args: [env, argv] }); if (_evalCache.has(cacheKey)) { return _evalCache.get(cacheKey); } const cached = webpackConfig(env, argv); _evalCache.set(cacheKey, cached); while (_evalCache.size > MAX_CACHE) { // remove oldest item _evalCache.delete(_evalCache.keys().next().value); } return cached; } /** * Find the full path to 'source', given 'file' as a full reference path. * * resolveImport('./foo', '/Users/ben/bar.js') => '/Users/ben/foo.js' * @param {string} source - the module to resolve; i.e './some-module' * @param {string} file - the importing file's full path; i.e. '/usr/local/bin/file.js' * @param {object} settings - the webpack config file name, as well as cwd * @example * options: { * // Path to the webpack config * config: 'webpack.config.js', * // Path to be used to determine where to resolve webpack from * // (may differ from the cwd in some cases) * cwd: process.cwd() * } * @return {string?} the resolved path to source, undefined if not resolved, or null * if resolved to a non-FS resource (i.e. script tag at page load) */ exports.resolve = function (source, file, settings) { // strip loaders const finalBang = source.lastIndexOf('!'); if (finalBang >= 0) { source = source.slice(finalBang + 1); } // strip resource query const finalQuestionMark = source.lastIndexOf('?'); if (finalQuestionMark >= 0) { source = source.slice(0, finalQuestionMark); } let webpackConfig; const _configPath = settings && settings.config; /** * Attempt to set the current working directory. * If none is passed, default to the `cwd` where the config is located. */ const cwd = settings && settings.cwd; const configIndex = settings && settings['config-index']; const env = settings && settings.env; const argv = settings && typeof settings.argv !== 'undefined' ? settings.argv : {}; const shouldCacheFunctionConfig = settings && settings.cache; let packageDir; let configPath = typeof _configPath === 'string' && _configPath.startsWith('.') ? path.resolve(_configPath) : _configPath; log('Config path from settings:', configPath); // see if we've got a config path, a config object, an array of config objects or a config function if (!configPath || typeof configPath === 'string') { // see if we've got an absolute path if (!configPath || !path.isAbsolute(configPath)) { // if not, find ancestral package.json and use its directory as base for the path packageDir = findRoot(path.resolve(file)); if (!packageDir) { throw new Error('package not found above ' + file); } } configPath = findConfigPath(configPath, packageDir); log('Config path resolved to:', configPath); if (configPath) { try { webpackConfig = require(configPath); } catch (e) { console.log('Error resolving webpackConfig', e); throw e; } } else { log('No config path found relative to', file, '; using {}'); webpackConfig = {}; } if (webpackConfig && webpackConfig.default) { log('Using ES6 module "default" key instead of module.exports.'); webpackConfig = webpackConfig.default; } } else { webpackConfig = configPath; configPath = null; } if (typeof webpackConfig === 'function') { webpackConfig = shouldCacheFunctionConfig ? evaluateFunctionConfigCached(configPath, webpackConfig, env, argv) : webpackConfig(env, argv); } if (isArray(webpackConfig)) { webpackConfig = webpackConfig.map((cfg) => { if (typeof cfg === 'function') { return cfg(env, argv); } return cfg; }); if (typeof configIndex !== 'undefined' && webpackConfig.length > configIndex) { webpackConfig = webpackConfig[configIndex]; } else { for (let i = 0; i < webpackConfig.length; i++) { if (webpackConfig[i].resolve) { webpackConfig = webpackConfig[i]; break; } } } } if (typeof webpackConfig.then === 'function') { webpackConfig = {}; console.warn('Webpack config returns a `Promise`; that signature is not supported at the moment. Using empty object instead.'); } if (webpackConfig == null) { webpackConfig = {}; console.warn('No webpack configuration with a "resolve" field found. Using empty object instead.'); } log('Using config: ', webpackConfig); const resolveSync = getResolveSync(configPath, webpackConfig, cwd); // externals if (findExternal(source, webpackConfig.externals, path.dirname(file), resolveSync)) { return { found: true, path: null }; } // otherwise, resolve "normally" try { return { found: true, path: resolveSync(path.dirname(file), source) }; } catch (err) { if (isCore(source)) { return { found: true, path: null }; } log('Error during module resolution:', err); return { found: false }; } };