stylelint-plugin-rhythmguard 2.0.0 → 2.0.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 +12 -0
- package/README.md +2 -0
- package/examples/audit-dashboard.mjs +95 -0
- package/examples/audit-figma-export.mjs +81 -0
- package/package.json +20 -1
- package/src/cli/init.js +69 -41
- package/types/audit.d.ts +156 -0
- package/types/config.d.ts +5 -0
- package/types/eslint.d.ts +10 -0
- package/types/index.d.ts +65 -0
- package/types/presets.d.ts +14 -0
- package/types/rule.d.ts +20 -0
- package/types/shared.d.ts +66 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ The format follows Keep a Changelog principles and semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.0.1] - 2026-06-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added TypeScript declaration files for the public plugin, config, rule, preset, ESLint companion, and Audit 2.0 API exports.
|
|
14
|
+
- Added dependency-free Audit 2.0 dashboard and Figma-friendly export examples under `examples/`.
|
|
15
|
+
- Added CI adoption and motion-default evidence docs for safer baseline-based rollout.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed `rhythmguard init` scripted input handling so multiple prompts work reliably in piped/CI contexts.
|
|
20
|
+
|
|
9
21
|
## [2.0.0] - 2026-05-23
|
|
10
22
|
|
|
11
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -291,6 +291,8 @@ Framework-specific setup for Vue, Lit, Astro, and SvelteKit: [`docs/FRAMEWORKS.m
|
|
|
291
291
|
|
|
292
292
|
- Side-by-side tool fit guide with migration snippets: [`docs/COMPARISON.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/COMPARISON.md)
|
|
293
293
|
- Audit 2.0 validation and roadmap: [`docs/AUDIT_2_VALIDATION.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/AUDIT_2_VALIDATION.md)
|
|
294
|
+
- CI baseline rollout recipe: [`docs/CI_ADOPTION.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/CI_ADOPTION.md)
|
|
295
|
+
- Programmatic dashboard and Figma-friendly export examples: [`docs/AUDIT_API_EXAMPLES.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/AUDIT_API_EXAMPLES.md)
|
|
294
296
|
- Real-world before/after excerpts from public repos: [`docs/ADOPTION_DIFFS.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/ADOPTION_DIFFS.md)
|
|
295
297
|
- Distribution submissions to Stylelint discovery surfaces: [`docs/DISTRIBUTION.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/DISTRIBUTION.md)
|
|
296
298
|
|
|
@@ -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, '<')
|
|
20
|
+
.replace(/>/g, '>')
|
|
21
|
+
.replace(/"/g, '"');
|
|
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.
|
|
3
|
+
"version": "2.0.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"
|
|
@@ -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",
|
package/src/cli/init.js
CHANGED
|
@@ -4,17 +4,39 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const readline = require('node:readline');
|
|
6
6
|
|
|
7
|
-
function
|
|
7
|
+
function createPrompter() {
|
|
8
|
+
if (!process.stdin.isTTY) {
|
|
9
|
+
const answers = fs.readFileSync(0, 'utf8').split(/\r?\n/);
|
|
10
|
+
let answerIndex = 0;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
ask(question) {
|
|
14
|
+
process.stdout.write(question);
|
|
15
|
+
const answer = answers[answerIndex] || '';
|
|
16
|
+
answerIndex += 1;
|
|
17
|
+
return Promise.resolve(answer.trim().toLowerCase());
|
|
18
|
+
},
|
|
19
|
+
close() {},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
const rl = readline.createInterface({
|
|
9
24
|
input: process.stdin,
|
|
10
25
|
output: process.stdout,
|
|
11
26
|
});
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
ask(question) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
rl.question(question, (answer) => {
|
|
32
|
+
resolve(answer.trim().toLowerCase());
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
close() {
|
|
14
37
|
rl.close();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
function detect() {
|
|
@@ -80,49 +102,55 @@ function selectProfile(stack) {
|
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
async function run() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
const prompter = createPrompter();
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
process.stdout.write('\nRhythmguard Init\n\n');
|
|
109
|
+
|
|
110
|
+
const stack = detect();
|
|
111
|
+
|
|
112
|
+
// Report detection
|
|
113
|
+
const detected = [];
|
|
114
|
+
if (stack.tailwind) detected.push('Tailwind CSS');
|
|
115
|
+
if (stack.nextjs) detected.push('Next.js');
|
|
116
|
+
if (detected.length > 0) {
|
|
117
|
+
process.stdout.write(`Detected: ${detected.join(', ')}\n`);
|
|
118
|
+
} else {
|
|
119
|
+
process.stdout.write('Detected: plain CSS project\n');
|
|
120
|
+
}
|
|
96
121
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
122
|
+
// Warn about existing config
|
|
123
|
+
if (stack.hasExistingConfig) {
|
|
124
|
+
process.stdout.write('\n⚠ Existing Stylelint config found.\n');
|
|
125
|
+
const answer = await prompter.ask('Overwrite? (y/n) ');
|
|
126
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
127
|
+
process.stdout.write('Aborted.\n');
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const profile = selectProfile(stack);
|
|
133
|
+
process.stdout.write(`\nProfile: ${profile}\n`);
|
|
134
|
+
|
|
135
|
+
const answer = await prompter.ask('Write .stylelintrc.json? (y/n) ');
|
|
101
136
|
if (answer !== 'y' && answer !== 'yes') {
|
|
102
137
|
process.stdout.write('Aborted.\n');
|
|
103
138
|
process.exit(0);
|
|
104
139
|
}
|
|
105
|
-
}
|
|
106
140
|
|
|
107
|
-
|
|
108
|
-
|
|
141
|
+
const config = {
|
|
142
|
+
extends: [`stylelint-plugin-rhythmguard/configs/${profile}`],
|
|
143
|
+
};
|
|
109
144
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
process.stdout.write('Aborted.\n');
|
|
113
|
-
process.exit(0);
|
|
114
|
-
}
|
|
145
|
+
const configPath = path.join(process.cwd(), '.stylelintrc.json');
|
|
146
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
115
147
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
process.stdout.write(`\n✓ Wrote ${configPath}\n`);
|
|
124
|
-
process.stdout.write(`\nNext steps:\n`);
|
|
125
|
-
process.stdout.write(` npx stylelint "src/**/*.css"\n\n`);
|
|
148
|
+
process.stdout.write(`\n✓ Wrote ${configPath}\n`);
|
|
149
|
+
process.stdout.write(`\nNext steps:\n`);
|
|
150
|
+
process.stdout.write(` npx stylelint "src/**/*.css"\n\n`);
|
|
151
|
+
} finally {
|
|
152
|
+
prompter.close();
|
|
153
|
+
}
|
|
126
154
|
}
|
|
127
155
|
|
|
128
156
|
run();
|
package/types/audit.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export type AuditTokenKind =
|
|
2
|
+
| "all"
|
|
3
|
+
| "motion"
|
|
4
|
+
| "radius"
|
|
5
|
+
| "size"
|
|
6
|
+
| "spacing"
|
|
7
|
+
| "typography";
|
|
8
|
+
|
|
9
|
+
export type AuditTokenSourceFormat =
|
|
10
|
+
| "auto"
|
|
11
|
+
| "css"
|
|
12
|
+
| "dtcg"
|
|
13
|
+
| "flat-json"
|
|
14
|
+
| "style-dictionary";
|
|
15
|
+
|
|
16
|
+
export interface AuditTokenSource {
|
|
17
|
+
baseDir?: string;
|
|
18
|
+
file?: string;
|
|
19
|
+
format?: AuditTokenSourceFormat;
|
|
20
|
+
path?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AuditOptions {
|
|
24
|
+
baseFontSize?: number;
|
|
25
|
+
baselinePath?: string;
|
|
26
|
+
config?: string;
|
|
27
|
+
dir?: string;
|
|
28
|
+
failOnNewDrift?: boolean;
|
|
29
|
+
format?: "json" | "json-v1" | "markdown" | "text" | "html";
|
|
30
|
+
ignorePath?: string;
|
|
31
|
+
ignorePatterns?: string[];
|
|
32
|
+
includeMotion?: boolean;
|
|
33
|
+
maxFindings?: number;
|
|
34
|
+
minCleanliness?: number;
|
|
35
|
+
output?: string;
|
|
36
|
+
scale?: Array<number | string>;
|
|
37
|
+
since?: string;
|
|
38
|
+
sinceBaseline?: boolean;
|
|
39
|
+
staged?: boolean;
|
|
40
|
+
tokenCandidateMinCount?: number;
|
|
41
|
+
tokenKind?: AuditTokenKind;
|
|
42
|
+
tokenSources?: AuditTokenSource[];
|
|
43
|
+
writeBaseline?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AuditSummary {
|
|
47
|
+
cleanliness: number;
|
|
48
|
+
filesWithFindings: number;
|
|
49
|
+
findingCount: number;
|
|
50
|
+
motionFindingCount?: number;
|
|
51
|
+
tokenOpportunityCount?: number;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AuditScanned {
|
|
56
|
+
cssFiles: number;
|
|
57
|
+
templateFiles: number;
|
|
58
|
+
totalFiles?: number;
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AuditFinding {
|
|
63
|
+
column?: number;
|
|
64
|
+
file: string;
|
|
65
|
+
key?: string;
|
|
66
|
+
line?: number;
|
|
67
|
+
message: string;
|
|
68
|
+
property?: string;
|
|
69
|
+
rule?: string;
|
|
70
|
+
type: string;
|
|
71
|
+
value?: string;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface AuditBaselineComparison {
|
|
76
|
+
baselineFindings: number;
|
|
77
|
+
newFindings: AuditFinding[];
|
|
78
|
+
newFindingsCount: number;
|
|
79
|
+
resolvedFindings: AuditFinding[];
|
|
80
|
+
resolvedFindingsCount: number;
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface AuditReport {
|
|
85
|
+
baseline?: AuditBaselineComparison | null;
|
|
86
|
+
config?: string | null;
|
|
87
|
+
directory: string;
|
|
88
|
+
findings: {
|
|
89
|
+
css: AuditFinding[];
|
|
90
|
+
motion: AuditFinding[];
|
|
91
|
+
tailwind: AuditFinding[];
|
|
92
|
+
};
|
|
93
|
+
scanned: AuditScanned;
|
|
94
|
+
summary: AuditSummary;
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface AuditContractReport {
|
|
99
|
+
baseline: AuditBaselineComparison | null;
|
|
100
|
+
command: {
|
|
101
|
+
config?: string | null;
|
|
102
|
+
directory: string;
|
|
103
|
+
scanScope: string;
|
|
104
|
+
};
|
|
105
|
+
contracts: {
|
|
106
|
+
motion?: unknown;
|
|
107
|
+
scale: {
|
|
108
|
+
cleanliness?: unknown;
|
|
109
|
+
offScaleValues?: unknown;
|
|
110
|
+
tokenOpportunities?: unknown;
|
|
111
|
+
};
|
|
112
|
+
tokens?: unknown;
|
|
113
|
+
};
|
|
114
|
+
findings: unknown;
|
|
115
|
+
scanned: AuditScanned;
|
|
116
|
+
schemaVersion: "2.0";
|
|
117
|
+
summary: AuditSummary;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface TokenSourceReport {
|
|
121
|
+
file: string;
|
|
122
|
+
format: AuditTokenSourceFormat;
|
|
123
|
+
requestedFormat: AuditTokenSourceFormat;
|
|
124
|
+
tokenCount: number;
|
|
125
|
+
warnings: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ParsedTokenSources {
|
|
129
|
+
definitions: Map<string, unknown>;
|
|
130
|
+
sources: TokenSourceReport[];
|
|
131
|
+
warnings: string[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const AUDIT_JSON_SCHEMA: Readonly<Record<string, unknown>>;
|
|
135
|
+
|
|
136
|
+
export function createAuditReport(options?: AuditOptions): Promise<AuditReport>;
|
|
137
|
+
|
|
138
|
+
export function loadAuditConfig(options?: AuditOptions): Record<string, unknown>;
|
|
139
|
+
|
|
140
|
+
export function parseTokenSources(options?: {
|
|
141
|
+
baseFontSize?: number;
|
|
142
|
+
sources?: AuditTokenSource[];
|
|
143
|
+
tokenKind?: AuditTokenKind;
|
|
144
|
+
}): ParsedTokenSources;
|
|
145
|
+
|
|
146
|
+
export function toAuditContractReport(report: AuditReport): AuditContractReport;
|
|
147
|
+
|
|
148
|
+
declare const audit: {
|
|
149
|
+
AUDIT_JSON_SCHEMA: typeof AUDIT_JSON_SCHEMA;
|
|
150
|
+
createAuditReport: typeof createAuditReport;
|
|
151
|
+
loadAuditConfig: typeof loadAuditConfig;
|
|
152
|
+
parseTokenSources: typeof parseTokenSources;
|
|
153
|
+
toAuditContractReport: typeof toAuditContractReport;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export default audit;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RhythmguardEslintPlugin } from "./shared";
|
|
2
|
+
|
|
3
|
+
export type { EslintRuleModule, RhythmguardEslintPlugin } from "./shared";
|
|
4
|
+
|
|
5
|
+
export const configs: RhythmguardEslintPlugin["configs"];
|
|
6
|
+
export const rules: RhythmguardEslintPlugin["rules"];
|
|
7
|
+
|
|
8
|
+
declare const plugin: RhythmguardEslintPlugin;
|
|
9
|
+
|
|
10
|
+
export default plugin;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RhythmguardEslintPlugin,
|
|
3
|
+
RhythmguardPresets,
|
|
4
|
+
RhythmguardStylelintConfig,
|
|
5
|
+
} from "./shared";
|
|
6
|
+
import type * as auditApi from "./audit";
|
|
7
|
+
import type { StylelintRuleModule } from "./rule";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
EslintRuleModule,
|
|
11
|
+
RhythmguardEslintPlugin,
|
|
12
|
+
RhythmguardPresets,
|
|
13
|
+
RhythmguardRuleConfig,
|
|
14
|
+
RhythmguardRuleOptions,
|
|
15
|
+
RhythmguardStylelintConfig,
|
|
16
|
+
ScalePresetMetadata,
|
|
17
|
+
ScaleValue,
|
|
18
|
+
} from "./shared";
|
|
19
|
+
export type {
|
|
20
|
+
AuditBaselineComparison,
|
|
21
|
+
AuditContractReport,
|
|
22
|
+
AuditFinding,
|
|
23
|
+
AuditOptions,
|
|
24
|
+
AuditReport,
|
|
25
|
+
AuditScanned,
|
|
26
|
+
AuditSummary,
|
|
27
|
+
AuditTokenKind,
|
|
28
|
+
AuditTokenSource,
|
|
29
|
+
AuditTokenSourceFormat,
|
|
30
|
+
ParsedTokenSources,
|
|
31
|
+
TokenSourceReport,
|
|
32
|
+
} from "./audit";
|
|
33
|
+
export type { StylelintRuleModule } from "./rule";
|
|
34
|
+
|
|
35
|
+
export interface RhythmguardPlugin extends Array<StylelintRuleModule> {
|
|
36
|
+
audit: typeof auditApi;
|
|
37
|
+
configs: {
|
|
38
|
+
expanded: RhythmguardStylelintConfig;
|
|
39
|
+
logical: RhythmguardStylelintConfig;
|
|
40
|
+
migration: RhythmguardStylelintConfig;
|
|
41
|
+
motion: RhythmguardStylelintConfig;
|
|
42
|
+
recommended: RhythmguardStylelintConfig;
|
|
43
|
+
strict: RhythmguardStylelintConfig;
|
|
44
|
+
tailwind: RhythmguardStylelintConfig;
|
|
45
|
+
[configName: string]: RhythmguardStylelintConfig;
|
|
46
|
+
};
|
|
47
|
+
eslint: RhythmguardEslintPlugin;
|
|
48
|
+
presets: RhythmguardPresets;
|
|
49
|
+
rules: {
|
|
50
|
+
"rhythmguard/no-offscale-transform": StylelintRuleModule;
|
|
51
|
+
"rhythmguard/prefer-token": StylelintRuleModule;
|
|
52
|
+
"rhythmguard/use-motion-scale": StylelintRuleModule;
|
|
53
|
+
"rhythmguard/use-scale": StylelintRuleModule;
|
|
54
|
+
[ruleName: string]: StylelintRuleModule;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
declare const plugin: RhythmguardPlugin;
|
|
59
|
+
|
|
60
|
+
export const audit: typeof auditApi;
|
|
61
|
+
export const configs: RhythmguardPlugin["configs"];
|
|
62
|
+
export const eslint: RhythmguardEslintPlugin;
|
|
63
|
+
export const presets: RhythmguardPresets;
|
|
64
|
+
export const rules: RhythmguardPlugin["rules"];
|
|
65
|
+
export default plugin;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RhythmguardPresets } from "./shared";
|
|
2
|
+
|
|
3
|
+
export type { RhythmguardPresets, ScalePresetMetadata } from "./shared";
|
|
4
|
+
|
|
5
|
+
export const communityScaleMetadata: RhythmguardPresets["communityScaleMetadata"];
|
|
6
|
+
export const getCommunityScaleMetadata: RhythmguardPresets["getCommunityScaleMetadata"];
|
|
7
|
+
export const getScalePreset: RhythmguardPresets["getScalePreset"];
|
|
8
|
+
export const listCommunityScalePresetNames: RhythmguardPresets["listCommunityScalePresetNames"];
|
|
9
|
+
export const listScalePresetNames: RhythmguardPresets["listScalePresetNames"];
|
|
10
|
+
export const scales: RhythmguardPresets["scales"];
|
|
11
|
+
|
|
12
|
+
declare const presets: RhythmguardPresets;
|
|
13
|
+
|
|
14
|
+
export default presets;
|
package/types/rule.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RhythmguardRuleOptions } from "./shared";
|
|
2
|
+
|
|
3
|
+
export interface StylelintRuleModule {
|
|
4
|
+
messages: Record<string, unknown>;
|
|
5
|
+
meta: {
|
|
6
|
+
fixable?: boolean;
|
|
7
|
+
url?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
ruleName: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare const rule: StylelintRuleModule;
|
|
15
|
+
|
|
16
|
+
export const messages: StylelintRuleModule["messages"];
|
|
17
|
+
export const meta: StylelintRuleModule["meta"];
|
|
18
|
+
export const ruleName: string;
|
|
19
|
+
export type { RhythmguardRuleOptions };
|
|
20
|
+
export default rule;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type ScaleValue = number | string;
|
|
2
|
+
|
|
3
|
+
export type RuleSeverity = boolean | "always" | "never";
|
|
4
|
+
|
|
5
|
+
export interface RhythmguardRuleOptions {
|
|
6
|
+
baseFontSize?: number;
|
|
7
|
+
customScale?: ScaleValue[];
|
|
8
|
+
includeMathFunctions?: boolean;
|
|
9
|
+
preset?: string;
|
|
10
|
+
properties?: Array<string | RegExp>;
|
|
11
|
+
scale?: ScaleValue[];
|
|
12
|
+
tokenMap?: Record<string, string>;
|
|
13
|
+
tokenMapFile?: string;
|
|
14
|
+
tokenMapFromCssCustomProperties?: boolean;
|
|
15
|
+
tokenMapFromTailwindSpacing?: boolean;
|
|
16
|
+
tokenPattern?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type RhythmguardRuleConfig =
|
|
20
|
+
| null
|
|
21
|
+
| RuleSeverity
|
|
22
|
+
| [RuleSeverity, RhythmguardRuleOptions];
|
|
23
|
+
|
|
24
|
+
export interface RhythmguardStylelintConfig {
|
|
25
|
+
customSyntax?: string;
|
|
26
|
+
extends?: string | string[];
|
|
27
|
+
ignoreFiles?: string | string[];
|
|
28
|
+
plugins?: string[];
|
|
29
|
+
rules?: Record<string, RhythmguardRuleConfig>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ScalePresetMetadata {
|
|
33
|
+
aliases?: string[];
|
|
34
|
+
base?: number;
|
|
35
|
+
description?: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
values?: number[];
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RhythmguardPresets {
|
|
42
|
+
communityScaleMetadata: Record<string, ScalePresetMetadata>;
|
|
43
|
+
getCommunityScaleMetadata(name: string): ScalePresetMetadata | undefined;
|
|
44
|
+
getScalePreset(name: string): readonly number[] | undefined;
|
|
45
|
+
listCommunityScalePresetNames(): string[];
|
|
46
|
+
listScalePresetNames(): string[];
|
|
47
|
+
scales: Record<string, readonly number[]>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface EslintRuleModule {
|
|
51
|
+
meta: Record<string, unknown>;
|
|
52
|
+
create(context: unknown): Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RhythmguardEslintPlugin {
|
|
56
|
+
configs: {
|
|
57
|
+
recommended: {
|
|
58
|
+
rules: Record<string, "off" | "warn" | "error" | number>;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
rules: {
|
|
62
|
+
"tailwind-class-use-motion-scale": EslintRuleModule;
|
|
63
|
+
"tailwind-class-use-scale": EslintRuleModule;
|
|
64
|
+
[ruleName: string]: EslintRuleModule;
|
|
65
|
+
};
|
|
66
|
+
}
|