Add watchOptions to tsconfig and allow supplying them on command line as well (#35615)

* Create different watch options in compiler options

* Thread through the new watch options

* Actually use the options passed through for watch strategy

* Support delay on updating child directory watches

* Make watchOptions separate from compilerOptions

* Support passing watch options from command line

* Handle displaying of watchOptions
This commit is contained in:
Sheetal Nandi
2019-12-11 13:26:44 -08:00
committed by GitHub
parent 4212484ae1
commit 236012e47b
40 changed files with 2417 additions and 720 deletions
+65 -16
View File
@@ -170,6 +170,7 @@ namespace ts.server {
}
const compilerOptionConverters = prepareConvertersForEnumLikeCompilerOptions(optionDeclarations);
const watchOptionsConverters = prepareConvertersForEnumLikeCompilerOptions(optionsForWatch);
const indentStyle = createMapFromTemplate({
none: IndentStyle.None,
block: IndentStyle.Block,
@@ -247,6 +248,18 @@ namespace ts.server {
return <any>protocolOptions;
}
export function convertWatchOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): WatchOptions | undefined {
let result: WatchOptions | undefined;
watchOptionsConverters.forEach((mappedValues, id) => {
const propertyValue = protocolOptions[id];
if (propertyValue === undefined) return;
(result || (result = {}))[id] = isString(propertyValue) ?
mappedValues.get(propertyValue.toLowerCase()) :
propertyValue;
});
return result;
}
export function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind {
return isString(scriptKindName) ? convertScriptKindName(scriptKindName) : scriptKindName;
}
@@ -277,6 +290,7 @@ namespace ts.server {
preferences: protocol.UserPreferences;
hostInfo: string;
extraFileExtensions?: FileExtensionInfo[];
watchOptions?: WatchOptions;
}
export interface OpenConfiguredProjectResult {
@@ -545,6 +559,8 @@ namespace ts.server {
private compilerOptionsForInferredProjects: CompilerOptions | undefined;
private compilerOptionsForInferredProjectsPerProjectRoot = createMap<CompilerOptions>();
private watchOptionsForInferredProjects: WatchOptions | undefined;
private watchOptionsForInferredProjectsPerProjectRoot = createMap<WatchOptions | false>();
/**
* Project size for configured or external projects
*/
@@ -638,7 +654,7 @@ namespace ts.server {
formatCodeOptions: getDefaultFormatCodeSettings(this.host.newLine),
preferences: emptyOptions,
hostInfo: "Unknown host",
extraFileExtensions: []
extraFileExtensions: [],
};
this.documentRegistry = createDocumentRegistryInternal(this.host.useCaseSensitiveFileNames, this.currentDirectory, this);
@@ -852,6 +868,7 @@ namespace ts.server {
Debug.assert(projectRootPath === undefined || this.useInferredProjectPerProjectRoot, "Setting compiler options per project root path is only supported when useInferredProjectPerProjectRoot is enabled");
const compilerOptions = convertCompilerOptions(projectCompilerOptions);
const watchOptions = convertWatchOptions(projectCompilerOptions);
// always set 'allowNonTsExtensions' for inferred projects since user cannot configure it from the outside
// previously we did not expose a way for user to change these settings and this option was enabled by default
@@ -859,9 +876,11 @@ namespace ts.server {
const canonicalProjectRootPath = projectRootPath && this.toCanonicalFileName(projectRootPath);
if (canonicalProjectRootPath) {
this.compilerOptionsForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, compilerOptions);
this.watchOptionsForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, watchOptions || false);
}
else {
this.compilerOptionsForInferredProjects = compilerOptions;
this.watchOptionsForInferredProjects = watchOptions;
}
for (const project of this.inferredProjects) {
@@ -877,6 +896,7 @@ namespace ts.server {
project.projectRootPath === canonicalProjectRootPath :
!project.projectRootPath || !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath)) {
project.setCompilerOptions(compilerOptions);
project.setWatchOptions(watchOptions);
project.compileOnSaveEnabled = compilerOptions.compileOnSave!;
project.markAsDirty();
this.delayUpdateProjectGraph(project);
@@ -1101,6 +1121,7 @@ namespace ts.server {
}
},
flags,
this.getWatchOptions(project),
WatchType.WildcardDirectory,
project
);
@@ -1112,7 +1133,8 @@ namespace ts.server {
return this.configFileExistenceInfoCache.get(project.canonicalConfigFilePath)!;
}
private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) {
/*@internal*/
onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) {
const configFileExistenceInfo = this.getConfigFileExistenceInfo(project);
if (eventKind === FileWatcherEventKind.Deleted) {
// Update the cached status
@@ -1464,6 +1486,7 @@ namespace ts.server {
configFileName,
(_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind),
PollingInterval.High,
this.hostConfiguration.watchOptions,
WatchType.ConfigFileForInferredRoot
) :
noopFileWatcher;
@@ -1727,6 +1750,7 @@ namespace ts.server {
private createExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition, excludedFiles: NormalizedPath[]) {
const compilerOptions = convertCompilerOptions(options);
const watchOptions = convertWatchOptions(options);
const project = new ExternalProject(
projectFileName,
this,
@@ -1734,7 +1758,10 @@ namespace ts.server {
compilerOptions,
/*lastFileExceededProgramSize*/ this.getFilenameForExceededTotalSizeLimitForNonTsFiles(projectFileName, compilerOptions, files, externalFilePropertyReader),
options.compileOnSave === undefined ? true : options.compileOnSave,
/*projectFilePath*/ undefined, this.currentPluginConfigOverrides);
/*projectFilePath*/ undefined,
this.currentPluginConfigOverrides,
watchOptions
);
project.excludedFiles = excludedFiles;
this.addFilesToNonInferredProject(project, files, externalFilePropertyReader, typeAcquisition);
@@ -1805,14 +1832,7 @@ namespace ts.server {
this.documentRegistry,
cachedDirectoryStructureHost);
// TODO: We probably should also watch the configFiles that are extended
project.configFileWatcher = this.watchFactory.watchFile(
this.host,
configFileName,
(_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind),
PollingInterval.High,
WatchType.ConfigFile,
project
);
project.createConfigFileWatcher();
this.configuredProjects.set(project.canonicalConfigFilePath, project);
this.setConfigFileExistenceByNewConfiguredProject(project);
return project;
@@ -1864,7 +1884,9 @@ namespace ts.server {
/*existingOptions*/ {},
configFilename,
/*resolutionStack*/[],
this.hostConfiguration.extraFileExtensions);
this.hostConfiguration.extraFileExtensions,
/*extendedConfigCache*/ undefined,
);
if (parsedCommandLine.errors.length) {
configFileErrors.push(...parsedCommandLine.errors);
@@ -1898,12 +1920,14 @@ namespace ts.server {
project.stopWatchingWildCards();
}
else {
project.setCompilerOptions(compilerOptions);
project.setWatchOptions(parsedCommandLine.watchOptions);
project.enableLanguageService();
project.watchWildcards(createMapFromTemplate(parsedCommandLine.wildcardDirectories!)); // TODO: GH#18217
}
project.enablePluginsWithOptions(compilerOptions, this.currentPluginConfigOverrides);
const filesToAdd = parsedCommandLine.fileNames.concat(project.getExternalFiles());
this.updateRootAndOptionsOfNonInferredProject(project, filesToAdd, fileNamePropertyReader, compilerOptions, parsedCommandLine.typeAcquisition!, parsedCommandLine.compileOnSave);
this.updateRootAndOptionsOfNonInferredProject(project, filesToAdd, fileNamePropertyReader, compilerOptions, parsedCommandLine.typeAcquisition!, parsedCommandLine.compileOnSave, parsedCommandLine.watchOptions);
}
private updateNonInferredProjectFiles<T>(project: ExternalProject | ConfiguredProject, files: T[], propertyReader: FilePropertyReader<T>) {
@@ -1979,8 +2003,9 @@ namespace ts.server {
project.markAsDirty();
}
private updateRootAndOptionsOfNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean | undefined) {
private updateRootAndOptionsOfNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean | undefined, watchOptions: WatchOptions | undefined) {
project.setCompilerOptions(newOptions);
project.setWatchOptions(watchOptions);
// VS only set the CompileOnSaveEnabled option in the request if the option was changed recently
// therefore if it is undefined, it should not be updated.
if (compileOnSave !== undefined) {
@@ -2108,7 +2133,14 @@ namespace ts.server {
private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject {
const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects!; // TODO: GH#18217
const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory, this.currentPluginConfigOverrides);
let watchOptions: WatchOptions | false | undefined;
if (projectRootPath) {
watchOptions = this.watchOptionsForInferredProjectsPerProjectRoot.get(projectRootPath);
}
if (watchOptions === undefined) {
watchOptions = this.watchOptionsForInferredProjects;
}
const project = new InferredProject(this, this.documentRegistry, compilerOptions, watchOptions || undefined, projectRootPath, currentDirectory, this.currentPluginConfigOverrides);
if (isSingleInferredProject) {
this.inferredProjects.unshift(project);
}
@@ -2197,6 +2229,7 @@ namespace ts.server {
info.fileName,
(fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path),
PollingInterval.Medium,
this.hostConfiguration.watchOptions,
info.path,
WatchType.ClosedScriptInfo
);
@@ -2243,6 +2276,7 @@ namespace ts.server {
}
},
WatchDirectoryFlags.Recursive,
this.hostConfiguration.watchOptions,
WatchType.NodeModulesForClosedScriptInfo
);
const result: ScriptInfoInNodeModulesWatcher = {
@@ -2470,6 +2504,7 @@ namespace ts.server {
}
},
PollingInterval.High,
this.hostConfiguration.watchOptions,
WatchType.MissingSourceMapFile,
);
return fileWatcher;
@@ -2554,9 +2589,22 @@ namespace ts.server {
this.reloadProjects();
this.logger.info("Host file extension mappings updated");
}
if (args.watchOptions) {
this.hostConfiguration.watchOptions = convertWatchOptions(args.watchOptions);
this.logger.info(`Host watch options changed to ${JSON.stringify(this.hostConfiguration.watchOptions)}, it will be take effect for next watches.`);
}
}
}
/*@internal*/
getWatchOptions(project: Project) {
const projectOptions = project.getWatchOptions();
return projectOptions && this.hostConfiguration.watchOptions ?
{ ...this.hostConfiguration.watchOptions, ...projectOptions } :
projectOptions || this.hostConfiguration.watchOptions;
}
closeLog() {
this.logger.close();
}
@@ -3344,6 +3392,7 @@ namespace ts.server {
externalProject.excludedFiles = excludedFiles;
if (!tsConfigFiles) {
const compilerOptions = convertCompilerOptions(proj.options);
const watchOptions = convertWatchOptions(proj.options);
const lastFileExceededProgramSize = this.getFilenameForExceededTotalSizeLimitForNonTsFiles(proj.projectFileName, compilerOptions, proj.rootFiles, externalFilePropertyReader);
if (lastFileExceededProgramSize) {
externalProject.disableLanguageService(lastFileExceededProgramSize);
@@ -3353,7 +3402,7 @@ namespace ts.server {
}
// external project already exists and not config files were added - update the project and return;
// The graph update here isnt postponed since any file open operation needs all updated external projects
this.updateRootAndOptionsOfNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, compilerOptions, proj.typeAcquisition, proj.options.compileOnSave);
this.updateRootAndOptionsOfNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, compilerOptions, proj.typeAcquisition, proj.options.compileOnSave, watchOptions);
externalProject.updateGraph();
return;
}
+48 -1
View File
@@ -257,6 +257,7 @@ namespace ts.server {
lastFileExceededProgramSize: string | undefined,
private compilerOptions: CompilerOptions,
public compileOnSaveEnabled: boolean,
protected watchOptions: WatchOptions | undefined,
directoryStructureHost: DirectoryStructureHost,
currentDirectory: string | undefined,
customRealpath?: (s: string) => string) {
@@ -472,6 +473,7 @@ namespace ts.server {
directory,
cb,
flags,
this.projectService.getWatchOptions(this),
WatchType.FailedLookupLocations,
this
);
@@ -489,6 +491,7 @@ namespace ts.server {
directory,
cb,
flags,
this.projectService.getWatchOptions(this),
WatchType.TypeRoots,
this
);
@@ -1170,6 +1173,7 @@ namespace ts.server {
}
},
PollingInterval.Medium,
this.projectService.getWatchOptions(this),
WatchType.MissingFile,
this
);
@@ -1213,6 +1217,7 @@ namespace ts.server {
generatedFile,
() => this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this),
PollingInterval.High,
this.projectService.getWatchOptions(this),
WatchType.MissingGeneratedFile,
this
)
@@ -1283,6 +1288,16 @@ namespace ts.server {
}
}
/*@internal*/
setWatchOptions(watchOptions: WatchOptions | undefined) {
this.watchOptions = watchOptions;
}
/*@internal*/
getWatchOptions(): WatchOptions | undefined {
return this.watchOptions;
}
/* @internal */
getChangesSinceVersion(lastKnownVersion?: number): ProjectFilesWithTSDiagnostics {
// Update the graph only if initial configured project load is not pending
@@ -1516,6 +1531,7 @@ namespace ts.server {
}
},
PollingInterval.Low,
this.projectService.getWatchOptions(this),
WatchType.PackageJsonFile,
));
}
@@ -1594,6 +1610,7 @@ namespace ts.server {
projectService: ProjectService,
documentRegistry: DocumentRegistry,
compilerOptions: CompilerOptions,
watchOptions: WatchOptions | undefined,
projectRootPath: NormalizedPath | undefined,
currentDirectory: string | undefined,
pluginConfigOverrides: Map<any> | undefined) {
@@ -1606,6 +1623,7 @@ namespace ts.server {
/*lastFileExceededProgramSize*/ undefined,
compilerOptions,
/*compileOnSaveEnabled*/ false,
watchOptions,
projectService.host,
currentDirectory);
this.projectRootPath = projectRootPath && projectService.toCanonicalFileName(projectRootPath);
@@ -1730,6 +1748,7 @@ namespace ts.server {
/*lastFileExceededProgramSize*/ undefined,
/*compilerOptions*/ {},
/*compileOnSaveEnabled*/ false,
/*watchOptions*/ undefined,
cachedDirectoryStructureHost,
getDirectoryPath(configFileName),
projectService.host.realpath && (s => this.getRealpath(s))
@@ -1889,6 +1908,32 @@ namespace ts.server {
) || false;
}
/* @internal */
setWatchOptions(watchOptions: WatchOptions | undefined) {
const oldOptions = this.getWatchOptions();
super.setWatchOptions(watchOptions);
// If watch options different than older options
if (this.isInitialLoadPending() &&
!isJsonEqual(oldOptions, this.getWatchOptions())) {
const oldWatcher = this.configFileWatcher;
this.createConfigFileWatcher();
if (oldWatcher) oldWatcher.close();
}
}
/* @internal */
createConfigFileWatcher() {
this.configFileWatcher = this.projectService.watchFactory.watchFile(
this.projectService.host,
this.getConfigFilePath(),
(_fileName, eventKind) => this.projectService.onConfigChangedForConfiguredProject(this, eventKind),
PollingInterval.High,
this.projectService.getWatchOptions(this),
WatchType.ConfigFile,
this
);
}
/**
* If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph
* @returns: true if set of files in the project stays the same and false - otherwise.
@@ -2111,7 +2156,8 @@ namespace ts.server {
lastFileExceededProgramSize: string | undefined,
public compileOnSaveEnabled: boolean,
projectFilePath?: string,
pluginConfigOverrides?: Map<any>) {
pluginConfigOverrides?: Map<any>,
watchOptions?: WatchOptions) {
super(externalProjectName,
ProjectKind.External,
projectService,
@@ -2120,6 +2166,7 @@ namespace ts.server {
lastFileExceededProgramSize,
compilerOptions,
compileOnSaveEnabled,
watchOptions,
projectService.host,
getDirectoryPath(projectFilePath || normalizeSlashes(externalProjectName)));
this.enableGlobalPlugins(this.getCompilerOptions(), pluginConfigOverrides);
+31 -1
View File
@@ -1276,7 +1276,7 @@ namespace ts.server.protocol {
* For external projects, some of the project settings are sent together with
* compiler settings.
*/
export type ExternalProjectCompilerOptions = CompilerOptions & CompileOnSaveMixin;
export type ExternalProjectCompilerOptions = CompilerOptions & CompileOnSaveMixin & WatchOptions;
/**
* Contains information about current project version
@@ -1404,6 +1404,36 @@ namespace ts.server.protocol {
* The host's additional supported .js file extensions
*/
extraFileExtensions?: FileExtensionInfo[];
watchOptions?: WatchOptions;
}
export const enum WatchFileKind {
FixedPollingInterval = "FixedPollingInterval",
PriorityPollingInterval = "PriorityPollingInterval",
DynamicPriorityPolling = "DynamicPriorityPolling",
UseFsEvents = "UseFsEvents",
UseFsEventsOnParentDirectory = "UseFsEventsOnParentDirectory",
}
export const enum WatchDirectoryKind {
UseFsEvents = "UseFsEvents",
FixedPollingInterval = "FixedPollingInterval",
DynamicPriorityPolling = "DynamicPriorityPolling",
}
export const enum PollingWatchKind {
FixedInterval = "FixedInterval",
PriorityInterval = "PriorityInterval",
DynamicPriority = "DynamicPriority",
}
export interface WatchOptions {
watchFile?: WatchFileKind | ts.WatchFileKind;
watchDirectory?: WatchDirectoryKind | ts.WatchDirectoryKind;
fallbackPolling?: PollingWatchKind | ts.PollingWatchKind;
synchronousWatchDirectory?: boolean;
[option: string]: CompilerOptionsValue | undefined;
}
/**
+2 -2
View File
@@ -7,8 +7,8 @@ declare namespace ts.server {
export type RequireResult = { module: {}, error: undefined } | { module: undefined, error: { stack?: string, message?: string } };
export interface ServerHost extends System {
watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher;
watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher;
watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher;
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any;
clearTimeout(timeoutId: any): void;
setImmediate(callback: (...args: any[]) => void, ...args: any[]): any;
+2 -1
View File
@@ -36,6 +36,7 @@ namespace ts.server {
projectName: project.getProjectName(),
fileNames: project.getFileNames(/*excludeFilesFromExternalLibraries*/ true, /*excludeConfigFiles*/ true).concat(project.getExcludedFiles() as NormalizedPath[]),
compilerOptions: project.getCompilationSettings(),
watchOptions: project.projectService.getWatchOptions(project),
typeAcquisition,
unresolvedImports,
projectRootPath: project.getCurrentDirectory() as Path,
@@ -119,4 +120,4 @@ namespace ts.server {
export function createSortedArray<T>(): SortedArray<T> {
return [] as any as SortedArray<T>; // TODO: GH#19873
}
}
}