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] implement notify proxy to reload browser #661

Merged
merged 13 commits into from
Apr 4, 2024
3 changes: 3 additions & 0 deletions cmd/templ/generatecmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type GenerationEvent struct {
}

func (cmd Generate) Run(ctx context.Context) (err error) {
if cmd.Args.NotifyProxy {
return proxy.NotifyProxy(cmd.Args.ProxyBind, cmd.Args.ProxyPort)
}
if cmd.Args.Watch && cmd.Args.FileName != "" {
return fmt.Errorf("cannot watch a single file, remove the -f or -watch flag")
}
Expand Down
1 change: 1 addition & 0 deletions cmd/templ/generatecmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Arguments struct {
ProxyBind string
ProxyPort int
Proxy string
NotifyProxy bool
WorkerCount int
GenerateSourceMapVisualisations bool
IncludeVersion bool
Expand Down
23 changes: 21 additions & 2 deletions cmd/templ/generatecmd/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,17 @@ func (p *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if r.URL.Path == "/_templ/reload/events" {
// Provides a list of messages including a reload message.
p.sse.ServeHTTP(w, r)
switch r.Method {
case http.MethodGet:
// Provides a list of messages including a reload message.
p.sse.ServeHTTP(w, r)
return
case http.MethodPost:
// Send a reload message to all connected clients.
p.sse.Send("message", "reload")
return
}
http.Error(w, "only GET or POST method allowed", http.StatusMethodNotAllowed)
return
}
p.p.ServeHTTP(w, r)
Expand Down Expand Up @@ -180,3 +189,13 @@ func (rt *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {

return nil, fmt.Errorf("max retries reached")
}

func NotifyProxy(host string, port int) error {
urlStr := fmt.Sprintf("http://%s:%d/_templ/reload/events", host, port)
req, err := http.NewRequest(http.MethodPost, urlStr, nil)
if err != nil {
return err
}
_, err = http.DefaultClient.Do(req)
return err
}
96 changes: 96 additions & 0 deletions cmd/templ/generatecmd/proxy/proxy_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package proxy

import (
"bufio"
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"

"github.com/google/go-cmp/cmp"
)
Expand Down Expand Up @@ -210,4 +216,94 @@ func TestProxy(t *testing.T) {
t.Errorf("unexpected response body (-got +want):\n%s", diff)
}
})

t.Run("notify-proxy: sending POST request to /_templ/reload/events should receive reload sse event", func(t *testing.T) {
// Arrange 1: create a test proxy server
a-h marked this conversation as resolved.
Show resolved Hide resolved
dummyHandler := func(w http.ResponseWriter, r *http.Request) {}
dummyServer := httptest.NewServer(http.HandlerFunc(dummyHandler))
defer dummyServer.Close()

u, err := url.Parse(dummyServer.URL)
if err != nil {
t.Fatalf("unexpected error parsing URL: %v", err)
}
handler := New("0.0.0.0", 0, u)
proxyServer := httptest.NewServer(handler)
defer proxyServer.Close()

u2, err := url.Parse(proxyServer.URL)
if err != nil {
t.Fatalf("unexpected error parsing URL: %v", err)
}
port, err := strconv.Atoi(u2.Port())
if err != nil {
t.Fatalf("unexpected error parsing port: %v", err)
}

// Arrange 2: start a goroutine to listen for sse events
a-h marked this conversation as resolved.
Show resolved Hide resolved
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

errChan := make(chan error)
sseRespCh := make(chan string)
sseListening := make(chan bool) // coordination channel to ensure the sse listener is started before notifying the proxy
a-h marked this conversation as resolved.
Show resolved Hide resolved
go func() {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/_templ/reload/events", proxyServer.URL), nil)
if err != nil {
errChan <- err
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
errChan <- err
return
}
defer resp.Body.Close()

sseListening <- true
lines := []string{}
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
lines = append(lines, scanner.Text())
if scanner.Text() == "data: reload" {
sseRespCh <- strings.Join(lines, "\n")
return
}
}
err = scanner.Err()
// we expect the connection to be closed by the server: this is the only way to terminate the sse connection
a-h marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
errChan <- err
return
}
}()

// Act: notify the proxy
a-h marked this conversation as resolved.
Show resolved Hide resolved
select { // either sse is listening or an error occurred
a-h marked this conversation as resolved.
Show resolved Hide resolved
case <-sseListening:
err = NotifyProxy(u2.Hostname(), port)
if err != nil {
t.Fatalf("unexpected error notifying proxy: %v", err)
}
case err := <-errChan:
if err == nil {
t.Fatalf("unexpected sse response: %v", err)
}
}

// Assert
a-h marked this conversation as resolved.
Show resolved Hide resolved
select { // either sse has a expected response or an error or timeout occurred
a-h marked this conversation as resolved.
Show resolved Hide resolved
case resp := <-sseRespCh:
if !strings.Contains(resp, "event: message\ndata: reload") {
t.Errorf("expected sse reload event to be received, got: %q", resp)
}
case err := <-errChan:
if err == nil {
t.Fatalf("unexpected sse response: %v", err)
}
case <-ctx.Done():
t.Fatalf("timeout waiting for sse response")
}
})
}
4 changes: 4 additions & 0 deletions cmd/templ/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ Args:
The port the proxy will listen on. (default 7331)
-proxybind
The address the proxy will listen on. (default 127.0.0.1)
-notify-proxy
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.
-w
Number of workers to use when generating code. (default runtime.NumCPUs)
-pprof
Expand Down Expand Up @@ -134,6 +136,7 @@ func generateCmd(w io.Writer, args []string) (code int) {
proxyFlag := cmd.String("proxy", "", "")
proxyPortFlag := cmd.Int("proxyport", 7331, "")
proxyBindFlag := cmd.String("proxybind", "127.0.0.1", "")
notifyProxyFlag := cmd.Bool("notify-proxy", false, "")
workerCountFlag := cmd.Int("w", runtime.NumCPU(), "")
pprofPortFlag := cmd.Int("pprof", 0, "")
keepOrphanedFilesFlag := cmd.Bool("keep-orphaned-files", false, "")
Expand Down Expand Up @@ -169,6 +172,7 @@ func generateCmd(w io.Writer, args []string) (code int) {
Proxy: *proxyFlag,
ProxyPort: *proxyPortFlag,
ProxyBind: *proxyBindFlag,
NotifyProxy: *notifyProxyFlag,
WorkerCount: *workerCountFlag,
GenerateSourceMapVisualisations: *sourceMapVisualisationsFlag,
IncludeVersion: *includeVersionFlag,
Expand Down