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 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
- Thanks for contributing to `stylelint-plugin-rhythmguard`.
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
- ## Development Setup
5
+ ## Ways to contribute
6
6
 
7
- ```bash
8
- npm ci
9
- npm run lint
10
- npm test
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
- Optional checks:
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
- npm run test:compat-floor
17
- npm run test:coverage
18
- npm run test:pack-smoke
19
- npm run scales:validate
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
- ## Community Scale Contributions
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
- Rhythmguard accepts community presets through JSON files in `scales/community`.
32
+ ## The local gate
25
33
 
26
- Create a new scale file:
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 scales:add -- --name my-team-scale --base 8 --steps 0,4,8,12,16,24,32
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
- Then validate:
42
+ Optional, depending on what you touched:
33
43
 
34
44
  ```bash
35
- npm run scales:validate
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
- Scale files must pass schema and collision checks. See [`docs/COMMUNITY_SCALES.md`](./docs/COMMUNITY_SCALES.md) for the full spec and policy.
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
- ## Compatibility Targets
53
+ ### Pull requests from forks
41
54
 
42
- - Stylelint `^16.0.0 || ^17.0.0`. Stylelint `16.0.0` has known autofix and API differences; CI runs the floor-compat suite against it and the full suite non-blocking.
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
- ## Performance Benchmarking
57
+ ## How we write changes
47
58
 
48
- ```bash
49
- npm run bench:perf
50
- npm run bench:perf:fix
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
- Compares runtime against `stylelint-scales` on a deterministic corpus. Method and arguments: [`docs/BENCHMARKING.md`](./docs/BENCHMARKING.md).
67
+ **Prose without em dashes.** House style, applied to docs and messages alike.
54
68
 
55
- ## Semver Rules
69
+ ## Semver
56
70
 
57
- - Patch (`x.y.Z`): bug fixes, docs updates, non-breaking internal changes.
58
- - Minor (`x.Y.z`): backward-compatible new options, presets, or behavior.
59
- - Major (`X.y.z`): any breaking behavior change for existing rules/configs.
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
- Breaking examples:
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
- - changing default scale behavior
64
- - changing autofix behavior in a non-compatible way
65
- - changing/removing exported config entry points
77
+ ## Compatibility
66
78
 
67
- ## Rule Change Requirements
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
- When changing rule logic:
84
+ ## Review
70
85
 
71
- 1. add/adjust tests for the behavior
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
- ## Release Workflow
88
+ ## Releases
78
89
 
79
- 1. Run the local gate: `npm run lint && npm run typecheck && npm test && npm run test:pack-smoke`.
80
- 2. Update `CHANGELOG.md` and bump `package.json`.
81
- 3. Create a GitHub release. `release.yml` runs the Node and Stylelint matrix on the self-hosted runners, then publishes to npm with provenance when `NPM_TOKEN` is configured, or skips publish with a notice when it is not.
82
- 4. `post-publish-smoke.yml` installs the published version in a clean project.
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
- Keep `configs/recommended` and `configs/strict` stable across minors. Rule docs live in `docs/rules/`; a test checks that every rule's `meta.url` points at its page.
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=3" width="100%" alt="Rhythmguard banner showing spacing scale ruler and lint output" />
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
- Spacing scale and design-token governance for CSS and Tailwind. `padding: 13px` and `p-[13px]` get reported with the nearest on-scale values, and fixed to them when you ask.
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
  [![CI](https://img.shields.io/github/actions/workflow/status/petrilahdelma/stylelint-plugin-rhythmguard/ci.yml?branch=main&label=ci)](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/actions/workflows/ci.yml)
10
- [![npm version](https://img.shields.io/npm/v/stylelint-plugin-rhythmguard.svg)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
10
+ [![npm version](https://img.shields.io/npm/v/stylelint-plugin-rhythmguard?label=npm&color=1f6feb)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
11
11
  [![npm downloads](https://img.shields.io/npm/dm/stylelint-plugin-rhythmguard.svg)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
12
12
  [![License: MIT](https://img.shields.io/badge/license-MIT-white.svg)](./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), and it does not parse SCSS in the audit. Pair it with a color linter if you need one; do not expect one tool to do both.
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`, `react-tailwind`, `expanded`, `logical`, `migration`, `motion`, 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).
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 18.18 or newer for Stylelint 16, Node 20.19 or newer for Stylelint 17. CommonJS and ESM entry points, TypeScript declarations for every export. The CI matrix runs Node 18, 20 and 22 against Stylelint 16.0.0, 16.x and 17.x.
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
@@ -4,8 +4,8 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | --- | --- |
7
- | 0.1.x | Yes |
8
- | < 0.1.0 | No |
7
+ | 2.x | Yes |
8
+ | < 2.0 | No, upgrade to 2.x |
9
9
 
10
10
  ## Reporting a Vulnerability
11
11
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "2.2.0",
4
- "description": "Token governance for CSS and Tailwind enforce spacing scales, require design tokens, catch arbitrary values",
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": ">=18.18.0"
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 enterprise UIs.",
3
+ "description": "Decimal-friendly product spacing scale for dashboard-heavy UIs.",
4
4
  "base": 10,
5
- "steps": [0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64, 80],
6
- "aliases": ["decimal-10", "enterprise-10"],
7
- "tags": ["product", "dashboard", "community"],
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
  }
@@ -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}% |`,
@@ -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
  ];
@@ -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
- return [];
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 result = await stylelint.lint({
278
- files: cssFiles,
279
- config: {
280
- plugins: [pluginPath],
281
- rules,
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
- return result.results || [];
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');