Skip to content

Juice 12#612

Draft
cossssmin wants to merge 20 commits into
masterfrom
juice-12
Draft

Juice 12#612
cossssmin wants to merge 20 commits into
masterfrom
juice-12

Conversation

@cossssmin
Copy link
Copy Markdown
Collaborator

@cossssmin cossssmin commented May 24, 2026

Juice 12

Modernizes the entire stack: ESM-only, PostCSS-based parsers, CSS Nesting + spec-correct selector specificity, Node 22.12+ floor, Vitest 4 with 100% line coverage.

Installation

Juice 12 is currently available in beta on the next tag, check it out:

npm i juice@next

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 [BUG] It stuck in an infinite loop when parsing specific css. #390, Infinite loop when parsing certain CSS rules #471, and Crash on unexpected properties #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.
  • Dropped 'use strict'; directives across the codebase — no-ops in ESM modules.
  • Removed devDependencies: mocha, should, batch, browserify. Added: vitest, @vitest/coverage-v8, postcss, postcss-safe-parser, postcss-selector-parser, postcss-nesting.

Migration notes (for downstream consumers)

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

@cossssmin cossssmin marked this pull request as draft May 24, 2026 15:56
@jrit
Copy link
Copy Markdown
Collaborator

jrit commented May 24, 2026

Amazing!! <3

@cossssmin
Copy link
Copy Markdown
Collaborator Author

@jrit I will release a beta v12 first to test it for a while in real projects and hopefully get feedback from users too.

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

Labels

None yet

Projects

None yet

2 participants