Skip to content

Commit 809ebe0

Browse files
committed
hugofs: Make node_modules a "special case" mount
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
1 parent 08a0679 commit 809ebe0

File tree

2 files changed

+76
-3
lines changed

2 files changed

+76
-3
lines changed

‎hugofs/hugofs_integration_test.go‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package hugofs_test
1515

1616
import (
17+
"strings"
1718
"testing"
1819

1920
qt "github.com/frankban/quicktest"
@@ -44,3 +45,42 @@ All.
4445
b.Assert(err, qt.IsNotNil)
4546
b.Assert(err.Error(), qt.Contains, "mount source must be a local path for modules/themes")
4647
}
48+
49+
// Issue 14089.
50+
func TestMountNodeMoudulesFromTheme(t *testing.T) {
51+
filesTemplate := `
52+
-- hugo.toml --
53+
disableKinds = ["taxonomy", "term", "rss"]
54+
theme = "mytheme"
55+
-- node_modules/bootstrap/foo.txt --
56+
foo project.
57+
-- layouts/all.html --
58+
{{ $foo := resources.Get "vendor/bootstrap/foo.txt" }}
59+
Foo: {{ with $foo }}{{ .Content }}{{ else }}Fail{{ end }}
60+
-- themes/mytheme/hugo.toml --
61+
[[module.mounts]]
62+
source = 'NODE_MODULES_SOURCE' # tries first in theme, then in project root
63+
target = 'assets/vendor/bootstrap'
64+
65+
`
66+
runFiles := func(files string) *hugolib.IntegrationTestBuilder {
67+
return hugolib.Test(t, files, hugolib.TestOptOsFs())
68+
}
69+
files := strings.ReplaceAll(filesTemplate, "NODE_MODULES_SOURCE", "node_modules/bootstrap")
70+
b := runFiles(files)
71+
b.AssertFileContent("public/index.html", "Foo: foo project.")
72+
73+
// This is for backwards compatibility. ../../node_modules/bootstrap works exactly the same as node_modules/bootstrap.
74+
files = strings.ReplaceAll(filesTemplate, "NODE_MODULES_SOURCE", "../../node_modules/bootstrap")
75+
b = runFiles(files)
76+
b.AssertFileContent("public/index.html", "Foo: foo project.")
77+
78+
files = strings.ReplaceAll(filesTemplate, "NODE_MODULES_SOURCE", "node_modules/bootstrap")
79+
files += `
80+
-- themes/mytheme/node_modules/bootstrap/foo.txt --
81+
foo theme.
82+
`
83+
84+
b = runFiles(files)
85+
b.AssertFileContent("public/index.html", "Foo: foo theme.")
86+
}

‎modules/collect.go‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,19 @@ func (c *collector) mountCommonJSConfig(owner *moduleAdapter, mounts []Mount) ([
695695
return mounts, nil
696696
}
697697

698+
func (c *collector) nodeModulesRoot(s string) string {
699+
s = filepath.ToSlash(s)
700+
if strings.HasPrefix(s, "node_modules/") {
701+
return s
702+
}
703+
if strings.HasPrefix(s, "../../node_modules/") {
704+
// See #14083. This was a common construct to mount node_modules from the project root.
705+
// This started failing in v0.152.0 when we tightened the validation.
706+
return strings.TrimPrefix(s, "../../")
707+
}
708+
return ""
709+
}
710+
698711
func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
699712
var out []Mount
700713
dir := owner.Dir()
@@ -706,6 +719,17 @@ func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mou
706719
return nil, errors.New(errMsg + ": both source and target must be set")
707720
}
708721

722+
// Special case for node_modules imports in themes/modules.
723+
// See #14089.
724+
var isModuleNodeModulesImport bool
725+
if !owner.projectMod {
726+
nodeModulesImportSource := c.nodeModulesRoot(mnt.Source)
727+
if nodeModulesImportSource != "" {
728+
isModuleNodeModulesImport = true
729+
mnt.Source = nodeModulesImportSource
730+
}
731+
}
732+
709733
mnt.Source = filepath.Clean(mnt.Source)
710734
mnt.Target = filepath.Clean(mnt.Target)
711735
var sourceDir string
@@ -741,9 +765,18 @@ func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mou
741765
}
742766
f.Close()
743767
} else {
744-
// TODO(bep) commenting out for now, as this will create to much noise.
745-
// c.logger.Warnf("module %q: mount source %q does not exist", owner.Path(), sourceDir)
746-
continue
768+
if isModuleNodeModulesImport {
769+
// A module imported a path inside node_modules, but it didn't exist.
770+
// Make this a special case and also try relative to the project root.
771+
sourceDir = filepath.Join(c.ccfg.WorkingDir, mnt.Source)
772+
_, err := c.fs.Stat(sourceDir)
773+
if err != nil {
774+
continue
775+
}
776+
mnt.Source = sourceDir
777+
} else {
778+
continue
779+
}
747780
}
748781
}
749782

0 commit comments

Comments
 (0)