Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add templ diagnose command #840

Merged
merged 5 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ If applicable, add screenshots or screen captures to help explain your problem.
**Logs**
If the issue is related to IDE support, run through the LSP troubleshooting section at https://templ.guide/commands-and-tools/ide-support/#troubleshooting-1 and include logs from templ

**`templ diagnose` output**
Run `templ diagnose` and include the output.

**Desktop (please complete the following information):**
- OS: [e.g. MacOS, Linux, Windows, WSL]
- templ CLI version (`templ version`)
Expand Down
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.747
0.2.749
157 changes: 157 additions & 0 deletions cmd/templ/diagnosecmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package diagnosecmd

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"runtime"
"strings"

"github.com/a-h/templ"
"github.com/a-h/templ/cmd/templ/lspcmd/pls"
)

type Arguments struct {
JSON bool `flag:"json" help:"Output the diagnostics as JSON."`
}

type Diagnostics struct {
OS struct {
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
} `json:"os"`
Go Diagnostic `json:"go"`
Gopls Diagnostic `json:"gopls"`
Templ Diagnostic `json:"templ"`
}

type Diagnostic struct {
Location string `json:"location"`
Version string `json:"version"`
OK bool `json:"ok"`
Message string `json:"message,omitempty"`
}

func diagnoseGo() (d Diagnostic) {
// Find Go.
var err error
d.Location, err = exec.LookPath("go")
if err != nil {
d.Message = fmt.Sprintf("failed to find go: %v", err)
return
}
// Run go to find the version.
cmd := exec.Command(d.Location, "version")
v, err := cmd.Output()
if err != nil {
d.Message = fmt.Sprintf("failed to get go version, check that Go is installed: %v", err)
return
}
d.Version = strings.TrimSpace(string(v))
d.OK = true
return
}

func diagnoseGopls() (d Diagnostic) {
var err error
d.Location, err = pls.FindGopls()
if err != nil {
d.Message = fmt.Sprintf("failed to find gopls: %v", err)
return
}
cmd := exec.Command(d.Location, "version")
v, err := cmd.Output()
if err != nil {
d.Message = fmt.Sprintf("failed to get gopls version: %v", err)
return
}
d.Version = strings.TrimSpace(string(v))
d.OK = true
return
}

func diagnoseTempl() (d Diagnostic) {
// Find templ.
var err error
d.Location, err = findTempl()
if err != nil {
d.Message = err.Error()
return
}
// Run templ to find the version.
cmd := exec.Command(d.Location, "version")
v, err := cmd.Output()
if err != nil {
d.Message = fmt.Sprintf("failed to get templ version: %v", err)
return
}
d.Version = strings.TrimSpace(string(v))
if d.Version != templ.Version() {
d.Message = fmt.Sprintf("version mismatch - you're running %q at the command line, but the version in the path is %q", templ.Version(), d.Version)
return
}
d.OK = true
return
}

func findTempl() (location string, err error) {
executableName := "templ"
if runtime.GOOS == "windows" {
executableName = "templ.exe"
}
executableName, err = exec.LookPath(executableName)
if err == nil {
// Found on the path.
return executableName, nil
}

// Unexpected error.
if !errors.Is(err, exec.ErrNotFound) {
return "", fmt.Errorf("unexpected error looking for templ: %w", err)
}

return "", fmt.Errorf("templ is not in the path (%q). You can install templ with `go install github.com/a-h/templ/cmd/templ@latest`", os.Getenv("PATH"))
}

func diagnose() (d Diagnostics) {
d.OS.GOOS = runtime.GOOS
d.OS.GOARCH = runtime.GOARCH
d.Go = diagnoseGo()
d.Gopls = diagnoseGopls()
d.Templ = diagnoseTempl()
return
}

func Run(ctx context.Context, log *slog.Logger, stdout io.Writer, args Arguments) (err error) {
diagnostics := diagnose()
if args.JSON {
enc := json.NewEncoder(stdout)
enc.SetIndent("", " ")
return enc.Encode(diagnostics)
}
log.Info("os", slog.String("goos", diagnostics.OS.GOOS), slog.String("goarch", diagnostics.OS.GOARCH))
logDiagnostic(ctx, log, "go", diagnostics.Go)
logDiagnostic(ctx, log, "gopls", diagnostics.Gopls)
logDiagnostic(ctx, log, "templ", diagnostics.Templ)
return nil
}

func logDiagnostic(ctx context.Context, log *slog.Logger, name string, d Diagnostic) {
level := slog.LevelInfo
if !d.OK {
level = slog.LevelError
}
args := []any{
slog.String("location", d.Location),
slog.String("version", d.Version),
}
if d.Message != "" {
args = append(args, slog.String("message", d.Message))
}
log.Log(ctx, level, name, args...)
}
6 changes: 3 additions & 3 deletions cmd/templ/lspcmd/pls/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ func (opts Options) AsArguments() []string {
return args
}

func findGopls() (location string, err error) {
func FindGopls() (location string, err error) {
executableName := "gopls"
if runtime.GOOS == "windows" {
executableName = "gopls.exe"
}

_, err = exec.LookPath(executableName)
executableName, err = exec.LookPath(executableName)
if err == nil {
// Found on the path.
return executableName, nil
Expand Down Expand Up @@ -72,7 +72,7 @@ func findGopls() (location string, err error) {

// NewGopls starts gopls and opens up a jsonrpc2 connection to it.
func NewGopls(ctx context.Context, log *zap.Logger, opts Options) (rwc io.ReadWriteCloser, err error) {
location, err := findGopls()
location, err := FindGopls()
if err != nil {
return nil, err
}
Expand Down
57 changes: 57 additions & 0 deletions cmd/templ/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"runtime"

"github.com/a-h/templ"
"github.com/a-h/templ/cmd/templ/diagnosecmd"
"github.com/a-h/templ/cmd/templ/fmtcmd"
"github.com/a-h/templ/cmd/templ/generatecmd"
"github.com/a-h/templ/cmd/templ/lspcmd"
Expand All @@ -35,6 +36,7 @@ commands:
generate Generates Go code from templ files
fmt Formats templ files
lsp Starts a language server for templ files
diagnose Diagnose the templ environment
version Prints the version
`

Expand All @@ -44,6 +46,8 @@ func run(stdin io.Reader, stdout, stderr io.Writer, args []string) (code int) {
return 64 // EX_USAGE
}
switch args[1] {
case "diagnose":
return diagnoseCmd(stdout, stderr, args[2:])
case "generate":
return generateCmd(stdout, stderr, args[2:])
case "fmt":
Expand Down Expand Up @@ -80,6 +84,59 @@ func newLogger(logLevel string, verbose bool, stderr io.Writer) *slog.Logger {
}))
}

const diagnoseUsageText = `usage: templ diagnose [<args>...]

Diagnoses the templ environment.

Args:
-json
Output diagnostics in JSON format to stdout. (default false)
-v
Set log verbosity level to "debug". (default "info")
-log-level
Set log verbosity level. (default "info", options: "debug", "info", "warn", "error")
-help
Print help and exit.
`

func diagnoseCmd(stdout, stderr io.Writer, args []string) (code int) {
cmd := flag.NewFlagSet("diagnose", flag.ExitOnError)
jsonFlag := cmd.Bool("json", false, "")
verboseFlag := cmd.Bool("v", false, "")
logLevelFlag := cmd.String("log-level", "info", "")
helpFlag := cmd.Bool("help", false, "")
err := cmd.Parse(args)
if err != nil {
fmt.Fprint(stderr, diagnoseUsageText)
return 64 // EX_USAGE
}
if *helpFlag {
fmt.Fprint(stdout, diagnoseUsageText)
return
}

log := newLogger(*logLevelFlag, *verboseFlag, stderr)

ctx, cancel := context.WithCancel(context.Background())
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
go func() {
<-signalChan
fmt.Fprintln(stderr, "Stopping...")
cancel()
}()

err = diagnosecmd.Run(ctx, log, stdout, diagnosecmd.Arguments{
JSON: *jsonFlag,
})
if err != nil {
color.New(color.FgRed).Fprint(stderr, "(✗) ")
fmt.Fprintln(stderr, "Command failed: "+err.Error())
return 1
}
return 0
}

const generateUsageText = `usage: templ generate [<args>...]

Generates Go code from templ files.
Expand Down
6 changes: 6 additions & 0 deletions cmd/templ/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ func TestMain(t *testing.T) {
expectedStdout: lspUsageText,
expectedCode: 0,
},
{
name: `"templ diagnose --help" prints usage`,
args: []string{"templ", "diagnose", "--help"},
expectedStdout: diagnoseUsageText,
expectedCode: 0,
},
}

for _, test := range tests {
Expand Down
20 changes: 12 additions & 8 deletions docs/docs/09-commands-and-tools/01-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
`templ` provides a command line interface. Most users will only need to run the `templ generate` command to generate Go code from `*.templ` files.

```
usage: templ <command> [parameters]
To see help text, you can run:
templ generate --help
templ fmt --help
templ lsp --help
templ version
examples:
templ generate
usage: templ <command> [<args>...]

templ - build HTML UIs with Go

See docs at https://templ.guide

commands:
generate Generates Go code from templ files
fmt Formats templ files
lsp Starts a language server for templ files
diagnose Diagnose the templ environment
version Prints the version
```

## Generating Go code from templ files
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/09-commands-and-tools/02-ide-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,3 +433,7 @@ The logs can be quite verbose, since almost every keypress results in additional
### Look at the web server

The web server option provides an insight into the internal state of the language server. It may provide insight into what's going wrong.

### Run templ diagnose

The `templ diagnose` command outputs information that's useful in debugging issues.