Path: blob/main/resolvers/webpack/index.js
829 views
'use strict';12const findRoot = require('find-root');3const path = require('path');4const get = require('lodash/get');5const isEqual = require('lodash/isEqual');6const find = require('array-find');7const interpret = require('interpret');8const fs = require('fs');9const isCore = require('is-core-module');10const resolve = require('resolve');11const semver = require('semver');12const has = require('has');13const isRegex = require('is-regex');1415const log = require('debug')('eslint-plugin-import:resolver:webpack');1617exports.interfaceVersion = 2;1819/**20* Find the full path to 'source', given 'file' as a full reference path.21*22* resolveImport('./foo', '/Users/ben/bar.js') => '/Users/ben/foo.js'23* @param {string} source - the module to resolve; i.e './some-module'24* @param {string} file - the importing file's full path; i.e. '/usr/local/bin/file.js'25* @param {object} settings - the webpack config file name, as well as cwd26* @example27* options: {28* // Path to the webpack config29* config: 'webpack.config.js',30* // Path to be used to determine where to resolve webpack from31* // (may differ from the cwd in some cases)32* cwd: process.cwd()33* }34* @return {string?} the resolved path to source, undefined if not resolved, or null35* if resolved to a non-FS resource (i.e. script tag at page load)36*/37exports.resolve = function (source, file, settings) {3839// strip loaders40const finalBang = source.lastIndexOf('!');41if (finalBang >= 0) {42source = source.slice(finalBang + 1);43}4445// strip resource query46const finalQuestionMark = source.lastIndexOf('?');47if (finalQuestionMark >= 0) {48source = source.slice(0, finalQuestionMark);49}5051let webpackConfig;5253const _configPath = get(settings, 'config');54/**55* Attempt to set the current working directory.56* If none is passed, default to the `cwd` where the config is located.57*/58const cwd = get(settings, 'cwd');59const configIndex = get(settings, 'config-index');60const env = get(settings, 'env');61const argv = get(settings, 'argv', {});62let packageDir;6364let configPath = typeof _configPath === 'string' && _configPath.startsWith('.')65? path.resolve(_configPath)66: _configPath;6768log('Config path from settings:', configPath);6970// see if we've got a config path, a config object, an array of config objects or a config function71if (!configPath || typeof configPath === 'string') {7273// see if we've got an absolute path74if (!configPath || !path.isAbsolute(configPath)) {75// if not, find ancestral package.json and use its directory as base for the path76packageDir = findRoot(path.resolve(file));77if (!packageDir) throw new Error('package not found above ' + file);78}7980configPath = findConfigPath(configPath, packageDir);8182log('Config path resolved to:', configPath);83if (configPath) {84try {85webpackConfig = require(configPath);86} catch (e) {87console.log('Error resolving webpackConfig', e);88throw e;89}90} else {91log('No config path found relative to', file, '; using {}');92webpackConfig = {};93}9495if (webpackConfig && webpackConfig.default) {96log('Using ES6 module "default" key instead of module.exports.');97webpackConfig = webpackConfig.default;98}99100} else {101webpackConfig = configPath;102configPath = null;103}104105if (typeof webpackConfig === 'function') {106webpackConfig = webpackConfig(env, argv);107}108109if (Array.isArray(webpackConfig)) {110webpackConfig = webpackConfig.map(cfg => {111if (typeof cfg === 'function') {112return cfg(env, argv);113}114115return cfg;116});117118if (typeof configIndex !== 'undefined' && webpackConfig.length > configIndex) {119webpackConfig = webpackConfig[configIndex];120} else {121webpackConfig = find(webpackConfig, function findFirstWithResolve(config) {122return !!config.resolve;123});124}125}126127if (typeof webpackConfig.then === 'function') {128webpackConfig = {};129130console.warn('Webpack config returns a `Promise`; that signature is not supported at the moment. Using empty object instead.');131}132133if (webpackConfig == null) {134webpackConfig = {};135136console.warn('No webpack configuration with a "resolve" field found. Using empty object instead.');137}138139log('Using config: ', webpackConfig);140141const resolveSync = getResolveSync(configPath, webpackConfig, cwd);142143// externals144if (findExternal(source, webpackConfig.externals, path.dirname(file), resolveSync)) {145return { found: true, path: null };146}147148// otherwise, resolve "normally"149150try {151return { found: true, path: resolveSync(path.dirname(file), source) };152} catch (err) {153if (isCore(source)) {154return { found: true, path: null };155}156157log('Error during module resolution:', err);158return { found: false };159}160};161162const MAX_CACHE = 10;163const _cache = [];164function getResolveSync(configPath, webpackConfig, cwd) {165const cacheKey = { configPath, webpackConfig };166let cached = find(_cache, function (entry) { return isEqual(entry.key, cacheKey); });167if (!cached) {168cached = {169key: cacheKey,170value: createResolveSync(configPath, webpackConfig, cwd),171};172// put in front and pop last item173if (_cache.unshift(cached) > MAX_CACHE) {174_cache.pop();175}176}177return cached.value;178}179180function createResolveSync(configPath, webpackConfig, cwd) {181let webpackRequire;182let basedir = null;183184if (typeof configPath === 'string') {185// This can be changed via the settings passed in when defining the resolver186basedir = cwd || configPath;187log(`Attempting to load webpack path from ${basedir}`);188}189190try {191// Attempt to resolve webpack from the given `basedir`192const webpackFilename = resolve.sync('webpack', { basedir, preserveSymlinks: false });193const webpackResolveOpts = { basedir: path.dirname(webpackFilename), preserveSymlinks: false };194195webpackRequire = function (id) {196return require(resolve.sync(id, webpackResolveOpts));197};198} catch (e) {199// Something has gone wrong (or we're in a test). Use our own bundled200// enhanced-resolve.201log('Using bundled enhanced-resolve.');202webpackRequire = require;203}204205const enhancedResolvePackage = webpackRequire('enhanced-resolve/package.json');206const enhancedResolveVersion = enhancedResolvePackage.version;207log('enhanced-resolve version:', enhancedResolveVersion);208209const resolveConfig = webpackConfig.resolve || {};210211if (semver.major(enhancedResolveVersion) >= 2) {212return createWebpack2ResolveSync(webpackRequire, resolveConfig);213}214215return createWebpack1ResolveSync(webpackRequire, resolveConfig, webpackConfig.plugins);216}217218/**219* webpack 2 defaults:220* https://github.com/webpack/webpack/blob/v2.1.0-beta.20/lib/WebpackOptionsDefaulter.js#L72-L87221* @type {Object}222*/223const webpack2DefaultResolveConfig = {224unsafeCache: true, // Probably a no-op, since how can we cache anything at all here?225modules: ['node_modules'],226extensions: ['.js', '.json'],227aliasFields: ['browser'],228mainFields: ['browser', 'module', 'main'],229};230231function createWebpack2ResolveSync(webpackRequire, resolveConfig) {232const EnhancedResolve = webpackRequire('enhanced-resolve');233234return EnhancedResolve.create.sync(Object.assign({}, webpack2DefaultResolveConfig, resolveConfig));235}236237/**238* webpack 1 defaults: https://webpack.github.io/docs/configuration.html#resolve-packagemains239* @type {Array}240*/241const webpack1DefaultMains = [242'webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main',243];244245// adapted from tests &246// https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L322247function createWebpack1ResolveSync(webpackRequire, resolveConfig, plugins) {248const Resolver = webpackRequire('enhanced-resolve/lib/Resolver');249const SyncNodeJsInputFileSystem = webpackRequire('enhanced-resolve/lib/SyncNodeJsInputFileSystem');250251const ModuleAliasPlugin = webpackRequire('enhanced-resolve/lib/ModuleAliasPlugin');252const ModulesInDirectoriesPlugin = webpackRequire('enhanced-resolve/lib/ModulesInDirectoriesPlugin');253const ModulesInRootPlugin = webpackRequire('enhanced-resolve/lib/ModulesInRootPlugin');254const ModuleAsFilePlugin = webpackRequire('enhanced-resolve/lib/ModuleAsFilePlugin');255const ModuleAsDirectoryPlugin = webpackRequire('enhanced-resolve/lib/ModuleAsDirectoryPlugin');256const DirectoryDescriptionFilePlugin = webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFilePlugin');257const DirectoryDefaultFilePlugin = webpackRequire('enhanced-resolve/lib/DirectoryDefaultFilePlugin');258const FileAppendPlugin = webpackRequire('enhanced-resolve/lib/FileAppendPlugin');259const ResultSymlinkPlugin = webpackRequire('enhanced-resolve/lib/ResultSymlinkPlugin');260const DirectoryDescriptionFileFieldAliasPlugin = webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFileFieldAliasPlugin');261262const resolver = new Resolver(new SyncNodeJsInputFileSystem());263264resolver.apply(265resolveConfig.packageAlias266? new DirectoryDescriptionFileFieldAliasPlugin('package.json', resolveConfig.packageAlias)267: function () {},268new ModuleAliasPlugin(resolveConfig.alias || {}),269makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.root),270new ModulesInDirectoriesPlugin(271'module',272resolveConfig.modulesDirectories || resolveConfig.modules || ['web_modules', 'node_modules'],273),274makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.fallback),275new ModuleAsFilePlugin('module'),276new ModuleAsDirectoryPlugin('module'),277new DirectoryDescriptionFilePlugin(278'package.json',279['module', 'jsnext:main'].concat(resolveConfig.packageMains || webpack1DefaultMains),280),281new DirectoryDefaultFilePlugin(['index']),282new FileAppendPlugin(resolveConfig.extensions || ['', '.webpack.js', '.web.js', '.js']),283new ResultSymlinkPlugin(),284);285286287const resolvePlugins = [];288289// support webpack.ResolverPlugin290if (plugins) {291plugins.forEach(function (plugin) {292if (293plugin.constructor &&294plugin.constructor.name === 'ResolverPlugin' &&295Array.isArray(plugin.plugins)296) {297resolvePlugins.push.apply(resolvePlugins, plugin.plugins);298}299});300}301302resolver.apply.apply(resolver, resolvePlugins);303304return function () {305return resolver.resolveSync.apply(resolver, arguments);306};307}308309/* eslint-disable */310// from https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L365311function makeRootPlugin(ModulesInRootPlugin, name, root) {312if (typeof root === 'string') {313return new ModulesInRootPlugin(name, root);314} else if (Array.isArray(root)) {315return function() {316root.forEach(function (root) {317this.apply(new ModulesInRootPlugin(name, root));318}, this);319};320}321return function () {};322}323/* eslint-enable */324325function findExternal(source, externals, context, resolveSync) {326if (!externals) return false;327328// string match329if (typeof externals === 'string') return (source === externals);330331// array: recurse332if (Array.isArray(externals)) {333return externals.some(function (e) { return findExternal(source, e, context, resolveSync); });334}335336if (isRegex(externals)) {337return externals.test(source);338}339340if (typeof externals === 'function') {341let functionExternalFound = false;342const callback = function (err, value) {343if (err) {344functionExternalFound = false;345} else {346functionExternalFound = findExternal(source, value, context, resolveSync);347}348};349// - for prior webpack 5, 'externals function' uses 3 arguments350// - for webpack 5, the count of arguments is less than 3351if (externals.length === 3) {352externals.call(null, context, source, callback);353} else {354const ctx = {355context,356request: source,357contextInfo: {358issuer: '',359issuerLayer: null,360compiler: '',361},362getResolve: () => (resolveContext, requestToResolve, cb) => {363if (cb) {364try {365cb(null, resolveSync(resolveContext, requestToResolve));366} catch (e) {367cb(e);368}369} else {370log('getResolve without callback not supported');371return Promise.reject(new Error('Not supported'));372}373},374};375const result = externals.call(null, ctx, callback);376// todo handling Promise object (using synchronous-promise package?)377if (result && typeof result.then === 'function') {378log('Asynchronous functions for externals not supported');379}380}381return functionExternalFound;382}383384// else, vanilla object385for (const key in externals) {386if (!has(externals, key)) continue;387if (source === key) return true;388}389return false;390}391392function findConfigPath(configPath, packageDir) {393const extensions = Object.keys(interpret.extensions).sort(function (a, b) {394return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length;395});396let extension;397398399if (configPath) {400// extensions is not reused below, so safe to mutate it here.401extensions.reverse();402extensions.forEach(function (maybeExtension) {403if (extension) {404return;405}406407if (configPath.substr(-maybeExtension.length) === maybeExtension) {408extension = maybeExtension;409}410});411412// see if we've got an absolute path413if (!path.isAbsolute(configPath)) {414configPath = path.join(packageDir, configPath);415}416} else {417extensions.forEach(function (maybeExtension) {418if (extension) {419return;420}421422const maybePath = path.resolve(423path.join(packageDir, 'webpack.config' + maybeExtension),424);425if (fs.existsSync(maybePath)) {426configPath = maybePath;427extension = maybeExtension;428}429});430}431432registerCompiler(interpret.extensions[extension]);433return configPath;434}435436function registerCompiler(moduleDescriptor) {437if (moduleDescriptor) {438if (typeof moduleDescriptor === 'string') {439require(moduleDescriptor);440} else if (!Array.isArray(moduleDescriptor)) {441moduleDescriptor.register(require(moduleDescriptor.module));442} else {443for (let i = 0; i < moduleDescriptor.length; i++) {444try {445registerCompiler(moduleDescriptor[i]);446break;447} catch (e) {448log('Failed to register compiler for moduleDescriptor[]:', i, moduleDescriptor);449}450}451}452}453}454455456