@vernikr/size-report 2.4.0 → 2.6.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/README.md +989 -1081
- package/bin/postinstall.js +17 -18
- package/bin/size.js +2 -2
- package/package.json +3 -4
- package/src/args.js +72 -72
- package/src/artifact.js +14 -14
- package/src/check.js +41 -42
- package/src/cli.js +26 -29
- package/src/config.js +87 -91
- package/src/css.js +14 -14
- package/src/data.js +26 -50
- package/src/derived.js +31 -35
- package/src/doctor.js +95 -99
- package/src/explain.js +46 -47
- package/src/git.js +66 -71
- package/src/history.js +74 -83
- package/src/hook.js +130 -149
- package/src/init.js +37 -37
- package/src/journal.js +17 -15
- package/src/locales.js +31 -22
- package/src/metrics.js +72 -89
- package/src/minify.js +28 -27
- package/src/modes.js +57 -60
- package/src/optional.js +13 -11
- package/src/page/app.css +76 -94
- package/src/page/app.js +124 -80
- package/src/page/build.js +193 -50
- package/src/page/dom.js +8 -9
- package/src/page/panel.js +157 -69
- package/src/page/payload.js +168 -0
- package/src/page/state.js +144 -104
- package/src/page/table.js +270 -86
- package/src/parse-worker.js +10 -10
- package/src/parse.js +43 -45
- package/src/project.js +100 -104
- package/src/refusal.js +75 -76
- package/src/size-table.js +41 -76
- package/src/strip/forms.js +5 -5
- package/src/strip/guard.js +28 -28
- package/src/strip/js.js +27 -27
- package/src/strip.js +17 -21
- package/src/table.css +54 -19
- package/src/tokens.js +27 -27
- package/src/tool.js +10 -11
- package/templates/README.md +71 -77
- package/templates/ci.yml +33 -33
- package/templates/size-report.config.json +3 -3
- package/CHANGELOG.md +0 -690
package/src/page/panel.js
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
1
|
import { appEl, appBox, appOffBox } from './dom.js';
|
|
2
2
|
import { appData, appUi, appView, appFileAt, appFoldSet, appMeasured } from './state.js';
|
|
3
3
|
|
|
4
|
-
/*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
/* Only a file sets a file's checkbox: both a category and a folder in the tree are ways to set the same checkboxes as
|
|
5
|
+
* a group and keep no state of their own. Otherwise one and the same decision would live in two places and drift
|
|
6
|
+
* apart. */
|
|
7
7
|
function appFileBox(i) {
|
|
8
8
|
const f = appData.files[i];
|
|
9
|
-
const where = appFileAt(i) + (f.path === null ?
|
|
10
|
-
|
|
11
|
-
+ (f.categoryBy === 'config' ?
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const where = appFileAt(i) + (f.path === null ? appUi.notOnHead : '');
|
|
10
|
+
const box = appBox(f.label, where + appUi.category
|
|
11
|
+
+ (f.categoryBy === 'config' ? appUi.categoryFromConfig : appUi.categoryByExtension),
|
|
12
|
+
appView.files[i], (e) => appSwitch(i, e.target.checked));
|
|
13
|
+
appFields.file[i] = box.querySelector('input');
|
|
14
|
+
return box;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
/* A file that is not in the report: it stands in its place in the tree with its checkbox off and unavailable — no
|
|
18
|
+
* numbers were measured for it, so there is nothing to switch. The reader sees the reason in the tooltip rather than
|
|
19
|
+
* guessing it from the look. */
|
|
20
20
|
function appUnmeasuredBox(entry) {
|
|
21
21
|
return appOffBox(entry.path.split('/').pop(), entry.path + ' · '
|
|
22
22
|
+ (entry.why === 'rule' ? appUi.notMeasuredRule : appUi.notMeasuredChoice));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/*
|
|
26
|
-
*
|
|
25
|
+
/* Every measured file of a subtree — what a folder's switch controls: a file outside the report has nothing to
|
|
26
|
+
* switch. */
|
|
27
27
|
function appIndexes(node) {
|
|
28
28
|
const out = node.files.slice();
|
|
29
29
|
node.dirs.forEach((sub) => { out.push(...appIndexes(sub)); });
|
|
30
30
|
return out;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/*
|
|
33
|
+
/* How many files a subtree holds — including the ones that made it into no report. */
|
|
34
34
|
function appCount(node) {
|
|
35
35
|
let n = node.files.length + node.others.length;
|
|
36
36
|
node.dirs.forEach((sub) => { n += appCount(sub); });
|
|
37
37
|
return n;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/*
|
|
40
|
+
/* A tree node: the measured files (columns), the project's other files and the subfolders. */
|
|
41
41
|
function appNode() {
|
|
42
42
|
return { files: [], others: [], dirs: new Map() };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
function appDirHead(name, sub) {
|
|
45
|
+
/* A folder's switch: its checkbox carries the whole subtree with it and shows three states — every file on, some,
|
|
46
|
+
* none. The number of files stands next to it; when the folder also holds files outside the report, it is written as
|
|
47
|
+
* a fraction ("2/5"): what matters to the reader is that the folder holds five files while two are measured. A folder
|
|
48
|
+
* without a single measured file stays in place with its checkbox off and unavailable: there is nothing to switch on
|
|
49
|
+
* in it. */
|
|
50
|
+
function appDirHead(name, here, sub) {
|
|
51
51
|
const idx = appIndexes(sub);
|
|
52
52
|
const total = appCount(sub);
|
|
53
53
|
const label = name + '/';
|
|
@@ -58,28 +58,24 @@ function appDirHead(name, sub) {
|
|
|
58
58
|
const on = idx.map((i) => appView.files[i]);
|
|
59
59
|
const every = on.every((v) => v);
|
|
60
60
|
head = appBox(label, appUi.dir.replace('{name}', name).replace('{n}', idx.length),
|
|
61
|
-
every, (e) =>
|
|
62
|
-
idx.forEach((i) => { appView.files[i] = e.target.checked; });
|
|
63
|
-
appRender();
|
|
64
|
-
}, 'dir');
|
|
61
|
+
every, (e) => appSwitchGroup(idx, e.target.checked), 'dir');
|
|
65
62
|
head.querySelector('input').indeterminate = !every && on.some((v) => v);
|
|
63
|
+
appFields.dir[here] = head;
|
|
66
64
|
}
|
|
67
65
|
head.appendChild(appEl('span', 'n', idx.length === total ? String(total) : idx.length + '/' + total));
|
|
68
66
|
return head;
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
/*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* же нажатие.
|
|
69
|
+
/* The folder's sign is a click target of its own, separate from the checkbox: the checkbox answers for the numbers (it
|
|
70
|
+
* switches the subtree's files on), while the sign answers for how much of the tree is visible. One target for two
|
|
71
|
+
* different decisions would mean a folder can be folded only together with switching its files on. The sign is drawn
|
|
72
|
+
* as a span rather than a button and stands beside the label rather than inside it: a label is one click target, and a
|
|
73
|
+
* control nested in it would be reached as that same target.
|
|
77
74
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* вещи, которые читатель и видит: класс, знак и запись в памяти. */
|
|
75
|
+
* A click on the sign rebuilds nothing: the subtree lies in the markup and a class on the row hides it. A rebuild here
|
|
76
|
+
* would be honest work for nothing — it counts the whole table (every row by every column) and so pays for numbers
|
|
77
|
+
* folding does not change. That is why only the three things the reader sees change: the class, the sign and the note
|
|
78
|
+
* in the memory. */
|
|
83
79
|
function appFoldBox(name, path) {
|
|
84
80
|
const folded = appView.folded[path] === true;
|
|
85
81
|
const box = appEl('span', 'fold', folded ? '▸' : '▾');
|
|
@@ -95,8 +91,8 @@ function appFoldBox(name, path) {
|
|
|
95
91
|
return box;
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
/*
|
|
99
|
-
*
|
|
94
|
+
/* The leaves of one level: measured files and files outside the report mixed together and sorted by name, the way a
|
|
95
|
+
* file tree reads, rather than as separate lists. */
|
|
100
96
|
function appLeaves(node) {
|
|
101
97
|
const items = node.files.map((i) => ({ name: appData.files[i].label, i: i, entry: null }));
|
|
102
98
|
node.others.forEach((entry) => {
|
|
@@ -105,30 +101,29 @@ function appLeaves(node) {
|
|
|
105
101
|
return items.sort((a, b) => (a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)));
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
/*
|
|
109
|
-
*
|
|
104
|
+
/* A folder row: the folding sign, the checkbox with the number of files and the subtree. A folded folder differs by
|
|
105
|
+
* its class alone — the markup stays the same. */
|
|
110
106
|
function appDir(name, sub, prefix) {
|
|
111
107
|
const here = prefix === '' ? name : prefix + '/' + name;
|
|
112
108
|
const folded = appView.folded[here] === true;
|
|
113
109
|
const li = appEl('li', folded ? 'folded' : null);
|
|
114
110
|
li.appendChild(appFoldBox(name, here));
|
|
115
|
-
li.appendChild(appDirHead(name, sub));
|
|
111
|
+
li.appendChild(appDirHead(name, here, sub));
|
|
116
112
|
li.appendChild(appTreeList(sub, here));
|
|
117
113
|
return li;
|
|
118
114
|
}
|
|
119
115
|
|
|
120
|
-
//
|
|
116
|
+
// A leaf row: a measured file comes with a checkbox, a file outside the report with one switched off.
|
|
121
117
|
function appLeaf(leaf) {
|
|
122
118
|
const li = appEl('li');
|
|
123
119
|
li.appendChild(leaf.entry === null ? appFileBox(leaf.i) : appUnmeasuredBox(leaf.entry));
|
|
124
120
|
return li;
|
|
125
121
|
}
|
|
126
122
|
|
|
127
|
-
/*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* а найти его по-прежнему можно — оно там же, где было. */
|
|
123
|
+
/* The nodes of one level: first everything in the report (folders alphabetically, then the leaves in the order
|
|
124
|
+
* `appLeaves` gives), then what is not in it — folders without a measured file and files outside the columns. That is
|
|
125
|
+
* not a matter of taste: everything outside the report has its checkbox off and unavailable, so at the end of the list
|
|
126
|
+
* it does not distract from what is in the table, while it can still be found — in the same place where it was. */
|
|
132
127
|
function appTreeList(node, prefix) {
|
|
133
128
|
const list = appEl('ul', 'tree');
|
|
134
129
|
const dirs = [...node.dirs.keys()].sort()
|
|
@@ -142,8 +137,8 @@ function appTreeList(node, prefix) {
|
|
|
142
137
|
return list;
|
|
143
138
|
}
|
|
144
139
|
|
|
145
|
-
/*
|
|
146
|
-
*
|
|
140
|
+
/* A leaf's place in the tree: the path is split on "/" and the intermediate folders are created on the way. One place
|
|
141
|
+
* for measured and other files alike — otherwise they would drift apart in folders. */
|
|
147
142
|
function appLeafAt(node, p, i, entry) {
|
|
148
143
|
const parts = p.split('/');
|
|
149
144
|
for (let d = 0; d < parts.length - 1; d++) {
|
|
@@ -154,11 +149,10 @@ function appLeafAt(node, p, i, entry) {
|
|
|
154
149
|
else node.others.push(entry);
|
|
155
150
|
}
|
|
156
151
|
|
|
157
|
-
/*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* пути. */
|
|
152
|
+
/* The page's tree is the project's tree: the nodes come from the catalogue (every path git sees), which is why it also
|
|
153
|
+
* holds files outside the report. A measured leaf is a column, and its path is the one in the file's caption; a column
|
|
154
|
+
* whose file is gone from HEAD never entered the catalogue (the index does not hold it) and stands in place by the last
|
|
155
|
+
* path known. */
|
|
162
156
|
function appTree() {
|
|
163
157
|
const root = appNode();
|
|
164
158
|
appData.files.forEach((_f, i) => appLeafAt(root, appFileAt(i), i, null));
|
|
@@ -168,6 +162,10 @@ function appTree() {
|
|
|
168
162
|
return appTreeList(root, '');
|
|
169
163
|
}
|
|
170
164
|
|
|
165
|
+
/* The panel: built once, at the first drawing, and afterwards only its fields change. A rebuild would count the whole
|
|
166
|
+
* table for nothing — the tree, the counters and the tooltips are the same after every click — while it would also
|
|
167
|
+
* take the reader's place in the list away: the scroll of the panel and of the tree, and the field under the
|
|
168
|
+
* keyboard, would have to be put back by hand. Nothing of that is here, because there is nothing to put back. */
|
|
171
169
|
export function appPanel() {
|
|
172
170
|
const panel = document.getElementById('panel');
|
|
173
171
|
panel.textContent = '';
|
|
@@ -176,16 +174,17 @@ export function appPanel() {
|
|
|
176
174
|
metrics.appendChild(appEl('legend', null, appUi.metrics));
|
|
177
175
|
const mrow = appEl('div', 'row');
|
|
178
176
|
appData.metrics.forEach((m) => {
|
|
179
|
-
const
|
|
180
|
-
mrow.appendChild(appBox(m.label, m.note + ' · ' + word, appView.metrics[m.key], (e) => {
|
|
177
|
+
const box = appBox(m.label, m.note, appView.metrics[m.key], (e) => {
|
|
181
178
|
appView.metrics[m.key] = e.target.checked;
|
|
182
|
-
|
|
183
|
-
}, 'metric')
|
|
179
|
+
appSwitchMetric();
|
|
180
|
+
}, 'metric');
|
|
181
|
+
appFields.metric[m.key] = box.querySelector('input');
|
|
182
|
+
mrow.appendChild(box);
|
|
184
183
|
});
|
|
185
184
|
metrics.appendChild(mrow);
|
|
186
|
-
/*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
185
|
+
/* What produced each number is visible rather than hidden in a tooltip: the token dictionary and the way of
|
|
186
|
+
* compression are chosen by the settings of the run, the page has nothing to switch them with, and the reader needs
|
|
187
|
+
* to know this without pointing a mouse. */
|
|
189
188
|
appData.metrics.forEach((m) => {
|
|
190
189
|
metrics.appendChild(appEl('p', 'about', m.label + ' — ' + appUi.methodLabel + ' ' + m.method));
|
|
191
190
|
});
|
|
@@ -193,19 +192,108 @@ export function appPanel() {
|
|
|
193
192
|
|
|
194
193
|
const files = appEl('fieldset', 'files');
|
|
195
194
|
files.appendChild(appEl('legend', null, appUi.files));
|
|
196
|
-
/*
|
|
197
|
-
*
|
|
195
|
+
/* The row of categories is marked by a class: the file list scrolls, and the row stays in sight (its stickiness lives
|
|
196
|
+
* in the wide layout, which is where the panel scrolls). */
|
|
198
197
|
const cats = appEl('div', 'row cats');
|
|
199
198
|
appData.categories.forEach((cat) => {
|
|
200
199
|
const idx = [];
|
|
201
200
|
appData.files.forEach((f, i) => { if (f.category === cat.key) idx.push(i); });
|
|
202
|
-
|
|
203
|
-
(e) =>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}, 'all'));
|
|
201
|
+
const box = appBox(cat.label, appUi.all + ' · ' + cat.label, idx.every((i) => appView.files[i]),
|
|
202
|
+
(e) => appSwitchGroup(idx, e.target.checked), 'all');
|
|
203
|
+
appFields.cat[cat.key] = box.querySelector('input');
|
|
204
|
+
cats.appendChild(box);
|
|
207
205
|
});
|
|
208
206
|
files.appendChild(cats);
|
|
209
207
|
files.appendChild(appTree());
|
|
210
208
|
panel.appendChild(files);
|
|
211
209
|
}
|
|
210
|
+
|
|
211
|
+
/* -------- the panel's fields after a choice -------- */
|
|
212
|
+
|
|
213
|
+
/* The fields of the panel by name: the markup is built once, so a click needs a reference to the field it changes
|
|
214
|
+
* rather than a rebuild of the panel. Only a file owns a state — a folder and a category are ways to set the same
|
|
215
|
+
* boxes — which is why their fields are read from the files below them rather than kept. */
|
|
216
|
+
const appFields = { metric: {}, file: {}, dir: {}, cat: {} };
|
|
217
|
+
|
|
218
|
+
/* The boxes of a row's own subtree, read from the tree itself: the panel keeps no second list of the files a folder
|
|
219
|
+
* holds, and what the reader sees is exactly the boxes that are here. A file outside the report stands in the tree
|
|
220
|
+
* with nothing to switch, hence it is left out. */
|
|
221
|
+
function appRowBoxes(box) {
|
|
222
|
+
return [...box.closest('li').querySelectorAll('.box:not(.dir):not(.plain) input')];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// A folder's field from its files: all on — checked, some — the third state, none — simply unchecked.
|
|
226
|
+
function appDirState(box) {
|
|
227
|
+
const boxes = appRowBoxes(box);
|
|
228
|
+
const on = boxes.filter((b) => b.checked).length;
|
|
229
|
+
const input = box.querySelector('input');
|
|
230
|
+
input.checked = on === boxes.length;
|
|
231
|
+
input.indeterminate = on > 0 && on < boxes.length;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* A category's field from its files — the same rule, taken from the data: a category's files are named by the
|
|
235
|
+
* category itself (`category`), and the boxes of the tree are a different view of the same files. */
|
|
236
|
+
function appCatState(key) {
|
|
237
|
+
let all = 0;
|
|
238
|
+
let on = 0;
|
|
239
|
+
appData.files.forEach((f, i) => {
|
|
240
|
+
if (f.category !== key) return;
|
|
241
|
+
all++;
|
|
242
|
+
if (appView.files[i] === true) on++;
|
|
243
|
+
});
|
|
244
|
+
const input = appFields.cat[key];
|
|
245
|
+
input.checked = on === all;
|
|
246
|
+
input.indeterminate = on > 0 && on < all;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* The folders a file lies in: the prefixes of its path, from the root down. The tree's folders are exactly those
|
|
250
|
+
* prefixes — that is how it is built (`appLeafAt`) — so no second naming rule is needed. */
|
|
251
|
+
function appDirPath(path) {
|
|
252
|
+
const parts = path.split('/');
|
|
253
|
+
return parts.slice(0, -1).map((_part, i) => parts.slice(0, i + 1).join('/'));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* A click reaches a folder's field through the files below it: every folder on the path of a switched file shows the
|
|
257
|
+
* share of what is left on. A folder without measured files has a field of its own too — it is off and unavailable,
|
|
258
|
+
* which is not the reader's state and must not be overwritten here. */
|
|
259
|
+
function appDirsOf(indexes) {
|
|
260
|
+
const seen = {};
|
|
261
|
+
indexes.forEach((i) => {
|
|
262
|
+
appDirPath(appFileAt(i)).forEach((path) => {
|
|
263
|
+
if (seen[path] === true) return;
|
|
264
|
+
seen[path] = true;
|
|
265
|
+
if (appFields.dir[path] !== undefined) appDirState(appFields.dir[path]);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// The same for the quick buttons of the categories: every category one of the switched files belongs to.
|
|
271
|
+
function appCatsOf(indexes) {
|
|
272
|
+
const seen = {};
|
|
273
|
+
indexes.forEach((i) => {
|
|
274
|
+
const key = appData.files[i].category;
|
|
275
|
+
if (seen[key] === true || appFields.cat[key] === undefined) return;
|
|
276
|
+
seen[key] = true;
|
|
277
|
+
appCatState(key);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* What a click changed, written where it stands: the files' own boxes, then the fields of the folders and categories
|
|
282
|
+
* that hold them. Nothing is rebuilt, and no field the choice did not reach is touched. */
|
|
283
|
+
export function appPanelState(indexes) {
|
|
284
|
+
indexes.forEach((i) => {
|
|
285
|
+
const input = appFields.file[i];
|
|
286
|
+
if (input !== undefined) input.checked = appView.files[i] === true;
|
|
287
|
+
});
|
|
288
|
+
appDirsOf(indexes);
|
|
289
|
+
appCatsOf(indexes);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* The whole panel from the view: what a link, a record from the memory and the first drawing need. The metric boxes
|
|
293
|
+
* are the reader's own click otherwise, which is why they are not refreshed on a file's switch. */
|
|
294
|
+
export function appPanelAll() {
|
|
295
|
+
appData.categories.forEach((c) => appCatState(c.key));
|
|
296
|
+
appData.metrics.forEach((m) => { appFields.metric[m.key].checked = appView.metrics[m.key] === true; });
|
|
297
|
+
appData.files.forEach((_f, i) => { appFields.file[i].checked = appView.files[i] === true; });
|
|
298
|
+
Object.keys(appFields.dir).forEach((path) => appDirState(appFields.dir[path]));
|
|
299
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* The page's block: how the file carries it (gzipped and base64 encoded, `appUnpack`) and how the block in
|
|
2
|
+
* sparse form is turned back into the model the page already spoke.
|
|
3
|
+
*
|
|
4
|
+
* The packing is a **transport rather than the shape**: what comes out of the unpacker is the very block the
|
|
5
|
+
* page received before this chapter knew about gzip, with no field added or removed, and `--data`/`--json`
|
|
6
|
+
* answer with the dense contract as they always did. It buys the artifact's weight (the block is the whole file)
|
|
7
|
+
* and pays for it in two ways, both on purpose: the block can no longer be read by eye or by `diff`, and
|
|
8
|
+
* unpacking is **asynchronous** — the platform's own `DecompressionStream` is the only unpacker here, no library
|
|
9
|
+
* travels in the page and nothing is fetched, so the first drawing waits for a promise where it used to happen
|
|
10
|
+
* during the parse.
|
|
11
|
+
*
|
|
12
|
+
* Why the block is sparse. Of 56 019 cells of this repository's report the non-empty ones hold 1 183 distinct
|
|
13
|
+
* triples, and about a thousand cells differ from the row above: nine tenths of the block is yesterday's
|
|
14
|
+
* numbers written again. So the block keeps, for every file, the rows in which it appeared (absolute numbers),
|
|
15
|
+
* moved (deltas against its own previous record) or disappeared, and this chapter puts the snapshots back
|
|
16
|
+
* together. The whole history walk is O(number of changes) rather than O(rows × files).
|
|
17
|
+
*
|
|
18
|
+
* The model after `appDecode` is exactly `--data`: the calculation (`rowModel`, `totalsOf`, `cellParts`,
|
|
19
|
+
* `valueParts`), the table and the panel know nothing about the sparse form, so there is no second way to
|
|
20
|
+
* count a row. A value that did not move is **one object shared by the rows that hold it** — the heap keeps
|
|
21
|
+
* the distinct numbers rather than a copy per commit — and because every consumer reads `v[metric]` and
|
|
22
|
+
* compares nothing by identity, sharing changes no answer.
|
|
23
|
+
*
|
|
24
|
+
* Two rules of the chapters bind here as well: no `import` lines and no module state (the text is pasted into
|
|
25
|
+
* one file), and nothing but declarations (the round-trip check evaluates this file on its own).
|
|
26
|
+
*
|
|
27
|
+
* The dictionary (`strs`) is walked by the encoder in a stated order — files in the column order, rows in the
|
|
28
|
+
* history order, and only the order of the first appearance decides an index. The artifact is rebuilt after
|
|
29
|
+
* every commit and has to come out byte-identical on any machine, so the order of the walk is part of the
|
|
30
|
+
* format rather than a detail of the encoder.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/* The block as the file carries it: the tag's `data-pack` says which packing it is, so the page asks rather than
|
|
34
|
+
* guesses, and a packing it does not know is an error rather than a half-read block. A tag without the marker
|
|
35
|
+
* carries the block itself — the same page works for a build that packs nothing.
|
|
36
|
+
*
|
|
37
|
+
* `DecompressionStream` answers with streams rather than with bytes, hence the reader loop: the promise this
|
|
38
|
+
* function is *is* the price of the weight (see the file's note). `atob` gives one character per byte, and every
|
|
39
|
+
* character above 127 has to be taken back as a byte (`charCodeAt`) rather than as text; `TextDecoder` turns the
|
|
40
|
+
* inflated bytes into the JSON text, which is UTF-8 with the report's own words in it. */
|
|
41
|
+
export async function appUnpack(el) {
|
|
42
|
+
const pack = el.getAttribute('data-pack');
|
|
43
|
+
const text = el.textContent;
|
|
44
|
+
if (pack === null) return text;
|
|
45
|
+
if (pack !== 'base64+gzip') throw new Error('the page’s data is packed as “' + pack + '”, which this page cannot read');
|
|
46
|
+
const stream = new DecompressionStream('gzip');
|
|
47
|
+
const sink = stream.writable.getWriter();
|
|
48
|
+
/* The writing is not waited for before the reading: a stream that is filled before it is drained would stall on
|
|
49
|
+
* its own backpressure. A failure of the write surfaces in the reading loop, which is what this promise returns. */
|
|
50
|
+
const feeding = sink.write(appBytes(text)).then(() => sink.close()).catch(() => null);
|
|
51
|
+
const source = stream.readable.getReader();
|
|
52
|
+
const parts = [];
|
|
53
|
+
for (;;) {
|
|
54
|
+
const step = await source.read();
|
|
55
|
+
if (step.done) break;
|
|
56
|
+
parts.push(step.value);
|
|
57
|
+
}
|
|
58
|
+
await feeding;
|
|
59
|
+
return new TextDecoder().decode(appJoined(parts));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// base64 read as bytes: the browser's `atob` hands out one character per byte, whatever the byte.
|
|
63
|
+
function appBytes(text) {
|
|
64
|
+
const raw = atob(text);
|
|
65
|
+
const bytes = new Uint8Array(raw.length);
|
|
66
|
+
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
|
|
67
|
+
return bytes;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* The chunks of an inflated stream as one array: a chunk boundary falls wherever the platform put it. The length is
|
|
71
|
+
* summed by hand rather than by `reduce` — the page's shell computes no totals of its own, and this chapter is part
|
|
72
|
+
* of the shell the checks read (`test/page-view.test.js`). */
|
|
73
|
+
function appJoined(parts) {
|
|
74
|
+
let size = 0;
|
|
75
|
+
parts.forEach((part) => { size += part.length; });
|
|
76
|
+
const all = new Uint8Array(size);
|
|
77
|
+
let at = 0;
|
|
78
|
+
parts.forEach((part) => { all.set(part, at); at += part.length; });
|
|
79
|
+
return all;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The dictionary's entry: a text the block does not carry stays absent rather than becoming an empty string.
|
|
83
|
+
function appText(p, i) {
|
|
84
|
+
return i === null ? null : p.strs[i];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A value as the page reads it: the metrics by their keys, in the order the block lists them.
|
|
88
|
+
function appValue(keys, nums) {
|
|
89
|
+
const out = {};
|
|
90
|
+
keys.forEach((key, mi) => { out[key] = nums[mi]; });
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* One record of a file's history applied to what the file was: the numbers are absolute when the file was
|
|
95
|
+
* absent and deltas against its own previous record otherwise. A record with nothing but a row means the file
|
|
96
|
+
* is gone from that revision, which is the same `null` the dense contract carries. What comes out is the
|
|
97
|
+
* object every later row shares until the file moves again. */
|
|
98
|
+
function appStep(keys, was, rec) {
|
|
99
|
+
if (rec.length === 1) return null;
|
|
100
|
+
const nums = rec.slice(1);
|
|
101
|
+
if (was === null) return appValue(keys, nums);
|
|
102
|
+
return appValue(keys, nums.map((d, mi) => was[keys[mi]] + d));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* The history unrolled, once, into a snapshot per commit — the shape the contract hands out. The records of
|
|
106
|
+
* every file are consumed in the order of the rows, and each row takes what the files were at it: a file whose
|
|
107
|
+
* record has not come around is the very object it was in the row above.
|
|
108
|
+
*
|
|
109
|
+
* The tail of the walk is "now": the report's last row is its last commit that moved a number, the commits
|
|
110
|
+
* after it moved none, and what the files were at the end is the state at HEAD — which is what the dense
|
|
111
|
+
* `now` is. */
|
|
112
|
+
function appUnroll(p, keys) {
|
|
113
|
+
const at = p.files.map(() => 0);
|
|
114
|
+
const live = p.files.map(() => null);
|
|
115
|
+
const rows = [];
|
|
116
|
+
for (let r = 0; r < p.rows.length; r++) {
|
|
117
|
+
const row = [];
|
|
118
|
+
p.files.forEach((_f, i) => {
|
|
119
|
+
const rec = p.hist[i][at[i]];
|
|
120
|
+
if (rec !== undefined && rec[0] === r) {
|
|
121
|
+
live[i] = appStep(keys, live[i], rec);
|
|
122
|
+
at[i]++;
|
|
123
|
+
}
|
|
124
|
+
row.push(live[i]);
|
|
125
|
+
});
|
|
126
|
+
rows.push(row);
|
|
127
|
+
}
|
|
128
|
+
return { rows: rows, now: live.slice() };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* A commit row: the caption and where it leads. The address is the block's one prefix plus what the row kept
|
|
132
|
+
* of its own link with the sha, and `added` travels as a mark rather than as a word. The name is not `appRow`:
|
|
133
|
+
* the chapters are pasted into **one scope**, where the table's own `appRow` would quietly win and this one
|
|
134
|
+
* would never be called (`test/page-view.test.js` holds the names apart for that very reason). */
|
|
135
|
+
function appRowOf(p, r, values) {
|
|
136
|
+
const section = r[3] === null ? null : { id: appText(p, r[3]), head: appText(p, r[4]), added: r[5] === 1 };
|
|
137
|
+
return {
|
|
138
|
+
sha: appText(p, r[0]),
|
|
139
|
+
when: appText(p, r[1]),
|
|
140
|
+
subject: appText(p, r[2]),
|
|
141
|
+
section: section,
|
|
142
|
+
href: r[6] === null ? null : p.hrefPrefix + appText(p, r[6]),
|
|
143
|
+
values: values
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* The whole block, unrolled into the dense contract the page reads: `schema: 2` is refused by `appRecordOk`
|
|
148
|
+
* rather than unrolled — a record written for a block of another form describes another choice. */
|
|
149
|
+
export function appDecode(p) {
|
|
150
|
+
const keys = p.metrics.map((m) => appText(p, m[0]));
|
|
151
|
+
const hist = appUnroll(p, keys);
|
|
152
|
+
const last = p.files.map(() => false);
|
|
153
|
+
p.last.forEach((i) => { last[i] = true; });
|
|
154
|
+
return {
|
|
155
|
+
schema: p.schema,
|
|
156
|
+
tool: p.tool,
|
|
157
|
+
report: p.report,
|
|
158
|
+
metrics: p.metrics.map((m) => ({ key: appText(p, m[0]), label: appText(p, m[1]),
|
|
159
|
+
note: appText(p, m[2]), method: appText(p, m[3]) })),
|
|
160
|
+
categories: p.cats.map((c) => ({ key: appText(p, c[0]), label: appText(p, c[1]) })),
|
|
161
|
+
files: p.files.map((f) => ({ label: appText(p, f[0]), path: appText(p, f[1]),
|
|
162
|
+
paths: f[2].map((i) => p.strs[i]), category: appText(p, f[3]), categoryBy: appText(p, f[4]) })),
|
|
163
|
+
catalog: p.catalog.map((e) => ({ path: appText(p, e[0]), why: appText(p, e[1]) })),
|
|
164
|
+
rows: p.rows.map((r, ri) => appRowOf(p, r, hist.rows[ri])),
|
|
165
|
+
now: hist.now,
|
|
166
|
+
last: last
|
|
167
|
+
};
|
|
168
|
+
}
|