package jsbundler // CSS pipeline: compiles the Tailwind entry stylesheet (frontend/css/style.css) // with the native Go Tailwind v4 engine (tailwind.go — twCompile/scanSources, no // goja), feeding it the utility-class candidates scanned from source files, then // minifies via tdewolff/minify. style.css is the single entry/config for both // bundles — they differ only in which source files are scanned for candidates. import ( "fmt" "os" "path/filepath" "github.com/tdewolff/minify/v2" mincss "github.com/tdewolff/minify/v2/css" "kjol/tw" ) // Tailwind source patterns - configured here rather than in CSS so each bundle // scans only the files it actually needs. The scanner filters by the suffix of // each pattern, so separate entries are needed for .js and .ts source files. var twSourcesSPA = []string{ "../src/**/*.js", "../src/**/*.ts", "../src/**/*.jsx", "../src/**/*.tsx", "../../internal/handlers/templates/**/*.html", } var twSourcesPublic = []string{ "../../internal/handlers/templates/**/*.html", // Public pages authored as Solid components in .tsx (SSR'd via goja). "../src/pages/public/**/*.tsx", "../src/pages/public/**/*.jsx", "../src/pages/public/**/*.ts", "../src/pages/public/**/*.js", // Shared UI component library. If a public page renders any ui/ component // (data tables, tabs, icons, etc.), its classes must be in this bundle too, // so scan the whole library rather than just the env badge / layout. "../src/ui/**/*.ts", "../src/ui/**/*.js", } // styleEntry is the Tailwind entry/config, relative to frontendDir. Both the SPA // and public bundles compile it; include.css (a former one-line passthrough) is gone. const styleEntry = "css/style.css" var m *minify.M func init() { m = minify.New() m.AddFunc("text/css", mincss.Minify) } func bundleSPACSS() (bundleStats, error) { return compileCSSBundle("SPA", twSourcesSPA, "bundle.min.css") } func bundlePublicCSS() (bundleStats, error) { return compileCSSBundle("public", twSourcesPublic, "public.bundle.min.css") } // compileCSSBundle scans twSources for candidates, compiles style.css with the // official Tailwind compiler, minifies, and writes outName to wwwroot. It prints // a timing line for the Tailwind step so the compile can be profiled. func compileCSSBundle(label string, twSources []string, outName string) (bundleStats, error) { entryPath := filepath.Join(frontendDir, styleEntry) src, err := os.ReadFile(entryPath) if err != nil { return bundleStats{}, fmt.Errorf("reading %s: %w", styleEntry, err) } cssDir := filepath.Dir(entryPath) // Candidate classes come from the app sources (patterns relative to the css // dir) plus the shared kit tree scanned directly, so kit component classes are // present even though the kit lives outside the app frontend. candidates := tw.Scan(cssDir, twSources) kitCands := tw.Scan(uikitDir(), []string{"**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"}) candidates = dedupStrings(append(candidates, kitCands...)) // Prepend the shared @theme scaffold (kjol jsruntime/styles/theme.css) ahead of the // app's brand style.css so its tokens/vars are in scope. Absent in single-tree // mode (the app's style.css is already complete). input := string(src) if tp := themeCSSPath(); tp != "" { if theme, e := os.ReadFile(tp); e == nil { input = string(theme) + "\n" + input } } compiled, count, err := tw.Compile(input, cssDir, candidates) if err != nil { return bundleStats{}, fmt.Errorf("tailwind compile (%s): %w", label, err) } fmt.Printf(" Tailwind (%s): %d candidates, %d utilities compiled\n", label, len(candidates), count) minified, err := m.String("text/css", compiled) if err != nil { return bundleStats{}, fmt.Errorf("minifying %s CSS: %w", label, err) } outPath := filepath.Join(outputDir, outName) if err := os.WriteFile(outPath, []byte(minified), 0644); err != nil { return bundleStats{}, err } return bundleStats{files: len(candidates), bytes: len(minified)}, nil } // dedupStrings returns in with duplicates removed, preserving first-seen order. func dedupStrings(in []string) []string { seen := make(map[string]bool, len(in)) out := make([]string, 0, len(in)) for _, s := range in { if !seen[s] { seen[s] = true out = append(out, s) } } return out }