Skip to content

Commit f1f7965

Browse files
authored
chore(web): fix local web dev (#2233)
* chore(web): fix local web dev * chore: now it actually works * chore: actually fix the root problem this time * chore: make linter happy * chore: fix tests
1 parent eca0c36 commit f1f7965

File tree

2 files changed

+88
-39
lines changed

2 files changed

+88
-39
lines changed

web/static.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ type Web struct {
3939
assets fs.FS
4040
// initialState is the initial state to be passed to the React app
4141
initialState InitialState
42-
logger logrus.FieldLogger
42+
// wheter we're running in dev mode or not
43+
isDev bool
44+
logger logrus.FieldLogger
4345
}
4446

4547
type WebOption func(*Web)
@@ -68,25 +70,45 @@ func New(initialState InitialState, opts ...WebOption) (*Web, error) {
6870
opt(web)
6971
}
7072

73+
// TODO we might consider moving this env var evaluation to the CLI and make it an overarching property of the project
74+
if os.Getenv("EC_DEV_ENV") == "true" {
75+
web.isDev = true
76+
}
77+
7178
if web.logger == nil {
7279
web.logger = logrus.New().WithField("component", "web")
7380
}
7481

7582
if web.assets == nil {
76-
web.assets = DefaultAssetsFS()
83+
// By default, when running in dev mode we want to dinamically read our assets from source
84+
if web.isDev {
85+
web.assets = os.DirFS("./web/dist")
86+
} else {
87+
web.assets = DefaultAssetsFS()
88+
}
7789
}
7890

7991
if web.htmlTemplate == nil {
80-
htmlTemplate, err := template.ParseFS(web.assets, "index.html")
92+
err := web.loadHTMLTemplate()
8193
if err != nil {
82-
return nil, fmt.Errorf("failed to parse HTML template: %w", err)
94+
return nil, err
8395
}
84-
web.htmlTemplate = htmlTemplate
8596
}
8697

8798
return web, nil
8899
}
89100

101+
// loadHTMLTemplate parses the `index.html` file from the `web.assets` FS and stores it in the struct's `htmlTemplate`
102+
// property
103+
func (web *Web) loadHTMLTemplate() error {
104+
htmlTemplate, err := template.ParseFS(web.assets, "index.html")
105+
if err != nil {
106+
return fmt.Errorf("failed to parse HTML template: %w", err)
107+
}
108+
web.htmlTemplate = htmlTemplate
109+
return nil
110+
}
111+
90112
func (web *Web) rootHandler(w http.ResponseWriter, r *http.Request) {
91113
stateJSON, err := json.Marshal(web.initialState)
92114
if err != nil {
@@ -108,6 +130,12 @@ func (web *Web) rootHandler(w http.ResponseWriter, r *http.Request) {
108130
// Create a buffer to store the rendered template
109131
buf := new(bytes.Buffer)
110132

133+
// When running in dev mode we need to parse the HTML template on every requeest since the JS build might have
134+
// generated a new index.html and set of assets.
135+
if web.isDev {
136+
web.loadHTMLTemplate()
137+
}
138+
111139
// Execute the template and write to the buffer
112140
err = web.htmlTemplate.Execute(buf, data)
113141
if err != nil {
@@ -126,14 +154,7 @@ func (web *Web) rootHandler(w http.ResponseWriter, r *http.Request) {
126154
}
127155

128156
func (web *Web) RegisterRoutes(router *mux.Router) {
129-
130-
var webFS http.Handler
131-
if os.Getenv("EC_DEV_ENV") == "true" {
132-
webFS = http.FileServer(http.FS(os.DirFS("./dist")))
133-
} else {
134-
webFS = http.FileServer(http.FS(web.assets))
135-
}
136-
157+
webFS := http.FileServer(http.FS(web.assets))
137158
router.PathPrefix("/assets").Methods("GET").Handler(webFS)
138159
router.PathPrefix("/").Methods("GET").HandlerFunc(web.rootHandler)
139160
}

web/static_test.go

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"net/http"
77
"net/http/httptest"
88
"os"
9+
"path"
10+
"runtime"
911
"testing"
1012
"testing/fstest"
1113

@@ -29,6 +31,19 @@ func overrideDefaultFS(mockFS fstest.MapFS) func() {
2931
}
3032
}
3133

34+
var htmlTemplate = []byte(`<!DOCTYPE html>
35+
<html>
36+
<head>
37+
<title>{{.Title}}</title>
38+
</head>
39+
<body>
40+
<script>
41+
window.__INITIAL_STATE__ = {{.InitialState}};
42+
</script>
43+
</body>
44+
</html>`,
45+
)
46+
3247
// createMockFS creates a standard mock filesystem for testing
3348
func createMockFS() fstest.MapFS {
3449
// Create mock assets
@@ -42,18 +57,7 @@ func createMockFS() fstest.MapFS {
4257
Mode: 0644,
4358
},
4459
"index.html": &fstest.MapFile{
45-
Data: []byte(`<!DOCTYPE html>
46-
<html>
47-
<head>
48-
<title>{{.Title}}</title>
49-
</head>
50-
<body>
51-
<script>
52-
window.__INITIAL_STATE__ = {{.InitialState}};
53-
</script>
54-
</body>
55-
</html>`,
56-
),
60+
Data: htmlTemplate,
5761
Mode: 0644,
5862
},
5963
}
@@ -315,31 +319,39 @@ func TestRegisterRoutes(t *testing.T) {
315319
})
316320
}
317321
func TestRegisterRoutesWithDevEnv(t *testing.T) {
318-
// Create initial state
319-
initialState := InitialState{
320-
Title: "Test Title",
321-
Icon: "test-icon.png",
322+
// We need to change the current working directory because in `go test` this will be the package directory
323+
// We want to mimic prod/local dev behaviour where cwd will be under the root of the project
324+
_, filename, _, _ := runtime.Caller(0)
325+
dir := path.Join(path.Dir(filename), "..")
326+
err := os.Chdir(dir)
327+
if err != nil {
328+
t.Fatalf("failed to change cwd to root of the project: %s", err)
322329
}
330+
defer os.Chdir(path.Dir(filename))
323331

324332
// Create a test logger
325333
logger, _ := logtest.NewNullLogger()
334+
// Set the development environment variable
335+
t.Setenv("EC_DEV_ENV", "true")
326336

327-
// Create a new Web instance
328-
web, err := New(initialState, WithLogger(logger), WithAssetsFS(createMockFS()))
329-
require.NoError(t, err, "Failed to create Web instance")
330-
331-
// Create temporary dist directory structure for development
332-
err = os.MkdirAll("dist/assets", 0755)
337+
// Create temporary dist directory structure to mimic what we use for development
338+
err = os.MkdirAll("./web/dist/assets", 0755)
333339
require.NoError(t, err, "Failed to create dist directory")
334-
defer os.RemoveAll("dist/assets") // Clean up after test
340+
defer os.RemoveAll("./web/dist/assets") // Clean up after test
335341

336342
// Create a test file in the dist/assets directory
337343
devFileContent := "console.log('Development mode!');"
338-
err = os.WriteFile("dist/assets/test-file-dev-app.js", []byte(devFileContent), 0644)
344+
err = os.WriteFile("./web/dist/assets/test-file-dev-app.js", []byte(devFileContent), 0644)
339345
require.NoError(t, err, "Failed to write dev file")
340346

341-
// Set the development environment variable
342-
t.Setenv("EC_DEV_ENV", "true")
347+
// Create a index.hmtl test file in the dist/ directory to be used as template by the web server
348+
err = os.WriteFile("./web/dist/index.html", []byte(htmlTemplate), 0644)
349+
require.NoError(t, err, "Failed to write dev file")
350+
351+
// Create a new Web instance
352+
web, err := New(InitialState{}, WithLogger(logger))
353+
require.NoError(t, err, "Failed to create Web instance")
354+
343355
// Create router and register routes
344356
router := mux.NewRouter()
345357
web.RegisterRoutes(router)
@@ -358,6 +370,22 @@ func TestRegisterRoutesWithDevEnv(t *testing.T) {
358370
assert.Equal(t, devFileContent, recorder.Body.String(), "Response should contain the dev file content from local filesystem")
359371
})
360372

373+
t.Run("Changes to the file are reflected and served", func(t *testing.T) {
374+
newDevFileContent := devFileContent + "console.log('such a change, very wow');"
375+
err = os.WriteFile("./web/dist/assets/test-file-dev-app.js", []byte(newDevFileContent), 0644)
376+
req := httptest.NewRequest("GET", "/assets/test-file-dev-app.js", nil)
377+
recorder := httptest.NewRecorder()
378+
379+
// Serve the request
380+
router.ServeHTTP(recorder, req)
381+
382+
// Check status code
383+
assert.Equal(t, http.StatusOK, recorder.Code, "Should return status OK for dev file")
384+
385+
// Check that the new dev file content is served from local filesystem
386+
assert.Equal(t, newDevFileContent, recorder.Body.String(), "Response should contain the dev file content from local filesystem")
387+
})
388+
361389
t.Run("Non-existent File Returns 404", func(t *testing.T) {
362390
req := httptest.NewRequest("GET", "/assets/non-existent.js", nil)
363391
recorder := httptest.NewRecorder()

0 commit comments

Comments
 (0)