diff --git a/Jakefile.js b/Jakefile.js index 2026544a249bc..fa600250649c6 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -1199,23 +1199,12 @@ task("update-sublime", ["local", serverFile], function () { }); var tslintRuleDir = "scripts/tslint/rules"; -var tslintRules = [ - "booleanTriviaRule", - "debugAssertRule", - "nextLineRule", - "noBomRule", - "noDoubleSpaceRule", - "noIncrementDecrementRule", - "noInOperatorRule", - "noTypeAssertionWhitespaceRule", - "objectLiteralSurroundingSpaceRule", - "typeOperatorSpacingRule", -]; +var tslintRules = fs.readdirSync(tslintRuleDir); var tslintRulesFiles = tslintRules.map(function (p) { - return path.join(tslintRuleDir, p + ".ts"); + return path.join(tslintRuleDir, p); }); var tslintRulesOutFiles = tslintRules.map(function (p) { - return path.join(builtLocalDirectory, "tslint/rules", p + ".js"); + return path.join(builtLocalDirectory, "tslint/rules", p.replace(".ts", ".js")); }); var tslintFormattersDir = "scripts/tslint/formatters"; var tslintFormatters = [ diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 7e44a9608f0ca..886a2194bd9d4 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -1,511 +1,581 @@ -/// +/// +/*@internal*/ namespace ts { - export interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - } - - export interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } -} - -/* @internal */ -namespace ts { - export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, - cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { - const outputFiles: OutputFile[] = []; - const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); - return { outputFiles, emitSkipped: emitResult.emitSkipped }; - - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { - outputFiles.push({ name: fileName, writeByteOrderMark, text }); - } - } - - export interface Builder { - /** Called to inform builder about new program */ - updateProgram(newProgram: Program): void; - - /** Gets the files affected by the file path */ - getFilesAffectedBy(program: Program, path: Path): ReadonlyArray; - - /** Emit the changed files and clear the cache of the changed files */ - emitChangedFiles(program: Program, writeFileCallback: WriteFileCallback): ReadonlyArray; - - /** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */ - getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): ReadonlyArray; - - /** Called to reset the status of the builder */ - clear(): void; - } - - interface EmitHandler { + /** + * State to store the changed files, affected files and cache semantic diagnostics + */ + export interface BuilderProgramState extends BuilderState { /** - * Called when sourceFile is added to the program + * Cache of semantic diagnostics for files with their Path being the key */ - onAddSourceFile(program: Program, sourceFile: SourceFile): void; + semanticDiagnosticsPerFile: Map> | undefined; /** - * Called when sourceFile is removed from the program + * The map has key by source file's path that has been changed */ - onRemoveSourceFile(path: Path): void; + changedFilesSet: Map; /** - * For all source files, either "onUpdateSourceFile" or "onUpdateSourceFileWithSameVersion" will be called. - * If the builder is sure that the source file needs an update, "onUpdateSourceFile" will be called; - * otherwise "onUpdateSourceFileWithSameVersion" will be called. + * Set of affected files being iterated */ - onUpdateSourceFile(program: Program, sourceFile: SourceFile): void; + affectedFiles: ReadonlyArray | undefined; /** - * For all source files, either "onUpdateSourceFile" or "onUpdateSourceFileWithSameVersion" will be called. - * If the builder is sure that the source file needs an update, "onUpdateSourceFile" will be called; - * otherwise "onUpdateSourceFileWithSameVersion" will be called. - * This function should return whether the source file should be marked as changed (meaning that something associated with file has changed, e.g. module resolution) + * Current index to retrieve affected file from */ - onUpdateSourceFileWithSameVersion(program: Program, sourceFile: SourceFile): boolean; + affectedFilesIndex: number | undefined; /** - * Gets the files affected by the script info which has updated shape from the known one + * Current changed file for iterating over affected files */ - getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray; - } - - interface FileInfo { - version: string; - signature: string; - } - - export interface BuilderOptions { - getCanonicalFileName: GetCanonicalFileName; - computeHash: (data: string) => string; + currentChangedFilePath: Path | undefined; + /** + * Map of file signatures, with key being file path, calculated while getting current changed file's affected files + * These will be commited whenever the iteration through affected files of current changed file is complete + */ + currentAffectedFilesSignatures: Map | undefined; + /** + * Already seen affected files + */ + seenAffectedFiles: Map | undefined; + /** + * program corresponding to this state + */ + program: Program; } - export function createBuilder(options: BuilderOptions): Builder { - let isModuleEmit: boolean | undefined; - const fileInfos = createMap(); - const semanticDiagnosticsPerFile = createMap>(); - /** The map has key by source file's path that has been changed */ - const changedFilesSet = createMap(); - const hasShapeChanged = createMap(); - let allFilesExcludingDefaultLibraryFile: ReadonlyArray | undefined; - let emitHandler: EmitHandler; - return { - updateProgram, - getFilesAffectedBy, - emitChangedFiles, - getSemanticDiagnostics, - clear - }; - - function createProgramGraph(program: Program) { - const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None; - if (isModuleEmit !== currentIsModuleEmit) { - isModuleEmit = currentIsModuleEmit; - emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler(); - fileInfos.clear(); - semanticDiagnosticsPerFile.clear(); - } - hasShapeChanged.clear(); - allFilesExcludingDefaultLibraryFile = undefined; - mutateMap( - fileInfos, - arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path), - { - // Add new file info - createNewValue: (_path, sourceFile) => addNewFileInfo(program, sourceFile), - // Remove existing file info - onDeleteValue: removeExistingFileInfo, - // We will update in place instead of deleting existing value and adding new one - onExistingValue: (existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile) - } - ); + function hasSameKeys(map1: ReadonlyMap | undefined, map2: ReadonlyMap | undefined) { + if (map1 === undefined) { + return map2 === undefined; } - - function registerChangedFile(path: Path) { - changedFilesSet.set(path, true); - // All changed files need to re-evaluate its semantic diagnostics - semanticDiagnosticsPerFile.delete(path); - } - - function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo { - registerChangedFile(sourceFile.path); - emitHandler.onAddSourceFile(program, sourceFile); - return { version: sourceFile.version, signature: undefined }; + if (map2 === undefined) { + return map1 === undefined; } + // Has same size and every key is present in both maps + return map1.size === map2.size && !forEachKey(map1, key => !map2.has(key)); + } - function removeExistingFileInfo(_existingFileInfo: FileInfo, path: Path) { - // Since we dont need to track removed file as changed file - // We can just remove its diagnostics - changedFilesSet.delete(path); - semanticDiagnosticsPerFile.delete(path); - emitHandler.onRemoveSourceFile(path); + /** + * Create the state so that we can iterate on changedFiles/affected files + */ + function createBuilderProgramState(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly): BuilderProgramState { + const state = BuilderState.create(newProgram, getCanonicalFileName, oldState) as BuilderProgramState; + state.program = newProgram; + const compilerOptions = newProgram.getCompilerOptions(); + if (!compilerOptions.outFile && !compilerOptions.out) { + state.semanticDiagnosticsPerFile = createMap>(); } - - function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile) { - if (existingInfo.version !== sourceFile.version) { - registerChangedFile(sourceFile.path); - existingInfo.version = sourceFile.version; - emitHandler.onUpdateSourceFile(program, sourceFile); + state.changedFilesSet = createMap(); + const useOldState = BuilderState.canReuseOldState(state.referencedMap, oldState); + const canCopySemanticDiagnostics = useOldState && oldState.semanticDiagnosticsPerFile && !!state.semanticDiagnosticsPerFile; + if (useOldState) { + // Verify the sanity of old state + if (!oldState.currentChangedFilePath) { + Debug.assert(!oldState.affectedFiles && (!oldState.currentAffectedFilesSignatures || !oldState.currentAffectedFilesSignatures.size), "Cannot reuse if only few affected files of currentChangedFile were iterated"); } - else if (emitHandler.onUpdateSourceFileWithSameVersion(program, sourceFile)) { - registerChangedFile(sourceFile.path); + if (canCopySemanticDiagnostics) { + Debug.assert(!forEachKey(oldState.changedFilesSet, path => oldState.semanticDiagnosticsPerFile.has(path)), "Semantic diagnostics shouldnt be available for changed files"); } - } - function ensureProgramGraph(program: Program) { - if (!emitHandler) { - createProgramGraph(program); - } + // Copy old state's changed files set + copyEntries(oldState.changedFilesSet, state.changedFilesSet); } - function updateProgram(newProgram: Program) { - if (emitHandler) { - createProgramGraph(newProgram); + // Update changed files and copy semantic diagnostics if we can + const referencedMap = state.referencedMap; + const oldReferencedMap = useOldState && oldState.referencedMap; + state.fileInfos.forEach((info, sourceFilePath) => { + let oldInfo: Readonly; + let newReferences: BuilderState.ReferencedSet; + + // if not using old state, every file is changed + if (!useOldState || + // File wasnt present in old state + !(oldInfo = oldState.fileInfos.get(sourceFilePath)) || + // versions dont match + oldInfo.version !== info.version || + // Referenced files changed + !hasSameKeys(newReferences = referencedMap && referencedMap.get(sourceFilePath), oldReferencedMap && oldReferencedMap.get(sourceFilePath)) || + // Referenced file was deleted in the new program + newReferences && forEachKey(newReferences, path => !state.fileInfos.has(path) && oldState.fileInfos.has(path))) { + // Register file as changed file and do not copy semantic diagnostics, since all changed files need to be re-evaluated + state.changedFilesSet.set(sourceFilePath, true); } - } + else if (canCopySemanticDiagnostics) { + // Unchanged file copy diagnostics + const diagnostics = oldState.semanticDiagnosticsPerFile.get(sourceFilePath); + if (diagnostics) { + state.semanticDiagnosticsPerFile.set(sourceFilePath, diagnostics); + } + } + }); - function getFilesAffectedBy(program: Program, path: Path): ReadonlyArray { - ensureProgramGraph(program); + return state; + } - const sourceFile = program.getSourceFileByPath(path); - if (!sourceFile) { - return emptyArray; - } + /** + * Verifies that source file is ok to be used in calls that arent handled by next + */ + function assertSourceFileOkWithoutNextAffectedCall(state: BuilderProgramState, sourceFile: SourceFile | undefined) { + Debug.assert(!sourceFile || !state.affectedFiles || state.affectedFiles[state.affectedFilesIndex - 1] !== sourceFile || !state.semanticDiagnosticsPerFile.has(sourceFile.path)); + } - if (!updateShapeSignature(program, sourceFile)) { - return [sourceFile]; - } - return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile); - } + /** + * This function returns the next affected file to be processed. + * Note that until doneAffected is called it would keep reporting same result + * This is to allow the callers to be able to actually remove affected file only when the operation is complete + * eg. if during diagnostics check cancellation token ends up cancelling the request, the affected file should be retained + */ + function getNextAffectedFile(state: BuilderProgramState, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash): SourceFile | Program | undefined { + while (true) { + const { affectedFiles } = state; + if (affectedFiles) { + const { seenAffectedFiles, semanticDiagnosticsPerFile } = state; + let { affectedFilesIndex } = state; + while (affectedFilesIndex < affectedFiles.length) { + const affectedFile = affectedFiles[affectedFilesIndex]; + if (!seenAffectedFiles.has(affectedFile.path)) { + // Set the next affected file as seen and remove the cached semantic diagnostics + state.affectedFilesIndex = affectedFilesIndex; + semanticDiagnosticsPerFile.delete(affectedFile.path); + return affectedFile; + } + seenAffectedFiles.set(affectedFile.path, true); + affectedFilesIndex++; + } - function emitChangedFiles(program: Program, writeFileCallback: WriteFileCallback): ReadonlyArray { - ensureProgramGraph(program); - const compilerOptions = program.getCompilerOptions(); + // Remove the changed file from the change set + state.changedFilesSet.delete(state.currentChangedFilePath); + state.currentChangedFilePath = undefined; + // Commit the changes in file signature + BuilderState.updateSignaturesFromCache(state, state.currentAffectedFilesSignatures); + state.currentAffectedFilesSignatures.clear(); + state.affectedFiles = undefined; + } - if (!changedFilesSet.size) { - return emptyArray; + // Get next changed file + const nextKey = state.changedFilesSet.keys().next(); + if (nextKey.done) { + // Done + return undefined; } - // With --out or --outFile all outputs go into single file, do it only once + // With --out or --outFile all outputs go into single file + // so operations are performed directly on program, return program + const compilerOptions = state.program.getCompilerOptions(); if (compilerOptions.outFile || compilerOptions.out) { - Debug.assert(semanticDiagnosticsPerFile.size === 0); - changedFilesSet.clear(); - return [program.emit(/*targetSourceFile*/ undefined, writeFileCallback)]; + Debug.assert(!state.semanticDiagnosticsPerFile); + return state.program; } - const seenFiles = createMap(); - let result: EmitResult[] | undefined; - changedFilesSet.forEach((_true, path) => { - // Get the affected Files by this program - const affectedFiles = getFilesAffectedBy(program, path as Path); - affectedFiles.forEach(affectedFile => { - // Affected files shouldnt have cached diagnostics - semanticDiagnosticsPerFile.delete(affectedFile.path); - - if (!seenFiles.has(affectedFile.path)) { - seenFiles.set(affectedFile.path, true); - - // Emit the affected file - (result || (result = [])).push(program.emit(affectedFile, writeFileCallback)); - } - }); - }); - changedFilesSet.clear(); - return result || emptyArray; + // Get next batch of affected files + state.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures || createMap(); + state.affectedFiles = BuilderState.getFilesAffectedBy(state, state.program, nextKey.value as Path, cancellationToken, computeHash, state.currentAffectedFilesSignatures); + state.currentChangedFilePath = nextKey.value as Path; + state.semanticDiagnosticsPerFile.delete(nextKey.value as Path); + state.affectedFilesIndex = 0; + state.seenAffectedFiles = state.seenAffectedFiles || createMap(); } + } - function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): ReadonlyArray { - ensureProgramGraph(program); - Debug.assert(changedFilesSet.size === 0); + /** + * This is called after completing operation on the next affected file. + * The operations here are postponed to ensure that cancellation during the iteration is handled correctly + */ + function doneWithAffectedFile(state: BuilderProgramState, affected: SourceFile | Program) { + if (affected === state.program) { + state.changedFilesSet.clear(); + } + else { + state.seenAffectedFiles.set((affected as SourceFile).path, true); + state.affectedFilesIndex++; + } + } - const compilerOptions = program.getCompilerOptions(); - if (compilerOptions.outFile || compilerOptions.out) { - Debug.assert(semanticDiagnosticsPerFile.size === 0); - // We dont need to cache the diagnostics just return them from program - return program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); - } + /** + * Returns the result with affected file + */ + function toAffectedFileResult(state: BuilderProgramState, result: T, affected: SourceFile | Program): AffectedFileResult { + doneWithAffectedFile(state, affected); + return { result, affected }; + } - let diagnostics: Diagnostic[]; - for (const sourceFile of program.getSourceFiles()) { - diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(program, sourceFile, cancellationToken)); - } - return diagnostics || emptyArray; + /** + * Gets the semantic diagnostics either from cache if present, or otherwise from program and caches it + * Note that it is assumed that the when asked about semantic diagnostics, the file has been taken out of affected files/changed file set + */ + function getSemanticDiagnosticsOfFile(state: BuilderProgramState, sourceFile: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray { + const path = sourceFile.path; + const cachedDiagnostics = state.semanticDiagnosticsPerFile.get(path); + // Report the semantic diagnostics from the cache if we already have those diagnostics present + if (cachedDiagnostics) { + return cachedDiagnostics; } - function getSemanticDiagnosticsOfFile(program: Program, sourceFile: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray { - const path = sourceFile.path; - const cachedDiagnostics = semanticDiagnosticsPerFile.get(path); - // Report the semantic diagnostics from the cache if we already have those diagnostics present - if (cachedDiagnostics) { - return cachedDiagnostics; - } + // Diagnostics werent cached, get them from program, and cache the result + const diagnostics = state.program.getSemanticDiagnostics(sourceFile, cancellationToken); + state.semanticDiagnosticsPerFile.set(path, diagnostics); + return diagnostics; + } + + export enum BuilderProgramKind { + SemanticDiagnosticsBuilderProgram, + EmitAndSemanticDiagnosticsBuilderProgram + } + + export interface BuilderCreationParameters { + newProgram: Program; + host: BuilderProgramHost; + oldProgram: BuilderProgram | undefined; + } - // Diagnostics werent cached, get them from program, and cache the result - const diagnostics = program.getSemanticDiagnostics(sourceFile, cancellationToken); - semanticDiagnosticsPerFile.set(path, diagnostics); - return diagnostics; + export function getBuilderCreationParameters(newProgramOrRootNames: Program | ReadonlyArray, hostOrOptions: BuilderProgramHost | CompilerOptions, oldProgramOrHost?: CompilerHost | BuilderProgram, oldProgram?: BuilderProgram): BuilderCreationParameters { + let host: BuilderProgramHost; + let newProgram: Program; + if (isArray(newProgramOrRootNames)) { + newProgram = createProgram(newProgramOrRootNames, hostOrOptions as CompilerOptions, oldProgramOrHost as CompilerHost, oldProgram && oldProgram.getProgram()); + host = oldProgramOrHost as CompilerHost; } + else { + newProgram = newProgramOrRootNames as Program; + host = hostOrOptions as BuilderProgramHost; + oldProgram = oldProgramOrHost as BuilderProgram; + } + return { host, newProgram, oldProgram }; + } - function clear() { - isModuleEmit = undefined; - emitHandler = undefined; - fileInfos.clear(); - semanticDiagnosticsPerFile.clear(); - changedFilesSet.clear(); - hasShapeChanged.clear(); + export function createBuilderProgram(kind: BuilderProgramKind.SemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): SemanticDiagnosticsBuilderProgram; + export function createBuilderProgram(kind: BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): EmitAndSemanticDiagnosticsBuilderProgram; + export function createBuilderProgram(kind: BuilderProgramKind, { newProgram, host, oldProgram }: BuilderCreationParameters) { + // Return same program if underlying program doesnt change + let oldState = oldProgram && oldProgram.getState(); + if (oldState && newProgram === oldState.program) { + newProgram = undefined; + oldState = undefined; + return oldProgram; } /** - * For script files that contains only ambient external modules, although they are not actually external module files, - * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, - * there are no point to rebuild all script files if these special files have changed. However, if any statement - * in the file is not ambient external module, we treat it as a regular script file. + * Create the canonical file name for identity */ - function containsOnlyAmbientModules(sourceFile: SourceFile) { - for (const statement of sourceFile.statements) { - if (!isModuleWithStringLiteralName(statement)) { - return false; - } - } - return true; - } - + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); /** - * @return {boolean} indicates if the shape signature has changed since last update. + * Computing hash to for signature verification */ - function updateShapeSignature(program: Program, sourceFile: SourceFile) { - Debug.assert(!!sourceFile); + const computeHash = host.createHash || identity; + const state = createBuilderProgramState(newProgram, getCanonicalFileName, oldState); + + // To ensure that we arent storing any references to old program or new program without state + newProgram = undefined; + oldProgram = undefined; + oldState = undefined; + + const result: BuilderProgram = { + getState: () => state, + getProgram: () => state.program, + getCompilerOptions: () => state.program.getCompilerOptions(), + getSourceFile: fileName => state.program.getSourceFile(fileName), + getSourceFiles: () => state.program.getSourceFiles(), + getOptionsDiagnostics: cancellationToken => state.program.getOptionsDiagnostics(cancellationToken), + getGlobalDiagnostics: cancellationToken => state.program.getGlobalDiagnostics(cancellationToken), + getSyntacticDiagnostics: (sourceFile, cancellationToken) => state.program.getSyntacticDiagnostics(sourceFile, cancellationToken), + getSemanticDiagnostics, + emit, + getAllDependencies: sourceFile => BuilderState.getAllDependencies(state, state.program, sourceFile), + getCurrentDirectory: () => state.program.getCurrentDirectory() + }; - // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate - if (hasShapeChanged.has(sourceFile.path)) { - return false; - } + if (kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram) { + (result as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; + } + else if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + (result as EmitAndSemanticDiagnosticsBuilderProgram).emitNextAffectedFile = emitNextAffectedFile; + } + else { + notImplemented(); + } - hasShapeChanged.set(sourceFile.path, true); - const info = fileInfos.get(sourceFile.path); - Debug.assert(!!info); + return result; - const prevSignature = info.signature; - let latestSignature: string; - if (sourceFile.isDeclarationFile) { - latestSignature = sourceFile.version; - info.signature = latestSignature; - } - else { - const emitOutput = getFileEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true); - if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { - latestSignature = options.computeHash(emitOutput.outputFiles[0].text); - info.signature = latestSignature; - } - else { - latestSignature = prevSignature; - } + /** + * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + function emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult { + const affected = getNextAffectedFile(state, cancellationToken, computeHash); + if (!affected) { + // Done + return undefined; } - return !prevSignature || latestSignature !== prevSignature; + return toAffectedFileResult( + state, + // When whole program is affected, do emit only once (eg when --out or --outFile is specified) + // Otherwise just affected file + state.program.emit(affected === state.program ? undefined : affected as SourceFile, writeFile || host.writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers), + affected + ); } /** - * Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true + * Emits the JavaScript and declaration files. + * When targetSource file is specified, emits the files corresponding to that source file, + * otherwise for the whole program. + * In case of EmitAndSemanticDiagnosticsBuilderProgram, when targetSourceFile is specified, + * it is assumed that that file is handled from affected file list. If targetSourceFile is not specified, + * it will only emit all the affected files instead of whole program + * + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files */ - function getReferencedFiles(program: Program, sourceFile: SourceFile): Map | undefined { - let referencedFiles: Map | undefined; - - // We need to use a set here since the code can contain the same import twice, - // but that will only be one dependency. - // To avoid invernal conversion, the key of the referencedFiles map must be of type Path - if (sourceFile.imports && sourceFile.imports.length > 0) { - const checker: TypeChecker = program.getTypeChecker(); - for (const importName of sourceFile.imports) { - const symbol = checker.getSymbolAtLocation(importName); - if (symbol && symbol.declarations && symbol.declarations[0]) { - const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]); - if (declarationSourceFile) { - addReferencedFile(declarationSourceFile.path); - } + function emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult { + if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + assertSourceFileOkWithoutNextAffectedCall(state, targetSourceFile); + if (!targetSourceFile) { + // Emit and report any errors we ran into. + let sourceMaps: SourceMapData[] = []; + let emitSkipped: boolean; + let diagnostics: Diagnostic[]; + let emittedFiles: string[] = []; + + let affectedEmitResult: AffectedFileResult; + while (affectedEmitResult = emitNextAffectedFile(writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers)) { + emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped; + diagnostics = addRange(diagnostics, affectedEmitResult.result.diagnostics); + emittedFiles = addRange(emittedFiles, affectedEmitResult.result.emittedFiles); + sourceMaps = addRange(sourceMaps, affectedEmitResult.result.sourceMaps); } + return { + emitSkipped, + diagnostics: diagnostics || emptyArray, + emittedFiles, + sourceMaps + }; } } + return state.program.emit(targetSourceFile, writeFile || host.writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + } - const sourceFileDirectory = getDirectoryPath(sourceFile.path); - // Handle triple slash references - if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { - for (const referencedFile of sourceFile.referencedFiles) { - const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, options.getCanonicalFileName); - addReferencedFile(referencedPath); + /** + * Return the semantic diagnostics for the next affected file or undefined if iteration is complete + * If provided ignoreSourceFile would be called before getting the diagnostics and would ignore the sourceFile if the returned value was true + */ + function getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult> { + while (true) { + const affected = getNextAffectedFile(state, cancellationToken, computeHash); + if (!affected) { + // Done + return undefined; + } + else if (affected === state.program) { + // When whole program is affected, get all semantic diagnostics (eg when --out or --outFile is specified) + return toAffectedFileResult( + state, + state.program.getSemanticDiagnostics(/*targetSourceFile*/ undefined, cancellationToken), + affected + ); } - } - - // Handle type reference directives - if (sourceFile.resolvedTypeReferenceDirectiveNames) { - sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { - if (!resolvedTypeReferenceDirective) { - return; - } - - const fileName = resolvedTypeReferenceDirective.resolvedFileName; - const typeFilePath = toPath(fileName, sourceFileDirectory, options.getCanonicalFileName); - addReferencedFile(typeFilePath); - }); - } - - return referencedFiles; - function addReferencedFile(referencedPath: Path) { - if (!referencedFiles) { - referencedFiles = createMap(); + // Get diagnostics for the affected file if its not ignored + if (ignoreSourceFile && ignoreSourceFile(affected as SourceFile)) { + // Get next affected file + doneWithAffectedFile(state, affected); + continue; } - referencedFiles.set(referencedPath, true); + + return toAffectedFileResult( + state, + getSemanticDiagnosticsOfFile(state, affected as SourceFile, cancellationToken), + affected + ); } } /** - * Gets all files of the program excluding the default library file + * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program + * The semantic diagnostics are cached and managed here + * Note that it is assumed that when asked about semantic diagnostics through this API, + * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics + * In case of SemanticDiagnosticsBuilderProgram if the source file is not provided, + * it will iterate through all the affected files, to ensure that cache stays valid and yet provide a way to get all semantic diagnostics */ - function getAllFilesExcludingDefaultLibraryFile(program: Program, firstSourceFile: SourceFile): ReadonlyArray { - // Use cached result - if (allFilesExcludingDefaultLibraryFile) { - return allFilesExcludingDefaultLibraryFile; + function getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray { + assertSourceFileOkWithoutNextAffectedCall(state, sourceFile); + const compilerOptions = state.program.getCompilerOptions(); + if (compilerOptions.outFile || compilerOptions.out) { + Debug.assert(!state.semanticDiagnosticsPerFile); + // We dont need to cache the diagnostics just return them from program + return state.program.getSemanticDiagnostics(sourceFile, cancellationToken); } - let result: SourceFile[]; - addSourceFile(firstSourceFile); - for (const sourceFile of program.getSourceFiles()) { - if (sourceFile !== firstSourceFile) { - addSourceFile(sourceFile); - } + if (sourceFile) { + return getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken); } - allFilesExcludingDefaultLibraryFile = result || emptyArray; - return allFilesExcludingDefaultLibraryFile; - function addSourceFile(sourceFile: SourceFile) { - if (!program.isSourceFileDefaultLibrary(sourceFile)) { - (result || (result = [])).push(sourceFile); + if (kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram) { + // When semantic builder asks for diagnostics of the whole program, + // ensure that all the affected files are handled + let affected: SourceFile | Program | undefined; + while (affected = getNextAffectedFile(state, cancellationToken, computeHash)) { + doneWithAffectedFile(state, affected); } } - } - function getNonModuleEmitHandler(): EmitHandler { - return { - onAddSourceFile: noop, - onRemoveSourceFile: noop, - onUpdateSourceFile: noop, - onUpdateSourceFileWithSameVersion: returnFalse, - getFilesAffectedByUpdatedShape - }; - - function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray { - const options = program.getCompilerOptions(); - // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, - // so returning the file itself is good enough. - if (options && (options.out || options.outFile)) { - return [sourceFile]; - } - return getAllFilesExcludingDefaultLibraryFile(program, sourceFile); + let diagnostics: Diagnostic[]; + for (const sourceFile of state.program.getSourceFiles()) { + diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken)); } + return diagnostics || emptyArray; } + } +} - function getModuleEmitHandler(): EmitHandler { - const references = createMap>(); - return { - onAddSourceFile: setReferences, - onRemoveSourceFile, - onUpdateSourceFile: updateReferences, - onUpdateSourceFileWithSameVersion: updateReferencesTrackingChangedReferences, - getFilesAffectedByUpdatedShape - }; - - function setReferences(program: Program, sourceFile: SourceFile) { - const newReferences = getReferencedFiles(program, sourceFile); - if (newReferences) { - references.set(sourceFile.path, newReferences); - } - } - - function updateReferences(program: Program, sourceFile: SourceFile) { - const newReferences = getReferencedFiles(program, sourceFile); - if (newReferences) { - references.set(sourceFile.path, newReferences); - } - else { - references.delete(sourceFile.path); - } - } - - function updateReferencesTrackingChangedReferences(program: Program, sourceFile: SourceFile) { - const newReferences = getReferencedFiles(program, sourceFile); - if (!newReferences) { - // Changed if we had references - return references.delete(sourceFile.path); - } - - const oldReferences = references.get(sourceFile.path); - references.set(sourceFile.path, newReferences); - if (!oldReferences || oldReferences.size !== newReferences.size) { - return true; - } +namespace ts { + export type AffectedFileResult = { result: T; affected: SourceFile | Program; } | undefined; - // If there are any new references that werent present previously there is change - return forEachEntry(newReferences, (_true, referencedPath) => !oldReferences.delete(referencedPath)) || - // Otherwise its changed if there are more references previously than now - !!oldReferences.size; - } + export interface BuilderProgramHost { + /** + * return true if file names are treated with case sensitivity + */ + useCaseSensitiveFileNames(): boolean; + /** + * If provided this would be used this hash instead of actual file shape text for detecting changes + */ + createHash?: (data: string) => string; + /** + * When emit or emitNextAffectedFile are called without writeFile, + * this callback if present would be used to write files + */ + writeFile?: WriteFileCallback; + } - function onRemoveSourceFile(removedFilePath: Path) { - // Remove existing references - references.forEach((referencesInFile, filePath) => { - if (referencesInFile.has(removedFilePath)) { - // add files referencing the removedFilePath, as changed files too - const referencedByInfo = fileInfos.get(filePath); - if (referencedByInfo) { - registerChangedFile(filePath as Path); - } - } - }); - // Delete the entry for the removed file path - references.delete(removedFilePath); - } + /** + * Builder to manage the program state changes + */ + export interface BuilderProgram { + /*@internal*/ + getState(): BuilderProgramState; + /** + * Returns current program + */ + getProgram(): Program; + /** + * Get compiler options of the program + */ + getCompilerOptions(): CompilerOptions; + /** + * Get the source file in the program with file name + */ + getSourceFile(fileName: string): SourceFile | undefined; + /** + * Get a list of files in the program + */ + getSourceFiles(): ReadonlyArray; + /** + * Get the diagnostics for compiler options + */ + getOptionsDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Get the diagnostics that dont belong to any file + */ + getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Get the syntax diagnostics, for all source files if source file is not supplied + */ + getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Get all the dependencies of the file + */ + getAllDependencies(sourceFile: SourceFile): ReadonlyArray; + /** + * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program + * The semantic diagnostics are cached and managed here + * Note that it is assumed that when asked about semantic diagnostics through this API, + * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics + * In case of SemanticDiagnosticsBuilderProgram if the source file is not provided, + * it will iterate through all the affected files, to ensure that cache stays valid and yet provide a way to get all semantic diagnostics + */ + getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Emits the JavaScript and declaration files. + * When targetSource file is specified, emits the files corresponding to that source file, + * otherwise for the whole program. + * In case of EmitAndSemanticDiagnosticsBuilderProgram, when targetSourceFile is specified, + * it is assumed that that file is handled from affected file list. If targetSourceFile is not specified, + * it will only emit all the affected files instead of whole program + * + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult; + /** + * Get the current directory of the program + */ + getCurrentDirectory(): string; + } - function getReferencedByPaths(referencedFilePath: Path) { - return arrayFrom(mapDefinedIterator(references.entries(), ([filePath, referencesInFile]) => - referencesInFile.has(referencedFilePath) ? filePath as Path : undefined - )); - } + /** + * The builder that caches the semantic diagnostics for the program and handles the changed files and affected files + */ + export interface SemanticDiagnosticsBuilderProgram extends BuilderProgram { + /** + * Gets the semantic diagnostics from the program for the next affected file and caches it + * Returns undefined if the iteration is complete + */ + getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult>; + } - function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray { - if (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile)) { - return getAllFilesExcludingDefaultLibraryFile(program, sourceFile); - } + /** + * The builder that can handle the changes in program and iterate through changed file to emit the files + * The semantic diagnostics are cached per file and managed by clearing for the changed/affected files + */ + export interface EmitAndSemanticDiagnosticsBuilderProgram extends BuilderProgram { + /** + * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult; + } - const compilerOptions = program.getCompilerOptions(); - if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) { - return [sourceFile]; - } + /** + * Create the builder to manage semantic diagnostics and cache them + */ + export function createSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: SemanticDiagnosticsBuilderProgram): SemanticDiagnosticsBuilderProgram; + export function createSemanticDiagnosticsBuilderProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: SemanticDiagnosticsBuilderProgram): SemanticDiagnosticsBuilderProgram; + export function createSemanticDiagnosticsBuilderProgram(newProgramOrRootNames: Program | ReadonlyArray, hostOrOptions: BuilderProgramHost | CompilerOptions, oldProgramOrHost?: CompilerHost | SemanticDiagnosticsBuilderProgram, oldProgram?: SemanticDiagnosticsBuilderProgram) { + return createBuilderProgram(BuilderProgramKind.SemanticDiagnosticsBuilderProgram, getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, oldProgram)); + } - // Now we need to if each file in the referencedBy list has a shape change as well. - // Because if so, its own referencedBy files need to be saved as well to make the - // emitting result consistent with files on disk. - const seenFileNamesMap = createMap(); - - // Start with the paths this file was referenced by - const path = sourceFile.path; - seenFileNamesMap.set(path, sourceFile); - const queue = getReferencedByPaths(path); - while (queue.length > 0) { - const currentPath = queue.pop(); - if (!seenFileNamesMap.has(currentPath)) { - const currentSourceFile = program.getSourceFileByPath(currentPath); - seenFileNamesMap.set(currentPath, currentSourceFile); - if (currentSourceFile && updateShapeSignature(program, currentSourceFile)) { - queue.push(...getReferencedByPaths(currentPath)); - } - } - } + /** + * Create the builder that can handle the changes in program and iterate through changed files + * to emit the those files and manage semantic diagnostics cache as well + */ + export function createEmitAndSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram): EmitAndSemanticDiagnosticsBuilderProgram; + export function createEmitAndSemanticDiagnosticsBuilderProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram): EmitAndSemanticDiagnosticsBuilderProgram; + export function createEmitAndSemanticDiagnosticsBuilderProgram(newProgramOrRootNames: Program | ReadonlyArray, hostOrOptions: BuilderProgramHost | CompilerOptions, oldProgramOrHost?: CompilerHost | EmitAndSemanticDiagnosticsBuilderProgram, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram) { + return createBuilderProgram(BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, oldProgram)); + } - // Return array of values that needs emit - return arrayFrom(mapDefinedIterator(seenFileNamesMap.values(), value => value)); - } - } + /** + * Creates a builder thats just abstraction over program and can be used with watch + */ + export function createAbstractBuilder(newProgram: Program, host: BuilderProgramHost, oldProgram?: BuilderProgram): BuilderProgram; + export function createAbstractBuilder(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: BuilderProgram): BuilderProgram; + export function createAbstractBuilder(newProgramOrRootNames: Program | ReadonlyArray, hostOrOptions: BuilderProgramHost | CompilerOptions, oldProgramOrHost?: CompilerHost | BuilderProgram, oldProgram?: BuilderProgram): BuilderProgram { + const { newProgram: program } = getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, oldProgram); + return { + // Only return program, all other methods are not implemented + getProgram: () => program, + getState: notImplemented, + getCompilerOptions: notImplemented, + getSourceFile: notImplemented, + getSourceFiles: notImplemented, + getOptionsDiagnostics: notImplemented, + getGlobalDiagnostics: notImplemented, + getSyntacticDiagnostics: notImplemented, + getSemanticDiagnostics: notImplemented, + emit: notImplemented, + getAllDependencies: notImplemented, + getCurrentDirectory: notImplemented + }; } } diff --git a/src/compiler/builderState.ts b/src/compiler/builderState.ts new file mode 100644 index 0000000000000..581aa05f12c16 --- /dev/null +++ b/src/compiler/builderState.ts @@ -0,0 +1,384 @@ +/// +namespace ts { + export interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + } + + export interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; + } +} + +/*@internal*/ +namespace ts { + export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, + cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { + const outputFiles: OutputFile[] = []; + const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + return { outputFiles, emitSkipped: emitResult.emitSkipped }; + + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { + outputFiles.push({ name: fileName, writeByteOrderMark, text }); + } + } + + export interface BuilderState { + /** + * Information of the file eg. its version, signature etc + */ + fileInfos: Map; + /** + * Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled + * Otherwise undefined + * Thus non undefined value indicates, module emit + */ + readonly referencedMap: ReadonlyMap | undefined; + /** + * Map of files that have already called update signature. + * That means hence forth these files are assumed to have + * no change in their signature for this version of the program + */ + hasCalledUpdateShapeSignature: Map; + /** + * Cache of all files excluding default library file for the current program + */ + allFilesExcludingDefaultLibraryFile: ReadonlyArray | undefined; + /** + * Cache of all the file names + */ + allFileNames: ReadonlyArray | undefined; + } +} + +/*@internal*/ +namespace ts.BuilderState { + /** + * Information about the source file: Its version and optional signature from last emit + */ + export interface FileInfo { + readonly version: string; + signature: string | undefined; + } + /** + * Referenced files with values for the keys as referenced file's path to be true + */ + export type ReferencedSet = ReadonlyMap; + /** + * Compute the hash to store the shape of the file + */ + export type ComputeHash = (data: string) => string; + + /** + * Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true + */ + function getReferencedFiles(program: Program, sourceFile: SourceFile, getCanonicalFileName: GetCanonicalFileName): Map | undefined { + let referencedFiles: Map | undefined; + + // We need to use a set here since the code can contain the same import twice, + // but that will only be one dependency. + // To avoid invernal conversion, the key of the referencedFiles map must be of type Path + if (sourceFile.imports && sourceFile.imports.length > 0) { + const checker: TypeChecker = program.getTypeChecker(); + for (const importName of sourceFile.imports) { + const symbol = checker.getSymbolAtLocation(importName); + if (symbol && symbol.declarations && symbol.declarations[0]) { + const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]); + if (declarationSourceFile) { + addReferencedFile(declarationSourceFile.path); + } + } + } + } + + const sourceFileDirectory = getDirectoryPath(sourceFile.path); + // Handle triple slash references + if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { + for (const referencedFile of sourceFile.referencedFiles) { + const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName); + addReferencedFile(referencedPath); + } + } + + // Handle type reference directives + if (sourceFile.resolvedTypeReferenceDirectiveNames) { + sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { + if (!resolvedTypeReferenceDirective) { + return; + } + + const fileName = resolvedTypeReferenceDirective.resolvedFileName; + const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName); + addReferencedFile(typeFilePath); + }); + } + + return referencedFiles; + + function addReferencedFile(referencedPath: Path) { + if (!referencedFiles) { + referencedFiles = createMap(); + } + referencedFiles.set(referencedPath, true); + } + } + + /** + * Returns true if oldState is reusable, that is the emitKind = module/non module has not changed + */ + export function canReuseOldState(newReferencedMap: ReadonlyMap, oldState: Readonly | undefined) { + return oldState && !oldState.referencedMap === !newReferencedMap; + } + + /** + * Creates the state of file references and signature for the new program from oldState if it is safe + */ + export function create(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly): BuilderState { + const fileInfos = createMap(); + const referencedMap = newProgram.getCompilerOptions().module !== ModuleKind.None ? createMap() : undefined; + const hasCalledUpdateShapeSignature = createMap(); + const useOldState = canReuseOldState(referencedMap, oldState); + + // Create the reference map, and set the file infos + for (const sourceFile of newProgram.getSourceFiles()) { + const version = sourceFile.version; + const oldInfo = useOldState && oldState.fileInfos.get(sourceFile.path); + if (referencedMap) { + const newReferences = getReferencedFiles(newProgram, sourceFile, getCanonicalFileName); + if (newReferences) { + referencedMap.set(sourceFile.path, newReferences); + } + } + fileInfos.set(sourceFile.path, { version, signature: oldInfo && oldInfo.signature }); + } + + return { + fileInfos, + referencedMap, + hasCalledUpdateShapeSignature, + allFilesExcludingDefaultLibraryFile: undefined, + allFileNames: undefined + }; + } + + /** + * Gets the files affected by the path from the program + */ + export function getFilesAffectedBy(state: BuilderState, programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, cacheToUpdateSignature?: Map): ReadonlyArray { + // Since the operation could be cancelled, the signatures are always stored in the cache + // They will be commited once it is safe to use them + // eg when calling this api from tsserver, if there is no cancellation of the operation + // In the other cases the affected files signatures are commited only after the iteration through the result is complete + const signatureCache = cacheToUpdateSignature || createMap(); + const sourceFile = programOfThisState.getSourceFileByPath(path); + if (!sourceFile) { + return emptyArray; + } + + if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash)) { + return [sourceFile]; + } + + const result = (state.referencedMap ? getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit)(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash); + if (!cacheToUpdateSignature) { + // Commit all the signatures in the signature cache + updateSignaturesFromCache(state, signatureCache); + } + return result; + } + + /** + * Updates the signatures from the cache into state's fileinfo signatures + * This should be called whenever it is safe to commit the state of the builder + */ + export function updateSignaturesFromCache(state: BuilderState, signatureCache: Map) { + signatureCache.forEach((signature, path) => { + state.fileInfos.get(path).signature = signature; + state.hasCalledUpdateShapeSignature.set(path, true); + }); + } + + /** + * Returns if the shape of the signature has changed since last emit + */ + function updateShapeSignature(state: Readonly, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash) { + Debug.assert(!!sourceFile); + + // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate + if (state.hasCalledUpdateShapeSignature.has(sourceFile.path) || cacheToUpdateSignature.has(sourceFile.path)) { + return false; + } + + const info = state.fileInfos.get(sourceFile.path); + Debug.assert(!!info); + + const prevSignature = info.signature; + let latestSignature: string; + if (sourceFile.isDeclarationFile) { + latestSignature = sourceFile.version; + } + else { + const emitOutput = getFileEmitOutput(programOfThisState, sourceFile, /*emitOnlyDtsFiles*/ true, cancellationToken); + if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { + latestSignature = computeHash(emitOutput.outputFiles[0].text); + } + else { + latestSignature = prevSignature; + } + } + cacheToUpdateSignature.set(sourceFile.path, latestSignature); + + return !prevSignature || latestSignature !== prevSignature; + } + + /** + * Get all the dependencies of the sourceFile + */ + export function getAllDependencies(state: BuilderState, programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray { + const compilerOptions = programOfThisState.getCompilerOptions(); + // With --out or --outFile all outputs go into single file, all files depend on each other + if (compilerOptions.outFile || compilerOptions.out) { + return getAllFileNames(state, programOfThisState); + } + + // If this is non module emit, or its a global file, it depends on all the source files + if (!state.referencedMap || (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile))) { + return getAllFileNames(state, programOfThisState); + } + + // Get the references, traversing deep from the referenceMap + const seenMap = createMap(); + const queue = [sourceFile.path]; + while (queue.length) { + const path = queue.pop(); + if (!seenMap.has(path)) { + seenMap.set(path, true); + const references = state.referencedMap.get(path); + if (references) { + const iterator = references.keys(); + for (let { value, done } = iterator.next(); !done; { value, done } = iterator.next()) { + queue.push(value as Path); + } + } + } + } + + return arrayFrom(mapDefinedIterator(seenMap.keys(), path => { + const file = programOfThisState.getSourceFileByPath(path as Path); + return file ? file.fileName : path; + })); + } + + /** + * Gets the names of all files from the program + */ + function getAllFileNames(state: BuilderState, programOfThisState: Program): ReadonlyArray { + if (!state.allFileNames) { + const sourceFiles = programOfThisState.getSourceFiles(); + state.allFileNames = sourceFiles === emptyArray ? emptyArray : sourceFiles.map(file => file.fileName); + } + return state.allFileNames; + } + + /** + * Gets the files referenced by the the file path + */ + function getReferencedByPaths(state: Readonly, referencedFilePath: Path) { + return arrayFrom(mapDefinedIterator(state.referencedMap.entries(), ([filePath, referencesInFile]) => + referencesInFile.has(referencedFilePath) ? filePath as Path : undefined + )); + } + + /** + * For script files that contains only ambient external modules, although they are not actually external module files, + * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, + * there are no point to rebuild all script files if these special files have changed. However, if any statement + * in the file is not ambient external module, we treat it as a regular script file. + */ + function containsOnlyAmbientModules(sourceFile: SourceFile) { + for (const statement of sourceFile.statements) { + if (!isModuleWithStringLiteralName(statement)) { + return false; + } + } + return true; + } + + /** + * Gets all files of the program excluding the default library file + */ + function getAllFilesExcludingDefaultLibraryFile(state: BuilderState, programOfThisState: Program, firstSourceFile: SourceFile): ReadonlyArray { + // Use cached result + if (state.allFilesExcludingDefaultLibraryFile) { + return state.allFilesExcludingDefaultLibraryFile; + } + + let result: SourceFile[]; + addSourceFile(firstSourceFile); + for (const sourceFile of programOfThisState.getSourceFiles()) { + if (sourceFile !== firstSourceFile) { + addSourceFile(sourceFile); + } + } + state.allFilesExcludingDefaultLibraryFile = result || emptyArray; + return state.allFilesExcludingDefaultLibraryFile; + + function addSourceFile(sourceFile: SourceFile) { + if (!programOfThisState.isSourceFileDefaultLibrary(sourceFile)) { + (result || (result = [])).push(sourceFile); + } + } + } + + /** + * When program emits non modular code, gets the files affected by the sourceFile whose shape has changed + */ + function getFilesAffectedByUpdatedShapeWhenNonModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile) { + const compilerOptions = programOfThisState.getCompilerOptions(); + // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, + // so returning the file itself is good enough. + if (compilerOptions && (compilerOptions.out || compilerOptions.outFile)) { + return [sourceFileWithUpdatedShape]; + } + return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape); + } + + /** + * When program emits modular code, gets the files affected by the sourceFile whose shape has changed + */ + function getFilesAffectedByUpdatedShapeWhenModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash | undefined) { + if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) { + return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape); + } + + const compilerOptions = programOfThisState.getCompilerOptions(); + if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) { + return [sourceFileWithUpdatedShape]; + } + + // Now we need to if each file in the referencedBy list has a shape change as well. + // Because if so, its own referencedBy files need to be saved as well to make the + // emitting result consistent with files on disk. + const seenFileNamesMap = createMap(); + + // Start with the paths this file was referenced by + seenFileNamesMap.set(sourceFileWithUpdatedShape.path, sourceFileWithUpdatedShape); + const queue = getReferencedByPaths(state, sourceFileWithUpdatedShape.path); + while (queue.length > 0) { + const currentPath = queue.pop(); + if (!seenFileNamesMap.has(currentPath)) { + const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath); + seenFileNamesMap.set(currentPath, currentSourceFile); + if (currentSourceFile && updateShapeSignature(state, programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken, computeHash)) { + queue.push(...getReferencedByPaths(state, currentPath)); + } + } + } + + // Return array of values that needs emit + // Return array of values that needs emit + return arrayFrom(mapDefinedIterator(seenFileNamesMap.values(), value => value)); + } +} diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1e66ee7cbb401..828e7d289270c 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -749,10 +749,12 @@ namespace ts { return _jsxNamespace; } - function getEmitResolver(sourceFile: SourceFile, cancellationToken: CancellationToken) { + function getEmitResolver(sourceFile: SourceFile, cancellationToken: CancellationToken, ignoreDiagnostics?: boolean) { // Ensure we have all the type information in place for this file so that all the // emitter questions of this resolver will return the right information. - getDiagnostics(sourceFile, cancellationToken); + if (!ignoreDiagnostics) { + getDiagnostics(sourceFile, cancellationToken); + } return emitResolver; } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index cae9c84127229..ae506fd616a15 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1461,6 +1461,9 @@ namespace ts { /** Returns its argument. */ export function identity(x: T) { return x; } + /** Returns lower case string */ + export function toLowerCase(x: string) { return x.toLowerCase(); } + /** Throws an error because a function is not implemented. */ export function notImplemented(): never { throw new Error("Not implemented"); @@ -2931,9 +2934,7 @@ namespace ts { export type GetCanonicalFileName = (fileName: string) => string; export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): GetCanonicalFileName { - return useCaseSensitiveFileNames - ? ((fileName) => fileName) - : ((fileName) => fileName.toLowerCase()); + return useCaseSensitiveFileNames ? identity : toLowerCase; } /** @@ -3058,223 +3059,10 @@ namespace ts { export function assertTypeIsNever(_: never): void { } // tslint:disable-line no-empty - export interface FileAndDirectoryExistence { - fileExists: boolean; - directoryExists: boolean; - } - - export interface CachedDirectoryStructureHost extends DirectoryStructureHost { - /** Returns the queried result for the file exists and directory exists if at all it was done */ - addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path): FileAndDirectoryExistence | undefined; - addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind): void; - clearCache(): void; - } - - interface MutableFileSystemEntries { - readonly files: string[]; - readonly directories: string[]; - } - export const emptyFileSystemEntries: FileSystemEntries = { files: emptyArray, directories: emptyArray }; - export function createCachedDirectoryStructureHost(host: DirectoryStructureHost): CachedDirectoryStructureHost { - const cachedReadDirectoryResult = createMap(); - const getCurrentDirectory = memoize(() => host.getCurrentDirectory()); - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); - return { - useCaseSensitiveFileNames: host.useCaseSensitiveFileNames, - newLine: host.newLine, - readFile: (path, encoding) => host.readFile(path, encoding), - write: s => host.write(s), - writeFile, - fileExists, - directoryExists, - createDirectory, - getCurrentDirectory, - getDirectories, - readDirectory, - addOrDeleteFileOrDirectory, - addOrDeleteFile, - clearCache, - exit: code => host.exit(code) - }; - - function toPath(fileName: string) { - return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName); - } - - function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined { - return cachedReadDirectoryResult.get(rootDirPath); - } - - function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined { - return getCachedFileSystemEntries(getDirectoryPath(path)); - } - - function getBaseNameOfFileName(fileName: string) { - return getBaseFileName(normalizePath(fileName)); - } - - function createCachedFileSystemEntries(rootDir: string, rootDirPath: Path) { - const resultFromHost: MutableFileSystemEntries = { - files: map(host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || [], - directories: host.getDirectories(rootDir) || [] - }; - - cachedReadDirectoryResult.set(rootDirPath, resultFromHost); - return resultFromHost; - } - - /** - * If the readDirectory result was already cached, it returns that - * Otherwise gets result from host and caches it. - * The host request is done under try catch block to avoid caching incorrect result - */ - function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined { - const cachedResult = getCachedFileSystemEntries(rootDirPath); - if (cachedResult) { - return cachedResult; - } - - try { - return createCachedFileSystemEntries(rootDir, rootDirPath); - } - catch (_e) { - // If there is exception to read directories, dont cache the result and direct the calls to host - Debug.assert(!cachedReadDirectoryResult.has(rootDirPath)); - return undefined; - } - } - - function fileNameEqual(name1: string, name2: string) { - return getCanonicalFileName(name1) === getCanonicalFileName(name2); - } - - function hasEntry(entries: ReadonlyArray, name: string) { - return some(entries, file => fileNameEqual(file, name)); - } - - function updateFileSystemEntry(entries: string[], baseName: string, isValid: boolean) { - if (hasEntry(entries, baseName)) { - if (!isValid) { - return filterMutate(entries, entry => !fileNameEqual(entry, baseName)); - } - } - else if (isValid) { - return entries.push(baseName); - } - } - - function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { - const path = toPath(fileName); - const result = getCachedFileSystemEntriesForBaseDir(path); - if (result) { - updateFilesOfFileSystemEntry(result, getBaseNameOfFileName(fileName), /*fileExists*/ true); - } - return host.writeFile(fileName, data, writeByteOrderMark); - } - - function fileExists(fileName: string): boolean { - const path = toPath(fileName); - const result = getCachedFileSystemEntriesForBaseDir(path); - return result && hasEntry(result.files, getBaseNameOfFileName(fileName)) || - host.fileExists(fileName); - } - - function directoryExists(dirPath: string): boolean { - const path = toPath(dirPath); - return cachedReadDirectoryResult.has(path) || host.directoryExists(dirPath); - } - - function createDirectory(dirPath: string) { - const path = toPath(dirPath); - const result = getCachedFileSystemEntriesForBaseDir(path); - const baseFileName = getBaseNameOfFileName(dirPath); - if (result) { - updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true); - } - host.createDirectory(dirPath); - } - - function getDirectories(rootDir: string): string[] { - const rootDirPath = toPath(rootDir); - const result = tryReadDirectory(rootDir, rootDirPath); - if (result) { - return result.directories.slice(); - } - return host.getDirectories(rootDir); - } - - function readDirectory(rootDir: string, extensions?: ReadonlyArray, excludes?: ReadonlyArray, includes?: ReadonlyArray, depth?: number): string[] { - const rootDirPath = toPath(rootDir); - const result = tryReadDirectory(rootDir, rootDirPath); - if (result) { - return matchFiles(rootDir, extensions, excludes, includes, host.useCaseSensitiveFileNames, getCurrentDirectory(), depth, getFileSystemEntries); - } - return host.readDirectory(rootDir, extensions, excludes, includes, depth); - - function getFileSystemEntries(dir: string) { - const path = toPath(dir); - if (path === rootDirPath) { - return result; - } - return tryReadDirectory(dir, path) || emptyFileSystemEntries; - } - } - - function addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path) { - const existingResult = getCachedFileSystemEntries(fileOrDirectoryPath); - if (existingResult) { - // Just clear the cache for now - // For now just clear the cache, since this could mean that multiple level entries might need to be re-evaluated - clearCache(); - } - else { - // This was earlier a file (hence not in cached directory contents) - // or we never cached the directory containing it - const parentResult = getCachedFileSystemEntriesForBaseDir(fileOrDirectoryPath); - if (parentResult) { - const baseName = getBaseNameOfFileName(fileOrDirectory); - if (parentResult) { - const fsQueryResult: FileAndDirectoryExistence = { - fileExists: host.fileExists(fileOrDirectoryPath), - directoryExists: host.directoryExists(fileOrDirectoryPath) - }; - if (fsQueryResult.directoryExists || hasEntry(parentResult.directories, baseName)) { - // Folder added or removed, clear the cache instead of updating the folder and its structure - clearCache(); - } - else { - // No need to update the directory structure, just files - updateFilesOfFileSystemEntry(parentResult, baseName, fsQueryResult.fileExists); - } - return fsQueryResult; - } - } - } - } - - function addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind) { - if (eventKind === FileWatcherEventKind.Changed) { - return; - } - - const parentResult = getCachedFileSystemEntriesForBaseDir(filePath); - if (parentResult) { - updateFilesOfFileSystemEntry(parentResult, getBaseNameOfFileName(fileName), eventKind === FileWatcherEventKind.Created); - } - } - - function updateFilesOfFileSystemEntry(parentResult: MutableFileSystemEntries, baseName: string, fileExists: boolean) { - updateFileSystemEntry(parentResult.files, baseName, fileExists); - } - - function clearCache() { - cachedReadDirectoryResult.clear(); - } - } export function singleElementArray(t: T | undefined): T[] | undefined { return t === undefined ? undefined : [t]; diff --git a/src/compiler/declarationEmitter.ts b/src/compiler/declarationEmitter.ts index aa1ec82a63164..1a35f520ce560 100644 --- a/src/compiler/declarationEmitter.ts +++ b/src/compiler/declarationEmitter.ts @@ -2000,7 +2000,7 @@ namespace ts { export function writeDeclarationFile(declarationFilePath: string, sourceFileOrBundle: SourceFile | Bundle, host: EmitHost, resolver: EmitResolver, emitterDiagnostics: DiagnosticCollection, emitOnlyDtsFiles: boolean) { const emitDeclarationResult = emitDeclarations(host, resolver, emitterDiagnostics, declarationFilePath, sourceFileOrBundle, emitOnlyDtsFiles); const emitSkipped = emitDeclarationResult.reportedDeclarationError || host.isEmitBlocked(declarationFilePath) || host.getCompilerOptions().noEmit; - if (!emitSkipped) { + if (!emitSkipped || emitOnlyDtsFiles) { const sourceFiles = sourceFileOrBundle.kind === SyntaxKind.Bundle ? sourceFileOrBundle.sourceFiles : [sourceFileOrBundle]; const declarationOutput = emitDeclarationResult.referencesOutput + getDeclarationOutput(emitDeclarationResult.synchronousDeclarationOutput, emitDeclarationResult.moduleElementDeclarationEmitInfo); diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 37fae7195ab10..bd45857fc51dc 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1,7 +1,6 @@ /// /// /// -/// namespace ts { const ignoreDiagnosticCommentRegEx = /(^\s*$)|(^\s*\/\/\/?\s*(@ts-ignore)?)/; @@ -1141,32 +1140,34 @@ namespace ts { function emitWorker(program: Program, sourceFile: SourceFile, writeFileCallback: WriteFileCallback, cancellationToken: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult { let declarationDiagnostics: ReadonlyArray = []; - if (options.noEmit) { - return { diagnostics: declarationDiagnostics, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true }; - } - - // If the noEmitOnError flag is set, then check if we have any errors so far. If so, - // immediately bail out. Note that we pass 'undefined' for 'sourceFile' so that we - // get any preEmit diagnostics, not just the ones - if (options.noEmitOnError) { - const diagnostics = [ - ...program.getOptionsDiagnostics(cancellationToken), - ...program.getSyntacticDiagnostics(sourceFile, cancellationToken), - ...program.getGlobalDiagnostics(cancellationToken), - ...program.getSemanticDiagnostics(sourceFile, cancellationToken) - ]; - - if (diagnostics.length === 0 && program.getCompilerOptions().declaration) { - declarationDiagnostics = program.getDeclarationDiagnostics(/*sourceFile*/ undefined, cancellationToken); + if (!emitOnlyDtsFiles) { + if (options.noEmit) { + return { diagnostics: declarationDiagnostics, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true }; } - if (diagnostics.length > 0 || declarationDiagnostics.length > 0) { - return { - diagnostics: concatenate(diagnostics, declarationDiagnostics), - sourceMaps: undefined, - emittedFiles: undefined, - emitSkipped: true - }; + // If the noEmitOnError flag is set, then check if we have any errors so far. If so, + // immediately bail out. Note that we pass 'undefined' for 'sourceFile' so that we + // get any preEmit diagnostics, not just the ones + if (options.noEmitOnError) { + const diagnostics = [ + ...program.getOptionsDiagnostics(cancellationToken), + ...program.getSyntacticDiagnostics(sourceFile, cancellationToken), + ...program.getGlobalDiagnostics(cancellationToken), + ...program.getSemanticDiagnostics(sourceFile, cancellationToken) + ]; + + if (diagnostics.length === 0 && program.getCompilerOptions().declaration) { + declarationDiagnostics = program.getDeclarationDiagnostics(/*sourceFile*/ undefined, cancellationToken); + } + + if (diagnostics.length > 0 || declarationDiagnostics.length > 0) { + return { + diagnostics: concatenate(diagnostics, declarationDiagnostics), + sourceMaps: undefined, + emittedFiles: undefined, + emitSkipped: true + }; + } } } @@ -1178,7 +1179,7 @@ namespace ts { // This is because in the -out scenario all files need to be emitted, and therefore all // files need to be type checked. And the way to specify that all files need to be type // checked is to not pass the file to getEmitResolver. - const emitResolver = getDiagnosticsProducingTypeChecker().getEmitResolver((options.outFile || options.out) ? undefined : sourceFile); + const emitResolver = getDiagnosticsProducingTypeChecker().getEmitResolver((options.outFile || options.out) ? undefined : sourceFile, cancellationToken, emitOnlyDtsFiles); performance.mark("beforeEmit"); diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 2ed14e0d41e86..e907d2d62b728 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -9,12 +9,12 @@ namespace ts { startRecordingFilesWithChangedResolutions(): void; finishRecordingFilesWithChangedResolutions(): Path[]; - resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, logChanges: boolean): ResolvedModuleFull[]; + resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined): ResolvedModuleFull[]; resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; invalidateResolutionOfFile(filePath: Path): void; removeResolutionsOfFile(filePath: Path): void; - createHasInvalidatedResolution(): HasInvalidatedResolution; + createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution; startCachingPerDirectoryResolution(): void; finishCachingPerDirectoryResolution(): void; @@ -47,7 +47,7 @@ namespace ts { onInvalidatedResolution(): void; watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; onChangedAutomaticTypeDirectiveNames(): void; - getCachedDirectoryStructureHost?(): CachedDirectoryStructureHost; + getCachedDirectoryStructureHost(): CachedDirectoryStructureHost | undefined; projectName?: string; getGlobalCache?(): string | undefined; writeLog(s: string): void; @@ -73,7 +73,7 @@ namespace ts { type GetResolutionWithResolvedFileName = (resolution: T) => R; - export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string): ResolutionCache { + export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string, logChangesWhenResolvingModule: boolean): ResolutionCache { let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; let filesWithInvalidatedResolutions: Map | undefined; let allFilesHaveInvalidatedResolution = false; @@ -88,6 +88,7 @@ namespace ts { const perDirectoryResolvedTypeReferenceDirectives = createMap>(); const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory()); + const cachedDirectoryStructureHost = resolutionHost.getCachedDirectoryStructureHost(); /** * These are the extensions that failed lookup files will have by default, @@ -159,8 +160,8 @@ namespace ts { return collected; } - function createHasInvalidatedResolution(): HasInvalidatedResolution { - if (allFilesHaveInvalidatedResolution) { + function createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution { + if (allFilesHaveInvalidatedResolution || forceAllFilesAsInvalidated) { // Any file asked would have invalidated resolution filesWithInvalidatedResolutions = undefined; return returnTrue; @@ -307,12 +308,12 @@ namespace ts { ); } - function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, logChanges: boolean): ResolvedModuleFull[] { + function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined): ResolvedModuleFull[] { return resolveNamesWithLocalCache( moduleNames, containingFile, resolvedModuleNames, perDirectoryResolvedModuleNames, resolveModuleName, getResolvedModule, - reusedNames, logChanges + reusedNames, logChangesWhenResolvingModule ); } @@ -468,9 +469,9 @@ namespace ts { function createDirectoryWatcher(directory: string, dirPath: Path) { return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrDirectory => { const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); - if (resolutionHost.getCachedDirectoryStructureHost) { + if (cachedDirectoryStructureHost) { // Since the file existance changed, update the sourceFiles cache - resolutionHost.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); + cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); } // If the files are added to project root or node_modules directory, always run through the invalidation process @@ -601,9 +602,9 @@ namespace ts { // Create new watch and recursive info return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => { const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); - if (resolutionHost.getCachedDirectoryStructureHost) { + if (cachedDirectoryStructureHost) { // Since the file existance changed, update the sourceFiles cache - resolutionHost.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); + cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); } // For now just recompile diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index d5eeb6a58cceb..3256eef4dc2de 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -30,27 +30,14 @@ namespace ts { mtime?: Date; } - /** - * Partial interface of the System thats needed to support the caching of directory structure - */ - export interface DirectoryStructureHost { + export interface System { + args: string[]; newLine: string; useCaseSensitiveFileNames: boolean; write(s: string): void; readFile(path: string, encoding?: string): string | undefined; - writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; - fileExists(path: string): boolean; - directoryExists(path: string): boolean; - createDirectory(path: string): void; - getCurrentDirectory(): string; - getDirectories(path: string): string[]; - readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; - exit(exitCode?: number): void; - } - - export interface System extends DirectoryStructureHost { - args: string[]; getFileSize?(path: string): number; + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; /** * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that * use native OS file watching @@ -58,7 +45,13 @@ namespace ts { watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; resolvePath(path: string): string; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + createDirectory(path: string): void; getExecutingFilePath(): string; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; /** * This should be cryptographically secure. @@ -66,6 +59,7 @@ namespace ts { */ createHash?(data: string): string; getMemoryUsage?(): number; + exit(exitCode?: number): void; realpath?(path: string): string; /*@internal*/ getEnvironmentVariable(name: string): string; /*@internal*/ tryEnableSourceMapsForHost?(): void; diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 01fb45e4f7d6c..15e4867d7f7a1 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -21,10 +21,10 @@ namespace ts { return diagnostic.messageText; } - let reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticSimply); + let reportDiagnostic = createDiagnosticReporter(sys); function udpateReportDiagnostic(options: CompilerOptions) { if (options.pretty) { - reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticWithColorAndContext); + reportDiagnostic = createDiagnosticReporter(sys, /*pretty*/ true); } } @@ -55,7 +55,7 @@ namespace ts { // If there are any errors due to command line parsing and/or // setting up localization, report them and quit. if (commandLine.errors.length > 0) { - reportDiagnostics(commandLine.errors, reportDiagnostic); + commandLine.errors.forEach(reportDiagnostic); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } @@ -110,12 +110,11 @@ namespace ts { const commandLineOptions = commandLine.options; if (configFileName) { - const reportWatchDiagnostic = createWatchDiagnosticReporter(); - const configParseResult = parseConfigFile(configFileName, commandLineOptions, sys, reportDiagnostic, reportWatchDiagnostic); + const configParseResult = parseConfigFileWithSystem(configFileName, commandLineOptions, sys, reportDiagnostic); udpateReportDiagnostic(configParseResult.options); if (isWatchSet(configParseResult.options)) { reportWatchModeWithoutSysSupport(); - createWatchModeWithConfigFile(configParseResult, commandLineOptions, createWatchingSystemHost(reportWatchDiagnostic)); + createWatchOfConfigFile(configParseResult, commandLineOptions); } else { performCompilation(configParseResult.fileNames, configParseResult.options); @@ -125,7 +124,7 @@ namespace ts { udpateReportDiagnostic(commandLineOptions); if (isWatchSet(commandLineOptions)) { reportWatchModeWithoutSysSupport(); - createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions, createWatchingSystemHost()); + createWatchOfFilesAndCompilerOptions(commandLine.fileNames, commandLineOptions); } else { performCompilation(commandLine.fileNames, commandLineOptions); @@ -145,44 +144,42 @@ namespace ts { enableStatistics(compilerOptions); const program = createProgram(rootFileNames, compilerOptions, compilerHost); - const exitStatus = compileProgram(program); - + const exitStatus = emitFilesAndReportErrors(program, reportDiagnostic, s => sys.write(s + sys.newLine)); reportStatistics(program); return sys.exit(exitStatus); } - function createWatchingSystemHost(reportWatchDiagnostic?: DiagnosticReporter) { - const watchingHost = ts.createWatchingSystemHost(/*pretty*/ undefined, sys, parseConfigFile, reportDiagnostic, reportWatchDiagnostic); - watchingHost.beforeCompile = enableStatistics; - const afterCompile = watchingHost.afterCompile; - watchingHost.afterCompile = (host, program, builder) => { - afterCompile(host, program, builder); - reportStatistics(program); + function updateWatchCompilationHost(watchCompilerHost: WatchCompilerHost) { + const compileUsingBuilder = watchCompilerHost.createProgram; + watchCompilerHost.createProgram = (rootNames, options, host, oldProgram) => { + enableStatistics(options); + return compileUsingBuilder(rootNames, options, host, oldProgram); + }; + const emitFilesUsingBuilder = watchCompilerHost.afterProgramCreate; + watchCompilerHost.afterProgramCreate = builderProgram => { + emitFilesUsingBuilder(builderProgram); + reportStatistics(builderProgram.getProgram()); }; - return watchingHost; } - function compileProgram(program: Program): ExitStatus { - let diagnostics: Diagnostic[]; - - // First get and report any syntactic errors. - diagnostics = program.getSyntacticDiagnostics().slice(); - - // If we didn't have any syntactic errors, then also try getting the global and - // semantic errors. - if (diagnostics.length === 0) { - diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()); - - if (diagnostics.length === 0) { - diagnostics = program.getSemanticDiagnostics().slice(); - } - } + function createWatchStatusReporter(options: CompilerOptions) { + return ts.createWatchStatusReporter(sys, !!options.pretty); + } - // Emit and report any errors we ran into. - const { emittedFiles, emitSkipped, diagnostics: emitDiagnostics } = program.emit(); - addRange(diagnostics, emitDiagnostics); + function createWatchOfConfigFile(configParseResult: ParsedCommandLine, optionsToExtend: CompilerOptions) { + const watchCompilerHost = ts.createWatchCompilerHostOfConfigFile(configParseResult.options.configFilePath, optionsToExtend, sys, /*createProgram*/ undefined, reportDiagnostic, createWatchStatusReporter(configParseResult.options)); + updateWatchCompilationHost(watchCompilerHost); + watchCompilerHost.rootFiles = configParseResult.fileNames; + watchCompilerHost.options = configParseResult.options; + watchCompilerHost.configFileSpecs = configParseResult.configFileSpecs; + watchCompilerHost.configFileWildCardDirectories = configParseResult.wildcardDirectories; + createWatchProgram(watchCompilerHost); + } - return handleEmitOutputAndReportErrors(sys, program, emittedFiles, emitSkipped, diagnostics, reportDiagnostic); + function createWatchOfFilesAndCompilerOptions(rootFiles: string[], options: CompilerOptions) { + const watchCompilerHost = ts.createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, sys, /*createProgram*/ undefined, reportDiagnostic, createWatchStatusReporter(options)); + updateWatchCompilationHost(watchCompilerHost); + createWatchProgram(watchCompilerHost); } function enableStatistics(compilerOptions: CompilerOptions) { diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 07f69ddfe2830..92d9f099441f8 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -38,6 +38,7 @@ "emitter.ts", "watchUtilities.ts", "program.ts", + "builderState.ts", "builder.ts", "resolutionCache.ts", "watch.ts", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 703b04699b8ac..f6ca2f45d3401 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2892,7 +2892,7 @@ namespace ts { // Should not be called directly. Should only be accessed through the Program instance. /* @internal */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[]; /* @internal */ getGlobalDiagnostics(): Diagnostic[]; - /* @internal */ getEmitResolver(sourceFile?: SourceFile, cancellationToken?: CancellationToken): EmitResolver; + /* @internal */ getEmitResolver(sourceFile?: SourceFile, cancellationToken?: CancellationToken, ignoreDiagnostics?: boolean): EmitResolver; /* @internal */ getNodeCount(): number; /* @internal */ getIdentifierCount(): number; @@ -4471,6 +4471,7 @@ namespace ts { /* @internal */ onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions): void; /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; + createHash?(data: string): string; } /* @internal */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 4189ef89e75df..6a421bf6948da 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3380,14 +3380,14 @@ namespace ts { const carriageReturnLineFeed = "\r\n"; const lineFeed = "\n"; - export function getNewLineCharacter(options: CompilerOptions | PrinterOptions, system?: { newLine: string }): string { + export function getNewLineCharacter(options: CompilerOptions | PrinterOptions, getNewLine?: () => string): string { switch (options.newLine) { case NewLineKind.CarriageReturnLineFeed: return carriageReturnLineFeed; case NewLineKind.LineFeed: return lineFeed; } - return system ? system.newLine : sys ? sys.newLine : carriageReturnLineFeed; + return getNewLine ? getNewLine() : sys ? sys.newLine : carriageReturnLineFeed; } /** diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 79e26f5986cf8..beccb65f7b802 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -2,128 +2,161 @@ /// /// -/* @internal */ +/*@internal*/ namespace ts { - export type DiagnosticReporter = (diagnostic: Diagnostic) => void; - export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: DirectoryStructureHost, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine; - export interface WatchingSystemHost { - // FS system to use - system: System; - - // parse config file - parseConfigFile: ParseConfigFile; - - // Reporting errors - reportDiagnostic: DiagnosticReporter; - reportWatchDiagnostic: DiagnosticReporter; - - // Callbacks to do custom action before creating program and after creating program - beforeCompile(compilerOptions: CompilerOptions): void; - afterCompile(host: DirectoryStructureHost, program: Program, builder: Builder): void; - - // Only for testing - maxNumberOfFilesToIterateForInvalidation?: number; - } - - const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = sys ? { + const sysFormatDiagnosticsHost: FormatDiagnosticsHost = sys ? { getCurrentDirectory: () => sys.getCurrentDirectory(), getNewLine: () => sys.newLine, getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames) } : undefined; - export function createDiagnosticReporter(system = sys, worker = reportDiagnosticSimply, formatDiagnosticsHost?: FormatDiagnosticsHost): DiagnosticReporter { - return diagnostic => worker(diagnostic, getFormatDiagnosticsHost(), system); - - function getFormatDiagnosticsHost() { - return formatDiagnosticsHost || (formatDiagnosticsHost = system === sys ? defaultFormatDiagnosticsHost : { - getCurrentDirectory: () => system.getCurrentDirectory(), - getNewLine: () => system.newLine, - getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames), - }); + /** + * Create a function that reports error by writing to the system and handles the formating of the diagnostic + */ + export function createDiagnosticReporter(system: System, pretty?: boolean): DiagnosticReporter { + const host: FormatDiagnosticsHost = system === sys ? sysFormatDiagnosticsHost : { + getCurrentDirectory: () => system.getCurrentDirectory(), + getNewLine: () => system.newLine, + getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames), + }; + if (!pretty) { + return diagnostic => system.write(ts.formatDiagnostic(diagnostic, host)); } - } - export function createWatchDiagnosticReporter(system = sys): DiagnosticReporter { + const diagnostics: Diagnostic[] = new Array(1); return diagnostic => { - let output = new Date().toLocaleTimeString() + " - "; - output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${system.newLine + system.newLine + system.newLine}`; - system.write(output); + diagnostics[0] = diagnostic; + system.write(formatDiagnosticsWithColorAndContext(diagnostics, host) + host.getNewLine()); + diagnostics[0] = undefined; }; } - /** @internal */ - export function createWatchDiagnosticReporterWithColor(system = sys): DiagnosticReporter { - return diagnostic => { + function clearScreenIfNotWatchingForFileChanges(system: System, diagnostic: Diagnostic) { + if (system.clearScreen && diagnostic.code !== Diagnostics.Compilation_complete_Watching_for_file_changes.code) { + system.clearScreen(); + } + } + + /** + * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic + */ + export function createWatchStatusReporter(system: System, pretty?: boolean): WatchStatusReporter { + return pretty ? + (diagnostic: Diagnostic, newLine: string) => { + clearScreenIfNotWatchingForFileChanges(system, diagnostic); let output = `[${ formatColorAndReset(new Date().toLocaleTimeString(), ForegroundColorEscapeSequences.Grey) }] `; - output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${system.newLine + system.newLine + system.newLine}`; + output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${newLine + newLine + newLine}`; + system.write(output); + } : + (diagnostic: Diagnostic, newLine: string) => { + clearScreenIfNotWatchingForFileChanges(system, diagnostic); + let output = new Date().toLocaleTimeString() + " - "; + output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${newLine + newLine + newLine}`; system.write(output); }; } - export function reportDiagnostics(diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter): void { - for (const diagnostic of diagnostics) { - reportDiagnostic(diagnostic); - } + /** + * Interface extending ParseConfigHost to support ParseConfigFile that reads config file and reports errors + */ + export interface ParseConfigFileHost extends ParseConfigHost, ConfigFileDiagnosticsReporter { + getCurrentDirectory(): string; } - export function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System): void { - system.write(ts.formatDiagnostic(diagnostic, host)); + /** Parses config file using System interface */ + export function parseConfigFileWithSystem(configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter) { + const host: ParseConfigFileHost = system; + host.onConfigFileDiagnostic = reportDiagnostic; + host.onUnRecoverableConfigFileDiagnostic = diagnostic => reportUnrecoverableDiagnostic(sys, reportDiagnostic, diagnostic); + const result = parseConfigFile(configFileName, optionsToExtend, host); + host.onConfigFileDiagnostic = undefined; + host.onUnRecoverableConfigFileDiagnostic = undefined; + return result; } - export function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System): void { - system.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine()); - } - - export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: DirectoryStructureHost, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter): ParsedCommandLine { + /** + * Reads the config file, reports errors if any and exits if the config file cannot be found + */ + export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost): ParsedCommandLine | undefined { let configFileText: string; try { - configFileText = system.readFile(configFileName); + configFileText = host.readFile(configFileName); } catch (e) { const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); - reportWatchDiagnostic(error); - system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; + host.onUnRecoverableConfigFileDiagnostic(error); + return undefined; } if (!configFileText) { const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); - reportDiagnostics([error], reportDiagnostic); - system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; + host.onUnRecoverableConfigFileDiagnostic(error); + return undefined; } const result = parseJsonText(configFileName, configFileText); - reportDiagnostics(result.parseDiagnostics, reportDiagnostic); + result.parseDiagnostics.forEach(diagnostic => host.onConfigFileDiagnostic(diagnostic)); - const cwd = system.getCurrentDirectory(); - const configParseResult = parseJsonSourceFileConfigFileContent(result, system, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); - reportDiagnostics(configParseResult.errors, reportDiagnostic); + const cwd = host.getCurrentDirectory(); + const configParseResult = parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); + configParseResult.errors.forEach(diagnostic => host.onConfigFileDiagnostic(diagnostic)); return configParseResult; } - function reportEmittedFiles(files: string[], system: DirectoryStructureHost): void { - if (!files || files.length === 0) { - return; - } - const currentDir = system.getCurrentDirectory(); - for (const file of files) { - const filepath = getNormalizedAbsolutePath(file, currentDir); - system.write(`TSFILE: ${filepath}${system.newLine}`); - } + /** + * Program structure needed to emit the files and report diagnostics + */ + export interface ProgramToEmitFilesAndReportErrors { + getCurrentDirectory(): string; + getCompilerOptions(): CompilerOptions; + getSourceFiles(): ReadonlyArray; + getSyntacticDiagnostics(): ReadonlyArray; + getOptionsDiagnostics(): ReadonlyArray; + getGlobalDiagnostics(): ReadonlyArray; + getSemanticDiagnostics(): ReadonlyArray; + emit(): EmitResult; } - export function handleEmitOutputAndReportErrors(system: DirectoryStructureHost, program: Program, - emittedFiles: string[], emitSkipped: boolean, - diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter - ): ExitStatus { - reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), reportDiagnostic); - reportEmittedFiles(emittedFiles, system); + /** + * Helper that emit files, report diagnostics and lists emitted and/or source files depending on compiler options + */ + export function emitFilesAndReportErrors(program: ProgramToEmitFilesAndReportErrors, reportDiagnostic: DiagnosticReporter, writeFileName?: (s: string) => void) { + // First get and report any syntactic errors. + const diagnostics = program.getSyntacticDiagnostics().slice(); + let reportSemanticDiagnostics = false; + + // If we didn't have any syntactic errors, then also try getting the global and + // semantic errors. + if (diagnostics.length === 0) { + addRange(diagnostics, program.getOptionsDiagnostics()); + addRange(diagnostics, program.getGlobalDiagnostics()); - if (program.getCompilerOptions().listFiles) { - forEach(program.getSourceFiles(), file => { - system.write(file.fileName + system.newLine); + if (diagnostics.length === 0) { + reportSemanticDiagnostics = true; + } + } + + // Emit and report any errors we ran into. + const { emittedFiles, emitSkipped, diagnostics: emitDiagnostics } = program.emit(); + addRange(diagnostics, emitDiagnostics); + + if (reportSemanticDiagnostics) { + addRange(diagnostics, program.getSemanticDiagnostics()); + } + + sortAndDeduplicateDiagnostics(diagnostics).forEach(reportDiagnostic); + if (writeFileName) { + const currentDir = program.getCurrentDirectory(); + forEach(emittedFiles, file => { + const filepath = getNormalizedAbsolutePath(file, currentDir); + writeFileName(`TSFILE: ${filepath}`); }); + + if (program.getCompilerOptions().listFiles) { + forEach(program.getSourceFiles(), file => { + writeFileName(file.fileName); + }); + } } if (emitSkipped && diagnostics.length > 0) { @@ -138,110 +171,268 @@ namespace ts { return ExitStatus.Success; } - export function createWatchingSystemHost(pretty?: DiagnosticStyle, system = sys, - parseConfigFile?: ParseConfigFile, reportDiagnostic?: DiagnosticReporter, - reportWatchDiagnostic?: DiagnosticReporter - ): WatchingSystemHost { - reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system, pretty ? reportDiagnosticWithColorAndContext : reportDiagnosticSimply); - reportWatchDiagnostic = reportWatchDiagnostic || pretty ? createWatchDiagnosticReporterWithColor(system) : createWatchDiagnosticReporter(system); - parseConfigFile = parseConfigFile || ts.parseConfigFile; + const noopFileWatcher: FileWatcher = { close: noop }; + + /** + * Creates the watch compiler host that can be extended with config file or root file names and options host + */ + function createWatchCompilerHost(system = sys, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHost { + if (!createProgram) { + createProgram = createEmitAndSemanticDiagnosticsBuilderProgram as any; + } + + let host: DirectoryStructureHost = system; + const useCaseSensitiveFileNames = () => system.useCaseSensitiveFileNames; + const writeFileName = (s: string) => system.write(s + system.newLine); return { - system, - parseConfigFile, - reportDiagnostic, - reportWatchDiagnostic, - beforeCompile: noop, - afterCompile: compileWatchedProgram, + useCaseSensitiveFileNames, + getNewLine: () => system.newLine, + getCurrentDirectory: () => system.getCurrentDirectory(), + getDefaultLibLocation, + getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), + fileExists: path => system.fileExists(path), + readFile: (path, encoding) => system.readFile(path, encoding), + directoryExists: path => system.directoryExists(path), + getDirectories: path => system.getDirectories(path), + readDirectory: (path, extensions, exclude, include, depth) => system.readDirectory(path, extensions, exclude, include, depth), + realpath: system.realpath && (path => system.realpath(path)), + getEnvironmentVariable: system.getEnvironmentVariable && (name => system.getEnvironmentVariable(name)), + watchFile: system.watchFile ? ((path, callback, pollingInterval) => system.watchFile(path, callback, pollingInterval)) : () => noopFileWatcher, + watchDirectory: system.watchDirectory ? ((path, callback, recursive) => system.watchDirectory(path, callback, recursive)) : () => noopFileWatcher, + setTimeout: system.setTimeout ? ((callback, ms, ...args: any[]) => system.setTimeout.call(system, callback, ms, ...args)) : noop, + clearTimeout: system.clearTimeout ? (timeoutId => system.clearTimeout(timeoutId)) : noop, + trace: s => system.write(s), + onWatchStatusChange: reportWatchStatus || createWatchStatusReporter(system), + createDirectory: path => system.createDirectory(path), + writeFile: (path, data, writeByteOrderMark) => system.writeFile(path, data, writeByteOrderMark), + onCachedDirectoryStructureHostCreate: cacheHost => host = cacheHost || system, + createHash: system.createHash && (s => system.createHash(s)), + createProgram, + afterProgramCreate: emitFilesAndReportErrorUsingBuilder }; - function compileWatchedProgram(host: DirectoryStructureHost, program: Program, builder: Builder) { - // First get and report any syntactic errors. - const diagnostics = program.getSyntacticDiagnostics().slice(); - let reportSemanticDiagnostics = false; + function getDefaultLibLocation() { + return getDirectoryPath(normalizePath(system.getExecutingFilePath())); + } - // If we didn't have any syntactic errors, then also try getting the global and - // semantic errors. - if (diagnostics.length === 0) { - addRange(diagnostics, program.getOptionsDiagnostics()); - addRange(diagnostics, program.getGlobalDiagnostics()); + function emitFilesAndReportErrorUsingBuilder(builderProgram: BuilderProgram) { + emitFilesAndReportErrors(builderProgram, reportDiagnostic, writeFileName); + } + } - if (diagnostics.length === 0) { - reportSemanticDiagnostics = true; - } - } + /** + * Report error and exit + */ + function reportUnrecoverableDiagnostic(system: System, reportDiagnostic: DiagnosticReporter, diagnostic: Diagnostic) { + reportDiagnostic(diagnostic); + system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } - // Emit and report any errors we ran into. - const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; - let sourceMaps: SourceMapData[]; - let emitSkipped: boolean; + /** + * 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 { + reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system); + const host = createWatchCompilerHost(system, createProgram, reportDiagnostic, reportWatchStatus) as WatchCompilerHostOfConfigFile; + host.onConfigFileDiagnostic = reportDiagnostic; + host.onUnRecoverableConfigFileDiagnostic = diagnostic => reportUnrecoverableDiagnostic(system, reportDiagnostic, diagnostic); + host.configFileName = configFileName; + host.optionsToExtend = optionsToExtend; + return host; + } - const result = builder.emitChangedFiles(program, writeFile); - if (result.length === 0) { - emitSkipped = true; - } - else { - for (const emitOutput of result) { - if (emitOutput.emitSkipped) { - emitSkipped = true; - } - addRange(diagnostics, emitOutput.diagnostics); - sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); - } - } + /** + * 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): WatchCompilerHostOfFilesAndCompilerOptions { + const host = createWatchCompilerHost(system, createProgram, reportDiagnostic || createDiagnosticReporter(system), reportWatchStatus) as WatchCompilerHostOfFilesAndCompilerOptions; + host.rootFiles = rootFiles; + host.options = options; + return host; + } +} - if (reportSemanticDiagnostics) { - addRange(diagnostics, builder.getSemanticDiagnostics(program)); - } - return handleEmitOutputAndReportErrors(host, program, emittedFiles, emitSkipped, - diagnostics, reportDiagnostic); - - function ensureDirectoriesExist(directoryPath: string) { - if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { - const parentDirectory = getDirectoryPath(directoryPath); - ensureDirectoriesExist(parentDirectory); - host.createDirectory(directoryPath); - } - } +namespace ts { + export type DiagnosticReporter = (diagnostic: Diagnostic) => void; + export type WatchStatusReporter = (diagnostic: Diagnostic, newLine: string) => void; + export type CreateProgram = (rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: T) => T; + export interface WatchCompilerHost { + /** + * Used to create the program when need for program creation or recreation detected + */ + createProgram: CreateProgram; + /** If provided, callback to invoke after every new program creation */ + afterProgramCreate?(program: T): void; + /** If provided, called with Diagnostic message that informs about change in watch status */ + onWatchStatusChange?(diagnostic: Diagnostic, newLine: string): void; - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, onError: (message: string) => void) { - try { - performance.mark("beforeIOWrite"); - ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); + // Only for testing + /*@internal*/ + maxNumberOfFilesToIterateForInvalidation?: number; - host.writeFile(fileName, text, writeByteOrderMark); + // Sub set of compiler host methods to read and generate new program + useCaseSensitiveFileNames(): boolean; + getNewLine(): string; + getCurrentDirectory(): string; + getDefaultLibFileName(options: CompilerOptions): string; + getDefaultLibLocation?(): string; + createHash?(data: string): string; + + /** + * Use to check file presence for source files and + * if resolveModuleNames is not provided (complier is in charge of module resolution) then module files as well + */ + fileExists(path: string): boolean; + /** + * Use to read file text for source files and + * if resolveModuleNames is not provided (complier is in charge of module resolution) then module files as well + */ + readFile(path: string, encoding?: string): string | undefined; + + /** If provided, used for module resolution as well as to handle directory structure */ + directoryExists?(path: string): boolean; + /** If provided, used in resolutions as well as handling directory structure */ + getDirectories?(path: string): string[]; + /** If provided, used to cache and handle directory structure modifications */ + readDirectory?(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + + /** Symbol links resolution */ + realpath?(path: string): string; + /** If provided would be used to write log about compilation */ + trace?(s: string): void; + /** If provided is used to get the environment variable */ + getEnvironmentVariable?(name: string): string; + + /** If provided, used to resolve the module names, otherwise typescript's default module resolution */ + resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; + /** If provided, used to resolve type reference directives, otherwise typescript's default resolution */ + resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): (ResolvedTypeReferenceDirective | undefined)[]; + + /** 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; + /** 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; + /** 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 */ + clearTimeout?(timeoutId: any): void; + } - performance.mark("afterIOWrite"); - performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + /** Internal interface used to wire emit through same host */ + /*@internal*/ + export interface WatchCompilerHost { + createDirectory?(path: string): void; + writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void; + onCachedDirectoryStructureHostCreate?(host: CachedDirectoryStructureHost): void; + } - if (emittedFiles) { - emittedFiles.push(fileName); - } - } - catch (e) { - if (onError) { - onError(e.message); - } - } - } - } + /** + * Host to create watch with root files and options + */ + export interface WatchCompilerHostOfFilesAndCompilerOptions extends WatchCompilerHost { + /** root files to use to generate program */ + rootFiles: string[]; + + /** Compiler options */ + options: CompilerOptions; + } + + /** + * Reports config file diagnostics + */ + export interface ConfigFileDiagnosticsReporter { + /** + * Reports the diagnostics in reading/writing or parsing of the config file + */ + onConfigFileDiagnostic: DiagnosticReporter; + + /** + * Reports unrecoverable error when parsing config file + */ + onUnRecoverableConfigFileDiagnostic: DiagnosticReporter; } - export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend: CompilerOptions = {}, watchingHost?: WatchingSystemHost) { - return createWatchMode(configParseResult.fileNames, configParseResult.options, watchingHost, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend); + /** + * Host to create watch with config file + */ + export interface WatchCompilerHostOfConfigFile extends WatchCompilerHost, ConfigFileDiagnosticsReporter { + /** Name of the config file to compile */ + configFileName: string; + + /** Options to extend */ + optionsToExtend?: CompilerOptions; + + /** + * Used to generate source file names from the config file and its include, exclude, files rules + * and also to cache the directory stucture + */ + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; } - export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions, watchingHost?: WatchingSystemHost) { - return createWatchMode(rootFileNames, compilerOptions, watchingHost); + /** + * Host to create watch with config file that is already parsed (from tsc) + */ + /*@internal*/ + export interface WatchCompilerHostOfConfigFile extends WatchCompilerHost { + rootFiles?: string[]; + options?: CompilerOptions; + optionsToExtend?: CompilerOptions; + configFileSpecs?: ConfigFileSpecs; + configFileWildCardDirectories?: MapLike; } - interface HostFileInfo { - version: number; - sourceFile: SourceFile; - fileWatcher: FileWatcher; + export interface Watch { + /** Synchronize with host and get updated program */ + getProgram(): T; + /** Gets the existing program without synchronizing with changes on host */ + /*@internal*/ + getCurrentProgram(): T; } - function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, watchingHost?: WatchingSystemHost, configFileName?: string, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike, optionsToExtendForConfigFile?: CompilerOptions) { - let program: Program; + /** + * Creates the watch what generates program using the config file + */ + export interface WatchOfConfigFile extends Watch { + } + + /** + * Creates the watch that generates program using the root files and compiler options + */ + export interface WatchOfFilesAndCompilerOptions extends Watch { + /** Updates the root files in the program, only if this is not config file compilation */ + updateRootFileNames(fileNames: string[]): void; + } + + /** + * Create the watch compiler host for either configFile or fileNames and its options + */ + export function createWatchCompilerHost(rootFiles: string[], options: CompilerOptions, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfFilesAndCompilerOptions; + export function createWatchCompilerHost(configFileName: string, optionsToExtend: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfConfigFile; + export function createWatchCompilerHost(rootFilesOrConfigFileName: string | string[], options: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfFilesAndCompilerOptions | WatchCompilerHostOfConfigFile { + if (isArray(rootFilesOrConfigFileName)) { + return createWatchCompilerHostOfFilesAndCompilerOptions(rootFilesOrConfigFileName, options, system, createProgram, reportDiagnostic, reportWatchStatus); + } + else { + return createWatchCompilerHostOfConfigFile(rootFilesOrConfigFileName, options, system, createProgram, reportDiagnostic, reportWatchStatus); + } + } + + /** + * Creates the watch from the host for root files and compiler options + */ + export function createWatchProgram(host: WatchCompilerHostOfFilesAndCompilerOptions): WatchOfFilesAndCompilerOptions; + /** + * Creates the watch from the host for config file + */ + export function createWatchProgram(host: WatchCompilerHostOfConfigFile): WatchOfConfigFile; + export function createWatchProgram(host: WatchCompilerHostOfFilesAndCompilerOptions & WatchCompilerHostOfConfigFile): WatchOfFilesAndCompilerOptions | WatchOfConfigFile { + interface HostFileInfo { + version: number; + sourceFile: SourceFile; + fileWatcher: FileWatcher; + } + + let builderProgram: T; let reloadLevel: ConfigFileProgramReloadLevel; // level to indicate if the program needs to be reloaded from config file/just filenames etc let missingFilesMap: Map; // Map of file watchers for the missing files let watchedWildcardDirectories: Map; // map of watchers for the wild card directories in the config file @@ -252,112 +443,143 @@ namespace ts { let hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations let hasChangedAutomaticTypeDirectiveNames = false; // True if the automatic type directives have changed - const loggingEnabled = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; - const writeLog: (s: string) => void = loggingEnabled ? s => { system.write(s); system.write(system.newLine); } : noop; + const useCaseSensitiveFileNames = host.useCaseSensitiveFileNames(); + const currentDirectory = host.getCurrentDirectory(); + const getCurrentDirectory = () => currentDirectory; + const readFile: (path: string, encoding?: string) => string | undefined = (path, encoding) => host.readFile(path, encoding); + const { configFileName, optionsToExtend: optionsToExtendForConfigFile = {}, createProgram } = host; + let { rootFiles: rootFileNames, options: compilerOptions, configFileSpecs, configFileWildCardDirectories } = host; + + const cachedDirectoryStructureHost = configFileName && createCachedDirectoryStructureHost(host, currentDirectory, useCaseSensitiveFileNames); + if (cachedDirectoryStructureHost && host.onCachedDirectoryStructureHostCreate) { + host.onCachedDirectoryStructureHostCreate(cachedDirectoryStructureHost); + } + const directoryStructureHost: DirectoryStructureHost = cachedDirectoryStructureHost || host; + const parseConfigFileHost: ParseConfigFileHost = { + useCaseSensitiveFileNames, + readDirectory: (path, extensions, exclude, include, depth) => directoryStructureHost.readDirectory(path, extensions, exclude, include, depth), + fileExists: path => host.fileExists(path), + readFile, + getCurrentDirectory, + onConfigFileDiagnostic: host.onConfigFileDiagnostic, + onUnRecoverableConfigFileDiagnostic: host.onUnRecoverableConfigFileDiagnostic + }; + + // From tsc we want to get already parsed result and hence check for rootFileNames + if (configFileName && !rootFileNames) { + parseConfigFile(); + } + + const trace = host.trace && ((s: string) => { host.trace(s + newLine); }); + const loggingEnabled = trace && (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics); + const writeLog = loggingEnabled ? trace : noop; const watchFile = compilerOptions.extendedDiagnostics ? ts.addFileWatcherWithLogging : loggingEnabled ? ts.addFileWatcherWithOnlyTriggerLogging : ts.addFileWatcher; const watchFilePath = compilerOptions.extendedDiagnostics ? ts.addFilePathWatcherWithLogging : ts.addFilePathWatcher; const watchDirectoryWorker = compilerOptions.extendedDiagnostics ? ts.addDirectoryWatcherWithLogging : ts.addDirectoryWatcher; - watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); - const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; - - const directoryStructureHost = configFileName ? createCachedDirectoryStructureHost(system) : system; if (configFileName) { - watchFile(system, configFileName, scheduleProgramReload, writeLog); + watchFile(host, configFileName, scheduleProgramReload, writeLog); } - const getCurrentDirectory = memoize(() => directoryStructureHost.getCurrentDirectory()); - const realpath = system.realpath && ((path: string) => system.realpath(path)); - const getCachedDirectoryStructureHost = configFileName && (() => directoryStructureHost as CachedDirectoryStructureHost); - const getCanonicalFileName = createGetCanonicalFileName(system.useCaseSensitiveFileNames); - let newLine = getNewLineCharacter(compilerOptions, system); + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + let newLine = updateNewLine(); const compilerHost: CompilerHost & ResolutionCacheHost = { // Members for CompilerHost getSourceFile: (fileName, languageVersion, onError?, shouldCreateNewSourceFile?) => getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile), getSourceFileByPath: getVersionedSourceFileByPath, - getDefaultLibLocation, - getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), - writeFile: notImplemented, + getDefaultLibLocation: host.getDefaultLibLocation && (() => host.getDefaultLibLocation()), + getDefaultLibFileName: options => host.getDefaultLibFileName(options), + writeFile, getCurrentDirectory, - useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, getCanonicalFileName, getNewLine: () => newLine, fileExists, - readFile: fileName => system.readFile(fileName), - trace: s => system.write(s + newLine), - directoryExists: directoryName => directoryStructureHost.directoryExists(directoryName), - getEnvironmentVariable: name => system.getEnvironmentVariable ? system.getEnvironmentVariable(name) : "", - getDirectories: path => directoryStructureHost.getDirectories(path), - realpath, - resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile), - resolveModuleNames: (moduleNames, containingFile, reusedNames?) => resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, /*logChanges*/ false), + readFile, + trace, + directoryExists: directoryStructureHost.directoryExists && (path => directoryStructureHost.directoryExists(path)), + getDirectories: directoryStructureHost.getDirectories && (path => directoryStructureHost.getDirectories(path)), + realpath: host.realpath && (s => host.realpath(s)), + getEnvironmentVariable: host.getEnvironmentVariable ? (name => host.getEnvironmentVariable(name)) : (() => ""), onReleaseOldSourceFile, + createHash: host.createHash && (data => host.createHash(data)), // Members for ResolutionCacheHost toPath, getCompilationSettings: () => compilerOptions, watchDirectoryOfFailedLookupLocation: watchDirectory, watchTypeRootsDirectory: watchDirectory, - getCachedDirectoryStructureHost, + getCachedDirectoryStructureHost: () => cachedDirectoryStructureHost, onInvalidatedResolution: scheduleProgramUpdate, onChangedAutomaticTypeDirectiveNames: () => { hasChangedAutomaticTypeDirectiveNames = true; scheduleProgramUpdate(); }, - maxNumberOfFilesToIterateForInvalidation: watchingHost.maxNumberOfFilesToIterateForInvalidation, + maxNumberOfFilesToIterateForInvalidation: host.maxNumberOfFilesToIterateForInvalidation, getCurrentProgram, writeLog }; // Cache for the module resolution const resolutionCache = createResolutionCache(compilerHost, configFileName ? - getDirectoryPath(getNormalizedAbsolutePath(configFileName, getCurrentDirectory())) : - getCurrentDirectory() + getDirectoryPath(getNormalizedAbsolutePath(configFileName, currentDirectory)) : + currentDirectory, + /*logChangesWhenResolvingModule*/ false ); - // There is no extra check needed since we can just rely on the program to decide emit - const builder = createBuilder({ getCanonicalFileName, computeHash }); - - clearHostScreen(); - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode)); + // Resolve module using host module resolution strategy if provided otherwise use resolution cache to resolve module names + compilerHost.resolveModuleNames = host.resolveModuleNames ? + ((moduleNames, containingFile, reusedNames) => host.resolveModuleNames(moduleNames, containingFile, reusedNames)) : + ((moduleNames, containingFile, reusedNames) => resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames)); + compilerHost.resolveTypeReferenceDirectives = host.resolveTypeReferenceDirectives ? + ((typeDirectiveNames, containingFile) => host.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile)) : + ((typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile)); + const userProvidedResolution = !!host.resolveModuleNames || !!host.resolveTypeReferenceDirectives; + + reportWatchDiagnostic(Diagnostics.Starting_compilation_in_watch_mode); synchronizeProgram(); // Update the wild card directory watch watchConfigFileWildCardDirectories(); - return getCurrentProgram; + return configFileName ? + { getCurrentProgram: getCurrentBuilderProgram, getProgram: synchronizeProgram } : + { getCurrentProgram: getCurrentBuilderProgram, getProgram: synchronizeProgram, updateRootFileNames }; + + function getCurrentBuilderProgram() { + return builderProgram; + } function getCurrentProgram() { - return program; + return builderProgram && builderProgram.getProgram(); } function synchronizeProgram() { writeLog(`Synchronizing program`); + const program = getCurrentProgram(); if (hasChangedCompilerOptions) { - newLine = getNewLineCharacter(compilerOptions, system); + newLine = updateNewLine(); if (program && changesAffectModuleResolution(program.getCompilerOptions(), compilerOptions)) { resolutionCache.clear(); } } - const hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(); - if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames)) { - return; + // All resolutions are invalid if user provided resolutions + const hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(userProvidedResolution); + if (isProgramUptoDate(getCurrentProgram(), rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames)) { + return builderProgram; } - beforeCompile(compilerOptions); - // Compile the program const needsUpdateInTypeRootWatch = hasChangedCompilerOptions || !program; hasChangedCompilerOptions = false; resolutionCache.startCachingPerDirectoryResolution(); compilerHost.hasInvalidatedResolution = hasInvalidatedResolution; compilerHost.hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames; - program = createProgram(rootFileNames, compilerOptions, compilerHost, program); + builderProgram = createProgram(rootFileNames, compilerOptions, compilerHost, builderProgram); resolutionCache.finishCachingPerDirectoryResolution(); - builder.updateProgram(program); // Update watches - updateMissingFilePathsWatch(program, missingFilesMap || (missingFilesMap = createMap()), watchMissingFilePath); + updateMissingFilePathsWatch(builderProgram.getProgram(), missingFilesMap || (missingFilesMap = createMap()), watchMissingFilePath); if (needsUpdateInTypeRootWatch) { resolutionCache.updateTypeRootsWatch(); } @@ -376,12 +598,25 @@ namespace ts { missingFilePathsRequestedForRelease = undefined; } - afterCompile(directoryStructureHost, program, builder); - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); + if (host.afterProgramCreate) { + host.afterProgramCreate(builderProgram); + } + reportWatchDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes); + return builderProgram; + } + + function updateRootFileNames(files: string[]) { + Debug.assert(!configFileName, "Cannot update root file names with config file watch mode"); + rootFileNames = files; + scheduleProgramUpdate(); + } + + function updateNewLine() { + return getNewLineCharacter(compilerOptions, () => host.getNewLine()); } function toPath(fileName: string) { - return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName); + return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } function fileExists(fileName: string) { @@ -394,10 +629,6 @@ namespace ts { return directoryStructureHost.fileExists(fileName); } - function getDefaultLibLocation(): string { - return getDirectoryPath(normalizePath(system.getExecutingFilePath())); - } - function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { const hostSourceFile = sourceFilesCache.get(path); // No source file on the host @@ -416,7 +647,7 @@ namespace ts { hostSourceFile.sourceFile = sourceFile; sourceFile.version = hostSourceFile.version.toString(); if (!hostSourceFile.fileWatcher) { - hostSourceFile.fileWatcher = watchFilePath(system, fileName, onSourceFileChange, path, writeLog); + hostSourceFile.fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog); } } else { @@ -428,9 +659,9 @@ namespace ts { else { let fileWatcher: FileWatcher; if (sourceFile) { - sourceFile.version = "0"; - fileWatcher = watchFilePath(system, fileName, onSourceFileChange, path, writeLog); - sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); + sourceFile.version = "1"; + fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog); + sourceFilesCache.set(path, { sourceFile, version: 1, fileWatcher }); } else { sourceFilesCache.set(path, "0"); @@ -444,7 +675,7 @@ namespace ts { let text: string; try { performance.mark("beforeIORead"); - text = system.readFile(fileName, compilerOptions.charset); + text = host.readFile(fileName, compilerOptions.charset); performance.mark("afterIORead"); performance.measure("I/O Read", "beforeIORead", "afterIORead"); } @@ -492,18 +723,24 @@ namespace ts { } } + function reportWatchDiagnostic(message: DiagnosticMessage) { + if (host.onWatchStatusChange) { + host.onWatchStatusChange(createCompilerDiagnostic(message), newLine); + } + } + // Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch // operations (such as saving all modified files in an editor) a chance to complete before we kick // off a new compilation. function scheduleProgramUpdate() { - if (!system.setTimeout || !system.clearTimeout) { + if (!host.setTimeout || !host.clearTimeout) { return; } if (timerToUpdateProgram) { - system.clearTimeout(timerToUpdateProgram); + host.clearTimeout(timerToUpdateProgram); } - timerToUpdateProgram = system.setTimeout(updateProgram, 250); + timerToUpdateProgram = host.setTimeout(updateProgram, 250); } function scheduleProgramReload() { @@ -512,17 +749,9 @@ namespace ts { scheduleProgramUpdate(); } - function clearHostScreen() { - if (watchingHost.system.clearScreen) { - watchingHost.system.clearScreen(); - } - } - function updateProgram() { - clearHostScreen(); - timerToUpdateProgram = undefined; - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); + reportWatchDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation); switch (reloadLevel) { case ConfigFileProgramReloadLevel.Partial: @@ -530,14 +759,15 @@ namespace ts { case ConfigFileProgramReloadLevel.Full: return reloadConfigFile(); default: - return synchronizeProgram(); + synchronizeProgram(); + return; } } function reloadFileNamesFromConfigFile() { - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, directoryStructureHost); + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, parseConfigFileHost); if (!configFileSpecs.filesSpecs && result.fileNames.length === 0) { - reportDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName)); + host.onConfigFileDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName)); } rootFileNames = result.fileNames; @@ -549,21 +779,25 @@ namespace ts { writeLog(`Reloading config file: ${configFileName}`); reloadLevel = ConfigFileProgramReloadLevel.None; - const cachedHost = directoryStructureHost as CachedDirectoryStructureHost; - cachedHost.clearCache(); - const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost, reportDiagnostic, reportWatchDiagnostic); - rootFileNames = configParseResult.fileNames; - compilerOptions = configParseResult.options; + if (cachedDirectoryStructureHost) { + cachedDirectoryStructureHost.clearCache(); + } + parseConfigFile(); hasChangedCompilerOptions = true; - configFileSpecs = configParseResult.configFileSpecs; - configFileWildCardDirectories = configParseResult.wildcardDirectories; - synchronizeProgram(); // Update the wild card directory watch watchConfigFileWildCardDirectories(); } + function parseConfigFile() { + const configParseResult = ts.parseConfigFile(configFileName, optionsToExtendForConfigFile, parseConfigFileHost); + rootFileNames = configParseResult.fileNames; + compilerOptions = configParseResult.options; + configFileSpecs = configParseResult.configFileSpecs; + configFileWildCardDirectories = configParseResult.wildcardDirectories; + } + function onSourceFileChange(fileName: string, eventKind: FileWatcherEventKind, path: Path) { updateCachedSystemWithFile(fileName, path, eventKind); const hostSourceFile = sourceFilesCache.get(path); @@ -573,7 +807,7 @@ namespace ts { resolutionCache.invalidateResolutionOfFile(path); if (!isString(hostSourceFile)) { hostSourceFile.fileWatcher.close(); - sourceFilesCache.set(path, (hostSourceFile.version++).toString()); + sourceFilesCache.set(path, (++hostSourceFile.version).toString()); } } else { @@ -593,17 +827,17 @@ namespace ts { } function updateCachedSystemWithFile(fileName: string, path: Path, eventKind: FileWatcherEventKind) { - if (configFileName) { - (directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFile(fileName, path, eventKind); + if (cachedDirectoryStructureHost) { + cachedDirectoryStructureHost.addOrDeleteFile(fileName, path, eventKind); } } function watchDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { - return watchDirectoryWorker(system, directory, cb, flags, writeLog); + return watchDirectoryWorker(host, directory, cb, flags, writeLog); } function watchMissingFilePath(missingFilePath: Path) { - return watchFilePath(system, missingFilePath, onMissingFileChange, missingFilePath, writeLog); + return watchFilePath(host, missingFilePath, onMissingFileChange, missingFilePath, writeLog); } function onMissingFileChange(fileName: string, eventKind: FileWatcherEventKind, missingFilePath: Path) { @@ -622,11 +856,16 @@ namespace ts { } function watchConfigFileWildCardDirectories() { - updateWatchingWildcardDirectories( - watchedWildcardDirectories || (watchedWildcardDirectories = createMap()), - createMapFromTemplate(configFileWildCardDirectories), - watchWildcardDirectory - ); + if (configFileWildCardDirectories) { + updateWatchingWildcardDirectories( + watchedWildcardDirectories || (watchedWildcardDirectories = createMap()), + createMapFromTemplate(configFileWildCardDirectories), + watchWildcardDirectory + ); + } + else if (watchedWildcardDirectories) { + clearMap(watchedWildcardDirectories, closeFileWatcherOf); + } } function watchWildcardDirectory(directory: string, flags: WatchDirectoryFlags) { @@ -638,7 +877,7 @@ namespace ts { const fileOrDirectoryPath = toPath(fileOrDirectory); // Since the file existance changed, update the sourceFiles cache - const result = (directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); + const result = cachedDirectoryStructureHost && cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); // Instead of deleting the file, mark it as changed instead // Many times node calls add/remove/file when watching directories recursively @@ -669,8 +908,29 @@ namespace ts { ); } - function computeHash(data: string) { - return system.createHash ? system.createHash(data) : data; + function ensureDirectoriesExist(directoryPath: string) { + if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { + const parentDirectory = getDirectoryPath(directoryPath); + ensureDirectoriesExist(parentDirectory); + host.createDirectory(directoryPath); + } + } + + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, onError: (message: string) => void) { + try { + performance.mark("beforeIOWrite"); + ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); + + host.writeFile(fileName, text, writeByteOrderMark); + + performance.mark("afterIOWrite"); + performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + } + catch (e) { + if (onError) { + onError(e.message); + } + } } } } diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 0bc6920479be4..f5c614d9d577c 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -2,6 +2,247 @@ /* @internal */ namespace ts { + /** + * Partial interface of the System thats needed to support the caching of directory structure + */ + export interface DirectoryStructureHost { + fileExists(path: string): boolean; + readFile(path: string, encoding?: string): string | undefined; + + directoryExists?(path: string): boolean; + getDirectories?(path: string): string[]; + readDirectory?(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + + createDirectory?(path: string): void; + writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void; + } + + interface FileAndDirectoryExistence { + fileExists: boolean; + directoryExists: boolean; + } + + export interface CachedDirectoryStructureHost extends DirectoryStructureHost { + useCaseSensitiveFileNames: boolean; + + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + + /** Returns the queried result for the file exists and directory exists if at all it was done */ + addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path): FileAndDirectoryExistence | undefined; + addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind): void; + clearCache(): void; + } + + interface MutableFileSystemEntries { + readonly files: string[]; + readonly directories: string[]; + } + + export function createCachedDirectoryStructureHost(host: DirectoryStructureHost, currentDirectory: string, useCaseSensitiveFileNames: boolean): CachedDirectoryStructureHost | undefined { + if (!host.getDirectories || !host.readDirectory) { + return undefined; + } + + const cachedReadDirectoryResult = createMap(); + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + return { + useCaseSensitiveFileNames, + fileExists, + readFile: (path, encoding) => host.readFile(path, encoding), + directoryExists: host.directoryExists && directoryExists, + getDirectories, + readDirectory, + createDirectory: host.createDirectory && createDirectory, + writeFile: host.writeFile && writeFile, + addOrDeleteFileOrDirectory, + addOrDeleteFile, + clearCache + }; + + function toPath(fileName: string) { + return ts.toPath(fileName, currentDirectory, getCanonicalFileName); + } + + function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined { + return cachedReadDirectoryResult.get(rootDirPath); + } + + function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined { + return getCachedFileSystemEntries(getDirectoryPath(path)); + } + + function getBaseNameOfFileName(fileName: string) { + return getBaseFileName(normalizePath(fileName)); + } + + function createCachedFileSystemEntries(rootDir: string, rootDirPath: Path) { + const resultFromHost: MutableFileSystemEntries = { + files: map(host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || [], + directories: host.getDirectories(rootDir) || [] + }; + + cachedReadDirectoryResult.set(rootDirPath, resultFromHost); + return resultFromHost; + } + + /** + * If the readDirectory result was already cached, it returns that + * Otherwise gets result from host and caches it. + * The host request is done under try catch block to avoid caching incorrect result + */ + function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined { + const cachedResult = getCachedFileSystemEntries(rootDirPath); + if (cachedResult) { + return cachedResult; + } + + try { + return createCachedFileSystemEntries(rootDir, rootDirPath); + } + catch (_e) { + // If there is exception to read directories, dont cache the result and direct the calls to host + Debug.assert(!cachedReadDirectoryResult.has(rootDirPath)); + return undefined; + } + } + + function fileNameEqual(name1: string, name2: string) { + return getCanonicalFileName(name1) === getCanonicalFileName(name2); + } + + function hasEntry(entries: ReadonlyArray, name: string) { + return some(entries, file => fileNameEqual(file, name)); + } + + function updateFileSystemEntry(entries: string[], baseName: string, isValid: boolean) { + if (hasEntry(entries, baseName)) { + if (!isValid) { + return filterMutate(entries, entry => !fileNameEqual(entry, baseName)); + } + } + else if (isValid) { + return entries.push(baseName); + } + } + + function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { + const path = toPath(fileName); + const result = getCachedFileSystemEntriesForBaseDir(path); + if (result) { + updateFilesOfFileSystemEntry(result, getBaseNameOfFileName(fileName), /*fileExists*/ true); + } + return host.writeFile(fileName, data, writeByteOrderMark); + } + + function fileExists(fileName: string): boolean { + const path = toPath(fileName); + const result = getCachedFileSystemEntriesForBaseDir(path); + return result && hasEntry(result.files, getBaseNameOfFileName(fileName)) || + host.fileExists(fileName); + } + + function directoryExists(dirPath: string): boolean { + const path = toPath(dirPath); + return cachedReadDirectoryResult.has(path) || host.directoryExists(dirPath); + } + + function createDirectory(dirPath: string) { + const path = toPath(dirPath); + const result = getCachedFileSystemEntriesForBaseDir(path); + const baseFileName = getBaseNameOfFileName(dirPath); + if (result) { + updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true); + } + host.createDirectory(dirPath); + } + + function getDirectories(rootDir: string): string[] { + const rootDirPath = toPath(rootDir); + const result = tryReadDirectory(rootDir, rootDirPath); + if (result) { + return result.directories.slice(); + } + return host.getDirectories(rootDir); + } + + function readDirectory(rootDir: string, extensions?: ReadonlyArray, excludes?: ReadonlyArray, includes?: ReadonlyArray, depth?: number): string[] { + const rootDirPath = toPath(rootDir); + const result = tryReadDirectory(rootDir, rootDirPath); + if (result) { + return matchFiles(rootDir, extensions, excludes, includes, useCaseSensitiveFileNames, currentDirectory, depth, getFileSystemEntries); + } + return host.readDirectory(rootDir, extensions, excludes, includes, depth); + + function getFileSystemEntries(dir: string) { + const path = toPath(dir); + if (path === rootDirPath) { + return result; + } + return tryReadDirectory(dir, path) || emptyFileSystemEntries; + } + } + + function addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path) { + const existingResult = getCachedFileSystemEntries(fileOrDirectoryPath); + if (existingResult) { + // Just clear the cache for now + // For now just clear the cache, since this could mean that multiple level entries might need to be re-evaluated + clearCache(); + return undefined; + } + + const parentResult = getCachedFileSystemEntriesForBaseDir(fileOrDirectoryPath); + if (!parentResult) { + return undefined; + } + + // This was earlier a file (hence not in cached directory contents) + // or we never cached the directory containing it + + if (!host.directoryExists) { + // Since host doesnt support directory exists, clear the cache as otherwise it might not be same + clearCache(); + return undefined; + } + + const baseName = getBaseNameOfFileName(fileOrDirectory); + const fsQueryResult: FileAndDirectoryExistence = { + fileExists: host.fileExists(fileOrDirectoryPath), + directoryExists: host.directoryExists(fileOrDirectoryPath) + }; + if (fsQueryResult.directoryExists || hasEntry(parentResult.directories, baseName)) { + // Folder added or removed, clear the cache instead of updating the folder and its structure + clearCache(); + } + else { + // No need to update the directory structure, just files + updateFilesOfFileSystemEntry(parentResult, baseName, fsQueryResult.fileExists); + } + return fsQueryResult; + + } + + function addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind) { + if (eventKind === FileWatcherEventKind.Changed) { + return; + } + + const parentResult = getCachedFileSystemEntriesForBaseDir(filePath); + if (parentResult) { + updateFilesOfFileSystemEntry(parentResult, getBaseNameOfFileName(fileName), eventKind === FileWatcherEventKind.Created); + } + } + + function updateFilesOfFileSystemEntry(parentResult: MutableFileSystemEntries, baseName: string, fileExists: boolean) { + updateFileSystemEntry(parentResult.files, baseName, fileExists); + } + + function clearCache() { + cachedReadDirectoryResult.clear(); + } + } + export enum ConfigFileProgramReloadLevel { None, /** Update the file name list from the disk */ @@ -90,53 +331,61 @@ namespace ts { return program.isEmittedFile(file); } - export function addFileWatcher(host: System, file: string, cb: FileWatcherCallback): FileWatcher { + export interface WatchFileHost { + watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; + } + + export function addFileWatcher(host: WatchFileHost, file: string, cb: FileWatcherCallback): FileWatcher { return host.watchFile(file, cb); } - export function addFileWatcherWithLogging(host: System, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { + export function addFileWatcherWithLogging(host: WatchFileHost, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { const watcherCaption = `FileWatcher:: `; return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ false, host, file, cb); } - export function addFileWatcherWithOnlyTriggerLogging(host: System, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { + export function addFileWatcherWithOnlyTriggerLogging(host: WatchFileHost, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { const watcherCaption = `FileWatcher:: `; return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ true, host, file, cb); } export type FilePathWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, filePath: Path) => void; - export function addFilePathWatcher(host: System, file: string, cb: FilePathWatcherCallback, path: Path): FileWatcher { + export function addFilePathWatcher(host: WatchFileHost, file: string, cb: FilePathWatcherCallback, path: Path): FileWatcher { return host.watchFile(file, (fileName, eventKind) => cb(fileName, eventKind, path)); } - export function addFilePathWatcherWithLogging(host: System, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { + export function addFilePathWatcherWithLogging(host: WatchFileHost, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { const watcherCaption = `FileWatcher:: `; return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ false, host, file, cb, path); } - export function addFilePathWatcherWithOnlyTriggerLogging(host: System, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { + export function addFilePathWatcherWithOnlyTriggerLogging(host: WatchFileHost, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { const watcherCaption = `FileWatcher:: `; return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ true, host, file, cb, path); } - export function addDirectoryWatcher(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { + export interface WatchDirectoryHost { + watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; + } + + export function addDirectoryWatcher(host: WatchDirectoryHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; return host.watchDirectory(directory, cb, recursive); } - export function addDirectoryWatcherWithLogging(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { + export function addDirectoryWatcherWithLogging(host: WatchDirectoryHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { const watcherCaption = `DirectoryWatcher ${(flags & WatchDirectoryFlags.Recursive) !== 0 ? "recursive" : ""}:: `; return createWatcherWithLogging(addDirectoryWatcher, watcherCaption, log, /*logOnlyTrigger*/ false, host, directory, cb, flags); } - export function addDirectoryWatcherWithOnlyTriggerLogging(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { + export function addDirectoryWatcherWithOnlyTriggerLogging(host: WatchDirectoryHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { const watcherCaption = `DirectoryWatcher ${(flags & WatchDirectoryFlags.Recursive) !== 0 ? "recursive" : ""}:: `; return createWatcherWithLogging(addDirectoryWatcher, watcherCaption, log, /*logOnlyTrigger*/ true, host, directory, cb, flags); } type WatchCallback = (fileName: string, cbOptional1?: T, optional?: U) => void; - type AddWatch = (host: System, file: string, cb: WatchCallback, optional?: U) => FileWatcher; - function createWatcherWithLogging(addWatch: AddWatch, watcherCaption: string, log: (s: string) => void, logOnlyTrigger: boolean, host: System, file: string, cb: WatchCallback, optional?: U): FileWatcher { + type AddWatch = (host: H, file: string, cb: WatchCallback, optional?: U) => FileWatcher; + function createWatcherWithLogging(addWatch: AddWatch, watcherCaption: string, log: (s: string) => void, logOnlyTrigger: boolean, host: H, file: string, cb: WatchCallback, optional?: U): FileWatcher { const info = `PathInfo: ${file}`; if (!logOnlyTrigger) { log(`${watcherCaption}Added: ${info}`); diff --git a/src/harness/unittests/builder.ts b/src/harness/unittests/builder.ts index a5d880fa55156..8808a151c0494 100644 --- a/src/harness/unittests/builder.ts +++ b/src/harness/unittests/builder.ts @@ -41,22 +41,97 @@ namespace ts { program = updateProgramFile(program, "/b.ts", "namespace B { export const x = 1; }"); assertChanges(["/b.js", "/a.js"]); }); + + it("keeps the file in affected files if cancellation token throws during the operation", () => { + const files: NamedSourceText[] = [ + { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") }, + { name: "/b.ts", text: SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, + { name: "/c.ts", text: SourceText.New("", "", "export const c = 0;") }, + { name: "/d.ts", text: SourceText.New("", "", "export const dd = 0;") }, + { name: "/e.ts", text: SourceText.New("", "", "export const ee = 0;") }, + ]; + + let program = newProgram(files, ["/d.ts", "/e.ts", "/a.ts"], {}); + const assertChanges = makeAssertChangesWithCancellationToken(() => program); + // No cancellation + assertChanges(["/d.js", "/e.js", "/c.js", "/b.js", "/a.js"]); + + // cancel when emitting a.ts + program = updateProgramFile(program, "/a.ts", "export function foo() { }"); + assertChanges(["/a.js"], 0); + // Change d.ts and verify previously pending a.ts is emitted as well + program = updateProgramFile(program, "/d.ts", "export function bar() { }"); + assertChanges(["/a.js", "/d.js"]); + + // Cancel when emitting b.js + program = updateProgramFile(program, "/b.ts", "export class b { foo() { c + 1; } }"); + program = updateProgramFile(program, "/d.ts", "export function bar2() { }"); + assertChanges(["/d.js", "/b.js", "/a.js"], 1); + // Change e.ts and verify previously b.js as well as a.js get emitted again since previous change was consumed completely but not d.ts + program = updateProgramFile(program, "/e.ts", "export function bar3() { }"); + assertChanges(["/b.js", "/a.js", "/e.js"]); + + // Cancel in the middle of affected files list after b.js emit + program = updateProgramFile(program, "/b.ts", "export class b { foo2() { c + 1; } }"); + assertChanges(["/b.js", "/a.js"], 1); + // Change e.ts and verify previously b.js as well as a.js get emitted again since previous change was consumed completely but not d.ts + program = updateProgramFile(program, "/e.ts", "export function bar5() { }"); + assertChanges(["/b.js", "/a.js", "/e.js"]); + }); }); function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray) => void { - const builder = createBuilder({ - getCanonicalFileName: identity, - computeHash: identity - }); + const host: BuilderProgramHost = { useCaseSensitiveFileNames: returnTrue }; + let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined; return fileNames => { const program = getProgram(); - builder.updateProgram(program); + builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram); const outputFileNames: string[] = []; - builder.emitChangedFiles(program, fileName => outputFileNames.push(fileName)); + // tslint:disable-next-line no-empty + while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName))) { + } assert.deepEqual(outputFileNames, fileNames); }; } + function makeAssertChangesWithCancellationToken(getProgram: () => Program): (fileNames: ReadonlyArray, cancelAfterEmitLength?: number) => void { + const host: BuilderProgramHost = { useCaseSensitiveFileNames: returnTrue }; + let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined; + let cancel = false; + const cancellationToken: CancellationToken = { + isCancellationRequested: () => cancel, + throwIfCancellationRequested: () => { + if (cancel) { + throw new OperationCanceledException(); + } + }, + }; + return (fileNames, cancelAfterEmitLength?: number) => { + cancel = false; + let operationWasCancelled = false; + const program = getProgram(); + builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram); + const outputFileNames: string[] = []; + try { + // tslint:disable-next-line no-empty + do { + assert.isFalse(cancel); + if (outputFileNames.length === cancelAfterEmitLength) { + cancel = true; + } + } while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName), cancellationToken)); + } + catch (e) { + assert.isFalse(operationWasCancelled); + assert(e instanceof OperationCanceledException, e.toString()); + operationWasCancelled = true; + } + assert.equal(cancel, operationWasCancelled); + assert.equal(operationWasCancelled, fileNames.length > cancelAfterEmitLength); + assert.deepEqual(outputFileNames, fileNames.slice(0, cancelAfterEmitLength)); + }; + } + function updateProgramFile(program: ProgramWithSourceTexts, fileName: string, fileContent: string): ProgramWithSourceTexts { return updateProgram(program, program.getRootFileNames(), program.getCompilerOptions(), files => { updateProgramText(files, fileName, fileContent); diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index 4a76a479b1831..454e97b313311 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -884,7 +884,6 @@ namespace ts { }); }); - import TestSystem = ts.TestFSWithWatch.TestServerHost; type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; import createTestSystem = ts.TestFSWithWatch.createWatchedSystem; import libFile = ts.TestFSWithWatch.libFile; @@ -910,30 +909,21 @@ namespace ts { return JSON.parse(JSON.stringify(filesOrOptions)); } - function createWatchingSystemHost(host: TestSystem) { - return ts.createWatchingSystemHost(/*pretty*/ undefined, host); - } - - function verifyProgramWithoutConfigFile(watchingSystemHost: WatchingSystemHost, rootFiles: string[], options: CompilerOptions) { - const program = createWatchModeWithoutConfigFile(rootFiles, options, watchingSystemHost)(); + function verifyProgramWithoutConfigFile(system: System, rootFiles: string[], options: CompilerOptions) { + const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, system)).getCurrentProgram().getProgram(); verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options)); } - function getConfigParseResult(watchingSystemHost: WatchingSystemHost, configFileName: string) { - return parseConfigFile(configFileName, {}, watchingSystemHost.system, watchingSystemHost.reportDiagnostic, watchingSystemHost.reportWatchDiagnostic); - } - - function verifyProgramWithConfigFile(watchingSystemHost: WatchingSystemHost, configFile: string) { - const result = getConfigParseResult(watchingSystemHost, configFile); - const program = createWatchModeWithConfigFile(result, {}, watchingSystemHost)(); - const { fileNames, options } = getConfigParseResult(watchingSystemHost, configFile); + function verifyProgramWithConfigFile(system: System, configFileName: string) { + const program = createWatchProgram(createWatchCompilerHostOfConfigFile(configFileName, {}, system)).getCurrentProgram().getProgram(); + const { fileNames, options } = parseConfigFileWithSystem(configFileName, {}, system, notImplemented); verifyProgramIsUptoDate(program, fileNames, options); } function verifyProgram(files: FileOrFolder[], rootFiles: string[], options: CompilerOptions, configFile: string) { - const watchingSystemHost = createWatchingSystemHost(createTestSystem(files)); - verifyProgramWithoutConfigFile(watchingSystemHost, rootFiles, options); - verifyProgramWithConfigFile(watchingSystemHost, configFile); + const system = createTestSystem(files); + verifyProgramWithoutConfigFile(system, rootFiles, options); + verifyProgramWithConfigFile(system, configFile); } it("has empty options", () => { @@ -1044,11 +1034,9 @@ namespace ts { }; const configFile: FileOrFolder = { path: "/src/tsconfig.json", - content: JSON.stringify({ compilerOptions, include: ["packages/**/ *.ts"] }) + content: JSON.stringify({ compilerOptions, include: ["packages/**/*.ts"] }) }; - - const watchingSystemHost = createWatchingSystemHost(createTestSystem([app, module1, module2, module3, libFile, configFile])); - verifyProgramWithConfigFile(watchingSystemHost, configFile.path); + verifyProgramWithConfigFile(createTestSystem([app, module1, module2, module3, libFile, configFile]), configFile.path); }); }); } diff --git a/src/harness/unittests/session.ts b/src/harness/unittests/session.ts index f46e173716c43..7d1e5b0816c87 100644 --- a/src/harness/unittests/session.ts +++ b/src/harness/unittests/session.ts @@ -4,6 +4,7 @@ const expect: typeof _chai.expect = _chai.expect; namespace ts.server { let lastWrittenToHost: string; + const noopFileWatcher: FileWatcher = { close: noop }; const mockHost: ServerHost = { args: [], newLine: "\n", @@ -26,6 +27,8 @@ namespace ts.server { setImmediate: () => 0, clearImmediate: noop, createHash: Harness.mockHash, + watchFile: () => noopFileWatcher, + watchDirectory: () => noopFileWatcher }; class TestSession extends Session { diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 9e01021155c72..a2b79b00dc76a 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -22,24 +22,16 @@ namespace ts.tscWatch { checkFileNames(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); } - function createWatchingSystemHost(system: WatchedSystem) { - return ts.createWatchingSystemHost(/*pretty*/ undefined, system); + function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = ts.createWatchCompilerHostOfConfigFile(configFileName, {}, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + return () => watch.getCurrentProgram().getProgram(); } - function parseConfigFile(configFileName: string, watchingSystemHost: WatchingSystemHost) { - return ts.parseConfigFile(configFileName, {}, watchingSystemHost.system, watchingSystemHost.reportDiagnostic, watchingSystemHost.reportWatchDiagnostic); - } - - function createWatchModeWithConfigFile(configFilePath: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { - const watchingSystemHost = createWatchingSystemHost(host); - watchingSystemHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const configFileResult = parseConfigFile(configFilePath, watchingSystemHost); - return ts.createWatchModeWithConfigFile(configFileResult, {}, watchingSystemHost); - } - - function createWatchModeWithoutConfigFile(fileNames: string[], host: WatchedSystem, options: CompilerOptions = {}) { - const watchingSystemHost = createWatchingSystemHost(host); - return ts.createWatchModeWithoutConfigFile(fileNames, options, watchingSystemHost); + function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}) { + const watch = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host)); + return () => watch.getCurrentProgram().getProgram(); } function getEmittedLineForMultiFileOutput(file: FileOrFolder, host: WatchedSystem) { @@ -218,7 +210,7 @@ namespace ts.tscWatch { content: `export let x: number` }; const host = createWatchedSystem([appFile, moduleFile, libFile]); - const watch = createWatchModeWithoutConfigFile([appFile.path], host); + const watch = createWatchOfFilesAndCompilerOptions([appFile.path], host); checkProgramActualFiles(watch(), [appFile.path, libFile.path, moduleFile.path]); @@ -243,7 +235,7 @@ namespace ts.tscWatch { const host = createWatchedSystem([f1, config], { useCaseSensitiveFileNames: false }); const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); - const watch = createWatchModeWithConfigFile(upperCaseConfigFilePath, host); + const watch = createWatchOfConfigFile(upperCaseConfigFilePath, host); checkProgramActualFiles(watch(), [combinePaths(getDirectoryPath(upperCaseConfigFilePath), getBaseFileName(f1.path))]); }); @@ -272,14 +264,10 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([configFile, libFile, file1, file2, file3]); - const watchingSystemHost = createWatchingSystemHost(host); - const configFileResult = parseConfigFile(configFile.path, watchingSystemHost); - assert.equal(configFileResult.errors.length, 0, `expect no errors in config file, got ${JSON.stringify(configFileResult.errors)}`); - - const watch = ts.createWatchModeWithConfigFile(configFileResult, {}, watchingSystemHost); + const watch = createWatchProgram(createWatchCompilerHostOfConfigFile(configFile.path, {}, host, /*createProgram*/ undefined, notImplemented)); - checkProgramActualFiles(watch(), [file1.path, libFile.path, file2.path]); - checkProgramRootFiles(watch(), [file1.path, file2.path]); + checkProgramActualFiles(watch.getCurrentProgram().getProgram(), [file1.path, libFile.path, file2.path]); + checkProgramRootFiles(watch.getCurrentProgram().getProgram(), [file1.path, file2.path]); checkWatchedFiles(host, [configFile.path, file1.path, file2.path, libFile.path]); const configDir = getDirectoryPath(configFile.path); checkWatchedDirectories(host, [configDir, combinePaths(configDir, projectSystem.nodeModulesAtTypes)], /*recursive*/ true); @@ -295,7 +283,7 @@ namespace ts.tscWatch { content: `{}` }; const host = createWatchedSystem([commonFile1, libFile, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); const configDir = getDirectoryPath(configFile.path); checkWatchedDirectories(host, [configDir, combinePaths(configDir, projectSystem.nodeModulesAtTypes)], /*recursive*/ true); @@ -319,7 +307,7 @@ namespace ts.tscWatch { }` }; const host = createWatchedSystem([commonFile1, commonFile2, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); const commonFile3 = "/a/b/commonFile3.ts"; checkProgramRootFiles(watch(), [commonFile1.path, commonFile3]); @@ -332,7 +320,7 @@ namespace ts.tscWatch { content: `{}` }; const host = createWatchedSystem([commonFile1, commonFile2, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); // delete commonFile2 @@ -354,7 +342,7 @@ namespace ts.tscWatch { let x = y` }; const host = createWatchedSystem([file1, libFile]); - const watch = createWatchModeWithoutConfigFile([file1.path], host); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); checkProgramRootFiles(watch(), [file1.path]); checkProgramActualFiles(watch(), [file1.path, libFile.path]); @@ -380,7 +368,7 @@ namespace ts.tscWatch { }; const files = [commonFile1, commonFile2, configFile]; const host = createWatchedSystem(files); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); configFile.content = `{ @@ -407,7 +395,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([commonFile1, commonFile2, excludedFile1, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); }); @@ -435,7 +423,7 @@ namespace ts.tscWatch { }; const files = [file1, nodeModuleFile, classicModuleFile, configFile]; const host = createWatchedSystem(files); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [file1.path]); checkProgramActualFiles(watch(), [file1.path, nodeModuleFile.path]); @@ -463,7 +451,7 @@ namespace ts.tscWatch { }` }; const host = createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); }); @@ -481,7 +469,7 @@ namespace ts.tscWatch { content: `export let y = 1;` }; const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file1.path], host); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); checkProgramRootFiles(watch(), [file1.path]); checkProgramActualFiles(watch(), [file1.path, file2.path]); @@ -510,7 +498,7 @@ namespace ts.tscWatch { content: `export let y = 1;` }; const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file1.path], host); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); host.reloadFS([file1, file3]); @@ -533,7 +521,7 @@ namespace ts.tscWatch { content: `export let y = 1;` }; const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file1.path, file3.path], host); + const watch = createWatchOfFilesAndCompilerOptions([file1.path, file3.path], host); checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); host.reloadFS([file1, file3]); @@ -561,7 +549,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file1, file2, file3, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [file2.path, file3.path]); checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); @@ -583,10 +571,10 @@ namespace ts.tscWatch { content: "export let y = 1;" }; const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file2.path, file3.path], host); + const watch = createWatchOfFilesAndCompilerOptions([file2.path, file3.path], host); checkProgramActualFiles(watch(), [file2.path, file3.path]); - const watch2 = createWatchModeWithoutConfigFile([file1.path], host); + const watch2 = createWatchOfFilesAndCompilerOptions([file1.path], host); checkProgramActualFiles(watch2(), [file1.path, file2.path, file3.path]); // Previous program shouldnt be updated @@ -609,7 +597,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file1, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramActualFiles(watch(), [file1.path]); host.reloadFS([file1, file2, configFile]); @@ -634,7 +622,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file1, file2, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramActualFiles(watch(), [file1.path]); @@ -664,7 +652,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file1, file2, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramActualFiles(watch(), [file1.path, file2.path]); const modifiedConfigFile = { @@ -692,7 +680,7 @@ namespace ts.tscWatch { content: JSON.stringify({ compilerOptions: {} }) }; const host = createWatchedSystem([file1, file2, libFile, config]); - const watch = createWatchModeWithConfigFile(config.path, host); + const watch = createWatchOfConfigFile(config.path, host); checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); @@ -716,7 +704,7 @@ namespace ts.tscWatch { content: "{" }; const host = createWatchedSystem([file1, corruptedConfig]); - const watch = createWatchModeWithConfigFile(corruptedConfig.path, host); + const watch = createWatchOfConfigFile(corruptedConfig.path, host); checkProgramActualFiles(watch(), [file1.path]); }); @@ -766,7 +754,7 @@ namespace ts.tscWatch { }) }; const host = createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); - const watch = createWatchModeWithConfigFile(config1.path, host); + const watch = createWatchOfConfigFile(config1.path, host); checkProgramActualFiles(watch(), [libES5.path, app.path]); @@ -791,7 +779,7 @@ namespace ts.tscWatch { }) }; const host = createWatchedSystem([f, config]); - const watch = createWatchModeWithConfigFile(config.path, host); + const watch = createWatchOfConfigFile(config.path, host); checkProgramActualFiles(watch(), [f.path]); }); @@ -805,7 +793,7 @@ namespace ts.tscWatch { content: 'import * as T from "./moduleFile"; T.bar();' }; const host = createWatchedSystem([moduleFile, file1, libFile]); - const watch = createWatchModeWithoutConfigFile([file1.path], host); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); const moduleFileOldPath = moduleFile.path; @@ -837,7 +825,7 @@ namespace ts.tscWatch { content: `{}` }; const host = createWatchedSystem([moduleFile, file1, configFile, libFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); const moduleFileOldPath = moduleFile.path; @@ -872,7 +860,7 @@ namespace ts.tscWatch { path: "/a/c" }; const host = createWatchedSystem([f1, config, node, cwd], { currentDirectory: cwd.path }); - const watch = createWatchModeWithConfigFile(config.path, host); + const watch = createWatchOfConfigFile(config.path, host); checkProgramActualFiles(watch(), [f1.path, node.path]); }); @@ -887,7 +875,7 @@ namespace ts.tscWatch { content: 'import * as T from "./moduleFile"; T.bar();' }; const host = createWatchedSystem([file1, libFile]); - const watch = createWatchModeWithoutConfigFile([file1.path], host); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); checkOutputErrors(host, [ getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") @@ -914,7 +902,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file, configFile, libFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkOutputErrors(host, [ getUnknownCompilerOption(watch(), configFile, "foo"), getUnknownCompilerOption(watch(), configFile, "allowJS") @@ -934,7 +922,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file, configFile, libFile]); - createWatchModeWithConfigFile(configFile.path, host); + createWatchOfConfigFile(configFile.path, host); checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); }); @@ -951,7 +939,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file, configFile, libFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); configFile.content = `{ @@ -987,7 +975,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file1, configFile, libFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramActualFiles(watch(), [libFile.path]); }); @@ -1013,7 +1001,7 @@ namespace ts.tscWatch { content: `export const x: number` }; const host = createWatchedSystem([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); - const watch = createWatchModeWithConfigFile(config.path, host); + const watch = createWatchOfConfigFile(config.path, host); checkProgramActualFiles(watch(), [t1.path, t2.path]); }); @@ -1024,7 +1012,7 @@ namespace ts.tscWatch { content: "let x = 1" }; const host = createWatchedSystem([f, libFile]); - const watch = createWatchModeWithoutConfigFile([f.path], host, { allowNonTsExtensions: true }); + const watch = createWatchOfFilesAndCompilerOptions([f.path], host, { allowNonTsExtensions: true }); checkProgramActualFiles(watch(), [f.path, libFile.path]); }); @@ -1052,7 +1040,7 @@ namespace ts.tscWatch { const files = [file, libFile, configFile]; const host = createWatchedSystem(files); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); const errors = () => [ getDiagnosticOfFile(watch().getCompilerOptions().configFile, configFile.content.indexOf('"allowJs"'), '"allowJs"'.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, "allowJs", "declaration"), getDiagnosticOfFile(watch().getCompilerOptions().configFile, configFile.content.indexOf('"declaration"'), '"declaration"'.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, "allowJs", "declaration") @@ -1089,7 +1077,7 @@ namespace ts.tscWatch { }) }; const host = createWatchedSystem([file1, file2, libFile, tsconfig], { currentDirectory: proj }); - const watch = createWatchModeWithConfigFile(tsconfig.path, host, /*maxNumberOfFilesToIterateForInvalidation*/1); + const watch = createWatchOfConfigFile(tsconfig.path, host, /*maxNumberOfFilesToIterateForInvalidation*/1); checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); assert.isTrue(host.fileExists("build/file1.js")); @@ -1138,7 +1126,7 @@ namespace ts.tscWatch { const files = [f1, f2, config, libFile]; host.reloadFS(files); - createWatchModeWithConfigFile(config.path, host); + createWatchOfConfigFile(config.path, host); const allEmittedLines = getEmittedLines(files); checkOutputContains(host, allEmittedLines); @@ -1200,7 +1188,7 @@ namespace ts.tscWatch { mapOfFilesWritten.set(p, count ? count + 1 : 1); return originalWriteFile(p, content); }; - createWatchModeWithConfigFile(configFile.path, host); + createWatchOfConfigFile(configFile.path, host); if (useOutFile) { // Only out file assert.equal(mapOfFilesWritten.size, 1); @@ -1284,7 +1272,7 @@ namespace ts.tscWatch { host.reloadFS(firstReloadFileList ? getFiles(firstReloadFileList) : files); // Initial compile - createWatchModeWithConfigFile(configFile.path, host); + createWatchOfConfigFile(configFile.path, host); if (firstCompilationEmitFiles) { checkAffectedLines(host, getFiles(firstCompilationEmitFiles), allEmittedFiles); } @@ -1595,11 +1583,11 @@ namespace ts.tscWatch { // Initial compile if (configFile) { - createWatchModeWithConfigFile(configFile.path, host); + createWatchOfConfigFile(configFile.path, host); } else { // First file as the root - createWatchModeWithoutConfigFile([files[0].path], host, { listEmittedFiles: true }); + createWatchOfFilesAndCompilerOptions([files[0].path], host, { listEmittedFiles: true }); } checkOutputContains(host, allEmittedFiles); @@ -1719,7 +1707,7 @@ namespace ts.tscWatch { const files = [root, imported, libFile]; const host = createWatchedSystem(files); - const watch = createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); @@ -1820,7 +1808,7 @@ namespace ts.tscWatch { return originalFileExists.call(host, fileName); }; - const watch = createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); checkOutputErrors(host, [ @@ -1862,7 +1850,7 @@ namespace ts.tscWatch { return originalFileExists.call(host, fileName); }; - const watch = createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); @@ -1911,7 +1899,7 @@ declare module "fs" { const filesWithNodeType = files.concat(packageJson, nodeType); const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); - const watch = createWatchModeWithoutConfigFile([root.path], host, { }); + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); checkOutputErrors(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") @@ -1953,7 +1941,7 @@ declare module "fs" { const files = [root, file, libFile]; const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); - const watch = createWatchModeWithoutConfigFile([root.path, file.path], host, {}); + const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); checkOutputErrors(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") @@ -1995,7 +1983,7 @@ declare module "fs" { const outDirFolder = "/a/b/projects/myProject/dist/"; const programFiles = [file1, file2, module1, libFile]; const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramActualFiles(watch(), programFiles.map(f => f.path)); checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); const expectedFiles: ExpectedFile[] = [ @@ -2072,7 +2060,7 @@ declare module "fs" { }; const files = [configFile, file1, file2, libFile]; const host = createWatchedSystem(files); - const watch = createWatchModeWithConfigFile(configFile.path, host); + const watch = createWatchOfConfigFile(configFile.path, host); checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); file1.content = "var zz30 = 100;"; @@ -2094,7 +2082,7 @@ declare module "fs" { }; const host = createWatchedSystem([file]); - createWatchModeWithoutConfigFile([file.path], host); + createWatchOfFilesAndCompilerOptions([file.path], host); host.runQueuedTimeoutCallbacks(); host.checkScreenClears(1); @@ -2106,7 +2094,7 @@ declare module "fs" { content: "" }; const host = createWatchedSystem([file]); - createWatchModeWithoutConfigFile([file.path], host); + createWatchOfFilesAndCompilerOptions([file.path], host); const modifiedFile = { ...file, diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 90129e8e0f55b..381917d71c7f0 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -479,7 +479,7 @@ interface Array {}` private invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind) { const callbacks = this.watchedFiles.get(this.toPath(fileFullPath)); - invokeWatcherCallbacks(callbacks, ({ cb, fileName }) => cb(fileName, eventKind)); + invokeWatcherCallbacks(callbacks, ({ cb }) => cb(fileFullPath, eventKind)); } private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ade58a5c43780..575e00c5b0e6c 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1495,7 +1495,7 @@ namespace ts.server { } private createConfiguredProject(configFileName: NormalizedPath) { - const cachedDirectoryStructureHost = createCachedDirectoryStructureHost(this.host); + const cachedDirectoryStructureHost = createCachedDirectoryStructureHost(this.host, this.host.getCurrentDirectory(), this.host.useCaseSensitiveFileNames); const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedDirectoryStructureHost); this.logger.info(`Opened configuration file ${configFileName}`); const languageServiceEnabled = !this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); @@ -1800,11 +1800,11 @@ namespace ts.server { return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); } - getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) { return this.getOrCreateScriptInfoWorker(fileName, this.currentDirectory, openedByClient, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn); } - private getOrCreateScriptInfoWorker(fileName: NormalizedPath, currentDirectory: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { + private getOrCreateScriptInfoWorker(fileName: NormalizedPath, currentDirectory: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) { Debug.assert(fileContent === undefined || openedByClient, "ScriptInfo needs to be opened by client to be able to set its user defined content"); const path = normalizedPathToPath(fileName, currentDirectory, this.toCanonicalFileName); let info = this.getScriptInfoForPath(path); diff --git a/src/server/project.ts b/src/server/project.ts index a25e6870bdab2..140bab4fc06ca 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -3,7 +3,7 @@ /// /// /// -/// +/// namespace ts.server { @@ -140,7 +140,7 @@ namespace ts.server { /*@internal*/ resolutionCache: ResolutionCache; - private builder: Builder; + private builderState: BuilderState | undefined; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ @@ -202,6 +202,9 @@ namespace ts.server { /*@internal*/ readonly currentDirectory: string; + /*@internal*/ + public directoryStructureHost: DirectoryStructureHost; + /*@internal*/ constructor( /*@internal*/readonly projectName: string, @@ -212,8 +215,9 @@ namespace ts.server { languageServiceEnabled: boolean, private compilerOptions: CompilerOptions, public compileOnSaveEnabled: boolean, - /*@internal*/public directoryStructureHost: DirectoryStructureHost, + directoryStructureHost: DirectoryStructureHost, currentDirectory: string | undefined) { + this.directoryStructureHost = directoryStructureHost; this.currentDirectory = this.projectService.getNormalizedAbsolutePath(currentDirectory || ""); this.cancellationToken = new ThrottledCancellationToken(this.projectService.cancellationToken, this.projectService.throttleWaitMilliseconds); @@ -238,7 +242,7 @@ namespace ts.server { } // Use the current directory as resolution root only if the project created using current directory string - this.resolutionCache = createResolutionCache(this, currentDirectory && this.currentDirectory); + this.resolutionCache = createResolutionCache(this, currentDirectory && this.currentDirectory, /*logChangesWhenResolvingModule*/ true); this.languageService = createLanguageService(this, this.documentRegistry); if (!languageServiceEnabled) { this.disableLanguageService(); @@ -267,7 +271,7 @@ namespace ts.server { } getNewLine() { - return this.directoryStructureHost.newLine; + return this.projectService.host.newLine; } getProjectVersion() { @@ -335,7 +339,7 @@ namespace ts.server { } useCaseSensitiveFileNames() { - return this.directoryStructureHost.useCaseSensitiveFileNames; + return this.projectService.host.useCaseSensitiveFileNames; } readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { @@ -343,7 +347,7 @@ namespace ts.server { } readFile(fileName: string): string | undefined { - return this.directoryStructureHost.readFile(fileName); + return this.projectService.host.readFile(fileName); } fileExists(file: string): boolean { @@ -354,7 +358,7 @@ namespace ts.server { } resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModuleFull[] { - return this.resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, /*logChanges*/ true); + return this.resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames); } resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { @@ -369,6 +373,11 @@ namespace ts.server { return this.directoryStructureHost.getDirectories(path); } + /*@internal*/ + getCachedDirectoryStructureHost(): CachedDirectoryStructureHost { + return undefined; + } + /*@internal*/ toPath(fileName: string) { return toPath(fileName, this.currentDirectory, this.projectService.toCanonicalFileName); @@ -443,15 +452,6 @@ namespace ts.server { return this.languageService; } - private ensureBuilder() { - if (!this.builder) { - this.builder = createBuilder({ - getCanonicalFileName: this.projectService.toCanonicalFileName, - computeHash: data => this.projectService.host.createHash(data) - }); - } - } - private shouldEmitFile(scriptInfo: ScriptInfo) { return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent(); } @@ -461,8 +461,8 @@ namespace ts.server { return []; } this.updateGraph(); - this.ensureBuilder(); - return mapDefined(this.builder.getFilesAffectedBy(this.program, scriptInfo.path), + this.builderState = BuilderState.create(this.program, this.projectService.toCanonicalFileName, this.builderState); + return mapDefined(BuilderState.getFilesAffectedBy(this.builderState, this.program, scriptInfo.path, this.cancellationToken, data => this.projectService.host.createHash(data)), sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined); } @@ -498,6 +498,7 @@ namespace ts.server { } this.languageService.cleanupSemanticCache(); this.languageServiceEnabled = false; + this.builderState = undefined; this.resolutionCache.closeTypeRootsWatch(); this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false); } @@ -557,7 +558,7 @@ namespace ts.server { this.rootFilesMap = undefined; this.externalFiles = undefined; this.program = undefined; - this.builder = undefined; + this.builderState = undefined; this.resolutionCache.clear(); this.resolutionCache = undefined; this.cachedUnresolvedImportsPerFile = undefined; @@ -813,15 +814,9 @@ namespace ts.server { if (this.setTypings(cachedTypings)) { hasChanges = this.updateGraphWorker() || hasChanges; } - if (this.builder) { - this.builder.updateProgram(this.program); - } } else { this.lastCachedUnresolvedImportsList = undefined; - if (this.builder) { - this.builder.clear(); - } } if (hasChanges) { @@ -921,7 +916,7 @@ namespace ts.server { missingFilePath, (fileName, eventKind) => { if (this.projectKind === ProjectKind.Configured) { - (this.directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFile(fileName, missingFilePath, eventKind); + this.getCachedDirectoryStructureHost().addOrDeleteFile(fileName, missingFilePath, eventKind); } if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 051279ff3cb3f..dbadf5c88e7ba 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -345,7 +345,7 @@ namespace ts.server { detachAllProjects() { for (const p of this.containingProjects) { if (p.projectKind === ProjectKind.Configured) { - (p.directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted); + p.getCachedDirectoryStructureHost().addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted); } const isInfoRoot = p.isRoot(this); // detach is unnecessary since we'll clean the list of containing projects anyways diff --git a/src/server/types.ts b/src/server/types.ts index 32132ed278b22..93ffeeccff197 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -11,6 +11,8 @@ declare namespace ts.server { 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; setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; clearTimeout(timeoutId: any): void; setImmediate(callback: (...args: any[]) => void, ...args: any[]): any; @@ -129,4 +131,4 @@ declare namespace ts.server { createDirectory(path: string): void; watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; } -} \ No newline at end of file +} diff --git a/src/services/services.ts b/src/services/services.ts index 1692da9872b93..00442f73b0422 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1255,7 +1255,7 @@ namespace ts { getCancellationToken: () => cancellationToken, getCanonicalFileName, useCaseSensitiveFileNames: () => useCaseSensitivefileNames, - getNewLine: () => getNewLineCharacter(newSettings, { newLine: getNewLineOrDefaultFromHost(host) }), + getNewLine: () => getNewLineCharacter(newSettings, () => getNewLineOrDefaultFromHost(host)), getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: noop, getCurrentDirectory: () => currentDirectory, diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index d73014a93a24a..13a7a30d845fb 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -1,4 +1,4 @@ -{ +{ "extends": "../tsconfig-base", "compilerOptions": { "removeComments": false, @@ -37,6 +37,11 @@ "../compiler/declarationEmitter.ts", "../compiler/emitter.ts", "../compiler/program.ts", + "../compiler/builderState.ts", + "../compiler/builder.ts", + "../compiler/resolutionCache.ts", + "../compiler/watch.ts", + "../compiler/watchUtilities.ts", "../compiler/commandLineParser.ts", "../compiler/diagnosticInformationMap.generated.ts", "types.ts", diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 887fb50f76ab0..cd1b577b71173 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2506,6 +2506,7 @@ declare namespace ts { */ resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): (ResolvedTypeReferenceDirective | undefined)[]; getEnvironmentVariable?(name: string): string; + createHash?(data: string): string; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -2829,26 +2830,14 @@ declare namespace ts { callback: FileWatcherCallback; mtime?: Date; } - /** - * Partial interface of the System thats needed to support the caching of directory structure - */ - interface DirectoryStructureHost { + interface System { + args: string[]; newLine: string; useCaseSensitiveFileNames: boolean; write(s: string): void; readFile(path: string, encoding?: string): string | undefined; - writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; - fileExists(path: string): boolean; - directoryExists(path: string): boolean; - createDirectory(path: string): void; - getCurrentDirectory(): string; - getDirectories(path: string): string[]; - readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; - exit(exitCode?: number): void; - } - interface System extends DirectoryStructureHost { - args: string[]; getFileSize?(path: string): number; + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; /** * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that * use native OS file watching @@ -2856,7 +2845,13 @@ declare namespace ts { watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; resolvePath(path: string): string; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + createDirectory(path: string): void; getExecutingFilePath(): string; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; /** * This should be cryptographically secure. @@ -2864,6 +2859,7 @@ declare namespace ts { */ createHash?(data: string): string; getMemoryUsage?(): number; + exit(exitCode?: number): void; realpath?(path: string): string; setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; clearTimeout?(timeoutId: any): void; @@ -3870,17 +3866,6 @@ declare namespace ts { declare namespace ts { function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers): Printer; } -declare namespace ts { - interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - } - interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } -} declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string | undefined; function resolveTripleslashReference(moduleName: string, containingFile: string): string; @@ -4815,6 +4800,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; setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; clearTimeout(timeoutId: any): void; setImmediate(callback: (...args: any[]) => void, ...args: any[]): any; @@ -7351,6 +7338,17 @@ declare namespace ts.server { onProjectClosed(project: Project): void; } } +declare namespace ts { + interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + } + interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; + } +} declare namespace ts.server { enum ProjectKind { Inferred = 0, @@ -7394,7 +7392,6 @@ declare namespace ts.server { private documentRegistry; private compilerOptions; compileOnSaveEnabled: boolean; - directoryStructureHost: DirectoryStructureHost; private rootFiles; private rootFilesMap; private program; @@ -7407,7 +7404,7 @@ declare namespace ts.server { languageServiceEnabled: boolean; readonly trace?: (s: string) => void; readonly realpath?: (path: string) => string; - private builder; + private builderState; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ @@ -7468,7 +7465,6 @@ declare namespace ts.server { getGlobalProjectErrors(): ReadonlyArray; getAllProjectErrors(): ReadonlyArray; getLanguageService(ensureSynchronized?: boolean): LanguageService; - private ensureBuilder(); private shouldEmitFile(scriptInfo); getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[]; /** @@ -7863,7 +7859,9 @@ declare namespace ts.server { getScriptInfo(uncheckedFileName: string): ScriptInfo; private watchClosedScriptInfo(info); private stopWatchingScriptInfo(info); - getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost): ScriptInfo; + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { + fileExists(path: string): boolean; + }): ScriptInfo; private getOrCreateScriptInfoWorker(fileName, currentDirectory, openedByClient, fileContent?, scriptKind?, hasMixedContent?, hostToQueryFileExistsOn?); /** * This gets the script info for the normalized path. If the path is not rooted disk path then the open script info with project root context is preferred diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d5259f39f7fc2..3cbca96f8fa97 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2506,6 +2506,7 @@ declare namespace ts { */ resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): (ResolvedTypeReferenceDirective | undefined)[]; getEnvironmentVariable?(name: string): string; + createHash?(data: string): string; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -2829,26 +2830,14 @@ declare namespace ts { callback: FileWatcherCallback; mtime?: Date; } - /** - * Partial interface of the System thats needed to support the caching of directory structure - */ - interface DirectoryStructureHost { + interface System { + args: string[]; newLine: string; useCaseSensitiveFileNames: boolean; write(s: string): void; readFile(path: string, encoding?: string): string | undefined; - writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; - fileExists(path: string): boolean; - directoryExists(path: string): boolean; - createDirectory(path: string): void; - getCurrentDirectory(): string; - getDirectories(path: string): string[]; - readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; - exit(exitCode?: number): void; - } - interface System extends DirectoryStructureHost { - args: string[]; getFileSize?(path: string): number; + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; /** * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that * use native OS file watching @@ -2856,7 +2845,13 @@ declare namespace ts { watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; resolvePath(path: string): string; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + createDirectory(path: string): void; getExecutingFilePath(): string; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; /** * This should be cryptographically secure. @@ -2864,6 +2859,7 @@ declare namespace ts { */ createHash?(data: string): string; getMemoryUsage?(): number; + exit(exitCode?: number): void; realpath?(path: string): string; setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; clearTimeout?(timeoutId: any): void; @@ -3817,17 +3813,6 @@ declare namespace ts { declare namespace ts { function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers): Printer; } -declare namespace ts { - interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - } - interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } -} declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string | undefined; function resolveTripleslashReference(moduleName: string, containingFile: string): string; @@ -3857,6 +3842,258 @@ declare namespace ts { */ function createProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program; } +declare namespace ts { + interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + } + interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; + } +} +declare namespace ts { + type AffectedFileResult = { + result: T; + affected: SourceFile | Program; + } | undefined; + interface BuilderProgramHost { + /** + * return true if file names are treated with case sensitivity + */ + useCaseSensitiveFileNames(): boolean; + /** + * If provided this would be used this hash instead of actual file shape text for detecting changes + */ + createHash?: (data: string) => string; + /** + * When emit or emitNextAffectedFile are called without writeFile, + * this callback if present would be used to write files + */ + writeFile?: WriteFileCallback; + } + /** + * Builder to manage the program state changes + */ + interface BuilderProgram { + /** + * Returns current program + */ + getProgram(): Program; + /** + * Get compiler options of the program + */ + getCompilerOptions(): CompilerOptions; + /** + * Get the source file in the program with file name + */ + getSourceFile(fileName: string): SourceFile | undefined; + /** + * Get a list of files in the program + */ + getSourceFiles(): ReadonlyArray; + /** + * Get the diagnostics for compiler options + */ + getOptionsDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Get the diagnostics that dont belong to any file + */ + getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Get the syntax diagnostics, for all source files if source file is not supplied + */ + getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Get all the dependencies of the file + */ + getAllDependencies(sourceFile: SourceFile): ReadonlyArray; + /** + * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program + * The semantic diagnostics are cached and managed here + * Note that it is assumed that when asked about semantic diagnostics through this API, + * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics + * In case of SemanticDiagnosticsBuilderProgram if the source file is not provided, + * it will iterate through all the affected files, to ensure that cache stays valid and yet provide a way to get all semantic diagnostics + */ + getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Emits the JavaScript and declaration files. + * When targetSource file is specified, emits the files corresponding to that source file, + * otherwise for the whole program. + * In case of EmitAndSemanticDiagnosticsBuilderProgram, when targetSourceFile is specified, + * it is assumed that that file is handled from affected file list. If targetSourceFile is not specified, + * it will only emit all the affected files instead of whole program + * + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult; + /** + * Get the current directory of the program + */ + getCurrentDirectory(): string; + } + /** + * The builder that caches the semantic diagnostics for the program and handles the changed files and affected files + */ + interface SemanticDiagnosticsBuilderProgram extends BuilderProgram { + /** + * Gets the semantic diagnostics from the program for the next affected file and caches it + * Returns undefined if the iteration is complete + */ + getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult>; + } + /** + * The builder that can handle the changes in program and iterate through changed file to emit the files + * The semantic diagnostics are cached per file and managed by clearing for the changed/affected files + */ + interface EmitAndSemanticDiagnosticsBuilderProgram extends BuilderProgram { + /** + * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult; + } + /** + * Create the builder to manage semantic diagnostics and cache them + */ + function createSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: SemanticDiagnosticsBuilderProgram): SemanticDiagnosticsBuilderProgram; + function createSemanticDiagnosticsBuilderProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: SemanticDiagnosticsBuilderProgram): SemanticDiagnosticsBuilderProgram; + /** + * Create the builder that can handle the changes in program and iterate through changed files + * to emit the those files and manage semantic diagnostics cache as well + */ + function createEmitAndSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram): EmitAndSemanticDiagnosticsBuilderProgram; + function createEmitAndSemanticDiagnosticsBuilderProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram): EmitAndSemanticDiagnosticsBuilderProgram; + /** + * Creates a builder thats just abstraction over program and can be used with watch + */ + function createAbstractBuilder(newProgram: Program, host: BuilderProgramHost, oldProgram?: BuilderProgram): BuilderProgram; + function createAbstractBuilder(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: BuilderProgram): BuilderProgram; +} +declare namespace ts { + type DiagnosticReporter = (diagnostic: Diagnostic) => void; + type WatchStatusReporter = (diagnostic: Diagnostic, newLine: string) => void; + type CreateProgram = (rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: T) => T; + interface WatchCompilerHost { + /** + * Used to create the program when need for program creation or recreation detected + */ + createProgram: CreateProgram; + /** If provided, callback to invoke after every new program creation */ + afterProgramCreate?(program: T): void; + /** If provided, called with Diagnostic message that informs about change in watch status */ + onWatchStatusChange?(diagnostic: Diagnostic, newLine: string): void; + useCaseSensitiveFileNames(): boolean; + getNewLine(): string; + getCurrentDirectory(): string; + getDefaultLibFileName(options: CompilerOptions): string; + getDefaultLibLocation?(): string; + createHash?(data: string): string; + /** + * Use to check file presence for source files and + * if resolveModuleNames is not provided (complier is in charge of module resolution) then module files as well + */ + fileExists(path: string): boolean; + /** + * Use to read file text for source files and + * if resolveModuleNames is not provided (complier is in charge of module resolution) then module files as well + */ + readFile(path: string, encoding?: string): string | undefined; + /** If provided, used for module resolution as well as to handle directory structure */ + directoryExists?(path: string): boolean; + /** If provided, used in resolutions as well as handling directory structure */ + getDirectories?(path: string): string[]; + /** If provided, used to cache and handle directory structure modifications */ + readDirectory?(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + /** Symbol links resolution */ + realpath?(path: string): string; + /** If provided would be used to write log about compilation */ + trace?(s: string): void; + /** If provided is used to get the environment variable */ + getEnvironmentVariable?(name: string): string; + /** If provided, used to resolve the module names, otherwise typescript's default module resolution */ + resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; + /** If provided, used to resolve type reference directives, otherwise typescript's default resolution */ + resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): (ResolvedTypeReferenceDirective | undefined)[]; + /** 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; + /** 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; + /** 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 */ + clearTimeout?(timeoutId: any): void; + } + /** + * Host to create watch with root files and options + */ + interface WatchCompilerHostOfFilesAndCompilerOptions extends WatchCompilerHost { + /** root files to use to generate program */ + rootFiles: string[]; + /** Compiler options */ + options: CompilerOptions; + } + /** + * Reports config file diagnostics + */ + interface ConfigFileDiagnosticsReporter { + /** + * Reports the diagnostics in reading/writing or parsing of the config file + */ + onConfigFileDiagnostic: DiagnosticReporter; + /** + * Reports unrecoverable error when parsing config file + */ + onUnRecoverableConfigFileDiagnostic: DiagnosticReporter; + } + /** + * Host to create watch with config file + */ + interface WatchCompilerHostOfConfigFile extends WatchCompilerHost, ConfigFileDiagnosticsReporter { + /** Name of the config file to compile */ + configFileName: string; + /** Options to extend */ + optionsToExtend?: CompilerOptions; + /** + * Used to generate source file names from the config file and its include, exclude, files rules + * and also to cache the directory stucture + */ + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + } + interface Watch { + /** Synchronize with host and get updated program */ + getProgram(): T; + } + /** + * Creates the watch what generates program using the config file + */ + interface WatchOfConfigFile extends Watch { + } + /** + * Creates the watch that generates program using the root files and compiler options + */ + interface WatchOfFilesAndCompilerOptions extends Watch { + /** Updates the root files in the program, only if this is not config file compilation */ + updateRootFileNames(fileNames: string[]): void; + } + /** + * Create the watch compiler host for either configFile or fileNames and its options + */ + function createWatchCompilerHost(rootFiles: string[], options: CompilerOptions, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfFilesAndCompilerOptions; + function createWatchCompilerHost(configFileName: string, optionsToExtend: CompilerOptions | undefined, system: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): WatchCompilerHostOfConfigFile; + /** + * Creates the watch from the host for root files and compiler options + */ + function createWatchProgram(host: WatchCompilerHostOfFilesAndCompilerOptions): WatchOfFilesAndCompilerOptions; + /** + * Creates the watch from the host for config file + */ + function createWatchProgram(host: WatchCompilerHostOfConfigFile): WatchOfConfigFile; +} declare namespace ts { function parseCommandLine(commandLine: ReadonlyArray, readFile?: (path: string) => string | undefined): ParsedCommandLine; /**