Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

a dev / watch mode that doesn't modify _templ.go files #700

Open
joefitzgerald opened this issue Apr 24, 2024 · 14 comments
Open

a dev / watch mode that doesn't modify _templ.go files #700

joefitzgerald opened this issue Apr 24, 2024 · 14 comments
Labels
cmd enhancement New feature or request NeedsDecision Issue needs some more discussion so a decision can be made

Comments

@joefitzgerald
Copy link

I love using dev mode; it's a super productive feature! Thanks for adding it 馃槃

The feature could be improved by ensuring that dev mode does not cause a dirty working copy for files that have not been modified. I find myself perilously close to accidentally committing modified <file>_templ.go files, where the diff is basically:

-               _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<the contents of the rendered template here>")
+               templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1)

I get why it is written this way, but it would be absolutely wonderful if I could run dev mode, make changes to my .templ files, and be able to commit modified _templ.go files without fear of committing the wrong thing.

This would suggest an alternate mechanism for determining whether the go application is in dev mode at runtime. A few ideas of the top of my head:

  • It could be an alternate mode requiring the use of an environment variable (TEMPL_DEV_MODE)
  • It could use detection of the _templ.txt files, and automatically load them if they are present (perhaps paired with an environment variable to avoid this work when running a production application)
  • Other options?
@joefitzgerald
Copy link
Author

In the meantime, I have added a pre-commit hook to my repo, which prevents you from committing without exiting watch mode:

.git/hooks/pre-commit (don't forget to make the file executable: chmod u+x .git/hooks/pre-commit)

#!/bin/bash

check_for_templ_watch_mode() {
    # Get the output of 'git diff --cached' command
    diff_output=$(git diff --cached)

    # Check if the specified string is present in the diff output
    if [[ "$diff_output" =~ "templ.WriteWatchModeString" ]]; then
    	cat <<\EOF
Error: It looks like you're running templ in watch mode.

The string 'templ.WriteWatchModeString' is present in the commit.

Exit templ watch mode to proceed.
EOF
        exit 1  # Reject the commit
    fi
}

# Call the function to check the diff
check_for_templ_watch_mode

# If the check passes, allow the commit
exit 0

Example usage:

$ git commit -m "update template"
Error: It looks like you're running templ in watch mode.

The string 'templ.WriteWatchModeString' is present in the commit.

Exit templ watch mode to proceed.

@a-h
Copy link
Owner

a-h commented Apr 30, 2024

Thanks, I've been thinking about this, and I also came up with the TEMPL_DEV_MODE idea - that templ generate --watch should generate the same code for dev mode and prod mode, with the environment variable used to enable the local file check behaviour.

That should stop any thrashing of local files (and corresponding git status annoyance) until you exit dev mode, and the compiled files are updated.

Should be fairly easy to implement, but would need to check it doesn't harm runtime performance too badly, since it would involve a number of additional code branches.

@joefitzgerald
Copy link
Author

If the environment variable were only checked once (perhaps via func init() { }) per execution of the application, I would imagine the performance impact would be negligible.

I would be open to making a PR. Can you point me to areas in the code base that I should look through? I'm unfamiliar with it, so I want to avoid missing a key part of the codebase.

@joerdav
Copy link
Collaborator

joerdav commented Apr 30, 2024

I don't think that the performance impact would come from the reading of the config. Currently in prod builds the "raw html" bits of the code are generated as string literals, and in the dev mode it reads from a txt file.

We will need to create a way for generated files to do both depending on the mode, this'll likely involve some function calls and allocations (maybe).

@joerdav
Copy link
Collaborator

joerdav commented Apr 30, 2024

An example of how this could work is using the new std lib cmp.Or function:

// generated code
templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(cmp.Or(templ.WatchModeString(1), "<div class=\"border border-gray-100 rounded shadow p-2\">"))
if templ_7745c5c3_Err != nil {
	return templ_7745c5c3_Err
}

Where templ.WatchModeString is a func(int) string

@joefitzgerald
Copy link
Author

joefitzgerald commented Apr 30, 2024

Given you're generating code, you could avoid a function call in production by adding a (cheap) conditional check based on a global variable.

e.g.

// somewhere deep inside templ
var DevMode bool
func init() {
    DevMode = strings.TrimSpace(os.Getenv("TEMPL_DEV_MODE")) != ""
}

// somewhere in a _templ.go file
func MyView() templ.Component {
    ...
    if (templ.DevMode) {
        templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1)
    } else {
        _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<the production stuff>")
    }
}

@a-h
Copy link
Owner

a-h commented May 6, 2024

From a quick benchmark (https://github.com/a-h/function-vs-variable-vs-if)

goos: darwin
goarch: arm64
pkg: function-vs-variable-vs-if
BenchmarkPlain-10               366917121                3.112 ns/op
BenchmarkPlainVar-10            227294608                5.283 ns/op
BenchmarkPlainConst-10          386069434                3.105 ns/op
BenchmarkIfDev-10               90382713                12.13 ns/op
BenchmarkIfProd-10              100000000               10.54 ns/op
BenchmarkVarDev-10              202231369                5.901 ns/op
BenchmarkVarProd-10             227322704                5.296 ns/op
BenchmarkOrDev-10               203148361                5.903 ns/op
BenchmarkOrProd-10              227268368                5.288 ns/op
PASS

Basically, this is the fastest dev mode variant:

func Var(w io.Writer, devMode bool) {
	s := "prod"
	if devMode {
		s = "dev"
	}
	w.Write([]byte(s))
}

The cost is roughly an additional 2ns per string write operation. There's no difference on the function call because it likely gets inlined due to the small function size.

So there is a genuine (albeit small) cost to the idea of universal generation for dev and prod mode.

@joerdav joerdav added enhancement New feature or request cmd NeedsDecision Issue needs some more discussion so a decision can be made labels May 14, 2024
@a-h
Copy link
Owner

a-h commented May 15, 2024

New idea. Get each file to contain a list of strings constants (at the end of each file), each file would create its own variable name for the strings.

// Slice behaviour.

var strings = [][]byte{
	[]byte("prod"),
}

// Load the values from the dev file.
func UpdateStrings() {
	strings[0] = []byte("dev")
}

func Slice(w io.Writer) {
	w.Write(strings[0])
}

Switching to dev mode is then just a case of loading the string values from disk. The generated files are then only changed if the content changes.

Performance is on par with the existing behaviour:

1 146 /Users/adrian/github.com/a-h/function-vs-variable-vs-if % go test -bench .
goos: darwin
goarch: arm64
pkg: function-vs-variable-vs-if
BenchmarkPlain-10               371899047                3.108 ns/op
BenchmarkPlainVar-10            227073768                5.282 ns/op
BenchmarkPlainConst-10          385540412                3.109 ns/op
BenchmarkIfDev-10               96177604                12.26 ns/op
BenchmarkIfProd-10              100000000               10.56 ns/op
BenchmarkVarDev-10              203252289                5.910 ns/op
BenchmarkVarProd-10             227222448                5.282 ns/op
BenchmarkOrDev-10               203265229                5.906 ns/op
BenchmarkOrProd-10              226484836                5.286 ns/op
BenchmarkSlice-10               383917892                3.114 ns/op
PASS
ok      function-vs-variable-vs-if      15.682s

@joerdav
Copy link
Collaborator

joerdav commented May 15, 2024

That's a good shout. In this design how would a call to UpdateStrings be done, I'm struggling to visualize it?

@bastianwegge
Copy link
Contributor

Is it okay to vote for removing the "watch" mode here? I don't think this is a responsibility that templ should have and setting it up should just be plumbing a couple of already existing technologies together.

I created a little example (https://github.com/bastianwegge/go-templ-reload) we use with a small dev-team on a daily basis and everybody (so far) seems happy with it. This example uses go's native net/http to create a server and uses air and taskfile for updating when necessary. I'd be happy to create a PR for this example.

@joerdav
Copy link
Collaborator

joerdav commented May 20, 2024

@bastianwegge I think there is a place for that if it suits you. But the watch mode within templ allows for a much faster feedback loop, it prevents the need for a recompile of the project on each change, so you get sub second responses.

@joerdav
Copy link
Collaborator

joerdav commented May 20, 2024

"Sub second" probably isn't doing it justice 馃槀

@bastianwegge
Copy link
Contributor

@joerdav I think I get what the watch mode is supposed to do, but I don't get why this functionality is something that resides within templ. If you're not building a component library, you're probably using a server framework like echo/gin/fiber or native http, I haven't found templ's watch mode particularly useful for that purpose. That's just my experience and I'd love to hear why or how templ watch mode can improve the process here.

If the answer is "it's just another way" then I'd argue that we'd be better of providing a simple example and removing the unnecessary JS / Proxy code from templs codebase. Less is more!

@joerdav
Copy link
Collaborator

joerdav commented May 29, 2024

As of now it's not just another way. The watch mode doesn't just watch for changes and recompile, it also stores some of the templates separate to the go binary, so that as you edit they can be updated without recompiling your application, this definitely wouldn't be possible with an external tool, as it involves templ to keep track of these template parts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cmd enhancement New feature or request NeedsDecision Issue needs some more discussion so a decision can be made
Projects
None yet
Development

No branches or pull requests

4 participants