tocss/scss: Improve SCSS project vs themes import resolution
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 31 Jul 2018 07:34:56 +0000 (09:34 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 31 Jul 2018 08:54:10 +0000 (10:54 +0200)
Before this commit, only SASS/SCSS  components imported from main.scss at first level can be overwritten by homonymous files in projects or over-preceding theme components.

This commit fixes that by implementing a custom import resolver which will be tried first. This resolver will make sure that the project/theme hierarchy is always respected.

Fixes #5008

hugolib/resource_chain_test.go
resource/tocss/scss/tocss.go

index 61ae7e611263d8710b505f900e60c9dbd7904590..3d13d3912dddf8d830c1be73bb7005d833db6f75 100644 (file)
@@ -79,6 +79,76 @@ T1: {{ $r.Content }}
 
 }
 
+func TestSCSSWithThemeOverrides(t *testing.T) {
+       if !scss.Supports() {
+               t.Skip("Skip SCSS")
+       }
+       assert := require.New(t)
+       workDir, clean, err := createTempDir("hugo-scss-include")
+       assert.NoError(err)
+       defer clean()
+
+       theme := "mytheme"
+       themesDir := filepath.Join(workDir, "themes")
+       themeDirs := filepath.Join(themesDir, theme)
+       v := viper.New()
+       v.Set("workingDir", workDir)
+       v.Set("theme", theme)
+       b := newTestSitesBuilder(t).WithLogger(loggers.NewWarningLogger())
+       b.WithViper(v)
+       b.WithWorkingDir(workDir)
+       // Need to use OS fs for this.
+       b.Fs = hugofs.NewDefault(v)
+
+       fooDir := filepath.Join(workDir, "node_modules", "foo")
+       scssDir := filepath.Join(workDir, "assets", "scss")
+       scssThemeDir := filepath.Join(themeDirs, "assets", "scss")
+       assert.NoError(os.MkdirAll(fooDir, 0777))
+       assert.NoError(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777))
+       assert.NoError(os.MkdirAll(filepath.Join(workDir, "data"), 0777))
+       assert.NoError(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777))
+       assert.NoError(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777))
+       assert.NoError(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777))
+       assert.NoError(os.MkdirAll(filepath.Join(scssDir, "components"), 0777))
+       assert.NoError(os.MkdirAll(filepath.Join(scssThemeDir, "components"), 0777))
+
+       b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_imports.scss"), `
+@import "moo";
+
+`)
+
+       b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_moo.scss"), `
+$moolor: #fff;
+
+moo {
+  color: $moolor;
+}
+`)
+
+       b.WithSourceFile(filepath.Join(scssThemeDir, "main.scss"), `
+@import "components/imports";
+
+`)
+
+       b.WithSourceFile(filepath.Join(scssDir, "components", "_moo.scss"), `
+$moolor: #ccc;
+
+moo {
+  color: $moolor;
+}
+`)
+
+       b.WithTemplatesAdded("index.html", `
+{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) ) }}
+{{ $r := resources.Get "scss/main.scss" |  toCSS $cssOpts  | minify  }}
+T1: {{ $r.Content }}
+`)
+       b.Build(BuildCfg{})
+
+       b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: moo{color:#ccc}`)
+
+}
+
 func TestResourceChain(t *testing.T) {
        t.Parallel()
 
index 715d5fd9f5795c62a9090cbe3c818e6d8d690d30..c50b054b774f0ddd62101074de7af574396606a7 100644 (file)
@@ -26,6 +26,7 @@ import (
        "github.com/bep/go-tocss/scss/libsass"
        "github.com/bep/go-tocss/tocss"
        "github.com/gohugoio/hugo/helpers"
+       "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/media"
        "github.com/gohugoio/hugo/resource"
 )
@@ -48,14 +49,64 @@ func (t *toCSSTransformation) Transform(ctx *resource.ResourceTransformationCtx)
        outName = path.Base(ctx.OutPath)
 
        options := t.options
-
-       options.to.IncludePaths = t.c.sfs.RealDirs(path.Dir(ctx.SourcePath))
+       baseDir := path.Dir(ctx.SourcePath)
+       options.to.IncludePaths = t.c.sfs.RealDirs(baseDir)
 
        // Append any workDir relative include paths
        for _, ip := range options.from.IncludePaths {
                options.to.IncludePaths = append(options.to.IncludePaths, t.c.workFs.RealDirs(filepath.Clean(ip))...)
        }
 
+       // To allow for overrides of SCSS files anywhere in the project/theme hierarchy, we need
+       // to help libsass revolve the filename by looking in the composite filesystem first.
+       // We add the entry directories for both project and themes to the include paths list, but
+       // that only work for overrides on the top level.
+       options.to.ImportResolver = func(url string, prev string) (newUrl string, body string, resolved bool) {
+               // We get URL paths from LibSASS, but we need file paths.
+               url = filepath.FromSlash(url)
+               prev = filepath.FromSlash(prev)
+
+               var basePath string
+               urlDir := filepath.Dir(url)
+               var prevDir string
+               if prev == "stdin" {
+                       prevDir = baseDir
+               } else {
+                       prevDir = t.c.sfs.MakePathRelative(filepath.Dir(prev))
+                       if prevDir == "" {
+                               // Not a member of this filesystem. Let LibSASS handle it.
+                               return "", "", false
+                       }
+               }
+
+               basePath = filepath.Join(prevDir, urlDir)
+               name := filepath.Base(url)
+
+               // Libsass throws an error in cases where you have several possible candidates.
+               // We make this simpler and pick the first match.
+               var namePatterns []string
+               if strings.Contains(name, ".") {
+                       namePatterns = []string{"_%s", "%s"}
+               } else if strings.HasPrefix(name, "_") {
+                       namePatterns = []string{"_%s.scss", "_%s.sass"}
+               } else {
+                       namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass"}
+               }
+
+               for _, namePattern := range namePatterns {
+                       filenameToCheck := filepath.Join(basePath, fmt.Sprintf(namePattern, name))
+                       fi, err := t.c.sfs.Fs.Stat(filenameToCheck)
+                       if err == nil {
+                               if fir, ok := fi.(hugofs.RealFilenameInfo); ok {
+                                       return fir.RealFilename(), "", true
+                               }
+                       }
+               }
+
+               // Not found, let LibSASS handle it
+               return "", "", false
+       }
+
        if ctx.InMediaType.SubType == media.SASSType.SubType {
                options.to.SassSyntax = true
        }