@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.
Files changed (48) hide show
  1. package/README.md +989 -1081
  2. package/bin/postinstall.js +17 -18
  3. package/bin/size.js +2 -2
  4. package/package.json +3 -4
  5. package/src/args.js +72 -72
  6. package/src/artifact.js +14 -14
  7. package/src/check.js +41 -42
  8. package/src/cli.js +26 -29
  9. package/src/config.js +87 -91
  10. package/src/css.js +14 -14
  11. package/src/data.js +26 -50
  12. package/src/derived.js +31 -35
  13. package/src/doctor.js +95 -99
  14. package/src/explain.js +46 -47
  15. package/src/git.js +66 -71
  16. package/src/history.js +74 -83
  17. package/src/hook.js +130 -149
  18. package/src/init.js +37 -37
  19. package/src/journal.js +17 -15
  20. package/src/locales.js +31 -22
  21. package/src/metrics.js +72 -89
  22. package/src/minify.js +28 -27
  23. package/src/modes.js +57 -60
  24. package/src/optional.js +13 -11
  25. package/src/page/app.css +76 -94
  26. package/src/page/app.js +124 -80
  27. package/src/page/build.js +193 -50
  28. package/src/page/dom.js +8 -9
  29. package/src/page/panel.js +157 -69
  30. package/src/page/payload.js +168 -0
  31. package/src/page/state.js +144 -104
  32. package/src/page/table.js +270 -86
  33. package/src/parse-worker.js +10 -10
  34. package/src/parse.js +43 -45
  35. package/src/project.js +100 -104
  36. package/src/refusal.js +75 -76
  37. package/src/size-table.js +41 -76
  38. package/src/strip/forms.js +5 -5
  39. package/src/strip/guard.js +28 -28
  40. package/src/strip/js.js +27 -27
  41. package/src/strip.js +17 -21
  42. package/src/table.css +54 -19
  43. package/src/tokens.js +27 -27
  44. package/src/tool.js +10 -11
  45. package/templates/README.md +71 -77
  46. package/templates/ci.yml +33 -33
  47. package/templates/size-report.config.json +3 -3
  48. package/CHANGELOG.md +0 -690
package/src/page/table.js CHANGED
@@ -1,51 +1,51 @@
1
- import { cellParts, commitParts, nowModel, rowModel, valueParts } from '../derived.js';
1
+ import { cellParts, commitParts, deltaOf, nowModel, rowModel, valueParts } from '../derived.js';
2
2
  import { appEl } from './dom.js';
3
- import { appData, appUi, appView, appMetric } from './state.js';
4
-
5
- /* Приближённая клетка: пометка берётся из данных движка, а не выводится здесь из
6
- * пути файла, правило точности живёт там же, где считаются числа. У клетки
7
- * файла это её собственное число, у итога худшее из вошедших в него, иначе
8
- * сумма обещала бы точность, которой нет у слагаемых. Возвращается описание
9
- * метрики (её способ и идёт в подсказку клетки) либо ничего. */
10
- export function appApprox(where, files, key) {
11
- const marks = appData.approx[key];
12
- if (marks === undefined) return null;
13
- const row = where === 'now' ? marks.now : marks.rows;
14
- const base = where === 'now' ? 0 : where * appData.files.length;
15
- for (let i = 0; i < files.length; i++) {
16
- if (row.charAt(base + files[i]) === '1') return appMetric[key];
17
- }
18
- return null;
19
- }
20
-
21
- /* Класс клетки собирается в одном месте: и пометка приближения, и пропуск
22
- * («файла нет») — свойства самой клетки, а не её содержимого. */
23
- function appCellClass(first, miss, approx) {
24
- return 'num' + (first ? ' g' : '') + (miss ? ' miss' : '') + (approx === null ? '' : ' approx');
25
- }
26
-
27
- // Разметка клетки строки-коммита: правила в cellParts, здесь только узел.
28
- export function appCell(cell, first, approx) {
29
- const parts = cellParts(cell.value, cell.delta, '');
30
- const td = appEl('td', appCellClass(first, parts.miss, approx));
31
- if (approx !== null) td.title = appUi.approxCell + approx.method;
3
+ import { appData, appUi, appView } from './state.js';
4
+
5
+ /* The table is built once and then only shown and hidden. That rests on three things that do not depend on the
6
+ * reader: a cell's content and colour come from the pair (commit, file) and a metric (`cellParts`, `valueParts`),
7
+ * the order of the columns comes from the files alone, and only the visibility and the totals are the choice's.
8
+ * Hence nothing below builds a node after the first drawing — a click writes a class and a number.
9
+ *
10
+ * What makes this cheap is a cache of references to the nodes of every column (`appTable` returns it): reaching a
11
+ * cell through the markup costs several times what changing it does (measured: `tr.children[i]` 0.0254 ms against
12
+ * `classList.toggle` 0.0070 ms), and the first drawing creates every node anyway, so collecting them is free. The
13
+ * columns of the fixed layout a `<col>` per column, carrying the counted width — are collected there too.
14
+ *
15
+ * The totals are the only numbers that move with the choice, and they are carried rather than recounted: a sum is
16
+ * linear, so a file switched off subtracts exactly its own numbers — the same integers the first drawing wrote.
17
+ */
18
+
19
+ /* The cell's class: the metric's track (`m0`, `m1`, … — one class per metric, which is what hides a whole metric
20
+ * at once), the group it belongs to (every cell of a group, not the first one: the group's left border is carried
21
+ * by the first *enabled* metric, and the styling decides which that is), and the gap ("no such file"). */
22
+ function appCellClass(mi, miss) {
23
+ return 'num g m' + mi + (miss ? ' miss' : '');
24
+ }
25
+
26
+ /* A cell filled where it stands: the totals are written again on every click, and a cell has to forget its own
27
+ * content firsta delta is a node of its own, so `textContent` alone would leave it behind. */
28
+ function appFill(td, parts) {
29
+ td.textContent = '';
32
30
  if (parts.dir === null) td.textContent = parts.text;
33
31
  else td.appendChild(appEl('span', 'delta ' + parts.dir, parts.text));
34
32
  return td;
35
33
  }
36
34
 
37
- // Разметка клетки верхней строки: правила в valueParts.
38
- export function appValueCell(value, first, approx) {
35
+ // The markup of a commit row's cell: the rules live in cellParts, only the node is here.
36
+ export function appCell(cell, mi) {
37
+ const parts = cellParts(cell.value, cell.delta, '−');
38
+ return appFill(appEl('td', appCellClass(mi, parts.miss)), parts);
39
+ }
40
+
41
+ // The markup of the top row's cell: the rules live in valueParts.
42
+ export function appValueCell(value, mi) {
39
43
  const parts = valueParts(value);
40
- const td = appEl('td', appCellClass(first, parts.miss, approx));
41
- if (approx !== null) td.title = appUi.approxCell + approx.method;
42
- td.textContent = parts.text;
43
- return td;
44
+ return appFill(appEl('td', appCellClass(mi, parts.miss)), parts);
44
45
  }
45
46
 
46
- /* Подпись коммита той же разметкой, что в статической таблице: дата, тема,
47
- * метка раздела журнала. Ширину колонки и обрезку длинной темы задаёт общая часть
48
- * оформления, поэтому колонка не прыгает при переключении файлов. */
47
+ /* A commit's caption: the date, the subject, the journal section's mark. The column's width and the clipping of a long
48
+ * subject come from the shared part of the styling, which is why the column does not jump when files are switched. */
49
49
  export function appCommit(row) {
50
50
  const parts = commitParts(row, appData.report.showSha, row.href);
51
51
  const name = parts.href ? appEl('a', 'subj', parts.subject) : appEl('span', 'subj', parts.subject);
@@ -63,15 +63,9 @@ export function appCommit(row) {
63
63
  return th;
64
64
  }
65
65
 
66
- export function appSubHead(metrics) {
67
- const tr = appEl('tr');
68
- metrics.forEach((m, mi) => tr.appendChild(appEl('th', mi === 0 ? 'g' : '', m.label)));
69
- return tr;
70
- }
71
-
72
- /* Состояния пустоты: когда чисел не будет вовсе, страница говорит об этом словами,
73
- * а не сеткой без колонок. Файлы можно выключить все — тогда остаётся общий объём,
74
- * и подсказка объясняет, почему колонок нет. */
66
+ /* The empty states: when there will be no numbers at all, the page says so in words rather than showing a grid without
67
+ * columns. Every file can be switched off — then the total volume remains, and the note explains why there are no
68
+ * columns. The table itself stands there either way: this is about what is shown, not about what exists. */
75
69
  export function appState(metricsCount, filesCount) {
76
70
  const state = document.getElementById('state');
77
71
  const text = metricsCount === 0 ? appUi.empty : (filesCount === 0 ? appUi.noFiles : '');
@@ -80,26 +74,44 @@ export function appState(metricsCount, filesCount) {
80
74
  document.getElementById('shell').hidden = metricsCount === 0;
81
75
  }
82
76
 
83
- /* Шапка: строка групп (итог и файлы) и строка метрик под ней. Метрики повторяются
84
- * на каждый файл, поэтому подшапка собирается один раз, а дальше её узлы
85
- * переезжают в следующие копий разметки не заводится. */
86
- export function appHead(shown, files, metrics) {
77
+ /* The order of the columns: the files the last commit touched come first — the report is rebuilt after every
78
+ * commit, and a reader's first question is what that edit brought. Inside each part the order stays as it comes
79
+ * from the settings, and it depends on the files rather than on the choice: that is what lets a column be hidden
80
+ * without moving the others. */
81
+ export function appOrder() {
82
+ const files = [];
83
+ appData.files.forEach((_f, i) => files.push(i));
84
+ files.sort((a, b) => (appData.last[a] === true ? 0 : 1) - (appData.last[b] === true ? 0 : 1));
85
+ return files;
86
+ }
87
+
88
+ /* The header: a row of groups (the total and the files) and a row of metrics under it — every metric of every file,
89
+ * because nothing here knows the choice. The headings whose `colSpan` follows the number of enabled metrics and the
90
+ * headings of each file's column are collected in the cache: they are the nodes a click has to touch. */
91
+ export function appHead(files, metrics, cache) {
87
92
  const head = appEl('tr');
88
93
  const commit = appEl('th', 'c-commit', appUi.commit);
89
94
  commit.rowSpan = 2;
90
95
  head.appendChild(commit);
91
- const total = appEl('th', 'g', appUi.total);
92
- total.colSpan = metrics.length;
93
- head.appendChild(total);
94
- files.forEach((i) => {
95
- const th = appEl('th', 'g', appData.files[i].label);
96
+ const subs = appEl('tr');
97
+ const group = (label, i) => {
98
+ const th = appEl('th', 'gh', label);
96
99
  th.colSpan = metrics.length;
97
100
  head.appendChild(th);
98
- });
99
- const subs = appSubHead(shown);
100
- files.forEach(() => {
101
- const more = appSubHead(shown);
102
- while (more.firstChild) subs.appendChild(more.firstChild);
101
+ cache.spans.push(th);
102
+ /* The group's own heading belongs to the column: hiding a file has to take its caption with it, or the header
103
+ * would keep a name over numbers that are gone. */
104
+ if (i !== null) cache.cols[i].push(th);
105
+ metrics.forEach((_key, mi) => {
106
+ const cell = appEl('th', 'g m' + mi, appData.metrics[mi].label);
107
+ subs.appendChild(cell);
108
+ if (i !== null) cache.cols[i].push(cell);
109
+ });
110
+ };
111
+ group(appUi.total, null);
112
+ files.forEach((i) => {
113
+ cache.cols[i] = [];
114
+ group(appData.files[i].label, i);
103
115
  });
104
116
  const thead = appEl('thead');
105
117
  thead.appendChild(head);
@@ -107,39 +119,211 @@ export function appHead(shown, files, metrics) {
107
119
  return thead;
108
120
  }
109
121
 
110
- /* Строка-коммит: подпись и числа. Дельты считает общий расчёт (`rowModel`) тот
111
- * же, что считает статическую таблицу; здесь только узлы. Пометка приближения
112
- * своя у каждой клетки: у итога по всем вошедшим файлам, у файла по нему самому. */
113
- export function appRow(r, metrics, files) {
122
+ /* A text's breadth in `ch`, the unit a column is measured in: a digit is exactly one `ch` in this table's font
123
+ * (measured in Chrome: 8.67px against 8.67px), and a thin space is counted as a third of a digit nothing else is
124
+ * discounted. Without that one fraction every number column would come out a fifth wider than the number in it (the
125
+ * grouping makes a text 21 % narrower than its characters, measured), which would give back more than the padding and
126
+ * the clipping of this step save. What is left over-measures — a letter is 0.84 of a `ch`, a slash 0.48 — and that is
127
+ * the safe side: a cell clips nothing, so a column a character short would show a number running over its neighbour. */
128
+ function appBreadth(text) {
129
+ let wide = 0;
130
+ for (let i = 0; i < text.length; i++) wide += text[i] === '\u2009' ? 0.35 : 1;
131
+ return wide;
132
+ }
133
+
134
+ /* The widest text of every column, counted before a single node is made: the numbers of the commit rows — through
135
+ * `rowModel`, the very model the cells are drawn from, so a column cannot be sized for a number other than the one
136
+ * that will stand in it — the absolute sizes of the "now" row and the metric's own caption. */
137
+ function appWidest(files, keys) {
138
+ const wide = files.map(() => keys.map(() => 0));
139
+ const total = keys.map(() => 0);
140
+ const put = (into, mi, breadth) => { if (breadth > into[mi]) into[mi] = breadth; };
141
+ const delta = (cell) => appBreadth(cellParts(cell.value, cell.delta, '−').text);
142
+ const value = (v) => appBreadth(valueParts(v).text);
143
+ appData.rows.forEach((row, r) => {
144
+ const model = rowModel(row.values, r === 0 ? null : appData.rows[r - 1].values, keys);
145
+ model.total.forEach((cell, mi) => put(total, mi, delta(cell)));
146
+ files.forEach((i) => model.files[i].forEach((cell, mi) => put(wide[i], mi, delta(cell))));
147
+ });
148
+ const now = nowModel(appData.now, keys);
149
+ now.total.forEach((v, mi) => put(total, mi, value(v)));
150
+ files.forEach((i) => now.files[i].forEach((v, mi) => put(wide[i], mi, value(v))));
151
+ keys.forEach((_key, mi) => {
152
+ const caption = appBreadth(appData.metrics[mi].label);
153
+ put(total, mi, caption);
154
+ wide.forEach((per) => put(per, mi, caption));
155
+ });
156
+ return { total: total, files: wide };
157
+ }
158
+
159
+ /* A group's heading — a file's name, the word over the total — is one line over the columns of that group
160
+ * (`white-space: nowrap` in the shared styling), so the group as a whole has to be wide enough for it, or the names of
161
+ * two neighbouring groups would run into one another. The caption is divided among the metrics of the group rather than
162
+ * weighed against their sum: every column carries at least its share, so the group can never come out narrower than the
163
+ * caption, while a column that is wider anyway keeps its own count. */
164
+ function appGroupWide(per, label) {
165
+ const share = Math.ceil(appBreadth(label) / per.length);
166
+ return per.map((n) => Math.max(n, share));
167
+ }
168
+
169
+ /* The columns, before the rows: `table-layout: fixed` reads the first row of the table and takes the widths from there,
170
+ * so they are known while the table is built rather than measured by the browser over every cell of it. A column
171
+ * carries the metric's class (`m0`, `m1`, …) — which is what hides a whole metric at once — and its counted width in
172
+ * `ch`; the styling adds the padding and the border to it (`#grid col`), because a count and a drawing are one sum
173
+ * rather than two. The columns of a file are collected where its cells are, so one switch writes one class over both. */
174
+ function appCols(files, wide, cache) {
175
+ const group = appEl('colgroup');
176
+ const add = (n, mi) => {
177
+ const col = appEl('col', 'm' + mi);
178
+ col.style.setProperty('--ch', n + 'ch');
179
+ group.appendChild(col);
180
+ return col;
181
+ };
182
+ group.appendChild(appEl('col', 'c-commit'));
183
+ appGroupWide(wide.total, appUi.total).forEach(add);
184
+ files.forEach((i) => appGroupWide(wide.files[i], appData.files[i].label)
185
+ .forEach((n, mi) => { cache.cols[i].push(add(n, mi)); }));
186
+ return group;
187
+ }
188
+
189
+ /* A file's share of a row: one cell per metric, in the order of the table's columns, and each is put into its
190
+ * column's cache while it is made. The maker is what tells a commit's delta from an absolute size at HEAD — the walk
191
+ * over the files is one, and there is no second one to drift away. */
192
+ function appRowCells(tr, model, files, cache, make) {
193
+ files.forEach((i) => {
194
+ model.files[i].forEach((value, mi) => {
195
+ const td = make(value, mi);
196
+ cache.cols[i].push(td);
197
+ tr.appendChild(td);
198
+ });
199
+ });
200
+ }
201
+
202
+ /* A commit row: the caption and the numbers. The deltas come from the shared calculation (`rowModel`) rather than
203
+ * from here — two ways to count one row would be two answers. The model is taken whole (every file) because the
204
+ * table holds every column, and the column order of the table is applied by taking the model by the file's own
205
+ * index: the model lists the files the way the data does, not the way the columns stand. */
206
+ export function appRow(r, files, metrics, cache) {
114
207
  const row = appData.rows[r];
115
208
  const prev = r === 0 ? null : appData.rows[r - 1];
116
- const model = rowModel(row.values, prev === null ? null : prev.values, metrics, appView.files);
209
+ const model = rowModel(row.values, prev === null ? null : prev.values, metrics);
117
210
  const tr = appEl('tr');
118
211
  tr.appendChild(appCommit(row));
119
212
  model.total.forEach((cell, mi) => {
120
- tr.appendChild(appCell(cell, mi === 0, appApprox(r, files, metrics[mi])));
213
+ const td = appCell(cell, mi);
214
+ cache.sums[mi][r] = cell.value;
215
+ cache.cells[mi][r] = td;
216
+ tr.appendChild(td);
121
217
  });
122
- model.files.forEach((cells, fi) => cells.forEach((cell, mi) => {
123
- tr.appendChild(appCell(cell, mi === 0, appApprox(r, [files[fi]], metrics[mi])));
124
- }));
218
+ appRowCells(tr, model, files, cache, appCell);
125
219
  return tr;
126
220
  }
127
221
 
128
- /* Тело: строки коммитов снизу вверх (свежие первыми) плюс верхняя строка «сейчас»
129
- * с абсолютными размерами на HEAD. Дельты под ней сходятся с ней, поэтому она и
130
- * стоит первой. */
131
- export function appBody(metrics, files) {
222
+ /* The top row holds the absolute sizes at HEAD: an absolute number stands in the table once, and it is the one every
223
+ * delta below it adds up to. Its numbers are the state at HEAD rather than the last commit's cells, which is why the
224
+ * running sums take them from here instead of assuming they are the row above. */
225
+ export function appNow(files, metrics, cache) {
226
+ const tr = appEl('tr', 'now');
227
+ tr.appendChild(appEl('th', 'c-commit', appUi.now));
228
+ const model = nowModel(appData.now, metrics);
229
+ const last = appData.rows.length;
230
+ model.total.forEach((v, mi) => {
231
+ const td = appValueCell(v, mi);
232
+ cache.sums[mi][last] = v;
233
+ cache.cells[mi][last] = td;
234
+ tr.appendChild(td);
235
+ });
236
+ appRowCells(tr, model, files, cache, appValueCell);
237
+ return tr;
238
+ }
239
+
240
+ /* The body: the commit rows from the newest down, plus the "now" row with the absolute sizes at HEAD. The deltas
241
+ * under it add up to it, which is why it stands first. */
242
+ export function appBody(files, metrics, cache) {
132
243
  const body = appEl('tbody');
133
- for (let r = appData.rows.length - 1; r >= 0; r--) body.appendChild(appRow(r, metrics, files));
134
-
135
- const now = appEl('tr', 'now');
136
- now.appendChild(appEl('th', 'c-commit', appUi.now));
137
- const nowCells = nowModel(appData.now, metrics, appView.files);
138
- nowCells.total.forEach((v, mi) => now.appendChild(appValueCell(v, mi === 0,
139
- appApprox('now', files, metrics[mi]))));
140
- nowCells.files.forEach((cells, fi) => cells.forEach((v, mi) => {
141
- now.appendChild(appValueCell(v, mi === 0, appApprox('now', [files[fi]], metrics[mi])));
142
- }));
143
- body.insertBefore(now, body.firstChild);
244
+ for (let r = appData.rows.length - 1; r >= 0; r--) body.appendChild(appRow(r, files, metrics, cache));
245
+ body.insertBefore(appNow(files, metrics, cache), body.firstChild);
144
246
  return body;
145
247
  }
248
+
249
+ /* The whole table, built once. What the cache holds is what the reader's choice works with: the nodes of each file's
250
+ * column — its cells, its headings and its `<col>` (`cols`), the group headings whose colSpan follows the enabled
251
+ * metrics (`spans`), the cells of the totals (`cells`), the running sums (`sums`) and those same sums with every
252
+ * column on (`all`) — the sums a view painted again starts from. */
253
+ export function appTable(grid) {
254
+ const metrics = appData.metrics.map((m) => m.key);
255
+ const files = appOrder();
256
+ const cache = { grid: grid, keys: metrics, cols: [], spans: [], cells: [], sums: [], all: [] };
257
+ metrics.forEach(() => {
258
+ cache.cells.push([]);
259
+ cache.sums.push([]);
260
+ });
261
+ grid.textContent = '';
262
+ grid.appendChild(appHead(files, metrics, cache));
263
+ grid.appendChild(appBody(files, metrics, cache));
264
+ /* The columns stand before the rows rather than beside them: a `<colgroup>` is where the fixed layout reads its
265
+ * widths from, and it has to be in the markup before the browser settles on a layout — the drawing is one pass, not
266
+ * two. */
267
+ grid.insertBefore(appCols(files, appWidest(files, metrics), cache), grid.firstChild);
268
+ cache.all = cache.sums.map((row) => row.slice());
269
+ return cache;
270
+ }
271
+
272
+ /* A file's column shown or hidden: its cells and its headings together, one class per node and no new node. A hidden
273
+ * column keeps its place in the markup — the order of the columns is the files' business, not the choice's. */
274
+ export function appColumn(cache, i, on) {
275
+ cache.cols[i].forEach((node) => node.classList.toggle('off', !on));
276
+ }
277
+
278
+ /* A file's share of the totals: switched off it takes exactly its own numbers out of the running sums, switched on
279
+ * it puts them back. Two rows per file are the whole of it — the numbers are the data's, and a sum of integers is
280
+ * exact in either direction. */
281
+ export function appContribute(cache, i, on) {
282
+ const sign = on ? 1 : -1;
283
+ appData.rows.forEach((row, r) => {
284
+ const values = row.values[i];
285
+ if (values === null) return;
286
+ cache.keys.forEach((key, mi) => { cache.sums[mi][r] += sign * values[key]; });
287
+ });
288
+ const values = appData.now[i];
289
+ if (values === null) return;
290
+ const last = appData.rows.length;
291
+ cache.keys.forEach((key, mi) => { cache.sums[mi][last] += sign * values[key]; });
292
+ }
293
+
294
+ /* The totals written where they stand: a data row shows the change against the row below it, the "now" row the
295
+ * absolute size — the same rules the first drawing used (`cellParts`, `valueParts`), so the reader sees one kind of
296
+ * number and not two. */
297
+ export function appTotals(cache) {
298
+ const last = appData.rows.length;
299
+ cache.cells.forEach((cells, mi) => {
300
+ cells.forEach((td, r) => {
301
+ if (r === last) { appFill(td, valueParts(cache.sums[mi][r])); return; }
302
+ appFill(td, cellParts(cache.sums[mi][r], deltaOf(cache.sums[mi][r], r === 0 ? null : cache.sums[mi][r - 1]), '−'));
303
+ });
304
+ });
305
+ }
306
+
307
+ /* The running sums of the whole view again: a link, a record from the memory and the first drawing change the choice
308
+ * as a whole rather than a column, so the sums start from the sums with every column on and take the switched-off
309
+ * ones out — one copy of the numbers instead of a recount. */
310
+ export function appTotalsReset(cache) {
311
+ cache.sums = cache.all.map((row) => row.slice());
312
+ appData.files.forEach((_f, i) => { if (!appView.files[i]) appContribute(cache, i, false); });
313
+ appTotals(cache);
314
+ }
315
+
316
+ /* The metrics of the choice: a class on the table hides every cell of a metric at once, and the group headings follow
317
+ * how many are left — two things that change, instead of a pass over the metric's cells (measured: 2.20 ms and 265
318
+ * operations against 433 ms and 56 496 for walking them). `.m-none` is what the styling reads to drop the group's
319
+ * left border when there is nothing left to separate. */
320
+ export function appMetrics(cache) {
321
+ let on = 0;
322
+ cache.keys.forEach((key, mi) => {
323
+ const visible = appView.metrics[key] === true;
324
+ if (visible) on++;
325
+ cache.grid.classList.toggle('m-off-' + mi, !visible);
326
+ });
327
+ cache.grid.classList.toggle('m-none', on === 0);
328
+ cache.spans.forEach((th) => { th.colSpan = on === 0 ? 1 : on; });
329
+ }
@@ -1,18 +1,18 @@
1
1
  import vm from 'vm';
2
2
  import { workerData } from 'worker_threads';
3
3
 
4
- /* Рабочий поток разбора: компилирует текст и отвечает причиной (или её
5
- * отсутствием). Исполнения нет — `SourceTextModule` только разбирает текст,
6
- * поэтому ни `import`, ни код модуля не выполняются: файл проекта остаётся
7
- * чужим кодом, который никто не запускает.
4
+ /* The parsing worker: it compiles the text and answers with a reason (or with the absence of
5
+ * one). Nothing is executed — `SourceTextModule` only parses the text, so neither `import`
6
+ * nor the code of the module runs: the file of the project stays foreign code that nobody
7
+ * launches.
8
8
  *
9
- * Флаги приходят от главного потока (`--experimental-vm-modules` без него
10
- * `vm.SourceTextModule` не существует, `--no-warnings` иначе предупреждение об
11
- * эксперименте ушло бы в вывод команды). Модуля может не быть: тогда ответ несёт
12
- * `available: false`, и главный поток возвращается к запуску `node --check`.
9
+ * The flags arrive from the main thread (`--experimental-vm-modules`, without which
10
+ * `vm.SourceTextModule` does not exist, and `--no-warnings` lest the experimental warning end
11
+ * up in the command's output). The module may be absent: then the answer carries
12
+ * `available: false`, and the main thread falls back to launching `node --check`.
13
13
  *
14
- * Готовность ответа отмечается в общей памяти: главный поток ждёт её синхронно
15
- * (`Atomics.wait`), потому что измерение истории синхронное.
14
+ * Readiness is marked in shared memory: the main thread waits for it synchronously
15
+ * (`Atomics.wait`), because measuring the history is synchronous.
16
16
  */
17
17
  const { port, sig } = workerData;
18
18
  const available = typeof vm.SourceTextModule === 'function';
package/src/parse.js CHANGED
@@ -5,49 +5,45 @@ import { execFileSync } from 'child_process';
5
5
  import { Worker, MessageChannel, receiveMessageOnPort } from 'worker_threads';
6
6
  import { fileURLToPath } from 'url';
7
7
 
8
- /* Разбор модуля: один рабочий поток на прогон вместо запуска Node на каждую
9
- * клетку.
8
+ /* Parsing a module: one worker per run instead of launching Node per cell.
10
9
  *
11
- * Зачем. Гард компиляции обязан понимать модуль (`import`/`export` в `.js`
12
- * обычное дело у проекта с бандлером, см. `strip.js`), а единственный разбор
13
- * модуля без исполнения `vm.SourceTextModule` живёт только под флагом
14
- * `--experimental-vm-modules`, которого у процесса нет. Раньше это решалось
15
- * запуском `node --check` на каждую клетку: 97 мс на запуск и минуты на истории,
16
- * где модуль меняется каждым коммитом.
10
+ * Why. The compile guard has to understand a module (`import`/`export` inside `.js` is
11
+ * ordinary in a project with a bundler — see `strip.js`), while the only parse of a module
12
+ * without executing it, `vm.SourceTextModule`, exists only under `--experimental-vm-modules`,
13
+ * which this process is not started with. It used to be solved by `node --check` per cell:
14
+ * 97 ms per launch and minutes across a history where a module changes at every commit.
17
15
  *
18
- * Как. Поток поднимается при первом модуле и живёт до конца прогона, поэтому
19
- * скриптовые проекты за него не платят вовсе. Обмен синхронный измерение
20
- * истории синхронное: запрос уходит `postMessage`, готовность ответа отмечается
21
- * в `SharedArrayBuffer`, а ответ забирается `receiveMessageOnPort` (тот же приём,
22
- * что в примере Node для синхронного канала в поток). Поток `unref`-нут: команда
23
- * заканчивается вместе со своей работой, а не вместе с потоком.
16
+ * How. The worker starts at the first module and lives until the end of the run, so a
17
+ * script-only project never pays for it. The exchange is synchronous, as history measurement
18
+ * is: the request goes out through `postMessage`, readiness is marked in a `SharedArrayBuffer`,
19
+ * and the answer is taken with `receiveMessageOnPort` the same trick as in Node's example of
20
+ * a synchronous channel to a worker. The worker is `unref`-ed: the command ends with its
21
+ * work, not with the thread.
24
22
  *
25
- * Чем платит. Старт потока разовая цена (~55 мс на машине замера), и разбор
26
- * опирается на экспериментальный API: флаг `--experimental-vm-modules` передаётся
27
- * самому потоку, поэтому команда пользователя не меняется. Текст передаётся в
28
- * поток копией на файлах в десятки мегабайт это десятки миллисекунд, всё ещё
29
- * дешевле запуска процесса.
23
+ * What it costs. Starting the worker is a one-off (~55 ms on the machine the measurement was
24
+ * taken on), and the parsing relies on an experimental API: the flag is passed to the worker
25
+ * itself, so the user's command does not change. The text is copied into the worker — tens of
26
+ * milliseconds for files of tens of megabytes, still cheaper than launching a process.
30
27
  *
31
- * Куда отступает. К запуску `node --check` — медленнее, но не мягче: когда файла
32
- * потока нет (неполная упаковка), когда поток не ответил за отведённое время
33
- * (умер) и когда в потоке не оказалось `vm.SourceTextModule` (Node без модулей
34
- * vm). Отступление молчаливое: сломавшийся быстрый путь стоит секунд, а не
35
- * правильности, и его место стережёт проверка способа разбора (`parseMode`).
28
+ * Where it falls back. To `node --check` — slower, not weaker: when the worker file is missing
29
+ * (an incomplete package), when the worker did not answer in time (it died), and when it turns
30
+ * out to have no `vm.SourceTextModule` (a Node without the vm module). The fallback is silent:
31
+ * a broken fast path costs seconds rather than correctness, and its place is watched by the
32
+ * parse-mode check (`parseMode`).
36
33
  */
37
34
 
38
35
  const WORKER_FILE = fileURLToPath(new URL('./parse-worker.js', import.meta.url));
39
36
  const FLAGS = ['--experimental-vm-modules', '--no-warnings'];
40
37
  const WAIT_MS = 2000;
41
38
 
42
- let parser = null; // живой поток { worker, port, sig } или null
43
- let hopeless = false; // поток не поднялся: второй раз не пробуем
39
+ let parser = null; // the live thread { worker, port, sig } or null
40
+ let hopeless = false; // the thread did not come up: no second try
44
41
  let seq = 0;
45
- let mode = null; // 'thread' | 'node' — чем разобран последний модуль
42
+ let mode = null; // 'thread' | 'node' — how the last module was parsed
46
43
 
47
- /* Причина, по которой текст не разбирается как модуль, или null, если
48
- * разбирается. «Не удалось проверить» наружу не выходит никогда: разбор без
49
- * потока уходит в `node --check`, а он отвечает тем же причиной или её
50
- * отсутствием. */
44
+ /* The reason the text does not parse as a module, or null if it does. "Could not check"
45
+ * never leaves this function: without a worker the parse goes to `node --check`, which
46
+ * answers the same way with a reason or with its absence. */
51
47
  export function moduleError(text) {
52
48
  const fromThread = inThread(text);
53
49
  if (fromThread !== undefined) {
@@ -58,9 +54,9 @@ export function moduleError(text) {
58
54
  return onNodeCheck(text);
59
55
  }
60
56
 
61
- /* Способ разбора последнего модуля. Нужен, чтобы быстрый путь не деградировал
62
- * молча: проверка утверждает, что на проекте с модулями он действительно поток,
63
- * а не прежний запуск. */
57
+ /* How the last module was parsed. Kept so that the fast path cannot degrade silently: a
58
+ * check asserts that on a project with modules it really is the worker and not the old
59
+ * launch. */
64
60
  export function parseMode() {
65
61
  return mode;
66
62
  }
@@ -69,15 +65,15 @@ function inThread(text) {
69
65
  const live = start();
70
66
  if (!live) return undefined;
71
67
  const id = ++seq;
72
- // Сначала ноль в том же слове, которым поток отмечает готовность: ответ,
73
- // пришедший раньше ожидания, иначе было бы видно как «ещё не начинали».
68
+ // Zero goes into the same word the worker marks readiness with: an answer arriving before
69
+ // the wait would otherwise look like "not started yet".
74
70
  Atomics.store(live.sig, 0, 0);
75
71
  live.port.postMessage({ id: id, text: text });
76
72
  if (Atomics.wait(live.sig, 0, 0, WAIT_MS) === 'timed-out') return bury();
77
73
  for (;;) {
78
74
  const got = receiveMessageOnPort(live.port);
79
75
  if (!got) return bury();
80
- if (got.message.id !== id) continue; // прежний ответ (поток отвечал не нам)
76
+ if (got.message.id !== id) continue; // a stale answer (it replied to another request)
81
77
  if (got.message.available === false) return bury();
82
78
  return got.message.error === null ? null : String(got.message.error);
83
79
  }
@@ -95,8 +91,8 @@ function start() {
95
91
  const worker = new Worker(WORKER_FILE, {
96
92
  execArgv: FLAGS, workerData: { port: port2, sig: sig }, transferList: [port2]
97
93
  });
98
- // Без обработчика ошибка потока стала бы исключением процесса, а её место
99
- // в отступлении к запуску.
94
+ // Without a handler a worker error would become an exception of the process, while its
95
+ // place is in the fallback to a launch.
100
96
  worker.on('error', bury);
101
97
  worker.on('exit', bury);
102
98
  worker.unref();
@@ -107,7 +103,8 @@ function start() {
107
103
  return parser;
108
104
  }
109
105
 
110
- // Поток больше не годится: дальше разбираем запуском, и вернуться уже некуда.
106
+ // The worker is no longer usable: parsing goes through a launch from here on, with no way
107
+ // back.
111
108
  function bury() {
112
109
  const dead = parser;
113
110
  parser = null;
@@ -116,10 +113,10 @@ function bury() {
116
113
  return undefined;
117
114
  }
118
115
 
119
- /* Отступление: `node --check` по временному файлу. Расширение `.mjs` здесь не
120
- * косметика у временного файла нет манифеста, и только расширение говорит
121
- * Node, что текст надо читать как модуль. Причина берётся из `stderr`: там
122
- * сначала эхо строки с ошибкой, потом сам `SyntaxError` и стек. */
116
+ /* The fallback: `node --check` on a temporary file. The `.mjs` extension is not cosmetic —
117
+ * a temporary file has no manifest, and only the extension tells Node to read the text as a
118
+ * module. The reason comes from `stderr`, which holds the echoed offending line first, then
119
+ * the `SyntaxError` itself and the stack. */
123
120
  function onNodeCheck(text) {
124
121
  const tmp = path.join(os.tmpdir(), 'size-table-guard-' + process.pid + '-mod.mjs');
125
122
  try {
@@ -129,7 +126,8 @@ function onNodeCheck(text) {
129
126
  } catch (e) {
130
127
  const lines = String((e && e.stderr) || (e && e.message) || e).split('\n')
131
128
  .map((l) => l.trim()).filter((l) => l !== '');
132
- return lines.find((l) => /^\w*Error\b/.test(l)) || lines[0] || 'модуль не разбирается';
129
+ // The fallback reason travels into a printed refusal, so it is worded like the rest of the output.
130
+ return lines.find((l) => /^\w*Error\b/.test(l)) || lines[0] || 'the module does not parse';
133
131
  } finally {
134
132
  fs.rmSync(tmp, { force: true });
135
133
  }