Skip to content

Commit c846d1b

Browse files
authored
fix: use temp directory for watch mode files, fixes 1093 (#1099)
1 parent f82e9a8 commit c846d1b

File tree

7 files changed

+105
-30
lines changed

7 files changed

+105
-30
lines changed

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.850
1+
0.3.851

cmd/templ/generatecmd/cmd.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"sync/atomic"
1818
"time"
1919

20+
templruntime "github.com/a-h/templ/runtime"
21+
2022
"github.com/a-h/templ"
2123
"github.com/a-h/templ/cmd/templ/generatecmd/modcheck"
2224
"github.com/a-h/templ/cmd/templ/generatecmd/proxy"
@@ -28,7 +30,7 @@ import (
2830
"github.com/fsnotify/fsnotify"
2931
)
3032

31-
const defaultWatchPattern = `(.+\.go$)|(.+\.templ$)|(.+_templ\.txt$)`
33+
const defaultWatchPattern = `(.+\.go$)|(.+\.templ$)`
3234

3335
func NewGenerate(log *slog.Logger, args Arguments) (g *Generate, err error) {
3436
g = &Generate{
@@ -104,6 +106,7 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
104106
cmd.Log.Warn("templ version check: " + err.Error())
105107
}
106108

109+
cmd.Log.Debug("Creating filesystem event handler")
107110
fseh := NewFSEventHandler(
108111
cmd.Log,
109112
cmd.Args.Path,
@@ -189,7 +192,6 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
189192
"All post-generation events processed, deleting watch mode text files",
190193
slog.Int64("errorCount", errorCount.Load()),
191194
)
192-
193195
fileEvents := make(chan fsnotify.Event)
194196
go func() {
195197
if err := watcher.WalkFiles(ctx, cmd.Args.Path, cmd.WatchPattern, fileEvents); err != nil {
@@ -200,8 +202,9 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
200202
close(fileEvents)
201203
}()
202204
for event := range fileEvents {
203-
if strings.HasSuffix(event.Name, "_templ.txt") {
204-
if err = os.Remove(event.Name); err != nil {
205+
if strings.HasSuffix(event.Name, "_templ.go") || strings.HasSuffix(event.Name, ".templ") {
206+
watchModeFileName := templruntime.GetDevModeTextFileName(event.Name)
207+
if err := os.Remove(watchModeFileName); err != nil && !errors.Is(err, os.ErrNotExist) {
205208
cmd.Log.Warn("Failed to remove watch mode text file", slog.Any("error", err))
206209
}
207210
}

cmd/templ/generatecmd/eventhandler.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/a-h/templ/cmd/templ/visualize"
2222
"github.com/a-h/templ/generator"
2323
"github.com/a-h/templ/parser/v2"
24+
"github.com/a-h/templ/runtime"
2425
"github.com/fsnotify/fsnotify"
2526
)
2627

@@ -118,20 +119,6 @@ func (h *FSEventHandler) HandleEvent(ctx context.Context, event fsnotify.Event)
118119
}
119120
return GenerateResult{Updated: true, GoUpdated: true, TextUpdated: false}, nil
120121
}
121-
// Handle _templ.txt files.
122-
if !event.Has(fsnotify.Remove) && strings.HasSuffix(event.Name, "_templ.txt") {
123-
if h.devMode {
124-
// Don't delete the file in dev mode, ignore changes to it, since the .templ file
125-
// must have been updated in order to trigger a change in the _templ.txt file.
126-
return GenerateResult{Updated: false, GoUpdated: false, TextUpdated: false}, nil
127-
}
128-
h.Log.Debug("Deleting watch mode file", slog.String("file", event.Name))
129-
if err = os.Remove(event.Name); err != nil {
130-
h.Log.Warn("Failed to remove watch mode text file", slog.Any("error", err))
131-
return GenerateResult{}, nil
132-
}
133-
return GenerateResult{}, nil
134-
}
135122

136123
// If the file hasn't been updated since the last time we processed it, ignore it.
137124
lastModTime, updatedModTime := h.UpsertLastModTime(event.Name)
@@ -274,7 +261,8 @@ func (h *FSEventHandler) generate(ctx context.Context, fileName string) (result
274261

275262
// Add the txt file if it has changed.
276263
if h.devMode {
277-
txtFileName := strings.TrimSuffix(fileName, ".templ") + "_templ.txt"
264+
txtFileName := runtime.GetDevModeTextFileName(fileName)
265+
h.Log.Debug("Writing development mode text file", slog.String("file", fileName), slog.String("output", txtFileName))
278266
joined := strings.Join(generatorOutput.Literals, "\n")
279267
txtHash := sha256.Sum256([]byte(joined))
280268
if h.UpsertHash(txtFileName, txtHash) {

cmd/templ/generatecmd/main_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/a-h/templ/cmd/templ/testproject"
14+
"github.com/a-h/templ/runtime"
1415
"golang.org/x/sync/errgroup"
1516
)
1617

@@ -69,13 +70,14 @@ func TestGenerate(t *testing.T) {
6970
})
7071

7172
// Check the templates_templ.go file was created, with backoff.
73+
devModeTextFileName := runtime.GetDevModeTextFileName(path.Join(dir, "templates_templ.go"))
7274
for i := 0; i < 5; i++ {
7375
time.Sleep(time.Second * time.Duration(i))
7476
_, err = os.Stat(path.Join(dir, "templates_templ.go"))
7577
if err != nil {
7678
continue
7779
}
78-
_, err = os.Stat(path.Join(dir, "templates_templ.txt"))
80+
_, err = os.Stat(devModeTextFileName)
7981
if err != nil {
8082
continue
8183
}
@@ -91,7 +93,7 @@ func TestGenerate(t *testing.T) {
9193
}
9294

9395
// Check the templates_templ.txt file was removed.
94-
_, err = os.Stat(path.Join(dir, "templates_templ.txt"))
96+
_, err = os.Stat(path.Join(dir, devModeTextFileName))
9597
if err == nil {
9698
t.Fatalf("templates_templ.txt was not removed")
9799
}
@@ -110,14 +112,14 @@ func TestDefaultWatchPattern(t *testing.T) {
110112
matches: false,
111113
},
112114
{
113-
name: "*_templ.txt matches, Windows",
115+
name: "*_templ.txt is no longer matched, Windows",
114116
input: `C:\Users\adrian\github.com\a-h\templ\cmd\templ\testproject\strings_templ.txt`,
115-
matches: true,
117+
matches: false,
116118
},
117119
{
118-
name: "*_templ.txt matches, Unix",
120+
name: "*_templ.txt is no longer matched, Unix",
119121
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/strings_templ.txt",
120-
matches: true,
122+
matches: false,
121123
},
122124
{
123125
name: "*.templ files match, Windows",

cmd/templ/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ func generateCmd(stdout, stderr io.Writer, args []string) (code int) {
208208
includeVersionFlag := cmd.Bool("include-version", true, "")
209209
includeTimestampFlag := cmd.Bool("include-timestamp", false, "")
210210
watchFlag := cmd.Bool("watch", false, "")
211-
watchPatternFlag := cmd.String("watch-pattern", "(.+\\.go$)|(.+\\.templ$)|(.+_templ\\.txt$)", "")
211+
watchPatternFlag := cmd.String("watch-pattern", "(.+\\.go$)|(.+\\.templ$)", "")
212212
openBrowserFlag := cmd.Bool("open-browser", true, "")
213213
cmdFlag := cmd.String("cmd", "", "")
214214
proxyFlag := cmd.String("proxy", "", "")

runtime/watchmode.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package runtime
22

33
import (
4+
"crypto/sha256"
5+
"encoding/hex"
46
"errors"
57
"fmt"
68
"io"
79
"os"
10+
"path/filepath"
811
"runtime"
912
"strconv"
1013
"strings"
@@ -14,6 +17,42 @@ import (
1417

1518
var developmentMode = os.Getenv("TEMPL_DEV_MODE") == "true"
1619

20+
func GetDevModeTextFileName(templFileName string) string {
21+
if strings.HasSuffix(templFileName, "_templ.go") {
22+
templFileName = strings.TrimSuffix(templFileName, "_templ.go") + ".templ"
23+
}
24+
absFileName, err := filepath.Abs(templFileName)
25+
if err != nil {
26+
absFileName = templFileName
27+
}
28+
absFileName, err = filepath.EvalSymlinks(absFileName)
29+
if err != nil {
30+
absFileName = templFileName
31+
}
32+
absFileName = normalizePath(absFileName)
33+
34+
hashedFileName := sha256.Sum256([]byte(absFileName))
35+
outputFileName := fmt.Sprintf("templ_%s.txt", hex.EncodeToString(hashedFileName[:]))
36+
37+
root := os.TempDir()
38+
if os.Getenv("TEMPL_DEV_MODE_ROOT") != "" {
39+
root = os.Getenv("TEMPL_DEV_MODE_ROOT")
40+
}
41+
42+
return filepath.Join(root, outputFileName)
43+
}
44+
45+
// normalizePath converts Windows paths to Unix style paths.
46+
func normalizePath(p string) string {
47+
p = strings.ReplaceAll(filepath.Clean(p), `\`, `/`)
48+
parts := strings.SplitN(p, ":", 2)
49+
if len(parts) == 2 && len(parts[0]) == 1 {
50+
drive := strings.ToLower(parts[0])
51+
p = "/" + drive + parts[1]
52+
}
53+
return p
54+
}
55+
1756
// WriteString writes the string to the writer. If development mode is enabled
1857
// s is replaced with the string at the index in the _templ.txt file.
1958
func WriteString(w io.Writer, index int, s string) (err error) {
@@ -22,13 +61,16 @@ func WriteString(w io.Writer, index int, s string) (err error) {
2261
if !strings.HasSuffix(path, "_templ.go") {
2362
return errors.New("templ: attempt to use WriteString from a non templ file")
2463
}
25-
txtFilePath := strings.Replace(path, "_templ.go", "_templ.txt", 1)
64+
path, err := filepath.EvalSymlinks(path)
65+
if err != nil {
66+
return fmt.Errorf("templ: failed to eval symlinks for %q: %w", path, err)
67+
}
2668

69+
txtFilePath := GetDevModeTextFileName(path)
2770
literals, err := getWatchedStrings(txtFilePath)
2871
if err != nil {
29-
return fmt.Errorf("templ: failed to cache strings: %w", err)
72+
return fmt.Errorf("templ: failed to get watched strings for %q: %w", path, err)
3073
}
31-
3274
if index > len(literals) {
3375
return fmt.Errorf("templ: failed to find line %d in %s", index, txtFilePath)
3476
}

runtime/watchmode_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package runtime
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
func TestWatchMode(t *testing.T) {
9+
os.Setenv("TEMPL_DEV_MODE_ROOT", "/tmp")
10+
defer os.Unsetenv("TEMPL_DEV_MODE_ROOT")
11+
12+
t.Run("GetDevModeTextFileName respects the TEMPL_DEV_MODE_ROOT environment variable", func(t *testing.T) {
13+
expected := "/tmp/templ_14a26e43676c091fa17a7f4eccbbf62a44339e3cc6454b9a82c042227a21757f.txt"
14+
actual := GetDevModeTextFileName("test.templ")
15+
if actual != expected {
16+
t.Errorf("got %q, want %q", actual, expected)
17+
}
18+
})
19+
t.Run("GetDevModeTextFileName replaces _templ.go with .templ", func(t *testing.T) {
20+
expected := "/tmp/templ_14a26e43676c091fa17a7f4eccbbf62a44339e3cc6454b9a82c042227a21757f.txt"
21+
actual := GetDevModeTextFileName("test_templ.go")
22+
if actual != expected {
23+
t.Errorf("got %q, want %q", actual, expected)
24+
}
25+
})
26+
t.Run("GetDevModeTextFileName accepts absolute Linux paths", func(t *testing.T) {
27+
expected := "/tmp/templ_629591f679da14bbba764530c2965c6c8d3a8931f0ba867104c2ec441691ae22.txt"
28+
actual := GetDevModeTextFileName("/home/user/test.templ")
29+
if actual != expected {
30+
t.Errorf("got %q, want %q", actual, expected)
31+
}
32+
})
33+
t.Run("GetDevModeTextFileName accepts absolute Windows paths, which are normalized to Unix style before hashing", func(t *testing.T) {
34+
expected := "/tmp/templ_f0321c47222350b736aaa2d18a2b313be03da4fd4ebd80af5745434d8776376f.txt"
35+
actual := GetDevModeTextFileName(`C:\Windows\System32\test.templ`)
36+
if actual != expected {
37+
t.Errorf("got %q, want %q", actual, expected)
38+
}
39+
})
40+
}

0 commit comments

Comments
 (0)