mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
a7cbcf15f0
Before this commit, we did dynamic loader resolution for CSS bundling for resources resolved by Hugo while any fallback to ESBuild would fall back to a (potentially) empty loaders config. This revises the logic to always use a static list (see below) if `loaders` is not set. This should be easier do document and less confusing for the end user. ```` ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".avif", ".woff", ".woff2", ".ttf", ".eot", ".otf" ```` Fixes #14619
323 lines
9.5 KiB
Go
323 lines
9.5 KiB
Go
// Copyright 2026 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 css_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gohugoio/hugo/htesting"
|
|
"github.com/gohugoio/hugo/hugolib"
|
|
)
|
|
|
|
func TestCSSBuild(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
-- assets/a/pixel.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- assets/css/main.css --
|
|
@import "tailwindcss";
|
|
@import url('https://example.org/foo.css');
|
|
@import "./foo.css";
|
|
@import "./bar.css" layer(mylayer);
|
|
@import "./bar.css" layer;
|
|
@import './baz.css' screen and (min-width: 800px);
|
|
@import 'relnodot.css' supports(display: grid);
|
|
@import 'relnodotboth.css';
|
|
@import '/relnodotboth2.css';
|
|
body {
|
|
background-image: url("a/pixel.png");
|
|
}
|
|
.mask1 {
|
|
mask-image: url(a/pixel.png);
|
|
mask-repeat: no-repeat;
|
|
}
|
|
-- assets/css/foo.css --
|
|
p { background: red; }
|
|
-- assets/css/bar.css --
|
|
div { background: blue; }
|
|
-- assets/css/baz.css --
|
|
span { background: green; }
|
|
-- assets/css/relnodot.css --
|
|
article { background: yellow; }
|
|
-- assets/css/relnodotboth.css --
|
|
.relnodotbothrelative { background: green; }
|
|
-- assets/relnodotboth.css --
|
|
.relnodotbothroot { background: blue; }
|
|
-- assets/css/relnodotboth2.css --
|
|
.relnodotboth2relative { background: green; }
|
|
-- assets/relnodotboth2.css --
|
|
.relnodotboth2root { background: blue; }
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ $opts := (dict "minify" true "target" (slice "chrome108" "firefox116" "safari16.4" "edge115" "ios16.4" "opera101") "loaders" (dict ".png" "dataurl") "externals" (slice "tailwindcss"))}}
|
|
{{ with . | css.Build $opts }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css",
|
|
`-webkit-mask-repeat`, // browser prefix.
|
|
"data:image/png;base64", // dataurl loader
|
|
`@import"tailwindcss";`, // external
|
|
`@import"https://example.org/foo.css";`, // external
|
|
"mylayer{div", // imported layer
|
|
"@media screen and (min-width:800px){span", // imported media query
|
|
"@supports (display: grid){article", // imported supports
|
|
"relnodotbothrelative", //@import 'relnodotboth.css'; file exists in both assets and project root.
|
|
"relnodotboth2root", //@import '/relnodotboth2.css'; file exists in both assets and project root.
|
|
)
|
|
}
|
|
|
|
func TestCSSBuildEdit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableKinds = ["taxonomy", "term", "rss"]
|
|
-- assets/images/bar.svg --
|
|
barsvg
|
|
-- assets/images/foo.svg --
|
|
foosvg
|
|
-- assets/css/main.css --
|
|
@import "./foo.css";
|
|
@import "./bar.css" layer(mylayer);
|
|
.main { background: red; }
|
|
-- assets/css/foo.css --
|
|
.foo { background: green; }
|
|
-- assets/css/bar.css --
|
|
.bar { background-image: url("images/bar.svg"); }
|
|
-- layouts/all.html --
|
|
All. No CSS here.
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ $opts := (dict "minify" true) }}
|
|
{{ with . | css.Build $opts }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
b := hugolib.TestRunning(t, files, hugolib.TestOptOsFs())
|
|
|
|
b.AssertFileContent("public/css/main.css", `.foo{background:green}@layer mylayer{.bar{background-image:url("./bar-Y35ORVQM.svg")}}`)
|
|
|
|
// Edit svg
|
|
b.EditFileReplaceAll("assets/images/bar.svg", "barsvg", "newbarsvg").Build()
|
|
b.AssertRenderCountPage(1)
|
|
b.AssertFileContent("public/css/main.css", `bar-LVHHRPN5.svg`) // new hash.
|
|
b.AssertFileContent("public/css/bar-LVHHRPN5.svg", "newbarsvg")
|
|
|
|
// Edit foo.css
|
|
b.EditFileReplaceAll("assets/css/foo.css", "green", "red").Build()
|
|
b.AssertRenderCountPage(1)
|
|
b.AssertFileContent("public/css/main.css", `.foo{background:red}`) // updated content.
|
|
|
|
// Edit bar.css
|
|
b.EditFileReplaceAll("assets/css/bar.css", "bar.svg", "foo.svg").Build()
|
|
b.AssertRenderCountPage(1)
|
|
b.AssertFileContent("public/css/main.css", `foo-52JTT5GU.svg`)
|
|
b.AssertFileContent("public/css/foo-52JTT5GU.svg", "foosvg")
|
|
|
|
// Edit main.css
|
|
b.EditFileReplaceAll("assets/css/main.css", "red", "blue").Build()
|
|
b.AssertRenderCountPage(1)
|
|
b.AssertFileContent("public/css/main.css", `.main{background:#00f}`) // updated content, blue gets minified to #00f.
|
|
}
|
|
|
|
func TestCSSBuildSourceMaps(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
filesTemplate := `
|
|
-- hugo.toml --
|
|
-- assets/a/pixel.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- assets/css/main.css --
|
|
@import "./foo.css";
|
|
@import "./bar.css" layer(mylayer);
|
|
@import "./bar.css" layer;
|
|
@import './baz.css' screen and (min-width: 800px);
|
|
|
|
.main {
|
|
background-color: red;
|
|
}
|
|
-- assets/css/foo.css --
|
|
p { background: red; }
|
|
-- assets/css/bar.css --
|
|
div { background: blue; }
|
|
-- assets/css/baz.css --
|
|
span { background: green; }
|
|
-- assets/css/qux.css --
|
|
article { background: yellow; }
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ $opts := (dict "minify" false "sourceMap" "SOURCE_MAP" "sourcesContent" SOURCES_CONTENT "loaders" (dict ".png" "dataurl"))}}
|
|
{{ with . | css.Build $opts }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
r := strings.NewReplacer(
|
|
"SOURCE_MAP", "linked",
|
|
"SOURCES_CONTENT", "true",
|
|
)
|
|
|
|
files := r.Replace(filesTemplate)
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css", "/*# sourceMappingURL=main.css.map */")
|
|
b.AssertFileContent("public/css/main.css.map",
|
|
`"sourcesContent":["p { background: red; }"`,
|
|
"AAAA;AAAI,cAAY;AAAK",
|
|
)
|
|
|
|
r = strings.NewReplacer(
|
|
"SOURCE_MAP", "external",
|
|
"SOURCES_CONTENT", "true",
|
|
)
|
|
|
|
files = r.Replace(filesTemplate)
|
|
|
|
b = hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css", "! sourceMappingURL")
|
|
b.AssertFileContent("public/css/main.css.map",
|
|
`"sourcesContent":["p { background: red; }"`,
|
|
"AAAA;AAAI,cAAY;AAAK",
|
|
)
|
|
|
|
r = strings.NewReplacer(
|
|
"SOURCE_MAP", "external",
|
|
"SOURCES_CONTENT", "false",
|
|
)
|
|
|
|
files = r.Replace(filesTemplate)
|
|
|
|
b = hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css.map",
|
|
`"sourcesContent":null,"`,
|
|
"AAAA;AAAI,cAAY;AAAK",
|
|
)
|
|
|
|
r = strings.NewReplacer(
|
|
"SOURCE_MAP", "inline",
|
|
"SOURCES_CONTENT", "false",
|
|
)
|
|
|
|
files = r.Replace(filesTemplate)
|
|
|
|
b = hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileExists("public/css/main.css.map", false)
|
|
b.AssertFileContent("public/css/main.css", "sourceMappingURL=data:application/json;base64,")
|
|
}
|
|
|
|
func TestCSSBuildMultihost(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableKinds = ["taxonomy", "term"]
|
|
defaultContentLanguage = "en"
|
|
defaultContentLanguageInSubDir = true
|
|
[languages]
|
|
[languages.en]
|
|
baseURL = "https://example.en"
|
|
weight = 1
|
|
[languages.fr]
|
|
baseURL = "https://example.fr"
|
|
weight = 2
|
|
-- assets/a/pixel.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- assets/css/main.css --
|
|
body {
|
|
background-image: url("a/pixel.png");
|
|
color: red;
|
|
}
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ with . | css.Build (dict "minify" true) }}
|
|
CSS: {{ .RelPermalink }}|{{ .Content }}
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
|
|
for _, lang := range []string{"en", "fr"} {
|
|
b.AssertFileContent("public/"+lang+"/css/main.css", `./pixel-NJRUOINY.png`)
|
|
b.AssertFileExists("public/"+lang+"/css/pixel-NJRUOINY.png", true)
|
|
}
|
|
}
|
|
|
|
func TestCSSBuildLoadersDefault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
-- assets/a/pixel.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- static/b/issue14619.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- assets/css/main.css --
|
|
body {
|
|
background-image: url("a/pixel.png");
|
|
}
|
|
div {
|
|
background-image: url("static/b/issue14619.png");
|
|
}
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ with . | css.Build (dict "minify" true) }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css", `./pixel-NJRUOINY.png`)
|
|
b.AssertFileExists("public/css/pixel-NJRUOINY.png", true)
|
|
b.AssertFileContent("public/css/main.css", `url("./issue14619-NJRUOINY.png")`)
|
|
b.AssertFileExists("public/css/issue14619-NJRUOINY.png", true)
|
|
}
|
|
|
|
func TestCSSBuildBootstrapFromNPM(t *testing.T) {
|
|
htesting.SkipSlowTestUnlessCI(t)
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableKinds = ["taxonomy", "term"]
|
|
-- assets/css/main.css --
|
|
@import "bootstrap";
|
|
-- package.json --
|
|
{
|
|
"devDependencies": {
|
|
"bootstrap": "5.3.8"
|
|
}
|
|
}
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ with . | css.Build (dict "minify" true) }}
|
|
CSS size: {{ .Content | len }}|{{ .RelPermalink }}
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWithNpmInstall())
|
|
b.AssertFileContent("public/css/main.css", `--bs-indigo: #6610f2;`)
|
|
}
|