Skip to content

Commit

Permalink
Fix and add integration test for the Bootstrap SCSS module for both D…
Browse files Browse the repository at this point in the history
…art Sass and Libsass

This fixes the reverse filesystem lookup (absolute filename to path relative to the composite filesystem).

The old logic had some assumptions about the locality of the actual files that didn't work in more complex scenarios.

This commit now also adds the popular Bootstrap SCSS Hugo module to the CI build (both for libsass and dartsass transpiler), so we can hopefully avoid similar future breakage.

Fixes #12178
  • Loading branch information
bep committed Mar 1, 2024
1 parent 7023cf0 commit 0d6e593
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 37 deletions.
49 changes: 26 additions & 23 deletions hugofs/rootmapping_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"path"
"path/filepath"
"strings"
"sync/atomic"

"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/paths"
Expand All @@ -43,6 +44,7 @@ var _ ReverseLookupProvder = (*RootMappingFs)(nil)
func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) {
rootMapToReal := radix.New()
realMapToRoot := radix.New()
id := fmt.Sprintf("rfs-%d", rootMappingFsCounter.Add(1))

addMapping := func(key string, rm RootMapping, to *radix.Tree) {
var mappings []RootMapping
Expand Down Expand Up @@ -76,6 +78,16 @@ func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) {
rm.Meta = NewFileMeta()
}

if rm.FromBase == "" {
panic(" rm.FromBase is empty")
}

rm.Meta.Component = rm.FromBase
rm.Meta.Module = rm.Module
rm.Meta.ModuleOrdinal = rm.ModuleOrdinal
rm.Meta.IsProject = rm.IsProject
rm.Meta.BaseDir = rm.ToBase

if !fi.IsDir() {
// We do allow single file mounts.
// However, the file system logic will be much simpler with just directories.
Expand Down Expand Up @@ -122,19 +134,9 @@ func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) {
}
}

if rm.FromBase == "" {
panic(" rm.FromBase is empty")
}

// Extract "blog" from "content/blog"
rm.path = strings.TrimPrefix(strings.TrimPrefix(rm.From, rm.FromBase), filepathSeparator)

rm.Meta.SourceRoot = fi.(MetaProvider).Meta().Filename
rm.Meta.BaseDir = rm.ToBase
rm.Meta.Module = rm.Module
rm.Meta.ModuleOrdinal = rm.ModuleOrdinal
rm.Meta.Component = rm.FromBase
rm.Meta.IsProject = rm.IsProject

meta := rm.Meta.Copy()

Expand All @@ -156,6 +158,7 @@ func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) {
}

rfs := &RootMappingFs{
id: id,
Fs: fs,
rootMapToReal: rootMapToReal,
realMapToRoot: realMapToRoot,
Expand Down Expand Up @@ -227,11 +230,14 @@ var _ FilesystemUnwrapper = (*RootMappingFs)(nil)
// is directories only, and they will be returned in Readdir and Readdirnames
// in the order given.
type RootMappingFs struct {
id string
afero.Fs
rootMapToReal *radix.Tree
realMapToRoot *radix.Tree
}

var rootMappingFsCounter atomic.Int32

func (fs *RootMappingFs) Mounts(base string) ([]FileMetaInfo, error) {
base = filepathSeparator + fs.cleanName(base)
roots := fs.getRootsWithPrefix(base)
Expand Down Expand Up @@ -263,6 +269,10 @@ func (fs *RootMappingFs) Mounts(base string) ([]FileMetaInfo, error) {
return fss, nil
}

func (fs *RootMappingFs) Key() string {
return fs.id
}

func (fs *RootMappingFs) UnwrapFilesystem() afero.Fs {
return fs.Fs
}
Expand Down Expand Up @@ -320,16 +330,16 @@ func (c ComponentPath) ComponentPathJoined() string {
}

type ReverseLookupProvder interface {
ReverseLookup(filename string, checkExists bool) ([]ComponentPath, error)
ReverseLookupComponent(component, filename string, checkExists bool) ([]ComponentPath, error)
ReverseLookup(filename string) ([]ComponentPath, error)
ReverseLookupComponent(component, filename string) ([]ComponentPath, error)
}

// func (fs *RootMappingFs) ReverseStat(filename string) ([]FileMetaInfo, error)
func (fs *RootMappingFs) ReverseLookup(filename string, checkExists bool) ([]ComponentPath, error) {
return fs.ReverseLookupComponent("", filename, checkExists)
func (fs *RootMappingFs) ReverseLookup(filename string) ([]ComponentPath, error) {
return fs.ReverseLookupComponent("", filename)
}

func (fs *RootMappingFs) ReverseLookupComponent(component, filename string, checkExists bool) ([]ComponentPath, error) {
func (fs *RootMappingFs) ReverseLookupComponent(component, filename string) ([]ComponentPath, error) {
filename = fs.cleanName(filename)
key := filepathSeparator + filename

Expand Down Expand Up @@ -360,14 +370,6 @@ func (fs *RootMappingFs) ReverseLookupComponent(component, filename string, chec
} else {
// Now we know that this file _could_ be in this fs.
filename = filepathSeparator + filepath.Join(first.path, dir, name)

if checkExists {
// Confirm that it exists.
_, err := fs.Stat(first.FromBase + filename)
if err != nil {
continue
}
}
}

cps = append(cps, ComponentPath{
Expand Down Expand Up @@ -667,6 +669,7 @@ func (fs *RootMappingFs) doStat(name string) ([]FileMetaInfo, error) {
var fis []FileMetaInfo

for _, rm := range roots {

var fi FileMetaInfo
fi, err = fs.statRoot(rm, name)
if err == nil {
Expand Down
6 changes: 3 additions & 3 deletions hugofs/rootmapping_fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,20 +276,20 @@ func TestRootMappingFsMount(t *testing.T) {

// Test ReverseLookup.
// Single file mounts.
cps, err := rfs.ReverseLookup(filepath.FromSlash("singlefiles/no.txt"), true)
cps, err := rfs.ReverseLookup(filepath.FromSlash("singlefiles/no.txt"))
c.Assert(err, qt.IsNil)
c.Assert(cps, qt.DeepEquals, []ComponentPath{
{Component: "content", Path: "singles/p1.md", Lang: "no"},
})

cps, err = rfs.ReverseLookup(filepath.FromSlash("singlefiles/sv.txt"), true)
cps, err = rfs.ReverseLookup(filepath.FromSlash("singlefiles/sv.txt"))
c.Assert(err, qt.IsNil)
c.Assert(cps, qt.DeepEquals, []ComponentPath{
{Component: "content", Path: "singles/p1.md", Lang: "sv"},
})

// File inside directory mount.
cps, err = rfs.ReverseLookup(filepath.FromSlash("mynoblogcontent/test.txt"), true)
cps, err = rfs.ReverseLookup(filepath.FromSlash("mynoblogcontent/test.txt"))
c.Assert(err, qt.IsNil)
c.Assert(cps, qt.DeepEquals, []ComponentPath{
{Component: "content", Path: "blog/test.txt", Lang: "no"},
Expand Down
34 changes: 24 additions & 10 deletions hugolib/filesystems/basefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ type SourceFilesystem struct {
// This is a virtual composite filesystem. It expects path relative to a context.
Fs afero.Fs

// The source filesystem (usually the OS filesystem).
SourceFs afero.Fs

// When syncing a source folder to the target (e.g. /public), this may
// be set to publish into a subfolder. This is used for static syncing
// in multihost mode.
Expand Down Expand Up @@ -320,10 +323,10 @@ func (s SourceFilesystems) IsContent(filename string) bool {
}

// ResolvePaths resolves the given filename to a list of paths in the filesystems.
func (s *SourceFilesystems) ResolvePaths(filename string, checkExists bool) []hugofs.ComponentPath {
func (s *SourceFilesystems) ResolvePaths(filename string) []hugofs.ComponentPath {
var cpss []hugofs.ComponentPath
for _, rfs := range s.RootFss {
cps, err := rfs.ReverseLookup(filename, checkExists)
cps, err := rfs.ReverseLookup(filename)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -362,7 +365,17 @@ func (d *SourceFilesystem) ReverseLookup(filename string, checkExists bool) ([]h
var cps []hugofs.ComponentPath
hugofs.WalkFilesystems(d.Fs, func(fs afero.Fs) bool {
if rfs, ok := fs.(hugofs.ReverseLookupProvder); ok {
if c, err := rfs.ReverseLookupComponent(d.Name, filename, checkExists); err == nil {
if c, err := rfs.ReverseLookupComponent(d.Name, filename); err == nil {
if checkExists {
n := 0
for _, cp := range c {
if _, err := d.Fs.Stat(filepath.FromSlash(cp.Path)); err == nil {
c[n] = cp
n++
}
}
c = c[:n]
}
cps = append(cps, c...)
}
}
Expand All @@ -379,11 +392,12 @@ func (d *SourceFilesystem) mounts() []hugofs.FileMetaInfo {
if err == nil {
m = append(m, mounts...)
}

}
return false
})

// Filter out any mounts not belonging to this filesystem.
// TODO(bep) I think this is superflous.
n := 0
for _, mm := range m {
if mm.Meta().Component == d.Name {
Expand All @@ -392,6 +406,7 @@ func (d *SourceFilesystem) mounts() []hugofs.FileMetaInfo {
}
}
m = m[:n]

return m
}

Expand Down Expand Up @@ -428,10 +443,8 @@ func (d *SourceFilesystem) RealDirs(from string) []string {
if !m.IsDir() {
continue
}
meta := m.Meta()
_, err := d.Fs.Stat(from)
if err == nil {
dirname := filepath.Join(meta.Filename, from)
dirname := filepath.Join(m.Meta().Filename, from)
if _, err := d.SourceFs.Stat(dirname); err == nil {
dirnames = append(dirnames, dirname)
}
}
Expand Down Expand Up @@ -519,8 +532,9 @@ func newSourceFilesystemsBuilder(p *paths.Paths, logger loggers.Logger, b *BaseF

func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs) *SourceFilesystem {
return &SourceFilesystem{
Name: name,
Fs: fs,
Name: name,
Fs: fs,
SourceFs: b.sourceFs,
}
}

Expand Down
2 changes: 1 addition & 1 deletion hugolib/hugo_sites_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf

isChangedDir := statErr == nil && fi.IsDir()

cpss := h.BaseFs.ResolvePaths(ev.Name, !removed)
cpss := h.BaseFs.ResolvePaths(ev.Name)
pss := make([]*paths.Path, len(cpss))
for i, cps := range cpss {
p := cps.Path
Expand Down
7 changes: 7 additions & 0 deletions hugolib/integrationtest_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ type lockingBuffer struct {
bytes.Buffer
}

func (b *lockingBuffer) ReadFrom(r io.Reader) (n int64, err error) {
b.Lock()
n, err = b.Buffer.ReadFrom(r)
b.Unlock()
return
}

func (b *lockingBuffer) Write(p []byte) (n int, err error) {
b.Lock()
n, err = b.Buffer.Write(p)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

"github.com/bep/logg"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
)
Expand Down Expand Up @@ -525,3 +526,39 @@ T1: {{ $r.Content }}
b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:13:0: number`)
b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:14:0: number`)
}

// Note: This test is more or less duplicated in both of the SCSS packages (libsass and dartsass).
func TestBootstrap(t *testing.T) {
t.Parallel()
if !dartsass.Supports() {
t.Skip()
}
if !htesting.IsCI() {
t.Skip("skip (slow) test in non-CI environment")
}

files := `
-- hugo.toml --
disableKinds = ["term", "taxonomy", "section", "page"]
[module]
[[module.imports]]
path="github.com/gohugoio/hugo-mod-bootstrap-scss/v5"
-- go.mod --
module github.com/gohugoio/tests/testHugoModules
-- assets/scss/main.scss --
@import "bootstrap/bootstrap";
-- layouts/index.html --
{{ $cssOpts := (dict "transpiler" "dartsass" ) }}
{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts }}
Styles: {{ $r.RelPermalink }}
`

b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: true,
}).Build()

b.AssertFileContent("public/index.html", "Styles: /scss/main.css")
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

qt "github.com/frankban/quicktest"

"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
)
Expand Down Expand Up @@ -290,3 +291,39 @@ T1: {{ $r.Content }}

b.AssertFileContent("public/index.html", `T1: body body{background:url(images/hero.jpg) no-repeat center/cover;font-family:Hugo's New Roman}p{color:blue;font-size:var 24px}b{color:green}`)
}

// Note: This test is more or less duplicated in both of the SCSS packages (libsass and dartsass).
func TestBootstrap(t *testing.T) {
t.Parallel()
if !scss.Supports() {
t.Skip()
}
if !htesting.IsCI() {
t.Skip("skip (slow) test in non-CI environment")
}

files := `
-- hugo.toml --
disableKinds = ["term", "taxonomy", "section", "page"]
[module]
[[module.imports]]
path="github.com/gohugoio/hugo-mod-bootstrap-scss/v5"
-- go.mod --
module github.com/gohugoio/tests/testHugoModules
-- assets/scss/main.scss --
@import "bootstrap/bootstrap";
-- layouts/index.html --
{{ $cssOpts := (dict "transpiler" "libsass" ) }}
{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts }}
Styles: {{ $r.RelPermalink }}
`

b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: true,
}).Build()

b.AssertFileContent("public/index.html", "Styles: /scss/main.css")
}

0 comments on commit 0d6e593

Please sign in to comment.