Skip to content

Commit 8571c63

Browse files
committed
Add new importModuleSpecifierPreference value
1 parent 23cb2d8 commit 8571c63

File tree

10 files changed

+131
-15
lines changed

10 files changed

+131
-15
lines changed

src/compiler/moduleSpecifiers.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers.
22
/* @internal */
33
namespace 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 === "external-non-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);
@@ -174,13 +180,34 @@ namespace ts.moduleSpecifiers {
174180
const bundledPkgReference = bundledPackageName ? combinePaths(bundledPackageName, relativeToBaseUrl) : relativeToBaseUrl;
175181
const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(bundledPkgReference, ending, compilerOptions);
176182
const fromPaths = paths && tryGetModuleNameFromPaths(removeFileExtension(bundledPkgReference), importRelativeToBaseUrl, paths);
177-
const nonRelative = fromPaths === undefined ? importRelativeToBaseUrl : fromPaths;
183+
const nonRelative = fromPaths === undefined ? importRelativeToBaseUrl : fromPaths.path;
178184

179185
if (relativePreference === RelativePreference.NonRelative) {
180186
return nonRelative;
181187
}
182188

183-
if (relativePreference !== RelativePreference.Auto) Debug.assertNever(relativePreference);
189+
if (relativePreference === RelativePreference.ExternalNonRelative) {
190+
Debug.assertIsDefined(host.getNearestAncestorDirectoryWithPackageJson);
191+
const projectDirectory = host.getCurrentDirectory();
192+
const modulePath = toPath(moduleFileName, projectDirectory, getCanonicalFileName);
193+
const sourceIsInternal = startsWith(sourceDirectory, projectDirectory);
194+
const targetIsInternal = startsWith(modulePath, projectDirectory);
195+
if (sourceIsInternal && !targetIsInternal || !sourceIsInternal && targetIsInternal) {
196+
// 1. The import path crosses the boundary of the tsconfig.json-containing directory.
197+
return nonRelative;
198+
}
199+
200+
const nearestTargetPackageJson = host.getNearestAncestorDirectoryWithPackageJson(getDirectoryPath(modulePath));
201+
const nearestSourcePackageJson = host.getNearestAncestorDirectoryWithPackageJson(sourceDirectory);
202+
if (nearestSourcePackageJson !== nearestTargetPackageJson) {
203+
// 2. The importing and imported files are part of different packages.
204+
return nonRelative;
205+
}
206+
207+
return relativePath;
208+
}
209+
210+
if (relativePreference !== RelativePreference.Shortest) Debug.assertNever(relativePreference);
184211

185212
// Prefer a relative import over a baseUrl import if it has fewer components.
186213
return isPathRelativeToParent(nonRelative) || countPathComponents(relativePath) < countPathComponents(nonRelative) ? relativePath : nonRelative;
@@ -325,7 +352,7 @@ namespace ts.moduleSpecifiers {
325352
}
326353
}
327354

328-
function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: MapLike<readonly string[]>): string | undefined {
355+
function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: MapLike<readonly string[]>): { key: string, path: string } | undefined {
329356
for (const key in paths) {
330357
for (const patternText of paths[key]) {
331358
const pattern = removeFileExtension(normalizePath(patternText));
@@ -338,11 +365,11 @@ namespace ts.moduleSpecifiers {
338365
endsWith(relativeToBaseUrl, suffix) ||
339366
!suffix && relativeToBaseUrl === removeTrailingDirectorySeparator(prefix)) {
340367
const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length);
341-
return key.replace("*", matchedStar);
368+
return { key, path: key.replace("*", matchedStar) };
342369
}
343370
}
344371
else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) {
345-
return key;
372+
return { key, path: key };
346373
}
347374
}
348375
}
@@ -431,7 +458,7 @@ namespace ts.moduleSpecifiers {
431458
versionPaths.paths
432459
);
433460
if (fromPaths !== undefined) {
434-
moduleFileToTry = combinePaths(packageRootPath, fromPaths);
461+
moduleFileToTry = combinePaths(packageRootPath, fromPaths.path);
435462
}
436463
}
437464

src/compiler/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7847,6 +7847,7 @@ namespace ts {
78477847
realpath?(path: string): string;
78487848
getSymlinkCache?(): SymlinkCache;
78497849
getGlobalTypingsCacheLocation?(): string | undefined;
7850+
getNearestAncestorDirectoryWithPackageJson?(fileName: string, rootDir?: string): string | undefined;
78507851

78517852
getSourceFiles(): readonly SourceFile[];
78527853
readonly redirectTargetsMap: RedirectTargetsMap;
@@ -8148,7 +8149,7 @@ namespace ts {
81488149
readonly includeCompletionsForModuleExports?: boolean;
81498150
readonly includeAutomaticOptionalChainCompletions?: boolean;
81508151
readonly includeCompletionsWithInsertText?: boolean;
8151-
readonly importModuleSpecifierPreference?: "auto" | "relative" | "non-relative";
8152+
readonly importModuleSpecifierPreference?: "shortest" | "external-non-relative" | "relative" | "non-relative";
81528153
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */
81538154
readonly importModuleSpecifierEnding?: "auto" | "minimal" | "index" | "js";
81548155
readonly allowTextChangesInNewFiles?: boolean;

src/harness/fourslashImpl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2927,6 +2927,10 @@ namespace FourSlash {
29272927
}
29282928
const range = ts.firstOrUndefined(ranges);
29292929

2930+
if (preferences) {
2931+
this.configure(preferences);
2932+
}
2933+
29302934
const codeFixes = this.getCodeFixes(fileName, errorCode, preferences).filter(f => f.fixName === ts.codefix.importFixName);
29312935

29322936
if (codeFixes.length === 0) {

src/server/editorServices.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3778,7 +3778,7 @@ namespace ts.server {
37783778
const info = packageJsonCache.getInDirectory(directory);
37793779
if (info) result.push(info);
37803780
}
3781-
if (rootPath && rootPath === this.toPath(directory)) {
3781+
if (rootPath && rootPath === directory) {
37823782
return true;
37833783
}
37843784
};
@@ -3787,6 +3787,20 @@ namespace ts.server {
37873787
return result;
37883788
}
37893789

3790+
/*@internal*/
3791+
getNearestAncestorDirectoryWithPackageJson(fileName: string): string | undefined {
3792+
return forEachAncestorDirectory(fileName, directory => {
3793+
switch (this.packageJsonCache.directoryHasPackageJson(this.toPath(directory))) {
3794+
case Ternary.True: return directory;
3795+
case Ternary.False: return undefined;
3796+
case Ternary.Maybe:
3797+
return this.host.fileExists(combinePaths(directory, "package.json"))
3798+
? directory
3799+
: undefined;
3800+
}
3801+
});
3802+
}
3803+
37903804
/*@internal*/
37913805
private watchPackageJsonFile(path: Path) {
37923806
const watchers = this.packageJsonFilesMap || (this.packageJsonFilesMap = new Map());

src/server/project.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,6 +1625,11 @@ namespace ts.server {
16251625
return this.projectService.getPackageJsonsVisibleToFile(fileName, rootDir);
16261626
}
16271627

1628+
/*@internal*/
1629+
getNearestAncestorDirectoryWithPackageJson(fileName: string): string | undefined {
1630+
return this.projectService.getNearestAncestorDirectoryWithPackageJson(fileName);
1631+
}
1632+
16281633
/*@internal*/
16291634
getPackageJsonsForAutoImport(rootDir?: string): readonly PackageJsonInfo[] {
16301635
const packageJsons = this.getPackageJsonsVisibleToFile(combinePaths(this.currentDirectory, inferredTypesContainingFile), rootDir);
@@ -1955,6 +1960,10 @@ namespace ts.server {
19551960
return super.updateGraph();
19561961
}
19571962

1963+
hasRoots() {
1964+
return !!this.rootFileNames?.length;
1965+
}
1966+
19581967
markAsDirty() {
19591968
this.rootFileNames = undefined;
19601969
super.markAsDirty();

src/server/protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3217,7 +3217,7 @@ namespace ts.server.protocol {
32173217
* values, with insertion text to replace preceding `.` tokens with `?.`.
32183218
*/
32193219
readonly includeAutomaticOptionalChainCompletions?: boolean;
3220-
readonly importModuleSpecifierPreference?: "auto" | "relative" | "non-relative";
3220+
readonly importModuleSpecifierPreference?: "shortest" | "external-non-relative" | "relative" | "non-relative";
32213221
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */
32223222
readonly importModuleSpecifierEnding?: "auto" | "minimal" | "index" | "js";
32233223
readonly allowTextChangesInNewFiles?: boolean;

src/services/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ namespace ts {
301301
/* @internal */
302302
getPackageJsonsVisibleToFile?(fileName: string, rootDir?: string): readonly PackageJsonInfo[];
303303
/* @internal */
304+
getNearestAncestorDirectoryWithPackageJson?(fileName: string, rootDir?: string): string | undefined;
305+
/* @internal */
304306
getPackageJsonsForAutoImport?(rootDir?: string): readonly PackageJsonInfo[];
305307
/* @internal */
306308
getImportSuggestionsCache?(): Completions.ImportSuggestionsForFileCache;

src/services/utilities.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1773,7 +1773,8 @@ namespace ts {
17731773
redirectTargetsMap: program.redirectTargetsMap,
17741774
getProjectReferenceRedirect: fileName => program.getProjectReferenceRedirect(fileName),
17751775
isSourceOfProjectReferenceRedirect: fileName => program.isSourceOfProjectReferenceRedirect(fileName),
1776-
getCompilerOptions: () => program.getCompilerOptions()
1776+
getCompilerOptions: () => program.getCompilerOptions(),
1777+
getNearestAncestorDirectoryWithPackageJson: maybeBind(host, host.getNearestAncestorDirectoryWithPackageJson),
17771778
};
17781779
}
17791780

tests/cases/fourslash/fourslash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ declare namespace FourSlashInterface {
610610
readonly includeCompletionsForModuleExports?: boolean;
611611
readonly includeInsertTextCompletions?: boolean;
612612
readonly includeAutomaticOptionalChainCompletions?: boolean;
613-
readonly importModuleSpecifierPreference?: "auto" | "relative" | "non-relative";
613+
readonly importModuleSpecifierPreference?: "shortest" | "external-non-relative" | "relative" | "non-relative";
614614
readonly importModuleSpecifierEnding?: "minimal" | "index" | "js";
615615
}
616616
interface CompletionsOptions {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/// <reference path="../fourslash.ts" />
2+
3+
// @Filename: /tsconfig.base.json
4+
//// {
5+
//// "compilerOptions": {
6+
//// "module": "commonjs",
7+
//// "paths": {
8+
//// "pkg-1/*": ["./packages/pkg-1/src/*"],
9+
//// "pkg-2/*": ["./packages/pkg-2/src/*"]
10+
//// }
11+
//// }
12+
//// }
13+
14+
// @Filename: /packages/pkg-1/package.json
15+
//// { "dependencies": { "pkg-2": "*" } }
16+
17+
// @Filename: /packages/pkg-1/tsconfig.json
18+
//// {
19+
//// "extends": "../../tsconfig.base.json",
20+
//// "references": [
21+
//// { "path": "../pkg-2" }
22+
//// ]
23+
//// }
24+
25+
// @Filename: /packages/pkg-1/src/index.ts
26+
//// Pkg2/*external*/
27+
28+
// @Filename: /packages/pkg-2/package.json
29+
//// { "types": "dist/index.d.ts" }
30+
31+
// @Filename: /packages/pkg-2/tsconfig.json
32+
//// {
33+
//// "extends": "../../tsconfig.base.json",
34+
//// "compilerOptions": { "outDir": "dist", "rootDir": "src", "composite": true }
35+
//// }
36+
37+
// @Filename: /packages/pkg-2/src/index.ts
38+
//// import "./utils";
39+
40+
// @Filename: /packages/pkg-2/src/utils.ts
41+
//// export const Pkg2 = {};
42+
43+
// @Filename: /packages/pkg-2/src/blah/foo/data.ts
44+
//// Pkg2/*internal*/
45+
46+
// @link: /packages/pkg-2 -> /packages/pkg-1/node_modules/pkg-2
47+
48+
format.setOption("newline", "\n");
49+
50+
goTo.marker("external");
51+
verify.importFixAtPosition([`import { Pkg2 } from "pkg-2/utils";\n\nPkg2`], /*errorCode*/ undefined, {
52+
importModuleSpecifierPreference: "external-non-relative"
53+
});
54+
55+
goTo.marker("internal");
56+
verify.importFixAtPosition([`import { Pkg2 } from "../../utils";\n\nPkg2`], /*errorCode*/ undefined, {
57+
importModuleSpecifierPreference: "external-non-relative"
58+
});

0 commit comments

Comments
 (0)