| Name | Modified | Size | Downloads / 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.jsis ESM. Modern bundlers (Vite, webpack 5, esbuild, Rollup, Parcel 2+) handle it via the"browser"condition in theexportsmap. Browserify is no longer supported, it cannot parse ESM. README updated to point at modern bundlers. - CSS parser swapped:
mensch→postcss+postcss-safe-parser. Selector parser swapped:slick→postcss-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.outfiles refreshed to match. - Spec-correct selector specificity for
:is(),:where(),:has(),:not()per CSS Selectors Level 4::where(...)contributes 0, the others contributemax(spec(args)). Previously juice treated all four as a generic:pseudoand:notused "first arg only" (a slick legacy). Any cascade resolution that depended on the old quirks will produce different inlined styles. commanderupgraded to v14,entitiesupgraded to v8 (ESM-only),cheeriopinned to1.2.0.- TypeScript declarations renamed
juice.d.ts → index.d.tsand restructured for ESM default-export resolution. Import asimport 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 viapostcss-nestingbefore inlining. Previously these were silently dropped. No behavior change for already-flat CSS. @containerand@layerat-rules are preserved through inlining (mirrors how@media/@font-face/@keyframesalready worked). New optionspreserveContainerQueriesandpreserveLayersdefault totrue.- 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
typescondition inexportsand resolve correctly undernodenextmodule resolution.
Tooling
- Test runner: Mocha → Vitest 4 with
@vitest/coverage-v8. New scripts:npm test,npm run test:watch,npm run coverage. The previously brokentestcoverscript is gone. - CLI extraction:
bin/juice's logic moved intolib/cli.jsas a testablecli.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 dedicatedtest/typescript/tsconfig.jsonwithnodenextresolution. - Test assertions migrated from Node's
assertto Vitest'sexpect: 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