Merge pull request #1001 from stffabi/feature/fsfs-assets

proposal: [v2] Support fs.FS for assets
This commit is contained in:
Lea Anthony
2021-12-07 06:03:59 +11:00
committed by GitHub
18 changed files with 241 additions and 180 deletions
+73 -12
View File
@@ -63,6 +63,7 @@ type devFlags struct {
compilerCommand string
assetDir string
extensions string
reloadDirs string
openBrowser bool
noReload bool
wailsjsdir string
@@ -83,8 +84,9 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
flags := defaultDevFlags()
command.StringFlag("ldflags", "optional ldflags", &flags.ldflags)
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &flags.compilerCommand)
command.StringFlag("assetdir", "Serve assets from the given directory", &flags.assetDir)
command.StringFlag("assetdir", "Serve assets from the given directory instead of using the provided asset FS", &flags.assetDir)
command.StringFlag("e", "Extensions to trigger rebuilds (comma separated) eg go", &flags.extensions)
command.StringFlag("reloaddirs", "Additional directories to trigger reloads (comma separated)", &flags.reloadDirs)
command.BoolFlag("browser", "Open application in browser", &flags.openBrowser)
command.BoolFlag("noreload", "Disable reload on asset change", &flags.noReload)
command.StringFlag("wailsjsdir", "Directory to generate the Wails JS modules", &flags.wailsjsdir)
@@ -301,10 +303,6 @@ func loadAndMergeProjectConfig(cwd string, flags *devFlags) (*project.Project, e
var shouldSaveConfig bool
if projectConfig.AssetDirectory == "" && flags.assetDir == "" {
return nil, fmt.Errorf("No asset directory provided. Please use -assetdir to indicate which directory contains your built assets.")
}
if flags.assetDir == "" && projectConfig.AssetDirectory != "" {
flags.assetDir = projectConfig.AssetDirectory
}
@@ -313,9 +311,19 @@ func loadAndMergeProjectConfig(cwd string, flags *devFlags) (*project.Project, e
projectConfig.AssetDirectory = filepath.ToSlash(flags.assetDir)
}
flags.assetDir, err = filepath.Abs(flags.assetDir)
if err != nil {
return nil, err
if flags.assetDir != "" {
flags.assetDir, err = filepath.Abs(flags.assetDir)
if err != nil {
return nil, err
}
}
if flags.reloadDirs == "" && projectConfig.ReloadDirectories != "" {
flags.reloadDirs = projectConfig.ReloadDirectories
}
if flags.reloadDirs != projectConfig.ReloadDirectories {
projectConfig.ReloadDirectories = filepath.ToSlash(flags.reloadDirs)
}
if flags.devServerURL == defaultDevServerURL && projectConfig.DevServerURL != defaultDevServerURL && projectConfig.DevServerURL != "" {
@@ -503,11 +511,26 @@ func doWatcherLoop(buildOptions *build.Options, debugBinaryProcess *process.Proc
newBinaryProcess *process.Process
)
var extensionsThatTriggerARebuild = sliceToMap(strings.Split(flags.extensions, ","))
var dirsThatTriggerAReload []string
for _, dir := range strings.Split(flags.reloadDirs, ",") {
if dir == "" {
continue
}
path, err := filepath.Abs(dir)
if err != nil {
LogRed("Unable to expand reloadDir '%s': %s", dir, err)
continue
}
dirsThatTriggerAReload = append(dirsThatTriggerAReload, path)
}
quit := false
interval := time.Duration(flags.debounceMS) * time.Millisecond
timer := time.NewTimer(interval)
rebuild := false
reload := false
assetDir := ""
changedPaths := map[string]struct{}{}
for quit == false {
//reload := false
select {
@@ -519,12 +542,13 @@ func doWatcherLoop(buildOptions *build.Options, debugBinaryProcess *process.Proc
// Check for file writes
if item.Op&fsnotify.Write == fsnotify.Write {
// Ignore directories
if fs.DirExists(item.Name) {
itemName := item.Name
if fs.DirExists(itemName) {
continue
}
// Iterate all file patterns
ext := filepath.Ext(item.Name)
ext := filepath.Ext(itemName)
if ext != "" {
ext = ext[1:]
if _, exists := extensionsThatTriggerARebuild[ext]; exists {
@@ -534,9 +558,17 @@ func doWatcherLoop(buildOptions *build.Options, debugBinaryProcess *process.Proc
}
}
if strings.HasPrefix(item.Name, flags.assetDir) {
reload = true
for _, reloadDir := range dirsThatTriggerAReload {
if strings.HasPrefix(itemName, reloadDir) {
reload = true
break
}
}
if !reload {
changedPaths[filepath.Dir(itemName)] = struct{}{}
}
timer.Reset(interval)
}
// Check for new directories
@@ -568,6 +600,35 @@ func doWatcherLoop(buildOptions *build.Options, debugBinaryProcess *process.Proc
debugBinaryProcess = newBinaryProcess
}
}
if len(changedPaths) != 0 {
if assetDir == "" {
resp, err := http.Get("http://localhost:34115/wails/assetdir")
if err != nil {
LogRed("Error during retrieving assetdir: %s", err.Error())
} else {
content, err := io.ReadAll(resp.Body)
if err != nil {
LogRed("Error reading assetdir from devserver: %s", err.Error())
} else {
assetDir = string(content)
}
resp.Body.Close()
}
}
if assetDir != "" {
for path := range changedPaths {
if strings.HasPrefix(path, assetDir) {
reload = true
break
}
}
} else if len(dirsThatTriggerAReload) == 0 {
LogRed("Reloading couldn't be triggered: Please specify -assetdir or -reloaddirs")
}
changedPaths = map[string]struct{}{}
}
if reload {
reload = false
_, err = http.Get("http://localhost:34115/wails/reload")
@@ -10,7 +10,6 @@ The next steps to complete the template are:
- It is really important to ensure `helpurl` is valid as this is where users of the template will be directed for help.
2. Update `README.md`.
3. Edit `wails.json` and ensure all fields are correct, especially:
- `assetdir` - path to your assets
- `wailsjsdir` - path to generate wailsjs modules
- `frontend:install` - The command to install your frontend dependencies
- `frontend:build` - The command to build your frontend
@@ -1,7 +1,6 @@
{
"name": "{{.ProjectName}}",
"outputfilename": "{{.BinaryName}}",
"assetdir": "frontend/dist",
"frontend:install": "npm install",
"frontend:build": "npm run build",
"author": {
@@ -36,7 +36,6 @@
<working_directory value="$PROJECT_DIR$"/>
<go_parameters value="-gcflags &quot;all=-N -l&quot; -tags dev -o {{.PathToDesktopBinary}}"/>
<useCustomBuildTags value="true"/>
<parameters value="-assetdir {{.AssetDir}}"/>
<envs>
<env name="CGO_ENABLED" value="&quot;{{.CGOEnabled}}&quot;"/>
</envs>
@@ -9,11 +9,7 @@
"program": "${workspaceFolder}/{{.PathToDesktopBinary}}",
"preLaunchTask": "build",
"cwd": "${workspaceFolder}",
"env": {},
"args": [
"-assetdir",
"{{.AssetDir}}"
]
"env": {}
}
]
}
@@ -42,7 +42,6 @@ type Data struct {
AuthorNameAndEmail string
WailsDirectory string
GoSDKPath string
AssetDir string
WindowsFlags string
CGOEnabled string
OutputFile string
@@ -60,7 +59,6 @@ type Options struct {
InitGit bool
AuthorName string
AuthorEmail string
AssetDir string
IDE string
ProjectNameFilename string // The project name but as a valid filename
WailsVersion string
@@ -261,7 +259,6 @@ func Install(options *Options) (bool, *Template, error) {
AuthorName: options.AuthorName,
WailsVersion: options.WailsVersion,
GoSDKPath: options.GoSDKPath,
AssetDir: options.AssetDir,
}
// Create a formatted name and email combo.
@@ -408,22 +405,6 @@ func installIDEFiles(o ideOptions) error {
binaryName += ".exe"
}
// Parse wails.json for assetdir
wailsJSONBytes, err := os.ReadFile(filepath.Join(o.options.TargetDir, "wails.json"))
if err != nil {
return err
}
var wailsJSON map[string]interface{}
err = json.Unmarshal(wailsJSONBytes, &wailsJSON)
if err != nil {
return err
}
assetDir := wailsJSON["assetdir"]
if assetDir == "" {
return fmt.Errorf("Unable to find 'assetdir' in 'wails.json' ")
}
o.options.AssetDir = assetDir.(string)
o.options.PathToDesktopBinary = filepath.ToSlash(filepath.Join("build", "bin", binaryName))
o.options.WindowsFlags = ""
@@ -1,7 +1,6 @@
{
"name": "{{.ProjectName}}",
"outputfilename": "{{.BinaryName}}",
"assetdir": "frontend/dist",
"frontend:install": "npm install",
"frontend:build": "npm run build",
"wailsjsdir": "./frontend",
@@ -1,7 +1,6 @@
{
"name": "{{.ProjectName}}",
"outputfilename": "{{.BinaryName}}",
"assetdir": "frontend/src",
"wailsjsdir": "./frontend",
"author": {
"name": "{{.AuthorName}}",
+53 -3
View File
@@ -5,7 +5,10 @@ package appng
import (
"context"
"embed"
"flag"
"fmt"
iofs "io/fs"
"os"
"path/filepath"
@@ -91,12 +94,30 @@ func CreateApp(appoptions *options.App) (*App, error) {
}
}
if assetdir == "" {
// If no assetdir has been defined, let's try to infer it from the project root and the asset FS.
assetdir, err = tryInferAssetDirFromFS(appoptions.Assets)
if err != nil {
return nil, err
}
}
if assetdir != "" {
// Let's override the assets to serve from on disk, if needed
absdir, err := filepath.Abs(assetdir)
if err != nil {
return nil, err
}
myLogger.Info("Serving assets from disk: %s", absdir)
appoptions.Assets = os.DirFS(absdir)
ctx = context.WithValue(ctx, "assetdir", assetdir)
}
if devServerURL != "" {
ctx = context.WithValue(ctx, "devserverurl", devServerURL)
}
if assetdir != "" {
ctx = context.WithValue(ctx, "assetdir", assetdir)
}
if loglevel != "" {
level, err := pkglogger.StringToLogLevel(loglevel)
@@ -203,3 +224,32 @@ func generateBindings(bindings *binding.Bindings) error {
return nil
}
func tryInferAssetDirFromFS(assets iofs.FS) (string, error) {
if _, isEmbedFs := assets.(embed.FS); !isEmbedFs {
// We only infer the assetdir for embed.FS assets
return "", nil
}
path, err := fs.FindPathToFile(assets, "index.html")
if err != nil {
return "", err
}
path, err = filepath.Abs(path)
if err != nil {
return "", err
}
if _, err := os.Stat(filepath.Join(path, "index.html")); err != nil {
if os.IsNotExist(err) {
err = fmt.Errorf(
"inferred assetdir '%s' does not exist or does not contain an 'index.html' file, "+
"please specify it with -assetdir or set it in wails.json",
path)
}
return "", err
}
return path, nil
}
@@ -5,10 +5,13 @@ package assetserver
import (
"bytes"
"context"
"io/fs"
"strings"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/internal/logger"
"golang.org/x/net/html"
"path/filepath"
)
/*
@@ -18,35 +21,41 @@ It injects a websocket based IPC script into `index.html`.
*/
import (
"os"
)
type BrowserAssetServer struct {
runtimeJS []byte
assetdir string
appOptions *options.App
assets fs.FS
runtimeJS []byte
logger *logger.Logger
}
func NewBrowserAssetServer(assetdir string, bindingsJSON string, appOptions *options.App) (*BrowserAssetServer, error) {
result := &BrowserAssetServer{
assetdir: assetdir,
appOptions: appOptions,
func NewBrowserAssetServer(ctx context.Context, assets fs.FS, bindingsJSON string) (*BrowserAssetServer, error) {
result := &BrowserAssetServer{}
_logger := ctx.Value("logger")
if _logger != nil {
result.logger = _logger.(*logger.Logger)
}
var err error
result.assets, err = prepareAssetsForServing(assets)
if err != nil {
return nil, err
}
var buffer bytes.Buffer
buffer.WriteString(`window.wailsbindings='` + bindingsJSON + `';` + "\n")
buffer.Write(runtime.RuntimeDesktopJS)
result.runtimeJS = buffer.Bytes()
return result, nil
}
func (a *BrowserAssetServer) loadFileFromDisk(filename string) ([]byte, error) {
return os.ReadFile(filepath.Join(a.assetdir, filename))
func (d *BrowserAssetServer) LogDebug(message string, args ...interface{}) {
if d.logger != nil {
d.logger.Debug("[BrowserAssetServer] "+message, args...)
}
}
func (a *BrowserAssetServer) processIndexHTML() ([]byte, error) {
indexHTML, err := a.loadFileFromDisk("index.html")
indexHTML, err := fs.ReadFile(a.assets, "index.html")
if err != nil {
return nil, err
}
@@ -96,7 +105,9 @@ func (a *BrowserAssetServer) Load(filename string) ([]byte, string, error) {
case "/wails/ipc.js":
content = runtime.WebsocketIPC
default:
content, err = a.loadFileFromDisk(filename)
filename = strings.TrimPrefix(filename, "/")
a.LogDebug("Loading file: %s", filename)
content, err = fs.ReadFile(a.assets, filename)
}
if err != nil {
return nil, "", err
@@ -0,0 +1,25 @@
package assetserver
import (
iofs "io/fs"
"path"
"github.com/wailsapp/wails/v2/internal/fs"
)
func prepareAssetsForServing(assets iofs.FS) (iofs.FS, error) {
if _, err := assets.Open("."); err != nil {
return nil, err
}
subDir, err := fs.FindPathToFile(assets, "index.html")
if err != nil {
return nil, err
}
assets, err = iofs.Sub(assets, path.Clean(subDir))
if err != nil {
return nil, err
}
return assets, nil
}
@@ -3,26 +3,21 @@ package assetserver
import (
"bytes"
"context"
"embed"
"fmt"
"github.com/leaanthony/debme"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
"io/fs"
"log"
"path/filepath"
"strings"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
)
type DesktopAssetServer struct {
assets debme.Debme
assets fs.FS
runtimeJS []byte
assetdir string
logger *logger.Logger
}
func NewDesktopAssetServer(ctx context.Context, assets embed.FS, bindingsJSON string) (*DesktopAssetServer, error) {
func NewDesktopAssetServer(ctx context.Context, assets fs.FS, bindingsJSON string) (*DesktopAssetServer, error) {
result := &DesktopAssetServer{}
_logger := ctx.Value("logger")
@@ -30,22 +25,18 @@ func NewDesktopAssetServer(ctx context.Context, assets embed.FS, bindingsJSON st
result.logger = _logger.(*logger.Logger)
}
_assetdir := ctx.Value("assetdir")
if _assetdir != nil {
result.assetdir = _assetdir.(string)
absdir, err := filepath.Abs(result.assetdir)
if err != nil {
return nil, err
}
result.LogDebug("Loading assets from: %s", absdir)
var err error
result.assets, err = prepareAssetsForServing(assets)
if err != nil {
return nil, err
}
var buffer bytes.Buffer
buffer.WriteString(`window.wailsbindings='` + bindingsJSON + `';` + "\n")
buffer.Write(runtime.RuntimeDesktopJS)
result.runtimeJS = buffer.Bytes()
err := result.init(assets)
return result, err
return result, nil
}
func (d *DesktopAssetServer) LogDebug(message string, args ...interface{}) {
@@ -54,63 +45,8 @@ func (d *DesktopAssetServer) LogDebug(message string, args ...interface{}) {
}
}
func (d *DesktopAssetServer) SetAssetDir(assetdir string) {
d.assetdir = assetdir
}
func PathToIndexHTML(assets embed.FS) (string, error) {
stat, err := fs.Stat(assets, "index.html")
if stat != nil {
return ".", nil
}
var indexFiles slicer.StringSlicer
err = fs.WalkDir(assets, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, "index.html") {
indexFiles.Add(path)
}
return nil
})
if err != nil {
return "", err
}
if indexFiles.Length() > 1 {
return "", fmt.Errorf("multiple 'index.html' files found in assets")
}
path, _ := filepath.Split(indexFiles.AsSlice()[0])
return path, nil
}
func processAssets(assets embed.FS) (debme.Debme, error) {
result, err := debme.FS(assets, ".")
if err != nil {
return result, err
}
// Find index.html
path, err := PathToIndexHTML(assets)
if err != nil {
return debme.Debme{}, err
}
return debme.FS(assets, path)
}
func (a *DesktopAssetServer) init(assets embed.FS) error {
var err error
a.assets, err = processAssets(assets)
if err != nil {
return err
}
return nil
}
func (a *DesktopAssetServer) processIndexHTML() ([]byte, error) {
indexHTML, err := a.ReadFile("index.html")
indexHTML, err := fs.ReadFile(a.assets, "index.html")
if err != nil {
return nil, err
}
@@ -146,7 +82,9 @@ func (a *DesktopAssetServer) Load(filename string) ([]byte, string, error) {
case "/wails/ipc.js":
content = runtime.DesktopIPC
default:
content, err = a.ReadFile(filename)
filename = strings.TrimPrefix(filename, "/")
a.LogDebug("Loading file: %s", filename)
content, err = fs.ReadFile(a.assets, filename)
}
if err != nil {
return nil, "", err
@@ -1,13 +0,0 @@
//go:build dev
package assetserver
import (
"os"
"path/filepath"
)
func (a *DesktopAssetServer) ReadFile(filename string) ([]byte, error) {
a.LogDebug("Loading file from disk: %s", filename)
return os.ReadFile(filepath.Join(a.assetdir, filename))
}
@@ -1,7 +0,0 @@
//go:build production
package assetserver
func (a *DesktopAssetServer) ReadFile(filename string) ([]byte, error) {
return a.assets.ReadFile(filename)
}
+12 -19
View File
@@ -10,7 +10,6 @@ import (
"fmt"
"io/fs"
"log"
"path/filepath"
"strings"
"sync"
"time"
@@ -50,6 +49,11 @@ func (d *DevWebServer) WindowReload() {
func (d *DevWebServer) Run(ctx context.Context) error {
d.ctx = ctx
assetdir, _ := ctx.Value("assetdir").(string)
d.server.Get("/wails/assetdir", func(fctx *fiber.Ctx) error {
return fctx.SendString(assetdir)
})
d.server.Get("/wails/reload", func(fctx *fiber.Ctx) error {
d.WindowReload()
d.desktopFrontend.WindowReload()
@@ -100,25 +104,14 @@ func (d *DevWebServer) Run(ctx context.Context) error {
_devServerURL := ctx.Value("devserverurl")
if _devServerURL == "http://localhost:34115" {
// Setup internal dev server
_assetdir := ctx.Value("assetdir")
if _assetdir == nil {
return fmt.Errorf("no assetdir provided")
bindingsJSON, err := d.appBindings.ToJSON()
if err != nil {
log.Fatal(err)
}
if _assetdir != nil {
assetdir := _assetdir.(string)
bindingsJSON, err := d.appBindings.ToJSON()
if err != nil {
log.Fatal(err)
}
d.assetServer, err = assetserver.NewBrowserAssetServer(assetdir, bindingsJSON, d.appoptions)
if err != nil {
log.Fatal(err)
}
absdir, err := filepath.Abs(assetdir)
if err != nil {
return err
}
d.LogDebug("Serving assets from: %s", absdir)
d.assetServer, err = assetserver.NewBrowserAssetServer(ctx, d.appoptions.Assets, bindingsJSON)
if err != nil {
log.Fatal(err)
}
d.server.Get("*", d.loadAsset)
+29
View File
@@ -4,9 +4,11 @@ import (
"crypto/md5"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"unsafe"
"github.com/leaanthony/slicer"
@@ -394,3 +396,30 @@ func MoveDirExtended(src string, dst string, ignore []string) (err error) {
return
}
func FindPathToFile(fsys fs.FS, file string) (string, error) {
stat, _ := fs.Stat(fsys, file)
if stat != nil {
return ".", nil
}
var indexFiles slicer.StringSlicer
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, file) {
indexFiles.Add(path)
}
return nil
})
if err != nil {
return "", err
}
if indexFiles.Length() > 1 {
return "", fmt.Errorf("multiple '%s' files found in assets", file)
}
path, _ := filepath.Split(indexFiles.AsSlice()[0])
return path, nil
}
+2
View File
@@ -15,6 +15,8 @@ type Project struct {
Name string `json:"name"`
AssetDirectory string `json:"assetdir"`
ReloadDirectories string `json:"reloaddirs"`
BuildCommand string `json:"frontend:build"`
InstallCommand string `json:"frontend:install"`
DevCommand string `json:"frontend:dev"`
+3 -3
View File
@@ -2,11 +2,11 @@ package options
import (
"context"
"embed"
"github.com/wailsapp/wails/v2/pkg/options/linux"
"io/fs"
"log"
"runtime"
"github.com/wailsapp/wails/v2/pkg/options/linux"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows"
@@ -41,7 +41,7 @@ type App struct {
HideWindowOnClose bool
AlwaysOnTop bool
RGBA *RGBA
Assets embed.FS
Assets fs.FS
Menu *menu.Menu
Logger logger.Logger `json:"-"`
LogLevel logger.LogLevel