Skip to content

Commit f27fe0d

Browse files
authored
Merge pull request #12153 from Microsoft/tsconfigMixedContentSupport
Adding tsconfig.json mixed content (script block) support
2 parents 798d080 + c40508c commit f27fe0d

File tree

9 files changed

+141
-22
lines changed

9 files changed

+141
-22
lines changed

src/compiler/commandLineParser.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,7 @@ namespace ts {
841841
* @param basePath A root directory to resolve relative path entries in the config
842842
* file to. e.g. outDir
843843
*/
844-
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = []): ParsedCommandLine {
844+
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], extraFileExtensions: FileExtensionInfo[] = []): ParsedCommandLine {
845845
const errors: Diagnostic[] = [];
846846
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
847847
const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName);
@@ -981,7 +981,7 @@ namespace ts {
981981
includeSpecs = ["**/*"];
982982
}
983983

984-
const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors);
984+
const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions);
985985

986986
if (result.fileNames.length === 0 && !hasProperty(json, "files") && resolutionStack.length === 0) {
987987
errors.push(
@@ -1185,7 +1185,7 @@ namespace ts {
11851185
* @param host The host used to resolve files and directories.
11861186
* @param errors An array for diagnostic reporting.
11871187
*/
1188-
function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[]): ExpandResult {
1188+
function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], extraFileExtensions: FileExtensionInfo[]): ExpandResult {
11891189
basePath = normalizePath(basePath);
11901190

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

12201220
// Rather than requery this for each file and filespec, we query the supported extensions
12211221
// once and store it on the expansion context.
1222-
const supportedExtensions = getSupportedExtensions(options);
1222+
const supportedExtensions = getSupportedExtensions(options, extraFileExtensions);
12231223

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

src/compiler/core.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1924,8 +1924,18 @@ namespace ts {
19241924
export const supportedJavascriptExtensions = [".js", ".jsx"];
19251925
const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions);
19261926

1927-
export function getSupportedExtensions(options?: CompilerOptions): string[] {
1928-
return options && options.allowJs ? allSupportedExtensions : supportedTypeScriptExtensions;
1927+
export function getSupportedExtensions(options?: CompilerOptions, extraFileExtensions?: FileExtensionInfo[]): string[] {
1928+
const needAllExtensions = options && options.allowJs;
1929+
if (!extraFileExtensions || extraFileExtensions.length === 0) {
1930+
return needAllExtensions ? allSupportedExtensions : supportedTypeScriptExtensions;
1931+
}
1932+
const extensions = (needAllExtensions ? allSupportedExtensions : supportedTypeScriptExtensions).slice(0);
1933+
for (const extInfo of extraFileExtensions) {
1934+
if (needAllExtensions || extInfo.scriptKind === ScriptKind.TS) {
1935+
extensions.push(extInfo.extension);
1936+
}
1937+
}
1938+
return extensions;
19291939
}
19301940

19311941
export function hasJavaScriptFileExtension(fileName: string) {
@@ -1936,10 +1946,10 @@ namespace ts {
19361946
return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension));
19371947
}
19381948

1939-
export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions) {
1949+
export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, extraFileExtensions?: FileExtensionInfo[]) {
19401950
if (!fileName) { return false; }
19411951

1942-
for (const extension of getSupportedExtensions(compilerOptions)) {
1952+
for (const extension of getSupportedExtensions(compilerOptions, extraFileExtensions)) {
19431953
if (fileExtensionIs(fileName, extension)) {
19441954
return true;
19451955
}

src/compiler/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3086,6 +3086,12 @@ namespace ts {
30863086
ThisProperty
30873087
}
30883088

3089+
export interface FileExtensionInfo {
3090+
extension: string;
3091+
scriptKind: ScriptKind;
3092+
isMixedContent: boolean;
3093+
}
3094+
30893095
export interface DiagnosticMessage {
30903096
key: string;
30913097
category: DiagnosticCategory;

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,67 @@ namespace ts.projectSystem {
15911591
checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
15921592
});
15931593

1594+
it("tsconfig script block support", () => {
1595+
const file1 = {
1596+
path: "/a/b/f1.ts",
1597+
content: ` `
1598+
};
1599+
const file2 = {
1600+
path: "/a/b/f2.html",
1601+
content: `var hello = "hello";`
1602+
};
1603+
const config = {
1604+
path: "/a/b/tsconfig.json",
1605+
content: JSON.stringify({ compilerOptions: { allowJs: true } })
1606+
};
1607+
const host = createServerHost([file1, file2, config]);
1608+
const session = createSession(host);
1609+
openFilesForSession([file1], session);
1610+
const projectService = session.getProjectService();
1611+
1612+
// HTML file will not be included in any projects yet
1613+
checkNumberOfProjects(projectService, { configuredProjects: 1 });
1614+
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]);
1615+
1616+
// Specify .html extension as mixed content
1617+
const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }];
1618+
const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { extraFileExtensions });
1619+
session.executeCommand(configureHostRequest).response;
1620+
1621+
// HTML file still not included in the project as it is closed
1622+
checkNumberOfProjects(projectService, { configuredProjects: 1 });
1623+
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]);
1624+
1625+
// Open HTML file
1626+
projectService.applyChangesInOpenFiles(
1627+
/*openFiles*/[{ fileName: file2.path, hasMixedContent: true, scriptKind: ScriptKind.JS, content: `var hello = "hello";` }],
1628+
/*changedFiles*/undefined,
1629+
/*closedFiles*/undefined);
1630+
1631+
// Now HTML file is included in the project
1632+
checkNumberOfProjects(projectService, { configuredProjects: 1 });
1633+
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]);
1634+
1635+
// Check identifiers defined in HTML content are available in .ts file
1636+
const project = projectService.configuredProjects[0];
1637+
let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1);
1638+
assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`);
1639+
1640+
// Close HTML file
1641+
projectService.applyChangesInOpenFiles(
1642+
/*openFiles*/undefined,
1643+
/*changedFiles*/undefined,
1644+
/*closedFiles*/[file2.path]);
1645+
1646+
// HTML file is still included in project
1647+
checkNumberOfProjects(projectService, { configuredProjects: 1 });
1648+
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]);
1649+
1650+
// Check identifiers defined in HTML content are not available in .ts file
1651+
completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5);
1652+
assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`);
1653+
});
1654+
15941655
it("project structure update is deferred if files are not added\removed", () => {
15951656
const file1 = {
15961657
path: "/a/b/f1.ts",

src/server/editorServices.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ namespace ts.server {
108108
export interface HostConfiguration {
109109
formatCodeOptions: FormatCodeSettings;
110110
hostInfo: string;
111+
extraFileExtensions?: FileExtensionInfo[];
111112
}
112113

113114
interface ConfigFileConversionResult {
@@ -132,13 +133,16 @@ namespace ts.server {
132133
interface FilePropertyReader<T> {
133134
getFileName(f: T): string;
134135
getScriptKind(f: T): ScriptKind;
135-
hasMixedContent(f: T): boolean;
136+
hasMixedContent(f: T, extraFileExtensions: FileExtensionInfo[]): boolean;
136137
}
137138

138139
const fileNamePropertyReader: FilePropertyReader<string> = {
139140
getFileName: x => x,
140141
getScriptKind: _ => undefined,
141-
hasMixedContent: _ => false
142+
hasMixedContent: (fileName, extraFileExtensions) => {
143+
const mixedContentExtensions = ts.map(ts.filter(extraFileExtensions, item => item.isMixedContent), item => item.extension);
144+
return forEach(mixedContentExtensions, extension => fileExtensionIs(fileName, extension))
145+
}
142146
};
143147

144148
const externalFilePropertyReader: FilePropertyReader<protocol.ExternalFile> = {
@@ -282,7 +286,8 @@ namespace ts.server {
282286

283287
this.hostConfiguration = {
284288
formatCodeOptions: getDefaultFormatCodeSettings(this.host),
285-
hostInfo: "Unknown host"
289+
hostInfo: "Unknown host",
290+
extraFileExtensions: []
286291
};
287292

288293
this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory());
@@ -486,7 +491,7 @@ namespace ts.server {
486491
// If a change was made inside "folder/file", node will trigger the callback twice:
487492
// one with the fileName being "folder/file", and the other one with "folder".
488493
// We don't respond to the second one.
489-
if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions())) {
494+
if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) {
490495
return;
491496
}
492497

@@ -642,6 +647,9 @@ namespace ts.server {
642647
let projectsToRemove: Project[];
643648
for (const p of info.containingProjects) {
644649
if (p.projectKind === ProjectKind.Configured) {
650+
if (info.hasMixedContent) {
651+
info.registerFileUpdate();
652+
}
645653
// last open file in configured project - close it
646654
if ((<ConfiguredProject>p).deleteOpenRef() === 0) {
647655
(projectsToRemove || (projectsToRemove = [])).push(p);
@@ -810,7 +818,9 @@ namespace ts.server {
810818
this.host,
811819
getDirectoryPath(configFilename),
812820
/*existingOptions*/ {},
813-
configFilename);
821+
configFilename,
822+
/*resolutionStack*/ [],
823+
this.hostConfiguration.extraFileExtensions);
814824

815825
if (parsedCommandLine.errors.length) {
816826
errors = concatenate(errors, parsedCommandLine.errors);
@@ -914,7 +924,7 @@ namespace ts.server {
914924
for (const f of files) {
915925
const rootFilename = propertyReader.getFileName(f);
916926
const scriptKind = propertyReader.getScriptKind(f);
917-
const hasMixedContent = propertyReader.hasMixedContent(f);
927+
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions);
918928
if (this.host.fileExists(rootFilename)) {
919929
const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent);
920930
project.addRoot(info);
@@ -960,7 +970,7 @@ namespace ts.server {
960970
rootFilesChanged = true;
961971
if (!scriptInfo) {
962972
const scriptKind = propertyReader.getScriptKind(f);
963-
const hasMixedContent = propertyReader.hasMixedContent(f);
973+
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions);
964974
scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent);
965975
}
966976
}
@@ -1110,6 +1120,9 @@ namespace ts.server {
11101120
if (info) {
11111121
if (openedByClient && !info.isScriptOpen()) {
11121122
info.open(fileContent);
1123+
if (hasMixedContent) {
1124+
info.registerFileUpdate();
1125+
}
11131126
}
11141127
else if (fileContent !== undefined) {
11151128
info.reload(fileContent);
@@ -1144,6 +1157,10 @@ namespace ts.server {
11441157
mergeMaps(this.hostConfiguration.formatCodeOptions, convertFormatOptions(args.formatOptions));
11451158
this.logger.info("Format host information updated");
11461159
}
1160+
if (args.extraFileExtensions) {
1161+
this.hostConfiguration.extraFileExtensions = args.extraFileExtensions;
1162+
this.logger.info("Host file extension mappings updated");
1163+
}
11471164
}
11481165
}
11491166

src/server/project.ts

Lines changed: 15 additions & 4 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"/>
@@ -187,6 +187,10 @@ namespace ts.server {
187187
public languageServiceEnabled = true;
188188

189189
builder: Builder;
190+
/**
191+
* Set of files names that were updated since the last call to getChangesSinceVersion.
192+
*/
193+
private updatedFileNames: Map<string>;
190194
/**
191195
* Set of files that was returned from the last call to getChangesSinceVersion.
192196
*/
@@ -480,6 +484,10 @@ namespace ts.server {
480484
this.markAsDirty();
481485
}
482486

487+
registerFileUpdate(fileName: string) {
488+
(this.updatedFileNames || (this.updatedFileNames = createMap<string>()))[fileName] = fileName;
489+
}
490+
483491
markAsDirty() {
484492
this.projectStateVersion++;
485493
}
@@ -667,10 +675,12 @@ namespace ts.server {
667675
isInferred: this.projectKind === ProjectKind.Inferred,
668676
options: this.getCompilerOptions()
669677
};
678+
const updatedFileNames = this.updatedFileNames;
679+
this.updatedFileNames = undefined;
670680
// check if requested version is the same that we have reported last time
671681
if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) {
672-
// if current structure version is the same - return info witout any changes
673-
if (this.projectStructureVersion == this.lastReportedVersion) {
682+
// if current structure version is the same - return info without any changes
683+
if (this.projectStructureVersion == this.lastReportedVersion && !updatedFileNames) {
674684
return { info, projectErrors: this.projectErrors };
675685
}
676686
// compute and return the difference
@@ -679,6 +689,7 @@ namespace ts.server {
679689

680690
const added: string[] = [];
681691
const removed: string[] = [];
692+
const updated: string[] = getOwnKeys(updatedFileNames);
682693
for (const id in currentFiles) {
683694
if (!hasProperty(lastReportedFileNames, id)) {
684695
added.push(id);
@@ -691,7 +702,7 @@ namespace ts.server {
691702
}
692703
this.lastReportedFileNames = currentFiles;
693704
this.lastReportedVersion = this.projectStructureVersion;
694-
return { info, changes: { added, removed }, projectErrors: this.projectErrors };
705+
return { info, changes: { added, removed, updated }, projectErrors: this.projectErrors };
695706
}
696707
else {
697708
// unknown version - return everything

src/server/protocol.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/**
22
* Declaration module describing the TypeScript Server protocol
33
*/
44
namespace ts.server.protocol {
@@ -918,6 +918,10 @@ namespace ts.server.protocol {
918918
* List of removed files
919919
*/
920920
removed: string[];
921+
/**
922+
* List of updated files
923+
*/
924+
updated: string[];
921925
}
922926

923927
/**
@@ -990,6 +994,11 @@ namespace ts.server.protocol {
990994
* The format options to use during formatting and other code editing features.
991995
*/
992996
formatOptions?: FormatCodeSettings;
997+
998+
/**
999+
* The host's additional supported file extensions
1000+
*/
1001+
extraFileExtensions?: FileExtensionInfo[];
9931002
}
9941003

9951004
/**

src/server/scriptInfo.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ namespace ts.server {
170170

171171
private isOpen: boolean;
172172

173-
// TODO: allow to update hasMixedContent from the outside
174173
constructor(
175174
private readonly host: ServerHost,
176175
readonly fileName: NormalizedPath,
@@ -268,6 +267,12 @@ namespace ts.server {
268267
return this.containingProjects[0];
269268
}
270269

270+
registerFileUpdate(): void {
271+
for (const p of this.containingProjects) {
272+
p.registerFileUpdate(this.path);
273+
}
274+
}
275+
271276
setFormatOptions(formatSettings: FormatCodeSettings): void {
272277
if (formatSettings) {
273278
if (!this.formatCodeSettings) {

src/server/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/// <reference path="types.d.ts" />
1+
/// <reference path="types.d.ts" />
22
/// <reference path="shared.ts" />
33

44
namespace ts.server {

0 commit comments

Comments
 (0)