Repository: gohugoio/hugo (language: go)

Issue:
Expose underlying error messages for postcss
When using the extended binary with postcss, I wonder if there's a way the error messages can be improved in some scenarios.  I've spent some time today a little puzzled with this error message:

```
Error: Error building site: POSTCSS: failed to transform "scss/main.css" (text/css). Check your PostCSS installation; install with "npm install postcss-cli". See https://gohugo.io/hugo-pipes/postcss/: this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information
```

After triple-checking I was using hugo extended, and had the node modules installed, I dug into the hugo source code and my issue was actually due to not having `npx` on my path (node/npm were).  It looks like any errors with the npx call get handled as if postcss is not supported.

It'd be great if we could get something like:

```
Error: Error building site: POSTCSS: npx: not found
```
where `npx: not found` is what I get when I try from a shell.



Discussion / hints:
Oh man, I know exactly how you feel. I am dealing with this same error right now, and have found 3 different root causes and suggested fixes online for it. Unfortunately, none of them fixed my problem. I hoped that turning on `--verbose` would help, but that only makes hugo tell me more about things that are working right, not about things that are breaking.
I'm getting this exact same error message, and I haven't been able to get to the bottom of it:

```
hugo version
hugo v0.115.3+extended darwin/arm64 BuildDate=unknown
```

```
which npx
/Users/sajo/.nvm/versions/node/v18.16.1/bin/npx
```

```
ls themes/hugo-product-launch/node_modules
. . . postcss-cli . . .
```
@xonev 

You are using nvm, so I am suspicious of environment variables. Hugo's [default security policy ](https://gohugo.io/about/security-model/#security-policy) with v0.115.2 and later allows access to this predefined list of environment variables:

```text
[security.exec]
osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$']
```

Please change this to:

```text
[security.exec]
osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|NPM_)$']
```

And test again. 

To allow access to all environment variables:

```text
[security.exec]
osEnv = ['.*']

### How to force hugo to use the postcss that is installed?

I am seeing this same error, adding an entry to the security.exec block had no effect. 
I have tested this with npm, with yarn
I have tested this with hugo 115.3, 112, 111 and the issue is persistent across versions.


- I am not using npx, nvm or npm but have switched to using yarn.

- Whether using npm or yarn I am seeing the error:

> _Error: error building site: POSTCSS: failed to transform "scss/main.css" (text/css). Check your PostCSS installation; install with "npm install postcss-cli". See https://gohugo.io/hugo-pipes/postcss/: this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information_

- Using `- yarn list --depth=2` in the gitlab pipeline yaml file I am able to see that all dependencies are installed globally.
- The error message seems to imply that the installed version of postcss doesn't actually work.

Since I am at kitchen sink stage of troubleshooting I will include this pipeline log snippet w
` success Saved 70 new dependencies.
info Direct dependencies
├─ autoprefixer@10.4.14
├─ postcss-cli@10.1.0
├─ postcss-reporter@7.0.5
└─ postcss@8.4.26
info All dependencies
├─ @nodelib/fs.scandir@2.1.5
├─ @nodelib/fs.stat@2.0.5
├─ @nodelib/fs.walk@1.2.8
├─ ansi-regex@5.0.1
├─ ansi-styles@4.3.0
├─ anymatch@3.1.3
├─ autoprefixer@10.4.14
├─ binary-extensions@2.2.0
├─ braces@3.0.2
├─ browserslist@4.21.9
├─ caniuse-lite@1.0.30001516
├─ chokidar@3.5.3
├─ cliui@8.0.1
├─ color-convert@2.0.1
├─ color-name@1.1.4
├─ dependency-graph@0.11.0
├─ dir-glob@3.0.1
├─ electron-to-chromium@1.4.461
├─ emoji-regex@8.0.0
├─ fast-glob@3.3.0
├─ fastq@1.15.0
├─ fill-range@7.0.1
├─ fraction.js@4.2.0
├─ fs-extra@11.1.1
├─ get-caller-file@2.0.5
├─ get-stdin@9.0.0
├─ glob-parent@5.1.2
├─ globby@13.2.2
├─ graceful-fs@4.2.11
├─ ignore@5.2.4
├─ is-binary-path@2.1.0
├─ is-extglob@2.1.1
├─ is-fullwidth-code-point@3.0.0
├─ is-glob@4.0.3
├─ is-number@7.0.0
├─ jsonfile@6.1.0
├─ lilconfig@2.1.0
├─ merge2@1.4.1
├─ micromatch@4.0.5
├─ nanoid@3.3.6
├─ node-releases@2.0.13
├─ normalize-path@3.0.0
├─ normalize-range@0.1.2
├─ path-type@4.0.0
├─ picomatch@2.3.1
├─ pify@2.3.0
├─ postcss-cli@10.1.0
├─ postcss-load-config@4.0.1
├─ postcss-reporter@7.0.5
├─ postcss-value-parser@4.2.0
├─ postcss@8.4.26
├─ pretty-hrtime@1.0.3
├─ queue-microtask@1.2.3
├─ read-cache@1.0.0
├─ readdirp@3.6.0
├─ require-directory@2.1.1
├─ reusify@1.0.4
├─ run-parallel@1.2.0
├─ slash@5.1.0
├─ source-map-js@1.0.2
├─ string-width@4.2.3
├─ strip-ansi@6.0.1
├─ thenby@1.3.4
├─ to-regex-range@5.0.1
├─ update-browserslist-db@1.0.11
├─ wrap-ansi@7.0.0
├─ y18n@5.0.8
├─ yaml@2.3.1
├─ yargs-parser@21.1.1
└─ yargs@17.7.2
Done in 2.42s.
+ echo '$ node --version'
$ node --version
+ node --version
v16.20.1
+ echo '$ hugo mod get $THEME_URL'
$ hugo mod get $THEME_URL
+ hugo mod get github.com/google/docsy@v0.7.1
hugo: downloading modules …
hugo: collected modules in 20956 ms
+ echo '$ git submodule update --init --recursive'
$ git submodule update --init --recursive
+ git submodule update --init --recursive
+ echo '$ apk add --no-cache git go curl bash'
$ apk add --no-cache git go curl bash
+ apk add --no-cache git go curl bash
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
OK: 563 MiB in 45 packages
+ echo '$ apk add --update nodejs yarn'
$ apk add --update nodejs yarn
+ apk add --update nodejs yarn
OK: 563 MiB in 45 packages
+ echo '$ yarn install --force'
$ yarn install --force
+ yarn install --force
yarn install v1.22.19`



Fix patch:
diff --git a/common/herrors/errors.go b/common/herrors/errors.go
index 4d86423629c..598c50b32fd 100644
--- a/common/herrors/errors.go
+++ b/common/herrors/errors.go
@@ -59,11 +59,34 @@ func GetGID() uint64 {
 	return n
 }
 
+// IsFeatureNotAvailableError returns true if the given error is or contains a FeatureNotAvailableError.
+func IsFeatureNotAvailableError(err error) bool {
+	return errors.Is(err, &FeatureNotAvailableError{})
+}
+
 // ErrFeatureNotAvailable denotes that a feature is unavailable.
 //
 // We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
 // and this error is used to signal those situations.
-var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")
+var ErrFeatureNotAvailable = &FeatureNotAvailableError{Cause: errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")}
+
+// FeatureNotAvailableError is an error type used to signal that a feature is not available.
+type FeatureNotAvailableError struct {
+	Cause error
+}
+
+func (e *FeatureNotAvailableError) Unwrap() error {
+	return e.Cause
+}
+
+func (e *FeatureNotAvailableError) Error() string {
+	return e.Cause.Error()
+}
+
+func (e *FeatureNotAvailableError) Is(target error) bool {
+	_, ok := target.(*FeatureNotAvailableError)
+	return ok
+}
 
 // Must panics if err != nil.
 func Must(err error) {
diff --git a/config/commonConfig.go b/config/commonConfig.go
index 2c6497b3488..5cf526708c9 100644
--- a/config/commonConfig.go
+++ b/config/commonConfig.go
@@ -148,7 +148,7 @@ func (b BuildConfig) UseResourceCache(err error) bool {
 	}
 
 	if b.UseResourceCacheWhen == "fallback" {
-		return err == herrors.ErrFeatureNotAvailable
+		return herrors.IsFeatureNotAvailableError(err)
 	}
 
 	return true
diff --git a/resources/resource_transformers/babel/babel.go b/resources/resource_transformers/babel/babel.go
index 5f8fcb00f7e..2999d73cb05 100644
--- a/resources/resource_transformers/babel/babel.go
+++ b/resources/resource_transformers/babel/babel.go
@@ -181,7 +181,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
 	if err != nil {
 		if hexec.IsNotFound(err) {
 			// This may be on a CI server etc. Will fall back to pre-built assets.
-			return herrors.ErrFeatureNotAvailable
+			return &herrors.FeatureNotAvailableError{Cause: err}
 		}
 		return err
 	}
@@ -200,7 +200,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
 	err = cmd.Run()
 	if err != nil {
 		if hexec.IsNotFound(err) {
-			return herrors.ErrFeatureNotAvailable
+			return &herrors.FeatureNotAvailableError{Cause: err}
 		}
 		return fmt.Errorf(errBuf.String()+": %w", err)
 	}
diff --git a/resources/resource_transformers/postcss/postcss.go b/resources/resource_transformers/postcss/postcss.go
index 0836072468e..a65fa3783a0 100644
--- a/resources/resource_transformers/postcss/postcss.go
+++ b/resources/resource_transformers/postcss/postcss.go
@@ -205,7 +205,7 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
 	if err != nil {
 		if hexec.IsNotFound(err) {
 			// This may be on a CI server etc. Will fall back to pre-built assets.
-			return herrors.ErrFeatureNotAvailable
+			return &herrors.FeatureNotAvailableError{Cause: err}
 		}
 		return err
 	}
@@ -240,7 +240,9 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
 	err = cmd.Run()
 	if err != nil {
 		if hexec.IsNotFound(err) {
-			return herrors.ErrFeatureNotAvailable
+			return &herrors.FeatureNotAvailableError{
+				Cause: err,
+			}
 		}
 		return imp.toFileError(errBuf.String())
 	}
diff --git a/resources/transform.go b/resources/transform.go
index 9e5b576257f..eb7d8fb0b3c 100644
--- a/resources/transform.go
+++ b/resources/transform.go
@@ -449,7 +449,7 @@ func (r *resourceAdapter) transform(publish, setContent bool) error {
 		newErr := func(err error) error {
 			msg := fmt.Sprintf("%s: failed to transform %q (%s)", strings.ToUpper(tr.Key().Name), tctx.InPath, tctx.InMediaType.Type)
 
-			if err == herrors.ErrFeatureNotAvailable {
+			if herrors.IsFeatureNotAvailableError(err) {
 				var errMsg string
 				if tr.Key().Name == "postcss" {
 					// This transformation is not available in this


Test patch:
diff --git a/common/herrors/errors_test.go b/common/herrors/errors_test.go
index 1e07300284e..223782e23ad 100644
--- a/common/herrors/errors_test.go
+++ b/common/herrors/errors_test.go
@@ -14,6 +14,7 @@
 package herrors
 
 import (
+	"errors"
 	"fmt"
 	"testing"
 
@@ -34,3 +35,12 @@ func TestIsNotExist(t *testing.T) {
 	// os.IsNotExist returns false for wrapped errors.
 	c.Assert(IsNotExist(fmt.Errorf("foo: %w", afero.ErrFileNotFound)), qt.Equals, true)
 }
+
+func TestIsFeatureNotAvailableError(t *testing.T) {
+	c := qt.New(t)
+
+	c.Assert(IsFeatureNotAvailableError(ErrFeatureNotAvailable), qt.Equals, true)
+	c.Assert(IsFeatureNotAvailableError(&FeatureNotAvailableError{}), qt.Equals, true)
+	c.Assert(IsFeatureNotAvailableError(errors.New("asdf")), qt.Equals, false)
+
+}
diff --git a/resources/resource_transformers/postcss/integration_test.go b/resources/resource_transformers/postcss/integration_test.go
index c920fe17d46..74aaa2661c7 100644
--- a/resources/resource_transformers/postcss/integration_test.go
+++ b/resources/resource_transformers/postcss/integration_test.go
@@ -168,6 +168,25 @@ func TestTransformPostCSSError(t *testing.T) {
 
 }
 
+func TestTransformPostCSSNotInstalledError(t *testing.T) {
+	if !htesting.IsCI() {
+		t.Skip("Skip long running test when running locally")
+	}
+
+	c := qt.New(t)
+
+	s, err := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           c,
+			NeedsOsFS:   true,
+			TxtarString: postCSSIntegrationTestFiles,
+		}).BuildE()
+
+	s.AssertIsFileError(err)
+	c.Assert(err.Error(), qt.Contains, `binary with name "npx" not found`)
+
+}
+
 // #9895
 func TestTransformPostCSSImportError(t *testing.T) {
 	if !htesting.IsCI() {