From d98a9a0150b4b363b27c44eb336443b02581112f Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 14 May 2018 18:27:52 -0700 Subject: [PATCH 01/48] WIP --- src/compiler/tsbuild.ts | 69 ++++++++++++++++++++++++++++++++++++++ src/compiler/tsconfig.json | 3 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/compiler/tsbuild.ts diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts new file mode 100644 index 00000000000..ee41584d1fe --- /dev/null +++ b/src/compiler/tsbuild.ts @@ -0,0 +1,69 @@ +namespace ts { +/* + interface BuildContext { + unchangedOutputs: FileMap; + } + + + + interface FileMap { + setValue(fileName: string, value: T): void; + getValue(fileName: string): T | never; + getValueOrUndefined(fileName: string): T | undefined; + getValueOrDefault(fileName: string, defaultValue: T): T; + tryGetValue(fileName: string): [false, undefined] | [true, T]; + } + + function createFileMap(): FileMap { + const lookup: { [key: string]: T } = Object.create(null); + + return { + setValue, + getValue, + getValueOrUndefined, + getValueOrDefault, + tryGetValue + } + + function setValue(fileName: string, value: T) { + lookup[normalizePath(fileName)] = value; + } + + function getValue(fileName: string): T | never { + const f = normalizePath(fileName); + if (f in lookup) { + return lookup[f]; + } else { + throw new Error(`No value corresponding to ${fileName} exists in this map`); + } + } + + function getValueOrUndefined(fileName: string): T | undefined { + const f = normalizePath(fileName); + if (f in lookup) { + return lookup[f]; + } else { + return undefined; + } + } + + function getValueOrDefault(fileName: string, defaultValue: T): T { + const f = normalizePath(fileName); + if (f in lookup) { + return lookup[f]; + } else { + return defaultValue; + } + } + + function tryGetValue(fileName: string): [false, undefined] | [true, T] { + const f = normalizePath(fileName); + if (f in lookup) { + return [true as true, lookup[f]]; + } else { + return [false as false, undefined]; + } + } + } + */ +} diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 46e5384434a..0db027d555f 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -46,6 +46,7 @@ "resolutionCache.ts", "watch.ts", "commandLineParser.ts", - "tsc.ts" + "tsc.ts", + "tsbuild.ts" ] } From aeb562519952b6fb2c7d24955e3542ed278b6375 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 17 May 2018 16:39:15 -0700 Subject: [PATCH 02/48] WIP --- src/compiler/tsbuild.ts | 323 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 320 insertions(+), 3 deletions(-) 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 + } + } } From a4eb15635b15fc5062e010dc4e95d025e4a24843 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 17 May 2018 16:52:21 -0700 Subject: [PATCH 03/48] WIP, need to lint --- src/compiler/tsbuild.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 0c14778abe5..e0929e355b2 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -260,6 +260,16 @@ namespace ts { const configFileCache = createConfigFileCache(host); function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { + const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath); + if (prior !== undefined) { + return prior; + } + const actual = getUpToDateStatusWorker(project, context); + context.projectStatus.setValue(project.options.configFilePath, actual); + return actual; + } + + function getUpToDateStatusWorker(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { let newestInputFileName: string = '???'; let newestInputFileTime = MinimumDate; // Get timestamps of input files @@ -381,6 +391,6 @@ namespace ts { return { getUpToDateStatus - } + }; } } From b2720adbc31bf7692db3a163f1f87be24af0eca6 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 21 May 2018 10:56:24 -0700 Subject: [PATCH 04/48] Lint --- src/compiler/tsbuild.ts | 51 ++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index e0929e355b2..40c23dc300d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,6 +1,6 @@ namespace ts { - const MinimumDate = new Date(-8640000000000000); - const MaximumDate = new Date(8640000000000000); + const minimumDate = new Date(-8640000000000000); + const maximumDate = new Date(8640000000000000); /** * A BuildContext tracks what's going on during the course of a build. @@ -133,7 +133,8 @@ namespace ts { * A FileMap maintains a normalized-key to value relationship */ function createFileMap(): FileMap { - const lookup: { [key: string]: T } = Object.create(null); + // tslint:disable-next-line:no-null-keyword + const lookup: { [key: string]: T } = Object.create(/*prototype*/ null); return { setValue, @@ -141,7 +142,7 @@ namespace ts { getValueOrUndefined, getValueOrDefault, tryGetValue - } + }; function setValue(fileName: string, value: T) { lookup[normalizePath(fileName)] = value; @@ -151,7 +152,8 @@ namespace ts { const f = normalizePath(fileName); if (f in lookup) { return lookup[f]; - } else { + } + else { throw new Error(`No value corresponding to ${fileName} exists in this map`); } } @@ -160,7 +162,8 @@ namespace ts { const f = normalizePath(fileName); if (f in lookup) { return lookup[f]; - } else { + } + else { return undefined; } } @@ -169,7 +172,8 @@ namespace ts { const f = normalizePath(fileName); if (f in lookup) { return lookup[f]; - } else { + } + else { return defaultValue; } } @@ -178,26 +182,27 @@ namespace ts { const f = normalizePath(fileName); if (f in lookup) { return [true as true, lookup[f]]; - } else { + } + else { return [false as false, undefined]; } } } - function getOutputDeclarationFileName(inputFileName: string, configFile: ts.ParsedCommandLine) { - const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, true); + function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine) { + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ 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) { + function getOutputJavaScriptFileName(inputFileName: string, configFile: ParsedCommandLine) { // TODO handle JSX: Preserve - const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, true); + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ 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 { + function getOutputFileNames(inputFileName: string, configFile: ParsedCommandLine): ReadonlyArray { if (configFile.options.outFile) { return emptyArray; } @@ -213,7 +218,7 @@ namespace ts { return outputs; } - function getOutFileOutputs(project: ts.ParsedCommandLine): ReadonlyArray { + function getOutFileOutputs(project: ParsedCommandLine): ReadonlyArray { Debug.assert(!!project.options.outFile, "outFile must be set"); const outputs: string[] = []; outputs.push(project.options.outFile); @@ -226,7 +231,7 @@ namespace ts { return outputs; } - function rootDirOfOptions(opts: ts.CompilerOptions, configFileName: string) { + function rootDirOfOptions(opts: CompilerOptions, configFileName: string) { return opts.rootDir || path.dirname(configFileName); } @@ -245,7 +250,7 @@ namespace ts { return { parseConfigFile - } + }; } function newer(date1: Date, date2: Date): Date { @@ -270,8 +275,8 @@ namespace ts { } function getUpToDateStatusWorker(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { - let newestInputFileName: string = '???'; - let newestInputFileTime = MinimumDate; + let newestInputFileName: string = undefined!; + let newestInputFileTime = minimumDate; // Get timestamps of input files for (const inputFile of project.fileNames) { if (!host.fileExists(inputFile)) { @@ -301,10 +306,10 @@ namespace ts { } // 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; + let oldestOutputFileName: string = undefined!; + let oldestOutputFileTime: Date = minimumDate; + let newestOutputFileTime: Date = maximumDate; + let newestDeclarationFileContentChangedTime: Date = minimumDate; for (const output of outputs) { // Output is missing if (!host.fileExists(output)) { @@ -356,7 +361,7 @@ namespace ts { return { type: UpToDateStatusType.UpstreamOutOfDate, upstreamProjectName: ref.path - } + }; } // If the upstream project's newest file is older than our oldest output, we From b1dedf254037bd2cfcac2d5033495902aa793045 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 21 May 2018 13:05:14 -0700 Subject: [PATCH 05/48] WIP --- src/compiler/tsbuild.ts | 211 +++++++++++++++++++++++++++++++++++++++- src/compiler/types.ts | 2 + 2 files changed, 211 insertions(+), 2 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 40c23dc300d..af04c67a977 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -19,6 +19,12 @@ namespace ts { projectStatus: FileMap; } + type Mapper = ReturnType; + interface DependencyGraph { + buildQueue: string[][]; + dependencyMap: Mapper; + } + enum BuildResultFlags { None = 0, @@ -189,6 +195,47 @@ namespace ts { } } + export function createDependencyMapper() { + const childToParents: { [key: string]: string[] } = {}; + const parentToChildren: { [key: string]: string[] } = {}; + const allKeys: string[] = []; + + function addReference(childConfigFileName: string, parentConfigFileName: string): void { + addEntry(childToParents, childConfigFileName, parentConfigFileName); + addEntry(parentToChildren, parentConfigFileName, childConfigFileName); + } + + function getReferencesTo(parentConfigFileName: string): string[] { + return parentToChildren[normalizePath(parentConfigFileName)] || []; + } + + function getReferencesOf(childConfigFileName: string): string[] { + return childToParents[normalizePath(childConfigFileName)] || []; + } + + function getKeys(): ReadonlyArray { + return allKeys; + } + + function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: string, element: string) { + key = normalizePath(key); + element = normalizePath(element); + const arr = (mapToAddTo[key] = mapToAddTo[key] || []); + if (arr.indexOf(element) < 0) { + arr.push(element); + } + if (allKeys.indexOf(key) < 0) allKeys.push(key); + if (allKeys.indexOf(element) < 0) allKeys.push(element); + } + + return { + addReference, + getReferencesTo, + getReferencesOf, + getKeys + }; + } + function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine) { const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ true); const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); @@ -232,7 +279,7 @@ namespace ts { } function rootDirOfOptions(opts: CompilerOptions, configFileName: string) { - return opts.rootDir || path.dirname(configFileName); + return opts.rootDir || getDirectoryPath(configFileName); } function createConfigFileCache(host: CompilerHost) { @@ -261,7 +308,12 @@ namespace ts { return date2 < date1 ? date2 : date1; } + function isDeclarationFile(fileName: string) { + return fileExtensionIs(fileName, ".d.ts"); + } + function createSolutionBuilder(host: CompilerHost) { + const diagReporter = createDiagnosticReporter(sys, /*pretty*/true); const configFileCache = createConfigFileCache(host); function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { @@ -394,8 +446,163 @@ namespace ts { }; } + function createDependencyGraph(roots: string[]): DependencyGraph { + // This is a list of list of projects that need to be built. + // The ordering here is "backwards", i.e. the first entry in the array is the last set of projects that need to be built; + // and the last entry is the first set of projects to be built. + // Each subarray is effectively unordered. + // We traverse the reference graph from each root, then "clean" the list by removing + // any entry that is duplicated to its right. + const buildQueue: string[][] = []; + const dependencyMap = createDependencyMapper(); + let buildQueuePosition = 0; + for (const root of roots) { + const config = configFileCache.parseConfigFile(root); + if (config === undefined) { + throw new Error(`Could not parse ${root}`); + } + enumerateReferences(normalizePath(root), config); + } + removeDuplicatesFromBuildQueue(buildQueue); + + return { + buildQueue, + dependencyMap + }; + + function enumerateReferences(fileName: string, root: ts.ParsedCommandLine): void { + const myBuildLevel = buildQueue[buildQueuePosition] = buildQueue[buildQueuePosition] || []; + if (myBuildLevel.indexOf(fileName) < 0) { + myBuildLevel.push(fileName); + } + + const refs = root.projectReferences; + if (refs === undefined) return; + buildQueuePosition++; + for (const ref of refs) { + dependencyMap.addReference(fileName, ref.path); + const resolvedRef = configFileCache.parseConfigFile(ref.path); + if (resolvedRef === undefined) continue; + enumerateReferences(normalizePath(ref.path), resolvedRef); + } + buildQueuePosition--; + } + + /** + * Removes entries from arrays which appear in later arrays. + * TODO: Use a lookup object to optimize this a bit? + */ + function removeDuplicatesFromBuildQueue(queue: string[][]): void { + // No need to check the last array + for (let i = 0; i < queue.length - 1; i++) { + queue[i] = queue[i].filter(fn => !occursAfter(fn, i + 1)); + } + + function occursAfter(s: string, start: number) { + for (let i = start; i < queue.length; i++) { + if (queue[i].indexOf(s) >= 0) return true; + } + return false; + } + } + } + + function buildSingleProject(proj: string, context: BuildContext) { + let resultFlags = BuildResultFlags.None; + resultFlags |= BuildResultFlags.DeclarationOutputUnchanged; + + const configFile = configFileCache.parseConfigFile(proj); + if (!configFile) { + // Failed to read the config file + resultFlags |= BuildResultFlags.ConfigFileErrors; + return resultFlags; + } + + if (configFile.fileNames.length === 0) { + // Nothing to build - must be a solution file, basically + return BuildResultFlags.None; + } + + const programOptions: CreateProgramOptions = { + projectReferences: configFile.projectReferences, + host: host, + rootNames: configFile.fileNames, + options: configFile.options + }; + const program = ts.createProgram(programOptions); + + // Don't emit anything in the presence of syntactic errors or options diagnostics + const syntaxDiagnostics = [...program.getOptionsDiagnostics(), ...program.getSyntacticDiagnostics()]; + if (syntaxDiagnostics.length) { + resultFlags |= BuildResultFlags.SyntaxErrors; + for (const diag of syntaxDiagnostics) { + diagReporter(diag); + } + return resultFlags; + } + + // Don't emit .d.ts if there are decl file errors + if (program.getCompilerOptions().declaration) { + const declDiagnostics = program.getDeclarationDiagnostics(); + if (declDiagnostics.length) { + resultFlags |= BuildResultFlags.DeclarationEmitErrors; + for (const diag of declDiagnostics) { + diagReporter(diag); + } + } + return resultFlags; + } + + const semanticDiagnostics = [...program.getSemanticDiagnostics()]; + if (semanticDiagnostics.length) { + resultFlags |= BuildResultFlags.TypeErrors; + for (const diag of semanticDiagnostics) { + diagReporter(diag); + } + return resultFlags; + } + + program.emit(undefined, (fileName, content, writeBom, onError) => { + let priorChangeTime: Date | undefined; + + if (isDeclarationFile(fileName) && host.fileExists(fileName)) { + if (host.readFile(fileName) === content) { + resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; + priorChangeTime = host.getLastWriteTime && host.getLastWriteTime(fileName); + } + } + + host.writeFile(fileName, content, writeBom, onError, emptyArray); + if (priorChangeTime !== undefined) { + context.unchangedOutputs.setValue(fileName, priorChangeTime); + } + }); + + return resultFlags; + } + + function buildProjects(configFileNames: string[], context: BuildContext) { + // Establish what needs to be built + const graph = createDependencyGraph(configFileNames); + + const queue = graph.buildQueue; + while (queue.length > 0) { + const next = queue[0].pop()!; + + const result = buildSingleProject(next, context); + if (result & BuildResultFlags.AnyErrors) { + break; + } + + if (queue[0].length === 0) { + queue.pop(); + } + } + } + return { - getUpToDateStatus + getUpToDateStatus, + buildProjects }; } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4b2a7c080d2..3da916244d6 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4768,6 +4768,8 @@ namespace ts { /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; createHash?(data: string): string; + + getLastWriteTime?(fileName: string): Date; } /* @internal */ From 7e0825a3e7adc9878834cd2e334efa2fa14f48af Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 21 May 2018 17:47:58 -0700 Subject: [PATCH 06/48] Clean, etc --- src/compiler/diagnosticMessages.json | 37 +++ src/compiler/program.ts | 7 +- src/compiler/sys.ts | 21 ++ src/compiler/tsbuild.ts | 343 ++++++++++++++++++++++----- src/compiler/tsc.ts | 4 + src/compiler/tsconfig.json | 2 +- src/compiler/types.ts | 4 +- 7 files changed, 350 insertions(+), 68 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 2956faef0b3..1077db69ef7 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3593,6 +3593,43 @@ "category": "Error", "code": 6309 }, + "Project '{0}' is out of date because oldest output '{1}' is older than newest input '{2}'": { + "category": "Message", + "code": 6350 + }, + "Project '{0}' is up to date because newest input '{1}' is older than oldest output '{2}'": { + "category": "Message", + "code": 6351 + }, + "Project '{0}' is out of date because output file '{1}' does not exist": { + "category": "Message", + "code": 6352 + }, + + "Project '{0}' is up to date with its upstream types": { + "category": "Message", + "code": 6353 + }, + "Sorted list of input projects: {0}": { + "category": "Message", + "code": 6354 + }, + "Would delete the following files:{0}": { + "category": "Message", + "code": 6355 + }, + "Would build project '{0}'": { + "category": "Message", + "code": 6356 + }, + "Building project '{0}'...": { + "category": "Message", + "code": 6357 + }, + "Updating output timestamps of project '{0}'...": { + "category": "Message", + "code": 6358 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 365e948f749..5f8eb330d20 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -189,7 +189,10 @@ namespace ts { getEnvironmentVariable: name => sys.getEnvironmentVariable ? sys.getEnvironmentVariable(name) : "", getDirectories: (path: string) => sys.getDirectories(path), realpath, - readDirectory: (path, extensions, include, exclude, depth) => sys.readDirectory(path, extensions, include, exclude, depth) + readDirectory: (path, extensions, include, exclude, depth) => sys.readDirectory(path, extensions, include, exclude, depth), + getModifiedTime: path => sys.getModifiedTime(path), + setModifiedTime: (path, date) => sys.setModifiedTime(path, date), + deleteFile: path => sys.deleteFile(path) }; } @@ -2692,7 +2695,7 @@ namespace ts { /** * Returns the target config filename of a project reference */ - function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined { + export function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined { if (!host.fileExists(ref.path)) { return combinePaths(ref.path, "tsconfig.json"); } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 688400fc6d5..6993a3b438c 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -432,6 +432,7 @@ namespace ts { readFile(path: string, encoding?: string): string | undefined; 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 @@ -447,6 +448,8 @@ namespace ts { getDirectories(path: string): string[]; readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; /** * This should be cryptographically secure. * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) @@ -589,6 +592,8 @@ namespace ts { }, readDirectory, getModifiedTime, + setModifiedTime, + deleteFile, createHash: _crypto ? createMD5HashUsingNativeCrypto : generateDjb2Hash, getMemoryUsage() { if (global.gc) { @@ -1063,6 +1068,22 @@ namespace ts { } } + function setModifiedTime(path: string, time: Date) { + try { + _fs.utimesSync(path, time, time); + } + catch (e) { + } + } + + function deleteFile(path: string) { + try { + return _fs.unlinkSync(path); + } + catch (e) { + } + } + /** * djb2 hashing algorithm * http://www.cse.yorku.ca/~oz/hash.html diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index af04c67a977..b59e6ee4e95 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -8,6 +8,7 @@ namespace ts { * but unchanged, because this enables fast downstream updates */ interface BuildContext { + options: BuildOptions; /** * Map from output file name to its pre-build timestamp */ @@ -17,6 +18,8 @@ namespace ts { * Map from config file name to up-to-date status */ projectStatus: FileMap; + + verbose(diag: DiagnosticMessage, ...args: any[]): void; } type Mapper = ReturnType; @@ -25,6 +28,12 @@ namespace ts { dependencyMap: Mapper; } + interface BuildOptions { + dry: boolean; + force: boolean; + verbose: boolean; + } + enum BuildResultFlags { None = 0, @@ -51,7 +60,7 @@ namespace ts { 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. + * but all of its outputs are actually newer than the previous identical outputs of its (.d.ts) inputs. * This means we can Pseudo-build (just touch timestamps), as if we had actually built this project. */ UpToDateWithUpstreamTypes, @@ -286,11 +295,12 @@ namespace ts { const cache = createFileMap(); const configParseHost = parseConfigHostFromCompilerHost(host); - // TODO: Cache invalidation! + // TODO: Cache invalidation under --watch! function parseConfigFile(configFilePath: string) { const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; - const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, configFilePath); + const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, getDirectoryPath(configFilePath)); + parsed.options.configFilePath = configFilePath; cache.setValue(configFilePath, parsed); return parsed; } @@ -312,21 +322,109 @@ namespace ts { return fileExtensionIs(fileName, ".d.ts"); } - function createSolutionBuilder(host: CompilerHost) { + function createBuildContext(options: BuildOptions): BuildContext { + const verboseDiag = options.verbose && createDiagnosticReporter(sys, /*pretty*/ false); + return { + options, + projectStatus: createFileMap(), + unchangedOutputs: createFileMap(), + verbose: options.verbose ? (diag, ...args) => { + verboseDiag(createCompilerDiagnostic(diag, ...args)); + } : () => undefined + }; + } + + export function performBuild(args: string[]) { + const diagReporter = createDiagnosticReporter(sys, /*pretty*/true); + const host = createCompilerHost({}); + + let verbose = false; + let dry = false; + let force = false; + let clean = false; + + const projects: string[] = []; + for (let i = 0; i < args.length; i++) { + switch (args[i].toLowerCase()) { + case "-v": + case "--verbose": + verbose = true; + continue; + case "-d": + case "--dry": + dry = true; + continue; + case "-f": + case "--force": + force = true; + continue; + case "--clean": + clean = true; + continue; + } + // Not a flag, parse as filename + addProject(args[i]); + } + + if (projects.length === 0) { + // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." + addProject("."); + } + + const context = createBuildContext({ verbose, dry, force }); + const builder = createSolutionBuilder(host, context); + if (clean) { + builder.cleanProjects(projects); + } + else { + builder.buildProjects(projects); + } + + function addProject(projectSpecification: string) { + const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification); + const refPath = resolveProjectReferencePath(host, { path: fileName }); + if (!host.fileExists(refPath)) { + diagReporter(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); + } + projects.push(refPath); + + } + } + + export function createSolutionBuilder(host: CompilerHost, context: BuildContext) { const diagReporter = createDiagnosticReporter(sys, /*pretty*/true); const configFileCache = createConfigFileCache(host); + return { + getUpToDateStatus, + buildProjects, + cleanProjects + }; + function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath); if (prior !== undefined) { return prior; } - const actual = getUpToDateStatusWorker(project, context); + const actual = getUpToDateStatusWorker(project); context.projectStatus.setValue(project.options.configFilePath, actual); return actual; } - function getUpToDateStatusWorker(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { + function getAllProjectOutputs(project: ParsedCommandLine): ReadonlyArray { + if (project.options.outFile) { + return getOutFileOutputs(project); + } + else { + const outputs: string[] = []; + for (const inputFile of project.fileNames) { + (outputs as string[]).push(...getOutputFileNames(inputFile, project)); + } + return outputs; + } + } + + function getUpToDateStatusWorker(project: ParsedCommandLine): UpToDateStatus { let newestInputFileName: string = undefined!; let newestInputFileTime = minimumDate; // Get timestamps of input files @@ -338,7 +436,7 @@ namespace ts { }; } - const inputTime = sys.getModifiedTime(inputFile); + const inputTime = host.getModifiedTime(inputFile); if (inputTime > newestInputFileTime) { newestInputFileName = inputFile; newestInputFileTime = inputTime; @@ -346,21 +444,12 @@ namespace ts { } // 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)); - } - } + const outputs = getAllProjectOutputs(project); // Now see if all outputs are newer than the newest input let oldestOutputFileName: string = undefined!; - let oldestOutputFileTime: Date = minimumDate; - let newestOutputFileTime: Date = maximumDate; + let oldestOutputFileTime: Date = maximumDate; + let newestOutputFileTime: Date = minimumDate; let newestDeclarationFileContentChangedTime: Date = minimumDate; for (const output of outputs) { // Output is missing @@ -371,7 +460,7 @@ namespace ts { }; } - const outputTime = sys.getModifiedTime(output); + const outputTime = host.getModifiedTime(output); // If an output is older than the newest input, we can stop checking if (outputTime < newestInputFileTime) { return { @@ -391,13 +480,13 @@ namespace ts { // 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")) { + if (isDeclarationFile(output)) { const unchangedTime = context.unchangedOutputs.getValueOrUndefined(output); if (unchangedTime !== undefined) { newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); } else { - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, sys.getModifiedTime(output)); + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, host.getModifiedTime(output)); } } } @@ -405,36 +494,40 @@ namespace ts { 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 (project.projectReferences) { + for (const ref of project.projectReferences) { + const resolvedRef = resolveProjectReferencePath(host, ref); + const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef), context); - // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) - if (refStatus.type !== UpToDateStatusType.UpToDate) { + // 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 + Debug.assert(oldestOutputFileName !== undefined, "Should have an oldest output filename here"); return { - type: UpToDateStatusType.UpstreamOutOfDate, - upstreamProjectName: ref.path + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: oldestOutputFileName, + newerProjectName: 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 @@ -446,6 +539,7 @@ namespace ts { }; } + // TODO: Use the better algorithm function createDependencyGraph(roots: string[]): DependencyGraph { // This is a list of list of projects that need to be built. // The ordering here is "backwards", i.e. the first entry in the array is the last set of projects that need to be built; @@ -480,10 +574,11 @@ namespace ts { if (refs === undefined) return; buildQueuePosition++; for (const ref of refs) { - dependencyMap.addReference(fileName, ref.path); - const resolvedRef = configFileCache.parseConfigFile(ref.path); + const actualPath = resolveProjectReferencePath(host, ref); + dependencyMap.addReference(fileName, actualPath); + const resolvedRef = configFileCache.parseConfigFile(actualPath); if (resolvedRef === undefined) continue; - enumerateReferences(normalizePath(ref.path), resolvedRef); + enumerateReferences(normalizePath(actualPath), resolvedRef); } buildQueuePosition--; } @@ -507,7 +602,14 @@ namespace ts { } } - function buildSingleProject(proj: string, context: BuildContext) { + // TODO Accept parsedCommandLine + function buildSingleProject(proj: string) { + if (context.options.dry) { + diagReporter(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); + } + + context.verbose(Diagnostics.Building_project_0, proj); + let resultFlags = BuildResultFlags.None; resultFlags |= BuildResultFlags.DeclarationOutputUnchanged; @@ -549,8 +651,8 @@ namespace ts { for (const diag of declDiagnostics) { diagReporter(diag); } + return resultFlags; } - return resultFlags; } const semanticDiagnostics = [...program.getSemanticDiagnostics()]; @@ -562,47 +664,160 @@ namespace ts { return resultFlags; } + let newestDeclarationFileContentChangedTime = minimumDate; program.emit(undefined, (fileName, content, writeBom, onError) => { let priorChangeTime: Date | undefined; if (isDeclarationFile(fileName) && host.fileExists(fileName)) { if (host.readFile(fileName) === content) { + // Check for unchanged .d.ts files resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - priorChangeTime = host.getLastWriteTime && host.getLastWriteTime(fileName); + priorChangeTime = host.getModifiedTime && host.getModifiedTime(fileName); } } host.writeFile(fileName, content, writeBom, onError, emptyArray); if (priorChangeTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); context.unchangedOutputs.setValue(fileName, priorChangeTime); } }); + context.projectStatus.setValue(proj, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime } as UpToDateStatus); + return resultFlags; } - function buildProjects(configFileNames: string[], context: BuildContext) { + function updateOutputTimestamps(proj: ParsedCommandLine) { + if (context.options.dry) { + diagReporter(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj.options.configFilePath)); + return; + } + + context.verbose(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath); + const now = new Date(); + const outputs = getAllProjectOutputs(proj); + let priorNewestUpdateTime = minimumDate; + for (const file of outputs) { + if (isDeclarationFile(file)) { + priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file)); + } + host.setModifiedTime(file, now); + } + + context.projectStatus.setValue(proj.options.configFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); + } + + function cleanProjects(configFileNames: string[]) { + // Get the same graph for cleaning we'd use for building + const graph = createDependencyGraph(configFileNames); + + const fileReport: string[] = []; + for (const level of graph.buildQueue) { + for (const proj of level) { + const parsed = configFileCache.parseConfigFile(proj); + const outputs = getAllProjectOutputs(parsed); + for (const output of outputs) { + if (host.fileExists(output)) { + if (context.options.dry) { + fileReport.push(output); + } + else { + host.deleteFile(output); + } + } + } + } + } + + if (context.options.dry) { + diagReporter(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join(""))); + } + } + + function buildProjects(configFileNames: string[]) { // Establish what needs to be built const graph = createDependencyGraph(configFileNames); const queue = graph.buildQueue; - while (queue.length > 0) { - const next = queue[0].pop()!; + reportBuildQueue(graph); - const result = buildSingleProject(next, context); + let next: string; + while (next = getNext()) { + const proj = configFileCache.parseConfigFile(next); + const status = getUpToDateStatus(proj, context); + reportProjectStatus(next, status); + + if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { + // Up to date, skip + continue; + } + + if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !context.options.force) { + // Fake build + updateOutputTimestamps(proj); + continue; + } + + const result = buildSingleProject(next); if (result & BuildResultFlags.AnyErrors) { break; } + } - if (queue[0].length === 0) { - queue.pop(); + function getNext(): string | undefined { + if (queue.length === 0) { + return undefined; } + while (queue.length > 0) { + const last = queue[queue.length - 1]; + if (last.length === 0) { + queue.pop(); + continue; + } + return last.pop()!; + } + return undefined; } } - return { - getUpToDateStatus, - buildProjects - }; + function reportBuildQueue(graph: DependencyGraph) { + if (!context.options.verbose) return; + + const names: string[] = []; + for (const level of graph.buildQueue) { + for (const el of level) { + names.push(el); + } + } + names.reverse(); + context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join("")); + } + + function reportProjectStatus(configFileName: string, status: UpToDateStatus) { + if (!context.options.verbose) return; + switch (status.type) { + case UpToDateStatusType.OutOfDateWithSelf: + context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, configFileName, status.outOfDateOutputFileName, status.newerInputFileName); + return; + case UpToDateStatusType.OutOfDateWithUpstream: + context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, configFileName, status.outOfDateOutputFileName, status.newerProjectName); + return; + case UpToDateStatusType.OutputMissing: + context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFileName, status.missingOutputFileName); + return; + case UpToDateStatusType.UpToDate: + context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestDeclarationFileContentChangedTime as any, status.newestOutputFileTime); + return; + case UpToDateStatusType.UpToDateWithUpstreamTypes: + context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); + return; + case UpToDateStatusType.UpstreamOutOfDate: + context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); + return; + default: + throw new Error(`Invalid build status - ${UpToDateStatusType[status.type]}`); + } + } } } diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 7617fc62cb6..51f3db7d616 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -47,6 +47,10 @@ namespace ts { } export function executeCommandLine(args: string[]): void { + if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) { + return performBuild(args.slice(1)); + } + const commandLine = parseCommandLine(args); // Configuration file name (if any) diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 0db027d555f..2bf59003dfd 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -46,7 +46,7 @@ "resolutionCache.ts", "watch.ts", "commandLineParser.ts", + "tsbuild.ts", "tsc.ts", - "tsbuild.ts" ] } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 3da916244d6..1275a8ef312 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4769,7 +4769,9 @@ namespace ts { /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; createHash?(data: string): string; - getLastWriteTime?(fileName: string): Date; + getModifiedTime?(fileName: string): Date; + setModifiedTime?(fileName: string, date: Date): void; + deleteFile?(fileName: string): void; } /* @internal */ From 6d04378e905d7d55d51011b40d5b4c5b331f657c Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 21 May 2018 19:40:09 -0700 Subject: [PATCH 07/48] Testing WIP --- src/compiler/sys.ts | 2 + src/compiler/tsbuild.ts | 68 +++++++++++++-------- src/compiler/tsc.ts | 4 +- src/harness/fakes.ts | 12 ++++ src/harness/tsconfig.json | 1 + src/harness/unittests/tsbuild.ts | 71 ++++++++++++++++++++++ src/harness/vfs.ts | 20 +++++- tests/projects/sample1/core/index.ts | 2 + tests/projects/sample1/core/tsconfig.json | 3 + tests/projects/sample1/logic/index.ts | 4 ++ tests/projects/sample1/logic/tsconfig.json | 5 ++ tests/projects/sample1/tests/index.ts | 5 ++ tests/projects/sample1/tests/tsconfig.json | 6 ++ tests/projects/sample1/ui/index.ts | 5 ++ tests/projects/sample1/ui/tsconfig.json | 5 ++ 15 files changed, 184 insertions(+), 29 deletions(-) create mode 100644 src/harness/unittests/tsbuild.ts create mode 100644 tests/projects/sample1/core/index.ts create mode 100644 tests/projects/sample1/core/tsconfig.json create mode 100644 tests/projects/sample1/logic/index.ts create mode 100644 tests/projects/sample1/logic/tsconfig.json create mode 100644 tests/projects/sample1/tests/index.ts create mode 100644 tests/projects/sample1/tests/tsconfig.json create mode 100644 tests/projects/sample1/ui/index.ts create mode 100644 tests/projects/sample1/ui/tsconfig.json diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 6993a3b438c..6b9c0e8710c 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -1073,6 +1073,7 @@ namespace ts { _fs.utimesSync(path, time, time); } catch (e) { + return; } } @@ -1081,6 +1082,7 @@ namespace ts { return _fs.unlinkSync(path); } catch (e) { + return; } } diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index b59e6ee4e95..d9c3eb4212d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -7,7 +7,7 @@ namespace ts { * The primary thing we track here is which files were written to, * but unchanged, because this enables fast downstream updates */ - interface BuildContext { + export interface BuildContext { options: BuildOptions; /** * Map from output file name to its pre-build timestamp @@ -299,6 +299,9 @@ namespace ts { function parseConfigFile(configFilePath: string) { const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; + if (sourceFile === undefined) { + return undefined; + } const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, getDirectoryPath(configFilePath)); parsed.options.configFilePath = configFilePath; cache.setValue(configFilePath, parsed); @@ -322,7 +325,7 @@ namespace ts { return fileExtensionIs(fileName, ".d.ts"); } - function createBuildContext(options: BuildOptions): BuildContext { + export function createBuildContext(options: BuildOptions): BuildContext { const verboseDiag = options.verbose && createDiagnosticReporter(sys, /*pretty*/ false); return { options, @@ -334,18 +337,15 @@ namespace ts { }; } - export function performBuild(args: string[]) { - const diagReporter = createDiagnosticReporter(sys, /*pretty*/true); - const host = createCompilerHost({}); - + export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[]) { let verbose = false; let dry = false; let force = false; let clean = false; const projects: string[] = []; - for (let i = 0; i < args.length; i++) { - switch (args[i].toLowerCase()) { + for (const arg of args) { + switch (arg.toLowerCase()) { case "-v": case "--verbose": verbose = true; @@ -363,7 +363,7 @@ namespace ts { continue; } // Not a flag, parse as filename - addProject(args[i]); + addProject(arg); } if (projects.length === 0) { @@ -372,7 +372,7 @@ namespace ts { } const context = createBuildContext({ verbose, dry, force }); - const builder = createSolutionBuilder(host, context); + const builder = createSolutionBuilder(host, reportDiagnostic, context); if (clean) { builder.cleanProjects(projects); } @@ -384,15 +384,14 @@ namespace ts { const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification); const refPath = resolveProjectReferencePath(host, { path: fileName }); if (!host.fileExists(refPath)) { - diagReporter(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); } projects.push(refPath); } } - export function createSolutionBuilder(host: CompilerHost, context: BuildContext) { - const diagReporter = createDiagnosticReporter(sys, /*pretty*/true); + export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, context: BuildContext) { const configFileCache = createConfigFileCache(host); return { @@ -418,7 +417,7 @@ namespace ts { else { const outputs: string[] = []; for (const inputFile of project.fileNames) { - (outputs as string[]).push(...getOutputFileNames(inputFile, project)); + outputs.push(...getOutputFileNames(inputFile, project)); } return outputs; } @@ -553,7 +552,8 @@ namespace ts { for (const root of roots) { const config = configFileCache.parseConfigFile(root); if (config === undefined) { - throw new Error(`Could not parse ${root}`); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, root)); + continue; } enumerateReferences(normalizePath(root), config); } @@ -564,7 +564,7 @@ namespace ts { dependencyMap }; - function enumerateReferences(fileName: string, root: ts.ParsedCommandLine): void { + function enumerateReferences(fileName: string, root: ParsedCommandLine): void { const myBuildLevel = buildQueue[buildQueuePosition] = buildQueue[buildQueuePosition] || []; if (myBuildLevel.indexOf(fileName) < 0) { myBuildLevel.push(fileName); @@ -605,7 +605,7 @@ namespace ts { // TODO Accept parsedCommandLine function buildSingleProject(proj: string) { if (context.options.dry) { - diagReporter(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); } context.verbose(Diagnostics.Building_project_0, proj); @@ -627,18 +627,18 @@ namespace ts { const programOptions: CreateProgramOptions = { projectReferences: configFile.projectReferences, - host: host, + host, rootNames: configFile.fileNames, options: configFile.options }; - const program = ts.createProgram(programOptions); + const program = createProgram(programOptions); // Don't emit anything in the presence of syntactic errors or options diagnostics const syntaxDiagnostics = [...program.getOptionsDiagnostics(), ...program.getSyntacticDiagnostics()]; if (syntaxDiagnostics.length) { resultFlags |= BuildResultFlags.SyntaxErrors; for (const diag of syntaxDiagnostics) { - diagReporter(diag); + reportDiagnostic(diag); } return resultFlags; } @@ -649,7 +649,7 @@ namespace ts { if (declDiagnostics.length) { resultFlags |= BuildResultFlags.DeclarationEmitErrors; for (const diag of declDiagnostics) { - diagReporter(diag); + reportDiagnostic(diag); } return resultFlags; } @@ -659,13 +659,13 @@ namespace ts { if (semanticDiagnostics.length) { resultFlags |= BuildResultFlags.TypeErrors; for (const diag of semanticDiagnostics) { - diagReporter(diag); + reportDiagnostic(diag); } return resultFlags; } let newestDeclarationFileContentChangedTime = minimumDate; - program.emit(undefined, (fileName, content, writeBom, onError) => { + program.emit(/*targetSourceFile*/ undefined, (fileName, content, writeBom, onError) => { let priorChangeTime: Date | undefined; if (isDeclarationFile(fileName) && host.fileExists(fileName)) { @@ -690,7 +690,7 @@ namespace ts { function updateOutputTimestamps(proj: ParsedCommandLine) { if (context.options.dry) { - diagReporter(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj.options.configFilePath)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj.options.configFilePath)); return; } @@ -731,13 +731,29 @@ namespace ts { } if (context.options.dry) { - diagReporter(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join(""))); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join(""))); } } function buildProjects(configFileNames: string[]) { + const resolvedNames: string[] = []; + for (const name of configFileNames) { + let fullPath = resolvePath(host.getCurrentDirectory(), name); + if (host.fileExists(fullPath)) { + resolvedNames.push(fullPath); + continue; + } + fullPath = combinePaths(fullPath, "tsconfig.json"); + if (host.fileExists(fullPath)) { + resolvedNames.push(fullPath); + continue; + } + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); + return; + } + // Establish what needs to be built - const graph = createDependencyGraph(configFileNames); + const graph = createDependencyGraph(resolvedNames); const queue = graph.buildQueue; reportBuildQueue(graph); diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 51f3db7d616..ba05092c11b 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -48,9 +48,9 @@ namespace ts { export function executeCommandLine(args: string[]): void { if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) { - return performBuild(args.slice(1)); + return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1)); } - + const commandLine = parseCommandLine(args); // Configuration file name (if any) diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 4fdd30c940e..f5a2861d385 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -131,6 +131,10 @@ namespace fakes { return stats ? stats.mtime : undefined; } + public setModifiedTime(path: string, time: Date) { + this.vfs.utimesSync(path, time, time); + } + public createHash(data: string): string { return data; } @@ -252,6 +256,14 @@ namespace fakes { return this.sys.directoryExists(directoryName); } + public getModifiedTime(fileName: string) { + return this.sys.getModifiedTime(fileName); + } + + public setModifiedTime(fileName: string, time: Date) { + return this.sys.setModifiedTime(fileName, time); + } + public getDirectories(path: string): string[] { return this.sys.getDirectories(path); } diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 4a95cd44928..ab857eb520d 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -52,6 +52,7 @@ "../compiler/builder.ts", "../compiler/resolutionCache.ts", "../compiler/watch.ts", + "../compiler/tsbuild.ts", "../compiler/commandLineParser.ts", "../services/types.ts", diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts new file mode 100644 index 00000000000..5bd713f377f --- /dev/null +++ b/src/harness/unittests/tsbuild.ts @@ -0,0 +1,71 @@ +/// + +namespace ts { + let currentTime = 100; + const bfs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); + const lastDiagnostics: Diagnostic[] = []; + const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); + + const sampleRoot = resolvePath(__dirname, "../../tests/projects/sample1"); + loadFsMirror(bfs, sampleRoot, "/src"); + bfs.mkdirpSync("/lib"); + bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))); + bfs.meta.set("defaultLibLocation", "/lib"); + bfs.makeReadonly(); + + describe("tsbuild tests", () => { + it("builds the referenced project", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, createBuildContext({ dry: false, force: false, verbose: false })); + + fs.chdir("/src/tests"); + fs.debugPrint(); + builder.buildProjects(["."]); + printDiagnostics(); + fs.debugPrint(); + assertDiagnosticMessages(Diagnostics.File_0_does_not_exist); + + tick(); + }); + }); + + function assertDiagnosticMessages(...expected: DiagnosticMessage[]) { + const actual = lastDiagnostics.slice(); + actual.sort((a, b) => b.code - a.code); + expected.sort((a, b) => b.code - a.code); + if (actual.length !== expected.length) { + assert.fail(actual, expected, `Diagnostic arrays did not match - expected ${actual.join(",")}, got ${expected.join(",")}`); + } + for (let i = 0; i < actual.length; i++) { + if (actual[i].code !== expected[i].code) { + assert.fail(actual[i].messageText, expected[i].message, "Mismatched error code"); + } + } + } + + export function printDiagnostics() { + const out = createDiagnosticReporter(sys); + for (const d of lastDiagnostics) { + out(d); + } + } + + function tick() { + currentTime += 10; + } + function time() { + return currentTime; + } + + function loadFsMirror(vfs: vfs.FileSystem, localRoot: string, virtualRoot: string) { + vfs.mkdirpSync(virtualRoot); + for (const path of Harness.IO.readDirectory(localRoot)) { + const file = getBaseFileName(path); + vfs.writeFileSync(virtualRoot + "/" + file, Harness.IO.readFile(localRoot + "/" + file)); + } + for (const dir of Harness.IO.getDirectories(localRoot)){ + loadFsMirror(vfs, localRoot + "/" + dir, virtualRoot + "/" + dir); + } + } +} \ No newline at end of file diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 34a48f59091..f05a47ad64c 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -5,6 +5,11 @@ namespace vfs { */ export const builtFolder = "/.ts"; + /** + * Posix-style path to additional mountable folders (./tests/projects in this repo) + */ + export const projectsFolder = "/.projects"; + /** * Posix-style path to additional test libraries */ @@ -404,7 +409,18 @@ namespace vfs { } /** - * Get file status. + * Change file access times + * + * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. + */ + public utimesSync(path: string, atime: Date, mtime: Date) { + const entry = this._walk(this._resolve(path)); + entry.node.atimeMs = +atime; + entry.node.mtimeMs = +mtime; + } + + /** + * Get file status. If `path` is a symbolic link, it is dereferenced. * * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/lstat.html * @@ -414,6 +430,7 @@ namespace vfs { return this._stat(this._walk(this._resolve(path), /*noFollow*/ true)); } + private _stat(entry: WalkResult) { const node = entry.node; if (!node) throw createIOError("ENOENT"); @@ -1282,6 +1299,7 @@ namespace vfs { files: { [builtFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "built/local"), resolver), [testLibFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/lib"), resolver), + [projectsFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/projects"), resolver), [srcFolder]: {} }, cwd: srcFolder, diff --git a/tests/projects/sample1/core/index.ts b/tests/projects/sample1/core/index.ts new file mode 100644 index 00000000000..9ade19f5e2e --- /dev/null +++ b/tests/projects/sample1/core/index.ts @@ -0,0 +1,2 @@ +export function leftPad(s: string, n: number) { return s + n; } +export function multiply(a: number, b: number) { return a * b; } diff --git a/tests/projects/sample1/core/tsconfig.json b/tests/projects/sample1/core/tsconfig.json new file mode 100644 index 00000000000..a514dfe8a03 --- /dev/null +++ b/tests/projects/sample1/core/tsconfig.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/tests/projects/sample1/logic/index.ts b/tests/projects/sample1/logic/index.ts new file mode 100644 index 00000000000..cccadd1718b --- /dev/null +++ b/tests/projects/sample1/logic/index.ts @@ -0,0 +1,4 @@ +import * as c from '../core'; +export function getSecondsInDay() { + return c.multiply(10, 15); +} diff --git a/tests/projects/sample1/logic/tsconfig.json b/tests/projects/sample1/logic/tsconfig.json new file mode 100644 index 00000000000..3c2056eec88 --- /dev/null +++ b/tests/projects/sample1/logic/tsconfig.json @@ -0,0 +1,5 @@ +{ + "references": [ + { "path": "../core" } + ] +} diff --git a/tests/projects/sample1/tests/index.ts b/tests/projects/sample1/tests/index.ts new file mode 100644 index 00000000000..e9a1ebde3e1 --- /dev/null +++ b/tests/projects/sample1/tests/index.ts @@ -0,0 +1,5 @@ +import * as c from '../core'; +import * as logic from '../logic'; + +c.leftPad("", 10); +logic.getSecondsInDay(); diff --git a/tests/projects/sample1/tests/tsconfig.json b/tests/projects/sample1/tests/tsconfig.json new file mode 100644 index 00000000000..dd1bf3bae6b --- /dev/null +++ b/tests/projects/sample1/tests/tsconfig.json @@ -0,0 +1,6 @@ +{ + "references": [ + { "path": "../core" }, + { "path": "../logic" } + ] +} \ No newline at end of file diff --git a/tests/projects/sample1/ui/index.ts b/tests/projects/sample1/ui/index.ts new file mode 100644 index 00000000000..9d7e7e3a89e --- /dev/null +++ b/tests/projects/sample1/ui/index.ts @@ -0,0 +1,5 @@ +import * as logic from '../logic'; + +export function run() { + console.log(logic.getSecondsInDay()); +} diff --git a/tests/projects/sample1/ui/tsconfig.json b/tests/projects/sample1/ui/tsconfig.json new file mode 100644 index 00000000000..45eff16d4d9 --- /dev/null +++ b/tests/projects/sample1/ui/tsconfig.json @@ -0,0 +1,5 @@ +{ + "references": [ + { "path": "../logic" } + ] +} From 76d2ba64df3e520a58cb58925d418c3e760793dc Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 21 May 2018 21:24:29 -0700 Subject: [PATCH 08/48] Testing WIP --- src/compiler/tsbuild.ts | 22 +++--- src/harness/unittests/tsbuild.ts | 88 ++++++++++++++++++---- tests/projects/sample1/core/index.ts | 1 + tests/projects/sample1/core/tsconfig.json | 5 +- tests/projects/sample1/logic/index.ts | 2 +- tests/projects/sample1/logic/tsconfig.json | 4 + tests/projects/sample1/tests/index.ts | 4 +- tests/projects/sample1/tests/tsconfig.json | 3 +- tests/projects/sample1/ui/tsconfig.json | 2 +- 9 files changed, 101 insertions(+), 30 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index d9c3eb4212d..34f013fa017 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -317,16 +317,12 @@ namespace ts { return date2 > date1 ? date2 : date1; } - function older(date1: Date, date2: Date): Date { - return date2 < date1 ? date2 : date1; - } - function isDeclarationFile(fileName: string) { return fileExtensionIs(fileName, ".d.ts"); } - export function createBuildContext(options: BuildOptions): BuildContext { - const verboseDiag = options.verbose && createDiagnosticReporter(sys, /*pretty*/ false); + export function createBuildContext(options: BuildOptions, reportDiagnostic: DiagnosticReporter): BuildContext { + const verboseDiag = options.verbose && reportDiagnostic; return { options, projectStatus: createFileMap(), @@ -371,8 +367,7 @@ namespace ts { addProject("."); } - const context = createBuildContext({ verbose, dry, force }); - const builder = createSolutionBuilder(host, reportDiagnostic, context); + const builder = createSolutionBuilder(host, reportDiagnostic, { verbose, dry, force }); if (clean) { builder.cleanProjects(projects); } @@ -391,8 +386,9 @@ namespace ts { } } - export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, context: BuildContext) { + export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, options: BuildOptions) { const configFileCache = createConfigFileCache(host); + let context: BuildContext = undefined!; return { getUpToDateStatus, @@ -473,7 +469,7 @@ namespace ts { oldestOutputFileTime = outputTime; oldestOutputFileName = output; } - newestOutputFileTime = older(newestOutputFileTime, outputTime); + newestOutputFileTime = newer(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 @@ -709,6 +705,8 @@ namespace ts { } function cleanProjects(configFileNames: string[]) { + context = createBuildContext(options, reportDiagnostic); + // Get the same graph for cleaning we'd use for building const graph = createDependencyGraph(configFileNames); @@ -736,6 +734,8 @@ namespace ts { } function buildProjects(configFileNames: string[]) { + context = createBuildContext(options, reportDiagnostic); + const resolvedNames: string[] = []; for (const name of configFileNames) { let fullPath = resolvePath(host.getCurrentDirectory(), name); @@ -823,7 +823,7 @@ namespace ts { context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFileName, status.missingOutputFileName); return; case UpToDateStatusType.UpToDate: - context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestDeclarationFileContentChangedTime as any, status.newestOutputFileTime); + context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestInputFileTime, status.newestOutputFileTime); return; case UpToDateStatusType.UpToDateWithUpstreamTypes: context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 5bd713f377f..809d7bb1b0b 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -3,7 +3,7 @@ namespace ts { let currentTime = 100; const bfs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); - const lastDiagnostics: Diagnostic[] = []; + let lastDiagnostics: Diagnostic[] = []; const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); const sampleRoot = resolvePath(__dirname, "../../tests/projects/sample1"); @@ -12,47 +12,109 @@ namespace ts { bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))); bfs.meta.set("defaultLibLocation", "/lib"); bfs.makeReadonly(); + tick(); describe("tsbuild tests", () => { - it("builds the referenced project", () => { + it("can build the sample project 'tests' without error", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, createBuildContext({ dry: false, force: false, verbose: false })); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); fs.chdir("/src/tests"); - fs.debugPrint(); builder.buildProjects(["."]); - printDiagnostics(); - fs.debugPrint(); - assertDiagnosticMessages(Diagnostics.File_0_does_not_exist); + assertDiagnosticMessages(/*empty*/); + }); + it("can detect when and what to rebuild", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); + + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0); tick(); + + // All three projects are up to date + clearDiagnostics(); + builder.buildProjects(["."]); + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); + tick(); + + // Update a file in the leaf node (tests), only it should rebuild the last one + clearDiagnostics(); + fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); + builder.buildProjects(["."]); + + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + Diagnostics.Building_project_0); + tick(); + + // Update a file in the parent (without affecting types), should get fast downstream builds + clearDiagnostics(); + replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); + builder.buildProjects(["."]); + + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Updating_output_timestamps_of_project_0, + Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Updating_output_timestamps_of_project_0); }); }); + function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); + } + const old = fs.readFileSync(path, 'utf-8'); + if (old.indexOf(oldText) < 0) { + throw new Error(`Text "${oldText}" does not exist in file ${path}`); + } + const newContent = old.replace(oldText, newText); + fs.writeFileSync(path, newContent, 'utf-8'); + } + function assertDiagnosticMessages(...expected: DiagnosticMessage[]) { const actual = lastDiagnostics.slice(); - actual.sort((a, b) => b.code - a.code); - expected.sort((a, b) => b.code - a.code); if (actual.length !== expected.length) { - assert.fail(actual, expected, `Diagnostic arrays did not match - expected ${actual.join(",")}, got ${expected.join(",")}`); + assert.fail(actual, expected, `Diagnostic arrays did not match - expected\r\n${actual.map(a => " " + a.messageText).join("\r\n")}\r\ngot\r\n${expected.map(e => " " + e.message).join("\r\n")}`); } for (let i = 0; i < actual.length; i++) { if (actual[i].code !== expected[i].code) { - assert.fail(actual[i].messageText, expected[i].message, "Mismatched error code"); + assert.fail(actual[i].messageText, expected[i].message, `Mismatched error code - expected diagnostic ${i} "${actual[i].messageText}" to match ${expected[i].message}`); } } } - export function printDiagnostics() { + function clearDiagnostics() { + lastDiagnostics = []; + } + + export function printDiagnostics(header = "== Diagnostics ==") { const out = createDiagnosticReporter(sys); + sys.write(header + "\r\n"); for (const d of lastDiagnostics) { out(d); } } function tick() { - currentTime += 10; + currentTime += 100000; } function time() { return currentTime; diff --git a/tests/projects/sample1/core/index.ts b/tests/projects/sample1/core/index.ts index 9ade19f5e2e..529a7f549ec 100644 --- a/tests/projects/sample1/core/index.ts +++ b/tests/projects/sample1/core/index.ts @@ -1,2 +1,3 @@ +export const someString: string = "HELLO WORLD"; export function leftPad(s: string, n: number) { return s + n; } export function multiply(a: number, b: number) { return a * b; } diff --git a/tests/projects/sample1/core/tsconfig.json b/tests/projects/sample1/core/tsconfig.json index a514dfe8a03..b8332f5c476 100644 --- a/tests/projects/sample1/core/tsconfig.json +++ b/tests/projects/sample1/core/tsconfig.json @@ -1,3 +1,6 @@ { - + "compilerOptions": { + "composite": true, + "declaration": true + } } \ No newline at end of file diff --git a/tests/projects/sample1/logic/index.ts b/tests/projects/sample1/logic/index.ts index cccadd1718b..fd6b2106bb8 100644 --- a/tests/projects/sample1/logic/index.ts +++ b/tests/projects/sample1/logic/index.ts @@ -1,4 +1,4 @@ -import * as c from '../core'; +import * as c from '../core/index'; export function getSecondsInDay() { return c.multiply(10, 15); } diff --git a/tests/projects/sample1/logic/tsconfig.json b/tests/projects/sample1/logic/tsconfig.json index 3c2056eec88..a58b3a9f48e 100644 --- a/tests/projects/sample1/logic/tsconfig.json +++ b/tests/projects/sample1/logic/tsconfig.json @@ -1,4 +1,8 @@ { + "compilerOptions": { + "composite": true, + "declaration": true + }, "references": [ { "path": "../core" } ] diff --git a/tests/projects/sample1/tests/index.ts b/tests/projects/sample1/tests/index.ts index e9a1ebde3e1..f89dcd08a82 100644 --- a/tests/projects/sample1/tests/index.ts +++ b/tests/projects/sample1/tests/index.ts @@ -1,5 +1,5 @@ -import * as c from '../core'; -import * as logic from '../logic'; +import * as c from '../core/index'; +import * as logic from '../logic/index'; c.leftPad("", 10); logic.getSecondsInDay(); diff --git a/tests/projects/sample1/tests/tsconfig.json b/tests/projects/sample1/tests/tsconfig.json index dd1bf3bae6b..437d8ca6fb3 100644 --- a/tests/projects/sample1/tests/tsconfig.json +++ b/tests/projects/sample1/tests/tsconfig.json @@ -2,5 +2,6 @@ "references": [ { "path": "../core" }, { "path": "../logic" } - ] + ], + "files": ["index.ts"] } \ No newline at end of file diff --git a/tests/projects/sample1/ui/tsconfig.json b/tests/projects/sample1/ui/tsconfig.json index 45eff16d4d9..d843e35c549 100644 --- a/tests/projects/sample1/ui/tsconfig.json +++ b/tests/projects/sample1/ui/tsconfig.json @@ -1,5 +1,5 @@ { "references": [ - { "path": "../logic" } + { "path": "../logic/index" } ] } From 5a9a88320a4438a726b999c5d5ed0e77565c3437 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 22 May 2018 00:15:27 -0700 Subject: [PATCH 09/48] WIP --- src/compiler/tsbuild.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 34f013fa017..ac2462b2c07 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -388,15 +388,25 @@ namespace ts { export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, options: BuildOptions) { const configFileCache = createConfigFileCache(host); - let context: BuildContext = undefined!; + let context = createBuildContext(options, reportDiagnostic); return { getUpToDateStatus, + getUpToDateStatusOfFile, buildProjects, - cleanProjects + cleanProjects, + resetBuildContext }; - function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { + function resetBuildContext() { + context = createBuildContext(options, reportDiagnostic); + } + + function getUpToDateStatusOfFile(configFileName: string): UpToDateStatus { + return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); + } + + function getUpToDateStatus(project: ParsedCommandLine): UpToDateStatus { const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath); if (prior !== undefined) { return prior; @@ -705,8 +715,6 @@ namespace ts { } function cleanProjects(configFileNames: string[]) { - context = createBuildContext(options, reportDiagnostic); - // Get the same graph for cleaning we'd use for building const graph = createDependencyGraph(configFileNames); @@ -734,8 +742,6 @@ namespace ts { } function buildProjects(configFileNames: string[]) { - context = createBuildContext(options, reportDiagnostic); - const resolvedNames: string[] = []; for (const name of configFileNames) { let fullPath = resolvePath(host.getCurrentDirectory(), name); From 18f2baa88bdb1728cc5428285ec56d8eada4186e Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 22 May 2018 10:16:24 -0700 Subject: [PATCH 10/48] Fix errors --- src/compiler/tsbuild.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index ac2462b2c07..b157a1a4fe4 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -502,7 +502,7 @@ namespace ts { if (project.projectReferences) { for (const ref of project.projectReferences) { const resolvedRef = resolveProjectReferencePath(host, ref); - const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef), context); + const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) if (refStatus.type !== UpToDateStatusType.UpToDate) { @@ -767,7 +767,7 @@ namespace ts { let next: string; while (next = getNext()) { const proj = configFileCache.parseConfigFile(next); - const status = getUpToDateStatus(proj, context); + const status = getUpToDateStatus(proj); reportProjectStatus(next, status); if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { From fa07830ca9d970f6695b4aa210c9bace26be7993 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 22 May 2018 12:53:31 -0700 Subject: [PATCH 11/48] Passing tests --- src/compiler/diagnosticMessages.json | 4 +++ src/compiler/tsbuild.ts | 11 +++++-- src/harness/unittests/tsbuild.ts | 44 ++++++++++++++++++---------- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 1077db69ef7..8f1da78f1e9 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3630,6 +3630,10 @@ "category": "Message", "code": 6358 }, + "Project '{0}' is up to date because it was previously built": { + "category": "Message", + "code": 6359 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index b157a1a4fe4..10ed5280f71 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -514,13 +514,13 @@ namespace ts { // 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) { + 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) { + if (refStatus.newestDeclarationFileContentChangedTime <= oldestOutputFileTime) { pseudoUpToDate = true; continue; } @@ -829,7 +829,12 @@ namespace ts { context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFileName, status.missingOutputFileName); return; case UpToDateStatusType.UpToDate: - context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestInputFileTime, status.newestOutputFileTime); + if (status.newestInputFileTime !== undefined) { + context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestInputFileTime, status.newestOutputFileTime); + } + else { + context.verbose(Diagnostics.Project_0_is_up_to_date_because_it_was_previously_built, configFileName); + } return; case UpToDateStatusType.UpToDateWithUpstreamTypes: context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 809d7bb1b0b..5ac4e19e769 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -1,5 +1,3 @@ -/// - namespace ts { let currentTime = 100; const bfs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); @@ -14,8 +12,8 @@ namespace ts { bfs.makeReadonly(); tick(); - describe("tsbuild tests", () => { - it("can build the sample project 'tests' without error", () => { + describe("tsbuild - sanity check of clean build of 'sample1' project", () => { + it("can build the sample project 'sample1' without error", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); @@ -24,13 +22,17 @@ namespace ts { builder.buildProjects(["."]); assertDiagnosticMessages(/*empty*/); }); + }); - it("can detect when and what to rebuild", () => { - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); + describe("tsbuild - can detect when and what to rebuild", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); - fs.chdir("/src/tests"); + fs.chdir("/src/tests"); + + it("Builds the project", () => { + builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, @@ -40,19 +42,25 @@ namespace ts { Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0); tick(); + }); - // All three projects are up to date + // All three projects are up to date + it("Detects that all projects are up to date", () => { clearDiagnostics(); + builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); tick(); + }); - // Update a file in the leaf node (tests), only it should rebuild the last one + // Update a file in the leaf node (tests), only it should rebuild the last one + it("Only builds the leaf node project", () => { clearDiagnostics(); fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); + builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, @@ -61,10 +69,13 @@ namespace ts { Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, Diagnostics.Building_project_0); tick(); + }); - // Update a file in the parent (without affecting types), should get fast downstream builds + // Update a file in the parent (without affecting types), should get fast downstream builds + it("Detects type-only changes in upstream projects", () => { clearDiagnostics(); replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); + builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, @@ -81,12 +92,12 @@ namespace ts { if (!fs.statSync(path).isFile()) { throw new Error(`File ${path} does not exist`); } - const old = fs.readFileSync(path, 'utf-8'); + const old = fs.readFileSync(path, "utf-8"); if (old.indexOf(oldText) < 0) { throw new Error(`Text "${oldText}" does not exist in file ${path}`); } const newContent = old.replace(oldText, newText); - fs.writeFileSync(path, newContent, 'utf-8'); + fs.writeFileSync(path, newContent, "utf-8"); } function assertDiagnosticMessages(...expected: DiagnosticMessage[]) { @@ -114,8 +125,9 @@ namespace ts { } function tick() { - currentTime += 100000; + currentTime += 60_000; } + function time() { return currentTime; } @@ -126,7 +138,7 @@ namespace ts { const file = getBaseFileName(path); vfs.writeFileSync(virtualRoot + "/" + file, Harness.IO.readFile(localRoot + "/" + file)); } - for (const dir of Harness.IO.getDirectories(localRoot)){ + for (const dir of Harness.IO.getDirectories(localRoot)) { loadFsMirror(vfs, localRoot + "/" + dir, virtualRoot + "/" + dir); } } From 5a664be278ed6213d7ff54a4515ca6a130f4a8eb Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 22 May 2018 13:34:52 -0700 Subject: [PATCH 12/48] Baseline accept --- tests/baselines/reference/api/tsserverlibrary.d.ts | 9 +++++++++ tests/baselines/reference/api/typescript.d.ts | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index c069dbf4d04..3a7fe516e59 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2630,6 +2630,9 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): (ResolvedTypeReferenceDirective | undefined)[]; getEnvironmentVariable?(name: string): string; createHash?(data: string): string; + getModifiedTime?(fileName: string): Date; + setModifiedTime?(fileName: string, date: Date): void; + deleteFile?(fileName: string): void; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -2979,6 +2982,8 @@ declare namespace ts { getDirectories(path: string): string[]; readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; /** * This should be cryptographically secure. * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) @@ -4003,6 +4008,10 @@ declare namespace ts { */ function createProgram(createProgramOptions: CreateProgramOptions): Program; function createProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, configFileParsingDiagnostics?: ReadonlyArray): Program; + /** + * Returns the target config filename of a project reference + */ + function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined; } declare namespace ts { interface EmitOutput { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 4887de7fd2f..4fc382b5d2b 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2630,6 +2630,9 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): (ResolvedTypeReferenceDirective | undefined)[]; getEnvironmentVariable?(name: string): string; createHash?(data: string): string; + getModifiedTime?(fileName: string): Date; + setModifiedTime?(fileName: string, date: Date): void; + deleteFile?(fileName: string): void; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -2979,6 +2982,8 @@ declare namespace ts { getDirectories(path: string): string[]; readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; /** * This should be cryptographically secure. * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) @@ -4003,6 +4008,10 @@ declare namespace ts { */ function createProgram(createProgramOptions: CreateProgramOptions): Program; function createProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, configFileParsingDiagnostics?: ReadonlyArray): Program; + /** + * Returns the target config filename of a project reference + */ + function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined; } declare namespace ts { interface EmitOutput { From d4a56b910f2dfa6571937eda4f1d24b5144dcf6a Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 22 May 2018 18:10:03 -0700 Subject: [PATCH 13/48] WIP more tests --- src/harness/unittests/tsbuild.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 5ac4e19e769..e84fa403475 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -21,6 +21,12 @@ namespace ts { fs.chdir("/src/tests"); builder.buildProjects(["."]); assertDiagnosticMessages(/*empty*/); + + // Check for outputs. Not an exhaustive list + const expectedOutputs = ["/src/tests/index.js", "/src/core/index.js", "/src/core/index.d.ts"]; + for (const output of expectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } }); }); From fc22b5b146a651fa018d9d68f0ed2bf6a9eac8e1 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 10:59:07 -0700 Subject: [PATCH 14/48] WIP --- src/compiler/diagnosticMessages.json | 4 ++ src/compiler/tsbuild.ts | 52 +++++++++++----- src/harness/fakes.ts | 8 +++ src/harness/unittests/tsbuild.ts | 90 +++++++++++++++++++++++++++- 4 files changed, 138 insertions(+), 16 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 8f1da78f1e9..bfd261802f4 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3634,6 +3634,10 @@ "category": "Message", "code": 6359 }, + "Project '{0}' is up to date": { + "category": "Message", + "code": 6360 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 10ed5280f71..ec9e18899aa 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -608,10 +608,11 @@ namespace ts { } } - // TODO Accept parsedCommandLine + // TODO Accept parsedCommandLine instead? function buildSingleProject(proj: string) { if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); + return; } context.verbose(Diagnostics.Building_project_0, proj); @@ -714,34 +715,47 @@ namespace ts { context.projectStatus.setValue(proj.options.configFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); } - function cleanProjects(configFileNames: string[]) { - // Get the same graph for cleaning we'd use for building - const graph = createDependencyGraph(configFileNames); + function getFilesToClean(configFileNames: string[]): string[] | undefined { + const resolvedNames: string[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return; - const fileReport: string[] = []; + // Get the same graph for cleaning we'd use for building + const graph = createDependencyGraph(resolvedNames); + + const filesToDelete: string[] = []; for (const level of graph.buildQueue) { for (const proj of level) { const parsed = configFileCache.parseConfigFile(proj); const outputs = getAllProjectOutputs(parsed); for (const output of outputs) { if (host.fileExists(output)) { - if (context.options.dry) { - fileReport.push(output); - } - else { - host.deleteFile(output); - } + filesToDelete.push(output); } } } } + return filesToDelete; + } + + function cleanProjects(configFileNames: string[]) { + const filesToDelete = getFilesToClean(configFileNames); if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join(""))); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); + } + else { + if (!host.deleteFile) { + throw new Error("Host does not support deleting files"); + } + + for (const output of filesToDelete) { + host.deleteFile(output); + } } } - function buildProjects(configFileNames: string[]) { + // TODO add branding to resolved filenames + function resolveProjectNames(configFileNames: string[]): string[] | undefined { const resolvedNames: string[] = []; for (const name of configFileNames) { let fullPath = resolvePath(host.getCurrentDirectory(), name); @@ -755,8 +769,14 @@ namespace ts { continue; } reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); - return; + return undefined; } + return resolvedNames; + } + + function buildProjects(configFileNames: string[]) { + const resolvedNames: string[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return; // Establish what needs to be built const graph = createDependencyGraph(resolvedNames); @@ -772,6 +792,10 @@ namespace ts { if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { // Up to date, skip + if (options.dry) { + // In a dry build, inform the user of this fact + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date)); + } continue; } diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index f5a2861d385..958629a0988 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -51,6 +51,10 @@ namespace fakes { this.vfs.writeFileSync(path, writeByteOrderMark ? utils.addUTF8ByteOrderMark(data) : data); } + public deleteFile(path: string) { + this.vfs.unlinkSync(path); + } + public fileExists(path: string) { const stats = this._getStats(path); return stats ? stats.isFile() : false; @@ -248,6 +252,10 @@ namespace fakes { return this.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); } + public deleteFile(fileName: string) { + this.sys.deleteFile(fileName); + } + public fileExists(fileName: string): boolean { return this.sys.fileExists(fileName); } diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index e84fa403475..a761e154898 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -11,6 +11,9 @@ namespace ts { bfs.meta.set("defaultLibLocation", "/lib"); bfs.makeReadonly(); tick(); + const allExpectedOutputs = ["/src/tests/index.js", + "/src/core/index.js", "/src/core/index.d.ts", + "/src/logic/index.js", "/src/logic/index.d.ts"]; describe("tsbuild - sanity check of clean build of 'sample1' project", () => { it("can build the sample project 'sample1' without error", () => { @@ -23,13 +26,95 @@ namespace ts { assertDiagnosticMessages(/*empty*/); // Check for outputs. Not an exhaustive list - const expectedOutputs = ["/src/tests/index.js", "/src/core/index.js", "/src/core/index.d.ts"]; - for (const output of expectedOutputs) { + for (const output of allExpectedOutputs) { assert(fs.existsSync(output), `Expect file ${output} to exist`); } }); }); + describe("tsbuild - dry builds", () => { + it("doesn't write any files in a dry build", () => { + clearDiagnostics(); + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); + + // Check for outputs to not be written. Not an exhaustive list + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + }); + + it("indicates that it would skip builds during a dry build", () => { + clearDiagnostics(); + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + + let builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + tick(); + + clearDiagnostics(); + builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildProjects(["."]); + assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); + }); + }); + + describe("tsbuild - clean builds", () => { + it("removes all files it built", () => { + clearDiagnostics(); + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + // Verify they exist + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + builder.cleanProjects(["."]); + // Verify they are gone + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + // Subsequent clean shouldn't throw / etc + builder.cleanProjects(["."]); + }); + }); + + describe("tsbuild - force builds", () => { + it("always builds under --force", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: true, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + let currentTime = time(); + checkOutputTimestamps(currentTime); + + tick(); + Debug.assert(time() !== currentTime, "Time moves on"); + currentTime = time(); + builder.buildProjects(["."]); + checkOutputTimestamps(currentTime); + + function checkOutputTimestamps(expected: number) { + // Check timestamps + for (const output of allExpectedOutputs) { + const actual = fs.statSync(output).mtimeMs; + assert(actual === expected, `File ${output} has timestamp ${actual}, expected ${expected}`); + } + } + }); + }); + describe("tsbuild - can detect when and what to rebuild", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); @@ -38,6 +123,7 @@ namespace ts { fs.chdir("/src/tests"); it("Builds the project", () => { + clearDiagnostics(); builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, From 8a883ae2047e915cb080eefa42dd2fbf3ea7f1c0 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 12:38:33 -0700 Subject: [PATCH 15/48] Comments --- src/compiler/tsbuild.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index ec9e18899aa..406d9a3c2ff 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -252,7 +252,6 @@ namespace ts { } function getOutputJavaScriptFileName(inputFileName: string, configFile: ParsedCommandLine) { - // TODO handle JSX: Preserve const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ 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"); @@ -295,7 +294,7 @@ namespace ts { const cache = createFileMap(); const configParseHost = parseConfigHostFromCompilerHost(host); - // TODO: Cache invalidation under --watch! + // TODO: Cache invalidation under --watch function parseConfigFile(configFilePath: string) { const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; @@ -591,7 +590,6 @@ namespace ts { /** * Removes entries from arrays which appear in later arrays. - * TODO: Use a lookup object to optimize this a bit? */ function removeDuplicatesFromBuildQueue(queue: string[][]): void { // No need to check the last array From 07812796d18cba1f54805d47c978bc77ea55a4f7 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 12:46:28 -0700 Subject: [PATCH 16/48] Add resolution branding --- src/compiler/tsbuild.ts | 48 ++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 406d9a3c2ff..b2ccdbd5f6c 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,4 +1,6 @@ namespace ts { + type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never }; + const minimumDate = new Date(-8640000000000000); const maximumDate = new Date(8640000000000000); @@ -24,7 +26,7 @@ namespace ts { type Mapper = ReturnType; interface DependencyGraph { - buildQueue: string[][]; + buildQueue: ResolvedConfigFileName[][]; dependencyMap: Mapper; } @@ -296,7 +298,7 @@ namespace ts { // TODO: Cache invalidation under --watch - function parseConfigFile(configFilePath: string) { + function parseConfigFile(configFilePath: ResolvedConfigFileName) { const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; if (sourceFile === undefined) { return undefined; @@ -401,7 +403,7 @@ namespace ts { context = createBuildContext(options, reportDiagnostic); } - function getUpToDateStatusOfFile(configFileName: string): UpToDateStatus { + function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); } @@ -500,7 +502,7 @@ namespace ts { // See if any of its upstream projects are newer than it if (project.projectReferences) { for (const ref of project.projectReferences) { - const resolvedRef = resolveProjectReferencePath(host, ref); + const resolvedRef = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName; const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) @@ -544,14 +546,14 @@ namespace ts { } // TODO: Use the better algorithm - function createDependencyGraph(roots: string[]): DependencyGraph { + function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph { // This is a list of list of projects that need to be built. // The ordering here is "backwards", i.e. the first entry in the array is the last set of projects that need to be built; // and the last entry is the first set of projects to be built. // Each subarray is effectively unordered. // We traverse the reference graph from each root, then "clean" the list by removing // any entry that is duplicated to its right. - const buildQueue: string[][] = []; + const buildQueue: ResolvedConfigFileName[][] = []; const dependencyMap = createDependencyMapper(); let buildQueuePosition = 0; for (const root of roots) { @@ -560,7 +562,7 @@ namespace ts { reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, root)); continue; } - enumerateReferences(normalizePath(root), config); + enumerateReferences(normalizePath(root) as ResolvedConfigFileName, config); } removeDuplicatesFromBuildQueue(buildQueue); @@ -569,7 +571,7 @@ namespace ts { dependencyMap }; - function enumerateReferences(fileName: string, root: ParsedCommandLine): void { + function enumerateReferences(fileName: ResolvedConfigFileName, root: ParsedCommandLine): void { const myBuildLevel = buildQueue[buildQueuePosition] = buildQueue[buildQueuePosition] || []; if (myBuildLevel.indexOf(fileName) < 0) { myBuildLevel.push(fileName); @@ -579,11 +581,11 @@ namespace ts { if (refs === undefined) return; buildQueuePosition++; for (const ref of refs) { - const actualPath = resolveProjectReferencePath(host, ref); + const actualPath = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName; dependencyMap.addReference(fileName, actualPath); const resolvedRef = configFileCache.parseConfigFile(actualPath); if (resolvedRef === undefined) continue; - enumerateReferences(normalizePath(actualPath), resolvedRef); + enumerateReferences(normalizePath(actualPath) as ResolvedConfigFileName, resolvedRef); } buildQueuePosition--; } @@ -607,7 +609,7 @@ namespace ts { } // TODO Accept parsedCommandLine instead? - function buildSingleProject(proj: string) { + function buildSingleProject(proj: ResolvedConfigFileName) { if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); return; @@ -713,8 +715,8 @@ namespace ts { context.projectStatus.setValue(proj.options.configFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); } - function getFilesToClean(configFileNames: string[]): string[] | undefined { - const resolvedNames: string[] | undefined = resolveProjectNames(configFileNames); + function getFilesToClean(configFileNames: ResolvedConfigFileName[]): string[] | undefined { + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); if (resolvedNames === undefined) return; // Get the same graph for cleaning we'd use for building @@ -736,7 +738,10 @@ namespace ts { } function cleanProjects(configFileNames: string[]) { - const filesToDelete = getFilesToClean(configFileNames); + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return; + + const filesToDelete = getFilesToClean(resolvedNames); if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); @@ -752,18 +757,17 @@ namespace ts { } } - // TODO add branding to resolved filenames - function resolveProjectNames(configFileNames: string[]): string[] | undefined { - const resolvedNames: string[] = []; + function resolveProjectNames(configFileNames: string[]): ResolvedConfigFileName[] | undefined { + const resolvedNames: ResolvedConfigFileName[] = []; for (const name of configFileNames) { let fullPath = resolvePath(host.getCurrentDirectory(), name); if (host.fileExists(fullPath)) { - resolvedNames.push(fullPath); + resolvedNames.push(fullPath as ResolvedConfigFileName); continue; } fullPath = combinePaths(fullPath, "tsconfig.json"); if (host.fileExists(fullPath)) { - resolvedNames.push(fullPath); + resolvedNames.push(fullPath as ResolvedConfigFileName); continue; } reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); @@ -773,7 +777,7 @@ namespace ts { } function buildProjects(configFileNames: string[]) { - const resolvedNames: string[] | undefined = resolveProjectNames(configFileNames); + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); if (resolvedNames === undefined) return; // Establish what needs to be built @@ -782,7 +786,7 @@ namespace ts { const queue = graph.buildQueue; reportBuildQueue(graph); - let next: string; + let next: ResolvedConfigFileName; while (next = getNext()) { const proj = configFileCache.parseConfigFile(next); const status = getUpToDateStatus(proj); @@ -809,7 +813,7 @@ namespace ts { } } - function getNext(): string | undefined { + function getNext(): ResolvedConfigFileName | undefined { if (queue.length === 0) { return undefined; } From d9bfbfe3bef15f01defc5b0db20e2f0be9660db0 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 13:01:44 -0700 Subject: [PATCH 17/48] Fix tests run in parallel --- src/harness/unittests/tsbuild.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index a761e154898..1596cc4ec19 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -21,6 +21,7 @@ namespace ts { const host = new fakes.CompilerHost(fs); const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); + clearDiagnostics(); fs.chdir("/src/tests"); builder.buildProjects(["."]); assertDiagnosticMessages(/*empty*/); From 19ab8abbb9deb35ef65228c1347878a41fd8eb7d Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 13:42:12 -0700 Subject: [PATCH 18/48] Fix strictNullChecks breaks --- src/compiler/program.ts | 6 +-- src/compiler/tsbuild.ts | 66 ++++++++++++++++++++++---------- src/harness/unittests/tsbuild.ts | 6 +-- src/harness/vfs.ts | 3 ++ 4 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index b4b31226837..5b39c79aeac 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -190,9 +190,9 @@ namespace ts { getDirectories: (path: string) => sys.getDirectories(path), realpath, readDirectory: (path, extensions, include, exclude, depth) => sys.readDirectory(path, extensions, include, exclude, depth), - getModifiedTime: path => sys.getModifiedTime(path), - setModifiedTime: (path, date) => sys.setModifiedTime(path, date), - deleteFile: path => sys.deleteFile(path) + getModifiedTime: sys.getModifiedTime && (path => sys.getModifiedTime!(path)), + setModifiedTime: sys.setModifiedTime && ((path, date) => sys.setModifiedTime!(path, date)), + deleteFile: sys.deleteFile && (path => sys.deleteFile!(path)) }; } diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index b2ccdbd5f6c..bbd8e68301f 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -248,14 +248,14 @@ namespace ts { } function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine) { - const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ true); - const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath!), inputFileName, /*ignoreCase*/ 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: ParsedCommandLine) { - const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ true); - const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath!), inputFileName, /*ignoreCase*/ 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"); } @@ -276,7 +276,9 @@ namespace ts { } function getOutFileOutputs(project: ParsedCommandLine): ReadonlyArray { - Debug.assert(!!project.options.outFile, "outFile must be set"); + if (!project.options.outFile) { + throw new Error("Assert - outFile must be set"); + } const outputs: string[] = []; outputs.push(project.options.outFile); if (project.options.declaration) { @@ -328,9 +330,7 @@ namespace ts { options, projectStatus: createFileMap(), unchangedOutputs: createFileMap(), - verbose: options.verbose ? (diag, ...args) => { - verboseDiag(createCompilerDiagnostic(diag, ...args)); - } : () => undefined + verbose: verboseDiag ? (diag, ...args) => verboseDiag(createCompilerDiagnostic(diag, ...args)) : () => undefined }; } @@ -379,6 +379,11 @@ namespace ts { function addProject(projectSpecification: string) { const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification); const refPath = resolveProjectReferencePath(host, { path: fileName }); + if (!refPath) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, projectSpecification)); + return; + } + if (!host.fileExists(refPath)) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); } @@ -388,6 +393,10 @@ namespace ts { } export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, options: BuildOptions) { + if (!host.getModifiedTime || !host.setModifiedTime) { + throw new Error("Host must support timestamp APIs"); + } + const configFileCache = createConfigFileCache(host); let context = createBuildContext(options, reportDiagnostic); @@ -407,13 +416,17 @@ namespace ts { return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); } - function getUpToDateStatus(project: ParsedCommandLine): UpToDateStatus { - const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath); + function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { + if (project === undefined) { + return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; + } + + const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath!); if (prior !== undefined) { return prior; } const actual = getUpToDateStatusWorker(project); - context.projectStatus.setValue(project.options.configFilePath, actual); + context.projectStatus.setValue(project.options.configFilePath!, actual); return actual; } @@ -442,7 +455,7 @@ namespace ts { }; } - const inputTime = host.getModifiedTime(inputFile); + const inputTime = host.getModifiedTime!(inputFile); if (inputTime > newestInputFileTime) { newestInputFileName = inputFile; newestInputFileTime = inputTime; @@ -466,7 +479,7 @@ namespace ts { }; } - const outputTime = host.getModifiedTime(output); + const outputTime = host.getModifiedTime!(output); // If an output is older than the newest input, we can stop checking if (outputTime < newestInputFileTime) { return { @@ -492,7 +505,7 @@ namespace ts { newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); } else { - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, host.getModifiedTime(output)); + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, host.getModifiedTime!(output)); } } } @@ -609,10 +622,10 @@ namespace ts { } // TODO Accept parsedCommandLine instead? - function buildSingleProject(proj: ResolvedConfigFileName) { + function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); - return; + return BuildResultFlags.Success; } context.verbose(Diagnostics.Building_project_0, proj); @@ -707,17 +720,17 @@ namespace ts { let priorNewestUpdateTime = minimumDate; for (const file of outputs) { if (isDeclarationFile(file)) { - priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file)); + priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime!(file)); } - host.setModifiedTime(file, now); + host.setModifiedTime!(file, now); } - context.projectStatus.setValue(proj.options.configFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); + context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); } function getFilesToClean(configFileNames: ResolvedConfigFileName[]): string[] | undefined { const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); - if (resolvedNames === undefined) return; + if (resolvedNames === undefined) return undefined; // Get the same graph for cleaning we'd use for building const graph = createDependencyGraph(resolvedNames); @@ -726,6 +739,10 @@ namespace ts { for (const level of graph.buildQueue) { for (const proj of level) { const parsed = configFileCache.parseConfigFile(proj); + if (parsed === undefined) { + // File has gone missing; fine to ignore here + continue; + } const outputs = getAllProjectOutputs(parsed); for (const output of outputs) { if (host.fileExists(output)) { @@ -742,6 +759,9 @@ namespace ts { if (resolvedNames === undefined) return; const filesToDelete = getFilesToClean(resolvedNames); + if (filesToDelete === undefined) { + return; + } if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); @@ -786,9 +806,13 @@ namespace ts { const queue = graph.buildQueue; reportBuildQueue(graph); - let next: ResolvedConfigFileName; + let next: ResolvedConfigFileName | undefined; while (next = getNext()) { const proj = configFileCache.parseConfigFile(next); + if (proj === undefined) { + break; + } + const status = getUpToDateStatus(proj); reportProjectStatus(next, status); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 1596cc4ec19..4570c7598e7 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -7,7 +7,7 @@ namespace ts { const sampleRoot = resolvePath(__dirname, "../../tests/projects/sample1"); loadFsMirror(bfs, sampleRoot, "/src"); bfs.mkdirpSync("/lib"); - bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))); + bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))!); bfs.meta.set("defaultLibLocation", "/lib"); bfs.makeReadonly(); tick(); @@ -196,7 +196,7 @@ namespace ts { function assertDiagnosticMessages(...expected: DiagnosticMessage[]) { const actual = lastDiagnostics.slice(); if (actual.length !== expected.length) { - assert.fail(actual, expected, `Diagnostic arrays did not match - expected\r\n${actual.map(a => " " + a.messageText).join("\r\n")}\r\ngot\r\n${expected.map(e => " " + e.message).join("\r\n")}`); + assert.fail(actual, expected, `Diagnostic arrays did not match - got\r\n${actual.map(a => " " + a.messageText).join("\r\n")}\r\nexpected\r\n${expected.map(e => " " + e.message).join("\r\n")}`); } for (let i = 0; i < actual.length; i++) { if (actual[i].code !== expected[i].code) { @@ -229,7 +229,7 @@ namespace ts { vfs.mkdirpSync(virtualRoot); for (const path of Harness.IO.readDirectory(localRoot)) { const file = getBaseFileName(path); - vfs.writeFileSync(virtualRoot + "/" + file, Harness.IO.readFile(localRoot + "/" + file)); + vfs.writeFileSync(virtualRoot + "/" + file, Harness.IO.readFile(localRoot + "/" + file)!); } for (const dir of Harness.IO.getDirectories(localRoot)) { loadFsMirror(vfs, localRoot + "/" + dir, virtualRoot + "/" + dir); diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 09b2e39cd88..be8b6c3fed7 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -415,6 +415,9 @@ namespace vfs { */ public utimesSync(path: string, atime: Date, mtime: Date) { const entry = this._walk(this._resolve(path)); + if (!entry || !entry.node) { + throw createIOError("ENOENT"); + } entry.node.atimeMs = +atime; entry.node.mtimeMs = +mtime; } From cb8aa9b1a3b4d8abbb748d0bb3e733e9649d38bf Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 16:21:06 -0700 Subject: [PATCH 19/48] Don't use invalid cached SourceFiles --- src/harness/fakes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index ec731237db4..1bb358698a2 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -332,7 +332,7 @@ namespace fakes { if (cacheKey) { const meta = this.vfs.filemeta(canonicalFileName); const sourceFileFromMetadata = meta.get(cacheKey) as ts.SourceFile | undefined; - if (sourceFileFromMetadata) { + if (sourceFileFromMetadata && sourceFileFromMetadata.getFullText() === content) { this._sourceFiles.set(canonicalFileName, sourceFileFromMetadata); return sourceFileFromMetadata; } From 8ac795ba01f6457157f6bf4883a4c6a6c4ed9dee Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 16:21:32 -0700 Subject: [PATCH 20/48] Correctly skip upstream-blocked projects --- src/compiler/diagnosticMessages.json | 8 +++ src/compiler/tsbuild.ts | 94 ++++++++++++++++++++-------- src/harness/unittests/tsbuild.ts | 25 ++++++++ 3 files changed, 100 insertions(+), 27 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 40b32186d62..266233a7cdd 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3644,6 +3644,14 @@ "category": "Message", "code": 6360 }, + "Skipping build of project '{0}' because its upstream project '{1}' has errors": { + "category": "Message", + "code": 6361 + }, + "Project '{0}' can't be built because it depends on a project with errors": { + "category": "Message", + "code": 6362 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index bbd8e68301f..2b08474f019 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -69,7 +69,8 @@ namespace ts { OutputMissing, OutOfDateWithSelf, OutOfDateWithUpstream, - UpstreamOutOfDate + UpstreamOutOfDate, + UpstreamBlocked } type UpToDateStatus = @@ -78,7 +79,8 @@ namespace ts { | StatusOutputMissing | StatusOutOfDateWithSelf | StatusOutOfDateWithUpstream - | StatusUpstreamOutOfDate; + | StatusUpstreamOutOfDate + | StatusUpstreamBlocked; /** * The project can't be built at all in its current state. For example, @@ -128,6 +130,14 @@ namespace ts { upstreamProjectName: string; } + /** + * This project depends an upstream project with build errors + */ + interface StatusUpstreamBlocked { + type: UpToDateStatusType.UpstreamBlocked; + upstreamProjectName: string; + } + /** * One or more of the project's outputs is older than the newest output of * an upstream project. @@ -466,33 +476,33 @@ namespace ts { const outputs = getAllProjectOutputs(project); // Now see if all outputs are newer than the newest input - let oldestOutputFileName: string = undefined!; + let oldestOutputFileName: string | undefined; let oldestOutputFileTime: Date = maximumDate; let newestOutputFileTime: Date = minimumDate; let newestDeclarationFileContentChangedTime: Date = minimumDate; + let missingOutputFileName: string | undefined; + let isOutOfDateWithInputs = false; for (const output of outputs) { - // Output is missing + // Output is missing; can stop checking + // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status if (!host.fileExists(output)) { - return { - type: UpToDateStatusType.OutputMissing, - missingOutputFileName: output - }; + missingOutputFileName = output; + break; } const outputTime = host.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; } + + // If an output is older than the newest input, we can stop checking + // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status + if (outputTime < newestInputFileTime) { + isOutOfDateWithInputs = true; + break; + } + newestOutputFileTime = newer(newestOutputFileTime, outputTime); // Keep track of when the most recent time a .d.ts file was changed. @@ -511,13 +521,19 @@ namespace ts { } 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 if (project.projectReferences) { for (const ref of project.projectReferences) { const resolvedRef = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName; const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); + // An upstream project is blocked + if (refStatus.type === UpToDateStatusType.Unbuildable) { + return { + type: UpToDateStatusType.UpstreamBlocked, + upstreamProjectName: ref.path + }; + } + // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) if (refStatus.type !== UpToDateStatusType.UpToDate) { return { @@ -543,12 +559,27 @@ namespace ts { Debug.assert(oldestOutputFileName !== undefined, "Should have an oldest output filename here"); return { type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: oldestOutputFileName, + outOfDateOutputFileName: oldestOutputFileName!, newerProjectName: ref.path }; } } + if (missingOutputFileName !== undefined) { + return { + type: UpToDateStatusType.OutputMissing, + missingOutputFileName + }; + } + + if (isOutOfDateWithInputs) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: oldestOutputFileName!, + newerInputFileName: newestInputFileName + }; + } + // Up to date return { type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, @@ -637,6 +668,7 @@ namespace ts { if (!configFile) { // Failed to read the config file resultFlags |= BuildResultFlags.ConfigFileErrors; + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); return resultFlags; } @@ -660,6 +692,7 @@ namespace ts { for (const diag of syntaxDiagnostics) { reportDiagnostic(diag); } + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Syntactic errors" }); return resultFlags; } @@ -671,6 +704,7 @@ namespace ts { for (const diag of declDiagnostics) { reportDiagnostic(diag); } + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Declaration file errors" }); return resultFlags; } } @@ -681,6 +715,7 @@ namespace ts { for (const diag of semanticDiagnostics) { reportDiagnostic(diag); } + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Semantic errors" }); return resultFlags; } @@ -704,7 +739,6 @@ namespace ts { }); context.projectStatus.setValue(proj, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime } as UpToDateStatus); - return resultFlags; } @@ -812,15 +846,15 @@ namespace ts { if (proj === undefined) { break; } - const status = getUpToDateStatus(proj); reportProjectStatus(next, status); + const projName = proj.options.configFilePath; if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { // Up to date, skip if (options.dry) { // In a dry build, inform the user of this fact - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date, projName)); } continue; } @@ -831,10 +865,12 @@ namespace ts { continue; } - const result = buildSingleProject(next); - if (result & BuildResultFlags.AnyErrors) { - break; + if (status.type === UpToDateStatusType.UpstreamBlocked) { + context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors, projName, status.upstreamProjectName); + continue; } + + buildSingleProject(next); } function getNext(): ResolvedConfigFileName | undefined { @@ -889,9 +925,13 @@ namespace ts { case UpToDateStatusType.UpToDateWithUpstreamTypes: context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); return; - case UpToDateStatusType.UpstreamOutOfDate: + case UpToDateStatusType.UpstreamOutOfDate: context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); return; + case UpToDateStatusType.UpstreamBlocked: + context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); + return; + default: throw new Error(`Invalid build status - ${UpToDateStatusType[status.type]}`); } diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 4570c7598e7..f1f38bd2f9f 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -181,6 +181,31 @@ namespace ts { }); }); + describe("tsbuild - downstream-blocked compilations", () => { + it("won't build downstream projects if upstream projects have errors", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); + + clearDiagnostics(); + + // Induce an error in the middle project + replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + assertDiagnosticMessages( + Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Property_0_does_not_exist_on_type_1, + Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, + Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors + ); + }); + }); + function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { if (!fs.statSync(path).isFile()) { throw new Error(`File ${path} does not exist`); From 1863d3fd48f986f851739ceb2e6d7561244c6c40 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 17:48:45 -0700 Subject: [PATCH 21/48] Graph ordering test WIP --- src/compiler/tsbuild.ts | 201 ++++++++++++++++--------------- src/harness/unittests/tsbuild.ts | 42 +++++++ 2 files changed, 144 insertions(+), 99 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 2b08474f019..4bd6c13914d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,4 +1,9 @@ namespace ts { + /** + * Branded string for keeping track of when we've turned an ambiguous path + * specified like "./blah" to an absolute path to an actual + * tsconfig file, e.g. "/root/blah/tsconfig.json" + */ type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never }; const minimumDate = new Date(-8640000000000000); @@ -6,8 +11,16 @@ namespace ts { /** * 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 + * + * Callers may invoke any number of build requests within the same context; + * until the context is reset, each project will only be built at most once. + * + * Example: In a standard setup where project B depends on project A, and both are out of date, + * a failed build of A will result in A remaining out of date. When we try to build + * B, we should immediately bail instead of recomputing A's up-to-date status again. + * + * This also matters for performing fast (i.e. fake) downstream builds of projects + * when their upstream .d.ts files haven't changed content (but have newer timestamps) */ export interface BuildContext { options: BuildOptions; @@ -21,6 +34,9 @@ namespace ts { */ projectStatus: FileMap; + /** + * Issue a verbose diagnostic message. No-ops when options.verbose is false. + */ verbose(diag: DiagnosticMessage, ...args: any[]): void; } @@ -74,86 +90,86 @@ namespace ts { } type UpToDateStatus = - | StatusUnbuildable - | StatusUpToDate - | StatusOutputMissing - | StatusOutOfDateWithSelf - | StatusOutOfDateWithUpstream - | StatusUpstreamOutOfDate - | StatusUpstreamBlocked; + | Status.Unbuildable + | Status.UpToDate + | Status.OutputMissing + | Status.OutOfDateWithSelf + | Status.OutOfDateWithUpstream + | Status.UpstreamOutOfDate + | Status.UpstreamBlocked; - /** - * 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; + namespace Status { /** - * The name of the first output file that didn't exist + * 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 */ - missingOutputFileName: string; - } + export interface Unbuildable { + type: UpToDateStatusType.Unbuildable; + reason: string; + } - /** - * One or more of the project's outputs is older than its newest input. - */ - interface StatusOutOfDateWithSelf { - type: UpToDateStatusType.OutOfDateWithSelf; - outOfDateOutputFileName: string; - newerInputFileName: string; - } + /** + * The project is up to date with respect to its inputs. + * We track what the newest input file is. + */ + export interface UpToDate { + type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes; + newestInputFileTime: Date; + newestDeclarationFileContentChangedTime: Date; + newestOutputFileTime: Date; + } - /** - * 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 outputs of the project does not exist. + */ + export interface OutputMissing { + type: UpToDateStatusType.OutputMissing; + /** + * The name of the first output file that didn't exist + */ + missingOutputFileName: string; + } - /** - * This project depends an upstream project with build errors - */ - interface StatusUpstreamBlocked { - type: UpToDateStatusType.UpstreamBlocked; - upstreamProjectName: string; - } + /** + * One or more of the project's outputs is older than its newest input. + */ + export interface OutOfDateWithSelf { + type: UpToDateStatusType.OutOfDateWithSelf; + outOfDateOutputFileName: string; + newerInputFileName: 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; + /** + * This project depends on an out-of-date project, so shouldn't be built yet + */ + export interface UpstreamOutOfDate { + type: UpToDateStatusType.UpstreamOutOfDate; + upstreamProjectName: string; + } + + /** + * This project depends an upstream project with build errors + */ + export interface UpstreamBlocked { + type: UpToDateStatusType.UpstreamBlocked; + upstreamProjectName: string; + } + + /** + * One or more of the project's outputs is older than the newest output of + * an upstream project. + */ + export interface OutOfDateWithUpstream { + type: UpToDateStatusType.OutOfDateWithUpstream; + outOfDateOutputFileName: string; + newerProjectName: string; + } } interface FileMap { setValue(fileName: string, value: T): void; getValue(fileName: string): T | never; getValueOrUndefined(fileName: string): T | undefined; - getValueOrDefault(fileName: string, defaultValue: T): T; - tryGetValue(fileName: string): [false, undefined] | [true, T]; } /** @@ -167,8 +183,6 @@ namespace ts { setValue, getValue, getValueOrUndefined, - getValueOrDefault, - tryGetValue }; function setValue(fileName: string, value: T) { @@ -194,26 +208,6 @@ namespace ts { return undefined; } } - - function getValueOrDefault(fileName: string, defaultValue: T): T { - const f = normalizePath(fileName); - if (f in lookup) { - return lookup[f]; - } - else { - return defaultValue; - } - } - - function tryGetValue(fileName: string): [false, undefined] | [true, T] { - const f = normalizePath(fileName); - if (f in lookup) { - return [true as true, lookup[f]]; - } - else { - return [false as false, undefined]; - } - } } export function createDependencyMapper() { @@ -402,13 +396,13 @@ namespace ts { } } - export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, options: BuildOptions) { + export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) { if (!host.getModifiedTime || !host.setModifiedTime) { throw new Error("Host must support timestamp APIs"); } const configFileCache = createConfigFileCache(host); - let context = createBuildContext(options, reportDiagnostic); + let context = createBuildContext(defaultOptions, reportDiagnostic); return { getUpToDateStatus, @@ -418,8 +412,8 @@ namespace ts { resetBuildContext }; - function resetBuildContext() { - context = createBuildContext(options, reportDiagnostic); + function resetBuildContext(opts = defaultOptions) { + context = createBuildContext(opts, reportDiagnostic); } function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { @@ -798,7 +792,7 @@ namespace ts { } if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); } else { if (!host.deleteFile) { @@ -852,7 +846,7 @@ namespace ts { const projName = proj.options.configFilePath; if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { // Up to date, skip - if (options.dry) { + if (defaultOptions.dry) { // In a dry build, inform the user of this fact reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date, projName)); } @@ -889,6 +883,9 @@ namespace ts { } } + /** + * Report the build ordering inferred from the current project graph if we're in verbose mode + */ function reportBuildQueue(graph: DependencyGraph) { if (!context.options.verbose) return; @@ -902,6 +899,9 @@ namespace ts { context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join("")); } + /** + * Report the up-to-date status of a project if we're in verbose mode + */ function reportProjectStatus(configFileName: string, status: UpToDateStatus) { if (!context.options.verbose) return; switch (status.type) { @@ -931,9 +931,12 @@ namespace ts { case UpToDateStatusType.UpstreamBlocked: context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); return; - + case UpToDateStatusType.Unbuildable: + // TODO different error + context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); + return; default: - throw new Error(`Invalid build status - ${UpToDateStatusType[status.type]}`); + assertTypeIsNever(status); } } } diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index f1f38bd2f9f..4d2e5febdef 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -206,6 +206,48 @@ namespace ts { }); }); + describe("tsbuild - graph-ordering", () => { + it("orders the graph correctly", () => { + const fs = new vfs.FileSystem(false); + const host = new fakes.CompilerHost(fs); + const deps: [string, string][] = [ + ["A", "B"], + ["B", "C"], + ["A", "C"], + ["B", "D"], + ["C", "D"], + ["C", "E"], + ["F", "E"] + ]; + + writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildProjects(["/project/A", "/project/G"]); + printDiagnostics(); + }); + + function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] { + const projFileNames: string[] = []; + for (const dep of deps) { + if (projectNames.indexOf(dep[0]) < 0) throw new Error(`Invalid dependency - project ${dep[0]} does not exist`); + if (projectNames.indexOf(dep[1]) < 0) throw new Error(`Invalid dependency - project ${dep[1]} does not exist`); + } + for (const proj of projectNames) { + fileSystem.mkdirpSync(`/project/${proj}`); + fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}"); + const configFileName = `/project/${proj}/tsconfig.json`; + const configContent = JSON.stringify({ + compilerOptions: { composite: true }, + files: [`./${proj}.ts`], + references: deps.filter(d => d[0] === proj).map(d => ({ path: `../${d[1]}` })) + }, undefined, 2); + fileSystem.writeFileSync(configFileName, configContent); + projFileNames.push(configFileName); + } + return projFileNames; + } + }); + function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { if (!fs.statSync(path).isFile()) { throw new Error(`File ${path} does not exist`); From a7fcbcd3a446e77d52d899dd1f451924d9d62146 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 25 May 2018 16:06:33 -0700 Subject: [PATCH 22/48] Use better toposorting algorithm --- src/compiler/tsbuild.ts | 191 +++++++++++++++---------------- src/harness/unittests/tsbuild.ts | 58 +++++++--- 2 files changed, 132 insertions(+), 117 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 4bd6c13914d..8e50800fff6 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -4,7 +4,7 @@ namespace ts { * specified like "./blah" to an absolute path to an actual * tsconfig file, e.g. "/root/blah/tsconfig.json" */ - type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never }; + export type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never }; const minimumDate = new Date(-8640000000000000); const maximumDate = new Date(8640000000000000); @@ -42,7 +42,7 @@ namespace ts { type Mapper = ReturnType; interface DependencyGraph { - buildQueue: ResolvedConfigFileName[][]; + buildQueue: ResolvedConfigFileName[]; dependencyMap: Mapper; } @@ -409,7 +409,8 @@ namespace ts { getUpToDateStatusOfFile, buildProjects, cleanProjects, - resetBuildContext + resetBuildContext, + getBuildGraph }; function resetBuildContext(opts = defaultOptions) { @@ -420,6 +421,13 @@ namespace ts { return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); } + function getBuildGraph(configFileNames: string[]) { + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return; + + return createDependencyGraph(resolvedNames); + } + function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { if (project === undefined) { return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; @@ -583,66 +591,62 @@ namespace ts { }; } - // TODO: Use the better algorithm - function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph { - // This is a list of list of projects that need to be built. - // The ordering here is "backwards", i.e. the first entry in the array is the last set of projects that need to be built; - // and the last entry is the first set of projects to be built. - // Each subarray is effectively unordered. - // We traverse the reference graph from each root, then "clean" the list by removing - // any entry that is duplicated to its right. - const buildQueue: ResolvedConfigFileName[][] = []; - const dependencyMap = createDependencyMapper(); - let buildQueuePosition = 0; + function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph | undefined { + const temporaryMarks: { [path: string]: true } = {}; + const permanentMarks: { [path: string]: true } = {}; + const circularityReportStack: string[] = []; + const buildOrder: ResolvedConfigFileName[] = []; + const graph = createDependencyMapper(); + + let hadError = false; + for (const root of roots) { - const config = configFileCache.parseConfigFile(root); - if (config === undefined) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, root)); - continue; - } - enumerateReferences(normalizePath(root) as ResolvedConfigFileName, config); + visit(root); + } + + if (hadError) { + return undefined; } - removeDuplicatesFromBuildQueue(buildQueue); return { - buildQueue, - dependencyMap + buildQueue: buildOrder, + dependencyMap: graph }; - function enumerateReferences(fileName: ResolvedConfigFileName, root: ParsedCommandLine): void { - const myBuildLevel = buildQueue[buildQueuePosition] = buildQueue[buildQueuePosition] || []; - if (myBuildLevel.indexOf(fileName) < 0) { - myBuildLevel.push(fileName); - } - - const refs = root.projectReferences; - if (refs === undefined) return; - buildQueuePosition++; - for (const ref of refs) { - const actualPath = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName; - dependencyMap.addReference(fileName, actualPath); - const resolvedRef = configFileCache.parseConfigFile(actualPath); - if (resolvedRef === undefined) continue; - enumerateReferences(normalizePath(actualPath) as ResolvedConfigFileName, resolvedRef); - } - buildQueuePosition--; - } - - /** - * Removes entries from arrays which appear in later arrays. - */ - function removeDuplicatesFromBuildQueue(queue: string[][]): void { - // No need to check the last array - for (let i = 0; i < queue.length - 1; i++) { - queue[i] = queue[i].filter(fn => !occursAfter(fn, i + 1)); - } - - function occursAfter(s: string, start: number) { - for (let i = start; i < queue.length; i++) { - if (queue[i].indexOf(s) >= 0) return true; + function visit(projPath: ResolvedConfigFileName, inCircularContext = false) { + // Already visited + if (permanentMarks[projPath]) return; + // Circular + if (temporaryMarks[projPath]) { + if (!inCircularContext) { + hadError = true; + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n"))); + return; } - return false; } + + temporaryMarks[projPath] = true; + circularityReportStack.push(projPath); + const parsed = configFileCache.parseConfigFile(projPath); + if (parsed === undefined) { + hadError = true; + return; + } + if (parsed.projectReferences) { + for (const ref of parsed.projectReferences) { + const resolvedRefPath = resolveProjectName(ref.path); + if (resolvedRefPath === undefined) { + hadError = true; + break; + } + visit(resolvedRefPath, inCircularContext || ref.circular); + graph.addReference(projPath, resolvedRefPath); + } + } + + circularityReportStack.pop(); + permanentMarks[projPath] = true; + buildOrder.push(projPath); } } @@ -762,20 +766,19 @@ namespace ts { // Get the same graph for cleaning we'd use for building const graph = createDependencyGraph(resolvedNames); + if (graph === undefined) return undefined; const filesToDelete: string[] = []; - for (const level of graph.buildQueue) { - for (const proj of level) { - const parsed = configFileCache.parseConfigFile(proj); - if (parsed === undefined) { - // File has gone missing; fine to ignore here - continue; - } - const outputs = getAllProjectOutputs(parsed); - for (const output of outputs) { - if (host.fileExists(output)) { - filesToDelete.push(output); - } + for (const proj of graph.buildQueue) { + const parsed = configFileCache.parseConfigFile(proj); + if (parsed === undefined) { + // File has gone missing; fine to ignore here + continue; + } + const outputs = getAllProjectOutputs(parsed); + for (const output of outputs) { + if (host.fileExists(output)) { + filesToDelete.push(output); } } } @@ -805,21 +808,27 @@ namespace ts { } } + function resolveProjectName(name: string): ResolvedConfigFileName | undefined { + let fullPath = resolvePath(host.getCurrentDirectory(), name); + if (host.fileExists(fullPath)) { + return fullPath as ResolvedConfigFileName; + } + fullPath = combinePaths(fullPath, "tsconfig.json"); + if (host.fileExists(fullPath)) { + return fullPath as ResolvedConfigFileName; + } + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); + return undefined; + } + function resolveProjectNames(configFileNames: string[]): ResolvedConfigFileName[] | undefined { const resolvedNames: ResolvedConfigFileName[] = []; for (const name of configFileNames) { - let fullPath = resolvePath(host.getCurrentDirectory(), name); - if (host.fileExists(fullPath)) { - resolvedNames.push(fullPath as ResolvedConfigFileName); - continue; + const resolved = resolveProjectName(name); + if (resolved === undefined) { + return undefined; } - fullPath = combinePaths(fullPath, "tsconfig.json"); - if (host.fileExists(fullPath)) { - resolvedNames.push(fullPath as ResolvedConfigFileName); - continue; - } - reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); - return undefined; + resolvedNames.push(resolved); } return resolvedNames; } @@ -830,12 +839,12 @@ namespace ts { // Establish what needs to be built const graph = createDependencyGraph(resolvedNames); + if (graph === undefined) return; const queue = graph.buildQueue; reportBuildQueue(graph); - let next: ResolvedConfigFileName | undefined; - while (next = getNext()) { + for (const next of queue) { const proj = configFileCache.parseConfigFile(next); if (proj === undefined) { break; @@ -866,21 +875,6 @@ namespace ts { buildSingleProject(next); } - - function getNext(): ResolvedConfigFileName | undefined { - if (queue.length === 0) { - return undefined; - } - while (queue.length > 0) { - const last = queue[queue.length - 1]; - if (last.length === 0) { - queue.pop(); - continue; - } - return last.pop()!; - } - return undefined; - } } /** @@ -890,12 +884,9 @@ namespace ts { if (!context.options.verbose) return; const names: string[] = []; - for (const level of graph.buildQueue) { - for (const el of level) { - names.push(el); - } + for (const name of graph.buildQueue) { + names.push(name); } - names.reverse(); context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join("")); } diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 4d2e5febdef..08818d6ac3f 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -207,25 +207,49 @@ namespace ts { }); describe("tsbuild - graph-ordering", () => { - it("orders the graph correctly", () => { - const fs = new vfs.FileSystem(false); - const host = new fakes.CompilerHost(fs); - const deps: [string, string][] = [ - ["A", "B"], - ["B", "C"], - ["A", "C"], - ["B", "D"], - ["C", "D"], - ["C", "E"], - ["F", "E"] - ]; + const fs = new vfs.FileSystem(false); + const host = new fakes.CompilerHost(fs); + const deps: [string, string][] = [ + ["A", "B"], + ["B", "C"], + ["A", "C"], + ["B", "D"], + ["C", "D"], + ["C", "E"], + ["F", "E"] + ]; - writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); - builder.buildProjects(["/project/A", "/project/G"]); - printDiagnostics(); + writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); + + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + + it("orders the graph correctly - specify two roots", () => { + checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]); }); + it("orders the graph correctly - multiple parts of the same graph in various orders", () => { + // TODO add cases here + }); + + function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { + const projFileNames = rootNames.map(getProjectFileName); + const graph = builder.getBuildGraph(projFileNames); + if (graph === undefined) throw new Error("Graph shouldn't be undefined"); + + assert.sameMembers(graph.buildQueue, expectedBuildSet.map(getProjectFileName)); + + for (const dep of deps) { + const child = getProjectFileName(dep[0]); + if (graph.buildQueue.indexOf(child) < 0) continue; + const parent = getProjectFileName(dep[1]); + assert.isAbove(graph.buildQueue.indexOf(child), graph.buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`); + } + } + + function getProjectFileName(proj: string) { + return `/project/${proj}/tsconfig.json` as ResolvedConfigFileName; + } + function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] { const projFileNames: string[] = []; for (const dep of deps) { @@ -235,7 +259,7 @@ namespace ts { for (const proj of projectNames) { fileSystem.mkdirpSync(`/project/${proj}`); fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}"); - const configFileName = `/project/${proj}/tsconfig.json`; + const configFileName = getProjectFileName(proj); const configContent = JSON.stringify({ compilerOptions: { composite: true }, files: [`./${proj}.ts`], From 70fa29b627af7192ac0a78b73e4ae242f8169a72 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 29 May 2018 10:00:54 -0700 Subject: [PATCH 23/48] Add graph ordering tests --- src/compiler/tsbuild.ts | 1 - src/harness/unittests/tsbuild.ts | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 8e50800fff6..978aa102c8a 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -650,7 +650,6 @@ namespace ts { } } - // TODO Accept parsedCommandLine instead? function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 08818d6ac3f..ed1fd967952 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -228,7 +228,15 @@ namespace ts { }); it("orders the graph correctly - multiple parts of the same graph in various orders", () => { - // TODO add cases here + checkGraphOrdering(["A"], ["A", "B", "C", "D", "E"]); + checkGraphOrdering(["A", "C", "D"], ["A", "B", "C", "D", "E"]); + checkGraphOrdering(["D", "C", "A"], ["A", "B", "C", "D", "E"]); + }); + + it("orders the graph correctly - other orderings", () => { + checkGraphOrdering(["F"], ["F", "E"]); + checkGraphOrdering(["E"], ["E"]); + checkGraphOrdering(["F", "C", "A"], ["A", "B", "C", "D", "E", "F"]); }); function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { From 129f747ccc478881f67a89d677e7d8661bc48db5 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 29 May 2018 14:28:25 -0700 Subject: [PATCH 24/48] VFS fixes --- src/harness/vfs.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index be8b6c3fed7..ba5905b8ea6 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -414,12 +414,16 @@ namespace vfs { * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ public utimesSync(path: string, atime: Date, mtime: Date) { + if (this.isReadonly) throw createIOError("EROFS"); + if (!isFinite(+atime) || !isFinite(+mtime)) throw createIOError("EINVAL"); + const entry = this._walk(this._resolve(path)); if (!entry || !entry.node) { throw createIOError("ENOENT"); } entry.node.atimeMs = +atime; entry.node.mtimeMs = +mtime; + entry.node.ctimeMs = this.time(); } /** From f8c4301f14dad4ee4dd910231a67087a81212e33 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 30 May 2018 10:00:35 -0700 Subject: [PATCH 25/48] Add more errors; commandline help for --build; invalid flag combo detection --- src/compiler/commandLineParser.ts | 127 +++++++++++++++++++++++++ src/compiler/diagnosticMessages.json | 28 ++++++ src/compiler/tsbuild.ts | 64 +++++++++++++ src/compiler/tsc.ts | 137 +++------------------------ src/compiler/types.ts | 3 + 5 files changed, 236 insertions(+), 123 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 2f205f1e44e..fb0fcab4654 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -49,6 +49,14 @@ namespace ts { paramType: Diagnostics.FILE_OR_DIRECTORY, description: Diagnostics.Compile_the_project_given_the_path_to_its_configuration_file_or_to_a_folder_with_a_tsconfig_json, }, + { + name: "build", + type: "boolean", + shortName: "b", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Build_one_or_more_projects_and_their_dependencies_if_out_of_date + }, { name: "pretty", type: "boolean", @@ -943,6 +951,125 @@ namespace ts { } + function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { + const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); + return diagnostic.messageText; + } + + /* @internal */ + export function printVersion() { + sys.write(getDiagnosticText(Diagnostics.Version_0, version) + sys.newLine); + } + + /* @internal */ + export function printHelp(optionsList: CommandLineOption[], syntaxPrefix = "") { + const output: string[] = []; + + // We want to align our "syntax" and "examples" commands to a certain margin. + const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length; + const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length; + let marginLength = Math.max(syntaxLength, examplesLength); + + // Build up the syntactic skeleton. + let syntax = makePadding(marginLength - syntaxLength); + syntax += `tsc ${syntaxPrefix}[${getDiagnosticText(Diagnostics.options)}] [${getDiagnosticText(Diagnostics.file)}...]`; + + output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax)); + output.push(sys.newLine + sys.newLine); + + // Build up the list of examples. + const padding = makePadding(marginLength); + output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine); + output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine); + output.push(padding + "tsc @args.txt" + sys.newLine); + output.push(padding + "tsc --build tsconfig.json" + sys.newLine); + output.push(sys.newLine); + + output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine); + + // We want our descriptions to align at the same column in our output, + // so we keep track of the longest option usage string. + marginLength = 0; + const usageColumn: string[] = []; // Things like "-d, --declaration" go in here. + const descriptionColumn: string[] = []; + + const optionsDescriptionMap = createMap(); // Map between option.description and list of option.type if it is a kind + + for (const option of optionsList) { + // If an option lacks a description, + // it is not officially supported. + if (!option.description) { + continue; + } + + let usageText = " "; + if (option.shortName) { + usageText += "-" + option.shortName; + usageText += getParamType(option); + usageText += ", "; + } + + usageText += "--" + option.name; + usageText += getParamType(option); + + usageColumn.push(usageText); + let description: string; + + if (option.name === "lib") { + description = getDiagnosticText(option.description); + const element = (option).element; + const typeMap = >element.type; + optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`)); + } + else { + description = getDiagnosticText(option.description); + } + + descriptionColumn.push(description); + + // Set the new margin for the description column if necessary. + marginLength = Math.max(usageText.length, marginLength); + } + + // Special case that can't fit in the loop. + const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">"; + usageColumn.push(usageText); + descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file)); + marginLength = Math.max(usageText.length, marginLength); + + // Print out each row, aligning all the descriptions on the same column. + for (let i = 0; i < usageColumn.length; i++) { + const usage = usageColumn[i]; + const description = descriptionColumn[i]; + const kindsList = optionsDescriptionMap.get(description); + output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine); + + if (kindsList) { + output.push(makePadding(marginLength + 4)); + for (const kind of kindsList) { + output.push(kind + " "); + } + output.push(sys.newLine); + } + } + + for (const line of output) { + sys.write(line); + } + return; + + function getParamType(option: CommandLineOption) { + if (option.paramType !== undefined) { + return " " + getDiagnosticText(option.paramType); + } + return ""; + } + + function makePadding(paddingLength: number): string { + return Array(paddingLength + 1).join(" "); + } + } + export type DiagnosticReporter = (diagnostic: Diagnostic) => void; /** * Reports config file diagnostics diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index d5d8c459ee0..f76b0a39fbd 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3657,6 +3657,34 @@ "category": "Message", "code": 6362 }, + "Build one or more projects and their dependencies, if out-of-date": { + "category": "Message", + "code": 6363 + }, + "Delete the outputs of all projects": { + "category": "Message", + "code": 6364 + }, + "Enable verbose logging": { + "category": "Message", + "code": 6365 + }, + "Show what would be built (or deleted, if specified with --clean)": { + "category": "Message", + "code": 6366 + }, + "Build all projects, including those that appear to be up-to-date": { + "category": "Message", + "code": 6367 + }, + "Option '--build' must be the first command line argument.": { + "category": "Error", + "code": 6368 + }, + "Options '{0}' and '{1}' cannot be combined.": { + "category": "Error", + "code": 6369 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 978aa102c8a..665125b4dcc 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -338,11 +338,48 @@ namespace ts { }; } + const buildOpts: CommandLineOption[] = [ + { + name: "verbose", + shortName: "v", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Enable_verbose_logging, + type: "boolean" + }, + { + name: "dry", + shortName: "d", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, + type: "boolean" + }, + { + name: "force", + shortName: "f", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, + type: "boolean" + }, + { + name: "clean", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Delete_the_outputs_of_all_projects, + type: "boolean" + }, + { + name: "watch", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Watch_input_files, + type: "boolean" + } + ]; + export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[]) { let verbose = false; let dry = false; let force = false; let clean = false; + let watch = false; const projects: string[] = []; for (const arg of args) { @@ -362,11 +399,38 @@ namespace ts { case "--clean": clean = true; continue; + case "--watch": + case "-w": + watch = true; + continue; + + case "--?": + case "-?": + case "--help": + return printHelp(buildOpts, "--build "); } // Not a flag, parse as filename addProject(arg); } + // Nonsensical combinations + if (clean && force) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); + return; + } + if (clean && verbose) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); + return; + } + if (clean && watch) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); + return; + } + if (watch && dry) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); + return; + } + if (projects.length === 0) { // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." addProject("."); diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 62abd834180..9800d2e768a 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -12,11 +12,6 @@ namespace ts { return count; } - function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { - const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); - return diagnostic.messageText; - } - let reportDiagnostic = createDiagnosticReporter(sys); function updateReportDiagnostic(options: CompilerOptions) { if (shouldBePretty(options)) { @@ -46,6 +41,13 @@ namespace ts { return s; } + function getOptionsForHelp(commandLine: ParsedCommandLine) { + // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") + return !!commandLine.options.all ? + sort(optionDeclarations, (a, b) => compareStringsCaseInsensitive(a.name, b.name)) : + filter(optionDeclarations.slice(), v => !!v.showInSimplifiedHelpView); + } + export function executeCommandLine(args: string[]): void { if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) { return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1)); @@ -53,6 +55,11 @@ namespace ts { const commandLine = parseCommandLine(args); + if (commandLine.options.build) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_build_must_be_the_first_command_line_argument)); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + // Configuration file name (if any) let configFileName: string | undefined; if (commandLine.options.locale) { @@ -78,7 +85,7 @@ namespace ts { if (commandLine.options.help || commandLine.options.all) { printVersion(); - printHelp(!!commandLine.options.all); + printHelp(getOptionsForHelp(commandLine)); return sys.exit(ExitStatus.Success); } @@ -111,7 +118,7 @@ namespace ts { if (commandLine.fileNames.length === 0 && !configFileName) { printVersion(); - printHelp(!!commandLine.options.all); + printHelp(getOptionsForHelp(commandLine)); return sys.exit(ExitStatus.Success); } @@ -275,122 +282,6 @@ namespace ts { } } - function printVersion() { - sys.write(getDiagnosticText(Diagnostics.Version_0, version) + sys.newLine); - } - - function printHelp(showAllOptions: boolean) { - const output: string[] = []; - - // We want to align our "syntax" and "examples" commands to a certain margin. - const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length; - const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length; - let marginLength = Math.max(syntaxLength, examplesLength); - - // Build up the syntactic skeleton. - let syntax = makePadding(marginLength - syntaxLength); - syntax += "tsc [" + getDiagnosticText(Diagnostics.options) + "] [" + getDiagnosticText(Diagnostics.file) + " ...]"; - - output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax)); - output.push(sys.newLine + sys.newLine); - - // Build up the list of examples. - const padding = makePadding(marginLength); - output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine); - output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine); - output.push(padding + "tsc @args.txt" + sys.newLine); - output.push(sys.newLine); - - output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine); - - // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") - const optsList = showAllOptions ? - sort(optionDeclarations, (a, b) => compareStringsCaseInsensitive(a.name, b.name)) : - filter(optionDeclarations.slice(), v => !!v.showInSimplifiedHelpView); - - // We want our descriptions to align at the same column in our output, - // so we keep track of the longest option usage string. - marginLength = 0; - const usageColumn: string[] = []; // Things like "-d, --declaration" go in here. - const descriptionColumn: string[] = []; - - const optionsDescriptionMap = createMap(); // Map between option.description and list of option.type if it is a kind - - for (const option of optsList) { - // If an option lacks a description, - // it is not officially supported. - if (!option.description) { - continue; - } - - let usageText = " "; - if (option.shortName) { - usageText += "-" + option.shortName; - usageText += getParamType(option); - usageText += ", "; - } - - usageText += "--" + option.name; - usageText += getParamType(option); - - usageColumn.push(usageText); - let description: string; - - if (option.name === "lib") { - description = getDiagnosticText(option.description); - const element = (option).element; - const typeMap = >element.type; - optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`)); - } - else { - description = getDiagnosticText(option.description); - } - - descriptionColumn.push(description); - - // Set the new margin for the description column if necessary. - marginLength = Math.max(usageText.length, marginLength); - } - - // Special case that can't fit in the loop. - const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">"; - usageColumn.push(usageText); - descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file)); - marginLength = Math.max(usageText.length, marginLength); - - // Print out each row, aligning all the descriptions on the same column. - for (let i = 0; i < usageColumn.length; i++) { - const usage = usageColumn[i]; - const description = descriptionColumn[i]; - const kindsList = optionsDescriptionMap.get(description); - output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine); - - if (kindsList) { - output.push(makePadding(marginLength + 4)); - for (const kind of kindsList) { - output.push(kind + " "); - } - output.push(sys.newLine); - } - } - - for (const line of output) { - sys.write(line); - } - return; - - function getParamType(option: CommandLineOption) { - if (option.paramType !== undefined) { - return " " + getDiagnosticText(option.paramType); - } - return ""; - } - - function makePadding(paddingLength: number): string { - return Array(paddingLength + 1).join(" "); - } - } - function writeConfigFile(options: CompilerOptions, fileNames: string[]) { const currentDirectory = sys.getCurrentDirectory(); const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json")); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index fff1224ba96..be57e9c6443 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4281,6 +4281,9 @@ namespace ts { allowUnusedLabels?: boolean; alwaysStrict?: boolean; // Always combine with strict property baseUrl?: string; + /** An error if set - this should only go through the -b pipeline and not actually be observed */ + /*@internal*/ + build?: boolean; charset?: string; checkJs?: boolean; /* @internal */ configFilePath?: string; From cd7a844a48c8d2bd31e5eb1df49cd0dce8a5a8d2 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 30 May 2018 10:12:51 -0700 Subject: [PATCH 26/48] We definitely have a type system --- src/compiler/tsbuild.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 665125b4dcc..2a509a8c746 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -286,7 +286,8 @@ namespace ts { const outputs: string[] = []; outputs.push(project.options.outFile); if (project.options.declaration) { - const dts = outputs.push(changeExtension(project.options.outFile, ".d.ts")); + const dts = changeExtension(project.options.outFile, ".d.ts"); + outputs.push(dts); if (project.options.declarationMap) { outputs.push(dts + ".map"); } From 8adbf85e0e80a9044e325a06e2ba7b2c8a7d6562 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 30 May 2018 17:53:26 -0700 Subject: [PATCH 27/48] Candidate sectional sourcemap emit implementation --- src/compiler/factory.ts | 7 +- src/compiler/program.ts | 4 +- src/compiler/sourcemap.ts | 115 ++++++++++++++++-- src/compiler/transformers/declarations.ts | 2 +- src/compiler/transformers/ts.ts | 2 +- src/compiler/types.ts | 3 + src/compiler/utilities.ts | 4 + .../reference/api/tsserverlibrary.d.ts | 8 +- tests/baselines/reference/api/typescript.d.ts | 8 +- 9 files changed, 133 insertions(+), 20 deletions(-) diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 6085cc550c4..111be243769 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -2585,16 +2585,19 @@ namespace ts { return node; } - export function createUnparsedSourceFile(text: string): UnparsedSource { + export function createUnparsedSourceFile(text: string, map?: string): UnparsedSource { const node = createNode(SyntaxKind.UnparsedSource); node.text = text; + node.sourceMapText = map; return node; } - export function createInputFiles(javascript: string, declaration: string): InputFiles { + export function createInputFiles(javascript: string, declaration: string, javascriptMapText?: string, declarationMapText?: string): InputFiles { const node = createNode(SyntaxKind.InputFiles); node.javascriptText = javascript; + node.javascriptMapText = javascriptMapText; node.declarationText = declaration; + node.declarationMapText = declarationMapText; return node; } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 4dfb9039ccf..2c8a702e139 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1192,8 +1192,10 @@ namespace ts { const dtsFilename = changeExtension(resolvedRefOpts.options.outFile, ".d.ts"); const js = host.readFile(resolvedRefOpts.options.outFile) || `/* Input file ${resolvedRefOpts.options.outFile} was missing */\r\n`; + const jsMap = host.readFile(resolvedRefOpts.options.outFile + ".map"); // TODO: try to read sourceMappingUrl comment from the js file const dts = host.readFile(dtsFilename) || `/* Input file ${dtsFilename} was missing */\r\n`; - const node = createInputFiles(js, dts); + const dtsMap = host.readFile(dtsFilename + ".map"); + const node = createInputFiles(js, dts, jsMap, dtsMap); nodes.push(node); } } diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index a274404f320..1f8723b9c80 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -99,6 +99,10 @@ namespace ts { let sourceMapDataList: SourceMapData[] | undefined; let disabled: boolean = !(compilerOptions.sourceMap || compilerOptions.inlineSourceMap); + let completedSections: SourceMapSectionDefinition[]; + let sectionStartLine: number; + let sectionStartColumn: number; + return { initialize, reset, @@ -146,6 +150,9 @@ namespace ts { lastEncodedNameIndex = 0; // Initialize source map data + completedSections = []; + sectionStartLine = 0; + sectionStartColumn = 0; sourceMapData = { sourceMapFilePath, jsSourceMappingURL: !compilerOptions.inlineSourceMap ? getBaseFileName(normalizeSlashes(sourceMapFilePath)) : undefined!, // TODO: GH#18217 @@ -214,6 +221,68 @@ namespace ts { lastEncodedNameIndex = undefined; sourceMapData = undefined!; sourceMapDataList = undefined!; + completedSections = undefined!; + sectionStartLine = undefined!; + sectionStartColumn = undefined!; + } + + interface SourceMapSection { + version: 3; + file: string; + sourceRoot?: string; + sources: string[]; + names?: string[]; + mappings: string; + sourcesContent?: string[]; + sections?: undefined; + } + + type SourceMapSectionDefinition = + | { offset: { line: number, column: number }, url: string } // Included for completeness + | { offset: { line: number, column: number }, map: SourceMap }; + + interface SectionalSourceMap { + version: 3; + file: string; + sections: SourceMapSectionDefinition[]; + } + + type SourceMap = SectionalSourceMap | SourceMapSection; + + function captureSection(): SourceMapSection { + return { + version: 3, + file: sourceMapData.sourceMapFile, + sourceRoot: sourceMapData.sourceMapSourceRoot, + sources: sourceMapData.sourceMapSources, + names: sourceMapData.sourceMapNames, + mappings: sourceMapData.sourceMapMappings, + sourcesContent: sourceMapData.sourceMapSourcesContent, + }; + } + + function resetSectionalData(): void { + sourceMapData.sourceMapSources = []; + sourceMapData.sourceMapNames = []; + sourceMapData.sourceMapMappings = ""; + sourceMapData.sourceMapSourcesContent = compilerOptions.inlineSources ? [] : undefined; + } + + function generateMap(): SourceMap { + if (completedSections.length) { + const last = { + offset: { line: sectionStartLine, column: sectionStartColumn }, + map: captureSection() + }; + return { + version: 3, + file: last.map.file, + sections: [...completedSections, last] + }; + } + else { + return captureSection(); + } } // Encoding for sourcemap span @@ -284,8 +353,8 @@ namespace ts { sourceLinePos.line++; sourceLinePos.character++; - const emittedLine = writer.getLine(); - const emittedColumn = writer.getColumn(); + const emittedLine = writer.getLine() - sectionStartLine; + const emittedColumn = emittedLine === 0 ? writer.getColumn() - sectionStartColumn : writer.getColumn(); // If this location wasn't recorded or the location in source is going backwards, record the span if (!lastRecordedSourceMapSpan || @@ -333,6 +402,38 @@ namespace ts { } if (node) { + if (isUnparsedSource(node) && node.sourceMapText !== undefined) { + if (lastRecordedSourceMapSpan && lastRecordedSourceMapSpan === lastEncodedSourceMapSpan) { // If we've recorded some spans, save them + completedSections.push({ offset: { line: sectionStartLine, column: sectionStartColumn }, map: captureSection() }); + resetSectionalData(); + } + const text = node.sourceMapText; + let parsed: {} | undefined; + try { + parsed = JSON.parse(text); + } + catch { + // empty + } + const offset = { line: writer.getLine(), column: writer.getColumn() }; + completedSections.push(parsed + ? { + offset, + map: parsed as SourceMap + } + : { + offset, + // This is just passes the buck on sourcemaps we don't really understand, instead of issuing an error (which would be difficult this late) + url: `data:application/json;charset=utf-8;base64,${base64encode(sys, text)}` + } + ); + const emitResult = emitCallback(hint, node); + sectionStartLine = writer.getLine(); + sectionStartColumn = writer.getColumn(); + lastRecordedSourceMapSpan = undefined!; + lastEncodedSourceMapSpan = undefined!; + return emitResult; + } const emitNode = node.emitNode; const emitFlags = emitNode && emitNode.flags || EmitFlags.None; const range = emitNode && emitNode.sourceMapRange; @@ -452,15 +553,7 @@ namespace ts { encodeLastRecordedSourceMapSpan(); - return JSON.stringify({ - version: 3, - file: sourceMapData.sourceMapFile, - sourceRoot: sourceMapData.sourceMapSourceRoot, - sources: sourceMapData.sourceMapSources, - names: sourceMapData.sourceMapNames, - mappings: sourceMapData.sourceMapMappings, - sourcesContent: sourceMapData.sourceMapSourcesContent, - }); + return JSON.stringify(generateMap()); } /** diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index a92448e5290..3469ef125a2 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -180,7 +180,7 @@ namespace ts { } ), mapDefined(node.prepends, prepend => { if (prepend.kind === SyntaxKind.InputFiles) { - return createUnparsedSourceFile(prepend.declarationText); + return createUnparsedSourceFile(prepend.declarationText, prepend.declarationMapText); } })); bundle.syntheticFileReferences = []; diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index cc244d92204..03bafc314c7 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -100,7 +100,7 @@ namespace ts { function transformBundle(node: Bundle) { return createBundle(node.sourceFiles.map(transformSourceFile), mapDefined(node.prepends, prepend => { if (prepend.kind === SyntaxKind.InputFiles) { - return createUnparsedSourceFile(prepend.javascriptText); + return createUnparsedSourceFile(prepend.javascriptText, prepend.javascriptMapText); } return prepend; })); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b13f4aea613..5275cdb7dfd 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2652,12 +2652,15 @@ namespace ts { export interface InputFiles extends Node { kind: SyntaxKind.InputFiles; javascriptText: string; + javascriptMapText?: string; declarationText: string; + declarationMapText?: string; } export interface UnparsedSource extends Node { kind: SyntaxKind.UnparsedSource; text: string; + sourceMapText?: string; } export interface JsonSourceFile extends SourceFile { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 426c0d8d1da..48289a43836 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -5454,6 +5454,10 @@ namespace ts { return node.kind === SyntaxKind.Bundle; } + export function isUnparsedSource(node: Node): node is UnparsedSource { + return node.kind === SyntaxKind.UnparsedSource; + } + // JSDoc export function isJSDocTypeExpression(node: Node): node is JSDocTypeExpression { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index a6657b4d341..aa40d060bda 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1669,11 +1669,14 @@ declare namespace ts { interface InputFiles extends Node { kind: SyntaxKind.InputFiles; javascriptText: string; + javascriptMapText?: string; declarationText: string; + declarationMapText?: string; } interface UnparsedSource extends Node { kind: SyntaxKind.UnparsedSource; text: string; + sourceMapText?: string; } interface JsonSourceFile extends SourceFile { statements: NodeArray; @@ -3371,6 +3374,7 @@ declare namespace ts { function isEnumMember(node: Node): node is EnumMember; function isSourceFile(node: Node): node is SourceFile; function isBundle(node: Node): node is Bundle; + function isUnparsedSource(node: Node): node is UnparsedSource; function isJSDocTypeExpression(node: Node): node is JSDocTypeExpression; function isJSDocAllType(node: JSDocAllType): node is JSDocAllType; function isJSDocUnknownType(node: Node): node is JSDocUnknownType; @@ -3826,8 +3830,8 @@ declare namespace ts { function createCommaList(elements: ReadonlyArray): CommaListExpression; function updateCommaList(node: CommaListExpression, elements: ReadonlyArray): CommaListExpression; function createBundle(sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; - function createUnparsedSourceFile(text: string): UnparsedSource; - function createInputFiles(javascript: string, declaration: string): InputFiles; + function createUnparsedSourceFile(text: string, map?: string): UnparsedSource; + function createInputFiles(javascript: string, declaration: string, javascriptMapText?: string, declarationMapText?: string): InputFiles; function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray): CallExpression; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray, param: ParameterDeclaration, paramValue: Expression): CallExpression; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d71bb5117ba..25f29c4f479 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1669,11 +1669,14 @@ declare namespace ts { interface InputFiles extends Node { kind: SyntaxKind.InputFiles; javascriptText: string; + javascriptMapText?: string; declarationText: string; + declarationMapText?: string; } interface UnparsedSource extends Node { kind: SyntaxKind.UnparsedSource; text: string; + sourceMapText?: string; } interface JsonSourceFile extends SourceFile { statements: NodeArray; @@ -3371,6 +3374,7 @@ declare namespace ts { function isEnumMember(node: Node): node is EnumMember; function isSourceFile(node: Node): node is SourceFile; function isBundle(node: Node): node is Bundle; + function isUnparsedSource(node: Node): node is UnparsedSource; function isJSDocTypeExpression(node: Node): node is JSDocTypeExpression; function isJSDocAllType(node: JSDocAllType): node is JSDocAllType; function isJSDocUnknownType(node: Node): node is JSDocUnknownType; @@ -3826,8 +3830,8 @@ declare namespace ts { function createCommaList(elements: ReadonlyArray): CommaListExpression; function updateCommaList(node: CommaListExpression, elements: ReadonlyArray): CommaListExpression; function createBundle(sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; - function createUnparsedSourceFile(text: string): UnparsedSource; - function createInputFiles(javascript: string, declaration: string): InputFiles; + function createUnparsedSourceFile(text: string, map?: string): UnparsedSource; + function createInputFiles(javascript: string, declaration: string, javascriptMapText?: string, declarationMapText?: string): InputFiles; function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray): CallExpression; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray, param: ParameterDeclaration, paramValue: Expression): CallExpression; From eb1973a4fd129eb7deb5aad5a4f376bba396c21b Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 30 May 2018 19:29:47 -0700 Subject: [PATCH 28/48] Fixup sourcemap positions and text writer calculations --- src/compiler/emitter.ts | 2 +- src/compiler/sourcemap.ts | 35 ++++++++++--------- src/compiler/utilities.ts | 28 +++++++++------ .../baselines/reference/tsxErrorRecovery1.js | 2 +- .../tsxStatelessFunctionComponents3.js | 2 +- 5 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 5e53605decf..570dfc99653 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1026,7 +1026,7 @@ namespace ts { // SyntaxKind.UnparsedSource function emitUnparsedSource(unparsed: UnparsedSource) { - write(unparsed.text); + writer.rawWrite(unparsed.text); } // diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index 1f8723b9c80..a93aedebf27 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -151,8 +151,8 @@ namespace ts { // Initialize source map data completedSections = []; - sectionStartLine = 0; - sectionStartColumn = 0; + sectionStartLine = 1; + sectionStartColumn = 1; sourceMapData = { sourceMapFilePath, jsSourceMappingURL: !compilerOptions.inlineSourceMap ? getBaseFileName(normalizeSlashes(sourceMapFilePath)) : undefined!, // TODO: GH#18217 @@ -270,14 +270,11 @@ namespace ts { function generateMap(): SourceMap { if (completedSections.length) { - const last = { - offset: { line: sectionStartLine, column: sectionStartColumn }, - map: captureSection() - }; + captureSectionalSpanIfNeeded(/*reset*/ false); return { version: 3, - file: last.map.file, - sections: [...completedSections, last] + file: sourceMapData.sourceMapFile, + sections: completedSections }; } else { @@ -353,8 +350,8 @@ namespace ts { sourceLinePos.line++; sourceLinePos.character++; - const emittedLine = writer.getLine() - sectionStartLine; - const emittedColumn = emittedLine === 0 ? writer.getColumn() - sectionStartColumn : writer.getColumn(); + const emittedLine = writer.getLine() - sectionStartLine + 1; + const emittedColumn = emittedLine === 0 ? (writer.getColumn() - sectionStartColumn + 1) : writer.getColumn(); // If this location wasn't recorded or the location in source is going backwards, record the span if (!lastRecordedSourceMapSpan || @@ -389,6 +386,15 @@ namespace ts { } } + function captureSectionalSpanIfNeeded(reset: boolean) { + if (lastRecordedSourceMapSpan && lastRecordedSourceMapSpan === lastEncodedSourceMapSpan) { // If we've recorded some spans, save them + completedSections.push({ offset: { line: sectionStartLine - 1, column: sectionStartColumn - 1 }, map: captureSection() }); + if (reset) { + resetSectionalData(); + } + } + } + /** * Emits a node with possible leading and trailing source maps. * @@ -403,10 +409,7 @@ namespace ts { if (node) { if (isUnparsedSource(node) && node.sourceMapText !== undefined) { - if (lastRecordedSourceMapSpan && lastRecordedSourceMapSpan === lastEncodedSourceMapSpan) { // If we've recorded some spans, save them - completedSections.push({ offset: { line: sectionStartLine, column: sectionStartColumn }, map: captureSection() }); - resetSectionalData(); - } + captureSectionalSpanIfNeeded(/*reset*/ true); const text = node.sourceMapText; let parsed: {} | undefined; try { @@ -415,7 +418,7 @@ namespace ts { catch { // empty } - const offset = { line: writer.getLine(), column: writer.getColumn() }; + const offset = { line: writer.getLine() - 1, column: writer.getColumn() - 1 }; completedSections.push(parsed ? { offset, @@ -431,7 +434,7 @@ namespace ts { sectionStartLine = writer.getLine(); sectionStartColumn = writer.getColumn(); lastRecordedSourceMapSpan = undefined!; - lastEncodedSourceMapSpan = undefined!; + lastEncodedSourceMapSpan = defaultLastEncodedSourceMapSpan; return emitResult; } const emitNode = node.emitNode; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 48289a43836..70fafe8fb66 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2825,13 +2825,26 @@ namespace ts { let lineCount: number; let linePos: number; + function updateLineCountAndPosFor(s: string) { + const lineStartsOfS = computeLineStarts(s); + if (lineStartsOfS.length > 1) { + lineCount = lineCount + lineStartsOfS.length - 1; + linePos = output.length - s.length + last(lineStartsOfS); + lineStart = (linePos - output.length) === 0; + } + else { + lineStart = false; + } + } + function write(s: string) { if (s && s.length) { if (lineStart) { - output += getIndentString(indent); + s = getIndentString(indent) + s; lineStart = false; } output += s; + updateLineCountAndPosFor(s); } } @@ -2845,21 +2858,14 @@ namespace ts { function rawWrite(s: string) { if (s !== undefined) { - if (lineStart) { - lineStart = false; - } output += s; + updateLineCountAndPosFor(s); } } function writeLiteral(s: string) { if (s && s.length) { write(s); - const lineStartsOfS = computeLineStarts(s); - if (lineStartsOfS.length > 1) { - lineCount = lineCount + lineStartsOfS.length - 1; - linePos = output.length - s.length + last(lineStartsOfS); - } } } @@ -2873,7 +2879,9 @@ namespace ts { } function writeTextOfNode(text: string, node: Node) { - write(getTextOfNodeFromSourceText(text, node)); + const s = getTextOfNodeFromSourceText(text, node); + write(s); + updateLineCountAndPosFor(s); } reset(); diff --git a/tests/baselines/reference/tsxErrorRecovery1.js b/tests/baselines/reference/tsxErrorRecovery1.js index 7abf1346c35..d91c464c6f9 100644 --- a/tests/baselines/reference/tsxErrorRecovery1.js +++ b/tests/baselines/reference/tsxErrorRecovery1.js @@ -14,5 +14,5 @@ function foo() { } // Shouldn't see any errors down here var y = {a} 1 }; -; + ; } diff --git a/tests/baselines/reference/tsxStatelessFunctionComponents3.js b/tests/baselines/reference/tsxStatelessFunctionComponents3.js index e7bfc980229..152c7e6ba4f 100644 --- a/tests/baselines/reference/tsxStatelessFunctionComponents3.js +++ b/tests/baselines/reference/tsxStatelessFunctionComponents3.js @@ -27,7 +27,7 @@ define(["require", "exports", "react"], function (require, exports, React) { // Should be OK var MainMenu = function (props) { return (

Main Menu

-
); }; + ); }; var App = function (_a) { var children = _a.children; return (
From d21c03ab9d9f280dc822875d8120824220da2c4d Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 31 May 2018 14:38:26 -0700 Subject: [PATCH 29/48] Invalidation + separated downstream builds --- src/compiler/diagnosticMessages.json | 4 + src/compiler/tsbuild.ts | 194 ++++++++++++++++++++++----- src/harness/unittests/tsbuild.ts | 96 ++++++++----- 3 files changed, 227 insertions(+), 67 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f76b0a39fbd..e9022bff0df 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3685,6 +3685,10 @@ "category": "Error", "code": 6369 }, + "Skipping clean because not all projects could be located": { + "category": "Error", + "code": 6340 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 2a509a8c746..1f36f09393b 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -38,6 +38,10 @@ namespace ts { * Issue a verbose diagnostic message. No-ops when options.verbose is false. */ verbose(diag: DiagnosticMessage, ...args: any[]): void; + + invalidatedProjects: FileMap; + queuedProjects: FileMap; + missingRoots: Map; } type Mapper = ReturnType; @@ -73,7 +77,7 @@ namespace ts { AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors } - enum UpToDateStatusType { + export enum UpToDateStatusType { Unbuildable, UpToDate, /** @@ -89,7 +93,7 @@ namespace ts { UpstreamBlocked } - type UpToDateStatus = + export type UpToDateStatus = | Status.Unbuildable | Status.UpToDate | Status.OutputMissing @@ -98,7 +102,7 @@ namespace ts { | Status.UpstreamOutOfDate | Status.UpstreamBlocked; - namespace Status { + export namespace Status { /** * 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 @@ -170,6 +174,9 @@ namespace ts { setValue(fileName: string, value: T): void; getValue(fileName: string): T | never; getValueOrUndefined(fileName: string): T | undefined; + hasKey(fileName: string): boolean; + removeKey(fileName: string): void; + getKeys(): string[]; } /** @@ -183,8 +190,23 @@ namespace ts { setValue, getValue, getValueOrUndefined, + removeKey, + getKeys, + hasKey }; + function getKeys(): string[] { + return Object.keys(lookup); + } + + function hasKey(fileName: string) { + return normalizePath(fileName) in lookup; + } + + function removeKey(fileName: string) { + delete lookup[fileName]; + } + function setValue(fileName: string, value: T) { lookup[normalizePath(fileName)] = value; } @@ -211,30 +233,30 @@ namespace ts { } export function createDependencyMapper() { - const childToParents: { [key: string]: string[] } = {}; - const parentToChildren: { [key: string]: string[] } = {}; - const allKeys: string[] = []; + const childToParents: { [key: string]: ResolvedConfigFileName[] } = {}; + const parentToChildren: { [key: string]: ResolvedConfigFileName[] } = {}; + const allKeys: ResolvedConfigFileName[] = []; - function addReference(childConfigFileName: string, parentConfigFileName: string): void { + function addReference(childConfigFileName: ResolvedConfigFileName, parentConfigFileName: ResolvedConfigFileName): void { addEntry(childToParents, childConfigFileName, parentConfigFileName); addEntry(parentToChildren, parentConfigFileName, childConfigFileName); } - function getReferencesTo(parentConfigFileName: string): string[] { + function getReferencesTo(parentConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] { return parentToChildren[normalizePath(parentConfigFileName)] || []; } - function getReferencesOf(childConfigFileName: string): string[] { + function getReferencesOf(childConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] { return childToParents[normalizePath(childConfigFileName)] || []; } - function getKeys(): ReadonlyArray { + function getKeys(): ReadonlyArray { return allKeys; } - function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: string, element: string) { - key = normalizePath(key); - element = normalizePath(element); + function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: ResolvedConfigFileName, element: ResolvedConfigFileName) { + key = normalizePath(key) as ResolvedConfigFileName; + element = normalizePath(element) as ResolvedConfigFileName; const arr = (mapToAddTo[key] = mapToAddTo[key] || []); if (arr.indexOf(element) < 0) { arr.push(element); @@ -316,8 +338,13 @@ namespace ts { return parsed; } + function removeKey(configFilePath: ResolvedConfigFileName) { + cache.removeKey(configFilePath); + } + return { - parseConfigFile + parseConfigFile, + removeKey }; } @@ -331,11 +358,19 @@ namespace ts { export function createBuildContext(options: BuildOptions, reportDiagnostic: DiagnosticReporter): BuildContext { const verboseDiag = options.verbose && reportDiagnostic; + + const invalidatedProjects = createFileMap(); + const queuedProjects = createFileMap(); + const missingRoots = createMap(); + return { options, projectStatus: createFileMap(), unchangedOutputs: createFileMap(), - verbose: verboseDiag ? (diag, ...args) => verboseDiag(createCompilerDiagnostic(diag, ...args)) : () => undefined + verbose: verboseDiag ? (diag, ...args) => verboseDiag(createCompilerDiagnostic(diag, ...args)) : () => undefined, + invalidatedProjects, + missingRoots, + queuedProjects }; } @@ -437,12 +472,12 @@ namespace ts { addProject("."); } - const builder = createSolutionBuilder(host, reportDiagnostic, { verbose, dry, force }); + const builder = createSolutionBuilder(host, projects, reportDiagnostic, { verbose, dry, force }); if (clean) { - builder.cleanProjects(projects); + builder.cleanAllProjects(); } else { - builder.buildProjects(projects); + builder.buildAllProjects(); } function addProject(projectSpecification: string) { @@ -461,7 +496,11 @@ namespace ts { } } - export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) { + /** + * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but + * can dynamically add/remove other projects based on changes on the rootNames' references + */ + export function createSolutionBuilder(host: CompilerHost, rootNames: ReadonlyArray, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) { if (!host.getModifiedTime || !host.setModifiedTime) { throw new Error("Host must support timestamp APIs"); } @@ -470,12 +509,18 @@ namespace ts { let context = createBuildContext(defaultOptions, reportDiagnostic); return { + buildAllProjects, getUpToDateStatus, getUpToDateStatusOfFile, - buildProjects, - cleanProjects, + cleanAllProjects, resetBuildContext, - getBuildGraph + getBuildGraph, + + invalidateProject, + buildInvalidatedProjects, + buildDependentInvalidatedProjects, + + resolveProjectName }; function resetBuildContext(opts = defaultOptions) { @@ -486,13 +531,17 @@ namespace ts { return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); } - function getBuildGraph(configFileNames: string[]) { + function getBuildGraph(configFileNames: ReadonlyArray) { const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); - if (resolvedNames === undefined) return; + if (resolvedNames === undefined) return undefined; return createDependencyGraph(resolvedNames); } + function getGlobalDependencyGraph() { + return getBuildGraph(rootNames); + } + function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { if (project === undefined) { return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; @@ -507,6 +556,73 @@ namespace ts { return actual; } + function invalidateProject(configFileName: string) { + const resolved = resolveProjectName(configFileName); + if (resolved === undefined) { + // If this was a rootName, we need to track it as missing. + // Otherwise we can just ignore it and have it possibly surface as an error in any downstream projects, + // if they exist + + // TODO: do those things + return; + } + + configFileCache.removeKey(resolved); + context.invalidatedProjects.setValue(resolved, true); + context.projectStatus.removeKey(resolved); + + const graph = getGlobalDependencyGraph()!; + if (graph) { + queueBuildForDownstreamReferences(resolved); + } + + // Mark all downstream projects of this one needing to be built "later" + function queueBuildForDownstreamReferences(root: ResolvedConfigFileName) { + debugger; + const deps = graph.dependencyMap.getReferencesTo(root); + for (const ref of deps) { + // Can skip circular references + if (!context.queuedProjects.hasKey(ref)) { + context.queuedProjects.setValue(ref, true); + queueBuildForDownstreamReferences(ref); + } + } + } + } + + function buildInvalidatedProjects() { + buildSomeProjects(p => context.invalidatedProjects.hasKey(p)); + } + + function buildDependentInvalidatedProjects() { + buildSomeProjects(p => context.queuedProjects.hasKey(p)); + } + + function buildSomeProjects(predicate: (projName: ResolvedConfigFileName) => boolean) { + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(rootNames); + if (resolvedNames === undefined) return; + + const graph = createDependencyGraph(resolvedNames)!; + for (const next of graph.buildQueue) { + if (!predicate(next)) continue; + + const resolved = resolveProjectName(next); + if (!resolved) continue; // ?? + const proj = configFileCache.parseConfigFile(resolved); + if (!proj) continue; // ? + + const status = getUpToDateStatus(proj); + reportProjectStatus(next, status); + + if (status.type === UpToDateStatusType.UpstreamBlocked) { + context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors, resolved, status.upstreamProjectName); + continue; + } + + buildSingleProject(next); + } + } + function getAllProjectOutputs(project: ParsedCommandLine): ReadonlyArray { if (project.options.outFile) { return getOutFileOutputs(project); @@ -824,7 +940,7 @@ namespace ts { context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); } - function getFilesToClean(configFileNames: ResolvedConfigFileName[]): string[] | undefined { + function getFilesToClean(configFileNames: ReadonlyArray): string[] | undefined { const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); if (resolvedNames === undefined) return undefined; @@ -849,12 +965,24 @@ namespace ts { return filesToDelete; } - function cleanProjects(configFileNames: string[]) { - const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); - if (resolvedNames === undefined) return; + function getAllProjectsInScope(): ReadonlyArray | undefined { + const resolvedNames = resolveProjectNames(rootNames); + if (resolvedNames === undefined) return undefined; + const graph = createDependencyGraph(resolvedNames); + if (graph === undefined) return undefined; + return graph.buildQueue; + } + + function cleanAllProjects() { + const resolvedNames: ReadonlyArray | undefined = getAllProjectsInScope(); + if (resolvedNames === undefined) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located)); + return; + } const filesToDelete = getFilesToClean(resolvedNames); if (filesToDelete === undefined) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located)); return; } @@ -885,7 +1013,7 @@ namespace ts { return undefined; } - function resolveProjectNames(configFileNames: string[]): ResolvedConfigFileName[] | undefined { + function resolveProjectNames(configFileNames: ReadonlyArray): ResolvedConfigFileName[] | undefined { const resolvedNames: ResolvedConfigFileName[] = []; for (const name of configFileNames) { const resolved = resolveProjectName(name); @@ -897,12 +1025,8 @@ namespace ts { return resolvedNames; } - function buildProjects(configFileNames: string[]) { - const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); - if (resolvedNames === undefined) return; - - // Establish what needs to be built - const graph = createDependencyGraph(resolvedNames); + function buildAllProjects() { + const graph = getGlobalDependencyGraph(); if (graph === undefined) return; const queue = graph.buildQueue; diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index ed1fd967952..46fee63a7c0 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -19,11 +19,10 @@ namespace ts { it("can build the sample project 'sample1' without error", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); clearDiagnostics(); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages(/*empty*/); // Check for outputs. Not an exhaustive list @@ -38,9 +37,8 @@ namespace ts { clearDiagnostics(); const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); // Check for outputs to not be written. Not an exhaustive list @@ -54,14 +52,13 @@ namespace ts { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - let builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + let builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); tick(); clearDiagnostics(); - builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); - builder.buildProjects(["."]); + builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); }); }); @@ -72,20 +69,19 @@ namespace ts { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); // Verify they exist for (const output of allExpectedOutputs) { assert(fs.existsSync(output), `Expect file ${output} to exist`); } - builder.cleanProjects(["."]); + builder.cleanAllProjects(); // Verify they are gone for (const output of allExpectedOutputs) { assert(!fs.existsSync(output), `Expect file ${output} to not exist`); } // Subsequent clean shouldn't throw / etc - builder.cleanProjects(["."]); + builder.cleanAllProjects(); }); }); @@ -94,16 +90,15 @@ namespace ts { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: true, verbose: false }); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: true, verbose: false }); + builder.buildAllProjects(); let currentTime = time(); checkOutputTimestamps(currentTime); tick(); Debug.assert(time() !== currentTime, "Time moves on"); currentTime = time(); - builder.buildProjects(["."]); + builder.buildAllProjects(); checkOutputTimestamps(currentTime); function checkOutputTimestamps(expected: number) { @@ -119,14 +114,12 @@ namespace ts { describe("tsbuild - can detect when and what to rebuild", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); - - fs.chdir("/src/tests"); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); it("Builds the project", () => { clearDiagnostics(); builder.resetBuildContext(); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0, @@ -141,7 +134,7 @@ namespace ts { it("Detects that all projects are up to date", () => { clearDiagnostics(); builder.resetBuildContext(); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, @@ -154,7 +147,7 @@ namespace ts { clearDiagnostics(); fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); builder.resetBuildContext(); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, @@ -169,7 +162,7 @@ namespace ts { clearDiagnostics(); replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); builder.resetBuildContext(); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, @@ -185,14 +178,13 @@ namespace ts { it("won't build downstream projects if upstream projects have errors", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); clearDiagnostics(); // Induce an error in the middle project replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages( Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, @@ -221,8 +213,6 @@ namespace ts { writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); - it("orders the graph correctly - specify two roots", () => { checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]); }); @@ -240,6 +230,8 @@ namespace ts { }); function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { + const builder = createSolutionBuilder(host, rootNames, reportDiagnostic, { dry: true, force: false, verbose: false }); + const projFileNames = rootNames.map(getProjectFileName); const graph = builder.getBuildGraph(projFileNames); if (graph === undefined) throw new Error("Graph shouldn't be undefined"); @@ -280,6 +272,39 @@ namespace ts { } }); + describe("tsbuild - project invalidation", () => { + it ("invalidates projects correctly", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*empty*/); + + // Update a timestamp in the middle project + tick(); + touch(fs, "/src/logic/index.ts"); + // Because we haven't reset the build context, the builder should assume there's nothing to do right now + const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")!); + assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); + + // Rebuild this project + tick(); + builder.invalidateProject("/src/logic"); + builder.buildInvalidatedProjects(); + // The file should be updated + assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); + assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); + + // Build downstream projects should update 'tests', but not 'core' + tick(); + builder.buildDependentInvalidatedProjects(); + assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt"); + assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); + }); + }); + function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { if (!fs.statSync(path).isFile()) { throw new Error(`File ${path} does not exist`); @@ -324,6 +349,13 @@ namespace ts { return currentTime; } + function touch(fs: vfs.FileSystem, path: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); + } + fs.utimesSync(path, new Date(time()), new Date(time())); + } + function loadFsMirror(vfs: vfs.FileSystem, localRoot: string, virtualRoot: string) { vfs.mkdirpSync(virtualRoot); for (const path of Harness.IO.readDirectory(localRoot)) { From 7a1de6142be4cb293f409e69ded0cdc581995f4e Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 1 Jun 2018 09:44:35 -0700 Subject: [PATCH 30/48] Scaffold watch mode --- src/compiler/tsbuild.ts | 34 ++++++++++++++++++++++++++++++---- src/compiler/tsc.ts | 2 +- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 1f36f09393b..974edd6c60c 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -332,6 +332,7 @@ namespace ts { if (sourceFile === undefined) { return undefined; } + const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, getDirectoryPath(configFilePath)); parsed.options.configFilePath = configFilePath; cache.setValue(configFilePath, parsed); @@ -410,7 +411,7 @@ namespace ts { } ]; - export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[]) { + export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[], system?: System) { let verbose = false; let dry = false; let force = false; @@ -472,7 +473,7 @@ namespace ts { addProject("."); } - const builder = createSolutionBuilder(host, projects, reportDiagnostic, { verbose, dry, force }); + const builder = createSolutionBuilder(host, projects, reportDiagnostic, { verbose, dry, force }, system); if (clean) { builder.cleanAllProjects(); } @@ -480,6 +481,10 @@ namespace ts { builder.buildAllProjects(); } + if (watch) { + return builder.startWatching(); + } + function addProject(projectSpecification: string) { const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification); const refPath = resolveProjectReferencePath(host, { path: fileName }); @@ -500,7 +505,7 @@ namespace ts { * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but * can dynamically add/remove other projects based on changes on the rootNames' references */ - export function createSolutionBuilder(host: CompilerHost, rootNames: ReadonlyArray, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) { + export function createSolutionBuilder(host: CompilerHost, rootNames: ReadonlyArray, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions, system?: System) { if (!host.getModifiedTime || !host.setModifiedTime) { throw new Error("Host must support timestamp APIs"); } @@ -520,9 +525,30 @@ namespace ts { buildInvalidatedProjects, buildDependentInvalidatedProjects, - resolveProjectName + resolveProjectName, + + startWatching }; + function startWatching() { + if (!system) throw new Error("System host must be provided if using --watch"); + if (!system.watchFile || !system.watchDirectory || !system.setTimeout) throw new Error("System host must support watchFile / watchDirectory / setTimeout if using --watch"); + + const graph = getGlobalDependencyGraph()!; + for (const resolved of graph.buildQueue) { + const cfg = configFileCache.parseConfigFile(resolved); + if (cfg) { + for (const input of cfg.fileNames) { + system.watchFile(input, () => { + invalidateProject(resolved); + system.setTimeout!(buildInvalidatedProjects, 100); + system.setTimeout!(buildDependentInvalidatedProjects, 3000); + }); + } + } + } + } + function resetBuildContext(opts = defaultOptions) { context = createBuildContext(opts, reportDiagnostic); } diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 9800d2e768a..744379aa696 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -50,7 +50,7 @@ namespace ts { export function executeCommandLine(args: string[]): void { if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) { - return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1)); + return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1), sys); } const commandLine = parseCommandLine(args); From 7e9e29ce64218568ab7d33e6423838268238fc38 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 1 Jun 2018 13:12:57 -0700 Subject: [PATCH 31/48] Store + use the resolved path of sourceFiles in proj. ref. scenarios --- src/compiler/program.ts | 3 ++- src/compiler/types.ts | 1 + src/server/project.ts | 4 ++-- src/services/services.ts | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 4dfb9039ccf..f797cac82e4 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -987,7 +987,7 @@ namespace ts { for (const oldSourceFile of oldSourceFiles) { let newSourceFile = host.getSourceFileByPath - ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile) + ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath || oldSourceFile.path, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile) : host.getSourceFile(oldSourceFile.fileName, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile); // TODO: GH#18217 if (!newSourceFile) { @@ -1991,6 +1991,7 @@ namespace ts { if (file) { sourceFilesFoundSearchingNodeModules.set(path, currentNodeModulesDepth > 0); file.path = path; + file.resolvedPath = toPath(fileName); if (host.useCaseSensitiveFileNames()) { const pathLowerCase = path.toLowerCase(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a3cdb32df07..8d9f801c81e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2551,6 +2551,7 @@ namespace ts { fileName: string; /* @internal */ path: Path; text: string; + /* @internal */ resolvedPath: Path; /** * If two source files are for the same version of the same package, one will redirect to the other. diff --git a/src/server/project.ts b/src/server/project.ts index f6f68f66387..4986df27d98 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -629,8 +629,8 @@ namespace ts.server { return this.rootFiles; } return map(this.program.getSourceFiles(), sourceFile => { - const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.path); - Debug.assert(!!scriptInfo, "getScriptInfo", () => `scriptInfo for a file '${sourceFile.fileName}' Path: '${sourceFile.path}' is missing.`); + const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.resolvedPath || sourceFile.path); + Debug.assert(!!scriptInfo, "getScriptInfo", () => `scriptInfo for a file '${sourceFile.fileName}' Path: '${sourceFile.path}' / '${sourceFile.resolvedPath}' is missing.`); return scriptInfo!; }); } diff --git a/src/services/services.ts b/src/services/services.ts index dcea57b7159..b40f24112ef 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -540,6 +540,7 @@ namespace ts { public _declarationBrand: any; public fileName: string; public path: Path; + public resolvedPath: Path; public text: string; public scriptSnapshot: IScriptSnapshot; public lineMap: ReadonlyArray; From 17dc380ec8a5d007ba46d9fb7523d1da07317fc4 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 13:19:41 -0700 Subject: [PATCH 32/48] Reorganize tsbuild unit test file --- src/harness/unittests/tsbuild.ts | 444 +++++++++++++++++-------------- 1 file changed, 239 insertions(+), 205 deletions(-) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 46fee63a7c0..48f568a65ac 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -1,202 +1,257 @@ namespace ts { let currentTime = 100; - const bfs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); let lastDiagnostics: Diagnostic[] = []; const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); - const sampleRoot = resolvePath(__dirname, "../../tests/projects/sample1"); - loadFsMirror(bfs, sampleRoot, "/src"); - bfs.mkdirpSync("/lib"); - bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))!); - bfs.meta.set("defaultLibLocation", "/lib"); - bfs.makeReadonly(); - tick(); - const allExpectedOutputs = ["/src/tests/index.js", - "/src/core/index.js", "/src/core/index.d.ts", - "/src/logic/index.js", "/src/logic/index.d.ts"]; + namespace Sample1 { + tick(); + const projFs = loadProjectFromDisk("../../tests/projects/sample1"); - describe("tsbuild - sanity check of clean build of 'sample1' project", () => { - it("can build the sample project 'sample1' without error", () => { - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + const allExpectedOutputs = ["/src/tests/index.js", + "/src/core/index.js", "/src/core/index.d.ts", + "/src/logic/index.js", "/src/logic/index.d.ts"]; - clearDiagnostics(); - builder.buildAllProjects(); - assertDiagnosticMessages(/*empty*/); + describe("tsbuild - sanity check of clean build of 'sample1' project", () => { + it("can build the sample project 'sample1' without error", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); - // Check for outputs. Not an exhaustive list - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - }); - }); + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*empty*/); - describe("tsbuild - dry builds", () => { - it("doesn't write any files in a dry build", () => { - clearDiagnostics(); - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); - builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); - - // Check for outputs to not be written. Not an exhaustive list - for (const output of allExpectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } - }); - - it("indicates that it would skip builds during a dry build", () => { - clearDiagnostics(); - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - - let builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); - tick(); - - clearDiagnostics(); - builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); - builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); - }); - }); - - describe("tsbuild - clean builds", () => { - it("removes all files it built", () => { - clearDiagnostics(); - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); - // Verify they exist - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - builder.cleanAllProjects(); - // Verify they are gone - for (const output of allExpectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } - // Subsequent clean shouldn't throw / etc - builder.cleanAllProjects(); - }); - }); - - describe("tsbuild - force builds", () => { - it("always builds under --force", () => { - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: true, verbose: false }); - builder.buildAllProjects(); - let currentTime = time(); - checkOutputTimestamps(currentTime); - - tick(); - Debug.assert(time() !== currentTime, "Time moves on"); - currentTime = time(); - builder.buildAllProjects(); - checkOutputTimestamps(currentTime); - - function checkOutputTimestamps(expected: number) { - // Check timestamps + // Check for outputs. Not an exhaustive list for (const output of allExpectedOutputs) { - const actual = fs.statSync(output).mtimeMs; - assert(actual === expected, `File ${output} has timestamp ${actual}, expected ${expected}`); + assert(fs.existsSync(output), `Expect file ${output} to exist`); } - } - }); - }); - - describe("tsbuild - can detect when and what to rebuild", () => { - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); - - it("Builds the project", () => { - clearDiagnostics(); - builder.resetBuildContext(); - builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, - Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - Diagnostics.Building_project_0, - Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - Diagnostics.Building_project_0, - Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - Diagnostics.Building_project_0); - tick(); + }); }); - // All three projects are up to date - it("Detects that all projects are up to date", () => { - clearDiagnostics(); - builder.resetBuildContext(); - builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, - Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, - Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, - Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); - tick(); + describe("tsbuild - dry builds", () => { + it("doesn't write any files in a dry build", () => { + clearDiagnostics(); + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); + + // Check for outputs to not be written. Not an exhaustive list + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + }); + + it("indicates that it would skip builds during a dry build", () => { + clearDiagnostics(); + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + + let builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); + tick(); + + clearDiagnostics(); + builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); + }); }); - // Update a file in the leaf node (tests), only it should rebuild the last one - it("Only builds the leaf node project", () => { - clearDiagnostics(); - fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); - builder.resetBuildContext(); - builder.buildAllProjects(); + describe("tsbuild - clean builds", () => { + it("removes all files it built", () => { + clearDiagnostics(); + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, - Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, - Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, - Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, - Diagnostics.Building_project_0); - tick(); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); + // Verify they exist + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + builder.cleanAllProjects(); + // Verify they are gone + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + // Subsequent clean shouldn't throw / etc + builder.cleanAllProjects(); + }); }); - // Update a file in the parent (without affecting types), should get fast downstream builds - it("Detects type-only changes in upstream projects", () => { - clearDiagnostics(); - replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); - builder.resetBuildContext(); - builder.buildAllProjects(); + describe("tsbuild - force builds", () => { + it("always builds under --force", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, - Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, - Diagnostics.Building_project_0, - Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, - Diagnostics.Updating_output_timestamps_of_project_0, - Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, - Diagnostics.Updating_output_timestamps_of_project_0); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: true, verbose: false }); + builder.buildAllProjects(); + let currentTime = time(); + checkOutputTimestamps(currentTime); + + tick(); + Debug.assert(time() !== currentTime, "Time moves on"); + currentTime = time(); + builder.buildAllProjects(); + checkOutputTimestamps(currentTime); + + function checkOutputTimestamps(expected: number) { + // Check timestamps + for (const output of allExpectedOutputs) { + const actual = fs.statSync(output).mtimeMs; + assert(actual === expected, `File ${output} has timestamp ${actual}, expected ${expected}`); + } + } + }); }); - }); - describe("tsbuild - downstream-blocked compilations", () => { - it("won't build downstream projects if upstream projects have errors", () => { - const fs = bfs.shadow(); + describe("tsbuild - can detect when and what to rebuild", () => { + const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); - clearDiagnostics(); + it("Builds the project", () => { + clearDiagnostics(); + builder.resetBuildContext(); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0); + tick(); + }); - // Induce an error in the middle project - replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); - builder.buildAllProjects(); - assertDiagnosticMessages( - Diagnostics.Sorted_list_of_input_projects_Colon_0, - Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - Diagnostics.Building_project_0, - Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - Diagnostics.Building_project_0, - Diagnostics.Property_0_does_not_exist_on_type_1, - Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, - Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors - ); + // All three projects are up to date + it("Detects that all projects are up to date", () => { + clearDiagnostics(); + builder.resetBuildContext(); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); + tick(); + }); + + // Update a file in the leaf node (tests), only it should rebuild the last one + it("Only builds the leaf node project", () => { + clearDiagnostics(); + fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); + builder.resetBuildContext(); + builder.buildAllProjects(); + + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + Diagnostics.Building_project_0); + tick(); + }); + + // Update a file in the parent (without affecting types), should get fast downstream builds + it("Detects type-only changes in upstream projects", () => { + clearDiagnostics(); + replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); + builder.resetBuildContext(); + builder.buildAllProjects(); + + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Updating_output_timestamps_of_project_0, + Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Updating_output_timestamps_of_project_0); + }); }); - }); + + describe("tsbuild - downstream-blocked compilations", () => { + it("won't build downstream projects if upstream projects have errors", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); + + clearDiagnostics(); + + // Induce an error in the middle project + replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); + builder.buildAllProjects(); + assertDiagnosticMessages( + Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Property_0_does_not_exist_on_type_1, + Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, + Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors + ); + }); + }); + + describe("tsbuild - project invalidation", () => { + it("invalidates projects correctly", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*empty*/); + + // Update a timestamp in the middle project + tick(); + touch(fs, "/src/logic/index.ts"); + // Because we haven't reset the build context, the builder should assume there's nothing to do right now + const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")!); + assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); + + // Rebuild this project + tick(); + builder.invalidateProject("/src/logic"); + builder.buildInvalidatedProjects(); + // The file should be updated + assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); + assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); + + // Build downstream projects should update 'tests', but not 'core' + tick(); + builder.buildDependentInvalidatedProjects(); + assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt"); + assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); + }); + }); + } + + namespace OutFile { + const outFileFs = loadProjectFromDisk("../../tests/projects/outfile-concat"); + + describe("tsbuild - baseline sectioned sourcemaps", () => { + const fs = outFileFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/third"], reportDiagnostic, { dry: false, force: false, verbose: false }); + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*none*/); + + it("Generates files matching the baseline", () => { + Harness.Baseline.runBaseline("outfile-concat.js", () => { + return fs.readFileSync("/src/third/third-output.js", 'utf-8'); + }); + + Harness.Baseline.runBaseline("outfile-concat.js.map", () => { + return fs.readFileSync("/src/third/third-output.js.map", 'utf-8'); + }); + }); + }); + } + + void OutFile, Sample1; describe("tsbuild - graph-ordering", () => { const fs = new vfs.FileSystem(false); @@ -272,38 +327,6 @@ namespace ts { } }); - describe("tsbuild - project invalidation", () => { - it ("invalidates projects correctly", () => { - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); - - clearDiagnostics(); - builder.buildAllProjects(); - assertDiagnosticMessages(/*empty*/); - - // Update a timestamp in the middle project - tick(); - touch(fs, "/src/logic/index.ts"); - // Because we haven't reset the build context, the builder should assume there's nothing to do right now - const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")!); - assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); - - // Rebuild this project - tick(); - builder.invalidateProject("/src/logic"); - builder.buildInvalidatedProjects(); - // The file should be updated - assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); - assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); - - // Build downstream projects should update 'tests', but not 'core' - tick(); - builder.buildDependentInvalidatedProjects(); - assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt"); - assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); - }); - }); function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { if (!fs.statSync(path).isFile()) { @@ -356,6 +379,17 @@ namespace ts { fs.utimesSync(path, new Date(time()), new Date(time())); } + function loadProjectFromDisk(root: string): vfs.FileSystem { + const fs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); + const rootPath = resolvePath(__dirname, root); + loadFsMirror(fs, rootPath, "/src"); + fs.mkdirpSync("/lib"); + fs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))!); + fs.meta.set("defaultLibLocation", "/lib"); + fs.makeReadonly(); + return fs; + } + function loadFsMirror(vfs: vfs.FileSystem, localRoot: string, virtualRoot: string) { vfs.mkdirpSync(virtualRoot); for (const path of Harness.IO.readDirectory(localRoot)) { From 514a0d85a58821206171f561ba608990a3f4a130 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 13:19:52 -0700 Subject: [PATCH 33/48] Add outfile-concat project refs test --- .../projects/outfile-concat/first/first_part1.ts | 11 +++++++++++ .../projects/outfile-concat/first/first_part2.ts | 1 + .../projects/outfile-concat/first/first_part3.ts | 3 +++ .../projects/outfile-concat/first/tsconfig.json | 14 ++++++++++++++ .../outfile-concat/second/second_part1.ts | 11 +++++++++++ .../outfile-concat/second/second_part2.ts | 5 +++++ .../projects/outfile-concat/second/tsconfig.json | 14 ++++++++++++++ .../projects/outfile-concat/third/third_part1.ts | 2 ++ .../projects/outfile-concat/third/tsconfig.json | 16 ++++++++++++++++ 9 files changed, 77 insertions(+) create mode 100644 tests/projects/outfile-concat/first/first_part1.ts create mode 100644 tests/projects/outfile-concat/first/first_part2.ts create mode 100644 tests/projects/outfile-concat/first/first_part3.ts create mode 100644 tests/projects/outfile-concat/first/tsconfig.json create mode 100644 tests/projects/outfile-concat/second/second_part1.ts create mode 100644 tests/projects/outfile-concat/second/second_part2.ts create mode 100644 tests/projects/outfile-concat/second/tsconfig.json create mode 100644 tests/projects/outfile-concat/third/third_part1.ts create mode 100644 tests/projects/outfile-concat/third/tsconfig.json diff --git a/tests/projects/outfile-concat/first/first_part1.ts b/tests/projects/outfile-concat/first/first_part1.ts new file mode 100644 index 00000000000..b8810033aaa --- /dev/null +++ b/tests/projects/outfile-concat/first/first_part1.ts @@ -0,0 +1,11 @@ +interface TheFirst { + none: any; +} + +const s = "Hello, world"; + +interface NoJsForHereEither { + none: any; +} + +console.log(s); diff --git a/tests/projects/outfile-concat/first/first_part2.ts b/tests/projects/outfile-concat/first/first_part2.ts new file mode 100644 index 00000000000..bd60d3eba9f --- /dev/null +++ b/tests/projects/outfile-concat/first/first_part2.ts @@ -0,0 +1 @@ +console.log(f()); diff --git a/tests/projects/outfile-concat/first/first_part3.ts b/tests/projects/outfile-concat/first/first_part3.ts new file mode 100644 index 00000000000..6f497fc490a --- /dev/null +++ b/tests/projects/outfile-concat/first/first_part3.ts @@ -0,0 +1,3 @@ +function f() { + return "JS does hoists"; +} \ No newline at end of file diff --git a/tests/projects/outfile-concat/first/tsconfig.json b/tests/projects/outfile-concat/first/tsconfig.json new file mode 100644 index 00000000000..75562e332f2 --- /dev/null +++ b/tests/projects/outfile-concat/first/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es5", + "composite": true, + "removeComments": true, + "strict": false, + "sourceMap": true, + "declarationMap": true, + "declaration": true, + "outFile": "./first-output.js" + }, + "references": [ + ] +} diff --git a/tests/projects/outfile-concat/second/second_part1.ts b/tests/projects/outfile-concat/second/second_part1.ts new file mode 100644 index 00000000000..2b995fbe4a5 --- /dev/null +++ b/tests/projects/outfile-concat/second/second_part1.ts @@ -0,0 +1,11 @@ +namespace N { + // Comment text +} + +namespace N { + function f() { + console.log('testing'); + } + + f(); +} diff --git a/tests/projects/outfile-concat/second/second_part2.ts b/tests/projects/outfile-concat/second/second_part2.ts new file mode 100644 index 00000000000..b81737e8915 --- /dev/null +++ b/tests/projects/outfile-concat/second/second_part2.ts @@ -0,0 +1,5 @@ +class C { + doSomething() { + console.log("something got done"); + } +} diff --git a/tests/projects/outfile-concat/second/tsconfig.json b/tests/projects/outfile-concat/second/tsconfig.json new file mode 100644 index 00000000000..42d93ee56fb --- /dev/null +++ b/tests/projects/outfile-concat/second/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es5", + "composite": true, + "removeComments": true, + "strict": false, + "sourceMap": true, + "declarationMap": true, + "declaration": true, + "outFile": "./second-output.js" + }, + "references": [ + ] +} diff --git a/tests/projects/outfile-concat/third/third_part1.ts b/tests/projects/outfile-concat/third/third_part1.ts new file mode 100644 index 00000000000..948688ae5ff --- /dev/null +++ b/tests/projects/outfile-concat/third/third_part1.ts @@ -0,0 +1,2 @@ +var c = new C(); +c.doSomething(); diff --git a/tests/projects/outfile-concat/third/tsconfig.json b/tests/projects/outfile-concat/third/tsconfig.json new file mode 100644 index 00000000000..195272b2d94 --- /dev/null +++ b/tests/projects/outfile-concat/third/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "composite": true, + "removeComments": true, + "strict": false, + "sourceMap": true, + "declarationMap": true, + "declaration": true, + "outFile": "./third-output.js" + }, + "references": [ + { "path": "../first", "prepend": true }, + { "path": "../second", "prepend": true }, + ] +} From 394e29f9d12743ac21d3431b7be2eee13ae2afb3 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 13:20:01 -0700 Subject: [PATCH 34/48] Accept baselines for sourcemap sections --- tests/baselines/reference/outfile-concat.js | 26 +++++++++++++++++++ .../baselines/reference/outfile-concat.js.map | 1 + 2 files changed, 27 insertions(+) create mode 100644 tests/baselines/reference/outfile-concat.js create mode 100644 tests/baselines/reference/outfile-concat.js.map diff --git a/tests/baselines/reference/outfile-concat.js b/tests/baselines/reference/outfile-concat.js new file mode 100644 index 00000000000..e3e5aa3d5f0 --- /dev/null +++ b/tests/baselines/reference/outfile-concat.js @@ -0,0 +1,26 @@ +var s = "Hello, world"; +console.log(s); +console.log(f()); +function f() { + return "JS does hoists"; +} +//# sourceMappingURL=first-output.js.map +var N; +(function (N) { + function f() { + console.log('testing'); + } + f(); +})(N || (N = {})); +var C = (function () { + function C() { + } + C.prototype.doSomething = function () { + console.log("something got done"); + }; + return C; +}()); +//# sourceMappingURL=second-output.js.map +var c = new C(); +c.doSomething(); +//# sourceMappingURL=third-output.js.map \ No newline at end of file diff --git a/tests/baselines/reference/outfile-concat.js.map b/tests/baselines/reference/outfile-concat.js.map new file mode 100644 index 00000000000..f8220832b2a --- /dev/null +++ b/tests/baselines/reference/outfile-concat.js.map @@ -0,0 +1 @@ +{"version":3,"file":"third-output.js","sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"file":"first-output.js","sourceRoot":"","sources":["first_part1.ts","first_part2.ts","first_part3.ts"],"names":[],"mappings":"AAIA,IAAM,CAAC,GAAG,cAAc,CAAC;AAMzB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;ACVf,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;ACAjB;IACI,OAAO,gBAAgB,CAAC;AAC5B,CAAC"}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"second-output.js","sourceRoot":"","sources":["second_part1.ts","second_part2.ts"],"names":[],"mappings":"AAIA,IAAU,CAAC,CAMV;AAND,WAAU,CAAC;IACP;QACI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAED,CAAC,EAAE,CAAC;AACR,CAAC,EANS,CAAC,KAAD,CAAC,QAMV;ACVD;IAAA;IAIA,CAAC;IAHG,uBAAW,GAAX;QACI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACtC,CAAC;IACL,QAAC;AAAD,CAAC,AAJD,IAIC"}},{"offset":{"line":22,"column":41},"map":{"version":3,"file":"third-output.js","sourceRoot":"","sources":["third_part1.ts"],"names":[],"mappings":";AAAA,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AAChB,CAAC,CAAC,WAAW,EAAE,CAAC"}}]} \ No newline at end of file From e40778070ed380ce10ccf97f2eb031f8ab7e876d Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 14:06:13 -0700 Subject: [PATCH 35/48] Complicate the paths of the project for better sourcemap testing --- src/harness/unittests/tsbuild.ts | 22 ++++++++--- src/harness/vfs.ts | 14 ++++--- .../reference/outfile-concat-fileListing.txt | 39 +++++++++++++++++++ tests/baselines/reference/third-output.js | 26 +++++++++++++ tests/baselines/reference/third-output.js.map | 1 + .../outfile-concat/first/tsconfig.json | 2 +- .../outfile-concat/second/tsconfig.json | 2 +- .../outfile-concat/third/tsconfig.json | 2 +- 8 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 tests/baselines/reference/outfile-concat-fileListing.txt create mode 100644 tests/baselines/reference/third-output.js create mode 100644 tests/baselines/reference/third-output.js.map diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 48f568a65ac..22f68fba866 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -239,13 +239,23 @@ namespace ts { builder.buildAllProjects(); assertDiagnosticMessages(/*none*/); - it("Generates files matching the baseline", () => { - Harness.Baseline.runBaseline("outfile-concat.js", () => { - return fs.readFileSync("/src/third/third-output.js", 'utf-8'); + const files = [ + "/src/third/thirdjs/output/third-output.js", + "/src/third/thirdjs/output/third-output.js.map" + ]; + + + for (const file of files) { + it(`Generates files matching the baseline - ${file}`, () => { + Harness.Baseline.runBaseline(getBaseFileName(file), () => { + return fs.readFileSync(file, 'utf-8'); + }); }); - - Harness.Baseline.runBaseline("outfile-concat.js.map", () => { - return fs.readFileSync("/src/third/third-output.js.map", 'utf-8'); + } + + it(`Generates files matching the baseline - file listing for outFile-concat`, () => { + Harness.Baseline.runBaseline("outfile-concat-fileListing.txt", () => { + return fs.getFileListing(); }); }); }); diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index ba5905b8ea6..186f3610dcd 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -353,10 +353,7 @@ namespace vfs { if (!result.node) this._mkdir(result); } - /** - * Print diagnostic information about the structure of the file system to the console. - */ - public debugPrint(): void { + public getFileListing(): string { let result = ""; const printLinks = (dirname: string | undefined, links: collections.SortedMap) => { const iterator = collections.getIterator(links); @@ -384,7 +381,14 @@ namespace vfs { } }; printLinks(/*dirname*/ undefined, this._getRootLinks()); - console.log(result); + return result; + } + + /** + * Print diagnostic information about the structure of the file system to the console. + */ + public debugPrint(): void { + console.log(this.getFileListing()); } // POSIX API (aligns with NodeJS "fs" module API) diff --git a/tests/baselines/reference/outfile-concat-fileListing.txt b/tests/baselines/reference/outfile-concat-fileListing.txt new file mode 100644 index 00000000000..3fca9338912 --- /dev/null +++ b/tests/baselines/reference/outfile-concat-fileListing.txt @@ -0,0 +1,39 @@ +*/ + /lib/ + /lib/lib.d.ts + /src/ + /src/2/ + /src/2/second-output.d.ts + /src/2/second-output.d.ts.map + /src/2/second-output.js + /src/2/second-output.js.map + /src/first/ + /src/first/bin/ + /src/first/bin/first-output.d.ts + /src/first/bin/first-output.d.ts.map + /src/first/bin/first-output.js + /src/first/bin/first-output.js.map + /src/first/first_part1.ts + /src/first/first_part2.ts + /src/first/first_part3.ts + /src/first/tsconfig.json + /src/first_part1.ts + /src/first_part2.ts + /src/first_part3.ts + /src/second/ + /src/second/second_part1.ts + /src/second/second_part2.ts + /src/second/tsconfig.json + /src/second_part1.ts + /src/second_part2.ts + /src/third/ + /src/third/third_part1.ts + /src/third/thirdjs/ + /src/third/thirdjs/output/ + /src/third/thirdjs/output/third-output.d.ts + /src/third/thirdjs/output/third-output.d.ts.map + /src/third/thirdjs/output/third-output.js + /src/third/thirdjs/output/third-output.js.map + /src/third/tsconfig.json + /src/third_part1.ts + /src/tsconfig.json \ No newline at end of file diff --git a/tests/baselines/reference/third-output.js b/tests/baselines/reference/third-output.js new file mode 100644 index 00000000000..e3e5aa3d5f0 --- /dev/null +++ b/tests/baselines/reference/third-output.js @@ -0,0 +1,26 @@ +var s = "Hello, world"; +console.log(s); +console.log(f()); +function f() { + return "JS does hoists"; +} +//# sourceMappingURL=first-output.js.map +var N; +(function (N) { + function f() { + console.log('testing'); + } + f(); +})(N || (N = {})); +var C = (function () { + function C() { + } + C.prototype.doSomething = function () { + console.log("something got done"); + }; + return C; +}()); +//# sourceMappingURL=second-output.js.map +var c = new C(); +c.doSomething(); +//# sourceMappingURL=third-output.js.map \ No newline at end of file diff --git a/tests/baselines/reference/third-output.js.map b/tests/baselines/reference/third-output.js.map new file mode 100644 index 00000000000..70b9ad69d53 --- /dev/null +++ b/tests/baselines/reference/third-output.js.map @@ -0,0 +1 @@ +{"version":3,"file":"third-output.js","sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"file":"first-output.js","sourceRoot":"","sources":["../first_part1.ts","../first_part2.ts","../first_part3.ts"],"names":[],"mappings":"AAIA,IAAM,CAAC,GAAG,cAAc,CAAC;AAMzB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;ACVf,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;ACAjB;IACI,OAAO,gBAAgB,CAAC;AAC5B,CAAC"}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"second-output.js","sourceRoot":"","sources":["../second/second_part1.ts","../second/second_part2.ts"],"names":[],"mappings":"AAIA,IAAU,CAAC,CAMV;AAND,WAAU,CAAC;IACP;QACI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAED,CAAC,EAAE,CAAC;AACR,CAAC,EANS,CAAC,KAAD,CAAC,QAMV;ACVD;IAAA;IAIA,CAAC;IAHG,uBAAW,GAAX;QACI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACtC,CAAC;IACL,QAAC;AAAD,CAAC,AAJD,IAIC"}},{"offset":{"line":22,"column":41},"map":{"version":3,"file":"third-output.js","sourceRoot":"","sources":["../../third_part1.ts"],"names":[],"mappings":";AAAA,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AAChB,CAAC,CAAC,WAAW,EAAE,CAAC"}}]} \ No newline at end of file diff --git a/tests/projects/outfile-concat/first/tsconfig.json b/tests/projects/outfile-concat/first/tsconfig.json index 75562e332f2..8370f6512b8 100644 --- a/tests/projects/outfile-concat/first/tsconfig.json +++ b/tests/projects/outfile-concat/first/tsconfig.json @@ -7,7 +7,7 @@ "sourceMap": true, "declarationMap": true, "declaration": true, - "outFile": "./first-output.js" + "outFile": "./bin/first-output.js" }, "references": [ ] diff --git a/tests/projects/outfile-concat/second/tsconfig.json b/tests/projects/outfile-concat/second/tsconfig.json index 42d93ee56fb..d835cff6d66 100644 --- a/tests/projects/outfile-concat/second/tsconfig.json +++ b/tests/projects/outfile-concat/second/tsconfig.json @@ -7,7 +7,7 @@ "sourceMap": true, "declarationMap": true, "declaration": true, - "outFile": "./second-output.js" + "outFile": "../2/second-output.js" }, "references": [ ] diff --git a/tests/projects/outfile-concat/third/tsconfig.json b/tests/projects/outfile-concat/third/tsconfig.json index 195272b2d94..18c98608db1 100644 --- a/tests/projects/outfile-concat/third/tsconfig.json +++ b/tests/projects/outfile-concat/third/tsconfig.json @@ -7,7 +7,7 @@ "sourceMap": true, "declarationMap": true, "declaration": true, - "outFile": "./third-output.js" + "outFile": "./thirdjs/output/third-output.js" }, "references": [ { "path": "../first", "prepend": true }, From 449d60cdffdf43ef29f8202ff96da946b134bb05 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 14:14:16 -0700 Subject: [PATCH 36/48] Don't crash on no args --- src/compiler/tsc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 744379aa696..149439d9b67 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -49,7 +49,7 @@ namespace ts { } export function executeCommandLine(args: string[]): void { - if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) { + if (args.length > 0 && ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b"))) { return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1), sys); } From 5065c540cdb007265efbce50a0b07becba5b9769 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 14:16:50 -0700 Subject: [PATCH 37/48] Tidy --- src/harness/unittests/tsbuild.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 22f68fba866..06b447a50e8 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -3,7 +3,7 @@ namespace ts { let lastDiagnostics: Diagnostic[] = []; const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); - namespace Sample1 { + export namespace Sample1 { tick(); const projFs = loadProjectFromDisk("../../tests/projects/sample1"); @@ -228,7 +228,7 @@ namespace ts { }); } - namespace OutFile { + export namespace OutFile { const outFileFs = loadProjectFromDisk("../../tests/projects/outfile-concat"); describe("tsbuild - baseline sectioned sourcemaps", () => { @@ -244,7 +244,6 @@ namespace ts { "/src/third/thirdjs/output/third-output.js.map" ]; - for (const file of files) { it(`Generates files matching the baseline - ${file}`, () => { Harness.Baseline.runBaseline(getBaseFileName(file), () => { @@ -261,8 +260,6 @@ namespace ts { }); } - void OutFile, Sample1; - describe("tsbuild - graph-ordering", () => { const fs = new vfs.FileSystem(false); const host = new fakes.CompilerHost(fs); From fad8f67093ef4d047e84bd05e7b203da817fd63b Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 14:17:55 -0700 Subject: [PATCH 38/48] Lint --- src/harness/unittests/tsbuild.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 06b447a50e8..244f8c43fb0 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -199,18 +199,18 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); - + clearDiagnostics(); builder.buildAllProjects(); assertDiagnosticMessages(/*empty*/); - + // Update a timestamp in the middle project tick(); touch(fs, "/src/logic/index.ts"); // Because we haven't reset the build context, the builder should assume there's nothing to do right now const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")!); assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); - + // Rebuild this project tick(); builder.invalidateProject("/src/logic"); @@ -218,7 +218,7 @@ namespace ts { // The file should be updated assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); - + // Build downstream projects should update 'tests', but not 'core' tick(); builder.buildDependentInvalidatedProjects(); @@ -247,7 +247,7 @@ namespace ts { for (const file of files) { it(`Generates files matching the baseline - ${file}`, () => { Harness.Baseline.runBaseline(getBaseFileName(file), () => { - return fs.readFileSync(file, 'utf-8'); + return fs.readFileSync(file, "utf-8"); }); }); } From 7ad9d57cc783f6eacb7924e1af0e8aa20d468573 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 16:19:54 -0700 Subject: [PATCH 39/48] Include filename when reporting not found --- src/harness/vfs.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 186f3610dcd..0e16b0fe715 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -444,7 +444,7 @@ namespace vfs { private _stat(entry: WalkResult) { const node = entry.node; - if (!node) throw createIOError("ENOENT"); + if (!node) throw createIOError(`ENOENT`, entry.realpath); return new Stats( node.dev, node.ino, @@ -1155,8 +1155,8 @@ namespace vfs { EROFS: "file system is read-only" }); - export function createIOError(code: keyof typeof IOErrorMessages) { - const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]}`); + export function createIOError(code: keyof typeof IOErrorMessages, details: string = "") { + const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]} ${details}`); err.code = code; if (Error.captureStackTrace) Error.captureStackTrace(err, createIOError); return err; From 5111f4d5416eaf6d050b165981f2df0c24575f91 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 16:20:07 -0700 Subject: [PATCH 40/48] Move proj ref logic to the right place --- src/compiler/program.ts | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 82e35c8182e..40a1bdcafff 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -618,25 +618,27 @@ namespace ts { // A parallel array to projectReferences storing the results of reading in the referenced tsconfig files const resolvedProjectReferences: (ResolvedProjectReference | undefined)[] | undefined = projectReferences ? [] : undefined; const projectReferenceRedirects: Map = createMap(); - if (projectReferences) { - for (const ref of projectReferences) { - const parsedRef = parseProjectReferenceConfigFile(ref); - resolvedProjectReferences!.push(parsedRef); - if (parsedRef) { - if (parsedRef.commandLine.options.outFile) { - const dtsOutfile = changeExtension(parsedRef.commandLine.options.outFile, ".d.ts"); - processSourceFile(dtsOutfile, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); - } - addProjectReferenceRedirects(parsedRef.commandLine, projectReferenceRedirects); - } - } - } - + const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); const structuralIsReused = tryReuseStructureFromOldProgram(); if (structuralIsReused !== StructureIsReused.Completely) { processingDefaultLibFiles = []; processingOtherFiles = []; + + if (projectReferences) { + for (const ref of projectReferences) { + const parsedRef = parseProjectReferenceConfigFile(ref); + resolvedProjectReferences!.push(parsedRef); + if (parsedRef) { + if (parsedRef.commandLine.options.outFile) { + const dtsOutfile = changeExtension(parsedRef.commandLine.options.outFile, ".d.ts"); + processSourceFile(dtsOutfile, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + } + addProjectReferenceRedirects(parsedRef.commandLine, projectReferenceRedirects); + } + } + } + forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false)); // load type declarations specified via 'types' argument or implicitly from types/ and node_modules/@types folders From 11df004c2c6587b69e4c65565f6b61df7a503898 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 16:20:20 -0700 Subject: [PATCH 41/48] Include all lib files in tsbuild harness --- src/harness/unittests/tsbuild.ts | 8 ++++++++ tests/baselines/reference/outfile-concat-fileListing.txt | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 244f8c43fb0..f076e0b34ab 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -391,6 +391,14 @@ namespace ts { const rootPath = resolvePath(__dirname, root); loadFsMirror(fs, rootPath, "/src"); fs.mkdirpSync("/lib"); + const libs = ["es5", "dom", "webworker.importscripts", "scripthost"]; + for (const lib of libs) { + const content = Harness.IO.readFile(combinePaths(Harness.libFolder, `lib.${lib}.d.ts`)); + if (content === undefined) { + throw new Error(`Failed to read lib ${lib}`); + } + fs.writeFileSync(`/lib/lib.${lib}.d.ts`, content); + } fs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))!); fs.meta.set("defaultLibLocation", "/lib"); fs.makeReadonly(); diff --git a/tests/baselines/reference/outfile-concat-fileListing.txt b/tests/baselines/reference/outfile-concat-fileListing.txt index 3fca9338912..fc6a1e7b28c 100644 --- a/tests/baselines/reference/outfile-concat-fileListing.txt +++ b/tests/baselines/reference/outfile-concat-fileListing.txt @@ -1,6 +1,10 @@ */ /lib/ /lib/lib.d.ts + /lib/lib.dom.d.ts + /lib/lib.es5.d.ts + /lib/lib.scripthost.d.ts + /lib/lib.webworker.importscripts.d.ts /src/ /src/2/ /src/2/second-output.d.ts From 856fc79ae75d194919df738f3d61194c1a0dcbfc Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 16:28:42 -0700 Subject: [PATCH 42/48] Lint --- src/compiler/program.ts | 2 +- src/harness/vfs.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 40a1bdcafff..b08f966864a 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -618,7 +618,7 @@ namespace ts { // A parallel array to projectReferences storing the results of reading in the referenced tsconfig files const resolvedProjectReferences: (ResolvedProjectReference | undefined)[] | undefined = projectReferences ? [] : undefined; const projectReferenceRedirects: Map = createMap(); - + const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); const structuralIsReused = tryReuseStructureFromOldProgram(); if (structuralIsReused !== StructureIsReused.Completely) { diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 0e16b0fe715..2c1c6f2dd54 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -1155,7 +1155,7 @@ namespace vfs { EROFS: "file system is read-only" }); - export function createIOError(code: keyof typeof IOErrorMessages, details: string = "") { + export function createIOError(code: keyof typeof IOErrorMessages, details = "") { const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]} ${details}`); err.code = code; if (Error.captureStackTrace) Error.captureStackTrace(err, createIOError); From 291289f8c2abef4c23b2b9ecf366479c38d6c476 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 6 Jun 2018 13:03:03 -0700 Subject: [PATCH 43/48] Update messages --- src/compiler/diagnosticMessages.json | 20 ++++++++++---------- src/compiler/tsbuild.ts | 25 +++++++++++-------------- src/harness/unittests/tsbuild.ts | 20 ++++++++++---------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3aa9ea2b174..f9106b721e2 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3633,19 +3633,19 @@ "code": 6352 }, - "Project '{0}' is up to date with its upstream types": { + "Project '{0}' is up to date with .d.ts files from its dependencies": { "category": "Message", "code": 6353 }, - "Sorted list of input projects: {0}": { + "Projects in this build: {0}": { "category": "Message", "code": 6354 }, - "Would delete the following files:{0}": { + "A non-dry build would delete the following files: {0}": { "category": "Message", "code": 6355 }, - "Would build project '{0}'": { + "A non-dry build would build project '{0}'": { "category": "Message", "code": 6356 }, @@ -3657,7 +3657,7 @@ "category": "Message", "code": 6358 }, - "Project '{0}' is up to date because it was previously built": { + "delete this - Project '{0}' is up to date because it was previously built": { "category": "Message", "code": 6359 }, @@ -3665,15 +3665,15 @@ "category": "Message", "code": 6360 }, - "Skipping build of project '{0}' because its upstream project '{1}' has errors": { + "Skipping build of project '{0}' because its dependency '{1}' has errors": { "category": "Message", "code": 6361 }, - "Project '{0}' can't be built because it depends on a project with errors": { + "Project '{0}' can't be built because its dependency '{1}' has errors": { "category": "Message", "code": 6362 }, - "Build one or more projects and their dependencies, if out-of-date": { + "Build one or more projects and their dependencies, if out of date": { "category": "Message", "code": 6363 }, @@ -3685,11 +3685,11 @@ "category": "Message", "code": 6365 }, - "Show what would be built (or deleted, if specified with --clean)": { + "Show what would be built (or deleted, if specified with '--clean')": { "category": "Message", "code": 6366 }, - "Build all projects, including those that appear to be up-to-date": { + "Build all projects, including those that appear to be up to date": { "category": "Message", "code": 6367 }, diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 974edd6c60c..e91c23d02d4 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -641,7 +641,7 @@ namespace ts { reportProjectStatus(next, status); if (status.type === UpToDateStatusType.UpstreamBlocked) { - context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors, resolved, status.upstreamProjectName); + context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); continue; } @@ -859,7 +859,7 @@ namespace ts { function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_build_project_0, proj)); return BuildResultFlags.Success; } @@ -948,7 +948,7 @@ namespace ts { function updateOutputTimestamps(proj: ParsedCommandLine) { if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj.options.configFilePath)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath)); return; } @@ -1013,7 +1013,7 @@ namespace ts { } if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); } else { if (!host.deleteFile) { @@ -1083,7 +1083,7 @@ namespace ts { } if (status.type === UpToDateStatusType.UpstreamBlocked) { - context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors, projName, status.upstreamProjectName); + context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName); continue; } @@ -1101,7 +1101,7 @@ namespace ts { for (const name of graph.buildQueue) { names.push(name); } - context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join("")); + context.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + s).join("")); } /** @@ -1123,22 +1123,19 @@ namespace ts { if (status.newestInputFileTime !== undefined) { context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestInputFileTime, status.newestOutputFileTime); } - else { - context.verbose(Diagnostics.Project_0_is_up_to_date_because_it_was_previously_built, configFileName); - } + // Don't report anything for "up to date because it was already built" -- too verbose return; case UpToDateStatusType.UpToDateWithUpstreamTypes: - context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); + context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, configFileName); return; case UpToDateStatusType.UpstreamOutOfDate: - context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); + context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, configFileName); return; case UpToDateStatusType.UpstreamBlocked: - context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); + context.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, configFileName, status.upstreamProjectName); return; case UpToDateStatusType.Unbuildable: - // TODO different error - context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); + context.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, configFileName, status.reason); return; default: assertTypeIsNever(status); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index f076e0b34ab..ff80a5b96af 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -35,7 +35,7 @@ namespace ts { const host = new fakes.CompilerHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); + assertDiagnosticMessages(Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0); // Check for outputs to not be written. Not an exhaustive list for (const output of allExpectedOutputs) { @@ -116,7 +116,7 @@ namespace ts { clearDiagnostics(); builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, @@ -131,7 +131,7 @@ namespace ts { clearDiagnostics(); builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); @@ -145,7 +145,7 @@ namespace ts { builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, @@ -160,12 +160,12 @@ namespace ts { builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, Diagnostics.Building_project_0, - Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, Diagnostics.Updating_output_timestamps_of_project_0, - Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, Diagnostics.Updating_output_timestamps_of_project_0); }); }); @@ -182,14 +182,14 @@ namespace ts { replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); builder.buildAllProjects(); assertDiagnosticMessages( - Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0, Diagnostics.Property_0_does_not_exist_on_type_1, - Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, - Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors + Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors ); }); }); From b107849a3a2f67df1f2d620835825c94aac888bd Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 6 Jun 2018 13:47:59 -0700 Subject: [PATCH 44/48] Improve handling of container projects and issue relative filename messages --- src/compiler/diagnosticMessages.json | 44 ++++++----- src/compiler/tsbuild.ts | 106 ++++++++++++++++++++------- 2 files changed, 103 insertions(+), 47 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f9106b721e2..668f3b5a6f0 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3632,78 +3632,82 @@ "category": "Message", "code": 6352 }, - - "Project '{0}' is up to date with .d.ts files from its dependencies": { + "Project '{0}' is out of date because its dependency '{1}' is out of date": { "category": "Message", "code": 6353 }, - "Projects in this build: {0}": { + + "Project '{0}' is up to date with .d.ts files from its dependencies": { "category": "Message", "code": 6354 }, - "A non-dry build would delete the following files: {0}": { + "Projects in this build: {0}": { "category": "Message", "code": 6355 }, - "A non-dry build would build project '{0}'": { + "A non-dry build would delete the following files: {0}": { "category": "Message", "code": 6356 }, - "Building project '{0}'...": { + "A non-dry build would build project '{0}'": { "category": "Message", "code": 6357 }, - "Updating output timestamps of project '{0}'...": { + "Building project '{0}'...": { "category": "Message", "code": 6358 }, - "delete this - Project '{0}' is up to date because it was previously built": { + "Updating output timestamps of project '{0}'...": { "category": "Message", "code": 6359 }, - "Project '{0}' is up to date": { + "delete this - Project '{0}' is up to date because it was previously built": { "category": "Message", "code": 6360 }, - "Skipping build of project '{0}' because its dependency '{1}' has errors": { + "Project '{0}' is up to date": { "category": "Message", "code": 6361 }, - "Project '{0}' can't be built because its dependency '{1}' has errors": { + "Skipping build of project '{0}' because its dependency '{1}' has errors": { "category": "Message", "code": 6362 }, - "Build one or more projects and their dependencies, if out of date": { + "Project '{0}' can't be built because its dependency '{1}' has errors": { "category": "Message", "code": 6363 }, - "Delete the outputs of all projects": { + "Build one or more projects and their dependencies, if out of date": { "category": "Message", "code": 6364 }, - "Enable verbose logging": { + "Delete the outputs of all projects": { "category": "Message", "code": 6365 }, - "Show what would be built (or deleted, if specified with '--clean')": { + "Enable verbose logging": { "category": "Message", "code": 6366 }, - "Build all projects, including those that appear to be up to date": { + "Show what would be built (or deleted, if specified with '--clean')": { "category": "Message", "code": 6367 }, - "Option '--build' must be the first command line argument.": { - "category": "Error", + "Build all projects, including those that appear to be up to date": { + "category": "Message", "code": 6368 }, - "Options '{0}' and '{1}' cannot be combined.": { + "Option '--build' must be the first command line argument.": { "category": "Error", "code": 6369 }, + "Options '{0}' and '{1}' cannot be combined.": { + "category": "Error", + "code": 6370 + }, "Skipping clean because not all projects could be located": { "category": "Error", - "code": 6340 + "code": 6371 }, "Variable '{0}' implicitly has an '{1}' type.": { diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index e91c23d02d4..db998d651c3 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -90,7 +90,12 @@ namespace ts { OutOfDateWithSelf, OutOfDateWithUpstream, UpstreamOutOfDate, - UpstreamBlocked + UpstreamBlocked, + + /** + * Projects with no outputs (i.e. "solution" files) + */ + ContainerOnly } export type UpToDateStatus = @@ -100,7 +105,8 @@ namespace ts { | Status.OutOfDateWithSelf | Status.OutOfDateWithUpstream | Status.UpstreamOutOfDate - | Status.UpstreamBlocked; + | Status.UpstreamBlocked + | Status.ContainerOnly; export namespace Status { /** @@ -112,6 +118,13 @@ namespace ts { reason: string; } + /** + * This project doesn't have any outputs, so "is it up to date" is a meaningless question. + */ + export interface ContainerOnly { + type: UpToDateStatusType.ContainerOnly; + } + /** * The project is up to date with respect to its inputs. * We track what the newest input file is. @@ -119,8 +132,11 @@ namespace ts { export interface UpToDate { type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes; newestInputFileTime: Date; + newestInputFileName: string; newestDeclarationFileContentChangedTime: Date; newestOutputFileTime: Date; + newestOutputFileName: string; + oldestOutputFileName: string; } /** @@ -684,12 +700,19 @@ namespace ts { // Collect the expected outputs of this project const outputs = getAllProjectOutputs(project); + if (outputs.length === 0) { + return { + type: UpToDateStatusType.ContainerOnly + }; + } + // Now see if all outputs are newer than the newest input - let oldestOutputFileName: string | undefined; - let oldestOutputFileTime: Date = maximumDate; - let newestOutputFileTime: Date = minimumDate; - let newestDeclarationFileContentChangedTime: Date = minimumDate; + let oldestOutputFileName = "(none)"; + let oldestOutputFileTime = maximumDate; + let newestOutputFileName = "(none)"; + let newestOutputFileTime = minimumDate; let missingOutputFileName: string | undefined; + let newestDeclarationFileContentChangedTime = minimumDate; let isOutOfDateWithInputs = false; for (const output of outputs) { // Output is missing; can stop checking @@ -712,7 +735,10 @@ namespace ts { break; } - newestOutputFileTime = newer(newestOutputFileTime, outputTime); + if (outputTime > newestOutputFileTime) { + newestOutputFileTime = outputTime; + newestOutputFileName = output; + } // 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 @@ -768,7 +794,7 @@ namespace ts { Debug.assert(oldestOutputFileName !== undefined, "Should have an oldest output filename here"); return { type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: oldestOutputFileName!, + outOfDateOutputFileName: oldestOutputFileName, newerProjectName: ref.path }; } @@ -784,7 +810,7 @@ namespace ts { if (isOutOfDateWithInputs) { return { type: UpToDateStatusType.OutOfDateWithSelf, - outOfDateOutputFileName: oldestOutputFileName!, + outOfDateOutputFileName: oldestOutputFileName, newerInputFileName: newestInputFileName }; } @@ -794,7 +820,10 @@ namespace ts { type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime, newestInputFileTime, - newestOutputFileTime + newestOutputFileTime, + newestInputFileName, + newestOutputFileName, + oldestOutputFileName }; } @@ -1087,6 +1116,11 @@ namespace ts { continue; } + if (status.type === UpToDateStatusType.ContainerOnly) { + // Do nothing + continue; + } + buildSingleProject(next); } } @@ -1101,7 +1135,11 @@ namespace ts { for (const name of graph.buildQueue) { names.push(name); } - context.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + s).join("")); + context.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + relName(s)).join("")); + } + + function relName(path: string): string { + return convertToRelativePath(path, host.getCurrentDirectory(), f => host.getCanonicalFileName(f)); } /** @@ -1111,32 +1149,46 @@ namespace ts { if (!context.options.verbose) return; switch (status.type) { case UpToDateStatusType.OutOfDateWithSelf: - context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, configFileName, status.outOfDateOutputFileName, status.newerInputFileName); - return; + return context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + relName(configFileName), + relName(status.outOfDateOutputFileName), + relName(status.newerInputFileName)); case UpToDateStatusType.OutOfDateWithUpstream: - context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, configFileName, status.outOfDateOutputFileName, status.newerProjectName); - return; + return context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + relName(configFileName), + relName(status.outOfDateOutputFileName), + relName(status.newerProjectName)); case UpToDateStatusType.OutputMissing: - context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFileName, status.missingOutputFileName); - return; + return context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + relName(configFileName), + relName(status.missingOutputFileName)); case UpToDateStatusType.UpToDate: if (status.newestInputFileTime !== undefined) { - context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestInputFileTime, status.newestOutputFileTime); + return context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + relName(configFileName), + relName(status.newestInputFileName), + relName(status.oldestOutputFileName)); } // Don't report anything for "up to date because it was already built" -- too verbose - return; + break; case UpToDateStatusType.UpToDateWithUpstreamTypes: - context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, configFileName); - return; + return context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, + relName(configFileName)); case UpToDateStatusType.UpstreamOutOfDate: - context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, configFileName); - return; + return context.verbose(Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, + relName(configFileName), + relName(status.upstreamProjectName)); case UpToDateStatusType.UpstreamBlocked: - context.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, configFileName, status.upstreamProjectName); - return; + return context.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + relName(configFileName), + relName(status.upstreamProjectName)); case UpToDateStatusType.Unbuildable: - context.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, configFileName, status.reason); - return; + return context.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, + relName(configFileName), + status.reason); + case UpToDateStatusType.ContainerOnly: + // Don't report status on "solution" projects + break; default: assertTypeIsNever(status); } From 21a65f5dc03d619b58d98a9e2cb0387720f73d96 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 6 Jun 2018 16:47:15 -0700 Subject: [PATCH 45/48] Improved watch mode --- src/compiler/tsbuild.ts | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index db998d651c3..c041540cdfe 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -341,8 +341,6 @@ namespace ts { const cache = createFileMap(); const configParseHost = parseConfigHostFromCompilerHost(host); - // TODO: Cache invalidation under --watch - function parseConfigFile(configFilePath: ResolvedConfigFileName) { const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; if (sourceFile === undefined) { @@ -529,6 +527,8 @@ namespace ts { const configFileCache = createConfigFileCache(host); let context = createBuildContext(defaultOptions, reportDiagnostic); + const existingWatchersForWildcards = createMap(); + return { buildAllProjects, getUpToDateStatus, @@ -551,18 +551,43 @@ namespace ts { if (!system.watchFile || !system.watchDirectory || !system.setTimeout) throw new Error("System host must support watchFile / watchDirectory / setTimeout if using --watch"); const graph = getGlobalDependencyGraph()!; + if (!graph.buildQueue) { + // Everything is broken - we don't even know what to watch. Give up. + return; + } + for (const resolved of graph.buildQueue) { const cfg = configFileCache.parseConfigFile(resolved); if (cfg) { + // Watch this file + system.watchFile!(resolved, () => { + configFileCache.removeKey(resolved); + invalidateProjectAndScheduleBuilds(resolved); + }); + + // Update watchers for wildcard directories + if (cfg.configFileSpecs) { + updateWatchingWildcardDirectories(existingWatchersForWildcards, createMapFromTemplate(cfg.configFileSpecs.wildcardDirectories), (dir, flags) => { + return system.watchDirectory!(dir, () => { + invalidateProjectAndScheduleBuilds(resolved); + }, !!(flags & WatchDirectoryFlags.Recursive)); + }); + } + + // Watch input files for (const input of cfg.fileNames) { system.watchFile(input, () => { - invalidateProject(resolved); - system.setTimeout!(buildInvalidatedProjects, 100); - system.setTimeout!(buildDependentInvalidatedProjects, 3000); + invalidateProjectAndScheduleBuilds(resolved); }); } } } + + function invalidateProjectAndScheduleBuilds(resolved: ResolvedConfigFileName) { + invalidateProject(resolved); + system!.setTimeout!(buildInvalidatedProjects, 100); + system!.setTimeout!(buildDependentInvalidatedProjects, 3000); + } } function resetBuildContext(opts = defaultOptions) { From 1de2f839f2f39f8fb6c185a2f4cb5be9173c2e34 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 8 Jun 2018 17:43:16 -0700 Subject: [PATCH 46/48] PR fixups --- src/compiler/tsbuild.ts | 174 +++++++++++++++---------------- src/compiler/tsc.ts | 10 +- src/harness/unittests/tsbuild.ts | 29 ++++-- 3 files changed, 110 insertions(+), 103 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index c041540cdfe..606fc1a27f6 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -9,6 +9,13 @@ namespace ts { const minimumDate = new Date(-8640000000000000); const maximumDate = new Date(8640000000000000); + export interface BuildHost { + verbose(diag: DiagnosticMessage, ...args: string[]): void; + error(diag: DiagnosticMessage, ...args: string[]): void; + errorDiagnostic(diag: Diagnostic): void; + message(diag: DiagnosticMessage, ...args: string[]): void; + } + /** * A BuildContext tracks what's going on during the course of a build. * @@ -34,11 +41,6 @@ namespace ts { */ projectStatus: FileMap; - /** - * Issue a verbose diagnostic message. No-ops when options.verbose is false. - */ - verbose(diag: DiagnosticMessage, ...args: any[]): void; - invalidatedProjects: FileMap; queuedProjects: FileMap; missingRoots: Map; @@ -371,9 +373,7 @@ namespace ts { return fileExtensionIs(fileName, ".d.ts"); } - export function createBuildContext(options: BuildOptions, reportDiagnostic: DiagnosticReporter): BuildContext { - const verboseDiag = options.verbose && reportDiagnostic; - + export function createBuildContext(options: BuildOptions): BuildContext { const invalidatedProjects = createFileMap(); const queuedProjects = createFileMap(); const missingRoots = createMap(); @@ -382,7 +382,6 @@ namespace ts { options, projectStatus: createFileMap(), unchangedOutputs: createFileMap(), - verbose: verboseDiag ? (diag, ...args) => verboseDiag(createCompilerDiagnostic(diag, ...args)) : () => undefined, invalidatedProjects, missingRoots, queuedProjects @@ -425,7 +424,7 @@ namespace ts { } ]; - export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[], system?: System) { + export function performBuild(args: string[], compilerHost: CompilerHost, buildHost: BuildHost, system?: System) { let verbose = false; let dry = false; let force = false; @@ -466,20 +465,16 @@ namespace ts { // Nonsensical combinations if (clean && force) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); - return; + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force"); } if (clean && verbose) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); - return; + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose"); } if (clean && watch) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); - return; + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch"); } if (watch && dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); - return; + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry"); } if (projects.length === 0) { @@ -487,7 +482,7 @@ namespace ts { addProject("."); } - const builder = createSolutionBuilder(host, projects, reportDiagnostic, { verbose, dry, force }, system); + const builder = createSolutionBuilder(compilerHost, buildHost, projects, { dry, force, verbose }, system); if (clean) { builder.cleanAllProjects(); } @@ -500,15 +495,14 @@ namespace ts { } function addProject(projectSpecification: string) { - const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification); - const refPath = resolveProjectReferencePath(host, { path: fileName }); + const fileName = resolvePath(compilerHost.getCurrentDirectory(), projectSpecification); + const refPath = resolveProjectReferencePath(compilerHost, { path: fileName }); if (!refPath) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, projectSpecification)); - return; + return buildHost.error(Diagnostics.File_0_does_not_exist, projectSpecification); } - if (!host.fileExists(refPath)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); + if (!compilerHost.fileExists(refPath)) { + return buildHost.error(Diagnostics.File_0_does_not_exist, fileName); } projects.push(refPath); @@ -519,13 +513,13 @@ namespace ts { * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but * can dynamically add/remove other projects based on changes on the rootNames' references */ - export function createSolutionBuilder(host: CompilerHost, rootNames: ReadonlyArray, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions, system?: System) { - if (!host.getModifiedTime || !host.setModifiedTime) { + export function createSolutionBuilder(compilerHost: CompilerHost, buildHost: BuildHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions, system?: System) { + if (!compilerHost.getModifiedTime || !compilerHost.setModifiedTime) { throw new Error("Host must support timestamp APIs"); } - const configFileCache = createConfigFileCache(host); - let context = createBuildContext(defaultOptions, reportDiagnostic); + const configFileCache = createConfigFileCache(compilerHost); + let context = createBuildContext(defaultOptions); const existingWatchersForWildcards = createMap(); @@ -560,7 +554,7 @@ namespace ts { const cfg = configFileCache.parseConfigFile(resolved); if (cfg) { // Watch this file - system.watchFile!(resolved, () => { + system.watchFile(resolved, () => { configFileCache.removeKey(resolved); invalidateProjectAndScheduleBuilds(resolved); }); @@ -573,7 +567,7 @@ namespace ts { }, !!(flags & WatchDirectoryFlags.Recursive)); }); } - + // Watch input files for (const input of cfg.fileNames) { system.watchFile(input, () => { @@ -591,7 +585,7 @@ namespace ts { } function resetBuildContext(opts = defaultOptions) { - context = createBuildContext(opts, reportDiagnostic); + context = createBuildContext(opts); } function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { @@ -679,10 +673,10 @@ namespace ts { if (!proj) continue; // ? const status = getUpToDateStatus(proj); - reportProjectStatus(next, status); + verboseReportProjectStatus(next, status); if (status.type === UpToDateStatusType.UpstreamBlocked) { - context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); + if (context.options.verbose) buildHost.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); continue; } @@ -708,14 +702,14 @@ namespace ts { let newestInputFileTime = minimumDate; // Get timestamps of input files for (const inputFile of project.fileNames) { - if (!host.fileExists(inputFile)) { + if (!compilerHost.fileExists(inputFile)) { return { type: UpToDateStatusType.Unbuildable, reason: `${inputFile} does not exist` }; } - const inputTime = host.getModifiedTime!(inputFile); + const inputTime = compilerHost.getModifiedTime!(inputFile); if (inputTime > newestInputFileTime) { newestInputFileName = inputFile; newestInputFileTime = inputTime; @@ -742,12 +736,12 @@ namespace ts { for (const output of outputs) { // Output is missing; can stop checking // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status - if (!host.fileExists(output)) { + if (!compilerHost.fileExists(output)) { missingOutputFileName = output; break; } - const outputTime = host.getModifiedTime!(output); + const outputTime = compilerHost.getModifiedTime!(output); if (outputTime < oldestOutputFileTime) { oldestOutputFileTime = outputTime; oldestOutputFileName = output; @@ -775,7 +769,7 @@ namespace ts { newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); } else { - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, host.getModifiedTime!(output)); + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, compilerHost.getModifiedTime!(output)); } } } @@ -783,7 +777,7 @@ namespace ts { let pseudoUpToDate = false; if (project.projectReferences) { for (const ref of project.projectReferences) { - const resolvedRef = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName; + const resolvedRef = resolveProjectReferencePath(compilerHost, ref) as ResolvedConfigFileName; const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); // An upstream project is blocked @@ -881,7 +875,7 @@ namespace ts { if (temporaryMarks[projPath]) { if (!inCircularContext) { hadError = true; - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n"))); + buildHost.error(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")); return; } } @@ -913,11 +907,11 @@ namespace ts { function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_build_project_0, proj)); + buildHost.message(Diagnostics.A_non_dry_build_would_build_project_0, proj); return BuildResultFlags.Success; } - context.verbose(Diagnostics.Building_project_0, proj); + if (context.options.verbose) buildHost.verbose(Diagnostics.Building_project_0, proj); let resultFlags = BuildResultFlags.None; resultFlags |= BuildResultFlags.DeclarationOutputUnchanged; @@ -937,7 +931,7 @@ namespace ts { const programOptions: CreateProgramOptions = { projectReferences: configFile.projectReferences, - host, + host: compilerHost, rootNames: configFile.fileNames, options: configFile.options }; @@ -948,7 +942,7 @@ namespace ts { if (syntaxDiagnostics.length) { resultFlags |= BuildResultFlags.SyntaxErrors; for (const diag of syntaxDiagnostics) { - reportDiagnostic(diag); + buildHost.errorDiagnostic(diag); } context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Syntactic errors" }); return resultFlags; @@ -960,18 +954,19 @@ namespace ts { if (declDiagnostics.length) { resultFlags |= BuildResultFlags.DeclarationEmitErrors; for (const diag of declDiagnostics) { - reportDiagnostic(diag); + buildHost.errorDiagnostic(diag); } context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Declaration file errors" }); return resultFlags; } } - const semanticDiagnostics = [...program.getSemanticDiagnostics()]; + // Same as above but now for semantic diagnostics + const semanticDiagnostics = program.getSemanticDiagnostics(); if (semanticDiagnostics.length) { resultFlags |= BuildResultFlags.TypeErrors; for (const diag of semanticDiagnostics) { - reportDiagnostic(diag); + buildHost.errorDiagnostic(diag); } context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Semantic errors" }); return resultFlags; @@ -981,15 +976,15 @@ namespace ts { program.emit(/*targetSourceFile*/ undefined, (fileName, content, writeBom, onError) => { let priorChangeTime: Date | undefined; - if (isDeclarationFile(fileName) && host.fileExists(fileName)) { - if (host.readFile(fileName) === content) { + if (isDeclarationFile(fileName) && compilerHost.fileExists(fileName)) { + if (compilerHost.readFile(fileName) === content) { // Check for unchanged .d.ts files resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - priorChangeTime = host.getModifiedTime && host.getModifiedTime(fileName); + priorChangeTime = compilerHost.getModifiedTime && compilerHost.getModifiedTime(fileName); } } - host.writeFile(fileName, content, writeBom, onError, emptyArray); + compilerHost.writeFile(fileName, content, writeBom, onError, emptyArray); if (priorChangeTime !== undefined) { newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); context.unchangedOutputs.setValue(fileName, priorChangeTime); @@ -1002,19 +997,18 @@ namespace ts { function updateOutputTimestamps(proj: ParsedCommandLine) { if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath)); - return; + return buildHost.message(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath!); } - context.verbose(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath); + if (context.options.verbose) buildHost.verbose(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath!); const now = new Date(); const outputs = getAllProjectOutputs(proj); let priorNewestUpdateTime = minimumDate; for (const file of outputs) { if (isDeclarationFile(file)) { - priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime!(file)); + priorNewestUpdateTime = newer(priorNewestUpdateTime, compilerHost.getModifiedTime!(file)); } - host.setModifiedTime!(file, now); + compilerHost.setModifiedTime!(file, now); } context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); @@ -1037,7 +1031,7 @@ namespace ts { } const outputs = getAllProjectOutputs(parsed); for (const output of outputs) { - if (host.fileExists(output)) { + if (compilerHost.fileExists(output)) { filesToDelete.push(output); } } @@ -1056,40 +1050,38 @@ namespace ts { function cleanAllProjects() { const resolvedNames: ReadonlyArray | undefined = getAllProjectsInScope(); if (resolvedNames === undefined) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located)); - return; + return buildHost.message(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located); } const filesToDelete = getFilesToClean(resolvedNames); if (filesToDelete === undefined) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located)); - return; + return buildHost.message(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located); } if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); + return buildHost.message(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); } - else { - if (!host.deleteFile) { - throw new Error("Host does not support deleting files"); - } - for (const output of filesToDelete) { - host.deleteFile(output); - } + // Do this check later to allow --clean --dry to function even if the host can't delete files + if (!compilerHost.deleteFile) { + throw new Error("Host does not support deleting files"); + } + + for (const output of filesToDelete) { + compilerHost.deleteFile(output); } } function resolveProjectName(name: string): ResolvedConfigFileName | undefined { - let fullPath = resolvePath(host.getCurrentDirectory(), name); - if (host.fileExists(fullPath)) { + const fullPath = resolvePath(compilerHost.getCurrentDirectory(), name); + if (compilerHost.fileExists(fullPath)) { return fullPath as ResolvedConfigFileName; } - fullPath = combinePaths(fullPath, "tsconfig.json"); - if (host.fileExists(fullPath)) { - return fullPath as ResolvedConfigFileName; + const fullPathWithTsconfig = combinePaths(fullPath, "tsconfig.json"); + if (compilerHost.fileExists(fullPathWithTsconfig)) { + return fullPathWithTsconfig as ResolvedConfigFileName; } - reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); + buildHost.error(Diagnostics.File_0_not_found, relName(fullPath)); return undefined; } @@ -1118,14 +1110,14 @@ namespace ts { break; } const status = getUpToDateStatus(proj); - reportProjectStatus(next, status); + verboseReportProjectStatus(next, status); - const projName = proj.options.configFilePath; + const projName = proj.options.configFilePath!; if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { // Up to date, skip if (defaultOptions.dry) { // In a dry build, inform the user of this fact - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date, projName)); + buildHost.message(Diagnostics.Project_0_is_up_to_date, projName); } continue; } @@ -1137,7 +1129,7 @@ namespace ts { } if (status.type === UpToDateStatusType.UpstreamBlocked) { - context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName); + if (context.options.verbose) buildHost.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName); continue; } @@ -1160,36 +1152,36 @@ namespace ts { for (const name of graph.buildQueue) { names.push(name); } - context.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + relName(s)).join("")); + if (context.options.verbose) buildHost.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + relName(s)).join("")); } function relName(path: string): string { - return convertToRelativePath(path, host.getCurrentDirectory(), f => host.getCanonicalFileName(f)); + return convertToRelativePath(path, compilerHost.getCurrentDirectory(), f => compilerHost.getCanonicalFileName(f)); } /** * Report the up-to-date status of a project if we're in verbose mode */ - function reportProjectStatus(configFileName: string, status: UpToDateStatus) { + function verboseReportProjectStatus(configFileName: string, status: UpToDateStatus) { if (!context.options.verbose) return; switch (status.type) { case UpToDateStatusType.OutOfDateWithSelf: - return context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relName(configFileName), relName(status.outOfDateOutputFileName), relName(status.newerInputFileName)); case UpToDateStatusType.OutOfDateWithUpstream: - return context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relName(configFileName), relName(status.outOfDateOutputFileName), relName(status.newerProjectName)); case UpToDateStatusType.OutputMissing: - return context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, relName(configFileName), relName(status.missingOutputFileName)); case UpToDateStatusType.UpToDate: if (status.newestInputFileTime !== undefined) { - return context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + return buildHost.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, relName(configFileName), relName(status.newestInputFileName), relName(status.oldestOutputFileName)); @@ -1197,18 +1189,18 @@ namespace ts { // Don't report anything for "up to date because it was already built" -- too verbose break; case UpToDateStatusType.UpToDateWithUpstreamTypes: - return context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, + return buildHost.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, relName(configFileName)); case UpToDateStatusType.UpstreamOutOfDate: - return context.verbose(Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, relName(configFileName), relName(status.upstreamProjectName)); case UpToDateStatusType.UpstreamBlocked: - return context.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + return buildHost.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, relName(configFileName), relName(status.upstreamProjectName)); case UpToDateStatusType.Unbuildable: - return context.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, + return buildHost.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, relName(configFileName), status.reason); case UpToDateStatusType.ContainerOnly: diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 149439d9b67..93834a6ea54 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -50,7 +50,15 @@ namespace ts { export function executeCommandLine(args: string[]): void { if (args.length > 0 && ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b"))) { - return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1), sys); + const reportDiag = createDiagnosticReporter(sys, /*pretty*/ true); + const report = (message: DiagnosticMessage, ...args: string[]) => reportDiag(createCompilerDiagnostic(message, ...args)); + const buildHost: BuildHost = { + error: report, + verbose: report, + message: report, + errorDiagnostic: d => reportDiag(d) + }; + return performBuild(args.slice(1), createCompilerHost({}), buildHost, sys); } const commandLine = parseCommandLine(args); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index ff80a5b96af..9d093f61e09 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -2,6 +2,13 @@ namespace ts { let currentTime = 100; let lastDiagnostics: Diagnostic[] = []; const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); + const report = (message: DiagnosticMessage, ...args: string[]) => reportDiagnostic(createCompilerDiagnostic(message, ...args)); + const buildHost: BuildHost = { + error: report, + verbose: report, + message: report, + errorDiagnostic: d => reportDiagnostic(d) + }; export namespace Sample1 { tick(); @@ -15,7 +22,7 @@ namespace ts { it("can build the sample project 'sample1' without error", () => { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); clearDiagnostics(); builder.buildAllProjects(); @@ -33,7 +40,7 @@ namespace ts { clearDiagnostics(); const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: true, force: false, verbose: false }); builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0); @@ -48,12 +55,12 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - let builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + let builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); builder.buildAllProjects(); tick(); clearDiagnostics(); - builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: true, force: false, verbose: false }); builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); }); @@ -65,7 +72,7 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); builder.buildAllProjects(); // Verify they exist for (const output of allExpectedOutputs) { @@ -86,7 +93,7 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: true, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: true, verbose: false }); builder.buildAllProjects(); let currentTime = time(); checkOutputTimestamps(currentTime); @@ -110,7 +117,7 @@ namespace ts { describe("tsbuild - can detect when and what to rebuild", () => { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: true }); it("Builds the project", () => { clearDiagnostics(); @@ -174,7 +181,7 @@ namespace ts { it("won't build downstream projects if upstream projects have errors", () => { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: true }); clearDiagnostics(); @@ -198,7 +205,7 @@ namespace ts { it("invalidates projects correctly", () => { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); clearDiagnostics(); builder.buildAllProjects(); @@ -234,7 +241,7 @@ namespace ts { describe("tsbuild - baseline sectioned sourcemaps", () => { const fs = outFileFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/third"], reportDiagnostic, { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/third"], { dry: false, force: false, verbose: false }); clearDiagnostics(); builder.buildAllProjects(); assertDiagnosticMessages(/*none*/); @@ -292,7 +299,7 @@ namespace ts { }); function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { - const builder = createSolutionBuilder(host, rootNames, reportDiagnostic, { dry: true, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, rootNames, { dry: true, force: false, verbose: false }); const projFileNames = rootNames.map(getProjectFileName); const graph = builder.getBuildGraph(projFileNames); From b97bc8e07112d43f624eb31c38c9001415fe5ee0 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Sat, 9 Jun 2018 09:15:17 -0700 Subject: [PATCH 47/48] Use native map --- src/compiler/tsbuild.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 606fc1a27f6..e13166d3099 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -202,7 +202,7 @@ namespace ts { */ function createFileMap(): FileMap { // tslint:disable-next-line:no-null-keyword - const lookup: { [key: string]: T } = Object.create(/*prototype*/ null); + const lookup = createMap(); return { setValue, @@ -218,21 +218,21 @@ namespace ts { } function hasKey(fileName: string) { - return normalizePath(fileName) in lookup; + return lookup.has(normalizePath(fileName)); } function removeKey(fileName: string) { - delete lookup[fileName]; + lookup.delete(normalizePath(fileName)); } function setValue(fileName: string, value: T) { - lookup[normalizePath(fileName)] = value; + lookup.set(normalizePath(fileName), value); } function getValue(fileName: string): T | never { const f = normalizePath(fileName); - if (f in lookup) { - return lookup[f]; + if (lookup.has(f)) { + return lookup.get(f)!; } else { throw new Error(`No value corresponding to ${fileName} exists in this map`); @@ -241,12 +241,7 @@ namespace ts { function getValueOrUndefined(fileName: string): T | undefined { const f = normalizePath(fileName); - if (f in lookup) { - return lookup[f]; - } - else { - return undefined; - } + return lookup.get(f); } } From 0f626fdcb3026452fe1ca333085c2b720add2ee6 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Sat, 9 Jun 2018 09:43:25 -0700 Subject: [PATCH 48/48] Last round PR comments --- src/compiler/tsbuild.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index e13166d3099..79306cc5d06 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -246,9 +246,9 @@ namespace ts { } export function createDependencyMapper() { - const childToParents: { [key: string]: ResolvedConfigFileName[] } = {}; - const parentToChildren: { [key: string]: ResolvedConfigFileName[] } = {}; - const allKeys: ResolvedConfigFileName[] = []; + const childToParents = createFileMap(); + const parentToChildren = createFileMap(); + const allKeys = createFileMap(); function addReference(childConfigFileName: ResolvedConfigFileName, parentConfigFileName: ResolvedConfigFileName): void { addEntry(childToParents, childConfigFileName, parentConfigFileName); @@ -256,26 +256,29 @@ namespace ts { } function getReferencesTo(parentConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] { - return parentToChildren[normalizePath(parentConfigFileName)] || []; + return parentToChildren.getValueOrUndefined(parentConfigFileName) || []; } function getReferencesOf(childConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] { - return childToParents[normalizePath(childConfigFileName)] || []; + return childToParents.getValueOrUndefined(childConfigFileName) || []; } function getKeys(): ReadonlyArray { - return allKeys; + return allKeys.getKeys() as ResolvedConfigFileName[]; } function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: ResolvedConfigFileName, element: ResolvedConfigFileName) { key = normalizePath(key) as ResolvedConfigFileName; element = normalizePath(element) as ResolvedConfigFileName; - const arr = (mapToAddTo[key] = mapToAddTo[key] || []); + let arr = mapToAddTo.getValueOrUndefined(key); + if (arr === undefined) { + mapToAddTo.setValue(key, arr = []); + } if (arr.indexOf(element) < 0) { arr.push(element); } - if (allKeys.indexOf(key) < 0) allKeys.push(key); - if (allKeys.indexOf(element) < 0) allKeys.push(element); + allKeys.setValue(key, true); + allKeys.setValue(element, true); } return { @@ -289,13 +292,13 @@ namespace ts { function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine) { const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath!), inputFileName, /*ignoreCase*/ true); const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath!), relativePath); - return changeExtension(outputPath, ".d.ts"); + return changeExtension(outputPath, Extension.Dts); } function getOutputJavaScriptFileName(inputFileName: string, configFile: ParsedCommandLine) { const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath!), inputFileName, /*ignoreCase*/ 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"); + return changeExtension(outputPath, (fileExtensionIs(inputFileName, Extension.Tsx) && configFile.options.jsx === JsxEmit.Preserve) ? Extension.Jsx : Extension.Js); } function getOutputFileNames(inputFileName: string, configFile: ParsedCommandLine): ReadonlyArray { @@ -316,12 +319,12 @@ namespace ts { function getOutFileOutputs(project: ParsedCommandLine): ReadonlyArray { if (!project.options.outFile) { - throw new Error("Assert - outFile must be set"); + return Debug.fail("outFile must be set"); } const outputs: string[] = []; outputs.push(project.options.outFile); if (project.options.declaration) { - const dts = changeExtension(project.options.outFile, ".d.ts"); + const dts = changeExtension(project.options.outFile, Extension.Dts); outputs.push(dts); if (project.options.declarationMap) { outputs.push(dts + ".map"); @@ -365,7 +368,7 @@ namespace ts { } function isDeclarationFile(fileName: string) { - return fileExtensionIs(fileName, ".d.ts"); + return fileExtensionIs(fileName, Extension.Dts); } export function createBuildContext(options: BuildOptions): BuildContext {