Skip to content

Commit 7dd30db

Browse files
committed
tsconfig.json mixed content support
1 parent c87bce1 commit 7dd30db

File tree

14 files changed

+159
-30
lines changed

14 files changed

+159
-30
lines changed

src/compiler/commandLineParser.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ namespace ts {
826826
* @param basePath A root directory to resolve relative path entries in the config
827827
* file to. e.g. outDir
828828
*/
829-
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = []): ParsedCommandLine {
829+
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], fileExtensionMap: FileExtensionMap = {}): ParsedCommandLine {
830830
const errors: Diagnostic[] = [];
831831
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
832832
const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName);
@@ -963,7 +963,7 @@ namespace ts {
963963
includeSpecs = ["**/*"];
964964
}
965965

966-
const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors);
966+
const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, fileExtensionMap);
967967

968968
if (result.fileNames.length === 0 && !hasProperty(json, "files") && resolutionStack.length === 0) {
969969
errors.push(
@@ -1165,7 +1165,7 @@ namespace ts {
11651165
* @param host The host used to resolve files and directories.
11661166
* @param errors An array for diagnostic reporting.
11671167
*/
1168-
function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[]): ExpandResult {
1168+
function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], fileExtensionMap: FileExtensionMap): ExpandResult {
11691169
basePath = normalizePath(basePath);
11701170

11711171
// The exclude spec list is converted into a regular expression, which allows us to quickly
@@ -1199,7 +1199,7 @@ namespace ts {
11991199

12001200
// Rather than requery this for each file and filespec, we query the supported extensions
12011201
// once and store it on the expansion context.
1202-
const supportedExtensions = getSupportedExtensions(options);
1202+
const supportedExtensions = getSupportedExtensions(options, fileExtensionMap);
12031203

12041204
// Literal files are always included verbatim. An "include" or "exclude" specification cannot
12051205
// remove a literal file.

src/compiler/core.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,8 +1912,16 @@ namespace ts {
19121912
export const supportedJavascriptExtensions = [".js", ".jsx"];
19131913
const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions);
19141914

1915-
export function getSupportedExtensions(options?: CompilerOptions): string[] {
1916-
return options && options.allowJs ? allSupportedExtensions : supportedTypeScriptExtensions;
1915+
export function getSupportedExtensions(options?: CompilerOptions, fileExtensionMap?: FileExtensionMap): string[] {
1916+
let typeScriptHostExtensions: string[] = [];
1917+
let allHostExtensions: string[] = [];
1918+
if (fileExtensionMap) {
1919+
allHostExtensions = concatenate(concatenate(fileExtensionMap.javaScript, fileExtensionMap.typeScript), fileExtensionMap.mixedContent);
1920+
typeScriptHostExtensions = fileExtensionMap.typeScript;
1921+
}
1922+
const allTypeScriptExtensions = concatenate(supportedTypeScriptExtensions, typeScriptHostExtensions);
1923+
const allExtensions = concatenate(allSupportedExtensions, allHostExtensions);
1924+
return options && options.allowJs ? allExtensions : allTypeScriptExtensions;
19171925
}
19181926

19191927
export function hasJavaScriptFileExtension(fileName: string) {
@@ -1924,10 +1932,10 @@ namespace ts {
19241932
return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension));
19251933
}
19261934

1927-
export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions) {
1935+
export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, fileExtensionMap?: FileExtensionMap) {
19281936
if (!fileName) { return false; }
19291937

1930-
for (const extension of getSupportedExtensions(compilerOptions)) {
1938+
for (const extension of getSupportedExtensions(compilerOptions, fileExtensionMap)) {
19311939
if (fileExtensionIs(fileName, extension)) {
19321940
return true;
19331941
}

src/compiler/program.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ namespace ts {
289289
return resolutions;
290290
}
291291

292-
export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program {
292+
export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, fileExtensionMap?: FileExtensionMap): Program {
293293
let program: Program;
294294
let files: SourceFile[] = [];
295295
let commonSourceDirectory: string;
@@ -324,7 +324,7 @@ namespace ts {
324324
let skipDefaultLib = options.noLib;
325325
const programDiagnostics = createDiagnosticCollection();
326326
const currentDirectory = host.getCurrentDirectory();
327-
const supportedExtensions = getSupportedExtensions(options);
327+
const supportedExtensions = getSupportedExtensions(options, fileExtensionMap);
328328

329329
// Map storing if there is emit blocking diagnostics for given input
330330
const hasEmitBlockingDiagnostics = createFileMap<boolean>(getCanonicalFileName);

src/compiler/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2997,6 +2997,12 @@ namespace ts {
29972997
ThisProperty
29982998
}
29992999

3000+
export interface FileExtensionMap {
3001+
javaScript?: string[];
3002+
typeScript?: string[];
3003+
mixedContent?: string[];
3004+
}
3005+
30003006
export interface DiagnosticMessage {
30013007
key: string;
30023008
category: DiagnosticCategory;

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,66 @@ namespace ts.projectSystem {
14411441
checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
14421442
});
14431443

1444+
it("tsconfig script block support", () => {
1445+
const file1 = {
1446+
path: "/a/b/f1.ts",
1447+
content: ` `
1448+
};
1449+
const file2 = {
1450+
path: "/a/b/f2.html",
1451+
content: `var hello = "hello";`
1452+
};
1453+
const config = {
1454+
path: "/a/b/tsconfig.json",
1455+
content: JSON.stringify({ compilerOptions: { allowJs: true } })
1456+
};
1457+
const host = createServerHost([file1, file2, config]);
1458+
const session = createSession(host);
1459+
openFilesForSession([file1], session);
1460+
const projectService = session.getProjectService();
1461+
1462+
// HTML file will not be included in any projects yet
1463+
checkNumberOfProjects(projectService, { configuredProjects: 1 });
1464+
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]);
1465+
1466+
// Specify .html extension as mixed content
1467+
const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { fileExtensionMap: { mixedContent: [".html"] } });
1468+
session.executeCommand(configureHostRequest).response;
1469+
1470+
// HTML file still not included in the project as it is closed
1471+
checkNumberOfProjects(projectService, { configuredProjects: 1 });
1472+
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]);
1473+
1474+
// Open HTML file
1475+
projectService.applyChangesInOpenFiles(
1476+
/*openFiles*/[{ fileName: file2.path, hasMixedContent: true, scriptKind: ScriptKind.JS, content: `var hello = "hello";` }],
1477+
/*changedFiles*/undefined,
1478+
/*closedFiles*/undefined);
1479+
1480+
// Now HTML file is included in the project
1481+
checkNumberOfProjects(projectService, { configuredProjects: 1 });
1482+
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]);
1483+
1484+
// Check identifiers defined in HTML content are available in .ts file
1485+
const project = projectService.configuredProjects[0];
1486+
let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1);
1487+
assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`);
1488+
1489+
// Close HTML file
1490+
projectService.applyChangesInOpenFiles(
1491+
/*openFiles*/undefined,
1492+
/*changedFiles*/undefined,
1493+
/*closedFiles*/[file2.path]);
1494+
1495+
// HTML file is still included in project
1496+
checkNumberOfProjects(projectService, { configuredProjects: 1 });
1497+
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]);
1498+
1499+
// Check identifiers defined in HTML content are not available in .ts file
1500+
completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5);
1501+
assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`);
1502+
});
1503+
14441504
it("project structure update is deferred if files are not added\removed", () => {
14451505
const file1 = {
14461506
path: "/a/b/f1.ts",

src/server/editorServices.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ namespace ts.server {
9090
export interface HostConfiguration {
9191
formatCodeOptions: FormatCodeSettings;
9292
hostInfo: string;
93+
fileExtensionMap?: FileExtensionMap;
9394
}
9495

9596
interface ConfigFileConversionResult {
@@ -114,13 +115,13 @@ namespace ts.server {
114115
interface FilePropertyReader<T> {
115116
getFileName(f: T): string;
116117
getScriptKind(f: T): ScriptKind;
117-
hasMixedContent(f: T): boolean;
118+
hasMixedContent(f: T, mixedContentExtensions: string[]): boolean;
118119
}
119120

120121
const fileNamePropertyReader: FilePropertyReader<string> = {
121122
getFileName: x => x,
122123
getScriptKind: _ => undefined,
123-
hasMixedContent: _ => false
124+
hasMixedContent: (fileName, mixedContentExtensions) => forEach(mixedContentExtensions, extension => fileExtensionIs(fileName, extension))
124125
};
125126

126127
const externalFilePropertyReader: FilePropertyReader<protocol.ExternalFile> = {
@@ -235,12 +236,12 @@ namespace ts.server {
235236
private readonly directoryWatchers: DirectoryWatchers;
236237
private readonly throttledOperations: ThrottledOperations;
237238

238-
private readonly hostConfiguration: HostConfiguration;
239-
240239
private changedFiles: ScriptInfo[];
241240

242241
private toCanonicalFileName: (f: string) => string;
243242

243+
public readonly hostConfiguration: HostConfiguration;
244+
244245
public lastDeletedFile: ScriptInfo;
245246

246247
constructor(public readonly host: ServerHost,
@@ -264,7 +265,8 @@ namespace ts.server {
264265

265266
this.hostConfiguration = {
266267
formatCodeOptions: getDefaultFormatCodeSettings(this.host),
267-
hostInfo: "Unknown host"
268+
hostInfo: "Unknown host",
269+
fileExtensionMap: {}
268270
};
269271

270272
this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory());
@@ -455,7 +457,7 @@ namespace ts.server {
455457
// If a change was made inside "folder/file", node will trigger the callback twice:
456458
// one with the fileName being "folder/file", and the other one with "folder".
457459
// We don't respond to the second one.
458-
if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions())) {
460+
if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.fileExtensionMap)) {
459461
return;
460462
}
461463

@@ -610,6 +612,9 @@ namespace ts.server {
610612
let projectsToRemove: Project[];
611613
for (const p of info.containingProjects) {
612614
if (p.projectKind === ProjectKind.Configured) {
615+
if (info.hasMixedContent) {
616+
info.hasChanges = true;
617+
}
613618
// last open file in configured project - close it
614619
if ((<ConfiguredProject>p).deleteOpenRef() === 0) {
615620
(projectsToRemove || (projectsToRemove = [])).push(p);
@@ -772,7 +777,9 @@ namespace ts.server {
772777
this.host,
773778
getDirectoryPath(configFilename),
774779
/*existingOptions*/ {},
775-
configFilename);
780+
configFilename,
781+
/*resolutionStack*/ [],
782+
this.hostConfiguration.fileExtensionMap);
776783

777784
if (parsedCommandLine.errors.length) {
778785
errors = concatenate(errors, parsedCommandLine.errors);
@@ -876,7 +883,7 @@ namespace ts.server {
876883
for (const f of files) {
877884
const rootFilename = propertyReader.getFileName(f);
878885
const scriptKind = propertyReader.getScriptKind(f);
879-
const hasMixedContent = propertyReader.hasMixedContent(f);
886+
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap.mixedContent);
880887
if (this.host.fileExists(rootFilename)) {
881888
const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent);
882889
project.addRoot(info);
@@ -922,7 +929,7 @@ namespace ts.server {
922929
rootFilesChanged = true;
923930
if (!scriptInfo) {
924931
const scriptKind = propertyReader.getScriptKind(f);
925-
const hasMixedContent = propertyReader.hasMixedContent(f);
932+
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap.mixedContent);
926933
scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent);
927934
}
928935
}
@@ -1072,6 +1079,9 @@ namespace ts.server {
10721079
}
10731080
if (openedByClient) {
10741081
info.isOpen = true;
1082+
if (hasMixedContent) {
1083+
info.hasChanges = true;
1084+
}
10751085
}
10761086
}
10771087
return info;
@@ -1103,6 +1113,10 @@ namespace ts.server {
11031113
mergeMaps(this.hostConfiguration.formatCodeOptions, convertFormatOptions(args.formatOptions));
11041114
this.logger.info("Format host information updated");
11051115
}
1116+
if (args.fileExtensionMap) {
1117+
this.hostConfiguration.fileExtensionMap = args.fileExtensionMap;
1118+
this.logger.info("Host file extension mappings updated");
1119+
}
11061120
}
11071121
}
11081122

@@ -1168,12 +1182,12 @@ namespace ts.server {
11681182
}
11691183

11701184
openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean): OpenConfiguredProjectResult {
1185+
const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
11711186
const { configFileName = undefined, configFileErrors = undefined }: OpenConfiguredProjectResult = this.findContainingExternalProject(fileName)
11721187
? {}
11731188
: this.openOrUpdateConfiguredProjectForFile(fileName);
11741189

11751190
// at this point if file is the part of some configured/external project then this project should be created
1176-
const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
11771191
this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true);
11781192
this.printProjects();
11791193
return { configFileName, configFileErrors };

src/server/lsHost.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace ts.server {
66
export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost, ServerLanguageServiceHost {
77
private compilationSettings: ts.CompilerOptions;
8+
private fileExtensionMap: FileExtensionMap;
89
private readonly resolvedModuleNames= createFileMap<Map<ResolvedModuleWithFailedLookupLocations>>();
910
private readonly resolvedTypeReferenceDirectives = createFileMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
1011
private readonly getCanonicalFileName: (fileName: string) => string;
@@ -143,6 +144,10 @@ namespace ts.server {
143144
return this.compilationSettings;
144145
}
145146

147+
getFileExtensionMap() {
148+
return this.fileExtensionMap;
149+
}
150+
146151
useCaseSensitiveFileNames() {
147152
return this.host.useCaseSensitiveFileNames;
148153
}
@@ -231,5 +236,9 @@ namespace ts.server {
231236
}
232237
this.compilationSettings = opt;
233238
}
239+
240+
setFileExtensionMap(fileExtensionMap: FileExtensionMap) {
241+
this.fileExtensionMap = fileExtensionMap || {};
242+
}
234243
}
235244
}

src/server/project.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/// <reference path="..\services\services.ts" />
1+
/// <reference path="..\services\services.ts" />
22
/// <reference path="utilities.ts"/>
33
/// <reference path="scriptInfo.ts"/>
44
/// <reference path="lsHost.ts"/>
@@ -202,6 +202,7 @@ namespace ts.server {
202202
enableLanguageService() {
203203
const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken);
204204
lsHost.setCompilationSettings(this.compilerOptions);
205+
lsHost.setFileExtensionMap(this.projectService.hostConfiguration.fileExtensionMap);
205206
this.languageService = ts.createLanguageService(lsHost, this.documentRegistry);
206207

207208
this.lsHost = lsHost;
@@ -462,6 +463,10 @@ namespace ts.server {
462463
return !hasChanges;
463464
}
464465

466+
private hasChangedFiles() {
467+
return this.rootFiles && forEach(this.rootFiles, info => info.hasChanges);
468+
}
469+
465470
private setTypings(typings: SortedReadonlyArray<string>): boolean {
466471
if (arrayIsEqualTo(this.typingFiles, typings)) {
467472
return false;
@@ -475,7 +480,7 @@ namespace ts.server {
475480
const oldProgram = this.program;
476481
this.program = this.languageService.getProgram();
477482

478-
let hasChanges = false;
483+
let hasChanges = this.hasChangedFiles();
479484
// bump up the version if
480485
// - oldProgram is not set - this is a first time updateGraph is called
481486
// - newProgram is different from the old program and structure of the old program was not reused.
@@ -578,6 +583,7 @@ namespace ts.server {
578583

579584
const added: string[] = [];
580585
const removed: string[] = [];
586+
const updated = this.rootFiles.filter(info => info.hasChanges).map(info => info.fileName);
581587
for (const id in currentFiles) {
582588
if (!hasProperty(lastReportedFileNames, id)) {
583589
added.push(id);
@@ -588,9 +594,12 @@ namespace ts.server {
588594
removed.push(id);
589595
}
590596
}
597+
for (const root of this.rootFiles) {
598+
root.hasChanges = false;
599+
}
591600
this.lastReportedFileNames = currentFiles;
592601
this.lastReportedVersion = this.projectStructureVersion;
593-
return { info, changes: { added, removed }, projectErrors: this.projectErrors };
602+
return { info, changes: { added, removed, updated }, projectErrors: this.projectErrors };
594603
}
595604
else {
596605
// unknown version - return everything

0 commit comments

Comments
 (0)