mirror of
https://github.com/gohugoio/hugo.git
synced 2025-04-27 14:10:31 +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"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/bep/logg"
|
||||||
"github.com/cli/safeexec"
|
"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"
|
||||||
"github.com/gohugoio/hugo/config/security"
|
"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.
|
// 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
|
var baseEnviron []string
|
||||||
for _, v := range os.Environ() {
|
for _, v := range os.Environ() {
|
||||||
k, _ := config.SplitEnvVar(v)
|
k, _ := config.SplitEnvVar(v)
|
||||||
|
@ -98,7 +101,9 @@ func New(cfg security.Config, workingDir string) *Exec {
|
||||||
return &Exec{
|
return &Exec{
|
||||||
sc: cfg,
|
sc: cfg,
|
||||||
workingDir: workingDir,
|
workingDir: workingDir,
|
||||||
|
infol: log.InfoCommand("exec"),
|
||||||
baseEnviron: baseEnviron,
|
baseEnviron: baseEnviron,
|
||||||
|
newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,10 +129,12 @@ func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
|
||||||
type Exec struct {
|
type Exec struct {
|
||||||
sc security.Config
|
sc security.Config
|
||||||
workingDir string
|
workingDir string
|
||||||
|
infol logg.LevelLogger
|
||||||
|
|
||||||
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
|
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
|
||||||
baseEnviron []string
|
baseEnviron []string
|
||||||
|
|
||||||
|
newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)]
|
||||||
npxInit sync.Once
|
npxInit sync.Once
|
||||||
npxAvailable bool
|
npxAvailable bool
|
||||||
}
|
}
|
||||||
|
@ -155,25 +162,86 @@ func (e *Exec) new(name string, fullyQualifiedName string, arg ...any) (Runner,
|
||||||
return cm.command(arg...)
|
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:
|
// Npx will in order:
|
||||||
// 1. Try fo find the binary in the WORKINGDIR/node_modules/.bin directory.
|
// 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>.
|
// 2. If not found, and npx is available, run npx --no-install <name> <args>.
|
||||||
// 3. Fall back to the PATH.
|
// 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) {
|
func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
|
||||||
// npx is slow, so first try the common case.
|
if err := e.sc.CheckAllowedExec(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
|
||||||
_, err := safeexec.LookPath(nodeBinFilename)
|
_, err := safeexec.LookPath(nodeBinFilename)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return e.new(name, nodeBinFilename, arg...)
|
return nil
|
||||||
}
|
}
|
||||||
|
return func(arg2 ...any) (Runner, error) {
|
||||||
|
return e.new(name, nodeBinFilename, arg2...)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
binaryLocationNpx: func() func(...any) (Runner, error) {
|
||||||
e.checkNpx()
|
e.checkNpx()
|
||||||
if e.npxAvailable {
|
if !e.npxAvailable {
|
||||||
r, err := e.npx(name, arg...)
|
return nil
|
||||||
if err == nil {
|
}
|
||||||
return r, 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 e.New(name, arg...)
|
return nil, &NotFoundError{name: name, method: fmt.Sprintf("in %s", locations[len(locations)-1])}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRunner(arg...)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -470,7 +470,7 @@ func (l *configLoader) loadModules(configs *Configs, ignoreModuleDoesNotExist bo
|
||||||
ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s))
|
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 {
|
hook := func(m *modules.ModulesConfig) error {
|
||||||
for _, tc := range m.AllModules {
|
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 {
|
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 {
|
if d.MemCache == nil {
|
||||||
|
|
|
@ -729,7 +729,7 @@ func (s *IntegrationTestBuilder) initBuilder() error {
|
||||||
sc := security.DefaultConfig
|
sc := security.DefaultConfig
|
||||||
sc.Exec.Allow, err = security.NewWhitelist("npm")
|
sc.Exec.Allow, err = security.NewWhitelist("npm")
|
||||||
s.Assert(err, qt.IsNil)
|
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")
|
command, err := ex.New("npm", "install")
|
||||||
s.Assert(err, qt.IsNil)
|
s.Assert(err, qt.IsNil)
|
||||||
s.Assert(command.Run(), qt.IsNil)
|
s.Assert(command.Run(), qt.IsNil)
|
||||||
|
|
|
@ -838,7 +838,7 @@ func (s *sitesBuilder) NpmInstall() hexec.Runner {
|
||||||
var err error
|
var err error
|
||||||
sc.Exec.Allow, err = security.NewWhitelist("npm")
|
sc.Exec.Allow, err = security.NewWhitelist("npm")
|
||||||
s.Assert(err, qt.IsNil)
|
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")
|
command, err := ex.New("npm", "install")
|
||||||
s.Assert(err, qt.IsNil)
|
s.Assert(err, qt.IsNil)
|
||||||
return command
|
return command
|
||||||
|
|
|
@ -313,7 +313,7 @@ allow = ['asciidoctor']
|
||||||
converter.ProviderConfig{
|
converter.ProviderConfig{
|
||||||
Logger: loggers.NewDefault(),
|
Logger: loggers.NewDefault(),
|
||||||
Conf: conf,
|
Conf: conf,
|
||||||
Exec: hexec.New(securityConfig, ""),
|
Exec: hexec.New(securityConfig, "", loggers.NewDefault()),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
|
@ -34,7 +34,7 @@ func TestConvert(t *testing.T) {
|
||||||
var err error
|
var err error
|
||||||
sc.Exec.Allow, err = security.NewWhitelist("pandoc")
|
sc.Exec.Allow, err = security.NewWhitelist("pandoc")
|
||||||
c.Assert(err, qt.IsNil)
|
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)
|
c.Assert(err, qt.IsNil)
|
||||||
conv, err := p.New(converter.DocumentContext{})
|
conv, err := p.New(converter.DocumentContext{})
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
|
@ -36,7 +36,7 @@ func TestConvert(t *testing.T) {
|
||||||
p, err := Provider.New(
|
p, err := Provider.New(
|
||||||
converter.ProviderConfig{
|
converter.ProviderConfig{
|
||||||
Logger: loggers.NewDefault(),
|
Logger: loggers.NewDefault(),
|
||||||
Exec: hexec.New(sc, ""),
|
Exec: hexec.New(sc, "", loggers.NewDefault()),
|
||||||
})
|
})
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
conv, err := p.New(converter.DocumentContext{})
|
conv, err := p.New(converter.DocumentContext{})
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/hexec"
|
"github.com/gohugoio/hugo/common/hexec"
|
||||||
|
"github.com/gohugoio/hugo/common/loggers"
|
||||||
"github.com/gohugoio/hugo/config/security"
|
"github.com/gohugoio/hugo/config/security"
|
||||||
"github.com/gohugoio/hugo/hugofs/glob"
|
"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,
|
WorkingDir: workingDir,
|
||||||
ThemesDir: themesDir,
|
ThemesDir: themesDir,
|
||||||
PublishDir: publishDir,
|
PublishDir: publishDir,
|
||||||
Exec: hexec.New(security.DefaultConfig, ""),
|
Exec: hexec.New(security.DefaultConfig, "", loggers.NewDefault()),
|
||||||
}
|
}
|
||||||
|
|
||||||
withConfig(&ccfg)
|
withConfig(&ccfg)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue