2147 lines
104 KiB
TypeScript
2147 lines
104 KiB
TypeScript
namespace ts {
|
|
const minimumDate = new Date(-8640000000000000);
|
|
const maximumDate = new Date(8640000000000000);
|
|
|
|
export interface BuildOptions {
|
|
dry?: boolean;
|
|
force?: boolean;
|
|
verbose?: boolean;
|
|
|
|
/*@internal*/ clean?: boolean;
|
|
/*@internal*/ watch?: boolean;
|
|
/*@internal*/ help?: boolean;
|
|
|
|
/*@internal*/ preserveWatchOutput?: boolean;
|
|
/*@internal*/ listEmittedFiles?: boolean;
|
|
/*@internal*/ listFiles?: boolean;
|
|
/*@internal*/ explainFiles?: boolean;
|
|
/*@internal*/ pretty?: boolean;
|
|
incremental?: boolean;
|
|
assumeChangesOnlyAffectDirectDependencies?: boolean;
|
|
|
|
traceResolution?: boolean;
|
|
/* @internal */ diagnostics?: boolean;
|
|
/* @internal */ extendedDiagnostics?: boolean;
|
|
/* @internal */ locale?: string;
|
|
/* @internal */ generateCpuProfile?: string;
|
|
/* @internal */ generateTrace?: string;
|
|
|
|
[option: string]: CompilerOptionsValue | undefined;
|
|
}
|
|
|
|
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,
|
|
EmitErrors = 1 << 6,
|
|
|
|
AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors
|
|
}
|
|
|
|
/*@internal*/
|
|
export type ResolvedConfigFilePath = ResolvedConfigFileName & Path;
|
|
|
|
function getOrCreateValueFromConfigFileMap<T>(configFileMap: ESMap<ResolvedConfigFilePath, T>, resolved: ResolvedConfigFilePath, createT: () => T): T {
|
|
const existingValue = configFileMap.get(resolved);
|
|
let newValue: T | undefined;
|
|
if (!existingValue) {
|
|
newValue = createT();
|
|
configFileMap.set(resolved, newValue);
|
|
}
|
|
return existingValue || newValue!;
|
|
}
|
|
|
|
function getOrCreateValueMapFromConfigFileMap<T>(configFileMap: ESMap<ResolvedConfigFilePath, ESMap<string, T>>, resolved: ResolvedConfigFilePath): ESMap<string, T> {
|
|
return getOrCreateValueFromConfigFileMap<ESMap<string, T>>(configFileMap, resolved, () => new Map());
|
|
}
|
|
|
|
function newer(date1: Date, date2: Date): Date {
|
|
return date2 > date1 ? date2 : date1;
|
|
}
|
|
|
|
function isDeclarationFile(fileName: string) {
|
|
return fileExtensionIs(fileName, Extension.Dts);
|
|
}
|
|
|
|
export type ReportEmitErrorSummary = (errorCount: number) => void;
|
|
|
|
export interface SolutionBuilderHostBase<T extends BuilderProgram> extends ProgramHost<T> {
|
|
createDirectory?(path: string): void;
|
|
/**
|
|
* Should provide create directory and writeFile if done of invalidatedProjects is not invoked with
|
|
* writeFileCallback
|
|
*/
|
|
writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void;
|
|
getCustomTransformers?: (project: string) => CustomTransformers | undefined;
|
|
|
|
getModifiedTime(fileName: string): Date | undefined;
|
|
setModifiedTime(fileName: string, date: Date): void;
|
|
deleteFile(fileName: string): void;
|
|
getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined;
|
|
|
|
reportDiagnostic: DiagnosticReporter; // Technically we want to move it out and allow steps of actions on Solution, but for now just merge stuff in build host here
|
|
reportSolutionBuilderStatus: DiagnosticReporter;
|
|
|
|
// TODO: To do better with watch mode and normal build mode api that creates program and emits files
|
|
// This currently helps enable --diagnostics and --extendedDiagnostics
|
|
afterProgramEmitAndDiagnostics?(program: T): void;
|
|
/*@internal*/ afterEmitBundle?(config: ParsedCommandLine): void;
|
|
|
|
// For testing
|
|
/*@internal*/ now?(): Date;
|
|
}
|
|
|
|
export interface SolutionBuilderHost<T extends BuilderProgram> extends SolutionBuilderHostBase<T> {
|
|
reportErrorSummary?: ReportEmitErrorSummary;
|
|
}
|
|
|
|
export interface SolutionBuilderWithWatchHost<T extends BuilderProgram> extends SolutionBuilderHostBase<T>, WatchHost {
|
|
}
|
|
|
|
/*@internal*/
|
|
export type BuildOrder = readonly ResolvedConfigFileName[];
|
|
/*@internal*/
|
|
export interface CircularBuildOrder {
|
|
buildOrder: BuildOrder;
|
|
circularDiagnostics: readonly Diagnostic[];
|
|
}
|
|
/*@internal*/
|
|
export type AnyBuildOrder = BuildOrder | CircularBuildOrder;
|
|
|
|
/*@internal*/
|
|
export function isCircularBuildOrder(buildOrder: AnyBuildOrder): buildOrder is CircularBuildOrder {
|
|
return !!buildOrder && !!(buildOrder as CircularBuildOrder).buildOrder;
|
|
}
|
|
|
|
/*@internal*/
|
|
export function getBuildOrderFromAnyBuildOrder(anyBuildOrder: AnyBuildOrder): BuildOrder {
|
|
return isCircularBuildOrder(anyBuildOrder) ? anyBuildOrder.buildOrder : anyBuildOrder;
|
|
}
|
|
|
|
export interface SolutionBuilder<T extends BuilderProgram> {
|
|
build(project?: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus;
|
|
clean(project?: string): ExitStatus;
|
|
buildReferences(project: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus;
|
|
cleanReferences(project?: string): ExitStatus;
|
|
getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject<T> | undefined;
|
|
|
|
// Currently used for testing but can be made public if needed:
|
|
/*@internal*/ getBuildOrder(): AnyBuildOrder;
|
|
|
|
// Testing only
|
|
/*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus;
|
|
/*@internal*/ invalidateProject(configFilePath: ResolvedConfigFilePath, reloadLevel?: ConfigFileProgramReloadLevel): void;
|
|
/*@internal*/ buildNextInvalidatedProject(): void;
|
|
/*@internal*/ getAllParsedConfigs(): readonly ParsedCommandLine[];
|
|
/*@internal*/ close(): void;
|
|
}
|
|
|
|
/**
|
|
* Create a function that reports watch status by writing to the system and handles the formating of the diagnostic
|
|
*/
|
|
export function createBuilderStatusReporter(system: System, pretty?: boolean): DiagnosticReporter {
|
|
return diagnostic => {
|
|
let output = pretty ? `[${formatColorAndReset(getLocaleTimeString(system), ForegroundColorEscapeSequences.Grey)}] ` : `${getLocaleTimeString(system)} - `;
|
|
output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${system.newLine + system.newLine}`;
|
|
system.write(output);
|
|
};
|
|
}
|
|
|
|
function createSolutionBuilderHostBase<T extends BuilderProgram>(system: System, createProgram: CreateProgram<T> | undefined, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter) {
|
|
const host = createProgramHost(system, createProgram) as SolutionBuilderHostBase<T>;
|
|
host.getModifiedTime = system.getModifiedTime ? path => system.getModifiedTime!(path) : returnUndefined;
|
|
host.setModifiedTime = system.setModifiedTime ? (path, date) => system.setModifiedTime!(path, date) : noop;
|
|
host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop;
|
|
host.reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system);
|
|
host.reportSolutionBuilderStatus = reportSolutionBuilderStatus || createBuilderStatusReporter(system);
|
|
host.now = maybeBind(system, system.now); // For testing
|
|
return host;
|
|
}
|
|
|
|
export function createSolutionBuilderHost<T extends BuilderProgram = EmitAndSemanticDiagnosticsBuilderProgram>(system = sys, createProgram?: CreateProgram<T>, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportErrorSummary?: ReportEmitErrorSummary) {
|
|
const host = createSolutionBuilderHostBase(system, createProgram, reportDiagnostic, reportSolutionBuilderStatus) as SolutionBuilderHost<T>;
|
|
host.reportErrorSummary = reportErrorSummary;
|
|
return host;
|
|
}
|
|
|
|
export function createSolutionBuilderWithWatchHost<T extends BuilderProgram = EmitAndSemanticDiagnosticsBuilderProgram>(system = sys, createProgram?: CreateProgram<T>, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter) {
|
|
const host = createSolutionBuilderHostBase(system, createProgram, reportDiagnostic, reportSolutionBuilderStatus) as SolutionBuilderWithWatchHost<T>;
|
|
const watchHost = createWatchHost(system, reportWatchStatus);
|
|
copyProperties(host, watchHost);
|
|
return host;
|
|
}
|
|
|
|
function getCompilerOptionsOfBuildOptions(buildOptions: BuildOptions): CompilerOptions {
|
|
const result = {} as CompilerOptions;
|
|
commonOptionsWithBuild.forEach(option => {
|
|
if (hasProperty(buildOptions, option.name)) result[option.name] = buildOptions[option.name];
|
|
});
|
|
return result;
|
|
}
|
|
|
|
export function createSolutionBuilder<T extends BuilderProgram>(host: SolutionBuilderHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder<T> {
|
|
return createSolutionBuilderWorker(/*watch*/ false, host, rootNames, defaultOptions);
|
|
}
|
|
|
|
export function createSolutionBuilderWithWatch<T extends BuilderProgram>(host: SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder<T> {
|
|
return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions, baseWatchOptions);
|
|
}
|
|
|
|
type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic;
|
|
interface SolutionBuilderStateCache {
|
|
originalReadFile: CompilerHost["readFile"];
|
|
originalFileExists: CompilerHost["fileExists"];
|
|
originalDirectoryExists: CompilerHost["directoryExists"];
|
|
originalCreateDirectory: CompilerHost["createDirectory"];
|
|
originalWriteFile: CompilerHost["writeFile"] | undefined;
|
|
originalReadFileWithCache: CompilerHost["readFile"];
|
|
originalGetSourceFile: CompilerHost["getSourceFile"];
|
|
}
|
|
|
|
interface SolutionBuilderState<T extends BuilderProgram = BuilderProgram> extends WatchFactory<WatchType, ResolvedConfigFileName> {
|
|
readonly host: SolutionBuilderHost<T>;
|
|
readonly hostWithWatch: SolutionBuilderWithWatchHost<T>;
|
|
readonly currentDirectory: string;
|
|
readonly getCanonicalFileName: GetCanonicalFileName;
|
|
readonly parseConfigFileHost: ParseConfigFileHost;
|
|
readonly write: ((s: string) => void) | undefined;
|
|
|
|
// State of solution
|
|
readonly options: BuildOptions;
|
|
readonly baseCompilerOptions: CompilerOptions;
|
|
readonly rootNames: readonly string[];
|
|
readonly baseWatchOptions: WatchOptions | undefined;
|
|
|
|
readonly resolvedConfigFilePaths: ESMap<string, ResolvedConfigFilePath>;
|
|
readonly configFileCache: ESMap<ResolvedConfigFilePath, ConfigFileCacheEntry>;
|
|
/** Map from config file name to up-to-date status */
|
|
readonly projectStatus: ESMap<ResolvedConfigFilePath, UpToDateStatus>;
|
|
readonly buildInfoChecked: ESMap<ResolvedConfigFilePath, true>;
|
|
readonly extendedConfigCache: ESMap<string, ExtendedConfigCacheEntry>;
|
|
|
|
readonly builderPrograms: ESMap<ResolvedConfigFilePath, T>;
|
|
readonly diagnostics: ESMap<ResolvedConfigFilePath, readonly Diagnostic[]>;
|
|
readonly projectPendingBuild: ESMap<ResolvedConfigFilePath, ConfigFileProgramReloadLevel>;
|
|
readonly projectErrorsReported: ESMap<ResolvedConfigFilePath, true>;
|
|
|
|
readonly compilerHost: CompilerHost;
|
|
readonly moduleResolutionCache: ModuleResolutionCache | undefined;
|
|
readonly typeReferenceDirectiveResolutionCache: TypeReferenceDirectiveResolutionCache | undefined;
|
|
|
|
// Mutable state
|
|
buildOrder: AnyBuildOrder | undefined;
|
|
readFileWithCache: (f: string) => string | undefined;
|
|
projectCompilerOptions: CompilerOptions;
|
|
cache: SolutionBuilderStateCache | undefined;
|
|
allProjectBuildPending: boolean;
|
|
needsSummary: boolean;
|
|
watchAllProjectsPending: boolean;
|
|
currentInvalidatedProject: InvalidatedProject<T> | undefined;
|
|
|
|
// Watch state
|
|
readonly watch: boolean;
|
|
readonly allWatchedWildcardDirectories: ESMap<ResolvedConfigFilePath, ESMap<string, WildcardDirectoryWatcher>>;
|
|
readonly allWatchedInputFiles: ESMap<ResolvedConfigFilePath, ESMap<Path, FileWatcher>>;
|
|
readonly allWatchedConfigFiles: ESMap<ResolvedConfigFilePath, FileWatcher>;
|
|
readonly allWatchedExtendedConfigFiles: ESMap<Path, SharedExtendedConfigFileWatcher<ResolvedConfigFilePath>>;
|
|
readonly allWatchedPackageJsonFiles: ESMap<ResolvedConfigFilePath, ESMap<Path, FileWatcher>>;
|
|
readonly lastCachedPackageJsonLookups: ESMap<ResolvedConfigFilePath, readonly (readonly [Path, object | boolean])[] | undefined>;
|
|
|
|
timerToBuildInvalidatedProject: any;
|
|
reportFileChangeDetected: boolean;
|
|
writeLog: (s: string) => void;
|
|
}
|
|
|
|
function createSolutionBuilderState<T extends BuilderProgram>(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost<T> | SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], options: BuildOptions, baseWatchOptions: WatchOptions | undefined): SolutionBuilderState<T> {
|
|
const host = hostOrHostWithWatch as SolutionBuilderHost<T>;
|
|
const hostWithWatch = hostOrHostWithWatch as SolutionBuilderWithWatchHost<T>;
|
|
const currentDirectory = host.getCurrentDirectory();
|
|
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames());
|
|
|
|
// State of the solution
|
|
const baseCompilerOptions = getCompilerOptionsOfBuildOptions(options);
|
|
const compilerHost = createCompilerHostFromProgramHost(host, () => state.projectCompilerOptions);
|
|
setGetSourceFileAsHashVersioned(compilerHost, host);
|
|
compilerHost.getParsedCommandLine = fileName => parseConfigFile(state, fileName as ResolvedConfigFileName, toResolvedConfigFilePath(state, fileName as ResolvedConfigFileName));
|
|
compilerHost.resolveModuleNames = maybeBind(host, host.resolveModuleNames);
|
|
compilerHost.resolveTypeReferenceDirectives = maybeBind(host, host.resolveTypeReferenceDirectives);
|
|
const moduleResolutionCache = !compilerHost.resolveModuleNames ? createModuleResolutionCache(currentDirectory, getCanonicalFileName) : undefined;
|
|
const typeReferenceDirectiveResolutionCache = !compilerHost.resolveTypeReferenceDirectives ? createTypeReferenceDirectiveResolutionCache(currentDirectory, getCanonicalFileName, /*options*/ undefined, moduleResolutionCache?.getPackageJsonInfoCache()) : undefined;
|
|
if (!compilerHost.resolveModuleNames) {
|
|
const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveModuleName(moduleName, containingFile, state.projectCompilerOptions, compilerHost, moduleResolutionCache, redirectedReference).resolvedModule!;
|
|
compilerHost.resolveModuleNames = (moduleNames, containingFile, _reusedNames, redirectedReference) =>
|
|
loadWithLocalCache<ResolvedModuleFull>(Debug.checkEachDefined(moduleNames), containingFile, redirectedReference, loader);
|
|
}
|
|
if (!compilerHost.resolveTypeReferenceDirectives) {
|
|
const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveTypeReferenceDirective(moduleName, containingFile, state.projectCompilerOptions, compilerHost, redirectedReference, state.typeReferenceDirectiveResolutionCache).resolvedTypeReferenceDirective!;
|
|
compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile, redirectedReference) =>
|
|
loadWithLocalCache<ResolvedTypeReferenceDirective>(Debug.checkEachDefined(typeReferenceDirectiveNames), containingFile, redirectedReference, loader);
|
|
}
|
|
|
|
const { watchFile, watchDirectory, writeLog } = createWatchFactory<ResolvedConfigFileName>(hostWithWatch, options);
|
|
|
|
const state: SolutionBuilderState<T> = {
|
|
host,
|
|
hostWithWatch,
|
|
currentDirectory,
|
|
getCanonicalFileName,
|
|
parseConfigFileHost: parseConfigHostFromCompilerHostLike(host),
|
|
write: maybeBind(host, host.trace),
|
|
|
|
// State of solution
|
|
options,
|
|
baseCompilerOptions,
|
|
rootNames,
|
|
baseWatchOptions,
|
|
|
|
resolvedConfigFilePaths: new Map(),
|
|
configFileCache: new Map(),
|
|
projectStatus: new Map(),
|
|
buildInfoChecked: new Map(),
|
|
extendedConfigCache: new Map(),
|
|
|
|
builderPrograms: new Map(),
|
|
diagnostics: new Map(),
|
|
projectPendingBuild: new Map(),
|
|
projectErrorsReported: new Map(),
|
|
|
|
compilerHost,
|
|
moduleResolutionCache,
|
|
typeReferenceDirectiveResolutionCache,
|
|
|
|
// Mutable state
|
|
buildOrder: undefined,
|
|
readFileWithCache: f => host.readFile(f),
|
|
projectCompilerOptions: baseCompilerOptions,
|
|
cache: undefined,
|
|
allProjectBuildPending: true,
|
|
needsSummary: true,
|
|
watchAllProjectsPending: watch,
|
|
currentInvalidatedProject: undefined,
|
|
|
|
// Watch state
|
|
watch,
|
|
allWatchedWildcardDirectories: new Map(),
|
|
allWatchedInputFiles: new Map(),
|
|
allWatchedConfigFiles: new Map(),
|
|
allWatchedExtendedConfigFiles: new Map(),
|
|
allWatchedPackageJsonFiles: new Map(),
|
|
lastCachedPackageJsonLookups: new Map(),
|
|
|
|
timerToBuildInvalidatedProject: undefined,
|
|
reportFileChangeDetected: false,
|
|
watchFile,
|
|
watchDirectory,
|
|
writeLog,
|
|
};
|
|
|
|
return state;
|
|
}
|
|
|
|
function toPath(state: SolutionBuilderState, fileName: string) {
|
|
return ts.toPath(fileName, state.currentDirectory, state.getCanonicalFileName);
|
|
}
|
|
|
|
function toResolvedConfigFilePath(state: SolutionBuilderState, fileName: ResolvedConfigFileName): ResolvedConfigFilePath {
|
|
const { resolvedConfigFilePaths } = state;
|
|
const path = resolvedConfigFilePaths.get(fileName);
|
|
if (path !== undefined) return path;
|
|
|
|
const resolvedPath = toPath(state, fileName) as ResolvedConfigFilePath;
|
|
resolvedConfigFilePaths.set(fileName, resolvedPath);
|
|
return resolvedPath;
|
|
}
|
|
|
|
function isParsedCommandLine(entry: ConfigFileCacheEntry): entry is ParsedCommandLine {
|
|
return !!(entry as ParsedCommandLine).options;
|
|
}
|
|
|
|
function getCachedParsedConfigFile(state: SolutionBuilderState, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined {
|
|
const value = state.configFileCache.get(configFilePath);
|
|
return value && isParsedCommandLine(value) ? value : undefined;
|
|
}
|
|
|
|
function parseConfigFile(state: SolutionBuilderState, configFileName: ResolvedConfigFileName, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined {
|
|
const { configFileCache } = state;
|
|
const value = configFileCache.get(configFilePath);
|
|
if (value) {
|
|
return isParsedCommandLine(value) ? value : undefined;
|
|
}
|
|
|
|
let diagnostic: Diagnostic | undefined;
|
|
const { parseConfigFileHost, baseCompilerOptions, baseWatchOptions, extendedConfigCache, host } = state;
|
|
let parsed: ParsedCommandLine | undefined;
|
|
if (host.getParsedCommandLine) {
|
|
parsed = host.getParsedCommandLine(configFileName);
|
|
if (!parsed) diagnostic = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName);
|
|
}
|
|
else {
|
|
parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d;
|
|
parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache, baseWatchOptions);
|
|
parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop;
|
|
}
|
|
configFileCache.set(configFilePath, parsed || diagnostic!);
|
|
return parsed;
|
|
}
|
|
|
|
function resolveProjectName(state: SolutionBuilderState, name: string): ResolvedConfigFileName {
|
|
return resolveConfigFileProjectName(resolvePath(state.currentDirectory, name));
|
|
}
|
|
|
|
function createBuildOrder(state: SolutionBuilderState, roots: readonly ResolvedConfigFileName[]): AnyBuildOrder {
|
|
const temporaryMarks = new Map<ResolvedConfigFilePath, true>();
|
|
const permanentMarks = new Map<ResolvedConfigFilePath, true>();
|
|
const circularityReportStack: string[] = [];
|
|
let buildOrder: ResolvedConfigFileName[] | undefined;
|
|
let circularDiagnostics: Diagnostic[] | undefined;
|
|
for (const root of roots) {
|
|
visit(root);
|
|
}
|
|
|
|
return circularDiagnostics ?
|
|
{ buildOrder: buildOrder || emptyArray, circularDiagnostics } :
|
|
buildOrder || emptyArray;
|
|
|
|
function visit(configFileName: ResolvedConfigFileName, inCircularContext?: boolean) {
|
|
const projPath = toResolvedConfigFilePath(state, configFileName);
|
|
// Already visited
|
|
if (permanentMarks.has(projPath)) return;
|
|
// Circular
|
|
if (temporaryMarks.has(projPath)) {
|
|
if (!inCircularContext) {
|
|
(circularDiagnostics || (circularDiagnostics = [])).push(
|
|
createCompilerDiagnostic(
|
|
Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0,
|
|
circularityReportStack.join("\r\n")
|
|
)
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
temporaryMarks.set(projPath, true);
|
|
circularityReportStack.push(configFileName);
|
|
const parsed = parseConfigFile(state, configFileName, projPath);
|
|
if (parsed && parsed.projectReferences) {
|
|
for (const ref of parsed.projectReferences) {
|
|
const resolvedRefPath = resolveProjectName(state, ref.path);
|
|
visit(resolvedRefPath, inCircularContext || ref.circular);
|
|
}
|
|
}
|
|
|
|
circularityReportStack.pop();
|
|
permanentMarks.set(projPath, true);
|
|
(buildOrder || (buildOrder = [])).push(configFileName);
|
|
}
|
|
}
|
|
|
|
function getBuildOrder(state: SolutionBuilderState) {
|
|
return state.buildOrder || createStateBuildOrder(state);
|
|
}
|
|
|
|
function createStateBuildOrder(state: SolutionBuilderState) {
|
|
const buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f)));
|
|
|
|
// Clear all to ResolvedConfigFilePaths cache to start fresh
|
|
state.resolvedConfigFilePaths.clear();
|
|
|
|
// TODO(rbuckton): Should be a `Set`, but that requires changing the code below that uses `mutateMapSkippingNewValues`
|
|
const currentProjects = new Map(
|
|
getBuildOrderFromAnyBuildOrder(buildOrder).map(
|
|
resolved => [toResolvedConfigFilePath(state, resolved), true as true])
|
|
);
|
|
|
|
const noopOnDelete = { onDeleteValue: noop };
|
|
// Config file cache
|
|
mutateMapSkippingNewValues(state.configFileCache, currentProjects, noopOnDelete);
|
|
mutateMapSkippingNewValues(state.projectStatus, currentProjects, noopOnDelete);
|
|
mutateMapSkippingNewValues(state.buildInfoChecked, currentProjects, noopOnDelete);
|
|
mutateMapSkippingNewValues(state.builderPrograms, currentProjects, noopOnDelete);
|
|
mutateMapSkippingNewValues(state.diagnostics, currentProjects, noopOnDelete);
|
|
mutateMapSkippingNewValues(state.projectPendingBuild, currentProjects, noopOnDelete);
|
|
mutateMapSkippingNewValues(state.projectErrorsReported, currentProjects, noopOnDelete);
|
|
|
|
// Remove watches for the program no longer in the solution
|
|
if (state.watch) {
|
|
mutateMapSkippingNewValues(
|
|
state.allWatchedConfigFiles,
|
|
currentProjects,
|
|
{ onDeleteValue: closeFileWatcher }
|
|
);
|
|
|
|
state.allWatchedExtendedConfigFiles.forEach(watcher => {
|
|
watcher.projects.forEach(project => {
|
|
if (!currentProjects.has(project)) {
|
|
watcher.projects.delete(project);
|
|
}
|
|
});
|
|
watcher.close();
|
|
});
|
|
|
|
mutateMapSkippingNewValues(
|
|
state.allWatchedWildcardDirectories,
|
|
currentProjects,
|
|
{ onDeleteValue: existingMap => existingMap.forEach(closeFileWatcherOf) }
|
|
);
|
|
|
|
mutateMapSkippingNewValues(
|
|
state.allWatchedInputFiles,
|
|
currentProjects,
|
|
{ onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) }
|
|
);
|
|
|
|
mutateMapSkippingNewValues(
|
|
state.allWatchedPackageJsonFiles,
|
|
currentProjects,
|
|
{ onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) }
|
|
);
|
|
}
|
|
return state.buildOrder = buildOrder;
|
|
}
|
|
|
|
function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined): AnyBuildOrder | undefined {
|
|
const resolvedProject = project && resolveProjectName(state, project);
|
|
const buildOrderFromState = getBuildOrder(state);
|
|
if (isCircularBuildOrder(buildOrderFromState)) return buildOrderFromState;
|
|
if (resolvedProject) {
|
|
const projectPath = toResolvedConfigFilePath(state, resolvedProject);
|
|
const projectIndex = findIndex(
|
|
buildOrderFromState,
|
|
configFileName => toResolvedConfigFilePath(state, configFileName) === projectPath
|
|
);
|
|
if (projectIndex === -1) return undefined;
|
|
}
|
|
const buildOrder = resolvedProject ? createBuildOrder(state, [resolvedProject]) as BuildOrder : buildOrderFromState;
|
|
Debug.assert(!isCircularBuildOrder(buildOrder));
|
|
Debug.assert(!onlyReferences || resolvedProject !== undefined);
|
|
Debug.assert(!onlyReferences || buildOrder[buildOrder.length - 1] === resolvedProject);
|
|
return onlyReferences ? buildOrder.slice(0, buildOrder.length - 1) : buildOrder;
|
|
}
|
|
|
|
function enableCache(state: SolutionBuilderState) {
|
|
if (state.cache) {
|
|
disableCache(state);
|
|
}
|
|
|
|
const { compilerHost, host } = state;
|
|
|
|
const originalReadFileWithCache = state.readFileWithCache;
|
|
const originalGetSourceFile = compilerHost.getSourceFile;
|
|
|
|
const {
|
|
originalReadFile, originalFileExists, originalDirectoryExists,
|
|
originalCreateDirectory, originalWriteFile,
|
|
getSourceFileWithCache, readFileWithCache
|
|
} = changeCompilerHostLikeToUseCache(
|
|
host,
|
|
fileName => toPath(state, fileName),
|
|
(...args) => originalGetSourceFile.call(compilerHost, ...args)
|
|
);
|
|
state.readFileWithCache = readFileWithCache;
|
|
compilerHost.getSourceFile = getSourceFileWithCache!;
|
|
|
|
state.cache = {
|
|
originalReadFile,
|
|
originalFileExists,
|
|
originalDirectoryExists,
|
|
originalCreateDirectory,
|
|
originalWriteFile,
|
|
originalReadFileWithCache,
|
|
originalGetSourceFile,
|
|
};
|
|
}
|
|
|
|
function disableCache(state: SolutionBuilderState) {
|
|
if (!state.cache) return;
|
|
|
|
const { cache, host, compilerHost, extendedConfigCache, moduleResolutionCache, typeReferenceDirectiveResolutionCache } = state;
|
|
|
|
host.readFile = cache.originalReadFile;
|
|
host.fileExists = cache.originalFileExists;
|
|
host.directoryExists = cache.originalDirectoryExists;
|
|
host.createDirectory = cache.originalCreateDirectory;
|
|
host.writeFile = cache.originalWriteFile;
|
|
compilerHost.getSourceFile = cache.originalGetSourceFile;
|
|
state.readFileWithCache = cache.originalReadFileWithCache;
|
|
extendedConfigCache.clear();
|
|
moduleResolutionCache?.clear();
|
|
typeReferenceDirectiveResolutionCache?.clear();
|
|
state.cache = undefined;
|
|
}
|
|
|
|
function clearProjectStatus(state: SolutionBuilderState, resolved: ResolvedConfigFilePath) {
|
|
state.projectStatus.delete(resolved);
|
|
state.diagnostics.delete(resolved);
|
|
}
|
|
|
|
function addProjToQueue({ projectPendingBuild }: SolutionBuilderState, proj: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) {
|
|
const value = projectPendingBuild.get(proj);
|
|
if (value === undefined) {
|
|
projectPendingBuild.set(proj, reloadLevel);
|
|
}
|
|
else if (value < reloadLevel) {
|
|
projectPendingBuild.set(proj, reloadLevel);
|
|
}
|
|
}
|
|
|
|
function setupInitialBuild(state: SolutionBuilderState, cancellationToken: CancellationToken | undefined) {
|
|
// Set initial build if not already built
|
|
if (!state.allProjectBuildPending) return;
|
|
state.allProjectBuildPending = false;
|
|
if (state.options.watch) reportWatchStatus(state, Diagnostics.Starting_compilation_in_watch_mode);
|
|
enableCache(state);
|
|
const buildOrder = getBuildOrderFromAnyBuildOrder(getBuildOrder(state));
|
|
buildOrder.forEach(configFileName =>
|
|
state.projectPendingBuild.set(
|
|
toResolvedConfigFilePath(state, configFileName),
|
|
ConfigFileProgramReloadLevel.None
|
|
)
|
|
);
|
|
|
|
if (cancellationToken) {
|
|
cancellationToken.throwIfCancellationRequested();
|
|
}
|
|
}
|
|
|
|
export enum InvalidatedProjectKind {
|
|
Build,
|
|
UpdateBundle,
|
|
UpdateOutputFileStamps
|
|
}
|
|
|
|
export interface InvalidatedProjectBase {
|
|
readonly kind: InvalidatedProjectKind;
|
|
readonly project: ResolvedConfigFileName;
|
|
/*@internal*/ readonly projectPath: ResolvedConfigFilePath;
|
|
/*@internal*/ readonly buildOrder: readonly ResolvedConfigFileName[];
|
|
/**
|
|
* To dispose this project and ensure that all the necessary actions are taken and state is updated accordingly
|
|
*/
|
|
done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): ExitStatus;
|
|
getCompilerOptions(): CompilerOptions;
|
|
getCurrentDirectory(): string;
|
|
}
|
|
|
|
export interface UpdateOutputFileStampsProject extends InvalidatedProjectBase {
|
|
readonly kind: InvalidatedProjectKind.UpdateOutputFileStamps;
|
|
updateOutputFileStatmps(): void;
|
|
}
|
|
|
|
export interface BuildInvalidedProject<T extends BuilderProgram> extends InvalidatedProjectBase {
|
|
readonly kind: InvalidatedProjectKind.Build;
|
|
/*
|
|
* Emitting with this builder program without the api provided for this project
|
|
* can result in build system going into invalid state as files written reflect the state of the project
|
|
*/
|
|
getBuilderProgram(): T | undefined;
|
|
getProgram(): Program | undefined;
|
|
getSourceFile(fileName: string): SourceFile | undefined;
|
|
getSourceFiles(): readonly SourceFile[];
|
|
getOptionsDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[];
|
|
getGlobalDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[];
|
|
getConfigFileParsingDiagnostics(): readonly Diagnostic[];
|
|
getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[];
|
|
getAllDependencies(sourceFile: SourceFile): readonly string[];
|
|
getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[];
|
|
getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult<readonly Diagnostic[]>;
|
|
/*
|
|
* Calling emit directly with targetSourceFile and emitOnlyDtsFiles set to true is not advised since
|
|
* emit in build system is responsible in updating status of the project
|
|
* If called with targetSourceFile and emitOnlyDtsFiles set to true, the emit just passes to underlying builder and
|
|
* wont reflect the status of file as being emitted in the builder
|
|
* (if that emit of that source file is required it would be emitted again when making sure invalidated project is completed)
|
|
* This emit is not considered actual emit (and hence uptodate status is not reflected if
|
|
*/
|
|
emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult | undefined;
|
|
// TODO(shkamat):: investigate later if we can emit even when there are declaration diagnostics
|
|
// emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileResult<EmitResult>;
|
|
}
|
|
|
|
export interface UpdateBundleProject<T extends BuilderProgram> extends InvalidatedProjectBase {
|
|
readonly kind: InvalidatedProjectKind.UpdateBundle;
|
|
emit(writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject<T> | undefined;
|
|
}
|
|
|
|
export type InvalidatedProject<T extends BuilderProgram> = UpdateOutputFileStampsProject | BuildInvalidedProject<T> | UpdateBundleProject<T>;
|
|
|
|
function doneInvalidatedProject(
|
|
state: SolutionBuilderState,
|
|
projectPath: ResolvedConfigFilePath
|
|
) {
|
|
state.projectPendingBuild.delete(projectPath);
|
|
state.currentInvalidatedProject = undefined;
|
|
return state.diagnostics.has(projectPath) ?
|
|
ExitStatus.DiagnosticsPresent_OutputsSkipped :
|
|
ExitStatus.Success;
|
|
}
|
|
|
|
function createUpdateOutputFileStampsProject(
|
|
state: SolutionBuilderState,
|
|
project: ResolvedConfigFileName,
|
|
projectPath: ResolvedConfigFilePath,
|
|
config: ParsedCommandLine,
|
|
buildOrder: readonly ResolvedConfigFileName[]
|
|
): UpdateOutputFileStampsProject {
|
|
let updateOutputFileStampsPending = true;
|
|
return {
|
|
kind: InvalidatedProjectKind.UpdateOutputFileStamps,
|
|
project,
|
|
projectPath,
|
|
buildOrder,
|
|
getCompilerOptions: () => config.options,
|
|
getCurrentDirectory: () => state.currentDirectory,
|
|
updateOutputFileStatmps: () => {
|
|
updateOutputTimestamps(state, config, projectPath);
|
|
updateOutputFileStampsPending = false;
|
|
},
|
|
done: () => {
|
|
if (updateOutputFileStampsPending) {
|
|
updateOutputTimestamps(state, config, projectPath);
|
|
}
|
|
return doneInvalidatedProject(state, projectPath);
|
|
}
|
|
};
|
|
}
|
|
|
|
enum BuildStep {
|
|
CreateProgram,
|
|
SyntaxDiagnostics,
|
|
SemanticDiagnostics,
|
|
Emit,
|
|
EmitBundle,
|
|
EmitBuildInfo,
|
|
BuildInvalidatedProjectOfBundle,
|
|
QueueReferencingProjects,
|
|
Done
|
|
}
|
|
|
|
function createBuildOrUpdateInvalidedProject<T extends BuilderProgram>(
|
|
kind: InvalidatedProjectKind.Build | InvalidatedProjectKind.UpdateBundle,
|
|
state: SolutionBuilderState<T>,
|
|
project: ResolvedConfigFileName,
|
|
projectPath: ResolvedConfigFilePath,
|
|
projectIndex: number,
|
|
config: ParsedCommandLine,
|
|
buildOrder: readonly ResolvedConfigFileName[],
|
|
): BuildInvalidedProject<T> | UpdateBundleProject<T> {
|
|
let step = kind === InvalidatedProjectKind.Build ? BuildStep.CreateProgram : BuildStep.EmitBundle;
|
|
let program: T | undefined;
|
|
let buildResult: BuildResultFlags | undefined;
|
|
let invalidatedProjectOfBundle: BuildInvalidedProject<T> | undefined;
|
|
|
|
return kind === InvalidatedProjectKind.Build ?
|
|
{
|
|
kind,
|
|
project,
|
|
projectPath,
|
|
buildOrder,
|
|
getCompilerOptions: () => config.options,
|
|
getCurrentDirectory: () => state.currentDirectory,
|
|
getBuilderProgram: () => withProgramOrUndefined(identity),
|
|
getProgram: () =>
|
|
withProgramOrUndefined(
|
|
program => program.getProgramOrUndefined()
|
|
),
|
|
getSourceFile: fileName =>
|
|
withProgramOrUndefined(
|
|
program => program.getSourceFile(fileName)
|
|
),
|
|
getSourceFiles: () =>
|
|
withProgramOrEmptyArray(
|
|
program => program.getSourceFiles()
|
|
),
|
|
getOptionsDiagnostics: cancellationToken =>
|
|
withProgramOrEmptyArray(
|
|
program => program.getOptionsDiagnostics(cancellationToken)
|
|
),
|
|
getGlobalDiagnostics: cancellationToken =>
|
|
withProgramOrEmptyArray(
|
|
program => program.getGlobalDiagnostics(cancellationToken)
|
|
),
|
|
getConfigFileParsingDiagnostics: () =>
|
|
withProgramOrEmptyArray(
|
|
program => program.getConfigFileParsingDiagnostics()
|
|
),
|
|
getSyntacticDiagnostics: (sourceFile, cancellationToken) =>
|
|
withProgramOrEmptyArray(
|
|
program => program.getSyntacticDiagnostics(sourceFile, cancellationToken)
|
|
),
|
|
getAllDependencies: sourceFile =>
|
|
withProgramOrEmptyArray(
|
|
program => program.getAllDependencies(sourceFile)
|
|
),
|
|
getSemanticDiagnostics: (sourceFile, cancellationToken) =>
|
|
withProgramOrEmptyArray(
|
|
program => program.getSemanticDiagnostics(sourceFile, cancellationToken)
|
|
),
|
|
getSemanticDiagnosticsOfNextAffectedFile: (cancellationToken, ignoreSourceFile) =>
|
|
withProgramOrUndefined(
|
|
program =>
|
|
((program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile) &&
|
|
(program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile(cancellationToken, ignoreSourceFile)
|
|
),
|
|
emit: (targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) => {
|
|
if (targetSourceFile || emitOnlyDtsFiles) {
|
|
return withProgramOrUndefined(
|
|
program => program.emit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers || state.host.getCustomTransformers?.(project))
|
|
);
|
|
}
|
|
executeSteps(BuildStep.SemanticDiagnostics, cancellationToken);
|
|
if (step === BuildStep.EmitBuildInfo) {
|
|
return emitBuildInfo(writeFile, cancellationToken);
|
|
}
|
|
if (step !== BuildStep.Emit) return undefined;
|
|
return emit(writeFile, cancellationToken, customTransformers);
|
|
},
|
|
done
|
|
} :
|
|
{
|
|
kind,
|
|
project,
|
|
projectPath,
|
|
buildOrder,
|
|
getCompilerOptions: () => config.options,
|
|
getCurrentDirectory: () => state.currentDirectory,
|
|
emit: (writeFile: WriteFileCallback | undefined, customTransformers: CustomTransformers | undefined) => {
|
|
if (step !== BuildStep.EmitBundle) return invalidatedProjectOfBundle;
|
|
return emitBundle(writeFile, customTransformers);
|
|
},
|
|
done,
|
|
};
|
|
|
|
function done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers) {
|
|
executeSteps(BuildStep.Done, cancellationToken, writeFile, customTransformers);
|
|
return doneInvalidatedProject(state, projectPath);
|
|
}
|
|
|
|
function withProgramOrUndefined<U>(action: (program: T) => U | undefined): U | undefined {
|
|
executeSteps(BuildStep.CreateProgram);
|
|
return program && action(program);
|
|
}
|
|
|
|
function withProgramOrEmptyArray<U>(action: (program: T) => readonly U[]): readonly U[] {
|
|
return withProgramOrUndefined(action) || emptyArray;
|
|
}
|
|
|
|
function createProgram() {
|
|
Debug.assert(program === undefined);
|
|
|
|
if (state.options.dry) {
|
|
reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, project);
|
|
buildResult = BuildResultFlags.Success;
|
|
step = BuildStep.QueueReferencingProjects;
|
|
return;
|
|
}
|
|
|
|
if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, project);
|
|
|
|
if (config.fileNames.length === 0) {
|
|
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
|
|
// Nothing to build - must be a solution file, basically
|
|
buildResult = BuildResultFlags.None;
|
|
step = BuildStep.QueueReferencingProjects;
|
|
return;
|
|
}
|
|
|
|
const { host, compilerHost } = state;
|
|
state.projectCompilerOptions = config.options;
|
|
// Update module resolution cache if needed
|
|
state.moduleResolutionCache?.update(config.options);
|
|
state.typeReferenceDirectiveResolutionCache?.update(config.options);
|
|
|
|
// Create program
|
|
program = host.createProgram(
|
|
config.fileNames,
|
|
config.options,
|
|
compilerHost,
|
|
getOldProgram(state, projectPath, config),
|
|
getConfigFileParsingDiagnostics(config),
|
|
config.projectReferences
|
|
);
|
|
state.lastCachedPackageJsonLookups.set(projectPath, state.moduleResolutionCache && map(
|
|
state.moduleResolutionCache.getPackageJsonInfoCache().entries(),
|
|
([path, data]) => ([state.host.realpath ? toPath(state, state.host.realpath(path)) : path, data] as const)
|
|
));
|
|
|
|
if (state.watch) {
|
|
state.builderPrograms.set(projectPath, program);
|
|
}
|
|
step++;
|
|
}
|
|
|
|
function handleDiagnostics(diagnostics: readonly Diagnostic[], errorFlags: BuildResultFlags, errorType: string) {
|
|
if (diagnostics.length) {
|
|
({ buildResult, step } = buildErrors(
|
|
state,
|
|
projectPath,
|
|
program,
|
|
config,
|
|
diagnostics,
|
|
errorFlags,
|
|
errorType
|
|
));
|
|
}
|
|
else {
|
|
step++;
|
|
}
|
|
}
|
|
|
|
function getSyntaxDiagnostics(cancellationToken?: CancellationToken) {
|
|
Debug.assertIsDefined(program);
|
|
handleDiagnostics(
|
|
[
|
|
...program.getConfigFileParsingDiagnostics(),
|
|
...program.getOptionsDiagnostics(cancellationToken),
|
|
...program.getGlobalDiagnostics(cancellationToken),
|
|
...program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken)
|
|
],
|
|
BuildResultFlags.SyntaxErrors,
|
|
"Syntactic"
|
|
);
|
|
}
|
|
|
|
function getSemanticDiagnostics(cancellationToken?: CancellationToken) {
|
|
handleDiagnostics(
|
|
Debug.checkDefined(program).getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken),
|
|
BuildResultFlags.TypeErrors,
|
|
"Semantic"
|
|
);
|
|
}
|
|
|
|
function emit(writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitResult {
|
|
Debug.assertIsDefined(program);
|
|
Debug.assert(step === BuildStep.Emit);
|
|
// Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly
|
|
program.backupState();
|
|
let declDiagnostics: Diagnostic[] | undefined;
|
|
const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d);
|
|
const outputFiles: OutputFile[] = [];
|
|
const { emitResult } = emitFilesAndReportErrors(
|
|
program,
|
|
reportDeclarationDiagnostics,
|
|
/*write*/ undefined,
|
|
/*reportSummary*/ undefined,
|
|
(name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }),
|
|
cancellationToken,
|
|
/*emitOnlyDts*/ false,
|
|
customTransformers || state.host.getCustomTransformers?.(project)
|
|
);
|
|
// Don't emit .d.ts if there are decl file errors
|
|
if (declDiagnostics) {
|
|
program.restoreState();
|
|
({ buildResult, step } = buildErrors(
|
|
state,
|
|
projectPath,
|
|
program,
|
|
config,
|
|
declDiagnostics,
|
|
BuildResultFlags.DeclarationEmitErrors,
|
|
"Declaration file"
|
|
));
|
|
return {
|
|
emitSkipped: true,
|
|
diagnostics: emitResult.diagnostics
|
|
};
|
|
}
|
|
|
|
// Actual Emit
|
|
const { host, compilerHost } = state;
|
|
let resultFlags = BuildResultFlags.DeclarationOutputUnchanged;
|
|
let newestDeclarationFileContentChangedTime = minimumDate;
|
|
let anyDtsChanged = false;
|
|
const emitterDiagnostics = createDiagnosticCollection();
|
|
const emittedOutputs = new Map<Path, string>();
|
|
outputFiles.forEach(({ name, text, writeByteOrderMark }) => {
|
|
let priorChangeTime: Date | undefined;
|
|
if (!anyDtsChanged && isDeclarationFile(name)) {
|
|
// Check for unchanged .d.ts files
|
|
if (host.fileExists(name) && state.readFileWithCache(name) === text) {
|
|
priorChangeTime = host.getModifiedTime(name);
|
|
}
|
|
else {
|
|
resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged;
|
|
anyDtsChanged = true;
|
|
}
|
|
}
|
|
|
|
emittedOutputs.set(toPath(state, name), name);
|
|
writeFile(writeFileCallback ? { writeFile: writeFileCallback } : compilerHost, emitterDiagnostics, name, text, writeByteOrderMark);
|
|
if (priorChangeTime !== undefined) {
|
|
newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime);
|
|
}
|
|
});
|
|
|
|
finishEmit(
|
|
emitterDiagnostics,
|
|
emittedOutputs,
|
|
newestDeclarationFileContentChangedTime,
|
|
/*newestDeclarationFileContentChangedTimeIsMaximumDate*/ anyDtsChanged,
|
|
outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()),
|
|
resultFlags
|
|
);
|
|
return emitResult;
|
|
}
|
|
|
|
function emitBuildInfo(writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult {
|
|
Debug.assertIsDefined(program);
|
|
Debug.assert(step === BuildStep.EmitBuildInfo);
|
|
const emitResult = program.emitBuildInfo(writeFileCallback, cancellationToken);
|
|
if (emitResult.diagnostics.length) {
|
|
reportErrors(state, emitResult.diagnostics);
|
|
state.diagnostics.set(projectPath, [...state.diagnostics.get(projectPath)!, ...emitResult.diagnostics]);
|
|
buildResult = BuildResultFlags.EmitErrors & buildResult!;
|
|
}
|
|
|
|
if (emitResult.emittedFiles && state.write) {
|
|
emitResult.emittedFiles.forEach(name => listEmittedFile(state, config, name));
|
|
}
|
|
afterProgramDone(state, program, config);
|
|
step = BuildStep.QueueReferencingProjects;
|
|
return emitResult;
|
|
}
|
|
|
|
function finishEmit(
|
|
emitterDiagnostics: DiagnosticCollection,
|
|
emittedOutputs: ESMap<Path, string>,
|
|
priorNewestUpdateTime: Date,
|
|
newestDeclarationFileContentChangedTimeIsMaximumDate: boolean,
|
|
oldestOutputFileName: string,
|
|
resultFlags: BuildResultFlags
|
|
) {
|
|
const emitDiagnostics = emitterDiagnostics.getDiagnostics();
|
|
if (emitDiagnostics.length) {
|
|
({ buildResult, step } = buildErrors(
|
|
state,
|
|
projectPath,
|
|
program,
|
|
config,
|
|
emitDiagnostics,
|
|
BuildResultFlags.EmitErrors,
|
|
"Emit"
|
|
));
|
|
return emitDiagnostics;
|
|
}
|
|
|
|
if (state.write) {
|
|
emittedOutputs.forEach(name => listEmittedFile(state, config, name));
|
|
}
|
|
|
|
// Update time stamps for rest of the outputs
|
|
const newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, priorNewestUpdateTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs);
|
|
state.diagnostics.delete(projectPath);
|
|
state.projectStatus.set(projectPath, {
|
|
type: UpToDateStatusType.UpToDate,
|
|
newestDeclarationFileContentChangedTime: newestDeclarationFileContentChangedTimeIsMaximumDate ?
|
|
maximumDate :
|
|
newestDeclarationFileContentChangedTime,
|
|
oldestOutputFileName
|
|
});
|
|
afterProgramDone(state, program, config);
|
|
step = BuildStep.QueueReferencingProjects;
|
|
buildResult = resultFlags;
|
|
return emitDiagnostics;
|
|
}
|
|
|
|
function emitBundle(writeFileCallback?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject<T> | undefined {
|
|
Debug.assert(kind === InvalidatedProjectKind.UpdateBundle);
|
|
if (state.options.dry) {
|
|
reportStatus(state, Diagnostics.A_non_dry_build_would_update_output_of_project_0, project);
|
|
buildResult = BuildResultFlags.Success;
|
|
step = BuildStep.QueueReferencingProjects;
|
|
return undefined;
|
|
}
|
|
|
|
if (state.options.verbose) reportStatus(state, Diagnostics.Updating_output_of_project_0, project);
|
|
|
|
// Update js, and source map
|
|
const { compilerHost } = state;
|
|
state.projectCompilerOptions = config.options;
|
|
const outputFiles = emitUsingBuildInfo(
|
|
config,
|
|
compilerHost,
|
|
ref => {
|
|
const refName = resolveProjectName(state, ref.path);
|
|
return parseConfigFile(state, refName, toResolvedConfigFilePath(state, refName));
|
|
},
|
|
customTransformers || state.host.getCustomTransformers?.(project)
|
|
);
|
|
|
|
if (isString(outputFiles)) {
|
|
reportStatus(state, Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, project, relName(state, outputFiles));
|
|
step = BuildStep.BuildInvalidatedProjectOfBundle;
|
|
return invalidatedProjectOfBundle = createBuildOrUpdateInvalidedProject(
|
|
InvalidatedProjectKind.Build,
|
|
state,
|
|
project,
|
|
projectPath,
|
|
projectIndex,
|
|
config,
|
|
buildOrder
|
|
) as BuildInvalidedProject<T>;
|
|
}
|
|
|
|
// Actual Emit
|
|
Debug.assert(!!outputFiles.length);
|
|
const emitterDiagnostics = createDiagnosticCollection();
|
|
const emittedOutputs = new Map<Path, string>();
|
|
outputFiles.forEach(({ name, text, writeByteOrderMark }) => {
|
|
emittedOutputs.set(toPath(state, name), name);
|
|
writeFile(writeFileCallback ? { writeFile: writeFileCallback } : compilerHost, emitterDiagnostics, name, text, writeByteOrderMark);
|
|
});
|
|
|
|
const emitDiagnostics = finishEmit(
|
|
emitterDiagnostics,
|
|
emittedOutputs,
|
|
minimumDate,
|
|
/*newestDeclarationFileContentChangedTimeIsMaximumDate*/ false,
|
|
outputFiles[0].name,
|
|
BuildResultFlags.DeclarationOutputUnchanged
|
|
);
|
|
return { emitSkipped: false, diagnostics: emitDiagnostics };
|
|
}
|
|
|
|
function executeSteps(till: BuildStep, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers) {
|
|
while (step <= till && step < BuildStep.Done) {
|
|
const currentStep = step;
|
|
switch (step) {
|
|
case BuildStep.CreateProgram:
|
|
createProgram();
|
|
break;
|
|
|
|
case BuildStep.SyntaxDiagnostics:
|
|
getSyntaxDiagnostics(cancellationToken);
|
|
break;
|
|
|
|
case BuildStep.SemanticDiagnostics:
|
|
getSemanticDiagnostics(cancellationToken);
|
|
break;
|
|
|
|
case BuildStep.Emit:
|
|
emit(writeFile, cancellationToken, customTransformers);
|
|
break;
|
|
|
|
case BuildStep.EmitBuildInfo:
|
|
emitBuildInfo(writeFile, cancellationToken);
|
|
break;
|
|
|
|
case BuildStep.EmitBundle:
|
|
emitBundle(writeFile, customTransformers);
|
|
break;
|
|
|
|
case BuildStep.BuildInvalidatedProjectOfBundle:
|
|
Debug.checkDefined(invalidatedProjectOfBundle).done(cancellationToken, writeFile, customTransformers);
|
|
step = BuildStep.Done;
|
|
break;
|
|
|
|
case BuildStep.QueueReferencingProjects:
|
|
queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, Debug.checkDefined(buildResult));
|
|
step++;
|
|
break;
|
|
|
|
// Should never be done
|
|
case BuildStep.Done:
|
|
default:
|
|
assertType<BuildStep.Done>(step);
|
|
|
|
}
|
|
Debug.assert(step > currentStep);
|
|
}
|
|
}
|
|
}
|
|
|
|
function needsBuild({ options }: SolutionBuilderState, status: UpToDateStatus, config: ParsedCommandLine) {
|
|
if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true;
|
|
return config.fileNames.length === 0 ||
|
|
!!getConfigFileParsingDiagnostics(config).length ||
|
|
!isIncrementalCompilation(config.options);
|
|
}
|
|
|
|
function getNextInvalidatedProject<T extends BuilderProgram>(
|
|
state: SolutionBuilderState<T>,
|
|
buildOrder: AnyBuildOrder,
|
|
reportQueue: boolean
|
|
): InvalidatedProject<T> | undefined {
|
|
if (!state.projectPendingBuild.size) return undefined;
|
|
if (isCircularBuildOrder(buildOrder)) return undefined;
|
|
if (state.currentInvalidatedProject) {
|
|
// Only if same buildOrder the currentInvalidated project can be sent again
|
|
return arrayIsEqualTo(state.currentInvalidatedProject.buildOrder, buildOrder) ?
|
|
state.currentInvalidatedProject :
|
|
undefined;
|
|
}
|
|
|
|
const { options, projectPendingBuild } = state;
|
|
for (let projectIndex = 0; projectIndex < buildOrder.length; projectIndex++) {
|
|
const project = buildOrder[projectIndex];
|
|
const projectPath = toResolvedConfigFilePath(state, project);
|
|
const reloadLevel = state.projectPendingBuild.get(projectPath);
|
|
if (reloadLevel === undefined) continue;
|
|
|
|
if (reportQueue) {
|
|
reportQueue = false;
|
|
reportBuildQueue(state, buildOrder);
|
|
}
|
|
|
|
const config = parseConfigFile(state, project, projectPath);
|
|
if (!config) {
|
|
reportParseConfigFileDiagnostic(state, projectPath);
|
|
projectPendingBuild.delete(projectPath);
|
|
continue;
|
|
}
|
|
|
|
if (reloadLevel === ConfigFileProgramReloadLevel.Full) {
|
|
watchConfigFile(state, project, projectPath, config);
|
|
watchExtendedConfigFiles(state, projectPath, config);
|
|
watchWildCardDirectories(state, project, projectPath, config);
|
|
watchInputFiles(state, project, projectPath, config);
|
|
watchPackageJsonFiles(state, project, projectPath, config);
|
|
}
|
|
else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) {
|
|
// Update file names
|
|
config.fileNames = getFileNamesFromConfigSpecs(config.options.configFile!.configFileSpecs!, getDirectoryPath(project), config.options, state.parseConfigFileHost);
|
|
updateErrorForNoInputFiles(config.fileNames, project, config.options.configFile!.configFileSpecs!, config.errors, canJsonReportNoInputFiles(config.raw));
|
|
watchInputFiles(state, project, projectPath, config);
|
|
watchPackageJsonFiles(state, project, projectPath, config);
|
|
}
|
|
|
|
const status = getUpToDateStatus(state, config, projectPath);
|
|
verboseReportProjectStatus(state, project, status);
|
|
if (!options.force) {
|
|
if (status.type === UpToDateStatusType.UpToDate) {
|
|
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
|
|
projectPendingBuild.delete(projectPath);
|
|
// Up to date, skip
|
|
if (options.dry) {
|
|
// In a dry build, inform the user of this fact
|
|
reportStatus(state, Diagnostics.Project_0_is_up_to_date, project);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) {
|
|
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
|
|
return createUpdateOutputFileStampsProject(
|
|
state,
|
|
project,
|
|
projectPath,
|
|
config,
|
|
buildOrder
|
|
);
|
|
}
|
|
}
|
|
|
|
if (status.type === UpToDateStatusType.UpstreamBlocked) {
|
|
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
|
|
projectPendingBuild.delete(projectPath);
|
|
if (options.verbose) {
|
|
reportStatus(
|
|
state,
|
|
status.upstreamProjectBlocked ?
|
|
Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_was_not_built :
|
|
Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors,
|
|
project,
|
|
status.upstreamProjectName
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (status.type === UpToDateStatusType.ContainerOnly) {
|
|
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
|
|
projectPendingBuild.delete(projectPath);
|
|
// Do nothing
|
|
continue;
|
|
}
|
|
|
|
return createBuildOrUpdateInvalidedProject(
|
|
needsBuild(state, status, config) ?
|
|
InvalidatedProjectKind.Build :
|
|
InvalidatedProjectKind.UpdateBundle,
|
|
state,
|
|
project,
|
|
projectPath,
|
|
projectIndex,
|
|
config,
|
|
buildOrder,
|
|
);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function listEmittedFile({ write }: SolutionBuilderState, proj: ParsedCommandLine, file: string) {
|
|
if (write && proj.options.listEmittedFiles) {
|
|
write(`TSFILE: ${file}`);
|
|
}
|
|
}
|
|
|
|
function getOldProgram<T extends BuilderProgram>({ options, builderPrograms, compilerHost }: SolutionBuilderState<T>, proj: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
|
|
if (options.force) return undefined;
|
|
const value = builderPrograms.get(proj);
|
|
if (value) return value;
|
|
return readBuilderProgram(parsed.options, compilerHost) as any as T;
|
|
}
|
|
|
|
function afterProgramDone<T extends BuilderProgram>(
|
|
state: SolutionBuilderState<T>,
|
|
program: T | undefined,
|
|
config: ParsedCommandLine
|
|
) {
|
|
if (program) {
|
|
if (program && state.write) listFiles(program, state.write);
|
|
if (state.host.afterProgramEmitAndDiagnostics) {
|
|
state.host.afterProgramEmitAndDiagnostics(program);
|
|
}
|
|
program.releaseProgram();
|
|
}
|
|
else if (state.host.afterEmitBundle) {
|
|
state.host.afterEmitBundle(config);
|
|
}
|
|
state.projectCompilerOptions = state.baseCompilerOptions;
|
|
}
|
|
|
|
function buildErrors<T extends BuilderProgram>(
|
|
state: SolutionBuilderState<T>,
|
|
resolvedPath: ResolvedConfigFilePath,
|
|
program: T | undefined,
|
|
config: ParsedCommandLine,
|
|
diagnostics: readonly Diagnostic[],
|
|
buildResult: BuildResultFlags,
|
|
errorType: string,
|
|
) {
|
|
const canEmitBuildInfo = !(buildResult & BuildResultFlags.SyntaxErrors) && program && !outFile(program.getCompilerOptions());
|
|
|
|
reportAndStoreErrors(state, resolvedPath, diagnostics);
|
|
state.projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: `${errorType} errors` });
|
|
if (canEmitBuildInfo) return { buildResult, step: BuildStep.EmitBuildInfo };
|
|
afterProgramDone(state, program, config);
|
|
return { buildResult, step: BuildStep.QueueReferencingProjects };
|
|
}
|
|
|
|
function checkConfigFileUpToDateStatus(state: SolutionBuilderState, configFile: string, oldestOutputFileTime: Date, oldestOutputFileName: string): Status.OutOfDateWithSelf | undefined {
|
|
// Check tsconfig time
|
|
const tsconfigTime = getModifiedTime(state.host, configFile);
|
|
if (oldestOutputFileTime < tsconfigTime) {
|
|
return {
|
|
type: UpToDateStatusType.OutOfDateWithSelf,
|
|
outOfDateOutputFileName: oldestOutputFileName,
|
|
newerInputFileName: configFile
|
|
};
|
|
}
|
|
}
|
|
|
|
function getUpToDateStatusWorker(state: SolutionBuilderState, project: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath): UpToDateStatus {
|
|
const force = !!state.options.force;
|
|
let newestInputFileName: string = undefined!;
|
|
let newestInputFileTime = minimumDate;
|
|
const { host } = state;
|
|
// Get timestamps of input files
|
|
for (const inputFile of project.fileNames) {
|
|
if (!host.fileExists(inputFile)) {
|
|
return {
|
|
type: UpToDateStatusType.Unbuildable,
|
|
reason: `${inputFile} does not exist`
|
|
};
|
|
}
|
|
|
|
if (!force) {
|
|
const inputTime = getModifiedTime(host, inputFile);
|
|
if (inputTime > newestInputFileTime) {
|
|
newestInputFileName = inputFile;
|
|
newestInputFileTime = inputTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Container if no files are specified in the project
|
|
if (!project.fileNames.length && !canJsonReportNoInputFiles(project.raw)) {
|
|
return {
|
|
type: UpToDateStatusType.ContainerOnly
|
|
};
|
|
}
|
|
|
|
// Collect the expected outputs of this project
|
|
const outputs = getAllProjectOutputs(project, !host.useCaseSensitiveFileNames());
|
|
|
|
// 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;
|
|
if (!force) {
|
|
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)) {
|
|
missingOutputFileName = output;
|
|
break;
|
|
}
|
|
|
|
const outputTime = getModifiedTime(host, 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 outputModifiedTime = getModifiedTime(host, output);
|
|
newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, outputModifiedTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
let pseudoUpToDate = false;
|
|
let usesPrepend = false;
|
|
let upstreamChangedProject: string | undefined;
|
|
if (project.projectReferences) {
|
|
state.projectStatus.set(resolvedPath, { type: UpToDateStatusType.ComputingUpstream });
|
|
for (const ref of project.projectReferences) {
|
|
usesPrepend = usesPrepend || !!(ref.prepend);
|
|
const resolvedRef = resolveProjectReferencePath(ref);
|
|
const resolvedRefPath = toResolvedConfigFilePath(state, resolvedRef);
|
|
const refStatus = getUpToDateStatus(state, parseConfigFile(state, resolvedRef, resolvedRefPath), resolvedRefPath);
|
|
|
|
// Its a circular reference ignore the status of this project
|
|
if (refStatus.type === UpToDateStatusType.ComputingUpstream ||
|
|
refStatus.type === UpToDateStatusType.ContainerOnly) { // Container only ignore this project
|
|
continue;
|
|
}
|
|
|
|
// An upstream project is blocked
|
|
if (refStatus.type === UpToDateStatusType.Unbuildable ||
|
|
refStatus.type === UpToDateStatusType.UpstreamBlocked) {
|
|
return {
|
|
type: UpToDateStatusType.UpstreamBlocked,
|
|
upstreamProjectName: ref.path,
|
|
upstreamProjectBlocked: refStatus.type === UpToDateStatusType.UpstreamBlocked
|
|
};
|
|
}
|
|
|
|
// 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
|
|
};
|
|
}
|
|
|
|
// Check oldest output file name only if there is no missing output file name
|
|
// (a check we will have skipped if this is a forced build)
|
|
if (!force && !missingOutputFileName) {
|
|
// 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 && 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 && refStatus.newestDeclarationFileContentChangedTime <= oldestOutputFileTime) {
|
|
pseudoUpToDate = true;
|
|
upstreamChangedProject = ref.path;
|
|
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
|
|
};
|
|
}
|
|
else {
|
|
// Check tsconfig time
|
|
const configStatus = checkConfigFileUpToDateStatus(state, project.options.configFilePath!, oldestOutputFileTime, oldestOutputFileName);
|
|
if (configStatus) return configStatus;
|
|
|
|
// Check extended config time
|
|
const extendedConfigStatus = forEach(project.options.configFile!.extendedSourceFiles || emptyArray, configFile => checkConfigFileUpToDateStatus(state, configFile, oldestOutputFileTime, oldestOutputFileName));
|
|
if (extendedConfigStatus) return extendedConfigStatus;
|
|
|
|
// Check package file time
|
|
const dependentPackageFileStatus = forEach(
|
|
state.lastCachedPackageJsonLookups.get(resolvedPath) || emptyArray,
|
|
([path]) => checkConfigFileUpToDateStatus(state, path, oldestOutputFileTime, oldestOutputFileName)
|
|
);
|
|
if (dependentPackageFileStatus) return dependentPackageFileStatus;
|
|
}
|
|
|
|
if (!force && !state.buildInfoChecked.has(resolvedPath)) {
|
|
state.buildInfoChecked.set(resolvedPath, true);
|
|
const buildInfoPath = getTsBuildInfoEmitOutputFilePath(project.options);
|
|
if (buildInfoPath) {
|
|
const value = state.readFileWithCache(buildInfoPath);
|
|
const buildInfo = value && getBuildInfo(value);
|
|
if (buildInfo && (buildInfo.bundle || buildInfo.program) && buildInfo.version !== version) {
|
|
return {
|
|
type: UpToDateStatusType.TsVersionOutputOfDate,
|
|
version: buildInfo.version
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (usesPrepend && pseudoUpToDate) {
|
|
return {
|
|
type: UpToDateStatusType.OutOfDateWithPrepend,
|
|
outOfDateOutputFileName: oldestOutputFileName,
|
|
newerProjectName: upstreamChangedProject!
|
|
};
|
|
}
|
|
|
|
// Up to date
|
|
return {
|
|
type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate,
|
|
newestDeclarationFileContentChangedTime,
|
|
newestInputFileTime,
|
|
newestOutputFileTime,
|
|
newestInputFileName,
|
|
newestOutputFileName,
|
|
oldestOutputFileName
|
|
};
|
|
}
|
|
|
|
function getUpToDateStatus(state: SolutionBuilderState, project: ParsedCommandLine | undefined, resolvedPath: ResolvedConfigFilePath): UpToDateStatus {
|
|
if (project === undefined) {
|
|
return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" };
|
|
}
|
|
|
|
const prior = state.projectStatus.get(resolvedPath);
|
|
if (prior !== undefined) {
|
|
return prior;
|
|
}
|
|
|
|
const actual = getUpToDateStatusWorker(state, project, resolvedPath);
|
|
state.projectStatus.set(resolvedPath, actual);
|
|
return actual;
|
|
}
|
|
|
|
function updateOutputTimestampsWorker(state: SolutionBuilderState, proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: ESMap<Path, string>) {
|
|
if (proj.options.noEmit) return priorNewestUpdateTime;
|
|
const { host } = state;
|
|
const outputs = getAllProjectOutputs(proj, !host.useCaseSensitiveFileNames());
|
|
if (!skipOutputs || outputs.length !== skipOutputs.size) {
|
|
let reportVerbose = !!state.options.verbose;
|
|
const now = host.now ? host.now() : new Date();
|
|
for (const file of outputs) {
|
|
if (skipOutputs && skipOutputs.has(toPath(state, file))) {
|
|
continue;
|
|
}
|
|
|
|
if (reportVerbose) {
|
|
reportVerbose = false;
|
|
reportStatus(state, verboseMessage, proj.options.configFilePath!);
|
|
}
|
|
|
|
if (isDeclarationFile(file)) {
|
|
priorNewestUpdateTime = newer(priorNewestUpdateTime, getModifiedTime(host, file));
|
|
}
|
|
|
|
host.setModifiedTime(file, now);
|
|
}
|
|
}
|
|
|
|
return priorNewestUpdateTime;
|
|
}
|
|
|
|
function updateOutputTimestamps(state: SolutionBuilderState, proj: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath) {
|
|
if (state.options.dry) {
|
|
return reportStatus(state, Diagnostics.A_non_dry_build_would_update_timestamps_for_output_of_project_0, proj.options.configFilePath!);
|
|
}
|
|
const priorNewestUpdateTime = updateOutputTimestampsWorker(state, proj, minimumDate, Diagnostics.Updating_output_timestamps_of_project_0);
|
|
state.projectStatus.set(resolvedPath, {
|
|
type: UpToDateStatusType.UpToDate,
|
|
newestDeclarationFileContentChangedTime: priorNewestUpdateTime,
|
|
oldestOutputFileName: getFirstProjectOutput(proj, !state.host.useCaseSensitiveFileNames())
|
|
});
|
|
}
|
|
|
|
function queueReferencingProjects(
|
|
state: SolutionBuilderState,
|
|
project: ResolvedConfigFileName,
|
|
projectPath: ResolvedConfigFilePath,
|
|
projectIndex: number,
|
|
config: ParsedCommandLine,
|
|
buildOrder: readonly ResolvedConfigFileName[],
|
|
buildResult: BuildResultFlags
|
|
) {
|
|
// Queue only if there are no errors
|
|
if (buildResult & BuildResultFlags.AnyErrors) return;
|
|
// Only composite projects can be referenced by other projects
|
|
if (!config.options.composite) return;
|
|
// Always use build order to queue projects
|
|
for (let index = projectIndex + 1; index < buildOrder.length; index++) {
|
|
const nextProject = buildOrder[index];
|
|
const nextProjectPath = toResolvedConfigFilePath(state, nextProject);
|
|
if (state.projectPendingBuild.has(nextProjectPath)) continue;
|
|
|
|
const nextProjectConfig = parseConfigFile(state, nextProject, nextProjectPath);
|
|
if (!nextProjectConfig || !nextProjectConfig.projectReferences) continue;
|
|
for (const ref of nextProjectConfig.projectReferences) {
|
|
const resolvedRefPath = resolveProjectName(state, ref.path);
|
|
if (toResolvedConfigFilePath(state, resolvedRefPath) !== projectPath) continue;
|
|
// If the project is referenced with prepend, always build downstream projects,
|
|
// If declaration output is changed, build the project
|
|
// otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps
|
|
const status = state.projectStatus.get(nextProjectPath);
|
|
if (status) {
|
|
switch (status.type) {
|
|
case UpToDateStatusType.UpToDate:
|
|
if (buildResult & BuildResultFlags.DeclarationOutputUnchanged) {
|
|
if (ref.prepend) {
|
|
state.projectStatus.set(nextProjectPath, {
|
|
type: UpToDateStatusType.OutOfDateWithPrepend,
|
|
outOfDateOutputFileName: status.oldestOutputFileName,
|
|
newerProjectName: project
|
|
});
|
|
}
|
|
else {
|
|
status.type = UpToDateStatusType.UpToDateWithUpstreamTypes;
|
|
}
|
|
break;
|
|
}
|
|
// falls through
|
|
|
|
case UpToDateStatusType.UpToDateWithUpstreamTypes:
|
|
case UpToDateStatusType.OutOfDateWithPrepend:
|
|
if (!(buildResult & BuildResultFlags.DeclarationOutputUnchanged)) {
|
|
state.projectStatus.set(nextProjectPath, {
|
|
type: UpToDateStatusType.OutOfDateWithUpstream,
|
|
outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName,
|
|
newerProjectName: project
|
|
});
|
|
}
|
|
break;
|
|
|
|
case UpToDateStatusType.UpstreamBlocked:
|
|
if (toResolvedConfigFilePath(state, resolveProjectName(state, status.upstreamProjectName)) === projectPath) {
|
|
clearProjectStatus(state, nextProjectPath);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
addProjToQueue(state, nextProjectPath, ConfigFileProgramReloadLevel.None);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function build(state: SolutionBuilderState, project?: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers, onlyReferences?: boolean): ExitStatus {
|
|
const buildOrder = getBuildOrderFor(state, project, onlyReferences);
|
|
if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped;
|
|
|
|
setupInitialBuild(state, cancellationToken);
|
|
|
|
let reportQueue = true;
|
|
let successfulProjects = 0;
|
|
while (true) {
|
|
const invalidatedProject = getNextInvalidatedProject(state, buildOrder, reportQueue);
|
|
if (!invalidatedProject) break;
|
|
reportQueue = false;
|
|
invalidatedProject.done(cancellationToken, writeFile, getCustomTransformers?.(invalidatedProject.project));
|
|
if (!state.diagnostics.has(invalidatedProject.projectPath)) successfulProjects++;
|
|
}
|
|
|
|
disableCache(state);
|
|
reportErrorSummary(state, buildOrder);
|
|
startWatching(state, buildOrder);
|
|
|
|
return isCircularBuildOrder(buildOrder)
|
|
? ExitStatus.ProjectReferenceCycle_OutputsSkipped
|
|
: !buildOrder.some(p => state.diagnostics.has(toResolvedConfigFilePath(state, p)))
|
|
? ExitStatus.Success
|
|
: successfulProjects
|
|
? ExitStatus.DiagnosticsPresent_OutputsGenerated
|
|
: ExitStatus.DiagnosticsPresent_OutputsSkipped;
|
|
}
|
|
|
|
function clean(state: SolutionBuilderState, project?: string, onlyReferences?: boolean) {
|
|
const buildOrder = getBuildOrderFor(state, project, onlyReferences);
|
|
if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped;
|
|
|
|
if (isCircularBuildOrder(buildOrder)) {
|
|
reportErrors(state, buildOrder.circularDiagnostics);
|
|
return ExitStatus.ProjectReferenceCycle_OutputsSkipped;
|
|
}
|
|
|
|
const { options, host } = state;
|
|
const filesToDelete = options.dry ? [] as string[] : undefined;
|
|
for (const proj of buildOrder) {
|
|
const resolvedPath = toResolvedConfigFilePath(state, proj);
|
|
const parsed = parseConfigFile(state, proj, resolvedPath);
|
|
if (parsed === undefined) {
|
|
// File has gone missing; fine to ignore here
|
|
reportParseConfigFileDiagnostic(state, resolvedPath);
|
|
continue;
|
|
}
|
|
const outputs = getAllProjectOutputs(parsed, !host.useCaseSensitiveFileNames());
|
|
if (!outputs.length) continue;
|
|
const inputFileNames = new Set(parsed.fileNames.map(f => toPath(state, f)));
|
|
for (const output of outputs) {
|
|
// If output name is same as input file name, do not delete and ignore the error
|
|
if (inputFileNames.has(toPath(state, output))) continue;
|
|
if (host.fileExists(output)) {
|
|
if (filesToDelete) {
|
|
filesToDelete.push(output);
|
|
}
|
|
else {
|
|
host.deleteFile(output);
|
|
invalidateProject(state, resolvedPath, ConfigFileProgramReloadLevel.None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (filesToDelete) {
|
|
reportStatus(state, Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""));
|
|
}
|
|
|
|
return ExitStatus.Success;
|
|
}
|
|
|
|
function invalidateProject(state: SolutionBuilderState, resolved: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) {
|
|
// If host implements getParsedCommandLine, we cant get list of files from parseConfigFileHost
|
|
if (state.host.getParsedCommandLine && reloadLevel === ConfigFileProgramReloadLevel.Partial) {
|
|
reloadLevel = ConfigFileProgramReloadLevel.Full;
|
|
}
|
|
if (reloadLevel === ConfigFileProgramReloadLevel.Full) {
|
|
state.configFileCache.delete(resolved);
|
|
state.buildOrder = undefined;
|
|
}
|
|
state.needsSummary = true;
|
|
clearProjectStatus(state, resolved);
|
|
addProjToQueue(state, resolved, reloadLevel);
|
|
enableCache(state);
|
|
}
|
|
|
|
function invalidateProjectAndScheduleBuilds(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) {
|
|
state.reportFileChangeDetected = true;
|
|
invalidateProject(state, resolvedPath, reloadLevel);
|
|
scheduleBuildInvalidatedProject(state);
|
|
}
|
|
|
|
function scheduleBuildInvalidatedProject(state: SolutionBuilderState) {
|
|
const { hostWithWatch } = state;
|
|
if (!hostWithWatch.setTimeout || !hostWithWatch.clearTimeout) {
|
|
return;
|
|
}
|
|
if (state.timerToBuildInvalidatedProject) {
|
|
hostWithWatch.clearTimeout(state.timerToBuildInvalidatedProject);
|
|
}
|
|
state.timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildNextInvalidatedProject, 250, state);
|
|
}
|
|
|
|
function buildNextInvalidatedProject(state: SolutionBuilderState) {
|
|
state.timerToBuildInvalidatedProject = undefined;
|
|
if (state.reportFileChangeDetected) {
|
|
state.reportFileChangeDetected = false;
|
|
state.projectErrorsReported.clear();
|
|
reportWatchStatus(state, Diagnostics.File_change_detected_Starting_incremental_compilation);
|
|
}
|
|
const buildOrder = getBuildOrder(state);
|
|
const invalidatedProject = getNextInvalidatedProject(state, buildOrder, /*reportQueue*/ false);
|
|
if (invalidatedProject) {
|
|
invalidatedProject.done();
|
|
if (state.projectPendingBuild.size) {
|
|
// Schedule next project for build
|
|
if (state.watch && !state.timerToBuildInvalidatedProject) {
|
|
scheduleBuildInvalidatedProject(state);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
disableCache(state);
|
|
reportErrorSummary(state, buildOrder);
|
|
}
|
|
|
|
function watchConfigFile(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) {
|
|
if (!state.watch || state.allWatchedConfigFiles.has(resolvedPath)) return;
|
|
state.allWatchedConfigFiles.set(resolvedPath, state.watchFile(
|
|
resolved,
|
|
() => {
|
|
invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Full);
|
|
},
|
|
PollingInterval.High,
|
|
parsed?.watchOptions,
|
|
WatchType.ConfigFile,
|
|
resolved
|
|
));
|
|
}
|
|
|
|
function watchExtendedConfigFiles(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) {
|
|
updateSharedExtendedConfigFileWatcher(
|
|
resolvedPath,
|
|
parsed?.options,
|
|
state.allWatchedExtendedConfigFiles,
|
|
(extendedConfigFileName, extendedConfigFilePath) => state.watchFile(
|
|
extendedConfigFileName,
|
|
() => state.allWatchedExtendedConfigFiles.get(extendedConfigFilePath)?.projects.forEach(projectConfigFilePath =>
|
|
invalidateProjectAndScheduleBuilds(state, projectConfigFilePath, ConfigFileProgramReloadLevel.Full)
|
|
),
|
|
PollingInterval.High,
|
|
parsed?.watchOptions,
|
|
WatchType.ExtendedConfigFile,
|
|
),
|
|
fileName => toPath(state, fileName),
|
|
);
|
|
}
|
|
|
|
function watchWildCardDirectories(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
|
|
if (!state.watch) return;
|
|
updateWatchingWildcardDirectories(
|
|
getOrCreateValueMapFromConfigFileMap(state.allWatchedWildcardDirectories, resolvedPath),
|
|
new Map(getEntries(parsed.wildcardDirectories!)),
|
|
(dir, flags) => state.watchDirectory(
|
|
dir,
|
|
fileOrDirectory => {
|
|
if (isIgnoredFileFromWildCardWatching({
|
|
watchedDirPath: toPath(state, dir),
|
|
fileOrDirectory,
|
|
fileOrDirectoryPath: toPath(state, fileOrDirectory),
|
|
configFileName: resolved,
|
|
currentDirectory: state.currentDirectory,
|
|
options: parsed.options,
|
|
program: state.builderPrograms.get(resolvedPath) || getCachedParsedConfigFile(state, resolvedPath)?.fileNames,
|
|
useCaseSensitiveFileNames: state.parseConfigFileHost.useCaseSensitiveFileNames,
|
|
writeLog: s => state.writeLog(s),
|
|
toPath: fileName => toPath(state, fileName)
|
|
})) return;
|
|
|
|
invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Partial);
|
|
},
|
|
flags,
|
|
parsed?.watchOptions,
|
|
WatchType.WildcardDirectory,
|
|
resolved
|
|
)
|
|
);
|
|
}
|
|
|
|
function watchInputFiles(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
|
|
if (!state.watch) return;
|
|
mutateMap(
|
|
getOrCreateValueMapFromConfigFileMap(state.allWatchedInputFiles, resolvedPath),
|
|
arrayToMap(parsed.fileNames, fileName => toPath(state, fileName)),
|
|
{
|
|
createNewValue: (_path, input) => state.watchFile(
|
|
input,
|
|
() => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.None),
|
|
PollingInterval.Low,
|
|
parsed?.watchOptions,
|
|
WatchType.SourceFile,
|
|
resolved
|
|
),
|
|
onDeleteValue: closeFileWatcher,
|
|
}
|
|
);
|
|
}
|
|
|
|
function watchPackageJsonFiles(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
|
|
if (!state.watch || !state.lastCachedPackageJsonLookups) return;
|
|
mutateMap(
|
|
getOrCreateValueMapFromConfigFileMap(state.allWatchedPackageJsonFiles, resolvedPath),
|
|
new Map(state.lastCachedPackageJsonLookups.get(resolvedPath)),
|
|
{
|
|
createNewValue: (path, _input) => state.watchFile(
|
|
path,
|
|
() => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Full),
|
|
PollingInterval.High,
|
|
parsed?.watchOptions,
|
|
WatchType.PackageJson,
|
|
resolved
|
|
),
|
|
onDeleteValue: closeFileWatcher,
|
|
}
|
|
);
|
|
}
|
|
|
|
function startWatching(state: SolutionBuilderState, buildOrder: AnyBuildOrder) {
|
|
if (!state.watchAllProjectsPending) return;
|
|
state.watchAllProjectsPending = false;
|
|
for (const resolved of getBuildOrderFromAnyBuildOrder(buildOrder)) {
|
|
const resolvedPath = toResolvedConfigFilePath(state, resolved);
|
|
const cfg = parseConfigFile(state, resolved, resolvedPath);
|
|
// Watch this file
|
|
watchConfigFile(state, resolved, resolvedPath, cfg);
|
|
watchExtendedConfigFiles(state, resolvedPath, cfg);
|
|
if (cfg) {
|
|
// Update watchers for wildcard directories
|
|
watchWildCardDirectories(state, resolved, resolvedPath, cfg);
|
|
|
|
// Watch input files
|
|
watchInputFiles(state, resolved, resolvedPath, cfg);
|
|
|
|
// Watch package json files
|
|
watchPackageJsonFiles(state, resolved, resolvedPath, cfg);
|
|
}
|
|
}
|
|
}
|
|
|
|
function stopWatching(state: SolutionBuilderState) {
|
|
clearMap(state.allWatchedConfigFiles, closeFileWatcher);
|
|
clearMap(state.allWatchedExtendedConfigFiles, closeFileWatcherOf);
|
|
clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf));
|
|
clearMap(state.allWatchedInputFiles, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcher));
|
|
clearMap(state.allWatchedPackageJsonFiles, watchedPacageJsonFiles => clearMap(watchedPacageJsonFiles, closeFileWatcher));
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function createSolutionBuilderWorker<T extends BuilderProgram>(watch: false, host: SolutionBuilderHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder<T>;
|
|
function createSolutionBuilderWorker<T extends BuilderProgram>(watch: true, host: SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder<T>;
|
|
function createSolutionBuilderWorker<T extends BuilderProgram>(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost<T> | SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], options: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder<T> {
|
|
const state = createSolutionBuilderState(watch, hostOrHostWithWatch, rootNames, options, baseWatchOptions);
|
|
return {
|
|
build: (project, cancellationToken, writeFile, getCustomTransformers) => build(state, project, cancellationToken, writeFile, getCustomTransformers),
|
|
clean: project => clean(state, project),
|
|
buildReferences: (project, cancellationToken, writeFile, getCustomTransformers) => build(state, project, cancellationToken, writeFile, getCustomTransformers, /*onlyReferences*/ true),
|
|
cleanReferences: project => clean(state, project, /*onlyReferences*/ true),
|
|
getNextInvalidatedProject: cancellationToken => {
|
|
setupInitialBuild(state, cancellationToken);
|
|
return getNextInvalidatedProject(state, getBuildOrder(state), /*reportQueue*/ false);
|
|
},
|
|
getBuildOrder: () => getBuildOrder(state),
|
|
getUpToDateStatusOfProject: project => {
|
|
const configFileName = resolveProjectName(state, project);
|
|
const configFilePath = toResolvedConfigFilePath(state, configFileName);
|
|
return getUpToDateStatus(state, parseConfigFile(state, configFileName, configFilePath), configFilePath);
|
|
},
|
|
invalidateProject: (configFilePath, reloadLevel) => invalidateProject(state, configFilePath, reloadLevel || ConfigFileProgramReloadLevel.None),
|
|
buildNextInvalidatedProject: () => buildNextInvalidatedProject(state),
|
|
getAllParsedConfigs: () => arrayFrom(mapDefinedIterator(
|
|
state.configFileCache.values(),
|
|
config => isParsedCommandLine(config) ? config : undefined
|
|
)),
|
|
close: () => stopWatching(state),
|
|
};
|
|
}
|
|
|
|
function relName(state: SolutionBuilderState, path: string): string {
|
|
return convertToRelativePath(path, state.currentDirectory, f => state.getCanonicalFileName(f));
|
|
}
|
|
|
|
function reportStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: string[]) {
|
|
state.host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args));
|
|
}
|
|
|
|
function reportWatchStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: (string | number | undefined)[]) {
|
|
state.hostWithWatch.onWatchStatusChange?.(createCompilerDiagnostic(message, ...args), state.host.getNewLine(), state.baseCompilerOptions);
|
|
}
|
|
|
|
function reportErrors({ host }: SolutionBuilderState, errors: readonly Diagnostic[]) {
|
|
errors.forEach(err => host.reportDiagnostic(err));
|
|
}
|
|
|
|
function reportAndStoreErrors(state: SolutionBuilderState, proj: ResolvedConfigFilePath, errors: readonly Diagnostic[]) {
|
|
reportErrors(state, errors);
|
|
state.projectErrorsReported.set(proj, true);
|
|
if (errors.length) {
|
|
state.diagnostics.set(proj, errors);
|
|
}
|
|
}
|
|
|
|
function reportParseConfigFileDiagnostic(state: SolutionBuilderState, proj: ResolvedConfigFilePath) {
|
|
reportAndStoreErrors(state, proj, [state.configFileCache.get(proj) as Diagnostic]);
|
|
}
|
|
|
|
function reportErrorSummary(state: SolutionBuilderState, buildOrder: AnyBuildOrder) {
|
|
if (!state.needsSummary) return;
|
|
state.needsSummary = false;
|
|
const canReportSummary = state.watch || !!state.host.reportErrorSummary;
|
|
const { diagnostics } = state;
|
|
let totalErrors = 0;
|
|
if (isCircularBuildOrder(buildOrder)) {
|
|
reportBuildQueue(state, buildOrder.buildOrder);
|
|
reportErrors(state, buildOrder.circularDiagnostics);
|
|
if (canReportSummary) totalErrors += getErrorCountForSummary(buildOrder.circularDiagnostics);
|
|
}
|
|
else {
|
|
// Report errors from the other projects
|
|
buildOrder.forEach(project => {
|
|
const projectPath = toResolvedConfigFilePath(state, project);
|
|
if (!state.projectErrorsReported.has(projectPath)) {
|
|
reportErrors(state, diagnostics.get(projectPath) || emptyArray);
|
|
}
|
|
});
|
|
if (canReportSummary) diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors));
|
|
}
|
|
|
|
if (state.watch) {
|
|
reportWatchStatus(state, getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors);
|
|
}
|
|
else if (state.host.reportErrorSummary) {
|
|
state.host.reportErrorSummary(totalErrors);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Report the build ordering inferred from the current project graph if we're in verbose mode
|
|
*/
|
|
function reportBuildQueue(state: SolutionBuilderState, buildQueue: readonly ResolvedConfigFileName[]) {
|
|
if (state.options.verbose) {
|
|
reportStatus(state, Diagnostics.Projects_in_this_build_Colon_0, buildQueue.map(s => "\r\n * " + relName(state, s)).join(""));
|
|
}
|
|
}
|
|
|
|
function reportUpToDateStatus(state: SolutionBuilderState, configFileName: string, status: UpToDateStatus) {
|
|
if (state.options.force && (status.type === UpToDateStatusType.UpToDate || status.type === UpToDateStatusType.UpToDateWithUpstreamTypes)) {
|
|
return reportStatus(
|
|
state,
|
|
Diagnostics.Project_0_is_being_forcibly_rebuilt,
|
|
relName(state, configFileName)
|
|
);
|
|
}
|
|
|
|
switch (status.type) {
|
|
case UpToDateStatusType.OutOfDateWithSelf:
|
|
return reportStatus(
|
|
state,
|
|
Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2,
|
|
relName(state, configFileName),
|
|
relName(state, status.outOfDateOutputFileName),
|
|
relName(state, status.newerInputFileName)
|
|
);
|
|
case UpToDateStatusType.OutOfDateWithUpstream:
|
|
return reportStatus(
|
|
state,
|
|
Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2,
|
|
relName(state, configFileName),
|
|
relName(state, status.outOfDateOutputFileName),
|
|
relName(state, status.newerProjectName)
|
|
);
|
|
case UpToDateStatusType.OutputMissing:
|
|
return reportStatus(
|
|
state,
|
|
Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist,
|
|
relName(state, configFileName),
|
|
relName(state, status.missingOutputFileName)
|
|
);
|
|
case UpToDateStatusType.UpToDate:
|
|
if (status.newestInputFileTime !== undefined) {
|
|
return reportStatus(
|
|
state,
|
|
Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2,
|
|
relName(state, configFileName),
|
|
relName(state, status.newestInputFileName || ""),
|
|
relName(state, status.oldestOutputFileName || "")
|
|
);
|
|
}
|
|
// Don't report anything for "up to date because it was already built" -- too verbose
|
|
break;
|
|
case UpToDateStatusType.OutOfDateWithPrepend:
|
|
return reportStatus(
|
|
state,
|
|
Diagnostics.Project_0_is_out_of_date_because_output_of_its_dependency_1_has_changed,
|
|
relName(state, configFileName),
|
|
relName(state, status.newerProjectName)
|
|
);
|
|
case UpToDateStatusType.UpToDateWithUpstreamTypes:
|
|
return reportStatus(
|
|
state,
|
|
Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies,
|
|
relName(state, configFileName)
|
|
);
|
|
case UpToDateStatusType.UpstreamOutOfDate:
|
|
return reportStatus(
|
|
state,
|
|
Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date,
|
|
relName(state, configFileName),
|
|
relName(state, status.upstreamProjectName)
|
|
);
|
|
case UpToDateStatusType.UpstreamBlocked:
|
|
return reportStatus(
|
|
state,
|
|
status.upstreamProjectBlocked ?
|
|
Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_was_not_built :
|
|
Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors,
|
|
relName(state, configFileName),
|
|
relName(state, status.upstreamProjectName)
|
|
);
|
|
case UpToDateStatusType.Unbuildable:
|
|
return reportStatus(
|
|
state,
|
|
Diagnostics.Failed_to_parse_file_0_Colon_1,
|
|
relName(state, configFileName),
|
|
status.reason
|
|
);
|
|
case UpToDateStatusType.TsVersionOutputOfDate:
|
|
return reportStatus(
|
|
state,
|
|
Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2,
|
|
relName(state, configFileName),
|
|
status.version,
|
|
version
|
|
);
|
|
case UpToDateStatusType.ContainerOnly:
|
|
// Don't report status on "solution" projects
|
|
// falls through
|
|
case UpToDateStatusType.ComputingUpstream:
|
|
// Should never leak from getUptoDateStatusWorker
|
|
break;
|
|
default:
|
|
assertType<never>(status);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Report the up-to-date status of a project if we're in verbose mode
|
|
*/
|
|
function verboseReportProjectStatus(state: SolutionBuilderState, configFileName: string, status: UpToDateStatus) {
|
|
if (state.options.verbose) {
|
|
reportUpToDateStatus(state, configFileName, status);
|
|
}
|
|
}
|
|
}
|