2025-10-15 10:12:44 +03:00

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
}