TypeScript/src/compiler/tsbuild.ts
2018-06-13 10:44:05 -07:00

1219 lines
50 KiB
TypeScript

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"
*/
export type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never };
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.
*
* 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;
/**
* Map from output file name to its pre-build timestamp
*/
unchangedOutputs: FileMap<Date>;
/**
* Map from config file name to up-to-date status
*/
projectStatus: FileMap<UpToDateStatus>;
invalidatedProjects: FileMap<true>;
queuedProjects: FileMap<true>;
missingRoots: Map<true>;
}
type Mapper = ReturnType<typeof createDependencyMapper>;
interface DependencyGraph {
buildQueue: ResolvedConfigFileName[];
dependencyMap: Mapper;
}
interface BuildOptions {
dry: boolean;
force: boolean;
verbose: boolean;
}
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
}
export 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 (.d.ts) inputs.
* This means we can Pseudo-build (just touch timestamps), as if we had actually built this project.
*/
UpToDateWithUpstreamTypes,
OutputMissing,
OutOfDateWithSelf,
OutOfDateWithUpstream,
UpstreamOutOfDate,
UpstreamBlocked,
/**
* Projects with no outputs (i.e. "solution" files)
*/
ContainerOnly
}
export type UpToDateStatus =
| Status.Unbuildable
| Status.UpToDate
| Status.OutputMissing
| Status.OutOfDateWithSelf
| Status.OutOfDateWithUpstream
| Status.UpstreamOutOfDate
| Status.UpstreamBlocked
| Status.ContainerOnly;
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
*/
export interface Unbuildable {
type: UpToDateStatusType.Unbuildable;
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.
*/
export interface UpToDate {
type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes;
newestInputFileTime: Date;
newestInputFileName: string;
newestDeclarationFileContentChangedTime: Date;
newestOutputFileTime: Date;
newestOutputFileName: string;
oldestOutputFileName: 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;
}
/**
* One or more of the project's outputs is older than its newest input.
*/
export interface OutOfDateWithSelf {
type: UpToDateStatusType.OutOfDateWithSelf;
outOfDateOutputFileName: string;
newerInputFileName: 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<T> {
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[];
}
/**
* A FileMap maintains a normalized-key to value relationship
*/
function createFileMap<T>(): FileMap<T> {
// tslint:disable-next-line:no-null-keyword
const lookup = createMap<T>();
return {
setValue,
getValue,
getValueOrUndefined,
removeKey,
getKeys,
hasKey
};
function getKeys(): string[] {
return Object.keys(lookup);
}
function hasKey(fileName: string) {
return lookup.has(normalizePath(fileName));
}
function removeKey(fileName: string) {
lookup.delete(normalizePath(fileName));
}
function setValue(fileName: string, value: T) {
lookup.set(normalizePath(fileName), value);
}
function getValue(fileName: string): T | never {
const f = normalizePath(fileName);
if (lookup.has(f)) {
return lookup.get(f)!;
}
else {
throw new Error(`No value corresponding to ${fileName} exists in this map`);
}
}
function getValueOrUndefined(fileName: string): T | undefined {
const f = normalizePath(fileName);
return lookup.get(f);
}
}
export function createDependencyMapper() {
const childToParents = createFileMap<ResolvedConfigFileName[]>();
const parentToChildren = createFileMap<ResolvedConfigFileName[]>();
const allKeys = createFileMap<true>();
function addReference(childConfigFileName: ResolvedConfigFileName, parentConfigFileName: ResolvedConfigFileName): void {
addEntry(childToParents, childConfigFileName, parentConfigFileName);
addEntry(parentToChildren, parentConfigFileName, childConfigFileName);
}
function getReferencesTo(parentConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] {
return parentToChildren.getValueOrUndefined(parentConfigFileName) || [];
}
function getReferencesOf(childConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] {
return childToParents.getValueOrUndefined(childConfigFileName) || [];
}
function getKeys(): ReadonlyArray<ResolvedConfigFileName> {
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;
let arr = mapToAddTo.getValueOrUndefined(key);
if (arr === undefined) {
mapToAddTo.setValue(key, arr = []);
}
if (arr.indexOf(element) < 0) {
arr.push(element);
}
allKeys.setValue(key, true);
allKeys.setValue(element, true);
}
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);
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, Extension.Tsx) && configFile.options.jsx === JsxEmit.Preserve) ? Extension.Jsx : Extension.Js);
}
function getOutputFileNames(inputFileName: string, configFile: ParsedCommandLine): ReadonlyArray<string> {
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: ParsedCommandLine): ReadonlyArray<string> {
if (!project.options.outFile) {
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, Extension.Dts);
outputs.push(dts);
if (project.options.declarationMap) {
outputs.push(dts + ".map");
}
}
return outputs;
}
function rootDirOfOptions(opts: CompilerOptions, configFileName: string) {
return opts.rootDir || getDirectoryPath(configFileName);
}
function createConfigFileCache(host: CompilerHost) {
const cache = createFileMap<ParsedCommandLine>();
const configParseHost = parseConfigHostFromCompilerHost(host);
function parseConfigFile(configFilePath: ResolvedConfigFileName) {
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);
return parsed;
}
function removeKey(configFilePath: ResolvedConfigFileName) {
cache.removeKey(configFilePath);
}
return {
parseConfigFile,
removeKey
};
}
function newer(date1: Date, date2: Date): Date {
return date2 > date1 ? date2 : date1;
}
function isDeclarationFile(fileName: string) {
return fileExtensionIs(fileName, Extension.Dts);
}
export function createBuildContext(options: BuildOptions): BuildContext {
const invalidatedProjects = createFileMap<true>();
const queuedProjects = createFileMap<true>();
const missingRoots = createMap<true>();
return {
options,
projectStatus: createFileMap(),
unchangedOutputs: createFileMap(),
invalidatedProjects,
missingRoots,
queuedProjects
};
}
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(args: string[], compilerHost: CompilerHost, buildHost: BuildHost, system?: System) {
let verbose = false;
let dry = false;
let force = false;
let clean = false;
let watch = false;
const projects: string[] = [];
for (const arg of args) {
switch (arg.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;
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) {
return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force");
}
if (clean && verbose) {
return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose");
}
if (clean && watch) {
return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch");
}
if (watch && dry) {
return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry");
}
if (projects.length === 0) {
// tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ."
addProject(".");
}
const builder = createSolutionBuilder(compilerHost, buildHost, projects, { dry, force, verbose }, system);
if (clean) {
builder.cleanAllProjects();
}
else {
builder.buildAllProjects();
}
if (watch) {
return builder.startWatching();
}
function addProject(projectSpecification: string) {
const fileName = resolvePath(compilerHost.getCurrentDirectory(), projectSpecification);
const refPath = resolveProjectReferencePath(compilerHost, { path: fileName });
if (!refPath) {
return buildHost.error(Diagnostics.File_0_does_not_exist, projectSpecification);
}
if (!compilerHost.fileExists(refPath)) {
return buildHost.error(Diagnostics.File_0_does_not_exist, fileName);
}
projects.push(refPath);
}
}
/**
* 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(compilerHost: CompilerHost, buildHost: BuildHost, rootNames: ReadonlyArray<string>, defaultOptions: BuildOptions, system?: System) {
if (!compilerHost.getModifiedTime || !compilerHost.setModifiedTime) {
throw new Error("Host must support timestamp APIs");
}
const configFileCache = createConfigFileCache(compilerHost);
let context = createBuildContext(defaultOptions);
const existingWatchersForWildcards = createMap<WildcardDirectoryWatcher>();
return {
buildAllProjects,
getUpToDateStatus,
getUpToDateStatusOfFile,
cleanAllProjects,
resetBuildContext,
getBuildGraph,
invalidateProject,
buildInvalidatedProjects,
buildDependentInvalidatedProjects,
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()!;
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, () => {
invalidateProjectAndScheduleBuilds(resolved);
});
}
}
}
function invalidateProjectAndScheduleBuilds(resolved: ResolvedConfigFileName) {
invalidateProject(resolved);
system!.setTimeout!(buildInvalidatedProjects, 100);
system!.setTimeout!(buildDependentInvalidatedProjects, 3000);
}
}
function resetBuildContext(opts = defaultOptions) {
context = createBuildContext(opts);
}
function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus {
return getUpToDateStatus(configFileCache.parseConfigFile(configFileName));
}
function getBuildGraph(configFileNames: ReadonlyArray<string>) {
const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames);
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" };
}
const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath!);
if (prior !== undefined) {
return prior;
}
const actual = getUpToDateStatusWorker(project);
context.projectStatus.setValue(project.options.configFilePath!, actual);
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);
verboseReportProjectStatus(next, status);
if (status.type === UpToDateStatusType.UpstreamBlocked) {
if (context.options.verbose) buildHost.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName);
continue;
}
buildSingleProject(next);
}
}
function getAllProjectOutputs(project: ParsedCommandLine): ReadonlyArray<string> {
if (project.options.outFile) {
return getOutFileOutputs(project);
}
else {
const outputs: string[] = [];
for (const inputFile of project.fileNames) {
outputs.push(...getOutputFileNames(inputFile, project));
}
return outputs;
}
}
function getUpToDateStatusWorker(project: ParsedCommandLine): UpToDateStatus {
let newestInputFileName: string = undefined!;
let newestInputFileTime = minimumDate;
// Get timestamps of input files
for (const inputFile of project.fileNames) {
if (!compilerHost.fileExists(inputFile)) {
return {
type: UpToDateStatusType.Unbuildable,
reason: `${inputFile} does not exist`
};
}
const inputTime = compilerHost.getModifiedTime!(inputFile);
if (inputTime > newestInputFileTime) {
newestInputFileName = inputFile;
newestInputFileTime = inputTime;
}
}
// 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 = "(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
// Don't immediately return because we can still be upstream-blocked, which is a higher-priority status
if (!compilerHost.fileExists(output)) {
missingOutputFileName = output;
break;
}
const outputTime = compilerHost.getModifiedTime!(output);
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;
}
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
// had its file touched but not had its contents changed - this allows us
// to skip a downstream typecheck
if (isDeclarationFile(output)) {
const unchangedTime = context.unchangedOutputs.getValueOrUndefined(output);
if (unchangedTime !== undefined) {
newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime);
}
else {
newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, compilerHost.getModifiedTime!(output));
}
}
}
let pseudoUpToDate = false;
let usesPrepend = false;
if (project.projectReferences) {
for (const ref of project.projectReferences) {
usesPrepend = usesPrepend || ref.prepend;
const resolvedRef = resolveProjectReferencePath(compilerHost, 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 {
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.OutOfDateWithUpstream,
outOfDateOutputFileName: oldestOutputFileName,
newerProjectName: ref.path
};
}
}
if (missingOutputFileName !== undefined) {
return {
type: UpToDateStatusType.OutputMissing,
missingOutputFileName
};
}
if (isOutOfDateWithInputs) {
return {
type: UpToDateStatusType.OutOfDateWithSelf,
outOfDateOutputFileName: oldestOutputFileName,
newerInputFileName: newestInputFileName
};
}
if (usesPrepend) {
psuedoUpToDate = false;
}
// Up to date
return {
type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate,
newestDeclarationFileContentChangedTime,
newestInputFileTime,
newestOutputFileTime,
newestInputFileName,
newestOutputFileName,
oldestOutputFileName
};
}
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) {
visit(root);
}
if (hadError) {
return undefined;
}
return {
buildQueue: buildOrder,
dependencyMap: graph
};
function visit(projPath: ResolvedConfigFileName, inCircularContext = false) {
// Already visited
if (permanentMarks[projPath]) return;
// Circular
if (temporaryMarks[projPath]) {
if (!inCircularContext) {
hadError = true;
buildHost.error(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n"));
return;
}
}
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);
}
}
function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags {
if (context.options.dry) {
buildHost.message(Diagnostics.A_non_dry_build_would_build_project_0, proj);
return BuildResultFlags.Success;
}
if (context.options.verbose) buildHost.verbose(Diagnostics.Building_project_0, proj);
let resultFlags = BuildResultFlags.None;
resultFlags |= BuildResultFlags.DeclarationOutputUnchanged;
const configFile = configFileCache.parseConfigFile(proj);
if (!configFile) {
// Failed to read the config file
resultFlags |= BuildResultFlags.ConfigFileErrors;
context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" });
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: compilerHost,
rootNames: configFile.fileNames,
options: configFile.options
};
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) {
buildHost.errorDiagnostic(diag);
}
context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Syntactic errors" });
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) {
buildHost.errorDiagnostic(diag);
}
context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Declaration file errors" });
return resultFlags;
}
}
// Same as above but now for semantic diagnostics
const semanticDiagnostics = program.getSemanticDiagnostics();
if (semanticDiagnostics.length) {
resultFlags |= BuildResultFlags.TypeErrors;
for (const diag of semanticDiagnostics) {
buildHost.errorDiagnostic(diag);
}
context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Semantic errors" });
return resultFlags;
}
let newestDeclarationFileContentChangedTime = minimumDate;
program.emit(/*targetSourceFile*/ undefined, (fileName, content, writeBom, onError) => {
let priorChangeTime: Date | undefined;
if (isDeclarationFile(fileName) && compilerHost.fileExists(fileName)) {
if (compilerHost.readFile(fileName) === content) {
// Check for unchanged .d.ts files
resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged;
priorChangeTime = compilerHost.getModifiedTime && compilerHost.getModifiedTime(fileName);
}
}
compilerHost.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 updateOutputTimestamps(proj: ParsedCommandLine) {
if (context.options.dry) {
return buildHost.message(Diagnostics.A_non_dry_build_would_build_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, compilerHost.getModifiedTime!(file));
}
compilerHost.setModifiedTime!(file, now);
}
context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus);
}
function getFilesToClean(configFileNames: ReadonlyArray<ResolvedConfigFileName>): string[] | undefined {
const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames);
if (resolvedNames === undefined) return undefined;
// 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 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 (compilerHost.fileExists(output)) {
filesToDelete.push(output);
}
}
}
return filesToDelete;
}
function getAllProjectsInScope(): ReadonlyArray<ResolvedConfigFileName> | 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<ResolvedConfigFileName> | undefined = getAllProjectsInScope();
if (resolvedNames === undefined) {
return buildHost.message(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located);
}
const filesToDelete = getFilesToClean(resolvedNames);
if (filesToDelete === undefined) {
return buildHost.message(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located);
}
if (context.options.dry) {
return buildHost.message(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""));
}
// 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 {
const fullPath = resolvePath(compilerHost.getCurrentDirectory(), name);
if (compilerHost.fileExists(fullPath)) {
return fullPath as ResolvedConfigFileName;
}
const fullPathWithTsconfig = combinePaths(fullPath, "tsconfig.json");
if (compilerHost.fileExists(fullPathWithTsconfig)) {
return fullPathWithTsconfig as ResolvedConfigFileName;
}
buildHost.error(Diagnostics.File_0_not_found, relName(fullPath));
return undefined;
}
function resolveProjectNames(configFileNames: ReadonlyArray<string>): ResolvedConfigFileName[] | undefined {
const resolvedNames: ResolvedConfigFileName[] = [];
for (const name of configFileNames) {
const resolved = resolveProjectName(name);
if (resolved === undefined) {
return undefined;
}
resolvedNames.push(resolved);
}
return resolvedNames;
}
function buildAllProjects() {
const graph = getGlobalDependencyGraph();
if (graph === undefined) return;
const queue = graph.buildQueue;
reportBuildQueue(graph);
for (const next of queue) {
const proj = configFileCache.parseConfigFile(next);
if (proj === undefined) {
break;
}
const status = getUpToDateStatus(proj);
verboseReportProjectStatus(next, status);
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
buildHost.message(Diagnostics.Project_0_is_up_to_date, projName);
}
continue;
}
if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !context.options.force) {
// Fake build
updateOutputTimestamps(proj);
continue;
}
if (status.type === UpToDateStatusType.UpstreamBlocked) {
if (context.options.verbose) buildHost.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName);
continue;
}
if (status.type === UpToDateStatusType.ContainerOnly) {
// Do nothing
continue;
}
buildSingleProject(next);
}
}
/**
* 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;
const names: string[] = [];
for (const name of graph.buildQueue) {
names.push(name);
}
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, compilerHost.getCurrentDirectory(), f => compilerHost.getCanonicalFileName(f));
}
/**
* Report the up-to-date status of a project if we're in verbose mode
*/
function verboseReportProjectStatus(configFileName: string, status: UpToDateStatus) {
if (!context.options.verbose) return;
switch (status.type) {
case UpToDateStatusType.OutOfDateWithSelf:
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 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 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 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));
}
// Don't report anything for "up to date because it was already built" -- too verbose
break;
case UpToDateStatusType.UpToDateWithUpstreamTypes:
return buildHost.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies,
relName(configFileName));
case UpToDateStatusType.UpstreamOutOfDate:
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 buildHost.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors,
relName(configFileName),
relName(status.upstreamProjectName));
case UpToDateStatusType.Unbuildable:
return buildHost.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);
}
}
}
}