resources: Support babel/postcss config variants

Allow modules to use .mjs and .cjs file extensions for Babel and PostCSS
configuration files instead of just .js.

Closes #15039
Closes #15040
Closes #15043
This commit is contained in:
Joe Mooring
2026-06-15 22:32:05 -07:00
committed by Bjørn Erik Pedersen
parent f013346667
commit 9d66d513ce
7 changed files with 247 additions and 26 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ var defaultBuild = BuildConfig{
CacheBusters: []CacheBuster{
{
Source: `(postcss|tailwind)\.config\.js`,
Source: `(postcss|tailwind)\.config\.(js|mjs|cjs)`,
Target: cssTargetCachebusterRe,
},
},
+19 -6
View File
@@ -1165,7 +1165,7 @@ config:
disableTags: false
enable: false
cacheBusters:
- source: '(postcss|tailwind)\.config\.js'
- source: '(postcss|tailwind)\.config\.(js|mjs|cjs)'
target: (css|styles|scss|sass)
noJSConfigInAssets: false
useResourceCacheWhen: fallback
@@ -1272,24 +1272,31 @@ config:
ignoreVendorPaths: ''
imaging:
anchor: smart
avif:
compression: lossy
encoderSpeed: 10
hint: photo
quality: 60
bgColor: ffffff
compression: lossy
exif:
disableDate: false
disableLatLong: false
excludeFields: GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance
includeFields: ''
jpeg:
quality: 75
meta:
fields:
- '! *{GPS,Exif,Exposure[MPB],Contrast,Resolution,Sharp,JPEG,Metering,Sensing,Saturation,ColorSpace,Flash,WhiteBalance}*'
sources:
- exif
- iptc
quality: 75
resampleFilter: box
webp:
compression: lossy
hint: photo
method: 2
quality: 75
useSharpYuv: false
languages:
en:
@@ -2043,6 +2050,8 @@ config:
weight: 0
sectionPagesMenu: ''
security:
allowContent:
- '! ^text/html$'
enableInlineShortcodes: false
exec:
allow:
@@ -4612,11 +4621,15 @@ tpl:
- highlight
Args:
- s
- lang
- opts
- args
Description: |-
Highlight returns a copy of s as an HTML string with syntax
Highlight returns a copy of CODE as an HTML string with syntax
highlighting applied.
transform.Highlight CODE [LANG] [OPTIONS]
LANG is optional; it can also be set via the type option in OPTIONS, which
makes this work the same way as HighlightCodeBlock.
Examples: []
HighlightCodeBlock:
Aliases: null
+1 -1
View File
@@ -656,7 +656,7 @@ func (c *collector) loadModules() error {
}
// Matches postcss.config.js etc.
var commonJSConfigs = regexp.MustCompile(`(babel|postcss|tailwind)\.config\.js`)
var commonJSConfigs = regexp.MustCompile(`^(babel|postcss|tailwind)\.config\.(c|m)?js$`)
func (c *collector) mountCommonJSConfig(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
for _, m := range mounts {
+15 -9
View File
@@ -130,17 +130,23 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
if t.options.Config != "" {
configFile = t.options.Config
} else {
configFile = "babel.config.js"
for _, name := range []string{"babel.config.js", "babel.config.mjs", "babel.config.cjs"} {
configFile = t.rs.BaseFs.ResolveJSConfigFile(name)
if configFile != "" {
break
}
}
}
configFile = filepath.Clean(configFile)
// We need an absolute filename to the config file.
if !filepath.IsAbs(configFile) {
configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
if configFile == "" && t.options.Config != "" {
// Only fail if the user specified config file is not found.
return fmt.Errorf("babel config %q not found", configFile)
if configFile != "" {
configFile = filepath.Clean(configFile)
// We need an absolute filename to the config file.
if !filepath.IsAbs(configFile) {
configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
if configFile == "" && t.options.Config != "" {
// Only fail if the user specified config file is not found.
return fmt.Errorf("babel config %q not found", t.options.Config)
}
}
}
@@ -14,9 +14,14 @@
package babel_test
import (
"os"
"path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib"
)
@@ -83,3 +88,103 @@ Transpiled3: {{ $transpiled.Permalink }}
b.AssertFileContent("public/index.html", `
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozL`)
}
// See Issue 15043.
// See Issue 15040.
func TestTransformBabelConfigResolution(t *testing.T) {
if !htesting.IsCI() {
t.Skip("Skip long running test when running locally")
}
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
[[module.imports]]
path = "github.com/bep/hugo-mod-nop"
[security]
[security.exec]
allow = ['^go$', '^node$', '^babel$']
-- assets/js/main.js --
/* A Car */
class Car {
constructor(brand) {
this.carname = brand;
}
}
-- layouts/home.html --
{{ $js := resources.Get "js/main.js" | babel }}
RelPermalink: {{ $js.RelPermalink }}|HasBody: {{ gt (len $js.Content) 0 }}|
-- package.json --
{
"devDependencies": {
"@babel/cli": "7.28.6",
"@babel/core": "7.28.6"
}
}
-- go.mod --
module github.com/example/project
go 1.26
replace github.com/bep/hugo-mod-nop => ../external-module
-- ../external-module/go.mod --
module github.com/bep/hugo-mod-nop
go 1.26
-- ../external-module/CONFIG_FILE_NAME --
CONFIG_FILE_CONTENT
`
tests := []struct {
name string
configFileName string
configFileContent string
}{
{
name: "mjs in module",
configFileName: "babel.config.mjs",
configFileContent: "export default {};\n",
},
{
name: "cjs in module",
configFileName: "babel.config.cjs",
configFileContent: "module.exports = {};\n",
},
{
name: "js in module",
configFileName: "babel.config.js",
configFileContent: "module.exports = {};\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := qt.New(t)
rootDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-integration-test")
c.Assert(err, qt.IsNil)
c.Cleanup(clean)
projectDir := filepath.Join(rootDir, "project")
moduleDir := filepath.Join(rootDir, "external-module")
c.Assert(os.MkdirAll(projectDir, 0o755), qt.IsNil)
c.Assert(os.MkdirAll(moduleDir, 0o755), qt.IsNil)
f := strings.ReplaceAll(files, "CONFIG_FILE_NAME", tt.configFileName)
f = strings.ReplaceAll(f, "CONFIG_FILE_CONTENT", tt.configFileContent)
b := hugolib.Test(c, f,
hugolib.TestOptWithConfig(func(cfg *hugolib.IntegrationTestConfig) {
cfg.WorkingDir = projectDir
cfg.NeedsOsFS = true
cfg.NeedsNpmInstall = true
}),
hugolib.TestOptInfo(),
)
b.AssertFileContent("public/index.html",
"RelPermalink: /js/main.js|HasBody: true|",
)
b.AssertLogContains(tt.configFileName)
})
}
}
@@ -157,17 +157,23 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
if options.Config != "" {
configFile = options.Config
} else {
configFile = "postcss.config.js"
for _, name := range []string{"postcss.config.js", "postcss.config.mjs", "postcss.config.cjs"} {
configFile = t.rs.BaseFs.ResolveJSConfigFile(name)
if configFile != "" {
break
}
}
}
configFile = filepath.Clean(configFile)
// We need an absolute filename to the config file.
if !filepath.IsAbs(configFile) {
configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
if configFile == "" && options.Config != "" {
// Only fail if the user specified config file is not found.
return fmt.Errorf("postcss config %q not found", options.Config)
if configFile != "" {
configFile = filepath.Clean(configFile)
// We need an absolute filename to the config file.
if !filepath.IsAbs(configFile) {
configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
if configFile == "" && options.Config != "" {
// Only fail if the user specified config file is not found.
return fmt.Errorf("postcss config %q not found", options.Config)
}
}
}
@@ -216,6 +216,97 @@ Styles Content: Len: 770917
}
}
// See Issue 15039.
// See Issue 15040.
func TestTransformPostCSSConfigResolution(t *testing.T) {
if !htesting.IsCI() {
t.Skip("Skip long running test when running locally")
}
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
[[module.imports]]
path = "github.com/bep/hugo-mod-nop"
-- assets/css/styles.css --
body { color: red }
-- layouts/home.html --
{{ $styles := resources.Get "css/styles.css" | css.PostCSS }}
RelPermalink: {{ $styles.RelPermalink }}|HasBody: {{ in $styles.Content "color:" }}|
-- package.json --
{
"devDependencies": {
"postcss-cli": "11.0.0"
}
}
-- go.mod --
module github.com/example/project
go 1.26
replace github.com/bep/hugo-mod-nop => ../external-module
-- ../external-module/go.mod --
module github.com/bep/hugo-mod-nop
go 1.26
-- ../external-module/CONFIG_FILE_NAME --
CONFIG_FILE_CONTENT
`
tests := []struct {
name string
configFileName string
configFileContent string
}{
{
name: "mjs in module",
configFileName: "postcss.config.mjs",
configFileContent: "export default {};\n",
},
{
name: "cjs in module",
configFileName: "postcss.config.cjs",
configFileContent: "module.exports = {};\n",
},
{
name: "js in module",
configFileName: "postcss.config.js",
configFileContent: "module.exports = {};\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := qt.New(t)
rootDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-integration-test")
c.Assert(err, qt.IsNil)
c.Cleanup(clean)
projectDir := filepath.Join(rootDir, "project")
moduleDir := filepath.Join(rootDir, "external-module")
c.Assert(os.MkdirAll(projectDir, 0o755), qt.IsNil)
c.Assert(os.MkdirAll(moduleDir, 0o755), qt.IsNil)
f := strings.ReplaceAll(files, "CONFIG_FILE_NAME", tt.configFileName)
f = strings.ReplaceAll(f, "CONFIG_FILE_CONTENT", tt.configFileContent)
b := hugolib.Test(c, f,
hugolib.TestOptWithConfig(func(cfg *hugolib.IntegrationTestConfig) {
cfg.WorkingDir = projectDir
cfg.NeedsOsFS = true
cfg.NeedsNpmInstall = true
}),
hugolib.TestOptInfo(),
)
b.AssertFileContent("public/index.html",
"RelPermalink: /css/styles.css|HasBody: true|",
)
b.AssertLogContains(tt.configFileName)
})
}
}
// See Issue 13987.
func TestTransformPostCSSESMConfigInModule(t *testing.T) {
if !htesting.IsCI() {