@govtechsg/oobee 0.10.89 → 0.10.90
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/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 +66 -38
- package/fb85adb0-5db6-4a09-8c80-05f030115004.txt +0 -0
- 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 +73 -46
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
import type { ItemsInfo } from './types.js';
|
|
5
|
+
|
|
6
|
+
export interface ItemsStoreEntry {
|
|
7
|
+
url: string;
|
|
8
|
+
pageTitle: string;
|
|
9
|
+
items: ItemsInfo[];
|
|
10
|
+
filePath?: string;
|
|
11
|
+
pageIndex?: number;
|
|
12
|
+
pageImagePath?: string;
|
|
13
|
+
metadata?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ItemsStore {
|
|
17
|
+
private basePath: string;
|
|
18
|
+
private ensuredDirs = new Set<string>();
|
|
19
|
+
|
|
20
|
+
constructor(storagePath: string) {
|
|
21
|
+
this.basePath = path.join(storagePath, 'tmp-items');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private sanitizeRuleId(ruleId: string): string {
|
|
25
|
+
return ruleId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private getRuleFilePath(category: string, ruleId: string): string {
|
|
29
|
+
return path.join(this.basePath, category, `${this.sanitizeRuleId(ruleId)}.jsonl`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async ensureDir(category: string): Promise<void> {
|
|
33
|
+
const dirPath = path.join(this.basePath, category);
|
|
34
|
+
if (!this.ensuredDirs.has(dirPath)) {
|
|
35
|
+
await fs.ensureDir(dirPath);
|
|
36
|
+
this.ensuredDirs.add(dirPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async appendPageItems(category: string, ruleId: string, entry: ItemsStoreEntry): Promise<void> {
|
|
41
|
+
await this.ensureDir(category);
|
|
42
|
+
const filePath = this.getRuleFilePath(category, ruleId);
|
|
43
|
+
const line = JSON.stringify(entry) + '\n';
|
|
44
|
+
await fs.appendFile(filePath, line, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async *readRuleItems(category: string, ruleId: string): AsyncGenerator<ItemsStoreEntry> {
|
|
48
|
+
const filePath = this.getRuleFilePath(category, ruleId);
|
|
49
|
+
if (!fs.existsSync(filePath)) return;
|
|
50
|
+
|
|
51
|
+
const fileStream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
52
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
53
|
+
|
|
54
|
+
for await (const line of rl) {
|
|
55
|
+
if (line.trim()) {
|
|
56
|
+
yield JSON.parse(line) as ItemsStoreEntry;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async readRuleItemsMap(category: string, ruleId: string): Promise<Map<string, ItemsStoreEntry>> {
|
|
62
|
+
const map = new Map<string, ItemsStoreEntry>();
|
|
63
|
+
for await (const entry of this.readRuleItems(category, ruleId)) {
|
|
64
|
+
const key = entry.pageIndex != null ? String(entry.pageIndex) : entry.url;
|
|
65
|
+
map.set(key, entry);
|
|
66
|
+
}
|
|
67
|
+
return map;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async cleanup(): Promise<void> {
|
|
71
|
+
await fs.rm(this.basePath, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -6,6 +6,7 @@ import { pipeline } from 'stream/promises';
|
|
|
6
6
|
import { a11yRuleShortDescriptionMap } from '../constants/constants.js';
|
|
7
7
|
import { consoleLogger } from '../logs.js';
|
|
8
8
|
import type { AllIssues } from './types.js';
|
|
9
|
+
import type { ItemsStore } from './itemsStore.js';
|
|
9
10
|
|
|
10
11
|
function* serializeObject(obj: any, depth = 0, indent = ' ') {
|
|
11
12
|
const currentIndent = indent.repeat(depth);
|
|
@@ -79,158 +80,151 @@ function writeLargeJsonToFile(obj: object, filePath: string) {
|
|
|
79
80
|
});
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
const writeLargeScanItemsJsonToFile = async (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const processNextWrite = async () => {
|
|
89
|
-
if (isWriting || writeQueue.length === 0) return;
|
|
90
|
-
|
|
91
|
-
isWriting = true;
|
|
92
|
-
const data = writeQueue.shift()!;
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
if (!writeStream.write(data)) {
|
|
96
|
-
await new Promise<void>(resolve => {
|
|
97
|
-
writeStream.once('drain', () => {
|
|
98
|
-
resolve();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
} catch (error) {
|
|
103
|
-
writeStream.destroy(error as Error);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
83
|
+
const writeLargeScanItemsJsonToFile = async (
|
|
84
|
+
obj: object,
|
|
85
|
+
filePath: string,
|
|
86
|
+
itemsStore?: ItemsStore,
|
|
87
|
+
) => {
|
|
88
|
+
const writeStream = fs.createWriteStream(filePath, { flags: 'a', encoding: 'utf8' });
|
|
106
89
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
processNextWrite();
|
|
114
|
-
};
|
|
90
|
+
const write = (data: string): Promise<void> => {
|
|
91
|
+
if (!writeStream.write(data)) {
|
|
92
|
+
return new Promise<void>(resolve => writeStream.once('drain', resolve));
|
|
93
|
+
}
|
|
94
|
+
return Promise.resolve();
|
|
95
|
+
};
|
|
115
96
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
97
|
+
try {
|
|
98
|
+
await write('{\n');
|
|
99
|
+
const keys = Object.keys(obj);
|
|
120
100
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
101
|
+
for (let i = 0; i < keys.length; i++) {
|
|
102
|
+
const key = keys[i];
|
|
103
|
+
const value = obj[key];
|
|
104
|
+
|
|
105
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
106
|
+
await write(` "${key}": ${JSON.stringify(value)}`);
|
|
107
|
+
} else {
|
|
108
|
+
await write(` "${key}": {\n`);
|
|
109
|
+
|
|
110
|
+
const { rules, ...otherProperties } = value;
|
|
111
|
+
const otherKeys = Object.keys(otherProperties);
|
|
112
|
+
|
|
113
|
+
for (let j = 0; j < otherKeys.length; j++) {
|
|
114
|
+
const propKey = otherKeys[j];
|
|
115
|
+
const propValue = otherProperties[propKey];
|
|
116
|
+
const propValueString =
|
|
117
|
+
propValue === null ||
|
|
118
|
+
typeof propValue === 'function' ||
|
|
119
|
+
typeof propValue === 'undefined'
|
|
120
|
+
? 'null'
|
|
121
|
+
: JSON.stringify(propValue);
|
|
122
|
+
await write(` "${propKey}": ${propValueString}`);
|
|
123
|
+
if (j < otherKeys.length - 1 || (rules && rules.length >= 0)) {
|
|
124
|
+
await write(',\n');
|
|
125
|
+
} else {
|
|
126
|
+
await write('\n');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
} else {
|
|
151
|
-
queueWrite('\n');
|
|
130
|
+
if (rules && Array.isArray(rules)) {
|
|
131
|
+
await write(' "rules": [\n');
|
|
132
|
+
|
|
133
|
+
for (let j = 0; j < rules.length; j++) {
|
|
134
|
+
const rule = rules[j];
|
|
135
|
+
await write(' {\n');
|
|
136
|
+
const { pagesAffected, ...otherRuleProperties } = rule;
|
|
137
|
+
const ruleKeys = Object.keys(otherRuleProperties);
|
|
138
|
+
|
|
139
|
+
for (let k = 0; k < ruleKeys.length; k++) {
|
|
140
|
+
const ruleKey = ruleKeys[k];
|
|
141
|
+
const ruleValue = otherRuleProperties[ruleKey];
|
|
142
|
+
const ruleValueString =
|
|
143
|
+
ruleValue === null ||
|
|
144
|
+
typeof ruleValue === 'function' ||
|
|
145
|
+
typeof ruleValue === 'undefined'
|
|
146
|
+
? 'null'
|
|
147
|
+
: JSON.stringify(ruleValue);
|
|
148
|
+
await write(` "${ruleKey}": ${ruleValueString}`);
|
|
149
|
+
if (k < ruleKeys.length - 1 || pagesAffected) {
|
|
150
|
+
await write(',\n');
|
|
151
|
+
} else {
|
|
152
|
+
await write('\n');
|
|
153
|
+
}
|
|
152
154
|
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
if (rules && Array.isArray(rules)) {
|
|
156
|
-
queueWrite(' "rules": [\n');
|
|
157
|
-
|
|
158
|
-
rules.forEach((rule, j) => {
|
|
159
|
-
queueWrite(' {\n');
|
|
160
|
-
const { pagesAffected, ...otherRuleProperties } = rule;
|
|
161
|
-
|
|
162
|
-
Object.entries(otherRuleProperties).forEach(([ruleKey, ruleValue], k) => {
|
|
163
|
-
const ruleValueString =
|
|
164
|
-
ruleValue === null ||
|
|
165
|
-
typeof ruleValue === 'function' ||
|
|
166
|
-
typeof ruleValue === 'undefined'
|
|
167
|
-
? 'null'
|
|
168
|
-
: JSON.stringify(ruleValue);
|
|
169
|
-
queueWrite(` "${ruleKey}": ${ruleValueString}`);
|
|
170
|
-
if (k < Object.keys(otherRuleProperties).length - 1 || pagesAffected) {
|
|
171
|
-
queueWrite(',\n');
|
|
172
|
-
} else {
|
|
173
|
-
queueWrite('\n');
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
155
|
|
|
177
|
-
|
|
178
|
-
|
|
156
|
+
if (pagesAffected && Array.isArray(pagesAffected)) {
|
|
157
|
+
// Load items from disk for this rule if itemsStore is available
|
|
158
|
+
let itemsMap: Map<string, any> | null = null;
|
|
159
|
+
if (itemsStore && rule.rule) {
|
|
160
|
+
itemsMap = await itemsStore.readRuleItemsMap(key, rule.rule);
|
|
161
|
+
}
|
|
179
162
|
|
|
180
|
-
|
|
181
|
-
const pageJson = JSON.stringify(page, null, 2)
|
|
182
|
-
.split('\n')
|
|
183
|
-
.map(line => ` ${line}`)
|
|
184
|
-
.join('\n');
|
|
163
|
+
await write(' "pagesAffected": [\n');
|
|
185
164
|
|
|
186
|
-
|
|
165
|
+
for (let p = 0; p < pagesAffected.length; p++) {
|
|
166
|
+
const page = pagesAffected[p];
|
|
167
|
+
let fullPage = page;
|
|
187
168
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
169
|
+
if (itemsMap) {
|
|
170
|
+
const lookupKey =
|
|
171
|
+
page.pageIndex != null ? String(page.pageIndex) : page.url;
|
|
172
|
+
const entry = itemsMap.get(lookupKey);
|
|
173
|
+
if (entry) {
|
|
174
|
+
// Strip itemsCount to match original scanItems.json format
|
|
175
|
+
const { itemsCount: _ic, ...pageWithoutCount } = page;
|
|
176
|
+
fullPage = { ...pageWithoutCount, items: entry.items };
|
|
192
177
|
}
|
|
193
|
-
}
|
|
178
|
+
}
|
|
194
179
|
|
|
195
|
-
|
|
196
|
-
|
|
180
|
+
const pageJson = JSON.stringify(fullPage, null, 2)
|
|
181
|
+
.split('\n')
|
|
182
|
+
.map(line => ` ${line}`)
|
|
183
|
+
.join('\n');
|
|
197
184
|
|
|
198
|
-
|
|
199
|
-
if (j < rules.length - 1) {
|
|
200
|
-
queueWrite(',\n');
|
|
201
|
-
} else {
|
|
202
|
-
queueWrite('\n');
|
|
203
|
-
}
|
|
204
|
-
});
|
|
185
|
+
await write(pageJson);
|
|
205
186
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
187
|
+
if (p < pagesAffected.length - 1) {
|
|
188
|
+
await write(',\n');
|
|
189
|
+
} else {
|
|
190
|
+
await write('\n');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
210
193
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
} else {
|
|
214
|
-
queueWrite('\n');
|
|
215
|
-
}
|
|
216
|
-
});
|
|
194
|
+
await write(' ]');
|
|
195
|
+
}
|
|
217
196
|
|
|
218
|
-
|
|
197
|
+
await write('\n }');
|
|
198
|
+
if (j < rules.length - 1) {
|
|
199
|
+
await write(',\n');
|
|
200
|
+
} else {
|
|
201
|
+
await write('\n');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
219
204
|
|
|
220
|
-
|
|
221
|
-
if (writeQueue.length === 0 && !isWriting) {
|
|
222
|
-
writeStream.end();
|
|
223
|
-
} else {
|
|
224
|
-
setTimeout(checkQueueAndEnd, 100);
|
|
205
|
+
await write(' ]');
|
|
225
206
|
}
|
|
226
|
-
|
|
207
|
+
await write('\n }');
|
|
208
|
+
}
|
|
227
209
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
210
|
+
if (i < keys.length - 1) {
|
|
211
|
+
await write(',\n');
|
|
212
|
+
} else {
|
|
213
|
+
await write('\n');
|
|
214
|
+
}
|
|
232
215
|
}
|
|
233
|
-
|
|
216
|
+
|
|
217
|
+
await write('}\n');
|
|
218
|
+
} finally {
|
|
219
|
+
writeStream.end();
|
|
220
|
+
await new Promise<void>((resolve, reject) => {
|
|
221
|
+
writeStream.on('finish', () => {
|
|
222
|
+
consoleLogger.info(`JSON file written successfully: ${filePath}`);
|
|
223
|
+
resolve();
|
|
224
|
+
});
|
|
225
|
+
writeStream.on('error', reject);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
234
228
|
};
|
|
235
229
|
|
|
236
230
|
async function compressJsonFileStreaming(inputPath: string, outputPath: string) {
|
|
@@ -247,12 +241,13 @@ const writeJsonFileAndCompressedJsonFile = async (
|
|
|
247
241
|
data: object,
|
|
248
242
|
storagePath: string,
|
|
249
243
|
filename: string,
|
|
244
|
+
itemsStore?: ItemsStore,
|
|
250
245
|
): Promise<{ jsonFilePath: string; base64FilePath: string }> => {
|
|
251
246
|
try {
|
|
252
247
|
consoleLogger.info(`Writing JSON to ${filename}.json`);
|
|
253
248
|
const jsonFilePath = path.join(storagePath, `${filename}.json`);
|
|
254
249
|
if (filename === 'scanItems') {
|
|
255
|
-
await writeLargeScanItemsJsonToFile(data, jsonFilePath);
|
|
250
|
+
await writeLargeScanItemsJsonToFile(data, jsonFilePath, itemsStore);
|
|
256
251
|
} else {
|
|
257
252
|
await writeLargeJsonToFile(data, jsonFilePath);
|
|
258
253
|
}
|
|
@@ -277,6 +272,7 @@ const writeJsonFileAndCompressedJsonFile = async (
|
|
|
277
272
|
const writeJsonAndBase64Files = async (
|
|
278
273
|
allIssues: AllIssues,
|
|
279
274
|
storagePath: string,
|
|
275
|
+
itemsStore?: ItemsStore,
|
|
280
276
|
): Promise<{
|
|
281
277
|
scanDataJsonFilePath: string;
|
|
282
278
|
scanDataBase64FilePath: string;
|
|
@@ -301,6 +297,7 @@ const writeJsonAndBase64Files = async (
|
|
|
301
297
|
{ oobeeAppVersion: allIssues.oobeeAppVersion, ...items },
|
|
302
298
|
storagePath,
|
|
303
299
|
'scanItems',
|
|
300
|
+
itemsStore,
|
|
304
301
|
);
|
|
305
302
|
|
|
306
303
|
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
|
@@ -345,22 +342,22 @@ const writeJsonAndBase64Files = async (
|
|
|
345
342
|
|
|
346
343
|
items.mustFix.rules.forEach(rule => {
|
|
347
344
|
rule.pagesAffected.forEach(page => {
|
|
348
|
-
page.itemsCount = page.items.length;
|
|
345
|
+
page.itemsCount = page.itemsCount ?? (Array.isArray(page.items) ? page.items.length : 0);
|
|
349
346
|
});
|
|
350
347
|
});
|
|
351
348
|
items.goodToFix.rules.forEach(rule => {
|
|
352
349
|
rule.pagesAffected.forEach(page => {
|
|
353
|
-
page.itemsCount = page.items.length;
|
|
350
|
+
page.itemsCount = page.itemsCount ?? (Array.isArray(page.items) ? page.items.length : 0);
|
|
354
351
|
});
|
|
355
352
|
});
|
|
356
353
|
items.needsReview.rules.forEach(rule => {
|
|
357
354
|
rule.pagesAffected.forEach(page => {
|
|
358
|
-
page.itemsCount = page.items.length;
|
|
355
|
+
page.itemsCount = page.itemsCount ?? (Array.isArray(page.items) ? page.items.length : 0);
|
|
359
356
|
});
|
|
360
357
|
});
|
|
361
358
|
items.passed.rules.forEach(rule => {
|
|
362
359
|
rule.pagesAffected.forEach(page => {
|
|
363
|
-
page.itemsCount = page.items.length;
|
|
360
|
+
page.itemsCount = page.itemsCount ?? (Array.isArray(page.items) ? page.items.length : 0);
|
|
364
361
|
});
|
|
365
362
|
});
|
|
366
363
|
|
|
@@ -40,8 +40,8 @@ export default function populateScanPagesDetail(allIssues: AllIssues): void {
|
|
|
40
40
|
const { rule: ruleId, conformance = [] } = rule;
|
|
41
41
|
|
|
42
42
|
rule.pagesAffected.forEach(p => {
|
|
43
|
-
const { url, pageTitle,
|
|
44
|
-
const itemsCount = items.length;
|
|
43
|
+
const { url, pageTitle, itemsCount: storedCount, items } = p as any;
|
|
44
|
+
const itemsCount: number = storedCount ?? (Array.isArray(items) ? items.length : 0);
|
|
45
45
|
|
|
46
46
|
if (!pagesMap[url]) {
|
|
47
47
|
pagesMap[url] = {
|