Skip to content

Commit

Permalink
feat: generate same dev and prod code, fixes #700 (#1027)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-h authored Dec 31, 2024
1 parent 6afd676 commit a66a237
Show file tree
Hide file tree
Showing 82 changed files with 872 additions and 626 deletions.
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.808
0.2.810
22 changes: 11 additions & 11 deletions benchmarks/templ/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 11 additions & 5 deletions cmd/templ/generatecmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log/slog"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -54,6 +55,7 @@ type Generate struct {

type GenerationEvent struct {
Event fsnotify.Event
Updated bool
GoUpdated bool
TextUpdated bool
}
Expand Down Expand Up @@ -114,7 +116,7 @@ func (cmd Generate) Run(ctx context.Context) (err error) {

// If we're processing a single file, don't bother setting up the channels/multithreaing.
if cmd.Args.FileName != "" {
_, _, err = fseh.HandleEvent(ctx, fsnotify.Event{
_, err = fseh.HandleEvent(ctx, fsnotify.Event{
Name: cmd.Args.FileName,
Op: fsnotify.Create,
})
Expand Down Expand Up @@ -219,15 +221,16 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
cmd.Log.Debug("Processing file", slog.String("file", event.Name))
defer eventsWG.Done()
defer func() { <-sem }()
goUpdated, textUpdated, err := fseh.HandleEvent(ctx, event)
r, err := fseh.HandleEvent(ctx, event)
if err != nil {
errs <- err
}
if goUpdated || textUpdated {
if r.GoUpdated || r.TextUpdated {
postGeneration <- &GenerationEvent{
Event: event,
GoUpdated: goUpdated,
TextUpdated: textUpdated,
Updated: r.Updated,
GoUpdated: r.GoUpdated,
TextUpdated: r.TextUpdated,
}
}
}(event)
Expand Down Expand Up @@ -273,6 +276,9 @@ func (cmd Generate) Run(ctx context.Context) (err error) {
postGenerationEventsWG.Add(1)
if cmd.Args.Command != "" && goUpdated {
cmd.Log.Debug("Executing command", slog.String("command", cmd.Args.Command))
if cmd.Args.Watch {
os.Setenv("TEMPL_DEV_MODE", "true")
}
if _, err := run.Run(ctx, cmd.Args.Path, cmd.Args.Command); err != nil {
cmd.Log.Error("Error executing command", slog.Any("error", err))
}
Expand Down
100 changes: 61 additions & 39 deletions cmd/templ/generatecmd/eventhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,17 @@ func NewFSEventHandler(
fileNameToLastModTimeMutex: &sync.Mutex{},
fileNameToError: make(map[string]struct{}),
fileNameToErrorMutex: &sync.Mutex{},
fileNameToOutput: make(map[string]generator.GeneratorOutput),
fileNameToOutputMutex: &sync.Mutex{},
devMode: devMode,
hashes: make(map[string][sha256.Size]byte),
hashesMutex: &sync.Mutex{},
genOpts: genOpts,
genSourceMapVis: genSourceMapVis,
DevMode: devMode,
keepOrphanedFiles: keepOrphanedFiles,
writer: fileWriter,
lazy: lazy,
}
if devMode {
fseh.genOpts = append(fseh.genOpts, generator.WithExtractStrings())
}
return fseh
}

Expand All @@ -80,71 +79,84 @@ type FSEventHandler struct {
fileNameToLastModTimeMutex *sync.Mutex
fileNameToError map[string]struct{}
fileNameToErrorMutex *sync.Mutex
fileNameToOutput map[string]generator.GeneratorOutput
fileNameToOutputMutex *sync.Mutex
devMode bool
hashes map[string][sha256.Size]byte
hashesMutex *sync.Mutex
genOpts []generator.GenerateOpt
genSourceMapVis bool
DevMode bool
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) {
type GenerateResult struct {
// Updated indicates that the file was updated.
Updated bool
// GoUpdated indicates that Go expressions were updated.
GoUpdated bool
// TextUpdated indicates that text literals were updated.
TextUpdated bool
}

func (h *FSEventHandler) HandleEvent(ctx context.Context, event fsnotify.Event) (result GenerateResult, err error) {
// Handle _templ.go files.
if !event.Has(fsnotify.Remove) && strings.HasSuffix(event.Name, "_templ.go") {
_, err = os.Stat(strings.TrimSuffix(event.Name, "_templ.go") + ".templ")
if !os.IsNotExist(err) {
return false, false, err
return GenerateResult{}, err
}
// File is orphaned.
if h.keepOrphanedFiles {
return false, false, nil
return GenerateResult{}, nil
}
h.Log.Debug("Deleting orphaned Go file", slog.String("file", event.Name))
if err = os.Remove(event.Name); err != nil {
h.Log.Warn("Failed to remove orphaned file", slog.Any("error", err))
}
return true, false, nil
return GenerateResult{Updated: true, GoUpdated: true, TextUpdated: false}, nil
}
// Handle _templ.txt files.
if !event.Has(fsnotify.Remove) && strings.HasSuffix(event.Name, "_templ.txt") {
if h.DevMode {
// Don't delete the file if we're in dev mode, but mark that text was updated.
return false, true, nil
if h.devMode {
// Don't delete the file in dev mode, ignore changes to it, since the .templ file
// must have been updated in order to trigger a change in the _templ.txt file.
return GenerateResult{Updated: false, GoUpdated: false, TextUpdated: false}, nil
}
h.Log.Debug("Deleting watch mode file", slog.String("file", event.Name))
if err = os.Remove(event.Name); err != nil {
h.Log.Warn("Failed to remove watch mode text file", slog.Any("error", err))
return false, false, nil
return GenerateResult{}, nil
}
return false, false, nil
return GenerateResult{}, nil
}

// Handle .templ files.
if !strings.HasSuffix(event.Name, ".templ") {
return false, false, nil
return GenerateResult{}, nil
}

// If the file hasn't been updated since the last time we processed it, ignore it.
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
return GenerateResult{}, 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
return GenerateResult{}, nil
}

// Start a processor.
start := time.Now()
goUpdated, textUpdated, diag, err := h.generate(ctx, event.Name)
var diag []parser.Diagnostic
result, diag, err = h.generate(ctx, event.Name)
if err != nil {
h.SetError(event.Name, true)
return goUpdated, textUpdated, fmt.Errorf("failed to generate code for %q: %w", event.Name, err)
return result, fmt.Errorf("failed to generate code for %q: %w", event.Name, err)
}
if len(diag) > 0 {
for _, d := range diag {
Expand All @@ -153,14 +165,14 @@ func (h *FSEventHandler) HandleEvent(ctx context.Context, event fsnotify.Event)
slog.String("to", fmt.Sprintf("%d:%d", d.Range.To.Line, d.Range.To.Col)),
)
}
return
return result, nil
}
if errorCleared, errorCount := h.SetError(event.Name, false); errorCleared {
h.Log.Info("Error cleared", slog.String("file", event.Name), slog.Int("errors", errorCount))
}
h.Log.Debug("Generated code", slog.String("file", event.Name), slog.Duration("in", time.Since(start)))

return goUpdated, textUpdated, nil
return result, nil
}

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

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

// Only use relative filenames to the basepath for filenames in runtime error messages.
absFilePath, err := filepath.Abs(fileName)
if err != nil {
return false, false, nil, fmt.Errorf("failed to get absolute path for %q: %w", fileName, err)
return GenerateResult{}, nil, fmt.Errorf("failed to get absolute path for %q: %w", fileName, err)
}
relFilePath, err := filepath.Rel(h.dir, absFilePath)
if err != nil {
return false, false, nil, fmt.Errorf("failed to get relative path for %q: %w", fileName, err)
return GenerateResult{}, nil, fmt.Errorf("failed to get relative path for %q: %w", fileName, err)
}
// Convert Windows file paths to Unix-style for consistency.
relFilePath = filepath.ToSlash(relFilePath)

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

formattedGoCode, err := format.Source(b.Bytes())
if err != nil {
err = remapErrorList(err, sourceMap, fileName)
return false, false, nil, fmt.Errorf("% source formatting error %w", fileName, err)
err = remapErrorList(err, generatorOutput.SourceMap, fileName)
return GenerateResult{}, nil, fmt.Errorf("%s source formatting error %w", fileName, err)
}

// Hash output, and write out the file if the goCodeHash has changed.
goCodeHash := sha256.Sum256(formattedGoCode)
if h.UpsertHash(targetFileName, goCodeHash) {
goUpdated = true
result.Updated = true
if err = h.writer(targetFileName, formattedGoCode); err != nil {
return false, false, nil, fmt.Errorf("failed to write target file %q: %w", targetFileName, err)
return result, nil, fmt.Errorf("failed to write target file %q: %w", targetFileName, err)
}
}

// Add the txt file if it has changed.
if len(literals) > 0 {
if h.devMode {
txtFileName := strings.TrimSuffix(fileName, ".templ") + "_templ.txt"
txtHash := sha256.Sum256([]byte(literals))
joined := strings.Join(generatorOutput.Literals, "\n")
txtHash := sha256.Sum256([]byte(joined))
if h.UpsertHash(txtFileName, txtHash) {
textUpdated = true
if err = os.WriteFile(txtFileName, []byte(literals), 0o644); err != nil {
return false, false, nil, fmt.Errorf("failed to write string literal file %q: %w", txtFileName, err)
result.TextUpdated = true
if err = os.WriteFile(txtFileName, []byte(joined), 0o644); err != nil {
return result, nil, fmt.Errorf("failed to write string literal file %q: %w", txtFileName, err)
}

// Check whether the change would require a recompilation to take effect.
h.fileNameToOutputMutex.Lock()
defer h.fileNameToOutputMutex.Unlock()
previous := h.fileNameToOutput[fileName]
if generator.HasChanged(previous, generatorOutput) {
result.GoUpdated = true
}
h.fileNameToOutput[fileName] = generatorOutput
}
}

parsedDiagnostics, err := parser.Diagnose(t)
if err != nil {
return goUpdated, textUpdated, nil, fmt.Errorf("%s diagnostics error: %w", fileName, err)
return result, nil, fmt.Errorf("%s diagnostics error: %w", fileName, err)
}

if h.genSourceMapVis {
err = generateSourceMapVisualisation(ctx, fileName, targetFileName, sourceMap)
err = generateSourceMapVisualisation(ctx, fileName, targetFileName, generatorOutput.SourceMap)
}

return goUpdated, textUpdated, parsedDiagnostics, err
return result, parsedDiagnostics, err
}

// Takes an error from the formatter and attempts to convert the positions reported in the target file to their positions
Expand Down
2 changes: 1 addition & 1 deletion cmd/templ/generatecmd/run/run_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func ignoreExited(err error) error {
return err
}

func Run(ctx context.Context, workingDir, input string) (cmd *exec.Cmd, err error) {
func Run(ctx context.Context, workingDir string, input string) (cmd *exec.Cmd, err error) {
m.Lock()
defer m.Unlock()
cmd, ok := running[input]
Expand Down
2 changes: 1 addition & 1 deletion cmd/templ/generatecmd/run/run_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func Stop(cmd *exec.Cmd) (err error) {
return kill.Run()
}

func Run(ctx context.Context, workingDir, input string) (cmd *exec.Cmd, err error) {
func Run(ctx context.Context, workingDir string, input string) (cmd *exec.Cmd, err error) {
m.Lock()
defer m.Unlock()
cmd, ok := running[input]
Expand Down
Loading

0 comments on commit a66a237

Please sign in to comment.