diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 423d3919a..ef2ce9125 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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 info` output** +Run `templ info` and include the output. + **Desktop (please complete the following information):** - OS: [e.g. MacOS, Linux, Windows, WSL] - templ CLI version (`templ version`) diff --git a/.version b/.version index baee64fae..df0fc5a41 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.2.747 \ No newline at end of file +0.2.749 \ No newline at end of file diff --git a/cmd/templ/infocmd/main.go b/cmd/templ/infocmd/main.go new file mode 100644 index 000000000..eddd93200 --- /dev/null +++ b/cmd/templ/infocmd/main.go @@ -0,0 +1,157 @@ +package infocmd + +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 info as JSON."` +} + +type Info struct { + OS struct { + GOOS string `json:"goos"` + GOARCH string `json:"goarch"` + } `json:"os"` + Go ToolInfo `json:"go"` + Gopls ToolInfo `json:"gopls"` + Templ ToolInfo `json:"templ"` +} + +type ToolInfo struct { + Location string `json:"location"` + Version string `json:"version"` + OK bool `json:"ok"` + Message string `json:"message,omitempty"` +} + +func getGoInfo() (d ToolInfo) { + // 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 getGoplsInfo() (d ToolInfo) { + 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 getTemplInfo() (d ToolInfo) { + // 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 getInfo() (d Info) { + d.OS.GOOS = runtime.GOOS + d.OS.GOARCH = runtime.GOARCH + d.Go = getGoInfo() + d.Gopls = getGoplsInfo() + d.Templ = getTemplInfo() + return +} + +func Run(ctx context.Context, log *slog.Logger, stdout io.Writer, args Arguments) (err error) { + info := getInfo() + if args.JSON { + enc := json.NewEncoder(stdout) + enc.SetIndent("", " ") + return enc.Encode(info) + } + log.Info("os", slog.String("goos", info.OS.GOOS), slog.String("goarch", info.OS.GOARCH)) + logInfo(ctx, log, "go", info.Go) + logInfo(ctx, log, "gopls", info.Gopls) + logInfo(ctx, log, "templ", info.Templ) + return nil +} + +func logInfo(ctx context.Context, log *slog.Logger, name string, ti ToolInfo) { + level := slog.LevelInfo + if !ti.OK { + level = slog.LevelError + } + args := []any{ + slog.String("location", ti.Location), + slog.String("version", ti.Version), + } + if ti.Message != "" { + args = append(args, slog.String("message", ti.Message)) + } + log.Log(ctx, level, name, args...) +} diff --git a/cmd/templ/lspcmd/pls/main.go b/cmd/templ/lspcmd/pls/main.go index 6792ea9ce..3986e76e3 100644 --- a/cmd/templ/lspcmd/pls/main.go +++ b/cmd/templ/lspcmd/pls/main.go @@ -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 @@ -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 } diff --git a/cmd/templ/main.go b/cmd/templ/main.go index 75d5237c8..ce6ad10ad 100644 --- a/cmd/templ/main.go +++ b/cmd/templ/main.go @@ -13,6 +13,7 @@ import ( "github.com/a-h/templ" "github.com/a-h/templ/cmd/templ/fmtcmd" "github.com/a-h/templ/cmd/templ/generatecmd" + "github.com/a-h/templ/cmd/templ/infocmd" "github.com/a-h/templ/cmd/templ/lspcmd" "github.com/a-h/templ/cmd/templ/sloghandler" "github.com/fatih/color" @@ -35,6 +36,7 @@ commands: generate Generates Go code from templ files fmt Formats templ files lsp Starts a language server for templ files + info Displays information about the templ environment version Prints the version ` @@ -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 "info": + return infoCmd(stdout, stderr, args[2:]) case "generate": return generateCmd(stdout, stderr, args[2:]) case "fmt": @@ -80,6 +84,59 @@ func newLogger(logLevel string, verbose bool, stderr io.Writer) *slog.Logger { })) } +const infoUsageText = `usage: templ info [...] + +Displays information about the templ environment. + +Args: + -json + Output information 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 infoCmd(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, infoUsageText) + return 64 // EX_USAGE + } + if *helpFlag { + fmt.Fprint(stdout, infoUsageText) + 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 = infocmd.Run(ctx, log, stdout, infocmd.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 [...] Generates Go code from templ files. diff --git a/cmd/templ/main_test.go b/cmd/templ/main_test.go index b9fb0dc12..5f05982c5 100644 --- a/cmd/templ/main_test.go +++ b/cmd/templ/main_test.go @@ -65,6 +65,12 @@ func TestMain(t *testing.T) { expectedStdout: lspUsageText, expectedCode: 0, }, + { + name: `"templ info --help" prints usage`, + args: []string{"templ", "info", "--help"}, + expectedStdout: infoUsageText, + expectedCode: 0, + }, } for _, test := range tests { diff --git a/docs/docs/09-commands-and-tools/01-cli.md b/docs/docs/09-commands-and-tools/01-cli.md index a24a3f62f..6d74fd5cd 100644 --- a/docs/docs/09-commands-and-tools/01-cli.md +++ b/docs/docs/09-commands-and-tools/01-cli.md @@ -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 [parameters] -To see help text, you can run: - templ generate --help - templ fmt --help - templ lsp --help - templ version -examples: - templ generate +usage: templ [...] + +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 + info Displays information about the templ environment + version Prints the version ``` ## Generating Go code from templ files diff --git a/docs/docs/09-commands-and-tools/02-ide-support.md b/docs/docs/09-commands-and-tools/02-ide-support.md index 27c825ac3..1d1f16f3a 100644 --- a/docs/docs/09-commands-and-tools/02-ide-support.md +++ b/docs/docs/09-commands-and-tools/02-ide-support.md @@ -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 info + +The `templ info` command outputs information that's useful for debugging issues.