1 |
|
2 | import type {
|
3 | PluginOptions,
|
4 | PackageJSON,
|
5 | FilePath,
|
6 | ResolveResult,
|
7 | Environment,
|
8 | } from '@parcel/types';
|
9 | import path from 'path';
|
10 | import {isGlob} from '@parcel/utils';
|
11 | import micromatch from 'micromatch';
|
12 | import builtins from './builtins';
|
13 | import nullthrows from 'nullthrows';
|
14 |
|
15 | const EMPTY_SHIM = require.resolve('./_empty');
|
16 |
|
17 | type InternalPackageJSON = PackageJSON & {pkgdir: string, ...};
|
18 | type Options = {|
|
19 | options: PluginOptions,
|
20 | extensions: Array<string>,
|
21 | mainFields: Array<string>,
|
22 | |};
|
23 |
|
24 | type Aliases =
|
25 | | string
|
26 | | {[string]: string, ...}
|
27 | | {[string]: string | boolean, ...};
|
28 | type Module = {|
|
29 | moduleName?: string,
|
30 | subPath?: string,
|
31 | moduleDir?: FilePath,
|
32 | filePath?: FilePath,
|
33 | |};
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | export default class NodeResolver {
|
49 | options: PluginOptions;
|
50 | extensions: Array<string>;
|
51 | mainFields: Array<string>;
|
52 | packageCache: Map<string, InternalPackageJSON>;
|
53 | rootPackage: InternalPackageJSON | null;
|
54 |
|
55 | constructor(opts: Options) {
|
56 | this.extensions = opts.extensions.map(ext =>
|
57 | ext.startsWith('.') ? ext : '.' + ext,
|
58 | );
|
59 | this.mainFields = opts.mainFields;
|
60 | this.options = opts.options;
|
61 | this.packageCache = new Map();
|
62 | this.rootPackage = null;
|
63 | }
|
64 |
|
65 | async resolve({
|
66 | filename,
|
67 | parent,
|
68 | isURL,
|
69 | env,
|
70 | }: {|
|
71 | filename: FilePath,
|
72 | parent: ?FilePath,
|
73 | isURL: boolean,
|
74 | env: Environment,
|
75 | |}): Promise<?ResolveResult> {
|
76 |
|
77 | let extensions = this.extensions.slice();
|
78 |
|
79 | if (parent) {
|
80 |
|
81 | let parentExt = path.extname(parent);
|
82 | extensions = [parentExt, ...extensions.filter(ext => ext !== parentExt)];
|
83 | }
|
84 |
|
85 | extensions.unshift('');
|
86 |
|
87 |
|
88 | let module = await this.resolveModule({filename, parent, isURL, env});
|
89 | if (!module) {
|
90 | return {isExcluded: true};
|
91 | }
|
92 |
|
93 | let resolved;
|
94 | if (module.moduleDir) {
|
95 | resolved = await this.loadNodeModules(module, extensions, env);
|
96 | } else if (module.filePath) {
|
97 | resolved = await this.loadRelative(module.filePath, extensions, env);
|
98 | }
|
99 |
|
100 | if (resolved) {
|
101 | return {
|
102 | filePath: resolved.path,
|
103 | sideEffects:
|
104 | resolved.pkg && !this.hasSideEffects(resolved.path, resolved.pkg)
|
105 | ? false
|
106 | : undefined,
|
107 | };
|
108 | }
|
109 |
|
110 | return null;
|
111 | }
|
112 |
|
113 | async resolveModule({
|
114 | filename,
|
115 | parent,
|
116 | isURL,
|
117 | env,
|
118 | }: {|
|
119 | filename: string,
|
120 | parent: ?FilePath,
|
121 | isURL: boolean,
|
122 | env: Environment,
|
123 | |}): Promise<?Module> {
|
124 | let dir = parent ? path.dirname(parent) : this.options.inputFS.cwd();
|
125 |
|
126 |
|
127 | if (parent) {
|
128 | filename = await this.resolveFilename(filename, dir, isURL);
|
129 | }
|
130 |
|
131 |
|
132 | filename = await this.loadAlias(filename, dir, env);
|
133 |
|
134 |
|
135 | if (path.isAbsolute(filename)) {
|
136 | return {
|
137 | filePath: filename,
|
138 | };
|
139 | }
|
140 |
|
141 | if (!this.shouldIncludeNodeModule(env, filename)) {
|
142 | return null;
|
143 | }
|
144 |
|
145 | let builtin = this.findBuiltin(filename, env);
|
146 | if (builtin || builtin === null) {
|
147 | return builtin;
|
148 | }
|
149 |
|
150 |
|
151 | let resolved;
|
152 | try {
|
153 | resolved = await this.findNodeModulePath(filename, dir);
|
154 | } catch (err) {
|
155 |
|
156 | }
|
157 |
|
158 |
|
159 | if (resolved === undefined) {
|
160 | let parts = this.getModuleParts(filename);
|
161 | resolved = {
|
162 | moduleName: parts[0],
|
163 | subPath: parts[1],
|
164 | };
|
165 | }
|
166 |
|
167 | return resolved;
|
168 | }
|
169 |
|
170 | shouldIncludeNodeModule({includeNodeModules}: Environment, name: string) {
|
171 | if (includeNodeModules === false) {
|
172 | return false;
|
173 | }
|
174 |
|
175 | if (Array.isArray(includeNodeModules)) {
|
176 | let parts = this.getModuleParts(name);
|
177 | return includeNodeModules.includes(parts[0]);
|
178 | }
|
179 |
|
180 | if (includeNodeModules && typeof includeNodeModules === 'object') {
|
181 | let parts = this.getModuleParts(name);
|
182 | let include = includeNodeModules[parts[0]];
|
183 | if (include != null) {
|
184 | return !!include;
|
185 | }
|
186 | }
|
187 |
|
188 | return true;
|
189 | }
|
190 |
|
191 | async resolveFilename(filename: string, dir: string, isURL: ?boolean) {
|
192 | switch (filename[0]) {
|
193 | case '/': {
|
194 |
|
195 | return path.resolve(this.options.rootDir, filename.slice(1));
|
196 | }
|
197 |
|
198 | case '~': {
|
199 |
|
200 |
|
201 | const insideNodeModules = dir.includes('node_modules');
|
202 |
|
203 | while (
|
204 | dir !== this.options.projectRoot &&
|
205 | path.basename(path.dirname(dir)) !== 'node_modules' &&
|
206 | (insideNodeModules ||
|
207 | !(await this.options.inputFS.exists(
|
208 | path.join(dir, 'package.json'),
|
209 | )))
|
210 | ) {
|
211 | dir = path.dirname(dir);
|
212 |
|
213 | if (dir === path.dirname(dir)) {
|
214 | dir = this.options.rootDir;
|
215 | break;
|
216 | }
|
217 | }
|
218 |
|
219 | return path.join(dir, filename.slice(1));
|
220 | }
|
221 |
|
222 | case '.': {
|
223 |
|
224 | return path.resolve(dir, filename);
|
225 | }
|
226 |
|
227 | default: {
|
228 | if (isURL) {
|
229 | return path.resolve(dir, filename);
|
230 | }
|
231 |
|
232 |
|
233 | return filename;
|
234 | }
|
235 | }
|
236 | }
|
237 |
|
238 | async loadRelative(
|
239 | filename: string,
|
240 | extensions: Array<string>,
|
241 | env: Environment,
|
242 | ) {
|
243 |
|
244 | let pkg = await this.findPackage(path.dirname(filename));
|
245 |
|
246 |
|
247 | return (
|
248 | (await this.loadAsFile({file: filename, extensions, env, pkg})) ||
|
249 | (await this.loadDirectory({dir: filename, extensions, env, pkg}))
|
250 | );
|
251 | }
|
252 | findBuiltin(filename: string, env: Environment) {
|
253 | if (builtins[filename]) {
|
254 | if (env.isNode()) {
|
255 | return null;
|
256 | }
|
257 |
|
258 | return {filePath: builtins[filename]};
|
259 | }
|
260 | }
|
261 |
|
262 | async findNodeModulePath(filename: string, dir: string) {
|
263 | let parts = this.getModuleParts(filename);
|
264 | let root = path.parse(dir).root;
|
265 |
|
266 | while (dir !== root) {
|
267 |
|
268 | if (path.basename(dir) === 'node_modules') {
|
269 | dir = path.dirname(dir);
|
270 | }
|
271 |
|
272 | try {
|
273 |
|
274 | let moduleDir = path.join(dir, 'node_modules', parts[0]);
|
275 | let stats = await this.options.inputFS.stat(moduleDir);
|
276 | if (stats.isDirectory()) {
|
277 | return {
|
278 | moduleName: parts[0],
|
279 | subPath: parts[1],
|
280 | moduleDir: moduleDir,
|
281 | filePath: path.join(dir, 'node_modules', filename),
|
282 | };
|
283 | }
|
284 | } catch (err) {
|
285 |
|
286 | }
|
287 |
|
288 |
|
289 | dir = path.dirname(dir);
|
290 | }
|
291 |
|
292 | return undefined;
|
293 | }
|
294 |
|
295 | async loadNodeModules(
|
296 | module: Module,
|
297 | extensions: Array<string>,
|
298 | env: Environment,
|
299 | ) {
|
300 | try {
|
301 |
|
302 |
|
303 | if (module.subPath && module.moduleDir) {
|
304 | let pkg = await this.readPackage(module.moduleDir);
|
305 | let res = await this.loadAsFile({
|
306 | file: nullthrows(module.filePath),
|
307 | extensions,
|
308 | env,
|
309 | pkg,
|
310 | });
|
311 | if (res) {
|
312 | return res;
|
313 | }
|
314 | }
|
315 |
|
316 |
|
317 | return await this.loadDirectory({
|
318 | dir: nullthrows(module.filePath),
|
319 | extensions,
|
320 | env,
|
321 | });
|
322 | } catch (e) {
|
323 |
|
324 | }
|
325 | }
|
326 |
|
327 | async isFile(file: FilePath) {
|
328 | try {
|
329 | let stat = await this.options.inputFS.stat(file);
|
330 | return stat.isFile() || stat.isFIFO();
|
331 | } catch (err) {
|
332 | return false;
|
333 | }
|
334 | }
|
335 |
|
336 | async loadDirectory({
|
337 | dir,
|
338 | extensions,
|
339 | env,
|
340 | pkg,
|
341 | }: {|
|
342 | dir: string,
|
343 | extensions: Array<string>,
|
344 | env: Environment,
|
345 | pkg?: InternalPackageJSON | null,
|
346 | |}) {
|
347 | try {
|
348 | pkg = await this.readPackage(dir);
|
349 |
|
350 |
|
351 | let entries = this.getPackageEntries(pkg, env);
|
352 |
|
353 | for (let file of entries) {
|
354 |
|
355 | const res =
|
356 | (await this.loadAsFile({file, extensions, env, pkg})) ||
|
357 | (await this.loadDirectory({dir: file, extensions, env, pkg}));
|
358 | if (res) {
|
359 | return res;
|
360 | }
|
361 | }
|
362 | } catch (err) {
|
363 |
|
364 | }
|
365 |
|
366 |
|
367 | return this.loadAsFile({
|
368 | file: path.join(dir, 'index'),
|
369 | extensions,
|
370 | env,
|
371 | pkg: pkg || null,
|
372 | });
|
373 | }
|
374 |
|
375 | async readPackage(dir: string): Promise<InternalPackageJSON> {
|
376 | let file = path.join(dir, 'package.json');
|
377 | let cached = this.packageCache.get(file);
|
378 |
|
379 | if (cached) {
|
380 | return cached;
|
381 | }
|
382 |
|
383 | let json = await this.options.inputFS.readFile(file, 'utf8');
|
384 | let pkg = JSON.parse(json);
|
385 |
|
386 | pkg.pkgfile = file;
|
387 | pkg.pkgdir = dir;
|
388 |
|
389 |
|
390 |
|
391 | if (pkg.source) {
|
392 | let realpath = await this.options.inputFS.realpath(file);
|
393 | if (realpath === file) {
|
394 | delete pkg.source;
|
395 | }
|
396 | }
|
397 |
|
398 | this.packageCache.set(file, pkg);
|
399 | return pkg;
|
400 | }
|
401 |
|
402 | getPackageEntries(pkg: InternalPackageJSON, env: Environment): Array<string> {
|
403 | return this.mainFields
|
404 | .map(field => {
|
405 | if (field === 'browser' && pkg.browser != null) {
|
406 | if (!env.isBrowser()) {
|
407 | return null;
|
408 | } else if (typeof pkg.browser === 'string') {
|
409 | return pkg.browser;
|
410 | } else if (typeof pkg.browser === 'object' && pkg.browser[pkg.name]) {
|
411 | return pkg.browser[pkg.name];
|
412 | }
|
413 | }
|
414 |
|
415 | return pkg[field];
|
416 | })
|
417 | .filter(entry => typeof entry === 'string')
|
418 | .map(main => {
|
419 |
|
420 | if (!main || main === '.' || main === './') {
|
421 | main = 'index';
|
422 | }
|
423 |
|
424 | if (typeof main !== 'string') {
|
425 | throw new Error('invariant: expected string');
|
426 | }
|
427 |
|
428 | return path.resolve(pkg.pkgdir, main);
|
429 | });
|
430 | }
|
431 |
|
432 | async loadAsFile({
|
433 | file,
|
434 | extensions,
|
435 | env,
|
436 | pkg,
|
437 | }: {|
|
438 | file: string,
|
439 | extensions: Array<string>,
|
440 | env: Environment,
|
441 | pkg: InternalPackageJSON | null,
|
442 | |}) {
|
443 |
|
444 | for (let f of await this.expandFile(file, extensions, env, pkg)) {
|
445 | if (await this.isFile(f)) {
|
446 | return {path: f, pkg};
|
447 | }
|
448 | }
|
449 | }
|
450 |
|
451 | async expandFile(
|
452 | file: string,
|
453 | extensions: Array<string>,
|
454 | env: Environment,
|
455 | pkg: InternalPackageJSON | null,
|
456 | expandAliases?: boolean = true,
|
457 | ): Promise<Array<string>> {
|
458 |
|
459 | let res = [];
|
460 | for (let ext of extensions) {
|
461 | let f = file + ext;
|
462 |
|
463 | if (expandAliases) {
|
464 | let alias = await this.resolveAliases(file + ext, env, pkg);
|
465 | if (alias !== f) {
|
466 | res = res.concat(
|
467 | await this.expandFile(alias, extensions, env, pkg, false),
|
468 | );
|
469 | }
|
470 | }
|
471 |
|
472 | res.push(f);
|
473 | }
|
474 |
|
475 | return res;
|
476 | }
|
477 |
|
478 | async resolveAliases(
|
479 | filename: string,
|
480 | env: Environment,
|
481 | pkg: InternalPackageJSON | null,
|
482 | ) {
|
483 |
|
484 | return this.resolvePackageAliases(
|
485 | await this.resolvePackageAliases(filename, env, pkg),
|
486 | env,
|
487 | this.rootPackage,
|
488 | );
|
489 | }
|
490 |
|
491 | async resolvePackageAliases(
|
492 | filename: string,
|
493 | env: Environment,
|
494 | pkg: InternalPackageJSON | null,
|
495 | ) {
|
496 | if (!pkg) {
|
497 | return filename;
|
498 | }
|
499 |
|
500 |
|
501 | return (
|
502 | (await this.getAlias(filename, pkg.pkgdir, pkg.source)) ||
|
503 | (await this.getAlias(filename, pkg.pkgdir, pkg.alias)) ||
|
504 | (env.isBrowser() &&
|
505 | (await this.getAlias(filename, pkg.pkgdir, pkg.browser))) ||
|
506 | filename
|
507 | );
|
508 | }
|
509 |
|
510 | async getAlias(filename: FilePath, dir: FilePath, aliases: ?Aliases) {
|
511 | if (!filename || !aliases || typeof aliases !== 'object') {
|
512 | return null;
|
513 | }
|
514 |
|
515 | let alias;
|
516 |
|
517 |
|
518 | if (path.isAbsolute(filename)) {
|
519 | filename = path.relative(dir, filename);
|
520 | if (filename[0] !== '.') {
|
521 | filename = './' + filename;
|
522 | }
|
523 |
|
524 | alias = await this.lookupAlias(aliases, filename, dir);
|
525 | } else {
|
526 |
|
527 | alias = await this.lookupAlias(aliases, filename, dir);
|
528 | if (alias == null) {
|
529 |
|
530 | let parts = this.getModuleParts(filename);
|
531 | alias = await this.lookupAlias(aliases, parts[0], dir);
|
532 | if (typeof alias === 'string') {
|
533 |
|
534 | alias = path.join(alias, ...parts.slice(1));
|
535 | }
|
536 | }
|
537 | }
|
538 |
|
539 |
|
540 | if (alias === false) {
|
541 | return EMPTY_SHIM;
|
542 | }
|
543 |
|
544 | return alias;
|
545 | }
|
546 |
|
547 | lookupAlias(aliases: Aliases, filename: FilePath, dir: FilePath) {
|
548 | if (typeof aliases !== 'object') {
|
549 | return null;
|
550 | }
|
551 |
|
552 |
|
553 | let alias = aliases[filename];
|
554 | if (alias == null) {
|
555 |
|
556 | for (let key in aliases) {
|
557 | let val = aliases[key];
|
558 | if (typeof val === 'string' && isGlob(key)) {
|
559 | let re = micromatch.makeRe(key, {capture: true});
|
560 | if (re.test(filename)) {
|
561 | alias = filename.replace(re, val);
|
562 | break;
|
563 | }
|
564 | }
|
565 | }
|
566 | }
|
567 |
|
568 | if (typeof alias === 'string') {
|
569 | return this.resolveFilename(alias, dir);
|
570 | } else if (alias === false) {
|
571 | return false;
|
572 | }
|
573 |
|
574 | return null;
|
575 | }
|
576 |
|
577 | async findPackage(dir: string) {
|
578 |
|
579 | let root = path.parse(dir).root;
|
580 | while (dir !== root && path.basename(dir) !== 'node_modules') {
|
581 | try {
|
582 | return await this.readPackage(dir);
|
583 | } catch (err) {
|
584 |
|
585 | }
|
586 |
|
587 | dir = path.dirname(dir);
|
588 | }
|
589 |
|
590 | return null;
|
591 | }
|
592 |
|
593 | async loadAlias(filename: string, dir: string, env: Environment) {
|
594 |
|
595 | if (!this.rootPackage) {
|
596 | this.rootPackage = await this.findPackage(this.options.rootDir);
|
597 | }
|
598 |
|
599 |
|
600 | let pkg = await this.findPackage(dir);
|
601 | return this.resolveAliases(filename, env, pkg);
|
602 | }
|
603 |
|
604 | getModuleParts(name: string) {
|
605 | let parts = path.normalize(name).split(path.sep);
|
606 | if (parts[0].charAt(0) === '@') {
|
607 |
|
608 | parts.splice(0, 2, `${parts[0]}/${parts[1]}`);
|
609 | }
|
610 |
|
611 | return parts;
|
612 | }
|
613 |
|
614 | hasSideEffects(filePath: FilePath, pkg: InternalPackageJSON) {
|
615 | switch (typeof pkg.sideEffects) {
|
616 | case 'boolean':
|
617 | return pkg.sideEffects;
|
618 | case 'string':
|
619 | return micromatch.isMatch(
|
620 | path.relative(pkg.pkgdir, filePath),
|
621 | pkg.sideEffects,
|
622 | {matchBase: true},
|
623 | );
|
624 | case 'object':
|
625 | return pkg.sideEffects.some(sideEffects =>
|
626 | this.hasSideEffects(filePath, {...pkg, sideEffects}),
|
627 | );
|
628 | }
|
629 |
|
630 | return true;
|
631 | }
|
632 | }
|