UNPKG

13 kBJavaScriptView Raw
1const builtins = require('./builtins');
2const nodeBuiltins = require('node-libs-browser');
3const path = require('path');
4const {isGlob} = require('./utils/glob');
5const fs = require('@parcel/fs');
6const micromatch = require('micromatch');
7const getModuleParts = require('./utils/getModuleParts');
8
9const EMPTY_SHIM = require.resolve('./builtins/_empty');
10
11/**
12 * This resolver implements a modified version of the node_modules resolution algorithm:
13 * https://nodejs.org/api/modules.html#modules_all_together
14 *
15 * In addition to the standard algorithm, Parcel supports:
16 * - All file extensions supported by Parcel.
17 * - Glob file paths
18 * - Absolute paths (e.g. /foo) resolved relative to the project root.
19 * - Tilde paths (e.g. ~/foo) resolved relative to the nearest module root in node_modules.
20 * - The package.json module, jsnext:main, and browser field as replacements for package.main.
21 * - The package.json browser and alias fields as an alias map within a local module.
22 * - The package.json alias field in the root package for global aliases across all modules.
23 */
24class Resolver {
25 constructor(options = {}) {
26 this.options = options;
27 this.cache = new Map();
28 this.packageCache = new Map();
29 this.rootPackage = null;
30 }
31
32 async resolve(input, parent) {
33 let filename = input;
34
35 // Check the cache first
36 let key = this.getCacheKey(filename, parent);
37 if (this.cache.has(key)) {
38 return this.cache.get(key);
39 }
40
41 // Check if this is a glob
42 if (isGlob(filename)) {
43 return {path: path.resolve(path.dirname(parent), filename)};
44 }
45
46 // Get file extensions to search
47 let extensions = Array.isArray(this.options.extensions)
48 ? this.options.extensions.slice()
49 : Object.keys(this.options.extensions);
50
51 if (parent) {
52 // parent's extension given high priority
53 const parentExt = path.extname(parent);
54 extensions = [parentExt, ...extensions.filter(ext => ext !== parentExt)];
55 }
56
57 extensions.unshift('');
58
59 // Resolve the module directory or local file path
60 let module = await this.resolveModule(filename, parent);
61 let resolved;
62
63 if (module.moduleDir) {
64 resolved = await this.loadNodeModules(module, extensions);
65 } else if (module.filePath) {
66 resolved = await this.loadRelative(module.filePath, extensions);
67 }
68
69 if (!resolved) {
70 let dir = parent ? path.dirname(parent) : process.cwd();
71 let err = new Error(`Cannot find module '${input}' from '${dir}'`);
72 err.code = 'MODULE_NOT_FOUND';
73 throw err;
74 }
75
76 this.cache.set(key, resolved);
77 return resolved;
78 }
79
80 async resolveModule(filename, parent) {
81 let dir = parent ? path.dirname(parent) : process.cwd();
82
83 // If this isn't the entrypoint, resolve the input file to an absolute path
84 if (parent) {
85 filename = this.resolveFilename(filename, dir);
86 }
87
88 // Resolve aliases in the parent module for this file.
89 filename = await this.loadAlias(filename, dir);
90
91 // Return just the file path if this is a file, not in node_modules
92 if (path.isAbsolute(filename)) {
93 return {
94 filePath: filename
95 };
96 }
97
98 // Resolve the module in node_modules
99 let resolved;
100 try {
101 resolved = await this.findNodeModulePath(filename, dir);
102 } catch (err) {
103 // ignore
104 }
105
106 // If we couldn't resolve the node_modules path, just return the module name info
107 if (!resolved) {
108 let parts = getModuleParts(filename);
109 resolved = {
110 moduleName: parts[0],
111 subPath: parts[1]
112 };
113 }
114
115 return resolved;
116 }
117
118 getCacheKey(filename, parent) {
119 return (parent ? path.dirname(parent) : '') + ':' + filename;
120 }
121
122 resolveFilename(filename, dir) {
123 switch (filename[0]) {
124 case '/':
125 // Absolute path. Resolve relative to project root.
126 return path.resolve(this.options.rootDir, filename.slice(1));
127
128 case '~':
129 // Tilde path. Resolve relative to nearest node_modules directory,
130 // or the project root - whichever comes first.
131 while (
132 dir !== this.options.rootDir &&
133 path.basename(path.dirname(dir)) !== 'node_modules'
134 ) {
135 dir = path.dirname(dir);
136
137 if (dir === path.dirname(dir)) {
138 dir = this.options.rootDir;
139 break;
140 }
141 }
142
143 return path.join(dir, filename.slice(1));
144
145 case '.':
146 // Relative path.
147 return path.resolve(dir, filename);
148
149 default:
150 // Module
151 return filename;
152 }
153 }
154
155 async loadRelative(filename, extensions) {
156 // Find a package.json file in the current package.
157 let pkg = await this.findPackage(path.dirname(filename));
158
159 // First try as a file, then as a directory.
160 return (
161 (await this.loadAsFile(filename, extensions, pkg)) ||
162 (await this.loadDirectory(filename, extensions, pkg)) // eslint-disable-line no-return-await
163 );
164 }
165
166 async findNodeModulePath(filename, dir) {
167 if (builtins[filename]) {
168 if (this.options.target === 'node' && filename in nodeBuiltins) {
169 throw new Error('Cannot resolve builtin module for node target');
170 }
171
172 return {filePath: builtins[filename]};
173 }
174
175 let parts = getModuleParts(filename);
176 let root = path.parse(dir).root;
177
178 while (dir !== root) {
179 // Skip node_modules directories
180 if (path.basename(dir) === 'node_modules') {
181 dir = path.dirname(dir);
182 }
183
184 try {
185 // First, check if the module directory exists. This prevents a lot of unnecessary checks later.
186 let moduleDir = path.join(dir, 'node_modules', parts[0]);
187 let stats = await fs.stat(moduleDir);
188 if (stats.isDirectory()) {
189 return {
190 moduleName: parts[0],
191 subPath: parts[1],
192 moduleDir: moduleDir,
193 filePath: path.join(dir, 'node_modules', filename)
194 };
195 }
196 } catch (err) {
197 // ignore
198 }
199
200 // Move up a directory
201 dir = path.dirname(dir);
202 }
203 }
204
205 async loadNodeModules(module, extensions) {
206 try {
207 // If a module was specified as a module sub-path (e.g. some-module/some/path),
208 // it is likely a file. Try loading it as a file first.
209 if (module.subPath) {
210 let pkg = await this.readPackage(module.moduleDir);
211 let res = await this.loadAsFile(module.filePath, extensions, pkg);
212 if (res) {
213 return res;
214 }
215 }
216
217 // Otherwise, load as a directory.
218 return await this.loadDirectory(module.filePath, extensions);
219 } catch (e) {
220 // ignore
221 }
222 }
223
224 async isFile(file) {
225 try {
226 let stat = await fs.stat(file);
227 return stat.isFile() || stat.isFIFO();
228 } catch (err) {
229 return false;
230 }
231 }
232
233 async loadDirectory(dir, extensions, pkg) {
234 try {
235 pkg = await this.readPackage(dir);
236
237 // Get a list of possible package entry points.
238 let entries = this.getPackageEntries(pkg);
239
240 for (let file of entries) {
241 // First try loading package.main as a file, then try as a directory.
242 const res =
243 (await this.loadAsFile(file, extensions, pkg)) ||
244 (await this.loadDirectory(file, extensions, pkg));
245 if (res) {
246 return res;
247 }
248 }
249 } catch (err) {
250 // ignore
251 }
252
253 // Fall back to an index file inside the directory.
254 return this.loadAsFile(path.join(dir, 'index'), extensions, pkg);
255 }
256
257 async readPackage(dir) {
258 let file = path.join(dir, 'package.json');
259 if (this.packageCache.has(file)) {
260 return this.packageCache.get(file);
261 }
262
263 let json = await fs.readFile(file, 'utf8');
264 let pkg = JSON.parse(json);
265
266 pkg.pkgfile = file;
267 pkg.pkgdir = dir;
268
269 // If the package has a `source` field, check if it is behind a symlink.
270 // If so, we treat the module as source code rather than a pre-compiled module.
271 if (pkg.source) {
272 let realpath = await fs.realpath(file);
273 if (realpath === file) {
274 delete pkg.source;
275 }
276 }
277
278 this.packageCache.set(file, pkg);
279 return pkg;
280 }
281
282 getBrowserField(pkg) {
283 let target = this.options.target || 'browser';
284 return target === 'browser' ? pkg.browser : null;
285 }
286
287 getPackageEntries(pkg) {
288 let browser = this.getBrowserField(pkg);
289 if (browser && typeof browser === 'object' && browser[pkg.name]) {
290 browser = browser[pkg.name];
291 }
292
293 // libraries like d3.js specifies node.js specific files in the "main" which breaks the build
294 // we use the "browser" or "module" field to get the full dependency tree if available.
295 // If this is a linked module with a `source` field, use that as the entry point.
296 return [pkg.source, browser, pkg.module, pkg.main]
297 .filter(entry => typeof entry === 'string')
298 .map(main => {
299 // Default to index file if no main field find
300 if (!main || main === '.' || main === './') {
301 main = 'index';
302 }
303
304 return path.resolve(pkg.pkgdir, main);
305 });
306 }
307
308 async loadAsFile(file, extensions, pkg) {
309 // Try all supported extensions
310 for (let f of this.expandFile(file, extensions, pkg)) {
311 if (await this.isFile(f)) {
312 return {path: f, pkg};
313 }
314 }
315 }
316
317 expandFile(file, extensions, pkg, expandAliases = true) {
318 // Expand extensions and aliases
319 let res = [];
320 for (let ext of extensions) {
321 let f = file + ext;
322
323 if (expandAliases) {
324 let alias = this.resolveAliases(file + ext, pkg);
325 if (alias !== f) {
326 res = res.concat(this.expandFile(alias, extensions, pkg, false));
327 }
328 }
329
330 res.push(f);
331 }
332
333 return res;
334 }
335
336 resolveAliases(filename, pkg) {
337 // First resolve local package aliases, then project global ones.
338 return this.resolvePackageAliases(
339 this.resolvePackageAliases(filename, pkg),
340 this.rootPackage
341 );
342 }
343
344 resolvePackageAliases(filename, pkg) {
345 if (!pkg) {
346 return filename;
347 }
348
349 // Resolve aliases in the package.source, package.alias, and package.browser fields.
350 return (
351 this.getAlias(filename, pkg.pkgdir, pkg.source) ||
352 this.getAlias(filename, pkg.pkgdir, pkg.alias) ||
353 this.getAlias(filename, pkg.pkgdir, this.getBrowserField(pkg)) ||
354 filename
355 );
356 }
357
358 getAlias(filename, dir, aliases) {
359 if (!filename || !aliases || typeof aliases !== 'object') {
360 return null;
361 }
362
363 let alias;
364
365 // If filename is an absolute path, get one relative to the package.json directory.
366 if (path.isAbsolute(filename)) {
367 filename = path.relative(dir, filename);
368 if (filename[0] !== '.') {
369 filename = './' + filename;
370 }
371
372 alias = this.lookupAlias(aliases, filename, dir);
373 } else {
374 // It is a node_module. First try the entire filename as a key.
375 alias = this.lookupAlias(aliases, filename, dir);
376 if (alias == null) {
377 // If it didn't match, try only the module name.
378 let parts = getModuleParts(filename);
379 alias = this.lookupAlias(aliases, parts[0], dir);
380 if (typeof alias === 'string') {
381 // Append the filename back onto the aliased module.
382 alias = path.join(alias, ...parts.slice(1));
383 }
384 }
385 }
386
387 // If the alias is set to `false`, return an empty file.
388 if (alias === false) {
389 return EMPTY_SHIM;
390 }
391
392 return alias;
393 }
394
395 lookupAlias(aliases, filename, dir) {
396 // First, try looking up the exact filename
397 let alias = aliases[filename];
398 if (alias == null) {
399 // Otherwise, try replacing glob keys
400 for (let key in aliases) {
401 if (isGlob(key)) {
402 let re = micromatch.makeRe(key, {capture: true});
403 if (re.test(filename)) {
404 alias = filename.replace(re, aliases[key]);
405 break;
406 }
407 }
408 }
409 // Or try a lookup replacing backslash characters with forward slash
410 if (alias == null && ~filename.indexOf('\\')) {
411 alias = aliases[filename.replace(/\\/g, '/')];
412 }
413 }
414
415 if (typeof alias === 'string') {
416 return this.resolveFilename(alias, dir);
417 }
418
419 return alias;
420 }
421
422 async findPackage(dir) {
423 // Find the nearest package.json file within the current node_modules folder
424 let root = path.parse(dir).root;
425 while (dir !== root && path.basename(dir) !== 'node_modules') {
426 try {
427 return await this.readPackage(dir);
428 } catch (err) {
429 // ignore
430 }
431
432 dir = path.dirname(dir);
433 }
434 }
435
436 async loadAlias(filename, dir) {
437 // Load the root project's package.json file if we haven't already
438 if (!this.rootPackage) {
439 this.rootPackage = await this.findPackage(this.options.rootDir);
440 }
441
442 // Load the local package, and resolve aliases
443 let pkg = await this.findPackage(dir);
444 return this.resolveAliases(filename, pkg);
445 }
446}
447
448module.exports = Resolver;