readysite / pkg / frontend / build.go
2.9 KB
build.go
package frontend

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/evanw/esbuild/pkg/api"
)

// Build compiles all components in SourceDir to OutputDir.
// If a bundler is configured (via WithBundler), it delegates to that bundler.
func (f *Frontend) Build() error {
	// Use bundler if configured
	if f.bundler != nil {
		return f.bundler.Build()
	}

	// Legacy build: discover component files
	entries, err := f.discoverComponents()
	if err != nil {
		return fmt.Errorf("discovering components: %w", err)
	}

	if len(entries) == 0 {
		// No components found, create empty bundle
		if err := os.MkdirAll(f.OutputDir, 0755); err != nil {
			return fmt.Errorf("creating output dir: %w", err)
		}
		return os.WriteFile(filepath.Join(f.OutputDir, "components.js"), []byte("// No components\nwindow.__render = () => {};\nwindow.__unmount = () => {};\n"), 0644)
	}

	// Build options with sensible defaults
	opts := api.BuildOptions{
		EntryPoints: entries,
		Bundle:      true,
		Outfile:     filepath.Join(f.OutputDir, "components.js"),
		Format:      api.FormatESModule,
		Target:      api.ES2020,
		Platform:    api.PlatformBrowser,
		Sourcemap:   api.SourceMapLinked,
		Write:       true,
		LogLevel:    api.LogLevelWarning,
		JSX:         api.JSXAutomatic,
		Define: map[string]string{
			"process.env.NODE_ENV": `"production"`,
		},
	}

	// Minify in production
	if !f.DevMode {
		opts.MinifyWhitespace = true
		opts.MinifyIdentifiers = true
		opts.MinifySyntax = true
	}

	// Create output directory
	if err := os.MkdirAll(f.OutputDir, 0755); err != nil {
		return fmt.Errorf("creating output dir: %w", err)
	}

	// Run esbuild
	result := api.Build(opts)

	// Check for errors
	if len(result.Errors) > 0 {
		var errMsgs []string
		for _, e := range result.Errors {
			errMsgs = append(errMsgs, e.Text)
		}
		return fmt.Errorf("build errors: %s", strings.Join(errMsgs, "; "))
	}

	return nil
}

// discoverComponents finds all component files in SourceDir.
func (f *Frontend) discoverComponents() ([]string, error) {
	var entries []string

	// Check if source directory exists
	if _, err := os.Stat(f.SourceDir); os.IsNotExist(err) {
		return nil, nil
	}

	// Walk the source directory
	err := filepath.Walk(f.SourceDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if info.IsDir() {
			return nil
		}

		// Include .tsx, .ts, .jsx, .js files
		ext := strings.ToLower(filepath.Ext(path))
		switch ext {
		case ".tsx", ".ts", ".jsx", ".js":
			entries = append(entries, path)
		}

		return nil
	})

	return entries, err
}

// BuildAndWatch builds components and starts watching for changes.
// This is a convenience method that combines Build() and Dev().
func (f *Frontend) BuildAndWatch() error {
	// Initial build
	if err := f.Build(); err != nil {
		return err
	}

	// Start dev server if in dev mode
	if f.DevMode {
		return f.Dev()
	}

	return nil
}
← Back