stylelint-plugin-rhythmguard 1.6.0 → 1.6.1
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 +8 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/cli/audit.js +144 -10
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ The format follows Keep a Changelog principles and semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.6.1] - 2026-05-23
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Added `rhythmguard audit --ignore` for pruning root-relative paths before scanning large repositories.
|
|
14
|
+
- Scoped audit traversal to scan-relevant CSS and template files instead of collecting every file under the audit root first.
|
|
15
|
+
- Added default audit skips for common generated directories such as `.svelte-kit`, `.turbo`, and `.vercel`.
|
|
16
|
+
|
|
9
17
|
## [1.6.0] - 2026-05-19
|
|
10
18
|
|
|
11
19
|
### Added
|
package/README.md
CHANGED
|
@@ -89,9 +89,10 @@ Use the audit CLI to create a design-system drift report before turning rules in
|
|
|
89
89
|
npx rhythmguard audit ./src
|
|
90
90
|
npx rhythmguard audit ./src --format markdown
|
|
91
91
|
npx rhythmguard audit ./src --json
|
|
92
|
+
npx rhythmguard audit . --ignore "apps/legacy/**" --ignore "vendor/**"
|
|
92
93
|
```
|
|
93
94
|
|
|
94
|
-
The report covers authored CSS declarations and Tailwind arbitrary spacing values in common template/source files. Markdown output is PR-ready for UX developers, UX designers, and design-system owners:
|
|
95
|
+
The report covers authored CSS declarations and Tailwind arbitrary spacing values in common template/source files. Scan paths are scoped to the directory argument, and `--ignore` accepts repeatable, root-relative glob patterns for large generated or legacy subtrees. Markdown output is PR-ready for UX developers, UX designers, and design-system owners:
|
|
95
96
|
|
|
96
97
|
```md
|
|
97
98
|
# Rhythmguard Design-System Audit
|
package/package.json
CHANGED
package/src/cli/audit.js
CHANGED
|
@@ -17,6 +17,9 @@ const SKIP_DIRS = new Set([
|
|
|
17
17
|
'.next',
|
|
18
18
|
'.nuxt',
|
|
19
19
|
'.omx',
|
|
20
|
+
'.svelte-kit',
|
|
21
|
+
'.turbo',
|
|
22
|
+
'.vercel',
|
|
20
23
|
'build',
|
|
21
24
|
'coverage',
|
|
22
25
|
'dist',
|
|
@@ -41,6 +44,7 @@ Options:
|
|
|
41
44
|
--format <text|json|markdown> Output format (default: text)
|
|
42
45
|
--json Alias for --format json
|
|
43
46
|
--markdown Alias for --format markdown
|
|
47
|
+
--ignore <pattern> Exclude root-relative path/glob (repeatable, comma-separated)
|
|
44
48
|
--scale <values> Comma-separated scale values (default: 0,4,8,12,16,24,32)
|
|
45
49
|
--base-font-size <number> px base for rem/em conversion (default: 16)
|
|
46
50
|
`;
|
|
@@ -50,6 +54,7 @@ function parseArgs(argv) {
|
|
|
50
54
|
baseFontSize: DEFAULT_BASE_FONT_SIZE,
|
|
51
55
|
dir: null,
|
|
52
56
|
format: 'text',
|
|
57
|
+
ignorePatterns: [],
|
|
53
58
|
scale: DEFAULT_SCALE,
|
|
54
59
|
};
|
|
55
60
|
|
|
@@ -71,6 +76,16 @@ function parseArgs(argv) {
|
|
|
71
76
|
continue;
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
if (arg === '--ignore') {
|
|
80
|
+
parsed.ignorePatterns.push(...parseIgnorePatterns(argv[++index]));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (arg.startsWith('--ignore=')) {
|
|
85
|
+
parsed.ignorePatterns.push(...parseIgnorePatterns(arg.slice('--ignore='.length)));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
if (arg === '--format') {
|
|
75
90
|
parsed.format = String(argv[++index] || '').toLowerCase();
|
|
76
91
|
continue;
|
|
@@ -116,6 +131,31 @@ function parseArgs(argv) {
|
|
|
116
131
|
return parsed;
|
|
117
132
|
}
|
|
118
133
|
|
|
134
|
+
function parseIgnorePatterns(raw) {
|
|
135
|
+
if (!raw) {
|
|
136
|
+
throw new Error('Missing value for --ignore.');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const patterns = String(raw).split(',')
|
|
140
|
+
.map((pattern) => normalizeIgnorePattern(pattern))
|
|
141
|
+
.filter(Boolean);
|
|
142
|
+
|
|
143
|
+
if (patterns.length === 0) {
|
|
144
|
+
throw new Error('--ignore must include at least one pattern.');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return patterns;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeIgnorePattern(pattern) {
|
|
151
|
+
return String(pattern)
|
|
152
|
+
.trim()
|
|
153
|
+
.replace(/\\/g, '/')
|
|
154
|
+
.replace(/^\/+/, '')
|
|
155
|
+
.replace(/^\.\//, '')
|
|
156
|
+
.replace(/\/+$/, '');
|
|
157
|
+
}
|
|
158
|
+
|
|
119
159
|
function parseScale(raw) {
|
|
120
160
|
if (!raw) {
|
|
121
161
|
throw new Error('Missing value for --scale.');
|
|
@@ -157,29 +197,125 @@ function assertDirectory(dir) {
|
|
|
157
197
|
process.exit(1);
|
|
158
198
|
}
|
|
159
199
|
|
|
200
|
+
if (!fs.statSync(resolvedDir).isDirectory()) {
|
|
201
|
+
process.stderr.write(`Not a directory: ${dir}\n`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
160
205
|
return resolvedDir;
|
|
161
206
|
}
|
|
162
207
|
|
|
163
|
-
function walkFiles(rootDir) {
|
|
164
|
-
const
|
|
208
|
+
function walkFiles(rootDir, ignorePatterns = []) {
|
|
209
|
+
const cssFiles = [];
|
|
210
|
+
const templateFiles = [];
|
|
211
|
+
const ignoreMatchers = createIgnoreMatchers(ignorePatterns);
|
|
165
212
|
|
|
166
213
|
function walk(currentDir) {
|
|
167
214
|
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
215
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
216
|
+
const relativePath = toPosixRelativePath(rootDir, fullPath);
|
|
217
|
+
|
|
218
|
+
if (shouldIgnorePath(relativePath, entry, ignoreMatchers)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
168
222
|
if (entry.isDirectory()) {
|
|
169
|
-
|
|
170
|
-
walk(path.join(currentDir, entry.name));
|
|
171
|
-
}
|
|
223
|
+
walk(fullPath);
|
|
172
224
|
continue;
|
|
173
225
|
}
|
|
174
226
|
|
|
175
227
|
if (entry.isFile()) {
|
|
176
|
-
|
|
228
|
+
if (isCssFile(fullPath)) {
|
|
229
|
+
cssFiles.push(fullPath);
|
|
230
|
+
} else if (isTemplateFile(fullPath)) {
|
|
231
|
+
templateFiles.push(fullPath);
|
|
232
|
+
}
|
|
177
233
|
}
|
|
178
234
|
}
|
|
179
235
|
}
|
|
180
236
|
|
|
181
237
|
walk(rootDir);
|
|
182
|
-
return
|
|
238
|
+
return { cssFiles, templateFiles };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function shouldIgnorePath(relativePath, entry, ignoreMatchers) {
|
|
242
|
+
return ignoreMatchers.some((matcher) => matcher.test(relativePath))
|
|
243
|
+
|| (entry.isDirectory() && SKIP_DIRS.has(entry.name));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function createIgnoreMatchers(patterns) {
|
|
247
|
+
const variants = new Set();
|
|
248
|
+
|
|
249
|
+
for (const pattern of patterns) {
|
|
250
|
+
addIgnorePatternVariants(variants, pattern);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return Array.from(variants, (pattern) => globToRegExp(pattern));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function addIgnorePatternVariants(variants, pattern) {
|
|
257
|
+
if (!pattern) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
variants.add(pattern);
|
|
262
|
+
|
|
263
|
+
if (!pattern.includes('/')) {
|
|
264
|
+
variants.add(`${pattern}/**`);
|
|
265
|
+
variants.add(`**/${pattern}`);
|
|
266
|
+
variants.add(`**/${pattern}/**`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (pattern.endsWith('/**')) {
|
|
271
|
+
variants.add(pattern.slice(0, -3));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!hasGlob(pattern)) {
|
|
276
|
+
variants.add(`${pattern}/**`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function hasGlob(pattern) {
|
|
281
|
+
return /[*?]/.test(pattern);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function globToRegExp(pattern) {
|
|
285
|
+
let source = '^';
|
|
286
|
+
|
|
287
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
288
|
+
const char = pattern[index];
|
|
289
|
+
const nextChar = pattern[index + 1];
|
|
290
|
+
|
|
291
|
+
if (char === '*' && nextChar === '*') {
|
|
292
|
+
source += '.*';
|
|
293
|
+
index++;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (char === '*') {
|
|
298
|
+
source += '[^/]*';
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (char === '?') {
|
|
303
|
+
source += '[^/]';
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
source += escapeRegExp(char);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return new RegExp(`${source}$`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function escapeRegExp(value) {
|
|
314
|
+
return value.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function toPosixRelativePath(rootDir, filePath) {
|
|
318
|
+
return path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
183
319
|
}
|
|
184
320
|
|
|
185
321
|
function isCssFile(filePath) {
|
|
@@ -605,9 +741,7 @@ async function run() {
|
|
|
605
741
|
}
|
|
606
742
|
|
|
607
743
|
const resolvedDir = assertDirectory(parsed.dir);
|
|
608
|
-
const
|
|
609
|
-
const cssFiles = allFiles.filter(isCssFile);
|
|
610
|
-
const templateFiles = allFiles.filter(isTemplateFile);
|
|
744
|
+
const { cssFiles, templateFiles } = walkFiles(resolvedDir, parsed.ignorePatterns);
|
|
611
745
|
const options = {
|
|
612
746
|
baseFontSize: parsed.baseFontSize,
|
|
613
747
|
scale: parsed.scale,
|