@@ -283,13 +283,25 @@ namespace ts.codefix {
283283 preferences : UserPreferences ,
284284 ) : ReadonlyArray < FixAddNewImport | FixUseImportType > {
285285 const isJs = isSourceFileJS ( sourceFile ) ;
286+ const { allowsImporting } = createLazyPackageJsonDependencyReader ( sourceFile , host ) ;
286287 const choicesForEachExportingModule = flatMap ( moduleSymbols , ( { moduleSymbol, importKind, exportedSymbolIsTypeOnly } ) =>
287288 moduleSpecifiers . getModuleSpecifiers ( moduleSymbol , program . getCompilerOptions ( ) , sourceFile , host , program . getSourceFiles ( ) , preferences , program . redirectTargetsMap )
288289 . map ( ( moduleSpecifier ) : FixAddNewImport | FixUseImportType =>
289290 // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types.
290291 exportedSymbolIsTypeOnly && isJs ? { kind : ImportFixKind . ImportType , moduleSpecifier, position : Debug . assertDefined ( position ) } : { kind : ImportFixKind . AddNew , moduleSpecifier, importKind } ) ) ;
291- // Sort to keep the shortest paths first
292- return sort ( choicesForEachExportingModule , ( a , b ) => a . moduleSpecifier . length - b . moduleSpecifier . length ) ;
292+
293+ // Sort by presence in package.json, then shortest paths first
294+ return sort ( choicesForEachExportingModule , ( a , b ) => {
295+ const allowsImportingA = allowsImporting ( a . moduleSpecifier ) ;
296+ const allowsImportingB = allowsImporting ( b . moduleSpecifier ) ;
297+ if ( allowsImportingA && ! allowsImportingB ) {
298+ return - 1 ;
299+ }
300+ if ( allowsImportingB && ! allowsImportingA ) {
301+ return 1 ;
302+ }
303+ return a . moduleSpecifier . length - b . moduleSpecifier . length ;
304+ } ) ;
293305 }
294306
295307 function getFixesForAddImport (
@@ -380,7 +392,8 @@ namespace ts.codefix {
380392 // "default" is a keyword and not a legal identifier for the import, so we don't expect it here
381393 Debug . assert ( symbolName !== InternalSymbolName . Default ) ;
382394
383- const fixes = arrayFrom ( flatMapIterator ( getExportInfos ( symbolName , getMeaningFromLocation ( symbolToken ) , cancellationToken , sourceFile , checker , program ) . entries ( ) , ( [ _ , exportInfos ] ) =>
395+ const exportInfos = getExportInfos ( symbolName , getMeaningFromLocation ( symbolToken ) , cancellationToken , sourceFile , checker , program , preferences , host ) ;
396+ const fixes = arrayFrom ( flatMapIterator ( exportInfos . entries ( ) , ( [ _ , exportInfos ] ) =>
384397 getFixForImport ( exportInfos , symbolName , symbolToken . getStart ( sourceFile ) , program , sourceFile , host , preferences ) ) ) ;
385398 return { fixes, symbolName } ;
386399 }
@@ -393,14 +406,16 @@ namespace ts.codefix {
393406 sourceFile : SourceFile ,
394407 checker : TypeChecker ,
395408 program : Program ,
409+ preferences : UserPreferences ,
410+ host : LanguageServiceHost
396411 ) : ReadonlyMap < ReadonlyArray < SymbolExportInfo > > {
397412 // For each original symbol, keep all re-exports of that symbol together so we can call `getCodeActionsForImport` on the whole group at once.
398413 // Maps symbol id to info for modules providing that symbol (original export + re-exports).
399414 const originalSymbolToExportInfos = createMultiMap < SymbolExportInfo > ( ) ;
400415 function addSymbol ( moduleSymbol : Symbol , exportedSymbol : Symbol , importKind : ImportKind ) : void {
401416 originalSymbolToExportInfos . add ( getUniqueSymbolId ( exportedSymbol , checker ) . toString ( ) , { moduleSymbol, importKind, exportedSymbolIsTypeOnly : isTypeOnlySymbol ( exportedSymbol , checker ) } ) ;
402417 }
403- forEachExternalModuleToImportFrom ( checker , sourceFile , program . getSourceFiles ( ) , moduleSymbol => {
418+ forEachExternalModuleToImportFrom ( checker , host , preferences , program . redirectTargetsMap , sourceFile , program . getSourceFiles ( ) , moduleSymbol => {
404419 cancellationToken . throwIfCancellationRequested ( ) ;
405420
406421 const defaultInfo = getDefaultLikeExportInfo ( moduleSymbol , checker , program . getCompilerOptions ( ) ) ;
@@ -561,12 +576,44 @@ namespace ts.codefix {
561576 return some ( declarations , decl => ! ! ( getMeaningFromDeclaration ( decl ) & meaning ) ) ;
562577 }
563578
564- export function forEachExternalModuleToImportFrom ( checker : TypeChecker , from : SourceFile , allSourceFiles : ReadonlyArray < SourceFile > , cb : ( module : Symbol ) => void ) {
579+ export function forEachExternalModuleToImportFrom ( checker : TypeChecker , host : LanguageServiceHost , preferences : UserPreferences , redirectTargetsMap : RedirectTargetsMap , from : SourceFile , allSourceFiles : ReadonlyArray < SourceFile > , cb : ( module : Symbol ) => void ) {
580+ const { allowsImporting } = createLazyPackageJsonDependencyReader ( from , host ) ;
581+ const compilerOptions = host . getCompilationSettings ( ) ;
582+ const getCanonicalFileName = hostGetCanonicalFileName ( host ) ;
565583 forEachExternalModule ( checker , allSourceFiles , ( module , sourceFile ) => {
566- if ( sourceFile === undefined || sourceFile !== from && isImportablePath ( from . fileName , sourceFile . fileName ) ) {
584+ if ( sourceFile === undefined && allowsImporting ( stripQuotes ( module . getName ( ) ) ) ) {
567585 cb ( module ) ;
568586 }
587+ else if ( sourceFile && sourceFile !== from && isImportablePath ( from . fileName , sourceFile . fileName ) ) {
588+ const moduleSpecifier = getNodeModulesPackageNameFromFileName ( sourceFile . fileName ) ;
589+ if ( ! moduleSpecifier || allowsImporting ( moduleSpecifier ) ) {
590+ cb ( module ) ;
591+ }
592+ }
569593 } ) ;
594+
595+ function getNodeModulesPackageNameFromFileName ( importedFileName : string ) : string | undefined {
596+ const specifier = moduleSpecifiers . getModuleSpecifier (
597+ compilerOptions ,
598+ from ,
599+ toPath ( from . fileName , /*basePath*/ undefined , getCanonicalFileName ) ,
600+ importedFileName ,
601+ host ,
602+ allSourceFiles ,
603+ preferences ,
604+ redirectTargetsMap ) ;
605+
606+ // Paths here are not node_modules, so we don’t care about them;
607+ // returning anything will trigger a lookup in package.json.
608+ if ( ! pathIsRelative ( specifier ) && ! isRootedDiskPath ( specifier ) ) {
609+ const components = getPathComponents ( getPackageNameFromTypesPackageName ( specifier ) ) . slice ( 1 ) ;
610+ // Scoped packages
611+ if ( startsWith ( components [ 0 ] , "@" ) ) {
612+ return `${ components [ 0 ] } /${ components [ 1 ] } ` ;
613+ }
614+ return components [ 0 ] ;
615+ }
616+ }
570617 }
571618
572619 function forEachExternalModule ( checker : TypeChecker , allSourceFiles : ReadonlyArray < SourceFile > , cb : ( module : Symbol , sourceFile : SourceFile | undefined ) => void ) {
@@ -620,4 +667,69 @@ namespace ts.codefix {
620667 // Need `|| "_"` to ensure result isn't empty.
621668 return ! isStringANonContextualKeyword ( res ) ? res || "_" : `_${ res } ` ;
622669 }
670+
671+ function createLazyPackageJsonDependencyReader ( fromFile : SourceFile , host : LanguageServiceHost ) {
672+ const packageJsonPaths = findPackageJsons ( getDirectoryPath ( fromFile . fileName ) , host ) ;
673+ const dependencyIterator = readPackageJsonDependencies ( host , packageJsonPaths ) ;
674+ let seenDeps : Map < true > | undefined ;
675+ let usesNodeCoreModules : boolean | undefined ;
676+ return { allowsImporting } ;
677+
678+ function containsDependency ( dependency : string ) {
679+ if ( ( seenDeps || ( seenDeps = createMap ( ) ) ) . has ( dependency ) ) {
680+ return true ;
681+ }
682+ let packageName : string | void ;
683+ while ( packageName = dependencyIterator . next ( ) . value ) {
684+ seenDeps . set ( packageName , true ) ;
685+ if ( packageName === dependency ) {
686+ return true ;
687+ }
688+ }
689+ return false ;
690+ }
691+
692+ function allowsImporting ( moduleSpecifier : string ) : boolean {
693+ if ( ! packageJsonPaths . length ) {
694+ return true ;
695+ }
696+
697+ // If we’re in JavaScript, it can be difficult to tell whether the user wants to import
698+ // from Node core modules or not. We can start by seeing if the user is actually using
699+ // any node core modules, as opposed to simply having @types/node accidentally as a
700+ // dependency of a dependency.
701+ if ( isSourceFileJS ( fromFile ) && JsTyping . nodeCoreModules . has ( moduleSpecifier ) ) {
702+ if ( usesNodeCoreModules === undefined ) {
703+ usesNodeCoreModules = consumesNodeCoreModules ( fromFile ) ;
704+ }
705+ if ( usesNodeCoreModules ) {
706+ return true ;
707+ }
708+ }
709+
710+ return containsDependency ( moduleSpecifier )
711+ || containsDependency ( getTypesPackageName ( moduleSpecifier ) ) ;
712+ }
713+ }
714+
715+ function * readPackageJsonDependencies ( host : LanguageServiceHost , packageJsonPaths : string [ ] ) {
716+ type PackageJson = Record < typeof dependencyKeys [ number ] , Record < string , string > | undefined > ;
717+ const dependencyKeys = [ "dependencies" , "devDependencies" , "optionalDependencies" ] as const ;
718+ for ( const fileName of packageJsonPaths ) {
719+ const content = readJson ( fileName , { readFile : host . readFile ? host . readFile . bind ( host ) : sys . readFile } ) as PackageJson ;
720+ for ( const key of dependencyKeys ) {
721+ const dependencies = content [ key ] ;
722+ if ( ! dependencies ) {
723+ continue ;
724+ }
725+ for ( const packageName in dependencies ) {
726+ yield packageName ;
727+ }
728+ }
729+ }
730+ }
731+
732+ function consumesNodeCoreModules ( sourceFile : SourceFile ) : boolean {
733+ return some ( sourceFile . imports , ( { text } ) => JsTyping . nodeCoreModules . has ( text ) ) ;
734+ }
623735}
0 commit comments