Skip to content

Commit

Permalink
fix: infinite loop if go.mod not found, add warning on missing templ …
Browse files Browse the repository at this point in the history
…dep (#441)

fixes #418
  • Loading branch information
a-h authored Jan 20, 2024
1 parent bb756d7 commit cac13b5
Show file tree
Hide file tree
Showing 13 changed files with 560 additions and 140 deletions.
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.529
0.2.532
114 changes: 7 additions & 107 deletions cmd/templ/generatecmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ import (
"net/http"
"net/url"
"os"
"os/signal"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
Expand All @@ -25,6 +23,7 @@ import (
_ "net/http/pprof"

"github.com/a-h/templ"
"github.com/a-h/templ/cmd/templ/generatecmd/modcheck"
"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"
Expand All @@ -33,8 +32,6 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/cli/browser"
"github.com/fatih/color"
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
)

type Arguments struct {
Expand All @@ -55,57 +52,22 @@ type Arguments struct {

var defaultWorkerCount = runtime.NumCPU()

func Run(w io.Writer, args Arguments) (err error) {
ctx, cancel := context.WithCancel(context.Background())
watchCtx, watchCancel := context.WithCancel(context.Background())

signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
defer func() {
signal.Stop(signalChan)
cancel()
}()
func Run(ctx context.Context, w io.Writer, args Arguments) (err error) {
if args.PPROFPort > 0 {
go func() {
_ = http.ListenAndServe(fmt.Sprintf("localhost:%d", args.PPROFPort), nil)
}()
}

go func() {
watching := args.Watch
for {
select {
case <-signalChan: // First signal, cancel context.
if watching {
fmt.Println("Stopping watch operation...")
watchCancel()
continue
}

if ctx.Err() != nil {
fmt.Fprintln(w, "\nHARD EXIT")
os.Exit(2) // hard exit
continue
}

fmt.Fprintln(w, "\nCancelling...")
cancel()

case <-ctx.Done():
break
}
}
}()

err = runCmd(ctx, watchCtx, w, args)
err = runCmd(ctx, w, args)
if errors.Is(err, context.Canceled) {
return nil
}

return err
}

func runCmd(ctx, watchCtx context.Context, w io.Writer, args Arguments) error {
func runCmd(ctx context.Context, w io.Writer, args Arguments) error {
var err error

if args.Watch && args.FileName != "" {
Expand Down Expand Up @@ -148,18 +110,18 @@ func runCmd(ctx, watchCtx context.Context, w io.Writer, args Arguments) error {
}
fmt.Fprintln(w, "Processing path:", args.Path)

if err := checkTemplVersion(args.Path); err != nil {
if err := modcheck.Check(args.Path); err != nil {
logWarning(w, "templ version check failed: %v\n", err)
}

if args.Watch {
err = generateWatched(watchCtx, w, args, opts, p)
err = generateWatched(ctx, w, args, opts, p)
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
}

return generateProduction(ctx, w, args, opts, p)
return generateProduction(context.Background(), w, args, opts, p)
}

func generateWatched(ctx context.Context, w io.Writer, args Arguments, opts []generator.GenerateOpt, p *proxy.Handler) error {
Expand Down Expand Up @@ -519,65 +481,3 @@ func logWithDecoration(w io.Writer, decoration string, col color.Attribute, form
color.New(col).Fprintf(w, "(%s) ", decoration)
fmt.Fprintf(w, format, a...)
}

// Replace "go 1.21.3" with "go 1.21" until https://github.com/golang/go/issues/61888 is fixed, see templ issue https://github.com/a-h/templ/issues/355
var goVersionRegexp = regexp.MustCompile(`\ngo (\d+\.\d+)(?:\D.+)\n`)

func patchGoVersion(moduleFileContents []byte) []byte {
return goVersionRegexp.ReplaceAll(moduleFileContents, []byte("\ngo $1\n"))
}

func checkTemplVersion(dir string) error {
// Walk up the directory tree, starting at dir, until we find a go.mod file.
// If it contains a go.mod file, parse it and find the templ version.
dir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
for {
current := filepath.Join(dir, "go.mod")
_, err := os.Stat(current)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to stat go.mod file: %w", err)
}
if os.IsNotExist(err) {
// Move up.
prev := dir
dir = filepath.Dir(dir)
if dir == prev {
return fmt.Errorf("could not find go.mod file")
}
continue
}
// Found a go.mod file.
// Read it and find the templ version.
m, err := os.ReadFile(current)
if err != nil {
return fmt.Errorf("failed to read go.mod file: %w", err)
}

// Replace "go 1.21.x" with "go 1.21".
m = patchGoVersion(m)

mf, err := modfile.Parse(current, m, nil)
if err != nil {
return fmt.Errorf("failed to parse go.mod file: %w", err)
}
if mf.Module.Mod.Path == "github.com/a-h/templ" {
// The go.mod file is for templ itself.
return nil
}
for _, r := range mf.Require {
if r.Mod.Path == "github.com/a-h/templ" {
cmp := semver.Compare(r.Mod.Version, templ.Version())
if cmp < 0 {
return fmt.Errorf("generator %v is newer than templ version %v found in go.mod file, consider running `go get -u github.com/a-h/templ` to upgrade", templ.Version(), r.Mod.Version)
}
if cmp > 0 {
return fmt.Errorf("generator %v is older than templ version %v found in go.mod file, consider upgrading templ CLI", templ.Version(), r.Mod.Version)
}
return nil
}
}
}
}
93 changes: 93 additions & 0 deletions cmd/templ/generatecmd/modcheck/modcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package modcheck

import (
"fmt"
"os"
"path/filepath"
"regexp"

"github.com/a-h/templ"
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
)

// WalkUp the directory tree, starting at dir, until we find a directory containing
// a go.mod file.
func WalkUp(dir string) (string, error) {
dir, err := filepath.Abs(dir)
if err != nil {
return "", fmt.Errorf("failed to get absolute path: %w", err)
}

var modFile string
for {
modFile = filepath.Join(dir, "go.mod")
_, err := os.Stat(modFile)
if err != nil && !os.IsNotExist(err) {
return "", fmt.Errorf("failed to stat go.mod file: %w", err)
}
if os.IsNotExist(err) {
// Move up.
prev := dir
dir = filepath.Dir(dir)
if dir == prev {
break
}
continue
}
break
}

// No file found.
if modFile == "" {
return dir, fmt.Errorf("could not find go.mod file")
}
return dir, nil
}

// Replace "go 1.21.3" with "go 1.21" until https://github.com/golang/go/issues/61888 is fixed, see templ issue https://github.com/a-h/templ/issues/355
var goVersionRegexp = regexp.MustCompile(`\ngo (\d+\.\d+)(?:\D.+)\n`)

func patchGoVersion(moduleFileContents []byte) []byte {
return goVersionRegexp.ReplaceAll(moduleFileContents, []byte("\ngo $1\n"))
}

func Check(dir string) error {
dir, err := WalkUp(dir)
if err != nil {
return err
}

// Found a go.mod file.
// Read it and find the templ version.
modFile := filepath.Join(dir, "go.mod")
m, err := os.ReadFile(modFile)
if err != nil {
return fmt.Errorf("failed to read go.mod file: %w", err)
}

// Replace "go 1.21.x" with "go 1.21".
m = patchGoVersion(m)

mf, err := modfile.Parse(modFile, m, nil)
if err != nil {
return fmt.Errorf("failed to parse go.mod file: %w", err)
}
if mf.Module.Mod.Path == "github.com/a-h/templ" {
// The go.mod file is for templ itself.
return nil
}
for _, r := range mf.Require {
if r.Mod.Path == "github.com/a-h/templ" {
cmp := semver.Compare(r.Mod.Version, templ.Version())
if cmp < 0 {
return fmt.Errorf("generator %v is newer than templ version %v found in go.mod file, consider running `go get -u github.com/a-h/templ` to upgrade", templ.Version(), r.Mod.Version)
}
if cmp > 0 {
return fmt.Errorf("generator %v is older than templ version %v found in go.mod file, consider upgrading templ CLI", templ.Version(), r.Mod.Version)
}
return nil
}
}
return fmt.Errorf("templ not found in go.mod file, run `go get github.com/a-h/templ to install it`")
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package generatecmd
package modcheck

import (
"testing"
Expand Down
21 changes: 6 additions & 15 deletions cmd/templ/generatecmd/run/run_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,20 @@ func KillAll() (err error) {
return
}

func Stop() (err error) {
m.Lock()
defer m.Unlock()
for _, cmd := range running {
err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
if err != nil {
return err
}
}
running = map[string]*exec.Cmd{}
return
func Stop(cmd *exec.Cmd) (err error) {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}

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 = Stop(); err != nil {
return
if err = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != nil {
return cmd, err
}
delete(running, input)
}
m.Lock()
defer m.Unlock()
parts := strings.Fields(input)
executable := parts[0]
args := []string{}
Expand Down
25 changes: 10 additions & 15 deletions cmd/templ/generatecmd/run/run_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,27 @@ func KillAll() (err error) {
return
}

func Stop() (err error) {
func Stop(cmd *exec.Cmd) (err error) {
kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
kill.Stderr = os.Stderr
kill.Stdout = os.Stdout
return kill.Run()
}

func Run(ctx context.Context, workingDir, input string) (cmd *exec.Cmd, err error) {
m.Lock()
defer m.Unlock()
for _, cmd := range running {
cmd, ok := running[input]
if ok {
kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
kill.Stderr = os.Stderr
kill.Stdout = os.Stdout
err := kill.Run()
if err != nil {
return err
}
}
running = map[string]*exec.Cmd{}
return
}

func Run(ctx context.Context, workingDir, input string) (cmd *exec.Cmd, err error) {
cmd, ok := running[input]
if ok {
if err = Stop(); err != nil {
return
}
delete(running, input)
}
m.Lock()
defer m.Unlock()
parts := strings.Fields(input)
executable := parts[0]
args := []string{}
Expand Down
7 changes: 7 additions & 0 deletions cmd/templ/generatecmd/testwatch/testdata/go.mod.embed
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module templ/testproject

go 1.21

require github.com/a-h/templ v0.2.513 // indirect

replace github.com/a-h/templ => {moduleRoot}
2 changes: 2 additions & 0 deletions cmd/templ/generatecmd/testwatch/testdata/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
Loading

0 comments on commit cac13b5

Please sign in to comment.