mirror of
https://github.com/gohugoio/hugo.git
synced 2025-04-26 21:51:02 +03:00
Fix NPX issue with TailwindCSS v4
This allows the `tailwindcss` CLI binary to live in the `PATH` for NPM-less projects. Fixes #13221
This commit is contained in:
parent
f024a5050e
commit
cfa0801815
9 changed files with 94 additions and 25 deletions
|
@ -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 (
|
||||
|
|
|
@ -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 {
|
||||
|
|
2
deps/deps.go
vendored
2
deps/deps.go
vendored
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{})
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue