@govtechsg/oobee 0.10.89 → 0.10.91
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/.github/workflows/bump-package-version.yml +10 -1
- package/.github/workflows/docker-push-ghcr.yml +5 -1
- package/.github/workflows/publish.yml +8 -3
- package/README.md +2 -0
- package/bf04540e-0894-4d00-98ec-c1be74c6f199.txt +0 -0
- package/dist/mergeAxeResults/itemsStore.js +51 -0
- package/dist/mergeAxeResults/jsonArtifacts.js +122 -132
- package/dist/mergeAxeResults/scanPages.js +2 -2
- package/dist/mergeAxeResults/writeCsv.js +115 -96
- package/dist/mergeAxeResults.js +94 -41
- package/oobee-client-scanner.js +6914 -6754
- package/package.json +1 -1
- package/src/mergeAxeResults/itemsStore.ts +73 -0
- package/src/mergeAxeResults/jsonArtifacts.ts +135 -138
- package/src/mergeAxeResults/scanPages.ts +2 -2
- package/src/mergeAxeResults/writeCsv.ts +129 -100
- package/src/mergeAxeResults.ts +102 -49
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
import { createWriteStream } from 'fs';
|
|
2
|
-
import { AsyncParser, ParserOptions } from '@json2csv/node';
|
|
3
2
|
import { a11yRuleShortDescriptionMap } from '../constants/constants.js';
|
|
4
3
|
import type { AllIssues, RuleInfo } from './types.js';
|
|
4
|
+
import type { ItemsStore } from './itemsStore.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function escapeCsvField(value: string): string {
|
|
7
|
+
if (value == null) return '';
|
|
8
|
+
const str = String(value);
|
|
9
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const writeCsv = async (
|
|
13
|
+
allIssues: AllIssues,
|
|
14
|
+
storagePath: string,
|
|
15
|
+
itemsStore?: ItemsStore,
|
|
16
|
+
): Promise<void> => {
|
|
7
17
|
const csvOutput = createWriteStream(`${storagePath}/report.csv`, { encoding: 'utf8' });
|
|
18
|
+
|
|
8
19
|
const formatPageViolation = (pageNum: number) => {
|
|
9
20
|
if (pageNum < 0) return 'Document';
|
|
10
21
|
return `Page ${pageNum}`;
|
|
11
22
|
};
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
const fields = [
|
|
25
|
+
'customFlowLabel',
|
|
26
|
+
'deviceChosen',
|
|
27
|
+
'scanCompletedAt',
|
|
28
|
+
'severity',
|
|
29
|
+
'issueId',
|
|
30
|
+
'issueDescription',
|
|
31
|
+
'wcagConformance',
|
|
32
|
+
'url',
|
|
33
|
+
'pageTitle',
|
|
34
|
+
'context',
|
|
35
|
+
'howToFix',
|
|
36
|
+
'axeImpact',
|
|
37
|
+
'xpath',
|
|
38
|
+
'learnMore',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
csvOutput.write(fields.map(escapeCsvField).join(',') + '\n');
|
|
42
|
+
|
|
43
|
+
const getRulesByCategory = (issues: AllIssues): [string, RuleInfo][] => {
|
|
16
44
|
return Object.entries(issues.items)
|
|
17
45
|
.filter(([category]) => category !== 'passed')
|
|
18
46
|
.reduce((prev: [string, RuleInfo][], [category, value]) => {
|
|
@@ -23,15 +51,14 @@ const writeCsv = async (allIssues: AllIssues, storagePath: string): Promise<void
|
|
|
23
51
|
return prev;
|
|
24
52
|
}, [])
|
|
25
53
|
.sort((a, b) => {
|
|
26
|
-
// sort rules according to severity, then ruleId
|
|
27
54
|
const compareCategory = -a[0].localeCompare(b[0]);
|
|
28
55
|
return compareCategory === 0 ? a[1].rule.localeCompare(b[1].rule) : compareCategory;
|
|
29
56
|
});
|
|
30
57
|
};
|
|
31
58
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
const rulesByCategory = getRulesByCategory(allIssues);
|
|
60
|
+
|
|
61
|
+
for (const [severity, rule] of rulesByCategory) {
|
|
35
62
|
const {
|
|
36
63
|
rule: issueId,
|
|
37
64
|
description: issueDescription,
|
|
@@ -41,104 +68,106 @@ const writeCsv = async (allIssues: AllIssues, storagePath: string): Promise<void
|
|
|
41
68
|
helpUrl: learnMore,
|
|
42
69
|
} = rule;
|
|
43
70
|
|
|
44
|
-
// format clauses as a string
|
|
45
71
|
const wcagConformance = conformance.join(',');
|
|
46
72
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const { url, items } = affectedPage;
|
|
51
|
-
items.forEach(item => {
|
|
52
|
-
const { html, message, xpath } = item;
|
|
53
|
-
const page = (item as any).page;
|
|
54
|
-
const howToFix = message.replace(/(\r\n|\n|\r)/g, '\\n'); // preserve newlines as \n
|
|
55
|
-
const violation = html || formatPageViolation(page); // page is a number, not a string
|
|
56
|
-
const context = violation.replace(/(\r\n|\n|\r)/g, ''); // remove newlines
|
|
57
|
-
|
|
58
|
-
results.push({
|
|
59
|
-
customFlowLabel: allIssues.customFlowLabel || '',
|
|
60
|
-
deviceChosen: allIssues.deviceChosen || '',
|
|
61
|
-
scanCompletedAt: allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
62
|
-
severity: severity || '',
|
|
63
|
-
issueId: issueId || '',
|
|
64
|
-
issueDescription: a11yRuleShortDescriptionMap[issueId] || issueDescription || '',
|
|
65
|
-
wcagConformance: wcagConformance || '',
|
|
66
|
-
url: url || '',
|
|
67
|
-
pageTitle: affectedPage.pageTitle || 'No page title',
|
|
68
|
-
context: context || '',
|
|
69
|
-
howToFix: howToFix || '',
|
|
70
|
-
axeImpact: axeImpact || '',
|
|
71
|
-
xpath: xpath || '',
|
|
72
|
-
learnMore: learnMore || '',
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
if (results.length === 0) return {};
|
|
77
|
-
return results;
|
|
78
|
-
};
|
|
73
|
+
if (itemsStore) {
|
|
74
|
+
const itemsMap = await itemsStore.readRuleItemsMap(severity, issueId);
|
|
75
|
+
const sortedPages = [...pagesAffected].sort((a, b) => (a.url || '').localeCompare(b.url || ''));
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
'deviceChosen',
|
|
85
|
-
'scanCompletedAt',
|
|
86
|
-
'severity',
|
|
87
|
-
'issueId',
|
|
88
|
-
'issueDescription',
|
|
89
|
-
'wcagConformance',
|
|
90
|
-
'url',
|
|
91
|
-
'pageTitle',
|
|
92
|
-
'context',
|
|
93
|
-
'howToFix',
|
|
94
|
-
'axeImpact',
|
|
95
|
-
'xpath',
|
|
96
|
-
'learnMore',
|
|
97
|
-
],
|
|
98
|
-
includeEmptyRows: true,
|
|
99
|
-
};
|
|
77
|
+
for (const affectedPage of sortedPages) {
|
|
78
|
+
const key = affectedPage.pageIndex != null ? String(affectedPage.pageIndex) : affectedPage.url;
|
|
79
|
+
const entry = itemsMap.get(key);
|
|
80
|
+
if (!entry) continue;
|
|
100
81
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
82
|
+
for (const item of entry.items) {
|
|
83
|
+
const { html, message, xpath } = item;
|
|
84
|
+
const page = (item as any).page;
|
|
85
|
+
const howToFix = (message || '').replace(/(\r\n|\n|\r)/g, '\\n');
|
|
86
|
+
const violation = html || formatPageViolation(page);
|
|
87
|
+
const context = violation.replace(/(\r\n|\n|\r)/g, '');
|
|
88
|
+
|
|
89
|
+
const row = [
|
|
90
|
+
allIssues.customFlowLabel || '',
|
|
91
|
+
allIssues.deviceChosen || '',
|
|
92
|
+
allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
93
|
+
severity || '',
|
|
94
|
+
issueId || '',
|
|
95
|
+
a11yRuleShortDescriptionMap[issueId] || issueDescription || '',
|
|
96
|
+
wcagConformance || '',
|
|
97
|
+
affectedPage.url || '',
|
|
98
|
+
affectedPage.pageTitle || 'No page title',
|
|
99
|
+
context || '',
|
|
100
|
+
howToFix || '',
|
|
101
|
+
axeImpact || '',
|
|
102
|
+
xpath || '',
|
|
103
|
+
learnMore || '',
|
|
104
|
+
].map(escapeCsvField);
|
|
105
|
+
|
|
106
|
+
csvOutput.write(row.join(',') + '\n');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
const sortedPages = [...pagesAffected].sort((a, b) => (a.url || '').localeCompare(b.url || ''));
|
|
111
|
+
|
|
112
|
+
for (const affectedPage of sortedPages) {
|
|
113
|
+
const items = (affectedPage as any).items || [];
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
const { html, message, xpath } = item;
|
|
116
|
+
const page = (item as any).page;
|
|
117
|
+
const howToFix = (message || '').replace(/(\r\n|\n|\r)/g, '\\n');
|
|
118
|
+
const violation = html || formatPageViolation(page);
|
|
119
|
+
const context = violation.replace(/(\r\n|\n|\r)/g, '');
|
|
120
|
+
|
|
121
|
+
const row = [
|
|
122
|
+
allIssues.customFlowLabel || '',
|
|
123
|
+
allIssues.deviceChosen || '',
|
|
124
|
+
allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
125
|
+
severity || '',
|
|
126
|
+
issueId || '',
|
|
127
|
+
a11yRuleShortDescriptionMap[issueId] || issueDescription || '',
|
|
128
|
+
wcagConformance || '',
|
|
129
|
+
affectedPage.url || '',
|
|
130
|
+
affectedPage.pageTitle || 'No page title',
|
|
131
|
+
context || '',
|
|
132
|
+
howToFix || '',
|
|
133
|
+
axeImpact || '',
|
|
134
|
+
xpath || '',
|
|
135
|
+
learnMore || '',
|
|
136
|
+
].map(escapeCsvField);
|
|
137
|
+
|
|
138
|
+
csvOutput.write(row.join(',') + '\n');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
133
141
|
}
|
|
142
|
+
}
|
|
134
143
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
if (allIssues.pagesNotScanned && allIssues.pagesNotScanned.length > 0) {
|
|
145
|
+
allIssues.pagesNotScanned.forEach(page => {
|
|
146
|
+
const row = [
|
|
147
|
+
allIssues.customFlowLabel || '',
|
|
148
|
+
allIssues.deviceChosen || '',
|
|
149
|
+
allIssues.endTime ? allIssues.endTime.toISOString() : '',
|
|
150
|
+
'error',
|
|
151
|
+
'error-pages-skipped',
|
|
152
|
+
page.metadata ? page.metadata : 'An unknown error caused the page to be skipped',
|
|
153
|
+
'',
|
|
154
|
+
(page as any).url || page || '',
|
|
155
|
+
'Error',
|
|
156
|
+
'',
|
|
157
|
+
'',
|
|
158
|
+
'',
|
|
159
|
+
'',
|
|
160
|
+
'',
|
|
161
|
+
].map(escapeCsvField);
|
|
162
|
+
|
|
163
|
+
csvOutput.write(row.join(',') + '\n');
|
|
164
|
+
});
|
|
165
|
+
}
|
|
138
166
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
csvOutput.
|
|
167
|
+
csvOutput.end();
|
|
168
|
+
await new Promise<void>((resolve, reject) => {
|
|
169
|
+
csvOutput.on('finish', resolve);
|
|
170
|
+
csvOutput.on('error', reject);
|
|
142
171
|
});
|
|
143
172
|
};
|
|
144
173
|
|
package/src/mergeAxeResults.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { consoleLogger } from './logs.js';
|
|
|
31
31
|
import itemTypeDescription from './constants/itemTypeDescription.js';
|
|
32
32
|
import { oobeeAiHtmlETL, oobeeAiRules } from './constants/oobeeAi.js';
|
|
33
33
|
import { buildHtmlGroups, convertItemsToReferences } from './mergeAxeResults/itemReferences.js';
|
|
34
|
+
import { ItemsStore } from './mergeAxeResults/itemsStore.js';
|
|
34
35
|
import {
|
|
35
36
|
compressJsonFileStreaming,
|
|
36
37
|
writeJsonAndBase64Files,
|
|
@@ -418,7 +419,7 @@ const writeSummaryPdf = async (
|
|
|
418
419
|
// Tracking WCAG occurrences
|
|
419
420
|
const wcagOccurrencesMap = new Map<string, number>();
|
|
420
421
|
|
|
421
|
-
const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
422
|
+
const pushResults = async (pageResults, allIssues, isCustomFlow, itemsStore: ItemsStore) => {
|
|
422
423
|
const { url, pageTitle, filePath } = pageResults;
|
|
423
424
|
|
|
424
425
|
const totalIssuesInPage = new Set();
|
|
@@ -433,15 +434,15 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
433
434
|
totalOccurrences: 0,
|
|
434
435
|
});
|
|
435
436
|
|
|
436
|
-
['mustFix', 'goodToFix', 'needsReview', 'passed']
|
|
437
|
-
if (!pageResults[category])
|
|
437
|
+
for (const category of ['mustFix', 'goodToFix', 'needsReview', 'passed'] as const) {
|
|
438
|
+
if (!pageResults[category]) continue;
|
|
438
439
|
|
|
439
440
|
const { totalItems, rules } = pageResults[category];
|
|
440
441
|
const currCategoryFromAllIssues = allIssues.items[category];
|
|
441
442
|
|
|
442
443
|
currCategoryFromAllIssues.totalItems += totalItems;
|
|
443
444
|
|
|
444
|
-
Object.keys(rules)
|
|
445
|
+
for (const rule of Object.keys(rules)) {
|
|
445
446
|
const {
|
|
446
447
|
description,
|
|
447
448
|
axeImpact,
|
|
@@ -457,7 +458,6 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
457
458
|
helpUrl,
|
|
458
459
|
conformance,
|
|
459
460
|
totalItems: 0,
|
|
460
|
-
// numberOfPagesAffectedAfterRedirects: 0,
|
|
461
461
|
pagesAffected: {},
|
|
462
462
|
};
|
|
463
463
|
}
|
|
@@ -470,7 +470,6 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
470
470
|
allIssues.wcagViolations.push(c);
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
// Track WCAG criteria occurrences for Sentry
|
|
474
473
|
const currentCount = wcagOccurrencesMap.get(c) || 0;
|
|
475
474
|
wcagOccurrencesMap.set(c, currentCount + count);
|
|
476
475
|
});
|
|
@@ -480,9 +479,6 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
480
479
|
|
|
481
480
|
currRuleFromAllIssues.totalItems += count;
|
|
482
481
|
|
|
483
|
-
// Build htmlGroups for pre-computed Group by HTML Element
|
|
484
|
-
buildHtmlGroups(currRuleFromAllIssues, items, url);
|
|
485
|
-
|
|
486
482
|
if (isCustomFlow) {
|
|
487
483
|
const { pageIndex, pageImagePath, metadata } = pageResults;
|
|
488
484
|
currRuleFromAllIssues.pagesAffected[pageIndex] = {
|
|
@@ -490,17 +486,31 @@ const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
|
490
486
|
pageTitle,
|
|
491
487
|
pageImagePath,
|
|
492
488
|
metadata,
|
|
493
|
-
|
|
489
|
+
itemsCount: items.length,
|
|
494
490
|
};
|
|
491
|
+
await itemsStore.appendPageItems(category, rule, {
|
|
492
|
+
url,
|
|
493
|
+
pageTitle,
|
|
494
|
+
items,
|
|
495
|
+
pageIndex,
|
|
496
|
+
pageImagePath,
|
|
497
|
+
metadata,
|
|
498
|
+
});
|
|
495
499
|
} else if (!(url in currRuleFromAllIssues.pagesAffected)) {
|
|
496
500
|
currRuleFromAllIssues.pagesAffected[url] = {
|
|
497
501
|
pageTitle,
|
|
498
|
-
|
|
502
|
+
itemsCount: items.length,
|
|
499
503
|
...(filePath && { filePath }),
|
|
500
504
|
};
|
|
505
|
+
await itemsStore.appendPageItems(category, rule, {
|
|
506
|
+
url,
|
|
507
|
+
pageTitle,
|
|
508
|
+
items,
|
|
509
|
+
...(filePath && { filePath }),
|
|
510
|
+
});
|
|
501
511
|
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
504
514
|
};
|
|
505
515
|
|
|
506
516
|
const getTopTenIssues = allIssues => {
|
|
@@ -561,26 +571,26 @@ const flattenAndSortResults = (allIssues: AllIssues, isCustomFlow: boolean) => {
|
|
|
561
571
|
.map(pageEntry => {
|
|
562
572
|
if (isCustomFlow) {
|
|
563
573
|
const [pageIndex, pageInfo] = pageEntry as unknown as [number, PageInfo];
|
|
564
|
-
// Only update the occurrences map if not passed.
|
|
565
574
|
if (category !== 'passed') {
|
|
566
575
|
urlOccurrencesMap.set(
|
|
567
576
|
pageInfo.url!,
|
|
568
|
-
(urlOccurrencesMap.get(pageInfo.url!) || 0) + pageInfo.
|
|
577
|
+
(urlOccurrencesMap.get(pageInfo.url!) || 0) + (pageInfo.itemsCount || 0),
|
|
569
578
|
);
|
|
570
579
|
}
|
|
571
580
|
return { pageIndex, ...pageInfo };
|
|
572
581
|
}
|
|
573
582
|
const [url, pageInfo] = pageEntry as unknown as [string, PageInfo];
|
|
574
583
|
if (category !== 'passed') {
|
|
575
|
-
urlOccurrencesMap.set(
|
|
584
|
+
urlOccurrencesMap.set(
|
|
585
|
+
url,
|
|
586
|
+
(urlOccurrencesMap.get(url) || 0) + (pageInfo.itemsCount || 0),
|
|
587
|
+
);
|
|
576
588
|
}
|
|
577
589
|
return { url, ...pageInfo };
|
|
578
590
|
})
|
|
579
|
-
|
|
580
|
-
.sort((page1, page2) => page2.items.length - page1.items.length);
|
|
591
|
+
.sort((page1, page2) => (page2.itemsCount || 0) - (page1.itemsCount || 0));
|
|
581
592
|
return { rule, ...ruleInfo };
|
|
582
593
|
})
|
|
583
|
-
// Sort the rules by totalItems (descending)
|
|
584
594
|
.sort((rule1, rule2) => rule2.totalItems - rule1.totalItems);
|
|
585
595
|
});
|
|
586
596
|
|
|
@@ -631,19 +641,24 @@ const extractRuleAiData = (
|
|
|
631
641
|
};
|
|
632
642
|
|
|
633
643
|
// This is for telemetry purposes called within mergeAxeResults.ts
|
|
634
|
-
export const createRuleIdJson = allIssues => {
|
|
644
|
+
export const createRuleIdJson = async (allIssues, itemsStore?: ItemsStore) => {
|
|
635
645
|
const compiledRuleJson = {};
|
|
636
646
|
|
|
637
|
-
['mustFix', 'goodToFix', 'needsReview']
|
|
638
|
-
allIssues.items[category].rules
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
+
for (const category of ['mustFix', 'goodToFix', 'needsReview'] as const) {
|
|
648
|
+
for (const rule of allIssues.items[category].rules) {
|
|
649
|
+
let allItems: any[] = [];
|
|
650
|
+
|
|
651
|
+
if (itemsStore) {
|
|
652
|
+
for await (const entry of itemsStore.readRuleItems(category, rule.rule)) {
|
|
653
|
+
allItems.push(...entry.items);
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
allItems = rule.pagesAffected.flatMap(page => page.items || []);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
compiledRuleJson[rule.rule] = extractRuleAiData(rule.rule, rule.totalItems, allItems);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
647
662
|
|
|
648
663
|
return compiledRuleJson;
|
|
649
664
|
};
|
|
@@ -815,20 +830,20 @@ const generateArtifacts = async (
|
|
|
815
830
|
};
|
|
816
831
|
|
|
817
832
|
const allFiles = await extractFileNames(intermediateDatasetsPath);
|
|
833
|
+
const itemsStore = new ItemsStore(storagePath);
|
|
818
834
|
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
});
|
|
835
|
+
for (const file of allFiles) {
|
|
836
|
+
try {
|
|
837
|
+
const pageResults = await parseContentToJson(`${intermediateDatasetsPath}/${file}`);
|
|
838
|
+
if (pageResults) {
|
|
839
|
+
await pushResults(pageResults, allIssues, isCustomFlow, itemsStore);
|
|
840
|
+
}
|
|
841
|
+
} catch (flattenIssuesError: any) {
|
|
842
|
+
consoleLogger.error(
|
|
843
|
+
`[generateArtifacts] Error processing ${file}: ${flattenIssuesError?.stack || flattenIssuesError}`,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
832
847
|
|
|
833
848
|
flattenAndSortResults(allIssues, isCustomFlow);
|
|
834
849
|
|
|
@@ -864,6 +879,15 @@ const generateArtifacts = async (
|
|
|
864
879
|
|
|
865
880
|
populateScanPagesDetail(allIssues);
|
|
866
881
|
|
|
882
|
+
// Build htmlGroups in a second pass from disk-backed items
|
|
883
|
+
for (const category of ['mustFix', 'goodToFix', 'needsReview', 'passed'] as const) {
|
|
884
|
+
for (const rule of allIssues.items[category].rules) {
|
|
885
|
+
for await (const entry of itemsStore.readRuleItems(category, rule.rule)) {
|
|
886
|
+
buildHtmlGroups(rule, entry.items, entry.url);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
867
891
|
allIssues.wcagPassPercentage = getWcagPassPercentage(
|
|
868
892
|
allIssues.wcagViolations,
|
|
869
893
|
allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa,
|
|
@@ -928,7 +952,7 @@ const generateArtifacts = async (
|
|
|
928
952
|
rest.minor = axeImpactCount.minor;
|
|
929
953
|
}
|
|
930
954
|
|
|
931
|
-
await writeCsv(allIssues, storagePath);
|
|
955
|
+
await writeCsv(allIssues, storagePath, itemsStore);
|
|
932
956
|
await writeSitemap(pagesScanned, storagePath);
|
|
933
957
|
const {
|
|
934
958
|
scanDataJsonFilePath,
|
|
@@ -945,7 +969,7 @@ const generateArtifacts = async (
|
|
|
945
969
|
scanPagesSummaryBase64FilePath,
|
|
946
970
|
scanDataJsonFileSize,
|
|
947
971
|
scanItemsJsonFileSize,
|
|
948
|
-
} = await writeJsonAndBase64Files(allIssues, storagePath);
|
|
972
|
+
} = await writeJsonAndBase64Files(allIssues, storagePath, itemsStore);
|
|
949
973
|
// Removed BIG_RESULTS_THRESHOLD check - always use full scanItems
|
|
950
974
|
|
|
951
975
|
await writeScanDetailsCsv(
|
|
@@ -996,13 +1020,36 @@ const generateArtifacts = async (
|
|
|
996
1020
|
1,
|
|
997
1021
|
);
|
|
998
1022
|
|
|
1023
|
+
// Suppress uncaught EPERM errors from lingering Crawlee async lock-file operations
|
|
1024
|
+
// (Windows holds mandatory file locks; Crawlee may still attempt mkdir on .json.lock
|
|
1025
|
+
// files after the crawl has finished). Without this, Node crashes with uncaughtException.
|
|
1026
|
+
const crawleeEpermHandler = (err: Error & { code?: string }) => {
|
|
1027
|
+
if (err.code === 'EPERM' && err.message?.includes('crawlee')) {
|
|
1028
|
+
consoleLogger.info(`Suppressed lingering Crawlee storage error: ${err.message}`);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
// Re-throw non-crawlee EPERM errors so they aren't silently swallowed
|
|
1032
|
+
throw err;
|
|
1033
|
+
};
|
|
1034
|
+
process.on('uncaughtException', crawleeEpermHandler);
|
|
1035
|
+
process.on('unhandledRejection', crawleeEpermHandler);
|
|
1036
|
+
|
|
999
1037
|
// Brief delay to allow lingering async crawlee storage operations to flush
|
|
1000
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1038
|
+
await new Promise(resolve => setTimeout(resolve, process.platform === 'win32' ? 5000 : 3000));
|
|
1001
1039
|
|
|
1040
|
+
const crawleePath = path.join(storagePath, 'crawlee');
|
|
1002
1041
|
try {
|
|
1003
|
-
await fs.promises.rm(
|
|
1042
|
+
await fs.promises.rm(crawleePath, { recursive: true, force: true });
|
|
1004
1043
|
} catch (error) {
|
|
1005
|
-
//
|
|
1044
|
+
// On Windows, retry once after a delay if the folder is still locked
|
|
1045
|
+
if (process.platform === 'win32') {
|
|
1046
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1047
|
+
try {
|
|
1048
|
+
await fs.promises.rm(crawleePath, { recursive: true, force: true });
|
|
1049
|
+
} catch {
|
|
1050
|
+
// Best-effort cleanup — leave the folder; report generation continues
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1006
1053
|
}
|
|
1007
1054
|
|
|
1008
1055
|
try {
|
|
@@ -1058,7 +1105,10 @@ const generateArtifacts = async (
|
|
|
1058
1105
|
}
|
|
1059
1106
|
|
|
1060
1107
|
// Generate scrubbed HTML Code Snippets
|
|
1061
|
-
const ruleIdJson = createRuleIdJson(allIssues);
|
|
1108
|
+
const ruleIdJson = await createRuleIdJson(allIssues, itemsStore);
|
|
1109
|
+
|
|
1110
|
+
// Clean up intermediate items files
|
|
1111
|
+
await itemsStore.cleanup();
|
|
1062
1112
|
|
|
1063
1113
|
// At the end of the function where results are generated, add:
|
|
1064
1114
|
try {
|
|
@@ -1085,6 +1135,9 @@ const generateArtifacts = async (
|
|
|
1085
1135
|
if (process.env.RUNNING_FROM_PH_GUI || process.env.OOBEE_VERBOSE)
|
|
1086
1136
|
console.log('Report generated successfully');
|
|
1087
1137
|
|
|
1138
|
+
process.removeListener('uncaughtException', crawleeEpermHandler);
|
|
1139
|
+
process.removeListener('unhandledRejection', crawleeEpermHandler);
|
|
1140
|
+
|
|
1088
1141
|
return ruleIdJson;
|
|
1089
1142
|
};
|
|
1090
1143
|
|