From 65c2618c1695e50bc9186db1f81ca796c102e169 Mon Sep 17 00:00:00 2001 From: Aerek Yasa <88772708+Aerek-Yasa@users.noreply.github.com> Date: Sun, 18 Aug 2024 13:31:44 +0200 Subject: [PATCH] feat: add support for lazy generation (#874) Co-authored-by: Adrian Hesketh --- cmd/templ/generatecmd/cmd.go | 2 ++ cmd/templ/generatecmd/eventhandler.go | 35 ++++++++++++++----- cmd/templ/generatecmd/main.go | 1 + .../test-eventhandler/eventhandler_test.go | 2 +- cmd/templ/main.go | 4 +++ .../02-template-generation.md | 2 ++ docs/docs/09-commands-and-tools/01-cli.md | 2 ++ 7 files changed, 39 insertions(+), 9 deletions(-) diff --git a/cmd/templ/generatecmd/cmd.go b/cmd/templ/generatecmd/cmd.go index dbbd2ccbe..4419dc9a9 100644 --- a/cmd/templ/generatecmd/cmd.go +++ b/cmd/templ/generatecmd/cmd.go @@ -98,6 +98,7 @@ func (cmd Generate) Run(ctx context.Context) (err error) { cmd.Args.GenerateSourceMapVisualisations, cmd.Args.KeepOrphanedFiles, cmd.Args.FileWriter, + cmd.Args.Lazy, ) // If we're processing a single file, don't bother setting up the channels/multithreaing. @@ -183,6 +184,7 @@ func (cmd Generate) Run(ctx context.Context) (err error) { cmd.Args.GenerateSourceMapVisualisations, cmd.Args.KeepOrphanedFiles, cmd.Args.FileWriter, + cmd.Args.Lazy, ) errorCount.Store(0) if err := watcher.WalkFiles(ctx, cmd.Args.Path, events); err != nil { diff --git a/cmd/templ/generatecmd/eventhandler.go b/cmd/templ/generatecmd/eventhandler.go index 945989acc..619af13c9 100644 --- a/cmd/templ/generatecmd/eventhandler.go +++ b/cmd/templ/generatecmd/eventhandler.go @@ -45,6 +45,7 @@ func NewFSEventHandler( genSourceMapVis bool, keepOrphanedFiles bool, fileWriter FileWriterFunc, + lazy bool, ) *FSEventHandler { if !path.IsAbs(dir) { dir, _ = filepath.Abs(dir) @@ -63,6 +64,7 @@ func NewFSEventHandler( DevMode: devMode, keepOrphanedFiles: keepOrphanedFiles, writer: fileWriter, + lazy: lazy, } if devMode { fseh.genOpts = append(fseh.genOpts, generator.WithExtractStrings()) @@ -86,6 +88,7 @@ type FSEventHandler struct { Errors []error keepOrphanedFiles bool writer func(string, []byte) error + lazy bool } func (h *FSEventHandler) HandleEvent(ctx context.Context, event fsnotify.Event) (goUpdated, textUpdated bool, err error) { @@ -125,10 +128,16 @@ func (h *FSEventHandler) HandleEvent(ctx context.Context, event fsnotify.Event) } // If the file hasn't been updated since the last time we processed it, ignore it. - if !h.UpsertLastModTime(event.Name) { + lastModTime, updatedModTime := h.UpsertLastModTime(event.Name) + if !updatedModTime { h.Log.Debug("Skipping file because it wasn't updated", slog.String("file", event.Name)) return false, false, nil } + // If the go file is newer than the templ file, skip generation, because it's up-to-date. + if h.lazy && goFileIsUpToDate(event.Name, lastModTime) { + h.Log.Debug("Skipping file because the Go file is up-to-date", slog.String("file", event.Name)) + return false, false, nil + } // Start a processor. start := time.Now() @@ -159,6 +168,15 @@ func (h *FSEventHandler) HandleEvent(ctx context.Context, event fsnotify.Event) return goUpdated, textUpdated, nil } +func goFileIsUpToDate(templFileName string, templFileLastMod time.Time) (upToDate bool) { + goFileName := strings.TrimSuffix(templFileName, ".templ") + "_templ.go" + goFileInfo, err := os.Stat(goFileName) + if err != nil { + return false + } + return goFileInfo.ModTime().After(templFileLastMod) +} + func (h *FSEventHandler) SetError(fileName string, hasError bool) (previouslyHadError bool, errorCount int) { h.fileNameToErrorMutex.Lock() defer h.fileNameToErrorMutex.Unlock() @@ -170,19 +188,20 @@ func (h *FSEventHandler) SetError(fileName string, hasError bool) (previouslyHad return previouslyHadError, len(h.fileNameToError) } -func (h *FSEventHandler) UpsertLastModTime(fileName string) (updated bool) { +func (h *FSEventHandler) UpsertLastModTime(fileName string) (modTime time.Time, updated bool) { fileInfo, err := os.Stat(fileName) if err != nil { - return false + return modTime, false } h.fileNameToLastModTimeMutex.Lock() defer h.fileNameToLastModTimeMutex.Unlock() - lastModTime := h.fileNameToLastModTime[fileName] - if !fileInfo.ModTime().After(lastModTime) { - return false + previousModTime := h.fileNameToLastModTime[fileName] + currentModTime := fileInfo.ModTime() + if !currentModTime.After(previousModTime) { + return currentModTime, false } - h.fileNameToLastModTime[fileName] = fileInfo.ModTime() - return true + h.fileNameToLastModTime[fileName] = currentModTime + return currentModTime, true } func (h *FSEventHandler) UpsertHash(fileName string, hash [sha256.Size]byte) (updated bool) { diff --git a/cmd/templ/generatecmd/main.go b/cmd/templ/generatecmd/main.go index d72121790..538f820a4 100644 --- a/cmd/templ/generatecmd/main.go +++ b/cmd/templ/generatecmd/main.go @@ -26,6 +26,7 @@ type Arguments struct { // PPROFPort is the port to run the pprof server on. PPROFPort int KeepOrphanedFiles bool + Lazy bool } func Run(ctx context.Context, log *slog.Logger, args Arguments) (err error) { diff --git a/cmd/templ/generatecmd/test-eventhandler/eventhandler_test.go b/cmd/templ/generatecmd/test-eventhandler/eventhandler_test.go index 2cc39a540..5e3e2d335 100644 --- a/cmd/templ/generatecmd/test-eventhandler/eventhandler_test.go +++ b/cmd/templ/generatecmd/test-eventhandler/eventhandler_test.go @@ -43,7 +43,7 @@ func TestErrorLocationMapping(t *testing.T) { slog := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) var fw generatecmd.FileWriterFunc - fseh := generatecmd.NewFSEventHandler(slog, ".", false, []generator.GenerateOpt{}, false, false, fw) + fseh := generatecmd.NewFSEventHandler(slog, ".", false, []generator.GenerateOpt{}, false, false, fw, false) for _, test := range tests { // The raw files cannot end in .templ because they will cause the generator to fail. Instead, // we create a tmp file that ends in .templ only for the duration of the test. diff --git a/cmd/templ/main.go b/cmd/templ/main.go index ce6ad10ad..7df4fd4bd 100644 --- a/cmd/templ/main.go +++ b/cmd/templ/main.go @@ -169,6 +169,8 @@ Args: If present, the command will issue a reload event to the proxy 127.0.0.1:7331, or use proxyport and proxybind to specify a different address. -w Number of workers to use when generating code. (default runtime.NumCPUs) + -lazy + Only generate .go files if the source .templ file is newer. -pprof Port to run the pprof server on. -keep-orphaned-files @@ -215,6 +217,7 @@ func generateCmd(stdout, stderr io.Writer, args []string) (code int) { keepOrphanedFilesFlag := cmd.Bool("keep-orphaned-files", false, "") verboseFlag := cmd.Bool("v", false, "") logLevelFlag := cmd.String("log-level", "info", "") + lazyFlag := cmd.Bool("lazy", false, "") helpFlag := cmd.Bool("help", false, "") err := cmd.Parse(args) if err != nil { @@ -259,6 +262,7 @@ func generateCmd(stdout, stderr io.Writer, args []string) (code int) { IncludeTimestamp: *includeTimestampFlag, PPROFPort: *pprofPortFlag, KeepOrphanedFiles: *keepOrphanedFilesFlag, + Lazy: *lazyFlag, }) if err != nil { color.New(color.FgRed).Fprint(stderr, "(✗) ") diff --git a/docs/docs/04-core-concepts/02-template-generation.md b/docs/docs/04-core-concepts/02-template-generation.md index f2b24eb36..b3f88f2f0 100644 --- a/docs/docs/04-core-concepts/02-template-generation.md +++ b/docs/docs/04-core-concepts/02-template-generation.md @@ -60,6 +60,8 @@ Args: If present, the command will issue a reload event to the proxy 127.0.0.1:7331, or use proxyport and proxybind to specify a different address. -w Number of workers to use when generating code. (default runtime.NumCPUs) + -lazy + Only generate .go files if the source .templ file is newer. -pprof Port to run the pprof server on. -keep-orphaned-files diff --git a/docs/docs/09-commands-and-tools/01-cli.md b/docs/docs/09-commands-and-tools/01-cli.md index 6d74fd5cd..9d1992834 100644 --- a/docs/docs/09-commands-and-tools/01-cli.md +++ b/docs/docs/09-commands-and-tools/01-cli.md @@ -51,6 +51,8 @@ Args: The address the proxy will listen on. (default 127.0.0.1) -w Number of workers to use when generating code. (default runtime.NumCPUs) + -lazy + Only generate .go files if the source .templ file is newer. -pprof Port to run the pprof server on. -keep-orphaned-files