diff --git a/src/harness/fakesHosts.ts b/src/harness/fakesHosts.ts index 43b842f45f9..5fc00b728e9 100644 --- a/src/harness/fakesHosts.ts +++ b/src/harness/fakesHosts.ts @@ -224,6 +224,7 @@ namespace fakes { private readonly _outputsMap: collections.SortedMap; public readonly traces: string[] = []; public readonly shouldAssertInvariants = !Harness.lightMode; + private _setParentNodes: boolean; private _sourceFiles: collections.SortedMap; private _parseConfigHost: ParseConfigHost | undefined; diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 5468068d922..959f816ae0e 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -991,6 +991,7 @@ namespace Harness.LanguageService { const serverHost = new SessionServerHost(clientHost); const opts: ts.server.SessionOptions = { host: serverHost, + fshost: undefined, // TODO: fshost tests will want to provide this for testing language service? cancellationToken: ts.server.nullCancellationToken, useSingleInferredProject: false, useInferredProjectPerProjectRoot: false, diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5a5df3881b4..712783b167f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -395,6 +395,7 @@ namespace ts.server { export interface ProjectServiceOptions { host: ServerHost; + fshost: ServerHost | undefined; logger: Logger; cancellationToken: HostCancellationToken; useSingleInferredProject: boolean; @@ -758,6 +759,7 @@ namespace ts.server { readonly toCanonicalFileName: (f: string) => string; public readonly host: ServerHost; + public readonly fshost: ServerHost | undefined; // TODOO: Should be TestFSWithWatch.VirtualServerHost | undefined public readonly logger: Logger; public readonly cancellationToken: HostCancellationToken; public readonly useSingleInferredProject: boolean; @@ -804,6 +806,7 @@ namespace ts.server { constructor(opts: ProjectServiceOptions) { this.host = opts.host; + this.fshost = opts.fshost; this.logger = opts.logger; this.cancellationToken = opts.cancellationToken; this.useSingleInferredProject = opts.useSingleInferredProject; @@ -834,8 +837,9 @@ namespace ts.server { if (this.host.realpath) { this.realpathToScriptInfos = createMultiMap(); } - this.currentDirectory = toNormalizedPath(this.host.getCurrentDirectory()); - this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); + const fshost = this.fshost || this.host + this.currentDirectory = toNormalizedPath(fshost.getCurrentDirectory()); + this.toCanonicalFileName = createGetCanonicalFileName(fshost.useCaseSensitiveFileNames); this.globalCacheLocationDirectoryPath = this.typingsInstaller.globalTypingsCacheLocation ? ensureTrailingDirectorySeparator(this.toPath(this.typingsInstaller.globalTypingsCacheLocation)) : undefined; @@ -859,7 +863,7 @@ namespace ts.server { extraFileExtensions: [], }; - this.documentRegistry = createDocumentRegistryInternal(this.host.useCaseSensitiveFileNames, this.currentDirectory, this); + this.documentRegistry = createDocumentRegistryInternal(fshost.useCaseSensitiveFileNames, this.currentDirectory, this); const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose : this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop; @@ -869,7 +873,7 @@ namespace ts.server { watchFile: returnNoopFileWatcher, watchDirectory: returnNoopFileWatcher, } : - getWatchFactory(this.host, watchLogLevel, log, getDetailWatchInfo); + getWatchFactory(fshost, watchLogLevel, log, getDetailWatchInfo); } toPath(fileName: string) { @@ -878,12 +882,12 @@ namespace ts.server { /*@internal*/ getExecutingFilePath() { - return this.getNormalizedAbsolutePath(this.host.getExecutingFilePath()); + return this.getNormalizedAbsolutePath((this.fshost || this.host).getExecutingFilePath()); } /*@internal*/ getNormalizedAbsolutePath(fileName: string) { - return getNormalizedAbsolutePath(fileName, this.host.getCurrentDirectory()); + return getNormalizedAbsolutePath(fileName, (this.fshost || this.host).getCurrentDirectory()); } /*@internal*/ @@ -922,7 +926,7 @@ namespace ts.server { private loadTypesMap() { try { - const fileContent = this.host.readFile(this.typesMapLocation!); // TODO: GH#18217 + const fileContent = (this.fshost || this.host).readFile(this.typesMapLocation!); // TODO: GH#18217 if (fileContent === undefined) { this.logger.info(`Provided types map file "${this.typesMapLocation}" doesn't exist`); return; @@ -1302,7 +1306,7 @@ namespace ts.server { const fileOrDirectoryPath = this.toPath(fileOrDirectory); const fsResult = config.cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); if (getBaseFileName(fileOrDirectoryPath) === "package.json" && !isInsideNodeModules(fileOrDirectoryPath) && - (fsResult && fsResult.fileExists || !fsResult && this.host.fileExists(fileOrDirectoryPath)) + (fsResult && fsResult.fileExists || !fsResult && (this.fshost || this.host).fileExists(fileOrDirectoryPath)) ) { this.logger.info(`Config: ${configFileName} Detected new package.json: ${fileOrDirectory}`); this.onAddPackageJson(fileOrDirectoryPath); @@ -1318,7 +1322,7 @@ namespace ts.server { currentDirectory: this.currentDirectory, options: config.parsedCommandLine!.options, program: configuredProjectForConfig?.getCurrentProgram() || config.parsedCommandLine!.fileNames, - useCaseSensitiveFileNames: this.host.useCaseSensitiveFileNames, + useCaseSensitiveFileNames: (this.fshost || this.host).useCaseSensitiveFileNames, writeLog: s => this.logger.info(s), toPath: s => this.toPath(s) })) return; @@ -1547,7 +1551,7 @@ namespace ts.server { // Closing file should trigger re-reading the file content from disk. This is // because the user may chose to discard the buffer content before saving // to the disk, and the server's version of the file can be out of sync. - const fileExists = info.isDynamic ? false : this.host.fileExists(info.fileName); + const fileExists = info.isDynamic ? false : (this.fshost || this.host).fileExists(info.fileName); info.close(fileExists); this.stopWatchingConfigFilesForClosedScriptInfo(info); @@ -1649,7 +1653,7 @@ namespace ts.server { // Or the whole chain of config files for the roots of the inferred projects // Cache the host value of file exists and add the info to map of open files impacted by this config file - const exists = this.host.fileExists(configFileName); + const exists = (this.fshost || this.host).fileExists(configFileName); let openFilesImpactedByConfigFile: ESMap | undefined; if (isOpenScriptInfo(info)) { (openFilesImpactedByConfigFile ||= new Map()).set(info.path, false); @@ -1779,7 +1783,7 @@ namespace ts.server { let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); if (!configFileExistenceInfo) { // Create the cache - configFileExistenceInfo = { exists: this.host.fileExists(configFileName) }; + configFileExistenceInfo = { exists: (this.fshost || this.host).fileExists(configFileName) }; this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo); } @@ -1836,7 +1840,7 @@ namespace ts.server { if (scriptInfo.isDynamic) return undefined; let searchPath = asNormalizedPath(getDirectoryPath(info.fileName)); - const isSearchPathInProjectRoot = () => containsPath(projectRootPath!, searchPath, this.currentDirectory, !this.host.useCaseSensitiveFileNames); + const isSearchPathInProjectRoot = () => containsPath(projectRootPath!, searchPath, this.currentDirectory, !(this.fshost || this.host).useCaseSensitiveFileNames); // If projectRootPath doesn't contain info.path, then do normal search for config file const anySearchPathOk = !projectRootPath || !isSearchPathInProjectRoot(); @@ -1951,7 +1955,8 @@ namespace ts.server { /** Get a filename if the language service exceeds the maximum allowed program size; otherwise returns undefined. */ private getFilenameForExceededTotalSizeLimitForNonTsFiles(name: string, options: CompilerOptions | undefined, fileNames: T[], propertyReader: FilePropertyReader): string | undefined { - if (options && options.disableSizeLimit || !this.host.getFileSize) { + const fshost = this.fshost || this.host + if (options && options.disableSizeLimit || !fshost.getFileSize) { return; } @@ -1967,12 +1972,12 @@ namespace ts.server { continue; } - totalNonTsFileSize += this.host.getFileSize(fileName); + totalNonTsFileSize += fshost.getFileSize(fileName); if (totalNonTsFileSize > maxProgramSizeForNonTsFiles || totalNonTsFileSize > availableSpace) { const top5LargestFiles = fileNames.map(f => propertyReader.getFileName(f)) .filter(name => !hasTSFileExtension(name)) - .map(name => ({ name, size: this.host.getFileSize!(name) })) + .map(name => ({ name, size: fshost.getFileSize!(name) })) .sort((a, b) => b.size - a.size) .slice(0, 5); this.logger.info(`Non TS file size exceeded limit (${totalNonTsFileSize}). Largest files: ${top5LargestFiles.map(file => `${file.name}:${file.size}`).join(", ")}`); @@ -2074,8 +2079,9 @@ namespace ts.server { configFileExistenceInfo.exists = true; } if (!configFileExistenceInfo.config) { + const fshost = this.fshost || this.host; configFileExistenceInfo.config = { - cachedDirectoryStructureHost: createCachedDirectoryStructureHost(this.host, this.host.getCurrentDirectory(), this.host.useCaseSensitiveFileNames)!, + cachedDirectoryStructureHost: createCachedDirectoryStructureHost(fshost, fshost.getCurrentDirectory(), fshost.useCaseSensitiveFileNames)!, projects: new Map(), reloadLevel: ConfigFileProgramReloadLevel.Full }; @@ -2175,11 +2181,12 @@ namespace ts.server { } // Parse the config file and ensure its cached + const fshost = this.fshost || this.host; const cachedDirectoryStructureHost = configFileExistenceInfo.config?.cachedDirectoryStructureHost || - createCachedDirectoryStructureHost(this.host, this.host.getCurrentDirectory(), this.host.useCaseSensitiveFileNames)!; + createCachedDirectoryStructureHost(fshost, fshost.getCurrentDirectory(), fshost.useCaseSensitiveFileNames)!; // Read updated contents from disk - const configFileContent = tryReadFile(configFilename, fileName => this.host.readFile(fileName)); + const configFileContent = tryReadFile(configFilename, fileName => fshost.readFile(fileName)); const configFile = parseJsonText(configFilename, isString(configFileContent) ? configFileContent : "") as TsConfigSourceFile; const configFileErrors = configFile.parseDiagnostics as Diagnostic[]; if (!isString(configFileContent)) configFileErrors.push(configFileContent); @@ -2474,11 +2481,12 @@ namespace ts.server { // we don't have an explicit root path, so we should try to find an inferred project // that more closely contains the file. let bestMatch: InferredProject | undefined; + const fshost = this.fshost || this.host; for (const project of this.inferredProjects) { // ignore single inferred projects (handled elsewhere) if (!project.projectRootPath) continue; // ignore inferred projects that don't contain the root's path - if (!containsPath(project.projectRootPath, info.path, this.host.getCurrentDirectory(), !this.host.useCaseSensitiveFileNames)) continue; + if (!containsPath(project.projectRootPath, info.path, fshost.getCurrentDirectory(), !fshost.useCaseSensitiveFileNames)) continue; // ignore inferred projects that are higher up in the project root. // TODO(rbuckton): Should we add the file as a root to these as well? if (bestMatch && bestMatch.projectRootPath!.length > project.projectRootPath.length) continue; @@ -2623,7 +2631,7 @@ namespace ts.server { (!this.globalCacheLocationDirectoryPath || !startsWith(info.path, this.globalCacheLocationDirectoryPath))) { const indexOfNodeModules = info.path.indexOf("/node_modules/"); - if (!this.host.getModifiedTime || indexOfNodeModules === -1) { + if (!(this.fshost || this.host).getModifiedTime || indexOfNodeModules === -1) { info.fileWatcher = this.watchFactory.watchFile( info.fileName, (_fileName, eventKind) => this.onSourceFileChanged(info, eventKind), @@ -2721,7 +2729,7 @@ namespace ts.server { } private getModifiedTime(info: ScriptInfo) { - return (this.host.getModifiedTime!(info.path) || missingFileModifiedTime).getTime(); + return ((this.fshost || this.host).getModifiedTime!(info.path) || missingFileModifiedTime).getTime(); } private refreshScriptInfo(info: ScriptInfo) { @@ -2784,10 +2792,10 @@ namespace ts.server { Debug.assert(!isRootedDiskPath(fileName) || this.currentDirectory === currentDirectory || !this.openFilesWithNonRootedDiskPath.has(this.toCanonicalFileName(fileName)), "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nOpen script files with non rooted disk path opened with current directory context cannot have same canonical names`); Debug.assert(!isDynamic || this.currentDirectory === currentDirectory || this.useInferredProjectPerProjectRoot, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.`); // If the file is not opened by client and the file doesnot exist on the disk, return - if (!openedByClient && !isDynamic && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) { + if (!openedByClient && !isDynamic && !(hostToQueryFileExistsOn || this.fshost || this.host).fileExists(fileName)) { return; } - info = new ScriptInfo(this.host, fileName, scriptKind!, !!hasMixedContent, path, this.filenameToScriptInfoVersion.get(path)); // TODO: GH#18217 + info = new ScriptInfo(this.fshost || this.host, fileName, scriptKind!, !!hasMixedContent, path, this.filenameToScriptInfoVersion.get(path)); // TODO: GH#18217 this.filenameToScriptInfo.set(info.path, info); this.filenameToScriptInfoVersion.delete(info.path); if (!openedByClient) { @@ -2825,7 +2833,7 @@ namespace ts.server { /*@internal*/ getDocumentPositionMapper(project: Project, generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined { // Since declaration info and map file watches arent updating project's directory structure host (which can cache file structure) use host - const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.host); + const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.fshost || this.host); if (!declarationInfo) { if (sourceFileName) { // Project contains source file and it generates the generated file name @@ -2862,7 +2870,7 @@ namespace ts.server { let mapFileNameFromDeclarationInfo: string | undefined; let readMapFile: ReadMapFile | undefined = (mapFileName, mapFileNameFromDts) => { - const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(mapFileName, project.currentDirectory, this.host); + const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(mapFileName, project.currentDirectory, this.fshost || this.host); if (!mapInfo) { mapFileNameFromDeclarationInfo = mapFileNameFromDts; return undefined; @@ -2941,7 +2949,7 @@ namespace ts.server { } // Need to look for other files. - const info = this.getOrCreateScriptInfoNotOpenedByClient(fileName, (project || this).currentDirectory, project ? project.directoryStructureHost : this.host); + const info = this.getOrCreateScriptInfoNotOpenedByClient(fileName, (project || this).currentDirectory, project ? project.directoryStructureHost : (this.fshost || this.host)); if (!info) return undefined; // Attach as source @@ -3062,7 +3070,7 @@ namespace ts.server { if (this.openFiles.has(info.path)) return; // Skip open files if (!info.fileWatcher) return; // not watched file // Handle as if file is changed or deleted - this.onSourceFileChanged(info, this.host.fileExists(info.fileName) ? FileWatcherEventKind.Changed : FileWatcherEventKind.Deleted); + this.onSourceFileChanged(info, (this.fshost || this.host).fileExists(info.fileName) ? FileWatcherEventKind.Changed : FileWatcherEventKind.Deleted); }); // Cancel all project updates since we will be updating them now this.pendingProjectUpdates.forEach((_project, projectName) => { @@ -3235,7 +3243,7 @@ namespace ts.server { const { fileName } = originalLocation; const scriptInfo = this.getScriptInfo(fileName); - if (!scriptInfo && !this.host.fileExists(fileName)) return undefined; + if (!scriptInfo && !(this.fshost || this.host).fileExists(fileName)) return undefined; const originalFileInfo: OriginalFileInfo = { fileName: toNormalizedPath(fileName), path: this.toPath(fileName) }; const configFileName = this.getConfigFileNameForFile(originalFileInfo); @@ -3307,7 +3315,7 @@ namespace ts.server { /** @internal */ fileExists(fileName: NormalizedPath): boolean { - return !!this.getScriptInfoForNormalizedPath(fileName) || this.host.fileExists(fileName); + return !!this.getScriptInfoForNormalizedPath(fileName) || (this.fshost || this.host).fileExists(fileName); } private findExternalProjectContainingOpenScriptInfo(info: ScriptInfo): ExternalProject | undefined { @@ -3689,8 +3697,8 @@ namespace ts.server { /* @internal */ updateFileSystem(createdFiles: Iterator | undefined, updatedFiles?: Iterator, deletedFiles?: string[]) { - // TODO: Not sure it's OK to use protocol.FileSystemRequestArgs here - const fs = this.host as TestFSWithWatch.VirtualServerHost; + Debug.assert(this.fshost); + const fs = this.fshost as TestFSWithWatch.VirtualServerHost; if (createdFiles) { let it; while (!(it = createdFiles.next()).done) { @@ -3987,7 +3995,7 @@ namespace ts.server { for (const file of proj.rootFiles) { const normalized = toNormalizedPath(file.fileName); if (getBaseConfigFileName(normalized)) { - if (this.serverMode === LanguageServiceMode.Semantic && this.host.fileExists(normalized)) { + if (this.serverMode === LanguageServiceMode.Semantic && (this.fshost || this.host).fileExists(normalized)) { (tsConfigFiles || (tsConfigFiles = [])).push(normalized); } } @@ -4142,7 +4150,7 @@ namespace ts.server { case Ternary.True: return directory; case Ternary.False: return undefined; case Ternary.Maybe: - return this.host.fileExists(combinePaths(directory, "package.json")) + return (this.fshost || this.host).fileExists(combinePaths(directory, "package.json")) ? directory : undefined; } diff --git a/src/server/packageJsonCache.ts b/src/server/packageJsonCache.ts index 3aa6129430a..e79169588ab 100644 --- a/src/server/packageJsonCache.ts +++ b/src/server/packageJsonCache.ts @@ -42,7 +42,7 @@ namespace ts.server { }; function addOrUpdate(fileName: Path) { - const packageJsonInfo = Debug.checkDefined(createPackageJsonInfo(fileName, host.host)); + const packageJsonInfo = Debug.checkDefined(createPackageJsonInfo(fileName, (host.fshost || host.host))); packageJsons.set(fileName, packageJsonInfo); directoriesWithoutPackageJson.delete(getDirectoryPath(fileName)); } diff --git a/src/server/project.ts b/src/server/project.ts index cabeb6d1e62..7653eeedccb 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -229,8 +229,8 @@ namespace ts.server { return hasOneOrMoreJsAndNoTsFiles(this); } - public static resolveModule(moduleName: string, initialDir: string, host: ServerHost, log: (message: string) => void, logErrors?: (message: string) => void): {} | undefined { - const resolvedPath = normalizeSlashes(host.resolvePath(combinePaths(initialDir, "node_modules"))); + public static resolveModule(moduleName: string, initialDir: string, host: ServerHost, fshost: ServerHost, log: (message: string) => void, logErrors?: (message: string) => void): {} | undefined { + const resolvedPath = normalizeSlashes(fshost.resolvePath(combinePaths(initialDir, "node_modules"))); log(`Loading ${moduleName} from ${initialDir} (resolved to ${resolvedPath})`); const result = host.require!(resolvedPath, moduleName); // TODO: GH#18217 if (result.error) { @@ -312,13 +312,14 @@ namespace ts.server { this.setInternalCompilerOptionsForEmittingJsFiles(); const host = this.projectService.host; + const fshost = this.projectService.fshost || this.projectService.host; if (this.projectService.logger.loggingEnabled()) { this.trace = s => this.writeLog(s); } else if (host.trace) { this.trace = s => host.trace!(s); } - this.realpath = maybeBind(host, host.realpath); + this.realpath = maybeBind(fshost, fshost.realpath); // Use the current directory as resolution root only if the project created using current directory string this.resolutionCache = createResolutionCache( @@ -450,19 +451,19 @@ namespace ts.server { } useCaseSensitiveFileNames() { - return this.projectService.host.useCaseSensitiveFileNames; + return (this.projectService.fshost || this.projectService.host).useCaseSensitiveFileNames; } readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { - return this.directoryStructureHost.readDirectory!(path, extensions, exclude, include, depth); + return this.directoryStructureHost.readDirectory!(path, extensions, exclude, include, depth) } readFile(fileName: string): string | undefined { - return this.projectService.host.readFile(fileName); + return (this.projectService.fshost || this.projectService.host).readFile(fileName); } writeFile(fileName: string, content: string): void { - return this.projectService.host.writeFile(fileName, content); + return (this.projectService.fshost || this.projectService.host).writeFile(fileName, content); } fileExists(file: string): boolean { @@ -1598,7 +1599,7 @@ namespace ts.server { (errorLogs || (errorLogs = [])).push(message); }; const resolvedModule = firstDefined(searchPaths, searchPath => - Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, log, logError) as PluginModuleFactory | undefined); + Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, this.projectService.fshost || this.projectService.host, log, logError) as PluginModuleFactory | undefined); if (resolvedModule) { const configurationOverride = pluginConfigOverrides && pluginConfigOverrides.get(pluginConfigEntry.name); if (configurationOverride) { @@ -1622,13 +1623,13 @@ namespace ts.server { this.projectService.logger.info(`Skipped loading plugin ${configEntry.name} because it did not expose a proper factory function`); return; } - + // TODO: serverHost very likely needs to be fshost, or an object that defaults to fshost const info: PluginCreateInfo = { config: configEntry, project: this, languageService: this.languageService, languageServiceHost: this, - serverHost: this.projectService.host, + serverHost: this.projectService.fshost || this.projectService.host, session: this.projectService.session }; @@ -1716,17 +1717,19 @@ namespace ts.server { /*@internal*/ getModuleResolutionHostForAutoImportProvider(): ModuleResolutionHost { if (this.program) { + const fshost = this.projectService.fshost || this.projectService.host; return { fileExists: this.program.fileExists, directoryExists: this.program.directoryExists, - realpath: this.program.realpath || this.projectService.host.realpath?.bind(this.projectService.host), + realpath: this.program.realpath || fshost.realpath?.bind(fshost), getCurrentDirectory: this.getCurrentDirectory.bind(this), - readFile: this.projectService.host.readFile.bind(this.projectService.host), - getDirectories: this.projectService.host.getDirectories.bind(this.projectService.host), + readFile: fshost.readFile.bind(fshost), + getDirectories: fshost.getDirectories.bind(fshost), trace: this.projectService.host.trace?.bind(this.projectService.host), useCaseSensitiveFileNames: this.program.useCaseSensitiveFileNames(), }; } + // TODO: Not sure whether this needs to be fshost or host or an object that can fall back fshost->host return this.projectService.host; } @@ -1864,7 +1867,7 @@ namespace ts.server { compilerOptions, /*compileOnSaveEnabled*/ false, watchOptions, - projectService.host, + projectService.fshost || projectService.host, currentDirectory); this.typeAcquisition = typeAcquisition; this.projectRootPath = projectRootPath && projectService.toCanonicalFileName(projectRootPath); @@ -2092,7 +2095,7 @@ namespace ts.server { compilerOptions, /*compileOnSaveEnabled*/ false, hostProject.getWatchOptions(), - hostProject.projectService.host, + hostProject.projectService.fshost || hostProject.projectService.host, hostProject.currentDirectory); this.rootFileNames = initialRootNames; @@ -2517,7 +2520,7 @@ namespace ts.server { compilerOptions, compileOnSaveEnabled, watchOptions, - projectService.host, + projectService.fshost || projectService.host, getDirectoryPath(projectFilePath || normalizeSlashes(externalProjectName))); this.enableGlobalPlugins(this.getCompilerOptions(), pluginConfigOverrides); } diff --git a/src/server/session.ts b/src/server/session.ts index b04a2a97eba..608cb09462a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -641,6 +641,7 @@ namespace ts.server { export interface SessionOptions { host: ServerHost; + fshost: ServerHost | undefined; cancellationToken: ServerCancellationToken; useSingleInferredProject: boolean; useInferredProjectPerProjectRoot: boolean; @@ -688,9 +689,12 @@ namespace ts.server { private suppressDiagnosticEvents?: boolean; private eventHandler: ProjectServiceEventHandler | undefined; private readonly noGetErrOnBackgroundUpdate?: boolean; + // TODO: Also need to do this in all of project too + private fshost: ServerHost | undefined constructor(opts: SessionOptions) { this.host = opts.host; + this.fshost = opts.fshost; this.cancellationToken = opts.cancellationToken; this.typingsInstaller = opts.typingsInstaller; this.byteLength = opts.byteLength; @@ -716,6 +720,7 @@ namespace ts.server { this.errorCheck = new MultistepOperation(multistepOperationHost); const settings: ProjectServiceOptions = { host: this.host, + fshost: this.fshost, logger: this.logger, cancellationToken: this.cancellationToken, useSingleInferredProject: opts.useSingleInferredProject, @@ -1954,7 +1959,8 @@ namespace ts.server { return args.richResponse ? { emitSkipped: true, diagnostics: [] } : false; } const scriptInfo = project.getScriptInfo(file)!; - const { emitSkipped, diagnostics } = project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark)); + const h = this.fshost || this.host; + const { emitSkipped, diagnostics } = project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => h.writeFile(path, data, writeByteOrderMark)); return args.richResponse ? { emitSkipped, @@ -2653,7 +2659,7 @@ namespace ts.server { } getCanonicalFileName(fileName: string) { - const name = this.host.useCaseSensitiveFileNames ? fileName : toFileNameLowerCase(fileName); + const name = (this.fshost || this.host).useCaseSensitiveFileNames ? fileName : toFileNameLowerCase(fileName); return normalizePath(name); } diff --git a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts index 71ec6573295..c51e398f188 100644 --- a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts +++ b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts @@ -56,7 +56,7 @@ ${file.fileContent}`; it("with updateFileSystem request", () => { // TODO: probably some other watcher tests, not sure what const host = TestFSWithWatch.createVirtualServerHost([], { executingFilePath: "/a/tsc.js" }); - const session = createSession(host); + const session = createVirtualFilesystemSession(host, host); const created = [app, file1, file2, file3, config, lib]; session.executeCommandSeq({ command: protocol.CommandTypes.UpdateFileSystem, diff --git a/src/testRunner/unittests/tsserver/helpers.ts b/src/testRunner/unittests/tsserver/helpers.ts index eb7c7465343..2dd0ca4e478 100644 --- a/src/testRunner/unittests/tsserver/helpers.ts +++ b/src/testRunner/unittests/tsserver/helpers.ts @@ -366,6 +366,7 @@ namespace ts.projectSystem { const sessionOptions: TestSessionOptions = { host, + fshost: undefined, cancellationToken: server.nullCancellationToken, useSingleInferredProject: false, useInferredProjectPerProjectRoot: false, @@ -413,6 +414,31 @@ namespace ts.projectSystem { } } + export function createVirtualFilesystemSession(host: server.ServerHost, fshost: TestFSWithWatch.VirtualServerHost | undefined, opts: Partial = {}) { + if (opts.typingsInstaller === undefined) { + opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); + } + + if (opts.eventHandler !== undefined) { + opts.canUseEvents = true; + } + + const sessionOptions: TestSessionOptions = { + host, + fshost, + cancellationToken: server.nullCancellationToken, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + typingsInstaller: undefined!, // TODO: GH#18217 + byteLength: Utils.byteLength, + hrtime: process.hrtime, + logger: opts.logger || createHasErrorMessageLogger(), + canUseEvents: false + }; + + return new TestSession({ ...sessionOptions, ...opts }); + } + export interface TestProjectServiceOptions extends server.ProjectServiceOptions { logger: Logger; } @@ -422,6 +448,7 @@ namespace ts.projectSystem { typingsInstaller: server.ITypingsInstaller, opts: Partial = {}) { super({ host, + fshost: undefined, // TODO: should provide this for fshost tests logger, session: undefined, cancellationToken, diff --git a/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index 6e4c430f954..ba31b292bd8 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -42,6 +42,7 @@ namespace ts.server { function createSession(): TestSession { const opts: SessionOptions = { host: mockHost, + fshost: undefined, cancellationToken: nullCancellationToken, useSingleInferredProject: false, useInferredProjectPerProjectRoot: false, @@ -465,6 +466,7 @@ namespace ts.server { constructor() { super({ host: mockHost, + fshost: undefined, cancellationToken: nullCancellationToken, useSingleInferredProject: false, useInferredProjectPerProjectRoot: false, @@ -512,6 +514,7 @@ namespace ts.server { constructor() { super({ host: mockHost, + fshost: undefined, cancellationToken: nullCancellationToken, useSingleInferredProject: false, useInferredProjectPerProjectRoot: false, @@ -580,6 +583,7 @@ namespace ts.server { constructor(private client: InProcClient) { super({ host: mockHost, + fshost: undefined, cancellationToken: nullCancellationToken, useSingleInferredProject: false, useInferredProjectPerProjectRoot: false, diff --git a/src/tsserver/nodeServer.ts b/src/tsserver/nodeServer.ts index c85943496e3..b6bcaf131ed 100644 --- a/src/tsserver/nodeServer.ts +++ b/src/tsserver/nodeServer.ts @@ -383,7 +383,7 @@ namespace ts.server { return eventPort !== undefined && !isNaN(eventPort) ? eventPort : undefined; } - function startNodeSession(options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken) { + function startNodeSession(options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, fs: ServerHost) { const childProcess: { fork(modulePath: string, args: string[], options?: { execArgv: string[], env?: MapLike }): NodeChildProcess; } = require("child_process"); @@ -680,7 +680,7 @@ namespace ts.server { this.event(body, eventName); }; - const host = sys as ServerHost; + const host = fs // sys as ServerHost; const typingsInstaller = disableAutomaticTypingAcquisition ? undefined @@ -788,7 +788,7 @@ namespace ts.server { const eventPort: number | undefined = parseEventPort(findArgument("--eventPort")); const typingSafeListLocation = findArgument(Arguments.TypingSafeListLocation)!; // TODO: GH#18217 - const typesMapLocation = findArgument(Arguments.TypesMapLocation) || combinePaths(getDirectoryPath(sys.getExecutingFilePath()), "typesMap.json"); + const typesMapLocation = findArgument(Arguments.TypesMapLocation) || combinePaths(getDirectoryPath(fs.getExecutingFilePath()), "typesMap.json"); const npmLocation = findArgument(Arguments.NpmLocation); const validateDefaultNpmLocation = hasArgument(Arguments.ValidateDefaultNpmLocation); const disableAutomaticTypingAcquisition = hasArgument("--disableAutomaticTypingAcquisition"); diff --git a/src/tsserver/server.ts b/src/tsserver/server.ts index 036dcf4c0ea..39af0f4df16 100644 --- a/src/tsserver/server.ts +++ b/src/tsserver/server.ts @@ -28,9 +28,9 @@ namespace ts.server { cancellationToken: ServerCancellationToken; serverMode: LanguageServiceMode | undefined; unknownServerMode?: string; - startSession: (option: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken) => void; + startSession: (option: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, fs: ServerHost) => void; } - function start({ args, logger, cancellationToken, serverMode, unknownServerMode, startSession: startServer }: StartInput, platform: string) { + function start({ args, logger, cancellationToken, serverMode, unknownServerMode, startSession: startServer }: StartInput, platform: string, fs: ServerHost) { const syntaxOnly = hasArgument("--syntaxOnly"); logger.info(`Starting TS Server`); @@ -70,31 +70,24 @@ namespace ts.server { serverMode }, logger, - cancellationToken + cancellationToken, + fs ); } setStackTraceLimit(); + // TODO: Not sure this is right + const fs = findArgument("vfs") ? TestFSWithWatch.createVirtualServerHost([]) : ts.sys as ServerHost // Cannot check process var directory in webworker so has to be typeof check here if (typeof process !== "undefined") { - start(initializeNodeSystem(), require("os").platform()); - } - // TODO: Not sure this is right - else if (findArgument("vfs")) { - // Get args from first message - const listener = (e: any) => { - removeEventListener("message", listener); - const args = e.data; - start(initializeVirtualFileSystem(args), "vfs"); - }; - addEventListener("message", listener); + start(initializeNodeSystem(), require("os").platform(), fs); } else { // Get args from first message const listener = (e: any) => { removeEventListener("message", listener); const args = e.data; - start(initializeWebSystem(args), "web"); + start(initializeWebSystem(args), "web", fs); }; addEventListener("message", listener); } diff --git a/src/tsserver/webServer.ts b/src/tsserver/webServer.ts index 5aac1613134..771dd2d58d1 100644 --- a/src/tsserver/webServer.ts +++ b/src/tsserver/webServer.ts @@ -46,18 +46,6 @@ namespace ts.server { }; } - export function initializeVirtualFileSystem(args: string[]): StartInput { - createVirtualFileSystem(); - return { - args, - logger: createLogger(), - cancellationToken: nullCancellationToken, - // Only semantic mode right now - serverMode: LanguageServiceMode.Semantic, - startSession: startVirtualFileSystemSession - }; - } - function createLogger() { const cmdLineVerbosity = getLogLevel(findArgument("--logVerbosity")); return cmdLineVerbosity !== undefined ? new MainProcessLogger(cmdLineVerbosity, { writeMessage }) : nullLogger; @@ -93,19 +81,6 @@ namespace ts.server { } } - - function createVirtualFileSystem() { - Debug.assert(sys === undefined); - // TODO: I don't think I need the XMLHttpRequest code since vfs will get its files from messages sent by the owner - // ...but this means I may not need the webSession-copied code in startVirtualFileSystemSession - const vfshost = TestFSWithWatch.createVirtualServerHost([]); - setSys(vfshost); - const localeStr = findArgument("--locale"); - if (localeStr) { - validateLocaleAndSetLanguage(localeStr, sys); - } - } - function hrtime(previous?: [number, number]) { const now = self.performance.now() * 1e-3; let seconds = Math.floor(now); @@ -121,9 +96,12 @@ namespace ts.server { return [seconds, nanoseconds]; } - function startWebSession(options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken) { + // TODO: Maybe should leave as ServerHost cast below and declare fs: System + // TODO: host is probably still a better name + function startWebSession(options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, fs: ServerHost) { class WorkerSession extends server.WorkerSession { constructor() { + // TODO: Not really sure this is the way to do it super(sys as ServerHost, { writeMessage }, options, logger, cancellationToken, hrtime); } @@ -145,28 +123,4 @@ namespace ts.server { // Start listening session.listen(); } - - function startVirtualFileSystemSession(options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken) { - class WorkerSession extends server.WorkerSession { - constructor() { - super(sys as ServerHost, { writeMessage }, options, logger, cancellationToken, hrtime); - } - - exit() { - this.logger.info("Exiting..."); - this.projectService.closeLog(); - close(); - } - - listen() { - addEventListener("message", (message: any) => { - this.onMessage(message.data); - }); - } - } - const session = new WorkerSession(); - - // Start listening - session.listen(); - } } diff --git a/src/typingsInstaller/nodeTypingsInstaller.ts b/src/typingsInstaller/nodeTypingsInstaller.ts index d2545c13f2f..d6c06aac850 100644 --- a/src/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/typingsInstaller/nodeTypingsInstaller.ts @@ -84,6 +84,7 @@ namespace ts.server.typingsInstaller { private delayedInitializationError: InitializationFailedResponse | undefined; constructor(globalTypingsCacheLocation: string, typingSafeListLocation: string, typesMapLocation: string, npmLocation: string | undefined, validateDefaultNpmLocation: boolean, throttleLimit: number, log: Log) { + // TODO: This just uses sys, I'm not sure how to circumvent that super( sys, globalTypingsCacheLocation, diff --git a/src/webServer/webServer.ts b/src/webServer/webServer.ts index 31f0a47915b..eb50071d0dd 100644 --- a/src/webServer/webServer.ts +++ b/src/webServer/webServer.ts @@ -181,6 +181,7 @@ namespace ts.server { constructor(host: ServerHost, private webHost: HostWithWriteMessage, options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, hrtime: SessionOptions["hrtime"]) { super({ host, + fshost: undefined, // TODO: Provide this? Check Sheetal's comments. cancellationToken, ...options, typingsInstaller: nullTypingsInstaller,