tailwind
API
tailwind
packageAPI reference for the tailwind
package.
Imports
(11)
S
struct
plugin
cmd/rfw/plugins/tailwind/tailwind.go:20-22
type plugin struct
Methods
Build
Method
Parameters
raw
json.RawMessage
Returns
error
func (*plugin) Build(raw json.RawMessage) error
{
logging.Log.Info("starting build", logging.F("plugin", "tailwind"))
cfg := struct {
Input string `json:"input"`
Output string `json:"output"`
Minify bool `json:"minify"`
Args []string `json:"args"`
}{
Input: "index.css",
Output: "tailwind.css",
Minify: true,
}
if len(raw) > 0 {
if err := json.Unmarshal(raw, &cfg); err != nil {
return fmt.Errorf("decode tailwind plugin config: %w", err)
}
}
input, err := validateProjectFile(cfg.Input, true)
if err != nil {
return fmt.Errorf("invalid tailwind input: %w", err)
}
output, err := validateProjectFile(cfg.Output, false)
if err != nil {
return fmt.Errorf("invalid tailwind output: %w", err)
}
if input == output {
return fmt.Errorf("tailwind input and output must differ")
}
useConfig, err := validateExtraArgs(cfg.Args)
if err != nil {
return err
}
p.output = output
root, err := os.OpenRoot(".")
if err != nil {
return fmt.Errorf("open project root: %w", err)
}
defer func() {
if closeErr := root.Close(); closeErr != nil {
logging.Log.Error("close project root", logging.F("plugin", "tailwind"), logging.F("error", closeErr.Error()))
}
}()
inputCSS, err := root.ReadFile(input)
if err != nil {
return fmt.Errorf("read tailwind input: %w", err)
}
cmd := tailwindCommand(cfg.Minify, useConfig)
cmd.Stdin = bytes.NewReader(inputCSS)
var generatedCSS bytes.Buffer
var diagnostics bytes.Buffer
cmd.Stdout = &generatedCSS
cmd.Stderr = &diagnostics
logging.Log.Info("running command", logging.F("plugin", "tailwind"), logging.F("args", strings.Join(cmd.Args, " ")))
if err := cmd.Run(); err != nil {
if errors.Is(err, exec.ErrNotFound) {
logging.Log.Warn("tailwindcss not found, please install it manually", logging.F("plugin", "tailwind"))
}
return fmt.Errorf("tailwind build failed: %s: %w", strings.TrimSpace(diagnostics.String()), err)
}
if err := writeProjectFile(root, output, generatedCSS.Bytes()); err != nil {
return fmt.Errorf("write tailwind output: %w", err)
}
logging.Log.Info("build complete", logging.F("plugin", "tailwind"))
return nil
}
ShouldRebuild
Method
Parameters
path
string
Returns
bool
func (*plugin) ShouldRebuild(path string) bool
{
if strings.HasSuffix(path, ".css") && !strings.HasSuffix(path, p.output) {
logging.Log.Info("rebuild triggered", logging.F("plugin", "tailwind"), logging.F("path", path))
return true
}
if strings.HasSuffix(path, ".rtml") || strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".go") {
logging.Log.Info("rebuild triggered", logging.F("plugin", "tailwind"), logging.F("path", path))
return true
}
return false
}
Fields
| Name | Type | Description |
|---|---|---|
| output | string |
F
function
init
cmd/rfw/plugins/tailwind/tailwind.go:24-26
func init()
{
plugins.Register(&plugin{})
}
F
function
tailwindCommand
Parameters
minify
bool
useConfig
bool
Returns
cmd/rfw/plugins/tailwind/tailwind.go:100-111
func tailwindCommand(minify, useConfig bool) *exec.Cmd
{
switch {
case minify && useConfig:
return exec.Command("tailwindcss", "-i", "-", "--minify", "--config", "tailwind.config.js")
case minify:
return exec.Command("tailwindcss", "-i", "-", "--minify")
case useConfig:
return exec.Command("tailwindcss", "-i", "-", "--config", "tailwind.config.js")
default:
return exec.Command("tailwindcss", "-i", "-")
}
}
F
function
writeProjectFile
Parameters
Returns
error
cmd/rfw/plugins/tailwind/tailwind.go:113-130
func writeProjectFile(root *os.Root, filePath string, content []byte) error
{
directory := filepath.Dir(filePath)
if directory != "." {
if err := root.MkdirAll(directory, 0o755); err != nil {
return err
}
}
if err := root.Remove(filePath); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
file, err := root.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
if err != nil {
return err
}
_, writeErr := file.Write(content)
closeErr := file.Close()
return errors.Join(writeErr, closeErr)
}
F
function
validateProjectFile
Parameters
filePath
string
mustExist
bool
Returns
string
error
cmd/rfw/plugins/tailwind/tailwind.go:132-174
func validateProjectFile(filePath string, mustExist bool) (string, error)
{
if filePath == "" || filepath.IsAbs(filePath) || strings.HasPrefix(filePath, "-") {
return "", fmt.Errorf("path %q must be a project-relative file", filePath)
}
cleaned := filepath.Clean(filePath)
if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("path %q escapes the project", filePath)
}
projectRoot, err := filepath.Abs(".")
if err != nil {
return "", err
}
resolvedRoot, err := filepath.EvalSymlinks(projectRoot)
if err != nil {
return "", err
}
candidate := filepath.Join(resolvedRoot, cleaned)
checkPath := candidate
if !mustExist {
for {
if _, statErr := os.Lstat(checkPath); statErr == nil {
break
} else if !os.IsNotExist(statErr) {
return "", statErr
}
checkPath = filepath.Dir(candidate)
if checkPath == resolvedRoot {
break
}
candidate = checkPath
}
}
resolved, err := filepath.EvalSymlinks(checkPath)
if err != nil {
return "", err
}
relative, err := filepath.Rel(resolvedRoot, resolved)
if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("path %q resolves outside the project", filePath)
}
return cleaned, nil
}
F
function
validateExtraArgs
Parameters
args
[]string
Returns
bool
error
cmd/rfw/plugins/tailwind/tailwind.go:176-191
func validateExtraArgs(args []string) (bool, error)
{
if len(args) == 0 {
return false, nil
}
if len(args) != 2 || args[0] != "--config" && args[0] != "-c" {
return false, fmt.Errorf("tailwind args only support the project-root tailwind.config.js")
}
configPath, err := validateProjectFile(args[1], true)
if err != nil {
return false, fmt.Errorf("invalid tailwind config: %w", err)
}
if configPath != "tailwind.config.js" {
return false, fmt.Errorf("tailwind config must be tailwind.config.js in the project root")
}
return true, nil
}
F
function
TestShouldRebuild
TestShouldRebuild ensures the plugin’s rebuild triggers are detected
correctly based on file paths and extensions.
Parameters
t
cmd/rfw/plugins/tailwind/tailwind_test.go:13-31
func TestShouldRebuild(t *testing.T)
{
p := &plugin{output: "tailwind.css"}
if !p.ShouldRebuild("style.css") {
t.Fatalf("expected css change to trigger rebuild")
}
if p.ShouldRebuild("tailwind.css") {
t.Fatalf("output file should not trigger rebuild")
}
if !p.ShouldRebuild("index.html") || !p.ShouldRebuild("tmpl.rtml") {
t.Fatalf("html/rtml should trigger rebuild")
}
if !p.ShouldRebuild("main.go") {
t.Fatalf("go files should trigger rebuild")
}
if p.ShouldRebuild("image.png") {
t.Fatalf("unrelated files should not trigger rebuild")
}
}
F
function
TestValidateProjectFile
Parameters
t
cmd/rfw/plugins/tailwind/tailwind_test.go:33-70
func TestValidateProjectFile(t *testing.T)
{
root := t.TempDir()
oldWorkingDirectory, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(root); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := os.Chdir(oldWorkingDirectory); err != nil {
t.Errorf("restore working directory: %v", err)
}
})
if err := os.WriteFile("input.css", []byte("body {}"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := validateProjectFile("input.css", true); err != nil {
t.Fatalf("expected project file to be valid: %v", err)
}
if _, err := validateProjectFile("dist/css/app.css", false); err != nil {
t.Fatalf("expected nested output to be valid: %v", err)
}
for _, filePath := range []string{"../input.css", "/tmp/input.css", "-o"} {
if _, err := validateProjectFile(filePath, false); err == nil {
t.Errorf("expected %q to be rejected", filePath)
}
}
outside := t.TempDir()
if err := os.Symlink(outside, filepath.Join(root, "outside")); err != nil {
t.Fatal(err)
}
if _, err := validateProjectFile("outside/output.css", false); err == nil {
t.Fatal("expected symlink escape to be rejected")
}
}
F
function
TestValidateExtraArgs
Parameters
t
cmd/rfw/plugins/tailwind/tailwind_test.go:72-102
func TestValidateExtraArgs(t *testing.T)
{
root := t.TempDir()
oldWorkingDirectory, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(root); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := os.Chdir(oldWorkingDirectory); err != nil {
t.Errorf("restore working directory: %v", err)
}
})
if err := os.WriteFile("tailwind.config.js", nil, 0o600); err != nil {
t.Fatal(err)
}
useConfig, err := validateExtraArgs([]string{"--config", "tailwind.config.js"})
if err != nil {
t.Fatalf("expected config argument to be valid: %v", err)
}
if !useConfig {
t.Fatal("expected config to be enabled")
}
for _, args := range [][]string{{"--watch"}, {"--config"}, {"--config", "../config.js"}, {"--config", "config/tailwind.config.js"}} {
if _, err := validateExtraArgs(args); err == nil {
t.Errorf("expected %v to be rejected", args)
}
}
}
F
function
TestTailwindCommandUsesFixedArguments
Parameters
t
cmd/rfw/plugins/tailwind/tailwind_test.go:104-134
func TestTailwindCommandUsesFixedArguments(t *testing.T)
{
tests := []struct {
name string
minify bool
useConfig bool
want []string
}{
{name: "default", want: []string{"tailwindcss", "-i", "-"}},
{name: "minified", minify: true, want: []string{"tailwindcss", "-i", "-", "--minify"}},
{name: "configured", useConfig: true, want: []string{"tailwindcss", "-i", "-", "--config", "tailwind.config.js"}},
{
name: "configured and minified",
minify: true,
useConfig: true,
want: []string{"tailwindcss", "-i", "-", "--minify", "--config", "tailwind.config.js"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
command := tailwindCommand(test.minify, test.useConfig)
if len(command.Args) != len(test.want) {
t.Fatalf("args %v, want %v", command.Args, test.want)
}
for index := range test.want {
if command.Args[index] != test.want[index] {
t.Fatalf("args %v, want %v", command.Args, test.want)
}
}
})
}
}