UNPKG

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