11// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers.
22/* @internal */
33namespace ts . moduleSpecifiers {
4- const enum RelativePreference { Relative , NonRelative , Auto }
4+ const enum RelativePreference { Relative , NonRelative , Shortest , ExternalNonRelative }
55 // See UserPreferences#importPathEnding
66 const enum Ending { Minimal , Index , JsExtension }
77
@@ -13,7 +13,11 @@ namespace ts.moduleSpecifiers {
1313
1414 function getPreferences ( { importModuleSpecifierPreference, importModuleSpecifierEnding } : UserPreferences , compilerOptions : CompilerOptions , importingSourceFile : SourceFile ) : Preferences {
1515 return {
16- relativePreference : importModuleSpecifierPreference === "relative" ? RelativePreference . Relative : importModuleSpecifierPreference === "non-relative" ? RelativePreference . NonRelative : RelativePreference . Auto ,
16+ relativePreference :
17+ importModuleSpecifierPreference === "relative" ? RelativePreference . Relative :
18+ importModuleSpecifierPreference === "non-relative" ? RelativePreference . NonRelative :
19+ importModuleSpecifierPreference === "project-relative" ? RelativePreference . ExternalNonRelative :
20+ RelativePreference . Shortest ,
1721 ending : getEnding ( ) ,
1822 } ;
1923 function getEnding ( ) : Ending {
@@ -147,17 +151,19 @@ namespace ts.moduleSpecifiers {
147151
148152 interface Info {
149153 readonly getCanonicalFileName : GetCanonicalFileName ;
154+ readonly importingSourceFileName : Path
150155 readonly sourceDirectory : Path ;
151156 }
152157 // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path
153158 function getInfo ( importingSourceFileName : Path , host : ModuleSpecifierResolutionHost ) : Info {
154159 const getCanonicalFileName = createGetCanonicalFileName ( host . useCaseSensitiveFileNames ? host . useCaseSensitiveFileNames ( ) : true ) ;
155160 const sourceDirectory = getDirectoryPath ( importingSourceFileName ) ;
156- return { getCanonicalFileName, sourceDirectory } ;
161+ return { getCanonicalFileName, importingSourceFileName , sourceDirectory } ;
157162 }
158163
159- function getLocalModuleSpecifier ( moduleFileName : string , { getCanonicalFileName , sourceDirectory } : Info , compilerOptions : CompilerOptions , host : ModuleSpecifierResolutionHost , { ending, relativePreference } : Preferences ) : string {
164+ function getLocalModuleSpecifier ( moduleFileName : string , info : Info , compilerOptions : CompilerOptions , host : ModuleSpecifierResolutionHost , { ending, relativePreference } : Preferences ) : string {
160165 const { baseUrl, paths, rootDirs, bundledPackageName } = compilerOptions ;
166+ const { sourceDirectory, getCanonicalFileName } = info ;
161167
162168 const relativePath = rootDirs && tryGetModuleNameFromRootDirs ( rootDirs , moduleFileName , sourceDirectory , getCanonicalFileName , ending , compilerOptions ) ||
163169 removeExtensionAndIndexPostFix ( ensurePathIsNonModuleName ( getRelativePathFromDirectory ( sourceDirectory , moduleFileName , getCanonicalFileName ) ) , ending , compilerOptions ) ;
@@ -183,7 +189,42 @@ namespace ts.moduleSpecifiers {
183189 return nonRelative ;
184190 }
185191
186- if ( relativePreference !== RelativePreference . Auto ) Debug . assertNever ( relativePreference ) ;
192+ if ( relativePreference === RelativePreference . ExternalNonRelative ) {
193+ const projectDirectory = host . getCurrentDirectory ( ) ;
194+ const modulePath = toPath ( moduleFileName , projectDirectory , getCanonicalFileName ) ;
195+ const sourceIsInternal = startsWith ( sourceDirectory , projectDirectory ) ;
196+ const targetIsInternal = startsWith ( modulePath , projectDirectory ) ;
197+ if ( sourceIsInternal && ! targetIsInternal || ! sourceIsInternal && targetIsInternal ) {
198+ // 1. The import path crosses the boundary of the tsconfig.json-containing directory.
199+ //
200+ // src/
201+ // tsconfig.json
202+ // index.ts -------
203+ // lib/ | (path crosses tsconfig.json)
204+ // imported.ts <---
205+ //
206+ return nonRelative ;
207+ }
208+
209+ const nearestTargetPackageJson = getNearestAncestorDirectoryWithPackageJson ( host , getDirectoryPath ( modulePath ) ) ;
210+ const nearestSourcePackageJson = getNearestAncestorDirectoryWithPackageJson ( host , sourceDirectory ) ;
211+ if ( nearestSourcePackageJson !== nearestTargetPackageJson ) {
212+ // 2. The importing and imported files are part of different packages.
213+ //
214+ // packages/a/
215+ // package.json
216+ // index.ts --------
217+ // packages/b/ | (path crosses package.json)
218+ // package.json |
219+ // component.ts <---
220+ //
221+ return nonRelative ;
222+ }
223+
224+ return relativePath ;
225+ }
226+
227+ if ( relativePreference !== RelativePreference . Shortest ) Debug . assertNever ( relativePreference ) ;
187228
188229 // Prefer a relative import over a baseUrl import if it has fewer components.
189230 return isPathRelativeToParent ( nonRelative ) || countPathComponents ( relativePath ) < countPathComponents ( nonRelative ) ? relativePath : nonRelative ;
@@ -213,6 +254,15 @@ namespace ts.moduleSpecifiers {
213254 ) ;
214255 }
215256
257+ function getNearestAncestorDirectoryWithPackageJson ( host : ModuleSpecifierResolutionHost , fileName : string ) {
258+ if ( host . getNearestAncestorDirectoryWithPackageJson ) {
259+ return host . getNearestAncestorDirectoryWithPackageJson ( fileName ) ;
260+ }
261+ return ! ! forEachAncestorDirectory ( fileName , directory => {
262+ return host . fileExists ( combinePaths ( directory , "package.json" ) ) ? true : undefined ;
263+ } ) ;
264+ }
265+
216266 export function forEachFileNameOfModule < T > (
217267 importingFileName : string ,
218268 importedFileName : string ,
0 commit comments