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
JavaScript
'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 };
}
};