UNPKG

14.7 kBJavaScriptView Raw
1'use strict';
2
3const findRoot = require('find-root');
4const path = require('path');
5const isEqual = require('lodash/isEqual');
6const find = require('array.prototype.find');
7const interpret = require('interpret');
8const fs = require('fs');
9const isCore = require('is-core-module');
10const resolve = require('resolve/sync');
11const semver = require('semver');
12const hasOwn = require('hasown');
13const isRegex = require('is-regex');
14
15const log = require('debug')('eslint-plugin-import:resolver:webpack');
16
17exports.interfaceVersion = 2;
18
19/**
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 cwd
26 * @example
27 * options: {
28 * // Path to the webpack config
29 * config: 'webpack.config.js',
30 * // Path to be used to determine where to resolve webpack from
31 * // (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 null
35 * if resolved to a non-FS resource (i.e. script tag at page load)
36 */
37exports.resolve = function (source, file, settings) {
38
39 // strip loaders
40 const finalBang = source.lastIndexOf('!');
41 if (finalBang >= 0) {
42 source = source.slice(finalBang + 1);
43 }
44
45 // strip resource query
46 const finalQuestionMark = source.lastIndexOf('?');
47 if (finalQuestionMark >= 0) {
48 source = source.slice(0, finalQuestionMark);
49 }
50
51 let webpackConfig;
52
53 const _configPath = settings && 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 */
58 const cwd = settings && settings.cwd;
59 const configIndex = settings && settings['config-index'];
60 const env = settings && settings.env;
61 const argv = settings && typeof settings.argv !== 'undefined' ? settings.argv : {};
62 let packageDir;
63
64 let configPath = typeof _configPath === 'string' && _configPath.startsWith('.')
65 ? path.resolve(_configPath)
66 : _configPath;
67
68 log('Config path from settings:', configPath);
69
70 // see if we've got a config path, a config object, an array of config objects or a config function
71 if (!configPath || typeof configPath === 'string') {
72
73 // see if we've got an absolute path
74 if (!configPath || !path.isAbsolute(configPath)) {
75 // if not, find ancestral package.json and use its directory as base for the path
76 packageDir = findRoot(path.resolve(file));
77 if (!packageDir) { throw new Error('package not found above ' + file); }
78 }
79
80 configPath = findConfigPath(configPath, packageDir);
81
82 log('Config path resolved to:', configPath);
83 if (configPath) {
84 try {
85 webpackConfig = require(configPath);
86 } catch (e) {
87 console.log('Error resolving webpackConfig', e);
88 throw e;
89 }
90 } else {
91 log('No config path found relative to', file, '; using {}');
92 webpackConfig = {};
93 }
94
95 if (webpackConfig && webpackConfig.default) {
96 log('Using ES6 module "default" key instead of module.exports.');
97 webpackConfig = webpackConfig.default;
98 }
99
100 } else {
101 webpackConfig = configPath;
102 configPath = null;
103 }
104
105 if (typeof webpackConfig === 'function') {
106 webpackConfig = webpackConfig(env, argv);
107 }
108
109 if (Array.isArray(webpackConfig)) {
110 webpackConfig = webpackConfig.map((cfg) => {
111 if (typeof cfg === 'function') {
112 return cfg(env, argv);
113 }
114
115 return cfg;
116 });
117
118 if (typeof configIndex !== 'undefined' && webpackConfig.length > configIndex) {
119 webpackConfig = webpackConfig[configIndex];
120 } else {
121 webpackConfig = find(webpackConfig, function findFirstWithResolve(config) {
122 return !!config.resolve;
123 });
124 }
125 }
126
127 if (typeof webpackConfig.then === 'function') {
128 webpackConfig = {};
129
130 console.warn('Webpack config returns a `Promise`; that signature is not supported at the moment. Using empty object instead.');
131 }
132
133 if (webpackConfig == null) {
134 webpackConfig = {};
135
136 console.warn('No webpack configuration with a "resolve" field found. Using empty object instead.');
137 }
138
139 log('Using config: ', webpackConfig);
140
141 const resolveSync = getResolveSync(configPath, webpackConfig, cwd);
142
143 // externals
144 if (findExternal(source, webpackConfig.externals, path.dirname(file), resolveSync)) {
145 return { found: true, path: null };
146 }
147
148 // otherwise, resolve "normally"
149
150 try {
151 return { found: true, path: resolveSync(path.dirname(file), source) };
152 } catch (err) {
153 if (isCore(source)) {
154 return { found: true, path: null };
155 }
156
157 log('Error during module resolution:', err);
158 return { found: false };
159 }
160};
161
162const MAX_CACHE = 10;
163const _cache = [];
164function getResolveSync(configPath, webpackConfig, cwd) {
165 const cacheKey = { configPath, webpackConfig };
166 let cached = find(_cache, function (entry) { return isEqual(entry.key, cacheKey); });
167 if (!cached) {
168 cached = {
169 key: cacheKey,
170 value: createResolveSync(configPath, webpackConfig, cwd),
171 };
172 // put in front and pop last item
173 if (_cache.unshift(cached) > MAX_CACHE) {
174 _cache.pop();
175 }
176 }
177 return cached.value;
178}
179
180function createResolveSync(configPath, webpackConfig, cwd) {
181 let webpackRequire;
182 let basedir = null;
183
184 if (typeof configPath === 'string') {
185 // This can be changed via the settings passed in when defining the resolver
186 basedir = cwd || path.dirname(configPath);
187 log(`Attempting to load webpack path from ${basedir}`);
188 }
189
190 try {
191 // Attempt to resolve webpack from the given `basedir`
192 const webpackFilename = resolve('webpack', { basedir, preserveSymlinks: false });
193 const webpackResolveOpts = { basedir: path.dirname(webpackFilename), preserveSymlinks: false };
194
195 webpackRequire = function (id) {
196 return require(resolve(id, webpackResolveOpts));
197 };
198 } catch (e) {
199 // Something has gone wrong (or we're in a test). Use our own bundled
200 // enhanced-resolve.
201 log('Using bundled enhanced-resolve.');
202 webpackRequire = require;
203 }
204
205 const enhancedResolvePackage = webpackRequire('enhanced-resolve/package.json');
206 const enhancedResolveVersion = enhancedResolvePackage.version;
207 log('enhanced-resolve version:', enhancedResolveVersion);
208
209 const resolveConfig = webpackConfig.resolve || {};
210
211 if (semver.major(enhancedResolveVersion) >= 2) {
212 return createWebpack2ResolveSync(webpackRequire, resolveConfig);
213 }
214
215 return createWebpack1ResolveSync(webpackRequire, resolveConfig, webpackConfig.plugins);
216}
217
218/**
219 * webpack 2 defaults:
220 * https://github.com/webpack/webpack/blob/v2.1.0-beta.20/lib/WebpackOptionsDefaulter.js#L72-L87
221 * @type {Object}
222 */
223const webpack2DefaultResolveConfig = {
224 unsafeCache: true, // Probably a no-op, since how can we cache anything at all here?
225 modules: ['node_modules'],
226 extensions: ['.js', '.json'],
227 aliasFields: ['browser'],
228 mainFields: ['browser', 'module', 'main'],
229};
230
231function createWebpack2ResolveSync(webpackRequire, resolveConfig) {
232 const EnhancedResolve = webpackRequire('enhanced-resolve');
233
234 return EnhancedResolve.create.sync(Object.assign({}, webpack2DefaultResolveConfig, resolveConfig));
235}
236
237/**
238 * webpack 1 defaults: https://webpack.github.io/docs/configuration.html#resolve-packagemains
239 * @type {Array}
240 */
241const webpack1DefaultMains = [
242 'webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main',
243];
244
245// adapted from tests &
246// https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L322
247function createWebpack1ResolveSync(webpackRequire, resolveConfig, plugins) {
248 const Resolver = webpackRequire('enhanced-resolve/lib/Resolver');
249 const SyncNodeJsInputFileSystem = webpackRequire('enhanced-resolve/lib/SyncNodeJsInputFileSystem');
250
251 const ModuleAliasPlugin = webpackRequire('enhanced-resolve/lib/ModuleAliasPlugin');
252 const ModulesInDirectoriesPlugin = webpackRequire('enhanced-resolve/lib/ModulesInDirectoriesPlugin');
253 const ModulesInRootPlugin = webpackRequire('enhanced-resolve/lib/ModulesInRootPlugin');
254 const ModuleAsFilePlugin = webpackRequire('enhanced-resolve/lib/ModuleAsFilePlugin');
255 const ModuleAsDirectoryPlugin = webpackRequire('enhanced-resolve/lib/ModuleAsDirectoryPlugin');
256 const DirectoryDescriptionFilePlugin = webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFilePlugin');
257 const DirectoryDefaultFilePlugin = webpackRequire('enhanced-resolve/lib/DirectoryDefaultFilePlugin');
258 const FileAppendPlugin = webpackRequire('enhanced-resolve/lib/FileAppendPlugin');
259 const ResultSymlinkPlugin = webpackRequire('enhanced-resolve/lib/ResultSymlinkPlugin');
260 const DirectoryDescriptionFileFieldAliasPlugin = webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFileFieldAliasPlugin');
261
262 const resolver = new Resolver(new SyncNodeJsInputFileSystem());
263
264 resolver.apply(
265 resolveConfig.packageAlias
266 ? new DirectoryDescriptionFileFieldAliasPlugin('package.json', resolveConfig.packageAlias)
267 : function () {},
268 new ModuleAliasPlugin(resolveConfig.alias || {}),
269 makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.root),
270 new ModulesInDirectoriesPlugin(
271 'module',
272 resolveConfig.modulesDirectories || resolveConfig.modules || ['web_modules', 'node_modules']
273 ),
274 makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.fallback),
275 new ModuleAsFilePlugin('module'),
276 new ModuleAsDirectoryPlugin('module'),
277 new DirectoryDescriptionFilePlugin(
278 'package.json',
279 ['module', 'jsnext:main'].concat(resolveConfig.packageMains || webpack1DefaultMains)
280 ),
281 new DirectoryDefaultFilePlugin(['index']),
282 new FileAppendPlugin(resolveConfig.extensions || ['', '.webpack.js', '.web.js', '.js']),
283 new ResultSymlinkPlugin()
284 );
285
286 const resolvePlugins = [];
287
288 // support webpack.ResolverPlugin
289 if (plugins) {
290 plugins.forEach(function (plugin) {
291 if (
292 plugin.constructor
293 && plugin.constructor.name === 'ResolverPlugin'
294 && Array.isArray(plugin.plugins)
295 ) {
296 resolvePlugins.push.apply(resolvePlugins, plugin.plugins);
297 }
298 });
299 }
300
301 resolver.apply.apply(resolver, resolvePlugins);
302
303 return function () {
304 return resolver.resolveSync.apply(resolver, arguments);
305 };
306}
307
308/* eslint-disable */
309// from https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L365
310function makeRootPlugin(ModulesInRootPlugin, name, root) {
311 if (typeof root === 'string') {
312 return new ModulesInRootPlugin(name, root);
313 } else if (Array.isArray(root)) {
314 return function() {
315 root.forEach(function (root) {
316 this.apply(new ModulesInRootPlugin(name, root));
317 }, this);
318 };
319 }
320 return function () {};
321}
322/* eslint-enable */
323
324function findExternal(source, externals, context, resolveSync) {
325 if (!externals) { return false; }
326
327 // string match
328 if (typeof externals === 'string') { return source === externals; }
329
330 // array: recurse
331 if (Array.isArray(externals)) {
332 return externals.some(function (e) { return findExternal(source, e, context, resolveSync); });
333 }
334
335 if (isRegex(externals)) {
336 return externals.test(source);
337 }
338
339 if (typeof externals === 'function') {
340 let functionExternalFound = false;
341 const callback = function (err, value) {
342 if (err) {
343 functionExternalFound = false;
344 } else {
345 functionExternalFound = findExternal(source, value, context, resolveSync);
346 }
347 };
348 // - for prior webpack 5, 'externals function' uses 3 arguments
349 // - for webpack 5, the count of arguments is less than 3
350 if (externals.length === 3) {
351 externals.call(null, context, source, callback);
352 } else {
353 const ctx = {
354 context,
355 request: source,
356 contextInfo: {
357 issuer: '',
358 issuerLayer: null,
359 compiler: '',
360 },
361 getResolve: () => (resolveContext, requestToResolve, cb) => {
362 if (cb) {
363 try {
364 cb(null, resolveSync(resolveContext, requestToResolve));
365 } catch (e) {
366 cb(e);
367 }
368 } else {
369 log('getResolve without callback not supported');
370 return Promise.reject(new Error('Not supported'));
371 }
372 },
373 };
374 const result = externals.call(null, ctx, callback);
375 // todo handling Promise object (using synchronous-promise package?)
376 if (result && typeof result.then === 'function') {
377 log('Asynchronous functions for externals not supported');
378 }
379 }
380 return functionExternalFound;
381 }
382
383 // else, vanilla object
384 for (const key in externals) {
385 if (!hasOwn(externals, key)) { continue; }
386 if (source === key) { return true; }
387 }
388 return false;
389}
390
391function findConfigPath(configPath, packageDir) {
392 const extensions = Object.keys(interpret.extensions).sort(function (a, b) {
393 return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length;
394 });
395 let extension;
396
397 if (configPath) {
398 // extensions is not reused below, so safe to mutate it here.
399 extensions.reverse();
400 extensions.forEach(function (maybeExtension) {
401 if (extension) {
402 return;
403 }
404
405 if (configPath.substr(-maybeExtension.length) === maybeExtension) {
406 extension = maybeExtension;
407 }
408 });
409
410 // see if we've got an absolute path
411 if (!path.isAbsolute(configPath)) {
412 configPath = path.join(packageDir, configPath);
413 }
414 } else {
415 extensions.forEach(function (maybeExtension) {
416 if (extension) {
417 return;
418 }
419
420 const maybePath = path.resolve(
421 path.join(packageDir, 'webpack.config' + maybeExtension)
422 );
423 if (fs.existsSync(maybePath)) {
424 configPath = maybePath;
425 extension = maybeExtension;
426 }
427 });
428 }
429
430 registerCompiler(interpret.extensions[extension]);
431 return configPath;
432}
433
434function registerCompiler(moduleDescriptor) {
435 if (moduleDescriptor) {
436 if (typeof moduleDescriptor === 'string') {
437 require(moduleDescriptor);
438 } else if (!Array.isArray(moduleDescriptor)) {
439 moduleDescriptor.register(require(moduleDescriptor.module));
440 } else {
441 for (let i = 0; i < moduleDescriptor.length; i++) {
442 try {
443 registerCompiler(moduleDescriptor[i]);
444 break;
445 } catch (e) {
446 log('Failed to register compiler for moduleDescriptor[]:', i, moduleDescriptor);
447 }
448 }
449 }
450 }
451}