diff --git a/.travis.yml b/.travis.yml index 10756e14d27..e25ebd65a46 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,13 +46,25 @@ cache: - $HOME/AppData/Local/go-build before_install: + - mkdir -p $HOME/bin + - export PATH="$HOME/bin":"$PATH"; + - echo "Home is $HOME"; - df -h - # https://travis-ci.community/t/go-cant-find-gcc-with-go1-11-1-on-windows/293/5 - if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install mingw -y; choco install -y --force nodejs; + curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.5/sass_embedded-1.0.0-beta.5-windows-x64.zip; + unzip sass_embedded-1.0.0-beta.5-windows-x64.zip -d $HOME/bin; export PATH=/c/tools/mingw64/bin:"$PATH"; fi + - if [ "$TRAVIS_OS_NAME" = "osx" ]; then + curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.5/sass_embedded-1.0.0-beta.5-macos-x64.tar.gz; + tar -xvf sass_embedded-1.0.0-beta.5-macos-x64.tar.gz -C $HOME/bin; + fi + - if [ "$TRAVIS_OS_NAME" = "linux" ]; then + curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.5/sass_embedded-1.0.0-beta.5-linux-x64.tar.gz; + tar -xvf sass_embedded-1.0.0-beta.5-linux-x64.tar.gz -C $HOME/bin; + fi - gem install asciidoctor - type asciidoctor diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go index 118ab851cf5..586a8c827c1 100644 --- a/common/herrors/error_locator.go +++ b/common/herrors/error_locator.go @@ -70,7 +70,7 @@ func (e ErrorContext) Position() text.Position { var _ causer = (*ErrorWithFileContext)(nil) -// ErrorWithFileContext is an error with some additional file context related +// ErrorWithFileCoxntext is an error with some additional file context related // to that error. type ErrorWithFileContext struct { cause error diff --git a/deps/deps.go b/deps/deps.go index c2919c9c55e..cacc1923148 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -94,6 +94,9 @@ type Deps struct { // BuildStartListeners will be notified before a build starts. BuildStartListeners *Listeners + // Resources that gets closed when the build is done or the server shuts down. + BuildClosers *Closers + // Atomic values set during a build. // This is common/global for all sites. BuildState *BuildState @@ -284,6 +287,7 @@ func New(cfg DepsCfg) (*Deps, error) { Site: cfg.Site, FileCaches: fileCaches, BuildStartListeners: &Listeners{}, + BuildClosers: &Closers{}, BuildState: buildState, Running: cfg.Running, Timeout: time.Duration(timeoutms) * time.Millisecond, @@ -297,6 +301,10 @@ func New(cfg DepsCfg) (*Deps, error) { return d, nil } +func (d *Deps) Close() error { + return d.BuildClosers.Close() +} + // ForLanguage creates a copy of the Deps with the language dependent // parts switched out. func (d Deps) ForLanguage(cfg DepsCfg, onCreated func(d *Deps) error) (*Deps, error) { @@ -399,3 +407,28 @@ func (b *BuildState) Incr() int { func NewBuildState() BuildState { return BuildState{} } + +type Closer interface { + Close() error +} + +type Closers struct { + mu sync.Mutex + cs []Closer +} + +func (cs *Closers) Add(c Closer) { + cs.mu.Lock() + defer cs.mu.Unlock() + cs.cs = append(cs.cs, c) +} + +func (cs *Closers) Close() error { + cs.mu.Lock() + defer cs.mu.Unlock() + for _, c := range cs.cs { + c.Close() + } + + return nil +} diff --git a/docs/content/en/hugo-pipes/scss-sass.md b/docs/content/en/hugo-pipes/scss-sass.md index 489d16e77fa..0cea1254f11 100755 --- a/docs/content/en/hugo-pipes/scss-sass.md +++ b/docs/content/en/hugo-pipes/scss-sass.md @@ -24,6 +24,11 @@ Any SASS or SCSS file can be transformed into a CSS file using `resources.ToCSS` ``` ### Options + +transpiler [string] {{< new-in "0.80.0" >}} + +: The `transpiler` to use, valid values are `libsass` (default) and `dartsass`. Note that the Embedded Dart Sass project is still in beta (beta 5 at the time of writing). The release is scheduled for Q1 2021. We will try to improve the installation process by then, but if you want to use Hugo with Dart Sass you need to download a release binary from [Embedded Dart Sass](https://github.com/sass/dart-sass-embedded/releases) and make sure it's in your PC's `$PATH` (or `%PATH%` on Windows). + targetPath [string] : If not set, the resource's target path will be the asset file original path with its extension replaced by `.css`. @@ -31,7 +36,7 @@ outputStyle [string] : Default is `nested`. Other available output styles are `expanded`, `compact` and `compressed`. precision [int] -: Precision of floating point math. +: Precision of floating point math. **Note:** This option is not supported by Dart Sass. enableSourceMap [bool] : When enabled, a source map will be generated. diff --git a/go.mod b/go.mod index d3a5b4af4e3..c8978561965 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/aws/aws-sdk-go v1.35.0 github.com/bep/debounce v1.2.0 github.com/bep/gitmap v1.1.2 + github.com/bep/godartsass v0.8.0 github.com/bep/golibsass v0.7.0 github.com/bep/tmc v0.5.1 github.com/cli/safeexec v1.0.0 diff --git a/go.sum b/go.sum index e46d649d919..79900b7e4d5 100644 --- a/go.sum +++ b/go.sum @@ -134,6 +134,8 @@ github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo= github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840= github.com/bep/gitmap v1.1.2/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY= +github.com/bep/godartsass v0.8.0 h1:zPQlC+yfX5wZIPZbvEcN6+4XWfr4BH6EvFRXnOC0Wic= +github.com/bep/godartsass v0.8.0/go.mod h1:nXQlHHk4H1ghUk6n/JkYKG5RD43yJfcfp5aHRqT/pc4= github.com/bep/golibsass v0.7.0 h1:/ocxgtPZ5rgp7FA+mktzyent+fAg82tJq4iMsTMBAtA= github.com/bep/golibsass v0.7.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= @@ -262,6 +264,7 @@ github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= @@ -839,6 +842,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index c1a4ab190b6..72bcaae533d 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -48,6 +48,11 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { // Make sure we don't trigger rebuilds in parallel. h.runningMu.Lock() defer h.runningMu.Unlock() + } else { + defer func() { + // TODO1 server close + h.Deps.BuildClosers.Close() + }() } ctx, task := trace.NewTask(context.Background(), "Build") diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index b5baa4d0bb3..bd08ad25007 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -20,6 +20,8 @@ import ( "math/rand" "os" + "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass" + "path/filepath" "runtime" "strings" @@ -45,33 +47,44 @@ import ( ) func TestSCSSWithIncludePaths(t *testing.T) { - if !scss.Supports() { - t.Skip("Skip SCSS") - } c := qt.New(t) - workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-scss-include") - c.Assert(err, qt.IsNil) - defer clean() - v := viper.New() - v.Set("workingDir", workDir) - b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger()) - // Need to use OS fs for this. - b.Fs = hugofs.NewDefault(v) - b.WithWorkingDir(workDir) - b.WithViper(v) + for _, test := range []struct { + name string + supports func() bool + }{ + {"libsass", func() bool { return scss.Supports() }}, + {"dartsass", func() bool { return dartsass.Supports() }}, + } { + + c.Run(test.name, func(c *qt.C) { + if !test.supports() { + c.Skip(fmt.Sprintf("Skip %s", test.name)) + } - fooDir := filepath.Join(workDir, "node_modules", "foo") - scssDir := filepath.Join(workDir, "assets", "scss") - c.Assert(os.MkdirAll(fooDir, 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "data"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(scssDir), 0777), qt.IsNil) - - b.WithSourceFile(filepath.Join(fooDir, "_moo.scss"), ` + workDir, clean, err := htesting.CreateTempDir(hugofs.Os, fmt.Sprintf("hugo-scss-include-%s", test.name)) + c.Assert(err, qt.IsNil) + defer clean() + + v := viper.New() + v.Set("workingDir", workDir) + b := newTestSitesBuilder(c).WithLogger(loggers.NewErrorLogger()) + // Need to use OS fs for this. + b.Fs = hugofs.NewDefault(v) + b.WithWorkingDir(workDir) + b.WithViper(v) + + fooDir := filepath.Join(workDir, "node_modules", "foo") + scssDir := filepath.Join(workDir, "assets", "scss") + c.Assert(os.MkdirAll(fooDir, 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "data"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(scssDir), 0777), qt.IsNil) + + b.WithSourceFile(filepath.Join(fooDir, "_moo.scss"), ` $moolor: #fff; moo { @@ -79,47 +92,63 @@ moo { } `) - b.WithSourceFile(filepath.Join(scssDir, "main.scss"), ` + b.WithSourceFile(filepath.Join(scssDir, "main.scss"), ` @import "moo"; `) - b.WithTemplatesAdded("index.html", ` -{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) ) }} + b.WithTemplatesAdded("index.html", fmt.Sprintf(` +{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo") "transpiler" %q ) }} {{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }} T1: {{ $r.Content }} -`) - b.Build(BuildCfg{}) +`, test.name)) + b.Build(BuildCfg{}) + + b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: moo{color:#fff}`) + }) + + } - b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: moo{color:#fff}`) } func TestSCSSWithRegularCSSImport(t *testing.T) { - if !scss.Supports() { - t.Skip("Skip SCSS") - } c := qt.New(t) - workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-scss-include") - c.Assert(err, qt.IsNil) - defer clean() - v := viper.New() - v.Set("workingDir", workDir) - b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger()) - // Need to use OS fs for this. - b.Fs = hugofs.NewDefault(v) - b.WithWorkingDir(workDir) - b.WithViper(v) + for _, test := range []struct { + name string + supports func() bool + }{ + {"libsass", func() bool { return scss.Supports() }}, + {"dartsass", func() bool { return dartsass.Supports() }}, + } { + + c.Run(test.name, func(c *qt.C) { + if !test.supports() { + c.Skip(fmt.Sprintf("Skip %s", test.name)) + } - scssDir := filepath.Join(workDir, "assets", "scss") - c.Assert(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "data"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(scssDir), 0777), qt.IsNil) + workDir, clean, err := htesting.CreateTempDir(hugofs.Os, fmt.Sprintf("hugo-scss-include-regular-%s", test.name)) + c.Assert(err, qt.IsNil) + defer clean() - b.WithSourceFile(filepath.Join(scssDir, "_moo.scss"), ` + v := viper.New() + v.Set("workingDir", workDir) + b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger()) + // Need to use OS fs for this. + b.Fs = hugofs.NewDefault(v) + b.WithWorkingDir(workDir) + b.WithViper(v) + + scssDir := filepath.Join(workDir, "assets", "scss") + c.Assert(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "data"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(scssDir), 0777), qt.IsNil) + b.WithSourceFile(filepath.Join(scssDir, "regular.css"), ``) + b.WithSourceFile(filepath.Join(scssDir, "another.css"), ``) + b.WithSourceFile(filepath.Join(scssDir, "_moo.scss"), ` $moolor: #fff; moo { @@ -127,7 +156,7 @@ moo { } `) - b.WithSourceFile(filepath.Join(scssDir, "main.scss"), ` + b.WithSourceFile(filepath.Join(scssDir, "main.scss"), ` @import "moo"; @import "regular.css"; @import "moo"; @@ -136,13 +165,17 @@ moo { /* foo */ `) - b.WithTemplatesAdded("index.html", ` -{{ $r := resources.Get "scss/main.scss" | toCSS }} + b.WithTemplatesAdded("index.html", fmt.Sprintf(` +{{ $r := resources.Get "scss/main.scss" | toCSS (dict "transpiler" %q) }} T1: {{ $r.Content | safeHTML }} -`) - b.Build(BuildCfg{}) +`, test.name)) + b.Build(BuildCfg{}) - b.AssertFileContent(filepath.Join(workDir, "public/index.html"), ` + if test.name == "libsass" { + // LibSass does not support regular CSS imports. There + // is an open bug about it that probably will never be resolved. + // Hugo works around this by preserving them in place: + b.AssertFileContent(filepath.Join(workDir, "public/index.html"), ` T1: moo { color: #fff; } @@ -154,47 +187,79 @@ moo { /* foo */ `) + } else { + // Dart Sass does not follow regular CSS import, but they + // get pulled to the top. + b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: @import "regular.css"; +@import "another.css"; +moo { + color: #fff; } -func TestSCSSWithThemeOverrides(t *testing.T) { - if !scss.Supports() { - t.Skip("Skip SCSS") +moo { + color: #fff; +} + +/* foo */`) + + } + }) } + +} + +func TestSCSSWithThemeOverrides(t *testing.T) { c := qt.New(t) - workDir, clean1, err := htesting.CreateTempDir(hugofs.Os, "hugo-scss-include") - c.Assert(err, qt.IsNil) - defer clean1() - 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.NewErrorLogger()) - // Need to use OS fs for this. - b.Fs = hugofs.NewDefault(v) - b.WithWorkingDir(workDir) - b.WithViper(v) + for _, test := range []struct { + name string + supports func() bool + }{ + {"libsass", func() bool { return scss.Supports() }}, + {"dartsass", func() bool { return dartsass.Supports() }}, + } { - fooDir := filepath.Join(workDir, "node_modules", "foo") - scssDir := filepath.Join(workDir, "assets", "scss") - scssThemeDir := filepath.Join(themeDirs, "assets", "scss") - c.Assert(os.MkdirAll(fooDir, 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "data"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(scssDir, "components"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(scssThemeDir, "components"), 0777), qt.IsNil) - - b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_imports.scss"), ` + c.Run(test.name, func(c *qt.C) { + if !test.supports() { + c.Skip(fmt.Sprintf("Skip %s", test.name)) + } + + workDir, clean1, err := htesting.CreateTempDir(hugofs.Os, fmt.Sprintf("hugo-scss-include-theme-overrides-%s", test.name)) + c.Assert(err, qt.IsNil) + defer clean1() + + 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(c).WithLogger(loggers.NewErrorLogger()) + // Need to use OS fs for this. + b.Fs = hugofs.NewDefault(v) + b.WithWorkingDir(workDir) + b.WithViper(v) + + fooDir := filepath.Join(workDir, "node_modules", "foo") + scssDir := filepath.Join(workDir, "assets", "scss") + scssThemeDir := filepath.Join(themeDirs, "assets", "scss") + c.Assert(os.MkdirAll(fooDir, 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "data"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(scssDir, "components"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(scssThemeDir, "components"), 0777), qt.IsNil) + + b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_imports.scss"), ` @import "moo"; @import "_boo"; +@import "_zoo"; + `) - b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_moo.scss"), ` + b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_moo.scss"), ` $moolor: #fff; moo { @@ -202,7 +267,16 @@ moo { } `) - b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_boo.scss"), ` + // Only in theme. + b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_zoo.scss"), ` +$zoolor: pink; + +zoo { + color: $zoolor; +} +`) + + b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_boo.scss"), ` $boolor: orange; boo { @@ -210,12 +284,12 @@ boo { } `) - b.WithSourceFile(filepath.Join(scssThemeDir, "main.scss"), ` + b.WithSourceFile(filepath.Join(scssThemeDir, "main.scss"), ` @import "components/imports"; `) - b.WithSourceFile(filepath.Join(scssDir, "components", "_moo.scss"), ` + b.WithSourceFile(filepath.Join(scssDir, "components", "_moo.scss"), ` $moolor: #ccc; moo { @@ -223,7 +297,7 @@ moo { } `) - b.WithSourceFile(filepath.Join(scssDir, "components", "_boo.scss"), ` + b.WithSourceFile(filepath.Join(scssDir, "components", "_boo.scss"), ` $boolor: green; boo { @@ -231,22 +305,43 @@ boo { } `) - b.WithTemplatesAdded("index.html", ` -{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) ) }} + b.WithTemplatesAdded("index.html", fmt.Sprintf(` +{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) "transpiler" %q ) }} {{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }} T1: {{ $r.Content }} -`) - b.Build(BuildCfg{}) +`, test.name)) + b.Build(BuildCfg{}) + + b.AssertFileContent( + filepath.Join(workDir, "public/index.html"), + `T1: moo{color:#ccc}boo{color:green}zoo{color:pink}`, + ) + }) + } - b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: moo{color:#ccc}boo{color:green}`) } // https://github.com/gohugoio/hugo/issues/6274 func TestSCSSWithIncludePathsSass(t *testing.T) { + c := qt.New(t) + + for _, test := range []struct { + name string + supports func() bool + }{ + {"libsass", func() bool { return scss.Supports() }}, + {"dartsass", func() bool { return dartsass.Supports() }}, + } { + + c.Run(test.name, func(c *qt.C) { + if !test.supports() { + c.Skip(fmt.Sprintf("Skip %s", test.name)) + } + }) + } if !scss.Supports() { t.Skip("Skip SCSS") } - c := qt.New(t) workDir, clean1, err := htesting.CreateTempDir(hugofs.Os, "hugo-scss-includepaths") c.Assert(err, qt.IsNil) defer clean1() diff --git a/magefile.go b/magefile.go index 16f630abca4..c37f1db6faf 100644 --- a/magefile.go +++ b/magefile.go @@ -181,7 +181,7 @@ func Test386() error { // Run tests func Test() error { env := map[string]string{"GOFLAGS": testGoFlags()} - return runCmd(env, goexe, "test", "./...", buildFlags(), "-tags", buildTags()) + return runCmd(env, goexe, "test", "-v", "./...", buildFlags(), "-tags", buildTags()) } // Run tests with race detector diff --git a/resources/resource_transformers/tocss/dartsass/client.go b/resources/resource_transformers/tocss/dartsass/client.go new file mode 100644 index 00000000000..1d8250dc5f1 --- /dev/null +++ b/resources/resource_transformers/tocss/dartsass/client.go @@ -0,0 +1,115 @@ +// Copyright 2020 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package godartsass integrates with the Dass Sass Embedded protocol to transpile +// SCSS/SASS. +package dartsass + +import ( + "io" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib/filesystems" + "github.com/gohugoio/hugo/resources" + "github.com/gohugoio/hugo/resources/resource" + "github.com/spf13/afero" + + "github.com/bep/godartsass" + "github.com/mitchellh/mapstructure" +) + +// used as part of the cache key. +const transformationName = "tocss-dart" + +func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error) { + if !Supports() { + return &Client{dartSassNoAvailable: true}, nil + } + transpiler, err := godartsass.Start(godartsass.Options{}) + if err != nil { + return nil, err + } + return &Client{sfs: fs, workFs: rs.BaseFs.Work, rs: rs, transpiler: transpiler}, nil +} + +type Client struct { + dartSassNoAvailable bool + rs *resources.Spec + sfs *filesystems.SourceFilesystem + workFs afero.Fs + transpiler *godartsass.Transpiler +} + +func (c *Client) ToCSS(res resources.ResourceTransformer, args map[string]interface{}) (resource.Resource, error) { + if c.dartSassNoAvailable { + return res.Transform(resources.NewFeatureNotAvailableTransformer(transformationName, args)) + } + return res.Transform(&transform{c: c, optsm: args}) +} + +func (c *Client) Close() error { + if c.transpiler == nil { + return nil + } + return c.transpiler.Close() +} + +func (c *Client) toCSS(args godartsass.Args, src io.Reader) (godartsass.Result, error) { + var res godartsass.Result + + in := helpers.ReaderToString(src) + args.Source = in + + res, err := c.transpiler.Execute(args) + if err != nil { + return res, err + } + + return res, err +} + +type Options struct { + + // Hugo, will by default, just replace the extension of the source + // to .css, e.g. "scss/main.scss" becomes "scss/main.css". You can + // control this by setting this, e.g. "styles/main.css" will create + // a Resource with that as a base for RelPermalink etc. + TargetPath string + + // Hugo automatically adds the entry directories (where the main.scss lives) + // for project and themes to the list of include paths sent to LibSASS. + // Any paths set in this setting will be appended. Note that these will be + // treated as relative to the working dir, i.e. no include paths outside the + // project/themes. + IncludePaths []string + + // Default is nested. + // One of nested, expanded, compact, compressed. + OutputStyle string + + // When enabled, Hugo will generate a source map. + EnableSourceMap bool +} + +func decodeOptions(m map[string]interface{}) (opts Options, err error) { + if m == nil { + return + } + err = mapstructure.WeakDecode(m, &opts) + + if opts.TargetPath != "" { + opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath) + } + + return +} diff --git a/resources/resource_transformers/tocss/dartsass/transform.go b/resources/resource_transformers/tocss/dartsass/transform.go new file mode 100644 index 00000000000..168b51f1e61 --- /dev/null +++ b/resources/resource_transformers/tocss/dartsass/transform.go @@ -0,0 +1,204 @@ +// Copyright 2020 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dartsass + +import ( + "fmt" + "io" + "path" + "path/filepath" + "strings" + + "github.com/gohugoio/hugo/common/herrors" + + "github.com/gohugoio/hugo/media" + + "github.com/gohugoio/hugo/resources" + + "github.com/gohugoio/hugo/resources/internal" + + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/hugofs" + + "github.com/bep/godartsass" + "github.com/cli/safeexec" +) + +// See https://github.com/sass/dart-sass-embedded/issues/24 +const stdinPlaceholder = "HUGOSTDIN" + +// Supports returns whether dart-sass-embedded is found in $PATH. +func Supports() bool { + p, err := safeexec.LookPath("dart-sass-embedded") + return err == nil && p != "" +} + +type transform struct { + optsm map[string]interface{} + c *Client +} + +func (t *transform) Key() internal.ResourceTransformationKey { + return internal.NewResourceTransformationKey(transformationName, t.optsm) +} + +func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error { + ctx.OutMediaType = media.CSSType + + opts, err := decodeOptions(t.optsm) + if err != nil { + return err + } + + if opts.TargetPath != "" { + ctx.OutPath = opts.TargetPath + } else { + ctx.ReplaceOutPathExtension(".css") + } + + baseDir := path.Dir(ctx.SourcePath) + + args := godartsass.Args{ + URL: stdinPlaceholder, + IncludePaths: t.c.sfs.RealDirs(baseDir), + ImportResolver: importResolver{ + baseDir: baseDir, + c: t.c, + }, + EnableSourceMap: opts.EnableSourceMap, + } + + // Append any workDir relative include paths + for _, ip := range opts.IncludePaths { + info, err := t.c.workFs.Stat(filepath.Clean(ip)) + if err == nil { + filename := info.(hugofs.FileMetaInfo).Meta().Filename() + args.IncludePaths = append(args.IncludePaths, filename) + } + } + + if ctx.InMediaType.SubType == media.SASSType.SubType { + args.SourceSyntax = godartsass.SourceSyntaxSASS + } + + res, err := t.c.toCSS(args, ctx.From) + if err != nil { + if sassErr, ok := err.(godartsass.SassError); ok { + start := sassErr.Span.Start + context := strings.TrimSpace(sassErr.Span.Context) + filename := filepath.FromSlash(removeFileScheme(sassErr.Span.Url)) + if filename == stdinPlaceholder { + if ctx.SourcePath == "" { + return sassErr + } + filename = t.c.sfs.RealFilename(ctx.SourcePath) + } + + offsetMatcher := func(m herrors.LineMatcher) bool { + return m.Offset+len(m.Line) >= start.Offset && strings.Contains(m.Line, context) + } + + ferr, ok := herrors.WithFileContextForFile( + herrors.NewFileError("scss", -1, -1, start.Column, sassErr), + filename, + filename, + hugofs.Os, + offsetMatcher) + + if !ok { + return sassErr + } + + return ferr + } + return err + } + + out := res.CSS + + _, err = io.WriteString(ctx.To, out) + if err != nil { + return err + } + + if opts.EnableSourceMap && res.SourceMap != "" { + if err := ctx.PublishSourceMap(res.SourceMap); err != nil { + return err + } + _, err = fmt.Fprintf(ctx.To, "\n\n/*# sourceMappingURL=%s */", path.Base(ctx.OutPath)+".map") + } + + return err +} + +type importResolver struct { + baseDir string + c *Client +} + +func removeFileScheme(url string) string { + return path.Clean(strings.TrimPrefix(url, "file:")) +} + +func (t importResolver) CanonicalizeURL(url string) string { + filePath := filepath.FromSlash(removeFileScheme(url)) + var prevDir string + var pathDir string + if strings.HasPrefix(url, "file:") { + var found bool + prevDir, found = t.c.sfs.MakePathRelative(filepath.Dir(filePath)) + + if !found { + // Not a member of this filesystem, let Dart Sass handle it. + return "" + } + } else { + prevDir = t.baseDir + pathDir = path.Dir(url) + } + + basePath := filepath.Join(prevDir, pathDir) + name := path.Base(filePath) + + // 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"} + } + + name = strings.TrimPrefix(name, "_") + + for _, namePattern := range namePatterns { + filenameToCheck := filepath.Join(basePath, fmt.Sprintf(namePattern, name)) + fi, err := t.c.sfs.Fs.Stat(filenameToCheck) + if err == nil { + if fim, ok := fi.(hugofs.FileMetaInfo); ok { + return "file://" + filepath.ToSlash(fim.Meta().Filename()) + } + } + } + + // Not found, let Dart Dass handle it + return "" +} + +func (t importResolver) Load(url string) string { + b, _ := afero.ReadFile(hugofs.Os, filepath.FromSlash(removeFileScheme(url))) + return string(b) +} diff --git a/resources/transform.go b/resources/transform.go index 9007ead180f..f276f00c39b 100644 --- a/resources/transform.go +++ b/resources/transform.go @@ -411,6 +411,9 @@ func (r *resourceAdapter) transform(publish, setContent bool) error { errMsg = ". Check your PostCSS installation; install with \"npm install postcss-cli\". See https://gohugo.io/hugo-pipes/postcss/" } else if tr.Key().Name == "tocss" { errMsg = ". Check your Hugo installation; you need the extended version to build SCSS/SASS." + } else if tr.Key().Name == "tocss-dart" { + errMsg = ". You need dart-sass-embedded in your system $PATH." + } else if tr.Key().Name == "babel" { errMsg = ". You need to install Babel, see https://gohugo.io/hugo-pipes/babel/" } @@ -442,6 +445,9 @@ func (r *resourceAdapter) transform(publish, setContent bool) error { if tryFileCache { f := r.target.tryTransformedFileCache(key, updates) if f == nil { + if err != nil { + return newErr(err) + } return newErr(errors.Errorf("resource %q not found in file cache", key)) } transformedContentr = f diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go index 73f3743b6d9..da2b4ca3b35 100644 --- a/tpl/resources/resources.go +++ b/tpl/resources/resources.go @@ -15,9 +15,12 @@ package resources import ( - "errors" "fmt" "path/filepath" + "sync" + + "github.com/gohugoio/hugo/common/maps" + "github.com/pkg/errors" "github.com/gohugoio/hugo/tpl/internal/resourcehelpers" @@ -35,7 +38,9 @@ import ( "github.com/gohugoio/hugo/resources/resource_transformers/minifier" "github.com/gohugoio/hugo/resources/resource_transformers/postcss" "github.com/gohugoio/hugo/resources/resource_transformers/templates" + "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass" "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss" + "github.com/spf13/cast" ) @@ -56,15 +61,15 @@ func New(deps *deps.Deps) (*Namespace, error) { } return &Namespace{ - deps: deps, - scssClient: scssClient, - createClient: create.New(deps.ResourceSpec), - bundlerClient: bundler.New(deps.ResourceSpec), - integrityClient: integrity.New(deps.ResourceSpec), - minifyClient: minifyClient, - postcssClient: postcss.New(deps.ResourceSpec), - templatesClient: templates.New(deps.ResourceSpec, deps), - babelClient: babel.New(deps.ResourceSpec), + deps: deps, + scssClientLibSass: scssClient, + createClient: create.New(deps.ResourceSpec), + bundlerClient: bundler.New(deps.ResourceSpec), + integrityClient: integrity.New(deps.ResourceSpec), + minifyClient: minifyClient, + postcssClient: postcss.New(deps.ResourceSpec), + templatesClient: templates.New(deps.ResourceSpec, deps), + babelClient: babel.New(deps.ResourceSpec), }, nil } @@ -72,14 +77,34 @@ func New(deps *deps.Deps) (*Namespace, error) { type Namespace struct { deps *deps.Deps - createClient *create.Client - bundlerClient *bundler.Client - scssClient *scss.Client - integrityClient *integrity.Client - minifyClient *minifier.Client - postcssClient *postcss.Client - babelClient *babel.Client - templatesClient *templates.Client + createClient *create.Client + bundlerClient *bundler.Client + scssClientLibSass *scss.Client + integrityClient *integrity.Client + minifyClient *minifier.Client + postcssClient *postcss.Client + babelClient *babel.Client + templatesClient *templates.Client + + // The Dart Client requires a os/exec process, so only + // create it if we really need it. + // This is mostly to avoid creating one per site build test. + scssClientDartSassInit sync.Once + scssClientDartSass *dartsass.Client +} + +func (ns *Namespace) getscssClientDartSass() (*dartsass.Client, error) { + var err error + ns.scssClientDartSassInit.Do(func() { + ns.scssClientDartSass, err = dartsass.New(ns.deps.BaseFs.Assets, ns.deps.ResourceSpec) + if err != nil { + return + } + ns.deps.BuildClosers.Add(ns.scssClientDartSass) + + }) + + return ns.scssClientDartSass, err } // Get locates the filename given in Hugo's assets filesystem @@ -230,12 +255,21 @@ func (ns *Namespace) Minify(r resources.ResourceTransformer) (resource.Resource, // ToCSS converts the given Resource to CSS. You can optional provide an Options // object or a target path (string) as first argument. func (ns *Namespace) ToCSS(args ...interface{}) (resource.Resource, error) { + const ( + // Transpiler implementation can be controlled from the client by + // setting the 'transpiler' option. + // Default is currently 'libsass', but that may change. + transpilerDart = "dartsass" + transpilerLibSass = "libsass" + ) + var ( r resources.ResourceTransformer m map[string]interface{} targetPath string err error ok bool + transpiler = transpilerLibSass ) r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args) @@ -247,17 +281,46 @@ func (ns *Namespace) ToCSS(args ...interface{}) (resource.Resource, error) { } } - var options scss.Options - if targetPath != "" { - options.TargetPath = helpers.ToSlashTrimLeading(targetPath) - } else if m != nil { - options, err = scss.DecodeOptions(m) - if err != nil { - return nil, err + if m != nil { + maps.ToLower(m) + if t, found := m["transpiler"]; found { + switch t { + case transpilerDart, transpilerLibSass: + transpiler = cast.ToString(t) + default: + return nil, errors.Errorf("unsupported transpiler %q; valid values are %q or %q", t, transpilerLibSass, transpilerDart) + } } } - return ns.scssClient.ToCSS(r, options) + if transpiler == transpilerLibSass { + var options scss.Options + if targetPath != "" { + options.TargetPath = helpers.ToSlashTrimLeading(targetPath) + } else if m != nil { + options, err = scss.DecodeOptions(m) + if err != nil { + return nil, err + } + } + + return ns.scssClientLibSass.ToCSS(r, options) + } + + if m == nil { + m = make(map[string]interface{}) + } + if targetPath != "" { + m["targetPath"] = targetPath + } + + client, err := ns.getscssClientDartSass() + if err != nil { + return nil, err + } + + return client.ToCSS(r, m) + } // PostCSS processes the given Resource with PostCSS diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index c142dd67286..67e95792458 100644 --- a/tpl/tplimpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -98,6 +98,7 @@ func TestTemplateFuncsExamples(t *testing.T) { depsCfg := newDepsConfig(v) depsCfg.Fs = fs d, err := deps.New(depsCfg) + defer d.Close() c.Assert(err, qt.IsNil) var data struct { @@ -163,6 +164,7 @@ func TestPartialCached(t *testing.T) { de, err := deps.New(config) c.Assert(err, qt.IsNil) + defer de.Close() c.Assert(de.LoadResources(), qt.IsNil) ns := partials.New(de) @@ -216,6 +218,7 @@ func doBenchmarkPartial(b *testing.B, f func(ns *partials.Namespace) error) { de, err := deps.New(config) c.Assert(err, qt.IsNil) + defer de.Close() c.Assert(de.LoadResources(), qt.IsNil) ns := partials.New(de) diff --git a/tpl/tplimpl/template_info_test.go b/tpl/tplimpl/template_info_test.go index db74c29a8d8..eaf57166a63 100644 --- a/tpl/tplimpl/template_info_test.go +++ b/tpl/tplimpl/template_info_test.go @@ -24,6 +24,7 @@ import ( func TestTemplateInfoShortcode(t *testing.T) { c := qt.New(t) d := newD(c) + defer d.Close() h := d.Tmpl().(*templateExec) c.Assert(h.AddTemplate("shortcodes/mytemplate.html", `