]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
hugofs: Make node_modules a "special case" mount
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 24 Oct 2025 08:36:32 +0000 (10:36 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 24 Oct 2025 15:12:46 +0000 (17:12 +0200)
For this and similar mounts in a theme:

```toml
[[module.mounts]]
source = 'node_modules/bootstrap'
target = 'assets/vendor/bootstrap'
```

We first check the theme itself, then the project root.

For backwards compatibility, we also make any `../../node_modules/...` `source` paths into `node_modules/...` paths when defined in themes/modules.

Fixes #14089

hugofs/hugofs_integration_test.go
modules/collect.go

index 1be1384f07829a6b8f1d09a474fa2129820cf2ae..e3fcfdcb0299b236f3d6a58d6319733c0cb73b54 100644 (file)
@@ -14,6 +14,7 @@
 package hugofs_test
 
 import (
+       "strings"
        "testing"
 
        qt "github.com/frankban/quicktest"
@@ -44,3 +45,42 @@ All.
        b.Assert(err, qt.IsNotNil)
        b.Assert(err.Error(), qt.Contains, "mount source must be a local path for modules/themes")
 }
+
+// Issue 14089.
+func TestMountNodeMoudulesFromTheme(t *testing.T) {
+       filesTemplate := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "rss"]
+theme = "mytheme"
+-- node_modules/bootstrap/foo.txt --
+foo project.
+-- layouts/all.html --
+{{ $foo := resources.Get "vendor/bootstrap/foo.txt" }}
+Foo: {{ with $foo }}{{ .Content }}{{ else }}Fail{{ end }}
+-- themes/mytheme/hugo.toml --
+[[module.mounts]]
+source = 'NODE_MODULES_SOURCE' # tries first in theme, then in project root
+target = 'assets/vendor/bootstrap'
+
+`
+       runFiles := func(files string) *hugolib.IntegrationTestBuilder {
+               return hugolib.Test(t, files, hugolib.TestOptOsFs())
+       }
+       files := strings.ReplaceAll(filesTemplate, "NODE_MODULES_SOURCE", "node_modules/bootstrap")
+       b := runFiles(files)
+       b.AssertFileContent("public/index.html", "Foo: foo project.")
+
+       // This is for backwards compatibility. ../../node_modules/bootstrap works exactly the same as node_modules/bootstrap.
+       files = strings.ReplaceAll(filesTemplate, "NODE_MODULES_SOURCE", "../../node_modules/bootstrap")
+       b = runFiles(files)
+       b.AssertFileContent("public/index.html", "Foo: foo project.")
+
+       files = strings.ReplaceAll(filesTemplate, "NODE_MODULES_SOURCE", "node_modules/bootstrap")
+       files += `
+-- themes/mytheme/node_modules/bootstrap/foo.txt --
+foo theme.
+`
+
+       b = runFiles(files)
+       b.AssertFileContent("public/index.html", "Foo: foo theme.")
+}
index 5f3a39e02a8cea737c6f0d4ca1ac47940f288d81..560c61d6a1baa551cf49992338397f444c653dfd 100644 (file)
@@ -695,6 +695,19 @@ func (c *collector) mountCommonJSConfig(owner *moduleAdapter, mounts []Mount) ([
        return mounts, nil
 }
 
+func (c *collector) nodeModulesRoot(s string) string {
+       s = filepath.ToSlash(s)
+       if strings.HasPrefix(s, "node_modules/") {
+               return s
+       }
+       if strings.HasPrefix(s, "../../node_modules/") {
+               // See #14083. This was a common construct to mount node_modules from the project root.
+               // This started failing in v0.152.0 when we tightened the validation.
+               return strings.TrimPrefix(s, "../../")
+       }
+       return ""
+}
+
 func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
        var out []Mount
        dir := owner.Dir()
@@ -706,6 +719,17 @@ func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mou
                        return nil, errors.New(errMsg + ": both source and target must be set")
                }
 
+               // Special case for node_modules imports in themes/modules.
+               // See #14089.
+               var isModuleNodeModulesImport bool
+               if !owner.projectMod {
+                       nodeModulesImportSource := c.nodeModulesRoot(mnt.Source)
+                       if nodeModulesImportSource != "" {
+                               isModuleNodeModulesImport = true
+                               mnt.Source = nodeModulesImportSource
+                       }
+               }
+
                mnt.Source = filepath.Clean(mnt.Source)
                mnt.Target = filepath.Clean(mnt.Target)
                var sourceDir string
@@ -741,9 +765,18 @@ func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mou
                                }
                                f.Close()
                        } else {
-                               // TODO(bep) commenting out for now, as this will create to much noise.
-                               // c.logger.Warnf("module %q: mount source %q does not exist", owner.Path(), sourceDir)
-                               continue
+                               if isModuleNodeModulesImport {
+                                       // A module imported a path inside node_modules, but it didn't exist.
+                                       // Make this a special case and also try relative to the project root.
+                                       sourceDir = filepath.Join(c.ccfg.WorkingDir, mnt.Source)
+                                       _, err := c.fs.Stat(sourceDir)
+                                       if err != nil {
+                                               continue
+                                       }
+                                       mnt.Source = sourceDir
+                               } else {
+                                       continue
+                               }
                        }
                }