@@ -26,7 +26,10 @@ import (
26
26
"strings"
27
27
"sync"
28
28
29
+ "github.com/bep/logg"
29
30
"github.com/cli/safeexec"
31
+ "github.com/gohugoio/hugo/common/loggers"
32
+ "github.com/gohugoio/hugo/common/maps"
30
33
"github.com/gohugoio/hugo/config"
31
34
"github.com/gohugoio/hugo/config/security"
32
35
)
@@ -86,7 +89,7 @@ var WithEnviron = func(env []string) func(c *commandeer) {
86
89
}
87
90
88
91
// 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 {
90
93
var baseEnviron []string
91
94
for _ , v := range os .Environ () {
92
95
k , _ := config .SplitEnvVar (v )
@@ -96,9 +99,11 @@ func New(cfg security.Config, workingDir string) *Exec {
96
99
}
97
100
98
101
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 )](),
102
107
}
103
108
}
104
109
@@ -124,12 +129,14 @@ func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
124
129
type Exec struct {
125
130
sc security.Config
126
131
workingDir string
132
+ infol logg.LevelLogger
127
133
128
134
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
129
135
baseEnviron []string
130
136
131
- npxInit sync.Once
132
- npxAvailable bool
137
+ newNPXRunnerCache * maps.Cache [string , func (arg ... any ) (Runner , error )]
138
+ npxInit sync.Once
139
+ npxAvailable bool
133
140
}
134
141
135
142
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,
155
162
return cm .command (arg ... )
156
163
}
157
164
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
+
158
185
// Npx will in order:
159
186
// 1. Try fo find the binary in the WORKINGDIR/node_modules/.bin directory.
160
187
// 2. If not found, and npx is available, run npx --no-install <name> <args>.
161
188
// 3. Fall back to the PATH.
189
+ // If name is "tailwindcss", we will try the PATH as the second option.
162
190
func (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
168
193
}
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 }
174
231
}
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
175
242
}
176
- return e .New (name , arg ... )
243
+
244
+ return newRunner (arg ... )
177
245
}
178
246
179
247
const (
0 commit comments