feat(build): add Wails frontend prebuild hook
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
7f1da7766a
commit
1265dc158b
2 changed files with 249 additions and 0 deletions
|
|
@ -66,6 +66,10 @@ func (b *WailsBuilder) Build(ctx context.Context, cfg *build.Config, targets []b
|
|||
}
|
||||
|
||||
// Wails v2 strategy: Use 'wails build'
|
||||
if err := b.PreBuild(ctx, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
if err := cfg.FS.EnsureDir(cfg.OutputDir); err != nil {
|
||||
return nil, coreerr.E("WailsBuilder.Build", "failed to create output directory", err)
|
||||
|
|
@ -86,6 +90,30 @@ func (b *WailsBuilder) Build(ctx context.Context, cfg *build.Config, targets []b
|
|||
return artifacts, nil
|
||||
}
|
||||
|
||||
// PreBuild runs the frontend build step before Wails compiles the desktop app.
|
||||
//
|
||||
// err := b.PreBuild(ctx, cfg) // runs `deno task build` or `npm run build`
|
||||
func (b *WailsBuilder) PreBuild(ctx context.Context, cfg *build.Config) error {
|
||||
if cfg == nil {
|
||||
return coreerr.E("WailsBuilder.PreBuild", "config is nil", nil)
|
||||
}
|
||||
|
||||
frontendDir, command, args, err := b.resolveFrontendBuild(cfg.FS, cfg.ProjectDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if command == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
output, err := ax.CombinedOutput(ctx, frontendDir, nil, command, args...)
|
||||
if err != nil {
|
||||
return coreerr.E("WailsBuilder.PreBuild", command+" build failed: "+output, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isWailsV3 checks if the project uses Wails v3 by inspecting go.mod.
|
||||
func (b *WailsBuilder) isWailsV3(fs io.Medium, dir string) bool {
|
||||
goModPath := ax.Join(dir, "go.mod")
|
||||
|
|
@ -96,6 +124,53 @@ func (b *WailsBuilder) isWailsV3(fs io.Medium, dir string) bool {
|
|||
return core.Contains(content, "github.com/wailsapp/wails/v3")
|
||||
}
|
||||
|
||||
// resolveFrontendBuild selects the frontend directory and build command.
|
||||
//
|
||||
// dir, command, args, err := b.resolveFrontendBuild(io.Local, ".")
|
||||
func (b *WailsBuilder) resolveFrontendBuild(fs io.Medium, projectDir string) (string, string, []string, error) {
|
||||
frontendDir := b.resolveFrontendDir(fs, projectDir)
|
||||
if frontendDir == "" {
|
||||
return "", "", nil, nil
|
||||
}
|
||||
|
||||
if b.hasDenoConfig(fs, frontendDir) {
|
||||
command, err := b.resolveDenoCli()
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
return frontendDir, command, []string{"task", "build"}, nil
|
||||
}
|
||||
|
||||
if fs.IsFile(ax.Join(frontendDir, "package.json")) {
|
||||
command, err := b.resolveNpmCli()
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
return frontendDir, command, []string{"run", "build"}, nil
|
||||
}
|
||||
|
||||
return "", "", nil, nil
|
||||
}
|
||||
|
||||
// resolveFrontendDir returns the directory that contains the frontend build manifest.
|
||||
func (b *WailsBuilder) resolveFrontendDir(fs io.Medium, projectDir string) string {
|
||||
frontendDir := ax.Join(projectDir, "frontend")
|
||||
if fs.IsDir(frontendDir) && (b.hasDenoConfig(fs, frontendDir) || fs.IsFile(ax.Join(frontendDir, "package.json"))) {
|
||||
return frontendDir
|
||||
}
|
||||
|
||||
if b.hasDenoConfig(fs, projectDir) || fs.IsFile(ax.Join(projectDir, "package.json")) {
|
||||
return projectDir
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// hasDenoConfig reports whether the frontend directory contains a Deno manifest.
|
||||
func (b *WailsBuilder) hasDenoConfig(fs io.Medium, dir string) bool {
|
||||
return fs.IsFile(ax.Join(dir, "deno.json")) || fs.IsFile(ax.Join(dir, "deno.jsonc"))
|
||||
}
|
||||
|
||||
// buildV2Target compiles for a single target platform using wails (v2).
|
||||
func (b *WailsBuilder) buildV2Target(ctx context.Context, cfg *build.Config, target build.Target) (build.Artifact, error) {
|
||||
wailsCommand, err := b.resolveWailsCli()
|
||||
|
|
@ -258,6 +333,40 @@ func (b *WailsBuilder) resolveWailsCli(paths ...string) (string, error) {
|
|||
return command, nil
|
||||
}
|
||||
|
||||
// resolveDenoCli returns the executable path for the deno CLI.
|
||||
func (b *WailsBuilder) resolveDenoCli(paths ...string) (string, error) {
|
||||
if len(paths) == 0 {
|
||||
paths = []string{
|
||||
"/usr/local/bin/deno",
|
||||
"/opt/homebrew/bin/deno",
|
||||
}
|
||||
}
|
||||
|
||||
command, err := ax.ResolveCommand("deno", paths...)
|
||||
if err != nil {
|
||||
return "", coreerr.E("WailsBuilder.resolveDenoCli", "deno CLI not found. Install it from https://deno.com/runtime", err)
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
|
||||
// resolveNpmCli returns the executable path for the npm CLI.
|
||||
func (b *WailsBuilder) resolveNpmCli(paths ...string) (string, error) {
|
||||
if len(paths) == 0 {
|
||||
paths = []string{
|
||||
"/usr/local/bin/npm",
|
||||
"/opt/homebrew/bin/npm",
|
||||
}
|
||||
}
|
||||
|
||||
command, err := ax.ResolveCommand("npm", paths...)
|
||||
if err != nil {
|
||||
return "", coreerr.E("WailsBuilder.resolveNpmCli", "npm CLI not found. Install Node.js from https://nodejs.org/", err)
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
|
||||
// detectPackageManager detects the frontend package manager based on lock files.
|
||||
// Returns "bun", "pnpm", "yarn", or "npm" (default).
|
||||
func detectPackageManager(fs io.Medium, dir string) string {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,12 @@ if [ -n "$log_file" ]; then
|
|||
printf '%s\n' "$@" > "$log_file"
|
||||
fi
|
||||
|
||||
sequence_file="${BUILD_SEQUENCE_FILE:-}"
|
||||
if [ -n "$sequence_file" ]; then
|
||||
printf '%s\n' "wails" >> "$sequence_file"
|
||||
printf '%s\n' "$@" >> "$sequence_file"
|
||||
fi
|
||||
|
||||
output_dir="build/bin"
|
||||
binary_name="testapp"
|
||||
mkdir -p "$output_dir"
|
||||
|
|
@ -103,6 +109,22 @@ chmod +x "$output_dir/$binary_name"
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func setupFakeFrontendCommand(t *testing.T, binDir, name string) {
|
||||
t.Helper()
|
||||
|
||||
script := strings.ReplaceAll(`#!/bin/sh
|
||||
set -eu
|
||||
|
||||
sequence_file="${BUILD_SEQUENCE_FILE:-}"
|
||||
if [ -n "$sequence_file" ]; then
|
||||
printf '%s\n' "__NAME__" >> "$sequence_file"
|
||||
printf '%s\n' "$@" >> "$sequence_file"
|
||||
fi
|
||||
`, "__NAME__", name)
|
||||
|
||||
require.NoError(t, ax.WriteFile(ax.Join(binDir, name), []byte(script), 0o755))
|
||||
}
|
||||
|
||||
func TestWails_WailsBuilderBuildTaskfile_Good(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
|
|
@ -227,6 +249,124 @@ func TestWails_WailsBuilderBuildV2Flags_Good(t *testing.T) {
|
|||
assert.Contains(t, args, "embed")
|
||||
}
|
||||
|
||||
func TestWails_WailsBuilderPreBuild_Good(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
t.Run("uses deno when deno manifest exists", func(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
setupFakeFrontendCommand(t, binDir, "deno")
|
||||
setupFakeFrontendCommand(t, binDir, "npm")
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
projectDir := setupWailsTestProject(t)
|
||||
frontendDir := ax.Join(projectDir, "frontend")
|
||||
require.NoError(t, ax.MkdirAll(frontendDir, 0o755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte(`{}`), 0o644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte(`{}`), 0o644))
|
||||
|
||||
logPath := ax.Join(t.TempDir(), "frontend.log")
|
||||
t.Setenv("BUILD_SEQUENCE_FILE", logPath)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
}
|
||||
|
||||
require.NoError(t, builder.PreBuild(context.Background(), cfg))
|
||||
|
||||
content, err := ax.ReadFile(logPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
|
||||
require.Len(t, lines, 3)
|
||||
assert.Equal(t, "deno", lines[0])
|
||||
assert.Equal(t, "task", lines[1])
|
||||
assert.Equal(t, "build", lines[2])
|
||||
})
|
||||
|
||||
t.Run("falls back to npm when only package.json exists", func(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
setupFakeFrontendCommand(t, binDir, "deno")
|
||||
setupFakeFrontendCommand(t, binDir, "npm")
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
projectDir := setupWailsTestProject(t)
|
||||
require.NoError(t, ax.WriteFile(ax.Join(projectDir, "package.json"), []byte(`{}`), 0o644))
|
||||
|
||||
logPath := ax.Join(t.TempDir(), "frontend.log")
|
||||
t.Setenv("BUILD_SEQUENCE_FILE", logPath)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
}
|
||||
|
||||
require.NoError(t, builder.PreBuild(context.Background(), cfg))
|
||||
|
||||
content, err := ax.ReadFile(logPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
|
||||
require.Len(t, lines, 3)
|
||||
assert.Equal(t, "npm", lines[0])
|
||||
assert.Equal(t, "run", lines[1])
|
||||
assert.Equal(t, "build", lines[2])
|
||||
})
|
||||
}
|
||||
|
||||
func TestWails_WailsBuilderBuildV2PreBuild_Good(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
binDir := t.TempDir()
|
||||
setupFakeFrontendCommand(t, binDir, "deno")
|
||||
setupFakeFrontendCommand(t, binDir, "npm")
|
||||
setupFakeWailsToolchain(t, binDir)
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
projectDir := setupWailsV2TestProject(t)
|
||||
frontendDir := ax.Join(projectDir, "frontend")
|
||||
require.NoError(t, ax.MkdirAll(frontendDir, 0o755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte(`{}`), 0o644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte(`{}`), 0o644))
|
||||
|
||||
outputDir := t.TempDir()
|
||||
sequencePath := ax.Join(t.TempDir(), "build-sequence.log")
|
||||
wailsLogPath := ax.Join(t.TempDir(), "wails.log")
|
||||
t.Setenv("BUILD_SEQUENCE_FILE", sequencePath)
|
||||
t.Setenv("WAILS_BUILD_LOG_FILE", wailsLogPath)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "testapp",
|
||||
}
|
||||
targets := []build.Target{
|
||||
{OS: runtime.GOOS, Arch: runtime.GOARCH},
|
||||
}
|
||||
|
||||
artifacts, err := builder.Build(context.Background(), cfg, targets)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, artifacts, 1)
|
||||
|
||||
content, err := ax.ReadFile(sequencePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
|
||||
require.GreaterOrEqual(t, len(lines), 4)
|
||||
assert.Equal(t, "deno", lines[0])
|
||||
assert.Equal(t, "task", lines[1])
|
||||
assert.Equal(t, "build", lines[2])
|
||||
assert.Equal(t, "wails", lines[3])
|
||||
}
|
||||
|
||||
func TestWails_WailsBuilderResolveWailsCli_Good(t *testing.T) {
|
||||
builder := NewWailsBuilder()
|
||||
fallbackDir := t.TempDir()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue