-
Notifications
You must be signed in to change notification settings - Fork 12.5k
/
moduleNameResolver.ts
1320 lines (1192 loc) · 72.8 KB
/
moduleNameResolver.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
namespace ts {
/* @internal */
export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void;
export function trace(host: ModuleResolutionHost): void {
host.trace!(formatMessage.apply(undefined, arguments));
}
/* @internal */
export function isTraceEnabled(compilerOptions: CompilerOptions, host: ModuleResolutionHost): boolean {
return !!compilerOptions.traceResolution && host.trace !== undefined;
}
/** Array that is only intended to be pushed to, never read. */
/* @internal */
export interface Push<T> {
push(value: T): void;
}
function withPackageId(packageId: PackageId | undefined, r: PathAndExtension | undefined): Resolved | undefined {
return r && { path: r.path, extension: r.ext, packageId };
}
function noPackageId(r: PathAndExtension | undefined): Resolved | undefined {
return withPackageId(/*packageId*/ undefined, r);
}
/** Result of trying to resolve a module. */
interface Resolved {
path: string;
extension: Extension;
packageId: PackageId | undefined;
/**
* When the resolved is not created from cache, the value is
* - string if original Path if it is symbolic link to the resolved path
* - undefined if path is not a symbolic link
* When the resolved is created using value from cache of ResolvedModuleWithFailedLookupLocations, the value is:
* - string if original Path if it is symbolic link to the resolved path
* - true if path is not a symbolic link - this indicates that the originalPath calculation is already done and needs to be skipped
*/
originalPath?: string | true;
}
/** Result of trying to resolve a module at a file. Needs to have 'packageId' added later. */
interface PathAndExtension {
path: string;
// (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.)
ext: Extension;
}
/**
* Kinds of file that we are currently looking for.
* Typically there is one pass with Extensions.TypeScript, then a second pass with Extensions.JavaScript.
*/
enum Extensions {
TypeScript, /** '.ts', '.tsx', or '.d.ts' */
JavaScript, /** '.js' or '.jsx' */
Json, /** '.json' */
DtsOnly /** Only '.d.ts' */
}
interface PathAndPackageId {
readonly fileName: string;
readonly packageId: PackageId | undefined;
}
/** Used with `Extensions.DtsOnly` to extract the path from TypeScript results. */
function resolvedTypeScriptOnly(resolved: Resolved | undefined): PathAndPackageId | undefined {
if (!resolved) {
return undefined;
}
Debug.assert(extensionIsTypeScript(resolved.extension));
return { fileName: resolved.path, packageId: resolved.packageId };
}
function createResolvedModuleWithFailedLookupLocations(resolved: Resolved | undefined, isExternalLibraryImport: boolean, failedLookupLocations: string[]): ResolvedModuleWithFailedLookupLocations {
return {
resolvedModule: resolved && { resolvedFileName: resolved.path, originalPath: resolved.originalPath === true ? undefined : resolved.originalPath, extension: resolved.extension, isExternalLibraryImport, packageId: resolved.packageId },
failedLookupLocations
};
}
interface ModuleResolutionState {
host: ModuleResolutionHost;
compilerOptions: CompilerOptions;
traceEnabled: boolean;
}
/** Just the fields that we use for module resolution. */
interface PackageJsonPathFields {
typings?: string;
types?: string;
main?: string;
}
interface PackageJson extends PackageJsonPathFields {
name?: string;
version?: string;
}
/** Reads from "main" or "types"/"typings" depending on `extensions`. */
function tryReadPackageJsonFields(readTypes: boolean, jsonContent: PackageJsonPathFields, baseDirectory: string, state: ModuleResolutionState): string | undefined {
return readTypes ? tryReadFromField("typings") || tryReadFromField("types") : tryReadFromField("main");
function tryReadFromField(fieldName: "typings" | "types" | "main"): string | undefined {
if (!hasProperty(jsonContent, fieldName)) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.package_json_does_not_have_a_0_field, fieldName);
}
return;
}
const fileName = jsonContent[fieldName];
if (!isString(fileName)) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, fieldName, typeof fileName);
}
return;
}
const path = normalizePath(combinePaths(baseDirectory, fileName));
if (state.traceEnabled) {
trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, fileName, path);
}
return path;
}
}
/* @internal */
export function readJson(path: string, host: { readFile(fileName: string): string | undefined }): object {
try {
const jsonText = host.readFile(path);
if (!jsonText) return {};
const result = parseConfigFileTextToJson(path, jsonText);
if (result.error) {
return {};
}
return result.config;
}
catch (e) {
// gracefully handle if readFile fails or returns not JSON
return {};
}
}
export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined {
if (options.typeRoots) {
return options.typeRoots;
}
let currentDirectory: string | undefined;
if (options.configFilePath) {
currentDirectory = getDirectoryPath(options.configFilePath);
}
else if (host.getCurrentDirectory) {
currentDirectory = host.getCurrentDirectory();
}
if (currentDirectory !== undefined) {
return getDefaultTypeRoots(currentDirectory, host);
}
}
/**
* Returns the path to every node_modules/@types directory from some ancestor directory.
* Returns undefined if there are none.
*/
function getDefaultTypeRoots(currentDirectory: string, host: { directoryExists?: (directoryName: string) => boolean }): string[] | undefined {
if (!host.directoryExists) {
return [combinePaths(currentDirectory, nodeModulesAtTypes)];
// And if it doesn't exist, tough.
}
let typeRoots: string[] | undefined;
forEachAncestorDirectory(normalizePath(currentDirectory), directory => {
const atTypes = combinePaths(directory, nodeModulesAtTypes);
if (host.directoryExists!(atTypes)) {
(typeRoots || (typeRoots = [])).push(atTypes);
}
return undefined;
});
return typeRoots;
}
const nodeModulesAtTypes = combinePaths("node_modules", "@types");
/**
* @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown.
* This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups
* is assumed to be the same as root directory of the project.
*/
export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost): ResolvedTypeReferenceDirectiveWithFailedLookupLocations {
const traceEnabled = isTraceEnabled(options, host);
const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled };
const typeRoots = getEffectiveTypeRoots(options, host);
if (traceEnabled) {
if (containingFile === undefined) {
if (typeRoots === undefined) {
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_not_set, typeReferenceDirectiveName);
}
else {
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_1, typeReferenceDirectiveName, typeRoots);
}
}
else {
if (typeRoots === undefined) {
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_not_set, typeReferenceDirectiveName, containingFile);
}
else {
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2, typeReferenceDirectiveName, containingFile, typeRoots);
}
}
}
const failedLookupLocations: string[] = [];
let resolved = primaryLookup();
let primary = true;
if (!resolved) {
resolved = secondaryLookup();
primary = false;
}
let resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective | undefined;
if (resolved) {
if (!options.preserveSymlinks) {
resolved = { ...resolved, fileName: realPath(resolved.fileName, host, traceEnabled) };
}
if (traceEnabled) {
trace(host, Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_primary_Colon_2, typeReferenceDirectiveName, resolved.fileName, primary);
}
resolvedTypeReferenceDirective = { primary, resolvedFileName: resolved.fileName, packageId: resolved.packageId };
}
return { resolvedTypeReferenceDirective, failedLookupLocations };
function primaryLookup(): PathAndPackageId | undefined {
// Check primary library paths
if (typeRoots && typeRoots.length) {
if (traceEnabled) {
trace(host, Diagnostics.Resolving_with_primary_search_path_0, typeRoots.join(", "));
}
return forEach(typeRoots, typeRoot => {
const candidate = combinePaths(typeRoot, typeReferenceDirectiveName);
const candidateDirectory = getDirectoryPath(candidate);
const directoryExists = directoryProbablyExists(candidateDirectory, host);
if (!directoryExists && traceEnabled) {
trace(host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidateDirectory);
}
return resolvedTypeScriptOnly(
loadNodeModuleFromDirectory(Extensions.DtsOnly, candidate, failedLookupLocations,
!directoryExists, moduleResolutionState));
});
}
else {
if (traceEnabled) {
trace(host, Diagnostics.Root_directory_cannot_be_determined_skipping_primary_search_paths);
}
}
}
function secondaryLookup(): PathAndPackageId | undefined {
const initialLocationForSecondaryLookup = containingFile && getDirectoryPath(containingFile);
if (initialLocationForSecondaryLookup !== undefined) {
// check secondary locations
if (traceEnabled) {
trace(host, Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup);
}
const result = loadModuleFromNodeModules(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, failedLookupLocations, moduleResolutionState, /*cache*/ undefined);
const resolvedFile = resolvedTypeScriptOnly(result && result.value);
if (!resolvedFile && traceEnabled) {
trace(host, Diagnostics.Type_reference_directive_0_was_not_resolved, typeReferenceDirectiveName);
}
return resolvedFile;
}
else {
if (traceEnabled) {
trace(host, Diagnostics.Containing_file_is_not_specified_and_root_directory_cannot_be_determined_skipping_lookup_in_node_modules_folder);
}
}
}
}
/**
* Given a set of options, returns the set of type directive names
* that should be included for this program automatically.
* This list could either come from the config file,
* or from enumerating the types root + initial secondary types lookup location.
* More type directives might appear in the program later as a result of loading actual source files;
* this list is only the set of defaults that are implicitly included.
*/
export function getAutomaticTypeDirectiveNames(options: CompilerOptions, host: ModuleResolutionHost): string[] {
// Use explicit type list from tsconfig.json
if (options.types) {
return options.types;
}
// Walk the primary type lookup locations
const result: string[] = [];
if (host.directoryExists && host.getDirectories) {
const typeRoots = getEffectiveTypeRoots(options, host);
if (typeRoots) {
for (const root of typeRoots) {
if (host.directoryExists(root)) {
for (const typeDirectivePath of host.getDirectories(root)) {
const normalized = normalizePath(typeDirectivePath);
const packageJsonPath = pathToPackageJson(combinePaths(root, normalized));
// `types-publisher` sometimes creates packages with `"typings": null` for packages that don't provide their own types.
// See `createNotNeededPackageJSON` in the types-publisher` repo.
// tslint:disable-next-line:no-null-keyword
const isNotNeededPackage = host.fileExists(packageJsonPath) && (readJson(packageJsonPath, host) as PackageJson).typings === null;
if (!isNotNeededPackage) {
// Return just the type directive names
result.push(getBaseFileName(normalized));
}
}
}
}
}
}
return result;
}
/**
* Cached module resolutions per containing directory.
* This assumes that any module id will have the same resolution for sibling files located in the same folder.
*/
export interface ModuleResolutionCache extends NonRelativeModuleNameResolutionCache {
getOrCreateCacheForDirectory(directoryName: string): Map<ResolvedModuleWithFailedLookupLocations>;
}
/**
* Stored map from non-relative module name to a table: directory -> result of module lookup in this directory
* We support only non-relative module names because resolution of relative module names is usually more deterministic and thus less expensive.
*/
export interface NonRelativeModuleNameResolutionCache {
getOrCreateCacheForModuleName(nonRelativeModuleName: string): PerModuleNameCache;
}
export interface PerModuleNameCache {
get(directory: string): ResolvedModuleWithFailedLookupLocations | undefined;
set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void;
}
export function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: (s: string) => string): ModuleResolutionCache {
return createModuleResolutionCacheWithMaps(
createMap<Map<ResolvedModuleWithFailedLookupLocations>>(),
createMap<PerModuleNameCache>(),
currentDirectory,
getCanonicalFileName
);
}
/*@internal*/
export function createModuleResolutionCacheWithMaps(
directoryToModuleNameMap: Map<Map<ResolvedModuleWithFailedLookupLocations>>,
moduleNameToDirectoryMap: Map<PerModuleNameCache>,
currentDirectory: string,
getCanonicalFileName: GetCanonicalFileName): ModuleResolutionCache {
return { getOrCreateCacheForDirectory, getOrCreateCacheForModuleName };
function getOrCreateCacheForDirectory(directoryName: string) {
const path = toPath(directoryName, currentDirectory, getCanonicalFileName);
let perFolderCache = directoryToModuleNameMap.get(path);
if (!perFolderCache) {
perFolderCache = createMap<ResolvedModuleWithFailedLookupLocations>();
directoryToModuleNameMap.set(path, perFolderCache);
}
return perFolderCache;
}
function getOrCreateCacheForModuleName(nonRelativeModuleName: string): PerModuleNameCache {
Debug.assert(!isExternalModuleNameRelative(nonRelativeModuleName));
let perModuleNameCache = moduleNameToDirectoryMap.get(nonRelativeModuleName);
if (!perModuleNameCache) {
perModuleNameCache = createPerModuleNameCache();
moduleNameToDirectoryMap.set(nonRelativeModuleName, perModuleNameCache);
}
return perModuleNameCache;
}
function createPerModuleNameCache(): PerModuleNameCache {
const directoryPathMap = createMap<ResolvedModuleWithFailedLookupLocations>();
return { get, set };
function get(directory: string): ResolvedModuleWithFailedLookupLocations | undefined {
return directoryPathMap.get(toPath(directory, currentDirectory, getCanonicalFileName));
}
/**
* At first this function add entry directory -> module resolution result to the table.
* Then it computes the set of parent folders for 'directory' that should have the same module resolution result
* and for every parent folder in set it adds entry: parent -> module resolution. .
* Lets say we first directory name: /a/b/c/d/e and resolution result is: /a/b/bar.ts.
* Set of parent folders that should have the same result will be:
* [
* /a/b/c/d, /a/b/c, /a/b
* ]
* this means that request for module resolution from file in any of these folder will be immediately found in cache.
*/
function set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void {
const path = toPath(directory, currentDirectory, getCanonicalFileName);
// if entry is already in cache do nothing
if (directoryPathMap.has(path)) {
return;
}
directoryPathMap.set(path, result);
const resolvedFileName = result.resolvedModule &&
(result.resolvedModule.originalPath || result.resolvedModule.resolvedFileName);
// find common prefix between directory and resolved file name
// this common prefix should be the shortest path that has the same resolution
// directory: /a/b/c/d/e
// resolvedFileName: /a/b/foo.d.ts
// commonPrefix: /a/b
// for failed lookups cache the result for every directory up to root
const commonPrefix = resolvedFileName && getCommonPrefix(path, resolvedFileName);
let current = path;
while (current !== commonPrefix) {
const parent = getDirectoryPath(current);
if (parent === current || directoryPathMap.has(parent)) {
break;
}
directoryPathMap.set(parent, result);
current = parent;
}
}
function getCommonPrefix(directory: Path, resolution: string) {
const resolutionDirectory = toPath(getDirectoryPath(resolution), currentDirectory, getCanonicalFileName);
// find first position where directory and resolution differs
let i = 0;
const limit = Math.min(directory.length, resolutionDirectory.length);
while (i < limit && directory.charCodeAt(i) === resolutionDirectory.charCodeAt(i)) {
i++;
}
if (i === directory.length && (resolutionDirectory.length === i || resolutionDirectory[i] === directorySeparator)) {
return directory;
}
const rootLength = getRootLength(directory);
if (i < rootLength) {
return undefined;
}
const sep = directory.lastIndexOf(directorySeparator, i - 1);
if (sep === -1) {
return undefined;
}
return directory.substr(0, Math.max(sep, rootLength));
}
}
}
export function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations | undefined {
const containingDirectory = getDirectoryPath(containingFile);
const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory);
return perFolderCache && perFolderCache.get(moduleName);
}
export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations {
const traceEnabled = isTraceEnabled(compilerOptions, host);
if (traceEnabled) {
trace(host, Diagnostics.Resolving_module_0_from_1, moduleName, containingFile);
}
const containingDirectory = getDirectoryPath(containingFile);
const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory);
let result = perFolderCache && perFolderCache.get(moduleName);
if (result) {
if (traceEnabled) {
trace(host, Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory);
}
}
else {
let moduleResolution = compilerOptions.moduleResolution;
if (moduleResolution === undefined) {
moduleResolution = getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic;
if (traceEnabled) {
trace(host, Diagnostics.Module_resolution_kind_is_not_specified_using_0, ModuleResolutionKind[moduleResolution]);
}
}
else {
if (traceEnabled) {
trace(host, Diagnostics.Explicitly_specified_module_resolution_kind_Colon_0, ModuleResolutionKind[moduleResolution]);
}
}
switch (moduleResolution) {
case ModuleResolutionKind.NodeJs:
result = nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache);
break;
case ModuleResolutionKind.Classic:
result = classicNameResolver(moduleName, containingFile, compilerOptions, host, cache);
break;
default:
return Debug.fail(`Unexpected moduleResolution: ${moduleResolution}`);
}
if (perFolderCache) {
perFolderCache.set(moduleName, result);
if (!isExternalModuleNameRelative(moduleName)) {
// put result in per-module name cache
cache!.getOrCreateCacheForModuleName(moduleName).set(containingDirectory, result);
}
}
}
if (traceEnabled) {
if (result.resolvedModule) {
trace(host, Diagnostics.Module_name_0_was_successfully_resolved_to_1, moduleName, result.resolvedModule.resolvedFileName);
}
else {
trace(host, Diagnostics.Module_name_0_was_not_resolved, moduleName);
}
}
return result;
}
/*
* Every module resolution kind can has its specific understanding how to load module from a specific path on disk
* I.e. for path '/a/b/c':
* - Node loader will first to try to check if '/a/b/c' points to a file with some supported extension and if this fails
* it will try to load module from directory: directory '/a/b/c' should exist and it should have either 'package.json' with
* 'typings' entry or file 'index' with some supported extension
* - Classic loader will only try to interpret '/a/b/c' as file.
*/
type ResolutionKindSpecificLoader = (extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState) => Resolved | undefined;
/**
* Any module resolution kind can be augmented with optional settings: 'baseUrl', 'paths' and 'rootDirs' - they are used to
* mitigate differences between design time structure of the project and its runtime counterpart so the same import name
* can be resolved successfully by TypeScript compiler and runtime module loader.
* If these settings are set then loading procedure will try to use them to resolve module name and it can of failure it will
* fallback to standard resolution routine.
*
* - baseUrl - this setting controls how non-relative module names are resolved. If this setting is specified then non-relative
* names will be resolved relative to baseUrl: i.e. if baseUrl is '/a/b' then candidate location to resolve module name 'c/d' will
* be '/a/b/c/d'
* - paths - this setting can only be used when baseUrl is specified. allows to tune how non-relative module names
* will be resolved based on the content of the module name.
* Structure of 'paths' compiler options
* 'paths': {
* pattern-1: [...substitutions],
* pattern-2: [...substitutions],
* ...
* pattern-n: [...substitutions]
* }
* Pattern here is a string that can contain zero or one '*' character. During module resolution module name will be matched against
* all patterns in the list. Matching for patterns that don't contain '*' means that module name must be equal to pattern respecting the case.
* If pattern contains '*' then to match pattern "<prefix>*<suffix>" module name must start with the <prefix> and end with <suffix>.
* <MatchedStar> denotes part of the module name between <prefix> and <suffix>.
* If module name can be matches with multiple patterns then pattern with the longest prefix will be picked.
* After selecting pattern we'll use list of substitutions to get candidate locations of the module and the try to load module
* from the candidate location.
* Substitution is a string that can contain zero or one '*'. To get candidate location from substitution we'll pick every
* substitution in the list and replace '*' with <MatchedStar> string. If candidate location is not rooted it
* will be converted to absolute using baseUrl.
* For example:
* baseUrl: /a/b/c
* "paths": {
* // match all module names
* "*": [
* "*", // use matched name as is,
* // <matched name> will be looked as /a/b/c/<matched name>
*
* "folder1/*" // substitution will convert matched name to 'folder1/<matched name>',
* // since it is not rooted then final candidate location will be /a/b/c/folder1/<matched name>
* ],
* // match module names that start with 'components/'
* "components/*": [ "/root/components/*" ] // substitution will convert /components/folder1/<matched name> to '/root/components/folder1/<matched name>',
* // it is rooted so it will be final candidate location
* }
*
* 'rootDirs' allows the project to be spreaded across multiple locations and resolve modules with relative names as if
* they were in the same location. For example lets say there are two files
* '/local/src/content/file1.ts'
* '/shared/components/contracts/src/content/protocols/file2.ts'
* After bundling content of '/shared/components/contracts/src' will be merged with '/local/src' so
* if file1 has the following import 'import {x} from "./protocols/file2"' it will be resolved successfully in runtime.
* 'rootDirs' provides the way to tell compiler that in order to get the whole project it should behave as if content of all
* root dirs were merged together.
* I.e. for the example above 'rootDirs' will have two entries: [ '/local/src', '/shared/components/contracts/src' ].
* Compiler will first convert './protocols/file2' into absolute path relative to the location of containing file:
* '/local/src/content/protocols/file2' and try to load it - failure.
* Then it will search 'rootDirs' looking for a longest matching prefix of this absolute path and if such prefix is found - absolute path will
* be converted to a path relative to found rootDir entry './content/protocols/file2' (*). As a last step compiler will check all remaining
* entries in 'rootDirs', use them to build absolute path out of (*) and try to resolve module from this location.
*/
function tryLoadModuleUsingOptionalResolutionSettings(extensions: Extensions, moduleName: string, containingDirectory: string, loader: ResolutionKindSpecificLoader,
failedLookupLocations: Push<string>, state: ModuleResolutionState): Resolved | undefined {
if (!isExternalModuleNameRelative(moduleName)) {
return tryLoadModuleUsingBaseUrl(extensions, moduleName, loader, failedLookupLocations, state);
}
else {
return tryLoadModuleUsingRootDirs(extensions, moduleName, containingDirectory, loader, failedLookupLocations, state);
}
}
function tryLoadModuleUsingRootDirs(extensions: Extensions, moduleName: string, containingDirectory: string, loader: ResolutionKindSpecificLoader,
failedLookupLocations: Push<string>, state: ModuleResolutionState): Resolved | undefined {
if (!state.compilerOptions.rootDirs) {
return undefined;
}
if (state.traceEnabled) {
trace(state.host, Diagnostics.rootDirs_option_is_set_using_it_to_resolve_relative_module_name_0, moduleName);
}
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
let matchedRootDir: string | undefined;
let matchedNormalizedPrefix: string | undefined;
for (const rootDir of state.compilerOptions.rootDirs) {
// rootDirs are expected to be absolute
// in case of tsconfig.json this will happen automatically - compiler will expand relative names
// using location of tsconfig.json as base location
let normalizedRoot = normalizePath(rootDir);
if (!endsWith(normalizedRoot, directorySeparator)) {
normalizedRoot += directorySeparator;
}
const isLongestMatchingPrefix =
startsWith(candidate, normalizedRoot) &&
(matchedNormalizedPrefix === undefined || matchedNormalizedPrefix.length < normalizedRoot.length);
if (state.traceEnabled) {
trace(state.host, Diagnostics.Checking_if_0_is_the_longest_matching_prefix_for_1_2, normalizedRoot, candidate, isLongestMatchingPrefix);
}
if (isLongestMatchingPrefix) {
matchedNormalizedPrefix = normalizedRoot;
matchedRootDir = rootDir;
}
}
if (matchedNormalizedPrefix) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Longest_matching_prefix_for_0_is_1, candidate, matchedNormalizedPrefix);
}
const suffix = candidate.substr(matchedNormalizedPrefix.length);
// first - try to load from a initial location
if (state.traceEnabled) {
trace(state.host, Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, matchedNormalizedPrefix, candidate);
}
const resolvedFileName = loader(extensions, candidate, failedLookupLocations, !directoryProbablyExists(containingDirectory, state.host), state);
if (resolvedFileName) {
return resolvedFileName;
}
if (state.traceEnabled) {
trace(state.host, Diagnostics.Trying_other_entries_in_rootDirs);
}
// then try to resolve using remaining entries in rootDirs
for (const rootDir of state.compilerOptions.rootDirs) {
if (rootDir === matchedRootDir) {
// skip the initially matched entry
continue;
}
const candidate = combinePaths(normalizePath(rootDir), suffix);
if (state.traceEnabled) {
trace(state.host, Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, rootDir, candidate);
}
const baseDirectory = getDirectoryPath(candidate);
const resolvedFileName = loader(extensions, candidate, failedLookupLocations, !directoryProbablyExists(baseDirectory, state.host), state);
if (resolvedFileName) {
return resolvedFileName;
}
}
if (state.traceEnabled) {
trace(state.host, Diagnostics.Module_resolution_using_rootDirs_has_failed);
}
}
return undefined;
}
function tryLoadModuleUsingBaseUrl(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, failedLookupLocations: Push<string>, state: ModuleResolutionState): Resolved | undefined {
if (!state.compilerOptions.baseUrl) {
return undefined;
}
if (state.traceEnabled) {
trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, state.compilerOptions.baseUrl, moduleName);
}
// string is for exact match
let matchedPattern: Pattern | string | undefined;
if (state.compilerOptions.paths) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName);
}
matchedPattern = matchPatternOrExact(getOwnKeys(state.compilerOptions.paths), moduleName);
}
if (matchedPattern) {
const matchedStar = isString(matchedPattern) ? undefined : matchedText(matchedPattern, moduleName);
const matchedPatternText = isString(matchedPattern) ? matchedPattern : patternText(matchedPattern);
if (state.traceEnabled) {
trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText);
}
return forEach(state.compilerOptions.paths![matchedPatternText], subst => {
const path = matchedStar ? subst.replace("*", matchedStar) : subst;
const candidate = normalizePath(combinePaths(state.compilerOptions.baseUrl!, path));
if (state.traceEnabled) {
trace(state.host, Diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path);
}
// A path mapping may have an extension, in contrast to an import, which should omit it.
const extension = tryGetExtensionFromPath(candidate);
if (extension !== undefined) {
const path = tryFile(candidate, failedLookupLocations, /*onlyRecordFailures*/ false, state);
if (path !== undefined) {
return noPackageId({ path, ext: extension });
}
}
return loader(extensions, candidate, failedLookupLocations, !directoryProbablyExists(getDirectoryPath(candidate), state.host), state);
});
}
else {
const candidate = normalizePath(combinePaths(state.compilerOptions.baseUrl, moduleName));
if (state.traceEnabled) {
trace(state.host, Diagnostics.Resolving_module_name_0_relative_to_base_url_1_2, moduleName, state.compilerOptions.baseUrl, candidate);
}
return loader(extensions, candidate, failedLookupLocations, !directoryProbablyExists(getDirectoryPath(candidate), state.host), state);
}
}
export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations {
return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, /*jsOnly*/ false);
}
/**
* Expose resolution logic to allow us to use Node module resolution logic from arbitrary locations.
* No way to do this with `require()`: https://github.com/nodejs/node/issues/5963
* Throws an error if the module can't be resolved.
*/
/* @internal */
export function resolveJavaScriptModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string {
const { resolvedModule, failedLookupLocations } =
nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, /*jsOnly*/ true);
if (!resolvedModule) {
throw new Error(`Could not resolve JS module '${moduleName}' starting at '${initialDir}'. Looked in: ${failedLookupLocations.join(", ")}`);
}
return resolvedModule.resolvedFileName;
}
function nodeModuleNameResolverWorker(moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, jsOnly: boolean): ResolvedModuleWithFailedLookupLocations {
const traceEnabled = isTraceEnabled(compilerOptions, host);
const failedLookupLocations: string[] = [];
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled };
const result = jsOnly ?
tryResolve(Extensions.JavaScript) :
(tryResolve(Extensions.TypeScript) ||
tryResolve(Extensions.JavaScript) ||
(compilerOptions.resolveJsonModule ? tryResolve(Extensions.Json) : undefined));
if (result && result.value) {
const { resolved, isExternalLibraryImport } = result.value;
return createResolvedModuleWithFailedLookupLocations(resolved, isExternalLibraryImport, failedLookupLocations);
}
return { resolvedModule: undefined, failedLookupLocations };
function tryResolve(extensions: Extensions): SearchResult<{ resolved: Resolved, isExternalLibraryImport: boolean }> {
const loader: ResolutionKindSpecificLoader = (extensions, candidate, failedLookupLocations, onlyRecordFailures, state) => nodeLoadModuleByRelativeName(extensions, candidate, failedLookupLocations, onlyRecordFailures, state, /*considerPackageJson*/ true);
const resolved = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loader, failedLookupLocations, state);
if (resolved) {
return toSearchResult({ resolved, isExternalLibraryImport: stringContains(resolved.path, nodeModulesPathPart) });
}
if (!isExternalModuleNameRelative(moduleName)) {
if (traceEnabled) {
trace(host, Diagnostics.Loading_module_0_from_node_modules_folder_target_file_type_1, moduleName, Extensions[extensions]);
}
const resolved = loadModuleFromNodeModules(extensions, moduleName, containingDirectory, failedLookupLocations, state, cache);
if (!resolved) return undefined;
let resolvedValue = resolved.value;
if (!compilerOptions.preserveSymlinks && resolvedValue && !resolvedValue.originalPath) {
const path = realPath(resolvedValue.path, host, traceEnabled);
const originalPath = path === resolvedValue.path ? undefined : resolvedValue.path;
resolvedValue = { ...resolvedValue, path, originalPath };
}
// For node_modules lookups, get the real path so that multiple accesses to an `npm link`-ed module do not create duplicate files.
return { value: resolvedValue && { resolved: resolvedValue, isExternalLibraryImport: true } };
}
else {
const { path: candidate, parts } = normalizePathAndParts(combinePaths(containingDirectory, moduleName));
const resolved = nodeLoadModuleByRelativeName(extensions, candidate, failedLookupLocations, /*onlyRecordFailures*/ false, state, /*considerPackageJson*/ true);
// Treat explicit "node_modules" import as an external library import.
return resolved && toSearchResult({ resolved, isExternalLibraryImport: contains(parts, "node_modules") });
}
}
}
function realPath(path: string, host: ModuleResolutionHost, traceEnabled: boolean): string {
if (!host.realpath) {
return path;
}
const real = normalizePath(host.realpath(path));
if (traceEnabled) {
trace(host, Diagnostics.Resolving_real_path_for_0_result_1, path, real);
}
Debug.assert(host.fileExists(real), `${path} linked to nonexistent file ${real}`); // tslint:disable-line
return real;
}
function nodeLoadModuleByRelativeName(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson: boolean): Resolved | undefined {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_type_1, candidate, Extensions[extensions]);
}
if (!hasTrailingDirectorySeparator(candidate)) {
if (!onlyRecordFailures) {
const parentOfCandidate = getDirectoryPath(candidate);
if (!directoryProbablyExists(parentOfCandidate, state.host)) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, parentOfCandidate);
}
onlyRecordFailures = true;
}
}
const resolvedFromFile = loadModuleFromFile(extensions, candidate, failedLookupLocations, onlyRecordFailures, state);
if (resolvedFromFile) {
const nm = considerPackageJson ? parseNodeModuleFromPath(resolvedFromFile) : undefined;
const packageId = nm && getPackageJsonInfo(nm.packageDirectory, nm.subModuleName, failedLookupLocations, /*onlyRecordFailures*/ false, state).packageId;
return withPackageId(packageId, resolvedFromFile);
}
}
if (!onlyRecordFailures) {
const candidateExists = directoryProbablyExists(candidate, state.host);
if (!candidateExists) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidate);
}
onlyRecordFailures = true;
}
}
return loadNodeModuleFromDirectory(extensions, candidate, failedLookupLocations, onlyRecordFailures, state, considerPackageJson);
}
/*@internal*/
export const nodeModulesPathPart = "/node_modules/";
/**
* This will be called on the successfully resolved path from `loadModuleFromFile`.
* (Not neeeded for `loadModuleFromNodeModules` as that looks up the `package.json` as part of resolution.)
*
* packageDirectory is the directory of the package itself.
* subModuleName is the path within the package.
* For `blah/node_modules/foo/index.d.ts` this is { packageDirectory: "foo", subModuleName: "index.d.ts" }. (Part before "/node_modules/" is ignored.)
* For `/node_modules/foo/bar.d.ts` this is { packageDirectory: "foo", subModuleName": "bar/index.d.ts" }.
* For `/node_modules/@types/foo/bar/index.d.ts` this is { packageDirectory: "@types/foo", subModuleName: "bar/index.d.ts" }.
* For `/node_modules/foo/bar/index.d.ts` this is { packageDirectory: "foo", subModuleName": "bar/index.d.ts" }.
*/
function parseNodeModuleFromPath(resolved: PathAndExtension): { packageDirectory: string, subModuleName: string } | undefined {
const path = normalizePath(resolved.path);
const idx = path.lastIndexOf(nodeModulesPathPart);
if (idx === -1) {
return undefined;
}
const indexAfterNodeModules = idx + nodeModulesPathPart.length;
let indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterNodeModules);
if (path.charCodeAt(indexAfterNodeModules) === CharacterCodes.at) {
indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterPackageName);
}
const packageDirectory = path.slice(0, indexAfterPackageName);
const subModuleName = removeExtension(path.slice(indexAfterPackageName + 1), resolved.ext) + Extension.Dts;
return { packageDirectory, subModuleName };
}
function moveToNextDirectorySeparatorIfAvailable(path: string, prevSeparatorIndex: number): number {
const nextSeparatorIndex = path.indexOf(directorySeparator, prevSeparatorIndex + 1);
return nextSeparatorIndex === -1 ? prevSeparatorIndex : nextSeparatorIndex;
}
function addExtensionAndIndex(path: string): string {
if (path === "") {
return "index.d.ts";
}
if (endsWith(path, ".d.ts")) {
return path;
}
if (endsWith(path, "/index")) {
return path + ".d.ts";
}
return path + "/index.d.ts";
}
/* @internal */
export function directoryProbablyExists(directoryName: string, host: { directoryExists?: (directoryName: string) => boolean }): boolean {
// if host does not support 'directoryExists' assume that directory will exist
return !host.directoryExists || host.directoryExists(directoryName);
}
function loadModuleFromFileNoPackageId(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined {
return noPackageId(loadModuleFromFile(extensions, candidate, failedLookupLocations, onlyRecordFailures, state));
}
/**
* @param {boolean} onlyRecordFailures - if true then function won't try to actually load files but instead record all attempts as failures. This flag is necessary
* in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations.
*/
function loadModuleFromFile(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
if (extensions === Extensions.Json) {
const extensionLess = tryRemoveExtension(candidate, Extension.Json);
return extensionLess === undefined ? undefined : tryAddingExtensions(extensionLess, extensions, failedLookupLocations, onlyRecordFailures, state);
}
// First, try adding an extension. An import of "foo" could be matched by a file "foo.ts", or "foo.js" by "foo.js.ts"
const resolvedByAddingExtension = tryAddingExtensions(candidate, extensions, failedLookupLocations, onlyRecordFailures, state);
if (resolvedByAddingExtension) {
return resolvedByAddingExtension;
}
// If that didn't work, try stripping a ".js" or ".jsx" extension and replacing it with a TypeScript one;
// e.g. "./foo.js" can be matched by "./foo.ts" or "./foo.d.ts"
if (hasJavaScriptFileExtension(candidate)) {
const extensionless = removeFileExtension(candidate);
if (state.traceEnabled) {
const extension = candidate.substring(extensionless.length);
trace(state.host, Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension);
}
return tryAddingExtensions(extensionless, extensions, failedLookupLocations, onlyRecordFailures, state);
}
}
/** Try to return an existing file that adds one of the `extensions` to `candidate`. */
function tryAddingExtensions(candidate: string, extensions: Extensions, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
if (!onlyRecordFailures) {
// check if containing folder exists - if it doesn't then just record failures for all supported extensions without disk probing
const directory = getDirectoryPath(candidate);
if (directory) {
onlyRecordFailures = !directoryProbablyExists(directory, state.host);
}
}
switch (extensions) {
case Extensions.DtsOnly:
return tryExtension(Extension.Dts);
case Extensions.TypeScript:
return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || tryExtension(Extension.Dts);
case Extensions.JavaScript:
return tryExtension(Extension.Js) || tryExtension(Extension.Jsx);
case Extensions.Json:
return tryExtension(Extension.Json);
}
function tryExtension(ext: Extension): PathAndExtension | undefined {
const path = tryFile(candidate + ext, failedLookupLocations, onlyRecordFailures, state);
return path === undefined ? undefined : { path, ext };
}
}
/** Return the file if it exists. */
function tryFile(fileName: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined {
if (!onlyRecordFailures) {
if (state.host.fileExists(fileName)) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.File_0_exist_use_it_as_a_name_resolution_result, fileName);
}
return fileName;
}
else {
if (state.traceEnabled) {
trace(state.host, Diagnostics.File_0_does_not_exist, fileName);
}
}
}
failedLookupLocations.push(fileName);
return undefined;
}
function loadNodeModuleFromDirectory(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson = true) {
const { packageJsonContent, packageId } = considerPackageJson
? getPackageJsonInfo(candidate, "", failedLookupLocations, onlyRecordFailures, state)
: { packageJsonContent: undefined, packageId: undefined };
return withPackageId(packageId, loadNodeModuleFromDirectoryWorker(extensions, candidate, failedLookupLocations, onlyRecordFailures, state, packageJsonContent));
}
function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState, packageJsonContent: PackageJsonPathFields | undefined): PathAndExtension | undefined {
const fromPackageJson = packageJsonContent && loadModuleFromPackageJson(packageJsonContent, extensions, candidate, failedLookupLocations, state);
if (fromPackageJson) {
return fromPackageJson;
}
const directoryExists = !onlyRecordFailures && directoryProbablyExists(candidate, state.host);
return loadModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocations, !directoryExists, state);
}
function getPackageJsonInfo(
nodeModuleDirectory: string,
subModuleName: string,
failedLookupLocations: Push<string>,
onlyRecordFailures: boolean,
state: ModuleResolutionState,