stylelint-plugin-rhythmguard 2.0.0 → 2.1.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.
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { createAuditReport, toAuditContractReport } from 'stylelint-plugin-rhythmguard/audit';
6
+
7
+ function readOption(argv, name, fallback) {
8
+ const index = argv.indexOf(name);
9
+ if (index === -1 || !argv[index + 1] || argv[index + 1].startsWith('--')) {
10
+ return fallback;
11
+ }
12
+
13
+ return argv[index + 1];
14
+ }
15
+
16
+ function escapeHtml(value) {
17
+ return String(value)
18
+ .replace(/&/g, '&')
19
+ .replace(/</g, '&lt;')
20
+ .replace(/>/g, '&gt;')
21
+ .replace(/"/g, '&quot;');
22
+ }
23
+
24
+ function collectFindings(findings) {
25
+ return [
26
+ ...(findings.css || []),
27
+ ...(findings.tailwind || []),
28
+ ...(findings.motion || []),
29
+ ];
30
+ }
31
+
32
+ function renderMetric(label, value) {
33
+ return `<article class="metric"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></article>`;
34
+ }
35
+
36
+ function renderDashboard(contract) {
37
+ const findings = collectFindings(contract.findings);
38
+ const topFindings = findings.slice(0, 12);
39
+ const cleanliness = contract.contracts.scale.cleanliness;
40
+ const score = typeof cleanliness === 'number' ? cleanliness : contract.summary.cleanliness;
41
+
42
+ return `<!doctype html>
43
+ <html lang="en">
44
+ <head>
45
+ <meta charset="utf-8">
46
+ <meta name="viewport" content="width=device-width, initial-scale=1">
47
+ <title>Rhythmguard Audit Dashboard</title>
48
+ <style>
49
+ body{font-family:Inter,ui-sans-serif,system-ui,sans-serif;margin:0;background:#f7f8fa;color:#111827;}
50
+ main{max-width:1120px;margin:0 auto;padding:32px 20px;}
51
+ h1{font-size:28px;margin:0 0 8px;}
52
+ p{color:#4b5563;margin:0 0 24px;}
53
+ .metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin:24px 0;}
54
+ .metric{border:1px solid #d8dde5;background:#fff;border-radius:8px;padding:16px;}
55
+ .metric span{display:block;color:#6b7280;font-size:12px;text-transform:uppercase;}
56
+ .metric strong{display:block;font-size:28px;margin-top:8px;}
57
+ table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #d8dde5;border-radius:8px;overflow:hidden;}
58
+ th,td{text-align:left;border-bottom:1px solid #e5e7eb;padding:10px 12px;font-size:14px;}
59
+ th{background:#f1f5f9;color:#374151;font-size:12px;text-transform:uppercase;}
60
+ tr:last-child td{border-bottom:0;}
61
+ code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;}
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <main>
66
+ <h1>Rhythmguard Audit Dashboard</h1>
67
+ <p>${escapeHtml(contract.command.directory)} · ${escapeHtml(contract.command.scanScope)} · schema ${escapeHtml(contract.schemaVersion)}</p>
68
+ <section class="metrics">
69
+ ${renderMetric('Scale cleanliness', `${score ?? 100}%`)}
70
+ ${renderMetric('CSS files', contract.scanned.cssFiles)}
71
+ ${renderMetric('Template files', contract.scanned.templateFiles)}
72
+ ${renderMetric('Findings', findings.length)}
73
+ </section>
74
+ <table>
75
+ <thead><tr><th>Type</th><th>File</th><th>Value</th><th>Message</th></tr></thead>
76
+ <tbody>
77
+ ${topFindings.map((finding) => `<tr><td>${escapeHtml(finding.type)}</td><td><code>${escapeHtml(finding.file)}</code></td><td><code>${escapeHtml(finding.value || finding.rawValue || '')}</code></td><td>${escapeHtml(finding.message || '')}</td></tr>`).join('\n')}
78
+ </tbody>
79
+ </table>
80
+ </main>
81
+ </body>
82
+ </html>`;
83
+ }
84
+
85
+ const argv = process.argv.slice(2);
86
+ const dir = readOption(argv, '--dir', argv.find((arg) => !arg.startsWith('--')) || 'src');
87
+ const output = readOption(argv, '--output', 'rhythmguard-dashboard.html');
88
+ const includeMotion = argv.includes('--include-motion');
89
+
90
+ const report = await createAuditReport({ dir, includeMotion });
91
+ const contract = toAuditContractReport(report);
92
+ const outputPath = path.resolve(process.cwd(), output);
93
+
94
+ fs.writeFileSync(outputPath, renderDashboard(contract));
95
+ process.stdout.write(`Wrote ${outputPath}\n`);
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { createAuditReport, toAuditContractReport } from 'stylelint-plugin-rhythmguard/audit';
6
+
7
+ function readOption(argv, name, fallback) {
8
+ const index = argv.indexOf(name);
9
+ if (index === -1 || !argv[index + 1] || argv[index + 1].startsWith('--')) {
10
+ return fallback;
11
+ }
12
+
13
+ return argv[index + 1];
14
+ }
15
+
16
+ function collectFindings(findings) {
17
+ return [
18
+ ...(findings.css || []),
19
+ ...(findings.tailwind || []),
20
+ ...(findings.motion || []),
21
+ ];
22
+ }
23
+
24
+ function countBy(items, getKey) {
25
+ const counts = new Map();
26
+ for (const item of items) {
27
+ const key = getKey(item) || 'unknown';
28
+ counts.set(key, (counts.get(key) || 0) + 1);
29
+ }
30
+
31
+ return [...counts.entries()]
32
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
33
+ .map(([label, value]) => ({ label, value }));
34
+ }
35
+
36
+ function buildFigmaPayload(contract) {
37
+ const findings = collectFindings(contract.findings);
38
+ const cleanliness = contract.contracts.scale.cleanliness;
39
+
40
+ return {
41
+ schema: 'rhythmguard.figma-export.v1',
42
+ generatedAt: new Date().toISOString(),
43
+ source: {
44
+ directory: contract.command.directory,
45
+ scanScope: contract.command.scanScope,
46
+ schemaVersion: contract.schemaVersion,
47
+ },
48
+ summaryCards: [
49
+ {
50
+ label: 'Scale cleanliness',
51
+ value: `${typeof cleanliness === 'number' ? cleanliness : contract.summary.cleanliness ?? 100}%`,
52
+ },
53
+ { label: 'CSS files', value: contract.scanned.cssFiles },
54
+ { label: 'Template files', value: contract.scanned.templateFiles },
55
+ { label: 'Findings', value: findings.length },
56
+ ],
57
+ charts: {
58
+ findingsByType: countBy(findings, (finding) => finding.type),
59
+ findingsByFile: countBy(findings, (finding) => finding.file).slice(0, 10),
60
+ },
61
+ findings: findings.slice(0, 25).map((finding) => ({
62
+ file: finding.file,
63
+ line: finding.line || null,
64
+ message: finding.message || '',
65
+ type: finding.type,
66
+ value: finding.value || finding.rawValue || null,
67
+ })),
68
+ };
69
+ }
70
+
71
+ const argv = process.argv.slice(2);
72
+ const dir = readOption(argv, '--dir', argv.find((arg) => !arg.startsWith('--')) || 'src');
73
+ const output = readOption(argv, '--output', 'rhythmguard-figma-export.json');
74
+ const includeMotion = argv.includes('--include-motion');
75
+
76
+ const report = await createAuditReport({ dir, includeMotion });
77
+ const contract = toAuditContractReport(report);
78
+ const outputPath = path.resolve(process.cwd(), output);
79
+
80
+ fs.writeFileSync(outputPath, `${JSON.stringify(buildFigmaPayload(contract), null, 2)}\n`);
81
+ process.stdout.write(`Wrote ${outputPath}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
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"
@@ -20,76 +20,95 @@
20
20
  ],
21
21
  "type": "commonjs",
22
22
  "main": "src/index.js",
23
+ "types": "./types/index.d.ts",
23
24
  "exports": {
24
25
  ".": {
26
+ "types": "./types/index.d.ts",
25
27
  "require": "./src/index.js",
26
28
  "import": "./src/index.mjs"
27
29
  },
28
30
  "./configs/recommended": {
31
+ "types": "./types/config.d.ts",
29
32
  "require": "./src/configs/recommended.js",
30
33
  "import": "./src/configs/recommended.mjs"
31
34
  },
32
35
  "./configs/strict": {
36
+ "types": "./types/config.d.ts",
33
37
  "require": "./src/configs/strict.js",
34
38
  "import": "./src/configs/strict.mjs"
35
39
  },
36
40
  "./configs/tailwind": {
41
+ "types": "./types/config.d.ts",
37
42
  "require": "./src/configs/tailwind.js",
38
43
  "import": "./src/configs/tailwind.mjs"
39
44
  },
40
45
  "./configs/expanded": {
46
+ "types": "./types/config.d.ts",
41
47
  "require": "./src/configs/expanded.js",
42
48
  "import": "./src/configs/expanded.mjs"
43
49
  },
44
50
  "./configs/logical": {
51
+ "types": "./types/config.d.ts",
45
52
  "require": "./src/configs/logical.js",
46
53
  "import": "./src/configs/logical.mjs"
47
54
  },
48
55
  "./configs/migration": {
56
+ "types": "./types/config.d.ts",
49
57
  "require": "./src/configs/migration.js",
50
58
  "import": "./src/configs/migration.mjs"
51
59
  },
52
60
  "./configs/motion": {
61
+ "types": "./types/config.d.ts",
53
62
  "require": "./src/configs/motion.js",
54
63
  "import": "./src/configs/motion.mjs"
55
64
  },
56
65
  "./configs/react-tailwind": {
66
+ "types": "./types/config.d.ts",
57
67
  "require": "./src/configs/react-tailwind.js",
58
68
  "import": "./src/configs/react-tailwind.mjs"
59
69
  },
60
70
  "./presets": {
71
+ "types": "./types/presets.d.ts",
61
72
  "require": "./src/presets/index.js",
62
73
  "import": "./src/presets/index.mjs"
63
74
  },
64
75
  "./audit": {
76
+ "types": "./types/audit.d.ts",
65
77
  "require": "./src/audit/index.js",
66
78
  "import": "./src/audit/index.mjs"
67
79
  },
68
80
  "./rules/use-scale": {
81
+ "types": "./types/rule.d.ts",
69
82
  "require": "./src/rules/use-scale/index.js",
70
83
  "import": "./src/rules/use-scale/index.mjs"
71
84
  },
72
85
  "./rules/prefer-token": {
86
+ "types": "./types/rule.d.ts",
73
87
  "require": "./src/rules/prefer-token/index.js",
74
88
  "import": "./src/rules/prefer-token/index.mjs"
75
89
  },
76
90
  "./rules/no-offscale-transform": {
91
+ "types": "./types/rule.d.ts",
77
92
  "require": "./src/rules/no-offscale-transform/index.js",
78
93
  "import": "./src/rules/no-offscale-transform/index.mjs"
79
94
  },
80
95
  "./rules/use-motion-scale": {
96
+ "types": "./types/rule.d.ts",
81
97
  "require": "./src/rules/use-motion-scale/index.js",
82
98
  "import": "./src/rules/use-motion-scale/index.mjs"
83
99
  },
84
100
  "./eslint": {
101
+ "types": "./types/eslint.d.ts",
85
102
  "require": "./src/eslint/index.js",
86
103
  "import": "./src/eslint/index.mjs"
87
104
  }
88
105
  },
89
106
  "files": [
107
+ "examples",
90
108
  "scales",
91
109
  "schemas",
92
110
  "src",
111
+ "types",
93
112
  "README.md",
94
113
  "CHANGELOG.md",
95
114
  "CONTRIBUTING.md",
@@ -102,16 +121,17 @@
102
121
  "lint": "eslint .",
103
122
  "scales:add": "node scripts/scales/add-scale.mjs",
104
123
  "scales:validate": "node scripts/scales/validate-community-scales.mjs",
105
- "test:pack-smoke": "node scripts/ci/pack-smoke.mjs",
106
- "test:npm-smoke": "node scripts/ci/npm-registry-smoke.mjs --package stylelint-plugin-rhythmguard --version latest",
107
124
  "test": "node --test test/*.test.js",
108
125
  "test:compat-floor": "node --test test/floor-compat.test.js",
126
+ "test:coverage": "c8 --reporter=text --reporter=lcov node --test test/*.test.js",
127
+ "test:npm-smoke": "node scripts/ci/npm-registry-smoke.mjs --package stylelint-plugin-rhythmguard --version latest",
128
+ "test:pack-smoke": "node scripts/ci/pack-smoke.mjs",
109
129
  "test:watch": "node --test --watch test/*.test.js",
110
- "test:coverage": "c8 --reporter=text --reporter=lcov node --test test/*.test.js"
130
+ "typecheck": "tsc -p tsconfig.typecheck.json"
111
131
  },
112
132
  "repository": {
113
133
  "type": "git",
114
- "url": "https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard"
134
+ "url": "git+https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard.git"
115
135
  },
116
136
  "homepage": "https://github.com/petrilahdelma/stylelint-plugin-rhythmguard#readme",
117
137
  "bugs": {
@@ -128,13 +148,15 @@
128
148
  },
129
149
  "devDependencies": {
130
150
  "@eslint/js": "^9.22.0",
151
+ "@types/node": "^22.20.1",
131
152
  "c8": "^10.1.3",
132
153
  "eslint": "^9.22.0",
133
154
  "geist": "^1.7.0",
134
155
  "postcss-value-parser": "^4.2.0",
135
156
  "stylelint": "^16.15.0",
157
+ "stylelint-plugin-logical-css": "^2.0.2",
136
158
  "stylelint-scales": "^5.0.0",
137
- "stylelint-plugin-logical-css": "^2.0.2"
159
+ "typescript": "^5.9.3"
138
160
  },
139
161
  "engines": {
140
162
  "node": ">=18.18.0"