diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index a91d12bb9006d..d50b37b100c42 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -754,7 +754,7 @@ namespace ts { function convertToReusableCompilerOptions(options: CompilerOptions, relativeToBuildInfo: (path: string) => string) { const result: CompilerOptions = {}; - const optionsNameMap = getOptionNameMap().optionNameMap; + const { optionsNameMap } = getOptionsNameMap(); for (const name in options) { if (hasProperty(options, name)) { @@ -1194,4 +1194,4 @@ namespace ts { return Debug.assertDefined(state.program); } } -} \ No newline at end of file +} diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 8596cc86eb62d..8bf71d7301d8a 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -73,6 +73,49 @@ namespace ts { /* @internal */ export const libMap = createMapFromEntries(libEntries); + // Watch related options + /* @internal */ + export const optionsForWatch: CommandLineOption[] = [ + { + name: "watchFile", + type: createMapFromTemplate({ + fixedpollinginterval: WatchFileKind.FixedPollingInterval, + prioritypollinginterval: WatchFileKind.PriorityPollingInterval, + dynamicprioritypolling: WatchFileKind.DynamicPriorityPolling, + usefsevents: WatchFileKind.UseFsEvents, + usefseventsonparentdirectory: WatchFileKind.UseFsEventsOnParentDirectory, + }), + category: Diagnostics.Advanced_Options, + description: Diagnostics.Specify_strategy_for_watching_file_Colon_FixedPollingInterval_default_PriorityPollingInterval_DynamicPriorityPolling_UseFsEvents_UseFsEventsOnParentDirectory, + }, + { + name: "watchDirectory", + type: createMapFromTemplate({ + usefsevents: WatchDirectoryKind.UseFsEvents, + fixedpollinginterval: WatchDirectoryKind.FixedPollingInterval, + dynamicprioritypolling: WatchDirectoryKind.DynamicPriorityPolling, + }), + category: Diagnostics.Advanced_Options, + description: Diagnostics.Specify_strategy_for_watching_directory_on_platforms_that_don_t_support_recursive_watching_natively_Colon_UseFsEvents_default_FixedPollingInterval_DynamicPriorityPolling, + }, + { + name: "fallbackPolling", + type: createMapFromTemplate({ + fixedinterval: PollingWatchKind.FixedInterval, + priorityinterval: PollingWatchKind.PriorityInterval, + dynamicpriority: PollingWatchKind.DynamicPriority, + }), + category: Diagnostics.Advanced_Options, + description: Diagnostics.Specify_strategy_for_creating_a_polling_watch_when_it_fails_to_create_using_file_system_events_Colon_FixedInterval_default_PriorityInterval_DynamicPriority, + }, + { + name: "synchronousWatchDirectory", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Synchronously_call_callbacks_and_update_the_state_of_directory_watchers_on_platforms_that_don_t_support_recursive_watching_natively, + }, + ]; + /* @internal */ export const commonOptionsWithBuild: CommandLineOption[] = [ { @@ -921,7 +964,7 @@ namespace ts { type: "object" }, description: Diagnostics.List_of_language_service_plugins - } + }, ]; /* @internal */ @@ -1008,11 +1051,32 @@ namespace ts { ]; /* @internal */ - export interface OptionNameMap { - optionNameMap: Map; + export interface OptionsNameMap { + optionsNameMap: Map; shortOptionNames: Map; } + /*@internal*/ + export function createOptionNameMap(optionDeclarations: readonly CommandLineOption[]): OptionsNameMap { + const optionsNameMap = createMap(); + const shortOptionNames = createMap(); + forEach(optionDeclarations, option => { + optionsNameMap.set(option.name.toLowerCase(), option); + if (option.shortName) { + shortOptionNames.set(option.shortName, option.name); + } + }); + + return { optionsNameMap, shortOptionNames }; + } + + let optionsNameMapCache: OptionsNameMap; + + /* @internal */ + export function getOptionsNameMap(): OptionsNameMap { + return optionsNameMapCache || (optionsNameMapCache = createOptionNameMap(optionDeclarations)); + } + /* @internal */ export const defaultInitCompilerOptions: CompilerOptions = { module: ModuleKind.CommonJS, @@ -1022,8 +1086,6 @@ namespace ts { forceConsistentCasingInFileNames: true }; - let optionNameMapCache: OptionNameMap; - /* @internal */ export function convertEnableAutoDiscoveryToEnable(typeAcquisition: TypeAcquisition): TypeAcquisition { // Convert deprecated typingOptions.enableAutoDiscovery to typeAcquisition.enable @@ -1037,25 +1099,6 @@ namespace ts { return typeAcquisition; } - /* @internal */ - export function getOptionNameMap(): OptionNameMap { - return optionNameMapCache || (optionNameMapCache = createOptionNameMap(optionDeclarations)); - } - - /*@internal*/ - export function createOptionNameMap(optionDeclarations: readonly CommandLineOption[]): OptionNameMap { - const optionNameMap = createMap(); - const shortOptionNames = createMap(); - forEach(optionDeclarations, option => { - optionNameMap.set(option.name.toLowerCase(), option); - if (option.shortName) { - shortOptionNames.set(option.shortName, option.name); - } - }); - - return { optionNameMap, shortOptionNames }; - } - /* @internal */ export function createCompilerDiagnosticForInvalidCustomType(opt: CommandLineOptionOfCustomType): Diagnostic { return createDiagnosticForInvalidCustomType(opt, createCompilerDiagnostic); @@ -1092,27 +1135,43 @@ namespace ts { } interface OptionsBase { - [option: string]: CompilerOptionsValue | undefined; + [option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined; } - interface ParseCommandLineWorkerDiagnostics { - unknownOptionDiagnostic: DiagnosticMessage, - unknownDidYouMeanDiagnostic: DiagnosticMessage, - optionTypeMismatchDiagnostic: DiagnosticMessage + interface ParseCommandLineWorkerDiagnostics extends DidYouMeanOptionsDiagnostics { + getOptionsNameMap: () => OptionsNameMap; + optionTypeMismatchDiagnostic: DiagnosticMessage; + } + + function getOptionName(option: CommandLineOption) { + return option.name; + } + + function createUnknownOptionError( + unknownOption: string, + diagnostics: DidYouMeanOptionsDiagnostics, + createDiagnostics: (message: DiagnosticMessage, arg0: string, arg1?: string) => Diagnostic, + unknownOptionErrorText?: string + ) { + const possibleOption = getSpellingSuggestion(unknownOption, diagnostics.optionDeclarations, getOptionName); + return possibleOption ? + createDiagnostics(diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : + createDiagnostics(diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption); } function parseCommandLineWorker( - getOptionNameMap: () => OptionNameMap, diagnostics: ParseCommandLineWorkerDiagnostics, commandLine: readonly string[], readFile?: (path: string) => string | undefined) { const options = {} as OptionsBase; + let watchOptions: WatchOptions | undefined; const fileNames: string[] = []; const errors: Diagnostic[] = []; parseStrings(commandLine); return { options, + watchOptions, fileNames, errors }; @@ -1126,57 +1185,18 @@ namespace ts { parseResponseFile(s.slice(1)); } else if (s.charCodeAt(0) === CharacterCodes.minus) { - const opt = getOptionDeclarationFromName(getOptionNameMap, s.slice(s.charCodeAt(1) === CharacterCodes.minus ? 2 : 1), /*allowShort*/ true); + const inputOptionName = s.slice(s.charCodeAt(1) === CharacterCodes.minus ? 2 : 1); + const opt = getOptionDeclarationFromName(diagnostics.getOptionsNameMap, inputOptionName, /*allowShort*/ true); if (opt) { - if (opt.isTSConfigOnly) { - errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file, opt.name)); - } - else { - // Check to see if no argument was provided (e.g. "--locale" is the last command-line argument). - if (!args[i] && opt.type !== "boolean") { - errors.push(createCompilerDiagnostic(diagnostics.optionTypeMismatchDiagnostic, opt.name)); - } - - switch (opt.type) { - case "number": - options[opt.name] = parseInt(args[i]); - i++; - break; - case "boolean": - // boolean flag has optional value true, false, others - const optValue = args[i]; - options[opt.name] = optValue !== "false"; - // consume next argument as boolean flag value - if (optValue === "false" || optValue === "true") { - i++; - } - break; - case "string": - options[opt.name] = args[i] || ""; - i++; - break; - case "list": - const result = parseListTypeOption(opt, args[i], errors); - options[opt.name] = result || []; - if (result) { - i++; - } - break; - // If not a primitive, the possible types are specified in what is effectively a map of options. - default: - options[opt.name] = parseCustomTypeOption(opt, args[i], errors); - i++; - break; - } - } + i = parseOptionValue(args, i, diagnostics, opt, options, errors); } else { - const possibleOption = getSpellingSuggestion(s, optionDeclarations, opt => `--${opt.name}`); - if (possibleOption) { - errors.push(createCompilerDiagnostic(diagnostics.unknownDidYouMeanDiagnostic, s, possibleOption.name)); + const watchOpt = getOptionDeclarationFromName(watchOptionsDidYouMeanDiagnostics.getOptionsNameMap, inputOptionName, /*allowShort*/ true); + if (watchOpt) { + i = parseOptionValue(args, i, watchOptionsDidYouMeanDiagnostics, watchOpt, watchOptions || (watchOptions = {}), errors); } else { - errors.push(createCompilerDiagnostic(diagnostics.unknownOptionDiagnostic, s)); + errors.push(createUnknownOptionError(inputOptionName, diagnostics, createCompilerDiagnostic, s)); } } } @@ -1220,23 +1240,77 @@ namespace ts { } } - const compilerOptionsDefaultDiagnostics = { + function parseOptionValue( + args: readonly string[], + i: number, + diagnostics: ParseCommandLineWorkerDiagnostics, + opt: CommandLineOption, + options: OptionsBase, + errors: Diagnostic[] + ) { + if (opt.isTSConfigOnly) { + errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file, opt.name)); + } + else { + // Check to see if no argument was provided (e.g. "--locale" is the last command-line argument). + if (!args[i] && opt.type !== "boolean") { + errors.push(createCompilerDiagnostic(diagnostics.optionTypeMismatchDiagnostic, opt.name, getCompilerOptionValueTypeString(opt))); + } + + switch (opt.type) { + case "number": + options[opt.name] = parseInt(args[i]); + i++; + break; + case "boolean": + // boolean flag has optional value true, false, others + const optValue = args[i]; + options[opt.name] = optValue !== "false"; + // consume next argument as boolean flag value + if (optValue === "false" || optValue === "true") { + i++; + } + break; + case "string": + options[opt.name] = args[i] || ""; + i++; + break; + case "list": + const result = parseListTypeOption(opt, args[i], errors); + options[opt.name] = result || []; + if (result) { + i++; + } + break; + // If not a primitive, the possible types are specified in what is effectively a map of options. + default: + options[opt.name] = parseCustomTypeOption(opt, args[i], errors); + i++; + break; + } + } + return i; + } + + const compilerOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { + getOptionsNameMap, + optionDeclarations, unknownOptionDiagnostic: Diagnostics.Unknown_compiler_option_0, unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, optionTypeMismatchDiagnostic: Diagnostics.Compiler_option_0_expects_an_argument }; export function parseCommandLine(commandLine: readonly string[], readFile?: (path: string) => string | undefined): ParsedCommandLine { - return parseCommandLineWorker(getOptionNameMap, compilerOptionsDefaultDiagnostics, commandLine, readFile); + return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile); } /** @internal */ export function getOptionFromName(optionName: string, allowShort?: boolean): CommandLineOption | undefined { - return getOptionDeclarationFromName(getOptionNameMap, optionName, allowShort); + return getOptionDeclarationFromName(getOptionsNameMap, optionName, allowShort); } - function getOptionDeclarationFromName(getOptionNameMap: () => OptionNameMap, optionName: string, allowShort = false): CommandLineOption | undefined { + function getOptionDeclarationFromName(getOptionNameMap: () => OptionsNameMap, optionName: string, allowShort = false): CommandLineOption | undefined { optionName = optionName.toLowerCase(); - const { optionNameMap, shortOptionNames } = getOptionNameMap(); + const { optionsNameMap, shortOptionNames } = getOptionNameMap(); // Try to translate short option names to their full equivalents. if (allowShort) { const short = shortOptionNames.get(optionName); @@ -1244,25 +1318,36 @@ namespace ts { optionName = short; } } - return optionNameMap.get(optionName); + return optionsNameMap.get(optionName); } /*@internal*/ export interface ParsedBuildCommand { buildOptions: BuildOptions; + watchOptions: WatchOptions | undefined; projects: string[]; errors: Diagnostic[]; } + let buildOptionsNameMapCache: OptionsNameMap; + function getBuildOptionsNameMap(): OptionsNameMap { + return buildOptionsNameMapCache || (buildOptionsNameMapCache = createOptionNameMap(buildOpts)); + } + + const buildOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { + getOptionsNameMap: getBuildOptionsNameMap, + optionDeclarations: buildOpts, + unknownOptionDiagnostic: Diagnostics.Unknown_build_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_build_option_0_Did_you_mean_1, + optionTypeMismatchDiagnostic: Diagnostics.Build_option_0_requires_a_value_of_type_1 + }; + /*@internal*/ export function parseBuildCommand(args: readonly string[]): ParsedBuildCommand { - let buildOptionNameMap: OptionNameMap | undefined; - const returnBuildOptionNameMap = () => (buildOptionNameMap || (buildOptionNameMap = createOptionNameMap(buildOpts))); - const { options, fileNames: projects, errors } = parseCommandLineWorker(returnBuildOptionNameMap, { - unknownOptionDiagnostic: Diagnostics.Unknown_build_option_0, - unknownDidYouMeanDiagnostic: Diagnostics.Unknown_build_option_0_Did_you_mean_1, - optionTypeMismatchDiagnostic: Diagnostics.Build_option_0_requires_a_value_of_type_1 - }, args); + const { options, watchOptions, fileNames: projects, errors } = parseCommandLineWorker( + buildOptionsDidYouMeanDiagnostics, + args + ); const buildOptions = options as BuildOptions; if (projects.length === 0) { @@ -1284,7 +1369,7 @@ namespace ts { errors.push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); } - return { buildOptions, projects, errors }; + return { buildOptions, watchOptions, projects, errors }; } /* @internal */ @@ -1318,7 +1403,8 @@ namespace ts { configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost, - extendedConfigCache?: Map + extendedConfigCache?: Map, + watchOptionsToExtend?: WatchOptions ): ParsedCommandLine | undefined { let configFileText: string | undefined; try { @@ -1348,7 +1434,8 @@ namespace ts { getNormalizedAbsolutePath(configFileName, cwd), /*resolutionStack*/ undefined, /*extraFileExtension*/ undefined, - extendedConfigCache + extendedConfigCache, + watchOptionsToExtend ); } @@ -1395,7 +1482,38 @@ namespace ts { } function commandLineOptionsToMap(options: readonly CommandLineOption[]) { - return arrayToMap(options, option => option.name); + return arrayToMap(options, getOptionName); + } + + const typeAcquisitionDidYouMeanDiagnostics: DidYouMeanOptionsDiagnostics = { + optionDeclarations: typeAcquisitionDeclarations, + unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1, + }; + + let watchOptionsNameMapCache: OptionsNameMap; + function getWatchOptionsNameMap(): OptionsNameMap { + return watchOptionsNameMapCache || (watchOptionsNameMapCache = createOptionNameMap(optionsForWatch)); + } + const watchOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { + getOptionsNameMap: getWatchOptionsNameMap, + optionDeclarations: optionsForWatch, + unknownOptionDiagnostic: Diagnostics.Unknown_watch_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_watch_option_0_Did_you_mean_1, + optionTypeMismatchDiagnostic: Diagnostics.Watch_option_0_requires_a_value_of_type_1 + }; + + let commandLineCompilerOptionsMapCache: Map; + function getCommandLineCompilerOptionsMap() { + return commandLineCompilerOptionsMapCache || (commandLineCompilerOptionsMapCache = commandLineOptionsToMap(optionDeclarations)); + } + let commandLineWatchOptionsMapCache: Map; + function getCommandLineWatchOptionsMap() { + return commandLineWatchOptionsMapCache || (commandLineWatchOptionsMapCache = commandLineOptionsToMap(optionsForWatch)); + } + let commandLineTypeAcquisitionMapCache: Map; + function getCommandLineTypeAcquisitionMap() { + return commandLineTypeAcquisitionMapCache || (commandLineTypeAcquisitionMapCache = commandLineOptionsToMap(typeAcquisitionDeclarations)); } let _tsconfigRootOptions: TsConfigOnlyOption; @@ -1408,29 +1526,26 @@ namespace ts { { name: "compilerOptions", type: "object", - elementOptions: commandLineOptionsToMap(optionDeclarations), - extraKeyDiagnostics: { - unknownOptionDiagnostic: Diagnostics.Unknown_compiler_option_0, - unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1 - }, + elementOptions: getCommandLineCompilerOptionsMap(), + extraKeyDiagnostics: compilerOptionsDidYouMeanDiagnostics, + }, + { + name: "watchOptions", + type: "object", + elementOptions: getCommandLineWatchOptionsMap(), + extraKeyDiagnostics: watchOptionsDidYouMeanDiagnostics, }, { name: "typingOptions", type: "object", - elementOptions: commandLineOptionsToMap(typeAcquisitionDeclarations), - extraKeyDiagnostics: { - unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0, - unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1 - }, + elementOptions: getCommandLineTypeAcquisitionMap(), + extraKeyDiagnostics: typeAcquisitionDidYouMeanDiagnostics, }, { name: "typeAcquisition", type: "object", - elementOptions: commandLineOptionsToMap(typeAcquisitionDeclarations), - extraKeyDiagnostics: { - unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0, - unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1 - } + elementOptions: getCommandLineTypeAcquisitionMap(), + extraKeyDiagnostics: typeAcquisitionDidYouMeanDiagnostics }, { name: "extends", @@ -1536,7 +1651,7 @@ namespace ts { function convertObjectLiteralExpressionToJson( node: ObjectLiteralExpression, knownOptions: Map | undefined, - extraKeyDiagnostics: DidYouMeanOptionalDiagnostics | undefined, + extraKeyDiagnostics: DidYouMeanOptionsDiagnostics | undefined, parentOption: string | undefined ): any { const result: any = returnValue ? {} : undefined; @@ -1558,13 +1673,11 @@ namespace ts { const option = keyText && knownOptions ? knownOptions.get(keyText) : undefined; if (keyText && extraKeyDiagnostics && !option) { if (knownOptions) { - const possibleOption = getSpellingSuggestion(keyText, arrayFrom(knownOptions.keys()), identity); - if (possibleOption) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownDidYouMeanDiagnostic, keyText, possibleOption)); - } - else { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownOptionDiagnostic, keyText)); - } + errors.push(createUnknownOptionError( + keyText, + extraKeyDiagnostics, + (message, arg0, arg1) => createDiagnosticForNodeInSourceFile(sourceFile, element.name, message, arg0, arg1) + )); } else { errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownOptionDiagnostic, keyText)); @@ -1765,9 +1878,10 @@ namespace ts { f => getRelativePathFromFile(getNormalizedAbsolutePath(configFileName, host.getCurrentDirectory()), getNormalizedAbsolutePath(f, host.getCurrentDirectory()), getCanonicalFileName) ); const optionMap = serializeCompilerOptions(configParseResult.options, { configFilePath: getNormalizedAbsolutePath(configFileName, host.getCurrentDirectory()), useCaseSensitiveFileNames: host.useCaseSensitiveFileNames }); + const watchOptionMap = configParseResult.watchOptions && serializeWatchOptions(configParseResult.watchOptions); const config = { compilerOptions: { - ...arrayFrom(optionMap.entries()).reduce((prev, cur) => ({ ...prev, [cur[0]]: cur[1] }), {}), + ...optionMapToObject(optionMap), showConfig: undefined, configFile: undefined, configFilePath: undefined, @@ -1779,6 +1893,7 @@ namespace ts { build: undefined, version: undefined, }, + watchOptions: watchOptionMap && optionMapToObject(watchOptionMap), references: map(configParseResult.projectReferences, r => ({ ...r, path: r.originalPath ? r.originalPath : "", originalPath: undefined })), files: length(files) ? files : undefined, ...(configParseResult.configFileSpecs ? { @@ -1790,6 +1905,12 @@ namespace ts { return config; } + function optionMapToObject(optionMap: Map): object { + return { + ...arrayFrom(optionMap.entries()).reduce((prev, cur) => ({ ...prev, [cur[0]]: cur[1] }), {}), + }; + } + function filterSameAsDefaultInclude(specs: readonly string[] | undefined) { if (!length(specs)) return undefined; if (length(specs) !== 1) return specs; @@ -1836,9 +1957,23 @@ namespace ts { }); } - function serializeCompilerOptions(options: CompilerOptions, pathOptions?: { configFilePath: string, useCaseSensitiveFileNames: boolean }): Map { + function serializeCompilerOptions( + options: CompilerOptions, + pathOptions?: { configFilePath: string, useCaseSensitiveFileNames: boolean } + ): Map { + return serializeOptionBaseObject(options, getOptionsNameMap(), pathOptions); + } + + function serializeWatchOptions(options: WatchOptions) { + return serializeOptionBaseObject(options, getWatchOptionsNameMap()); + } + + function serializeOptionBaseObject( + options: OptionsBase, + { optionsNameMap }: OptionsNameMap, + pathOptions?: { configFilePath: string, useCaseSensitiveFileNames: boolean } + ): Map { const result = createMap(); - const optionsNameMap = getOptionNameMap().optionNameMap; const getCanonicalFileName = pathOptions && createGetCanonicalFileName(pathOptions.useCaseSensitiveFileNames); for (const name in options) { @@ -1987,7 +2122,7 @@ namespace ts { /* @internal */ export function convertToOptionsWithAbsolutePaths(options: CompilerOptions, toAbsolutePath: (path: string) => string) { const result: CompilerOptions = {}; - const optionsNameMap = getOptionNameMap().optionNameMap; + const optionsNameMap = getOptionsNameMap().optionsNameMap; for (const name in options) { if (hasProperty(options, name)) { @@ -2026,8 +2161,8 @@ 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[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map): ParsedCommandLine { - return parseJsonConfigFileContentWorker(json, /*sourceFile*/ undefined, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); + export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map, existingWatchOptions?: WatchOptions): ParsedCommandLine { + return parseJsonConfigFileContentWorker(json, /*sourceFile*/ undefined, host, basePath, existingOptions, existingWatchOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); } /** @@ -2037,8 +2172,8 @@ namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map): ParsedCommandLine { - return parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); + export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map, existingWatchOptions?: WatchOptions): ParsedCommandLine { + return parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, existingWatchOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); } /*@internal*/ @@ -2073,6 +2208,7 @@ namespace ts { host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, + existingWatchOptions: WatchOptions | undefined, configFileName?: string, resolutionStack: Path[] = [], extraFileExtensions: readonly FileExtensionInfo[] = [], @@ -2084,12 +2220,17 @@ namespace ts { const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors, extendedConfigCache); const { raw } = parsedConfig; const options = extend(existingOptions, parsedConfig.options || {}); + const watchOptions = existingWatchOptions && parsedConfig.watchOptions ? + extend(existingWatchOptions, parsedConfig.watchOptions) : + parsedConfig.watchOptions || existingWatchOptions; + options.configFilePath = configFileName && normalizeSlashes(configFileName); setConfigFileInOptions(options, sourceFile); let projectReferences: ProjectReference[] | undefined; const { fileNames, wildcardDirectories, spec } = getFileNames(); return { options, + watchOptions, fileNames, projectReferences, typeAcquisition: parsedConfig.typeAcquisition || getDefaultTypeAcquisition(), @@ -2232,6 +2373,7 @@ namespace ts { export interface ParsedTsconfig { raw: any; options?: CompilerOptions; + watchOptions?: WatchOptions; typeAcquisition?: TypeAcquisition; /** * Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet @@ -2289,6 +2431,9 @@ namespace ts { raw.compileOnSave = baseRaw.compileOnSave; } ownConfig.options = assign({}, extendedConfig.options, ownConfig.options); + ownConfig.watchOptions = ownConfig.watchOptions && extendedConfig.watchOptions ? + assign({}, extendedConfig.watchOptions, ownConfig.watchOptions) : + ownConfig.watchOptions || extendedConfig.watchOptions; // TODO extend type typeAcquisition } } @@ -2311,6 +2456,7 @@ namespace ts { // typingOptions has been deprecated and is only supported for backward compatibility purposes. // It should be removed in future releases - use typeAcquisition instead. const typeAcquisition = convertTypeAcquisitionFromJsonWorker(json.typeAcquisition || json.typingOptions, basePath, errors, configFileName); + const watchOptions = convertWatchOptionsFromJsonWorker(json.watchOptions, basePath, errors); json.compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors); let extendedConfigPath: string | undefined; @@ -2323,7 +2469,7 @@ namespace ts { extendedConfigPath = getExtendsConfigPath(json.extends, host, newBase, errors, createCompilerDiagnostic); } } - return { raw: json, options, typeAcquisition, extendedConfigPath }; + return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; } function parseOwnConfigOfJsonSourceFile( @@ -2335,16 +2481,28 @@ namespace ts { ): ParsedTsconfig { const options = getDefaultCompilerOptions(configFileName); let typeAcquisition: TypeAcquisition | undefined, typingOptionstypeAcquisition: TypeAcquisition | undefined; + let watchOptions: WatchOptions | undefined; let extendedConfigPath: string | undefined; const optionsIterator: JsonConversionNotifier = { onSetValidOptionKeyValueInParent(parentOption: string, option: CommandLineOption, value: CompilerOptionsValue) { - Debug.assert(parentOption === "compilerOptions" || parentOption === "typeAcquisition" || parentOption === "typingOptions"); - const currentOption = parentOption === "compilerOptions" ? - options : - parentOption === "typeAcquisition" ? - (typeAcquisition || (typeAcquisition = getDefaultTypeAcquisition(configFileName))) : - (typingOptionstypeAcquisition || (typingOptionstypeAcquisition = getDefaultTypeAcquisition(configFileName))); + let currentOption; + switch (parentOption) { + case "compilerOptions": + currentOption = options; + break; + case "watchOptions": + currentOption = (watchOptions || (watchOptions = {})); + break; + case "typeAcquisition": + currentOption = (typeAcquisition || (typeAcquisition = getDefaultTypeAcquisition(configFileName))); + break; + case "typingOptions": + currentOption = (typingOptionstypeAcquisition || (typingOptionstypeAcquisition = getDefaultTypeAcquisition(configFileName))); + break; + default: + Debug.fail("Unknown option"); + } currentOption[option.name] = normalizeOptionValue(option, basePath, value); }, @@ -2386,7 +2544,7 @@ namespace ts { } } - return { raw: json, options, typeAcquisition, extendedConfigPath }; + return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; } function getExtendsConfigPath( @@ -2508,7 +2666,7 @@ namespace ts { basePath: string, errors: Push, configFileName?: string): CompilerOptions { const options = getDefaultCompilerOptions(configFileName); - convertOptionsFromJson(optionDeclarations, jsonOptions, basePath, options, compilerOptionsDefaultDiagnostics, errors); + convertOptionsFromJson(getCommandLineCompilerOptionsMap(), jsonOptions, basePath, options, compilerOptionsDidYouMeanDiagnostics, errors); if (configFileName) { options.configFilePath = normalizeSlashes(configFileName); } @@ -2525,40 +2683,35 @@ namespace ts { const options = getDefaultTypeAcquisition(configFileName); const typeAcquisition = convertEnableAutoDiscoveryToEnable(jsonOptions); - const diagnostics = { - unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0, - unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1 , - }; - convertOptionsFromJson(typeAcquisitionDeclarations, typeAcquisition, basePath, options, diagnostics, errors); - + convertOptionsFromJson(getCommandLineTypeAcquisitionMap(), typeAcquisition, basePath, options, typeAcquisitionDidYouMeanDiagnostics, errors); return options; } + function convertWatchOptionsFromJsonWorker(jsonOptions: any, basePath: string, errors: Push): WatchOptions | undefined { + return convertOptionsFromJson(getCommandLineWatchOptionsMap(), jsonOptions, basePath, /*defaultOptions*/ undefined, watchOptionsDidYouMeanDiagnostics, errors); + } - function convertOptionsFromJson(optionDeclarations: readonly CommandLineOption[], jsonOptions: any, basePath: string, - defaultOptions: CompilerOptions | TypeAcquisition, diagnostics: DidYouMeanOptionalDiagnostics, errors: Push) { + function convertOptionsFromJson(optionsNameMap: Map, jsonOptions: any, basePath: string, + defaultOptions: undefined, diagnostics: DidYouMeanOptionsDiagnostics, errors: Push): WatchOptions | undefined; + function convertOptionsFromJson(optionsNameMap: Map, jsonOptions: any, basePath: string, + defaultOptions: CompilerOptions | TypeAcquisition, diagnostics: DidYouMeanOptionsDiagnostics, errors: Push): CompilerOptions | TypeAcquisition; + function convertOptionsFromJson(optionsNameMap: Map, jsonOptions: any, basePath: string, + defaultOptions: CompilerOptions | TypeAcquisition | WatchOptions | undefined, diagnostics: DidYouMeanOptionsDiagnostics, errors: Push) { if (!jsonOptions) { return; } - const optionNameMap = commandLineOptionsToMap(optionDeclarations); - for (const id in jsonOptions) { - const opt = optionNameMap.get(id); + const opt = optionsNameMap.get(id); if (opt) { - defaultOptions[opt.name] = convertJsonOption(opt, jsonOptions[id], basePath, errors); + (defaultOptions || (defaultOptions = {}))[opt.name] = convertJsonOption(opt, jsonOptions[id], basePath, errors); } else { - const possibleOption = getSpellingSuggestion(id, optionDeclarations, opt => opt.name); - if (possibleOption) { - errors.push(createCompilerDiagnostic(diagnostics.unknownDidYouMeanDiagnostic, id, possibleOption.name)); - } - else { - errors.push(createCompilerDiagnostic(diagnostics.unknownOptionDiagnostic, id)); - } + errors.push(createUnknownOptionError(id, diagnostics, createCompilerDiagnostic)); } } + return defaultOptions; } function convertJsonOption(opt: CommandLineOption, value: any, basePath: string, errors: Push): CompilerOptionsValue { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index ff03c486f8b08..890e88f05eb08 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3313,6 +3313,18 @@ "category": "Error", "code": 5077 }, + "Unknown watch option '{0}'.": { + "category": "Error", + "code": 5078 + }, + "Unknown watch option '{0}'. Did you mean '{1}'?": { + "category": "Error", + "code": 5079 + }, + "Watch option '{0}' requires a value of type {1}.": { + "category": "Error", + "code": 5080 + }, "Generates a sourcemap for each corresponding '.d.ts' file.": { "category": "Message", @@ -4160,6 +4172,22 @@ "category": "Message", "code": 6224 }, + "Specify strategy for watching file: 'FixedPollingInterval' (default), 'PriorityPollingInterval', 'DynamicPriorityPolling', 'UseFsEvents', 'UseFsEventsOnParentDirectory'.": { + "category": "Message", + "code": 6225 + }, + "Specify strategy for watching directory on platforms that don't support recursive watching natively: 'UseFsEvents' (default), 'FixedPollingInterval', 'DynamicPriorityPolling'.": { + "category": "Message", + "code": 6226 + }, + "Specify strategy for creating a polling watch when it fails to create using file system events: 'FixedInterval' (default), 'PriorityInterval', 'DynamicPriority'.": { + "category": "Message", + "code": 6227 + }, + "Synchronously call callbacks and update the state of directory watchers on platforms that don't support recursive watching natively.": { + "category": "Message", + "code": 6228 + }, "Projects to reference": { "category": "Message", diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 176eead4fa0bf..514e4333af1e6 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -50,9 +50,9 @@ namespace ts { } /* @internal */ - export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval | undefined) => FileWatcher; + export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined) => FileWatcher; /* @internal */ - export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive?: boolean) => FileWatcher; + export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined) => FileWatcher; /* @internal */ export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time @@ -127,7 +127,10 @@ namespace ts { } /* @internal */ - export function createDynamicPriorityPollingWatchFile(host: { getModifiedTime: System["getModifiedTime"]; setTimeout: System["setTimeout"]; }): HostWatchFile { + export function createDynamicPriorityPollingWatchFile(host: { + getModifiedTime: NonNullable; + setTimeout: NonNullable; + }): HostWatchFile { interface WatchedFile extends ts.WatchedFile { isClosed?: boolean; unchangedPolls: number; @@ -296,11 +299,65 @@ namespace ts { } function scheduleNextPoll(pollingInterval: PollingInterval) { - pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout!(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingIntervalQueue(pollingInterval)); + pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingIntervalQueue(pollingInterval)); } function getModifiedTime(fileName: string) { - return host.getModifiedTime!(fileName) || missingFileModifiedTime; + return host.getModifiedTime(fileName) || missingFileModifiedTime; + } + } + + function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSensitiveFileNames: boolean): HostWatchFile { + // One file can have multiple watchers + const fileWatcherCallbacks = createMultiMap(); + const dirWatchers = createMap(); + const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames); + return nonPollingWatchFile; + + function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback, _pollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined): FileWatcher { + const filePath = toCanonicalName(fileName); + fileWatcherCallbacks.add(filePath, callback); + const dirPath = getDirectoryPath(filePath) || "."; + const watcher = dirWatchers.get(dirPath) || + createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath, fallbackOptions); + watcher.referenceCount++; + return { + close: () => { + if (watcher.referenceCount === 1) { + watcher.close(); + dirWatchers.delete(dirPath); + } + else { + watcher.referenceCount--; + } + fileWatcherCallbacks.remove(filePath, callback); + } + }; + } + + function createDirectoryWatcher(dirName: string, dirPath: string, fallbackOptions: WatchOptions | undefined) { + const watcher = fsWatch( + dirName, + FileSystemEntryKind.Directory, + (_eventName: string, relativeFileName) => { + // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" + if (!isString(relativeFileName)) { return; } + const fileName = getNormalizedAbsolutePath(relativeFileName, dirName); + // Some applications save a working file via rename operations + const callbacks = fileName && fileWatcherCallbacks.get(toCanonicalName(fileName)); + if (callbacks) { + for (const fileCallback of callbacks) { + fileCallback(fileName, FileWatcherEventKind.Changed); + } + } + }, + /*recursive*/ false, + PollingInterval.Medium, + fallbackOptions + ) as DirectoryWatcher; + watcher.referenceCount = 0; + dirWatchers.set(dirPath, watcher); + return watcher; } } @@ -317,7 +374,7 @@ namespace ts { const callbacksCache = createMultiMap(); const toCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - return (fileName, callback, pollingInterval) => { + return (fileName, callback, pollingInterval, options) => { const path = toCanonicalFileName(fileName); const existing = cache.get(path); if (existing) { @@ -331,7 +388,8 @@ namespace ts { callbacksCache.get(path), cb => cb(fileName, eventKind) ), - pollingInterval + pollingInterval, + options ), refCount: 1 }); @@ -394,6 +452,8 @@ namespace ts { getAccessibleSortedChildDirectories(path: string): readonly string[]; directoryExists(dir: string): boolean; realpath(s: string): string; + setTimeout: NonNullable; + clearTimeout: NonNullable; } /** @@ -402,7 +462,7 @@ namespace ts { * (eg on OS that dont support recursive watch using fs.watch use fs.watchFile) */ /*@internal*/ - export function createRecursiveDirectoryWatcher(host: RecursiveDirectoryWatcherHost): (directoryName: string, callback: DirectoryWatcherCallback) => FileWatcher { + export function createDirectoryWatcherSupportingRecursive(host: RecursiveDirectoryWatcherHost): HostWatchDirectory { interface ChildDirectoryWatcher extends FileWatcher { dirName: string; } @@ -414,16 +474,21 @@ namespace ts { } const cache = createMap(); - const callbackCache = createMultiMap(); + const callbackCache = createMultiMap<{ dirName: string; callback: DirectoryWatcherCallback; }>(); + const cacheToUpdateChildWatches = createMap<{ dirName: string; options: WatchOptions | undefined; }>(); + let timerToUpdateChildWatches: any; + const filePathComparer = getStringComparer(!host.useCaseSensitiveFileNames); const toCanonicalFilePath = createGetCanonicalFileName(host.useCaseSensitiveFileNames); - return createDirectoryWatcher; + return (dirName, callback, recursive, options) => recursive ? + createDirectoryWatcher(dirName, options, callback) : + host.watchDirectory(dirName, callback, recursive, options); /** * Create the directory watcher for the dirPath. */ - function createDirectoryWatcher(dirName: string, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher { + function createDirectoryWatcher(dirName: string, options: WatchOptions | undefined, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher { const dirPath = toCanonicalFilePath(dirName) as Path; let directoryWatcher = cache.get(dirPath); if (directoryWatcher) { @@ -434,32 +499,34 @@ namespace ts { watcher: host.watchDirectory(dirName, fileName => { if (isIgnoredPath(fileName)) return; - // Call the actual callback - callbackCache.forEach((callbacks, rootDirName) => { - if (rootDirName === dirPath || (startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === directorySeparator)) { - callbacks.forEach(callback => callback(fileName)); - } - }); + if (options?.synchronousWatchDirectory) { + // Call the actual callback + invokeCallbacks(dirPath, fileName); - // Iterate through existing children and update the watches if needed - updateChildWatches(dirName, dirPath); - }), + // Iterate through existing children and update the watches if needed + updateChildWatches(dirName, dirPath, options); + } + else { + nonSyncUpdateChildWatches(dirName, dirPath, fileName, options); + } + }, /*recursive*/ false, options), refCount: 1, childWatches: emptyArray }; cache.set(dirPath, directoryWatcher); - updateChildWatches(dirName, dirPath); + updateChildWatches(dirName, dirPath, options); } - if (callback) { - callbackCache.add(dirPath, callback); + const callbackToAdd = callback && { dirName, callback }; + if (callbackToAdd) { + callbackCache.add(dirPath, callbackToAdd); } return { dirName, close: () => { const directoryWatcher = Debug.assertDefined(cache.get(dirPath)); - if (callback) callbackCache.remove(dirPath, callback); + if (callbackToAdd) callbackCache.remove(dirPath, callbackToAdd); directoryWatcher.refCount--; if (directoryWatcher.refCount) return; @@ -471,18 +538,103 @@ namespace ts { }; } - function updateChildWatches(dirName: string, dirPath: Path) { + function invokeCallbacks(dirPath: Path, fileNameOrInvokeMap: string | Map) { + let fileName: string | undefined; + let invokeMap: Map | undefined; + if (isString(fileNameOrInvokeMap)) { + fileName = fileNameOrInvokeMap; + } + else { + invokeMap = fileNameOrInvokeMap; + } + // Call the actual callback + callbackCache.forEach((callbacks, rootDirName) => { + if (invokeMap && invokeMap.has(rootDirName)) return; + if (rootDirName === dirPath || (startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === directorySeparator)) { + if (invokeMap) { + invokeMap.set(rootDirName, true); + } + else { + callbacks.forEach(({ callback }) => callback(fileName!)); + } + } + }); + } + + function nonSyncUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) { + // Iterate through existing children and update the watches if needed + const parentWatcher = cache.get(dirPath); + if (parentWatcher && host.directoryExists(dirName)) { + // Schedule the update and postpone invoke for callbacks + scheduleUpdateChildWatches(dirName, dirPath, options); + return; + } + + // Call the actual callbacks and remove child watches + invokeCallbacks(dirPath, fileName); + removeChildWatches(parentWatcher); + } + + function scheduleUpdateChildWatches(dirName: string, dirPath: Path, options: WatchOptions | undefined) { + if (!cacheToUpdateChildWatches.has(dirPath)) { + cacheToUpdateChildWatches.set(dirPath, { dirName, options }); + } + if (timerToUpdateChildWatches) { + host.clearTimeout(timerToUpdateChildWatches); + timerToUpdateChildWatches = undefined; + } + timerToUpdateChildWatches = host.setTimeout(onTimerToUpdateChildWatches, 1000); + } + + function onTimerToUpdateChildWatches() { + timerToUpdateChildWatches = undefined; + sysLog(`sysLog:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size}`); + const start = timestamp(); + const invokeMap = createMap(); + + while (!timerToUpdateChildWatches && cacheToUpdateChildWatches.size) { + const { value: [dirPath, { dirName, options }], done } = cacheToUpdateChildWatches.entries().next(); + Debug.assert(!done); + cacheToUpdateChildWatches.delete(dirPath); + // Because the child refresh is fresh, we would need to invalidate whole root directory being watched + // to ensure that all the changes are reflected at this time + invokeCallbacks(dirPath as Path, invokeMap); + updateChildWatches(dirName, dirPath as Path, options); + } + + sysLog(`sysLog:: invokingWatchers:: ${timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`); + callbackCache.forEach((callbacks, rootDirName) => { + if (invokeMap.has(rootDirName)) { + callbacks.forEach(({ callback, dirName }) => callback(dirName)); + } + }); + + const elapsed = timestamp() - start; + sysLog(`sysLog:: Elapsed ${elapsed}ms:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size} ${timerToUpdateChildWatches}`); + } + + function removeChildWatches(parentWatcher: HostDirectoryWatcher | undefined) { + if (!parentWatcher) return; + const existingChildWatches = parentWatcher.childWatches; + parentWatcher.childWatches = emptyArray; + for (const childWatcher of existingChildWatches) { + childWatcher.close(); + removeChildWatches(cache.get(toCanonicalFilePath(childWatcher.dirName))); + } + } + + function updateChildWatches(dirName: string, dirPath: Path, options: WatchOptions | undefined) { // Iterate through existing children and update the watches if needed const parentWatcher = cache.get(dirPath); if (parentWatcher) { - parentWatcher.childWatches = watchChildDirectories(dirName, parentWatcher.childWatches); + parentWatcher.childWatches = watchChildDirectories(dirName, parentWatcher.childWatches, options); } } /** * Watch the directories in the parentDir */ - function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches): ChildWatches { + function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, options: WatchOptions | undefined): ChildWatches { let newChildWatches: ChildDirectoryWatcher[] | undefined; enumerateInsertsAndDeletes( host.directoryExists(parentDir) ? mapDefined(host.getAccessibleSortedChildDirectories(parentDir), child => { @@ -504,7 +656,7 @@ namespace ts { * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list */ function createAndAddChildDirectoryWatcher(childName: string) { - const result = createDirectoryWatcher(childName); + const result = createDirectoryWatcher(childName, options); addChildDirectoryWatcher(result); } @@ -527,6 +679,252 @@ namespace ts { } } + /*@internal*/ + export type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string | undefined) => void; + /*@internal*/ + export type FsWatch = (fileOrDirectory: string, entryKind: FileSystemEntryKind, callback: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined) => FileWatcher; + + /*@internal*/ + export const enum FileSystemEntryKind { + File, + Directory, + } + + /*@internal*/ + export function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { + return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", ""); + } + + function createFsWatchCallbackForFileWatcherCallback( + fileName: string, + callback: FileWatcherCallback, + fileExists: System["fileExists"] + ): FsWatchCallback { + return eventName => { + if (eventName === "rename") { + callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted); + } + else { + // Change + callback(fileName, FileWatcherEventKind.Changed); + } + }; + } + + function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback): FsWatchCallback { + return (eventName, relativeFileName) => { + // In watchDirectory we only care about adding and removing files (when event name is + // "rename"); changes made within files are handled by corresponding fileWatchers (when + // event name is "change") + if (eventName === "rename") { + // When deleting a file, the passed baseFileName is null + callback(!relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName))); + } + }; + } + + /*@internal*/ + export interface CreateSystemWatchFunctions { + // Polling watch file + pollingWatchFile: HostWatchFile; + // For dynamic polling watch file + getModifiedTime: NonNullable; + setTimeout: NonNullable; + clearTimeout: NonNullable; + // For fs events : + fsWatch: FsWatch; + fileExists: System["fileExists"]; + useCaseSensitiveFileNames: boolean; + fsSupportsRecursiveFsWatch: boolean; + directoryExists: System["directoryExists"]; + getAccessibleSortedChildDirectories(path: string): readonly string[]; + realpath(s: string): string; + // For backward compatibility environment variables + tscWatchFile: string | undefined; + useNonPollingWatchers?: boolean; + tscWatchDirectory: string | undefined; + } + + /*@internal*/ + export function createSystemWatchFunctions({ + pollingWatchFile, + getModifiedTime, + setTimeout, + clearTimeout, + fsWatch, + fileExists, + useCaseSensitiveFileNames, + fsSupportsRecursiveFsWatch, + directoryExists, + getAccessibleSortedChildDirectories, + realpath, + tscWatchFile, + useNonPollingWatchers, + tscWatchDirectory, + }: CreateSystemWatchFunctions): { watchFile: HostWatchFile; watchDirectory: HostWatchDirectory; } { + let dynamicPollingWatchFile: HostWatchFile | undefined; + let nonPollingWatchFile: HostWatchFile | undefined; + let hostRecursiveDirectoryWatcher: HostWatchDirectory | undefined; + return { + watchFile, + watchDirectory + }; + + function watchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined): FileWatcher { + options = updateOptionsForWatchFile(options, useNonPollingWatchers); + const watchFileKind = Debug.assertDefined(options.watchFile); + switch (watchFileKind) { + case WatchFileKind.FixedPollingInterval: + return pollingWatchFile(fileName, callback, PollingInterval.Low, /*options*/ undefined); + case WatchFileKind.PriorityPollingInterval: + return pollingWatchFile(fileName, callback, pollingInterval, /*options*/ undefined); + case WatchFileKind.DynamicPriorityPolling: + return ensureDynamicPollingWatchFile()(fileName, callback, pollingInterval, /*options*/ undefined); + case WatchFileKind.UseFsEvents: + return fsWatch( + fileName, + FileSystemEntryKind.File, + createFsWatchCallbackForFileWatcherCallback(fileName, callback, fileExists), + /*recursive*/ false, + pollingInterval, + getFallbackOptions(options) + ); + case WatchFileKind.UseFsEventsOnParentDirectory: + if (!nonPollingWatchFile) { + nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames); + } + return nonPollingWatchFile(fileName, callback, pollingInterval, getFallbackOptions(options)); + default: + Debug.assertNever(watchFileKind); + } + } + + function ensureDynamicPollingWatchFile() { + return dynamicPollingWatchFile || + (dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout })); + } + + function updateOptionsForWatchFile(options: WatchOptions | undefined, useNonPollingWatchers?: boolean): WatchOptions { + if (options && options.watchFile !== undefined) return options; + switch (tscWatchFile) { + case "PriorityPollingInterval": + // Use polling interval based on priority when create watch using host.watchFile + return { watchFile: WatchFileKind.PriorityPollingInterval }; + case "DynamicPriorityPolling": + // Use polling interval but change the interval depending on file changes and their default polling interval + return { watchFile: WatchFileKind.DynamicPriorityPolling }; + case "UseFsEvents": + // Use notifications from FS to watch with falling back to fs.watchFile + return generateWatchFileOptions(WatchFileKind.UseFsEvents, PollingWatchKind.PriorityInterval, options); + case "UseFsEventsWithFallbackDynamicPolling": + // Use notifications from FS to watch with falling back to dynamic watch file + return generateWatchFileOptions(WatchFileKind.UseFsEvents, PollingWatchKind.DynamicPriority, options); + case "UseFsEventsOnParentDirectory": + useNonPollingWatchers = true; + // fall through + default: + return useNonPollingWatchers ? + // Use notifications from FS to watch with falling back to fs.watchFile + generateWatchFileOptions(WatchFileKind.UseFsEventsOnParentDirectory, PollingWatchKind.PriorityInterval, options) : + // Default to do not use fixed polling interval + { watchFile: WatchFileKind.FixedPollingInterval }; + } + } + + function generateWatchFileOptions( + watchFile: WatchFileKind, + fallbackPolling: PollingWatchKind, + options: WatchOptions | undefined + ): WatchOptions { + const defaultFallbackPolling = options?.fallbackPolling; + return { + watchFile, + fallbackPolling: defaultFallbackPolling === undefined ? + fallbackPolling : + defaultFallbackPolling + }; + } + + function watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher { + if (fsSupportsRecursiveFsWatch) { + return fsWatch( + directoryName, + FileSystemEntryKind.Directory, + createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), + recursive, + PollingInterval.Medium, + getFallbackOptions(options) + ); + } + + if (!hostRecursiveDirectoryWatcher) { + hostRecursiveDirectoryWatcher = createDirectoryWatcherSupportingRecursive({ + useCaseSensitiveFileNames, + directoryExists, + getAccessibleSortedChildDirectories, + watchDirectory: nonRecursiveWatchDirectory, + realpath, + setTimeout, + clearTimeout + }); + } + return hostRecursiveDirectoryWatcher(directoryName, callback, recursive, options); + } + + function nonRecursiveWatchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher { + Debug.assert(!recursive); + options = updateOptionsForWatchDirectory(options); + const watchDirectoryKind = Debug.assertDefined(options.watchDirectory); + switch (watchDirectoryKind) { + case WatchDirectoryKind.FixedPollingInterval: + return pollingWatchFile( + directoryName, + () => callback(directoryName), + PollingInterval.Medium, + /*options*/ undefined + ); + case WatchDirectoryKind.DynamicPriorityPolling: + return ensureDynamicPollingWatchFile()( + directoryName, + () => callback(directoryName), + PollingInterval.Medium, + /*options*/ undefined + ); + case WatchDirectoryKind.UseFsEvents: + return fsWatch( + directoryName, + FileSystemEntryKind.Directory, + createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), + recursive, + PollingInterval.Medium, + getFallbackOptions(options) + ); + default: + Debug.assertNever(watchDirectoryKind); + } + } + + function updateOptionsForWatchDirectory(options: WatchOptions | undefined): WatchOptions { + if (options && options.watchDirectory !== undefined) return options; + switch (tscWatchDirectory) { + case "RecursiveDirectoryUsingFsWatchFile": + // Use polling interval based on priority when create watch using host.watchFile + return { watchDirectory: WatchDirectoryKind.FixedPollingInterval }; + case "RecursiveDirectoryUsingDynamicPriorityPolling": + // Use polling interval but change the interval depending on file changes and their default polling interval + return { watchDirectory: WatchDirectoryKind.DynamicPriorityPolling }; + default: + const defaultFallbackPolling = options?.fallbackPolling; + return { + watchDirectory: WatchDirectoryKind.UseFsEvents, + fallbackPolling: defaultFallbackPolling !== undefined ? + defaultFallbackPolling : + undefined + }; + } + } + } + /** * patch writefile to create folder before writing the file */ @@ -641,8 +1039,8 @@ namespace ts { * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that * use native OS file watching */ - 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; resolvePath(path: string): string; fileExists(path: string): boolean; directoryExists(path: string): boolean; @@ -769,17 +1167,25 @@ namespace ts { const platform: string = _os.platform(); const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); - - const enum FileSystemEntryKind { - File, - Directory - } - - const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; - const tscWatchFile = process.env.TSC_WATCHFILE; - const tscWatchDirectory = process.env.TSC_WATCHDIRECTORY; - const fsWatchFile = createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames); - let dynamicPollingWatchFile: HostWatchFile | undefined; + const fsSupportsRecursiveFsWatch = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin"); + const { watchFile, watchDirectory } = createSystemWatchFunctions({ + pollingWatchFile: createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames), + getModifiedTime, + setTimeout, + clearTimeout, + fsWatch, + useCaseSensitiveFileNames, + fileExists, + // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows + // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) + fsSupportsRecursiveFsWatch, + directoryExists, + getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, + realpath, + tscWatchFile: process.env.TSC_WATCHFILE, + useNonPollingWatchers: process.env.TSC_NONPOLLING_WATCHER, + tscWatchDirectory: process.env.TSC_WATCHDIRECTORY, + }); const nodeSystem: System = { args: process.argv.slice(2), newLine: _os.EOL, @@ -792,8 +1198,8 @@ namespace ts { }, readFile, writeFile, - watchFile: getWatchFile(), - watchDirectory: getWatchDirectory(), + watchFile, + watchDirectory, resolvePath: path => _path.resolve(path), fileExists, directoryExists, @@ -994,112 +1400,8 @@ namespace ts { }); } - function getWatchFile(): HostWatchFile { - switch (tscWatchFile) { - case "PriorityPollingInterval": - // Use polling interval based on priority when create watch using host.watchFile - return fsWatchFile; - case "DynamicPriorityPolling": - // Use polling interval but change the interval depending on file changes and their default polling interval - return createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }); - case "UseFsEvents": - // Use notifications from FS to watch with falling back to fs.watchFile - return watchFileUsingFsWatch; - case "UseFsEventsWithFallbackDynamicPolling": - // Use notifications from FS to watch with falling back to dynamic watch file - dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }); - return createWatchFileUsingDynamicWatchFile(dynamicPollingWatchFile); - case "UseFsEventsOnParentDirectory": - // Use notifications from FS to watch with falling back to fs.watchFile - return createNonPollingWatchFile(); - } - return useNonPollingWatchers ? - createNonPollingWatchFile() : - // Default to do not use polling interval as it is before this experiment branch - (fileName, callback) => fsWatchFile(fileName, callback, /*pollingInterval*/ undefined); - } - - function getWatchDirectory(): HostWatchDirectory { - // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows - // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) - const fsSupportsRecursive = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin"); - if (fsSupportsRecursive) { - return watchDirectoryUsingFsWatch; - } - - // defer watchDirectoryRecursively as it depends on `ts.createMap()` which may not be usable yet. - const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? - createWatchDirectoryUsing(fsWatchFile) : - tscWatchDirectory === "RecursiveDirectoryUsingDynamicPriorityPolling" ? - createWatchDirectoryUsing(dynamicPollingWatchFile || createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout })) : - watchDirectoryUsingFsWatch; - const watchDirectoryRecursively = createRecursiveDirectoryWatcher({ - useCaseSensitiveFileNames, - directoryExists, - getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, - watchDirectory, - realpath - }); - - return (directoryName, callback, recursive) => { - if (recursive) { - return watchDirectoryRecursively(directoryName, callback); - } - return watchDirectory(directoryName, callback); - }; - } - - function createNonPollingWatchFile() { - // One file can have multiple watchers - const fileWatcherCallbacks = createMultiMap(); - const dirWatchers = createMap(); - const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames); - return nonPollingWatchFile; - - function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback): FileWatcher { - const filePath = toCanonicalName(fileName); - fileWatcherCallbacks.add(filePath, callback); - const dirPath = getDirectoryPath(filePath) || "."; - const watcher = dirWatchers.get(dirPath) || createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath); - watcher.referenceCount++; - return { - close: () => { - if (watcher.referenceCount === 1) { - watcher.close(); - dirWatchers.delete(dirPath); - } - else { - watcher.referenceCount--; - } - fileWatcherCallbacks.remove(filePath, callback); - } - }; - } - - function createDirectoryWatcher(dirName: string, dirPath: string) { - const watcher = fsWatchDirectory( - dirName, - (_eventName: string, relativeFileName) => { - // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" - if (!isString(relativeFileName)) { return; } - const fileName = getNormalizedAbsolutePath(relativeFileName, dirName); - // Some applications save a working file via rename operations - const callbacks = fileName && fileWatcherCallbacks.get(toCanonicalName(fileName)); - if (callbacks) { - for (const fileCallback of callbacks) { - fileCallback(fileName, FileWatcherEventKind.Changed); - } - } - } - ) as DirectoryWatcher; - watcher.referenceCount = 0; - dirWatchers.set(dirPath, watcher); - return watcher; - } - } - - function fsWatchFileWorker(fileName: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher { - _fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged); + function fsWatchFileWorker(fileName: string, callback: FileWatcherCallback, pollingInterval: number): FileWatcher { + _fs.watchFile(fileName, { persistent: true, interval: pollingInterval }, fileChanged); let eventKind: FileWatcherEventKind; return { close: () => _fs.unwatchFile(fileName, fileChanged) @@ -1131,37 +1433,14 @@ namespace ts { } } - type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string | undefined) => void; - - function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { - return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", ""); - } - - function createFsWatchCallbackForFileWatcherCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback { - return eventName => { - if (eventName === "rename") { - callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted); - } - else { - // Change - callback(fileName, FileWatcherEventKind.Changed); - } - }; - } - - function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback): FsWatchCallback { - return (eventName, relativeFileName) => { - // In watchDirectory we only care about adding and removing files (when event name is - // "rename"); changes made within files are handled by corresponding fileWatchers (when - // event name is "change") - if (eventName === "rename") { - // When deleting a file, the passed baseFileName is null - callback(!relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName))); - } - }; - } - - function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind.File | FileSystemEntryKind.Directory, callback: FsWatchCallback, recursive: boolean, fallbackPollingWatchFile: HostWatchFile, pollingInterval?: number): FileWatcher { + function fsWatch( + fileOrDirectory: string, + entryKind: FileSystemEntryKind, + callback: FsWatchCallback, + recursive: boolean, + fallbackPollingInterval: PollingInterval, + fallbackOptions: WatchOptions | undefined + ): FileWatcher { let options: any; let lastDirectoryPartWithDirectorySeparator: string | undefined; let lastDirectoryPart: string | undefined; @@ -1205,7 +1484,7 @@ namespace ts { // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) if (options === undefined) { - if (isNode4OrLater && (process.platform === "win32" || process.platform === "darwin")) { + if (fsSupportsRecursiveFsWatch) { options = { persistent: true, recursive: !!recursive }; } else { @@ -1250,7 +1529,12 @@ namespace ts { */ function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher { sysLog(`sysLog:: ${fileOrDirectory}:: Changing to fsWatchFile`); - return fallbackPollingWatchFile(fileOrDirectory, createFileWatcherCallback(callback), pollingInterval); + return watchFile( + fileOrDirectory, + createFileWatcherCallback(callback), + fallbackPollingInterval, + fallbackOptions + ); } /** @@ -1258,37 +1542,22 @@ namespace ts { * and switch to existing file or directory when the missing filesystem entry is created */ function watchMissingFileSystemEntry(): FileWatcher { - return fallbackPollingWatchFile(fileOrDirectory, (_fileName, eventKind) => { - if (eventKind === FileWatcherEventKind.Created && fileSystemEntryExists(fileOrDirectory, entryKind)) { - // Call the callback for current file or directory - // For now it could be callback for the inner directory creation, - // but just return current directory, better than current no-op - invokeCallbackAndUpdateWatcher(watchPresentFileSystemEntry); - } - }, pollingInterval); + return watchFile( + fileOrDirectory, + (_fileName, eventKind) => { + if (eventKind === FileWatcherEventKind.Created && fileSystemEntryExists(fileOrDirectory, entryKind)) { + // Call the callback for current file or directory + // For now it could be callback for the inner directory creation, + // but just return current directory, better than current no-op + invokeCallbackAndUpdateWatcher(watchPresentFileSystemEntry); + } + }, + fallbackPollingInterval, + fallbackOptions + ); } } - function watchFileUsingFsWatch(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) { - return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval); - } - - function createWatchFileUsingDynamicWatchFile(watchFile: HostWatchFile): HostWatchFile { - return (fileName, callback, pollingInterval) => fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval); - } - - function fsWatchDirectory(directoryName: string, callback: FsWatchCallback, recursive?: boolean): FileWatcher { - return fsWatch(directoryName, FileSystemEntryKind.Directory, callback, !!recursive, fsWatchFile); - } - - function watchDirectoryUsingFsWatch(directoryName: string, callback: DirectoryWatcherCallback, recursive?: boolean) { - return fsWatchDirectory(directoryName, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), recursive); - } - - function createWatchDirectoryUsing(fsWatchFile: HostWatchFile): HostWatchDirectory { - return (directoryName, callback) => fsWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium); - } - function readFileWorker(fileName: string, _encoding?: string): string | undefined { if (!fileExists(fileName)) { return undefined; diff --git a/src/compiler/tsbuildPublic.ts b/src/compiler/tsbuildPublic.ts index 8b207a312fa3b..cbc4d168977ff 100644 --- a/src/compiler/tsbuildPublic.ts +++ b/src/compiler/tsbuildPublic.ts @@ -205,8 +205,8 @@ namespace ts { return createSolutionBuilderWorker(/*watch*/ false, host, rootNames, defaultOptions); } - export function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder { - return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions); + export function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder { + return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions, baseWatchOptions); } type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic; @@ -232,6 +232,7 @@ namespace ts { readonly options: BuildOptions; readonly baseCompilerOptions: CompilerOptions; readonly rootNames: readonly string[]; + readonly baseWatchOptions: WatchOptions | undefined; readonly resolvedConfigFilePaths: Map; readonly configFileCache: ConfigFileMap; @@ -272,7 +273,7 @@ namespace ts { writeLog: (s: string) => void; } - function createSolutionBuilderState(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: readonly string[], options: BuildOptions): SolutionBuilderState { + function createSolutionBuilderState(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: readonly string[], options: BuildOptions, baseWatchOptions: WatchOptions | undefined): SolutionBuilderState { const host = hostOrHostWithWatch as SolutionBuilderHost; const hostWithWatch = hostOrHostWithWatch as SolutionBuilderWithWatchHost; const currentDirectory = host.getCurrentDirectory(); @@ -306,6 +307,7 @@ namespace ts { options, baseCompilerOptions, rootNames, + baseWatchOptions, resolvedConfigFilePaths: createMap(), configFileCache: createConfigFileMap(), @@ -374,7 +376,7 @@ namespace ts { } let diagnostic: Diagnostic | undefined; - const { parseConfigFileHost, baseCompilerOptions, extendedConfigCache, host } = state; + const { parseConfigFileHost, baseCompilerOptions, baseWatchOptions, extendedConfigCache, host } = state; let parsed: ParsedCommandLine | undefined; if (host.getParsedCommandLine) { parsed = host.getParsedCommandLine(configFileName); @@ -382,7 +384,7 @@ namespace ts { } else { parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; - parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache); + parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache, baseWatchOptions); parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; } configFileCache.set(configFilePath, parsed || diagnostic!); @@ -1147,7 +1149,7 @@ namespace ts { } if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - watchConfigFile(state, project, projectPath); + watchConfigFile(state, project, projectPath, config); watchWildCardDirectories(state, project, projectPath, config); watchInputFiles(state, project, projectPath, config); } @@ -1751,7 +1753,7 @@ namespace ts { reportErrorSummary(state, buildOrder); } - function watchConfigFile(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath) { + function watchConfigFile(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) { if (!state.watch || state.allWatchedConfigFiles.has(resolvedPath)) return; state.allWatchedConfigFiles.set(resolvedPath, state.watchFile( state.hostWithWatch, @@ -1760,6 +1762,7 @@ namespace ts { invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Full); }, PollingInterval.High, + parsed?.watchOptions, WatchType.ConfigFile, resolved )); @@ -1820,6 +1823,7 @@ namespace ts { invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Partial); }, flags, + parsed?.watchOptions, WatchType.WildcardDirectory, resolved ) @@ -1837,6 +1841,7 @@ namespace ts { input, () => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.None), PollingInterval.Low, + parsed?.watchOptions, path as Path, WatchType.SourceFile, resolved @@ -1851,10 +1856,9 @@ namespace ts { state.watchAllProjectsPending = false; for (const resolved of getBuildOrderFromAnyBuildOrder(buildOrder)) { const resolvedPath = toResolvedConfigFilePath(state, resolved); - // Watch this file - watchConfigFile(state, resolved, resolvedPath); - const cfg = parseConfigFile(state, resolved, resolvedPath); + // Watch this file + watchConfigFile(state, resolved, resolvedPath, cfg); if (cfg) { // Update watchers for wildcard directories watchWildCardDirectories(state, resolved, resolvedPath, cfg); @@ -1870,9 +1874,9 @@ namespace ts { * can dynamically add/remove other projects based on changes on the rootNames' references */ function createSolutionBuilderWorker(watch: false, host: SolutionBuilderHost, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder; - function createSolutionBuilderWorker(watch: true, host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder; - function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: readonly string[], options: BuildOptions): SolutionBuilder { - const state = createSolutionBuilderState(watch, hostOrHostWithWatch, rootNames, options); + function createSolutionBuilderWorker(watch: true, host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder; + function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: readonly string[], options: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder { + const state = createSolutionBuilderState(watch, hostOrHostWithWatch, rootNames, options, baseWatchOptions); return { build: (project, cancellationToken) => build(state, project, cancellationToken), clean: project => clean(state, project), diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 89a88bc0321bd..1e0e32bcc45f0 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4929,6 +4929,26 @@ namespace ts { circular?: boolean; } + export enum WatchFileKind { + FixedPollingInterval, + PriorityPollingInterval, + DynamicPriorityPolling, + UseFsEvents, + UseFsEventsOnParentDirectory, + } + + export enum WatchDirectoryKind { + UseFsEvents, + FixedPollingInterval, + DynamicPriorityPolling, + } + + export enum PollingWatchKind { + FixedInterval, + PriorityInterval, + DynamicPriority, + } + export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | PluginImport[] | ProjectReference[] | null | undefined; export interface CompilerOptions { @@ -5043,6 +5063,15 @@ namespace ts { [option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined; } + export interface WatchOptions { + watchFile?: WatchFileKind; + watchDirectory?: WatchDirectoryKind; + fallbackPolling?: PollingWatchKind; + synchronousWatchDirectory?: boolean; + + [option: string]: CompilerOptionsValue | undefined; + } + export interface TypeAcquisition { /** * @deprecated typingOptions.enableAutoDiscovery @@ -5130,6 +5159,7 @@ namespace ts { typeAcquisition?: TypeAcquisition; fileNames: string[]; projectReferences?: readonly ProjectReference[]; + watchOptions?: WatchOptions; raw?: any; errors: Diagnostic[]; wildcardDirectories?: MapLike; @@ -5210,7 +5240,8 @@ namespace ts { } /* @internal */ - export interface DidYouMeanOptionalDiagnostics { + export interface DidYouMeanOptionsDiagnostics { + optionDeclarations: CommandLineOption[]; unknownOptionDiagnostic: DiagnosticMessage, unknownDidYouMeanDiagnostic: DiagnosticMessage, } @@ -5219,7 +5250,7 @@ namespace ts { export interface TsConfigOnlyOption extends CommandLineOptionBase { type: "object"; elementOptions?: Map; - extraKeyDiagnostics?: DidYouMeanOptionalDiagnostics; + extraKeyDiagnostics?: DidYouMeanOptionsDiagnostics; } /* @internal */ diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index aae84ffeed2c0..24d9463f96835 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -89,10 +89,10 @@ namespace ts { } /** Parses config file using System interface */ - export function parseConfigFileWithSystem(configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter) { + export function parseConfigFileWithSystem(configFileName: string, optionsToExtend: CompilerOptions, watchOptionsToExtend: WatchOptions | undefined, system: System, reportDiagnostic: DiagnosticReporter) { const host: ParseConfigFileHost = system; host.onUnRecoverableConfigFileDiagnostic = diagnostic => reportUnrecoverableDiagnostic(system, reportDiagnostic, diagnostic); - const result = getParsedCommandLineOfConfigFile(configFileName, optionsToExtend, host); + const result = getParsedCommandLineOfConfigFile(configFileName, optionsToExtend, host, /*extendedConfigCache*/ undefined, watchOptionsToExtend); host.onUnRecoverableConfigFileDiagnostic = undefined!; // TODO: GH#18217 return result; } @@ -419,22 +419,24 @@ namespace ts { /** * Creates the watch compiler host from system for config file in watch mode */ - export function createWatchCompilerHostOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfConfigFile { + export function createWatchCompilerHostOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions | undefined, watchOptionsToExtend: WatchOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfConfigFile { const diagnosticReporter = reportDiagnostic || createDiagnosticReporter(system); const host = createWatchCompilerHost(system, createProgram, diagnosticReporter, reportWatchStatus) as WatchCompilerHostOfConfigFile; host.onUnRecoverableConfigFileDiagnostic = diagnostic => reportUnrecoverableDiagnostic(system, diagnosticReporter, diagnostic); host.configFileName = configFileName; host.optionsToExtend = optionsToExtend; + host.watchOptionsToExtend = watchOptionsToExtend; return host; } /** * Creates the watch compiler host from system for compiling root files and options in watch mode */ - export function createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles: string[], options: CompilerOptions, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, projectReferences?: readonly ProjectReference[]): WatchCompilerHostOfFilesAndCompilerOptions { + export function createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles: string[], options: CompilerOptions, watchOptions: WatchOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, projectReferences?: readonly ProjectReference[]): WatchCompilerHostOfFilesAndCompilerOptions { const host = createWatchCompilerHost(system, createProgram, reportDiagnostic || createDiagnosticReporter(system), reportWatchStatus) as WatchCompilerHostOfFilesAndCompilerOptions; host.rootFiles = rootFiles; host.options = options; + host.watchOptions = watchOptions; host.projectReferences = projectReferences; return host; } diff --git a/src/compiler/watchPublic.ts b/src/compiler/watchPublic.ts index 6e92af866918c..3a8ef1debde2a 100644 --- a/src/compiler/watchPublic.ts +++ b/src/compiler/watchPublic.ts @@ -52,9 +52,9 @@ namespace ts { onWatchStatusChange?(diagnostic: Diagnostic, newLine: string, options: CompilerOptions, errorCount?: number): void; /** Used to watch changes in source files, missing files needed to update the program or config file */ - watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; + watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: CompilerOptions): FileWatcher; /** Used to watch resolved module's failed lookup locations, config file specs, type roots where auto type reference directives are added */ - watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; + watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: CompilerOptions): FileWatcher; /** If provided, will be used to set delayed compilation, so that multiple changes in short span are compiled together */ setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; /** If provided, will be used to reset existing delayed compilation */ @@ -133,6 +133,8 @@ namespace ts { /** Compiler options */ options: CompilerOptions; + watchOptions?: WatchOptions; + /** Project References */ projectReferences?: readonly ProjectReference[]; } @@ -147,6 +149,8 @@ namespace ts { /** Options to extend */ optionsToExtend?: CompilerOptions; + watchOptionsToExtend?: WatchOptions; + /** * Used to generate source file names from the config file and its include, exclude, files rules * and also to cache the directory stucture @@ -159,7 +163,6 @@ namespace ts { */ /*@internal*/ export interface WatchCompilerHostOfConfigFile extends WatchCompilerHost { - optionsToExtend?: CompilerOptions; configFileParsingResult?: ParsedCommandLine; } @@ -190,14 +193,14 @@ namespace ts { /** * Create the watch compiler host for either configFile or fileNames and its options */ - export function createWatchCompilerHost(configFileName: string, optionsToExtend: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfConfigFile; - export function createWatchCompilerHost(rootFiles: string[], options: CompilerOptions, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, projectReferences?: readonly ProjectReference[]): WatchCompilerHostOfFilesAndCompilerOptions; - export function createWatchCompilerHost(rootFilesOrConfigFileName: string | string[], options: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, projectReferences?: readonly ProjectReference[]): WatchCompilerHostOfFilesAndCompilerOptions | WatchCompilerHostOfConfigFile { + export function createWatchCompilerHost(configFileName: string, optionsToExtend: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, watchOptionsToExtend?: WatchOptions): WatchCompilerHostOfConfigFile; + export function createWatchCompilerHost(rootFiles: string[], options: CompilerOptions, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, projectReferences?: readonly ProjectReference[], watchOptions?: WatchOptions): WatchCompilerHostOfFilesAndCompilerOptions; + export function createWatchCompilerHost(rootFilesOrConfigFileName: string | string[], options: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, projectReferencesOrWatchOptionsToExtend?: readonly ProjectReference[] | WatchOptions, watchOptions?: WatchOptions): WatchCompilerHostOfFilesAndCompilerOptions | WatchCompilerHostOfConfigFile { if (isArray(rootFilesOrConfigFileName)) { - return createWatchCompilerHostOfFilesAndCompilerOptions(rootFilesOrConfigFileName, options!, system, createProgram, reportDiagnostic, reportWatchStatus, projectReferences); // TODO: GH#18217 + return createWatchCompilerHostOfFilesAndCompilerOptions(rootFilesOrConfigFileName, options!, watchOptions, system, createProgram, reportDiagnostic, reportWatchStatus, projectReferencesOrWatchOptionsToExtend as readonly ProjectReference[]); // TODO: GH#18217 } else { - return createWatchCompilerHostOfConfigFile(rootFilesOrConfigFileName, options, system, createProgram, reportDiagnostic, reportWatchStatus); + return createWatchCompilerHostOfConfigFile(rootFilesOrConfigFileName, options, projectReferencesOrWatchOptionsToExtend as WatchOptions, system, createProgram, reportDiagnostic, reportWatchStatus); } } @@ -236,8 +239,8 @@ namespace ts { const useCaseSensitiveFileNames = host.useCaseSensitiveFileNames(); const currentDirectory = host.getCurrentDirectory(); - const { configFileName, optionsToExtend: optionsToExtendForConfigFile = {}, createProgram } = host; - let { rootFiles: rootFileNames, options: compilerOptions, projectReferences } = host; + const { configFileName, optionsToExtend: optionsToExtendForConfigFile = {}, watchOptionsToExtend, createProgram } = host; + let { rootFiles: rootFileNames, options: compilerOptions, watchOptions, projectReferences } = host; let configFileSpecs: ConfigFileSpecs; let configFileParsingDiagnostics: Diagnostic[] | undefined; let canConfigFileJsonReportNoInputFiles = false; @@ -270,7 +273,7 @@ namespace ts { writeLog(`Current directory: ${currentDirectory} CaseSensitiveFileNames: ${useCaseSensitiveFileNames}`); let configFileWatcher: FileWatcher | undefined; if (configFileName) { - configFileWatcher = watchFile(host, configFileName, scheduleProgramReload, PollingInterval.High, WatchType.ConfigFile); + configFileWatcher = watchFile(host, configFileName, scheduleProgramReload, PollingInterval.High, watchOptions, WatchType.ConfigFile); } const compilerHost = createCompilerHostFromProgramHost(host, () => compilerOptions, directoryStructureHost) as CompilerHost & ResolutionCacheHost; @@ -285,8 +288,8 @@ namespace ts { // Members for ResolutionCacheHost compilerHost.toPath = toPath; compilerHost.getCompilationSettings = () => compilerOptions; - compilerHost.watchDirectoryOfFailedLookupLocation = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, WatchType.FailedLookupLocations); - compilerHost.watchTypeRootsDirectory = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, WatchType.TypeRoots); + compilerHost.watchDirectoryOfFailedLookupLocation = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, watchOptions, WatchType.FailedLookupLocations); + compilerHost.watchTypeRootsDirectory = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, watchOptions, WatchType.TypeRoots); compilerHost.getCachedDirectoryStructureHost = () => cachedDirectoryStructureHost; compilerHost.onInvalidatedResolution = scheduleProgramUpdate; compilerHost.onChangedAutomaticTypeDirectiveNames = () => { @@ -469,7 +472,7 @@ namespace ts { (hostSourceFile as FilePresentOnHost).sourceFile = sourceFile; hostSourceFile.version = sourceFile.version; if (!hostSourceFile.fileWatcher) { - hostSourceFile.fileWatcher = watchFilePath(host, fileName, onSourceFileChange, PollingInterval.Low, path, WatchType.SourceFile); + hostSourceFile.fileWatcher = watchFilePath(host, fileName, onSourceFileChange, PollingInterval.Low, watchOptions, path, WatchType.SourceFile); } } else { @@ -482,7 +485,7 @@ namespace ts { } else { if (sourceFile) { - const fileWatcher = watchFilePath(host, fileName, onSourceFileChange, PollingInterval.Low, path, WatchType.SourceFile); + const fileWatcher = watchFilePath(host, fileName, onSourceFileChange, PollingInterval.Low, watchOptions, path, WatchType.SourceFile); sourceFilesCache.set(path, { sourceFile, version: sourceFile.version, fileWatcher }); } else { @@ -611,12 +614,13 @@ namespace ts { } function parseConfigFile() { - setConfigFileParsingResult(getParsedCommandLineOfConfigFile(configFileName, optionsToExtendForConfigFile, parseConfigFileHost)!); // TODO: GH#18217 + setConfigFileParsingResult(getParsedCommandLineOfConfigFile(configFileName, optionsToExtendForConfigFile, parseConfigFileHost, /*extendedConfigCache*/ undefined, watchOptionsToExtend)!); // TODO: GH#18217 } function setConfigFileParsingResult(configFileParseResult: ParsedCommandLine) { rootFileNames = configFileParseResult.fileNames; compilerOptions = configFileParseResult.options; + watchOptions = configFileParseResult.watchOptions; configFileSpecs = configFileParseResult.configFileSpecs!; // TODO: GH#18217 projectReferences = configFileParseResult.projectReferences; configFileParsingDiagnostics = getConfigFileParsingDiagnostics(configFileParseResult).slice(); @@ -645,7 +649,7 @@ namespace ts { } function watchMissingFilePath(missingFilePath: Path) { - return watchFilePath(host, missingFilePath, onMissingFileChange, PollingInterval.Medium, missingFilePath, WatchType.MissingFile); + return watchFilePath(host, missingFilePath, onMissingFileChange, PollingInterval.Medium, watchOptions, missingFilePath, WatchType.MissingFile); } function onMissingFileChange(fileName: string, eventKind: FileWatcherEventKind, missingFilePath: Path) { @@ -709,6 +713,7 @@ namespace ts { } }, flags, + watchOptions, WatchType.WildcardDirectory ); } diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 6b6789b3b79e9..b2726db862e88 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -344,15 +344,15 @@ namespace ts { } export interface WatchFileHost { - watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; + watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher; } export interface WatchDirectoryHost { - watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; + watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher; } - export type WatchFile = (host: WatchFileHost, file: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, detailInfo1: X, detailInfo2?: Y) => FileWatcher; + export type WatchFile = (host: WatchFileHost, file: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined, detailInfo1: X, detailInfo2?: Y) => FileWatcher; export type FilePathWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, filePath: Path) => void; - export type WatchFilePath = (host: WatchFileHost, file: string, callback: FilePathWatcherCallback, pollingInterval: PollingInterval, path: Path, detailInfo1: X, detailInfo2?: Y) => FileWatcher; - export type WatchDirectory = (host: WatchDirectoryHost, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags, detailInfo1: X, detailInfo2?: Y) => FileWatcher; + export type WatchFilePath = (host: WatchFileHost, file: string, callback: FilePathWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined, path: Path, detailInfo1: X, detailInfo2?: Y) => FileWatcher; + export type WatchDirectory = (host: WatchDirectoryHost, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags, options: WatchOptions | undefined, detailInfo1: X, detailInfo2?: Y) => FileWatcher; export interface WatchFactory { watchFile: WatchFile; @@ -364,9 +364,13 @@ namespace ts { return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFile, watchDirectory); } - function getWatchFactoryWith(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo: GetDetailWatchInfo | undefined, - watchFile: (host: WatchFileHost, file: string, callback: FileWatcherCallback, watchPriority: PollingInterval) => FileWatcher, - watchDirectory: (host: WatchDirectoryHost, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags) => FileWatcher): WatchFactory { + function getWatchFactoryWith( + watchLogLevel: WatchLogLevel, + log: (s: string) => void, + getDetailWatchInfo: GetDetailWatchInfo | undefined, + watchFile: (host: WatchFileHost, file: string, callback: FileWatcherCallback, watchPriority: PollingInterval, options: WatchOptions | undefined) => FileWatcher, + watchDirectory: (host: WatchDirectoryHost, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags, options: WatchOptions | undefined) => FileWatcher + ): WatchFactory { const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchFile); const createFilePathWatcher: CreateFileWatcher = watchLogLevel === WatchLogLevel.None ? watchFilePath : createFileWatcher; const createDirectoryWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchDirectory); @@ -374,32 +378,32 @@ namespace ts { setSysLog(s => log(s)); } return { - watchFile: (host, file, callback, pollingInterval, detailInfo1, detailInfo2) => - createFileWatcher(host, file, callback, pollingInterval, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo), - watchFilePath: (host, file, callback, pollingInterval, path, detailInfo1, detailInfo2) => - createFilePathWatcher(host, file, callback, pollingInterval, path, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo), - watchDirectory: (host, directory, callback, flags, detailInfo1, detailInfo2) => - createDirectoryWatcher(host, directory, callback, flags, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchDirectory, log, "DirectoryWatcher", getDetailWatchInfo) + watchFile: (host, file, callback, pollingInterval, options, detailInfo1, detailInfo2) => + createFileWatcher(host, file, callback, pollingInterval, options, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo), + watchFilePath: (host, file, callback, pollingInterval, options, path, detailInfo1, detailInfo2) => + createFilePathWatcher(host, file, callback, pollingInterval, options, path, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo), + watchDirectory: (host, directory, callback, flags, options, detailInfo1, detailInfo2) => + createDirectoryWatcher(host, directory, callback, flags, options, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchDirectory, log, "DirectoryWatcher", getDetailWatchInfo) }; + } - function watchFilePath(host: WatchFileHost, file: string, callback: FilePathWatcherCallback, pollingInterval: PollingInterval, path: Path): FileWatcher { - return watchFile(host, file, (fileName, eventKind) => callback(fileName, eventKind, path), pollingInterval); - } + function watchFile(host: WatchFileHost, file: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined): FileWatcher { + return host.watchFile(file, callback, pollingInterval, options); } - function watchFile(host: WatchFileHost, file: string, callback: FileWatcherCallback, pollingInterval: PollingInterval): FileWatcher { - return host.watchFile(file, callback, pollingInterval); + function watchFilePath(host: WatchFileHost, file: string, callback: FilePathWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined, path: Path): FileWatcher { + return watchFile(host, file, (fileName, eventKind) => callback(fileName, eventKind, path), pollingInterval, options); } - function watchDirectory(host: WatchDirectoryHost, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { - return host.watchDirectory(directory, callback, (flags & WatchDirectoryFlags.Recursive) !== 0); + function watchDirectory(host: WatchDirectoryHost, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags, options: WatchOptions | undefined): FileWatcher { + return host.watchDirectory(directory, callback, (flags & WatchDirectoryFlags.Recursive) !== 0, options); } type WatchCallback = (fileName: string, cbOptional?: T, passThrough?: U) => void; - type AddWatch = (host: H, file: string, cb: WatchCallback, flags: T, passThrough?: V, detailInfo1?: undefined, detailInfo2?: undefined) => FileWatcher; + type AddWatch = (host: H, file: string, cb: WatchCallback, flags: T, options: WatchOptions | undefined, passThrough?: V, detailInfo1?: undefined, detailInfo2?: undefined) => FileWatcher; export type GetDetailWatchInfo = (detailInfo1: X, detailInfo2: Y | undefined) => string; - type CreateFileWatcher = (host: H, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined) => FileWatcher; + type CreateFileWatcher = (host: H, file: string, cb: WatchCallback, flags: T, options: WatchOptions | undefined, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined) => FileWatcher; function getCreateFileWatcher(watchLogLevel: WatchLogLevel, addWatch: AddWatch): CreateFileWatcher { switch (watchLogLevel) { case WatchLogLevel.None: @@ -411,27 +415,27 @@ namespace ts { } } - function createFileWatcherWithLogging(host: H, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { - log(`${watchCaption}:: Added:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`); - const watcher = createFileWatcherWithTriggerLogging(host, file, cb, flags, passThrough, detailInfo1, detailInfo2, addWatch, log, watchCaption, getDetailWatchInfo); + function createFileWatcherWithLogging(host: H, file: string, cb: WatchCallback, flags: T, options: WatchOptions | undefined, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { + log(`${watchCaption}:: Added:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`); + const watcher = createFileWatcherWithTriggerLogging(host, file, cb, flags, options, passThrough, detailInfo1, detailInfo2, addWatch, log, watchCaption, getDetailWatchInfo); return { close: () => { - log(`${watchCaption}:: Close:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`); + log(`${watchCaption}:: Close:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`); watcher.close(); } }; } - function createDirectoryWatcherWithLogging(host: H, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { - const watchInfo = `${watchCaption}:: Added:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`; + function createDirectoryWatcherWithLogging(host: H, file: string, cb: WatchCallback, flags: T, options: WatchOptions | undefined, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { + const watchInfo = `${watchCaption}:: Added:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`; log(watchInfo); const start = timestamp(); - const watcher = createFileWatcherWithTriggerLogging(host, file, cb, flags, passThrough, detailInfo1, detailInfo2, addWatch, log, watchCaption, getDetailWatchInfo); + const watcher = createFileWatcherWithTriggerLogging(host, file, cb, flags, options, passThrough, detailInfo1, detailInfo2, addWatch, log, watchCaption, getDetailWatchInfo); const elapsed = timestamp() - start; log(`Elapsed:: ${elapsed}ms ${watchInfo}`); return { close: () => { - const watchInfo = `${watchCaption}:: Close:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`; + const watchInfo = `${watchCaption}:: Close:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`; log(watchInfo); const start = timestamp(); watcher.close(); @@ -441,19 +445,28 @@ namespace ts { }; } - function createFileWatcherWithTriggerLogging(host: H, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { + function createFileWatcherWithTriggerLogging(host: H, file: string, cb: WatchCallback, flags: T, options: WatchOptions | undefined, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { return addWatch(host, file, (fileName, cbOptional) => { - const triggerredInfo = `${watchCaption}:: Triggered with ${fileName} ${cbOptional !== undefined ? cbOptional : ""}:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`; + const triggerredInfo = `${watchCaption}:: Triggered with ${fileName} ${cbOptional !== undefined ? cbOptional : ""}:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`; log(triggerredInfo); const start = timestamp(); cb(fileName, cbOptional, passThrough); const elapsed = timestamp() - start; log(`Elapsed:: ${elapsed}ms ${triggerredInfo}`); - }, flags); + }, flags, options); + } + + export function getFallbackOptions(options: WatchOptions | undefined): WatchOptions { + const fallbackPolling = options?.fallbackPolling; + return { + watchFile: fallbackPolling !== undefined ? + fallbackPolling as unknown as WatchFileKind : + WatchFileKind.PriorityPollingInterval + }; } - function getWatchInfo(file: string, flags: T, detailInfo1: X, detailInfo2: Y | undefined, getDetailWatchInfo: GetDetailWatchInfo | undefined) { - return `WatchInfo: ${file} ${flags} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : detailInfo2 === undefined ? detailInfo1 : `${detailInfo1} ${detailInfo2}`}`; + function getWatchInfo(file: string, flags: T, options: WatchOptions | undefined, detailInfo1: X, detailInfo2: Y | undefined, getDetailWatchInfo: GetDetailWatchInfo | undefined) { + return `WatchInfo: ${file} ${flags} ${JSON.stringify(options)} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : detailInfo2 === undefined ? detailInfo1 : `${detailInfo1} ${detailInfo2}`}`; } export function closeFileWatcherOf(objWithWatcher: T) { diff --git a/src/executeCommandLine/executeCommandLine.ts b/src/executeCommandLine/executeCommandLine.ts index d730de4a981d3..94d7a19f30e25 100644 --- a/src/executeCommandLine/executeCommandLine.ts +++ b/src/executeCommandLine/executeCommandLine.ts @@ -251,7 +251,7 @@ namespace ts { fileName => getNormalizedAbsolutePath(fileName, currentDirectory) ); if (configFileName) { - const configParseResult = parseConfigFileWithSystem(configFileName, commandLineOptions, sys, reportDiagnostic)!; // TODO: GH#18217 + const configParseResult = parseConfigFileWithSystem(configFileName, commandLineOptions, commandLine.watchOptions, sys, reportDiagnostic)!; // TODO: GH#18217 if (commandLineOptions.showConfig) { if (configParseResult.errors.length !== 0) { reportDiagnostic = updateReportDiagnostic( @@ -277,7 +277,8 @@ namespace ts { sys, reportDiagnostic, configParseResult, - commandLineOptions + commandLineOptions, + commandLine.watchOptions ); } else if (isIncrementalCompilation(configParseResult.options)) { @@ -314,7 +315,8 @@ namespace ts { sys, reportDiagnostic, commandLine.fileNames, - commandLineOptions + commandLineOptions, + commandLine.watchOptions ); } else if (isIncrementalCompilation(commandLineOptions)) { @@ -389,6 +391,7 @@ namespace ts { sys: System, cb: ExecuteCommandLineCallbacks | undefined, buildOptions: BuildOptions, + watchOptions: WatchOptions | undefined, projects: string[], errors: Diagnostic[] ) { @@ -437,7 +440,7 @@ namespace ts { if (cb && cb.onSolutionBuilderHostCreate) cb.onSolutionBuilderHostCreate(buildHost); updateCreateProgram(sys, buildHost); buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(sys, program.getProgram()); - const builder = createSolutionBuilderWithWatch(buildHost, projects, buildOptions); + const builder = createSolutionBuilderWithWatch(buildHost, projects, buildOptions, watchOptions); builder.build(); return; } @@ -463,12 +466,13 @@ namespace ts { cb: ExecuteCommandLineCallbacks | undefined, args: readonly string[] ) { - const { buildOptions, projects, errors } = parseBuildCommand(args); + const { buildOptions, watchOptions, projects, errors } = parseBuildCommand(args); if (buildOptions.generateCpuProfile && sys.enableCPUProfiler) { sys.enableCPUProfiler(buildOptions.generateCpuProfile, () => performBuildWorker( sys, cb, buildOptions, + watchOptions, projects, errors )); @@ -478,6 +482,7 @@ namespace ts { sys, cb, buildOptions, + watchOptions, projects, errors ); @@ -576,11 +581,13 @@ namespace ts { sys: System, reportDiagnostic: DiagnosticReporter, configParseResult: ParsedCommandLine, - optionsToExtend: CompilerOptions + optionsToExtend: CompilerOptions, + watchOptionsToExtend: WatchOptions | undefined ) { const watchCompilerHost = createWatchCompilerHostOfConfigFile( configParseResult.options.configFilePath!, optionsToExtend, + watchOptionsToExtend, sys, /*createProgram*/ undefined, reportDiagnostic, @@ -595,11 +602,13 @@ namespace ts { sys: System, reportDiagnostic: DiagnosticReporter, rootFiles: string[], - options: CompilerOptions + options: CompilerOptions, + watchOptions: WatchOptions | undefined ) { const watchCompilerHost = createWatchCompilerHostOfFilesAndCompilerOptions( rootFiles, options, + watchOptions, sys, /*createProgram*/ undefined, reportDiagnostic, diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 75d08adfcab8e..5b29c8532e6d7 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -37,6 +37,8 @@ interface Array { length: number; [n: number]: T; }` newLine?: string; windowsStyleRoot?: string; environmentVariables?: Map; + runWithoutRecursiveWatches?: boolean; + runWithFallbackPolling?: boolean; } export function createWatchedSystem(fileOrFolderList: readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost { @@ -120,6 +122,11 @@ interface Array { length: number; [n: number]: T; }` } } + function createWatcher(map: MultiMap, path: string, callback: T): FileWatcher { + map.add(path, callback); + return { close: () => map.remove(path, callback) }; + } + function getDiffInKeys(map: Map, expectedKeys: readonly string[]) { if (map.size === expectedKeys.length) { return ""; @@ -151,60 +158,99 @@ interface Array { length: number; [n: number]: T; }` assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}${getDiffInKeys(map, expectedKeys)}`); } - function checkMapKeys(caption: string, map: Map, expectedKeys: readonly string[]) { - verifyMapSize(caption, map, expectedKeys); - for (const name of expectedKeys) { - assert.isTrue(map.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(map.keys())}`); - } - } - - export function checkMultiMapKeyCount(caption: string, actual: MultiMap, expectedKeys: ReadonlyMap): void; - export function checkMultiMapKeyCount(caption: string, actual: MultiMap, expectedKeys: readonly string[], eachKeyCount: number): void; - export function checkMultiMapKeyCount(caption: string, actual: MultiMap, expectedKeysMapOrArray: ReadonlyMap | readonly string[], eachKeyCount?: number) { - const expectedKeys = isArray(expectedKeysMapOrArray) ? arrayToMap(expectedKeysMapOrArray, s => s, () => eachKeyCount!) : expectedKeysMapOrArray; - verifyMapSize(caption, actual, arrayFrom(expectedKeys.keys())); + export type MapValueTester = [Map | undefined, (value: T) => U]; + + export function checkMap(caption: string, actual: MultiMap, expectedKeys: ReadonlyMap, valueTester?: MapValueTester): void; + export function checkMap(caption: string, actual: MultiMap, expectedKeys: readonly string[], eachKeyCount: number, valueTester?: MapValueTester): void; + export function checkMap(caption: string, actual: Map | MultiMap, expectedKeys: readonly string[], eachKeyCount: undefined): void; + export function checkMap( + caption: string, + actual: Map | MultiMap, + expectedKeysMapOrArray: ReadonlyMap | readonly string[], + eachKeyCountOrValueTester?: number | MapValueTester, + valueTester?: MapValueTester) { + const expectedKeys = isArray(expectedKeysMapOrArray) ? arrayToMap(expectedKeysMapOrArray, s => s, () => eachKeyCountOrValueTester as number) : expectedKeysMapOrArray; + verifyMapSize(caption, actual, isArray(expectedKeysMapOrArray) ? expectedKeysMapOrArray : arrayFrom(expectedKeys.keys())); + if (!isNumber(eachKeyCountOrValueTester)) { + valueTester = eachKeyCountOrValueTester; + } + const [expectedValues, valueMapper] = valueTester || [undefined, undefined!]; expectedKeys.forEach((count, name) => { assert.isTrue(actual.has(name), `${caption}: expected to contain ${name}, actual keys: ${arrayFrom(actual.keys())}`); - assert.equal(actual.get(name)!.length, count, `${caption}: Expected to be have ${count} entries for ${name}. Actual entry: ${JSON.stringify(actual.get(name))}`); + // Check key information only if eachKeyCount is provided + if (!isArray(expectedKeysMapOrArray) || eachKeyCountOrValueTester !== undefined) { + assert.equal((actual as MultiMap).get(name)!.length, count, `${caption}: Expected to be have ${count} entries for ${name}. Actual entry: ${JSON.stringify(actual.get(name))}`); + if (expectedValues) { + assert.deepEqual( + (actual as MultiMap).get(name)!.map(valueMapper), + expectedValues.get(name), + `${caption}:: expected values mismatch for ${name}` + ); + } + } }); } export function checkArray(caption: string, actual: readonly string[], expected: readonly string[]) { - checkMapKeys(caption, arrayToMap(actual, identity), expected); - assert.equal(actual.length, expected.length, `${caption}: incorrect actual number of files, expected:\r\n${expected.join("\r\n")}\r\ngot: ${actual.join("\r\n")}`); - for (const f of expected) { - assert.isTrue(contains(actual, f), `${caption}: expected to find ${f} in ${actual}`); - } + checkMap(caption, arrayToMap(actual, identity), expected, /*eachKeyCount*/ undefined); } export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[], additionalInfo?: string) { - checkMapKeys(`watchedFiles:: ${additionalInfo || ""}::`, host.watchedFiles, expectedFiles); + checkMap(`watchedFiles:: ${additionalInfo || ""}::`, host.watchedFiles, expectedFiles, /*eachKeyCount*/ undefined); } - export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: ReadonlyMap): void; - export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: readonly string[], eachFileWatchCount: number): void; - export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: ReadonlyMap | readonly string[], eachFileWatchCount?: number) { + export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: ReadonlyMap, expectedPollingIntervals?: Map): void; + export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: readonly string[], eachFileWatchCount: number, expectedPollingIntervals?: Map): void; + export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: ReadonlyMap | readonly string[], eachFileWatchCount?: number | Map, expectedPollingIntervals?: Map) { + if (!isNumber(eachFileWatchCount)) expectedPollingIntervals = eachFileWatchCount; if (isArray(expectedFiles)) { - checkMultiMapKeyCount("watchedFiles", host.watchedFiles, expectedFiles, eachFileWatchCount!); + checkMap( + "watchedFiles", + host.watchedFiles, + expectedFiles, + eachFileWatchCount as number, + [expectedPollingIntervals, ({ pollingInterval }) => pollingInterval] + ); } else { - checkMultiMapKeyCount("watchedFiles", host.watchedFiles, expectedFiles); + checkMap( + "watchedFiles", + host.watchedFiles, + expectedFiles, + [expectedPollingIntervals, ({ pollingInterval }) => pollingInterval] + ); } } export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive: boolean) { - checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); + checkMap(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.fsWatchesRecursive : host.fsWatches, expectedDirectories, /*eachKeyCount*/ undefined); } - export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: ReadonlyMap, recursive: boolean): void; - export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: readonly string[], eachDirectoryWatchCount: number, recursive: boolean): void; - export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: ReadonlyMap | readonly string[], recursiveOrEachDirectoryWatchCount: boolean | number, recursive?: boolean) { + export interface FallbackPollingOptions { + fallbackPollingInterval: PollingInterval; + fallbackOptions: WatchOptions | undefined; + } + export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: ReadonlyMap, recursive: boolean, expectedFallbacks?: Map): void; + export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: readonly string[], eachDirectoryWatchCount: number, recursive: boolean, expectedFallbacks?: Map): void; + export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: ReadonlyMap | readonly string[], recursiveOrEachDirectoryWatchCount: boolean | number, recursiveOrExpectedFallbacks?: boolean | Map, expectedFallbacks?: Map) { + if (typeof recursiveOrExpectedFallbacks !== "boolean") expectedFallbacks = recursiveOrExpectedFallbacks; if (isArray(expectedDirectories)) { - checkMultiMapKeyCount(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories, recursiveOrEachDirectoryWatchCount as number); + checkMap( + `fsWatches${recursiveOrExpectedFallbacks ? " recursive" : ""}`, + recursiveOrExpectedFallbacks as boolean ? host.fsWatchesRecursive : host.fsWatches, + expectedDirectories, + recursiveOrEachDirectoryWatchCount as number, + [expectedFallbacks, ({ fallbackPollingInterval, fallbackOptions }) => ({ fallbackPollingInterval, fallbackOptions })] + ); } else { - recursive = recursiveOrEachDirectoryWatchCount as boolean; - checkMultiMapKeyCount(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); + recursiveOrExpectedFallbacks = recursiveOrEachDirectoryWatchCount as boolean; + checkMap( + `fsWatches{recursive ? " recursive" : ""}`, + recursiveOrExpectedFallbacks ? host.fsWatchesRecursive : host.fsWatches, + expectedDirectories, + [expectedFallbacks, ({ fallbackPollingInterval, fallbackOptions }) => ({ fallbackPollingInterval, fallbackOptions })] + ); } } @@ -279,11 +325,13 @@ interface Array { length: number; [n: number]: T; }` export interface TestFileWatcher { cb: FileWatcherCallback; fileName: string; + pollingInterval: PollingInterval; } - export interface TestDirectoryWatcher { - cb: DirectoryWatcherCallback; - directoryName: string; + export interface TestFsWatcher { + cb: FsWatchCallback; + fallbackPollingInterval: PollingInterval; + fallbackOptions: WatchOptions | undefined; } export interface ReloadWatchInvokeOptions { @@ -330,25 +378,26 @@ interface Array { length: number; [n: number]: T; }` private immediateCallbacks = new Callbacks(); readonly screenClears: number[] = []; - readonly watchedDirectories = createMultiMap(); - readonly watchedDirectoriesRecursive = createMultiMap(); readonly watchedFiles = createMultiMap(); + readonly fsWatches = createMultiMap(); + readonly fsWatchesRecursive = createMultiMap(); + runWithFallbackPolling: boolean; public readonly useCaseSensitiveFileNames: boolean; public readonly newLine: string; public readonly windowsStyleRoot?: string; private readonly environmentVariables?: Map; private readonly executingFilePath: string; private readonly currentDirectory: string; - private readonly customWatchFile: HostWatchFile | undefined; - private readonly customRecursiveWatchDirectory: HostWatchDirectory | undefined; public require: ((initialPath: string, moduleName: string) => RequireResult) | undefined; - + watchFile: HostWatchFile; + watchDirectory: HostWatchDirectory; constructor( public withSafeList: boolean, fileOrFolderorSymLinkList: readonly FileOrFolderOrSymLink[], { useCaseSensitiveFileNames, executingFilePath, currentDirectory, - newLine, windowsStyleRoot, environmentVariables + newLine, windowsStyleRoot, environmentVariables, + runWithoutRecursiveWatches, runWithFallbackPolling }: TestServerHostCreationParameters = {}) { this.useCaseSensitiveFileNames = !!useCaseSensitiveFileNames; this.newLine = newLine || "\n"; @@ -359,56 +408,35 @@ interface Array { length: number; [n: number]: T; }` this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName); this.executingFilePath = this.getHostSpecificPath(executingFilePath || getExecutingFilePathFromLibFile()); this.currentDirectory = this.getHostSpecificPath(currentDirectory); - this.reloadFS(fileOrFolderorSymLinkList); - const tscWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") as Tsc_WatchFile; - switch (tscWatchFile) { - case Tsc_WatchFile.DynamicPolling: - this.customWatchFile = createDynamicPriorityPollingWatchFile(this); - break; - case Tsc_WatchFile.SingleFileWatcherPerName: - this.customWatchFile = createSingleFileWatcherPerName( + this.runWithFallbackPolling = !!runWithFallbackPolling; + const tscWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE"); + const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY"); + const { watchFile, watchDirectory } = createSystemWatchFunctions({ + // We dont have polling watch file + // it is essentially fsWatch but lets get that separate from fsWatch and + // into watchedFiles for easier testing + pollingWatchFile: tscWatchFile === Tsc_WatchFile.SingleFileWatcherPerName ? + createSingleFileWatcherPerName( this.watchFileWorker.bind(this), this.useCaseSensitiveFileNames - ); - break; - case undefined: - break; - default: - Debug.assertNever(tscWatchFile); - } - - const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY") as Tsc_WatchDirectory; - if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { - const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchFile(directory, () => cb(directory), PollingInterval.Medium); - this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ - useCaseSensitiveFileNames: this.useCaseSensitiveFileNames, - directoryExists: path => this.directoryExists(path), - getAccessibleSortedChildDirectories: path => this.getDirectories(path), - watchDirectory, - realpath: s => this.realpath(s) - }); - } - else if (tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory) { - const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchDirectory(directory, fileName => cb(fileName), /*recursive*/ false); - this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ - useCaseSensitiveFileNames: this.useCaseSensitiveFileNames, - directoryExists: path => this.directoryExists(path), - getAccessibleSortedChildDirectories: path => this.getDirectories(path), - watchDirectory, - realpath: s => this.realpath(s) - }); - } - else if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { - const watchFile = createDynamicPriorityPollingWatchFile(this); - const watchDirectory: HostWatchDirectory = (directory, cb) => watchFile(directory, () => cb(directory), PollingInterval.Medium); - this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ - useCaseSensitiveFileNames: this.useCaseSensitiveFileNames, - directoryExists: path => this.directoryExists(path), - getAccessibleSortedChildDirectories: path => this.getDirectories(path), - watchDirectory, - realpath: s => this.realpath(s) - }); - } + ) : + this.watchFileWorker.bind(this), + getModifiedTime: this.getModifiedTime.bind(this), + setTimeout: this.setTimeout.bind(this), + clearTimeout: this.clearTimeout.bind(this), + fsWatch: this.fsWatch.bind(this), + fileExists: this.fileExists.bind(this), + useCaseSensitiveFileNames: this.useCaseSensitiveFileNames, + fsSupportsRecursiveFsWatch: tscWatchDirectory ? false : !runWithoutRecursiveWatches, + directoryExists: this.directoryExists.bind(this), + getAccessibleSortedChildDirectories: path => this.getDirectories(path), + realpath: this.realpath.bind(this), + tscWatchFile, + tscWatchDirectory + }); + this.watchFile = watchFile; + this.watchDirectory = watchDirectory; + this.reloadFS(fileOrFolderorSymLinkList); } getNewLine() { @@ -473,6 +501,7 @@ interface Array { length: number; [n: number]: T; }` else { // Folder update: Nothing to do. currentEntry.modifiedTime = this.now(); + this.invokeFsWatches(currentEntry.fullPath, "change"); } } } @@ -510,10 +539,13 @@ interface Array { length: number; [n: number]: T; }` currentEntry.modifiedTime = this.now(); this.fs.get(getDirectoryPath(currentEntry.path))!.modifiedTime = this.now(); if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) { - this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath); + const directoryFullPath = getDirectoryPath(currentEntry.fullPath); + this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, /*useFileNameInCallback*/ true); + this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.fullPath); + this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.fullPath); } else { - this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed); + this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed); } } } @@ -564,7 +596,7 @@ interface Array { length: number; [n: number]: T; }` private renameFolderEntries(oldFolder: FsFolder, newFolder: FsFolder) { for (const entry of oldFolder.entries) { this.fs.delete(entry.path); - this.invokeFileWatcher(entry.fullPath, FileWatcherEventKind.Deleted); + this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Deleted); entry.fullPath = combinePaths(newFolder.fullPath, getBaseFileName(entry.fullPath)); entry.path = this.toPath(entry.fullPath); @@ -572,7 +604,7 @@ interface Array { length: number; [n: number]: T; }` newFolder.entries.push(entry); } this.fs.set(entry.path, entry); - this.invokeFileWatcher(entry.fullPath, FileWatcherEventKind.Created); + this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Created); if (isFsFolder(entry)) { this.renameFolderEntries(entry, entry); } @@ -631,12 +663,8 @@ interface Array { length: number; [n: number]: T; }` if (ignoreWatch) { return; } - this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); - if (isFsFolder(fileOrDirectory)) { - this.invokeDirectoryWatcher(fileOrDirectory.fullPath, fileOrDirectory.fullPath); - this.invokeWatchedDirectoriesRecursiveCallback(fileOrDirectory.fullPath, fileOrDirectory.fullPath); - } - this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath); + this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created); + this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed); } private removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRemovableLeafFolder: (folder: FsFolder) => boolean, isRenaming = false) { @@ -649,23 +677,15 @@ interface Array { length: number; [n: number]: T; }` } this.fs.delete(fileOrDirectory.path); - this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); if (isFsFolder(fileOrDirectory)) { Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming); - // Invoke directory and recursive directory watcher for the folder - // Here we arent invoking recursive directory watchers for the base folders - // since that is something we would want to do for both file as well as folder we are deleting - this.invokeWatchedDirectoriesCallback(fileOrDirectory.fullPath, ""); - this.invokeWatchedDirectoriesRecursiveCallback(fileOrDirectory.fullPath, ""); } - - if (basePath !== fileOrDirectory.path) { - if (baseFolder.entries.length === 0 && isRemovableLeafFolder(baseFolder)) { - this.removeFileOrFolder(baseFolder, isRemovableLeafFolder); - } - else { - this.invokeRecursiveDirectoryWatcher(baseFolder.fullPath, fileOrDirectory.fullPath); - } + this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); + this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed); + if (basePath !== fileOrDirectory.path && + baseFolder.entries.length === 0 && + isRemovableLeafFolder(baseFolder)) { + this.removeFileOrFolder(baseFolder, isRemovableLeafFolder); } } @@ -694,48 +714,72 @@ interface Array { length: number; [n: number]: T; }` this.removeFileOrFolder(currentEntry, returnFalse); } - // For overriding the methods - invokeWatchedDirectoriesCallback(folderFullPath: string, relativePath: string) { - invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath)); + private watchFileWorker(fileName: string, cb: FileWatcherCallback, pollingInterval: PollingInterval) { + return createWatcher( + this.watchedFiles, + this.toFullPath(fileName), + { fileName, cb, pollingInterval } + ); + } + + private fsWatch( + fileOrDirectory: string, + _entryKind: FileSystemEntryKind, + cb: FsWatchCallback, + recursive: boolean, + fallbackPollingInterval: PollingInterval, + fallbackOptions: WatchOptions | undefined): FileWatcher { + return this.runWithFallbackPolling ? + this.watchFile( + fileOrDirectory, + createFileWatcherCallback(cb), + fallbackPollingInterval, + fallbackOptions + ) : + createWatcher( + recursive ? this.fsWatchesRecursive : this.fsWatches, + this.toFullPath(fileOrDirectory), + { cb, fallbackPollingInterval, fallbackOptions } + ); + } + + invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, useFileNameInCallback?: boolean) { + invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(fileFullPath)), ({ cb, fileName }) => cb(useFileNameInCallback ? fileName : fileFullPath, eventKind)); } - invokeWatchedDirectoriesRecursiveCallback(folderFullPath: string, relativePath: string) { - invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath)); + private fsWatchCallback(map: MultiMap, fullPath: string, eventName: "rename" | "change", entryFullPath?: string) { + invokeWatcherCallbacks(map.get(this.toPath(fullPath)), ({ cb }) => cb(eventName, entryFullPath ? this.getRelativePathToDirectory(fullPath, entryFullPath) : "")); } - private invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, useFileNameInCallback?: boolean) { - invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(fileFullPath)), ({ cb, fileName }) => cb(useFileNameInCallback ? fileName : fileFullPath, eventKind)); + invokeFsWatchesCallbacks(fullPath: string, eventName: "rename" | "change", entryFullPath?: string) { + this.fsWatchCallback(this.fsWatches, fullPath, eventName, entryFullPath); + } + + invokeFsWatchesRecursiveCallbacks(fullPath: string, eventName: "rename" | "change", entryFullPath?: string) { + this.fsWatchCallback(this.fsWatchesRecursive, fullPath, eventName, entryFullPath); } private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) { return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); } - /** - * This will call the directory watcher for the folderFullPath and recursive directory watchers for this and base folders - */ - private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { - const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName); - // Folder is changed when the directory watcher is invoked - this.invokeFileWatcher(folderFullPath, FileWatcherEventKind.Changed, /*useFileNameInCallback*/ true); - this.invokeWatchedDirectoriesCallback(folderFullPath, relativePath); - this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); + private invokeRecursiveFsWatches(fullPath: string, eventName: "rename" | "change", entryFullPath?: string) { + this.invokeFsWatchesRecursiveCallbacks(fullPath, eventName, entryFullPath); + const basePath = getDirectoryPath(fullPath); + if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { + this.invokeRecursiveFsWatches(basePath, eventName, entryFullPath || fullPath); + } } - private directoryCallback({ cb, directoryName }: TestDirectoryWatcher, relativePath: string) { - cb(combinePaths(directoryName, relativePath)); + private invokeFsWatches(fullPath: string, eventName: "rename" | "change") { + this.invokeFsWatchesCallbacks(fullPath, eventName); + this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, fullPath); + this.invokeRecursiveFsWatches(fullPath, eventName); } - /** - * This will call the recursive directory watcher for this directory as well as all the base directories - */ - private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) { - const relativePath = this.getRelativePathToDirectory(fullPath, fileName); - this.invokeWatchedDirectoriesRecursiveCallback(fullPath, relativePath); - const basePath = getDirectoryPath(fullPath); - if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { - this.invokeRecursiveDirectoryWatcher(basePath, fileName); - } + private invokeFileAndFsWatches(fileOrFolderFullPath: string, eventKind: FileWatcherEventKind) { + this.invokeFileWatcher(fileOrFolderFullPath, eventKind); + this.invokeFsWatches(fileOrFolderFullPath, eventKind === FileWatcherEventKind.Changed ? "change" : "rename"); } private toFsEntry(path: string): FSEntryBase { @@ -874,22 +918,6 @@ interface Array { length: number; [n: number]: T; }` }, path => this.realpath(path)); } - watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher { - if (recursive && this.customRecursiveWatchDirectory) { - return this.customRecursiveWatchDirectory(directoryName, cb, /*recursive*/ true); - } - const path = this.toFullPath(directoryName); - const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; - const callback: TestDirectoryWatcher = { - cb, - directoryName - }; - map.add(path, callback); - return { - close: () => map.remove(path, callback) - }; - } - createHash(s: string): string { return Harness.mockHash(s); } @@ -898,21 +926,6 @@ interface Array { length: number; [n: number]: T; }` return sys.createSHA256Hash!(s); } - watchFile(fileName: string, cb: FileWatcherCallback, pollingInterval: number) { - if (this.customWatchFile) { - return this.customWatchFile(fileName, cb, pollingInterval); - } - - return this.watchFileWorker(fileName, cb); - } - - private watchFileWorker(fileName: string, cb: FileWatcherCallback) { - const path = this.toFullPath(fileName); - const callback: TestFileWatcher = { fileName, cb }; - this.watchedFiles.add(path, callback); - return { close: () => this.watchedFiles.remove(path, callback) }; - } - // TOOD: record and invoke callbacks to simulate timer events setTimeout(callback: TimeOutCallback, _time: number, ...args: any[]) { return this.timeoutCallbacks.register(callback, args); diff --git a/src/jsTyping/types.ts b/src/jsTyping/types.ts index 793e03b854313..6da6200c2c0dc 100644 --- a/src/jsTyping/types.ts +++ b/src/jsTyping/types.ts @@ -14,6 +14,7 @@ declare namespace ts.server { readonly fileNames: string[]; readonly projectRootPath: Path; readonly compilerOptions: CompilerOptions; + readonly watchOptions?: WatchOptions; readonly typeAcquisition: TypeAcquisition; readonly unresolvedImports: SortedReadonlyArray; readonly cachePath?: string; @@ -81,8 +82,8 @@ declare namespace ts.server { useCaseSensitiveFileNames: boolean; writeFile(path: string, content: string): void; createDirectory(path: string): void; - watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; - watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; + watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: CompilerOptions): FileWatcher; + watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: CompilerOptions): FileWatcher; } export interface SetTypings extends ProjectResponse { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d8f6f13e6d0ae..81d54197be2aa 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -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 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(); + private watchOptionsForInferredProjects: WatchOptions | undefined; + private watchOptionsForInferredProjectsPerProjectRoot = createMap(); /** * 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(project: ExternalProject | ConfiguredProject, files: T[], propertyReader: FilePropertyReader) { @@ -1979,8 +2003,9 @@ namespace ts.server { project.markAsDirty(); } - private updateRootAndOptionsOfNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean | undefined) { + private updateRootAndOptionsOfNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, 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; } diff --git a/src/server/project.ts b/src/server/project.ts index 1abad8d09b202..0013fe0da0b19 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -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 | 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) { + pluginConfigOverrides?: Map, + 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); diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 1d3f12fe7490b..00fce8b14c054 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -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; } /** diff --git a/src/server/types.ts b/src/server/types.ts index 222dd0fdf2e46..671e854440df3 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -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; diff --git a/src/server/utilitiesPublic.ts b/src/server/utilitiesPublic.ts index 522266a223257..676435dbc916b 100644 --- a/src/server/utilitiesPublic.ts +++ b/src/server/utilitiesPublic.ts @@ -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(): SortedArray { return [] as any as SortedArray; // TODO: GH#19873 } -} \ No newline at end of file +} diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 7c75fc10a989d..72e9f9aa3f39f 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -83,6 +83,7 @@ "unittests/config/projectReferences.ts", "unittests/config/showConfig.ts", "unittests/config/tsconfigParsing.ts", + "unittests/config/tsconfigParsingWatchOptions.ts", "unittests/evaluation/asyncArrow.ts", "unittests/evaluation/asyncGenerator.ts", "unittests/evaluation/awaiter.ts", diff --git a/src/testRunner/unittests/config/commandLineParsing.ts b/src/testRunner/unittests/config/commandLineParsing.ts index 6ad786fbfc4fd..84a601f0c5436 100644 --- a/src/testRunner/unittests/config/commandLineParsing.ts +++ b/src/testRunner/unittests/config/commandLineParsing.ts @@ -6,6 +6,7 @@ namespace ts { const parsedCompilerOptions = JSON.stringify(parsed.options); const expectedCompilerOptions = JSON.stringify(expectedParsedCommandLine.options); assert.equal(parsedCompilerOptions, expectedCompilerOptions); + assert.deepEqual(parsed.watchOptions, expectedParsedCommandLine.watchOptions); const parsedErrors = parsed.errors; const expectedErrors = expectedParsedCommandLine.errors; @@ -45,7 +46,7 @@ namespace ts { assertParseResult(["--declarations", "--allowTS"], { errors: [ { - messageText:"Unknown compiler option '--declarations'. Did you mean 'declaration'?", + messageText: "Unknown compiler option '--declarations'. Did you mean 'declaration'?", category: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, code: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.code, file: undefined, @@ -412,6 +413,75 @@ namespace ts { options: { tsBuildInfoFile: "build.tsbuildinfo" } }); }); + + describe("Watch options", () => { + it("parse --watchFile", () => { + assertParseResult(["--watchFile", "UseFsEvents", "0.ts"], + { + errors: [], + fileNames: ["0.ts"], + options: {}, + watchOptions: { watchFile: WatchFileKind.UseFsEvents } + }); + }); + + it("parse --watchDirectory", () => { + assertParseResult(["--watchDirectory", "FixedPollingInterval", "0.ts"], + { + errors: [], + fileNames: ["0.ts"], + options: {}, + watchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } + }); + }); + + it("parse --fallbackPolling", () => { + assertParseResult(["--fallbackPolling", "PriorityInterval", "0.ts"], + { + errors: [], + fileNames: ["0.ts"], + options: {}, + watchOptions: { fallbackPolling: PollingWatchKind.PriorityInterval } + }); + }); + + it("parse --synchronousWatchDirectory", () => { + assertParseResult(["--synchronousWatchDirectory", "0.ts"], + { + errors: [], + fileNames: ["0.ts"], + options: {}, + watchOptions: { synchronousWatchDirectory: true } + }); + }); + + it("errors on missing argument to --fallbackPolling", () => { + assertParseResult(["0.ts", "--fallbackPolling"], + { + errors: [ + { + messageText: "Watch option 'fallbackPolling' requires a value of type string.", + category: Diagnostics.Watch_option_0_requires_a_value_of_type_1.category, + code: Diagnostics.Watch_option_0_requires_a_value_of_type_1.code, + file: undefined, + start: undefined, + length: undefined + }, + { + messageText: "Argument for '--fallbackPolling' option must be: 'fixedinterval', 'priorityinterval', 'dynamicpriority'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined + } + ], + fileNames: ["0.ts"], + options: {}, + watchOptions: { fallbackPolling: undefined } + }); + }); + }); }); describe("unittests:: config:: commandLineParsing:: parseBuildOptions", () => { @@ -420,6 +490,7 @@ namespace ts { const parsedBuildOptions = JSON.stringify(parsed.buildOptions); const expectedBuildOptions = JSON.stringify(expectedParsedBuildCommand.buildOptions); assert.equal(parsedBuildOptions, expectedBuildOptions); + assert.deepEqual(parsed.watchOptions, expectedParsedBuildCommand.watchOptions); const parsedErrors = parsed.errors; const expectedErrors = expectedParsedBuildCommand.errors; @@ -442,7 +513,8 @@ namespace ts { { errors: [], projects: ["."], - buildOptions: {} + buildOptions: {}, + watchOptions: undefined }); }); @@ -452,7 +524,8 @@ namespace ts { { errors: [], projects: ["tests"], - buildOptions: { verbose: true, force: true } + buildOptions: { verbose: true, force: true }, + watchOptions: undefined }); }); @@ -469,7 +542,8 @@ namespace ts { length: undefined, }], projects: ["."], - buildOptions: { verbose: true } + buildOptions: { verbose: true }, + watchOptions: undefined }); }); @@ -478,7 +552,7 @@ namespace ts { assertParseResult(["--listFilesOnly"], { errors: [{ - messageText:"Unknown build option '--listFilesOnly'.", + messageText: "Unknown build option '--listFilesOnly'.", category: Diagnostics.Unknown_build_option_0.category, code: Diagnostics.Unknown_build_option_0.code, file: undefined, @@ -486,7 +560,8 @@ namespace ts { length: undefined, }], projects: ["."], - buildOptions: {} + buildOptions: {}, + watchOptions: undefined, }); }); @@ -496,7 +571,8 @@ namespace ts { { errors: [], projects: ["src", "tests"], - buildOptions: { force: true, verbose: true } + buildOptions: { force: true, verbose: true }, + watchOptions: undefined, }); }); @@ -506,7 +582,8 @@ namespace ts { { errors: [], projects: ["src", "tests"], - buildOptions: { force: true, verbose: true } + buildOptions: { force: true, verbose: true }, + watchOptions: undefined, }); }); @@ -516,7 +593,8 @@ namespace ts { { errors: [], projects: ["src", "tests"], - buildOptions: { force: true, verbose: true } + buildOptions: { force: true, verbose: true }, + watchOptions: undefined, }); }); @@ -526,7 +604,8 @@ namespace ts { { errors: [], projects: ["tests"], - buildOptions: { incremental: true } + buildOptions: { incremental: true }, + watchOptions: undefined, }); }); @@ -536,7 +615,8 @@ namespace ts { { errors: [], projects: ["src"], - buildOptions: { locale: "en-us" } + buildOptions: { locale: "en-us" }, + watchOptions: undefined, }); }); @@ -553,7 +633,8 @@ namespace ts { length: undefined }], projects: ["build.tsbuildinfo", "tests"], - buildOptions: { } + buildOptions: {}, + watchOptions: undefined, }); }); @@ -572,7 +653,8 @@ namespace ts { length: undefined, }], projects: ["."], - buildOptions: { [flag1]: true, [flag2]: true } + buildOptions: { [flag1]: true, [flag2]: true }, + watchOptions: undefined, }); }); } @@ -582,7 +664,74 @@ namespace ts { verifyInvalidCombination("clean", "watch"); verifyInvalidCombination("watch", "dry"); }); - }); + describe("Watch options", () => { + it("parse --watchFile", () => { + assertParseResult(["--watchFile", "UseFsEvents", "--verbose"], + { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { watchFile: WatchFileKind.UseFsEvents } + }); + }); + + it("parse --watchDirectory", () => { + assertParseResult(["--watchDirectory", "FixedPollingInterval", "--verbose"], + { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } + }); + }); + + it("parse --fallbackPolling", () => { + assertParseResult(["--fallbackPolling", "PriorityInterval", "--verbose"], + { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { fallbackPolling: PollingWatchKind.PriorityInterval } + }); + }); + + it("parse --synchronousWatchDirectory", () => { + assertParseResult(["--synchronousWatchDirectory", "--verbose"], + { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { synchronousWatchDirectory: true } + }); + }); + it("errors on missing argument", () => { + assertParseResult(["--verbose", "--fallbackPolling"], + { + errors: [ + { + messageText: "Watch option 'fallbackPolling' requires a value of type string.", + category: Diagnostics.Watch_option_0_requires_a_value_of_type_1.category, + code: Diagnostics.Watch_option_0_requires_a_value_of_type_1.code, + file: undefined, + start: undefined, + length: undefined + }, + { + messageText: "Argument for '--fallbackPolling' option must be: 'fixedinterval', 'priorityinterval', 'dynamicpriority'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined + } + ], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { fallbackPolling: undefined } + }); + }); + }); + }); } diff --git a/src/testRunner/unittests/config/showConfig.ts b/src/testRunner/unittests/config/showConfig.ts index ca2e76e25a66c..d017e64e333ae 100644 --- a/src/testRunner/unittests/config/showConfig.ts +++ b/src/testRunner/unittests/config/showConfig.ts @@ -102,16 +102,33 @@ namespace ts { ] }); + showTSConfigCorrectly("Show TSConfig with watch options", ["-p", "tsconfig.json"], { + watchOptions: { + watchFile: "DynamicPriorityPolling" + }, + include: [ + "./src/**/*" + ] + }); + // Bulk validation of all option declarations for (const option of optionDeclarations) { - if (option.name === "project") continue; - let configObject: object | undefined; + baselineOption(option, /*isCompilerOptions*/ true); + } + + for (const option of optionsForWatch) { + baselineOption(option, /*isCompilerOptions*/ false); + } + + function baselineOption(option: CommandLineOption, isCompilerOptions: boolean) { + if (option.name === "project") return; let args: string[]; + let optionValue: object | undefined; switch (option.type) { case "boolean": { if (option.isTSConfigOnly) { args = ["-p", "tsconfig.json"]; - configObject = { compilerOptions: { [option.name]: true } }; + optionValue = { [option.name]: true }; } else { args = [`--${option.name}`]; @@ -121,7 +138,7 @@ namespace ts { case "list": { if (option.isTSConfigOnly) { args = ["-p", "tsconfig.json"]; - configObject = { compilerOptions: { [option.name]: [] } }; + optionValue = { [option.name]: [] }; } else { args = [`--${option.name}`]; @@ -131,7 +148,7 @@ namespace ts { case "string": { if (option.isTSConfigOnly) { args = ["-p", "tsconfig.json"]; - configObject = { compilerOptions: { [option.name]: "someString" } }; + optionValue = { [option.name]: "someString" }; } else { args = [`--${option.name}`, "someString"]; @@ -141,7 +158,7 @@ namespace ts { case "number": { if (option.isTSConfigOnly) { args = ["-p", "tsconfig.json"]; - configObject = { compilerOptions: { [option.name]: 0 } }; + optionValue = { [option.name]: 0 }; } else { args = [`--${option.name}`, "0"]; @@ -150,7 +167,7 @@ namespace ts { } case "object": { args = ["-p", "tsconfig.json"]; - configObject = { compilerOptions: { [option.name]: {} } }; + optionValue = { [option.name]: {} }; break; } default: { @@ -159,7 +176,7 @@ namespace ts { const val = iterResult.value; if (option.isTSConfigOnly) { args = ["-p", "tsconfig.json"]; - configObject = { compilerOptions: { [option.name]: val } }; + optionValue = { [option.name]: val }; } else { args = [`--${option.name}`, val]; @@ -167,6 +184,9 @@ namespace ts { break; } } + + const configObject = optionValue && + (isCompilerOptions ? { compilerOptions: optionValue } : { watchOptions: optionValue }); showTSConfigCorrectly(`Shows tsconfig for single option/${option.name}`, args, configObject); } }); diff --git a/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts b/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts new file mode 100644 index 0000000000000..83e224e492527 --- /dev/null +++ b/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts @@ -0,0 +1,178 @@ +namespace ts { + describe("unittests:: config:: tsconfigParsingWatchOptions:: parseConfigFileTextToJson", () => { + function createParseConfigHost(additionalFiles?: vfs.FileSet) { + return new fakes.ParseConfigHost( + new vfs.FileSystem( + /*ignoreCase*/ false, + { + cwd: "/", + files: { "/": {}, "/a.ts": "", ...additionalFiles } + } + ) + ); + } + function getParsedCommandJson(json: object, additionalFiles?: vfs.FileSet, existingWatchOptions?: WatchOptions) { + return parseJsonConfigFileContent( + json, + createParseConfigHost(additionalFiles), + "/", + /*existingOptions*/ undefined, + "tsconfig.json", + /*resolutionStack*/ undefined, + /*extraFileExtensions*/ undefined, + /*extendedConfigCache*/ undefined, + existingWatchOptions, + ); + } + + function getParsedCommandJsonNode(json: object, additionalFiles?: vfs.FileSet, existingWatchOptions?: WatchOptions) { + const parsed = parseJsonText("tsconfig.json", JSON.stringify(json)); + return parseJsonSourceFileConfigFileContent( + parsed, + createParseConfigHost(additionalFiles), + "/", + /*existingOptions*/ undefined, + "tsconfig.json", + /*resolutionStack*/ undefined, + /*extraFileExtensions*/ undefined, + /*extendedConfigCache*/ undefined, + existingWatchOptions, + ); + } + + interface VerifyWatchOptions { + json: object; + expectedOptions: WatchOptions | undefined; + additionalFiles?: vfs.FileSet; + existingWatchOptions?: WatchOptions | undefined; + } + + function verifyWatchOptions(scenario: () => VerifyWatchOptions[]) { + it("with json api", () => { + for (const { json, expectedOptions, additionalFiles, existingWatchOptions } of scenario()) { + const parsed = getParsedCommandJson(json, additionalFiles, existingWatchOptions); + assert.deepEqual(parsed.watchOptions, expectedOptions); + } + }); + + it("with json source file api", () => { + for (const { json, expectedOptions, additionalFiles, existingWatchOptions } of scenario()) { + const parsed = getParsedCommandJsonNode(json, additionalFiles, existingWatchOptions); + assert.deepEqual(parsed.watchOptions, expectedOptions); + } + }); + } + + describe("no watchOptions specified option", () => { + verifyWatchOptions(() => [{ + json: {}, + expectedOptions: undefined + }]); + }); + + describe("empty watchOptions specified option", () => { + verifyWatchOptions(() => [{ + json: { watchOptions: {} }, + expectedOptions: undefined + }]); + }); + + describe("extending config file", () => { + describe("when extending config file without watchOptions", () => { + verifyWatchOptions(() => [ + { + json: { + extends: "./base.json", + watchOptions: { watchFile: "UseFsEvents" } + }, + expectedOptions: { watchFile: WatchFileKind.UseFsEvents }, + additionalFiles: { "/base.json": "{}" } + }, + { + json: { extends: "./base.json", }, + expectedOptions: undefined, + additionalFiles: { "/base.json": "{}" } + } + ]); + }); + + describe("when extending config file with watchOptions", () => { + verifyWatchOptions(() => [ + { + json: { + extends: "./base.json", + watchOptions: { + watchFile: "UseFsEvents", + } + }, + expectedOptions: { + watchFile: WatchFileKind.UseFsEvents, + watchDirectory: WatchDirectoryKind.FixedPollingInterval + }, + additionalFiles: { + "/base.json": JSON.stringify({ + watchOptions: { + watchFile: "UseFsEventsOnParentDirectory", + watchDirectory: "FixedPollingInterval" + } + }) + } + }, + { + json: { + extends: "./base.json", + }, + expectedOptions: { + watchFile: WatchFileKind.UseFsEventsOnParentDirectory, + watchDirectory: WatchDirectoryKind.FixedPollingInterval + }, + additionalFiles: { + "/base.json": JSON.stringify({ + watchOptions: { + watchFile: "UseFsEventsOnParentDirectory", + watchDirectory: "FixedPollingInterval" + } + }) + } + } + ]); + }); + }); + + describe("different options", () => { + verifyWatchOptions(() => [ + { + json: { watchOptions: { watchFile: "UseFsEvents" } }, + expectedOptions: { watchFile: WatchFileKind.UseFsEvents } + }, + { + json: { watchOptions: { watchDirectory: "UseFsEvents" } }, + expectedOptions: { watchDirectory: WatchDirectoryKind.UseFsEvents } + }, + { + json: { watchOptions: { fallbackPolling: "DynamicPriority" } }, + expectedOptions: { fallbackPolling: PollingWatchKind.DynamicPriority } + }, + { + json: { watchOptions: { synchronousWatchDirectory: true } }, + expectedOptions: { synchronousWatchDirectory: true } + } + ]); + }); + + describe("watch options extending passed in watch options", () => { + verifyWatchOptions(() => [ + { + json: { watchOptions: { watchFile: "UseFsEvents" } }, + expectedOptions: { watchFile: WatchFileKind.UseFsEvents, watchDirectory: WatchDirectoryKind.FixedPollingInterval }, + existingWatchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } + }, + { + json: {}, + expectedOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval }, + existingWatchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } + }, + ]); + }); + }); +} diff --git a/src/testRunner/unittests/reuseProgramStructure.ts b/src/testRunner/unittests/reuseProgramStructure.ts index b9c1dc1a37a42..0f468d76b0129 100644 --- a/src/testRunner/unittests/reuseProgramStructure.ts +++ b/src/testRunner/unittests/reuseProgramStructure.ts @@ -928,13 +928,13 @@ namespace ts { } function verifyProgramWithoutConfigFile(system: System, rootFiles: string[], options: CompilerOptions) { - const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, system)).getCurrentProgram().getProgram(); + const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, /*watchOptions*/ undefined, system)).getCurrentProgram().getProgram(); verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options)); } function verifyProgramWithConfigFile(system: System, configFileName: string) { - const program = createWatchProgram(createWatchCompilerHostOfConfigFile(configFileName, {}, system)).getCurrentProgram().getProgram(); - const { fileNames, options } = parseConfigFileWithSystem(configFileName, {}, system, notImplemented)!; // TODO: GH#18217 + const program = createWatchProgram(createWatchCompilerHostOfConfigFile(configFileName, {}, /*watchOptionsToExtend*/ undefined, system)).getCurrentProgram().getProgram(); + const { fileNames, options } = parseConfigFileWithSystem(configFileName, {}, /*watchOptionsToExtend*/ undefined, system, notImplemented)!; // TODO: GH#18217 verifyProgramIsUptoDate(program, fileNames, options); } diff --git a/src/testRunner/unittests/tscWatch/consoleClearing.ts b/src/testRunner/unittests/tscWatch/consoleClearing.ts index d0669a2327d1b..cebfd5b7bc3de 100644 --- a/src/testRunner/unittests/tscWatch/consoleClearing.ts +++ b/src/testRunner/unittests/tscWatch/consoleClearing.ts @@ -2,8 +2,8 @@ namespace ts.tscWatch { describe("unittests:: tsc-watch:: console clearing", () => { const currentDirectoryLog = "Current directory: / CaseSensitiveFileNames: false\n"; const fileWatcherAddedLog = [ - "FileWatcher:: Added:: WatchInfo: /f.ts 250 Source file\n", - "FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 Source file\n" + "FileWatcher:: Added:: WatchInfo: /f.ts 250 undefined Source file\n", + "FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 undefined Source file\n" ]; const file: File = { @@ -35,9 +35,9 @@ namespace ts.tscWatch { host.modifyFile(file.path, "//"); host.runQueuedTimeoutCallbacks(); checkOutputErrorsIncremental(host, emptyArray, disableConsoleClear, hasLog ? [ - "FileWatcher:: Triggered with /f.ts 1:: WatchInfo: /f.ts 250 Source file\n", + "FileWatcher:: Triggered with /f.ts 1:: WatchInfo: /f.ts 250 undefined Source file\n", "Scheduling update\n", - "Elapsed:: 0ms FileWatcher:: Triggered with /f.ts 1:: WatchInfo: /f.ts 250 Source file\n" + "Elapsed:: 0ms FileWatcher:: Triggered with /f.ts 1:: WatchInfo: /f.ts 250 undefined Source file\n" ] : undefined, hasLog ? getProgramSynchronizingLog(options) : undefined); } @@ -86,8 +86,8 @@ namespace ts.tscWatch { const host = createWatchedSystem(files); const reportDiagnostic = createDiagnosticReporter(host); const optionsToExtend: CompilerOptions = {}; - const configParseResult = parseConfigFileWithSystem(configFile.path, optionsToExtend, host, reportDiagnostic)!; - const watchCompilerHost = createWatchCompilerHostOfConfigFile(configParseResult.options.configFilePath!, optionsToExtend, host, /*createProgram*/ undefined, reportDiagnostic, createWatchStatusReporter(host)); + const configParseResult = parseConfigFileWithSystem(configFile.path, optionsToExtend, /*watchOptionsToExtend*/ undefined, host, reportDiagnostic)!; + const watchCompilerHost = createWatchCompilerHostOfConfigFile(configParseResult.options.configFilePath!, optionsToExtend, /*watchOptionsToExtend*/ undefined, host, /*createProgram*/ undefined, reportDiagnostic, createWatchStatusReporter(host)); watchCompilerHost.configFileParsingResult = configParseResult; createWatchProgram(watchCompilerHost); verifyCompilation(host, compilerOptions); diff --git a/src/testRunner/unittests/tscWatch/helpers.ts b/src/testRunner/unittests/tscWatch/helpers.ts index d14c531f0d372..94df77891aebc 100644 --- a/src/testRunner/unittests/tscWatch/helpers.ts +++ b/src/testRunner/unittests/tscWatch/helpers.ts @@ -37,8 +37,8 @@ namespace ts.tscWatch { close(): void; } - export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, optionsToExtend?: CompilerOptions, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, optionsToExtend || {}, host); + export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, optionsToExtend?: CompilerOptions, watchOptionsToExtend?: WatchOptions, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, optionsToExtend || {}, watchOptionsToExtend, host); compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; const watch = createWatchProgram(compilerHost); const result = (() => watch.getCurrentProgram().getProgram()) as Watch; @@ -47,8 +47,8 @@ namespace ts.tscWatch { return result; } - export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host); + export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, watchOptions?: WatchOptions, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, watchOptions, host); compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; const watch = createWatchProgram(compilerHost); return () => watch.getCurrentProgram().getProgram(); diff --git a/src/testRunner/unittests/tscWatch/incremental.ts b/src/testRunner/unittests/tscWatch/incremental.ts index d966b77005a4f..b6e32dabe9503 100644 --- a/src/testRunner/unittests/tscWatch/incremental.ts +++ b/src/testRunner/unittests/tscWatch/incremental.ts @@ -35,7 +35,7 @@ namespace ts.tscWatch { function incrementalBuild(configFile: string, host: WatchedSystem, optionsToExtend?: CompilerOptions) { const reportDiagnostic = createDiagnosticReporter(host); - const config = parseConfigFileWithSystem(configFile, optionsToExtend || {}, host, reportDiagnostic); + const config = parseConfigFileWithSystem(configFile, optionsToExtend || {}, /*watchOptionsToExtend*/ undefined, host, reportDiagnostic); if (config) { performIncrementalCompilation({ rootNames: config.fileNames, @@ -547,7 +547,7 @@ namespace ts.tscWatch { const system = createWatchedSystem([libFile, file1, fileModified, config], { currentDirectory: project }); incrementalBuild("tsconfig.json", system); - const command = parseConfigFileWithSystem("tsconfig.json", {}, system, noop)!; + const command = parseConfigFileWithSystem("tsconfig.json", {}, /*watchOptionsToExtend*/ undefined, system, noop)!; const builderProgram = createIncrementalProgram({ rootNames: command.fileNames, options: command.options, diff --git a/src/testRunner/unittests/tscWatch/programUpdates.ts b/src/testRunner/unittests/tscWatch/programUpdates.ts index a2c008bfd900b..ee7b4019cf224 100644 --- a/src/testRunner/unittests/tscWatch/programUpdates.ts +++ b/src/testRunner/unittests/tscWatch/programUpdates.ts @@ -68,7 +68,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([configFile, libFile, file1, file2, file3]); - const watch = createWatchProgram(createWatchCompilerHostOfConfigFile(configFile.path, {}, host, /*createProgram*/ undefined, notImplemented)); + const watch = createWatchProgram(createWatchCompilerHostOfConfigFile(configFile.path, {}, /*watchOptionsToExtend*/ undefined, host, /*createProgram*/ undefined, notImplemented)); checkProgramActualFiles(watch.getCurrentProgram().getProgram(), [file1.path, libFile.path, file2.path]); checkProgramRootFiles(watch.getCurrentProgram().getProgram(), [file1.path, file2.path]); @@ -931,7 +931,7 @@ namespace ts.tscWatch { content: generateTSConfig(options, emptyArray, "\n") }; const host = createWatchedSystem([file1, file2, libFile, tsconfig], { currentDirectory: projectRoot }); - const watch = createWatchOfConfigFile(tsconfig.path, host, /*optionsToExtend*/ undefined, /*maxNumberOfFilesToIterateForInvalidation*/1); + const watch = createWatchOfConfigFile(tsconfig.path, host, /*optionsToExtend*/ undefined, /*watchOptionsToExtend*/ undefined, /*maxNumberOfFilesToIterateForInvalidation*/1); checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); outputFiles.forEach(f => host.fileExists(f)); diff --git a/src/testRunner/unittests/tscWatch/resolutionCache.ts b/src/testRunner/unittests/tscWatch/resolutionCache.ts index 1c78c412a7179..c87a5c618b38f 100644 --- a/src/testRunner/unittests/tscWatch/resolutionCache.ts +++ b/src/testRunner/unittests/tscWatch/resolutionCache.ts @@ -379,7 +379,7 @@ declare module "fs" { const expectedFiles = files.map(f => f.path); it("when watching node_modules in inferred project for failed lookup", () => { const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*maxNumberOfFilesToIterateForInvalidation*/ 1); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*watchOptions*/ undefined, /*maxNumberOfFilesToIterateForInvalidation*/ 1); checkProgramActualFiles(watch(), expectedFiles); host.checkTimeoutQueueLength(0); diff --git a/src/testRunner/unittests/tscWatch/watchApi.ts b/src/testRunner/unittests/tscWatch/watchApi.ts index 8f94ff005ecd5..716fc162261f0 100644 --- a/src/testRunner/unittests/tscWatch/watchApi.ts +++ b/src/testRunner/unittests/tscWatch/watchApi.ts @@ -20,7 +20,7 @@ namespace ts.tscWatch { it("verify that module resolution with json extension works when returned without extension", () => { const files = [libFile, mainFile, config, settingsJson]; const host = createWatchedSystem(files, { currentDirectory: projectRoot }); - const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, host); + const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, /*watchOptionsToExtend*/ undefined, host); const parsedCommandResult = parseJsonConfigFileContent(configFileJson, host, config.path); compilerHost.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { const result = resolveModuleName(m, containingFile, parsedCommandResult.options, compilerHost); @@ -58,7 +58,7 @@ namespace ts.tscWatch { const reportWatchStatus: WatchStatusReporter = (_, __, ___, errorCount) => { watchedErrorCount = errorCount; }; - const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, host, /*createProgram*/ undefined, /*reportDiagnostic*/ undefined, reportWatchStatus); + const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, /*watchOptionsToExtend*/ undefined, host, /*createProgram*/ undefined, /*reportDiagnostic*/ undefined, reportWatchStatus); createWatchProgram(compilerHost); assert.equal(watchedErrorCount, 2, "The error count was expected to be 2 for the file change"); }); diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index d4fee6c2c8e76..a9d5a85110764 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -68,7 +68,11 @@ namespace ts.tscWatch { const projectSrcFolder = `${projectFolder}/src`; const configFile: File = { path: `${projectFolder}/tsconfig.json`, - content: "{}" + content: JSON.stringify({ + watchOptions: { + synchronousWatchDirectory: true + } + }) }; const file: File = { path: `${projectSrcFolder}/file1.ts`, @@ -173,6 +177,277 @@ namespace ts.tscWatch { checkWatchedDirectories(host, [cwd, `${cwd}/node_modules`, `${cwd}/node_modules/@types`, `${cwd}/node_modules/reala`, `${cwd}/node_modules/realb`, `${cwd}/node_modules/reala/node_modules`, `${cwd}/node_modules/realb/node_modules`, `${cwd}/src`], /*recursive*/ false); }); + + it("with non synchronous watch directory", () => { + const configFile: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const file1: File = { + path: `${projectRoot}/src/file1.ts`, + content: `import { x } from "file2";` + }; + const file2: File = { + path: `${projectRoot}/node_modules/file2/index.d.ts`, + content: `export const x = 10;` + }; + const files = [libFile, file1, file2, configFile]; + const host = createWatchedSystem(files, { runWithoutRecursiveWatches: true }); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); + checkOutputErrorsInitial(host, emptyArray); + const watchedDirectories = [`${projectRoot}`, `${projectRoot}/src`, `${projectRoot}/node_modules`, `${projectRoot}/node_modules/file2`, `${projectRoot}/node_modules/@types`]; + checkWatchesWithFile2(); + host.checkTimeoutQueueLengthAndRun(1); // To update directory callbacks for file1.js output + host.checkTimeoutQueueLengthAndRun(1); // Update program again + host.checkTimeoutQueueLength(0); + checkOutputErrorsIncremental(host, emptyArray); + checkWatchesWithFile2(); + + // Remove directory node_modules + host.deleteFolder(`${projectRoot}/node_modules`, /*recursive*/ true); + host.checkTimeoutQueueLength(2); // 1. For updating program and 2. for updating child watches + host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 2); // Update program + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), file1, "file2") + ]); + checkWatchesWithoutFile2(); + + host.checkTimeoutQueueLengthAndRun(1); // To update directory watchers + host.checkTimeoutQueueLengthAndRun(1); // To Update program + host.checkTimeoutQueueLength(0); + checkWatchesWithoutFile2(); + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), file1, "file2") + ]); + + // npm install + host.createDirectory(`${projectRoot}/node_modules`); + host.checkTimeoutQueueLength(1); // To update folder structure + assert.deepEqual(host.getOutput(), emptyArray); + checkWatchesWithoutFile2(); + host.createDirectory(`${projectRoot}/node_modules/file2`); + host.checkTimeoutQueueLength(1); // To update folder structure + assert.deepEqual(host.getOutput(), emptyArray); + checkWatchesWithoutFile2(); + host.writeFile(file2.path, file2.content); + host.checkTimeoutQueueLength(1); // To update folder structure + assert.deepEqual(host.getOutput(), emptyArray); + checkWatchesWithoutFile2(); + + host.runQueuedTimeoutCallbacks(); + host.checkTimeoutQueueLength(1); // To Update the program + assert.deepEqual(host.getOutput(), emptyArray); + checkWatchedFiles(files.filter(f => f !== file2)); // Files like without file2 + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + checkNonRecursiveWatchedDirectories(watchedDirectories); // Directories like with file2 + + host.runQueuedTimeoutCallbacks(); + host.checkTimeoutQueueLength(0); + checkOutputErrorsIncremental(host, emptyArray); + checkWatchesWithFile2(); + + function checkWatchesWithFile2() { + checkWatchedFiles(files); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + checkNonRecursiveWatchedDirectories(watchedDirectories); + } + + function checkWatchesWithoutFile2() { + checkWatchedFiles(files.filter(f => f !== file2)); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + checkNonRecursiveWatchedDirectories(watchedDirectories.filter(f => f !== `${projectRoot}/node_modules/file2`)); + } + + function checkWatchedFiles(files: readonly File[]) { + checkWatchedFilesDetailed( + host, + files.map(f => f.path.toLowerCase()), + 1, + arrayToMap( + files, + f => f.path.toLowerCase(), + () => [PollingInterval.Low] + ) + ); + } + + function checkNonRecursiveWatchedDirectories(directories: readonly string[]) { + checkWatchedDirectoriesDetailed( + host, + directories, + 1, + /*recursive*/ false, + arrayToMap( + directories, + identity, + () => [{ + fallbackPollingInterval: PollingInterval.Medium, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + } + }); + }); + + describe("handles watch compiler options", () => { + it("with watchFile option", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + watchFile: "UseFsEvents" + } + }) + }; + const files = [libFile, commonFile1, commonFile2, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfConfigFile(configFile.path, host, { extendedDiagnostics: true }); + checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); + + // Instead of polling watch (= watchedFiles), uses fsWatch + checkWatchedFiles(host, emptyArray); + checkWatchedDirectoriesDetailed( + host, + files.map(f => f.path.toLowerCase()), + 1, + /*recursive*/ false, + arrayToMap( + files, + f => f.path.toLowerCase(), + f => [{ + fallbackPollingInterval: f === configFile ? PollingInterval.High : PollingInterval.Low, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + checkWatchedDirectoriesDetailed( + host, + ["/a/b", "/a/b/node_modules/@types"], + 1, + /*recursive*/ true, + arrayToMap( + ["/a/b", "/a/b/node_modules/@types"], + identity, + () => [{ + fallbackPollingInterval: PollingInterval.Medium, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + }); + + it("with watchDirectory option", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + watchDirectory: "UseFsEvents" + } + }) + }; + const files = [libFile, commonFile1, commonFile2, configFile]; + const host = createWatchedSystem(files, { runWithoutRecursiveWatches: true }); + const watch = createWatchOfConfigFile(configFile.path, host, { extendedDiagnostics: true }); + checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); + + checkWatchedFilesDetailed( + host, + files.map(f => f.path.toLowerCase()), + 1, + arrayToMap( + files, + f => f.path.toLowerCase(), + () => [PollingInterval.Low] + ) + ); + checkWatchedDirectoriesDetailed( + host, + ["/a/b", "/a/b/node_modules/@types"], + 1, + /*recursive*/ false, + arrayToMap( + ["/a/b", "/a/b/node_modules/@types"], + identity, + () => [{ + fallbackPollingInterval: PollingInterval.Medium, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + }); + + it("with fallbackPolling option", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + fallbackPolling: "PriorityInterval" + } + }) + }; + const files = [libFile, commonFile1, commonFile2, configFile]; + const host = createWatchedSystem(files, { runWithoutRecursiveWatches: true, runWithFallbackPolling: true }); + const watch = createWatchOfConfigFile(configFile.path, host, { extendedDiagnostics: true }); + checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); + const filePaths = files.map(f => f.path.toLowerCase()); + checkWatchedFilesDetailed( + host, + filePaths.concat(["/a/b", "/a/b/node_modules/@types"]), + 1, + arrayToMap( + filePaths.concat(["/a/b", "/a/b/node_modules/@types"]), + identity, + f => [contains(filePaths, f) ? PollingInterval.Low : PollingInterval.Medium] + ) + ); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + }); + + it("with watchFile as watch options to extend", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const files = [libFile, commonFile1, commonFile2, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfConfigFile(configFile.path, host, { extendedDiagnostics: true }, { watchFile: WatchFileKind.UseFsEvents }); + checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); + + // Instead of polling watch (= watchedFiles), uses fsWatch + checkWatchedFiles(host, emptyArray); + checkWatchedDirectoriesDetailed( + host, + files.map(f => f.path.toLowerCase()), + 1, + /*recursive*/ false, + arrayToMap( + files, + f => f.path.toLowerCase(), + f => [{ + fallbackPollingInterval: f === configFile ? PollingInterval.High : PollingInterval.Low, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + checkWatchedDirectoriesDetailed( + host, + ["/a/b", "/a/b/node_modules/@types"], + 1, + /*recursive*/ true, + arrayToMap( + ["/a/b", "/a/b/node_modules/@types"], + identity, + () => [{ + fallbackPollingInterval: PollingInterval.Medium, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + }); }); }); } diff --git a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts index 0e96e1dd82b90..5e6dc224cfcac 100644 --- a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts +++ b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts @@ -14,8 +14,9 @@ namespace ts.projectSystem { readDirectory = "readDirectory" } type CalledMaps = CalledMapsWithSingleArg | CalledMapsWithFiveArgs; + type CalledWithFiveArgs = [readonly string[], readonly string[], readonly string[], number]; function createCallsTrackingHost(host: TestServerHost) { - const calledMaps: Record> & Record> = { + const calledMaps: Record> & Record> = { fileExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.fileExists), directoryExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.directoryExists), getDirectories: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.getDirectories), @@ -65,11 +66,11 @@ namespace ts.projectSystem { } function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: Map) { - TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys); + TestFSWithWatch.checkMap(callback, calledMaps[callback], expectedKeys); } function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: readonly string[], nTimes: number) { - TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys, nTimes); + TestFSWithWatch.checkMap(callback, calledMaps[callback], expectedKeys, nTimes); } function verifyNoHostCalls() { @@ -689,10 +690,10 @@ namespace ts.projectSystem { }; files.push(debugTypesFile); // Do not invoke recursive directory watcher for anything other than node_module/@types - const invoker = host.invokeWatchedDirectoriesRecursiveCallback; - host.invokeWatchedDirectoriesRecursiveCallback = (fullPath, relativePath) => { + const invoker = host.invokeFsWatchesRecursiveCallbacks; + host.invokeFsWatchesRecursiveCallbacks = (fullPath, eventName, entryFullPath) => { if (fullPath.endsWith("@types")) { - invoker.call(host, fullPath, relativePath); + invoker.call(host, fullPath, eventName, entryFullPath); } }; host.reloadFS(files); diff --git a/src/testRunner/unittests/tsserver/watchEnvironment.ts b/src/testRunner/unittests/tsserver/watchEnvironment.ts index 63afebab2f62a..71757490147a1 100644 --- a/src/testRunner/unittests/tsserver/watchEnvironment.ts +++ b/src/testRunner/unittests/tsserver/watchEnvironment.ts @@ -6,7 +6,11 @@ namespace ts.projectSystem { const projectSrcFolder = `${projectFolder}/src`; const configFile: File = { path: `${projectFolder}/tsconfig.json`, - content: "{}" + content: JSON.stringify({ + watchOptions: { + synchronousWatchDirectory: true + } + }) }; const index: File = { path: `${projectSrcFolder}/index.ts`, @@ -246,4 +250,298 @@ namespace ts.projectSystem { verifyFilePathStyle("//vda1cs4850/c$/users/username/myprojects/project/x.js"); }); }); + + describe("unittests:: tsserver:: watchEnvironment:: handles watch compiler options", () => { + it("with watchFile option as host configuration", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const files = [libFile, commonFile2, configFile]; + const host = createServerHost(files.concat(commonFile1)); + const session = createSession(host); + session.executeCommandSeq({ + command: protocol.CommandTypes.Configure, + arguments: { + watchOptions: { + watchFile: protocol.WatchFileKind.UseFsEvents + } + } + }); + const service = session.getProjectService(); + openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); + checkProjectActualFiles( + service.configuredProjects.get(configFile.path)!, + files.map(f => f.path).concat(commonFile1.path) + ); + + // Instead of polling watch (= watchedFiles), uses fsWatch + checkWatchedFiles(host, emptyArray); + checkWatchedDirectoriesDetailed( + host, + files.map(f => f.path.toLowerCase()), + 1, + /*recursive*/ false, + arrayToMap( + files, + f => f.path.toLowerCase(), + f => [{ + fallbackPollingInterval: f === configFile ? PollingInterval.High : PollingInterval.Medium, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + checkWatchedDirectoriesDetailed( + host, + ["/a/b", "/a/b/node_modules/@types"], + 1, + /*recursive*/ true, + arrayToMap( + ["/a/b", "/a/b/node_modules/@types"], + identity, + () => [{ + fallbackPollingInterval: PollingInterval.Medium, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + }); + + it("with watchDirectory option as host configuration", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const files = [libFile, commonFile2, configFile]; + const host = createServerHost(files.concat(commonFile1), { runWithoutRecursiveWatches: true }); + const session = createSession(host); + session.executeCommandSeq({ + command: protocol.CommandTypes.Configure, + arguments: { + watchOptions: { + watchDirectory: protocol.WatchDirectoryKind.UseFsEvents + } + } + }); + const service = session.getProjectService(); + openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); + checkProjectActualFiles( + service.configuredProjects.get(configFile.path)!, + files.map(f => f.path).concat(commonFile1.path) + ); + + checkWatchedFilesDetailed( + host, + files.map(f => f.path.toLowerCase()), + 1, + arrayToMap( + files, + f => f.path.toLowerCase(), + () => [PollingInterval.Low] + ) + ); + checkWatchedDirectoriesDetailed( + host, + ["/a/b", "/a/b/node_modules/@types"], + 1, + /*recursive*/ false, + arrayToMap( + ["/a/b", "/a/b/node_modules/@types"], + identity, + () => [{ + fallbackPollingInterval: PollingInterval.Medium, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + }); + + it("with fallbackPolling option as host configuration", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const files = [libFile, commonFile2, configFile]; + const host = createServerHost(files.concat(commonFile1), { runWithoutRecursiveWatches: true, runWithFallbackPolling: true }); + const session = createSession(host); + session.executeCommandSeq({ + command: protocol.CommandTypes.Configure, + arguments: { + watchOptions: { + fallbackPolling: protocol.PollingWatchKind.PriorityInterval + } + } + }); + const service = session.getProjectService(); + openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); + checkProjectActualFiles( + service.configuredProjects.get(configFile.path)!, + files.map(f => f.path).concat(commonFile1.path) + ); + + const filePaths = files.map(f => f.path.toLowerCase()); + checkWatchedFilesDetailed( + host, + filePaths.concat(["/a/b", "/a/b/node_modules/@types"]), + 1, + arrayToMap( + filePaths.concat(["/a/b", "/a/b/node_modules/@types"]), + identity, + f => [contains(filePaths, f) ? PollingInterval.Low : PollingInterval.Medium] + ) + ); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + }); + + it("with watchFile option in configFile", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + watchFile: "UseFsEvents" + } + }) + }; + const files = [libFile, commonFile2, configFile]; + const host = createServerHost(files.concat(commonFile1)); + const session = createSession(host); + const service = session.getProjectService(); + openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); + checkProjectActualFiles( + service.configuredProjects.get(configFile.path)!, + files.map(f => f.path).concat(commonFile1.path) + ); + + // The closed script infos are watched using host settings + checkWatchedFilesDetailed( + host, + [libFile, commonFile2].map(f => f.path.toLowerCase()), + 1, + arrayToMap( + [libFile, commonFile2], + f => f.path.toLowerCase(), + () => [PollingInterval.Low] + ) + ); + // Config file with the setting with fsWatch + checkWatchedDirectoriesDetailed( + host, + [configFile.path.toLowerCase()], + 1, + /*recursive*/ false, + arrayToMap( + [configFile.path.toLowerCase()], + identity, + () => [{ + fallbackPollingInterval: PollingInterval.High, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + checkWatchedDirectoriesDetailed( + host, + ["/a/b", "/a/b/node_modules/@types"], + 1, + /*recursive*/ true, + arrayToMap( + ["/a/b", "/a/b/node_modules/@types"], + identity, + () => [{ + fallbackPollingInterval: PollingInterval.Medium, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + }); + + it("with watchDirectory option in configFile", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + watchDirectory: "UseFsEvents" + } + }) + }; + const files = [libFile, commonFile2, configFile]; + const host = createServerHost(files.concat(commonFile1), { runWithoutRecursiveWatches: true }); + const session = createSession(host); + const service = session.getProjectService(); + openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); + checkProjectActualFiles( + service.configuredProjects.get(configFile.path)!, + files.map(f => f.path).concat(commonFile1.path) + ); + + checkWatchedFilesDetailed( + host, + files.map(f => f.path.toLowerCase()), + 1, + arrayToMap( + files, + f => f.path.toLowerCase(), + () => [PollingInterval.Low] + ) + ); + checkWatchedDirectoriesDetailed( + host, + ["/a/b", "/a/b/node_modules/@types"], + 1, + /*recursive*/ false, + arrayToMap( + ["/a/b", "/a/b/node_modules/@types"], + identity, + () => [{ + fallbackPollingInterval: PollingInterval.Medium, + fallbackOptions: { watchFile: WatchFileKind.PriorityPollingInterval } + }] + ) + ); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + }); + + it("with fallbackPolling option in configFile", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + fallbackPolling: "PriorityInterval" + } + }) + }; + const files = [libFile, commonFile2, configFile]; + const host = createServerHost(files.concat(commonFile1), { runWithoutRecursiveWatches: true, runWithFallbackPolling: true }); + const session = createSession(host); + session.executeCommandSeq({ + command: protocol.CommandTypes.Configure, + arguments: { + watchOptions: { + fallbackPolling: protocol.PollingWatchKind.PriorityInterval + } + } + }); + const service = session.getProjectService(); + openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); + checkProjectActualFiles( + service.configuredProjects.get(configFile.path)!, + files.map(f => f.path).concat(commonFile1.path) + ); + + const filePaths = files.map(f => f.path.toLowerCase()); + checkWatchedFilesDetailed( + host, + filePaths.concat(["/a/b", "/a/b/node_modules/@types"]), + 1, + arrayToMap( + filePaths.concat(["/a/b", "/a/b/node_modules/@types"]), + identity, + f => [contains(filePaths, f) ? PollingInterval.Low : PollingInterval.Medium] + ) + ); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + }); + }); } diff --git a/src/tsserver/server.ts b/src/tsserver/server.ts index c6a38b7c7a2eb..4915158d7c1ad 100644 --- a/src/tsserver/server.ts +++ b/src/tsserver/server.ts @@ -825,9 +825,9 @@ namespace ts.server { const noopWatcher: FileWatcher = { close: noop }; // This is the function that catches the exceptions when watching directory, and yet lets project service continue to function // Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point - function watchDirectorySwallowingException(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher { + function watchDirectorySwallowingException(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher { try { - return originalWatchDirectory(path, callback, recursive); + return originalWatchDirectory(path, callback, recursive, options); } catch (e) { logger.info(`Exception when creating directory watcher: ${e.message}`); @@ -838,7 +838,7 @@ namespace ts.server { if (useWatchGuard) { const currentDrive = extractWatchDirectoryCacheKey(sys.resolvePath(sys.getCurrentDirectory()), /*currentDriveKey*/ undefined); const statusCache = createMap(); - sys.watchDirectory = (path, callback, recursive) => { + sys.watchDirectory = (path, callback, recursive, options) => { const cacheKey = extractWatchDirectoryCacheKey(path, currentDrive); let status = cacheKey && statusCache.get(cacheKey); if (status === undefined) { @@ -871,7 +871,7 @@ namespace ts.server { } if (status) { // this drive is safe to use - call real 'watchDirectory' - return watchDirectorySwallowingException(path, callback, recursive); + return watchDirectorySwallowingException(path, callback, recursive, options); } else { // this drive is unsafe - return no-op watcher diff --git a/src/typingsInstallerCore/typingsInstaller.ts b/src/typingsInstallerCore/typingsInstaller.ts index 2b94b8874203f..770d0bc163cd3 100644 --- a/src/typingsInstallerCore/typingsInstaller.ts +++ b/src/typingsInstallerCore/typingsInstaller.ts @@ -171,7 +171,7 @@ namespace ts.server.typingsInstaller { } // start watching files - this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch, req.projectRootPath); + this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch, req.projectRootPath, req.watchOptions); // install typings if (discoverTypingsResult.newTypingNames.length) { @@ -399,7 +399,7 @@ namespace ts.server.typingsInstaller { } } - private watchFiles(projectName: string, files: string[], projectRootPath: Path) { + private watchFiles(projectName: string, files: string[], projectRootPath: Path, options: WatchOptions | undefined) { if (!files.length) { // shut down existing watchers this.closeWatchers(projectName); @@ -439,7 +439,7 @@ namespace ts.server.typingsInstaller { watchers.isInvoked = true; this.sendResponse({ projectName, kind: ActionInvalidate }); } - }, /*pollingInterval*/ 2000) : + }, /*pollingInterval*/ 2000, options) : this.installTypingHost.watchDirectory!(path, f => { // TODO: GH#18217 if (isLoggingEnabled) { this.log.writeLine(`DirectoryWatcher:: Triggered with ${f} :: WatchInfo: ${path} recursive :: handler is already invoked '${watchers.isInvoked}'`); @@ -453,7 +453,7 @@ namespace ts.server.typingsInstaller { watchers.isInvoked = true; this.sendResponse({ projectName, kind: ActionInvalidate }); } - }, /*recursive*/ true); + }, /*recursive*/ true, options); watchers.set(canonicalPath, isLoggingEnabled ? { close: () => { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 03fd64da2d257..00890b067f855 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2576,6 +2576,23 @@ declare namespace ts { /** True if it is intended that this reference form a circularity */ circular?: boolean; } + export enum WatchFileKind { + FixedPollingInterval = 0, + PriorityPollingInterval = 1, + DynamicPriorityPolling = 2, + UseFsEvents = 3, + UseFsEventsOnParentDirectory = 4 + } + export enum WatchDirectoryKind { + UseFsEvents = 0, + FixedPollingInterval = 1, + DynamicPriorityPolling = 2 + } + export enum PollingWatchKind { + FixedInterval = 0, + PriorityInterval = 1, + DynamicPriority = 2 + } export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | PluginImport[] | ProjectReference[] | null | undefined; export interface CompilerOptions { allowJs?: boolean; @@ -2663,6 +2680,13 @@ declare namespace ts { useDefineForClassFields?: boolean; [option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined; } + export interface WatchOptions { + watchFile?: WatchFileKind; + watchDirectory?: WatchDirectoryKind; + fallbackPolling?: PollingWatchKind; + synchronousWatchDirectory?: boolean; + [option: string]: CompilerOptionsValue | undefined; + } export interface TypeAcquisition { /** * @deprecated typingOptions.enableAutoDiscovery @@ -2735,6 +2759,7 @@ declare namespace ts { typeAcquisition?: TypeAcquisition; fileNames: string[]; projectReferences?: readonly ProjectReference[]; + watchOptions?: WatchOptions; raw?: any; errors: Diagnostic[]; wildcardDirectories?: MapLike; @@ -3211,8 +3236,8 @@ declare namespace ts { * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that * use native OS file watching */ - 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; resolvePath(path: string): string; fileExists(path: string): boolean; directoryExists(path: string): boolean; @@ -3739,7 +3764,7 @@ declare namespace ts { /** * Reads the config file, reports errors if any and exits if the config file cannot be found */ - export function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost, extendedConfigCache?: Map): ParsedCommandLine | undefined; + export function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost, extendedConfigCache?: Map, watchOptionsToExtend?: WatchOptions): ParsedCommandLine | undefined; /** * Read tsconfig.json file * @param fileName The path to the config file @@ -3773,7 +3798,7 @@ declare 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[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map): ParsedCommandLine; + export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map, existingWatchOptions?: WatchOptions): ParsedCommandLine; /** * Parse the contents of a config file (tsconfig.json). * @param jsonNode The contents of the config file to parse @@ -3781,10 +3806,11 @@ declare namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map): ParsedCommandLine; + export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map, existingWatchOptions?: WatchOptions): ParsedCommandLine; export interface ParsedTsconfig { raw: any; options?: CompilerOptions; + watchOptions?: WatchOptions; typeAcquisition?: TypeAcquisition; /** * Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet @@ -4585,9 +4611,9 @@ declare namespace ts { /** If provided, called with Diagnostic message that informs about change in watch status */ onWatchStatusChange?(diagnostic: Diagnostic, newLine: string, options: CompilerOptions, errorCount?: number): void; /** Used to watch changes in source files, missing files needed to update the program or config file */ - watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; + watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: CompilerOptions): FileWatcher; /** Used to watch resolved module's failed lookup locations, config file specs, type roots where auto type reference directives are added */ - watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; + watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: CompilerOptions): FileWatcher; /** If provided, will be used to set delayed compilation, so that multiple changes in short span are compiled together */ setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; /** If provided, will be used to reset existing delayed compilation */ @@ -4643,6 +4669,7 @@ declare namespace ts { rootFiles: string[]; /** Compiler options */ options: CompilerOptions; + watchOptions?: WatchOptions; /** Project References */ projectReferences?: readonly ProjectReference[]; } @@ -4654,6 +4681,7 @@ declare namespace ts { configFileName: string; /** Options to extend */ optionsToExtend?: CompilerOptions; + watchOptionsToExtend?: WatchOptions; /** * Used to generate source file names from the config file and its include, exclude, files rules * and also to cache the directory stucture @@ -4681,8 +4709,8 @@ declare namespace ts { /** * Create the watch compiler host for either configFile or fileNames and its options */ - function createWatchCompilerHost(configFileName: string, optionsToExtend: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfConfigFile; - function createWatchCompilerHost(rootFiles: string[], options: CompilerOptions, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, projectReferences?: readonly ProjectReference[]): WatchCompilerHostOfFilesAndCompilerOptions; + function createWatchCompilerHost(configFileName: string, optionsToExtend: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, watchOptionsToExtend?: WatchOptions): WatchCompilerHostOfConfigFile; + function createWatchCompilerHost(rootFiles: string[], options: CompilerOptions, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, projectReferences?: readonly ProjectReference[], watchOptions?: WatchOptions): WatchCompilerHostOfFilesAndCompilerOptions; /** * Creates the watch from the host for root files and compiler options */ @@ -4736,7 +4764,7 @@ declare namespace ts { function createSolutionBuilderHost(system?: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportErrorSummary?: ReportEmitErrorSummary): SolutionBuilderHost; function createSolutionBuilderWithWatchHost(system?: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): SolutionBuilderWithWatchHost; function createSolutionBuilder(host: SolutionBuilderHost, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder; - function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder; enum InvalidatedProjectKind { Build = 0, UpdateBundle = 1, @@ -4797,6 +4825,7 @@ declare namespace ts.server { readonly fileNames: string[]; readonly projectRootPath: Path; readonly compilerOptions: CompilerOptions; + readonly watchOptions?: WatchOptions; readonly typeAcquisition: TypeAcquisition; readonly unresolvedImports: SortedReadonlyArray; readonly cachePath?: string; @@ -5913,8 +5942,8 @@ declare namespace ts.server { }; }; 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; @@ -6934,7 +6963,7 @@ declare namespace ts.server.protocol { * For external projects, some of the project settings are sent together with * compiler settings. */ - type ExternalProjectCompilerOptions = CompilerOptions & CompileOnSaveMixin; + type ExternalProjectCompilerOptions = CompilerOptions & CompileOnSaveMixin & WatchOptions; /** * Represents a set of changes that happen in project */ @@ -6974,6 +7003,31 @@ declare namespace ts.server.protocol { * The host's additional supported .js file extensions */ extraFileExtensions?: FileExtensionInfo[]; + watchOptions?: WatchOptions; + } + enum WatchFileKind { + FixedPollingInterval = "FixedPollingInterval", + PriorityPollingInterval = "PriorityPollingInterval", + DynamicPriorityPolling = "DynamicPriorityPolling", + UseFsEvents = "UseFsEvents", + UseFsEventsOnParentDirectory = "UseFsEventsOnParentDirectory" + } + enum WatchDirectoryKind { + UseFsEvents = "UseFsEvents", + FixedPollingInterval = "FixedPollingInterval", + DynamicPriorityPolling = "DynamicPriorityPolling" + } + enum PollingWatchKind { + FixedInterval = "FixedInterval", + PriorityInterval = "PriorityInterval", + DynamicPriority = "DynamicPriority" + } + interface WatchOptions { + watchFile?: WatchFileKind | ts.WatchFileKind; + watchDirectory?: WatchDirectoryKind | ts.WatchDirectoryKind; + fallbackPolling?: PollingWatchKind | ts.PollingWatchKind; + synchronousWatchDirectory?: boolean; + [option: string]: CompilerOptionsValue | undefined; } /** * Configure request; value of command field is "configure". Specifies @@ -8518,6 +8572,7 @@ declare namespace ts.server { private documentRegistry; private compilerOptions; compileOnSaveEnabled: boolean; + protected watchOptions: WatchOptions | undefined; private rootFiles; private rootFilesMap; private program; @@ -8874,6 +8929,7 @@ declare namespace ts.server { } export function convertFormatOptions(protocolOptions: protocol.FormatCodeSettings): FormatCodeSettings; export function convertCompilerOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): CompilerOptions & protocol.CompileOnSaveMixin; + export function convertWatchOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): WatchOptions | undefined; export function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind; export function convertScriptKindName(scriptKindName: protocol.ScriptKindName): ScriptKind.Unknown | ScriptKind.JS | ScriptKind.JSX | ScriptKind.TS | ScriptKind.TSX; export interface HostConfiguration { @@ -8881,6 +8937,7 @@ declare namespace ts.server { preferences: protocol.UserPreferences; hostInfo: string; extraFileExtensions?: FileExtensionInfo[]; + watchOptions?: WatchOptions; } export interface OpenConfiguredProjectResult { configFileName?: NormalizedPath; @@ -8937,6 +8994,8 @@ declare namespace ts.server { private readonly openFilesWithNonRootedDiskPath; private compilerOptionsForInferredProjects; private compilerOptionsForInferredProjectsPerProjectRoot; + private watchOptionsForInferredProjects; + private watchOptionsForInferredProjectsPerProjectRoot; /** * Project size for configured or external projects */ @@ -9003,7 +9062,6 @@ declare namespace ts.server { private delayUpdateSourceInfoProjects; private delayUpdateProjectsOfScriptInfoPath; private handleDeletedFile; - private onConfigChangedForConfiguredProject; /** * This is the callback function for the config file add/remove/change at any location * that matters to open script info but doesnt have configured project open diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 0e25996628552..89f8bb0543dea 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2576,6 +2576,23 @@ declare namespace ts { /** True if it is intended that this reference form a circularity */ circular?: boolean; } + export enum WatchFileKind { + FixedPollingInterval = 0, + PriorityPollingInterval = 1, + DynamicPriorityPolling = 2, + UseFsEvents = 3, + UseFsEventsOnParentDirectory = 4 + } + export enum WatchDirectoryKind { + UseFsEvents = 0, + FixedPollingInterval = 1, + DynamicPriorityPolling = 2 + } + export enum PollingWatchKind { + FixedInterval = 0, + PriorityInterval = 1, + DynamicPriority = 2 + } export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | PluginImport[] | ProjectReference[] | null | undefined; export interface CompilerOptions { allowJs?: boolean; @@ -2663,6 +2680,13 @@ declare namespace ts { useDefineForClassFields?: boolean; [option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined; } + export interface WatchOptions { + watchFile?: WatchFileKind; + watchDirectory?: WatchDirectoryKind; + fallbackPolling?: PollingWatchKind; + synchronousWatchDirectory?: boolean; + [option: string]: CompilerOptionsValue | undefined; + } export interface TypeAcquisition { /** * @deprecated typingOptions.enableAutoDiscovery @@ -2735,6 +2759,7 @@ declare namespace ts { typeAcquisition?: TypeAcquisition; fileNames: string[]; projectReferences?: readonly ProjectReference[]; + watchOptions?: WatchOptions; raw?: any; errors: Diagnostic[]; wildcardDirectories?: MapLike; @@ -3211,8 +3236,8 @@ declare namespace ts { * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that * use native OS file watching */ - 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; resolvePath(path: string): string; fileExists(path: string): boolean; directoryExists(path: string): boolean; @@ -3739,7 +3764,7 @@ declare namespace ts { /** * Reads the config file, reports errors if any and exits if the config file cannot be found */ - export function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost, extendedConfigCache?: Map): ParsedCommandLine | undefined; + export function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost, extendedConfigCache?: Map, watchOptionsToExtend?: WatchOptions): ParsedCommandLine | undefined; /** * Read tsconfig.json file * @param fileName The path to the config file @@ -3773,7 +3798,7 @@ declare 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[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map): ParsedCommandLine; + export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map, existingWatchOptions?: WatchOptions): ParsedCommandLine; /** * Parse the contents of a config file (tsconfig.json). * @param jsonNode The contents of the config file to parse @@ -3781,10 +3806,11 @@ declare namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map): ParsedCommandLine; + export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map, existingWatchOptions?: WatchOptions): ParsedCommandLine; export interface ParsedTsconfig { raw: any; options?: CompilerOptions; + watchOptions?: WatchOptions; typeAcquisition?: TypeAcquisition; /** * Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet @@ -4585,9 +4611,9 @@ declare namespace ts { /** If provided, called with Diagnostic message that informs about change in watch status */ onWatchStatusChange?(diagnostic: Diagnostic, newLine: string, options: CompilerOptions, errorCount?: number): void; /** Used to watch changes in source files, missing files needed to update the program or config file */ - watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; + watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: CompilerOptions): FileWatcher; /** Used to watch resolved module's failed lookup locations, config file specs, type roots where auto type reference directives are added */ - watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; + watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: CompilerOptions): FileWatcher; /** If provided, will be used to set delayed compilation, so that multiple changes in short span are compiled together */ setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; /** If provided, will be used to reset existing delayed compilation */ @@ -4643,6 +4669,7 @@ declare namespace ts { rootFiles: string[]; /** Compiler options */ options: CompilerOptions; + watchOptions?: WatchOptions; /** Project References */ projectReferences?: readonly ProjectReference[]; } @@ -4654,6 +4681,7 @@ declare namespace ts { configFileName: string; /** Options to extend */ optionsToExtend?: CompilerOptions; + watchOptionsToExtend?: WatchOptions; /** * Used to generate source file names from the config file and its include, exclude, files rules * and also to cache the directory stucture @@ -4681,8 +4709,8 @@ declare namespace ts { /** * Create the watch compiler host for either configFile or fileNames and its options */ - function createWatchCompilerHost(configFileName: string, optionsToExtend: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfConfigFile; - function createWatchCompilerHost(rootFiles: string[], options: CompilerOptions, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, projectReferences?: readonly ProjectReference[]): WatchCompilerHostOfFilesAndCompilerOptions; + function createWatchCompilerHost(configFileName: string, optionsToExtend: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, watchOptionsToExtend?: WatchOptions): WatchCompilerHostOfConfigFile; + function createWatchCompilerHost(rootFiles: string[], options: CompilerOptions, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter, projectReferences?: readonly ProjectReference[], watchOptions?: WatchOptions): WatchCompilerHostOfFilesAndCompilerOptions; /** * Creates the watch from the host for root files and compiler options */ @@ -4736,7 +4764,7 @@ declare namespace ts { function createSolutionBuilderHost(system?: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportErrorSummary?: ReportEmitErrorSummary): SolutionBuilderHost; function createSolutionBuilderWithWatchHost(system?: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): SolutionBuilderWithWatchHost; function createSolutionBuilder(host: SolutionBuilderHost, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder; - function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder; enum InvalidatedProjectKind { Build = 0, UpdateBundle = 1, @@ -4797,6 +4825,7 @@ declare namespace ts.server { readonly fileNames: string[]; readonly projectRootPath: Path; readonly compilerOptions: CompilerOptions; + readonly watchOptions?: WatchOptions; readonly typeAcquisition: TypeAcquisition; readonly unresolvedImports: SortedReadonlyArray; readonly cachePath?: string; diff --git a/tests/baselines/reference/showConfig/Show TSConfig with watch options/tsconfig.json b/tests/baselines/reference/showConfig/Show TSConfig with watch options/tsconfig.json new file mode 100644 index 0000000000000..7685275bbe730 --- /dev/null +++ b/tests/baselines/reference/showConfig/Show TSConfig with watch options/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": {}, + "watchOptions": { + "watchFile": "dynamicprioritypolling" + }, + "include": [ + "./src/**/*" + ] +} diff --git a/tests/baselines/reference/showConfig/Shows tsconfig for single option/fallbackPolling/tsconfig.json b/tests/baselines/reference/showConfig/Shows tsconfig for single option/fallbackPolling/tsconfig.json new file mode 100644 index 0000000000000..3985b8f84a2bb --- /dev/null +++ b/tests/baselines/reference/showConfig/Shows tsconfig for single option/fallbackPolling/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": {}, + "watchOptions": { + "fallbackPolling": "fixedinterval" + } +} diff --git a/tests/baselines/reference/showConfig/Shows tsconfig for single option/synchronousWatchDirectory/tsconfig.json b/tests/baselines/reference/showConfig/Shows tsconfig for single option/synchronousWatchDirectory/tsconfig.json new file mode 100644 index 0000000000000..f22b080df09b8 --- /dev/null +++ b/tests/baselines/reference/showConfig/Shows tsconfig for single option/synchronousWatchDirectory/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": {}, + "watchOptions": { + "synchronousWatchDirectory": true + } +} diff --git a/tests/baselines/reference/showConfig/Shows tsconfig for single option/watchDirectory/tsconfig.json b/tests/baselines/reference/showConfig/Shows tsconfig for single option/watchDirectory/tsconfig.json new file mode 100644 index 0000000000000..a5b93e65d9233 --- /dev/null +++ b/tests/baselines/reference/showConfig/Shows tsconfig for single option/watchDirectory/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": {}, + "watchOptions": { + "watchDirectory": "usefsevents" + } +} diff --git a/tests/baselines/reference/showConfig/Shows tsconfig for single option/watchFile/tsconfig.json b/tests/baselines/reference/showConfig/Shows tsconfig for single option/watchFile/tsconfig.json new file mode 100644 index 0000000000000..532addad08bfa --- /dev/null +++ b/tests/baselines/reference/showConfig/Shows tsconfig for single option/watchFile/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": {}, + "watchOptions": { + "watchFile": "fixedpollinginterval" + } +}