Skip to content

[@wordpress/scripts] postcss 8.5.24 can introduce a UTF-8 BOM in the middle of extracted CSS #81382

Description

@YvetteNikolov

Description

Since postcss 8.5.24 wp-scripts build can produce a UTF-8 BOM (EF BB BF) in the middle of the extracted CSS.

In our case the BOM was inserted immediately before :root { --color-palette-primary: …; } so all CSS custom properties in the theme defined in :root were dropped.

There is no warning or build error.

Image

Cause

The issue appears to be the interaction between sass-loader, Dart Sass, PostCSS 8.5.24, and mini-css-extract-plugin:

  1. sass-loader sets style: 'compressed' for production builds (src/utils.js#L287). @wordpress/scripts passes no sassOptions, so Sass keeps its default charset: true.
  2. With charset: true + style: 'compressed', Dart Sass prepends U+FEFF to the output of each compiled module whose CSS contains non-ASCII characters (a curly quote in content, an arrow, an emoj, etc).
  3. Before postcss 8.5.23, postcss-loader stripped that BOM: Input recorded hasBOM but the stringifier never wrote it back. postcss 8.5.24 Preserve the BOM when stringifying postcss/postcss#2119 now re-emits it in Stringifier#root.
  4. mini-css-extract-plugin then concatenates the chunk's CSS modules, so a BOM of module n ends up mid-file.

Proposed fix

Sass documents charset: false for this use case. In the Sass loader configuration webpack.config.js#L198-L208 :

 			{
 				test: /\.(sc|sa)ss$/,
 				use: [
 					...cssLoaders,
 					{
 						loader: require.resolve( 'sass-loader' ),
 						options: {
 							sourceMap: ! isProduction,
+							sassOptions: {
+								charset: false,
+							},
 						},
 					},
 				],
 			},

This prevents Sass from adding a BOM to individual CSS modules, so there is nothing for PostCSS to re-emit when the modules are concatenated.

Workarounds

Patch the sass rule in a project's own webpack config:

const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );

for ( const rule of defaultConfig.module.rules ) {
	if ( ! Array.isArray( rule.use ) ) {
		continue;
	}
	for ( const use of rule.use ) {
		if ( typeof use?.loader === 'string' && use.loader.includes( 'sass-loader' ) ) {
			use.options = {
				...use.options,
				sassOptions: { ...use.options?.sassOptions, charset: false },
			};
		}
	}
}

Or pin postcss: "overrides": { "postcss": "8.5.23" }.

Related

Step-by-step reproduction instructions

  1. Run this bash script for the setup (help with Claude)
mkdir wp-scripts-bom && cd wp-scripts-bom
npm init -y
npm i -D @wordpress/scripts

mkdir src
cat > src/index.js <<'EOF'
import './a.css';
import './b.scss';
EOF
printf '.first { color: red; }\n' > src/a.css
printf '.quote::before { content: "\xe2\x80\x9c"; }\n:root { --brand: #037d96; }\n' > src/b.scss

npx wp-scripts build
grep -oba $'\xef\xbb\xbf' build/index.css

Expected: no output.
Actual: the BOM sits between the two concatenated modules, directly before :root, making --brand unreachable in the browser.

Confirming the trigger - pin postcss below 8.5.24 and rebuild:

npm pkg set 'overrides.postcss=8.5.23' && npm install
npx wp-scripts build
grep -oba $'\xef\xbb\xbf' build/index.css   # no output

Screenshots, screen recording, code snippet

No response

Environment info

No response

Please confirm that you have searched existing issues in the repo.

  • Yes

Please confirm that you have tested with all plugins deactivated except Gutenberg.

  • Yes

Please confirm which theme type you used for testing.

  • Block
  • Classic
  • Hybrid (e.g. classic with theme.json)
  • Not sure

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

[Status] In ProgressTracking issues with work in progress[Type] BugAn existing feature does not function as intended

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions