Skip to content

Commit 881d5fd

Browse files
committed
Add css.Build
Fixes #14609
1 parent c47ec23 commit 881d5fd

File tree

9 files changed

+123
-11
lines changed

9 files changed

+123
-11
lines changed

internal/js/esbuild/batch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ var (
7272
func NewBatcherClient(deps *deps.Deps) (js.BatcherClient, error) {
7373
c := &BatcherClient{
7474
d: deps,
75-
buildClient: NewBuildClient(deps.BaseFs.Assets, deps.ResourceSpec),
75+
buildClient: NewBuildClient(deps.BaseFs.Assets, deps.ResourceSpec, false),
7676
createClient: create.New(deps.ResourceSpec),
7777
batcherStore: hmaps.NewCache[string, js.Batcher](),
7878
bundlesStore: hmaps.NewCache[string, js.BatchPackage](),

internal/js/esbuild/build.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import (
3232
)
3333

3434
// NewBuildClient creates a new BuildClient.
35-
func NewBuildClient(fs *filesystems.SourceFilesystem, rs *resources.Spec) *BuildClient {
35+
func NewBuildClient(fs *filesystems.SourceFilesystem, rs *resources.Spec, cssMode bool) *BuildClient {
3636
return &BuildClient{
3737
rs: rs,
3838
sfs: fs,
@@ -41,8 +41,9 @@ func NewBuildClient(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Build
4141

4242
// BuildClient is a client for building JavaScript resources using esbuild.
4343
type BuildClient struct {
44-
rs *resources.Spec
45-
sfs *filesystems.SourceFilesystem
44+
rs *resources.Spec
45+
sfs *filesystems.SourceFilesystem
46+
cssMode bool
4647
}
4748

4849
// Build builds the given JavaScript resources using esbuild with the given options.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2026 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
// Package js provides functions for building JavaScript resources
15+
package esbuild_test
16+
17+
import (
18+
"testing"
19+
20+
"github.com/gohugoio/hugo/hugolib"
21+
)
22+
23+
func TestBundleCSS(t *testing.T) {
24+
files := `
25+
-- hugo.toml --
26+
-- assets/a/pixel.png --
27+
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
28+
-- assets/css/main.css --
29+
@import "tailwindcss";
30+
@import url('https://example.org/foo.css');
31+
@plugin "@tailwindcss/typography";
32+
@variant dark (&:where(.dark, .dark *));
33+
34+
@import "./foo.css";
35+
@import "./bar.css" layer(mylayer);
36+
@import "./bar.css" layer;
37+
@import './baz.css' screen and (min-width: 800px);
38+
@import './qux.css' supports(display: grid);
39+
body {
40+
background-image: url("a/pixel.png");
41+
}
42+
.mask1 {
43+
mask-image: url(a/pixel.png);
44+
mask-repeat: no-repeat;
45+
}
46+
-- assets/css/foo.css --
47+
p { background: red; }
48+
-- assets/css/bar.css --
49+
div { background: blue; }
50+
-- assets/css/baz.css --
51+
span { background: green; }
52+
-- assets/css/qux.css --
53+
article { background: yellow; }
54+
-- layouts/home.html --
55+
{{ with resources.Get "css/main.css" }}
56+
{{ with . | css.Build (dict "minify" true "loaders" (dict ".png" "dataurl") "externals" (slice "tailwindcss")) }}
57+
<link rel="stylesheet" href="{{ .RelPermalink }}" />
58+
{{ end }}
59+
{{ end }}
60+
`
61+
62+
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
63+
b.AssertFileContent("public/css/main.css", `webkit`)
64+
}

internal/js/esbuild/options.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,15 @@ func (opts *Options) compile() (err error) {
224224
return
225225
}
226226

227+
// Engines.
228+
// TODO1
229+
engines := []api.Engine{
230+
{
231+
Name: api.EngineChrome,
232+
Version: "58",
233+
},
234+
}
235+
227236
var loaders map[string]api.Loader
228237
if opts.Loaders != nil {
229238
loaders = make(map[string]api.Loader)
@@ -252,6 +261,8 @@ func (opts *Options) compile() (err error) {
252261
loader = api.LoaderTSX
253262
case media.Builtin.JSXType.SubType:
254263
loader = api.LoaderJSX
264+
case media.Builtin.CSSType.SubType:
265+
loader = api.LoaderCSS
255266
default:
256267
err = fmt.Errorf("unsupported Media Type: %q", opts.MediaType)
257268
return
@@ -343,6 +354,7 @@ func (opts *Options) compile() (err error) {
343354
AbsWorkingDir: opts.AbsWorkingDir,
344355

345356
Target: target,
357+
Engines: engines,
346358
Format: format,
347359
Platform: platform,
348360
Sourcemap: sourceMap,

internal/js/esbuild/resolve.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ var extensionToLoaderMap = map[string]api.Loader{
6161
// This is a common sub-set of ESBuild's default extensions.
6262
// We assume that imports of JSON, CSS etc. will be using their full
6363
// name with extension.
64-
var commonExtensions = []string{".js", ".ts", ".tsx", ".jsx"}
64+
var commonExtensions = []string{".js", ".ts", ".tsx", ".jsx", ".css"}
6565

6666
// ResolveComponent resolves a component using the given resolver.
6767
func ResolveComponent[T any](impPath string, resolve func(string) (v T, found, isDir bool)) (v T, found bool) {
@@ -93,6 +93,7 @@ func ResolveComponent[T any](impPath string, resolve func(string) (v T, found, i
9393
}
9494

9595
base := filepath.Base(impPath)
96+
9697
if base == "index" {
9798
// try index.esm.js etc.
9899
v, found, _ = findFirst(impPath + ".esm")
@@ -159,6 +160,7 @@ func createBuildPlugins(rs *resources.Spec, assetsResolver *fsResolver, depsMana
159160

160161
resolveImport := func(args api.OnResolveArgs) (api.OnResolveResult, error) {
161162
impPath := args.Path
163+
162164
shimmed := false
163165
if opts.Shims != nil {
164166
override, found := opts.Shims[impPath]
@@ -182,9 +184,9 @@ func createBuildPlugins(rs *resources.Spec, assetsResolver *fsResolver, depsMana
182184
}
183185

184186
importer := args.Importer
185-
186187
isStdin := importer == stdinImporter
187188
var relDir string
189+
188190
if !isStdin {
189191
if after, ok := strings.CutPrefix(importer, PrefixHugoVirtual); ok {
190192
relDir = filepath.Dir(after)

resources/resource_transformers/js/build.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ type Client struct {
3131
}
3232

3333
// New creates a new client context.
34-
func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {
34+
func New(fs *filesystems.SourceFilesystem, rs *resources.Spec, cssMode bool) *Client {
3535
return &Client{
36-
c: esbuild.NewBuildClient(fs, rs),
36+
c: esbuild.NewBuildClient(fs, rs, cssMode),
3737
}
3838
}
3939

resources/resource_transformers/js/transform.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ func (t *buildTransformation) Key() internal.ResourceTransformationKey {
3535

3636
func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
3737
ctx.OutMediaType = media.Builtin.JavascriptType
38+
if ctx.InMediaType == media.Builtin.CSSType {
39+
ctx.OutMediaType = media.Builtin.CSSType
40+
}
3841

3942
var opts esbuild.Options
4043

@@ -49,7 +52,7 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
4952
if opts.TargetPath != "" {
5053
ctx.OutPath = opts.TargetPath
5154
} else {
52-
ctx.ReplaceOutPathExtension(".js")
55+
ctx.ReplaceOutPathExtension(ctx.OutMediaType.FirstSuffix.FullSuffix)
5356
}
5457

5558
src, err := io.ReadAll(ctx.From)

tpl/css/css.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/gohugoio/hugo/resources/resource"
1616
"github.com/gohugoio/hugo/resources/resource_transformers/babel"
1717
"github.com/gohugoio/hugo/resources/resource_transformers/cssjs"
18+
jstransform "github.com/gohugoio/hugo/resources/resource_transformers/js"
1819
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
1920
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/sass"
2021
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
@@ -32,6 +33,7 @@ type Namespace struct {
3233
postcssClient *cssjs.PostCSSClient
3334
tailwindcssClient *cssjs.TailwindCSSClient
3435
babelClient *babel.Client
36+
jsTransformClient *jstransform.Client
3537

3638
// The Dart Client requires a os/exec process, so only
3739
// create it if we really need it.
@@ -147,6 +149,33 @@ func (ns *Namespace) Sass(args ...any) (resource.Resource, error) {
147149
return client.ToCSS(r, m)
148150
}
149151

152+
// Build processes the given CSS Resource with ESBuild.
153+
// Note that this method is identical to the one in the js Namespace.
154+
func (ns *Namespace) Build(args ...any) (resource.Resource, error) {
155+
var (
156+
r resources.ResourceTransformer
157+
m map[string]any
158+
targetPath string
159+
err error
160+
ok bool
161+
)
162+
163+
r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args)
164+
165+
if !ok {
166+
r, m, err = resourcehelpers.ResolveArgs(args)
167+
if err != nil {
168+
return nil, err
169+
}
170+
}
171+
172+
if targetPath != "" {
173+
m = map[string]any{"targetPath": targetPath}
174+
}
175+
176+
return ns.jsTransformClient.Process(r, m)
177+
}
178+
150179
func init() {
151180
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
152181
scssClient, err := scss.New(d.BaseFs.Assets, d.ResourceSpec)
@@ -159,6 +188,7 @@ func init() {
159188
postcssClient: cssjs.NewPostCSSClient(d.ResourceSpec),
160189
tailwindcssClient: cssjs.NewTailwindCSSClient(d.ResourceSpec),
161190
babelClient: babel.New(d.ResourceSpec),
191+
jsTransformClient: jstransform.New(d.BaseFs.Assets, d.ResourceSpec, true),
162192
}
163193

164194
ns := &internal.TemplateFuncsNamespace{

tpl/js/js.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func New(d *deps.Deps) (*Namespace, error) {
3636

3737
return &Namespace{
3838
d: d,
39-
jsTransformClient: jstransform.New(d.BaseFs.Assets, d.ResourceSpec),
39+
jsTransformClient: jstransform.New(d.BaseFs.Assets, d.ResourceSpec, false),
4040
createClient: create.New(d.ResourceSpec),
4141
babelClient: babel.New(d.ResourceSpec),
4242
}, nil
@@ -51,7 +51,7 @@ type Namespace struct {
5151
babelClient *babel.Client
5252
}
5353

54-
// Build processes the given Resource with ESBuild.
54+
// Build processes the given JavaScript Resource with ESBuild.
5555
func (ns *Namespace) Build(args ...any) (resource.Resource, error) {
5656
var (
5757
r resources.ResourceTransformer

0 commit comments

Comments
 (0)