diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 5c4362af15291..452dedfa3568a 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -2812,7 +2812,8 @@ namespace ts { return defaultOptions; } - function convertJsonOption(opt: CommandLineOption, value: any, basePath: string, errors: Push): CompilerOptionsValue { + /*@internal*/ + export function convertJsonOption(opt: CommandLineOption, value: any, basePath: string, errors: Push): CompilerOptionsValue { if (isCompilerOptionsValue(opt, value)) { const optType = opt.type; if (optType === "list" && isArray(value)) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e8b1021eb9ae5..aaff8a3b371ce 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -251,17 +251,18 @@ namespace ts.server { return protocolOptions; } - export function convertWatchOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): WatchOptions | undefined { - let result: WatchOptions | undefined; - debugger; - watchOptionsConverters.forEach((mappedValues, id) => { - const propertyValue = protocolOptions[id]; + export function convertWatchOptions(protocolOptions: protocol.ExternalProjectCompilerOptions, currentDirectory?: string): WatchOptionsAndErrors | undefined { + let watchOptions: WatchOptions | undefined; + let errors: Diagnostic[] | undefined; + optionsForWatch.forEach(option => { + const propertyValue = protocolOptions[option.name]; if (propertyValue === undefined) return; - (result || (result = {}))[id] = isString(propertyValue) ? - mappedValues.get(propertyValue.toLowerCase()) : - propertyValue; + const mappedValues = watchOptionsConverters.get(option.name); + (watchOptions || (watchOptions = {}))[option.name] = mappedValues ? + isString(propertyValue) ? mappedValues.get(propertyValue.toLowerCase()) : propertyValue : + convertJsonOption(option, propertyValue, currentDirectory || "", errors || (errors = [])); }); - return result; + return watchOptions && { watchOptions, errors }; } export function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind { @@ -560,6 +561,11 @@ namespace ts.server { changes: Iterator; } + export interface WatchOptionsAndErrors { + watchOptions: WatchOptions; + errors: Diagnostic[] | undefined; + } + export class ProjectService { /*@internal*/ @@ -616,8 +622,8 @@ namespace ts.server { private compilerOptionsForInferredProjects: CompilerOptions | undefined; private compilerOptionsForInferredProjectsPerProjectRoot = createMap(); - private watchOptionsForInferredProjects: WatchOptions | undefined; - private watchOptionsForInferredProjectsPerProjectRoot = createMap(); + private watchOptionsForInferredProjects: WatchOptionsAndErrors | undefined; + private watchOptionsForInferredProjectsPerProjectRoot = createMap(); /** * Project size for configured or external projects */ @@ -949,7 +955,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); + const watchOptions = convertWatchOptions(projectCompilerOptions, projectRootPath); // 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 @@ -977,7 +983,8 @@ namespace ts.server { project.projectRootPath === canonicalProjectRootPath : !project.projectRootPath || !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath)) { project.setCompilerOptions(compilerOptions); - project.setWatchOptions(watchOptions); + project.setWatchOptions(watchOptions?.watchOptions); + project.setProjectErrors(watchOptions?.errors); project.compileOnSaveEnabled = compilerOptions.compileOnSave!; project.markAsDirty(); this.delayUpdateProjectGraph(project); @@ -1860,7 +1867,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 watchOptionsAndErrors = convertWatchOptions(options, getDirectoryPath(normalizeSlashes(projectFileName))); const project = new ExternalProject( projectFileName, this, @@ -1870,8 +1877,9 @@ namespace ts.server { options.compileOnSave === undefined ? true : options.compileOnSave, /*projectFilePath*/ undefined, this.currentPluginConfigOverrides, - watchOptions + watchOptionsAndErrors?.watchOptions ); + project.setProjectErrors(watchOptionsAndErrors?.errors); project.excludedFiles = excludedFiles; this.addFilesToNonInferredProject(project, files, externalFilePropertyReader, typeAcquisition); @@ -2246,14 +2254,16 @@ 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 - let watchOptions: WatchOptions | false | undefined; + let watchOptionsAndErrors: WatchOptionsAndErrors | false | undefined; if (projectRootPath) { - watchOptions = this.watchOptionsForInferredProjectsPerProjectRoot.get(projectRootPath); + watchOptionsAndErrors = this.watchOptionsForInferredProjectsPerProjectRoot.get(projectRootPath); } - if (watchOptions === undefined) { - watchOptions = this.watchOptionsForInferredProjects; + if (watchOptionsAndErrors === undefined) { + watchOptionsAndErrors = this.watchOptionsForInferredProjects; } - const project = new InferredProject(this, this.documentRegistry, compilerOptions, watchOptions || undefined, projectRootPath, currentDirectory, this.currentPluginConfigOverrides); + watchOptionsAndErrors = watchOptionsAndErrors || undefined; + const project = new InferredProject(this, this.documentRegistry, compilerOptions, watchOptionsAndErrors?.watchOptions, projectRootPath, currentDirectory, this.currentPluginConfigOverrides); + project.setProjectErrors(watchOptionsAndErrors?.errors); if (isSingleInferredProject) { this.inferredProjects.unshift(project); } @@ -2705,7 +2715,7 @@ namespace ts.server { } if (args.watchOptions) { - this.hostConfiguration.watchOptions = convertWatchOptions(args.watchOptions); + this.hostConfiguration.watchOptions = convertWatchOptions(args.watchOptions)?.watchOptions; this.logger.info(`Host watch options changed to ${JSON.stringify(this.hostConfiguration.watchOptions)}, it will be take effect for next watches.`); } } @@ -3579,7 +3589,7 @@ namespace ts.server { externalProject.excludedFiles = excludedFiles; if (!tsConfigFiles) { const compilerOptions = convertCompilerOptions(proj.options); - const watchOptions = convertWatchOptions(proj.options); + const watchOptionsAndErrors = convertWatchOptions(proj.options, externalProject.getCurrentDirectory()); const lastFileExceededProgramSize = this.getFilenameForExceededTotalSizeLimitForNonTsFiles(proj.projectFileName, compilerOptions, proj.rootFiles, externalFilePropertyReader); if (lastFileExceededProgramSize) { externalProject.disableLanguageService(lastFileExceededProgramSize); @@ -3587,9 +3597,10 @@ namespace ts.server { else { externalProject.enableLanguageService(); } + externalProject.setProjectErrors(watchOptionsAndErrors?.errors); // 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, watchOptions); + this.updateRootAndOptionsOfNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, compilerOptions, proj.typeAcquisition, proj.options.compileOnSave, watchOptionsAndErrors?.watchOptions); externalProject.updateGraph(); return; } diff --git a/src/server/project.ts b/src/server/project.ts index 9f585a394bb57..dba9215819d31 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -189,6 +189,8 @@ namespace ts.server { */ private projectStateVersion = 0; + protected projectErrors: Diagnostic[] | undefined; + protected isInitialLoadPending: () => boolean = returnFalse; /*@internal*/ @@ -565,11 +567,18 @@ namespace ts.server { * Get the errors that dont have any file name associated */ getGlobalProjectErrors(): readonly Diagnostic[] { - return emptyArray; + return filter(this.projectErrors, diagnostic => !diagnostic.file) || emptyArray; } + /** + * Get all the project errors + */ getAllProjectErrors(): readonly Diagnostic[] { - return emptyArray; + return this.projectErrors || emptyArray; + } + + setProjectErrors(projectErrors: Diagnostic[] | undefined) { + this.projectErrors = projectErrors; } getLanguageService(ensureSynchronized = true): LanguageService { @@ -742,6 +751,7 @@ namespace ts.server { this.resolutionCache = undefined!; this.cachedUnresolvedImportsPerFile = undefined!; this.directoryStructureHost = undefined!; + this.projectErrors = undefined; // Clean up file watchers waiting for missing files if (this.missingFilesMap) { @@ -1966,8 +1976,6 @@ namespace ts.server { /** Ref count to the project when opened from external project */ private externalProjectRefCount = 0; - private projectErrors: Diagnostic[] | undefined; - private projectReferences: readonly ProjectReference[] | undefined; /** Potential project references before the project is actually loaded (read config file) */ @@ -2135,24 +2143,6 @@ namespace ts.server { this.enableGlobalPlugins(options, pluginConfigOverrides); } - /** - * Get the errors that dont have any file name associated - */ - getGlobalProjectErrors(): readonly Diagnostic[] { - return filter(this.projectErrors, diagnostic => !diagnostic.file) || emptyArray; - } - - /** - * Get all the project errors - */ - getAllProjectErrors(): readonly Diagnostic[] { - return this.projectErrors || emptyArray; - } - - setProjectErrors(projectErrors: Diagnostic[]) { - this.projectErrors = projectErrors; - } - setTypeAcquisition(newTypeAcquisition: TypeAcquisition): void { this.typeAcquisition = this.removeLocalTypingsFromTypeAcquisition(newTypeAcquisition); } @@ -2186,7 +2176,6 @@ namespace ts.server { } this.stopWatchingWildCards(); - this.projectErrors = undefined; this.configFileSpecs = undefined; this.openFileWatchTriggered.clear(); this.compilerHost = undefined; diff --git a/src/testRunner/unittests/tsserver/watchEnvironment.ts b/src/testRunner/unittests/tsserver/watchEnvironment.ts index d15e0cdd5c1ba..f2bd262992717 100644 --- a/src/testRunner/unittests/tsserver/watchEnvironment.ts +++ b/src/testRunner/unittests/tsserver/watchEnvironment.ts @@ -567,11 +567,7 @@ namespace ts.projectSystem { }); describe("excludeDirectories", () => { - function setup(configureHost?: boolean) { - const configFile: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ include: ["src"], watchOptions: { excludeDirectories: ["node_modules"] } }) - }; + function setupFiles() { const main: File = { path: `${tscWatch.projectRoot}/src/main.ts`, content: `import { foo } from "bar"; foo();` @@ -584,17 +580,30 @@ namespace ts.projectSystem { path: `${tscWatch.projectRoot}/node_modules/bar/foo.d.ts`, content: `export function foo(): string;` }; - const files = [libFile, main, bar, foo, configFile]; - const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); - const service = createProjectService(host); + return { main, bar, foo }; + } + + function setupConfigureHost(service: TestProjectService, configureHost: boolean | undefined) { if (configureHost) { service.setHostConfiguration({ watchOptions: { excludeDirectories: ["node_modules"] } }); } + } + function setup(configureHost?: boolean) { + const configFile: File = { + path: `${tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ include: ["src"], watchOptions: { excludeDirectories: ["node_modules"] } }) + }; + const { main, bar, foo } = setupFiles(); + const files = [libFile, main, bar, foo, configFile]; + const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); + const service = createProjectService(host); + setupConfigureHost(service, configureHost); service.openClientFile(main.path); return { host, configFile }; } + it("with excludeDirectories option in configFile", () => { const { host, configFile } = setup(); checkWatchedFilesDetailed(host, [configFile.path, libFile.path], 1); @@ -621,6 +630,136 @@ namespace ts.projectSystem { /*recursive*/ true, ); }); + + function setupExternalProject(configureHost?: boolean) { + const { main, bar, foo } = setupFiles(); + const files = [libFile, main, bar, foo]; + const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); + const service = createProjectService(host); + setupConfigureHost(service, configureHost); + service.openExternalProject({ + projectFileName: `${tscWatch.projectRoot}/project.csproj`, + rootFiles: toExternalFiles([main.path, bar.path, foo.path]), + options: { excludeDirectories: ["node_modules"] } + }); + service.openClientFile(main.path); + return host; + } + + it("external project watch options", () => { + const host = setupExternalProject(); + checkWatchedFilesDetailed(host, [libFile.path], 1); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed( + host, + [`${tscWatch.projectRoot}/src`, `${tscWatch.projectRoot}/node_modules`], + 1, + /*recursive*/ true, + ); + }); + + it("external project watch options in host configuration", () => { + const host = setupExternalProject(/*configureHost*/ true); + checkWatchedFilesDetailed(host, [libFile.path], 1); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed( + host, + [`${tscWatch.projectRoot}/src`], + 1, + /*recursive*/ true, + ); + }); + + it("external project watch options errors", () => { + const { main, bar, foo } = setupFiles(); + const files = [libFile, main, bar, foo]; + const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); + const service = createProjectService(host); + service.openExternalProject({ + projectFileName: `${tscWatch.projectRoot}/project.csproj`, + rootFiles: toExternalFiles([main.path, bar.path, foo.path]), + options: { excludeDirectories: ["**/../*"] } + }); + service.openClientFile(main.path); + const project = service.externalProjects[0]; + assert.deepEqual(project.getAllProjectErrors(), [ + { + messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, + category: Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, + code: Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, + file: undefined, + start: undefined, + length: undefined, + reportsDeprecated: undefined, + reportsUnnecessary: undefined, + } + ]); + }); + + function setupInferredProject(configureHost?: boolean) { + const { main, bar, foo } = setupFiles(); + const files = [libFile, main, bar, foo]; + const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); + const service = createProjectService(host, {}, { useInferredProjectPerProjectRoot: true }); + setupConfigureHost(service, configureHost); + service.setCompilerOptionsForInferredProjects({ excludeDirectories: ["node_modules"] }, tscWatch.projectRoot); + service.openClientFile(main.path, main.content, ScriptKind.TS, tscWatch.projectRoot); + return host; + } + + it("inferred project watch options", () => { + const host = setupInferredProject(); + checkWatchedFilesDetailed( + host, + [libFile.path, `${tscWatch.projectRoot}/tsconfig.json`, `${tscWatch.projectRoot}/jsconfig.json`, `${tscWatch.projectRoot}/src/tsconfig.json`, `${tscWatch.projectRoot}/src/jsconfig.json`], + 1 + ); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed( + host, + [`${tscWatch.projectRoot}/src`, `${tscWatch.projectRoot}/node_modules`], + 1, + /*recursive*/ true, + ); + }); + + it("inferred project watch options in host configuration", () => { + const host = setupInferredProject(/*configureHost*/ true); + checkWatchedFilesDetailed( + host, + [libFile.path, `${tscWatch.projectRoot}/tsconfig.json`, `${tscWatch.projectRoot}/jsconfig.json`, `${tscWatch.projectRoot}/src/tsconfig.json`, `${tscWatch.projectRoot}/src/jsconfig.json`], + 1 + ); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed( + host, + [`${tscWatch.projectRoot}/src`], + 1, + /*recursive*/ true, + ); + }); + + it("inferred project watch options errors", () => { + const { main, bar, foo } = setupFiles(); + const files = [libFile, main, bar, foo]; + const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); + const service = createProjectService(host, {}, { useInferredProjectPerProjectRoot: true }); + service.setCompilerOptionsForInferredProjects({ excludeDirectories: ["**/../*"] }, tscWatch.projectRoot); + service.openClientFile(main.path, main.content, ScriptKind.TS, tscWatch.projectRoot); + const project = service.inferredProjects[0]; + assert.deepEqual(project.getAllProjectErrors(), [ + { + messageText: `File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '**/../*'.`, + category: Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.category, + code: Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0.code, + file: undefined, + start: undefined, + length: undefined, + reportsDeprecated: undefined, + reportsUnnecessary: undefined, + } + ]); + }); }); }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index f518dd8ee6b19..4928e4618ed53 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -9109,6 +9109,7 @@ declare namespace ts.server { * This property is different from projectStructureVersion since in most cases edits don't affect set of files in the project */ private projectStateVersion; + protected projectErrors: Diagnostic[] | undefined; protected isInitialLoadPending: () => boolean; private readonly cancellationToken; isNonTsProject(): boolean; @@ -9147,7 +9148,11 @@ declare namespace ts.server { * Get the errors that dont have any file name associated */ getGlobalProjectErrors(): readonly Diagnostic[]; + /** + * Get all the project errors + */ getAllProjectErrors(): readonly Diagnostic[]; + setProjectErrors(projectErrors: Diagnostic[] | undefined): void; getLanguageService(ensureSynchronized?: boolean): LanguageService; getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[]; /** @@ -9243,7 +9248,6 @@ declare namespace ts.server { readonly canonicalConfigFilePath: NormalizedPath; /** Ref count to the project when opened from external project */ private externalProjectRefCount; - private projectErrors; private projectReferences; /** * If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph @@ -9253,15 +9257,6 @@ declare namespace ts.server { getConfigFilePath(): NormalizedPath; getProjectReferences(): readonly ProjectReference[] | undefined; updateReferences(refs: readonly ProjectReference[] | undefined): void; - /** - * Get the errors that dont have any file name associated - */ - getGlobalProjectErrors(): readonly Diagnostic[]; - /** - * Get all the project errors - */ - getAllProjectErrors(): readonly Diagnostic[]; - setProjectErrors(projectErrors: Diagnostic[]): void; setTypeAcquisition(newTypeAcquisition: TypeAcquisition): void; getTypeAcquisition(): TypeAcquisition; close(): void; @@ -9412,7 +9407,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 convertWatchOptions(protocolOptions: protocol.ExternalProjectCompilerOptions, currentDirectory?: string): WatchOptionsAndErrors | 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 { @@ -9442,6 +9437,10 @@ declare namespace ts.server { typesMapLocation?: string; syntaxOnly?: boolean; } + export interface WatchOptionsAndErrors { + watchOptions: WatchOptions; + errors: Diagnostic[] | undefined; + } export class ProjectService { private readonly scriptInfoInNodeModulesWatchers; /**