diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec1e73ae8..c1d7031be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,10 @@ jobs: cache-dependency-path: | **/go.sum **/go.mod + - name: Install Node + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: "22" - name: Install Ruby uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0 with: diff --git a/common/hexec/exec.go b/common/hexec/exec.go index 79d48a483..baec66086 100644 --- a/common/hexec/exec.go +++ b/common/hexec/exec.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Hugo Authors. All rights reserved. +// Copyright 2026 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ package hexec import ( + "bufio" "bytes" "context" "errors" @@ -23,8 +24,8 @@ import ( "os/exec" "path/filepath" "regexp" + "slices" "strings" - "sync" "github.com/bep/logg" "github.com/gohugoio/hugo/common/hmaps" @@ -98,11 +99,11 @@ func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec { } return &Exec{ - sc: cfg, - workingDir: workingDir, - infol: log.InfoCommand("exec"), - baseEnviron: baseEnviron, - newNPXRunnerCache: hmaps.NewCache[string, func(arg ...any) (Runner, error)](), + sc: cfg, + workingDir: workingDir, + infol: log.InfoCommand("exec"), + baseEnviron: baseEnviron, + nodeRunnerCache: hmaps.NewCache[string, func(arg ...any) (Runner, error)](), } } @@ -121,9 +122,16 @@ type Exec struct { // os.Environ filtered by the Exec.OsEnviron whitelist filter. baseEnviron []string - newNPXRunnerCache *hmaps.Cache[string, func(arg ...any) (Runner, error)] - npxInit sync.Once - npxAvailable bool + // Additional absolute paths to allow reading from in the Node.js permission model. + nodeReadPaths []string + + nodeRunnerCache *hmaps.Cache[string, func(arg ...any) (Runner, error)] +} + +// SetNodeReadPaths sets additional absolute paths to allow reading from +// in the Node.js permission model (e.g. Hugo module cache directories). +func (e *Exec) SetNodeReadPaths(paths []string) { + e.nodeReadPaths = paths } func (e *Exec) New(name string, arg ...any) (Runner, error) { @@ -155,8 +163,6 @@ func (b binaryLocation) String() string { switch b { case binaryLocationNodeModules: return "node_modules/.bin" - case binaryLocationNpx: - return "npx" case binaryLocationPath: return "PATH" } @@ -165,64 +171,55 @@ func (b binaryLocation) String() string { 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 . -// 3. Fall back to the PATH. -// If name is "tailwindcss", we will try the PATH as the second option. +// Npx finds and runs a Node.js tool. The binary is located first in +// WORKINGDIR/node_modules/.bin, then in PATH. The tool is always invoked via +// "node [--permission ]