Skip to content

Commit 022a8b0

Browse files
jackieliia-h
andauthored
feat: add notify proxy to reload browser (#661)
Co-authored-by: Adrian Hesketh <[email protected]> Co-authored-by: Adrian Hesketh <[email protected]>
1 parent 72349ec commit 022a8b0

File tree

6 files changed

+140
-3
lines changed

6 files changed

+140
-3
lines changed

cmd/templ/generatecmd/cmd.go

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ type GenerationEvent struct {
4949
}
5050

5151
func (cmd Generate) Run(ctx context.Context) (err error) {
52+
if cmd.Args.NotifyProxy {
53+
return proxy.NotifyProxy(cmd.Args.ProxyBind, cmd.Args.ProxyPort)
54+
}
5255
if cmd.Args.Watch && cmd.Args.FileName != "" {
5356
return fmt.Errorf("cannot watch a single file, remove the -f or -watch flag")
5457
}

cmd/templ/generatecmd/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Arguments struct {
2121
ProxyBind string
2222
ProxyPort int
2323
Proxy string
24+
NotifyProxy bool
2425
WorkerCount int
2526
GenerateSourceMapVisualisations bool
2627
IncludeVersion bool

cmd/templ/generatecmd/proxy/proxy.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,17 @@ func (p *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
118118
return
119119
}
120120
if r.URL.Path == "/_templ/reload/events" {
121-
// Provides a list of messages including a reload message.
122-
p.sse.ServeHTTP(w, r)
121+
switch r.Method {
122+
case http.MethodGet:
123+
// Provides a list of messages including a reload message.
124+
p.sse.ServeHTTP(w, r)
125+
return
126+
case http.MethodPost:
127+
// Send a reload message to all connected clients.
128+
p.sse.Send("message", "reload")
129+
return
130+
}
131+
http.Error(w, "only GET or POST method allowed", http.StatusMethodNotAllowed)
123132
return
124133
}
125134
p.p.ServeHTTP(w, r)
@@ -180,3 +189,13 @@ func (rt *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
180189

181190
return nil, fmt.Errorf("max retries reached")
182191
}
192+
193+
func NotifyProxy(host string, port int) error {
194+
urlStr := fmt.Sprintf("http://%s:%d/_templ/reload/events", host, port)
195+
req, err := http.NewRequest(http.MethodPost, urlStr, nil)
196+
if err != nil {
197+
return err
198+
}
199+
_, err = http.DefaultClient.Do(req)
200+
return err
201+
}

cmd/templ/generatecmd/proxy/proxy_test.go

+96
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package proxy
22

33
import (
4+
"bufio"
45
"bytes"
56
"compress/gzip"
7+
"context"
68
"fmt"
79
"io"
810
"net/http"
11+
"net/http/httptest"
12+
"net/url"
13+
"strconv"
914
"strings"
1015
"testing"
16+
"time"
1117

1218
"github.com/google/go-cmp/cmp"
1319
)
@@ -210,4 +216,94 @@ func TestProxy(t *testing.T) {
210216
t.Errorf("unexpected response body (-got +want):\n%s", diff)
211217
}
212218
})
219+
220+
t.Run("notify-proxy: sending POST request to /_templ/reload/events should receive reload sse event", func(t *testing.T) {
221+
// Arrange 1: create a test proxy server.
222+
dummyHandler := func(w http.ResponseWriter, r *http.Request) {}
223+
dummyServer := httptest.NewServer(http.HandlerFunc(dummyHandler))
224+
defer dummyServer.Close()
225+
226+
u, err := url.Parse(dummyServer.URL)
227+
if err != nil {
228+
t.Fatalf("unexpected error parsing URL: %v", err)
229+
}
230+
handler := New("0.0.0.0", 0, u)
231+
proxyServer := httptest.NewServer(handler)
232+
defer proxyServer.Close()
233+
234+
u2, err := url.Parse(proxyServer.URL)
235+
if err != nil {
236+
t.Fatalf("unexpected error parsing URL: %v", err)
237+
}
238+
port, err := strconv.Atoi(u2.Port())
239+
if err != nil {
240+
t.Fatalf("unexpected error parsing port: %v", err)
241+
}
242+
243+
// Arrange 2: start a goroutine to listen for sse events.
244+
ctx := context.Background()
245+
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
246+
defer cancel()
247+
248+
errChan := make(chan error)
249+
sseRespCh := make(chan string)
250+
sseListening := make(chan bool) // Coordination channel that ensures the SSE listener is started before notifying the proxy.
251+
go func() {
252+
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/_templ/reload/events", proxyServer.URL), nil)
253+
if err != nil {
254+
errChan <- err
255+
return
256+
}
257+
resp, err := http.DefaultClient.Do(req)
258+
if err != nil {
259+
errChan <- err
260+
return
261+
}
262+
defer resp.Body.Close()
263+
264+
sseListening <- true
265+
lines := []string{}
266+
scanner := bufio.NewScanner(resp.Body)
267+
for scanner.Scan() {
268+
lines = append(lines, scanner.Text())
269+
if scanner.Text() == "data: reload" {
270+
sseRespCh <- strings.Join(lines, "\n")
271+
return
272+
}
273+
}
274+
err = scanner.Err()
275+
// We expect the connection to be closed by the server: this is the only way to terminate the sse connection.
276+
if err != nil {
277+
errChan <- err
278+
return
279+
}
280+
}()
281+
282+
// Act: notify the proxy.
283+
select { // Either SSE is listening or an error occurred.
284+
case <-sseListening:
285+
err = NotifyProxy(u2.Hostname(), port)
286+
if err != nil {
287+
t.Fatalf("unexpected error notifying proxy: %v", err)
288+
}
289+
case err := <-errChan:
290+
if err == nil {
291+
t.Fatalf("unexpected sse response: %v", err)
292+
}
293+
}
294+
295+
// Assert.
296+
select { // Either SSE has a expected response or an error or timeout occurred.
297+
case resp := <-sseRespCh:
298+
if !strings.Contains(resp, "event: message\ndata: reload") {
299+
t.Errorf("expected sse reload event to be received, got: %q", resp)
300+
}
301+
case err := <-errChan:
302+
if err == nil {
303+
t.Fatalf("unexpected sse response: %v", err)
304+
}
305+
case <-ctx.Done():
306+
t.Fatalf("timeout waiting for sse response")
307+
}
308+
})
213309
}

cmd/templ/main.go

+4
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ Args:
9191
The port the proxy will listen on. (default 7331)
9292
-proxybind
9393
The address the proxy will listen on. (default 127.0.0.1)
94+
-notify-proxy
95+
If present, the command will issue a reload event to the proxy 127.0.0.1:7331, or use proxyport and proxybind to specify a different address.
9496
-w
9597
Number of workers to use when generating code. (default runtime.NumCPUs)
9698
-pprof
@@ -134,6 +136,7 @@ func generateCmd(w io.Writer, args []string) (code int) {
134136
proxyFlag := cmd.String("proxy", "", "")
135137
proxyPortFlag := cmd.Int("proxyport", 7331, "")
136138
proxyBindFlag := cmd.String("proxybind", "127.0.0.1", "")
139+
notifyProxyFlag := cmd.Bool("notify-proxy", false, "")
137140
workerCountFlag := cmd.Int("w", runtime.NumCPU(), "")
138141
pprofPortFlag := cmd.Int("pprof", 0, "")
139142
keepOrphanedFilesFlag := cmd.Bool("keep-orphaned-files", false, "")
@@ -169,6 +172,7 @@ func generateCmd(w io.Writer, args []string) (code int) {
169172
Proxy: *proxyFlag,
170173
ProxyPort: *proxyPortFlag,
171174
ProxyBind: *proxyBindFlag,
175+
NotifyProxy: *notifyProxyFlag,
172176
WorkerCount: *workerCountFlag,
173177
GenerateSourceMapVisualisations: *sourceMapVisualisationsFlag,
174178
IncludeVersion: *includeVersionFlag,

docs/docs/09-commands-and-tools/03-hot-reload.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,20 @@ sequenceDiagram
8282
deactivate templ_proxy
8383
```
8484

85+
### Triggering hot reload from outside `templ generate --watch`
86+
87+
If you want to trigger a hot reload from outside `templ generate --watch` (e.g. if you're using `air`, `wgo` or another tool to build, but you want to use the templ hot reload proxy), you can use the `--notify-proxy` argument.
88+
89+
```shell
90+
templ generate --notify-proxy
91+
```
92+
93+
This will default to the default templ proxy address of `localhost:7331`, but can be changed with the `--proxybind` and `--proxyport` arguments.
94+
95+
```shell
96+
templ generate --notify-proxy --proxybind="localhost" --proxyport="8080"
97+
```
98+
8599
## Alternative 1: wgo
86100

87101
[wgo](https://github.com/bokwoon95/wgo):
@@ -96,7 +110,7 @@ To avoid a continous reloading files ending with `_templ.go` should be skipped v
96110

97111
## Alternative 2: air
98112

99-
Air's reload performance is better than templ's built-in feature due to its complex filesystem notification setup, but doesn't ship with a proxy to automatically reload pages, and requires a `toml` configuration file for operation.
113+
Air can handle `*.go` files, but doesn't ship with a proxy to automatically reload pages, and requires a `toml` configuration file for operation.
100114

101115
See https://github.com/cosmtrek/air for details.
102116

0 commit comments

Comments
 (0)