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

161 lines
5.6 KiB
Go

package execute
import (
"fmt"
"reflect"
"time"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"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"
)
type Watcher struct {
sys tsc.System
configFileName string
config *tsoptions.ParsedCommandLine
reportDiagnostic tsc.DiagnosticReporter
reportErrorSummary tsc.DiagnosticsReporter
testing tsc.CommandLineTesting
host compiler.CompilerHost
program *incremental.Program
prevModified map[string]time.Time
configModified bool
}
var _ tsc.Watcher = (*Watcher)(nil)
func createWatcher(sys tsc.System, configParseResult *tsoptions.ParsedCommandLine, reportDiagnostic tsc.DiagnosticReporter, reportErrorSummary tsc.DiagnosticsReporter, testing tsc.CommandLineTesting) *Watcher {
w := &Watcher{
sys: sys,
config: configParseResult,
reportDiagnostic: reportDiagnostic,
reportErrorSummary: reportErrorSummary,
testing: testing,
// reportWatchStatus: createWatchStatusReporter(sys, configParseResult.CompilerOptions().Pretty),
}
if configParseResult.ConfigFile != nil {
w.configFileName = configParseResult.ConfigFile.SourceFile.FileName()
}
return w
}
func (w *Watcher) start() {
w.host = compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), nil, getTraceFromSys(w.sys, w.testing))
w.program = incremental.ReadBuildInfoProgram(w.config, incremental.NewBuildInfoReader(w.host), w.host)
if w.testing == nil {
watchInterval := w.config.ParsedConfig.WatchOptions.WatchInterval()
for {
w.DoCycle()
time.Sleep(watchInterval)
}
} else {
// Initial compilation in test mode
w.DoCycle()
}
}
func (w *Watcher) DoCycle() {
// if this function is updated, make sure to update `RunWatchCycle` in export_test.go as needed
if w.hasErrorsInTsConfig() {
// these are unrecoverable errors--report them and do not build
return
}
// updateProgram()
w.program = incremental.NewProgram(compiler.NewProgram(compiler.ProgramOptions{
Config: w.config,
Host: w.host,
JSDocParsingMode: ast.JSDocParsingModeParseForTypeErrors,
}), w.program, nil, w.testing != nil)
if w.hasBeenModified(w.program.GetProgram()) {
fmt.Fprintln(w.sys.Writer(), "build starting at", w.sys.Now().Format("03:04:05 PM"))
timeStart := w.sys.Now()
w.compileAndEmit()
fmt.Fprintf(w.sys.Writer(), "build finished in %.3fs\n", w.sys.Now().Sub(timeStart).Seconds())
} else {
// print something???
// fmt.Fprintln(w.sys.Writer(), "no changes detected at ", w.sys.Now())
}
if w.testing != nil {
w.testing.OnProgram(w.program)
}
}
func (w *Watcher) compileAndEmit() {
// !!! output/error reporting is currently the same as non-watch mode
// diagnostics, emitResult, exitStatus :=
tsc.EmitFilesAndReportErrors(tsc.EmitInput{
Sys: w.sys,
ProgramLike: w.program,
Program: w.program.GetProgram(),
ReportDiagnostic: w.reportDiagnostic,
ReportErrorSummary: w.reportErrorSummary,
Writer: w.sys.Writer(),
CompileTimes: &tsc.CompileTimes{},
Testing: w.testing,
})
}
func (w *Watcher) hasErrorsInTsConfig() bool {
// only need to check and reparse tsconfig options/update host if we are watching a config file
extendedConfigCache := &tsc.ExtendedConfigCache{}
if w.configFileName != "" {
// !!! need to check that this merges compileroptions correctly. This differs from non-watch, since we allow overriding of previous options
configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, &core.CompilerOptions{}, w.sys, extendedConfigCache)
if len(errors) > 0 {
for _, e := range errors {
w.reportDiagnostic(e)
}
return true
}
// CompilerOptions contain fields which should not be compared; clone to get a copy without those set.
if !reflect.DeepEqual(w.config.CompilerOptions().Clone(), configParseResult.CompilerOptions().Clone()) {
// fmt.Fprintln(w.sys.Writer(), "build triggered due to config change")
w.configModified = true
}
w.config = configParseResult
}
w.host = compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), extendedConfigCache, getTraceFromSys(w.sys, w.testing))
return false
}
func (w *Watcher) hasBeenModified(program *compiler.Program) bool {
// checks watcher's snapshot against program file modified times
currState := map[string]time.Time{}
filesModified := w.configModified
for _, sourceFile := range program.SourceFiles() {
fileName := sourceFile.FileName()
s := w.sys.FS().Stat(fileName)
if s == nil {
// do nothing; if file is in program.SourceFiles() but is not found when calling Stat, file has been very recently deleted.
// deleted files are handled outside of this loop
continue
}
currState[fileName] = s.ModTime()
if !filesModified {
if currState[fileName] != w.prevModified[fileName] {
// fmt.Fprint(w.sys.Writer(), "build triggered from ", fileName, ": ", w.prevModified[fileName], " -> ", currState[fileName], "\n")
filesModified = true
}
// catch cases where no files are modified, but some were deleted
delete(w.prevModified, fileName)
}
}
if !filesModified && len(w.prevModified) > 0 {
// fmt.Fprintln(w.sys.Writer(), "build triggered due to deleted file")
filesModified = true
}
w.prevModified = currState
// reset state for next cycle
w.configModified = false
return filesModified
}