Only update file list when there are changes in the watched directories

This commit is contained in:
Sheetal Nandi
2017-07-11 13:38:12 -07:00
parent 9ff9476b4a
commit 62871cc0f9
5 changed files with 107 additions and 71 deletions

View File

@@ -1421,7 +1421,7 @@ namespace ts {
const options = extend(existingOptions, parsedConfig.options || {});
options.configFilePath = configFileName;
setConfigFileInOptions(options, sourceFile);
const { fileNames, wildcardDirectories } = getFileNames();
const { fileNames, wildcardDirectories, spec } = getFileNames();
return {
options,
fileNames,
@@ -1429,15 +1429,16 @@ namespace ts {
raw,
errors,
wildcardDirectories,
compileOnSave: !!raw.compileOnSave
compileOnSave: !!raw.compileOnSave,
configFileSpecs: spec
};
function getFileNames(): ExpandResult {
let fileNames: string[];
let filesSpecs: string[];
if (hasProperty(raw, "files")) {
if (isArray(raw["files"])) {
fileNames = <string[]>raw["files"];
if (fileNames.length === 0) {
filesSpecs = <string[]>raw["files"];
if (filesSpecs.length === 0) {
createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json");
}
}
@@ -1475,19 +1476,14 @@ namespace ts {
}
}
if (fileNames === undefined && includeSpecs === undefined) {
if (filesSpecs === undefined && includeSpecs === undefined) {
includeSpecs = ["**/*"];
}
const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions, sourceFile);
const result = matchFileNames(filesSpecs, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions, sourceFile);
if (result.fileNames.length === 0 && !hasProperty(raw, "files") && resolutionStack.length === 0) {
errors.push(
createCompilerDiagnostic(
Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2,
configFileName || "tsconfig.json",
JSON.stringify(includeSpecs || []),
JSON.stringify(excludeSpecs || [])));
errors.push(getErrorForNoInputFiles(result.spec, configFileName));
}
return result;
@@ -1500,6 +1496,14 @@ namespace ts {
}
}
export function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ConfigFileSpecs, configFileName?: string) {
return createCompilerDiagnostic(
Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2,
configFileName || "tsconfig.json",
JSON.stringify(includeSpecs || []),
JSON.stringify(excludeSpecs || []));
}
interface ParsedTsconfig {
raw: any;
options?: CompilerOptions;
@@ -1946,7 +1950,40 @@ 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[], extraFileExtensions: JsFileExtensionInfo[], jsonSourceFile: JsonSourceFile): ExpandResult {
function matchFileNames(filesSpecs: string[], includeSpecs: string[], excludeSpecs: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], extraFileExtensions: JsFileExtensionInfo[], jsonSourceFile: JsonSourceFile): ExpandResult {
basePath = normalizePath(basePath);
let validatedIncludeSpecs: string[], validatedExcludeSpecs: string[];
if (includeSpecs) {
validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include");
}
if (excludeSpecs) {
validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*allowTrailingRecursion*/ true, jsonSourceFile, "exclude");
}
// Wildcard directories (provided as part of a wildcard path) are stored in a
// file map that marks whether it was a regular wildcard match (with a `*` or `?` token),
// or a recursive directory. This information is used by filesystem watchers to monitor for
// new entries in these paths.
const wildcardDirectories = getWildcardDirectories(validatedIncludeSpecs, validatedExcludeSpecs, basePath, host.useCaseSensitiveFileNames);
const spec: ConfigFileSpecs = { filesSpecs, includeSpecs, excludeSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories };
return getFileNamesFromConfigSpecs(spec, basePath, options, host, extraFileExtensions);
}
/**
* Expands an array of file specifications.
*
* @param fileNames The literal file names to include.
* @param include The wildcard file specifications to include.
* @param exclude The wildcard file specifications to exclude.
* @param basePath The base path for any relative file specifications.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
*/
export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: JsFileExtensionInfo[]): ExpandResult {
basePath = normalizePath(basePath);
// The exclude spec list is converted into a regular expression, which allows us to quickly
@@ -1964,19 +2001,7 @@ namespace ts {
// via wildcard, and to handle extension priority.
const wildcardFileMap = createMap<string>();
if (include) {
include = validateSpecs(include, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include");
}
if (exclude) {
exclude = validateSpecs(exclude, errors, /*allowTrailingRecursion*/ true, jsonSourceFile, "exclude");
}
// Wildcard directories (provided as part of a wildcard path) are stored in a
// file map that marks whether it was a regular wildcard match (with a `*` or `?` token),
// or a recursive directory. This information is used by filesystem watchers to monitor for
// new entries in these paths.
const wildcardDirectories = getWildcardDirectories(include, exclude, basePath, host.useCaseSensitiveFileNames);
const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories } = spec;
// Rather than requery this for each file and filespec, we query the supported extensions
// once and store it on the expansion context.
@@ -1984,15 +2009,15 @@ namespace ts {
// Literal files are always included verbatim. An "include" or "exclude" specification cannot
// remove a literal file.
if (fileNames) {
for (const fileName of fileNames) {
if (filesSpecs) {
for (const fileName of filesSpecs) {
const file = combinePaths(basePath, fileName);
literalFileMap.set(keyMapper(file), file);
}
}
if (include && include.length > 0) {
for (const file of host.readDirectory(basePath, supportedExtensions, exclude, include, /*depth*/ undefined)) {
if (validatedIncludeSpecs && validatedIncludeSpecs.length > 0) {
for (const file of host.readDirectory(basePath, supportedExtensions, validatedExcludeSpecs, validatedIncludeSpecs, /*depth*/ undefined)) {
// If we have already included a literal or wildcard path with a
// higher priority extension, we should skip this file.
//
@@ -2020,7 +2045,8 @@ namespace ts {
const wildcardFiles = arrayFrom(wildcardFileMap.values());
return {
fileNames: literalFiles.concat(wildcardFiles),
wildcardDirectories
wildcardDirectories,
spec
};
}

View File

@@ -3731,6 +3731,7 @@ namespace ts {
errors: Diagnostic[];
wildcardDirectories?: MapLike<WatchDirectoryFlags>;
compileOnSave?: boolean;
configFileSpecs?: ConfigFileSpecs;
}
export const enum WatchDirectoryFlags {
@@ -3738,9 +3739,19 @@ namespace ts {
Recursive = 1 << 0,
}
export interface ConfigFileSpecs {
filesSpecs: string[];
includeSpecs: string[];
excludeSpecs: string[];
validatedIncludeSpecs: string[];
validatedExcludeSpecs: string[];
wildcardDirectories: MapLike<WatchDirectoryFlags>;
}
export interface ExpandResult {
fileNames: string[];
wildcardDirectories: MapLike<WatchDirectoryFlags>;
spec: ConfigFileSpecs;
}
/* @internal */

View File

@@ -1774,8 +1774,7 @@ namespace ts.projectSystem {
host.reloadFS([file1, file2, configFile]);
host.checkTimeoutQueueLength(1);
host.runQueuedTimeoutCallbacks(); // to execute throttled requests
host.checkTimeoutQueueLength(0); // TODO: update graph scheduling (instead of instant update graph)
checkNumberOfProjects(projectService, { configuredProjects: 1 });
checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]);

View File

@@ -617,33 +617,30 @@ namespace ts.server {
}
this.logger.info(`Detected source file changes: ${fileName}`);
this.throttledOperations.schedule(
project.getConfigFilePath(),
/*delay*/250,
() => this.handleFileAddOrRemoveInWatchedDirectoryOfProject(project, fileName));
}
private handleFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, triggerFile: string) {
// TODO: (sheetalkamat) this actually doesnt need to re-read the config file from the disk
// it just needs to update the file list of file names
// We might be able to do that by caching the info from first parse and add reusing this with the change in the file path
const { projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath());
this.reportConfigFileDiagnostics(project.getProjectName(), configFileErrors, triggerFile);
const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f)));
const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f)));
// We check if the project file list has changed. If so, we update the project.
if (!arrayIsEqualTo(currentRootFiles.sort(), newRootFiles.sort())) {
// For configured projects, the change is made outside the tsconfig file, and
// it is not likely to affect the project for other files opened by the client. We can
// just update the current project.
this.logger.info("Updating configured project");
this.updateConfiguredProject(project, projectOptions, configFileErrors);
// Call refreshInferredProjects to clean up inferred projects we may have
// created for the new files
const configFileSpecs = project.configFileSpecs;
const configFilename = normalizePath(project.getConfigFilePath());
// TODO: (sheetalkamat) use the host that caches - so we dont do file exists and read directory call
const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), this.host, this.hostConfiguration.extraFileExtensions);
const errors = project.getAllProjectErrors();
if (result.fileNames.length === 0) {
if (!configFileSpecs.filesSpecs) {
if (!some(errors, error => error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code)) {
errors.push(getErrorForNoInputFiles(configFileSpecs, configFilename));
}
if (!some(errors, error => error.code === Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code)) {
errors.push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename));
}
}
}
else {
filterMutate(errors, error =>
error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code &&
error.code !== Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code);
}
this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader);
// TODO: (sheetalkamat) schedule the update graph
if (!project.updateGraph()) {
this.refreshInferredProjects();
}
}
@@ -684,11 +681,6 @@ namespace ts.server {
this.reloadProjects();
}
private getCanonicalFileName(fileName: string) {
const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
return normalizePath(name);
}
private removeProject(project: Project) {
this.logger.info(`remove project: ${project.getRootFiles().toString()}`);
@@ -1009,7 +1001,7 @@ namespace ts.server {
};
}
return { success, projectOptions, configFileErrors: errors };
return { success, projectOptions, configFileErrors: errors, configFileSpecs: parsedCommandLine.configFileSpecs };
}
private exceededTotalSizeLimitForNonTsFiles<T>(name: string, options: CompilerOptions, fileNames: T[], propertyReader: FilePropertyReader<T>) {
@@ -1113,7 +1105,7 @@ namespace ts.server {
});
}
private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], clientFileName?: string) {
private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, clientFileName?: string) {
const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader);
const project = new ConfiguredProject(
configFileName,
@@ -1126,6 +1118,7 @@ namespace ts.server {
this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors);
project.configFileSpecs = configFileSpecs;
project.watchConfigFile((project, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind));
project.watchWildcards(projectOptions.wildcardDirectories);
project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path));
@@ -1156,15 +1149,15 @@ namespace ts.server {
}
private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) {
const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(configFileName);
const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName);
if (success) {
this.logger.info(`Opened configuration file ${configFileName}`);
}
return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, clientFileName);
return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, clientFileName);
}
private updateNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) {
private updateNonInferredProjectFiles<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>) {
const projectRootFilesMap = project.getRootFilesMap();
const newRootScriptInfoMap: Map<ProjectRoot> = createMap<ProjectRoot>();
@@ -1219,7 +1212,10 @@ namespace ts.server {
}
});
}
}
private updateNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) {
this.updateNonInferredProjectFiles(project, newUncheckedFiles, propertyReader);
project.setCompilerOptions(newOptions);
project.setTypeAcquisition(newTypeAcquisition);
@@ -1243,7 +1239,8 @@ namespace ts.server {
// note: the returned "success" is true does not mean the "configFileErrors" is empty.
// because we might have tolerated the errors and kept going. So always return the configFileErrors
// regardless the "success" here is true or not.
const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath());
const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath());
project.configFileSpecs = configFileSpecs;
if (!success) {
// reset project settings to default
this.updateNonInferredProject(project, [], fileNamePropertyReader, {}, {}, /*compileOnSave*/ false, configFileErrors);

View File

@@ -971,6 +971,9 @@ namespace ts.server {
private typeRootsWatchers: FileWatcher[];
readonly canonicalConfigFilePath: NormalizedPath;
/*@internal*/
configFileSpecs: ConfigFileSpecs;
private plugins: PluginModule[] = [];
/** Used for configured projects which may have multiple open roots */