package hugofs_test
import (
+ "strings"
"testing"
qt "github.com/frankban/quicktest"
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.")
+}
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()
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
}
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
+ }
}
}