856 lines
32 KiB
Go
856 lines
32 KiB
Go
package build
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnostics"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/execute/incremental"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/execute/tsc"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
|
|
)
|
|
|
|
type updateKind uint
|
|
|
|
const (
|
|
updateKindNone updateKind = iota
|
|
updateKindConfig
|
|
updateKindUpdate
|
|
)
|
|
|
|
type buildKind uint
|
|
|
|
const (
|
|
buildKindNone buildKind = iota
|
|
buildKindPseudo
|
|
buildKindProgram
|
|
)
|
|
|
|
type upstreamTask struct {
|
|
task *buildTask
|
|
refIndex int
|
|
}
|
|
type buildInfoEntry struct {
|
|
buildInfo *incremental.BuildInfo
|
|
path tspath.Path
|
|
mTime time.Time
|
|
dtsTime *time.Time
|
|
}
|
|
|
|
type taskResult struct {
|
|
builder strings.Builder
|
|
reportStatus tsc.DiagnosticReporter
|
|
diagnosticReporter tsc.DiagnosticReporter
|
|
exitStatus tsc.ExitStatus
|
|
statistics *tsc.Statistics
|
|
program *incremental.Program
|
|
buildKind buildKind
|
|
filesToDelete []string
|
|
}
|
|
|
|
type buildTask struct {
|
|
config string
|
|
resolved *tsoptions.ParsedCommandLine
|
|
upStream []*upstreamTask
|
|
downStream []*buildTask // Only set and used in watch mode
|
|
status *upToDateStatus
|
|
done chan struct{}
|
|
|
|
// task reporting
|
|
result *taskResult
|
|
prevReporter *buildTask
|
|
reportDone chan struct{}
|
|
|
|
// Watching things
|
|
configTime time.Time
|
|
extendedConfigTimes []time.Time
|
|
inputFiles []time.Time
|
|
|
|
buildInfoEntry *buildInfoEntry
|
|
buildInfoEntryMu sync.Mutex
|
|
|
|
errors []*ast.Diagnostic
|
|
pending atomic.Bool
|
|
isInitialCycle bool
|
|
downStreamUpdateMu sync.Mutex
|
|
dirty bool
|
|
}
|
|
|
|
func (t *buildTask) waitOnUpstream() {
|
|
for _, upstream := range t.upStream {
|
|
<-upstream.task.done
|
|
}
|
|
}
|
|
|
|
func (t *buildTask) unblockDownstream() {
|
|
t.pending.Store(false)
|
|
t.isInitialCycle = false
|
|
close(t.done)
|
|
}
|
|
|
|
func (t *buildTask) reportDiagnostic(err *ast.Diagnostic) {
|
|
t.errors = append(t.errors, err)
|
|
t.result.diagnosticReporter(err)
|
|
}
|
|
|
|
func (t *buildTask) report(orchestrator *Orchestrator, configPath tspath.Path, buildResult *orchestratorResult) {
|
|
if t.prevReporter != nil {
|
|
<-t.prevReporter.reportDone
|
|
}
|
|
if len(t.errors) > 0 {
|
|
buildResult.errors = append(core.IfElse(buildResult.errors != nil, buildResult.errors, []*ast.Diagnostic{}), t.errors...)
|
|
}
|
|
fmt.Fprint(orchestrator.opts.Sys.Writer(), t.result.builder.String())
|
|
if t.result.exitStatus > buildResult.result.Status {
|
|
buildResult.result.Status = t.result.exitStatus
|
|
}
|
|
if t.result.statistics != nil {
|
|
buildResult.statistics.Aggregate(t.result.statistics)
|
|
}
|
|
// If we built the program, or updated timestamps, or had errors, we need to
|
|
// delete files that are no longer needed
|
|
switch t.result.buildKind {
|
|
case buildKindProgram:
|
|
if orchestrator.opts.Testing != nil {
|
|
orchestrator.opts.Testing.OnProgram(t.result.program)
|
|
}
|
|
buildResult.statistics.ProjectsBuilt++
|
|
case buildKindPseudo:
|
|
buildResult.statistics.TimestampUpdates++
|
|
}
|
|
buildResult.filesToDelete = append(buildResult.filesToDelete, t.result.filesToDelete...)
|
|
t.result = nil
|
|
close(t.reportDone)
|
|
}
|
|
|
|
func (t *buildTask) buildProject(orchestrator *Orchestrator, path tspath.Path) {
|
|
// Wait on upstream tasks to complete
|
|
t.waitOnUpstream()
|
|
if t.pending.Load() {
|
|
t.status = t.getUpToDateStatus(orchestrator, path)
|
|
t.reportUpToDateStatus(orchestrator)
|
|
if !t.handleStatusThatDoesntRequireBuild(orchestrator) {
|
|
t.compileAndEmit(orchestrator, path)
|
|
t.updateDownstream(orchestrator, path)
|
|
} else {
|
|
if t.resolved != nil {
|
|
for _, diagnostic := range t.resolved.GetConfigFileParsingDiagnostics() {
|
|
t.reportDiagnostic(diagnostic)
|
|
}
|
|
}
|
|
if len(t.errors) > 0 {
|
|
t.result.exitStatus = tsc.ExitStatusDiagnosticsPresent_OutputsSkipped
|
|
}
|
|
}
|
|
} else {
|
|
if len(t.errors) > 0 {
|
|
t.reportUpToDateStatus(orchestrator)
|
|
for _, err := range t.errors {
|
|
// Should not add the diagnostics so just reporting
|
|
t.result.diagnosticReporter(err)
|
|
}
|
|
}
|
|
}
|
|
t.unblockDownstream()
|
|
}
|
|
|
|
func (t *buildTask) updateDownstream(orchestrator *Orchestrator, path tspath.Path) {
|
|
if t.isInitialCycle {
|
|
return
|
|
}
|
|
if orchestrator.opts.Command.BuildOptions.StopBuildOnErrors.IsTrue() && t.status.isError() {
|
|
return
|
|
}
|
|
|
|
for _, downStream := range t.downStream {
|
|
downStream.downStreamUpdateMu.Lock()
|
|
if downStream.status != nil {
|
|
switch downStream.status.kind {
|
|
case upToDateStatusTypeUpToDate:
|
|
if !t.result.program.HasChangedDtsFile() {
|
|
downStream.status = &upToDateStatus{kind: upToDateStatusTypeUpToDateWithUpstreamTypes, data: downStream.status.data}
|
|
break
|
|
}
|
|
fallthrough
|
|
case upToDateStatusTypeUpToDateWithUpstreamTypes,
|
|
upToDateStatusTypeUpToDateWithInputFileText:
|
|
if t.result.program.HasChangedDtsFile() {
|
|
downStream.status = &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{t.config, downStream.status.oldestOutputFileName()}}
|
|
}
|
|
case upToDateStatusTypeUpstreamErrors:
|
|
upstreamErrors := downStream.status.upstreamErrors()
|
|
refConfig := core.ResolveConfigFileNameOfProjectReference(upstreamErrors.ref)
|
|
if orchestrator.toPath(refConfig) == path {
|
|
downStream.resetStatus()
|
|
}
|
|
}
|
|
}
|
|
downStream.pending.Store(true)
|
|
downStream.downStreamUpdateMu.Unlock()
|
|
}
|
|
}
|
|
|
|
func (t *buildTask) compileAndEmit(orchestrator *Orchestrator, path tspath.Path) {
|
|
t.errors = nil
|
|
if orchestrator.opts.Command.BuildOptions.Verbose.IsTrue() {
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(diagnostics.Building_project_0, orchestrator.relativeFileName(t.config)))
|
|
}
|
|
|
|
// Real build
|
|
var compileTimes tsc.CompileTimes
|
|
configTime, _ := orchestrator.host.configTimes.Load(path)
|
|
compileTimes.ConfigTime = configTime
|
|
buildInfoReadStart := orchestrator.opts.Sys.Now()
|
|
var oldProgram *incremental.Program
|
|
if !orchestrator.opts.Command.BuildOptions.Force.IsTrue() {
|
|
oldProgram = incremental.ReadBuildInfoProgram(t.resolved, orchestrator.host, orchestrator.host)
|
|
}
|
|
compileTimes.BuildInfoReadTime = orchestrator.opts.Sys.Now().Sub(buildInfoReadStart)
|
|
parseStart := orchestrator.opts.Sys.Now()
|
|
program := compiler.NewProgram(compiler.ProgramOptions{
|
|
Config: t.resolved,
|
|
Host: &compilerHost{
|
|
host: orchestrator.host,
|
|
trace: tsc.GetTraceWithWriterFromSys(&t.result.builder, orchestrator.opts.Testing),
|
|
},
|
|
JSDocParsingMode: ast.JSDocParsingModeParseForTypeErrors,
|
|
})
|
|
compileTimes.ParseTime = orchestrator.opts.Sys.Now().Sub(parseStart)
|
|
changesComputeStart := orchestrator.opts.Sys.Now()
|
|
t.result.program = incremental.NewProgram(program, oldProgram, orchestrator.host, orchestrator.opts.Testing != nil)
|
|
compileTimes.ChangesComputeTime = orchestrator.opts.Sys.Now().Sub(changesComputeStart)
|
|
|
|
result, statistics := tsc.EmitAndReportStatistics(tsc.EmitInput{
|
|
Sys: orchestrator.opts.Sys,
|
|
ProgramLike: t.result.program,
|
|
Program: program,
|
|
Config: t.resolved,
|
|
ReportDiagnostic: t.reportDiagnostic,
|
|
ReportErrorSummary: tsc.QuietDiagnosticsReporter,
|
|
Writer: &t.result.builder,
|
|
WriteFile: func(fileName, text string, writeByteOrderMark bool, data *compiler.WriteFileData) error {
|
|
return t.writeFile(orchestrator, fileName, text, writeByteOrderMark, data)
|
|
},
|
|
CompileTimes: &compileTimes,
|
|
Testing: orchestrator.opts.Testing,
|
|
TestingMTimesCache: orchestrator.host.mTimes,
|
|
})
|
|
t.result.exitStatus = result.Status
|
|
t.result.statistics = statistics
|
|
if (!program.Options().NoEmitOnError.IsTrue() || len(result.Diagnostics) == 0) &&
|
|
(len(result.EmitResult.EmittedFiles) > 0 || t.status.kind != upToDateStatusTypeOutOfDateBuildInfoWithErrors) {
|
|
// Update time stamps for rest of the outputs
|
|
t.updateTimeStamps(orchestrator, result.EmitResult.EmittedFiles, diagnostics.Updating_unchanged_output_timestamps_of_project_0)
|
|
}
|
|
t.result.buildKind = buildKindProgram
|
|
if result.Status == tsc.ExitStatusDiagnosticsPresent_OutputsSkipped || result.Status == tsc.ExitStatusDiagnosticsPresent_OutputsGenerated {
|
|
t.status = &upToDateStatus{kind: upToDateStatusTypeBuildErrors}
|
|
} else {
|
|
var oldestOutputFileName string
|
|
if len(result.EmitResult.EmittedFiles) > 0 {
|
|
oldestOutputFileName = result.EmitResult.EmittedFiles[0]
|
|
} else {
|
|
oldestOutputFileName = core.FirstOrNilSeq(t.resolved.GetOutputFileNames())
|
|
}
|
|
t.status = &upToDateStatus{kind: upToDateStatusTypeUpToDate, data: oldestOutputFileName}
|
|
}
|
|
}
|
|
|
|
func (t *buildTask) handleStatusThatDoesntRequireBuild(orchestrator *Orchestrator) bool {
|
|
switch t.status.kind {
|
|
case upToDateStatusTypeUpToDate:
|
|
if orchestrator.opts.Command.BuildOptions.Dry.IsTrue() {
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(diagnostics.Project_0_is_up_to_date, t.config))
|
|
}
|
|
return true
|
|
case upToDateStatusTypeUpstreamErrors:
|
|
upstreamStatus := t.status.upstreamErrors()
|
|
if orchestrator.opts.Command.BuildOptions.Verbose.IsTrue() {
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
core.IfElse(
|
|
upstreamStatus.refHasUpstreamErrors,
|
|
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,
|
|
),
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(upstreamStatus.ref),
|
|
))
|
|
}
|
|
return true
|
|
case upToDateStatusTypeSolution:
|
|
return true
|
|
case upToDateStatusTypeConfigFileNotFound:
|
|
t.reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.File_0_not_found, t.config))
|
|
return true
|
|
}
|
|
|
|
// update timestamps
|
|
if t.status.isPseudoBuild() {
|
|
if orchestrator.opts.Command.BuildOptions.Dry.IsTrue() {
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(diagnostics.A_non_dry_build_would_update_timestamps_for_output_of_project_0, t.config))
|
|
t.status = &upToDateStatus{kind: upToDateStatusTypeUpToDate}
|
|
return true
|
|
}
|
|
|
|
t.updateTimeStamps(orchestrator, nil, diagnostics.Updating_output_timestamps_of_project_0)
|
|
t.status = &upToDateStatus{kind: upToDateStatusTypeUpToDate, data: t.status.data}
|
|
t.result.buildKind = buildKindPseudo
|
|
return true
|
|
}
|
|
|
|
if orchestrator.opts.Command.BuildOptions.Dry.IsTrue() {
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(diagnostics.A_non_dry_build_would_build_project_0, t.config))
|
|
t.status = &upToDateStatus{kind: upToDateStatusTypeUpToDate}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (t *buildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tspath.Path) *upToDateStatus {
|
|
if t.status != nil {
|
|
return t.status
|
|
}
|
|
// Config file not found
|
|
if t.resolved == nil {
|
|
return &upToDateStatus{kind: upToDateStatusTypeConfigFileNotFound}
|
|
}
|
|
|
|
// Solution - nothing to build
|
|
if len(t.resolved.FileNames()) == 0 && t.resolved.ProjectReferences() != nil {
|
|
return &upToDateStatus{kind: upToDateStatusTypeSolution}
|
|
}
|
|
|
|
for _, upstream := range t.upStream {
|
|
if orchestrator.opts.Command.BuildOptions.StopBuildOnErrors.IsTrue() && upstream.task.status.isError() {
|
|
// Upstream project has errors, so we cannot build this project
|
|
return &upToDateStatus{kind: upToDateStatusTypeUpstreamErrors, data: &upstreamErrors{t.resolved.ProjectReferences()[upstream.refIndex].Path, upstream.task.status.kind == upToDateStatusTypeUpstreamErrors}}
|
|
}
|
|
}
|
|
|
|
if orchestrator.opts.Command.BuildOptions.Force.IsTrue() {
|
|
return &upToDateStatus{kind: upToDateStatusTypeForceBuild}
|
|
}
|
|
|
|
// Check the build info
|
|
buildInfoPath := t.resolved.GetBuildInfoFileName()
|
|
buildInfo, buildInfoTime := t.loadOrStoreBuildInfo(orchestrator, configPath, buildInfoPath)
|
|
if buildInfo == nil {
|
|
return &upToDateStatus{kind: upToDateStatusTypeOutputMissing, data: buildInfoPath}
|
|
}
|
|
|
|
// build info version
|
|
if !buildInfo.IsValidVersion() {
|
|
return &upToDateStatus{kind: upToDateStatusTypeTsVersionOutputOfDate, data: buildInfo.Version}
|
|
}
|
|
|
|
// Report errors if build info indicates errors
|
|
if buildInfo.Errors || // Errors that need to be reported irrespective of "--noCheck"
|
|
(!t.resolved.CompilerOptions().NoCheck.IsTrue() && (buildInfo.SemanticErrors || buildInfo.CheckPending)) { // Errors without --noCheck
|
|
return &upToDateStatus{kind: upToDateStatusTypeOutOfDateBuildInfoWithErrors, data: buildInfoPath}
|
|
}
|
|
|
|
if t.resolved.CompilerOptions().IsIncremental() {
|
|
if !buildInfo.IsIncremental() {
|
|
// Program options out of date
|
|
return &upToDateStatus{kind: upToDateStatusTypeOutOfDateOptions, data: buildInfoPath}
|
|
}
|
|
|
|
// Errors need to be reported if build info has errors
|
|
if (t.resolved.CompilerOptions().GetEmitDeclarations() && buildInfo.EmitDiagnosticsPerFile != nil) || // Always reported errors
|
|
(!t.resolved.CompilerOptions().NoCheck.IsTrue() && // Semantic errors if not --noCheck
|
|
(buildInfo.ChangeFileSet != nil || buildInfo.SemanticDiagnosticsPerFile != nil)) {
|
|
return &upToDateStatus{kind: upToDateStatusTypeOutOfDateBuildInfoWithErrors, data: buildInfoPath}
|
|
}
|
|
|
|
// Pending emit files
|
|
if !t.resolved.CompilerOptions().NoEmit.IsTrue() &&
|
|
(buildInfo.ChangeFileSet != nil || buildInfo.AffectedFilesPendingEmit != nil) {
|
|
return &upToDateStatus{kind: upToDateStatusTypeOutOfDateBuildInfoWithPendingEmit, data: buildInfoPath}
|
|
}
|
|
|
|
// Some of the emit files like source map or dts etc are not yet done
|
|
if buildInfo.IsEmitPending(t.resolved, tspath.GetDirectoryPath(tspath.GetNormalizedAbsolutePath(buildInfoPath, orchestrator.comparePathsOptions.CurrentDirectory))) {
|
|
return &upToDateStatus{kind: upToDateStatusTypeOutOfDateOptions, data: buildInfoPath}
|
|
}
|
|
}
|
|
var inputTextUnchanged bool
|
|
oldestOutputFileAndTime := fileAndTime{buildInfoPath, buildInfoTime}
|
|
var newestInputFileAndTime fileAndTime
|
|
var seenRoots collections.Set[tspath.Path]
|
|
var buildInfoRootInfoReader *incremental.BuildInfoRootInfoReader
|
|
for _, inputFile := range t.resolved.FileNames() {
|
|
inputTime := orchestrator.host.GetMTime(inputFile)
|
|
if inputTime.IsZero() {
|
|
return &upToDateStatus{kind: upToDateStatusTypeInputFileMissing, data: inputFile}
|
|
}
|
|
inputPath := orchestrator.toPath(inputFile)
|
|
if inputTime.After(oldestOutputFileAndTime.time) {
|
|
var version string
|
|
var currentVersion string
|
|
if buildInfo.IsIncremental() {
|
|
if buildInfoRootInfoReader == nil {
|
|
buildInfoRootInfoReader = buildInfo.GetBuildInfoRootInfoReader(tspath.GetDirectoryPath(tspath.GetNormalizedAbsolutePath(buildInfoPath, orchestrator.comparePathsOptions.CurrentDirectory)), orchestrator.comparePathsOptions)
|
|
}
|
|
buildInfoFileInfo, resolvedInputPath := buildInfoRootInfoReader.GetBuildInfoFileInfo(inputPath)
|
|
if fileInfo := buildInfoFileInfo.GetFileInfo(); fileInfo != nil && fileInfo.Version() != "" {
|
|
version = fileInfo.Version()
|
|
if text, ok := orchestrator.host.FS().ReadFile(string(resolvedInputPath)); ok {
|
|
currentVersion = incremental.ComputeHash(text, orchestrator.opts.Testing != nil)
|
|
if version == currentVersion {
|
|
inputTextUnchanged = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if version == "" || version != currentVersion {
|
|
return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{inputFile, buildInfoPath}}
|
|
}
|
|
}
|
|
if inputTime.After(newestInputFileAndTime.time) {
|
|
newestInputFileAndTime = fileAndTime{inputFile, inputTime}
|
|
}
|
|
seenRoots.Add(inputPath)
|
|
}
|
|
|
|
if buildInfoRootInfoReader == nil {
|
|
buildInfoRootInfoReader = buildInfo.GetBuildInfoRootInfoReader(tspath.GetDirectoryPath(tspath.GetNormalizedAbsolutePath(buildInfoPath, orchestrator.comparePathsOptions.CurrentDirectory)), orchestrator.comparePathsOptions)
|
|
}
|
|
for root := range buildInfoRootInfoReader.Roots() {
|
|
if !seenRoots.Has(root) {
|
|
// File was root file when project was built but its not any more
|
|
return &upToDateStatus{kind: upToDateStatusTypeOutOfDateRoots, data: &inputOutputName{string(root), buildInfoPath}}
|
|
}
|
|
}
|
|
|
|
if !t.resolved.CompilerOptions().IsIncremental() {
|
|
// Check output file stamps
|
|
for outputFile := range t.resolved.GetOutputFileNames() {
|
|
outputTime := orchestrator.host.GetMTime(outputFile)
|
|
if outputTime.IsZero() {
|
|
// Output file missing
|
|
return &upToDateStatus{kind: upToDateStatusTypeOutputMissing, data: outputFile}
|
|
}
|
|
|
|
if outputTime.Before(newestInputFileAndTime.time) {
|
|
// Output file is older than input file
|
|
return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{newestInputFileAndTime.file, outputFile}}
|
|
}
|
|
|
|
if outputTime.Before(oldestOutputFileAndTime.time) {
|
|
oldestOutputFileAndTime = fileAndTime{outputFile, outputTime}
|
|
}
|
|
}
|
|
}
|
|
|
|
var refDtsUnchanged bool
|
|
for _, upstream := range t.upStream {
|
|
if upstream.task.status.kind == upToDateStatusTypeSolution {
|
|
// Not dependent on the status or this upstream project
|
|
// (eg: expected cycle was detected and hence skipped, or is solution)
|
|
continue
|
|
}
|
|
|
|
// If the upstream project's newest file is older than our oldest output,
|
|
// we can't be out of date because of it
|
|
// inputTime will not be present if we just built this project or updated timestamps
|
|
// - in that case we do want to either build or update timestamps
|
|
refInputOutputFileAndTime := upstream.task.status.inputOutputFileAndTime()
|
|
if refInputOutputFileAndTime != nil && !refInputOutputFileAndTime.input.time.IsZero() && refInputOutputFileAndTime.input.time.Before(oldestOutputFileAndTime.time) {
|
|
continue
|
|
}
|
|
|
|
// Check if tsbuildinfo path is shared, then we need to rebuild
|
|
if t.hasConflictingBuildInfo(orchestrator, upstream.task) {
|
|
// We have an output older than an upstream output - we are out of date
|
|
return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{t.resolved.ProjectReferences()[upstream.refIndex].Path, oldestOutputFileAndTime.file}}
|
|
}
|
|
|
|
// If the upstream project has only change .d.ts files, and we've built
|
|
// *after* those files, then we're "pseudo up to date" and eligible for a fast rebuild
|
|
newestDtsChangeTime := upstream.task.getLatestChangedDtsMTime(orchestrator)
|
|
if !newestDtsChangeTime.IsZero() && newestDtsChangeTime.Before(oldestOutputFileAndTime.time) {
|
|
refDtsUnchanged = true
|
|
continue
|
|
}
|
|
|
|
// We have an output older than an upstream output - we are out of date
|
|
return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{t.resolved.ProjectReferences()[upstream.refIndex].Path, oldestOutputFileAndTime.file}}
|
|
}
|
|
|
|
checkInputFileTime := func(inputFile string) *upToDateStatus {
|
|
inputTime := orchestrator.host.GetMTime(inputFile)
|
|
if inputTime.After(oldestOutputFileAndTime.time) {
|
|
// Output file is older than input file
|
|
return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{inputFile, oldestOutputFileAndTime.file}}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
configStatus := checkInputFileTime(t.config)
|
|
if configStatus != nil {
|
|
return configStatus
|
|
}
|
|
|
|
for _, extendedConfig := range t.resolved.ExtendedSourceFiles() {
|
|
extendedConfigStatus := checkInputFileTime(extendedConfig)
|
|
if extendedConfigStatus != nil {
|
|
return extendedConfigStatus
|
|
}
|
|
}
|
|
|
|
// !!! sheetal TODO : watch??
|
|
// // Check package file time
|
|
// const packageJsonLookups = state.lastCachedPackageJsonLookups.get(resolvedPath);
|
|
// const dependentPackageFileStatus = packageJsonLookups && forEachKey(
|
|
// packageJsonLookups,
|
|
// path => checkConfigFileUpToDateStatus(state, path, oldestOutputFileTime, oldestOutputFileName),
|
|
// );
|
|
// if (dependentPackageFileStatus) return dependentPackageFileStatus;
|
|
|
|
return &upToDateStatus{
|
|
kind: core.IfElse(
|
|
refDtsUnchanged,
|
|
upToDateStatusTypeUpToDateWithUpstreamTypes,
|
|
core.IfElse(inputTextUnchanged, upToDateStatusTypeUpToDateWithInputFileText, upToDateStatusTypeUpToDate),
|
|
),
|
|
data: &inputOutputFileAndTime{newestInputFileAndTime, oldestOutputFileAndTime, buildInfoPath},
|
|
}
|
|
}
|
|
|
|
func (t *buildTask) reportUpToDateStatus(orchestrator *Orchestrator) {
|
|
if !orchestrator.opts.Command.BuildOptions.Verbose.IsTrue() {
|
|
return
|
|
}
|
|
switch t.status.kind {
|
|
case upToDateStatusTypeConfigFileNotFound:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_out_of_date_because_config_file_does_not_exist,
|
|
orchestrator.relativeFileName(t.config),
|
|
))
|
|
case upToDateStatusTypeUpstreamErrors:
|
|
upstreamStatus := t.status.upstreamErrors()
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
core.IfElse(
|
|
upstreamStatus.refHasUpstreamErrors,
|
|
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,
|
|
),
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(upstreamStatus.ref),
|
|
))
|
|
case upToDateStatusTypeBuildErrors:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_out_of_date_because_it_has_errors,
|
|
orchestrator.relativeFileName(t.config),
|
|
))
|
|
case upToDateStatusTypeUpToDate:
|
|
// This is to ensure skipping verbose log for projects that were built,
|
|
// and then some other package changed but this package doesnt need update
|
|
if inputOutputFileAndTime := t.status.inputOutputFileAndTime(); inputOutputFileAndTime != nil {
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_output_2,
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(inputOutputFileAndTime.input.file),
|
|
orchestrator.relativeFileName(inputOutputFileAndTime.output.file),
|
|
))
|
|
}
|
|
case upToDateStatusTypeUpToDateWithUpstreamTypes:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies,
|
|
orchestrator.relativeFileName(t.config),
|
|
))
|
|
case upToDateStatusTypeUpToDateWithInputFileText:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_up_to_date_but_needs_to_update_timestamps_of_output_files_that_are_older_than_input_files,
|
|
orchestrator.relativeFileName(t.config),
|
|
))
|
|
case upToDateStatusTypeInputFileMissing:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_out_of_date_because_input_1_does_not_exist,
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(t.status.data.(string)),
|
|
))
|
|
case upToDateStatusTypeOutputMissing:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist,
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(t.status.data.(string)),
|
|
))
|
|
case upToDateStatusTypeInputFileNewer:
|
|
inputOutput := t.status.inputOutputName()
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_out_of_date_because_output_1_is_older_than_input_2,
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(inputOutput.output),
|
|
orchestrator.relativeFileName(inputOutput.input),
|
|
))
|
|
case upToDateStatusTypeOutOfDateBuildInfoWithPendingEmit:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_out_of_date_because_buildinfo_file_1_indicates_that_some_of_the_changes_were_not_emitted,
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(t.status.data.(string)),
|
|
))
|
|
case upToDateStatusTypeOutOfDateBuildInfoWithErrors:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_out_of_date_because_buildinfo_file_1_indicates_that_program_needs_to_report_errors,
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(t.status.data.(string)),
|
|
))
|
|
case upToDateStatusTypeOutOfDateOptions:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_out_of_date_because_buildinfo_file_1_indicates_there_is_change_in_compilerOptions,
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(t.status.data.(string)),
|
|
))
|
|
case upToDateStatusTypeOutOfDateRoots:
|
|
inputOutput := t.status.inputOutputName()
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_out_of_date_because_buildinfo_file_1_indicates_that_file_2_was_root_file_of_compilation_but_not_any_more,
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(inputOutput.output),
|
|
orchestrator.relativeFileName(inputOutput.input),
|
|
))
|
|
case upToDateStatusTypeTsVersionOutputOfDate:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2,
|
|
orchestrator.relativeFileName(t.config),
|
|
orchestrator.relativeFileName(t.status.data.(string)),
|
|
core.Version(),
|
|
))
|
|
case upToDateStatusTypeForceBuild:
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(
|
|
diagnostics.Project_0_is_being_forcibly_rebuilt,
|
|
orchestrator.relativeFileName(t.config),
|
|
))
|
|
case upToDateStatusTypeSolution:
|
|
// Does not need to report status
|
|
default:
|
|
panic(fmt.Sprintf("Unknown up to date status kind: %v", t.status.kind))
|
|
}
|
|
}
|
|
|
|
func (t *buildTask) canUpdateJsDtsOutputTimestamps() bool {
|
|
return !t.resolved.CompilerOptions().NoEmit.IsTrue() && !t.resolved.CompilerOptions().IsIncremental()
|
|
}
|
|
|
|
func (t *buildTask) updateTimeStamps(orchestrator *Orchestrator, emittedFiles []string, verboseMessage *diagnostics.Message) {
|
|
emitted := collections.NewSetFromItems(emittedFiles...)
|
|
var verboseMessageReported bool
|
|
buildInfoName := t.resolved.GetBuildInfoFileName()
|
|
now := orchestrator.opts.Sys.Now()
|
|
updateTimeStamp := func(file string) {
|
|
if emitted.Has(file) {
|
|
return
|
|
}
|
|
if !verboseMessageReported && orchestrator.opts.Command.BuildOptions.Verbose.IsTrue() {
|
|
t.result.reportStatus(ast.NewCompilerDiagnostic(verboseMessage, orchestrator.relativeFileName(t.config)))
|
|
verboseMessageReported = true
|
|
}
|
|
err := orchestrator.host.SetMTime(file, now)
|
|
if err == nil {
|
|
if file == buildInfoName {
|
|
t.buildInfoEntryMu.Lock()
|
|
if t.buildInfoEntry != nil {
|
|
t.buildInfoEntry.mTime = now
|
|
}
|
|
t.buildInfoEntryMu.Unlock()
|
|
} else if t.storeOutputTimeStamp(orchestrator) {
|
|
orchestrator.host.storeMTime(file, now)
|
|
}
|
|
}
|
|
}
|
|
|
|
if t.canUpdateJsDtsOutputTimestamps() {
|
|
for outputFile := range t.resolved.GetOutputFileNames() {
|
|
updateTimeStamp(outputFile)
|
|
}
|
|
}
|
|
updateTimeStamp(t.resolved.GetBuildInfoFileName())
|
|
}
|
|
|
|
func (t *buildTask) cleanProject(orchestrator *Orchestrator, path tspath.Path) {
|
|
if t.resolved == nil {
|
|
t.reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.File_0_not_found, t.config))
|
|
t.result.exitStatus = tsc.ExitStatusDiagnosticsPresent_OutputsSkipped
|
|
return
|
|
}
|
|
|
|
inputs := collections.NewSetFromItems(core.Map(t.resolved.FileNames(), orchestrator.toPath)...)
|
|
for outputFile := range t.resolved.GetOutputFileNames() {
|
|
t.cleanProjectOutput(orchestrator, outputFile, inputs)
|
|
}
|
|
t.cleanProjectOutput(orchestrator, t.resolved.GetBuildInfoFileName(), inputs)
|
|
}
|
|
|
|
func (t *buildTask) cleanProjectOutput(orchestrator *Orchestrator, outputFile string, inputs *collections.Set[tspath.Path]) {
|
|
outputPath := orchestrator.toPath(outputFile)
|
|
// If output name is same as input file name, do not delete and ignore the error
|
|
if inputs.Has(outputPath) {
|
|
return
|
|
}
|
|
if orchestrator.host.FS().FileExists(outputFile) {
|
|
if !orchestrator.opts.Command.BuildOptions.Dry.IsTrue() {
|
|
err := orchestrator.host.FS().Remove(outputFile)
|
|
if err != nil {
|
|
t.reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.Failed_to_delete_file_0, outputFile))
|
|
}
|
|
} else {
|
|
t.result.filesToDelete = append(t.result.filesToDelete, outputFile)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *buildTask) updateWatch(orchestrator *Orchestrator, oldCache *collections.SyncMap[tspath.Path, time.Time]) {
|
|
t.configTime = orchestrator.host.loadOrStoreMTime(t.config, oldCache, false)
|
|
if t.resolved != nil {
|
|
t.extendedConfigTimes = core.Map(t.resolved.ExtendedSourceFiles(), func(p string) time.Time {
|
|
return orchestrator.host.loadOrStoreMTime(p, oldCache, false)
|
|
})
|
|
t.inputFiles = core.Map(t.resolved.FileNames(), func(p string) time.Time {
|
|
return orchestrator.host.loadOrStoreMTime(p, oldCache, false)
|
|
})
|
|
if t.canUpdateJsDtsOutputTimestamps() {
|
|
for outputFile := range t.resolved.GetOutputFileNames() {
|
|
orchestrator.host.storeMTimeFromOldCache(outputFile, oldCache)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *buildTask) resetStatus() {
|
|
t.status = nil
|
|
t.pending.Store(true)
|
|
t.errors = nil
|
|
}
|
|
|
|
func (t *buildTask) resetConfig(orchestrator *Orchestrator, path tspath.Path) {
|
|
t.dirty = true
|
|
orchestrator.host.resolvedReferences.delete(path)
|
|
}
|
|
|
|
func (t *buildTask) hasUpdate(orchestrator *Orchestrator, path tspath.Path) updateKind {
|
|
var needsConfigUpdate bool
|
|
var needsUpdate bool
|
|
if configTime := orchestrator.host.GetMTime(t.config); configTime != t.configTime {
|
|
t.resetConfig(orchestrator, path)
|
|
needsConfigUpdate = true
|
|
}
|
|
if t.resolved != nil {
|
|
for index, file := range t.resolved.ExtendedSourceFiles() {
|
|
if orchestrator.host.GetMTime(file) != t.extendedConfigTimes[index] {
|
|
t.resetConfig(orchestrator, path)
|
|
needsConfigUpdate = true
|
|
}
|
|
}
|
|
for index, file := range t.resolved.FileNames() {
|
|
if orchestrator.host.GetMTime(file) != t.inputFiles[index] {
|
|
t.resetStatus()
|
|
needsUpdate = true
|
|
}
|
|
}
|
|
if !needsConfigUpdate {
|
|
configStart := orchestrator.opts.Sys.Now()
|
|
newConfig := t.resolved.ReloadFileNamesOfParsedCommandLine(orchestrator.host.FS())
|
|
configTime := orchestrator.opts.Sys.Now().Sub(configStart)
|
|
// Make new channels if needed later
|
|
t.reportDone = make(chan struct{})
|
|
t.done = make(chan struct{})
|
|
if !slices.Equal(t.resolved.FileNames(), newConfig.FileNames()) {
|
|
orchestrator.host.resolvedReferences.store(path, newConfig)
|
|
orchestrator.host.configTimes.Store(path, configTime)
|
|
t.resolved = newConfig
|
|
t.resetStatus()
|
|
needsUpdate = true
|
|
}
|
|
}
|
|
}
|
|
return core.IfElse(needsConfigUpdate, updateKindConfig, core.IfElse(needsUpdate, updateKindUpdate, updateKindNone))
|
|
}
|
|
|
|
func (t *buildTask) loadOrStoreBuildInfo(orchestrator *Orchestrator, configPath tspath.Path, buildInfoFileName string) (*incremental.BuildInfo, time.Time) {
|
|
path := orchestrator.toPath(buildInfoFileName)
|
|
t.buildInfoEntryMu.Lock()
|
|
defer t.buildInfoEntryMu.Unlock()
|
|
if t.buildInfoEntry != nil && t.buildInfoEntry.path == path {
|
|
return t.buildInfoEntry.buildInfo, t.buildInfoEntry.mTime
|
|
}
|
|
t.buildInfoEntry = &buildInfoEntry{
|
|
buildInfo: incremental.NewBuildInfoReader(orchestrator.host).ReadBuildInfo(t.resolved),
|
|
path: path,
|
|
}
|
|
var mTime time.Time
|
|
if t.buildInfoEntry.buildInfo != nil {
|
|
mTime = orchestrator.host.GetMTime(buildInfoFileName)
|
|
}
|
|
t.buildInfoEntry.mTime = mTime
|
|
return t.buildInfoEntry.buildInfo, mTime
|
|
}
|
|
|
|
func (t *buildTask) onBuildInfoEmit(orchestrator *Orchestrator, buildInfoFileName string, buildInfo *incremental.BuildInfo, hasChangedDtsFile bool) {
|
|
t.buildInfoEntryMu.Lock()
|
|
defer t.buildInfoEntryMu.Unlock()
|
|
var dtsTime *time.Time
|
|
mTime := orchestrator.opts.Sys.Now()
|
|
if hasChangedDtsFile {
|
|
dtsTime = &mTime
|
|
} else if t.buildInfoEntry != nil {
|
|
dtsTime = t.buildInfoEntry.dtsTime
|
|
}
|
|
t.buildInfoEntry = &buildInfoEntry{
|
|
buildInfo: buildInfo,
|
|
path: orchestrator.toPath(buildInfoFileName),
|
|
mTime: mTime,
|
|
dtsTime: dtsTime,
|
|
}
|
|
}
|
|
|
|
func (t *buildTask) hasConflictingBuildInfo(orchestrator *Orchestrator, upstream *buildTask) bool {
|
|
if t.buildInfoEntry != nil && upstream.buildInfoEntry != nil {
|
|
return t.buildInfoEntry.path == upstream.buildInfoEntry.path
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (t *buildTask) getLatestChangedDtsMTime(orchestrator *Orchestrator) time.Time {
|
|
t.buildInfoEntryMu.Lock()
|
|
defer t.buildInfoEntryMu.Unlock()
|
|
if t.buildInfoEntry.dtsTime != nil {
|
|
return *t.buildInfoEntry.dtsTime
|
|
}
|
|
dtsTime := orchestrator.host.GetMTime(
|
|
tspath.GetNormalizedAbsolutePath(
|
|
t.buildInfoEntry.buildInfo.LatestChangedDtsFile,
|
|
tspath.GetDirectoryPath(string(t.buildInfoEntry.path)),
|
|
),
|
|
)
|
|
t.buildInfoEntry.dtsTime = &dtsTime
|
|
return dtsTime
|
|
}
|
|
|
|
func (t *buildTask) storeOutputTimeStamp(orchestrator *Orchestrator) bool {
|
|
return orchestrator.opts.Command.CompilerOptions.Watch.IsTrue() && !t.resolved.CompilerOptions().IsIncremental()
|
|
}
|
|
|
|
func (t *buildTask) writeFile(orchestrator *Orchestrator, fileName string, text string, writeByteOrderMark bool, data *compiler.WriteFileData) error {
|
|
err := orchestrator.host.FS().WriteFile(fileName, text, writeByteOrderMark)
|
|
if err == nil {
|
|
if data != nil && data.BuildInfo != nil {
|
|
t.onBuildInfoEmit(orchestrator, fileName, data.BuildInfo.(*incremental.BuildInfo), t.result.program.HasChangedDtsFile())
|
|
} else if t.storeOutputTimeStamp(orchestrator) {
|
|
// Store time stamps
|
|
orchestrator.host.storeMTime(fileName, orchestrator.opts.Sys.Now())
|
|
}
|
|
}
|
|
return err
|
|
}
|