Skip to content

Releases: Automattic/juice

v12.1.1

15 Jun 14:57
46cbc51

Choose a tag to compare

  • build(deps-dev): bump vitest from 4.1.8 to 4.1.9 29e9e67
  • build(deps): bump postcss-selector-parser from 7.1.1 to 7.1.4 a988791
  • build(deps-dev): bump @types/node from 25.9.2 to 25.9.3 9baacd5

v12.1.0...v12.1.1

v12.1.0

04 Jun 16:05
54c05d2

Choose a tag to compare

  • feat: add data-juice-important attribute 7cb46ac

v12.0.0...v12.1.0

v12.0.0

04 Jun 09:53
5afeb21

Choose a tag to compare

Juice 12

Modernizes the entire stack: ESM-only, PostCSS-based parsers to support CSS Nesting + spec-correct selector specificity, Node 22.12+ floor, Vitest 4.

Breaking changes

  • Juice is now ESM-only. CJS consumers on Node ≥ 22.12 keep working transparently via Node's built-in require(esm) interop. Older Node versions need to upgrade.
  • Node ≥ 22.12.0 required. Drops support for Node 18 and Node 20. CI matrix is now Node 22, 24, 26.
  • Browser: client.js is ESM. Modern bundlers (Vite, webpack 5, esbuild, Rollup, Parcel 2+) handle it via the "browser" condition in the exports map. Browserify is no longer supported, it cannot parse ESM. README updated to point at modern bundlers.
  • CSS parser swapped: menschpostcss + postcss-safe-parser. Selector parser swapped: slickpostcss-selector-parser. Both old libs were unmaintained since 2022. Inlining semantics unchanged; preserved-CSS output inside <style> blocks is now canonically formatted (mensch had quirks like ;} squashed onto one line). 4 fixture .out files refreshed to match.
  • Spec-correct selector specificity for :is(), :where(), :has(), :not() per CSS Selectors Level 4: :where(...) contributes 0, the others contribute max(spec(args)). Previously juice treated all four as a generic :pseudo and :not used "first arg only" (a slick legacy). Any cascade resolution that depended on the old quirks will produce different inlined styles.
  • commander upgraded to v14, entities upgraded to v8 (ESM-only), cheerio pinned to 1.2.0.
  • TypeScript declarations renamed juice.d.ts → index.d.ts and restructured for ESM default-export resolution. Import as import juice from 'juice'.

New features

  • CSS Nesting (Level 1) is supported. Nested rules (.card { &:hover { ... } }), nested at-rules (.card { @media (...) { ... } }), the & parent selector, and bare nested selectors (per the 2023 CSSWG resolution) are flattened via postcss-nesting before inlining. Previously these were silently dropped. No behavior change for already-flat CSS.
  • @container and @layer at-rules are preserved through inlining (mirrors how @media/@font-face/@keyframes already worked). New options preserveContainerQueries and preserveLayers default to true.
  • Long-standing crashes/hangs in :not(a, b, …) are gone as a side effect of the parser swap — the underlying causes of #390, #471, and #398 were quirks in mensch/slick that don't exist in postcss.
  • TypeScript types ship via the types condition in exports and resolve correctly under nodenext module resolution.

Tooling

  • Test runner: Mocha → Vitest 4 with @vitest/coverage-v8. New scripts: npm test, npm run test:watch, npm run coverage. The previously broken testcover script is gone.
  • CLI extraction: bin/juice's logic moved into lib/cli.js as a testable cli.run(argv, deps) with dependency injection. The bin itself is now a 3-line ESM shim.
  • Test files renamed: cli.js → cli.test.js, test.js → integration.test.js, run.js → cases.test.js. TypeScript test now uses a dedicated test/typescript/tsconfig.json with nodenext resolution.
  • Test assertions migrated from Node's assert to Vitest's expect: better failure diffs, more matchers, idiomatic.
  • Removed devDependencies: mocha, should, batch, browserify. Added: vitest, @vitest/coverage-v8, postcss, postcss-safe-parser, postcss-selector-parser, postcss-nesting.

Migration notes

Scenario Action needed
Node 18 or 20 user Upgrade to Node 22.12+
const juice = require('juice') on Node ≥22.12 None, require(esm) handles it
const juice = require('juice') on older Node Upgrade Node, or switch to import juice from 'juice'
TypeScript import juice = require('juice') Switch to import juice from 'juice'
juice/client via Browserify Switch bundler (Vite, webpack 5, esbuild, Rollup, Parcel 2+)
Using :is/:where/:has in email CSS Specificity is now per-spec; cascade may resolve differently
Using @container or @layer They now pass through inlining instead of being silently dropped
Using CSS Nesting Was silently dropped — now flattened and inlined correctly

Fixes #390, fixes #392, fixes #398, fixes #403, fixes #471, fixes #557, fixes #587, fixes #593


v11.1.1...v12.0.0

v12.0.0-beta.2

03 Jun 11:55
ac5f477

Choose a tag to compare

v12.0.0-beta.2 Pre-release
Pre-release
  • feat: add data-juice-duplicates attribute 7c4e394

v12.0.0-beta.1...v12.0.0-beta.2

v12.0.0-beta.1

24 May 17:42
0afc499

Choose a tag to compare

v12.0.0-beta.1 Pre-release
Pre-release
  • fix: apply default options consistently across all client functions 04ebded

v12.0.0-beta.0...v12.0.0-beta.1

v12.0.0-beta.0

24 May 16:42
a4b1146

Choose a tag to compare

v12.0.0-beta.0 Pre-release
Pre-release

This is the first beta for Juice 12.

For details on this release, see the PR:

#612


  • feat: support CSS nesting via postcss-nesting 7ebefd5
  • test: migrate assertions from node assert to vitest expect c25f011
  • chore: drop redundant 'use strict' directives (ESM is strict by default) 879ff57
  • test(numbers): cover romanize NaN early-return d923614
  • test(selector): close coverage gaps in lib/selector.js 8932d29
  • test(utils): close coverage gaps in lib/utils.js 1f6ca6d
  • test(inline): close coverage gaps in lib/inline.js e8c5846
  • test(index): cover error paths and codeBlocks merge c3ff857
  • chore: update contributors list 7e49a76
  • feat!: modern CSS specificity + at-rule preservation d306f78
  • refactor!: replace mensch+slick with postcss 1926c82
  • refactor!: convert to ESM-only ffe9d67
  • build(deps): bump cheerio to 1.2.0 f2f16e6
  • chore: require node >=22.12.0 34ab2b2
  • build(deps): upgrade commander to ^14 6894ca9
  • refactor(cli): extract bin into testable cli.run() for coverage e030df3
  • test: migrate to vitest 4 with v8 coverage b8eefd1
  • build(deps-dev): bump @types/node from 25.3.3 to 25.3.5 de6d8d7
  • build(deps-dev): bump @types/node from 25.3.0 to 25.3.3 e42ecf6
  • build(deps-dev): bump @types/node from 25.2.3 to 25.3.0 18a1c32
  • build(deps-dev): bump @types/node from 25.2.0 to 25.2.3 bd54313
  • chore: update changelog.md f093f01

v11.1.1...v12.0.0-beta.0

v11.1.1

04 Feb 13:01

Choose a tag to compare

  • fix: data-embed style tags db64a51
  • build(deps): bump entities from 7.0.0 to 7.0.1 476d286
  • build(deps-dev): bump @types/node from 25.0.10 to 25.2.0 306ceb7

v11.1.0...v11.1.1

v11.1.0

05 Jan 16:35

Choose a tag to compare

New features


v11.1.0-2...v11.1.0

v11.1.0-2

02 Nov 13:09

Choose a tag to compare

v11.1.0-2 Pre-release
Pre-release
  • revert: selectors must match exactly de7a6ed

v11.1.0-1...v11.1.0-2

v11.1.0-1

02 Nov 12:42

Choose a tag to compare

v11.1.0-1 Pre-release
Pre-release

This pre-release adds the preservedSelectors option, which can be used to preserve CSS rules in <style> tags when removeStyleTags or removeInlinedSelectors are true.

For example, this:

juice(`
  <style>
    div { color: red; } 
    .preserve-me { background: blue; }
  </style>
  <div class="preserve-me">Test</div>
  `, 
  { 
    removeStyleTags: true, 
    preservedSelectors: ['.preserve-me'] 
  }
)

... would return this HTML:

<style>
  .preserve-me { background: blue; }
</style>

<div class="preserve-me" style="color: red; background: blue">Test</div>

  • chore: update readme.md 1ae20ef
  • refactor: selectors must match exactly 6b3ff8a
  • test: update preservedSelectors test e70f544
  • refactor: sort options interface alphabetically c0daa68
  • feat: preservedSelectors option fbd82ca

v11.1.0-0...v11.1.0-1