1
- import { relative , basename , extname , resolve , dirname , join } from 'path'
1
+ import { relative , basename , extname , resolve , dirname , join , isAbsolute } from 'path'
2
2
import sourceMapSupport = require( 'source-map-support' )
3
3
import * as ynModule from 'yn'
4
4
import { BaseError } from 'make-error'
5
5
import * as util from 'util'
6
6
import { fileURLToPath } from 'url'
7
- import * as _ts from 'typescript'
7
+ import type * as _ts from 'typescript'
8
8
import * as Module from 'module'
9
9
10
10
/**
@@ -95,6 +95,18 @@ export interface TSCommon {
95
95
formatDiagnosticsWithColorAndContext : typeof _ts . formatDiagnosticsWithColorAndContext
96
96
}
97
97
98
+ /**
99
+ * Compiler APIs we use that are marked internal and not included in TypeScript's public API declarations
100
+ */
101
+ interface TSInternal {
102
+ // https://github.com/microsoft/TypeScript/blob/4a34294908bed6701dcba2456ca7ac5eafe0ddff/src/compiler/core.ts#L1906-L1909
103
+ createGetCanonicalFileName ( useCaseSensitiveFileNames : boolean ) : TSInternal . GetCanonicalFileName
104
+ }
105
+ namespace TSInternal {
106
+ // https://github.com/microsoft/TypeScript/blob/4a34294908bed6701dcba2456ca7ac5eafe0ddff/src/compiler/core.ts#L1906
107
+ export type GetCanonicalFileName = ( fileName : string ) => string
108
+ }
109
+
98
110
/**
99
111
* Export the current version.
100
112
*/
@@ -515,6 +527,103 @@ export function create (rawOptions: CreateOptions = {}): Register {
515
527
let getOutput : ( code : string , fileName : string ) => SourceOutput
516
528
let getTypeInfo : ( _code : string , _fileName : string , _position : number ) => TypeInfo
517
529
530
+ const getCanonicalFileName = ( ts as unknown as TSInternal ) . createGetCanonicalFileName ( ts . sys . useCaseSensitiveFileNames )
531
+
532
+ // In a factory because these are shared across both CompilerHost and LanguageService codepaths
533
+ function createResolverFunctions ( serviceHost : _ts . ModuleResolutionHost ) {
534
+ const moduleResolutionCache = ts . createModuleResolutionCache ( cwd , getCanonicalFileName , config . options )
535
+ const knownInternalFilenames = new Set < string > ( )
536
+ /** "Buckets" (module directories) whose contents should be marked "internal" */
537
+ const internalBuckets = new Set < string > ( )
538
+
539
+ // Get bucket for a source filename. Bucket is the containing `./node_modules/*/` directory
540
+ // For '/project/node_modules/foo/node_modules/bar/lib/index.js' bucket is '/project/node_modules/foo/node_modules/bar/'
541
+ // For '/project/node_modules/foo/node_modules/@scope/bar/lib/index.js' bucket is '/project/node_modules/foo/node_modules/@scope/bar/'
542
+ const moduleBucketRe = / .* \/ n o d e _ m o d u l e s \/ (?: @ [ ^ \/ ] + \/ ) ? [ ^ \/ ] + \/ /
543
+ function getModuleBucket ( filename : string ) {
544
+ const find = moduleBucketRe . exec ( filename )
545
+ if ( find ) return find [ 0 ]
546
+ return ''
547
+ }
548
+
549
+ // Mark that this file and all siblings in its bucket should be "internal"
550
+ function markBucketOfFilenameInternal ( filename : string ) {
551
+ internalBuckets . add ( getModuleBucket ( filename ) )
552
+ }
553
+
554
+ function isFileInInternalBucket ( filename : string ) {
555
+ return internalBuckets . has ( getModuleBucket ( filename ) )
556
+ }
557
+
558
+ function isFileKnownToBeInternal ( filename : string ) {
559
+ return knownInternalFilenames . has ( filename )
560
+ }
561
+
562
+ /**
563
+ * If we need to emit JS for a file, force TS to consider it non-external
564
+ */
565
+ const fixupResolvedModule = ( resolvedModule : _ts . ResolvedModule | _ts . ResolvedTypeReferenceDirective ) => {
566
+ const { resolvedFileName } = resolvedModule
567
+ if ( resolvedFileName === undefined ) return
568
+ // .ts is always switched to internal
569
+ // .js is switched on-demand
570
+ if (
571
+ resolvedModule . isExternalLibraryImport && (
572
+ ( resolvedFileName . endsWith ( '.ts' ) && ! resolvedFileName . endsWith ( '.d.ts' ) ) ||
573
+ isFileKnownToBeInternal ( resolvedFileName ) ||
574
+ isFileInInternalBucket ( resolvedFileName )
575
+ )
576
+ ) {
577
+ resolvedModule . isExternalLibraryImport = false
578
+ }
579
+ if ( ! resolvedModule . isExternalLibraryImport ) {
580
+ knownInternalFilenames . add ( resolvedFileName )
581
+ }
582
+ }
583
+ /*
584
+ * NOTE:
585
+ * Older ts versions do not pass `redirectedReference` nor `options`.
586
+ * We must pass `redirectedReference` to newer ts versions, but cannot rely on `options`, hence the weird argument name
587
+ */
588
+ const resolveModuleNames : _ts . LanguageServiceHost [ 'resolveModuleNames' ] = ( moduleNames : string [ ] , containingFile : string , reusedNames : string [ ] | undefined , redirectedReference : _ts . ResolvedProjectReference | undefined , optionsOnlyWithNewerTsVersions : _ts . CompilerOptions ) : ( _ts . ResolvedModule | undefined ) [ ] => {
589
+ return moduleNames . map ( moduleName => {
590
+ const { resolvedModule } = ts . resolveModuleName ( moduleName , containingFile , config . options , serviceHost , moduleResolutionCache , redirectedReference )
591
+ if ( resolvedModule ) {
592
+ fixupResolvedModule ( resolvedModule )
593
+ }
594
+ return resolvedModule
595
+ } )
596
+ }
597
+
598
+ // language service never calls this, but TS docs recommend that we implement it
599
+ const getResolvedModuleWithFailedLookupLocationsFromCache : _ts . LanguageServiceHost [ 'getResolvedModuleWithFailedLookupLocationsFromCache' ] = ( moduleName , containingFile ) : _ts . ResolvedModuleWithFailedLookupLocations | undefined => {
600
+ const ret = ts . resolveModuleNameFromCache ( moduleName , containingFile , moduleResolutionCache )
601
+ if ( ret && ret . resolvedModule ) {
602
+ fixupResolvedModule ( ret . resolvedModule )
603
+ }
604
+ return ret
605
+ }
606
+
607
+ const resolveTypeReferenceDirectives : _ts . LanguageServiceHost [ 'resolveTypeReferenceDirectives' ] = ( typeDirectiveNames : string [ ] , containingFile : string , redirectedReference : _ts . ResolvedProjectReference | undefined , options : _ts . CompilerOptions ) : ( _ts . ResolvedTypeReferenceDirective | undefined ) [ ] => {
608
+ // Note: seems to be called with empty typeDirectiveNames array for all files.
609
+ return typeDirectiveNames . map ( typeDirectiveName => {
610
+ const { resolvedTypeReferenceDirective } = ts . resolveTypeReferenceDirective ( typeDirectiveName , containingFile , config . options , serviceHost , redirectedReference )
611
+ if ( resolvedTypeReferenceDirective ) {
612
+ fixupResolvedModule ( resolvedTypeReferenceDirective )
613
+ }
614
+ return resolvedTypeReferenceDirective
615
+ } )
616
+ }
617
+
618
+ return {
619
+ resolveModuleNames,
620
+ getResolvedModuleWithFailedLookupLocationsFromCache,
621
+ resolveTypeReferenceDirectives,
622
+ isFileKnownToBeInternal,
623
+ markBucketOfFilenameInternal
624
+ }
625
+ }
626
+
518
627
// Use full language services when the fast option is disabled.
519
628
if ( ! transpileOnly ) {
520
629
const fileContents = new Map < string , string > ( )
@@ -536,14 +645,15 @@ export function create (rawOptions: CreateOptions = {}): Register {
536
645
}
537
646
538
647
// Create the compiler host for type checking.
539
- const serviceHost : _ts . LanguageServiceHost = {
648
+ const serviceHost : _ts . LanguageServiceHost & Required < Pick < _ts . LanguageServiceHost , 'fileExists' | 'readFile' > > = {
540
649
getProjectVersion : ( ) => String ( projectVersion ) ,
541
650
getScriptFileNames : ( ) => Array . from ( rootFileNames ) ,
542
651
getScriptVersion : ( fileName : string ) => {
543
652
const version = fileVersions . get ( fileName )
544
653
return version ? version . toString ( ) : ''
545
654
} ,
546
655
getScriptSnapshot ( fileName : string ) {
656
+ // TODO ordering of this with getScriptVersion? Should they sync up?
547
657
let contents = fileContents . get ( fileName )
548
658
549
659
// Read contents into TypeScript memory cache.
@@ -563,21 +673,27 @@ export function create (rawOptions: CreateOptions = {}): Register {
563
673
getDirectories : cachedLookup ( debugFn ( 'getDirectories' , ts . sys . getDirectories ) ) ,
564
674
fileExists : cachedLookup ( debugFn ( 'fileExists' , fileExists ) ) ,
565
675
directoryExists : cachedLookup ( debugFn ( 'directoryExists' , ts . sys . directoryExists ) ) ,
676
+ realpath : ts . sys . realpath ? cachedLookup ( debugFn ( 'realpath' , ts . sys . realpath ) ) : undefined ,
566
677
getNewLine : ( ) => ts . sys . newLine ,
567
678
useCaseSensitiveFileNames : ( ) => ts . sys . useCaseSensitiveFileNames ,
568
679
getCurrentDirectory : ( ) => cwd ,
569
680
getCompilationSettings : ( ) => config . options ,
570
681
getDefaultLibFileName : ( ) => ts . getDefaultLibFilePath ( config . options ) ,
571
682
getCustomTransformers : getCustomTransformers
572
683
}
684
+ const { resolveModuleNames, getResolvedModuleWithFailedLookupLocationsFromCache, resolveTypeReferenceDirectives, isFileKnownToBeInternal, markBucketOfFilenameInternal } = createResolverFunctions ( serviceHost )
685
+ serviceHost . resolveModuleNames = resolveModuleNames
686
+ serviceHost . getResolvedModuleWithFailedLookupLocationsFromCache = getResolvedModuleWithFailedLookupLocationsFromCache
687
+ serviceHost . resolveTypeReferenceDirectives = resolveTypeReferenceDirectives
573
688
574
689
const registry = ts . createDocumentRegistry ( ts . sys . useCaseSensitiveFileNames , cwd )
575
690
const service = ts . createLanguageService ( serviceHost , registry )
576
691
577
692
const updateMemoryCache = ( contents : string , fileName : string ) => {
578
- // Add to `rootFiles` if not already there
579
- // This is necessary to force TS to emit output
580
- if ( ! rootFileNames . has ( fileName ) ) {
693
+ // Add to `rootFiles` as necessary, either to make TS include a file it has not seen,
694
+ // or to trigger a re-classification of files from external to internal.
695
+ if ( ! rootFileNames . has ( fileName ) && ! isFileKnownToBeInternal ( fileName ) ) {
696
+ markBucketOfFilenameInternal ( fileName )
581
697
rootFileNames . add ( fileName )
582
698
// Increment project version for every change to rootFileNames.
583
699
projectVersion ++
@@ -649,13 +765,15 @@ export function create (rawOptions: CreateOptions = {}): Register {
649
765
return { name, comment }
650
766
}
651
767
} else {
652
- const sys = {
768
+ const sys : _ts . System & _ts . FormatDiagnosticsHost = {
653
769
...ts . sys ,
654
770
...diagnosticHost ,
655
771
readFile : ( fileName : string ) => {
656
772
const cacheContents = fileContents . get ( fileName )
657
773
if ( cacheContents !== undefined ) return cacheContents
658
- return cachedReadFile ( fileName )
774
+ const contents = cachedReadFile ( fileName )
775
+ if ( contents ) fileContents . set ( fileName , contents )
776
+ return contents
659
777
} ,
660
778
readDirectory : ts . sys . readDirectory ,
661
779
getDirectories : cachedLookup ( debugFn ( 'getDirectories' , ts . sys . getDirectories ) ) ,
@@ -678,6 +796,9 @@ export function create (rawOptions: CreateOptions = {}): Register {
678
796
getDefaultLibFileName : ( ) => normalizeSlashes ( join ( dirname ( compiler ) , ts . getDefaultLibFileName ( config . options ) ) ) ,
679
797
useCaseSensitiveFileNames : ( ) => sys . useCaseSensitiveFileNames
680
798
}
799
+ const { resolveModuleNames, resolveTypeReferenceDirectives, isFileKnownToBeInternal, markBucketOfFilenameInternal } = createResolverFunctions ( host )
800
+ host . resolveModuleNames = resolveModuleNames
801
+ host . resolveTypeReferenceDirectives = resolveTypeReferenceDirectives
681
802
682
803
// Fallback for older TypeScript releases without incremental API.
683
804
let builderProgram = ts . createIncrementalProgram
@@ -704,17 +825,22 @@ export function create (rawOptions: CreateOptions = {}): Register {
704
825
705
826
// Set the file contents into cache manually.
706
827
const updateMemoryCache = ( contents : string , fileName : string ) => {
707
- const sourceFile = builderProgram . getSourceFile ( fileName )
708
-
709
- fileContents . set ( fileName , contents )
828
+ const previousContents = fileContents . get ( fileName )
829
+ const contentsChanged = previousContents !== contents
830
+ if ( contentsChanged ) {
831
+ fileContents . set ( fileName , contents )
832
+ }
710
833
711
834
// Add to `rootFiles` when discovered by compiler for the first time.
712
- if ( sourceFile === undefined ) {
835
+ let addedToRootFileNames = false
836
+ if ( ! rootFileNames . has ( fileName ) && ! isFileKnownToBeInternal ( fileName ) ) {
837
+ markBucketOfFilenameInternal ( fileName )
713
838
rootFileNames . add ( fileName )
839
+ addedToRootFileNames = true
714
840
}
715
841
716
842
// Update program when file changes.
717
- if ( sourceFile === undefined || sourceFile . text !== contents ) {
843
+ if ( addedToRootFileNames || contentsChanged ) {
718
844
builderProgram = ts . createEmitAndSemanticDiagnosticsBuilderProgram (
719
845
Array . from ( rootFileNames ) ,
720
846
config . options ,
0 commit comments