Download Latest Version v12.1.3 source code.zip (143.1 kB) Google Add to Preferred Sources
Home / v12.0.0
Name Modified Size InfoDownloads / Week
Parent folder
README.md 2026-06-04 4.6 kB
v12.0.0 source code.tar.gz 2026-06-04 72.9 kB
v12.0.0 source code.zip 2026-06-04 144.2 kB
Totals: 3 Items   221.7 kB 2

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]


https://github.com/Automattic/juice/compare/v11.1.1...v12.0.0

Source: README.md, updated 2026-06-04