From: Bjørn Erik Pedersen Date: Fri, 24 Oct 2025 08:36:32 +0000 (+0200) Subject: hugofs: Make node_modules a "special case" mount X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=809ebe01fa954a44a48c265f0c9a4e4845b9a0f8;p=brevno-suite%2Fhugo 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 --- diff --git a/hugofs/hugofs_integration_test.go b/hugofs/hugofs_integration_test.go index 1be1384f0..e3fcfdcb0 100644 --- a/hugofs/hugofs_integration_test.go +++ b/hugofs/hugofs_integration_test.go @@ -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.") +} diff --git a/modules/collect.go b/modules/collect.go index 5f3a39e02..560c61d6a 100644 --- a/modules/collect.go +++ b/modules/collect.go @@ -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 + } } }