@vernikr/size-report 2.5.0 → 2.7.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 +186 -81
- package/package.json +2 -2
- package/src/check.js +1 -1
- package/src/data.js +21 -23
- package/src/doctor.js +4 -4
- package/src/history.js +3 -9
- package/src/init.js +2 -1
- package/src/locales.js +17 -11
- package/src/metrics.js +45 -63
- package/src/minify.js +2 -2
- package/src/modes.js +8 -8
- package/src/optional.js +2 -2
- package/src/page/app.css +33 -8
- package/src/page/app.js +139 -71
- package/src/page/build.js +166 -19
- package/src/page/panel.js +135 -30
- package/src/page/payload.js +168 -0
- package/src/page/state.js +88 -63
- package/src/page/table.js +280 -73
- package/src/page/work.js +55 -0
- package/src/parse.js +1 -1
- package/src/project.js +2 -2
- package/src/strip/guard.js +4 -3
- package/src/strip.js +4 -5
- package/src/table.css +44 -8
- package/src/tokens.js +5 -6
- package/templates/README.md +3 -3
package/src/page/panel.js
CHANGED
|
@@ -7,12 +7,11 @@ import { appData, appUi, appView, appFileAt, appFoldSet, appMeasured } from './s
|
|
|
7
7
|
function appFileBox(i) {
|
|
8
8
|
const f = appData.files[i];
|
|
9
9
|
const where = appFileAt(i) + (f.path === null ? appUi.notOnHead : '');
|
|
10
|
-
|
|
10
|
+
const box = appBox(f.label, where + appUi.category
|
|
11
11
|
+ (f.categoryBy === 'config' ? appUi.categoryFromConfig : appUi.categoryByExtension),
|
|
12
|
-
appView.files[i], (e) =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
});
|
|
12
|
+
appView.files[i], (e) => appSwitch(i, e.target.checked));
|
|
13
|
+
appFields.file[i] = box.querySelector('input');
|
|
14
|
+
return box;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
/* A file that is not in the report: it stands in its place in the tree with its checkbox off and unavailable — no
|
|
@@ -48,7 +47,7 @@ function appNode() {
|
|
|
48
47
|
* a fraction ("2/5"): what matters to the reader is that the folder holds five files while two are measured. A folder
|
|
49
48
|
* without a single measured file stays in place with its checkbox off and unavailable: there is nothing to switch on
|
|
50
49
|
* in it. */
|
|
51
|
-
function appDirHead(name, sub) {
|
|
50
|
+
function appDirHead(name, here, sub) {
|
|
52
51
|
const idx = appIndexes(sub);
|
|
53
52
|
const total = appCount(sub);
|
|
54
53
|
const label = name + '/';
|
|
@@ -59,37 +58,49 @@ function appDirHead(name, sub) {
|
|
|
59
58
|
const on = idx.map((i) => appView.files[i]);
|
|
60
59
|
const every = on.every((v) => v);
|
|
61
60
|
head = appBox(label, appUi.dir.replace('{name}', name).replace('{n}', idx.length),
|
|
62
|
-
every, (e) =>
|
|
63
|
-
idx.forEach((i) => { appView.files[i] = e.target.checked; });
|
|
64
|
-
appRender();
|
|
65
|
-
}, 'dir');
|
|
61
|
+
every, (e) => appSwitchGroup(idx, e.target.checked), 'dir');
|
|
66
62
|
head.querySelector('input').indeterminate = !every && on.some((v) => v);
|
|
63
|
+
appFields.dir[here] = head;
|
|
67
64
|
}
|
|
68
65
|
head.appendChild(appEl('span', 'n', idx.length === total ? String(total) : idx.length + '/' + total));
|
|
69
66
|
return head;
|
|
70
67
|
}
|
|
71
68
|
|
|
69
|
+
/* The order inside one level, and the two rules of it: a hidden name (a leading dot) stands after every visible one —
|
|
70
|
+
* a project's service files are not what a reader looks for first — and otherwise the alphabet decides. "Other things
|
|
71
|
+
* being equal" is the whole of it: what the report holds stands before what it does not, and that partition is made
|
|
72
|
+
* before the names are compared. The rule is a function of two strings, which is what lets it be checked on its own
|
|
73
|
+
* rather than through a tree of a fixture that has no hidden files in it. */
|
|
74
|
+
export function appName(a, b) {
|
|
75
|
+
const hidden = (name) => (name.charAt(0) === '.' ? 1 : 0);
|
|
76
|
+
if (hidden(a) !== hidden(b)) return hidden(a) - hidden(b);
|
|
77
|
+
return a < b ? -1 : (a > b ? 1 : 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
/* The folder's sign is a click target of its own, separate from the checkbox: the checkbox answers for the numbers (it
|
|
73
81
|
* switches the subtree's files on), while the sign answers for how much of the tree is visible. One target for two
|
|
74
82
|
* different decisions would mean a folder can be folded only together with switching its files on. The sign is drawn
|
|
75
83
|
* as a span rather than a button and stands beside the label rather than inside it: a label is one click target, and a
|
|
76
84
|
* control nested in it would be reached as that same target.
|
|
77
85
|
*
|
|
86
|
+
* The tree opens folded — the sign of an untouched folder says so — and the reader's unfolding is what the memory
|
|
87
|
+
* keeps (`appFoldSet`).
|
|
88
|
+
*
|
|
78
89
|
* A click on the sign rebuilds nothing: the subtree lies in the markup and a class on the row hides it. A rebuild here
|
|
79
90
|
* would be honest work for nothing — it counts the whole table (every row by every column) and so pays for numbers
|
|
80
91
|
* folding does not change. That is why only the three things the reader sees change: the class, the sign and the note
|
|
81
92
|
* in the memory. */
|
|
82
93
|
function appFoldBox(name, path) {
|
|
83
|
-
const
|
|
84
|
-
const box = appEl('span', 'fold',
|
|
85
|
-
box.title = (
|
|
94
|
+
const open = appView.open[path] === true;
|
|
95
|
+
const box = appEl('span', 'fold', open ? '▾' : '▸');
|
|
96
|
+
box.title = (open ? appUi.foldClose : appUi.foldOpen).replace('{name}', name);
|
|
86
97
|
box.addEventListener('click', () => {
|
|
87
|
-
const now = !(appView.
|
|
98
|
+
const now = !(appView.open[path] === true);
|
|
88
99
|
appFoldSet(path, now);
|
|
89
100
|
const li = box.closest('li');
|
|
90
|
-
if (li !== null) li.classList.toggle('folded', now);
|
|
91
|
-
box.textContent = now ? '
|
|
92
|
-
box.title = (now ? appUi.
|
|
101
|
+
if (li !== null) li.classList.toggle('folded', !now);
|
|
102
|
+
box.textContent = now ? '▾' : '▸';
|
|
103
|
+
box.title = (now ? appUi.foldClose : appUi.foldOpen).replace('{name}', name);
|
|
93
104
|
});
|
|
94
105
|
return box;
|
|
95
106
|
}
|
|
@@ -101,17 +112,17 @@ function appLeaves(node) {
|
|
|
101
112
|
node.others.forEach((entry) => {
|
|
102
113
|
items.push({ name: entry.path.split('/').pop(), i: null, entry: entry });
|
|
103
114
|
});
|
|
104
|
-
return items.sort((a, b) => (a.name
|
|
115
|
+
return items.sort((a, b) => appName(a.name, b.name));
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
/* A folder row: the folding sign, the checkbox with the number of files and the subtree. A folded folder differs by
|
|
108
119
|
* its class alone — the markup stays the same. */
|
|
109
120
|
function appDir(name, sub, prefix) {
|
|
110
121
|
const here = prefix === '' ? name : prefix + '/' + name;
|
|
111
|
-
const folded = appView.
|
|
122
|
+
const folded = appView.open[here] !== true;
|
|
112
123
|
const li = appEl('li', folded ? 'folded' : null);
|
|
113
124
|
li.appendChild(appFoldBox(name, here));
|
|
114
|
-
li.appendChild(appDirHead(name, sub));
|
|
125
|
+
li.appendChild(appDirHead(name, here, sub));
|
|
115
126
|
li.appendChild(appTreeList(sub, here));
|
|
116
127
|
return li;
|
|
117
128
|
}
|
|
@@ -129,7 +140,7 @@ function appLeaf(leaf) {
|
|
|
129
140
|
* it does not distract from what is in the table, while it can still be found — in the same place where it was. */
|
|
130
141
|
function appTreeList(node, prefix) {
|
|
131
142
|
const list = appEl('ul', 'tree');
|
|
132
|
-
const dirs = [...node.dirs.keys()].sort()
|
|
143
|
+
const dirs = [...node.dirs.keys()].sort(appName)
|
|
133
144
|
.map((name) => ({ name: name, sub: node.dirs.get(name), inReport: appIndexes(node.dirs.get(name)).length > 0 }));
|
|
134
145
|
const leaves = appLeaves(node);
|
|
135
146
|
const inside = leaves.filter((leaf) => leaf.entry === null);
|
|
@@ -165,6 +176,10 @@ function appTree() {
|
|
|
165
176
|
return appTreeList(root, '');
|
|
166
177
|
}
|
|
167
178
|
|
|
179
|
+
/* The panel: built once, at the first drawing, and afterwards only its fields change. A rebuild would count the whole
|
|
180
|
+
* table for nothing — the tree, the counters and the tooltips are the same after every click — while it would also
|
|
181
|
+
* take the reader's place in the list away: the scroll of the panel and of the tree, and the field under the
|
|
182
|
+
* keyboard, would have to be put back by hand. Nothing of that is here, because there is nothing to put back. */
|
|
168
183
|
export function appPanel() {
|
|
169
184
|
const panel = document.getElementById('panel');
|
|
170
185
|
panel.textContent = '';
|
|
@@ -173,11 +188,12 @@ export function appPanel() {
|
|
|
173
188
|
metrics.appendChild(appEl('legend', null, appUi.metrics));
|
|
174
189
|
const mrow = appEl('div', 'row');
|
|
175
190
|
appData.metrics.forEach((m) => {
|
|
176
|
-
const
|
|
177
|
-
mrow.appendChild(appBox(m.label, m.note + ' · ' + word, appView.metrics[m.key], (e) => {
|
|
191
|
+
const box = appBox(m.label, m.note, appView.metrics[m.key], (e) => {
|
|
178
192
|
appView.metrics[m.key] = e.target.checked;
|
|
179
|
-
|
|
180
|
-
}, 'metric')
|
|
193
|
+
appSwitchMetric();
|
|
194
|
+
}, 'metric');
|
|
195
|
+
appFields.metric[m.key] = box.querySelector('input');
|
|
196
|
+
mrow.appendChild(box);
|
|
181
197
|
});
|
|
182
198
|
metrics.appendChild(mrow);
|
|
183
199
|
/* What produced each number is visible rather than hidden in a tooltip: the token dictionary and the way of
|
|
@@ -196,13 +212,102 @@ export function appPanel() {
|
|
|
196
212
|
appData.categories.forEach((cat) => {
|
|
197
213
|
const idx = [];
|
|
198
214
|
appData.files.forEach((f, i) => { if (f.category === cat.key) idx.push(i); });
|
|
199
|
-
|
|
200
|
-
(e) =>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}, 'all'));
|
|
215
|
+
const box = appBox(cat.label, appUi.all + ' · ' + cat.label, idx.every((i) => appView.files[i]),
|
|
216
|
+
(e) => appSwitchGroup(idx, e.target.checked), 'all');
|
|
217
|
+
appFields.cat[cat.key] = box.querySelector('input');
|
|
218
|
+
cats.appendChild(box);
|
|
204
219
|
});
|
|
205
220
|
files.appendChild(cats);
|
|
206
221
|
files.appendChild(appTree());
|
|
207
222
|
panel.appendChild(files);
|
|
208
223
|
}
|
|
224
|
+
|
|
225
|
+
/* -------- the panel's fields after a choice -------- */
|
|
226
|
+
|
|
227
|
+
/* The fields of the panel by name: the markup is built once, so a click needs a reference to the field it changes
|
|
228
|
+
* rather than a rebuild of the panel. Only a file owns a state — a folder and a category are ways to set the same
|
|
229
|
+
* boxes — which is why their fields are read from the files below them rather than kept. */
|
|
230
|
+
const appFields = { metric: {}, file: {}, dir: {}, cat: {} };
|
|
231
|
+
|
|
232
|
+
/* The boxes of a row's own subtree, read from the tree itself: the panel keeps no second list of the files a folder
|
|
233
|
+
* holds, and what the reader sees is exactly the boxes that are here. A file outside the report stands in the tree
|
|
234
|
+
* with nothing to switch, hence it is left out. */
|
|
235
|
+
function appRowBoxes(box) {
|
|
236
|
+
return [...box.closest('li').querySelectorAll('.box:not(.dir):not(.plain) input')];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// A folder's field from its files: all on — checked, some — the third state, none — simply unchecked.
|
|
240
|
+
function appDirState(box) {
|
|
241
|
+
const boxes = appRowBoxes(box);
|
|
242
|
+
const on = boxes.filter((b) => b.checked).length;
|
|
243
|
+
const input = box.querySelector('input');
|
|
244
|
+
input.checked = on === boxes.length;
|
|
245
|
+
input.indeterminate = on > 0 && on < boxes.length;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* A category's field from its files — the same rule, taken from the data: a category's files are named by the
|
|
249
|
+
* category itself (`category`), and the boxes of the tree are a different view of the same files. */
|
|
250
|
+
function appCatState(key) {
|
|
251
|
+
let all = 0;
|
|
252
|
+
let on = 0;
|
|
253
|
+
appData.files.forEach((f, i) => {
|
|
254
|
+
if (f.category !== key) return;
|
|
255
|
+
all++;
|
|
256
|
+
if (appView.files[i] === true) on++;
|
|
257
|
+
});
|
|
258
|
+
const input = appFields.cat[key];
|
|
259
|
+
input.checked = on === all;
|
|
260
|
+
input.indeterminate = on > 0 && on < all;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* The folders a file lies in: the prefixes of its path, from the root down. The tree's folders are exactly those
|
|
264
|
+
* prefixes — that is how it is built (`appLeafAt`) — so no second naming rule is needed. */
|
|
265
|
+
function appDirPath(path) {
|
|
266
|
+
const parts = path.split('/');
|
|
267
|
+
return parts.slice(0, -1).map((_part, i) => parts.slice(0, i + 1).join('/'));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* A click reaches a folder's field through the files below it: every folder on the path of a switched file shows the
|
|
271
|
+
* share of what is left on. A folder without measured files has a field of its own too — it is off and unavailable,
|
|
272
|
+
* which is not the reader's state and must not be overwritten here. */
|
|
273
|
+
function appDirsOf(indexes) {
|
|
274
|
+
const seen = {};
|
|
275
|
+
indexes.forEach((i) => {
|
|
276
|
+
appDirPath(appFileAt(i)).forEach((path) => {
|
|
277
|
+
if (seen[path] === true) return;
|
|
278
|
+
seen[path] = true;
|
|
279
|
+
if (appFields.dir[path] !== undefined) appDirState(appFields.dir[path]);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// The same for the quick buttons of the categories: every category one of the switched files belongs to.
|
|
285
|
+
function appCatsOf(indexes) {
|
|
286
|
+
const seen = {};
|
|
287
|
+
indexes.forEach((i) => {
|
|
288
|
+
const key = appData.files[i].category;
|
|
289
|
+
if (seen[key] === true || appFields.cat[key] === undefined) return;
|
|
290
|
+
seen[key] = true;
|
|
291
|
+
appCatState(key);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* What a click changed, written where it stands: the files' own boxes, then the fields of the folders and categories
|
|
296
|
+
* that hold them. Nothing is rebuilt, and no field the choice did not reach is touched. */
|
|
297
|
+
export function appPanelState(indexes) {
|
|
298
|
+
indexes.forEach((i) => {
|
|
299
|
+
const input = appFields.file[i];
|
|
300
|
+
if (input !== undefined) input.checked = appView.files[i] === true;
|
|
301
|
+
});
|
|
302
|
+
appDirsOf(indexes);
|
|
303
|
+
appCatsOf(indexes);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* The whole panel from the view: what a link, a record from the memory and the first drawing need. The metric boxes
|
|
307
|
+
* are the reader's own click otherwise, which is why they are not refreshed on a file's switch. */
|
|
308
|
+
export function appPanelAll() {
|
|
309
|
+
appData.categories.forEach((c) => appCatState(c.key));
|
|
310
|
+
appData.metrics.forEach((m) => { appFields.metric[m.key].checked = appView.metrics[m.key] === true; });
|
|
311
|
+
appData.files.forEach((_f, i) => { appFields.file[i].checked = appView.files[i] === true; });
|
|
312
|
+
Object.keys(appFields.dir).forEach((path) => appDirState(appFields.dir[path]));
|
|
313
|
+
}
|
|
@@ -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
|
+
}
|
package/src/page/state.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { appDecode } from './payload.js';
|
|
2
|
+
|
|
1
3
|
/* The page's choice state: what is switched on, how it survives a closing and how it travels as a link.
|
|
2
4
|
*
|
|
3
5
|
* An ordinary source file rather than a string inside the engine: a linter sees it, and it is pasted into the assembled
|
|
@@ -9,41 +11,61 @@
|
|
|
9
11
|
* network: the styling arrives in the same file, and the cell markup follows the rules of the shared part of the styling
|
|
10
12
|
* (`clip`, a commit's caption).
|
|
11
13
|
*
|
|
12
|
-
* The panel remembers the reader's choice between visits
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* The panel remembers the reader's choice between visits ("the choice's memory" below): the record is tied to the
|
|
15
|
+
* report's passport and keeps only what is switched off, by name, so someone else's record is not applied while a
|
|
16
|
+
* vanished name simply means nothing. The record stays in the browser's memory and nowhere else: a report opened from
|
|
17
|
+
* disk keeps a clean address, and a link made in an earlier release is still read (`appLinkUse`).
|
|
16
18
|
*
|
|
17
|
-
* The page
|
|
18
|
-
*
|
|
19
|
-
* second rule of accuracy. */
|
|
19
|
+
* The page draws no conclusion about how a number was obtained: the method of each metric arrives in the data, and the
|
|
20
|
+
* page prints it. There is no second rule of counting here, and no vocabulary of precision either. */
|
|
20
21
|
|
|
21
|
-
export const appData = JSON.parse(document.getElementById('data').textContent);
|
|
22
22
|
export const appUi = JSON.parse(document.getElementById('ui').textContent);
|
|
23
|
-
export const appView = { metrics: {}, files: [], folded: {} };
|
|
24
|
-
appData.metrics.forEach((m) => { appView.metrics[m.key] = true; });
|
|
25
|
-
appData.files.forEach(() => { appView.files.push(true); });
|
|
26
23
|
|
|
27
|
-
/*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
/* The model is not a constant any longer: the block in the file is **packed** (gzipped and base64 encoded,
|
|
25
|
+
* `appUnpack` of the payload chapter), so unpacking is asynchronous and the model is set once, by `appBoot`,
|
|
26
|
+
* before anything is drawn and before any chapter below reads it. The names, their shapes and their order are what
|
|
27
|
+
* they were; only their appearance moved — from the parse to that one call. */
|
|
28
|
+
export let appData = null;
|
|
29
|
+
export let appView = null;
|
|
30
|
+
export let appMetric = null;
|
|
31
|
+
export let appMeasured = null;
|
|
32
|
+
let appKey = null;
|
|
33
|
+
let appFoldKey = null;
|
|
31
34
|
|
|
32
|
-
/*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/* One place that turns the unpacked block into what the chapters speak: the sparse form is unrolled by the payload
|
|
36
|
+
* chapter into the dense contract every number comes from — snapshots per commit, the texts themselves, "now" as
|
|
37
|
+
* the state at HEAD — and the view starts switched on whole.
|
|
38
|
+
*
|
|
39
|
+
* A metric's description by key stands here too: the way the number was obtained is text under the switches, so the
|
|
40
|
+
* page holds no second answer about "counted with what".
|
|
41
|
+
*
|
|
42
|
+
* A column by a file's path is the pointer that decides whether a leaf of the tree is a checkbox or a caption (the
|
|
43
|
+
* page's tree is the project's tree while the numbers belong to columns only); the name follows the same rule as the
|
|
44
|
+
* choice's record (`appFileAt`), so the tree and the reader's memory cannot drift apart.
|
|
45
|
+
*
|
|
46
|
+
* The key the reader's memory lives under is counted here as well, because it is the report's passport: it depends
|
|
47
|
+
* on the data, and until the block is unpacked there is nothing to count it from. */
|
|
48
|
+
export function appBoot(text) {
|
|
49
|
+
appData = appDecode(JSON.parse(text));
|
|
50
|
+
/* The tree is folded as it opens: a project's tree is longer than the window, and the reader's first look is at a
|
|
51
|
+
* short list rather than at everything. What is remembered is the opposite — the folders the reader unfolded
|
|
52
|
+
* (`appFoldRead`). */
|
|
53
|
+
appView = { metrics: {}, files: [], open: {} };
|
|
54
|
+
appMetric = {};
|
|
55
|
+
appMeasured = {};
|
|
56
|
+
appData.metrics.forEach((m) => { appView.metrics[m.key] = true; appMetric[m.key] = m; });
|
|
57
|
+
appData.files.forEach((_f, i) => { appView.files.push(true); appMeasured[appFileAt(i)] = i; });
|
|
58
|
+
appKey = 'size-report:' + appPassport();
|
|
59
|
+
appFoldKey = appKey + ':tree';
|
|
60
|
+
}
|
|
37
61
|
|
|
38
62
|
/* A link is that same choice in the address, under a name of its own: someone else's anchor on the page does not count
|
|
39
|
-
* as a link, and there is nothing to argue with it about.
|
|
63
|
+
* as a link, and there is nothing to argue with it about. The page does not write it any more — it only reads what came
|
|
64
|
+
* with the address — and this paragraph is what such a link is read by. */
|
|
40
65
|
const APP_LINK = '#size-report=';
|
|
41
66
|
|
|
42
|
-
/*
|
|
43
|
-
*
|
|
44
|
-
* the message about the link has not faded yet. */
|
|
45
|
-
let appStartup = true;
|
|
46
|
-
let appForeign = false;
|
|
67
|
+
/* One circumstance of the first drawing, and it acts on it alone: the memory is not written while somebody else's link
|
|
68
|
+
* is open — what came in is not the reader's choice, and only his own action makes it his. */
|
|
47
69
|
let appTransient = false;
|
|
48
70
|
|
|
49
71
|
/* -------- the reader's memory of his choice -------- */
|
|
@@ -70,12 +92,19 @@ function appHash(text) {
|
|
|
70
92
|
* report's order. It is what tells one report from another — the record's key is chosen by it, so a choice made in
|
|
71
93
|
* someone else's report is not picked up. The package version and the top of the history are absent on purpose: this is
|
|
72
94
|
* the same report — updating the tool does not change what a column means, while a grown history is the very history the
|
|
73
|
-
* reader comes back to.
|
|
95
|
+
* reader comes back to.
|
|
96
|
+
*
|
|
97
|
+
* Counted once per document: it is a constant of the report, which depends on nothing the reader can change, and every
|
|
98
|
+
* click asks for it (the key of the memory and the passport of the record). A second count would be a second answer
|
|
99
|
+
* waiting to happen, and the labels it reads do not change while the page is open. */
|
|
100
|
+
let appPassportValue = null;
|
|
74
101
|
function appPassport() {
|
|
75
|
-
|
|
76
|
-
appData.
|
|
102
|
+
if (appPassportValue === null) {
|
|
103
|
+
appPassportValue = appHash([appData.tool.name, appData.schema, appData.report.artifact,
|
|
104
|
+
appData.report.title, appData.files.map((f) => f.label).join('|')].join('\n'));
|
|
105
|
+
}
|
|
106
|
+
return appPassportValue;
|
|
77
107
|
}
|
|
78
|
-
const appKey = 'size-report:' + appPassport();
|
|
79
108
|
|
|
80
109
|
/* One record of the choice for everything: it goes both into the memory and into the address, so there are no two formats
|
|
81
110
|
* of one state. Only what is switched off is kept, by name: "switched on" and "no record" are the same state, which is
|
|
@@ -93,32 +122,27 @@ function appRecordOk(rec) {
|
|
|
93
122
|
return rec !== null && typeof rec === 'object' && rec.v === 1 && rec.passport === appPassport();
|
|
94
123
|
}
|
|
95
124
|
|
|
125
|
+
/* The record goes into the browser's memory and nowhere else: it is written on the click itself, which is what survives
|
|
126
|
+
* a closing, and the page's address keeps a clean tail — the report is a local page whose address is copied as it is,
|
|
127
|
+
* and a reader's choice belongs in the browser that made it rather than in the tab's title bar. What a link sent from an
|
|
128
|
+
* earlier release holds is still read (`appLinkUse`), and it is not written into the reader's memory: what came in is
|
|
129
|
+
* not his choice until he changes something. */
|
|
96
130
|
export function appWrite() {
|
|
131
|
+
if (appTransient) return;
|
|
97
132
|
const rec = appRecord();
|
|
98
133
|
const empty = Object.keys(rec.metrics).length === 0 && Object.keys(rec.files).length === 0;
|
|
99
|
-
if (!appTransient) {
|
|
100
|
-
try {
|
|
101
|
-
if (empty) window.localStorage.removeItem(appKey);
|
|
102
|
-
else window.localStorage.setItem(appKey, JSON.stringify(rec));
|
|
103
|
-
} catch (_e) {
|
|
104
|
-
/* There is no memory (the browser grants this page none): the choice will not survive a closing, while the numbers
|
|
105
|
-
* and the markup do not depend on it. */
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/* The address is the link for a colleague, which is why it repeats the choice. But not during the first drawing and not
|
|
109
|
-
* when the link turned out to be someone else's: an address that came in is not ours, and the reader has yet to read
|
|
110
|
-
* it. */
|
|
111
|
-
if (appStartup || appForeign) return;
|
|
112
134
|
try {
|
|
113
|
-
window.
|
|
135
|
+
if (empty) window.localStorage.removeItem(appKey);
|
|
136
|
+
else window.localStorage.setItem(appKey, JSON.stringify(rec));
|
|
114
137
|
} catch (_e) {
|
|
115
|
-
/*
|
|
138
|
+
/* There is no memory (the browser grants this page none): the choice will not survive a closing, while the numbers
|
|
139
|
+
* and the markup do not depend on it. */
|
|
116
140
|
}
|
|
117
141
|
}
|
|
118
142
|
|
|
119
143
|
/* A reset to "everything on": the border between "this is no longer in the report" and "switched off" is the record
|
|
120
|
-
* rather than a missing value. A link carries the sender's whole choice, which is why it is applied to a clean view
|
|
121
|
-
* than on top of someone else's. */
|
|
144
|
+
* rather than a missing value. A link carries the sender's whole choice, which is why it is applied to a clean view
|
|
145
|
+
* rather than on top of someone else's. */
|
|
122
146
|
function appAll() {
|
|
123
147
|
appData.metrics.forEach((m) => { appView.metrics[m.key] = true; });
|
|
124
148
|
appData.files.forEach((_f, i) => { appView.files[i] = true; });
|
|
@@ -218,14 +242,15 @@ export function appApply(rec) {
|
|
|
218
242
|
appData.files.forEach((_f, i) => { if (files[appFileAt(i)] === false) appView.files[i] = false; });
|
|
219
243
|
}
|
|
220
244
|
|
|
221
|
-
/* -------- the
|
|
222
|
-
|
|
223
|
-
/* Folded folders are a memory of the same kind as the choice, but of a record of their own: it is about how much of the
|
|
224
|
-
* tree is visible rather than about which numbers are read. Hence it does not go into the address: a link is sent for the
|
|
225
|
-
* sake of the numbers, while an unfolded tree is the onlooker's business. As with the choice, only what is folded is kept
|
|
226
|
-
* (`true`), and a folder's name is its path ("src/page"), so a vanished name simply means nothing. */
|
|
227
|
-
const appFoldKey = appKey + ':tree';
|
|
245
|
+
/* -------- the unfolded tree -------- */
|
|
228
246
|
|
|
247
|
+
/* How much of the tree is visible is a memory of the same kind as the choice, but of a record of its own: it is about
|
|
248
|
+
* what the onlooker looks at rather than about which numbers are read, which is why it never goes into a link and never
|
|
249
|
+
* leaves the browser. The tree opens folded, so **the unfolded folders are what is kept** (`true`) — the default is the
|
|
250
|
+
* absence of the name, the same way "switched on" is the absence of a choice. A folder's name is its path ("src/page"),
|
|
251
|
+
* so a vanished name simply means nothing, and unfolding nothing is the state the page opens in: then the record is not
|
|
252
|
+
* kept at all rather than being kept empty. `appFoldKey` is set with the model (`appBoot`), for the reason the key
|
|
253
|
+
* itself is. */
|
|
229
254
|
export function appFoldRead() {
|
|
230
255
|
let text = null;
|
|
231
256
|
try {
|
|
@@ -241,19 +266,19 @@ export function appFoldRead() {
|
|
|
241
266
|
return;
|
|
242
267
|
}
|
|
243
268
|
if (!appRecordOk(rec)) return;
|
|
244
|
-
const
|
|
245
|
-
Object.keys(
|
|
269
|
+
const open = rec.open || {};
|
|
270
|
+
Object.keys(open).forEach((p) => { if (open[p] === true) appView.open[p] = true; });
|
|
246
271
|
}
|
|
247
272
|
|
|
248
|
-
export function appFoldSet(path,
|
|
249
|
-
if (
|
|
250
|
-
else delete appView.
|
|
251
|
-
const rec = { v: 1, passport: appPassport(),
|
|
273
|
+
export function appFoldSet(path, open) {
|
|
274
|
+
if (open) appView.open[path] = true;
|
|
275
|
+
else delete appView.open[path];
|
|
276
|
+
const rec = { v: 1, passport: appPassport(), open: Object.assign({}, appView.open) };
|
|
252
277
|
try {
|
|
253
|
-
if (Object.keys(rec.
|
|
278
|
+
if (Object.keys(rec.open).length === 0) window.localStorage.removeItem(appFoldKey);
|
|
254
279
|
else window.localStorage.setItem(appFoldKey, JSON.stringify(rec));
|
|
255
280
|
} catch (_e) {
|
|
256
|
-
/* There is no memory: what is
|
|
257
|
-
*
|
|
281
|
+
/* There is no memory: what is unfolded will not survive a closing, while the view does not depend on it — the tree
|
|
282
|
+
* is unfolded exactly the way the reader unfolded it just now. */
|
|
258
283
|
}
|
|
259
284
|
}
|