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