-
Notifications
You must be signed in to change notification settings - Fork 9
/
hotreload.go
166 lines (155 loc) · 5.47 KB
/
hotreload.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package go_ssr
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/gorilla/websocket"
"github.com/natewong1313/go-react-ssr/internal/utils"
"github.com/rs/zerolog"
"net/http"
"os"
"path/filepath"
"strings"
)
type HotReload struct {
engine *Engine
logger zerolog.Logger
connectedClients map[string][]*websocket.Conn
}
// newHotReload creates a new HotReload instance
func newHotReload(engine *Engine) *HotReload {
return &HotReload{
engine: engine,
logger: engine.Logger,
connectedClients: make(map[string][]*websocket.Conn),
}
}
// Start starts the hot reload server and watcher
func (hr *HotReload) Start() {
go hr.startServer()
go hr.startWatcher()
}
// startServer starts the hot reload websocket server
func (hr *HotReload) startServer() {
hr.logger.Info().Msgf("Hot reload websocket running on port %d", hr.engine.Config.HotReloadServerPort)
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
hr.logger.Err(err).Msg("Failed to upgrade websocket")
return
}
// Client should send routeID as first message
_, routeID, err := ws.ReadMessage()
if err != nil {
hr.logger.Err(err).Msg("Failed to read message from websocket")
return
}
err = ws.WriteMessage(1, []byte("Connected"))
if err != nil {
hr.logger.Err(err).Msg("Failed to write message to websocket")
return
}
// Add client to connectedClients
hr.connectedClients[string(routeID)] = append(hr.connectedClients[string(routeID)], ws)
})
err := http.ListenAndServe(fmt.Sprintf(":%d", hr.engine.Config.HotReloadServerPort), nil)
if err != nil {
hr.logger.Err(err).Msg("Hot reload server quit unexpectedly")
}
}
// startWatcher starts the file watcher
func (hr *HotReload) startWatcher() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
hr.logger.Err(err).Msg("Failed to start watcher")
return
}
defer watcher.Close()
// Walk through all files in the frontend directory and add them to the watcher
if err = filepath.Walk(hr.engine.Config.FrontendDir, func(path string, fi os.FileInfo, err error) error {
if fi.Mode().IsDir() {
return watcher.Add(path)
}
return nil
}); err != nil {
hr.logger.Err(err).Msg("Failed to add files in directory to watcher")
return
}
for {
select {
case event := <-watcher.Events:
// Watch for file created, deleted, updated, or renamed events
if event.Op.String() != "CHMOD" && !strings.Contains(event.Name, "gossr-temporary") {
filePath := utils.GetFullFilePath(event.Name)
hr.logger.Info().Msgf("File changed: %s, reloading", filePath)
// Store the routes that need to be reloaded
var routeIDS []string
switch {
case filePath == hr.engine.Config.LayoutFilePath: // If the layout file has been updated, reload all routes
routeIDS = hr.engine.CacheManager.GetAllRouteIDS()
case hr.layoutCSSFileUpdated(filePath): // If the global css file has been updated, rebuild it and reload all routes
if err := hr.engine.BuildLayoutCSSFile(); err != nil {
hr.logger.Err(err).Msg("Failed to build global css file")
continue
}
routeIDS = hr.engine.CacheManager.GetAllRouteIDS()
case hr.needsTailwindRecompile(filePath): // If tailwind is enabled and a React file has been updated, rebuild the global css file and reload all routes
if err := hr.engine.BuildLayoutCSSFile(); err != nil {
hr.logger.Err(err).Msg("Failed to build global css file")
continue
}
fallthrough
default:
// Get all route ids that use that file or have it as a dependency
routeIDS = hr.engine.CacheManager.GetRouteIDSWithFile(filePath)
}
// Find any parent files that import the file that was modified and delete their cached build
parentFiles := hr.engine.CacheManager.GetParentFilesFromDependency(filePath)
for _, parentFile := range parentFiles {
hr.engine.CacheManager.RemoveServerBuild(parentFile)
hr.engine.CacheManager.RemoveClientBuild(parentFile)
}
// Reload any routes that import the modified file
go hr.broadcastFileUpdateToClients(routeIDS)
}
case err := <-watcher.Errors:
hr.logger.Err(err).Msg("Error watching files")
}
}
}
// layoutCSSFileUpdated checks if the layout css file has been updated
func (hr *HotReload) layoutCSSFileUpdated(filePath string) bool {
return utils.GetFullFilePath(filePath) == hr.engine.Config.LayoutCSSFilePath
}
// needsTailwindRecompile checks if the file that was updated is a React file
func (hr *HotReload) needsTailwindRecompile(filePath string) bool {
if hr.engine.Config.TailwindConfigPath == "" {
return false
}
fileTypes := []string{".tsx", ".ts", ".jsx", ".js"}
for _, fileType := range fileTypes {
if strings.HasSuffix(filePath, fileType) {
return true
}
}
return false
}
// broadcastFileUpdateToClients sends a message to all connected clients to reload the page
func (hr *HotReload) broadcastFileUpdateToClients(routeIDS []string) {
// Iterate over each route ID
for _, routeID := range routeIDS {
// Find all clients listening for that route ID
for i, ws := range hr.connectedClients[routeID] {
// Send reload message to client
err := ws.WriteMessage(1, []byte("reload"))
if err != nil {
// remove client if browser is closed or page changed
hr.connectedClients[routeID] = append(hr.connectedClients[routeID][:i], hr.connectedClients[routeID][i+1:]...)
}
}
}
}