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 }