stylelint-plugin-rhythmguard 2.2.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 +28 -0
- package/CONTRIBUTING.md +66 -51
- package/README.md +6 -6
- package/SECURITY.md +2 -2
- package/package.json +21 -27
- package/scales/community/product-decimal-10.json +27 -4
- package/src/audit/contract.js +15 -0
- package/src/audit/render-markdown.js +2 -0
- package/src/audit/render-text.js +2 -0
- package/src/audit/report.js +24 -1
- package/src/audit/scan.js +67 -10
- package/src/cli/init.js +2 -3
- package/src/cli/quickstart.js +7 -3
- package/src/index.js +0 -4
- package/src/utils/scale-inference.js +108 -12
- package/src/utils/token-packages.json +48 -0
- package/src/utils/token-sources.js +282 -3
- package/types/__checks__/consumer.ts +0 -6
- package/types/audit.d.ts +6 -1
- package/types/index.d.ts +0 -4
- package/types/shared.d.ts +2 -0
- package/src/configs/expanded.js +0 -27
- package/src/configs/expanded.mjs +0 -4
- package/src/configs/logical.js +0 -16
- package/src/configs/logical.mjs +0 -4
- 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,34 @@ 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
|
+
|
|
9
37
|
## [2.2.0] - 2026-09-05
|
|
10
38
|
|
|
11
39
|
### Added
|
package/CONTRIBUTING.md
CHANGED
|
@@ -1,84 +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
|
|
75
|
-
5. run the quiet benchmark: `npm run bench:quiet -- --check`. It audits real public design systems at pinned commits and fails when the finding set or the inferred scale changes. If the change is intended, review the printed diff, run `npm run bench:quiet -- --update-snapshots`, and commit `benchmarks/quiet/snapshots/` with a sentence in the PR on why the new findings are right. CI runs the same check.
|
|
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.
|
|
76
87
|
|
|
77
|
-
##
|
|
88
|
+
## Releases
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
```
|
|
83
98
|
|
|
84
|
-
|
|
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,19 @@
|
|
|
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
|
-
What it is not: it does not check colors or hex values, the Stylelint rules do not see Tailwind class strings (that is the separate ESLint companion below)
|
|
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
17
|
|
|
18
18
|
## Start here
|
|
19
19
|
|
|
@@ -70,7 +70,7 @@ Every rule validates its options up front. Unknown option names and wrong shapes
|
|
|
70
70
|
|
|
71
71
|
## Configs
|
|
72
72
|
|
|
73
|
-
`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).
|
|
74
74
|
|
|
75
75
|
## Audit before you enforce
|
|
76
76
|
|
|
@@ -99,7 +99,7 @@ The audit scans CSS declarations, Tailwind class strings and your token contract
|
|
|
99
99
|
|
|
100
100
|
## Compatibility
|
|
101
101
|
|
|
102
|
-
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).
|
|
103
103
|
|
|
104
104
|
## Contributing and support
|
|
105
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
|
},
|
|
@@ -47,31 +47,11 @@
|
|
|
47
47
|
"require": "./src/configs/tailwind.js",
|
|
48
48
|
"import": "./src/configs/tailwind.mjs"
|
|
49
49
|
},
|
|
50
|
-
"./configs/expanded": {
|
|
51
|
-
"types": "./types/config.d.ts",
|
|
52
|
-
"require": "./src/configs/expanded.js",
|
|
53
|
-
"import": "./src/configs/expanded.mjs"
|
|
54
|
-
},
|
|
55
|
-
"./configs/logical": {
|
|
56
|
-
"types": "./types/config.d.ts",
|
|
57
|
-
"require": "./src/configs/logical.js",
|
|
58
|
-
"import": "./src/configs/logical.mjs"
|
|
59
|
-
},
|
|
60
|
-
"./configs/migration": {
|
|
61
|
-
"types": "./types/config.d.ts",
|
|
62
|
-
"require": "./src/configs/migration.js",
|
|
63
|
-
"import": "./src/configs/migration.mjs"
|
|
64
|
-
},
|
|
65
50
|
"./configs/motion": {
|
|
66
51
|
"types": "./types/config.d.ts",
|
|
67
52
|
"require": "./src/configs/motion.js",
|
|
68
53
|
"import": "./src/configs/motion.mjs"
|
|
69
54
|
},
|
|
70
|
-
"./configs/react-tailwind": {
|
|
71
|
-
"types": "./types/config.d.ts",
|
|
72
|
-
"require": "./src/configs/react-tailwind.js",
|
|
73
|
-
"import": "./src/configs/react-tailwind.mjs"
|
|
74
|
-
},
|
|
75
55
|
"./presets": {
|
|
76
56
|
"types": "./types/presets.d.ts",
|
|
77
57
|
"require": "./src/presets/index.js",
|
|
@@ -145,27 +125,41 @@
|
|
|
145
125
|
"email": "hello@petrilahdelma.com"
|
|
146
126
|
},
|
|
147
127
|
"peerDependencies": {
|
|
148
|
-
"stylelint": "^16.0.0 || ^17.0.0"
|
|
149
|
-
|
|
150
|
-
"dependencies": {
|
|
151
|
-
"known-css-properties": "^0.37.0",
|
|
128
|
+
"stylelint": "^16.0.0 || ^17.0.0",
|
|
129
|
+
"postcss-scss": "^4.0.0",
|
|
152
130
|
"stylelint-config-tailwindcss": "^1.0.1",
|
|
153
131
|
"stylelint-plugin-logical-css": "^2.0.2"
|
|
154
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
|
+
},
|
|
155
147
|
"devDependencies": {
|
|
156
148
|
"@eslint/js": "^9.22.0",
|
|
157
149
|
"@types/node": "^22.20.1",
|
|
158
150
|
"c8": "^10.1.3",
|
|
159
151
|
"eslint": "^9.22.0",
|
|
160
152
|
"geist": "^1.7.0",
|
|
153
|
+
"postcss-scss": "^4.0.9",
|
|
161
154
|
"postcss-value-parser": "^4.2.0",
|
|
162
155
|
"stylelint": "^16.15.0",
|
|
156
|
+
"stylelint-config-tailwindcss": "^1.0.1",
|
|
163
157
|
"stylelint-plugin-logical-css": "^2.0.2",
|
|
164
158
|
"stylelint-scales": "^5.0.0",
|
|
165
159
|
"typescript": "^5.9.3"
|
|
166
160
|
},
|
|
167
161
|
"engines": {
|
|
168
|
-
"node": ">=
|
|
162
|
+
"node": ">=20.19.0"
|
|
169
163
|
},
|
|
170
164
|
"publishConfig": {
|
|
171
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/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) {
|
|
@@ -245,6 +256,8 @@ function buildReport({
|
|
|
245
256
|
motionFindings,
|
|
246
257
|
scale,
|
|
247
258
|
scanScope,
|
|
259
|
+
scssFiles = 0,
|
|
260
|
+
scssSkipped = 0,
|
|
248
261
|
templateFiles,
|
|
249
262
|
tailwindFindings,
|
|
250
263
|
tokenCandidateMinCount,
|
|
@@ -312,6 +325,8 @@ function buildReport({
|
|
|
312
325
|
scanScope,
|
|
313
326
|
scanned: {
|
|
314
327
|
cssFiles: cssFiles.length,
|
|
328
|
+
scssFiles,
|
|
329
|
+
scssSkipped,
|
|
315
330
|
templateFiles: templateFiles.length,
|
|
316
331
|
totalFiles,
|
|
317
332
|
},
|
|
@@ -21,6 +21,8 @@ function renderMarkdown(report) {
|
|
|
21
21
|
'| --- | ---: |',
|
|
22
22
|
`| CSS files scanned | ${report.cssFilesScanned} |`,
|
|
23
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} |`] : []),
|
|
24
26
|
`| Files with issues | ${report.filesWithIssues} |`,
|
|
25
27
|
`| Total findings | ${report.totalWarnings} |`,
|
|
26
28
|
`| Scale cleanliness | ${report.scaleCleanliness}% |`,
|
package/src/audit/render-text.js
CHANGED
|
@@ -19,6 +19,8 @@ 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
26
|
];
|
package/src/audit/report.js
CHANGED
|
@@ -18,7 +18,7 @@ const {
|
|
|
18
18
|
} = require('./config');
|
|
19
19
|
const { buildReport, collectTokenDefinitions } = require('./contract');
|
|
20
20
|
const { DEFAULT_SCALE, formatPath } = require('./shared');
|
|
21
|
-
const { scaleFromDefinitions } = require('../utils/scale-inference');
|
|
21
|
+
const { discoverTokenPackages, scaleFromDefinitions } = require('../utils/scale-inference');
|
|
22
22
|
const {
|
|
23
23
|
assertDirectory,
|
|
24
24
|
collectCssFindings,
|
|
@@ -79,6 +79,8 @@ async function createAuditReport(options) {
|
|
|
79
79
|
motionFindings,
|
|
80
80
|
scale,
|
|
81
81
|
scanScope,
|
|
82
|
+
scssFiles: cssResults.scssFiles || 0,
|
|
83
|
+
scssSkipped: cssResults.scssSkipped || 0,
|
|
82
84
|
tailwindFindings: collectTailwindFindings(templateFiles, lintOptions),
|
|
83
85
|
templateFiles,
|
|
84
86
|
tokenCandidateMinCount: parsed.tokenCandidateMinCount,
|
|
@@ -98,6 +100,8 @@ async function createAuditReport(options) {
|
|
|
98
100
|
return report;
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
const NON_AUTHORED_SEGMENT = /(^|\/)(test|tests|__tests__|spec|specs|fixtures?|__fixtures__|__snapshots__|vendor|third[-_]?party|storybook-static)(\/|$)/i;
|
|
104
|
+
|
|
101
105
|
/**
|
|
102
106
|
* One project-level scale for the whole audit, with provenance.
|
|
103
107
|
* "auto": external token sources, then spacing custom properties across the scanned
|
|
@@ -128,6 +132,11 @@ function resolveAuditScale({ baseFontSize, cssFiles, requested, tokenSourceResul
|
|
|
128
132
|
const definitions = new Map();
|
|
129
133
|
const matchesKind = createTokenKindMatcher('spacing');
|
|
130
134
|
for (const filePath of cssFiles) {
|
|
135
|
+
// Test, fixture and vendored stylesheets often redefine tokens to exercise
|
|
136
|
+
// overrides; they are findings noise and must not shape the inferred scale.
|
|
137
|
+
if (NON_AUTHORED_SEGMENT.test(formatPath(filePath))) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
131
140
|
let text;
|
|
132
141
|
try {
|
|
133
142
|
text = fs.readFileSync(filePath, 'utf8');
|
|
@@ -155,6 +164,20 @@ function resolveAuditScale({ baseFontSize, cssFiles, requested, tokenSourceResul
|
|
|
155
164
|
}
|
|
156
165
|
}
|
|
157
166
|
|
|
167
|
+
const packageSources = discoverTokenPackages(process.cwd());
|
|
168
|
+
if (packageSources.length > 0) {
|
|
169
|
+
const parsedPackages = parseTokenSources({ baseFontSize, sources: packageSources, tokenKind: 'spacing' });
|
|
170
|
+
const values = scaleFromDefinitions(parsedPackages.definitions, baseFontSize);
|
|
171
|
+
if (values) {
|
|
172
|
+
return {
|
|
173
|
+
files: parsedPackages.sources.map((source) => source.file),
|
|
174
|
+
source: 'token-package',
|
|
175
|
+
tokenCount: parsedPackages.definitions.size,
|
|
176
|
+
values,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
158
181
|
return { files: [], source: 'fallback', tokenCount: 0, values: DEFAULT_SCALE };
|
|
159
182
|
}
|
|
160
183
|
|
package/src/audit/scan.js
CHANGED
|
@@ -230,8 +230,33 @@ function toPosixRelativePath(rootDir, filePath) {
|
|
|
230
230
|
return path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
function isScssFile(filePath) {
|
|
234
|
+
return filePath.endsWith('.scss');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// "CSS files" in the audit means authored stylesheets: .css always, .scss when
|
|
238
|
+
// postcss-scss can be resolved (see resolveScssSyntax).
|
|
233
239
|
function isCssFile(filePath) {
|
|
234
|
-
return filePath.endsWith('.css');
|
|
240
|
+
return filePath.endsWith('.css') || isScssFile(filePath);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let scssSyntaxCache;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* postcss-scss is an optional peer. Resolve it from the audited project first,
|
|
247
|
+
* then from this package. Returns null when unavailable; SCSS files are then
|
|
248
|
+
* counted as skipped instead of failing the audit.
|
|
249
|
+
*/
|
|
250
|
+
function resolveScssSyntax() {
|
|
251
|
+
if (scssSyntaxCache !== undefined) {
|
|
252
|
+
return scssSyntaxCache;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
scssSyntaxCache = require.resolve('postcss-scss', { paths: [process.cwd(), __dirname] });
|
|
256
|
+
} catch {
|
|
257
|
+
scssSyntaxCache = null;
|
|
258
|
+
}
|
|
259
|
+
return scssSyntaxCache;
|
|
235
260
|
}
|
|
236
261
|
|
|
237
262
|
function isTemplateFile(filePath) {
|
|
@@ -240,7 +265,10 @@ function isTemplateFile(filePath) {
|
|
|
240
265
|
|
|
241
266
|
async function runStylelintAudit(cssFiles, options) {
|
|
242
267
|
if (cssFiles.length === 0) {
|
|
243
|
-
|
|
268
|
+
const empty = [];
|
|
269
|
+
empty.scssFiles = 0;
|
|
270
|
+
empty.scssSkipped = 0;
|
|
271
|
+
return empty;
|
|
244
272
|
}
|
|
245
273
|
|
|
246
274
|
const { default: stylelint } = await import('stylelint');
|
|
@@ -274,15 +302,42 @@ async function runStylelintAudit(cssFiles, options) {
|
|
|
274
302
|
];
|
|
275
303
|
}
|
|
276
304
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
305
|
+
const plainFiles = cssFiles.filter((file) => !isScssFile(file));
|
|
306
|
+
const scssFiles = cssFiles.filter(isScssFile);
|
|
307
|
+
const results = [];
|
|
308
|
+
|
|
309
|
+
if (plainFiles.length > 0) {
|
|
310
|
+
const result = await stylelint.lint({
|
|
311
|
+
files: plainFiles,
|
|
312
|
+
config: {
|
|
313
|
+
plugins: [pluginPath],
|
|
314
|
+
rules,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
results.push(...(result.results || []));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let scssSkipped = 0;
|
|
321
|
+
if (scssFiles.length > 0) {
|
|
322
|
+
const scssSyntax = resolveScssSyntax();
|
|
323
|
+
if (scssSyntax) {
|
|
324
|
+
const result = await stylelint.lint({
|
|
325
|
+
files: scssFiles,
|
|
326
|
+
config: {
|
|
327
|
+
customSyntax: scssSyntax,
|
|
328
|
+
plugins: [pluginPath],
|
|
329
|
+
rules,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
results.push(...(result.results || []));
|
|
333
|
+
} else {
|
|
334
|
+
scssSkipped = scssFiles.length;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
284
337
|
|
|
285
|
-
|
|
338
|
+
results.scssFiles = scssFiles.length;
|
|
339
|
+
results.scssSkipped = scssSkipped;
|
|
340
|
+
return results;
|
|
286
341
|
}
|
|
287
342
|
|
|
288
343
|
function collectCssFindings(fileResults) {
|
|
@@ -535,6 +590,8 @@ module.exports = {
|
|
|
535
590
|
globToRegExp,
|
|
536
591
|
hasGlob,
|
|
537
592
|
isCssFile,
|
|
593
|
+
isScssFile,
|
|
594
|
+
resolveScssSyntax,
|
|
538
595
|
isPathInside,
|
|
539
596
|
isTemplateFile,
|
|
540
597
|
offsetToLineColumn,
|
package/src/cli/init.js
CHANGED
|
@@ -92,9 +92,6 @@ function detect() {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function selectProfile(stack) {
|
|
95
|
-
if (stack.nextjs && stack.tailwind) {
|
|
96
|
-
return 'react-tailwind';
|
|
97
|
-
}
|
|
98
95
|
if (stack.tailwind) {
|
|
99
96
|
return 'tailwind';
|
|
100
97
|
}
|
|
@@ -140,6 +137,8 @@ async function run() {
|
|
|
140
137
|
|
|
141
138
|
const config = {
|
|
142
139
|
extends: [`stylelint-plugin-rhythmguard/configs/${profile}`],
|
|
140
|
+
// Next.js build output is generated CSS; never lint it.
|
|
141
|
+
...(stack.nextjs ? { ignoreFiles: ['.next/**', 'out/**', 'node_modules/**'] } : {}),
|
|
143
142
|
};
|
|
144
143
|
|
|
145
144
|
const configPath = path.join(process.cwd(), '.stylelintrc.json');
|