Improve SASS errors

Fixes #9897
This commit is contained in:
Bjørn Erik Pedersen
2022-05-15 11:40:34 +02:00
parent 4b189d8fd9
commit fc9f315d86
24 changed files with 306 additions and 69 deletions

View File

@@ -20,7 +20,9 @@ import (
"io"
"strings"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
@@ -33,6 +35,10 @@ import (
// used as part of the cache key.
const transformationName = "tocss-dart"
// See https://github.com/sass/dart-sass-embedded/issues/24
// Note: This prefix must be all lower case.
const dartSassStdinPrefix = "hugostdin:"
func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error) {
if !Supports() {
return &Client{dartSassNotAvailable: true}, nil
@@ -44,7 +50,7 @@ func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error)
transpiler, err := godartsass.Start(godartsass.Options{
LogEventHandler: func(event godartsass.LogEvent) {
message := strings.ReplaceAll(event.Message, stdinPrefix, "")
message := strings.ReplaceAll(event.Message, dartSassStdinPrefix, "")
switch event.Type {
case godartsass.LogEventTypeDebug:
// Log as Info for now, we may adjust this if it gets too chatty.
@@ -94,7 +100,7 @@ func (c *Client) toCSS(args godartsass.Args, src io.Reader) (godartsass.Result,
if err.Error() == "unexpected EOF" {
return res, fmt.Errorf("got unexpected EOF when executing %q. The user running hugo must have read and execute permissions on this program. With execute permissions only, this error is thrown.", dartSassEmbeddedBinaryName)
}
return res, err
return res, herrors.NewFileErrorFromFileInErr(err, hugofs.Os, herrors.OffsetMatcher)
}
return res, err

View File

@@ -14,8 +14,10 @@
package dartsass_test
import (
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
jww "github.com/spf13/jwalterweatherman"
@@ -196,3 +198,76 @@ T1: {{ $r.Content }}
b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:1:0: bar`)
}
func TestTransformErrors(t *testing.T) {
if !dartsass.Supports() {
t.Skip()
}
c := qt.New(t)
const filesTemplate = `
-- config.toml --
-- assets/scss/components/_foo.scss --
/* comment line 1 */
$foocolor: #ccc;
foo {
color: $foocolor;
}
-- assets/scss/main.scss --
/* comment line 1 */
/* comment line 2 */
@import "components/foo";
/* comment line 4 */
$maincolor: #eee;
body {
color: $maincolor;
}
-- layouts/index.html --
{{ $cssOpts := dict "transpiler" "dartsass" }}
{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }}
T1: {{ $r.Content }}
`
c.Run("error in main", func(c *qt.C) {
b, err := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: c,
TxtarString: strings.Replace(filesTemplate, "$maincolor: #eee;", "$maincolor #eee;", 1),
NeedsOsFS: true,
}).BuildE()
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, `main.scss:8:13":`)
b.Assert(err.Error(), qt.Contains, `: expected ":".`)
fe := b.AssertIsFileError(err)
b.Assert(fe.ErrorContext(), qt.IsNotNil)
b.Assert(fe.ErrorContext().Lines, qt.DeepEquals, []string{" $maincolor #eee;", "", "body {", "\tcolor: $maincolor;", "}"})
b.Assert(fe.ErrorContext().ChromaLexer, qt.Equals, "scss")
})
c.Run("error in import", func(c *qt.C) {
b, err := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: c,
TxtarString: strings.Replace(filesTemplate, "$foocolor: #ccc;", "$foocolor #ccc;", 1),
NeedsOsFS: true,
}).BuildE()
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, `_foo.scss:2:10":`)
b.Assert(err.Error(), qt.Contains, `: expected ":".`)
fe := b.AssertIsFileError(err)
b.Assert(fe.ErrorContext(), qt.IsNotNil)
b.Assert(fe.ErrorContext().Lines, qt.DeepEquals, []string{"/* comment line 1 */", "$foocolor #ccc;", "", "foo {"})
b.Assert(fe.ErrorContext().ChromaLexer, qt.Equals, "scss")
})
}

View File

@@ -16,13 +16,12 @@ package dartsass
import (
"fmt"
"io"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/media"
@@ -38,9 +37,6 @@ import (
)
const (
// See https://github.com/sass/dart-sass-embedded/issues/24
// Note: This prefix must be all lower case.
stdinPrefix = "hugostdin:"
dartSassEmbeddedBinaryName = "dart-sass-embedded"
)
@@ -76,7 +72,7 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
}
baseDir := path.Dir(ctx.SourcePath)
filename := stdinPrefix
filename := dartSassStdinPrefix
if ctx.SourcePath != "" {
filename += t.c.sfs.RealFilename(ctx.SourcePath)
@@ -108,26 +104,6 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
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, _ := urlToFilename(sassErr.Span.Url)
if strings.HasPrefix(filename, stdinPrefix) {
filename = filename[len(stdinPrefix):]
}
offsetMatcher := func(m herrors.LineMatcher) int {
if m.Offset+len(m.Line) >= start.Offset && strings.Contains(m.Line, context) {
// We found the line, but return 0 to signal that we want to determine
// the column from the error.
return 0
}
return -1
}
return herrors.NewFileErrorFromFile(sassErr, filename, hugofs.Os, offsetMatcher)
}
return err
}
@@ -154,7 +130,7 @@ type importResolver struct {
}
func (t importResolver) CanonicalizeURL(url string) (string, error) {
filePath, isURL := urlToFilename(url)
filePath, isURL := paths.UrlToFilename(url)
var prevDir string
var pathDir string
if isURL {
@@ -200,23 +176,7 @@ func (t importResolver) CanonicalizeURL(url string) (string, error) {
}
func (t importResolver) Load(url string) (string, error) {
filename, _ := urlToFilename(url)
filename, _ := paths.UrlToFilename(url)
b, err := afero.ReadFile(hugofs.Os, filename)
return string(b), err
}
// TODO(bep) add tests
func urlToFilename(urls string) (string, bool) {
u, err := url.ParseRequestURI(urls)
if err != nil {
return filepath.FromSlash(urls), false
}
p := filepath.FromSlash(u.Path)
if u.Host != "" {
// C:\data\file.txt
p = strings.ToUpper(u.Host) + ":" + p
}
return p, true
}

View File

@@ -47,6 +47,7 @@ func (c *Client) ToCSS(res resources.ResourceTransformer, opts Options) (resourc
}
return res.Transform(&toCSSTransformation{c: c, options: internalOptions})
}
type toCSSTransformation struct {

View File

@@ -14,6 +14,8 @@
package scss_test
import (
"path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
@@ -133,7 +135,7 @@ moo {
-- config.toml --
theme = 'mytheme'
-- layouts/index.html --
{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) "transpiler" "dartsass" ) }}
{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) ) }}
{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }}
T1: {{ $r.Content }}
-- themes/mytheme/assets/scss/components/_boo.scss --
@@ -171,3 +173,75 @@ zoo {
b.AssertFileContent("public/index.html", `T1: moo{color:#ccc}boo{color:green}zoo{color:pink}`)
}
func TestTransformErrors(t *testing.T) {
if !scss.Supports() {
t.Skip()
}
c := qt.New(t)
const filesTemplate = `
-- config.toml --
theme = 'mytheme'
-- assets/scss/components/_foo.scss --
/* comment line 1 */
$foocolor: #ccc;
foo {
color: $foocolor;
}
-- themes/mytheme/assets/scss/main.scss --
/* comment line 1 */
/* comment line 2 */
@import "components/foo";
/* comment line 4 */
$maincolor: #eee;
body {
color: $maincolor;
}
-- layouts/index.html --
{{ $cssOpts := dict }}
{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }}
T1: {{ $r.Content }}
`
c.Run("error in main", func(c *qt.C) {
b, err := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: c,
TxtarString: strings.Replace(filesTemplate, "$maincolor: #eee;", "$maincolor #eee;", 1),
NeedsOsFS: true,
}).BuildE()
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`themes/mytheme/assets/scss/main.scss:6:1": expected ':' after $maincolor in assignment statement`))
fe := b.AssertIsFileError(err)
b.Assert(fe.ErrorContext(), qt.IsNotNil)
b.Assert(fe.ErrorContext().Lines, qt.DeepEquals, []string{"/* comment line 4 */", "", "$maincolor #eee;", "", "body {"})
b.Assert(fe.ErrorContext().ChromaLexer, qt.Equals, "scss")
})
c.Run("error in import", func(c *qt.C) {
b, err := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: c,
TxtarString: strings.Replace(filesTemplate, "$foocolor: #ccc;", "$foocolor #ccc;", 1),
NeedsOsFS: true,
}).BuildE()
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`assets/scss/components/_foo.scss:2:1": expected ':' after $foocolor in assignment statement`))
fe := b.AssertIsFileError(err)
b.Assert(fe.ErrorContext(), qt.IsNotNil)
b.Assert(fe.ErrorContext().Lines, qt.DeepEquals, []string{"/* comment line 1 */", "$foocolor #ccc;", "", "foo {"})
b.Assert(fe.ErrorContext().ChromaLexer, qt.Equals, "scss")
})
}

View File

@@ -20,10 +20,13 @@ import (
"fmt"
"io"
"path"
"path/filepath"
"strings"
"github.com/bep/golibsass/libsass"
"github.com/bep/golibsass/libsass/libsasserrors"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/media"
@@ -136,7 +139,14 @@ func (t *toCSSTransformation) Transform(ctx *resources.ResourceTransformationCtx
res, err := t.c.toCSS(options.to, ctx.To, ctx.From)
if err != nil {
return err
if sasserr, ok := err.(libsasserrors.Error); ok {
if sasserr.File == "stdin" && ctx.SourcePath != "" {
sasserr.File = t.c.sfs.RealFilename(ctx.SourcePath)
err = sasserr
}
}
return herrors.NewFileErrorFromFileInErr(err, hugofs.Os, nil)
}
if options.from.EnableSourceMap && res.SourceMapContent != "" {
@@ -180,7 +190,7 @@ func (c *Client) toCSS(options libsass.Options, dst io.Writer, src io.Reader) (l
res, err = transpiler.Execute(in)
if err != nil {
return res, fmt.Errorf("SCSS processing failed: %w", err)
return res, err
}
out := res.CSS