Skip to content

Commit a66a237

Browse files
authored
feat: generate same dev and prod code, fixes #700 (#1027)
1 parent 6afd676 commit a66a237

File tree

82 files changed

+872
-626
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+872
-626
lines changed

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.808
1+
0.2.810

benchmarks/templ/template_templ.go

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/templ/generatecmd/cmd.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"log/slog"
88
"net/http"
99
"net/url"
10+
"os"
1011
"path"
1112
"path/filepath"
1213
"regexp"
@@ -54,6 +55,7 @@ type Generate struct {
5455

5556
type GenerationEvent struct {
5657
Event fsnotify.Event
58+
Updated bool
5759
GoUpdated bool
5860
TextUpdated bool
5961
}
@@ -114,7 +116,7 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
114116

115117
// If we're processing a single file, don't bother setting up the channels/multithreaing.
116118
if cmd.Args.FileName != "" {
117-
_, _, err = fseh.HandleEvent(ctx, fsnotify.Event{
119+
_, err = fseh.HandleEvent(ctx, fsnotify.Event{
118120
Name: cmd.Args.FileName,
119121
Op: fsnotify.Create,
120122
})
@@ -219,15 +221,16 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
219221
cmd.Log.Debug("Processing file", slog.String("file", event.Name))
220222
defer eventsWG.Done()
221223
defer func() { <-sem }()
222-
goUpdated, textUpdated, err := fseh.HandleEvent(ctx, event)
224+
r, err := fseh.HandleEvent(ctx, event)
223225
if err != nil {
224226
errs <- err
225227
}
226-
if goUpdated || textUpdated {
228+
if r.GoUpdated || r.TextUpdated {
227229
postGeneration <- &GenerationEvent{
228230
Event: event,
229-
GoUpdated: goUpdated,
230-
TextUpdated: textUpdated,
231+
Updated: r.Updated,
232+
GoUpdated: r.GoUpdated,
233+
TextUpdated: r.TextUpdated,
231234
}
232235
}
233236
}(event)
@@ -273,6 +276,9 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
273276
postGenerationEventsWG.Add(1)
274277
if cmd.Args.Command != "" && goUpdated {
275278
cmd.Log.Debug("Executing command", slog.String("command", cmd.Args.Command))
279+
if cmd.Args.Watch {
280+
os.Setenv("TEMPL_DEV_MODE", "true")
281+
}
276282
if _, err := run.Run(ctx, cmd.Args.Path, cmd.Args.Command); err != nil {
277283
cmd.Log.Error("Error executing command", slog.Any("error", err))
278284
}

cmd/templ/generatecmd/eventhandler.go

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,17 @@ func NewFSEventHandler(
5757
fileNameToLastModTimeMutex: &sync.Mutex{},
5858
fileNameToError: make(map[string]struct{}),
5959
fileNameToErrorMutex: &sync.Mutex{},
60+
fileNameToOutput: make(map[string]generator.GeneratorOutput),
61+
fileNameToOutputMutex: &sync.Mutex{},
62+
devMode: devMode,
6063
hashes: make(map[string][sha256.Size]byte),
6164
hashesMutex: &sync.Mutex{},
6265
genOpts: genOpts,
6366
genSourceMapVis: genSourceMapVis,
64-
DevMode: devMode,
6567
keepOrphanedFiles: keepOrphanedFiles,
6668
writer: fileWriter,
6769
lazy: lazy,
6870
}
69-
if devMode {
70-
fseh.genOpts = append(fseh.genOpts, generator.WithExtractStrings())
71-
}
7271
return fseh
7372
}
7473

@@ -80,71 +79,84 @@ type FSEventHandler struct {
8079
fileNameToLastModTimeMutex *sync.Mutex
8180
fileNameToError map[string]struct{}
8281
fileNameToErrorMutex *sync.Mutex
82+
fileNameToOutput map[string]generator.GeneratorOutput
83+
fileNameToOutputMutex *sync.Mutex
84+
devMode bool
8385
hashes map[string][sha256.Size]byte
8486
hashesMutex *sync.Mutex
8587
genOpts []generator.GenerateOpt
8688
genSourceMapVis bool
87-
DevMode bool
8889
Errors []error
8990
keepOrphanedFiles bool
9091
writer func(string, []byte) error
9192
lazy bool
9293
}
9394

94-
func (h *FSEventHandler) HandleEvent(ctx context.Context, event fsnotify.Event) (goUpdated, textUpdated bool, err error) {
95+
type GenerateResult struct {
96+
// Updated indicates that the file was updated.
97+
Updated bool
98+
// GoUpdated indicates that Go expressions were updated.
99+
GoUpdated bool
100+
// TextUpdated indicates that text literals were updated.
101+
TextUpdated bool
102+
}
103+
104+
func (h *FSEventHandler) HandleEvent(ctx context.Context, event fsnotify.Event) (result GenerateResult, err error) {
95105
// Handle _templ.go files.
96106
if !event.Has(fsnotify.Remove) && strings.HasSuffix(event.Name, "_templ.go") {
97107
_, err = os.Stat(strings.TrimSuffix(event.Name, "_templ.go") + ".templ")
98108
if !os.IsNotExist(err) {
99-
return false, false, err
109+
return GenerateResult{}, err
100110
}
101111
// File is orphaned.
102112
if h.keepOrphanedFiles {
103-
return false, false, nil
113+
return GenerateResult{}, nil
104114
}
105115
h.Log.Debug("Deleting orphaned Go file", slog.String("file", event.Name))
106116
if err = os.Remove(event.Name); err != nil {
107117
h.Log.Warn("Failed to remove orphaned file", slog.Any("error", err))
108118
}
109-
return true, false, nil
119+
return GenerateResult{Updated: true, GoUpdated: true, TextUpdated: false}, nil
110120
}
111121
// Handle _templ.txt files.
112122
if !event.Has(fsnotify.Remove) && strings.HasSuffix(event.Name, "_templ.txt") {
113-
if h.DevMode {
114-
// Don't delete the file if we're in dev mode, but mark that text was updated.
115-
return false, true, nil
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
116127
}
117128
h.Log.Debug("Deleting watch mode file", slog.String("file", event.Name))
118129
if err = os.Remove(event.Name); err != nil {
119130
h.Log.Warn("Failed to remove watch mode text file", slog.Any("error", err))
120-
return false, false, nil
131+
return GenerateResult{}, nil
121132
}
122-
return false, false, nil
133+
return GenerateResult{}, nil
123134
}
124135

125136
// Handle .templ files.
126137
if !strings.HasSuffix(event.Name, ".templ") {
127-
return false, false, nil
138+
return GenerateResult{}, nil
128139
}
129140

130141
// If the file hasn't been updated since the last time we processed it, ignore it.
131142
lastModTime, updatedModTime := h.UpsertLastModTime(event.Name)
132143
if !updatedModTime {
133144
h.Log.Debug("Skipping file because it wasn't updated", slog.String("file", event.Name))
134-
return false, false, nil
145+
return GenerateResult{}, nil
135146
}
136147
// If the go file is newer than the templ file, skip generation, because it's up-to-date.
137148
if h.lazy && goFileIsUpToDate(event.Name, lastModTime) {
138149
h.Log.Debug("Skipping file because the Go file is up-to-date", slog.String("file", event.Name))
139-
return false, false, nil
150+
return GenerateResult{}, nil
140151
}
141152

142153
// Start a processor.
143154
start := time.Now()
144-
goUpdated, textUpdated, diag, err := h.generate(ctx, event.Name)
155+
var diag []parser.Diagnostic
156+
result, diag, err = h.generate(ctx, event.Name)
145157
if err != nil {
146158
h.SetError(event.Name, true)
147-
return goUpdated, textUpdated, fmt.Errorf("failed to generate code for %q: %w", event.Name, err)
159+
return result, fmt.Errorf("failed to generate code for %q: %w", event.Name, err)
148160
}
149161
if len(diag) > 0 {
150162
for _, d := range diag {
@@ -153,14 +165,14 @@ func (h *FSEventHandler) HandleEvent(ctx context.Context, event fsnotify.Event)
153165
slog.String("to", fmt.Sprintf("%d:%d", d.Range.To.Line, d.Range.To.Col)),
154166
)
155167
}
156-
return
168+
return result, nil
157169
}
158170
if errorCleared, errorCount := h.SetError(event.Name, false); errorCleared {
159171
h.Log.Info("Error cleared", slog.String("file", event.Name), slog.Int("errors", errorCount))
160172
}
161173
h.Log.Debug("Generated code", slog.String("file", event.Name), slog.Duration("in", time.Since(start)))
162174

163-
return goUpdated, textUpdated, nil
175+
return result, nil
164176
}
165177

166178
func goFileIsUpToDate(templFileName string, templFileLastMod time.Time) (upToDate bool) {
@@ -212,68 +224,78 @@ func (h *FSEventHandler) UpsertHash(fileName string, hash [sha256.Size]byte) (up
212224

213225
// generate Go code for a single template.
214226
// If a basePath is provided, the filename included in error messages is relative to it.
215-
func (h *FSEventHandler) generate(ctx context.Context, fileName string) (goUpdated, textUpdated bool, diagnostics []parser.Diagnostic, err error) {
227+
func (h *FSEventHandler) generate(ctx context.Context, fileName string) (result GenerateResult, diagnostics []parser.Diagnostic, err error) {
216228
t, err := parser.Parse(fileName)
217229
if err != nil {
218-
return false, false, nil, fmt.Errorf("%s parsing error: %w", fileName, err)
230+
return GenerateResult{}, nil, fmt.Errorf("%s parsing error: %w", fileName, err)
219231
}
220232
targetFileName := strings.TrimSuffix(fileName, ".templ") + "_templ.go"
221233

222234
// Only use relative filenames to the basepath for filenames in runtime error messages.
223235
absFilePath, err := filepath.Abs(fileName)
224236
if err != nil {
225-
return false, false, nil, fmt.Errorf("failed to get absolute path for %q: %w", fileName, err)
237+
return GenerateResult{}, nil, fmt.Errorf("failed to get absolute path for %q: %w", fileName, err)
226238
}
227239
relFilePath, err := filepath.Rel(h.dir, absFilePath)
228240
if err != nil {
229-
return false, false, nil, fmt.Errorf("failed to get relative path for %q: %w", fileName, err)
241+
return GenerateResult{}, nil, fmt.Errorf("failed to get relative path for %q: %w", fileName, err)
230242
}
231243
// Convert Windows file paths to Unix-style for consistency.
232244
relFilePath = filepath.ToSlash(relFilePath)
233245

234246
var b bytes.Buffer
235-
sourceMap, literals, err := generator.Generate(t, &b, append(h.genOpts, generator.WithFileName(relFilePath))...)
247+
generatorOutput, err := generator.Generate(t, &b, append(h.genOpts, generator.WithFileName(relFilePath))...)
236248
if err != nil {
237-
return false, false, nil, fmt.Errorf("%s generation error: %w", fileName, err)
249+
return GenerateResult{}, nil, fmt.Errorf("%s generation error: %w", fileName, err)
238250
}
239251

240252
formattedGoCode, err := format.Source(b.Bytes())
241253
if err != nil {
242-
err = remapErrorList(err, sourceMap, fileName)
243-
return false, false, nil, fmt.Errorf("% source formatting error %w", fileName, err)
254+
err = remapErrorList(err, generatorOutput.SourceMap, fileName)
255+
return GenerateResult{}, nil, fmt.Errorf("%s source formatting error %w", fileName, err)
244256
}
245257

246258
// Hash output, and write out the file if the goCodeHash has changed.
247259
goCodeHash := sha256.Sum256(formattedGoCode)
248260
if h.UpsertHash(targetFileName, goCodeHash) {
249-
goUpdated = true
261+
result.Updated = true
250262
if err = h.writer(targetFileName, formattedGoCode); err != nil {
251-
return false, false, nil, fmt.Errorf("failed to write target file %q: %w", targetFileName, err)
263+
return result, nil, fmt.Errorf("failed to write target file %q: %w", targetFileName, err)
252264
}
253265
}
254266

255267
// Add the txt file if it has changed.
256-
if len(literals) > 0 {
268+
if h.devMode {
257269
txtFileName := strings.TrimSuffix(fileName, ".templ") + "_templ.txt"
258-
txtHash := sha256.Sum256([]byte(literals))
270+
joined := strings.Join(generatorOutput.Literals, "\n")
271+
txtHash := sha256.Sum256([]byte(joined))
259272
if h.UpsertHash(txtFileName, txtHash) {
260-
textUpdated = true
261-
if err = os.WriteFile(txtFileName, []byte(literals), 0o644); err != nil {
262-
return false, false, nil, fmt.Errorf("failed to write string literal file %q: %w", txtFileName, err)
273+
result.TextUpdated = true
274+
if err = os.WriteFile(txtFileName, []byte(joined), 0o644); err != nil {
275+
return result, nil, fmt.Errorf("failed to write string literal file %q: %w", txtFileName, err)
276+
}
277+
278+
// Check whether the change would require a recompilation to take effect.
279+
h.fileNameToOutputMutex.Lock()
280+
defer h.fileNameToOutputMutex.Unlock()
281+
previous := h.fileNameToOutput[fileName]
282+
if generator.HasChanged(previous, generatorOutput) {
283+
result.GoUpdated = true
263284
}
285+
h.fileNameToOutput[fileName] = generatorOutput
264286
}
265287
}
266288

267289
parsedDiagnostics, err := parser.Diagnose(t)
268290
if err != nil {
269-
return goUpdated, textUpdated, nil, fmt.Errorf("%s diagnostics error: %w", fileName, err)
291+
return result, nil, fmt.Errorf("%s diagnostics error: %w", fileName, err)
270292
}
271293

272294
if h.genSourceMapVis {
273-
err = generateSourceMapVisualisation(ctx, fileName, targetFileName, sourceMap)
295+
err = generateSourceMapVisualisation(ctx, fileName, targetFileName, generatorOutput.SourceMap)
274296
}
275297

276-
return goUpdated, textUpdated, parsedDiagnostics, err
298+
return result, parsedDiagnostics, err
277299
}
278300

279301
// Takes an error from the formatter and attempts to convert the positions reported in the target file to their positions

cmd/templ/generatecmd/run/run_unix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func ignoreExited(err error) error {
5252
return err
5353
}
5454

55-
func Run(ctx context.Context, workingDir, input string) (cmd *exec.Cmd, err error) {
55+
func Run(ctx context.Context, workingDir string, input string) (cmd *exec.Cmd, err error) {
5656
m.Lock()
5757
defer m.Unlock()
5858
cmd, ok := running[input]

cmd/templ/generatecmd/run/run_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func Stop(cmd *exec.Cmd) (err error) {
3737
return kill.Run()
3838
}
3939

40-
func Run(ctx context.Context, workingDir, input string) (cmd *exec.Cmd, err error) {
40+
func Run(ctx context.Context, workingDir string, input string) (cmd *exec.Cmd, err error) {
4141
m.Lock()
4242
defer m.Unlock()
4343
cmd, ok := running[input]

0 commit comments

Comments
 (0)