diff --git a/cmd/templ/generatecmd/main.go b/cmd/templ/generatecmd/main.go index 6f5658a39..e0d88c8ba 100644 --- a/cmd/templ/generatecmd/main.go +++ b/cmd/templ/generatecmd/main.go @@ -4,43 +4,248 @@ import ( "bufio" "bytes" "context" + _ "embed" "errors" "fmt" "go/format" + "net/http" + "net/url" "os" + "os/signal" + "path" + "path/filepath" "runtime" "strings" "sync" "time" - "github.com/a-h/templ/cmd/templ/processor" + _ "net/http/pprof" + + "github.com/a-h/templ/cmd/templ/generatecmd/proxy" + "github.com/a-h/templ/cmd/templ/generatecmd/run" "github.com/a-h/templ/cmd/templ/visualize" "github.com/a-h/templ/generator" "github.com/a-h/templ/parser/v2" + "github.com/cenkalti/backoff/v4" + "github.com/cli/browser" ) type Arguments struct { FileName string Path string + Watch bool + Command string + ProxyPort int + Proxy string WorkerCount int GenerateSourceMapVisualisations bool + // PPROFPort is the port to run the pprof server on. + PPROFPort int } var defaultWorkerCount = runtime.NumCPU() func Run(args Arguments) (err error) { + ctx, cancel := context.WithCancel(context.Background()) + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt) + defer func() { + signal.Stop(signalChan) + cancel() + }() + if args.PPROFPort > 0 { + go func() { + _ = http.ListenAndServe(fmt.Sprintf("localhost:%d", args.PPROFPort), nil) + }() + } + go func() { + select { + case <-signalChan: // First signal, cancel context. + fmt.Println("\nCancelling...") + cancel() + case <-ctx.Done(): + } + <-signalChan // Second signal, hard exit. + os.Exit(2) + }() + err = runCmd(ctx, args) + if errors.Is(err, context.Canceled) { + return nil + } + return err +} + +func runCmd(ctx context.Context, args Arguments) (err error) { + start := time.Now() + if args.Watch && args.FileName != "" { + return fmt.Errorf("cannot watch a single file, remove the -f or -watch flag") + } if args.FileName != "" { - return processSingleFile(args.FileName, args.GenerateSourceMapVisualisations) + return processSingleFile(ctx, args.FileName, args.GenerateSourceMapVisualisations) } + var target *url.URL + if args.Proxy != "" { + target, err = url.Parse(args.Proxy) + if err != nil { + return fmt.Errorf("failed to parse proxy URL: %w", err) + } + } + if args.ProxyPort == 0 { + args.ProxyPort = 7331 + } + if args.WorkerCount == 0 { args.WorkerCount = defaultWorkerCount } - return processPath(args.Path, args.GenerateSourceMapVisualisations, args.WorkerCount) + if !path.IsAbs(args.Path) { + args.Path, err = filepath.Abs(args.Path) + if err != nil { + return + } + } + + var p *proxy.Handler + if args.Proxy != "" { + p = proxy.New(args.ProxyPort, target) + } + + fmt.Println("Processing path:", args.Path) + bo := backoff.NewExponentialBackOff() + bo.InitialInterval = time.Millisecond * 500 + bo.MaxInterval = time.Second * 3 + var firstRunComplete bool + fileNameToLastModTime := make(map[string]time.Time) + for !firstRunComplete || args.Watch { + changesFound, errs := processChanges(ctx, fileNameToLastModTime, args.Path, args.GenerateSourceMapVisualisations, args.WorkerCount) + if len(errs) > 0 { + if errors.Is(errs[0], context.Canceled) { + return errs[0] + } + fmt.Printf("Error processing path: %v\n", errors.Join(errs...)) + } + if changesFound > 0 { + fmt.Printf("Generated code for %d templates with %d errors in %s\n", changesFound, len(errs), time.Since(start)) + if args.Command != "" { + fmt.Printf("Executing command: %s\n", args.Command) + if _, err := run.Run(ctx, args.Path, args.Command); err != nil { + fmt.Printf("Error starting command: %v\n", err) + } + // Send server-sent event. + if p != nil { + p.SendSSE("message", "reload") + } + } + if !firstRunComplete && p != nil { + go func() { + fmt.Printf("Proxying from %s to target: %s\n", p.URL, p.Target.String()) + if err := http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", args.ProxyPort), p); err != nil { + fmt.Printf("Error starting proxy: %v\n", err) + } + }() + go func() { + fmt.Printf("Opening URL: %s\n", p.Target.String()) + if err := openURL(p.URL); err != nil { + fmt.Printf("Error opening URL: %v\n", err) + } + }() + } + } + if firstRunComplete { + if changesFound > 0 { + bo.Reset() + } + time.Sleep(bo.NextBackOff()) + } + firstRunComplete = true + start = time.Now() + } + return err +} + +func shouldSkipDir(dir string) bool { + if dir == "." { + return false + } + if dir == "vendor" || dir == "node_modules" { + return true + } + _, name := path.Split(dir) + // These directories are ignored by the Go tool. + if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") { + return true + } + return false } -func processSingleFile(fileName string, generateSourceMapVisualisations bool) error { +func processChanges(ctx context.Context, fileNameToLastModTime map[string]time.Time, path string, generateSourceMapVisualisations bool, maxWorkerCount int) (changesFound int, errs []error) { + sem := make(chan struct{}, maxWorkerCount) + var wg sync.WaitGroup + + err := filepath.WalkDir(path, func(path string, info os.DirEntry, err error) error { + if err != nil { + return err + } + if err = ctx.Err(); err != nil { + return err + } + if info.IsDir() && shouldSkipDir(path) { + return filepath.SkipDir + } + if info.IsDir() { + return nil + } + if strings.HasSuffix(path, ".templ") { + lastModTime := fileNameToLastModTime[path] + fileInfo, err := info.Info() + if err != nil { + return fmt.Errorf("failed to get file info: %w", err) + } + if fileInfo.ModTime().After(lastModTime) { + fileNameToLastModTime[path] = fileInfo.ModTime() + changesFound++ + + // Start a processor, but limit to maxWorkerCount. + sem <- struct{}{} + wg.Add(1) + go func() { + defer wg.Done() + if err := processSingleFile(ctx, path, generateSourceMapVisualisations); err != nil { + errs = append(errs, err) + } + <-sem + }() + } + } + return nil + }) + if err != nil { + errs = append(errs, err) + } + + wg.Wait() + + return changesFound, errs +} + +func openURL(url string) error { + backoff := backoff.NewExponentialBackOff() + backoff.InitialInterval = time.Second + var client http.Client + client.Timeout = 1 * time.Second + for { + if _, err := client.Get(url); err == nil { + break + } + d := backoff.NextBackOff() + fmt.Printf("Server not ready. Retrying in %v...\n", d) + time.Sleep(d) + } + return browser.OpenURL(url) +} + +func processSingleFile(ctx context.Context, fileName string, generateSourceMapVisualisations bool) error { start := time.Now() - err := compile(fileName, generateSourceMapVisualisations) + err := compile(ctx, fileName, generateSourceMapVisualisations) if err != nil { return err } @@ -48,28 +253,11 @@ func processSingleFile(fileName string, generateSourceMapVisualisations bool) er return err } -func processPath(path string, generateSourceMapVisualisations bool, workerCount int) (err error) { - start := time.Now() - results := make(chan processor.Result) - p := func(fileName string) error { - return compile(fileName, generateSourceMapVisualisations) - } - go processor.Process(path, p, workerCount, results) - var successCount, errorCount int - for r := range results { - if r.Error != nil { - err = errors.Join(err, fmt.Errorf("%s: %w", r.FileName, r.Error)) - errorCount++ - continue - } - successCount++ - fmt.Printf("%s complete in %v\n", r.FileName, r.Duration) +func compile(ctx context.Context, fileName string, generateSourceMapVisualisations bool) (err error) { + if err = ctx.Err(); err != nil { + return } - fmt.Printf("Generated code for %d templates with %d errors in %s\n", successCount+errorCount, errorCount, time.Since(start)) - return err -} -func compile(fileName string, generateSourceMapVisualisations bool) (err error) { t, err := parser.Parse(fileName) if err != nil { return fmt.Errorf("%s parsing error: %w", fileName, err) @@ -87,26 +275,20 @@ func compile(fileName string, generateSourceMapVisualisations bool) (err error) return fmt.Errorf("%s source formatting error: %w", fileName, err) } - w, err := os.Create(targetFileName) - if err != nil { - return fmt.Errorf("%s compilation error: %w", fileName, err) - } - if _, err := w.Write(data); err != nil { - return fmt.Errorf("%s compilation error: %w", fileName, err) - } - - defer w.Close() - if w.Sync() != nil { + if err = os.WriteFile(targetFileName, data, 0644); err != nil { return fmt.Errorf("%s write file error: %w", targetFileName, err) } if generateSourceMapVisualisations { - err = generateSourceMapVisualisation(fileName, targetFileName, sourceMap) + err = generateSourceMapVisualisation(ctx, fileName, targetFileName, sourceMap) } return } -func generateSourceMapVisualisation(templFileName, goFileName string, sourceMap *parser.SourceMap) error { +func generateSourceMapVisualisation(ctx context.Context, templFileName, goFileName string, sourceMap *parser.SourceMap) error { + if err := ctx.Err(); err != nil { + return err + } var templContents, goContents []byte var templErr, goErr error var wg sync.WaitGroup @@ -136,5 +318,5 @@ func generateSourceMapVisualisation(templFileName, goFileName string, sourceMap b := bufio.NewWriter(w) defer b.Flush() - return visualize.HTML(templFileName, string(templContents), string(goContents), sourceMap).Render(context.Background(), b) + return visualize.HTML(templFileName, string(templContents), string(goContents), sourceMap).Render(ctx, b) } diff --git a/cmd/templ/generatecmd/proxy/proxy.go b/cmd/templ/generatecmd/proxy/proxy.go new file mode 100644 index 000000000..8183ff156 --- /dev/null +++ b/cmd/templ/generatecmd/proxy/proxy.go @@ -0,0 +1,76 @@ +package proxy + +import ( + "fmt" + "io" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strconv" + "strings" + + "github.com/a-h/templ/cmd/templ/generatecmd/sse" + + _ "embed" +) + +//go:embed script.js +var script string + +const scriptTag = `` + +type Handler struct { + URL string + Target *url.URL + p *httputil.ReverseProxy + sse *sse.Handler +} + +func New(port int, target *url.URL) *Handler { + p := httputil.NewSingleHostReverseProxy(target) + p.ErrorLog = log.New(os.Stderr, "Proxy to target error: ", 0) + p.ModifyResponse = func(r *http.Response) error { + if contentType := r.Header.Get("Content-Type"); contentType != "text/html" { + return nil + } + body, err := io.ReadAll(r.Body) + if err != nil { + return err + } + updated := strings.Replace(string(body), "", scriptTag+"", -1) + r.Body = io.NopCloser(strings.NewReader(updated)) + r.ContentLength = int64(len(updated)) + r.Header.Set("Content-Length", strconv.Itoa(len(updated))) + return nil + } + return &Handler{ + URL: fmt.Sprintf("http://127.0.0.1:%d", port), + Target: target, + p: p, + sse: sse.New(), + } +} + +func (p *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/_templ/reload/script.js" { + // Provides a script that reloads the page. + w.Header().Add("Content-Type", "text/javascript") + _, err := io.WriteString(w, script) + if err != nil { + fmt.Printf("failed to write script: %v\n", err) + } + return + } + if r.URL.Path == "/_templ/reload/events" { + // Provides a list of messages including a reload message. + p.sse.ServeHTTP(w, r) + return + } + p.p.ServeHTTP(w, r) +} + +func (p *Handler) SendSSE(eventType string, data string) { + p.sse.Send(eventType, data) +} diff --git a/cmd/templ/generatecmd/proxy/script.js b/cmd/templ/generatecmd/proxy/script.js new file mode 100644 index 000000000..923901c31 --- /dev/null +++ b/cmd/templ/generatecmd/proxy/script.js @@ -0,0 +1,6 @@ +let src = new EventSource("/_templ/reload/events"); +src.onmessage = (event) => { + if (event && event.data === "reload") { + window.location.reload(); + } +}; diff --git a/cmd/templ/generatecmd/run/run.go b/cmd/templ/generatecmd/run/run.go new file mode 100644 index 000000000..b69d4d76b --- /dev/null +++ b/cmd/templ/generatecmd/run/run.go @@ -0,0 +1,41 @@ +package run + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + "sync" +) + +var m = &sync.Mutex{} +var running = map[string]*exec.Cmd{} + +func Run(ctx context.Context, workingDir, input string) (cmd *exec.Cmd, err error) { + m.Lock() + defer m.Unlock() + cmd, ok := running[input] + if ok { + if err = cmd.Process.Kill(); err != nil { + return nil, fmt.Errorf("failed to kill existing process: %w", err) + } + delete(running, input) + } + + parts := strings.SplitN(input, " ", 2) + executable := parts[0] + args := []string{} + if len(parts) > 1 { + args = append(args, parts[1]) + } + + cmd = exec.CommandContext(ctx, executable, args...) + cmd.Env = os.Environ() + cmd.Dir = workingDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + running[input] = cmd + err = cmd.Start() + return +} diff --git a/cmd/templ/generatecmd/sse/server.go b/cmd/templ/generatecmd/sse/server.go new file mode 100644 index 000000000..c847ed982 --- /dev/null +++ b/cmd/templ/generatecmd/sse/server.go @@ -0,0 +1,83 @@ +package sse + +import ( + _ "embed" + "fmt" + "net/http" + "sync" + "sync/atomic" + "time" +) + +func New() *Handler { + return &Handler{ + m: new(sync.Mutex), + requests: map[int64]chan event{}, + } +} + +type Handler struct { + m *sync.Mutex + counter int64 + requests map[int64]chan event +} + +type event struct { + Type string + Data string +} + +// Send an event to all connected clients. +func (s *Handler) Send(eventType string, data string) { + s.m.Lock() + defer s.m.Unlock() + for _, f := range s.requests { + f := f + go func(f chan event) { + f <- event{ + Type: eventType, + Data: data, + } + }(f) + } +} + +func (s *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + id := atomic.AddInt64(&s.counter, 1) + events := make(chan event) + s.requests[id] = events + defer func() { + s.m.Lock() + defer s.m.Unlock() + delete(s.requests, id) + close(events) + }() + + timer := time.NewTimer(0) +loop: + for { + select { + case <-timer.C: + if _, err := fmt.Fprintf(w, "event: message\ndata: ping\n\n"); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + timer.Reset(time.Second * 5) + case e := <-events: + fmt.Println("Sending reload event...") + if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", e.Type, e.Data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + case <-r.Context().Done(): + break loop + } + w.(http.Flusher).Flush() + } +} diff --git a/cmd/templ/main.go b/cmd/templ/main.go index 6361e4438..b3e473b79 100644 --- a/cmd/templ/main.go +++ b/cmd/templ/main.go @@ -74,10 +74,15 @@ examples: func generateCmd(args []string) { cmd := flag.NewFlagSet("generate", flag.ExitOnError) - fileName := cmd.String("f", "", "Optionally generates code for a single file, e.g. -f header.templ") - path := cmd.String("path", ".", "Generates code for all files in path.") + fileNameFlag := cmd.String("f", "", "Optionally generates code for a single file, e.g. -f header.templ") + pathFlag := cmd.String("path", ".", "Generates code for all files in path.") sourceMapVisualisations := cmd.Bool("sourceMapVisualisations", false, "Set to true to generate HTML files to visualise the templ code and its corresponding Go code.") - workerCount := cmd.Int("w", runtime.NumCPU(), "Number of workers to run in parallel.") + watchFlag := cmd.Bool("watch", false, "Set to true to watch the path for changes and regenerate code.") + cmdFlag := cmd.String("cmd", "", "Set the command to run after generating code.") + proxyFlag := cmd.String("proxy", "", "Set the URL to proxy after generating code and executing the command.") + proxyPortFlag := cmd.Int("proxyport", 7331, "The port the proxy will listen on.") + workerCountFlag := cmd.Int("w", runtime.NumCPU(), "Number of workers to run in parallel.") + pprofPortFlag := cmd.Int("pprof", 0, "Port to start pprof web server on.") helpFlag := cmd.Bool("help", false, "Print help and exit.") err := cmd.Parse(args) if err != nil || *helpFlag { @@ -85,10 +90,15 @@ func generateCmd(args []string) { return } err = generatecmd.Run(generatecmd.Arguments{ - FileName: *fileName, - Path: *path, - WorkerCount: *workerCount, + FileName: *fileNameFlag, + Path: *pathFlag, + Watch: *watchFlag, + Command: *cmdFlag, + Proxy: *proxyFlag, + ProxyPort: *proxyPortFlag, + WorkerCount: *workerCountFlag, GenerateSourceMapVisualisations: *sourceMapVisualisations, + PPROFPort: *pprofPortFlag, }) if err != nil { fmt.Println(err.Error()) diff --git a/cmd/templ/processor/processor.go b/cmd/templ/processor/processor.go index 8cf0dcb4d..4908716dd 100644 --- a/cmd/templ/processor/processor.go +++ b/cmd/templ/processor/processor.go @@ -72,4 +72,3 @@ func ProcessChannel(templates <-chan string, dir string, f func(fileName string) } wg.Wait() } - diff --git a/docs/docs/09-commands-and-tools/01-cli.md b/docs/docs/09-commands-and-tools/01-cli.md index 703077af1..18f2b2212 100644 --- a/docs/docs/09-commands-and-tools/01-cli.md +++ b/docs/docs/09-commands-and-tools/01-cli.md @@ -21,16 +21,26 @@ The `templ generate` command generates Go code from `*.templ` files in the curre The command provides additional options: ``` + -cmd string + Set the command to run after generating code. -f string Optionally generates code for a single file, e.g. -f header.templ -help Print help and exit. -path string Generates code for all files in path. (default ".") + -pprof int + Port to start pprof web server on. + -proxy string + Set the URL to proxy after generating code and executing the command. + -proxyport int + The port the proxy will listen on. (default 7331) -sourceMapVisualisations Set to true to generate HTML files to visualise the templ code and its corresponding Go code. -w int Number of workers to run in parallel. (default 4) + -watch + Set to true to watch the path for changes and regenerate code. ``` For example, to generate code for a single file: diff --git a/docs/docs/09-commands-and-tools/03-hot-reload.md b/docs/docs/09-commands-and-tools/03-hot-reload.md index cb2940648..0b14d307b 100644 --- a/docs/docs/09-commands-and-tools/03-hot-reload.md +++ b/docs/docs/09-commands-and-tools/03-hot-reload.md @@ -1,6 +1,26 @@ # Hot reload -Use https://github.com/cosmtrek/air with the following configuration. +## Built-in + +templ ships with hot reload. Since `fsnotify` and `rjeczalik/notify` filesystem watchers struggle to provide a working cross-platform behaviour, templ uses a basic `os.WalkDir` function to iterate through `*.templ` files on disk, and uses a backoff strategy to prevent excessive disk thrashing and reduce CPU usage. + +`templ generate --watch` will watch the current directory and will templ files if changes are detected. + +If the `--cmd` argument is set, templ start or restart the command once template code generation is complete. + +If the `--proxy` argument is set, templ will start a HTTP proxy pointed at the given address. The proxy rewrites HTML received from the given address and adds a script just before the `` tag that will reload the window with JavaScript once the changes are complete and the command has been executed. + +``` +templ generate --watch --proxy="http://localhost:8080" --cmd="runtest" +``` + +## Alternative + +Air's reload performance is better due to its complex filesystem notification setup, but doens't ship with a proxy to automatically reload pages, and requires a `toml` configuration file for operation. + +See https://github.com/cosmtrek/air for details. + +### Example configuration ```toml title=".air.toml" root = "." diff --git a/examples/counter-basic/components.templ b/examples/counter-basic/components.templ index 370e329d3..cd9b05cb9 100644 --- a/examples/counter-basic/components.templ +++ b/examples/counter-basic/components.templ @@ -37,10 +37,7 @@ templ page(global, user int) {
-
- @counts(global, user) - @form() -
+
@counts(global, user) @form()
diff --git a/go.mod b/go.mod index 26f614a7a..60f6acb77 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/a-h/parse v0.0.0-20230402144745-e6c8bc86e846 github.com/a-h/pathvars v0.0.12 github.com/a-h/protocol v0.0.0-20230224160810-b4eec67c1c22 + github.com/cenkalti/backoff/v4 v4.2.1 + github.com/cli/browser v1.2.0 github.com/google/go-cmp v0.5.9 github.com/natefinch/atomic v1.0.1 github.com/rs/cors v1.8.3 @@ -26,7 +28,7 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/net v0.9.0 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/sys v0.8.0 // indirect ) // replace github.com/a-h/parse => /Users/adrian/github.com/a-h/parse diff --git a/go.sum b/go.sum index edeb53c4a..e12acfa90 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,10 @@ github.com/a-h/protocol v0.0.0-20230224160810-b4eec67c1c22/go.mod h1:Gm0KywveHnk github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cli/browser v1.2.0 h1:yvU7e9qf97kZqGFX6n2zJPHsmSObY9ske+iCvKelvXg= +github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -61,12 +65,13 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=