Skip to content

Commit

Permalink
feat: add templ info command (#840)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-h authored Jul 8, 2024
1 parent 13f712b commit a98204d
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 12 deletions.
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 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`)
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/infocmd/main.go
Original file line number Diff line number Diff line change
@@ -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...)
}
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 @@ -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"
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
info Displays information about 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 "info":
return infoCmd(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 infoUsageText = `usage: templ info [<args>...]
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 [<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 info --help" prints usage`,
args: []string{"templ", "info", "--help"},
expectedStdout: infoUsageText,
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
info Displays information about 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 info

The `templ info` command outputs information that's useful for debugging issues.

0 comments on commit a98204d

Please sign in to comment.