From 9d66d513cee02e77c96c78059d5c3ae6b1c5dde9 Mon Sep 17 00:00:00 2001 From: Joe Mooring Date: Mon, 15 Jun 2026 22:32:05 -0700 Subject: [PATCH] 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 --- config/commonConfig.go | 2 +- docs/data/docs.yaml | 25 ++++- modules/collect.go | 2 +- .../resource_transformers/babel/babel.go | 24 ++-- .../babel/babel_integration_test.go | 105 ++++++++++++++++++ .../resource_transformers/cssjs/postcss.go | 24 ++-- .../cssjs/postcss_integration_test.go | 91 +++++++++++++++ 7 files changed, 247 insertions(+), 26 deletions(-) diff --git a/config/commonConfig.go b/config/commonConfig.go index 2c10ba9cf..361eb88f1 100644 --- a/config/commonConfig.go +++ b/config/commonConfig.go @@ -88,7 +88,7 @@ var defaultBuild = BuildConfig{ CacheBusters: []CacheBuster{ { - Source: `(postcss|tailwind)\.config\.js`, + Source: `(postcss|tailwind)\.config\.(js|mjs|cjs)`, Target: cssTargetCachebusterRe, }, }, diff --git a/docs/data/docs.yaml b/docs/data/docs.yaml index 87906d383..50443444f 100644 --- a/docs/data/docs.yaml +++ b/docs/data/docs.yaml @@ -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 diff --git a/modules/collect.go b/modules/collect.go index 082ddc925..5680f065c 100644 --- a/modules/collect.go +++ b/modules/collect.go @@ -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 { diff --git a/resources/resource_transformers/babel/babel.go b/resources/resource_transformers/babel/babel.go index 8336cd6e3..e88a025f7 100644 --- a/resources/resource_transformers/babel/babel.go +++ b/resources/resource_transformers/babel/babel.go @@ -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) + } } } diff --git a/resources/resource_transformers/babel/babel_integration_test.go b/resources/resource_transformers/babel/babel_integration_test.go index 41f867a51..a95185fb7 100644 --- a/resources/resource_transformers/babel/babel_integration_test.go +++ b/resources/resource_transformers/babel/babel_integration_test.go @@ -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) + }) + } +} diff --git a/resources/resource_transformers/cssjs/postcss.go b/resources/resource_transformers/cssjs/postcss.go index 13da2ab30..94295eea9 100644 --- a/resources/resource_transformers/cssjs/postcss.go +++ b/resources/resource_transformers/cssjs/postcss.go @@ -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) + } } } diff --git a/resources/resource_transformers/cssjs/postcss_integration_test.go b/resources/resource_transformers/cssjs/postcss_integration_test.go index 11ebbf4eb..6ef6726c5 100644 --- a/resources/resource_transformers/cssjs/postcss_integration_test.go +++ b/resources/resource_transformers/cssjs/postcss_integration_test.go @@ -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() {