-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
proto-preprocessor.js
151 lines (132 loc) · 4.27 KB
/
proto-preprocessor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/**
* @license
* Copyright 2018 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'fs';
import esMain from 'es-main';
/**
* @fileoverview Helper functions to transform an LHR into a proto-ready LHR.
*
* FIXME: This file is 100% technical debt. Our eventual goal is for the
* roundtrip JSON to match the Golden LHR 1:1.
*/
/**
* Transform an LHR into a proto-friendly, mostly-compatible LHR.
* @param {LH.Result} lhr
* @return {LH.Result}
*/
function processForProto(lhr) {
/** @type {LH.Result} */
const reportJson = JSON.parse(JSON.stringify(lhr));
// Clean up the configSettings
// Note: This is not strictly required for conversion if protobuf parsing is set to
// 'ignore unknown fields' in the language of conversion.
if (reportJson.configSettings) {
// The settings that are in both proto and LHR
const {
formFactor,
locale,
onlyCategories,
channel,
throttling,
screenEmulation,
throttlingMethod} = reportJson.configSettings;
// @ts-expect-error - intentionally only a subset of settings.
reportJson.configSettings = {
formFactor,
locale,
onlyCategories,
channel,
throttling,
screenEmulation,
throttlingMethod};
}
// Remove runtimeError if it is NO_ERROR
if (reportJson.runtimeError && reportJson.runtimeError.code === 'NO_ERROR') {
delete reportJson.runtimeError;
}
// Clean up actions that require 'audits' to exist
if (reportJson.audits) {
Object.keys(reportJson.audits).forEach(auditName => {
const audit = reportJson.audits[auditName];
// Rewrite 'not-applicable' and 'not_applicable' scoreDisplayMode to 'notApplicable'. #6201, #6783.
if (audit.scoreDisplayMode) {
// @ts-expect-error ts properly flags this as invalid as it should not happen,
// but remains in preprocessor to protect from proto translation errors from
// old LHRs.
// eslint-disable-next-line max-len
if (audit.scoreDisplayMode === 'not-applicable' || audit.scoreDisplayMode === 'not_applicable') {
audit.scoreDisplayMode = 'notApplicable';
}
}
// Normalize displayValue to always be a string, not an array. #6200
if (Array.isArray(audit.displayValue)) {
/** @type {Array<any>}*/
const values = [];
audit.displayValue.forEach(item => {
values.push(item);
});
audit.displayValue = values.join(' | ');
}
});
}
/**
* Execute `cb(obj, key)` on every object property where obj[key] is a string, recursively.
* @param {any} obj
* @param {(obj: Record<string, string>, key: string) => void} cb
*/
function iterateStrings(obj, cb) {
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
cb(obj, key);
} else {
iterateStrings(obj[key], cb);
}
});
} else if (Array.isArray(obj)) {
obj.forEach(item => {
if (typeof item === 'object' || Array.isArray(item)) {
iterateStrings(item, cb);
}
});
}
}
iterateStrings(reportJson, (obj, key) => {
const value = obj[key];
// Remove empty strings, as they are dropped after round-tripping anyway.
if (value === '') {
delete obj[key];
return;
}
// Sanitize lone surrogates.
// @ts-expect-error node 20
if (String.prototype.isWellFormed && !value.isWellFormed()) {
// @ts-expect-error node 20
obj[key] = value.toWellFormed();
}
});
return reportJson;
}
// Test if called from the CLI or as a module.
if (esMain(import.meta)) {
// read in the argv for the input & output
const args = process.argv.slice(2);
let input;
let output;
if (args.length) {
// find can return undefined, so default it to '' with OR
input = (args.find(flag => flag.startsWith('--in')) || '').replace('--in=', '');
output = (args.find(flag => flag.startsWith('--out')) || '').replace('--out=', '');
}
if (input && output) {
// process the file
const report = processForProto(JSON.parse(fs.readFileSync(input, 'utf-8')));
// write to output from argv
fs.writeFileSync(output, JSON.stringify(report), 'utf-8');
}
}
export {
processForProto,
};