@@ -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
166178func 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
0 commit comments