]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Fix NPX issue with TailwindCSS v4
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 7 Jan 2025 08:13:16 +0000 (09:13 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 7 Jan 2025 12:12:08 +0000 (13:12 +0100)
This allows the `tailwindcss` CLI binary to live in the `PATH` for NPM-less projects.

Fixes #13221

common/hexec/exec.go
config/allconfig/load.go
deps/deps.go
hugolib/integrationtest_builder.go
hugolib/testhelpers_test.go
markup/asciidocext/convert_test.go
markup/pandoc/convert_test.go
markup/rst/convert_test.go
modules/client_test.go

index 4f23d20f505cb1774aef5c43dc146e354fd750a8..1369e847cde52f99ac8d5206a4082fdeb82140f0 100644 (file)
@@ -26,7 +26,10 @@ import (
        "strings"
        "sync"
 
+       "github.com/bep/logg"
        "github.com/cli/safeexec"
+       "github.com/gohugoio/hugo/common/loggers"
+       "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/config/security"
 )
@@ -86,7 +89,7 @@ var WithEnviron = func(env []string) func(c *commandeer) {
 }
 
 // New creates a new Exec using the provided security config.
-func New(cfg security.Config, workingDir string) *Exec {
+func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
        var baseEnviron []string
        for _, v := range os.Environ() {
                k, _ := config.SplitEnvVar(v)
@@ -96,9 +99,11 @@ func New(cfg security.Config, workingDir string) *Exec {
        }
 
        return &Exec{
-               sc:          cfg,
-               workingDir:  workingDir,
-               baseEnviron: baseEnviron,
+               sc:                cfg,
+               workingDir:        workingDir,
+               infol:             log.InfoCommand("exec"),
+               baseEnviron:       baseEnviron,
+               newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
        }
 }
 
@@ -124,12 +129,14 @@ func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
 type Exec struct {
        sc         security.Config
        workingDir string
+       infol      logg.LevelLogger
 
        // os.Environ filtered by the Exec.OsEnviron whitelist filter.
        baseEnviron []string
 
-       npxInit      sync.Once
-       npxAvailable bool
+       newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)]
+       npxInit           sync.Once
+       npxAvailable      bool
 }
 
 func (e *Exec) New(name string, arg ...any) (Runner, error) {
@@ -155,25 +162,86 @@ func (e *Exec) new(name string, fullyQualifiedName string, arg ...any) (Runner,
        return cm.command(arg...)
 }
 
+type binaryLocation int
+
+func (b binaryLocation) String() string {
+       switch b {
+       case binaryLocationNodeModules:
+               return "node_modules/.bin"
+       case binaryLocationNpx:
+               return "npx"
+       case binaryLocationPath:
+               return "PATH"
+       }
+       return "unknown"
+}
+
+const (
+       binaryLocationNodeModules binaryLocation = iota + 1
+       binaryLocationNpx
+       binaryLocationPath
+)
+
 // 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 <name> <args>.
 // 3. Fall back to the PATH.
+// If name is "tailwindcss", we will try the PATH as the second option.
 func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
-       // npx is slow, so first try the common case.
-       nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
-       _, err := safeexec.LookPath(nodeBinFilename)
-       if err == nil {
-               return e.new(name, nodeBinFilename, arg...)
+       if err := e.sc.CheckAllowedExec(name); err != nil {
+               return nil, err
        }
-       e.checkNpx()
-       if e.npxAvailable {
-               r, err := e.npx(name, arg...)
-               if err == nil {
-                       return r, nil
+
+       newRunner, err := e.newNPXRunnerCache.GetOrCreate(name, func() (func(...any) (Runner, error), error) {
+               type tryFunc func() func(...any) (Runner, error)
+               tryFuncs := map[binaryLocation]tryFunc{
+                       binaryLocationNodeModules: func() func(...any) (Runner, error) {
+                               nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
+                               _, err := safeexec.LookPath(nodeBinFilename)
+                               if err != nil {
+                                       return nil
+                               }
+                               return func(arg2 ...any) (Runner, error) {
+                                       return e.new(name, nodeBinFilename, arg2...)
+                               }
+                       },
+                       binaryLocationNpx: func() func(...any) (Runner, error) {
+                               e.checkNpx()
+                               if !e.npxAvailable {
+                                       return nil
+                               }
+                               return func(arg2 ...any) (Runner, error) {
+                                       return e.npx(name, arg2...)
+                               }
+                       },
+                       binaryLocationPath: func() func(...any) (Runner, error) {
+                               if _, err := safeexec.LookPath(name); err != nil {
+                                       return nil
+                               }
+                               return func(arg2 ...any) (Runner, error) {
+                                       return e.New(name, arg2...)
+                               }
+                       },
+               }
+
+               locations := []binaryLocation{binaryLocationNodeModules, binaryLocationNpx, binaryLocationPath}
+               if name == "tailwindcss" {
+                       // See https://github.com/gohugoio/hugo/issues/13221#issuecomment-2574801253
+                       locations = []binaryLocation{binaryLocationNodeModules, binaryLocationPath, binaryLocationNpx}
                }
+               for _, loc := range locations {
+                       if f := tryFuncs[loc](); f != nil {
+                               e.infol.Logf("resolve %q using %s", name, loc)
+                               return f, nil
+                       }
+               }
+               return nil, &NotFoundError{name: name, method: fmt.Sprintf("in %s", locations[len(locations)-1])}
+       })
+       if err != nil {
+               return nil, err
        }
-       return e.New(name, arg...)
+
+       return newRunner(arg...)
 }
 
 const (
index 16e2019cfa95b2b7c6974ffe3fe50bfbe2ba792e..999e03645b055b4467ef04a8a27668ef12a4bb0e 100644 (file)
@@ -470,7 +470,7 @@ func (l *configLoader) loadModules(configs *Configs, ignoreModuleDoesNotExist bo
                ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s))
        }
 
-       ex := hexec.New(conf.Security, workingDir)
+       ex := hexec.New(conf.Security, workingDir, l.Logger)
 
        hook := func(m *modules.ModulesConfig) error {
                for _, tc := range m.AllModules {
index 56a3d36446aae5371795c6822d4fd76482e78ae1..34c41012cf30c01cedf3d820a07a0585bcaf252e 100644 (file)
@@ -188,7 +188,7 @@ func (d *Deps) Init() error {
        }
 
        if d.ExecHelper == nil {
-               d.ExecHelper = hexec.New(d.Conf.GetConfigSection("security").(security.Config), d.Conf.WorkingDir())
+               d.ExecHelper = hexec.New(d.Conf.GetConfigSection("security").(security.Config), d.Conf.WorkingDir(), d.Log)
        }
 
        if d.MemCache == nil {
index 7a6c040b16abf8d14aefd37023aa33e0f661b8bc..1880aa62ff2aa05320bced6f10e846d97cb45b5b 100644 (file)
@@ -729,7 +729,7 @@ func (s *IntegrationTestBuilder) initBuilder() error {
                        sc := security.DefaultConfig
                        sc.Exec.Allow, err = security.NewWhitelist("npm")
                        s.Assert(err, qt.IsNil)
-                       ex := hexec.New(sc, s.Cfg.WorkingDir)
+                       ex := hexec.New(sc, s.Cfg.WorkingDir, loggers.NewDefault())
                        command, err := ex.New("npm", "install")
                        s.Assert(err, qt.IsNil)
                        s.Assert(command.Run(), qt.IsNil)
index 08eb2178756ba6b971a677ab585b825dc8d7a91e..5c4e07498ea610f7e715ee06750ecfa58f71dcac 100644 (file)
@@ -838,7 +838,7 @@ func (s *sitesBuilder) NpmInstall() hexec.Runner {
        var err error
        sc.Exec.Allow, err = security.NewWhitelist("npm")
        s.Assert(err, qt.IsNil)
-       ex := hexec.New(sc, s.workingDir)
+       ex := hexec.New(sc, s.workingDir, loggers.NewDefault())
        command, err := ex.New("npm", "install")
        s.Assert(err, qt.IsNil)
        return command
index b3f63b4d8a482409d41c8d7ad3f2863dc1a85f90..e93cab00bb8b1e3d90a31f0cc95fe262699da280 100644 (file)
@@ -313,7 +313,7 @@ allow = ['asciidoctor']
                converter.ProviderConfig{
                        Logger: loggers.NewDefault(),
                        Conf:   conf,
-                       Exec:   hexec.New(securityConfig, ""),
+                       Exec:   hexec.New(securityConfig, "", loggers.NewDefault()),
                },
        )
        c.Assert(err, qt.IsNil)
index 8beedc1150cf154644f425d996227137f28fc4aa..dff6b1ed37ad1916b2a3922f4842d2b82bc6baeb 100644 (file)
@@ -34,7 +34,7 @@ func TestConvert(t *testing.T) {
        var err error
        sc.Exec.Allow, err = security.NewWhitelist("pandoc")
        c.Assert(err, qt.IsNil)
-       p, err := Provider.New(converter.ProviderConfig{Exec: hexec.New(sc, ""), Logger: loggers.NewDefault()})
+       p, err := Provider.New(converter.ProviderConfig{Exec: hexec.New(sc, "", loggers.NewDefault()), Logger: loggers.NewDefault()})
        c.Assert(err, qt.IsNil)
        conv, err := p.New(converter.DocumentContext{})
        c.Assert(err, qt.IsNil)
index 182858263b8317827dfa8c0b806c819aae0f9922..730e00acf86226239b77048005d28d39edd154f6 100644 (file)
@@ -36,7 +36,7 @@ func TestConvert(t *testing.T) {
        p, err := Provider.New(
                converter.ProviderConfig{
                        Logger: loggers.NewDefault(),
-                       Exec:   hexec.New(sc, ""),
+                       Exec:   hexec.New(sc, "", loggers.NewDefault()),
                })
        c.Assert(err, qt.IsNil)
        conv, err := p.New(converter.DocumentContext{})
index 0ee7e0dbc40cb10b4ab5c099913cb95571f0d518..6320e33385d59df20f115822236d080b5f44c123 100644 (file)
@@ -22,6 +22,7 @@ import (
        "testing"
 
        "github.com/gohugoio/hugo/common/hexec"
+       "github.com/gohugoio/hugo/common/loggers"
        "github.com/gohugoio/hugo/config/security"
        "github.com/gohugoio/hugo/hugofs/glob"
 
@@ -61,7 +62,7 @@ github.com/gohugoio/hugoTestModules1_darwin/modh2_2@v1.4.0 github.com/gohugoio/h
                        WorkingDir: workingDir,
                        ThemesDir:  themesDir,
                        PublishDir: publishDir,
-                       Exec:       hexec.New(security.DefaultConfig, ""),
+                       Exec:       hexec.New(security.DefaultConfig, "", loggers.NewDefault()),
                }
 
                withConfig(&ccfg)