ally-a11y 1.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/ACCESSIBILITY.md +205 -0
- package/LICENSE +21 -0
- package/README.md +940 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +528 -0
- package/dist/commands/audit-palette.d.ts +18 -0
- package/dist/commands/audit-palette.js +613 -0
- package/dist/commands/auto-pr.d.ts +19 -0
- package/dist/commands/auto-pr.js +434 -0
- package/dist/commands/badge.d.ts +11 -0
- package/dist/commands/badge.js +143 -0
- package/dist/commands/completion.d.ts +4 -0
- package/dist/commands/completion.js +185 -0
- package/dist/commands/crawl.d.ts +12 -0
- package/dist/commands/crawl.js +249 -0
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.js +233 -0
- package/dist/commands/explain.d.ts +12 -0
- package/dist/commands/explain.js +233 -0
- package/dist/commands/fix.d.ts +13 -0
- package/dist/commands/fix.js +668 -0
- package/dist/commands/health.d.ts +11 -0
- package/dist/commands/health.js +367 -0
- package/dist/commands/history.d.ts +10 -0
- package/dist/commands/history.js +191 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +164 -0
- package/dist/commands/learn.d.ts +8 -0
- package/dist/commands/learn.js +592 -0
- package/dist/commands/pr-check.d.ts +12 -0
- package/dist/commands/pr-check.js +270 -0
- package/dist/commands/report.d.ts +11 -0
- package/dist/commands/report.js +375 -0
- package/dist/commands/scan-storybook.d.ts +18 -0
- package/dist/commands/scan-storybook.js +402 -0
- package/dist/commands/scan.d.ts +25 -0
- package/dist/commands/scan.js +673 -0
- package/dist/commands/stats.d.ts +5 -0
- package/dist/commands/stats.js +137 -0
- package/dist/commands/tree.d.ts +12 -0
- package/dist/commands/tree.js +635 -0
- package/dist/commands/triage.d.ts +13 -0
- package/dist/commands/triage.js +327 -0
- package/dist/commands/watch.d.ts +17 -0
- package/dist/commands/watch.js +302 -0
- package/dist/types/index.d.ts +60 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/baseline.d.ts +62 -0
- package/dist/utils/baseline.js +169 -0
- package/dist/utils/browser.d.ts +78 -0
- package/dist/utils/browser.js +239 -0
- package/dist/utils/cache.d.ts +76 -0
- package/dist/utils/cache.js +178 -0
- package/dist/utils/config.d.ts +102 -0
- package/dist/utils/config.js +237 -0
- package/dist/utils/converters.d.ts +77 -0
- package/dist/utils/converters.js +200 -0
- package/dist/utils/copilot.d.ts +36 -0
- package/dist/utils/copilot.js +139 -0
- package/dist/utils/detect.d.ts +22 -0
- package/dist/utils/detect.js +197 -0
- package/dist/utils/enhanced-errors.d.ts +46 -0
- package/dist/utils/enhanced-errors.js +295 -0
- package/dist/utils/errors.d.ts +31 -0
- package/dist/utils/errors.js +149 -0
- package/dist/utils/fix-patterns.d.ts +56 -0
- package/dist/utils/fix-patterns.js +529 -0
- package/dist/utils/history-tracking.d.ts +94 -0
- package/dist/utils/history-tracking.js +230 -0
- package/dist/utils/history.d.ts +42 -0
- package/dist/utils/history.js +255 -0
- package/dist/utils/impact-scores.d.ts +44 -0
- package/dist/utils/impact-scores.js +257 -0
- package/dist/utils/retry.d.ts +24 -0
- package/dist/utils/retry.js +76 -0
- package/dist/utils/scanner.d.ts +74 -0
- package/dist/utils/scanner.js +606 -0
- package/dist/utils/scanner.test.d.ts +4 -0
- package/dist/utils/scanner.test.js +162 -0
- package/dist/utils/ui.d.ts +44 -0
- package/dist/utils/ui.js +276 -0
- package/mcp-server/dist/index.d.ts +8 -0
- package/mcp-server/dist/index.js +1923 -0
- package/package.json +88 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ally scan-storybook command - Scans Storybook stories for accessibility issues
|
|
3
|
+
*/
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { AccessibilityScanner, createReport, DEFAULT_STANDARD, } from '../utils/scanner.js';
|
|
9
|
+
import { printBanner, createSpinner, printSuccess, printError, printInfo, printWarning, printSummary, } from '../utils/ui.js';
|
|
10
|
+
import { withRetry } from '../utils/retry.js';
|
|
11
|
+
/**
|
|
12
|
+
* Fetch the Storybook stories index
|
|
13
|
+
* Tries Storybook 7+ format first, then falls back to Storybook 6
|
|
14
|
+
*/
|
|
15
|
+
async function fetchStoriesIndex(storybookUrl) {
|
|
16
|
+
const baseUrl = storybookUrl.replace(/\/$/, '');
|
|
17
|
+
// Try Storybook 7+ index.json first
|
|
18
|
+
try {
|
|
19
|
+
const indexResponse = await fetch(`${baseUrl}/index.json`);
|
|
20
|
+
if (indexResponse.ok) {
|
|
21
|
+
const index = await indexResponse.json();
|
|
22
|
+
const entries = Object.values(index.entries)
|
|
23
|
+
.filter(entry => entry.type === 'story'); // Filter out docs
|
|
24
|
+
return entries;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Fall through to try stories.json
|
|
29
|
+
}
|
|
30
|
+
// Try Storybook 6 stories.json
|
|
31
|
+
try {
|
|
32
|
+
const storiesResponse = await fetch(`${baseUrl}/stories.json`);
|
|
33
|
+
if (storiesResponse.ok) {
|
|
34
|
+
const storiesJson = await storiesResponse.json();
|
|
35
|
+
// Convert to common format
|
|
36
|
+
return Object.values(storiesJson.stories).map(story => ({
|
|
37
|
+
id: story.id,
|
|
38
|
+
title: story.title || story.kind,
|
|
39
|
+
name: story.name || story.story,
|
|
40
|
+
type: 'story',
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Fall through to error
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Could not fetch Storybook stories index from ${baseUrl}. ` +
|
|
48
|
+
`Tried /index.json (Storybook 7+) and /stories.json (Storybook 6). ` +
|
|
49
|
+
`Make sure Storybook is running and accessible.`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the iframe URL for a story
|
|
53
|
+
*/
|
|
54
|
+
function getStoryIframeUrl(storybookUrl, storyId) {
|
|
55
|
+
const baseUrl = storybookUrl.replace(/\/$/, '');
|
|
56
|
+
return `${baseUrl}/iframe.html?viewMode=story&id=${encodeURIComponent(storyId)}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Group stories by component title
|
|
60
|
+
*/
|
|
61
|
+
function groupStoriesByComponent(stories) {
|
|
62
|
+
const groups = new Map();
|
|
63
|
+
for (const story of stories) {
|
|
64
|
+
const existing = groups.get(story.title) || [];
|
|
65
|
+
existing.push(story);
|
|
66
|
+
groups.set(story.title, existing);
|
|
67
|
+
}
|
|
68
|
+
return groups;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Filter stories by pattern (matches against component title or story name)
|
|
72
|
+
*/
|
|
73
|
+
function filterStories(stories, pattern) {
|
|
74
|
+
const regex = new RegExp(pattern, 'i');
|
|
75
|
+
return stories.filter(story => regex.test(story.title) || regex.test(story.name) || regex.test(story.id));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Count errors and warnings from a scan result
|
|
79
|
+
*/
|
|
80
|
+
function countIssues(result) {
|
|
81
|
+
if (!result) {
|
|
82
|
+
return { errors: 0, warnings: 0 };
|
|
83
|
+
}
|
|
84
|
+
let errors = 0;
|
|
85
|
+
let warnings = 0;
|
|
86
|
+
for (const violation of result.violations) {
|
|
87
|
+
if (violation.impact === 'critical' || violation.impact === 'serious') {
|
|
88
|
+
errors++;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
warnings++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { errors, warnings };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get status icon for a story result
|
|
98
|
+
*/
|
|
99
|
+
function getStatusIcon(errors, warnings) {
|
|
100
|
+
if (errors > 0)
|
|
101
|
+
return chalk.red('X');
|
|
102
|
+
if (warnings > 0)
|
|
103
|
+
return chalk.yellow('!');
|
|
104
|
+
return chalk.green('v');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Format a single story result line
|
|
108
|
+
*/
|
|
109
|
+
function formatStoryLine(storyName, errors, warnings, isLast, issueDetails) {
|
|
110
|
+
const prefix = isLast ? '`-- ' : '|-- ';
|
|
111
|
+
const icon = getStatusIcon(errors, warnings);
|
|
112
|
+
let status;
|
|
113
|
+
if (errors > 0) {
|
|
114
|
+
const details = issueDetails ? ` (${issueDetails})` : '';
|
|
115
|
+
status = chalk.red(`${errors} error${errors !== 1 ? 's' : ''}${details}`);
|
|
116
|
+
}
|
|
117
|
+
else if (warnings > 0) {
|
|
118
|
+
const details = issueDetails ? ` (${issueDetails})` : '';
|
|
119
|
+
status = chalk.yellow(`${warnings} warning${warnings !== 1 ? 's' : ''}${details}`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
status = chalk.green('0 issues');
|
|
123
|
+
}
|
|
124
|
+
return ` ${prefix}${storyName}: ${icon} ${status}`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get the first issue ID from violations for display
|
|
128
|
+
*/
|
|
129
|
+
function getFirstIssueId(result) {
|
|
130
|
+
if (!result || result.violations.length === 0)
|
|
131
|
+
return undefined;
|
|
132
|
+
return result.violations[0].id;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Print tree-style component results
|
|
136
|
+
*/
|
|
137
|
+
function printComponentTree(componentResults) {
|
|
138
|
+
console.log(chalk.bold('\nComponent Results:'));
|
|
139
|
+
for (let i = 0; i < componentResults.length; i++) {
|
|
140
|
+
const component = componentResults[i];
|
|
141
|
+
const isLastComponent = i === componentResults.length - 1;
|
|
142
|
+
const componentPrefix = isLastComponent ? '`-- ' : '|-- ';
|
|
143
|
+
// Component header
|
|
144
|
+
console.log(chalk.cyan.bold(`${componentPrefix}${component.title}`));
|
|
145
|
+
// Stories
|
|
146
|
+
for (let j = 0; j < component.stories.length; j++) {
|
|
147
|
+
const story = component.stories[j];
|
|
148
|
+
const isLastStory = j === component.stories.length - 1;
|
|
149
|
+
const { errors, warnings } = countIssues(story.scanResult);
|
|
150
|
+
const issueId = getFirstIssueId(story.scanResult);
|
|
151
|
+
console.log(formatStoryLine(story.storyName, errors, warnings, isLastStory, issueId));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Print summary statistics
|
|
157
|
+
*/
|
|
158
|
+
function printStorybookSummary(componentResults, totalStories, passingStories, totalErrors, totalWarnings) {
|
|
159
|
+
const withIssues = totalStories - passingStories;
|
|
160
|
+
const passingPercent = totalStories > 0 ? Math.round((passingStories / totalStories) * 100) : 0;
|
|
161
|
+
const issuesPercent = totalStories > 0 ? Math.round((withIssues / totalStories) * 100) : 0;
|
|
162
|
+
console.log(chalk.bold('\nSummary:'));
|
|
163
|
+
console.log(` Components: ${componentResults.length}`);
|
|
164
|
+
console.log(` Stories: ${totalStories}`);
|
|
165
|
+
console.log(` Passing: ${chalk.green(passingStories.toString())} (${passingPercent}%)`);
|
|
166
|
+
console.log(` With issues: ${chalk[withIssues > 0 ? 'yellow' : 'green'](withIssues.toString())} (${issuesPercent}%)`);
|
|
167
|
+
console.log();
|
|
168
|
+
console.log(`Total: ${chalk.red(`${totalErrors} error${totalErrors !== 1 ? 's' : ''}`)}, ${chalk.yellow(`${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}`)}`);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Convert results to CSV format
|
|
172
|
+
*/
|
|
173
|
+
function convertToCsv(componentResults) {
|
|
174
|
+
const headers = ['component', 'story', 'story_id', 'errors', 'warnings', 'issues'];
|
|
175
|
+
const rows = [headers.join(',')];
|
|
176
|
+
for (const component of componentResults) {
|
|
177
|
+
for (const story of component.stories) {
|
|
178
|
+
const { errors, warnings } = countIssues(story.scanResult);
|
|
179
|
+
const issues = story.scanResult?.violations.map(v => v.id).join('; ') || '';
|
|
180
|
+
const row = [
|
|
181
|
+
escapeCsv(component.title),
|
|
182
|
+
escapeCsv(story.storyName),
|
|
183
|
+
escapeCsv(story.storyId),
|
|
184
|
+
errors.toString(),
|
|
185
|
+
warnings.toString(),
|
|
186
|
+
escapeCsv(issues),
|
|
187
|
+
];
|
|
188
|
+
rows.push(row.join(','));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return rows.join('\n');
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Escape a value for CSV output
|
|
195
|
+
*/
|
|
196
|
+
function escapeCsv(value) {
|
|
197
|
+
if (value.includes(',') || value.includes('"') || value.includes('\n') || value.includes('\r')) {
|
|
198
|
+
return '"' + value.replace(/"/g, '""') + '"';
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Main scan-storybook command
|
|
204
|
+
*/
|
|
205
|
+
export async function scanStorybookCommand(options = {}) {
|
|
206
|
+
printBanner();
|
|
207
|
+
const { url = 'http://localhost:6006', timeout = 10000, filter, format = 'default', output = '.ally', standard = DEFAULT_STANDARD, } = options;
|
|
208
|
+
console.log(chalk.cyan(`Scanning Storybook at ${url}...`));
|
|
209
|
+
console.log();
|
|
210
|
+
// Fetch stories index
|
|
211
|
+
const fetchSpinner = createSpinner('Fetching stories index...');
|
|
212
|
+
fetchSpinner.start();
|
|
213
|
+
let stories;
|
|
214
|
+
try {
|
|
215
|
+
stories = await fetchStoriesIndex(url);
|
|
216
|
+
fetchSpinner.succeed(`Found ${stories.length} stories`);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
fetchSpinner.fail('Failed to fetch stories index');
|
|
220
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
// Apply filter if provided
|
|
224
|
+
if (filter) {
|
|
225
|
+
const originalCount = stories.length;
|
|
226
|
+
stories = filterStories(stories, filter);
|
|
227
|
+
printInfo(`Filtered to ${stories.length} stories matching "${filter}" (from ${originalCount})`);
|
|
228
|
+
}
|
|
229
|
+
if (stories.length === 0) {
|
|
230
|
+
printWarning('No stories found to scan');
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
// Group by component
|
|
234
|
+
const groupedStories = groupStoriesByComponent(stories);
|
|
235
|
+
// Initialize scanner
|
|
236
|
+
const scanner = new AccessibilityScanner(timeout);
|
|
237
|
+
const componentResults = [];
|
|
238
|
+
const allScanResults = [];
|
|
239
|
+
try {
|
|
240
|
+
await scanner.init();
|
|
241
|
+
let scannedCount = 0;
|
|
242
|
+
const totalStories = stories.length;
|
|
243
|
+
// Scan each component's stories
|
|
244
|
+
for (const [componentTitle, componentStories] of groupedStories) {
|
|
245
|
+
const storyResults = [];
|
|
246
|
+
for (const story of componentStories) {
|
|
247
|
+
scannedCount++;
|
|
248
|
+
const storyUrl = getStoryIframeUrl(url, story.id);
|
|
249
|
+
const scanSpinner = createSpinner(`[${scannedCount}/${totalStories}] Scanning ${componentTitle}/${story.name}...`);
|
|
250
|
+
scanSpinner.start();
|
|
251
|
+
try {
|
|
252
|
+
// Scan with retry for transient errors
|
|
253
|
+
const result = await withRetry(() => scanner.scanUrl(storyUrl, standard), {
|
|
254
|
+
maxRetries: 2,
|
|
255
|
+
baseDelayMs: 500,
|
|
256
|
+
onRetry: (attempt, error, delayMs) => {
|
|
257
|
+
scanSpinner.stop();
|
|
258
|
+
printWarning(`Retry ${attempt}/2 for ${story.name} after ${delayMs / 1000}s (${error.message})`);
|
|
259
|
+
scanSpinner.start();
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
// Tag the result with story metadata
|
|
263
|
+
result.url = storyUrl;
|
|
264
|
+
result.file = `${componentTitle}/${story.name}`;
|
|
265
|
+
storyResults.push({
|
|
266
|
+
storyId: story.id,
|
|
267
|
+
storyName: story.name,
|
|
268
|
+
componentTitle: componentTitle,
|
|
269
|
+
scanResult: result,
|
|
270
|
+
});
|
|
271
|
+
allScanResults.push(result);
|
|
272
|
+
const { errors, warnings } = countIssues(result);
|
|
273
|
+
if (errors > 0) {
|
|
274
|
+
scanSpinner.fail(`${componentTitle}/${story.name}: ${errors} errors, ${warnings} warnings`);
|
|
275
|
+
}
|
|
276
|
+
else if (warnings > 0) {
|
|
277
|
+
scanSpinner.warn(`${componentTitle}/${story.name}: ${warnings} warnings`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
scanSpinner.succeed(`${componentTitle}/${story.name}: No issues`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
scanSpinner.fail(`${componentTitle}/${story.name}: Failed to scan`);
|
|
285
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
286
|
+
printError(` ${errorMessage}`);
|
|
287
|
+
storyResults.push({
|
|
288
|
+
storyId: story.id,
|
|
289
|
+
storyName: story.name,
|
|
290
|
+
componentTitle: componentTitle,
|
|
291
|
+
scanResult: null,
|
|
292
|
+
error: errorMessage,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Calculate component totals
|
|
297
|
+
let componentErrors = 0;
|
|
298
|
+
let componentWarnings = 0;
|
|
299
|
+
for (const storyResult of storyResults) {
|
|
300
|
+
const { errors, warnings } = countIssues(storyResult.scanResult);
|
|
301
|
+
componentErrors += errors;
|
|
302
|
+
componentWarnings += warnings;
|
|
303
|
+
}
|
|
304
|
+
componentResults.push({
|
|
305
|
+
title: componentTitle,
|
|
306
|
+
stories: storyResults,
|
|
307
|
+
totalErrors: componentErrors,
|
|
308
|
+
totalWarnings: componentWarnings,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// Calculate overall totals
|
|
312
|
+
let totalErrors = 0;
|
|
313
|
+
let totalWarnings = 0;
|
|
314
|
+
let passingStories = 0;
|
|
315
|
+
for (const component of componentResults) {
|
|
316
|
+
totalErrors += component.totalErrors;
|
|
317
|
+
totalWarnings += component.totalWarnings;
|
|
318
|
+
for (const story of component.stories) {
|
|
319
|
+
const { errors, warnings } = countIssues(story.scanResult);
|
|
320
|
+
if (errors === 0 && warnings === 0 && story.scanResult) {
|
|
321
|
+
passingStories++;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Output results based on format
|
|
326
|
+
if (format === 'json') {
|
|
327
|
+
const report = createReport(allScanResults);
|
|
328
|
+
console.log(JSON.stringify(report, null, 2));
|
|
329
|
+
return report;
|
|
330
|
+
}
|
|
331
|
+
else if (format === 'csv') {
|
|
332
|
+
const csvOutput = convertToCsv(componentResults);
|
|
333
|
+
console.log(csvOutput);
|
|
334
|
+
// Still create and return the report
|
|
335
|
+
const report = createReport(allScanResults);
|
|
336
|
+
return report;
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
// Default tree format
|
|
340
|
+
printComponentTree(componentResults);
|
|
341
|
+
printStorybookSummary(componentResults, stories.length, passingStories, totalErrors, totalWarnings);
|
|
342
|
+
// Create and save the report
|
|
343
|
+
const report = createReport(allScanResults);
|
|
344
|
+
// Print the standard summary box
|
|
345
|
+
if (allScanResults.length > 0) {
|
|
346
|
+
printSummary(report.summary);
|
|
347
|
+
}
|
|
348
|
+
// Save report
|
|
349
|
+
await saveReport(report, output, componentResults);
|
|
350
|
+
return report;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
printError(`Scan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
await scanner.close();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Save the scan report to disk
|
|
363
|
+
*/
|
|
364
|
+
async function saveReport(report, outputDir, componentResults) {
|
|
365
|
+
try {
|
|
366
|
+
// Ensure output directory exists
|
|
367
|
+
if (!existsSync(outputDir)) {
|
|
368
|
+
await mkdir(outputDir, { recursive: true });
|
|
369
|
+
}
|
|
370
|
+
// Save standard JSON report
|
|
371
|
+
const reportPath = resolve(outputDir, 'storybook-scan.json');
|
|
372
|
+
await writeFile(reportPath, JSON.stringify(report, null, 2));
|
|
373
|
+
printSuccess(`Report saved to ${reportPath}`);
|
|
374
|
+
// Also save as scan.json for compatibility with other commands
|
|
375
|
+
const defaultPath = resolve(outputDir, 'scan.json');
|
|
376
|
+
await writeFile(defaultPath, JSON.stringify(report, null, 2));
|
|
377
|
+
printInfo(`Also saved to ${defaultPath} for use with other ally commands`);
|
|
378
|
+
// Save component summary
|
|
379
|
+
const summaryPath = resolve(outputDir, 'storybook-summary.json');
|
|
380
|
+
const summary = {
|
|
381
|
+
scanDate: report.scanDate,
|
|
382
|
+
components: componentResults.map(c => ({
|
|
383
|
+
title: c.title,
|
|
384
|
+
stories: c.stories.map(s => ({
|
|
385
|
+
id: s.storyId,
|
|
386
|
+
name: s.storyName,
|
|
387
|
+
errors: countIssues(s.scanResult).errors,
|
|
388
|
+
warnings: countIssues(s.scanResult).warnings,
|
|
389
|
+
issues: s.scanResult?.violations.map(v => v.id) || [],
|
|
390
|
+
})),
|
|
391
|
+
totalErrors: c.totalErrors,
|
|
392
|
+
totalWarnings: c.totalWarnings,
|
|
393
|
+
})),
|
|
394
|
+
};
|
|
395
|
+
await writeFile(summaryPath, JSON.stringify(summary, null, 2));
|
|
396
|
+
printInfo(`Component summary saved to ${summaryPath}`);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
printError(`Failed to save report: ${error instanceof Error ? error.message : String(error)}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
export default scanStorybookCommand;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ally scan command - Scans files for accessibility violations
|
|
3
|
+
*/
|
|
4
|
+
import { type ColorBlindnessType, type WcagStandard, type BrowserType } from '../utils/scanner.js';
|
|
5
|
+
import type { AllyReport } from '../types/index.js';
|
|
6
|
+
type OutputFormat = 'json' | 'sarif' | 'junit' | 'csv';
|
|
7
|
+
interface ScanCommandOptions {
|
|
8
|
+
output?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
verbose?: boolean;
|
|
12
|
+
format?: OutputFormat;
|
|
13
|
+
simulate?: ColorBlindnessType;
|
|
14
|
+
standard?: WcagStandard;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
noCache?: boolean;
|
|
17
|
+
ci?: boolean;
|
|
18
|
+
browser?: BrowserType;
|
|
19
|
+
maxFiles?: number;
|
|
20
|
+
baseline?: boolean;
|
|
21
|
+
compareBaseline?: boolean;
|
|
22
|
+
failOnRegression?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function scanCommand(targetPath?: string, options?: ScanCommandOptions): Promise<AllyReport | null>;
|
|
25
|
+
export default scanCommand;
|