Skip to content

Commit fecc7c4

Browse files
committed
add support of extension preference
1 parent e4d4b0a commit fecc7c4

File tree

6 files changed

+251
-17
lines changed

6 files changed

+251
-17
lines changed

src/compiler/moduleSpecifiers.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace ts.moduleSpecifiers {
44
export interface ModuleSpecifierPreferences {
55
readonly importModuleSpecifierPreference?: "relative" | "non-relative";
6+
readonly includeExtensionInImports?: boolean
67
}
78

89
// Note: importingSourceFile is just for usesJsExtensionOnImports
@@ -17,7 +18,7 @@ namespace ts.moduleSpecifiers {
1718
): string {
1819
const info = getInfo(compilerOptions, importingSourceFile, importingSourceFileName, host);
1920
const modulePaths = getAllModulePaths(files, toFileName, info.getCanonicalFileName, host);
20-
return firstDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions)) ||
21+
return firstDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions, preferences)) ||
2122
first(getLocalModuleSpecifiers(toFileName, info, compilerOptions, preferences));
2223
}
2324

@@ -39,46 +40,47 @@ namespace ts.moduleSpecifiers {
3940
}
4041
const modulePaths = getAllModulePaths(files, getSourceFileOfNode(moduleSymbol.valueDeclaration).fileName, info.getCanonicalFileName, host);
4142

42-
const global = mapDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions));
43+
const global = mapDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions, preferences));
4344
return global.length ? global.map(g => [g]) : modulePaths.map(moduleFileName =>
4445
getLocalModuleSpecifiers(moduleFileName, info, compilerOptions, preferences));
4546
}
4647

4748
interface Info {
4849
readonly moduleResolutionKind: ModuleResolutionKind;
49-
readonly addJsExtension: boolean;
50+
readonly addExtension: boolean;
5051
readonly getCanonicalFileName: GetCanonicalFileName;
5152
readonly sourceDirectory: Path;
5253
}
5354
// importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path
5455
function getInfo(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info {
5556
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
56-
const addJsExtension = usesJsExtensionOnImports(importingSourceFile);
57+
const addExtension = usesExtensionOnImports(importingSourceFile);
5758
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true);
5859
const sourceDirectory = getDirectoryPath(importingSourceFileName);
59-
return { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory };
60+
return { moduleResolutionKind, addExtension, getCanonicalFileName, sourceDirectory };
6061
}
6162

6263
function getGlobalModuleSpecifier(
6364
moduleFileName: string,
64-
{ addJsExtension, getCanonicalFileName, sourceDirectory }: Info,
65+
{ addExtension, getCanonicalFileName, sourceDirectory }: Info,
6566
host: ModuleSpecifierResolutionHost,
6667
compilerOptions: CompilerOptions,
68+
preferences: ModuleSpecifierPreferences
6769
) {
68-
return tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension)
70+
return tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addExtension, preferences)
6971
|| tryGetModuleNameAsNodeModule(compilerOptions, moduleFileName, host, getCanonicalFileName, sourceDirectory);
7072
}
7173

7274
function getLocalModuleSpecifiers(
7375
moduleFileName: string,
74-
{ moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory }: Info,
76+
{ moduleResolutionKind, addExtension, getCanonicalFileName, sourceDirectory }: Info,
7577
compilerOptions: CompilerOptions,
7678
preferences: ModuleSpecifierPreferences,
7779
): ReadonlyArray<string> {
7880
const { baseUrl, paths, rootDirs } = compilerOptions;
7981

8082
const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName) ||
81-
removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension);
83+
removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addExtension, compilerOptions, preferences);
8284
if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") {
8385
return [relativePath];
8486
}
@@ -88,7 +90,7 @@ namespace ts.moduleSpecifiers {
8890
return [relativePath];
8991
}
9092

91-
const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, moduleResolutionKind, addJsExtension);
93+
const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, moduleResolutionKind, addExtension, compilerOptions, preferences);
9294
if (paths) {
9395
const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths);
9496
if (fromPaths) {
@@ -138,8 +140,8 @@ namespace ts.moduleSpecifiers {
138140
return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath];
139141
}
140142

141-
function usesJsExtensionOnImports({ imports }: SourceFile): boolean {
142-
return firstDefined(imports, ({ text }) => pathIsRelative(text) ? fileExtensionIs(text, Extension.Js) : undefined) || false;
143+
function usesExtensionOnImports({ imports }: SourceFile): boolean {
144+
return firstDefined(imports, ({ text }) => pathIsRelative(text) ? fileExtensionIsOneOf(text, [Extension.Js, Extension.Jsx]) : undefined) || false;
143145
}
144146

145147
function discoverProbableSymlinks(files: ReadonlyArray<SourceFile>, getCanonicalFileName: (file: string) => string, host: ModuleSpecifierResolutionHost) {
@@ -256,14 +258,15 @@ namespace ts.moduleSpecifiers {
256258
host: GetEffectiveTypeRootsHost,
257259
getCanonicalFileName: (file: string) => string,
258260
moduleFileName: string,
259-
addJsExtension: boolean,
261+
addExtension: boolean,
262+
preferences: ModuleSpecifierPreferences
260263
): string | undefined {
261264
const roots = getEffectiveTypeRoots(options, host);
262265
return firstDefined(roots, unNormalizedTypeRoot => {
263266
const typeRoot = toPath(unNormalizedTypeRoot, /*basePath*/ undefined, getCanonicalFileName);
264267
if (startsWith(moduleFileName, typeRoot)) {
265268
// For a type definition, we can strip `/index` even with classic resolution.
266-
return removeExtensionAndIndexPostFix(moduleFileName.substring(typeRoot.length + 1), ModuleResolutionKind.NodeJs, addJsExtension);
269+
return removeExtensionAndIndexPostFix(moduleFileName.substring(typeRoot.length + 1), ModuleResolutionKind.NodeJs, addExtension, options, preferences);
267270
}
268271
});
269272
}
@@ -408,10 +411,25 @@ namespace ts.moduleSpecifiers {
408411
});
409412
}
410413

411-
function removeExtensionAndIndexPostFix(fileName: string, moduleResolutionKind: ModuleResolutionKind, addJsExtension: boolean): string {
414+
function tryGetActualExtension(text: string, compilerOptions: CompilerOptions) {
415+
const extension = pathIsRelative(text) && tryGetExtensionFromPath(text);
416+
if (!extension) return undefined;
417+
418+
switch (extension) {
419+
case Extension.Ts:
420+
return Extension.Js;
421+
case Extension.Tsx:
422+
return compilerOptions.jsx === JsxEmit.React || compilerOptions.jsx === JsxEmit.ReactNative ? Extension.Js : Extension.Jsx;
423+
default:
424+
return extension;
425+
}
426+
}
427+
428+
function removeExtensionAndIndexPostFix(fileName: string, moduleResolutionKind: ModuleResolutionKind, addJsExtension: boolean, compilerOptions: CompilerOptions, preferences: ModuleSpecifierPreferences): string {
412429
const noExtension = removeFileExtension(fileName);
413-
return addJsExtension
414-
? noExtension + ".js"
430+
const actualExtension = tryGetActualExtension(fileName, compilerOptions);
431+
return (actualExtension && (preferences.includeExtensionInImports !== undefined && preferences.includeExtensionInImports || addJsExtension))
432+
? noExtension + actualExtension
415433
: moduleResolutionKind === ModuleResolutionKind.NodeJs
416434
? removeSuffix(noExtension, "/index")
417435
: noExtension;

src/server/protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2802,6 +2802,7 @@ namespace ts.server.protocol {
28022802
readonly includeCompletionsWithInsertText?: boolean;
28032803
readonly importModuleSpecifierPreference?: "relative" | "non-relative";
28042804
readonly allowTextChangesInNewFiles?: boolean;
2805+
readonly includeExtensionInImports?: boolean
28052806
}
28062807

28072808
export interface CompilerOptions {

src/services/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ namespace ts {
240240
readonly includeCompletionsWithInsertText?: boolean;
241241
readonly importModuleSpecifierPreference?: "relative" | "non-relative";
242242
readonly allowTextChangesInNewFiles?: boolean;
243+
readonly includeExtensionInImports?: boolean
243244
}
244245
/* @internal */
245246
export const emptyOptions = {};

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4303,6 +4303,103 @@ declare namespace ts {
43034303
function createAbstractBuilder(rootNames: ReadonlyArray<string> | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: ReadonlyArray<Diagnostic>): BuilderProgram;
43044304
}
43054305
declare namespace ts {
4306+
<<<<<<< HEAD
4307+
=======
4308+
/** This is the cache of module/typedirectives resolution that can be retained across program */
4309+
interface ResolutionCache {
4310+
startRecordingFilesWithChangedResolutions(): void;
4311+
finishRecordingFilesWithChangedResolutions(): Path[] | undefined;
4312+
resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined): ResolvedModuleFull[];
4313+
getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): CachedResolvedModuleWithFailedLookupLocations | undefined;
4314+
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
4315+
invalidateResolutionOfFile(filePath: Path): void;
4316+
removeResolutionsOfFile(filePath: Path): void;
4317+
setFilesWithInvalidatedNonRelativeUnresolvedImports(filesWithUnresolvedImports: Map<ReadonlyArray<string>>): void;
4318+
createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution;
4319+
startCachingPerDirectoryResolution(): void;
4320+
finishCachingPerDirectoryResolution(): void;
4321+
updateTypeRootsWatch(): void;
4322+
closeTypeRootsWatch(): void;
4323+
clear(): void;
4324+
}
4325+
interface ResolutionWithFailedLookupLocations {
4326+
readonly failedLookupLocations: ReadonlyArray<string>;
4327+
isInvalidated?: boolean;
4328+
refCount?: number;
4329+
}
4330+
interface CachedResolvedModuleWithFailedLookupLocations extends ResolvedModuleWithFailedLookupLocations, ResolutionWithFailedLookupLocations {
4331+
}
4332+
interface ResolutionCacheHost extends ModuleResolutionHost {
4333+
toPath(fileName: string): Path;
4334+
getCanonicalFileName: GetCanonicalFileName;
4335+
getCompilationSettings(): CompilerOptions;
4336+
watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher;
4337+
onInvalidatedResolution(): void;
4338+
watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher;
4339+
onChangedAutomaticTypeDirectiveNames(): void;
4340+
getCachedDirectoryStructureHost(): CachedDirectoryStructureHost | undefined;
4341+
projectName?: string;
4342+
getGlobalCache?(): string | undefined;
4343+
writeLog(s: string): void;
4344+
maxNumberOfFilesToIterateForInvalidation?: number;
4345+
getCurrentProgram(): Program;
4346+
}
4347+
const maxNumberOfFilesToIterateForInvalidation = 256;
4348+
function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, logChangesWhenResolvingModule: boolean): ResolutionCache;
4349+
}
4350+
declare namespace ts.moduleSpecifiers {
4351+
interface ModuleSpecifierPreferences {
4352+
readonly importModuleSpecifierPreference?: "relative" | "non-relative";
4353+
readonly includeExtensionInImports?: boolean;
4354+
}
4355+
function getModuleSpecifier(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, files: ReadonlyArray<SourceFile>, preferences?: ModuleSpecifierPreferences): string;
4356+
function getModuleSpecifiers(moduleSymbol: Symbol, compilerOptions: CompilerOptions, importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, files: ReadonlyArray<SourceFile>, preferences: ModuleSpecifierPreferences): ReadonlyArray<ReadonlyArray<string>>;
4357+
}
4358+
declare namespace ts {
4359+
/**
4360+
* Create a function that reports error by writing to the system and handles the formating of the diagnostic
4361+
*/
4362+
function createDiagnosticReporter(system: System, pretty?: boolean): DiagnosticReporter;
4363+
/** @internal */
4364+
const nonClearingMessageCodes: number[];
4365+
/** @internal */
4366+
const screenStartingMessageCodes: number[];
4367+
/**
4368+
* Create a function that reports watch status by writing to the system and handles the formating of the diagnostic
4369+
*/
4370+
function createWatchStatusReporter(system: System, pretty?: boolean): WatchStatusReporter;
4371+
/** Parses config file using System interface */
4372+
function parseConfigFileWithSystem(configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter): ParsedCommandLine | undefined;
4373+
/**
4374+
* Program structure needed to emit the files and report diagnostics
4375+
*/
4376+
interface ProgramToEmitFilesAndReportErrors {
4377+
getCurrentDirectory(): string;
4378+
getCompilerOptions(): CompilerOptions;
4379+
getSourceFiles(): ReadonlyArray<SourceFile>;
4380+
getSyntacticDiagnostics(): ReadonlyArray<Diagnostic>;
4381+
getOptionsDiagnostics(): ReadonlyArray<Diagnostic>;
4382+
getGlobalDiagnostics(): ReadonlyArray<Diagnostic>;
4383+
getSemanticDiagnostics(): ReadonlyArray<Diagnostic>;
4384+
getConfigFileParsingDiagnostics(): ReadonlyArray<Diagnostic>;
4385+
emit(): EmitResult;
4386+
}
4387+
type ReportEmitErrorSummary = (errorCount: number) => void;
4388+
/**
4389+
* Helper that emit files, report diagnostics and lists emitted and/or source files depending on compiler options
4390+
*/
4391+
function emitFilesAndReportErrors(program: ProgramToEmitFilesAndReportErrors, reportDiagnostic: DiagnosticReporter, writeFileName?: (s: string) => void, reportSummary?: ReportEmitErrorSummary): ExitStatus;
4392+
/**
4393+
* Creates the watch compiler host from system for config file in watch mode
4394+
*/
4395+
function createWatchCompilerHostOfConfigFile<T extends BuilderProgram = EmitAndSemanticDiagnosticsBuilderProgram>(configFileName: string, optionsToExtend: CompilerOptions | undefined, system: System, createProgram?: CreateProgram<T>, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfConfigFile<T>;
4396+
/**
4397+
* Creates the watch compiler host from system for compiling root files and options in watch mode
4398+
*/
4399+
function createWatchCompilerHostOfFilesAndCompilerOptions<T extends BuilderProgram = EmitAndSemanticDiagnosticsBuilderProgram>(rootFiles: string[], options: CompilerOptions, system: System, createProgram?: CreateProgram<T>, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfFilesAndCompilerOptions<T>;
4400+
}
4401+
declare namespace ts {
4402+
>>>>>>> add support of extension preference
43064403
type WatchStatusReporter = (diagnostic: Diagnostic, newLine: string, options: CompilerOptions) => void;
43074404
/** Create the program with rootNames and options, if they are undefined, oldProgram and new configFile diagnostics create new program */
43084405
type CreateProgram<T extends BuilderProgram> = (rootNames: ReadonlyArray<string> | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: T, configFileParsingDiagnostics?: ReadonlyArray<Diagnostic>) => T;
@@ -4811,6 +4908,7 @@ declare namespace ts {
48114908
readonly includeCompletionsWithInsertText?: boolean;
48124909
readonly importModuleSpecifierPreference?: "relative" | "non-relative";
48134910
readonly allowTextChangesInNewFiles?: boolean;
4911+
readonly includeExtensionInImports?: boolean;
48144912
}
48154913
interface LanguageService {
48164914
cleanupSemanticCache(): void;
@@ -7962,6 +8060,7 @@ declare namespace ts.server.protocol {
79628060
readonly includeCompletionsWithInsertText?: boolean;
79638061
readonly importModuleSpecifierPreference?: "relative" | "non-relative";
79648062
readonly allowTextChangesInNewFiles?: boolean;
8063+
readonly includeExtensionInImports?: boolean;
79658064
}
79668065
interface CompilerOptions {
79678066
allowJs?: boolean;

tests/baselines/reference/api/typescript.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4811,6 +4811,7 @@ declare namespace ts {
48114811
readonly includeCompletionsWithInsertText?: boolean;
48124812
readonly importModuleSpecifierPreference?: "relative" | "non-relative";
48134813
readonly allowTextChangesInNewFiles?: boolean;
4814+
readonly includeExtensionInImports?: boolean;
48144815
}
48154816
interface LanguageService {
48164817
cleanupSemanticCache(): void;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @moduleResolution: node
4+
// @noLib: true
5+
// @allowJs: true
6+
// @checkJs: true
7+
// @jsx: preserve
8+
9+
// @Filename: /a.js
10+
////export function a() {}
11+
12+
// @Filename: /b.ts
13+
////export function b() {}
14+
15+
// @Filename: /c.jsx
16+
////export function c() {}
17+
18+
// @Filename: /d.tsx
19+
////export function d() {}
20+
21+
// @Filename: /normalExt.ts
22+
////a;
23+
////b;
24+
////c;
25+
////d;
26+
27+
// @Filename: /includeExt.ts
28+
////a;
29+
////b;
30+
////c;
31+
////d;
32+
33+
// @Filename: /includeExt.js
34+
////a;
35+
////b;
36+
////c;
37+
////d;
38+
39+
40+
goTo.file("/normalExt.ts");
41+
verify.importFixAtPosition([
42+
`import { a } from "./a";
43+
44+
a;
45+
b;
46+
c;
47+
d;`, `import { b } from "./b";
48+
49+
a;
50+
b;
51+
c;
52+
d;`, `import { c } from "./c";
53+
54+
a;
55+
b;
56+
c;
57+
d;`, `import { d } from "./d";
58+
59+
a;
60+
b;
61+
c;
62+
d;`]);
63+
64+
goTo.file("/includeExt.ts");
65+
verify.importFixAtPosition([
66+
`import { a } from "./a.js";
67+
68+
a;
69+
b;
70+
c;
71+
d;`, `import { b } from "./b.js";
72+
73+
a;
74+
b;
75+
c;
76+
d;`, `import { c } from "./c.jsx";
77+
78+
a;
79+
b;
80+
c;
81+
d;`, `import { d } from "./d.jsx";
82+
83+
a;
84+
b;
85+
c;
86+
d;`], /* errorCode */ undefined, {
87+
includeExtensionInImports: true
88+
});
89+
90+
goTo.file("/includeExt.js");
91+
verify.importFixAtPosition([
92+
`import { a } from "./a.js";
93+
94+
a;
95+
b;
96+
c;
97+
d;`, `import { b } from "./b.js";
98+
99+
a;
100+
b;
101+
c;
102+
d;`, `import { c } from "./c.jsx";
103+
104+
a;
105+
b;
106+
c;
107+
d;`, `import { d } from "./d.jsx";
108+
109+
a;
110+
b;
111+
c;
112+
d;`], /* errorCode */ undefined, {
113+
includeExtensionInImports: true
114+
});

0 commit comments

Comments
 (0)