Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ All notable changes to CSS Dedup are documented in this file, which is (mostly)

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.12.1] - 2026-08-13

### Changed

* Renamed the positional argument to `path` in the help output and the README, matching that it takes files as well as directories

## [1.12.0] - 2026-08-12

### Changed

* Switched to analyzing the current directory when no file is given, instead of printing help and exiting “1”
* Switched to analyzing the current directory when no file is given, instead of printing help and exiting `1`
* Changed to ask for confirmation before `--fix` rewrites a working directory nobody named; an explicit target (`css-dedup --fix .`) runs unprompted

## [1.11.0] - 2026-08-05
Expand Down Expand Up @@ -109,8 +115,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Added

* Added `--exit-zero`/`-z` (and the `exitZero` config option) to exit “0” even when findings are skipped as unsafe to auto-merge or withheld by `--savings-only`, so a build pipeline doesn’t gate on them
- A file that fails to read or parse still exits “1” regardless of the flag.
* Added `--exit-zero`/`-z` (and the `exitZero` config option) to exit `0` even when findings are skipped as unsafe to auto-merge or withheld by `--savings-only`, so a build pipeline doesn’t gate on them
- A file that fails to read or parse still exits `1` regardless of the flag.
* Added `--no-exit-zero`/`-e` to override `exitZero: true` from a config file for a run

## [1.5.0] - 2026-07-22
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ The two aren’t always aligned, though: Folding a declaration into a shared sel
### CLI

```shell
npx css-dedup [options] [file…]
npx css-dedup [options] [path…]
```

Pass one or more files—each is analyzed (and, with `--fix`, rewritten) independently. Without a file, CSS Dedup analyzes the current directory. A directory is searched recursively for .css files (skipping node_modules and dotfolders); the result is unrolled into that same per-file list, so mixing files and directories works, too. Pass `-` instead of a file to read CSS from STDIN (can’t be combined with other file arguments); in `--fix` mode this prints the consolidated CSS to STDOUT, rather than writing a file, so it composes in a pipeline (status/summary output moves to STDERR in that case, keeping STDOUT pure CSS).
Pass one or more paths—each is analyzed (and, with `--fix`, rewritten) independently. Without a path, CSS Dedup analyzes the current directory. A directory is searched recursively for .css files (skipping node_modules and dotfolders); the result is unrolled into that same per-file list, so mixing files and directories works, too. Pass `-` instead of a file to read CSS from STDIN (can’t be combined with other file arguments); in `--fix` mode this prints the consolidated CSS to STDOUT, rather than writing a file, so it composes in a pipeline (status/summary output moves to STDERR in that case, keeping STDOUT pure CSS).

The input is CSS. A preprocessor source named as an argument (.scss, .sass, .less, .styl) is skipped. Run CSS Dedup on the compiled style sheet instead—duplication in a preprocessor source is often deliberate (one mixin used in ten places), it only becomes real duplication after compilation, and the byte figures the report is built around describe what actually ships. The reason for skipping rather than trying: Constructs like `@include`, `@extend`, `#{…}`, and `@if` decide what a rule finally contains, which is exactly what the merge-safety checks would need to see to know whether moving a declaration across rules is safe.

Expand All @@ -88,9 +88,9 @@ The input is CSS. A preprocessor source named as an argument (.scss, .sass, .les

`--ignore-selector` and `--ignore-path` are singular because they’re repeatable flags—each occurrence (`-i pattern1 -i pattern2`) adds one pattern. The corresponding config-file options, `ignoreSelectors` and `ignorePaths`, take an array. `--ignore-path` excludes whole files by path rather than by selector content, matched against each file’s path relative to the working directory—useful for keeping a directory scan out of a build output folder (`node_modules` and dotfolders are always skipped; nothing else is otherwise).

Without `--fix`, CSS Dedup only reports. Report mode still runs the same safety checks `--fix` would, though, so a finding that is considered unsafe to auto-merge (an intervening declaration on some other selector, say) is called out right there, alongside the byte estimate for whatever is safe—rather than the estimate silently going missing for that group. Exit code is “1” if default-mode rules find anything to report (or, with `--fix`, anything skipped as unsafe or withheld by `--savings-only`) in any of the given files—an aggressive-only finding that default rules wouldn’t otherwise report doesn’t, on its own, flip the exit code.
Without `--fix`, CSS Dedup only reports. Report mode still runs the same safety checks `--fix` would, though, so a finding that is considered unsafe to auto-merge (an intervening declaration on some other selector, say) is called out right there, alongside the byte estimate for whatever is safe—rather than the estimate silently going missing for that group. Exit code is `1` if default-mode rules find anything to report (or, with `--fix`, anything skipped as unsafe or withheld by `--savings-only`) in any of the given files—an aggressive-only finding that default rules wouldn’t otherwise report doesn’t, on its own, flip the exit code.

CSS Dedup only ever merges what it can prove safe: A duplicate group flagged unsafe to auto-merge is never merged, `--exit-zero` or not, and the finding still prints exactly the same either way—nothing about the flag changes what gets analyzed or reported, only what happens to the exit code. That’s for a build step that must otherwise succeed (e.g., a minification pipeline gating CI): `--fix` should not produce anything wrong (that would be a bug), so failing the build over a finding it just left on record for later is a separate call from whether the CSS itself is fine. Exit “1” still applies to a file that couldn’t be read or parsed.
CSS Dedup only ever merges what it can prove safe: A duplicate group flagged unsafe to auto-merge is never merged, `--exit-zero` or not, and the finding still prints exactly the same either way—nothing about the flag changes what gets analyzed or reported, only what happens to the exit code. That’s for a build step that must otherwise succeed (e.g., a minification pipeline gating CI): `--fix` should not produce anything wrong (that would be a bug), so failing the build over a finding it just left on record for later is a separate call from whether the CSS itself is fine. Exit `1` still applies to a file that couldn’t be read or parsed.

A file that fails to parse—invalid CSS, or a non-standard dialect PostCSS doesn’t accept—doesn’t stop the run: Its error is reported and CSS Dedup moves on to the rest.

Expand Down
2 changes: 0 additions & 2 deletions bin/css-dedup.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,6 @@ async function main() {
}

main().catch(err => {
// A setup failure is the user’s to fix, so it gets the same message
// every other resolution error gets
if (err.setupFailed) fail(styleText('red', err.message));
console.error(err);
process.exit(1);
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"devDependencies": {
"@eslint/js": "^10.0.1",
"eslint": "^10.8.1",
"globals": "^17.10.0",
"globals": "^17.11.0",
"typescript": "^7.0.2"
},
"engines": {
Expand Down Expand Up @@ -60,5 +60,5 @@
},
"type": "module",
"types": "src/index.d.ts",
"version": "1.12.0"
"version": "1.12.1"
}
8 changes: 3 additions & 5 deletions src/cli/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ const OPTIONS_CONFIG = {
help: { type: 'boolean', short: 'h', default: false },
};

const HELP = `Usage: css-dedup [options] [file…]
const HELP = `Usage: css-dedup [options] [path…]

Find (and optionally consolidate) duplicate CSS declarations.

Arguments:
file One or more CSS files or directories to analyze, defaulting to the current directory (directories are searched recursively for .css files, skipping node_modules and dotfolders); pass \`-\` to read from STDIN instead. Preprocessor sources (.scss, .sass, .less, .styl) are skipped—run CSS Dedup on the compiled style sheet.
path One or more CSS files or directories to analyze, defaulting to the current directory (directories are searched recursively for .css files, skipping node_modules and dotfolders); pass \`-\` to read from STDIN instead. Preprocessor sources (.scss, .sass, .less, .styl) are skipped—run CSS Dedup on the compiled style sheet.

Options:
-f, --fix Consolidate declarations that are safe to merge automatically, rewriting each file in place (or printing to STDOUT for \`-\`)
Expand Down Expand Up @@ -93,9 +93,7 @@ export function parseCliArgs(argv, { fail, showHelp }) {
return { values, positionals: implicitTarget ? ['.'] : positionals, implicitTarget };
}

// Marks a failure as the user’s to fix (a bad pattern, an unloadable config)
// rather than a bug, so `bin/css-dedup.js` prints the message instead of a
// stack trace
// Marks a failure as the user’s to fix rather than a bug
function setupError(message) {
const err = new Error(message);
err.setupFailed = true;
Expand Down