Skip to content

Replace node-sass with dart-sass#12156

Draft
desrosj wants to merge 3 commits into
WordPress:trunkfrom
desrosj:trac/63013-replace-node-sass-with-dart-sass
Draft

Replace node-sass with dart-sass#12156
desrosj wants to merge 3 commits into
WordPress:trunkfrom
desrosj:trac/63013-replace-node-sass-with-dart-sass

Conversation

@desrosj

@desrosj desrosj commented Jun 11, 2026

Copy link
Copy Markdown
Member

This replaces node-sass with sass (Dart Sass).

Looking into the issues raised on the Trac ticket, here is some output from Claude around each one.

The sass output ignores five of the selectors when it creates font overrides for each non-latin language: .not-found .page-title, .error-404 .page-title, input[type="button"], input[type="reset"], and input[type="submit"]. That was difficult to notice because the compiler rearranges the lists of language selectors.

This appears to be caused by the redundancy elimination part of Dart Sass related to the use of @extend.

Full output from Claude: Output ignores five of the selectors when it creates font overrides for each non-latin language
How the SCSS builds the non-latin overrides:

  // sass/mixins/_mixins-master.scss
  %non-latin-fonts {
    @each $lang, $font__fallback in $font__fallbacks {
      &:lang(#{$lang}) { font-family: unquote($font__fallback); }
    }
  }

  @mixin font-family($font_family: $font__body) {
    font-family: $font_family;
    @extend %non-latin-fonts;
  }

  Every selector list that calls @include font-family(...) gets @extended into the placeholder, producing one giant
  <every-selector>:lang(ar) { ... } rule per language. LibSass emitted that list verbatim. Dart Sass runs a "redundancy
  elimination" pass on extend results — if one selector in the merged list is a structural superselector of another
  (matches a superset of elements), the more specific one is dropped.

  For the .page-title cases this is provably what happened. In sass/typography/_headings.scss:

  - Lines 11/33/75/144 include .not-found .page-title and .error-404 .page-title in lists that @include font-family(...).
  - Line 54: .page-title { @include font-family($font__body); } — bare .page-title is also extended.

  So .page-title:lang(ar) is already in the merged list, and Dart Sass treats .not-found .page-title:lang(ar) and
  .error-404 .page-title:lang(ar) as redundant subselectors and drops them.

  The three input[type=...] cases come from sass/forms/_buttons.scss:

  .button, button, input[type="button"], input[type="reset"], input[type="submit"] {
    @include font-family($font__heading);
  }

  The bare button element is in the same list. Dart Sass's selector-unification math considers
  input[type="button"]:lang(ar) covered by other selectors in the merged result and trims them too — button:lang(ar) is
  the most likely cause here as the trimmer can over-eagerly treat attribute-restricted versions of an element selector as
   redundant when both end up in the same combined list.

  This is a real change in matched elements:
  - .not-found .page-title:lang(ar) is fine — .page-title:lang(ar) does match it, so the font fallback still applies via
  the kept selector. (No visual regression.)
  - input[type="button"]:lang(ar) etc. — these are only covered if you accept that button:lang(ar) covers them, but a
  <input type="submit"> is not a <button> element. So Arabic/Hebrew/etc. submit buttons would lose the non-latin font
  fallback. That is a real visual regression.

  The fix is to refactor the mixin away from @extend. Either:

  @mixin font-family($font_family: $font__body) {
    font-family: $font_family;
    @each $lang, $font__fallback in $font__fallbacks {
      &:lang(#{$lang}) { font-family: unquote($font__fallback); }
    }
  }

  This emits the same rules inline at every call site (slightly more CSS in bytes, no extend trimming, output is stable).
  Or keep @extend but split the placeholder into per-language placeholders. The inline @each is simpler and idiomatic for
  current Sass.

The #005177 blue becomes rgb(0, 80.5, 119), slightly reducing its green value.

This one seems to be caused by lighten()/darken(), which have been deprecated. I've gone and hard coded the intended hex values to address this for now.

Full output from Claude: Why are hexidecimal colors output as `rgb()` and slightly different?

These come from the lighten()/darken() calls the deprecation warnings flagged in _comments.scss:240 and elsewhere. The
math:

Browsers accept fractional rgb() (CSS Color L4), so the rendered color is rgb(0, 80.5, 119) — half a step off the prior
rgb(0, 81, 119). Imperceptible visually, but technically not byte-identical and slightly off from what the designer
presumably intended (#5177).

The right fix is the same as resolving the deprecation: replace lighten()/darken() calls with the intended literal hex
(best — that's what the designer originally meant), or wrap with color.adjust() which is the modern API. As a stopgap
you can wrap with round() to coerce channels back to integers.

</details>

> Many empty lines between rulesets are removed, but not all of them consistently. 

While annoying, this one does not require any action. It's just a formatting issue.

From Claude:
> Dart Sass's expanded formatter inserts blank lines around rule boundaries based on its own heuristics (e.g., always after @font-face, between top-level rules, but not consistently between sibling rules that came from a @for/@each loop). It does not preserve blank lines from your .scss source. There's no flag to make it match LibSass's "preserve source whitespace" behavior — this is just the formatter, and the inconsistency is baked in.

> Some comma-separated selectors share a line, where `node-sass` had given each its own line. 

Claude output:

> As noted before, this is the @extend merge grouping. Dart Sass joins selectors that came from the same extend operation with ,  on a single line and uses ,\n between groups; LibSass put every selector on its own line unconditionally. No formatter flag.

After pushing a bit further, I had it create a small PostCSS plugin to enforce this.

Trac ticket: Core-63013.

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Opus 4.7
Used for: Researching the reasons behind the differences in built files after updating.

---
**This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See [GitHub Pull Requests for Code Review](https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/) in the Core Handbook for more details.**

@desrosj desrosj self-assigned this Jun 11, 2026
@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Twenty Nineteen theme’s front-end build pipeline to replace node-sass (LibSass) with sass (Dart Sass), and adjusts theme Sass/PostCSS to keep the generated CSS output stable (fonts/selectors/colors) under the new compiler.

Changes:

  • Replace node-sass with sass and update build scripts accordingly.
  • Refactor the font-family() Sass mixin away from @extend-based generation for non‑latin font fallbacks.
  • Add a PostCSS step to enforce one selector per line, and hard-code a few color outputs previously produced by deprecated lighten()/darken().

Reviewed changes

Copilot reviewed 6 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/wp-content/themes/twentynineteen/style-editor.css Regenerated editor stylesheet output from Dart Sass + updated mixins/formatting.
src/wp-content/themes/twentynineteen/sass/variables-site/_colors.scss Replace deprecated lighten()/darken() outputs with literal hex values.
src/wp-content/themes/twentynineteen/sass/site/primary/_comments.scss Replace deprecated lighten() output with a literal hex value.
src/wp-content/themes/twentynineteen/sass/mixins/_mixins-master.scss Refactor font-family() mixin to inline non‑latin fallbacks instead of @extend.
src/wp-content/themes/twentynineteen/print.css Regenerated/normalized selector formatting in print CSS output.
src/wp-content/themes/twentynineteen/postcss.config.js Add a custom PostCSS plugin to reformat selector lists.
src/wp-content/themes/twentynineteen/package.json Swap dependency to sass and update build scripts to use the Sass CLI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/wp-content/themes/twentynineteen/postcss.config.js
Comment on lines 127 to 131
@mixin font-family( $font_family: $font__body ) {
font-family: $font_family;
@extend %non-latin-fonts;
}

/* Build our non-latin font styles */
%non-latin-fonts {
@each $lang, $font__fallback in $font__fallbacks {
&:lang(#{$lang}) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants