148 lines
4.9 KiB
Go
148 lines
4.9 KiB
Go
// Package bundler is the frontend build system: it drives esbuild's Go API for
|
|
// JS bundling, compiles Solid JSX/TSX and Tailwind v4 CSS with Go-native
|
|
// compilers (no Node, no Babel, no goja on the build path), and bakes the
|
|
// public-page SSR (which does still use goja to execute components). The
|
|
// `cmd/bundle` command is a thin CLI wrapper over this package.
|
|
//
|
|
// The package also carries the runtime SSR entry (RenderBundleWithData, in
|
|
// ssr.go/renderer.go) that the server imports for ISR.
|
|
//
|
|
// Pipeline stages live in sibling files:
|
|
//
|
|
// js.go esbuild-driven JS bundling + sourcemap fixup
|
|
// export_shim.go default/named export shim plugin
|
|
// import_check.go .js-extension import validation
|
|
// css.go Tailwind compile driver + minify
|
|
// tailwind.go official Tailwind v4 compiler run in goja
|
|
// candidates.go utility-class candidate scanner (feeds Tailwind's build())
|
|
// genssr.go public-page SSR bake + Go registry generation
|
|
// ssr.go/renderer.go the goja SSR engine (also used by the server at runtime)
|
|
// watch.go poll-and-rebuild watch loop
|
|
package bundler
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type bundleStats struct {
|
|
files int
|
|
bytes int
|
|
}
|
|
|
|
// Build runs the full one-shot build: FA icon subset, public-route generation,
|
|
// the JS/CSS bundles, and the SSR bake, printing a stats summary. c selects the
|
|
// app + kjol web trees (see Config); zero-value fields fall back to the
|
|
// single-tree defaults.
|
|
func Build(c Config) error {
|
|
Configure(c)
|
|
// esbuild silently resolves a `.js` import to a sibling `.ts`/`.tsx`/`.jsx`,
|
|
// which lets misnamed specifiers slip through. Catch them up front so the
|
|
// import path always names the file that actually exists on disk.
|
|
if violations := checkImportExtensions(); len(violations) > 0 {
|
|
reportImportViolations(violations)
|
|
return fmt.Errorf("%d import-extension violation(s)", len(violations))
|
|
}
|
|
|
|
buildStart := time.Now()
|
|
|
|
fmt.Println("Generating FA icon subset...")
|
|
if err := generateFAIcons(); err != nil {
|
|
return fmt.Errorf("FA icon generation failed: %w", err)
|
|
}
|
|
|
|
fmt.Println("Generating public routes...")
|
|
routesStart := time.Now()
|
|
if err := generatePublicRoutes(); err != nil {
|
|
return fmt.Errorf("public route generation failed: %w", err)
|
|
}
|
|
routesDur := time.Since(routesStart)
|
|
|
|
// The four bundles are independent — distinct output files, shared read-only
|
|
// inputs plus the generated files above — so run them in parallel. The Go
|
|
// Solid compiler and Go Tailwind engine are both stateless per call.
|
|
fmt.Println("Bundling JS + CSS...")
|
|
results := runBundlesParallel()
|
|
|
|
for _, r := range results {
|
|
if r.err != nil {
|
|
return fmt.Errorf("%s failed: %w", r.name, r.err)
|
|
}
|
|
}
|
|
|
|
// One column layout drives header, rows, and footer so every field lines up.
|
|
// Files is %6d because candidate counts run into the thousands (a %3d would
|
|
// overflow and shift Size/Time right on the CSS rows). Total width = 55.
|
|
sep := strings.Repeat("-", 55)
|
|
fmt.Println()
|
|
fmt.Printf("%-27s %6s %8s %9s\n", "Bundle", "Files", "Size", "Time")
|
|
fmt.Println(sep)
|
|
for _, r := range results {
|
|
printStats(r.name, r.stats, r.dur)
|
|
}
|
|
fmt.Println(sep)
|
|
// %-46s%9s places the time in the same field (cols 47-55) as the bundle rows.
|
|
fmt.Printf("%-46s%9s\n", "Public routes (SSR)", formatDuration(routesDur))
|
|
fmt.Printf("%-46s%9s\n", "Total build time", formatDuration(time.Since(buildStart)))
|
|
fmt.Println()
|
|
fmt.Println("Done!")
|
|
return nil
|
|
}
|
|
|
|
type bundleResult struct {
|
|
name string
|
|
stats bundleStats
|
|
dur time.Duration
|
|
err error
|
|
}
|
|
|
|
// runBundlesParallel runs the four output bundles concurrently and returns their
|
|
// results in a stable order. Per-bundle durations overlap, so they won't sum to
|
|
// wall-clock — that's the point.
|
|
func runBundlesParallel() []bundleResult {
|
|
jobs := []struct {
|
|
name string
|
|
fn func() (bundleStats, error)
|
|
}{
|
|
{"bundle.min.js", bundleJS},
|
|
{"public.bundle.min.js", bundlePublicJS},
|
|
{"bundle.min.css", bundleSPACSS},
|
|
{"public.bundle.min.css", bundlePublicCSS},
|
|
}
|
|
results := make([]bundleResult, len(jobs))
|
|
var wg sync.WaitGroup
|
|
for i, j := range jobs {
|
|
wg.Add(1)
|
|
go func(i int, name string, fn func() (bundleStats, error)) {
|
|
defer wg.Done()
|
|
t := time.Now()
|
|
s, err := fn()
|
|
results[i] = bundleResult{name: name, stats: s, dur: time.Since(t), err: err}
|
|
}(i, j.name, j.fn)
|
|
}
|
|
wg.Wait()
|
|
return results
|
|
}
|
|
|
|
func printStats(name string, s bundleStats, d time.Duration) {
|
|
fmt.Printf("%-27s %6d %8s %9s\n", name, s.files, formatSize(s.bytes), formatDuration(d))
|
|
}
|
|
|
|
// formatDuration renders a build-phase duration compactly: sub-second in ms,
|
|
// otherwise seconds with two decimals.
|
|
func formatDuration(d time.Duration) string {
|
|
if d < time.Second {
|
|
return fmt.Sprintf("%dms", d.Milliseconds())
|
|
}
|
|
return fmt.Sprintf("%.2fs", d.Seconds())
|
|
}
|
|
|
|
func formatSize(bytes int) string {
|
|
if bytes >= 1024*1024 {
|
|
return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
|
|
}
|
|
return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
|
|
}
|