stylelint-plugin-rhythmguard 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/CONTRIBUTING.md +66 -50
- package/README.md +17 -6
- package/SECURITY.md +2 -2
- package/package.json +27 -27
- package/scales/community/product-decimal-10.json +27 -4
- package/src/audit/args.js +6 -1
- package/src/audit/config.js +15 -5
- package/src/audit/contract.js +20 -0
- package/src/audit/render-markdown.js +13 -0
- package/src/audit/render-text.js +9 -1
- package/src/audit/report.js +105 -8
- package/src/audit/scan.js +67 -10
- package/src/cli/index.js +11 -5
- package/src/cli/init.js +7 -4
- package/src/cli/quickstart.js +207 -0
- package/src/configs/embed.js +24 -0
- package/src/configs/{logical.mjs → embed.mjs} +1 -1
- package/src/index.js +1 -4
- package/src/rules/no-offscale-transform/index.js +22 -0
- package/src/rules/prefer-token/index.js +28 -0
- package/src/rules/use-scale/index.js +34 -4
- package/src/utils/length.js +16 -0
- package/src/utils/options.js +53 -3
- package/src/utils/scale-inference.js +351 -0
- package/src/utils/token-map.js +4 -2
- package/src/utils/token-packages.json +48 -0
- package/src/utils/token-sources.js +313 -4
- package/types/__checks__/consumer.ts +10 -6
- package/types/audit.d.ts +21 -1
- package/types/index.d.ts +4 -4
- package/types/shared.d.ts +24 -1
- package/src/configs/expanded.js +0 -27
- package/src/configs/expanded.mjs +0 -4
- package/src/configs/logical.js +0 -16
- package/src/configs/migration.js +0 -31
- package/src/configs/migration.mjs +0 -4
- package/src/configs/react-tailwind.js +0 -25
- package/src/configs/react-tailwind.mjs +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,55 @@ The format follows Keep a Changelog principles and semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [3.0.0] - 2026-09-06
|
|
10
|
+
|
|
11
|
+
Upgrade notes: [`docs/MIGRATING_TO_3.md`](./docs/MIGRATING_TO_3.md).
|
|
12
|
+
|
|
13
|
+
### Breaking
|
|
14
|
+
|
|
15
|
+
- Node 20.19 or newer is required. Node 18 support is removed.
|
|
16
|
+
- `stylelint-config-tailwindcss` and `stylelint-plugin-logical-css` are optional peer dependencies instead of dependencies. Install `stylelint-config-tailwindcss` yourself if you extend `configs/tailwind`. The package now has one runtime dependency, `known-css-properties`.
|
|
17
|
+
- Removed configs `expanded`, `logical`, `migration` and `react-tailwind`. Their rule blocks are reproduced in the migration guide. `plugin.configs` has five keys: `embed`, `motion`, `recommended`, `strict`, `tailwind`.
|
|
18
|
+
- `rhythmguard init` writes the `tailwind` config plus Next.js build ignores for Next.js projects instead of `react-tailwind`.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
|
|
22
|
+
- Campaign media, slide images, outreach drafts and old planning documents are no longer in the repository. The README banner and the cover image referenced by a published article remain.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- The audit now scans `.scss` files through `postcss-scss` (new optional peer dependency). Without it, SCSS files are reported as skipped instead of ignored. `scanned.scssFiles` and `scanned.scssSkipped` join the JSON contract; text and Markdown output show the counts.
|
|
27
|
+
- Quiet benchmark widened from 7 to 20 public repositories, adding SCSS-first systems: Bootstrap, Primer CSS, Penpot, Mastodon, Gutenberg components, GitLab UI, Pico, Bulma, Spectrum CSS, USWDS, Carbon, Salesforce Lightning, wp-calypso components.
|
|
28
|
+
|
|
29
|
+
- `scale: "auto"` now reads spacing tokens shipped by installed design-token packages when the project's own stylesheets define none: `tailwindcss` (v4 `theme.css`), `@radix-ui/themes`, `@mantine/core`, `@primer/primitives`, `@shopify/polaris-tokens`, `@spectrum-css/tokens`. Provenance is `token-package`. Token-source entries accept a per-file `tokenPattern` for packages that name spacing differently. The allowlist is `src/utils/token-packages.json`.
|
|
30
|
+
- Sass variables and maps are token sources for `--scale auto`, for `scaleSources` files, and for `scale: "auto"` inside the rules when the linted `.scss` file declares them (with `postcss-scss`): `$spacer: 1rem`, `$spacing-01: 0.125rem`, and maps such as `$spacers: (1: $spacer * .25, ...)` with nested maps, variable references, `* / + -` arithmetic and `math.div()`. Unevaluable function calls, strings, keywords and interpolated keys are skipped. Bootstrap, Bulma, Carbon and USWDS now infer their own scales in the quiet benchmark.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- New README banner.
|
|
35
|
+
- Contributing: rewritten `CONTRIBUTING.md` with ways to contribute and the local gate, a Contributor Covenant code of conduct, a false-positive issue template, a current security policy, and a GitHub-hosted CI job so pull requests from forks get lint, typecheck and test feedback.
|
|
36
|
+
|
|
37
|
+
## [2.2.0] - 2026-09-05
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- Added `scale: "auto"` to `rhythmguard/use-scale`, `rhythmguard/no-offscale-transform` and `rhythmguard/prefer-token`. The scale is inferred from spacing tokens: `scaleSources` files, then `.rhythmguardrc.json` audit token sources, then the linted stylesheet's custom properties, then `tailwindConfigPath`, with a `rhythmic-4` fallback that is announced in the first report of the file. First matching source wins.
|
|
42
|
+
- Added the `scaleSources` rule option and made `tailwindConfigPath` available to every scale rule.
|
|
43
|
+
- Added `rhythmguard audit --scale auto`, which infers one project-level scale from token sources, then spacing custom properties across the scanned CSS, then the default. The JSON contract now carries `contracts.scale.values`, `source` and `files`; text and Markdown output print the scale and its source.
|
|
44
|
+
- Added the quiet benchmark (`npm run bench:quiet`): audits public design systems with `--scale auto`, classifies findings as drift, noise or allowance, and writes `docs/QUIET_BENCHMARK.md` with a per-repo false-positive rate. Manifest and classification rules live in `benchmarks/quiet/`.
|
|
45
|
+
- Scale inference now reads token values written as `calc(<length> * var(--factor))` (Radix Themes), expands a bare Tailwind v4 `--spacing` base into the default multiplier scale, and matches prefixed spacing tokens such as `--lb-spacing-md` while excluding `letter-spacing` and `word-spacing`.
|
|
46
|
+
|
|
47
|
+
- Added `docs/FOR_AGENTS.md` with a paste-ready `AGENTS.md` block and the commands a coding agent should run, and a "what it is not" paragraph at the top of the README (no color checking, class strings need the ESLint companion, no SCSS in the audit).
|
|
48
|
+
- The quiet benchmark now pins each repository to a recorded commit and keeps snapshots of its findings under `benchmarks/quiet/snapshots/`. `npm run bench:quiet -- --check` fails when the finding set or the inferred scale changes, `--update-snapshots` accepts a reviewed change, `--latest` audits upstream heads. A farm workflow runs the check on every change and the upstream run weekly.
|
|
49
|
+
- `npx rhythmguard` with no command now runs a zero-config quickstart: detects Tailwind (and its major version), Next.js, an existing Stylelint config and token files; infers the spacing scale from the project's own tokens; audits the current directory; and prints the exact `.stylelintrc.json` (plus an ESLint snippet for Tailwind) to paste. `--help` is unchanged; `rhythmguard quickstart` is an explicit alias.
|
|
50
|
+
- Added `stylelint-plugin-rhythmguard/configs/embed`: `use-scale` at warning level with `scale: "auto"`, no `extends`, shape frozen for 2.x. The entry point for shared-config authors who want to enable spacing governance for their consumers without knowing each consumer's scale. Guide in `docs/FOR_CONFIG_AUTHORS.md`.
|
|
51
|
+
- Added `allowHairlines` (default `true`) to `use-scale`, `no-offscale-transform` and `prefer-token`. Non-zero lengths that resolve to one CSS pixel or less (`1px`, `-1px`, `0.5px`, `0.0625rem`) are exempt: they compensate for borders and rendering, not spacing. The quiet benchmark showed them to be the only systematic false positive left across Radix Themes, Mantine, Primer React and Liveblocks. Set `allowHairlines: false` to restore the previous reports.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- `rhythmguard/prefer-token` no longer reports percentages such as `translate(-50%, -50%)` or `inset: 100%` as raw scale values. Percentages are never token candidates. This was the largest noise source in the quiet benchmark.
|
|
56
|
+
- `rhythmguard/use-scale` with `allowPercentages: false` now reports percentage lengths instead of silently skipping them.
|
|
57
|
+
|
|
9
58
|
## [2.1.0] - 2026-09-05
|
|
10
59
|
|
|
11
60
|
### Added
|
package/CONTRIBUTING.md
CHANGED
|
@@ -1,83 +1,99 @@
|
|
|
1
|
-
# Contributing
|
|
1
|
+
# Contributing to Rhythmguard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Thank you for being here. Rhythmguard is a small, opinionated tool with one job: keep spacing on a scale. Most of the useful work on it is small too, and a first contribution can land in an afternoon. This page tells you where the useful work is, how the checks run, and what to expect from review.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Ways to contribute
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
npm run
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
**Report a false positive.** A finding your team considers wrong is the most valuable report we get. It is how percentages and hairlines stopped being findings. Open a [false-positive report](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/new?template=false-positive.yml) with the CSS or class string and your config. No fix required.
|
|
8
|
+
|
|
9
|
+
**Add a repository to the quiet benchmark.** The benchmark audits public design systems and fails CI when the findings change. Adding a repo is one JSON entry in `benchmarks/quiet/repos.json` plus `npm run bench:quiet -- --only <name>` to create its snapshot. Systems with tokens in unusual places are the most useful additions.
|
|
10
|
+
|
|
11
|
+
**Review a drift list.** Each benchmark repo has a list of findings that heuristics call real drift. Labelling them as real or as an allowance in `benchmarks/quiet/labels/<repo>.json` with a one-line reason turns a heuristic number into a reviewed one, and any systematic pattern you find becomes a candidate default.
|
|
12
|
+
|
|
13
|
+
**Teach inference a new token source.** Sass variables and maps, token packages, framework theme files. If your scale lives somewhere the tool cannot see, the fix is a parser in `src/utils/token-sources.js` and a test.
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
**Improve a rule page.** Every rule has a page under `docs/rules/`. A clearer example or a missing edge case is a welcome PR.
|
|
16
|
+
|
|
17
|
+
**Contribute a community scale.** JSON files in `scales/community/`, scaffolded with `npm run scales:add -- --name my-team-scale --base 8 --steps 0,4,8,12,16,24,32` and checked with `npm run scales:validate`. Spec and policy in [`docs/COMMUNITY_SCALES.md`](./docs/COMMUNITY_SCALES.md).
|
|
18
|
+
|
|
19
|
+
Issues labelled [`good first issue`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/labels/good%20first%20issue) are scoped for a first PR. [`help wanted`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/labels/help%20wanted) marks work the maintainer would like a second pair of hands on. If you want to work on something larger, open an issue first so we can agree the shape before you write code.
|
|
20
|
+
|
|
21
|
+
## Getting set up
|
|
14
22
|
|
|
15
23
|
```bash
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
npm
|
|
19
|
-
npm
|
|
24
|
+
git clone https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard.git
|
|
25
|
+
cd stylelint-plugin-rhythmguard
|
|
26
|
+
npm ci
|
|
27
|
+
npm test
|
|
20
28
|
```
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
Node 20.19 or newer. No build step; the source runs as is. Tests use `node --test` and take about five seconds.
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
## The local gate
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
Run these before you push. They are what CI runs, so a green local run means a green PR.
|
|
27
35
|
|
|
28
36
|
```bash
|
|
29
|
-
npm run
|
|
37
|
+
npm run lint # eslint
|
|
38
|
+
npm run typecheck # the published TypeScript declarations against a consumer file and the examples
|
|
39
|
+
npm test # 150+ tests, Stylelint 16
|
|
30
40
|
```
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
Optional, depending on what you touched:
|
|
33
43
|
|
|
34
44
|
```bash
|
|
35
|
-
npm run
|
|
45
|
+
npm run test:compat-floor # Stylelint 16.0.0, the oldest supported
|
|
46
|
+
npm run test:pack-smoke # pack the tarball and install it in a temp project
|
|
47
|
+
npm run scales:validate # community scale JSON
|
|
48
|
+
npm run bench:quiet -- --check # findings on the benchmark repos must match snapshots
|
|
36
49
|
```
|
|
37
50
|
|
|
38
|
-
|
|
51
|
+
If your shell's npm registry is overridden by a corporate `.npmrc`, add `--registry https://registry.npmjs.org` to `npm ci` and prefix `test:pack-smoke` with `npm_config_registry=https://registry.npmjs.org`.
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
### Pull requests from forks
|
|
41
54
|
|
|
42
|
-
-
|
|
43
|
-
- Node `>=18.18.0` for Stylelint 16, `>=20.19.0` for Stylelint 17. The CI matrix excludes Node 18 with Stylelint 17.
|
|
44
|
-
- Dual CommonJS and ESM entry points; every export has a declaration under `types/`. `npm run typecheck` compiles the declarations, a consumer check file, and the examples.
|
|
55
|
+
CI for this repository runs on self-hosted runners for pushes and same-repo branches. For pull requests from forks a separate job runs on GitHub-hosted runners with Node 22 and current Stylelint 16, covering lint, typecheck and the test suite. You will see its result on your PR. The full matrix and the benchmark run after merge.
|
|
45
56
|
|
|
46
|
-
##
|
|
57
|
+
## How we write changes
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
**Tests first.** Every behaviour change starts with a failing test. If you send a fix without one, expect the review to ask for it; the test is what proves the fix is the right one and keeps it that way.
|
|
60
|
+
|
|
61
|
+
**Rules explain their defaults.** A new default, or a change to one, needs a written reason on the rule's page under `docs/rules/` and, where the benchmark can show it, a number. The hairline section in [`docs/rules/use-scale.md`](./docs/rules/use-scale.md#hairlines) is the model.
|
|
62
|
+
|
|
63
|
+
**The benchmark is the regression suite for real code.** If your change alters findings on any benchmark repository, `npm run bench:quiet -- --check` fails and prints the diff. Review it. If the new findings are right, run `npm run bench:quiet -- --update-snapshots`, commit `benchmarks/quiet/snapshots/`, and say in the PR why they are right.
|
|
64
|
+
|
|
65
|
+
**Fixes are deterministic.** Autofix snaps to the nearest scale step or to an explicit token map. It never guesses a token. Keep that property.
|
|
52
66
|
|
|
53
|
-
|
|
67
|
+
**Prose without em dashes.** House style, applied to docs and messages alike.
|
|
54
68
|
|
|
55
|
-
## Semver
|
|
69
|
+
## Semver
|
|
56
70
|
|
|
57
|
-
- Patch
|
|
58
|
-
- Minor
|
|
59
|
-
- Major
|
|
71
|
+
- Patch: bug fixes, docs, internal changes.
|
|
72
|
+
- Minor: new options, presets, sources, or behaviour that does not change existing reports.
|
|
73
|
+
- Major: any change to default reports for existing configs, to autofix behaviour, or to exported entry points.
|
|
60
74
|
|
|
61
|
-
|
|
75
|
+
`configs/recommended`, `configs/strict`, `configs/tailwind` and `configs/embed` stay stable within a major. The `embed` config's shape is frozen for 2.x because shared configs depend on it.
|
|
62
76
|
|
|
63
|
-
|
|
64
|
-
- changing autofix behavior in a non-compatible way
|
|
65
|
-
- changing/removing exported config entry points
|
|
77
|
+
## Compatibility
|
|
66
78
|
|
|
67
|
-
|
|
79
|
+
- Stylelint `^16.0.0 || ^17.0.0`. The 16.0.0 floor has known autofix differences; CI runs the floor suite against it.
|
|
80
|
+
- Node `>=20.19.0`.
|
|
81
|
+
- CommonJS and ESM entry points; every export has a declaration under `types/`.
|
|
82
|
+
- One runtime dependency, `known-css-properties`. `postcss-scss`, `stylelint-config-tailwindcss` and `stylelint-plugin-logical-css` are optional peers and dev dependencies here.
|
|
68
83
|
|
|
69
|
-
|
|
84
|
+
## Review
|
|
70
85
|
|
|
71
|
-
|
|
72
|
-
2. validate deterministic fix behavior
|
|
73
|
-
3. update the rule's page in `docs/rules/` if options or behavior changed
|
|
74
|
-
4. update CHANGELOG
|
|
86
|
+
The maintainer aims to respond to issues and PRs within a week. Small PRs merge faster than large ones; if a change can be split, split it. Reviews are about the change, never the person, and the [code of conduct](./CODE_OF_CONDUCT.md) applies everywhere in this project.
|
|
75
87
|
|
|
76
|
-
##
|
|
88
|
+
## Releases
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
Maintainer-run. Bump `package.json` and `CHANGELOG.md`, create a GitHub release, and `release.yml` verifies on the self-hosted matrix and publishes through npm trusted publishing (OIDC) from a GitHub-hosted job. Provenance is automatic. The checklist is in [`docs/RELEASE_CHECKLIST.md`](./docs/RELEASE_CHECKLIST.md).
|
|
91
|
+
|
|
92
|
+
## Benchmarking performance
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm run bench:perf
|
|
96
|
+
npm run bench:perf:fix
|
|
97
|
+
```
|
|
82
98
|
|
|
83
|
-
|
|
99
|
+
Compares runtime against `stylelint-scales` on a deterministic corpus. Method in [`docs/BENCHMARKING.md`](./docs/BENCHMARKING.md).
|
package/README.md
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-banner.svg?v=
|
|
2
|
+
<img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-banner.svg?v=4" width="100%" alt="Rhythmguard banner showing spacing scale ruler and lint output" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# stylelint-plugin-rhythmguard
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Nobody chose 13px. Rhythmguard catches off-scale spacing in CSS and Tailwind class strings, tells you the nearest steps on your scale, and snaps to them or to your tokens when you ask.
|
|
8
8
|
|
|
9
9
|
[](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/actions/workflows/ci.yml)
|
|
10
|
-
[](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
|
|
11
11
|
[](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
|
|
12
12
|
[](./LICENSE)
|
|
13
13
|
|
|
14
14
|
Rhythmguard is scale-aware rather than a blanket ban: values on your scale pass, values off it are reported with the two nearest steps, and tokens are only ever suggested from a map you control. It works on CSS declarations through Stylelint and on Tailwind class strings through an ESLint companion, and it ships an audit CLI so you can measure drift and ratchet it down before enforcing anything.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
What it is not: it does not check colors or hex values, and the Stylelint rules do not see Tailwind class strings (that is the separate ESLint companion below). Pair it with a color linter if you need one; do not expect one tool to do both. SCSS is audited when `postcss-scss` is installed.
|
|
17
|
+
|
|
18
|
+
## Start here
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx rhythmguard
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
No install, no config. It detects your stack and token files, infers your spacing scale from your own tokens, audits the current directory, and prints the exact `.stylelintrc.json` (and ESLint snippet for Tailwind) to paste. Then:
|
|
17
25
|
|
|
18
26
|
```bash
|
|
19
27
|
npm install --save-dev stylelint stylelint-plugin-rhythmguard
|
|
@@ -62,7 +70,7 @@ Every rule validates its options up front. Unknown option names and wrong shapes
|
|
|
62
70
|
|
|
63
71
|
## Configs
|
|
64
72
|
|
|
65
|
-
`recommended`, `strict`, `tailwind`, `
|
|
73
|
+
`recommended`, `strict`, `tailwind`, `motion` (experimental), and `embed` for authors of shared configs (see [docs/FOR_CONFIG_AUTHORS.md](docs/FOR_CONFIG_AUTHORS.md)). All are `stylelint-plugin-rhythmguard/configs/<name>`. What each enables, the full custom setup, and the scale-selection precedence are in [docs/CONFIGS.md](docs/CONFIGS.md). Built-in and community scale presets are in [docs/SCALE_PRESETS.md](docs/SCALE_PRESETS.md).
|
|
66
74
|
|
|
67
75
|
## Audit before you enforce
|
|
68
76
|
|
|
@@ -83,12 +91,15 @@ The audit scans CSS declarations, Tailwind class strings and your token contract
|
|
|
83
91
|
- [Framework setup](docs/FRAMEWORKS.md) for Vue, Lit, Astro and SvelteKit
|
|
84
92
|
- [Comparison with adjacent plugins](docs/COMPARISON.md) and migration recipes
|
|
85
93
|
- [Real before/after excerpts](docs/ADOPTION_DIFFS.md) from public codebases
|
|
94
|
+
- [For shared-config authors](docs/FOR_CONFIG_AUTHORS.md): the `embed` entry point and how inference works per consumer
|
|
95
|
+
- [For coding agents](docs/FOR_AGENTS.md): a paste-ready `AGENTS.md` block and the commands to run
|
|
96
|
+
- [Quiet benchmark](docs/QUIET_BENCHMARK.md): findings on public design systems, checked on every change
|
|
86
97
|
- [Product direction](docs/STRATEGY_2026-09.md)
|
|
87
98
|
- Browser playground: [petrilahdelma.github.io/stylelint-plugin-rhythmguard](https://petrilahdelma.github.io/stylelint-plugin-rhythmguard/)
|
|
88
99
|
|
|
89
100
|
## Compatibility
|
|
90
101
|
|
|
91
|
-
Stylelint 16 and 17. Node
|
|
102
|
+
Stylelint 16 and 17. Node 20.19 or newer. One runtime dependency (`known-css-properties`); `postcss-scss`, `stylelint-config-tailwindcss` and `stylelint-plugin-logical-css` are optional peers. CommonJS and ESM entry points, TypeScript declarations for every export. The CI matrix runs Node 20 and 22 against Stylelint 16.0.0, 16.x and 17.x. Upgrading from 2.x: [docs/MIGRATING_TO_3.md](docs/MIGRATING_TO_3.md).
|
|
92
103
|
|
|
93
104
|
## Contributing and support
|
|
94
105
|
|
package/SECURITY.md
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stylelint-plugin-rhythmguard",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Nobody chose 13px. Catches off-scale spacing in CSS and Tailwind class strings and snaps it to your scale or tokens. Stylelint rules, an ESLint companion, and an audit CLI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"rhythmguard": "src/cli/index.js"
|
|
7
7
|
},
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"require": "./src/configs/recommended.js",
|
|
33
33
|
"import": "./src/configs/recommended.mjs"
|
|
34
34
|
},
|
|
35
|
+
"./configs/embed": {
|
|
36
|
+
"types": "./types/config.d.ts",
|
|
37
|
+
"require": "./src/configs/embed.js",
|
|
38
|
+
"import": "./src/configs/embed.mjs"
|
|
39
|
+
},
|
|
35
40
|
"./configs/strict": {
|
|
36
41
|
"types": "./types/config.d.ts",
|
|
37
42
|
"require": "./src/configs/strict.js",
|
|
@@ -42,31 +47,11 @@
|
|
|
42
47
|
"require": "./src/configs/tailwind.js",
|
|
43
48
|
"import": "./src/configs/tailwind.mjs"
|
|
44
49
|
},
|
|
45
|
-
"./configs/expanded": {
|
|
46
|
-
"types": "./types/config.d.ts",
|
|
47
|
-
"require": "./src/configs/expanded.js",
|
|
48
|
-
"import": "./src/configs/expanded.mjs"
|
|
49
|
-
},
|
|
50
|
-
"./configs/logical": {
|
|
51
|
-
"types": "./types/config.d.ts",
|
|
52
|
-
"require": "./src/configs/logical.js",
|
|
53
|
-
"import": "./src/configs/logical.mjs"
|
|
54
|
-
},
|
|
55
|
-
"./configs/migration": {
|
|
56
|
-
"types": "./types/config.d.ts",
|
|
57
|
-
"require": "./src/configs/migration.js",
|
|
58
|
-
"import": "./src/configs/migration.mjs"
|
|
59
|
-
},
|
|
60
50
|
"./configs/motion": {
|
|
61
51
|
"types": "./types/config.d.ts",
|
|
62
52
|
"require": "./src/configs/motion.js",
|
|
63
53
|
"import": "./src/configs/motion.mjs"
|
|
64
54
|
},
|
|
65
|
-
"./configs/react-tailwind": {
|
|
66
|
-
"types": "./types/config.d.ts",
|
|
67
|
-
"require": "./src/configs/react-tailwind.js",
|
|
68
|
-
"import": "./src/configs/react-tailwind.mjs"
|
|
69
|
-
},
|
|
70
55
|
"./presets": {
|
|
71
56
|
"types": "./types/presets.d.ts",
|
|
72
57
|
"require": "./src/presets/index.js",
|
|
@@ -118,6 +103,7 @@
|
|
|
118
103
|
"scripts": {
|
|
119
104
|
"bench:perf": "node scripts/bench/compare.mjs",
|
|
120
105
|
"bench:perf:fix": "node scripts/bench/compare.mjs --fix",
|
|
106
|
+
"bench:quiet": "node scripts/bench/quiet.mjs",
|
|
121
107
|
"lint": "eslint .",
|
|
122
108
|
"scales:add": "node scripts/scales/add-scale.mjs",
|
|
123
109
|
"scales:validate": "node scripts/scales/validate-community-scales.mjs",
|
|
@@ -139,27 +125,41 @@
|
|
|
139
125
|
"email": "hello@petrilahdelma.com"
|
|
140
126
|
},
|
|
141
127
|
"peerDependencies": {
|
|
142
|
-
"stylelint": "^16.0.0 || ^17.0.0"
|
|
143
|
-
|
|
144
|
-
"dependencies": {
|
|
145
|
-
"known-css-properties": "^0.37.0",
|
|
128
|
+
"stylelint": "^16.0.0 || ^17.0.0",
|
|
129
|
+
"postcss-scss": "^4.0.0",
|
|
146
130
|
"stylelint-config-tailwindcss": "^1.0.1",
|
|
147
131
|
"stylelint-plugin-logical-css": "^2.0.2"
|
|
148
132
|
},
|
|
133
|
+
"peerDependenciesMeta": {
|
|
134
|
+
"postcss-scss": {
|
|
135
|
+
"optional": true
|
|
136
|
+
},
|
|
137
|
+
"stylelint-config-tailwindcss": {
|
|
138
|
+
"optional": true
|
|
139
|
+
},
|
|
140
|
+
"stylelint-plugin-logical-css": {
|
|
141
|
+
"optional": true
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"dependencies": {
|
|
145
|
+
"known-css-properties": "^0.37.0"
|
|
146
|
+
},
|
|
149
147
|
"devDependencies": {
|
|
150
148
|
"@eslint/js": "^9.22.0",
|
|
151
149
|
"@types/node": "^22.20.1",
|
|
152
150
|
"c8": "^10.1.3",
|
|
153
151
|
"eslint": "^9.22.0",
|
|
154
152
|
"geist": "^1.7.0",
|
|
153
|
+
"postcss-scss": "^4.0.9",
|
|
155
154
|
"postcss-value-parser": "^4.2.0",
|
|
156
155
|
"stylelint": "^16.15.0",
|
|
156
|
+
"stylelint-config-tailwindcss": "^1.0.1",
|
|
157
157
|
"stylelint-plugin-logical-css": "^2.0.2",
|
|
158
158
|
"stylelint-scales": "^5.0.0",
|
|
159
159
|
"typescript": "^5.9.3"
|
|
160
160
|
},
|
|
161
161
|
"engines": {
|
|
162
|
-
"node": ">=
|
|
162
|
+
"node": ">=20.19.0"
|
|
163
163
|
},
|
|
164
164
|
"publishConfig": {
|
|
165
165
|
"access": "public"
|
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "product-decimal-10",
|
|
3
|
-
"description": "Decimal-friendly product spacing scale for dashboard-heavy
|
|
3
|
+
"description": "Decimal-friendly product spacing scale for dashboard-heavy UIs.",
|
|
4
4
|
"base": 10,
|
|
5
|
-
"steps": [
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
"steps": [
|
|
6
|
+
0,
|
|
7
|
+
2,
|
|
8
|
+
4,
|
|
9
|
+
6,
|
|
10
|
+
8,
|
|
11
|
+
10,
|
|
12
|
+
12,
|
|
13
|
+
16,
|
|
14
|
+
20,
|
|
15
|
+
24,
|
|
16
|
+
32,
|
|
17
|
+
40,
|
|
18
|
+
48,
|
|
19
|
+
64,
|
|
20
|
+
80
|
|
21
|
+
],
|
|
22
|
+
"aliases": [
|
|
23
|
+
"decimal-10",
|
|
24
|
+
"enterprise-10"
|
|
25
|
+
],
|
|
26
|
+
"tags": [
|
|
27
|
+
"product",
|
|
28
|
+
"dashboard",
|
|
29
|
+
"community"
|
|
30
|
+
],
|
|
8
31
|
"contributor": "Petri Lahdelma",
|
|
9
32
|
"contributorUrl": "https://github.com/PetriLahdelma"
|
|
10
33
|
}
|
package/src/audit/args.js
CHANGED
|
@@ -36,7 +36,8 @@ Options:
|
|
|
36
36
|
--token-source-format <format> Token source format: auto, css, flat-json, style-dictionary, dtcg (default: auto)
|
|
37
37
|
--token-kind <kind> Token kind: spacing, radius, typography, size, motion, all (default: spacing)
|
|
38
38
|
--token-candidate-min-count <n> Minimum repeated raw value count for token candidates (default: 2)
|
|
39
|
-
--scale <values>
|
|
39
|
+
--scale <values|auto> Comma-separated scale values (default: 0,4,8,12,16,24,32);
|
|
40
|
+
auto infers the scale from token sources, then scanned CSS
|
|
40
41
|
--base-font-size <number> px base for rem/em conversion (default: 16)
|
|
41
42
|
`;
|
|
42
43
|
|
|
@@ -406,6 +407,10 @@ function parseScale(raw) {
|
|
|
406
407
|
throw new Error('Missing value for --scale.');
|
|
407
408
|
}
|
|
408
409
|
|
|
410
|
+
if (raw.trim().toLowerCase() === 'auto') {
|
|
411
|
+
return 'auto';
|
|
412
|
+
}
|
|
413
|
+
|
|
409
414
|
const scale = raw.split(',')
|
|
410
415
|
.map((part) => part.trim())
|
|
411
416
|
.filter(Boolean)
|
package/src/audit/config.js
CHANGED
|
@@ -134,11 +134,21 @@ function parseBooleanOption(value, optionName) {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
function normalizeCliTokenSources(sources, format) {
|
|
137
|
-
return sources.map((
|
|
138
|
-
baseDir
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
return sources.map((source) => {
|
|
138
|
+
// The programmatic API may pass typed { path | file, format?, baseDir? } entries.
|
|
139
|
+
if (source && typeof source === 'object') {
|
|
140
|
+
return {
|
|
141
|
+
baseDir: source.baseDir || process.cwd(),
|
|
142
|
+
format: source.format || format,
|
|
143
|
+
path: source.path || source.file,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
baseDir: process.cwd(),
|
|
148
|
+
format,
|
|
149
|
+
path: source,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
function normalizeConfigTokenSources(sources, baseDir) {
|
package/src/audit/contract.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const {
|
|
5
5
|
addDefinition,
|
|
6
|
+
collectScssTokens,
|
|
6
7
|
createTokenKindMatcher,
|
|
7
8
|
getNormalizedValueKeys,
|
|
8
9
|
} = require('../utils/token-sources');
|
|
@@ -112,6 +113,16 @@ function collectTokenDefinitions(source, file, definitions, matchesKind, baseFon
|
|
|
112
113
|
value: match[2].trim(),
|
|
113
114
|
});
|
|
114
115
|
}
|
|
116
|
+
|
|
117
|
+
for (const sassToken of collectScssTokens(source, matchesKind)) {
|
|
118
|
+
addDefinition(definitions, {
|
|
119
|
+
baseFontSize,
|
|
120
|
+
file,
|
|
121
|
+
source: file,
|
|
122
|
+
token: sassToken.token,
|
|
123
|
+
value: sassToken.value,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
function collectTokenUses(source, file, uses, matchesKind) {
|
|
@@ -243,7 +254,10 @@ function buildReport({
|
|
|
243
254
|
externalTokenDefinitions,
|
|
244
255
|
includeMotion,
|
|
245
256
|
motionFindings,
|
|
257
|
+
scale,
|
|
246
258
|
scanScope,
|
|
259
|
+
scssFiles = 0,
|
|
260
|
+
scssSkipped = 0,
|
|
247
261
|
templateFiles,
|
|
248
262
|
tailwindFindings,
|
|
249
263
|
tokenCandidateMinCount,
|
|
@@ -306,10 +320,13 @@ function buildReport({
|
|
|
306
320
|
values: Object.fromEntries(sortCountMap(motionValues).slice(0, 10)),
|
|
307
321
|
},
|
|
308
322
|
offScaleValues: Object.fromEntries(sortCountMap(offScaleValues).slice(0, 10)),
|
|
323
|
+
scale: scale || null,
|
|
309
324
|
scaleCleanliness,
|
|
310
325
|
scanScope,
|
|
311
326
|
scanned: {
|
|
312
327
|
cssFiles: cssFiles.length,
|
|
328
|
+
scssFiles,
|
|
329
|
+
scssSkipped,
|
|
313
330
|
templateFiles: templateFiles.length,
|
|
314
331
|
totalFiles,
|
|
315
332
|
},
|
|
@@ -370,8 +387,11 @@ function toAuditContractReport(report) {
|
|
|
370
387
|
motion: report.motion,
|
|
371
388
|
scale: {
|
|
372
389
|
cleanliness: report.scaleCleanliness,
|
|
390
|
+
files: report.scale ? report.scale.files : [],
|
|
373
391
|
offScaleValues: report.offScaleValues,
|
|
392
|
+
source: report.scale ? report.scale.source : 'default',
|
|
374
393
|
tokenOpportunities: report.tokenOpportunities,
|
|
394
|
+
values: report.scale ? report.scale.values : null,
|
|
375
395
|
},
|
|
376
396
|
tokens: report.tokenContract,
|
|
377
397
|
},
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
const { sortCountMap } = require('./contract');
|
|
4
4
|
const { escapeMarkdown } = require('./render-utils');
|
|
5
5
|
|
|
6
|
+
function describeScaleSource(scale) {
|
|
7
|
+
return scale.files.length > 0
|
|
8
|
+
? `${scale.source} (${scale.files.join(', ')})`
|
|
9
|
+
: scale.source;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
function renderMarkdown(report) {
|
|
7
13
|
const lines = [
|
|
8
14
|
'# Rhythmguard Design-System Audit',
|
|
@@ -15,11 +21,18 @@ function renderMarkdown(report) {
|
|
|
15
21
|
'| --- | ---: |',
|
|
16
22
|
`| CSS files scanned | ${report.cssFilesScanned} |`,
|
|
17
23
|
`| Template files scanned | ${report.templateFilesScanned} |`,
|
|
24
|
+
...(report.scanned && report.scanned.scssFiles > 0 ? [`| SCSS files scanned | ${report.scanned.scssFiles - report.scanned.scssSkipped} |`] : []),
|
|
25
|
+
...(report.scanned && report.scanned.scssSkipped > 0 ? [`| SCSS files skipped (install postcss-scss) | ${report.scanned.scssSkipped} |`] : []),
|
|
18
26
|
`| Files with issues | ${report.filesWithIssues} |`,
|
|
19
27
|
`| Total findings | ${report.totalWarnings} |`,
|
|
20
28
|
`| Scale cleanliness | ${report.scaleCleanliness}% |`,
|
|
21
29
|
];
|
|
22
30
|
|
|
31
|
+
if (report.scale) {
|
|
32
|
+
lines.push(`| Scale | ${report.scale.values.join(', ')} |`);
|
|
33
|
+
lines.push(`| Scale source | ${describeScaleSource(report.scale)} |`);
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
if (report.baseline) {
|
|
24
37
|
lines.push(`| New findings | ${report.baseline.newFindingsCount} |`);
|
|
25
38
|
lines.push(`| Resolved findings | ${report.baseline.resolvedFindingsCount} |`);
|
package/src/audit/render-text.js
CHANGED
|
@@ -19,11 +19,19 @@ function renderText(report) {
|
|
|
19
19
|
'',
|
|
20
20
|
` CSS files scanned ${String(report.cssFilesScanned).padStart(4)}`,
|
|
21
21
|
` Template files scanned ${String(report.templateFilesScanned).padStart(4)}`,
|
|
22
|
+
...(report.scanned && report.scanned.scssFiles > 0 ? [` SCSS files scanned ${String(report.scanned.scssFiles - report.scanned.scssSkipped).padStart(4)}`] : []),
|
|
23
|
+
...(report.scanned && report.scanned.scssSkipped > 0 ? [` SCSS files skipped ${String(report.scanned.scssSkipped).padStart(4)} (install postcss-scss to audit them)`] : []),
|
|
22
24
|
` Files with issues ${String(report.filesWithIssues).padStart(4)}`,
|
|
23
25
|
` Scale cleanliness ${scoreBar(report.scaleCleanliness)} ${report.scaleCleanliness}%`,
|
|
24
|
-
'',
|
|
25
26
|
];
|
|
26
27
|
|
|
28
|
+
if (report.scale) {
|
|
29
|
+
lines.push(` Scale ${report.scale.values.join(', ')}`);
|
|
30
|
+
const files = report.scale.files.length > 0 ? ` (${report.scale.files.join(', ')})` : '';
|
|
31
|
+
lines.push(` Scale source ${report.scale.source}${files}`);
|
|
32
|
+
}
|
|
33
|
+
lines.push('');
|
|
34
|
+
|
|
27
35
|
appendHistogram(lines, 'CSS OFF-SCALE VALUES', report.offScaleValues);
|
|
28
36
|
appendHistogram(lines, 'CSS TOKEN OPPORTUNITIES', report.tokenOpportunities);
|
|
29
37
|
appendHistogram(lines, 'TAILWIND CLASS-STRING DRIFT', report.tailwindArbitraryValues);
|