vendor tsgo
This commit is contained in:
845
tools/tsgo/internal/execute/build/buildtask.go
Normal file
845
tools/tsgo/internal/execute/build/buildtask.go
Normal file
@@ -0,0 +1,845 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/execute/incremental"
|
||||
"github.com/microsoft/typescript-go/internal/execute/tsc"
|
||||
"github.com/microsoft/typescript-go/internal/tsoptions"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
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{}
|
||||
|
||||
buildInfoEntry *buildInfoEntry
|
||||
buildInfoEntryMu sync.Mutex
|
||||
packageJsons []string
|
||||
|
||||
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.Command.Locale(), orchestrator.opts.Testing),
|
||||
},
|
||||
})
|
||||
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, data *compiler.WriteFileData) error {
|
||||
return t.writeFile(orchestrator, fileName, text, data)
|
||||
},
|
||||
CompileTimes: &compileTimes,
|
||||
Testing: orchestrator.opts.Testing,
|
||||
TestingMTimesCache: orchestrator.host.mTimes,
|
||||
})
|
||||
t.result.exitStatus = result.Status
|
||||
t.result.statistics = statistics
|
||||
t.packageJsons = t.result.program.PackageJsonLookupPaths()
|
||||
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()
|
||||
getBuildInfoDirectory := core.Memoize(func() string {
|
||||
return tspath.GetDirectoryPath(tspath.GetNormalizedAbsolutePath(buildInfoPath, orchestrator.comparePathsOptions.CurrentDirectory))
|
||||
})
|
||||
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, getBuildInfoDirectory()) {
|
||||
return &upToDateStatus{kind: upToDateStatusTypeOutOfDateOptions, data: buildInfoPath}
|
||||
}
|
||||
}
|
||||
var inputTextUnchanged bool
|
||||
oldestOutputFileAndTime := fileAndTime{buildInfoPath, buildInfoTime}
|
||||
var newestInputFileAndTime fileAndTime
|
||||
var seenRoots collections.Set[tspath.Path]
|
||||
getBuildInfoRootInfoReader := core.Memoize(func() *incremental.BuildInfoRootInfoReader {
|
||||
return buildInfo.GetBuildInfoRootInfoReader(getBuildInfoDirectory(), orchestrator.comparePathsOptions)
|
||||
})
|
||||
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() {
|
||||
buildInfoFileInfo, resolvedInputPath := getBuildInfoRootInfoReader().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)
|
||||
}
|
||||
|
||||
for root := range getBuildInfoRootInfoReader().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 buildInfo.IsIncremental() {
|
||||
var resolvedRoots collections.Set[tspath.Path]
|
||||
for root := range getBuildInfoRootInfoReader().Roots() {
|
||||
if _, resolved := getBuildInfoRootInfoReader().GetBuildInfoFileInfo(root); resolved != "" {
|
||||
resolvedRoots.Add(resolved)
|
||||
}
|
||||
}
|
||||
for index, buildInfoFileInfo := range buildInfo.FileInfos {
|
||||
buildInfoFileName := buildInfo.FileNames[index]
|
||||
// Lib files bundled with the compiler can change only with the version of the compiler,
|
||||
// which is already verified with buildInfo.Version
|
||||
if incremental.IsBuildInfoFileNameDefaultLibrary(buildInfoFileName) {
|
||||
continue
|
||||
}
|
||||
inputFile := tspath.GetNormalizedAbsolutePath(buildInfoFileName, getBuildInfoDirectory())
|
||||
inputPath := orchestrator.toPath(inputFile)
|
||||
// Root files are already checked
|
||||
if seenRoots.Has(inputPath) || resolvedRoots.Has(inputPath) {
|
||||
continue
|
||||
}
|
||||
inputTime := orchestrator.host.GetMTime(inputFile)
|
||||
if inputTime.IsZero() {
|
||||
// Input file that was part of the program is missing (eg: dependency was removed)
|
||||
return &upToDateStatus{kind: upToDateStatusTypeInputFileMissing, data: inputFile}
|
||||
}
|
||||
if inputTime.After(oldestOutputFileAndTime.time) {
|
||||
var currentVersion string
|
||||
version := buildInfoFileInfo.GetFileInfo().Version()
|
||||
if version != "" {
|
||||
if text, ok := orchestrator.host.FS().ReadFile(inputFile); ok {
|
||||
currentVersion = incremental.ComputeHash(text, orchestrator.opts.Testing != nil)
|
||||
}
|
||||
}
|
||||
if version == "" || version != currentVersion {
|
||||
return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{inputFile, buildInfoPath}}
|
||||
}
|
||||
inputTextUnchanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
for packageJson := range buildInfo.GetPackageJsons(getBuildInfoDirectory()) {
|
||||
packageJsonTime := orchestrator.host.GetMTime(packageJson)
|
||||
if packageJsonTime.IsZero() {
|
||||
return &upToDateStatus{kind: upToDateStatusTypeInputFileMissing, data: packageJson}
|
||||
}
|
||||
if packageJsonTime.After(oldestOutputFileAndTime.time) {
|
||||
return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{packageJson, oldestOutputFileAndTime.file}}
|
||||
}
|
||||
}
|
||||
for packageJson := range buildInfo.GetMissingPackageJsons(getBuildInfoDirectory()) {
|
||||
if !orchestrator.host.GetMTime(packageJson).IsZero() {
|
||||
return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{packageJson, oldestOutputFileAndTime.file}}
|
||||
}
|
||||
}
|
||||
t.packageJsons = slices.Collect(buildInfo.GetPackageJsons(getBuildInfoDirectory()))
|
||||
t.packageJsons = append(t.packageJsons, slices.Collect(buildInfo.GetMissingPackageJsons(getBuildInfoDirectory()))...)
|
||||
|
||||
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]) {
|
||||
if t.resolved != nil {
|
||||
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) 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, data *compiler.WriteFileData) error {
|
||||
err := orchestrator.host.FS().WriteFile(fileName, text)
|
||||
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
|
||||
}
|
||||
41
tools/tsgo/internal/execute/build/compilerHost.go
Normal file
41
tools/tsgo/internal/execute/build/compilerHost.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/tsoptions"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
type compilerHost struct {
|
||||
host *host
|
||||
trace func(msg *diagnostics.Message, args ...any)
|
||||
}
|
||||
|
||||
var _ compiler.CompilerHost = (*compilerHost)(nil)
|
||||
|
||||
func (h *compilerHost) FS() vfs.FS {
|
||||
return h.host.FS()
|
||||
}
|
||||
|
||||
func (h *compilerHost) DefaultLibraryPath() string {
|
||||
return h.host.DefaultLibraryPath()
|
||||
}
|
||||
|
||||
func (h *compilerHost) GetCurrentDirectory() string {
|
||||
return h.host.GetCurrentDirectory()
|
||||
}
|
||||
|
||||
func (h *compilerHost) Trace(msg *diagnostics.Message, args ...any) {
|
||||
h.trace(msg, args...)
|
||||
}
|
||||
|
||||
func (h *compilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile {
|
||||
return h.host.GetSourceFile(opts)
|
||||
}
|
||||
|
||||
func (h *compilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine {
|
||||
return h.host.GetResolvedProjectReference(fileName, path)
|
||||
}
|
||||
153
tools/tsgo/internal/execute/build/graph_test.go
Normal file
153
tools/tsgo/internal/execute/build/graph_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package build_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/execute/build"
|
||||
"github.com/microsoft/typescript-go/internal/execute/tsctests"
|
||||
"github.com/microsoft/typescript-go/internal/tsoptions"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestBuildOrderGenerator(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []*buildOrderTestCase{
|
||||
{"specify two roots", []string{"A", "G"}, []string{"D", "E", "C", "B", "A", "G"}, false},
|
||||
{"multiple parts of the same graph in various orders", []string{"A"}, []string{"D", "E", "C", "B", "A"}, false},
|
||||
{"multiple parts of the same graph in various orders", []string{"A", "C", "D"}, []string{"D", "E", "C", "B", "A"}, false},
|
||||
{"multiple parts of the same graph in various orders", []string{"D", "C", "A"}, []string{"D", "E", "C", "B", "A"}, false},
|
||||
{"other orderings", []string{"F"}, []string{"E", "F"}, false},
|
||||
{"other orderings", []string{"E"}, []string{"E"}, false},
|
||||
{"other orderings", []string{"F", "C", "A"}, []string{"E", "F", "D", "C", "B", "A"}, false},
|
||||
{"returns circular order", []string{"H"}, []string{"E", "J", "I", "H"}, true},
|
||||
{"returns circular order", []string{"A", "H"}, []string{"D", "E", "C", "B", "A", "J", "I", "H"}, true},
|
||||
}
|
||||
for _, testcase := range testCases {
|
||||
testcase.run(t)
|
||||
}
|
||||
}
|
||||
|
||||
type buildOrderTestCase struct {
|
||||
name string
|
||||
projects []string
|
||||
expected []string
|
||||
circular bool
|
||||
}
|
||||
|
||||
func (b *buildOrderTestCase) configName(project string) string {
|
||||
return fmt.Sprintf("/home/src/workspaces/project/%s/tsconfig.json", project)
|
||||
}
|
||||
|
||||
func (b *buildOrderTestCase) projectName(config string) string {
|
||||
str := strings.TrimPrefix(config, "/home/src/workspaces/project/")
|
||||
str = strings.TrimSuffix(str, "/tsconfig.json")
|
||||
return str
|
||||
}
|
||||
|
||||
func (b *buildOrderTestCase) run(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Run(b.name+" - "+strings.Join(b.projects, ","), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
files := make(map[string]any)
|
||||
deps := map[string][]string{
|
||||
"A": {"B", "C"},
|
||||
"B": {"C", "D"},
|
||||
"C": {"D", "E"},
|
||||
"F": {"E"},
|
||||
"H": {"I"},
|
||||
"I": {"J"},
|
||||
"J": {"H", "E"},
|
||||
}
|
||||
reverseDeps := map[string][]string{}
|
||||
for project, deps := range deps {
|
||||
for _, dep := range deps {
|
||||
reverseDeps[dep] = append(reverseDeps[dep], project)
|
||||
}
|
||||
}
|
||||
verifyDeps := func(orchestrator *build.Orchestrator, buildOrder []string, hasDownStream bool) {
|
||||
for index, project := range buildOrder {
|
||||
upstream := core.Map(orchestrator.Upstream(b.configName(project)), b.projectName)
|
||||
expectedUpstream := deps[project]
|
||||
assert.Assert(t, len(upstream) <= len(expectedUpstream), fmt.Sprintf("Expected upstream for %s to be at most %d, got %d", project, len(expectedUpstream), len(upstream)))
|
||||
for _, expected := range expectedUpstream {
|
||||
if slices.Contains(buildOrder[:index], expected) {
|
||||
assert.Assert(t, slices.Contains(upstream, expected), fmt.Sprintf("Expected upstream for %s to contain %s", project, expected))
|
||||
} else {
|
||||
assert.Assert(t, !slices.Contains(upstream, expected), fmt.Sprintf("Expected upstream for %s to not contain %s", project, expected))
|
||||
}
|
||||
}
|
||||
|
||||
downstream := core.Map(orchestrator.Downstream(b.configName(project)), b.projectName)
|
||||
expectedDownstream := core.IfElse(hasDownStream, reverseDeps[project], nil)
|
||||
assert.Assert(t, len(downstream) <= len(expectedDownstream), fmt.Sprintf("Expected downstream for %s to be at most %d, got %d", project, len(expectedDownstream), len(downstream)))
|
||||
for _, expected := range expectedDownstream {
|
||||
if slices.Contains(buildOrder[index+1:], expected) {
|
||||
assert.Assert(t, slices.Contains(downstream, expected), fmt.Sprintf("Expected downstream for %s to contain %s", project, expected))
|
||||
} else {
|
||||
assert.Assert(t, !slices.Contains(downstream, expected), fmt.Sprintf("Expected downstream for %s to not contain %s", project, expected))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, project := range []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"} {
|
||||
files[fmt.Sprintf("/home/src/workspaces/project/%s/%s.ts", project, project)] = "export {}"
|
||||
referencesStr := ""
|
||||
if deps, ok := deps[project]; ok {
|
||||
referencesStr = fmt.Sprintf(`, "references": [%s]`, strings.Join(core.Map(deps, func(dep string) string {
|
||||
return fmt.Sprintf(`{ "path": "../%s" }`, dep)
|
||||
}), ","))
|
||||
}
|
||||
files[b.configName(project)] = fmt.Sprintf(`{
|
||||
"compilerOptions": { "composite": true },
|
||||
"files": ["./%s.ts"],
|
||||
%s
|
||||
}`, project, referencesStr)
|
||||
}
|
||||
|
||||
sys := tsctests.NewTscSystem(files, true, "/home/src/workspaces/project")
|
||||
args := append([]string{"--build", "--dry"}, b.projects...)
|
||||
buildCommand := tsoptions.ParseBuildCommandLine(args, sys)
|
||||
orchestrator := build.NewOrchestrator(build.Options{
|
||||
Sys: sys,
|
||||
Command: buildCommand,
|
||||
})
|
||||
orchestrator.GenerateGraph(nil)
|
||||
buildOrder := core.Map(orchestrator.Order(), b.projectName)
|
||||
assert.DeepEqual(t, buildOrder, b.expected)
|
||||
verifyDeps(orchestrator, buildOrder, false)
|
||||
|
||||
if !b.circular {
|
||||
for project, projectDeps := range deps {
|
||||
child := b.configName(project)
|
||||
childIndex := slices.Index(buildOrder, child)
|
||||
if childIndex == -1 {
|
||||
continue
|
||||
}
|
||||
for _, dep := range projectDeps {
|
||||
parent := b.configName(dep)
|
||||
parentIndex := slices.Index(buildOrder, parent)
|
||||
|
||||
assert.Assert(t, childIndex > parentIndex, fmt.Sprintf("Expecting child %s to be built after parent %s", project, dep))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orchestrator.GenerateGraphReusingOldTasks()
|
||||
buildOrder2 := core.Map(orchestrator.Order(), b.projectName)
|
||||
assert.DeepEqual(t, buildOrder2, b.expected)
|
||||
|
||||
argsWatch := append([]string{"--build", "--watch"}, b.projects...)
|
||||
buildCommandWatch := tsoptions.ParseBuildCommandLine(argsWatch, sys)
|
||||
orchestrator = build.NewOrchestrator(build.Options{
|
||||
Sys: sys,
|
||||
Command: buildCommandWatch,
|
||||
})
|
||||
orchestrator.GenerateGraph(nil)
|
||||
buildOrder3 := core.Map(orchestrator.Order(), b.projectName)
|
||||
verifyDeps(orchestrator, buildOrder3, true)
|
||||
})
|
||||
}
|
||||
122
tools/tsgo/internal/execute/build/host.go
Normal file
122
tools/tsgo/internal/execute/build/host.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/execute/incremental"
|
||||
"github.com/microsoft/typescript-go/internal/execute/tsc"
|
||||
"github.com/microsoft/typescript-go/internal/tsoptions"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
type host struct {
|
||||
orchestrator *Orchestrator
|
||||
host compiler.CompilerHost
|
||||
|
||||
// Caches that last only for build cycle and then cleared out
|
||||
extendedConfigCache tsc.ExtendedConfigCache
|
||||
sourceFiles parseCache[ast.SourceFileParseOptions, *ast.SourceFile]
|
||||
configTimes collections.SyncMap[tspath.Path, time.Duration]
|
||||
|
||||
// caches that stay as long as they are needed
|
||||
resolvedReferences parseCache[tspath.Path, *tsoptions.ParsedCommandLine]
|
||||
mTimes *collections.SyncMap[tspath.Path, time.Time]
|
||||
}
|
||||
|
||||
var (
|
||||
_ compiler.CompilerHost = (*host)(nil)
|
||||
_ incremental.BuildInfoReader = (*host)(nil)
|
||||
_ incremental.Host = (*host)(nil)
|
||||
)
|
||||
|
||||
func (h *host) FS() vfs.FS {
|
||||
return h.host.FS()
|
||||
}
|
||||
|
||||
func (h *host) DefaultLibraryPath() string {
|
||||
return h.host.DefaultLibraryPath()
|
||||
}
|
||||
|
||||
func (h *host) GetCurrentDirectory() string {
|
||||
return h.host.GetCurrentDirectory()
|
||||
}
|
||||
|
||||
func (h *host) Trace(msg *diagnostics.Message, args ...any) {
|
||||
panic("build.Orchestrator.host does not support tracing, use a different host for tracing")
|
||||
}
|
||||
|
||||
func (h *host) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile {
|
||||
if tspath.IsDeclarationFileName(opts.FileName) || tspath.FileExtensionIs(opts.FileName, tspath.ExtensionJson) {
|
||||
// Cache dts and json files as they will be reused
|
||||
return h.sourceFiles.loadOrStore(opts, h.host.GetSourceFile, false /* allowZero */)
|
||||
}
|
||||
return h.host.GetSourceFile(opts)
|
||||
}
|
||||
|
||||
func (h *host) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine {
|
||||
return h.resolvedReferences.loadOrStore(path, func(path tspath.Path) *tsoptions.ParsedCommandLine {
|
||||
configStart := h.orchestrator.opts.Sys.Now()
|
||||
// Wrap command line options in "compilerOptions" key to match tsconfig.json structure
|
||||
var commandLineRaw *collections.OrderedMap[string, any]
|
||||
if raw, ok := h.orchestrator.opts.Command.Raw.(*collections.OrderedMap[string, any]); ok {
|
||||
wrapped := &collections.OrderedMap[string, any]{}
|
||||
wrapped.Set("compilerOptions", raw)
|
||||
commandLineRaw = wrapped
|
||||
}
|
||||
commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, h.orchestrator.opts.Command.CompilerOptions, commandLineRaw, h, &h.extendedConfigCache)
|
||||
configTime := h.orchestrator.opts.Sys.Now().Sub(configStart)
|
||||
h.configTimes.Store(path, configTime)
|
||||
return commandLine
|
||||
}, true /* allowZero */)
|
||||
}
|
||||
|
||||
func (h *host) ReadBuildInfo(config *tsoptions.ParsedCommandLine) *incremental.BuildInfo {
|
||||
configPath := h.orchestrator.toPath(config.ConfigName())
|
||||
task := h.orchestrator.getTask(configPath)
|
||||
buildInfo, _ := task.loadOrStoreBuildInfo(h.orchestrator, h.orchestrator.toPath(config.ConfigName()), config.GetBuildInfoFileName())
|
||||
return buildInfo
|
||||
}
|
||||
|
||||
func (h *host) GetMTime(file string) time.Time {
|
||||
return h.loadOrStoreMTime(file, nil, true)
|
||||
}
|
||||
|
||||
func (h *host) SetMTime(file string, mTime time.Time) error {
|
||||
return h.FS().Chtimes(file, time.Time{}, mTime)
|
||||
}
|
||||
|
||||
func (h *host) loadOrStoreMTime(file string, oldCache *collections.SyncMap[tspath.Path, time.Time], store bool) time.Time {
|
||||
path := h.orchestrator.toPath(file)
|
||||
if existing, loaded := h.mTimes.Load(path); loaded {
|
||||
return existing
|
||||
}
|
||||
var found bool
|
||||
var mTime time.Time
|
||||
if oldCache != nil {
|
||||
mTime, found = oldCache.Load(path)
|
||||
}
|
||||
if !found {
|
||||
mTime = incremental.GetMTime(h.host, file)
|
||||
}
|
||||
if store {
|
||||
mTime, _ = h.mTimes.LoadOrStore(path, mTime)
|
||||
}
|
||||
return mTime
|
||||
}
|
||||
|
||||
func (h *host) storeMTime(file string, mTime time.Time) {
|
||||
path := h.orchestrator.toPath(file)
|
||||
h.mTimes.Store(path, mTime)
|
||||
}
|
||||
|
||||
func (h *host) storeMTimeFromOldCache(file string, oldCache *collections.SyncMap[tspath.Path, time.Time]) {
|
||||
path := h.orchestrator.toPath(file)
|
||||
if mTime, found := oldCache.Load(path); found {
|
||||
h.mTimes.Store(path, mTime)
|
||||
}
|
||||
}
|
||||
713
tools/tsgo/internal/execute/build/orchestrator.go
Normal file
713
tools/tsgo/internal/execute/build/orchestrator.go
Normal file
@@ -0,0 +1,713 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/execute/incremental"
|
||||
"github.com/microsoft/typescript-go/internal/execute/tsc"
|
||||
"github.com/microsoft/typescript-go/internal/execute/watchmanager"
|
||||
"github.com/microsoft/typescript-go/internal/fswatch"
|
||||
"github.com/microsoft/typescript-go/internal/tsoptions"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/cachedvfs"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Sys tsc.System
|
||||
Command *tsoptions.ParsedBuildCommandLine
|
||||
Testing tsc.CommandLineTesting
|
||||
}
|
||||
|
||||
type orchestratorResult struct {
|
||||
result tsc.CommandLineResult
|
||||
errors []*ast.Diagnostic
|
||||
statistics tsc.Statistics
|
||||
filesToDelete []string
|
||||
}
|
||||
|
||||
func (b *orchestratorResult) report(o *Orchestrator) {
|
||||
if o.opts.Command.CompilerOptions.Watch.IsTrue() {
|
||||
o.watchStatusReporter(ast.NewCompilerDiagnostic(core.IfElse(len(b.errors) == 1, diagnostics.Found_1_error_Watching_for_file_changes, diagnostics.Found_0_errors_Watching_for_file_changes), len(b.errors)))
|
||||
} else {
|
||||
o.errorSummaryReporter(b.errors)
|
||||
}
|
||||
if b.filesToDelete != nil {
|
||||
o.createBuilderStatusReporter(nil)(
|
||||
ast.NewCompilerDiagnostic(
|
||||
diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0,
|
||||
strings.Join(core.Map(b.filesToDelete, func(f string) string {
|
||||
return "\r\n * " + f
|
||||
}), ""),
|
||||
),
|
||||
)
|
||||
}
|
||||
if !o.opts.Command.CompilerOptions.Diagnostics.IsTrue() && !o.opts.Command.CompilerOptions.ExtendedDiagnostics.IsTrue() {
|
||||
return
|
||||
}
|
||||
b.statistics.SetTotalTime(o.opts.Sys.SinceStart())
|
||||
b.statistics.Report(o.opts.Sys.Writer(), o.opts.Testing)
|
||||
}
|
||||
|
||||
type Orchestrator struct {
|
||||
opts Options
|
||||
comparePathsOptions tspath.ComparePathsOptions
|
||||
host *host
|
||||
|
||||
// order generation result
|
||||
tasks *collections.SyncMap[tspath.Path, *BuildTask]
|
||||
order []string
|
||||
errors []*ast.Diagnostic
|
||||
|
||||
errorSummaryReporter tsc.DiagnosticsReporter
|
||||
watchStatusReporter tsc.DiagnosticReporter
|
||||
|
||||
// fswatch event-based watching
|
||||
wm *watchmanager.WatchManager
|
||||
}
|
||||
|
||||
var _ tsc.Watcher = (*Orchestrator)(nil)
|
||||
|
||||
func (o *Orchestrator) relativeFileName(fileName string) string {
|
||||
return tspath.ConvertToRelativePath(fileName, o.comparePathsOptions)
|
||||
}
|
||||
|
||||
func (o *Orchestrator) toPath(fileName string) tspath.Path {
|
||||
return tspath.ToPath(fileName, o.comparePathsOptions.CurrentDirectory, o.comparePathsOptions.UseCaseSensitiveFileNames)
|
||||
}
|
||||
|
||||
func (o *Orchestrator) resolveBuildInfoFileName(fileName string, buildInfoDir string) string {
|
||||
if incremental.IsBuildInfoFileNameDefaultLibrary(fileName) {
|
||||
return tspath.CombinePaths(o.host.DefaultLibraryPath(), fileName)
|
||||
}
|
||||
return tspath.GetNormalizedAbsolutePath(fileName, buildInfoDir)
|
||||
}
|
||||
|
||||
func (o *Orchestrator) Order() []string {
|
||||
return o.order
|
||||
}
|
||||
|
||||
func (o *Orchestrator) Upstream(configName string) []string {
|
||||
path := o.toPath(configName)
|
||||
task := o.getTask(path)
|
||||
return core.Map(task.upStream, func(t *upstreamTask) string {
|
||||
return t.task.config
|
||||
})
|
||||
}
|
||||
|
||||
func (o *Orchestrator) Downstream(configName string) []string {
|
||||
path := o.toPath(configName)
|
||||
task := o.getTask(path)
|
||||
return core.Map(task.downStream, func(t *BuildTask) string {
|
||||
return t.config
|
||||
})
|
||||
}
|
||||
|
||||
func (o *Orchestrator) getTask(path tspath.Path) *BuildTask {
|
||||
task, ok := o.tasks.Load(path)
|
||||
if !ok {
|
||||
panic("No build task found for " + path)
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
func (o *Orchestrator) createBuildTasks(oldTasks *collections.SyncMap[tspath.Path, *BuildTask], configs []string, wg core.WorkGroup) {
|
||||
for _, config := range configs {
|
||||
wg.Queue(func() {
|
||||
path := o.toPath(config)
|
||||
var task *BuildTask
|
||||
var buildInfo *buildInfoEntry
|
||||
if oldTasks != nil {
|
||||
if existing, ok := oldTasks.Load(path); ok {
|
||||
if !existing.dirty {
|
||||
// Reuse existing task if config is same
|
||||
task = existing
|
||||
} else {
|
||||
buildInfo = existing.buildInfoEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
if task == nil {
|
||||
task = &BuildTask{config: config, isInitialCycle: oldTasks == nil}
|
||||
task.pending.Store(true)
|
||||
task.buildInfoEntry = buildInfo
|
||||
}
|
||||
if _, loaded := o.tasks.LoadOrStore(path, task); loaded {
|
||||
return
|
||||
}
|
||||
task.resolved = o.host.GetResolvedProjectReference(config, path)
|
||||
task.upStream = nil
|
||||
if task.resolved != nil {
|
||||
o.createBuildTasks(oldTasks, task.resolved.ResolvedProjectReferencePaths(), wg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Orchestrator) setupBuildTask(
|
||||
configName string,
|
||||
downStream *BuildTask,
|
||||
inCircularContext bool,
|
||||
completed *collections.Set[tspath.Path],
|
||||
analyzing *collections.Set[tspath.Path],
|
||||
circularityStack []string,
|
||||
) *BuildTask {
|
||||
path := o.toPath(configName)
|
||||
task := o.getTask(path)
|
||||
if !completed.Has(path) {
|
||||
if analyzing.Has(path) {
|
||||
if !inCircularContext {
|
||||
o.errors = append(o.errors, ast.NewCompilerDiagnostic(
|
||||
diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0,
|
||||
strings.Join(circularityStack, "\n"),
|
||||
))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
analyzing.Add(path)
|
||||
circularityStack = append(circularityStack, configName)
|
||||
if task.resolved != nil {
|
||||
for index, subReference := range task.resolved.ResolvedProjectReferencePaths() {
|
||||
upstream := o.setupBuildTask(subReference, task, inCircularContext || task.resolved.ProjectReferences()[index].Circular, completed, analyzing, circularityStack)
|
||||
if upstream != nil {
|
||||
task.upStream = append(task.upStream, &upstreamTask{task: upstream, refIndex: index})
|
||||
}
|
||||
}
|
||||
}
|
||||
circularityStack = circularityStack[:len(circularityStack)-1]
|
||||
completed.Add(path)
|
||||
task.reportDone = make(chan struct{})
|
||||
prev := core.LastOrNil(o.order)
|
||||
if prev != "" {
|
||||
task.prevReporter = o.getTask(o.toPath(prev))
|
||||
}
|
||||
task.done = make(chan struct{})
|
||||
o.order = append(o.order, configName)
|
||||
}
|
||||
if o.opts.Command.CompilerOptions.Watch.IsTrue() && downStream != nil {
|
||||
task.downStream = append(task.downStream, downStream)
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
func (o *Orchestrator) GenerateGraphReusingOldTasks() {
|
||||
tasks := o.tasks
|
||||
o.tasks = &collections.SyncMap[tspath.Path, *BuildTask]{}
|
||||
o.order = nil
|
||||
o.errors = nil
|
||||
o.GenerateGraph(tasks)
|
||||
}
|
||||
|
||||
func (o *Orchestrator) GenerateGraph(oldTasks *collections.SyncMap[tspath.Path, *BuildTask]) {
|
||||
projects := o.opts.Command.ResolvedProjectPaths()
|
||||
// Parse all config files in parallel
|
||||
wg := core.NewWorkGroup(o.opts.Command.CompilerOptions.SingleThreaded.IsTrue())
|
||||
o.createBuildTasks(oldTasks, projects, wg)
|
||||
wg.RunAndWait()
|
||||
|
||||
// Generate the graph
|
||||
completed := collections.Set[tspath.Path]{}
|
||||
analyzing := collections.Set[tspath.Path]{}
|
||||
circularityStack := []string{}
|
||||
for _, project := range projects {
|
||||
o.setupBuildTask(project, nil, false, &completed, &analyzing, circularityStack)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Orchestrator) Start(ctx context.Context) tsc.CommandLineResult {
|
||||
if o.opts.Command.CompilerOptions.Watch.IsTrue() {
|
||||
o.watchStatusReporter(ast.NewCompilerDiagnostic(diagnostics.Starting_compilation_in_watch_mode))
|
||||
}
|
||||
o.GenerateGraph(nil)
|
||||
result := o.buildOrClean()
|
||||
if o.opts.Command.CompilerOptions.Watch.IsTrue() {
|
||||
o.Watch(ctx)
|
||||
result.Watcher = o
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (o *Orchestrator) Watch(ctx context.Context) {
|
||||
o.wm.Lock()
|
||||
|
||||
if o.opts.Testing == nil {
|
||||
if o.opts.Sys.GetEnvironmentVariable("TS_WATCH_DEBUG") != "" {
|
||||
o.wm.DebugLog = o.opts.Sys.Writer()
|
||||
}
|
||||
o.wm.EnsureDefaultBackend()
|
||||
}
|
||||
|
||||
o.updateWatch()
|
||||
desiredDirs := o.computeDesiredWatches()
|
||||
if err := o.wm.ReconcileWatches(desiredDirs); err != nil {
|
||||
fmt.Fprintf(o.opts.Sys.Writer(), "%v\n", err)
|
||||
o.wm.ForceOverflow()
|
||||
}
|
||||
o.resetCaches()
|
||||
|
||||
o.wm.Unlock()
|
||||
|
||||
if o.opts.Testing == nil {
|
||||
o.wm.RunLoop(ctx, o.DoCycle)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Orchestrator) updateWatch() {
|
||||
oldCache := o.host.mTimes
|
||||
o.host.mTimes = &collections.SyncMap[tspath.Path, time.Time]{}
|
||||
o.rangeTask(func(path tspath.Path, task *BuildTask) {
|
||||
task.updateWatch(o, oldCache)
|
||||
})
|
||||
}
|
||||
|
||||
func (o *Orchestrator) resetCaches() {
|
||||
// Clean out all the caches
|
||||
cachesVfs := o.host.host.FS().(*cachedvfs.FS)
|
||||
cachesVfs.ClearCache()
|
||||
o.host.extendedConfigCache = tsc.ExtendedConfigCache{}
|
||||
o.host.sourceFiles.reset()
|
||||
o.host.configTimes = collections.SyncMap[tspath.Path, time.Duration]{}
|
||||
}
|
||||
|
||||
func (o *Orchestrator) checkTasksForEventChanges(changedPaths map[string]fswatch.EventKind, needsConfigUpdate, needsUpdate *atomic.Bool) {
|
||||
normalizedPaths := make(map[tspath.Path]fswatch.EventKind, len(changedPaths))
|
||||
for eventPath, kind := range changedPaths {
|
||||
normalizedPaths[o.toPath(eventPath)] = kind
|
||||
}
|
||||
|
||||
for i := range o.order {
|
||||
config := o.order[i]
|
||||
path := o.toPath(config)
|
||||
task := o.getTask(path)
|
||||
|
||||
configPath := o.toPath(task.config)
|
||||
if _, changed := normalizedPaths[configPath]; changed {
|
||||
task.resetConfig(o, path)
|
||||
needsConfigUpdate.Store(true)
|
||||
needsUpdate.Store(true)
|
||||
continue
|
||||
}
|
||||
|
||||
if task.resolved == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
configChanged := false
|
||||
for _, file := range task.resolved.ExtendedSourceFiles() {
|
||||
fp := o.toPath(file)
|
||||
if _, changed := normalizedPaths[fp]; changed {
|
||||
task.resetConfig(o, path)
|
||||
needsConfigUpdate.Store(true)
|
||||
needsUpdate.Store(true)
|
||||
configChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if configChanged {
|
||||
continue
|
||||
}
|
||||
|
||||
rootChanged := false
|
||||
fileNames := task.resolved.FileNames()
|
||||
roots := collections.NewSetWithSizeHint[tspath.Path](len(fileNames))
|
||||
for _, file := range fileNames {
|
||||
fp := o.toPath(file)
|
||||
roots.Add(fp)
|
||||
if !rootChanged {
|
||||
if _, changed := normalizedPaths[fp]; changed {
|
||||
task.resetStatus()
|
||||
needsUpdate.Store(true)
|
||||
rootChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !rootChanged {
|
||||
task.buildInfoEntryMu.Lock()
|
||||
bi := task.buildInfoEntry
|
||||
task.buildInfoEntryMu.Unlock()
|
||||
if bi != nil && bi.buildInfo != nil {
|
||||
buildInfoDir := tspath.GetDirectoryPath(string(bi.path))
|
||||
for _, fileName := range bi.buildInfo.FileNames {
|
||||
fp := o.toPath(o.resolveBuildInfoFileName(fileName, buildInfoDir))
|
||||
if roots.Has(fp) {
|
||||
continue
|
||||
}
|
||||
if _, changed := normalizedPaths[fp]; changed {
|
||||
task.resetStatus()
|
||||
needsUpdate.Store(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
for packageJson := range bi.buildInfo.GetPackageJsons(buildInfoDir) {
|
||||
if o.packageJsonLookupChanged(packageJson, normalizedPaths) {
|
||||
task.resetStatus()
|
||||
needsUpdate.Store(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
for packageJson := range bi.buildInfo.GetMissingPackageJsons(buildInfoDir) {
|
||||
if o.packageJsonLookupChanged(packageJson, normalizedPaths) {
|
||||
task.resetStatus()
|
||||
needsUpdate.Store(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, packageJson := range task.packageJsons {
|
||||
if o.packageJsonLookupChanged(packageJson, normalizedPaths) {
|
||||
task.resetStatus()
|
||||
needsUpdate.Store(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.reportDone = make(chan struct{})
|
||||
task.done = make(chan struct{})
|
||||
|
||||
newConfig := task.resolved.ReloadFileNamesOfParsedCommandLine(o.host.FS())
|
||||
if !slices.Equal(task.resolved.FileNames(), newConfig.FileNames()) {
|
||||
o.host.resolvedReferences.store(path, newConfig)
|
||||
task.resolved = newConfig
|
||||
task.resetStatus()
|
||||
needsUpdate.Store(true)
|
||||
}
|
||||
}
|
||||
|
||||
if !needsUpdate.Load() {
|
||||
opts := o.comparePathsOptions
|
||||
for eventPath := range changedPaths {
|
||||
if o.host.FS().DirectoryExists(eventPath) {
|
||||
if o.wm.IsPathUnderWatch(eventPath, opts) {
|
||||
o.rangeTask(func(path tspath.Path, task *BuildTask) {
|
||||
task.resetStatus()
|
||||
task.reportDone = make(chan struct{})
|
||||
task.done = make(chan struct{})
|
||||
})
|
||||
needsUpdate.Store(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Orchestrator) packageJsonLookupChanged(packageJson string, changedPaths map[tspath.Path]fswatch.EventKind) bool {
|
||||
packageJsonPath := o.toPath(packageJson)
|
||||
if _, changed := changedPaths[packageJsonPath]; changed {
|
||||
return true
|
||||
}
|
||||
for changedPath, kind := range changedPaths {
|
||||
if kind == fswatch.EventDelete && tspath.ContainsPath(string(changedPath), string(packageJsonPath), o.comparePathsOptions) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (o *Orchestrator) computeDesiredWatches() map[string]bool {
|
||||
desiredDirs := make(map[string]bool)
|
||||
|
||||
for i := range o.order {
|
||||
config := o.order[i]
|
||||
path := o.toPath(config)
|
||||
task := o.getTask(path)
|
||||
|
||||
// Watch config file directory
|
||||
configDir := tspath.GetDirectoryPath(task.config)
|
||||
realConfigDir := o.host.FS().Realpath(configDir)
|
||||
if _, has := desiredDirs[realConfigDir]; !has {
|
||||
desiredDirs[realConfigDir] = false
|
||||
}
|
||||
|
||||
if task.resolved == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extended config file directories
|
||||
for _, cfgPath := range task.resolved.ExtendedSourceFiles() {
|
||||
realPath := o.host.FS().Realpath(cfgPath)
|
||||
dir := tspath.GetDirectoryPath(realPath)
|
||||
if _, has := desiredDirs[dir]; !has {
|
||||
desiredDirs[dir] = false
|
||||
}
|
||||
}
|
||||
|
||||
// Wildcard directories from tsconfig
|
||||
for dir, recursive := range task.resolved.WildcardDirectories() {
|
||||
realDir := o.host.FS().Realpath(dir)
|
||||
if existing, has := desiredDirs[realDir]; has {
|
||||
desiredDirs[realDir] = existing || recursive
|
||||
} else {
|
||||
desiredDirs[realDir] = recursive
|
||||
}
|
||||
}
|
||||
|
||||
// Input file directories not already covered
|
||||
for _, fileName := range task.resolved.FileNames() {
|
||||
absPath := tspath.GetNormalizedAbsolutePath(fileName, o.opts.Sys.GetCurrentDirectory())
|
||||
dir := tspath.GetDirectoryPath(absPath)
|
||||
if !watchmanager.IsDirCoveredByWatch(desiredDirs, dir, o.comparePathsOptions) {
|
||||
if watchmanager.CanWatchDirectory(dir) {
|
||||
desiredDirs[dir] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Non-root dependency directories from buildinfo (e.g. node_modules .d.ts files).
|
||||
task.buildInfoEntryMu.Lock()
|
||||
bi := task.buildInfoEntry
|
||||
task.buildInfoEntryMu.Unlock()
|
||||
if bi != nil && bi.buildInfo != nil {
|
||||
buildInfoDir := tspath.GetDirectoryPath(string(bi.path))
|
||||
roots := collections.NewSetFromItems(core.Map(task.resolved.FileNames(), o.toPath)...)
|
||||
for _, fileName := range bi.buildInfo.FileNames {
|
||||
absPath := o.host.FS().Realpath(o.resolveBuildInfoFileName(fileName, buildInfoDir))
|
||||
fp := o.toPath(absPath)
|
||||
if roots.Has(fp) {
|
||||
continue
|
||||
}
|
||||
dir := tspath.GetDirectoryPath(absPath)
|
||||
if !watchmanager.IsDirCoveredByWatch(desiredDirs, dir, o.comparePathsOptions) {
|
||||
if watchmanager.CanWatchDirectory(dir) {
|
||||
desiredDirs[dir] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
for packageJson := range bi.buildInfo.GetPackageJsons(buildInfoDir) {
|
||||
o.addPackageJsonWatchDirs(desiredDirs, packageJson)
|
||||
}
|
||||
for packageJson := range bi.buildInfo.GetMissingPackageJsons(buildInfoDir) {
|
||||
o.addPackageJsonWatchDirs(desiredDirs, packageJson)
|
||||
}
|
||||
}
|
||||
for _, packageJson := range task.packageJsons {
|
||||
o.addPackageJsonWatchDirs(desiredDirs, packageJson)
|
||||
}
|
||||
}
|
||||
|
||||
return o.wm.ResolveDesiredDirs(desiredDirs)
|
||||
}
|
||||
|
||||
func (o *Orchestrator) addWatchDir(desiredDirs map[string]bool, dir string) {
|
||||
if !watchmanager.IsDirCoveredByWatch(desiredDirs, dir, o.comparePathsOptions) && watchmanager.CanWatchDirectory(dir) {
|
||||
desiredDirs[dir] = false
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Orchestrator) addPackageJsonWatchDirs(desiredDirs map[string]bool, packageJson string) {
|
||||
dir := tspath.GetDirectoryPath(packageJson)
|
||||
dirs := []string{dir}
|
||||
foundNodeModules := false
|
||||
for current := dir; ; {
|
||||
parent := tspath.GetDirectoryPath(current)
|
||||
if parent == "" || parent == current {
|
||||
break
|
||||
}
|
||||
dirs = append(dirs, parent)
|
||||
if tspath.GetBaseFileName(parent) == "node_modules" {
|
||||
foundNodeModules = true
|
||||
if grandparent := tspath.GetDirectoryPath(parent); grandparent != "" && grandparent != parent {
|
||||
dirs = append(dirs, grandparent)
|
||||
}
|
||||
break
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
|
||||
if !foundNodeModules {
|
||||
o.addWatchDir(desiredDirs, dir)
|
||||
return
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
o.addWatchDir(desiredDirs, dir)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Orchestrator) DoCycle() {
|
||||
o.wm.Lock()
|
||||
defer o.wm.Unlock()
|
||||
|
||||
changedPaths, overflow := o.wm.DrainEvents()
|
||||
hasEvents := len(changedPaths) > 0 || overflow
|
||||
|
||||
if !hasEvents {
|
||||
if o.wm.DebugLog != nil {
|
||||
fmt.Fprintf(o.wm.DebugLog, "[watch] DoCycle: no events, skipping\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var needsConfigUpdate atomic.Bool
|
||||
var needsUpdate atomic.Bool
|
||||
|
||||
if overflow {
|
||||
// Overflow: reset all tasks to force a full rebuild.
|
||||
o.rangeTask(func(path tspath.Path, task *BuildTask) {
|
||||
task.resetConfig(o, path)
|
||||
task.reportDone = make(chan struct{})
|
||||
task.done = make(chan struct{})
|
||||
})
|
||||
needsConfigUpdate.Store(true)
|
||||
needsUpdate.Store(true)
|
||||
} else {
|
||||
// Event-driven: check only tasks affected by changed paths
|
||||
o.checkTasksForEventChanges(changedPaths, &needsConfigUpdate, &needsUpdate)
|
||||
}
|
||||
|
||||
if !needsUpdate.Load() {
|
||||
o.resetCaches()
|
||||
return
|
||||
}
|
||||
|
||||
o.watchStatusReporter(ast.NewCompilerDiagnostic(diagnostics.File_change_detected_Starting_incremental_compilation))
|
||||
if needsConfigUpdate.Load() {
|
||||
// Generate new tasks
|
||||
o.GenerateGraphReusingOldTasks()
|
||||
}
|
||||
|
||||
o.buildOrClean()
|
||||
o.updateWatch()
|
||||
desiredDirs := o.computeDesiredWatches()
|
||||
if err := o.wm.ReconcileWatches(desiredDirs); err != nil {
|
||||
fmt.Fprintf(o.opts.Sys.Writer(), "%v\n", err)
|
||||
// Mark overflow so the next event triggers a full rebuild
|
||||
o.wm.ForceOverflow()
|
||||
}
|
||||
o.resetCaches()
|
||||
}
|
||||
|
||||
func (o *Orchestrator) buildOrClean() tsc.CommandLineResult {
|
||||
if !o.opts.Command.BuildOptions.Clean.IsTrue() && o.opts.Command.BuildOptions.Verbose.IsTrue() {
|
||||
o.createBuilderStatusReporter(nil)(ast.NewCompilerDiagnostic(
|
||||
diagnostics.Projects_in_this_build_Colon_0,
|
||||
strings.Join(core.Map(o.Order(), func(p string) string {
|
||||
return "\r\n * " + o.relativeFileName(p)
|
||||
}), ""),
|
||||
))
|
||||
}
|
||||
var buildResult orchestratorResult
|
||||
if len(o.errors) == 0 {
|
||||
buildResult.statistics.Projects = len(o.Order())
|
||||
o.rangeTask(func(path tspath.Path, task *BuildTask) {
|
||||
o.buildOrCleanProject(task, path, &buildResult)
|
||||
})
|
||||
} else {
|
||||
// Circularity errors prevent any project from being built
|
||||
buildResult.result.Status = tsc.ExitStatusProjectReferenceCycle_OutputsSkipped
|
||||
reportDiagnostic := o.createDiagnosticReporter(nil)
|
||||
for _, err := range o.errors {
|
||||
reportDiagnostic(err)
|
||||
}
|
||||
buildResult.errors = o.errors
|
||||
}
|
||||
buildResult.report(o)
|
||||
return buildResult.result
|
||||
}
|
||||
|
||||
func (o *Orchestrator) rangeTask(f func(path tspath.Path, task *BuildTask)) {
|
||||
numRoutines := 4
|
||||
if o.opts.Command.CompilerOptions.SingleThreaded.IsTrue() {
|
||||
numRoutines = 1
|
||||
} else if builders := o.opts.Command.BuildOptions.Builders; builders != nil {
|
||||
numRoutines = *builders
|
||||
}
|
||||
|
||||
var currentTaskIndex atomic.Int64
|
||||
getNextTask := func() (tspath.Path, *BuildTask, bool) {
|
||||
index := int(currentTaskIndex.Add(1) - 1)
|
||||
if index >= len(o.order) {
|
||||
return "", nil, false
|
||||
}
|
||||
config := o.order[index]
|
||||
path := o.toPath(config)
|
||||
task := o.getTask(path)
|
||||
return path, task, true
|
||||
}
|
||||
runTask := func() {
|
||||
for path, task, ok := getNextTask(); ok; path, task, ok = getNextTask() {
|
||||
f(path, task)
|
||||
}
|
||||
}
|
||||
|
||||
if numRoutines == 1 {
|
||||
runTask()
|
||||
} else {
|
||||
wg := core.NewWorkGroup(false)
|
||||
for range numRoutines {
|
||||
wg.Queue(runTask)
|
||||
}
|
||||
wg.RunAndWait()
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Orchestrator) buildOrCleanProject(task *BuildTask, path tspath.Path, buildResult *orchestratorResult) {
|
||||
task.result = &taskResult{}
|
||||
task.result.reportStatus = o.createBuilderStatusReporter(task)
|
||||
task.result.diagnosticReporter = o.createDiagnosticReporter(task)
|
||||
if !o.opts.Command.BuildOptions.Clean.IsTrue() {
|
||||
task.buildProject(o, path)
|
||||
} else {
|
||||
task.cleanProject(o, path)
|
||||
}
|
||||
task.report(o, path, buildResult)
|
||||
}
|
||||
|
||||
func (o *Orchestrator) getWriter(task *BuildTask) io.Writer {
|
||||
if task == nil {
|
||||
return o.opts.Sys.Writer()
|
||||
}
|
||||
return &task.result.builder
|
||||
}
|
||||
|
||||
func (o *Orchestrator) createBuilderStatusReporter(task *BuildTask) tsc.DiagnosticReporter {
|
||||
return tsc.CreateBuilderStatusReporter(o.opts.Sys, o.getWriter(task), o.opts.Command.Locale(), o.opts.Command.CompilerOptions, o.opts.Testing)
|
||||
}
|
||||
|
||||
func (o *Orchestrator) createDiagnosticReporter(task *BuildTask) tsc.DiagnosticReporter {
|
||||
return tsc.CreateDiagnosticReporter(o.opts.Sys, o.getWriter(task), o.opts.Command.Locale(), o.opts.Command.CompilerOptions)
|
||||
}
|
||||
|
||||
func NewOrchestrator(opts Options) *Orchestrator {
|
||||
wm := watchmanager.NewWatchManager(opts.Sys.Writer(), opts.Sys.FS().DirectoryExists)
|
||||
orchestrator := &Orchestrator{
|
||||
opts: opts,
|
||||
comparePathsOptions: tspath.ComparePathsOptions{
|
||||
CurrentDirectory: opts.Sys.GetCurrentDirectory(),
|
||||
UseCaseSensitiveFileNames: opts.Sys.FS().UseCaseSensitiveFileNames(),
|
||||
},
|
||||
tasks: &collections.SyncMap[tspath.Path, *BuildTask]{},
|
||||
wm: wm,
|
||||
}
|
||||
orchestrator.host = &host{
|
||||
orchestrator: orchestrator,
|
||||
host: compiler.NewCachedFSCompilerHost(
|
||||
orchestrator.opts.Sys.GetCurrentDirectory(),
|
||||
orchestrator.opts.Sys.FS(),
|
||||
orchestrator.opts.Sys.DefaultLibraryPath(),
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
mTimes: &collections.SyncMap[tspath.Path, time.Time]{},
|
||||
}
|
||||
if opts.Command.CompilerOptions.Watch.IsTrue() {
|
||||
orchestrator.watchStatusReporter = tsc.CreateWatchStatusReporter(opts.Sys, opts.Command.Locale(), opts.Command.CompilerOptions, opts.Testing)
|
||||
if t, ok := opts.Testing.(watchmanager.CommandLineTestingWithWatchBackend); ok {
|
||||
wm.SetBackend(t.WatchBackend())
|
||||
}
|
||||
} else {
|
||||
orchestrator.errorSummaryReporter = tsc.CreateReportErrorSummary(opts.Sys, opts.Command.Locale(), opts.Command.CompilerOptions)
|
||||
}
|
||||
return orchestrator
|
||||
}
|
||||
44
tools/tsgo/internal/execute/build/parseCache.go
Normal file
44
tools/tsgo/internal/execute/build/parseCache.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
)
|
||||
|
||||
type parseCacheEntry[V comparable] struct {
|
||||
value V
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type parseCache[K comparable, V comparable] struct {
|
||||
entries collections.SyncMap[K, *parseCacheEntry[V]]
|
||||
}
|
||||
|
||||
func (c *parseCache[K, V]) loadOrStore(key K, parse func(K) V, allowZero bool) V {
|
||||
newEntry := &parseCacheEntry[V]{}
|
||||
newEntry.mu.Lock()
|
||||
defer newEntry.mu.Unlock()
|
||||
if entry, loaded := c.entries.LoadOrStore(key, newEntry); loaded {
|
||||
entry.mu.Lock()
|
||||
defer entry.mu.Unlock()
|
||||
if allowZero || entry.value != *new(V) {
|
||||
return entry.value
|
||||
}
|
||||
newEntry = entry
|
||||
}
|
||||
newEntry.value = parse(key)
|
||||
return newEntry.value
|
||||
}
|
||||
|
||||
func (c *parseCache[K, V]) store(key K, value V) {
|
||||
c.entries.Store(key, &parseCacheEntry[V]{value: value})
|
||||
}
|
||||
|
||||
func (c *parseCache[K, V]) delete(key K) {
|
||||
c.entries.Delete(key)
|
||||
}
|
||||
|
||||
func (c *parseCache[K, V]) reset() {
|
||||
c.entries = collections.SyncMap[K, *parseCacheEntry[V]]{}
|
||||
}
|
||||
133
tools/tsgo/internal/execute/build/uptodatestatus.go
Normal file
133
tools/tsgo/internal/execute/build/uptodatestatus.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package build
|
||||
|
||||
import "time"
|
||||
|
||||
type upToDateStatusType uint16
|
||||
|
||||
const (
|
||||
// Errors:
|
||||
|
||||
// config file was not found
|
||||
upToDateStatusTypeConfigFileNotFound upToDateStatusType = iota
|
||||
// found errors during build
|
||||
upToDateStatusTypeBuildErrors
|
||||
// did not build because upstream project has errors - and we have option to stop build on upstream errors
|
||||
upToDateStatusTypeUpstreamErrors
|
||||
|
||||
// Its all good, no work to do
|
||||
upToDateStatusTypeUpToDate
|
||||
|
||||
// Pseudo-builds - touch timestamps, no actual build:
|
||||
|
||||
// The project appears out of date because its upstream inputs are newer than its outputs,
|
||||
// but all of its outputs are actually newer than the previous identical outputs of its (.d.ts) inputs.
|
||||
// This means we can Pseudo-build (just touch timestamps), as if we had actually built this project.
|
||||
upToDateStatusTypeUpToDateWithUpstreamTypes
|
||||
// The project appears up to date and even though input file changed, its text didnt so just need to update timestamps
|
||||
upToDateStatusTypeUpToDateWithInputFileText
|
||||
|
||||
// Needs build:
|
||||
|
||||
// input file is missing
|
||||
upToDateStatusTypeInputFileMissing
|
||||
// output file is missing
|
||||
upToDateStatusTypeOutputMissing
|
||||
// input file is newer than output file
|
||||
upToDateStatusTypeInputFileNewer
|
||||
// build info is out of date as we need to emit some files
|
||||
upToDateStatusTypeOutOfDateBuildInfoWithPendingEmit
|
||||
// build info indicates that project has errors and they need to be reported
|
||||
upToDateStatusTypeOutOfDateBuildInfoWithErrors
|
||||
// build info options indicate there is work to do based on changes in options
|
||||
upToDateStatusTypeOutOfDateOptions
|
||||
// file was root when built but not any more
|
||||
upToDateStatusTypeOutOfDateRoots
|
||||
// buildInfo.version mismatch with current ts version
|
||||
upToDateStatusTypeTsVersionOutputOfDate
|
||||
// build because --force was specified
|
||||
upToDateStatusTypeForceBuild
|
||||
|
||||
// solution file
|
||||
upToDateStatusTypeSolution
|
||||
)
|
||||
|
||||
type inputOutputName struct {
|
||||
input string
|
||||
output string
|
||||
}
|
||||
|
||||
type fileAndTime struct {
|
||||
file string
|
||||
time time.Time
|
||||
}
|
||||
|
||||
type inputOutputFileAndTime struct {
|
||||
input fileAndTime
|
||||
output fileAndTime
|
||||
buildInfo string
|
||||
}
|
||||
|
||||
type upstreamErrors struct {
|
||||
ref string
|
||||
refHasUpstreamErrors bool
|
||||
}
|
||||
|
||||
type upToDateStatus struct {
|
||||
kind upToDateStatusType
|
||||
data any
|
||||
}
|
||||
|
||||
func (s *upToDateStatus) isError() bool {
|
||||
switch s.kind {
|
||||
case upToDateStatusTypeConfigFileNotFound,
|
||||
upToDateStatusTypeBuildErrors,
|
||||
upToDateStatusTypeUpstreamErrors:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *upToDateStatus) isPseudoBuild() bool {
|
||||
switch s.kind {
|
||||
case upToDateStatusTypeUpToDateWithUpstreamTypes,
|
||||
upToDateStatusTypeUpToDateWithInputFileText:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *upToDateStatus) inputOutputFileAndTime() *inputOutputFileAndTime {
|
||||
data, ok := s.data.(*inputOutputFileAndTime)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *upToDateStatus) inputOutputName() *inputOutputName {
|
||||
data, ok := s.data.(*inputOutputName)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *upToDateStatus) oldestOutputFileName() string {
|
||||
if !s.isPseudoBuild() && s.kind != upToDateStatusTypeUpToDate {
|
||||
panic("only valid for up to date status of pseudo-build or up to date")
|
||||
}
|
||||
|
||||
if inputOutputFileAndTime := s.inputOutputFileAndTime(); inputOutputFileAndTime != nil {
|
||||
return inputOutputFileAndTime.output.file
|
||||
}
|
||||
if inputOutputName := s.inputOutputName(); inputOutputName != nil {
|
||||
return inputOutputName.output
|
||||
}
|
||||
return s.data.(string)
|
||||
}
|
||||
|
||||
func (s *upToDateStatus) upstreamErrors() *upstreamErrors {
|
||||
return s.data.(*upstreamErrors)
|
||||
}
|
||||
Reference in New Issue
Block a user