From 366f857eeecd8e536d582af1df351ffb877ab25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sun, 23 Jun 2024 12:49:10 +0200 Subject: [PATCH] Add css.TailwindCSS Closes #12618 Closes #12620 --- common/hexec/exec.go | 76 +++++++--- config/allconfig/load.go | 2 +- config/security/securityConfig.go | 1 + deps/deps.go | 2 +- hugolib/integrationtest_builder.go | 2 +- .../resource_transformers/postcss/postcss.go | 5 + .../postcss/tailwindcss.go | 141 ++++++++++++++++++ tpl/css/css.go | 18 ++- 8 files changed, 226 insertions(+), 21 deletions(-) create mode 100644 resources/resource_transformers/postcss/tailwindcss.go diff --git a/common/hexec/exec.go b/common/hexec/exec.go index 49291354dd0..db12f14ebb4 100644 --- a/common/hexec/exec.go +++ b/common/hexec/exec.go @@ -21,8 +21,10 @@ import ( "io" "os" "os/exec" + "path/filepath" "regexp" "strings" + "sync" "github.com/cli/safeexec" "github.com/gohugoio/hugo/config" @@ -84,7 +86,7 @@ var WithEnviron = func(env []string) func(c *commandeer) { } // New creates a new Exec using the provided security config. -func New(cfg security.Config) *Exec { +func New(cfg security.Config, workingDir string) *Exec { var baseEnviron []string for _, v := range os.Environ() { k, _ := config.SplitEnvVar(v) @@ -95,6 +97,7 @@ func New(cfg security.Config) *Exec { return &Exec{ sc: cfg, + workingDir: workingDir, baseEnviron: baseEnviron, } } @@ -119,15 +122,23 @@ func SafeCommand(name string, arg ...string) (*exec.Cmd, error) { // Exec enforces a security policy for commands run via os/exec. type Exec struct { - sc security.Config + sc security.Config + workingDir string // os.Environ filtered by the Exec.OsEnviron whitelist filter. baseEnviron []string + + npxInit sync.Once + npxAvailable bool +} + +func (e *Exec) New(name string, arg ...any) (Runner, error) { + return e.new(name, "", arg...) } // New will fail if name is not allowed according to the configured security policy. // Else a configured Runner will be returned ready to be Run. -func (e *Exec) New(name string, arg ...any) (Runner, error) { +func (e *Exec) new(name string, fullyQualifiedName string, arg ...any) (Runner, error) { if err := e.sc.CheckAllowedExec(name); err != nil { return nil, err } @@ -136,27 +147,51 @@ func (e *Exec) New(name string, arg ...any) (Runner, error) { copy(env, e.baseEnviron) cm := &commandeer{ - name: name, - env: env, + name: name, + fullyQualifiedName: fullyQualifiedName, + env: env, } return cm.command(arg...) } -// Npx will try to run npx, and if that fails, it will -// try to run the binary directly. +// Npx will in order: +// 1. Try fo find the binary in the WORKINGDIR/node_modules/.bin directory. +// 2. If not found, and npx is available, run npx --no-install . +// 3. Fall back to the PATH. func (e *Exec) Npx(name string, arg ...any) (Runner, error) { - r, err := e.npx(name, arg...) + // npx is slow, so first try the common case. + nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name) + _, err := safeexec.LookPath(nodeBinFilename) if err == nil { - return r, nil + return e.new(name, nodeBinFilename, arg...) + } + e.checkNpx() + if e.npxAvailable { + r, err := e.npx(name, arg...) + if err == nil { + return r, nil + } } return e.New(name, arg...) } +const ( + npxNoInstall = "--no-install" + npxBinary = "npx" + nodeModulesBinPath = "node_modules/.bin" +) + +func (e *Exec) checkNpx() { + e.npxInit.Do(func() { + e.npxAvailable = InPath(npxBinary) + }) +} + // npx is a convenience method to create a Runner running npx --no-install 2 { + return nil, errors.New("must not provide more arguments than resource object and options") + } + + r, m, err := resourcehelpers.ResolveArgs(args) + if err != nil { + return nil, err + } + + return ns.tailwindcssClient.Process(r, m) +} + +// Sass processes the given Resource with SASS. func (ns *Namespace) Sass(args ...any) (resource.Resource, error) { if len(args) > 2 { return nil, errors.New("must not provide more arguments than resource object and options") @@ -145,6 +160,7 @@ func init() { d: d, scssClientLibSass: scssClient, postcssClient: postcss.New(d.ResourceSpec), + tailwindcssClient: postcss.NewTailwindCSSClient(d.ResourceSpec), babelClient: babel.New(d.ResourceSpec), }