a-h / templ

A language for writing HTML user interfaces in Go.
https://templ.guide/
MIT License
7.86k stars 255 forks source link

Time stamp outputted #123

Closed gedw99 closed 10 months ago

gedw99 commented 1 year ago

I was wondering if we could output a time stamp in the generated code ?

Just as a comment at the top of the file. Sort of like Front matter for the templates.

the reason is because I have a use case where I have a ton of templates and I want to automate the creation of the compiled templates, and knowing when it was generated makes it easy for a file watcher to know if it needs to run go generate again.

the background is to do with providence . Say you have a template that is using other things. It could be other code or assets ( images or whatever ). If everything outputted has a time stamp and we know the providence graph then we can gen only when needed.

one way to do Tod is to use the “is-dirty” concept. This means that the thing is not the latest. The only way to know is to have a time stamp. Yes I know I could use the OS level but that is not useful in a distributed system when a file could come from somewhere else over a stream.

hopefully this is kind of interesting to others. Happy to discuss if your curious.

a-h commented 1 year ago

My initial thought would be that it would cause churn in git systems, because a timestamp introduces a change even if there's no meaningful change, and so I'm a little wary of adding something like that directly to the output files.

In distributed systems, you can't usually rely on the timestamps to be in sync due to clock skew and network processing latency.

So I thought that embedding a hash that's produced from the inputs to the template might be more useful.

That would provide a more consistent way to track it - i.e. you could verify that the output go code from templ generate was based on the current version of the templates. Although, maybe it would be just as fast to generate them again than calculate the hashes, making the exercise a bit pointless. 😁

This led me to think about what the inputs are that would require a templ generate to be executed. I think it might only be changes to the *.templ files themselves since...

If it is just changes to the templ files that make a templ generate required, then the rule is that if there's any *.templ file that's newer than the the oldest _templ.go file in a directory tree, then it's time to run go generate.

What do you think?

joerdav commented 1 year ago

I have the same concerns about git etc with putting time stamps in the template. PRs in code bases would be a mess, and things like the ensure-generated step in the templ pipeline wouldn't work.

The templ generate command wouldn't be a pure input output type thing.

Hashes on the other hand could make a lot of sense, you might need to intput the templ cli version into the hash too, as if you upgrade you would probably want it to regenerate.

a-h commented 12 months ago

As @joerdav points out, adding hashes and other information to the files really means encoding at least a hash of the version of templ in the generated files, which will cause git thrashing, so I don't think that adding this metadata to generated outputs is a good idea.

But... in terms of your use case. I recently implemented a filesystem walk to do hot reload in https://github.com/a-h/templ/commit/5e8644bdc5e309999eb5a5facb9cd74c6c3dcecb

It uses the last modified date from the local filesystem to determine whether to re-run the generation process, which you can get easily with https://pkg.go.dev/os#Stat

If you don't have the last modified date available in your use case because you pulled all the files from a repo or blob storage location and the last modified date on the filesystem doesn't reflect the last modified date in git, then you could use the last modified date from git or the blob store metadata.

I think that would do what you're trying to do without outputting any extra information into the generated code.

What do you think @gedw99?

gedw99 commented 12 months ago

Hash is perfect for me :)

I can then compare hashes as part of a cache miss .

if hash different then get the template from the origin server.

gedw99 commented 12 months ago

I was assuming it would be a hash of the template itself. A hash of the binary used to create the template is also useful for me too.

so if we can have both then lots of potential for reactive streaming architecture patterns are possible

gedw99 commented 12 months ago

It’s the same concept as how we have got hashes at Dev Tine / CI Time.

i just need a hash at run time.

At the end of the day it’s a type a providence system !! Git hash, templ hash etc.

Which is then applied to cache missing algo and hence reactive streams

a-h commented 10 months ago

Was thinking about this some more. It sounds like what you're after is a sort of Nix derivation.

In Nix, build processes are executed based on whether a set of inputs has changed.

So, { a, b, c } -> hash. If you change c, then { a, b, c^1 } -> updated_hash.

One interesting part of Nix is that you can also create a "binary cache" which stores the output of a build step based on its input hash.

For example, if I have a derivation in Nix that has { git, go1_21, templ_v0_2_300, some_go_files, some_templ_files } as inputs, I can get Nix to run go build for me and produce an output binary for a given architecture and operating system that is stored under the calculated hash of all the inputs.

If I store that in a binary cache like cachix, or a server that's accessible to me and trusted via SSH, and set up my fleet of machines to use it, the build will only happen once, and then the output will be re-used across multiple machines.

Here's an example Nix flake which uses templ to generate outputs based on the input.

If you add this to git and run nix build, it will create a result directory containing templates.txt.

{
  description = "templ";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
    templ.url = "github:a-h/templ";
  };

  outputs = { self, nixpkgs, templ }:
    let
      # Systems supported
      allSystems = [
        "x86_64-linux" # 64-bit Intel/AMD Linux
        "aarch64-linux" # 64-bit ARM Linux
        "x86_64-darwin" # 64-bit Intel macOS
        "aarch64-darwin" # 64-bit ARM macOS
      ];

      # Helper to provide system-specific attributes
      forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
        pkgs = import nixpkgs { inherit system; };
        system = system;
      });

      # This is a function with two params, pkgs and system.
      # If you run nix build you'll see a result directory appear.
      # Inside this, you'll get the build output of this step.
      buildTemplates = { pkgs, system }: pkgs.stdenv.mkDerivation {
        name = "templates";
        src = ./.;
        nativeBuildInputs = [
          templ.outputs.packages.${system}.default
        ];
        buildPhase = ''
          templ generate
          mkdir -p $out
          cp . -r $out
        '';
      };

      buildApp = { pkgs, system }:
        let
          # Call the buildTemplates function, and use the result as an input this function.
          templates = (buildTemplates { pkgs = pkgs; system = system; });
        in
        pkgs.stdenv.mkDerivation {
          name = "app";
          src = ./.;
          nativeBuildInputs = [
            templates
          ];
          buildPhase = ''
            mkdir -p $out
            ls -l ${templates}> $out/templates.txt
          '';
        };
    in
    {
      packages = forAllSystems ({ pkgs, system }: {
        # Call the buildApp function, passing the pkgs and system.
        default = buildApp {
          pkgs = pkgs;
          system = system;
        };
      });
    };
}
a-h commented 10 months ago

Ah, I forgot to get to the point.

I think that since tools like Nix exist that can create a hash based on a set of inputs, and decide whether a fresh build is required, it's not worth including anything special for it in templ.

I think the pain of git churn for generated files makes it not worthwhile as an addition, compared to the short duration of regenerating templates.

So, I think that the best outcome here is for the version number and generated date to be optionally included in generated files, with the default setting that it's disabled.

a-h commented 10 months ago

It's now possible to include a timestamp in the generated output by using:

templ generate -include-timestamp=true
0 0 /Users/adrian/github.com/a-h/templ % templ generate --help
  -cmd string
        Set the command to run after generating code.
  -f string
        Optionally generates code for a single file, e.g. -f header.templ
  -help
        Print help and exit.
  -include-timestamp
        Set to true to include the current time in the generated code.
  -include-version
        Set to false to skip inclusion of the templ version in the generated code. (default true)
  -path string
        Generates code for all files in path. (default ".")
  -pprof int
        Port to start pprof web server on.
  -proxy string
        Set the URL to proxy after generating code and executing the command.
  -proxyport int
        The port the proxy will listen on. (default 7331)
  -sourceMapVisualisations
        Set to true to generate HTML files to visualise the templ code and its corresponding Go code.
  -w int
        Number of workers to run in parallel. (default 10)
  -watch
        Set to true to watch the path for changes and regenerate code.