olivere / vite

Vite backend integration for Go
Other
27 stars 5 forks source link

Minimal Integration Option #8

Closed ge3224 closed 1 month ago

ge3224 commented 2 months ago

Add Support for Registering Pre-Parsed Templates

Description

Currently, only template strings can be integrated with the library because RegisterTemplate accepts string arguments for templates and handles its own parsing.

// RegisterTemplate adds a new template to the handler's template collection.
// The 'name' parameter should match the URL path where the template will be used.
// Use "index.html" for the root URL ("/").
//
// Parameters:
//   - name: String identifier for the template, corresponding to its URL path
//   - text: String content of the template
//
// Panics if a template with the given name is already registered.
func (h *Handler) RegisterTemplate(name, text string) {
  if h.templates == nil {
    h.templates = make(map[string]*template.Template)
  }
  if _, ok := h.templates[name]; ok {
    panic(fmt.Sprintf("vite: template %q already registered", name))
  }
  h.templates[name] = template.Must(template.New(name).Parse(text))
}

This limitation poses an issue when a Go backend is responsible for its own template parsing. For instance, an application might compose templates with partials to facilitate the reuse of common elements, such as a navigation bar.

There is no way to register these pre-parsed templates with the Vite handler.

Proposed Solution

To address this limitation while maintaining backwards compatibility, I propose adding a new method that allows the registration of pre-parsed templates. This method provides additional flexibility for applications that handle their own template parsing without disrupting existing functionality that relies on RegisterTemplate.

// RegisterParsedTemplate adds a new template to the handler's template collection.
// The 'name' parameter should match the URL path where the template will be used.
// Use "index.html" for the root URL ("/").
//
// Parameters:
//   - name: String identifier for the template, corresponding to its URL path
//   - tmpl: A pre-parsed *template.Template instance to be registered.
//
// Panics if a template with the given name is already registered.
func (h *Handler) RegisterParsedTemplate(name string, tmpl *template.Template) {
  if h.templates == nil {
    h.templates = make(map[string]*template.Template)
  }
  if _, ok := h.templates[name]; ok {
    panic(fmt.Sprintf("vite: template %q already registered", name))
  }
  h.templates[name] = tmpl
}

This following example demonstrates how the method might be used to integrate embedded templates with the Vite handler while leveraging the new RegisterParsedTemplate method:


//go:embed all:templates
var templates embed.FS

func main() {
  // Create a new Vite handler
  viteHandler, err := vite.NewHandler(vite.Config{
    FS:      os.DirFS("."),
    IsDev:   true,
    ViteURL: "http://localhost:5173",
  })
  if err != nil {
    panic(err)
  }

  // Create and parse the templates
  tmpl := template.New("")
  tmpl, err = tmpl.ParseFS(
    templates,
    "templates/header.html",
    "templates/footer.html",
    "templates/layout.html",
    "templates/index.html",
  )
  if err != nil {
    panic(err)
  }

  // Register the pre-parsed template with the Vite handler
  viteHandler.RegisterParsedTemplate("index.html", tmpl)

  // Further code to start the server, etc.
}

Conclusion

This change would boost the Vite handler’s flexibility and support different ways of handling templates. Looking forward to hearing your thoughts—thanks for considering it!

olivere commented 2 months ago

I'm not against it per-se, but I'm thinking about responsibilities and flexibility here. Maybe we're pushing too much into the library.

To pick up the thought I had in the other issue...

What if vite.Handler would only be responsible for injecting what's necessary to return valid HTML, and the application would be free to use/inject it however it likes? What if vite.Handler could be used more like middleware instead of being the central point your app is built around?

To make it more concrete, I'm thinking of application code that uses it something like this:

func Frontend(next http.Handler) http.Handler {
  // Setup the vite.Handler (middleware) here
  v := vite.NewHandler(...)
  // or
  v := vite.Middleware(...)

  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Check if v can handle the requested asset by consulting the manifest.
    // If so, return early.
    ...

    // Switch case based on r.URL.Path and e.g. initialize a Go template.
    ...

    // Consult v about the necessary things to inject into the template,
    // like the <script>...</script> and <link>...</link> in the HTML head.
    // Inject into the Go template and render.
    ...

    next.ServeHTTP(w, r)
  })
}

In your app, with, e.g., gorilla/mux router, you'd use it something like this ...

r := mux.NewRouter()
r.Use(Frontend)
...

By doing so, this library would not have any opinions on how your app renders HTML, which router it uses, and what else you want to do within your templating. It would simply handle the Vite backend part and help you add the required HTML snippets to your HTML templating logic.

I haven't thought this through in every detail. So think of it as a thought experiment only. Maybe I've overlooked dependencies between the app and the library. Happy to get feedback about it.

ge3224 commented 2 months ago

I really like this idea! The middleware-style approach seems promising and could lead to a cleaner separation of concerns.

How would this affect the developer experience for simpler use cases? Would there be a way to provide both this flexible approach and a simpler "batteries-included" option for those who prefer it?

While this might involve some breaking changes, the benefits in flexibility could be worth it.

Overall, I'm excited about the potential of this direction and would be happy to help out if needed.

What do you think of something like this?

// Custom ResponseWriter to capture the output
type customResponseWriter struct {
    http.ResponseWriter
    body []byte
}

func (crw *customResponseWriter) Write(b []byte) (int, error) {
    crw.body = append(crw.body, b...)
    return len(b), nil
}

// Helper function to inject a block into the HTML
func insertViteBlock(content []byte, placeholder, injection string) string {
    return strings.Replace(string(content), placeholder, injection, 1)
}

func (h *Handler) Middleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        crw := &customResponseWriter{ResponseWriter: w}

        next.ServeHTTP(crw, r)

        viteBlock := `
    {{- if .IsDev }}
        {{ .PluginReactPreamble }}
        <script type="module" src="{{ .ViteURL }}/@vite/client"></script>
        {{- if ne .ViteEntry "" }}
            <script type="module" src="{{ .ViteURL }}/{{ .ViteEntry }}"></script>
        {{- else }}
            <script type="module" src="{{ .ViteURL }}/src/main.tsx"></script>
        {{- end }}
    {{- else }}
        {{- if .StyleSheets }}
        {{ .StyleSheets }}
        {{- end }}
        {{- if .Modules }}
        {{ .Modules }}
        {{- end }}
        {{- if .PreloadModules }}
        {{ .PreloadModules }}
        {{- end }}
    {{- end }}
        `

        tmpl, err := template.New("vite").Parse(viteBlock)
        if err != nil {
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }

        page := pageData{
            IsDev:     h.isDev,
            ViteEntry: h.viteEntry,
            ViteURL:   h.viteURL,
        }
        // Handle both development and production modes.
        if h.isDev {
            page.PluginReactPreamble = template.HTML(PluginReactPreamble(h.viteURL))
        } else {
            var chunk *Chunk
            if page.ViteEntry == "" {
                chunk = h.manifest.GetEntryPoint()
            } else {
                entries := h.manifest.GetEntryPoints()
                for _, entry := range entries {
                    if page.ViteEntry == entry.Src {
                        chunk = entry
                        break
                    }
                }
                if chunk == nil {
                    http.Error(w, "Internal server error", http.StatusInternalServerError)
                    return
                }
            }
            page.StyleSheets = template.HTML(h.manifest.GenerateCSS(chunk.Src))
            page.Modules = template.HTML(h.manifest.GenerateModules(chunk.Src))
            page.PreloadModules = template.HTML(h.manifest.GeneratePreloadModules(chunk.Src))
        }

        var block strings.Builder
        err = tmpl.Execute(&block, page)
        if err != nil {
            http.Error(w, "Internal server error", http.StatusInternalServerError)
        }

        content := insertViteBlock(crw.body, "<meta name=\"vite\">", block.String())

        w.Write([]byte(content))
    }
}
ge3224 commented 2 months ago

The snippet provided above feels a bit ad hoc, so I wanted to share the progress I've made in implementing this concept more systematically. I've decoupled vite.Middleware from vite.Handler, so that it could be used as an alternative option.

// This block demonstrates the setup and usage of `vite.Middleware` in a Go
// web application.
//
// The Middleware is then applied to the root route ("/") of an HTTP mux,
// where it processes incoming requests before rendering a "index.tmpl"
// template.
//
// This setup allows a minimal integration of Vite with the Go web server,
// handling both development and production environments.

viteMiddleware, err := vite.NewMiddleware(vite.Config{
    FS:      viteFS,
    IsDev:   *isDev,
    ViteURL: "http://localhost:5173",
})
if err != nil {
    panic(err)
}

mux.HandleFunc("/", viteMiddleware.Use(func(w http.ResponseWriter, r *http.Request) {
  tmpl, err := template.New("index.tmpl").ParseFS(goIndex, "index.tmpl")

  if err != nil {
      panic(err)
  }

  if err = tmpl.Execute(w, nil); err != nil {
      panic(err)
  }
}))

I've created a pull request that demonstrates this approach. The PR includes:

  1. An implementation of vite.Middleware, decoupled from vite.Handler
  2. An example use case showing how it can be integrated into a Go web application
  3. Updated documentation reflecting these changes

You can find the PR here: Insert PR link

I'd love to get your thoughts on this implementation. Does this align with what you had in mind? Are there any areas you think could be improved or refined further?

olivere commented 2 months ago

Thank you so much. The issue is the better forum to discuss this, I agree (I shared my first thoughts on the PR).

My first gut feeling is that with your approach, the viteMiddleware is responsible for the rendering. This means that the library controls the process, not the application. My thought was that the application is responsible for calling viteMiddleware at the right time (or inject it into their middleware chain), and ask the middleware for the things to inject.

But don't worry, your idea makes sense. I have to test and feel it from the application developer's perspective. I'm busy during the weeks currently, so it might take some time to complete for me. Thank you very much for pushing this forward though.

danclaytondev commented 2 months ago

Hi, I have been working out how this library works to integrate vite into my golang app using Inertia.js. I have not set it up so far because I wanted to render my HTML separately rather than let this library handle templates. I very much agree with what you have said @olivere:

By doing so, this library would not have any opinions on how your app renders HTML, which router it uses, and what else you want to do within your templating. It would simply handle the Vite backend part and help you add the required HTML snippets to your HTML templating logic.

Personally I would prefer to use this library as a helper function, so I can write my own template, and call a function to generate my HTML elements for vite such as:

# in root.html
<head>
  {{ .vite }}
</head>

Perhaps this function could call similar code that @ge3224 has written above but rather than injecting it with the response writer, we can just generate it as a string and let the user of this library render the HTML themselves. I think it would be nice to offer both this level of flexibility and a 'batteries-included' approach you are talking about. The most complex part of using Vite that would stop me from implementing it manually is generating the HTML tags from the Vite manifest.json, which this library can already do well but is hidden inside the other templating functionality.

For me the use case with using Inertia.js, is that I only have 1 HTML template, so it is not difficult to call a function for the vite block, and I would rather avoid as much complexity as possible.

I am happy to contribute with a PR - thanks for all your work so far :)

olivere commented 2 months ago

Hi @danclaytondev. Nice to have you on board.

Your approach is what I had in mind. I think the library should be told how to work (production/development environment etc.) and then act like a library. When asked, return the specifics of what should be injected into your templating library of choice. Something like this:

type PageData struct {
  Scripts template.HTML
  ...
}

The app calls this library when rendering HTML:

func Index(w http.ResponseWriter, r *http.Request) {
  vite, err := v.Page(... <entrypoint> and other options ...)
  if err != nil { ... }

  // Now you can inject the "vite" into e.g. the Go HTML templating
  data := struct {
    Title string
    Vite  *vite.PageData
  }{
    Title: "My homepage",
    Vite:  vite,
  }
  err = tmpl.Execute(w, data)
  if err != nil { ... }
}

var tmpl = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{{.Title}}</title>
                {{ .Vite.Scripts }}                       <-- injecting the Vite script data here
    </head>
    <body>
          ...
    </body>
</html>`))

The app is then free to use the html/template package from Go standard library, or use something funny like the templ templating library. It's up to the application developer.

Of course, we should also handle the part that serves the Vite assets, probably best in the form of a simple file server that can be added to the router of the app.

I need something like this for an app I'm building myself next. But it may take me another few days to start. So, if you have the time and feel confident to approach this, feel free to do so.

EDIT 1: Just changed <endpoint> to <entrypoint>. EDIT 2: Comment on the file server behaviour to serve Vite assets.

danclaytondev commented 2 months ago

I've started a basic implementation, I will post a draft PR once it is a little better and then we can discuss :)

ge3224 commented 2 months ago

I really liked @danclaytondev's idea for a helper function approach. In fact, I was so excited about it that I went ahead and implemented it into a project I'm working on today.

In my enthusiasm, I also created a PR implementing this helper function feature. I apologize for submitting this PR before @danclaytondev had a chance to contribute their implementation.

@danclaytondev, I'm more than happy to step back if you'd like to take the lead on this feature. Alternatively, if you're open to it, perhaps we could collaborate on refining the implementation. I'm genuinely excited about this approach and would love to see it integrated into the library in the best possible way.

@olivere, I welcome any feedback or suggestions on the implementation. The PR is open, and I'm more than willing to make adjustments based on the community's input.

olivere commented 2 months ago

Hey! Thanks @danclaytondev and @ge3224. Yes, that's a good starter. That helper injects the necessary fragment into the application-provided HTML template.

Then we're just missing a http.Handler or http.HandlerFunc that handles the requests made by the injected fragment(s), i.e. renders the Vite-generated assets in production, right? In its current state, the vite.Handler does way more than that (renders templates etc.), which it doesn't necessarily need to do. In production, it should e.g. only consult the .vite/manifest.json and render those assets or 404.

I like the approach.

danclaytondev commented 2 months ago

Hi @ge3224, no need to apologize at all, it's great to see this progress! It was worth me starting a solution to learn a bit more about Vite, and the code I wrote is very similar to how you have set it up. I hope that means its a good solution :) I have a few minor comments that I will add to the PR later, but what you have done is great.

@olivere, what do you think is the best way to serve the files for production? I think we want to be unopinionated about what setup they are using (just http, or gin, echo etc). There is the FileServerFS that we are using already, but Echo has its own static file handler.

For production mode, if the library user needs to add the http.HandlerFunc that we could write for them, wouldn't it be just as easy for them to setup production file serving? I think it could be as easy as one line. In the readme for this package, we could document how to set it up for each of the common golang web frameworks, so they can copy and paste their own in, and we well document what URL and file structure to use. We could have examples like we do now. Obviously our helper function is already generating the url, so we need to make sure that it is easy to get those links to work, but they also might want to customise that URL already, or even use a CDN and host the files differently.

Finally, shall we setup a feature branch so we can contribute to this before it all goes into main. I can write some tests.

olivere commented 2 months ago

@danclaytondev Regarding your questions...

I think we should always rely on the standard library and not provide anything specific to a Go web framework. A fs.FS (that can be converted to http.FileSystem with http.FS(...)) would probably be best, or a http.Handler / http.HandlerFunc that simply does the right thing. More on that later.

What I do today is to embed the generated React app into my Go binary (with embed). I love Go for the ability to simply push around a single executable (configured via environment variables). I'd obviously love to still have that ability with this library. So if we could base something off a fs.FS or http.Handler(Func)—that'd be wonderful.

So I think we have to solve two problems in both development and production mode:

  1. Finding out what to inject and provide a way to the app to inject it into their HTML template of choice.
  2. Serving the Vite assets.

Development mode

In development mode, we don't have to do anything for step 2 as the Vite dev server would do the serving for us.

And step 1 is as simple as returning the preamble to the application.

Vite docs describe this in step 2.

Production mode

In production mode, it's a bit more complex.

I think step 2 could be solved by providing a fs.FS that renders the Vite asset (if it is a Vite asset). But we need to continue if it's not (more on that later).

Step 1 is harder: Here we need the "relative src path from project root" (as stated in the Vite docs in step 3) to know what to render. With that key, we can consult the .vite/manifest.json and render everything necessary. But we need to know the key, and the app is the only one who knows e.g. which entry point to serve.

My implementation (or should I say: interpretation) of that can be found in the manifest.go file in GenerateCSS etc.. So, given an entry point, we can inspect manifest.json to find out which CSS and scripts to inject into the HTML template.

Problem

Now, this is where I think I'm overcomplicating things and maybe you two could help me prove that and find a simple solution ;-)

When working with more complex React apps, you typically have something like a client-side router (e.g. React Router). Those routers typically want you to set up a routing table like /customers -> CustomersPage.tsx etc. The problem with that is that that /customers endpoint is not known to this library, and might not even be known to the Go backend as well.

What my Go backends typically do is to delegate to the React app when the request doesn't match any of its registered handlers.

Here's an example:

  1. /api/customers -> Must be handled by the Go backend. Stop when matching this endpoint. Otherwise...
  2. /assets/01.chunk.css -> Must be handled by e.g a fs.FS provided by this library. Otherwise...
  3. ...
  4. Fallback -> Must be handled by the Go backend. Render some HTML template that injects the right preamble/scripts/links into head (with the help of this library).

I wonder if this library can handle step 2 and still continue all the way to the fallback should the request not actually be a Vite asset. How can we do that with a simple fs.FS? It must continue when the asset is not found. That's why I initially thought of a HTTP middleware: Those are chains of HTTP handlers, and in step 2 you could just stop calling the next HTTP handler, and stop the chain.

What am I missing? Did I explain it in a understandable way? Is there even a problem? Please prove me wrong.

ge3224 commented 2 months ago

I've modified the example in the PR to more clearly demonstrate how static files, vite-managed assets, and Vite build files for prod could be served using the Go standard library. (See snippet below.)

The example demonstrates three distinct handlers for related requests:

  1. /static/<file>:

    • Serves as the public asset directory for Vite
    • Also used by Go to serve non-Vite static assets
  2. /assets/<file>:

    • Dedicated to serving Vite-managed assets
  3. /:

    • Root handler that responds with a Go template
    • Integrates both HTML structure and the Vite application

Admittedly, the example may be verbose, but it serves to illustrate how Vite can be integrated into a larger Go application. This approach could easily be adapted to different use cases and other libraries like Gorilla or Echo.

Additionally, the PR as it stands preserves the existing vite.Handler, maintaining a 'batteries-included' option for developers primarily focused on frontend work.

//go:embed all:static
var static embed.FS // embedding static files as @olivere mentions

// ....

mux := http.NewServeMux() // This is the HTTP multiplexer in the standard library, not Gorilla Mux

// Serve assets that Vite would normally treat as 'public' assets.
//
// In this example, the 'static' directory is used as a replacement for
// Vite's default 'public' folder. (The 'publicDir' property has been set to `false`
// in vite.config.ts.) We're using the 'static' directory to achieve similar
// functionality, but available to the Go backend and Vite.
//
// To use a static asset in our Vite frontend application, we import it in JS like this:
//
// ```js
// import viteLogo from '/static/vite.svg' // (instead of '/vite.svg')
// ```

var staticFileServer http.Handler
if *isDev {
  staticFileServer = http.FileServer(http.Dir("static"))
} else {
  staticFS, err := fs.Sub(static, "static")
  if err != nil {
      panic(err)
  }
  staticFileServer = http.FileServer(http.FS(staticFS))
}

mux.Handle("/static/", http.StripPrefix("/static/", staticFileServer))

// Serve Vite-managed assets from the Go backend, accommodating both
// development and production environments.
//
// Usage in Vite remains the same as in a standard Vite setup. The Go backend
// will serve the assets from the correct location based on the environment.

var viteAssetsFileServer http.Handler
var viteAssetsSrcAttribute string // vite changes this path, depending on dev or prod
if *isDev {
  viteAssetsFileServer = http.FileServer(http.Dir("src/assets"))
  viteAssetsSrcAttribute = "/src/assets/"
} else {
  viteAssetsFS, err := fs.Sub(static, "static/dist/assets")
  if err != nil {
      panic(err)
  }
  viteAssetsFileServer = http.FileServer(http.FS(viteAssetsFS))
  viteAssetsSrcAttribute = "/assets/"
}

mux.Handle(viteAssetsSrcAttribute, http.StripPrefix(viteAssetsSrcAttribute, viteAssetsFileServer))

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  // The filesystem used to access the build manifest.
  var viteFS fs.FS
  if *isDev {
    viteFS = os.DirFS(".")
  } else {
    fs, err := fs.Sub(static, "static/dist")
    if err != nil {
      panic(err)
    }
    viteFS = fs
  }

  // viteFragment generates the necessary HTML for Vite integration.
  //
  // It calls vite.HTMLFragment with a Config struct to create an HTML fragment
  // that includes all required Vite assets and scripts. This fragment is of type 
  // `template.HTML` and is intended to be applied as data, replacing a `{{ .Vite }}` 
  // placeholder in the <head> section of the primary handler's executed HTML template.
  viteFragment, err := vite.HTMLFragment(vite.Config{
    FS:      viteFS,
    IsDev:   *isDev,
    ViteURL: "http://localhost:5173",
  })
  if err != nil {
    panic(err)
  }

  tmpl, err := template.New("index.tmpl").ParseFS(goIndex, "index.tmpl")

  if err != nil {
    panic(err)
  }

  if err = tmpl.Execute(w, map[string]interface{}{
    "Vite": viteFragment,
  }); err != nil {
    panic(err)
  }
})
olivere commented 2 months ago

@ge3224 Thanks for the examples. I find it very helpful to have code to look at; for me, it's the best documentation I can find.