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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Token governance for CSS and Tailwind — enforce spacing scales, require design tokens, catch arbitrary values",
5
5
  "bin": {
6
6
  "rhythmguard": "src/cli/index.js"
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 files = [];
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
- if (!SKIP_DIRS.has(entry.name)) {
170
- walk(path.join(currentDir, entry.name));
171
- }
223
+ walk(fullPath);
172
224
  continue;
173
225
  }
174
226
 
175
227
  if (entry.isFile()) {
176
- files.push(path.join(currentDir, entry.name));
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 files;
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 allFiles = walkFiles(resolvedDir);
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,