Skip to content

Commit

Permalink
feat: Introduce custom watchers
Browse files Browse the repository at this point in the history
Custom watchers allow defining custom file change handlers.
A common case for those are JavaScript files that you
might want to bundle.
Another common usecase would be requiring a server restart
on config file changes.

fix: Handle WebSocket connection closure properly to avoid
leaking websocket handler goroutines.

fix: Abort health-check after restart when a termination signal
was received.

fix: Add space between prefix and body in package `internal/log`.
  • Loading branch information
romshark committed Jul 25, 2024
1 parent 65d6dd4 commit 8dfe094
Show file tree
Hide file tree
Showing 19 changed files with 1,028 additions and 619 deletions.
153 changes: 0 additions & 153 deletions config.go

This file was deleted.

50 changes: 50 additions & 0 deletions example-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,53 @@ app:
# app.flags defines the CLI arguments as a string provided
# to the application server executable.
flags: -host your-application:12000

# custom-watchers defines custom file change watchers executing arbitrary commands
# on certain file changes that isn't covered by a standard Templiér setup.
custom-watchers:
- name: "Bundle JS"
# cmd specifies the command to run when a JavaScript or JSX file is changed.
# This is optional and can be left empty since sometimes all you want to do is
# rebuild, or restart or simply reload the browser tabs.
cmd: npm run build

# include defines that this watcher will watch all JavaScript and JSX files.
include: ["*.js", "*.jsx"]

# fail-on-error specifies that in case cmd returns error code 1 the output
# of the execution should be displayed in the browser, just like
# for example if the Go compiler fails to compile.
fail-on-error: true

# debounce specifies how long to wait for more file changes
# after the first one occured before executing cmd.
# Default debounce duration is applied if left empty.
debounce:

# requires specifies that browser tabs need to be reloaded when a .js or .jsx file
# changed and cmd was successfuly executed, but the server doesn't need to be
# rebuilt or restarted.
# This option accepts the following values:
# - "" (or empty field) = no action, execute cmd and don't do anything else.
# - "reload" = reload all browser tabs.
# - "restart" = restart the server but don't rebuild it.
# - "rebuild" = re-lint, rebuild and restart the server.
requires: "reload"

- name: "Restart on config change"
# cmd specifies that no special command needs to be executed since this watcher
# just triggers a server restart.
cmd:

# include specifies what kind of configuration files need to be watched.
include: ["*.yaml", "*.yml", "*.toml"]

# fail-on-error doesn't need to be specified when cmd is empty. Default is false.
fail-on-error:

# debounce specifies default debounce duration.
debounce:

# requires specifies that when a config file changes the server needs
# to be restarted, but doesn't need to be rebuilt.
requires: "restart"
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/romshark/templier

go 1.22.2
go 1.22.5

require (
github.com/a-h/templ v0.2.747
Expand Down Expand Up @@ -38,12 +38,12 @@ require (
go.lsp.dev/uri v0.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/tools v0.22.0 // indirect
golang.org/x/tools v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
Expand All @@ -90,8 +90,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -123,8 +123,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
43 changes: 43 additions & 0 deletions internal/action/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Package action provides a simple helper for consolidating custom watcher actions.
package action

import "sync"

// Type is an action type.
type Type int8

const (
// ActionNone requires no rebuild, no restart, no reload.
ActionNone Type = iota

// ActionReload requires browser tab reload.
ActionReload

// ActionRestart requires restarting the server.
ActionRestart

// ActionRebuild requires rebuilding and restarting the server.
ActionRebuild
)

// SyncStatus action status for concurrent use.
type SyncStatus struct {
lock sync.Mutex
status Type
}

// Require sets the requirement status to t, if current requirement is a subset.
func (s *SyncStatus) Require(t Type) {
s.lock.Lock()
defer s.lock.Unlock()
if t > s.status {
s.status = t
}
}

// Load returns the current requirement status.
func (s *SyncStatus) Load() Type {
s.lock.Lock()
defer s.lock.Unlock()
return s.status
}
25 changes: 25 additions & 0 deletions internal/action/action_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package action_test

import (
"testing"

"github.com/romshark/templier/internal/action"
"github.com/stretchr/testify/require"
)

func TestRequire(t *testing.T) {
var s action.SyncStatus
require.Equal(t, action.ActionNone, s.Load())

s.Require(action.ActionReload)
require.Equal(t, action.ActionReload, s.Load(), "overwrite")

s.Require(action.ActionRestart)
require.Equal(t, action.ActionRestart, s.Load(), "overwrite")

s.Require(action.ActionReload)
require.Equal(t, action.ActionRestart, s.Load(), "no overwrite")

s.Require(action.ActionRebuild)
require.Equal(t, action.ActionRebuild, s.Load(), "overwrite")
}
37 changes: 31 additions & 6 deletions internal/templrun/templrun.go → internal/cmdrun/cmdrun.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,47 @@
package templrun
package cmdrun

import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"

"github.com/romshark/templier/internal/log"
"github.com/romshark/templier/internal/state"
"github.com/romshark/templier/internal/statetrack"
)

// RunWatch starts `templ generate --log-level debug --watch` and reads its
var ErrExitCode1 = errors.New("exit code 1")

// Run runs an arbitrary command and returns (output, ErrExitCode1)
// if it exits with error code 1, otherwise returns the original error.
func Run(
ctx context.Context, workDir string, cmd string, args ...string,
) (out []byte, err error) {
c := exec.CommandContext(ctx, cmd, args...)
c.Dir = workDir

out, err = c.CombinedOutput()
if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 1 {
return out, ErrExitCode1
} else if err != nil {
return nil, err
}
return out, nil
}

// Sh runs an arbitrary shell script and behaves similar to Run.
func Sh(ctx context.Context, workDir string, sh string) (out []byte, err error) {
return Run(ctx, workDir, "sh", "-c", sh)
}

// RunTemplWatch starts `templ generate --log-level debug --watch` and reads its
// stdout pipe for failure and success logs updating the state accordingly.
// When ctx is canceled the interrupt signal is sent to the watch process
// and graceful shutdown is awaited.
func RunWatch(ctx context.Context, workDir string, st *state.Tracker) error {
func RunTemplWatch(ctx context.Context, workDir string, st *statetrack.Tracker) error {
// Don't use CommandContext since it will kill the process
// which we don't want. We want the command to finish.
cmd := exec.Command("templ", "generate", "--log-level", "debug", "--watch")
Expand All @@ -39,9 +64,9 @@ func RunWatch(ctx context.Context, workDir string, st *state.Tracker) error {
b := scanner.Bytes()
switch {
case bytes.HasPrefix(b, bytesPrefixErr):
st.SetErrTempl(scanner.Text())
st.Set(statetrack.IndexTempl, scanner.Text())
case bytes.HasPrefix(b, bytesPrefixErrCleared):
st.SetErrTempl("")
st.Set(statetrack.IndexTempl, "")
}
}
if err := scanner.Err(); err != nil {
Expand Down
Loading

0 comments on commit 8dfe094

Please sign in to comment.