@lunawerx/normwind
Normalize Tailwinds.
A zero-config CLI and GitHub Action that finds bloated Tailwind utility classes and rewrites them into their short, canonical form.
NormWind is a zero-config CLI and GitHub Action that audits Tailwind CSS class strings in Vue, Svelte, Astro, HTML, JavaScript, and TypeScript files, then rewrites verbose utility combinations and non-canonical arbitrary values into their shorter canonical form using Tailwind's own canonicalization engine, without imposing a rules file or sort order on the project.
Tailwind codebases drift. Over time (especially with a dozen hands and a few AI assistants on the keyboard) class strings pile up combinations that are perfectly valid but noisier than they need to be. px-4 py-4 where p-4 would do. rounded-[24px] where rounded-3xl already exists. w-6 h-6 instead of size-6.
NormWind hunts those down. Point it at your project and it will either tell you what could be tightened, or fix it for you: safely, deterministically, and without forcing a formatter, a sort order, or any config on your repo.
npx @lunawerx/normwind # audit
npx @lunawerx/normwind --fix # audit, then safely rewrite
✨ At a glance
NormWind collapses two kinds of noise: verbose utility combinations and non-canonical arbitrary values.
| You wrote… | NormWind gives you |
|---|---|
px-4 py-4 |
p-4 |
pl-2 pr-2 |
px-2 |
mt-3 mb-3 |
my-3 |
w-6 h-6 |
size-6 |
content-center justify-center |
place-content-center |
rounded-[24px] |
rounded-3xl |
w-[100%] |
w-full |
h-[1.5rem] |
h-6 |
Every rewrite is backed by Tailwind's own engine (more on that below); NormWind never guesses.
🤔 Why you'd want it
Reach for NormWind when you want:
- 🧵 Shorter class strings that stay readable
- 🎯 Consistent shorthand across a whole team
- 🚫 Fewer arbitrary values when a named Tailwind token already exists
- 🛡️ CI enforcement so utility bloat never lands on
mainagain - 🩹 Safe autofixes: markup-first by default (Vue, Svelte, Astro, HTML), broader when you ask for it
- 🌱 Zero config: no rules file, no plugins to register, no opinions imposed
📦 Install
npm i -D @lunawerx/normwind
…or don't install it at all:
npx @lunawerx/normwind
Both command names are exposed, so pick whichever your fingers prefer:
normwind
normwinds
🚀 Quick start
# Audit the current project (exits 1 if there's anything to clean up)
npx @lunawerx/normwind
# Apply safe, markup-first fixes, then re-audit
npx @lunawerx/normwind --fix
# Go wide: fix across every supported file type
npx @lunawerx/normwind --fixall
# Preview a fix run without writing anything to disk
npx @lunawerx/normwind --fixall --dry-run
# Machine-readable output for CI and custom tooling
npx @lunawerx/normwind --json
# Scope it to a folder or a glob
npx @lunawerx/normwind src
npx @lunawerx/normwind "apps/web/**/*.{tsx,ts}"
🧠 What NormWind checks
Shorthand utility combinations
NormWind detects class groups that Tailwind can express with a shorter shorthand:
| Verbose | Canonical shorthand |
|---|---|
px-4 py-4 |
p-4 |
pl-2 pr-2 |
px-2 |
mt-3 mb-3 |
my-3 |
left-0 right-0 |
inset-x-0 |
top-0 bottom-0 |
inset-y-0 |
gap-x-4 gap-y-4 |
gap-4 |
overflow-x-hidden overflow-y-hidden |
overflow-hidden |
w-6 h-6 |
size-6 |
content-center justify-center |
place-content-center |
items-start justify-items-start |
place-items-start |
self-end justify-self-end |
place-self-end |
Canonical arbitrary values
It also catches arbitrary values that Tailwind's own design system can express as a named utility:
| Arbitrary | Canonical |
|---|---|
rounded-[24px] |
rounded-3xl |
w-[100%] |
w-full |
h-[1.5rem] |
h-6 |
p-[1rem] |
p-4 |
m-[8px] |
m-2 |
🔒 Why you can trust the rewrites
NormWind doesn't invent mappings:
- Shorthand merges come from
eslint-plugin-tailwindcss's Tailwind utility-group definitions, and, when the engine supports it, from Tailwind itself:designSystem.canonicalizeCandidates(classes, { collapse: true })replaces a set with one utility only when the compiled CSS of the replacement matches the set's. Tailwind's collapse only adds merges the group table misses (such astext-sm leading-6->text-sm/6); it never replaces or drops a merge the table finds (setNORMWIND_DISABLE_ENGINE_COLLAPSE=1to use the table alone). - Canonical values come from Tailwind's own
designSystem.canonicalizeCandidatesengine. - A merge is only applied when it cannot change the rendered CSS. Tailwind emits utilities in its own order (broad before narrow, and same-utility candidates sorted by value), not in authoring order. Collapsing
ml-2 mr-2intomx-2when the same class list already hasmx-8would hand the win tomx-8and silently change the margin. So whenever another utility in the same group targets the same property at a different value, NormWind resolves the before and after class lists through Tailwind's own engine and compares the resulting declarations; if they differ at all, the merge is skipped. Skipping costs a suggestion; applying would cost a silent visual regression.
This is deliberately conservative: an unusual class list can keep a merge NormWind could not prove safe, and that's the correct outcome.
🎛️ CLI reference
| Command | What it does |
|---|---|
normwind |
Audit supported files. Exits 1 when findings exist. |
normwind --reporter <text|json|sarif> |
Choose the output format. sarif emits SARIF 2.1.0 for GitHub code scanning and similar CI dashboards. |
normwind --json |
Print machine-readable audit output (alias for --reporter json). |
normwind --ignore <glob> |
Skip paths matching a glob. Repeatable. |
normwind --allow-empty |
Exit 0 instead of 2 when the given pattern(s) match no lintable files. |
normwind --diff-base <ref> |
Report, and exit 1 for, only findings on lines changed since the merge base of <ref> (e.g. origin/main), plus untracked files. Findings on other lines are counted on stderr, so an existing codebase can adopt the audit at full strictness. |
normwind --baseline <file> |
Adopt NormWind on a codebase that already has findings: only findings beyond each file's recorded count fail. See Adopting on an existing codebase. |
normwind --baseline <file> --update-baseline |
Write the current per-file counts to the baseline. Creates it once; afterwards it only lowers or removes counts. |
normwind --fix |
Apply safe fixes across every markup format (Vue, Svelte, Astro, HTML), then re-run the audit. |
normwind --fixall |
Apply broader fixes across every supported source type, including JS, MJS, CJS, TS, JSX, TSX, MTS, and CTS, then re-run the audit. |
normwind --fix --dry-run / normwind --fixall --dry-run |
Show which files would be rewritten without writing anything to disk. |
normwind --extract-canonical |
Extract canonical replacements in memory and print a summary. |
normwind --extract-canonical --write-canonical-files |
Write docs/reference/canonical-replacements.{json,md}. |
normwind --check-canonical |
Fail if the generated canonical files are missing or stale. |
normwind --cleanup-canonical-files |
Remove generated canonical files from docs/reference. |
normwind --suggest-named-theme-vars --theme-css <path> |
(opt-in) Suggest replacing utility-(--md-sys-color-x) with your project's named theme class. Off by default. |
normwind -- <path> |
End flag parsing, so a target that starts with a dash is treated as a pattern, not a flag. |
normwind -h, --help |
Print usage and exit 0. |
normwind -v, --version |
Print the installed normwind version and exit 0. |
🧾 Output
Text output groups findings by file, readable at a glance:
normwinds v3.5.0: 2 finding(s) across 1 linted file(s).
src/components/Card.vue
42:14 Classnames 'px-4, py-4' could be replaced by the 'p-4' shorthand!
67:10 The class 'rounded-[24px]' can be written as 'rounded-3xl'
JSON output is stable and CI-friendly:
{
"version": "3.5.0",
"ruleId": "tailwindcss/enforces-shorthand",
"lintedFiles": 1,
"findingCount": 1,
"findings": [
{
"filePath": "src/components/Card.vue",
"line": 42,
"column": 14,
"message": "Classnames 'px-4, py-4' could be replaced by the 'p-4' shorthand!"
}
]
}
Exit codes
| Code | Meaning |
|---|---|
0 |
No findings, or a requested maintenance command completed successfully. |
1 |
Audit findings exist, or canonical drift was detected. With --baseline: a file has more findings than its recorded count, a count dropped and the baseline was not lowered, or --update-baseline refused to raise a count. |
2 |
Usage or runtime error (unknown flag, invalid --reporter value, missing --theme-css value, unreadable --theme-css path, missing or malformed --baseline file, etc.); a pattern matched no lintable files (pass --allow-empty for 0 instead); --fix/--fixall/--dry-run was combined with --check-canonical, --extract-canonical, or --cleanup-canonical-files; --dry-run was passed without --fix/--fixall; --diff-base could not be read (not a git checkout, unknown ref) or was combined with a canonical-maintenance flag; or --fix/--fixall finished with one or more files skipped/failed (see the fix summary printed to stderr). |
🔧 Fix modes
--fix: the safe default
--fix is intentionally conservative. It covers every markup format (.vue, .svelte, .astro, .html/.htm) and is the best default for application projects that just want tidy, low-risk cleanup.
--fixall: the whole codebase
--fixall applies the broader class-string rewrite pass across all supported source types, including .js/.mjs/.cjs/.ts/.jsx/.tsx/.mts/.cts. Use it for explicit cleanup branches, codemods, or repos with solid review coverage.
Composite equivalences (truncate, place-content-*, place-items-*, place-self-*) are now applied by both --fix and --fixall, not just reported. Previously they were audit-only findings a fix run could never resolve, so a fix-then-audit CI loop could never reach exit 0.
Per-file fault isolation
Each file's read, transform, and write are isolated: a locked file (EBUSY/EPERM, common on Windows when an editor or antivirus has it open), a full disk (ENOSPC), or an edge-case parser exception on one file is logged and skipped. It never aborts the rest of the batch. At the end of a run with any skips or failures, a summary is printed to stderr (fix summary: N fixed, N skipped, N failed) with a line per affected file, and the process exits 2 so CI can distinguish a partial run from a clean one.
--dry-run: preview without writing
Add --dry-run to either --fix or --fixall to see which files would be rewritten (each is printed as [dry-run] would rewrite <path>) without touching anything on disk. Combine it with --json to get the same findings a real run would report, so you can review the diff a rewrite would produce before committing to it.
Always run NormWind's autofix from a clean working tree under version control (git or equivalent), and review the diff before committing.
--fix/--fixallwrite through an atomic temp-file-then-rename per file, preserve Unix file modes, and refuse to replace a file that changed after it was read. A working-tree safety net (commit, stash, or branch) is still the only way to cheaply undo a rewrite you don't like.--dry-runis the no-risk way to preview first.
📁 File matching
By default NormWind scans:
**/*.{vue,svelte,astro,html,htm,js,mjs,cjs,ts,jsx,tsx,mts,cts}
…and skips the usual generated / dependency folders. At any depth:
.git · .venv · .next · .nuxt · .output · .svelte-kit · .astro · .turbo · cdk.out · dist · node_modules · storybook-static · test-results
At the project root only:
.cache · .tmp · build · coverage · dist · out · test-results · vendor
Target specific paths or globs any time:
npx @lunawerx/normwind src
npx @lunawerx/normwind "src/**/*.vue"
npx @lunawerx/normwind "apps/web/**/*.{tsx,ts}"
Use -- to end flag parsing when a target itself starts with a dash: npx @lunawerx/normwind -- -weird-dir.
A pattern that matches no lintable files now exits 2 (see Exit codes); pass --allow-empty to keep the old exit-0 behavior.
🙈 Ignoring paths
Two ways to skip paths beyond the built-in generated-folder list:
--ignore <glob>: repeatable, always honored, on the CLI and via the GitHub Action'signoreinput..normwindignore: a project-local file, one glob per line,#for comments, read automatically from the scanned directory. A bare directory name (no glob syntax) means everything under it, the same convention.gitignoreuses.
.normwindignore is deliberately not read in GitHub Action mode. The file is checkout-controlled, so a pull request could otherwise edit it to silence the audit on the very files that PR changes. --ignore flags come from the workflow author, not the checkout, so they're always honored in both modes.
🎯 What gets scanned
NormWind only audits and fixes class strings anchored to class-bearing attributes:
class="...",className="...",:class="...", andv-bind:class="...": quoted values, JSX-brace values (className={...}), quoted strings nested inside those values (ternaries, object/array bindings), and static chunks of template literals.- Object-property form:
{ class: "..." }/{ className: "..." }when the object is passed directly to a recognized React/Vue/Preact-style render function such ascreateElement,h,jsx, or an imported alias of one of them. - Class-string builder calls: the string arguments of
clsx,cx,cn,classnames,classNames,cva,tv,twMerge, andtwJoin, including strings nested incva/tvvariant objects, arrays, ternaries, andcond && "...". Locally-aliased imports of these are resolved. Strings that aren't class lists (variant keys such as"lg") are filtered out by the same shape test used everywhere else. - Attribute names that merely end in
-class(e.g.data-class) are excluded: only the exact attribute names above match. - Template-literal
${...}interpolations no longer disqualify the whole literal; static chunks around an interpolation are still processed, and partial tokens straddling an interpolation boundary (e.g.h-${size}) are never touched. - Class strings containing a bare
*outside[...]/(...)brackets are conservatively skipped by both the audit and the fixer.
JavaScript, TypeScript, and JSX/TSX are parsed before extraction, and Vue SFC template/script boundaries are tracked structurally. Comments, serialized markup, regex literals, interpolated template text outside class props, title="..." text, and unrelated { class: "..." } data objects are therefore excluded. If a supported JavaScript/TypeScript source block cannot be parsed safely, the file is reported as failed and is left untouched.
Guarantee: strings outside real class-bearing attributes and recognized render-function props are never inspected or modified. Earlier versions scanned every quoted string in a file; that behavior is gone.
🎨 Named theme variables: --suggest-named-theme-vars / --theme-css (advanced)
NormWind can rewrite class tokens that reference a Tailwind v4 @theme variable in their long form (border-(--color-ink-400), border-[var(--color-ink-400)]/40, rounded-[var(--radius-sm)]) into the equivalent named-utility form (border-ink-400, border-ink-400/40, rounded-sm).
Two @theme patterns are supported:
Direct pattern: the project authors the theme key itself:
@theme {
--color-ink-400: var(--color-zinc-400);
--radius-sm: 0.125rem;
}
border-(--color-ink-400)/40 → border-ink-400/40, rounded-[var(--radius-sm)] → rounded-sm.
Forwarder pattern: the project forwards a Tailwind-namespaced theme var to a foreign root variable:
@theme {
--color-outline-variant: var(--md-sys-color-outline-variant);
}
border-(--md-sys-color-outline-variant) → border-outline-variant.
Both rewrites are gated by a per-token CSS rule-body equivalence check: NormWind asks Tailwind to compile both candidates and only emits the rewrite when the produced rule bodies are byte-equivalent (after substituting the forwarder, where applicable). Ambiguous forwarders, unknown variables, and prefix-mismatches (e.g. rounded-(--color-ink-400): rounded- is not a color utility) are silently skipped.
Audit (opt-in via flag):
npx @lunawerx/normwind \
--suggest-named-theme-vars \
--theme-css src/assets/css/theme.css \
--json
Fix (auto-engaged when --theme-css is provided):
npx @lunawerx/normwind \
--fixall \
--theme-css src/assets/css/theme.css
The audit suggestion flag is intentionally still opt-in to preserve the existing public audit contract. During --fix/--fixall the safety gate is the per-token equivalence check, so passing --theme-css alone is enough. When neither --theme-css nor --suggest-named-theme-vars is passed, NormWind's output is identical to v3.0.0; existing CI pipelines are unaffected.
🗂️ The bundled canonical snapshot
NormWind ships a generated Tailwind canonical-replacement snapshot so your first run is fast and deterministic: no cold-boot compilation, and nothing generated is written into your project:
docs/reference/canonical-replacements.jsondocs/reference/canonical-replacements.md
It's generated from Tailwind's own canonicalization engine for the exact Tailwind version this package bundles. At runtime, NormWind resolves a Tailwind v4 installation from the scanned project's working directory first and uses the bundled version only as a standalone fallback. That keeps autofixes aligned with the Tailwind version that will actually build the project.
Lookup order:
- A project-local snapshot at
docs/reference/canonical-replacements.json, if present. - The package-bundled snapshot, only when its Tailwind version matches the active project/fallback engine.
- The active Tailwind version's live design-system canonicalizer, for cache misses or a missing/mismatched snapshot.
Disk-cache entries are validated against the installed Tailwind version before use and are written atomically. To keep scans of untrusted code resource-bounded, a single run refuses to send more than 1,000 unique cache misses through Tailwind's live canonicalizer; that condition is reported as a runtime error instead of risking a Node out-of-memory crash.
Tailwind 4.0 does not expose the canonicalization API used by newer v4 releases. NormWind still audits and fixes shorthand groups on 4.0, but safely leaves arbitrary-value classes unchanged instead of applying newer bundled semantics.
Maintainers can regenerate and verify it:
npm run canonical:extract # regenerate
npm run canonical:check # verify (deterministic, CI-safe)
🤖 In CI
GitHub Action
Add inline annotations and fail a pull request when shorthand or canonical findings exist:
name: NormWind
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
tailwind-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: LunarWerxs/NormWind@v3
The Action is bundled, cross-platform, and read-only. It needs no API token, performs no runtime package installation, emits inline annotations plus a job summary, and writes a machine-readable JSON report to the runner. The report-path output remains available to later steps in the same job, where you can inspect it or pass it to your artifact uploader.
For PR safety, Action mode treats the checkout strictly as data: it does not execute repository dependencies or tools, does not trust a repository-provided NormWind snapshot, and always uses the Tailwind engine bundled with that Action release. The standalone CLI retains project-version-aware Tailwind resolution for trusted local use.
Pin @v3.7.0 or the release commit SHA instead of @v3 when you require an immutable dependency.
Action inputs and outputs
Inputs:
| Input | Default | Purpose |
|---|---|---|
working-directory |
. |
Directory to scan, relative to the repository root. |
patterns |
all supported files | Newline-delimited file paths, directories, or globs. |
theme-css |
none | Tailwind CSS entry file used to resolve project theme variables. |
suggest-named-theme-vars |
false |
Suggest named theme utilities; requires theme-css. |
fail-on-findings |
true |
Fail CI when normalization findings exist. |
max-annotations |
10 |
Inline annotation cap from 0 to 50; every finding remains in the summary/report. |
ignore |
none | Newline-delimited globs to skip. A .normwindignore file in the checkout is deliberately ignored in Action mode (the checkout is untrusted input), so use this workflow-authored input instead. |
sarif-file |
none | Path, relative to the working directory, to write a SARIF 2.1.0 report to. Pair it with github/codeql-action/upload-sarif to surface findings in code scanning. |
changed-lines-only |
false |
Annotate, write SARIF for, and fail on only the findings on lines the change touches; the JSON report still lists every finding. Check out with fetch-depth: 0 so the base branch is present. |
diff-base |
origin/<PR base branch> |
Ref changed-lines-only diffs against (its merge base with HEAD). Required outside a pull_request event. |
baseline |
none | Path, relative to the working directory, to a baseline of per-file finding counts (see Adopting on an existing codebase). Only findings beyond a file's count fail, and a count that dropped fails until the baseline is lowered. The Action only reads it, never rewrites it. |
Outputs: version, finding-count, linted-files, result, exit-code, report-path, and sarif-path (set only when sarif-file was provided).
Any CI provider
Use the CLI directly anywhere Node.js is available:
npx @lunawerx/normwind --json
That's it: exit code 1 fails the job, and the JSON payload is stable enough to feed a custom reporter or annotation step.
Adopting on an existing codebase
A project that already has hundreds of findings can still turn the gate on today. Record what is there once, commit the file, and point CI at it:
npx @lunawerx/normwind --baseline .normwind-baseline.json --update-baseline
The baseline is a small JSON file of per-file finding counts (paths relative to the directory NormWind runs in). With --baseline, a file may keep up to its recorded count; one more finding fails the run and reports every finding in that file, since line numbers move and any of them may be the new one. It is a ratchet, so the debt only goes down:
- A count that rises fails.
--update-baselinerefuses to raise a count or add a file; fix the new finding, or edit the file by hand if the increase is deliberate (the change then shows up in review). - A count that drops also fails, with a note on line 1 of that file, until you run
--update-baselineagain to lower it. That keeps the baseline honest instead of quietly leaving room for new findings to come back. - Entries for files outside the run's patterns are left alone; entries for deleted files are stale and removed on the next update. An incomplete scan (a skipped or unreadable file) never rewrites the baseline.
In the GitHub Action, pass the committed file through the baseline input. The Action only reads it. Because it lives in the checkout, a pull request that raises a count shows the change in its diff; guard the file with CODEOWNERS if that matters to you. The JSON report gains a baseline object (path, suppressed, exceeded, stale, updated) whenever --baseline is set.
🧰 What's in the box
The npm package intentionally publishes only runtime assets, brand assets, and reference docs:
bin/normwind.mjsdocs/reference/canonical-replacements.jsondocs/reference/canonical-replacements.mdassets/: logos and the share cardREADME.mdpackage.json
Tests and fixtures live in the source repo but are excluded from the packed tarball.
🧑💻 Development & maintainer commands
Run the full pre-push verification suite:
npm test # alias: npm run prepush
The pre-push suite verifies package metadata, canonical-snapshot integrity, canonical drift, regression fixtures, the rewrite-precision gate, live-Tailwind-vs-snapshot parity, CLI audit/fix smoke behavior, and npm pack --dry-run contents.
Other useful scripts:
npm run test:regression # run the regression fixtures
npm run test:regression:update # update fixtures after an intentional change
# (review each fixture's expected.golden.txt diff:
# the input with ~~~ under every finding, then the fixed output)
npm run test:compare # live canonicalizer vs bundled snapshot
npm run test:precision # score the rewrite rules against the labelled corpus
npm run canonical:extract # regenerate canonical replacement files
npm run canonical:check # verify canonical replacement files are current
npm run deps:sync # after a dependency bump: refresh bun.lock and rebuild dist/
Rewrite precision gate
A false positive is a bug in a rewrite rule, so the rate is measured, not assumed. test/precision/corpus.json holds labelled class strings: rewrite cases name the exact result --fixall must produce, and keep cases must be neither reported nor rewritten. npm run test:precision runs the whole corpus through the real CLI twice (audit, then --fixall) and fails when either pass drops below precision 0.90, recall 0.85, or rises above a false-positive rate of 0.10. A wrong rewrite counts as a false positive, and a corpus with no cases on one side of the label fails rather than scoring as perfect. Every misclassified case is listed by id.
A new rewrite rule lands with at least one rewrite case and one keep case that pins where the rule must stop.
Updating dependencies
This repo commits three things that a dependency bump has to move together:
| File | Written by | Enforced by |
|---|---|---|
package.json + package-lock.json |
npm |
npm ci in CI and the release workflow |
bun.lock |
bun |
prepush checks lockfiles agree on dependency versions (declared) and lockfiles agree on the whole resolved tree (transitive) |
dist/index.mjs + dist/normwind.mjs |
@vercel/ncc, via npm run build:action |
prepush check action: committed bundle is current |
Dependabot only knows about the first row. It has no way to update the other two, and no Dependabot configuration can fix that: bun.lock and package-lock.json describe the same package.json, so adding a second package-ecosystem would just open a second, conflicting PR for every bump. Every Dependabot PR therefore arrives red on purpose, and finishing it is a manual step:
gh pr checkout <number>
npm run deps:sync
git add bun.lock dist/ # only if they actually changed
git commit -m "chore(deps): sync bun.lock and dist/ with the npm bump"
git push
deps:sync needs Bun on PATH for its first step; nothing else in the repo does, and CI never runs it. If you do not have Bun, ask someone who does to push the bun.lock half.
Note that npm test chains its three suites with &&, so a stale bun.lock short-circuits before the bundle check ever runs. If you fix only the lockfile, expect the dist/ failure to appear on the next push. npm run deps:sync does both at once for exactly this reason.
Security bumps on a transitive package need one extra step
deps:sync is enough for anything Dependabot touches, because those are dependencies declared in package.json. It is not enough for a transitive one, which is what most advisories are:
npm audit fix # moves package-lock.json only
bun update <name> # bun.lock needs to be told explicitly
npm ci && npm run build:action # then resync the tree and the bundle
The bun install inside deps:sync will not move a pin that still satisfies its range. When nanoid was patched for GHSA-2v37-7h3g-55p8, 3.3.16 still satisfied its parent's ^3.3.16, so bun install left the vulnerable copy in place while npm ci installed the fixed one. The lockfiles agree on the whole resolved tree check exists to catch precisely that.
Resolving a dist/ conflict during a rebase
Rebuild the bundle. Never take either side.
npm run build:action && git add dist/
dist/ is generated, so neither side of a conflict is authoritative and a textual merge of two minified bundles is meaningless. Taking one side looks like it worked and then ships a stale bundle: the action: committed bundle is current check will fail, but only on the commit that carries the stale copy, which may not be the one that looked conflicted. That is precisely how it happened on 3fe92ad, which went red on all seven CI legs.
You should not have to remember this. .gitattributes marks dist/** as -merge, so git refuses to invent a merge result there: it conflicts, leaves the current branch's copy in place, and writes no conflict markers, so there is no half-merged bundle to commit by accident. The full rule lives in .gitattributes next to that line, and every staleness error from build:action repeats it.
Note that the bundle can change even when the source edit appears to have no effect on it: removing a dead helper still changed the output, because it was the second user of a name and the minifier stopped emitting that name once it had one user left. Do not skip the rebuild because the diff looks irrelevant.
📐 A note on Tailwind v4 & eslint-plugin-tailwindcss
NormWind deliberately uses eslint-plugin-tailwindcss's static group data instead of invoking the plugin's enforces-shorthand rule directly. Under Tailwind v4, the plugin's config path can return only separator and prefix, which prevents the rule from resolving many utility families. NormWind keeps the useful upstream group data while using its own Tailwind v4-compatible matcher and Tailwind's own v4 canonicalization engine, so you get the plugin's knowledge without its v4 blind spots.
As of eslint-plugin-tailwindcss 4.x, that group table lives in NormWind's own tree (lib/vendor/tailwind-classname-groups.mjs, MIT-licensed, full notice in THIRD-PARTY-NOTICES.md) rather than being imported from the package at run time: v4 rewrote shorthand classification to query a live Tailwind engine through an internal worker and no longer ships or exports the static table, so this is a one-time snapshot of the last version (3.18.3) that had it. eslint-plugin-tailwindcss itself stays a declared dependency, bumped alongside the rest.
📜 Changelog
v4.0.0: 2026-09-27 · Node 22 floor, gate on changed lines only, per-file baseline, Tailwind collapse merges
-
Breaking: Node 22 is now the minimum (
^22.18.0 || >=24.11.0). Node 20 reached end of life on 2026-04-30 and gets no security patches; an install on Node 20 now warns, and fails outright underengine-strict. Nothing else about how NormWind scans, reports or fixes depends on this, so if you are already on Node 22 or 24 the upgrade is a drop-in. -
Gate on changed lines only:
--diff-base <ref>(CLI) andchanged-lines-only/diff-base(Action) report, annotate, write SARIF for, and fail on only the findings on lines a change adds or edits, relative to the merge base with<ref>. Turning the audit on in an existing codebase no longer floods the first pull request with findings nobody touched; the CLI counts the rest on stderr and the Action's JSON report still lists every finding. With changed-lines gating on, the Action'sfinding-countandexit-codeoutputs describe the gated findings. -
Per-file count baseline (
--baseline,--update-baseline, Actionbaselineinput): adopt NormWind in CI on a codebase that already has findings. A file may keep the findings it had when the baseline was recorded; a new one fails, and a count that drops fails until the baseline is lowered, so the debt only goes down.--update-baselinecreates the file once and afterwards refuses to raise any count. See Adopting on an existing codebase. -
Rewrite-precision gate (development gate): a labelled corpus of must-rewrite and must-not-rewrite class strings now runs through the CLI on every
npm test, and the suite fails when aggregate precision, recall, or false-positive rate crosses a fixed floor (npm run test:precision). Nothing about scanning or--fixchanges. -
Tailwind's own collapse engine adds shorthand merges the group table misses. When the resolved Tailwind supports
canonicalizeCandidates(..., { collapse: true }), each variant/important group of a class list is also collapsed by Tailwind, which only swaps a set for a utility whose compiled CSS matches it. That reaches merges the vendored v3-era group table never listed and follows each Tailwind release. The table's findings always stand: an engine merge is added only when it shares no class with a table finding and its classes are not all in one table family, so nothing the table reports is lost. NormWind's render-safety proof still checks every added merge against the rest of the class list. Answers are cached with the other canonicalization results.NORMWIND_DISABLE_ENGINE_COLLAPSE=1uses the table alone.
v3.8.1: 2026-09-03 · maintenance release, no change to scanning, fixing, or output
Nothing in this release changes what NormWind reports or how --fix behaves. It is repository
housekeeping, published so the packaged README matches the source. If you are on v3.8.0 there is no
functional reason to upgrade.
- Dependency bump:
nanoid3.3.18 (GHSA-2v37-7h3g-55p8), reached througheslint-plugin-tailwindcss→postcss. This is not a fix you were exposed to: the advisory affects a package resolved on a^3.3.xrange, so a fresh install of any recent NormWind already picked up the patched version, and the bundled Marketplace Action does not containnanoidat all. The bump pins it in this repository's own lockfiles. - Lockfile drift is now caught on transitive packages, not just declared ones (development gate). This project keeps both an npm and a Bun lockfile, and the previous check only compared the dependencies named in
package.json, where most advisories never land. Thenanoidbump above is the worked example:npm audit fixmoved one lockfile while the other stayed on the vulnerable version, silently, because the old pin still satisfied its range. The check now compares the full resolved set of both files. - A merge conflict in the committed Action bundle must be resolved by rebuilding it, never by taking either side. Both are stale by definition, since each was generated from a different source tree than the merged one.
.gitattributesnow marksdist/**as unmergeable so Git stops attempting a meaningless three-way merge of two single-line minified files, the build script repeats the rule in its staleness error, and the README documents it. A rebase that resolved this file in favour of the upstream copy is what turned CI red across all seven legs before v3.8.0.
v3.8.0: 2026-08-09 · merge-safety correctness fix, SARIF reporter, ignore files, broader scanning
- Merge-safety gate for shorthand merges (correctness fix): a merge such as
ml-2 mr-2→mx-2is now applied only when Tailwind's own engine confirms the before and after class lists render identical CSS. Tailwind emits utilities in its own order, not authoring order, so a merge sharing a group with another utility at a different value (a pre-existingmx-8alongsideml-2 mr-2) could previously hand that other utility the win and silently change the rendered layout.--fix/--fixallnow skip any merge that isn't provably safe instead of applying it blind; this closes a real path to a silent visual regression from--fix. - New flags:
--reporter <text|json|sarif>(sarifemits SARIF 2.1.0 for GitHub code scanning;--jsonremains an alias for--reporter json),--ignore <glob>(repeatable),--allow-empty(exit0instead of2when a pattern matches nothing), and--to end flag parsing so a target can start with a dash. .normwindignore: a project-local ignore file (one glob per line,#comments, a bare directory name means everything under it) is now read automatically from the scanned directory. It is deliberately not read in GitHub Action mode, since the file is checkout-controlled and a pull request could otherwise silence the audit on the very files it changed;--ignoreflags are always honored in both modes.- Broader file matching:
.svelte,.astro(frontmatter skipped, markup scanned), and plain.html/.htmjoin the default scan alongside Vue/JS/TS. The generated-folder ignore list dropped project-specific paths and now covers.next,.nuxt,.output,.svelte-kit,.turbo, andstorybook-staticat any depth, plus root-only build folders such asbuild,coverage,out, andvendor. - Class-string builders are scanned:
clsx,cx,cn,classnames,classNames,cva,tv,twMerge, andtwJoincalls, including strings nested incva/tvvariant objects, arrays, ternaries, andcond && "...", with locally-aliased imports resolved. --fixcovers every markup format:.svelte,.astro, and.html/.htmjoin.vuein the safe default fixer, not just--fixall.- Composite equivalences are now fixable:
truncate,place-content-*,place-items-*, andplace-self-*findings are applied by--fix/--fixallinstead of being audit-only, so a fix-then-audit CI loop can actually reach exit0. - Stricter exit codes: a pattern matching no lintable files now exits
2instead of0(--allow-emptyrestores the old behavior), and--fix/--fixall/--dry-runcombined with a canonical-maintenance flag, or--dry-runwithout--fix/--fixall, now exits2instead of silently doing nothing.
v3.7.0: 2026-08-01 · GitHub Marketplace Action
- Native GitHub review feedback: the new
NormWind Tailwind AuditAction emits file-and-line annotations, a complete job summary, stable outputs, and a machine-readable JSON report. Findings can fail the job or run in advisory mode; incomplete scans always fail closed. - Self-contained and least privilege: the JavaScript Action bundles NormWind, Tailwind, Babel, and its reporting runtime; it installs nothing on the runner, requires no secret or write permission, strips inherited secrets from its scanner process, never executes checkout-provided dependencies/tools, and confines source/theme reads to the checked-out workspace.
- Reproducible Action releases: deterministic bundle generation, committed third-party license inventories, bundle-drift tests, and a pre-install local-Action smoke step now gate CI and releases. Moving Action tags no longer trigger duplicate npm publications.
- Dependency hardening: the transitive PostCSS version is refreshed past its source-map path-traversal advisory, leaving
npm auditclean.
v3.6.2: 2026-07-24 · project-version-aware Tailwind safety
- Project Tailwind wins: canonicalization now uses the Tailwind v4 installation resolved from the scanned project's working directory, with bundled Tailwind 4.3.3 retained as the zero-config fallback.
- Older v4 projects stay semantically stable: Tailwind 4.1/4.2 projects no longer receive 4.3-only rewrites such as the new system-font stack collapsing to their older
font-sansvalue. Snapshot and disk-cache entries are accepted only when their Tailwind version matches the active engine. - Safe Tailwind 4.0 behavior: shorthand auditing remains available, while arbitrary-value canonicalization becomes a no-op because Tailwind 4.0 does not expose the required canonicalization API.
- Compatibility regression coverage: the pre-push suite now proves both project-local engine selection and the Tailwind 4.0 no-op fallback without requiring network access.
v3.6.1: 2026-07-24 · Tailwind CSS 4.3 support
- Tailwind CSS 4.3.3 support: NormWind now bundles and validates against the current Tailwind 4.3 patch while retaining Node 20 compatibility.
- Canonical snapshot regenerated: the bundled reference now contains 12,438 replacements generated by Tailwind 4.3.3. Its new case-insensitive arbitrary hex-color canonicalization (
bg-[#FFF]→bg-white) is covered by an end-to-end regression. - Dependency automation fixed: routine minor/patch updates remain grouped, Tailwind updates get dedicated compatibility PRs, and the incompatible Babel 8 /
eslint-plugin-tailwindcss4 majors are deferred while Node 20 and the current group-data integration remain supported. - GitHub Actions update:
actions/setup-nodev7 is adopted at a full commit SHA after passing the complete Ubuntu/Windows and Node 20/22 matrix.
v3.6.0: 2026-07-24 · syntax-aware safety, bounded canonicalization, release hardening
- Syntax-aware extraction: JavaScript, TypeScript, JSX, and TSX are parsed before class strings are collected, while Vue SFC template/script boundaries are tracked structurally. Comments, serialized markup, regex literals, interpolations outside class props, and unrelated
{ class: "..." }data objects are no longer audit/fix candidates. Render-prop objects remain supported for recognizedh/createElement/JSX-runtime calls and imported aliases. - Fail-closed autofix: unparseable source, oversized files, read/stat failures, symlinks, concurrent editor saves, and per-file transform/write failures are surfaced with exit
2; unsafe files are left untouched while independent files continue. Atomic rewrites preserve Unix mode bits. - Bounded canonicalization and cache validation: live Tailwind canonicalization is capped, on-disk cache data is type/size validated, stale snapshot entries are compacted away, and cache replacement is atomic. Generated canonical data now encodes candidate spaces correctly and includes an arbitrary-color regression.
- Audit/fix parity: reverse size shorthands, arbitrary variants, equal width/height values, and named theme-variable import ordering now follow the same matching rules in audit and fix paths.
- Target and output correctness: explicit directories and glob patterns are unioned,
lintedFilesreflects successfully scanned files, skipped/failed work is summarized, and--dry-run --jsonkeeps stdout machine-readable. - Release and CI hardening: GitHub Actions are commit-SHA pinned with least-privilege permissions and timeouts, Dependabot covers npm and Actions, the release helper validates strict SemVer, stages/pushes exact release refs atomically, keeps GitHub credentials out of remotes/arguments, rolls back failed bumps, and uses network timeouts.
v3.5.0: 2026-07-09 · fault isolation, classifier parity, --dry-run, corner-family merger
- Fault-isolated
--fix/--fixall: a per-file try/catch means oneEBUSY/EPERM/transform throw no longer aborts the whole batch; a fixed/skipped/failed summary prints, with a distinct exit code when anything failed. - Unified audit/fix classifiers:
isLikelyTailwindUtility/isLikelyFixUtilityandmatchUtilityToBody/matchFixBodyValuenow share matching logic (bracket-variant and bare-body parity), closing gaps where--fixmissed findings the audit reported. --dry-run: pair with--fix/--fixallto see which files would be rewritten without touching disk.- Scanner guards: the fallback walker now skips oversized files and detects symlink loops instead of hanging or erroring.
- Corner and four-sides family merger rewritten: the fixer now uses the same family-table clustering the audit uses, so corner pairs (
rounded-tl+rounded-tr→rounded-t), border side pairs (border-t+border-b→border-y), and complete four-sides sets (rounded-t/r/b/l→rounded,border-t/r/b/l→border) converge instead of persisting as unfixable findings.
v3.4.2: 2026-07-06
- New brand: NormWind logo, wordmarks, and share card (
assets/), plus a redesigned README. - MIT LICENSE file added (the package always declared MIT; now the text ships too).
- Brand assets live in the repo only: the npm tarball stays lean (bin, canonical snapshot, README, LICENSE).
v3.4.1: 2026-07-06
- npm publishing moved to GitHub Actions with provenance. Pushing a
v*tag triggers theReleaseworkflow, which re-runs the full test suite and thennpm publish --provenance: packages now carry a verified build attestation linking them to this repo, commit, and workflow.scripts/release.pystill drives the release locally (tests, bump, tag, GitHub release) but now waits for the Actions publish and verifies the version is live on the registry;--publish-locallyremains as a fallback. Local releases no longer need an npm token. - Line-ending hardening in the shipped binary. The
--check-canonicaldrift check is tolerant of CRLF checkouts (gitautocrlfon fresh Windows clones previously failed it), and.gitattributespins the generated artifacts to LF. The v3.4.0 npm tarball already contained this fix; the tag now does too. - release.py pre-flight hardening: validates GitHub credentials (with
ghCLI token fallback) before any mutation, runs the npm dry-run at the new version, never echoes tokens, and always restores the clean git remote URL.
v3.4.0: 2026-07-06
- Anchored class-string extraction: audit and fix now only inspect
class=,className=,:class=, andv-bind:class=attribute values (quoted or JSX-brace form, including nested quoted strings inside bindings and static template-literal chunks) plus the object-property form{ class: "..." }/{ className: "..." }. The previous behavior (scanning every quoted string in a file) is gone; it could rewrite unrelated code such as SQL strings,title="..."text, ordata-classattributes. Attribute names that merely end in-classare excluded. - One shared extractor: audit and fix now use the same extraction path, so "audit clean" guarantees "
--fixis a no-op." Class strings containing a bare*outside[...]/(...)brackets are conservatively skipped by both. - Atomic writes:
--fix/--fixallnow write through a temp file + rename, so a crash mid-write can no longer truncate a source file. - Order-independent padding merge:
pt-4 pb-4 pl-4 pr-4now fully merges top-4regardless of declaration order (previously order-dependent). - Template literals:
${...}interpolations no longer disqualify the whole literal; static chunks are still audited/fixed, and partial tokens straddling an interpolation boundary (e.g.h-${size}) are never touched. - New flags:
-v/--versionprints the version and exits0. Unknown flags now error with exit2instead of being silently ignored. A--theme-csswith a missing value errors; a--theme-csspath that can't be read now fails loud (exit2) instead of silently disabling the feature. Patterns that match no files print a warning. -hnow works: previously only--helpwas recognized.- Documented exit codes:
0no findings,1findings,2usage/runtime error. - Version banner fixed: the CLI now reads its version from
package.jsonat runtime instead of a hardcoded string (the banner had been stuck onv3.1.1for two releases). - Portable canonical artifacts:
docs/reference/canonical-replacements.*no longer embed an absolute machine path or the tool version, sonpm test/--check-canonicalpasses on any machine. - File discovery fixes: ripgrep returning "no matches" is no longer misread as "ripgrep is missing"; the no-
rgfallback now applies glob patterns properly, and its ignore list matches ripgrep's (.venvand.gitadded). - Trailer text: the text report's trailer no longer references
npm run lint(a project-specific script); it now suggests--fix/--fixall.
v3.3.0: 2026-05-08 · direct-theme-key resolver, w/h order fix, multi-line variant brackets
- Direct-theme-key resolver:
--suggest-named-theme-varsnow collapses utilities that reference a registered@themevariable directly (e.g.border-(--color-ink-400)/40,text-(--color-ink-700),rounded-[var(--radius-sm)]) to the named form (border-ink-400/40,text-ink-700,rounded-sm). Previously the resolver only handled the forwarder pattern; it now also handles the direct pattern where the project authors the theme key itself. The CSS rule-body equivalence check still gates every emitted suggestion, so safety is unchanged. - Modifier-aware: opacity-style modifiers such as
/40and!important markers now flow through the named-theme-var resolver:border-(--color-ink-400)/40round-trips toborder-ink-400/40instead of being silently skipped. - Bracket-form audit coverage: the audit's arbitrary-token regex now matches an optional trailing
/<modifier>suffix, so tokens likeborder-[var(--color-ink-400)]/40reach the canonicalizer. The audit also chains Tailwind's canonicalizer into the named-theme resolver, so a single finding emits the most specific safe rewrite. - Theme CSS auto-engages the resolver during fix:
--fix/--fixallnow apply named-theme-var rewrites whenever--theme-cssis provided; the explicit--suggest-named-theme-varsflag is no longer required at fix time. - Order-agnostic
w/h→sizeshorthand: height-first authoring (h-5 w-5,hover:h-8 hover:w-8) now collapses tosize-5,hover:size-8alongside the width-first form. - Multi-line class strings with
data-[state=…]:variants are no longer skipped: the operator-character heuristic now ignores characters inside Tailwind's[…]and(…)brackets, so attribute-style variants such asdata-[state=open]:text-(--color-ink-1000)andaria-[expanded=true]:rotate-180no longer suppress fixes. - New regression fixtures:
reverse-size-shorthand,multiline-with-data-attr, andnamed-theme-var-direct. The regression harness now auto-supplies--theme-css/--suggest-named-theme-varsfor any fixture that ships a siblingtheme.css, and the prepush gate counts fixtures dynamically.
v3.2.0: 2026-05-08 · Vue :class extraction, multi-line class attrs, packaging hygiene
- Vue dynamic bindings: the class-attribute extractor now matches
:class="…"andv-bind:class="…"in addition to plainclass="…", so utilities embedded in Vue ternaries and object/array bindings are audited and fixed alongside their static siblings. - Multi-line class attributes: the extractor no longer terminates at the first newline, so class lists wrapped across multiple lines (common in templates and JSX) are picked up in full.
- Internal refactor: class-string extraction was split into focused helpers for readability; no behavior change beyond the two items above.
- Packaging hygiene: the
fileswhitelist listsbin/normwind.mjsexplicitly, and the prepush gate fails the publish if any*.bak/*.orig/*.swp/*.tmpstraggler lands in the tarball. - Single source of truth: the parent/
repo-clonewrapper layout collapsed into a single flat repo;bin/normwind.mjsis the only CLI source.
v3.1.1 · named-theme-var flag, theme-css entry inlining
--suggest-named-theme-varsflag (opt-in): detectsutility-(--var-name)classes and, when the project's@themedefines a single-step forwarder, suggests (or rewrites with--fixall) the equivalent named-theme class.--theme-css <path>flag: points NormWind at your Tailwind entry CSS; it recursively inlines local@importdirectives so a re-exportingstyle.cssis fully resolved. Package imports like@import "tailwindcss"are skipped.- Hash-namespaced resolver cache: the on-disk cache key is
themevar:<sha1(resolvedCss)>:<rawToken>, so two projects with disagreeing forwarders never poison each other's cache. - Fail-loud on missing
@theme: a one-line diagnostic instead of silently producing zero suggestions. - Runtime-equivalence gating: suggestions are emitted only when both candidates compile to byte-equivalent CSS rule bodies.
- Single-token paren-form support in
.vuefiles, and zero default change when the flag is omitted.
v3.1.0: 2026-04-29 · bundled canonical snapshot, CI drift check
- Bundled canonical snapshot: ships
docs/reference/canonical-replacements.json(12,069 entries) generated from Tailwind's owndesignSystem.canonicalizeCandidatesengine; no cold boot on first run. --check-canonicalflag: exits1when the bundled snapshot is missing or stale; CI-suitable.canonical:extract/canonical:checkscripts: the maintainer workflow for regenerating and verifying the snapshot.- 7-fixture regression harness, live-vs-snapshot parity test, and a 7-check pre-push suite.
- Tailwind v4 note: documents why the
eslint-plugin-tailwindcssrule is bypassed under v4.
v3.0.0 · initial public release
Initial public release. Shorthand auditor and autofixer for Tailwind CSS utility classes.
🔍 How it compares
NormWind sits next to two tools people already reach for when tidying Tailwind class strings, rather than replacing either:
prettier-plugin-tailwindcss(Tailwind Labs' own Prettier plugin) sorts class names into Tailwind's recommended order. By its own documentation it does not merge shorthand combinations or rewrite arbitrary values. NormWind does the opposite job, and the two compose fine in the same project: let Prettier sort, let NormWind shorten.eslint-plugin-tailwindcss'senforces-shorthandrule is an ESLint rule with its own autofix, and NormWind reuses its utility-group data (vendored, see "A note on Tailwind v4 & eslint-plugin-tailwindcss" above) rather than reimplementing it. But the rule runs inside an ESLint config, and under Tailwind v4 its config path can return onlyseparatorandprefix, which keeps it from resolving many utility families. NormWind runs standalone, as a CLI or GitHub Action, with no ESLint setup required, and also canonicalizes arbitrary values via Tailwind's own engine, which shorthand-focused linting doesn't cover.
❓ FAQ
Is NormWind free? Yes. NormWind is free, open-source software licensed under MIT. Install it with npm, or skip installation entirely and run it with npx. The GitHub Action is also free to use and requires no API token, no signup, and no paid tier; it's published on the GitHub Marketplace and the source is on GitHub.
Does it require any configuration?
No. NormWind is zero-config: there's no rules file to write, no sort order to choose, and no plugins to register. Point it at a project, a specific path, or a glob and it audits or fixes immediately. Optional flags like --ignore, --theme-css, or a .normwindignore file exist, but none are required to get started.
Does it modify my files automatically, or just report issues?
By default, running normwind only audits and reports findings; it never writes to disk unless you pass --fix or --fixall. --fix rewrites markup formats (Vue, Svelte, Astro, HTML) safely; --fixall extends that to JS/TS files too. Both write atomically and preserve file modes, but you should still run them from a clean, version-controlled working tree.
Is my code sent anywhere, or does it need an API key? No. NormWind runs entirely against your local files and your project's installed Tailwind engine; it needs no API token or account. The GitHub Action bundles NormWind, Tailwind, and Babel itself, installs nothing on the runner, and strips inherited secrets from its scanner process, so it has no documented path to send code out during a scan.
What are the system requirements?
NormWind requires Node.js ^22.18.0 or >=24.11.0, per its package.json engines field (Node 22.0-22.17 and 24.0-24.10 fall in the gap). Node 20 was dropped on 2026-09-10, after it reached end-of-life on 2026-04-30 and stopped receiving security patches; the floor narrowed further on 2026-09-14 to match @babel/parser 8's own declared engines range. It works against Tailwind CSS v4 projects: full shorthand and arbitrary-value canonicalization on Tailwind 4.1 through 4.3, and shorthand-only auditing on 4.0, since that release doesn't expose the canonicalization API NormWind depends on for arbitrary values.
Does it support Tailwind CSS v3?
The repo documents support for Tailwind CSS v4 only, spanning 4.0 through the bundled 4.3.3. Arbitrary-value canonicalization relies on Tailwind's designSystem.canonicalizeCandidates engine, a v4 API. No v3 compatibility is documented in the README or changelog, so treat NormWind as a v4-only tool unless a future release states otherwise.
How is it different from prettier-plugin-tailwindcss?
Tailwind's own Prettier plugin sorts class names into a recommended order; by its own documentation it doesn't merge shorthand or touch arbitrary values. NormWind does the opposite job: it collapses verbose combinations like px-4 py-4 into p-4 and rewrites arbitrary values like rounded-[24px] into rounded-3xl. The two tools solve different problems and can be used together.
What happens if a rewrite might change my page's rendered CSS?
NormWind skips it. Before merging classes like ml-2 mr-2 into mx-2, it compiles both the before and after class lists through Tailwind's own engine and compares the resulting CSS declarations. If another utility in the same group (say, an existing mx-8) would change which value wins, the merge is skipped rather than applied blind.
📄 License
Made by LunarWerx Studios. Check out sibling projects RepoYeti, SageThumbs, and QuickDictate.
NormWind: keep your Tailwind classes calm.
Related Packages
ESLint plugin to enforce canonical Tailwind CSS class names using Tailwind CSS v...
ESLint rules that keep utility classes on the element they style, for Tailwind C...
Reusable ESLint flat configs for TypeScript, React, Vitest, Vue, Tailwind CSS, a...
Version History
| Version | Released | License |
|---|---|---|
| 4.0.0 | MIT | |
| 3.8.1 | MIT | |
| 3.8.0 | MIT | |
| 3.7.0 | MIT | |
| 3.6.2 | MIT | |
| 3.6.1 | MIT | |
| 3.6.0 | MIT | |
| 3.5.1 | MIT | |
| 3.5.0 | MIT | |
| 3.4.2 | MIT | |
| 3.4.1 | MIT | |
| 3.4.0 | MIT | |
| 3.3.0 | MIT | |
| 3.2.0 | MIT | |
| 3.1.1 | MIT | |
| 3.1.0 | MIT | |
| 3.0.3 | MIT | |
| 3.0.2 | MIT | |
| 1.0.2 | MIT | |
| 1.0.1 | MIT | |
| 1.0.0 | MIT |