go-app icon indicating copy to clipboard operation
go-app copied to clipboard

Support for //go: embed

Open andrewrynhard opened this issue 3 years ago • 8 comments

I would like to be able to deliver a single binary with all the required assets baked into it. The new embed package in go 1.16 could make this easy. I tried embedding app.wasm, but app wants to use a local directory.

andrewrynhard avatar Feb 18 '21 21:02 andrewrynhard

There is cool stuff here. I already see a couple of things that would make building the package easier. I'll definitely take a look at this and see how to take advantage of it!

maxence-charriere avatar Feb 19 '21 08:02 maxence-charriere

I was able to do this by implementing app.ResourceProvider and http.Handler, similar to app.LocalDir.

type resourcesFS struct {
	http.Handler
}

func newResourcesFS(fsys fs.FS) app.ResourceProvider {
	return resourcesFS{
		Handler: http.FileServer(http.FS(fsys)),
	}
}

func (resourcesFS) Package() string { return "" }
func (resourcesFS) Static() string  { return "" }
func (resourcesFS) AppWASM() string { return "/web/app.wasm" }

And then in two separate files (to avoid embedding resources again in app.wasm):

embedded_resources.go:

// +build !wasm,!js

package main

import (
	"embed"
)

//go:embed web
var web embed.FS

and

empty_resources.go:

// +build wasm,js

package main

import (
	"io/fs"
)

var web fs.FS

Then when setting up your app.Handler:

handler := &app.Handler{
	Name:      "App",
	// ...
	Resources:    newResourcesFS(web),
}

dabbertorres avatar Jul 06 '21 19:07 dabbertorres

I was having a play with this embedded solution and found that this modified approach only requires one file instead of two:

main.go

package main
// ...

var web fs.FS

type resourcesFS struct {
	http.Handler
}

func newResourcesFS(fsys fs.FS) app.ResourceProvider {
	return resourcesFS{
		Handler: http.FileServer(http.FS(fsys)),
	}
}

func (resourcesFS) Package() string { return "" }
func (resourcesFS) Static() string  { return "" }
func (resourcesFS) AppWASM() string { return "/web/app.wasm" }

And then only a single conditional embed.go for the server:

embed.go

// +build !wasm,!js

package main

import (
	"embed"
)

//go:embed web
var embeddedWeb embed.FS

func init() {
	web = embeddedWeb
}

justinfx avatar Aug 22 '21 03:08 justinfx

And one more variation that might even be better, since it allows for making the embedding a build constraint:

main.go

package main

//...

var (
	resources app.ResourceProvider
)

func main() {
    // ...
    handler := &app.Handler{
        // ...
		Resources: resources,
    }
}

embed.go Note the added "embed" build constraint

// +build !wasm,!js,embed

package main

import (
	"embed"
	"net/http"
)

//go:embed web
var embeddedWeb embed.FS

func init() {
	resources = resourcesFS{
		Handler: http.FileServer(http.FS(embeddedWeb)),
	}
}

type resourcesFS struct {
	http.Handler
}

func (resourcesFS) Package() string { return "" }
func (resourcesFS) Static() string  { return "" }
func (resourcesFS) AppWASM() string { return "/web/app.wasm" }
# without embedding
$ go build -o ./server .

# with embedding
$ go build -o ./server -tags embed .

justinfx avatar Aug 22 '21 04:08 justinfx

Will the new version support this?

ystyle avatar Oct 09 '21 12:10 ystyle

Any progress on this?

qmilangowin avatar Feb 10 '24 16:02 qmilangowin

As we deploy the apps with docker there is usually no need to embed the "web" folder for us, but I was interested in this anyway. I think it is fairly simple to embed the web folder for v9 (and v10) using:

import (
	"embed"
	"net/http"

	"github.com/maxence-charriere/go-app/v9/pkg/app"
)

//go:embed web
var web embed.FS

var _ app.ResourceResolver = (*embeddedResourceResolver)(nil)

func ResourceFS(web embed.FS) app.ResourceResolver {
	return embeddedResourceResolver{
		Handler: http.FileServer(http.FS(web)),
	}
}

type embeddedResourceResolver struct {
	http.Handler
}

func (r embeddedResourceResolver) Resolve(location string) string {
	if location == "" {
		return "/"
	}
	return location
}

func main() {
    // ...
    handler := &app.Handler{
        // ...
	Resources: ResourceFS(web),
    }
}

P.S.: You want to separate WASM and server builds to not include the backend code in the frontend. So this should go in the backend part.

oderwat avatar Feb 11 '24 11:02 oderwat

For another example, here's a project I created that embeds all static content ( including the wasm ) in the resulting server binary. It follows the best practice @oderwat mentioned to not include backend code in the wasm and also has some other useful features when starting a new go-app project.

https://github.com/mlctrez/goappnew

mlctrez avatar Feb 11 '24 21:02 mlctrez