npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

overrule

v0.6.0

Published

catch Tailwind class conflicts before they ship. Native binary CLI plus the join/guard runtime.

Readme

overrule

tailwind-merge as a dev tool, not a dependency at all. The CLI is Rust now.

check reports class strings whose tokens fight and exits 1. fix rewrites each one to the form the merge would have produced, so it cannot change a pixel. cross compares the name tables against your compiled stylesheet and prints every disagreement. Same contract as the 0.3.x npm CLI, byte for byte where the engines agree: same text output, same JSON shapes, same exit codes, and ack snapshots interop both ways.

The runtime half ships from the same npm package and stays JavaScript, because dev-bundle work is JavaScript's turf. See Runtime.

Install

bun add -d overrule
# or
npm i -D overrule

The package is a native binary behind a 30-line shim: npm picks the right prebuilt from optionalDependencies (linux x64/arm64 plus x64 musl, macOS x64/arm64, windows x64/arm64) and npx overrule hands over to it. Any other platform builds from source with cargo build --release.

Run

npx overrule check src/                       # report conflicts, exit 1 if any
npx overrule fix src/                         # rewrite them, losers removed
npx overrule check src/ --css src/app.css     # judge with your stylesheet
npx overrule cross src/ --css src/app.css     # tables vs stylesheet
npx overrule cross src/ --ack acks.json       # CI gate: new disagreements only

--css and cross compile your classes with Tailwind itself, so they need node plus tailwindcss and @tailwindcss/node, 4.2 or newer, reachable from the CSS entry's project or from the directory you run in. Plain check and fix need nothing.

Runtime

The npm package is the JavaScript runtime too. The root entry has no oracle and no tailwind-merge in its import graph at all; oracles are explicit, imported from overrule/oracle and passed in. The manifest declares sideEffects: false, so when the DEV branch folds away, the oracle import goes with it. Wire the guard in dev and it vanishes in prod:

import { join, guard } from 'overrule';
import { createMapOracle } from 'overrule/map';
import map from './conflicts.json';
const cn = import.meta.env.DEV ? guard(join, createMapOracle(map)) : join;

The map is yours: npx overrule map src/ --css src/app.css --out conflicts.json compiles every token in your project with your real stylesheet, and createMapOracle judges with that. No name tables, no tailwind-merge anywhere: as of 0.6.0 it is not a dependency, not a peer, nothing. It remains the reference the Rust tables were verified against, one verdict at a time.

join concatenates clsx-style inputs and resolves nothing. guard wraps it and warns when a class string carries tokens that fight, so the conflict shows at dev time instead of leaving it to the cascade. overrule/test runs the same check in your suite through assertMergeFree and assertVariantsMergeFree. declareVariants builds a variant function from class strings with no merge engine behind it. The first three trace back to the v0.3.1 tag; the rest is newer.

overrule/props

createMergeProps(options) builds a function that composes component prop objects without tailwind-merge, one layer up from join. Class strings go through join, so conflicts still surface through guard and the oracle instead of resolving silently. Style objects and strings merge, same-named functions compose or chain, everything else is last value wins.

import { createMergeProps } from 'overrule/props';
const mergeProps = createMergeProps();
const props = mergeProps({ class: 'px-2', onclick: open }, { class: 'px-4', onclick: track });

With no options the merger is platform-neutral: a merged style stays an object, no attribute is dropped, functions are chained. Options opt into framework idioms: styleAs: 'string' serializes style to a string, dropFalseAttrs removes named boolean attrs when they merge to false, and isEventHandler marks which same-named functions compose with preventDefault (the rest chain). Framework-specific policy — the Svelte/bits-ui combination, say — is the consumer's to assemble from these options; overrule ships only the engine. The primitives ship too: composeEventHandlers, chain, mergeStyles, styleToObject, styleToString. Nothing rides along as a dependency, because the CSS parse and serialize are inline.

What moved, what stayed

| v0.3.1 | now | notes | | --- | --- | --- | | src/parse.ts | src/parse.rs | direct port | | src/oracle.ts | src/oracle.rs | tailwind-fuse plays tailwind-merge, see below | | src/scan.ts | src/scan.rs | direct port, plus rayon: files are judged in parallel | | src/css.ts | src/css.rs | direct port of the judging logic, pure like the original | | src/css-node.ts | src/bridge.rs + bridge/dump-asts.mjs | see below | | src/cli.ts | src/main.rs | check, fix, cross, --css, --json, --ack, annotations | | join, guard, overrule/test | the npm package | they ship in consumers' dev bundles and stay JavaScript |

The two design calls

tailwind-fuse instead of tailwind-merge. The tables side needs a merge engine and tailwind-merge is JavaScript. tailwind-fuse is the Rust port of it, so it slots into the same seam. But its tables lag, so the oracle carries a patch layer, each verdict taken from a tailwind-merge 3.6 run: ring widths through arbitrary values (the shadcn focus ring), outline-hidden, v4 gradient, size and position backgrounds, font-stretch and non-weight font families, text-shadow, text-base/7, and the overflow-x/y lump. It also only parses the v3 leading !, so the oracle rewrites font-normal! to !font-normal before judging to keep important and normal classes out of each other's way. What remains is false negatives: groups fuse has never heard of (mask-*, field-sizing-*) pass silently. It is the same lesson this tool is built on: name tables drift, and cross makes the drift visible. Judge with --css in CI; the bare tables are the quick local pass.

The stylesheet oracle still compiles with Tailwind itself. Reimplementing the compiler in Rust would recreate exactly the drift this tool exists to catch. So the judging logic (css.rs) is a pure port and the compiling is one batched node call: collect every token the scan will see, hand the batch to bridge/dump-asts.mjs, judge the ASTs in Rust. One subprocess per run, ~100ms. The script resolves @tailwindcss/node from the scanned project first, hopping through @tailwindcss/vite and friends for isolated installs like bun and pnpm, then from the invocation directory as the escape hatch for library packages that rightly declare no tailwind at all.

Test parity

Every test from the npm package maps here. bun install once so the cross test can compile; without it that one test skips.

| v0.3.1 | now | | --- | --- | | parse.test.ts, 16 cases | src/parse.rs, all 16 | | oracle.test.ts, engine behavior | src/oracle.rs, plus the findConflicts cases from guard.test.ts | | cli.test.ts, scanner and fix | src/scan.rs units + tests/scan.rs, fixed cross-checked against tw_merge | | css.test.ts, 22 cases | src/css.rs, all judging cases against pregenerated ASTs; the platform-neutrality test lives in src/lib.rs | | cli-bin.test.ts, binary end to end | tests/cli.rs, including the cross --ack gate against a real Tailwind compile | | join.test.ts, guard.test.ts, assert.test.ts | runtime/join.test.ts, guard.test.ts, merge-free.test.ts; props and declare-variants suites are newer |

tests/fixtures/asts*.json are candidatesToAst dumps from the tailwindcss in node_modules; node tests/fixtures/generate.mjs regenerates them after a bump.

Numbers

84,300 files (a production monorepo duplicated 100x), warm cache:

  • npm CLI (node): 1.52s
  • this, single-threaded: 1.15s
  • this, rayon: 0.28s

The single-threaded delta is small because the merge engine dominates, not the language. The parallel delta is the actual argument for Rust here, and it cost ten lines.

Porting notes worth remembering

  • The attr regex excludes BOTH quote types from the content on purpose. A Svelte interpolation {cond ? 'a' : 'b'} inside a double-quoted attribute always carries the other quote, so the exclusion is what keeps branch-split literals from being judged as one string. Relaxing it produced a false conflict across ternary branches within the hour.
  • libuv sorts scandir results, Rust's read_dir does not. The walker sorts entries by name so findings come out in the npm CLI's order and outputs stay diffable.
  • Byte indexing is safe everywhere the scanner slices, because every delimiter it looks for is ASCII and an ASCII byte in UTF-8 is always a real character.
  • The Oracle trait takes &self, so the token collector in the CLI holds its set behind a Mutex. It was a RefCell until scan_paths went parallel; the compiler rejected it the same minute.

Prior art

tailwind-merge is the engine this whole idea demotes to a checker, and tailwind-fuse carries its tables into Rust. Credit where due: the checking is only possible because they wrote the tables.

License

MIT