common/hexec: Make NODE_PATH a fallback for ESM bare imports

Node's ESM resolver does not consult NODE_PATH (unlike CJS require), so
an ESM postcss.config.js shipped by a Hugo theme fails when loaded from
the module cache: bare imports like `import x from "postcss-import"`
have no node_modules to walk up to.

Install a synchronous resolver hook (module.registerHooks) via
--import=data:... on every Node invocation. On ERR_MODULE_NOT_FOUND for
a bare specifier it resolves the package from each NODE_PATH entry via
createRequire().resolve(). No-op for relative, absolute, URL-scheme and
non-MODULE_NOT_FOUND failures. Synchronous hooks run on the main thread,
so no --allow-worker is needed under the Node permission model.

Fixes #13987

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bjørn Erik Pedersen
2026-05-13 17:05:09 +02:00
parent 123018de21
commit ae7bf74b3e
4 changed files with 157 additions and 0 deletions
+4
View File
@@ -234,6 +234,10 @@ func (e *Exec) newNode(name, scriptPath string, arg ...any) (Runner, error) {
for _, pa := range e.nodePermissionArgs(name, scriptPath) {
allArgs = append(allArgs, pa)
}
// Install an ESM resolver hook that makes NODE_PATH a fallback for failed
// bare imports, so postcss.config.js / babel.config.js / etc. written in
// ESM work when loaded from the Hugo module cache. See esmloader.mjs.
allArgs = append(allArgs, nodeESMLoaderImportArg())
allArgs = append(allArgs, scriptPath)
allArgs = append(allArgs, arg...)
// When the script lives outside the working dir (a globally installed