@@ -26,7 +26,10 @@ import (
2626 "strings"
2727 "sync"
2828
29+ "github.com/bep/logg"
2930 "github.com/cli/safeexec"
31+ "github.com/gohugoio/hugo/common/loggers"
32+ "github.com/gohugoio/hugo/common/maps"
3033 "github.com/gohugoio/hugo/config"
3134 "github.com/gohugoio/hugo/config/security"
3235)
@@ -86,7 +89,7 @@ var WithEnviron = func(env []string) func(c *commandeer) {
8689}
8790
8891// New creates a new Exec using the provided security config.
89- func New (cfg security.Config , workingDir string ) * Exec {
92+ func New (cfg security.Config , workingDir string , log loggers. Logger ) * Exec {
9093 var baseEnviron []string
9194 for _ , v := range os .Environ () {
9295 k , _ := config .SplitEnvVar (v )
@@ -96,9 +99,11 @@ func New(cfg security.Config, workingDir string) *Exec {
9699 }
97100
98101 return & Exec {
99- sc : cfg ,
100- workingDir : workingDir ,
101- baseEnviron : baseEnviron ,
102+ sc : cfg ,
103+ workingDir : workingDir ,
104+ infol : log .InfoCommand ("exec" ),
105+ baseEnviron : baseEnviron ,
106+ newNPXRunnerCache : maps .NewCache [string , func (arg ... any ) (Runner , error )](),
102107 }
103108}
104109
@@ -124,12 +129,14 @@ func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
124129type Exec struct {
125130 sc security.Config
126131 workingDir string
132+ infol logg.LevelLogger
127133
128134 // os.Environ filtered by the Exec.OsEnviron whitelist filter.
129135 baseEnviron []string
130136
131- npxInit sync.Once
132- npxAvailable bool
137+ newNPXRunnerCache * maps.Cache [string , func (arg ... any ) (Runner , error )]
138+ npxInit sync.Once
139+ npxAvailable bool
133140}
134141
135142func (e * Exec ) New (name string , arg ... any ) (Runner , error ) {
@@ -155,25 +162,86 @@ func (e *Exec) new(name string, fullyQualifiedName string, arg ...any) (Runner,
155162 return cm .command (arg ... )
156163}
157164
165+ type binaryLocation int
166+
167+ func (b binaryLocation ) String () string {
168+ switch b {
169+ case binaryLocationNodeModules :
170+ return "node_modules/.bin"
171+ case binaryLocationNpx :
172+ return "npx"
173+ case binaryLocationPath :
174+ return "PATH"
175+ }
176+ return "unknown"
177+ }
178+
179+ const (
180+ binaryLocationNodeModules binaryLocation = iota + 1
181+ binaryLocationNpx
182+ binaryLocationPath
183+ )
184+
158185// Npx will in order:
159186// 1. Try fo find the binary in the WORKINGDIR/node_modules/.bin directory.
160187// 2. If not found, and npx is available, run npx --no-install <name> <args>.
161188// 3. Fall back to the PATH.
189+ // If name is "tailwindcss", we will try the PATH as the second option.
162190func (e * Exec ) Npx (name string , arg ... any ) (Runner , error ) {
163- // npx is slow, so first try the common case.
164- nodeBinFilename := filepath .Join (e .workingDir , nodeModulesBinPath , name )
165- _ , err := safeexec .LookPath (nodeBinFilename )
166- if err == nil {
167- return e .new (name , nodeBinFilename , arg ... )
191+ if err := e .sc .CheckAllowedExec (name ); err != nil {
192+ return nil , err
168193 }
169- e .checkNpx ()
170- if e .npxAvailable {
171- r , err := e .npx (name , arg ... )
172- if err == nil {
173- return r , nil
194+
195+ newRunner , err := e .newNPXRunnerCache .GetOrCreate (name , func () (func (... any ) (Runner , error ), error ) {
196+ type tryFunc func () func (... any ) (Runner , error )
197+ tryFuncs := map [binaryLocation ]tryFunc {
198+ binaryLocationNodeModules : func () func (... any ) (Runner , error ) {
199+ nodeBinFilename := filepath .Join (e .workingDir , nodeModulesBinPath , name )
200+ _ , err := safeexec .LookPath (nodeBinFilename )
201+ if err != nil {
202+ return nil
203+ }
204+ return func (arg2 ... any ) (Runner , error ) {
205+ return e .new (name , nodeBinFilename , arg2 ... )
206+ }
207+ },
208+ binaryLocationNpx : func () func (... any ) (Runner , error ) {
209+ e .checkNpx ()
210+ if ! e .npxAvailable {
211+ return nil
212+ }
213+ return func (arg2 ... any ) (Runner , error ) {
214+ return e .npx (name , arg2 ... )
215+ }
216+ },
217+ binaryLocationPath : func () func (... any ) (Runner , error ) {
218+ if _ , err := safeexec .LookPath (name ); err != nil {
219+ return nil
220+ }
221+ return func (arg2 ... any ) (Runner , error ) {
222+ return e .New (name , arg2 ... )
223+ }
224+ },
225+ }
226+
227+ locations := []binaryLocation {binaryLocationNodeModules , binaryLocationNpx , binaryLocationPath }
228+ if name == "tailwindcss" {
229+ // See https://github.com/gohugoio/hugo/issues/13221#issuecomment-2574801253
230+ locations = []binaryLocation {binaryLocationNodeModules , binaryLocationPath , binaryLocationNpx }
174231 }
232+ for _ , loc := range locations {
233+ if f := tryFuncs [loc ](); f != nil {
234+ e .infol .Logf ("resolve %q using %s" , name , loc )
235+ return f , nil
236+ }
237+ }
238+ return nil , & NotFoundError {name : name , method : fmt .Sprintf ("in %s" , locations [len (locations )- 1 ])}
239+ })
240+ if err != nil {
241+ return nil , err
175242 }
176- return e .New (name , arg ... )
243+
244+ return newRunner (arg ... )
177245}
178246
179247const (
0 commit comments