Skip to content

Commit

Permalink
tsconfig.json mixed content support
Browse files Browse the repository at this point in the history
  • Loading branch information
jramsay committed Nov 14, 2016
1 parent c87bce1 commit da7f824
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 30 deletions.
8 changes: 4 additions & 4 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ namespace ts {
* @param basePath A root directory to resolve relative path entries in the config
* file to. e.g. outDir
*/
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = []): ParsedCommandLine {
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], mixedContentFileExtensions: string[] = []): ParsedCommandLine {
const errors: Diagnostic[] = [];
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName);
Expand Down Expand Up @@ -963,7 +963,7 @@ namespace ts {
includeSpecs = ["**/*"];
}

const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors);
const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, mixedContentFileExtensions);

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

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

// Rather than requery this for each file and filespec, we query the supported extensions
// once and store it on the expansion context.
const supportedExtensions = getSupportedExtensions(options);
const supportedExtensions = getSupportedExtensions(options, mixedContentFileExtensions);

// Literal files are always included verbatim. An "include" or "exclude" specification cannot
// remove a literal file.
Expand Down
8 changes: 4 additions & 4 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1912,8 +1912,8 @@ namespace ts {
export const supportedJavascriptExtensions = [".js", ".jsx"];
const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions);

export function getSupportedExtensions(options?: CompilerOptions): string[] {
return options && options.allowJs ? allSupportedExtensions : supportedTypeScriptExtensions;
export function getSupportedExtensions(options?: CompilerOptions, mixedContentFileExtensions?: string[]): string[] {
return options && options.allowJs ? concatenate(allSupportedExtensions, mixedContentFileExtensions) : supportedTypeScriptExtensions;
}

export function hasJavaScriptFileExtension(fileName: string) {
Expand All @@ -1924,10 +1924,10 @@ namespace ts {
return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension));
}

export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions) {
export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, mixedContentFileExtensions?: string[]) {
if (!fileName) { return false; }

for (const extension of getSupportedExtensions(compilerOptions)) {
for (const extension of getSupportedExtensions(compilerOptions, mixedContentFileExtensions)) {
if (fileExtensionIs(fileName, extension)) {
return true;
}
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ namespace ts {
return resolutions;
}

export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program {
export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, mixedContentFileExtensions?: string[]): Program {
let program: Program;
let files: SourceFile[] = [];
let commonSourceDirectory: string;
Expand Down Expand Up @@ -324,7 +324,7 @@ namespace ts {
let skipDefaultLib = options.noLib;
const programDiagnostics = createDiagnosticCollection();
const currentDirectory = host.getCurrentDirectory();
const supportedExtensions = getSupportedExtensions(options);
const supportedExtensions = getSupportedExtensions(options, mixedContentFileExtensions);

// Map storing if there is emit blocking diagnostics for given input
const hasEmitBlockingDiagnostics = createFileMap<boolean>(getCanonicalFileName);
Expand Down
60 changes: 60 additions & 0 deletions src/harness/unittests/tsserverProjectSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,66 @@ namespace ts.projectSystem {
checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
});

it("tsconfig script block support", () => {
const file1 = {
path: "/a/b/f1.ts",
content: ` `
};
const file2 = {
path: "/a/b/f2.html",
content: `var hello = "hello";`
};
const config = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ compilerOptions: { allowJs: true } })
};
const host = createServerHost([file1, file2, config]);
const session = createSession(host);
openFilesForSession([file1], session);
const projectService = session.getProjectService();

// HTML file will not be included in any projects yet
checkNumberOfProjects(projectService, { configuredProjects: 1 });
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]);

// Specify .html extension as mixed content
const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { mixedContentFileExtensions: [".html"] });
session.executeCommand(configureHostRequest).response;

// HTML file still not included in the project as it is closed
checkNumberOfProjects(projectService, { configuredProjects: 1 });
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]);

// Open HTML file
projectService.applyChangesInOpenFiles(
/*openFiles*/[{ fileName: file2.path, hasMixedContent: true, scriptKind: ScriptKind.JS, content: `var hello = "hello";` }],
/*changedFiles*/undefined,
/*closedFiles*/undefined);

// Now HTML file is included in the project
checkNumberOfProjects(projectService, { configuredProjects: 1 });
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]);

// Check identifiers defined in HTML content are available in .ts file
const project = projectService.configuredProjects[0];
let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1);
assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`);

// Close HTML file
projectService.applyChangesInOpenFiles(
/*openFiles*/undefined,
/*changedFiles*/undefined,
/*closedFiles*/[file2.path]);

// HTML file is still included in project
checkNumberOfProjects(projectService, { configuredProjects: 1 });
checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]);

// Check identifiers defined in HTML content are not available in .ts file
completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5);
assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`);
});

it("project structure update is deferred if files are not added\removed", () => {
const file1 = {
path: "/a/b/f1.ts",
Expand Down
34 changes: 24 additions & 10 deletions src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ namespace ts.server {
export interface HostConfiguration {
formatCodeOptions: FormatCodeSettings;
hostInfo: string;
mixedContentFileExtensions?: string[];
}

interface ConfigFileConversionResult {
Expand All @@ -114,13 +115,13 @@ namespace ts.server {
interface FilePropertyReader<T> {
getFileName(f: T): string;
getScriptKind(f: T): ScriptKind;
hasMixedContent(f: T): boolean;
hasMixedContent(f: T, mixedContentFileExtensions: string[]): boolean;
}

const fileNamePropertyReader: FilePropertyReader<string> = {
getFileName: x => x,
getScriptKind: _ => undefined,
hasMixedContent: _ => false
hasMixedContent: (fileName, mixedContentFileExtensions) => forEach(mixedContentFileExtensions, extension => fileExtensionIs(fileName, extension))
};

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

private readonly hostConfiguration: HostConfiguration;

private changedFiles: ScriptInfo[];

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

public readonly hostConfiguration: HostConfiguration;

public lastDeletedFile: ScriptInfo;

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

this.hostConfiguration = {
formatCodeOptions: getDefaultFormatCodeSettings(this.host),
hostInfo: "Unknown host"
hostInfo: "Unknown host",
mixedContentFileExtensions: []
};

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

Expand Down Expand Up @@ -610,6 +612,9 @@ namespace ts.server {
let projectsToRemove: Project[];
for (const p of info.containingProjects) {
if (p.projectKind === ProjectKind.Configured) {
if (info.hasMixedContent) {
info.hasChanges = true;
}
// last open file in configured project - close it
if ((<ConfiguredProject>p).deleteOpenRef() === 0) {
(projectsToRemove || (projectsToRemove = [])).push(p);
Expand Down Expand Up @@ -772,7 +777,9 @@ namespace ts.server {
this.host,
getDirectoryPath(configFilename),
/*existingOptions*/ {},
configFilename);
configFilename,
/*resolutionStack*/ [],
this.hostConfiguration.mixedContentFileExtensions);

if (parsedCommandLine.errors.length) {
errors = concatenate(errors, parsedCommandLine.errors);
Expand Down Expand Up @@ -876,7 +883,7 @@ namespace ts.server {
for (const f of files) {
const rootFilename = propertyReader.getFileName(f);
const scriptKind = propertyReader.getScriptKind(f);
const hasMixedContent = propertyReader.hasMixedContent(f);
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.mixedContentFileExtensions);
if (this.host.fileExists(rootFilename)) {
const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent);
project.addRoot(info);
Expand Down Expand Up @@ -922,7 +929,7 @@ namespace ts.server {
rootFilesChanged = true;
if (!scriptInfo) {
const scriptKind = propertyReader.getScriptKind(f);
const hasMixedContent = propertyReader.hasMixedContent(f);
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.mixedContentFileExtensions);
scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent);
}
}
Expand Down Expand Up @@ -1072,6 +1079,9 @@ namespace ts.server {
}
if (openedByClient) {
info.isOpen = true;
if (hasMixedContent) {
info.hasChanges = true;
}
}
}
return info;
Expand Down Expand Up @@ -1103,6 +1113,10 @@ namespace ts.server {
mergeMaps(this.hostConfiguration.formatCodeOptions, convertFormatOptions(args.formatOptions));
this.logger.info("Format host information updated");
}
if (args.mixedContentFileExtensions) {
this.hostConfiguration.mixedContentFileExtensions = args.mixedContentFileExtensions;
this.logger.info("Host mixed content file extensions updated");
}
}
}

Expand Down Expand Up @@ -1168,12 +1182,12 @@ namespace ts.server {
}

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

// at this point if file is the part of some configured/external project then this project should be created
const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true);
this.printProjects();
return { configFileName, configFileErrors };
Expand Down
9 changes: 9 additions & 0 deletions src/server/lsHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace ts.server {
export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost, ServerLanguageServiceHost {
private compilationSettings: ts.CompilerOptions;
private mixedContentFileExtensions: string[];
private readonly resolvedModuleNames= createFileMap<Map<ResolvedModuleWithFailedLookupLocations>>();
private readonly resolvedTypeReferenceDirectives = createFileMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
private readonly getCanonicalFileName: (fileName: string) => string;
Expand Down Expand Up @@ -143,6 +144,10 @@ namespace ts.server {
return this.compilationSettings;
}

getMixedContentFileExtensions() {
return this.mixedContentFileExtensions;
}

useCaseSensitiveFileNames() {
return this.host.useCaseSensitiveFileNames;
}
Expand Down Expand Up @@ -231,5 +236,9 @@ namespace ts.server {
}
this.compilationSettings = opt;
}

setMixedContentFileExtensions(mixedContentFileExtensions: string[]) {
this.mixedContentFileExtensions = mixedContentFileExtensions || [];
}
}
}
15 changes: 12 additions & 3 deletions src/server/project.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// <reference path="..\services\services.ts" />
/// <reference path="..\services\services.ts" />
/// <reference path="utilities.ts"/>
/// <reference path="scriptInfo.ts"/>
/// <reference path="lsHost.ts"/>
Expand Down Expand Up @@ -202,6 +202,7 @@ namespace ts.server {
enableLanguageService() {
const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken);
lsHost.setCompilationSettings(this.compilerOptions);
lsHost.setMixedContentFileExtensions(this.projectService.hostConfiguration.mixedContentFileExtensions);
this.languageService = ts.createLanguageService(lsHost, this.documentRegistry);

this.lsHost = lsHost;
Expand Down Expand Up @@ -462,6 +463,10 @@ namespace ts.server {
return !hasChanges;
}

private hasChangedFiles() {
return this.rootFiles && forEach(this.rootFiles, info => info.hasChanges);
}

private setTypings(typings: SortedReadonlyArray<string>): boolean {
if (arrayIsEqualTo(this.typingFiles, typings)) {
return false;
Expand All @@ -475,7 +480,7 @@ namespace ts.server {
const oldProgram = this.program;
this.program = this.languageService.getProgram();

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

const added: string[] = [];
const removed: string[] = [];
const updated = this.rootFiles.filter(info => info.hasChanges).map(info => info.fileName);
for (const id in currentFiles) {
if (!hasProperty(lastReportedFileNames, id)) {
added.push(id);
Expand All @@ -588,9 +594,12 @@ namespace ts.server {
removed.push(id);
}
}
for (const root of this.rootFiles) {
root.hasChanges = false;
}
this.lastReportedFileNames = currentFiles;
this.lastReportedVersion = this.projectStructureVersion;
return { info, changes: { added, removed }, projectErrors: this.projectErrors };
return { info, changes: { added, removed, updated }, projectErrors: this.projectErrors };
}
else {
// unknown version - return everything
Expand Down
9 changes: 9 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,10 @@ namespace ts.server.protocol {
* List of removed files
*/
removed: string[];
/**
* List of updated files
*/
updated: string[];
}

/**
Expand Down Expand Up @@ -986,6 +990,11 @@ namespace ts.server.protocol {
* The format options to use during formatting and other code editing features.
*/
formatOptions?: FormatCodeSettings;

/**
* List of host's supported mixed content file extensions
*/
mixedContentFileExtensions?: string[];
}

/**
Expand Down
Loading

0 comments on commit da7f824

Please sign in to comment.