diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index ee41584d1fe..0c14778abe5 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,10 +1,125 @@ namespace ts { -/* + const MinimumDate = new Date(-8640000000000000); + const MaximumDate = new Date(8640000000000000); + + /** + * A BuildContext tracks what's going on during the course of a build. + * The primary thing we track here is which files were written to, + * but unchanged, because this enables fast downstream updates + */ interface BuildContext { - unchangedOutputs: FileMap; + /** + * Map from output file name to its pre-build timestamp + */ + unchangedOutputs: FileMap; + + /** + * Map from config file name to up-to-date status + */ + projectStatus: FileMap; } + enum BuildResultFlags { + None = 0, + /** + * No errors of any kind occurred during build + */ + Success = 1 << 0, + /** + * None of the .d.ts files emitted by this build were + * different from the existing files on disk + */ + DeclarationOutputUnchanged = 1 << 1, + + ConfigFileErrors = 1 << 2, + SyntaxErrors = 1 << 3, + TypeErrors = 1 << 4, + DeclarationEmitErrors = 1 << 5, + + AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors + } + + enum UpToDateStatusType { + Unbuildable, + UpToDate, + /** + * The project appears out of date because its upstream inputs are newer than its outputs, + * but all of its outputs are actually newer than the previous identical outputs of its inputs. + * This means we can Pseudo-build (just touch timestamps), as if we had actually built this project. + */ + UpToDateWithUpstreamTypes, + OutputMissing, + OutOfDateWithSelf, + OutOfDateWithUpstream, + UpstreamOutOfDate + } + + type UpToDateStatus = + | StatusUnbuildable + | StatusUpToDate + | StatusOutputMissing + | StatusOutOfDateWithSelf + | StatusOutOfDateWithUpstream + | StatusUpstreamOutOfDate; + + /** + * The project can't be built at all in its current state. For example, + * its config file cannot be parsed, or it has a syntax error or missing file + */ + interface StatusUnbuildable { + type: UpToDateStatusType.Unbuildable; + reason: string; + } + + /** + * The project is up to date with respect to its inputs. + * We track what the newest input file is. + */ + interface StatusUpToDate { + type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes; + newestInputFileTime: Date; + newestDeclarationFileContentChangedTime: Date; + newestOutputFileTime: Date; + } + + /** + * One or more of the outputs of the project does not exist. + */ + interface StatusOutputMissing { + type: UpToDateStatusType.OutputMissing; + /** + * The name of the first output file that didn't exist + */ + missingOutputFileName: string; + } + + /** + * One or more of the project's outputs is older than its newest input. + */ + interface StatusOutOfDateWithSelf { + type: UpToDateStatusType.OutOfDateWithSelf; + outOfDateOutputFileName: string; + newerInputFileName: string; + } + + /** + * This project depends on an out-of-date project, so shouldn't be built yet + */ + interface StatusUpstreamOutOfDate { + type: UpToDateStatusType.UpstreamOutOfDate; + upstreamProjectName: string; + } + + /** + * One or more of the project's outputs is older than the newest output of + * an upstream project. + */ + interface StatusOutOfDateWithUpstream { + type: UpToDateStatusType.OutOfDateWithUpstream; + outOfDateOutputFileName: string; + newerProjectName: string; + } interface FileMap { setValue(fileName: string, value: T): void; @@ -14,6 +129,9 @@ namespace ts { tryGetValue(fileName: string): [false, undefined] | [true, T]; } + /** + * A FileMap maintains a normalized-key to value relationship + */ function createFileMap(): FileMap { const lookup: { [key: string]: T } = Object.create(null); @@ -65,5 +183,204 @@ namespace ts { } } } - */ + + function getOutputDeclarationFileName(inputFileName: string, configFile: ts.ParsedCommandLine) { + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, true); + const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); + return changeExtension(outputPath, ".d.ts"); + } + + function getOutputJavaScriptFileName(inputFileName: string, configFile: ts.ParsedCommandLine) { + // TODO handle JSX: Preserve + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, true); + const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); + return changeExtension(outputPath, (fileExtensionIs(inputFileName, ".tsx") && configFile.options.jsx === JsxEmit.Preserve) ? ".jsx" : ".js"); + } + + function getOutputFileNames(inputFileName: string, configFile: ts.ParsedCommandLine): ReadonlyArray { + if (configFile.options.outFile) { + return emptyArray; + } + + const outputs: string[] = []; + outputs.push(getOutputJavaScriptFileName(inputFileName, configFile)); + if (configFile.options.declaration) { + const dts = outputs.push(getOutputDeclarationFileName(inputFileName, configFile)); + if (configFile.options.declarationMap) { + outputs.push(dts + ".map"); + } + } + return outputs; + } + + function getOutFileOutputs(project: ts.ParsedCommandLine): ReadonlyArray { + Debug.assert(!!project.options.outFile, "outFile must be set"); + const outputs: string[] = []; + outputs.push(project.options.outFile); + if (project.options.declaration) { + const dts = outputs.push(changeExtension(project.options.outFile, ".d.ts")); + if (project.options.declarationMap) { + outputs.push(dts + ".map"); + } + } + return outputs; + } + + function rootDirOfOptions(opts: ts.CompilerOptions, configFileName: string) { + return opts.rootDir || path.dirname(configFileName); + } + + function createConfigFileCache(host: CompilerHost) { + const cache = createFileMap(); + const configParseHost = parseConfigHostFromCompilerHost(host); + + // TODO: Cache invalidation! + + function parseConfigFile(configFilePath: string) { + const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; + const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, configFilePath); + cache.setValue(configFilePath, parsed); + return parsed; + } + + return { + parseConfigFile + } + } + + function newer(date1: Date, date2: Date): Date { + return date2 > date1 ? date2 : date1; + } + + function older(date1: Date, date2: Date): Date { + return date2 < date1 ? date2 : date1; + } + + function createSolutionBuilder(host: CompilerHost) { + const configFileCache = createConfigFileCache(host); + + function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { + let newestInputFileName: string = '???'; + let newestInputFileTime = MinimumDate; + // Get timestamps of input files + for (const inputFile of project.fileNames) { + if (!host.fileExists(inputFile)) { + return { + type: UpToDateStatusType.Unbuildable, + reason: `${inputFile} does not exist` + }; + } + + const inputTime = sys.getModifiedTime(inputFile); + if (inputTime > newestInputFileTime) { + newestInputFileName = inputFile; + newestInputFileTime = inputTime; + } + } + + // Collect the expected outputs of this project + let outputs: ReadonlyArray; + if (project.options.outFile) { + outputs = getOutFileOutputs(project); + } + else { + outputs = []; + for (const inputFile of project.fileNames) { + (outputs as string[]).push(...getOutputFileNames(inputFile, project)); + } + } + + // Now see if all outputs are newer than the newest input + let oldestOutputFileName: string = "n/a"; + let oldestOutputFileTime: Date = MinimumDate; + let newestOutputFileTime: Date = MaximumDate; + let newestDeclarationFileContentChangedTime: Date = MinimumDate; + for (const output of outputs) { + // Output is missing + if (!host.fileExists(output)) { + return { + type: UpToDateStatusType.OutputMissing, + missingOutputFileName: output + }; + } + + const outputTime = sys.getModifiedTime(output); + // If an output is older than the newest input, we can stop checking + if (outputTime < newestInputFileTime) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: output, + newerInputFileName: newestInputFileName + }; + } + + if (outputTime < oldestOutputFileTime) { + oldestOutputFileTime = outputTime; + oldestOutputFileName = output; + } + newestOutputFileTime = older(newestOutputFileTime, outputTime); + + // Keep track of when the most recent time a .d.ts file was changed. + // In addition to file timestamps, we also keep track of when a .d.ts file + // had its file touched but not had its contents changed - this allows us + // to skip a downstream typecheck + if (fileExtensionIs(output, ".d.ts")) { + const unchangedTime = context.unchangedOutputs.getValueOrUndefined(output); + if (unchangedTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); + } + else { + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, sys.getModifiedTime(output)); + } + } + } + + let pseudoUpToDate = false; + // By here, we know the project is at least up-to-date with its own inputs. + // See if any of its upstream projects are newer than it + for (const ref of project.projectReferences) { + const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(ref.path), context); + + // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) + if (refStatus.type !== UpToDateStatusType.UpToDate) { + return { + type: UpToDateStatusType.UpstreamOutOfDate, + upstreamProjectName: ref.path + } + } + + // If the upstream project's newest file is older than our oldest output, we + // can't be out of date because of it + if (refStatus.newestInputFileTime < oldestOutputFileTime) { + continue; + } + + // If the upstream project has only change .d.ts files, and we've built + // *after* those files, then we're "psuedo up to date" and eligible for a fast rebuild + if (refStatus.newestDeclarationFileContentChangedTime < oldestOutputFileTime) { + pseudoUpToDate = true; + continue; + } + + // We have an output older than an upstream output - we are out of date + return { + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: oldestOutputFileName, + newerProjectName: ref.path + }; + } + + // Up to date + return { + type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime, + newestInputFileTime, + newestOutputFileTime + }; + } + + return { + getUpToDateStatus + } + } }