@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/state.js CHANGED
@@ -1,70 +1,83 @@
1
- /* Программа страницы отчёта: панель выбора с легендой и таблица.
1
+ import { appDecode } from './payload.js';
2
+
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
- * Это обычный исходник, а не строка в движке: его видит линтер, и он же
4
- * вклеивается в собранную страницу (`pageScript` снимает модульный синтаксис
5
- * импорт ниже и объявления с `export` из соседнего файла). Расчёт берётся из
6
- * производных величин, поэтому включение метрики, категории или файла зовёт тот
7
- * же расчёт, что считает статическую таблицу, и не может дать других чисел.
5
+ * An ordinary source file rather than a string inside the engine: a linter sees it, and it is pasted into the assembled
6
+ * page (`pageScript` strips module syntax — the `import` lines and the `export` keywords of the chapters). The numbers
7
+ * come from the derived quantities, so switching a metric, a category or a file calls the same calculation the report's
8
+ * numbers come from and cannot yield different ones.
8
9
  *
9
- * Страница один файл без внешних ссылок, поэтому здесь нет ни динамического
10
- * импорта, ни загрузки чего-либо по сети: оформление приходит тем же файлом,
11
- * а разметка клеток повторяет статическую таблицу (`clip` и подпись коммита —
12
- * правила общей части оформления).
10
+ * The page is one file without external references, so there is no dynamic import here and nothing is loaded over the
11
+ * network: the styling arrives in the same file, and the cell markup follows the rules of the shared part of the styling
12
+ * (`clip`, a commit's caption).
13
13
  *
14
- * Панель помнит выбор читателя между открытиями и умеет передать его ссылкой
15
- * («Память выбора» ниже): запись привязана к паспорту отчёта и хранит только
16
- * выключенное по именам, поэтому чужая запись не применяется, а исчезнувшее имя
17
- * просто ничего не значит. Та же запись ложится в адрес — его и отправляют коллеге.
14
+ * The panel remembers the reader's choice between visits and can hand it over as a link ("the choice's memory" below):
15
+ * the record is tied to the report's passport and keeps only what is switched off, by name, so someone else's record is
16
+ * not applied while a vanished name simply means nothing. The same record goes into the address — which is what one
17
+ * sends to a colleague.
18
18
  *
19
- * Точность числа страница не выводит сама: пометки приближённых клеток приходят в
20
- * данных, от того же правила, по которому названа точность метрики. Из путей и
21
- * форматов страница такого вывода не делает — второго правила точности не будет. */
22
-
23
- /* Импорт — одной строкой: модульный синтаксис снимается при вклейке построчно,
24
- * и оставшаяся строка `import` попала бы в страницу (её ловит проверка). */
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. */
25
21
 
26
- export const appData = JSON.parse(document.getElementById('data').textContent);
27
22
  export const appUi = JSON.parse(document.getElementById('ui').textContent);
28
- export const appView = { metrics: {}, files: [], folded: {} };
29
- appData.metrics.forEach((m) => { appView.metrics[m.key] = true; });
30
- appData.files.forEach(() => { appView.files.push(true); });
31
23
 
32
- /* Описание метрики по ключу: подсказка приближённой клетки называет способ её
33
- * числа тот же, что стоит в подписи метрики, поэтому двух ответов про «чем
34
- * посчитано» у страницы нет. */
35
- export const appMetric = {};
36
- appData.metrics.forEach((m) => { appMetric[m.key] = m; });
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;
37
34
 
38
- /* Колонка по пути файла: дерево страницы дерево проекта (все пути каталога), а
39
- * числа есть только у колонок, поэтому лист дерева по этому указателю и решает,
40
- * галочка он или подпись. Имя берётся тем же правилом, что у записи выбора
41
- * (`appFileAt`), — дерево и память читателя разойтись не могут. */
42
- export const appMeasured = {};
43
- appData.files.forEach((_f, i) => { appMeasured[appFileAt(i)] = i; });
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
+ appView = { metrics: {}, files: [], folded: {} };
51
+ appMetric = {};
52
+ appMeasured = {};
53
+ appData.metrics.forEach((m) => { appView.metrics[m.key] = true; appMetric[m.key] = m; });
54
+ appData.files.forEach((_f, i) => { appView.files.push(true); appMeasured[appFileAt(i)] = i; });
55
+ appKey = 'size-report:' + appPassport();
56
+ appFoldKey = appKey + ':tree';
57
+ }
44
58
 
45
- /* Ссылка это тот же выбор в адресе, под своим именем: чужой якорь страницы
46
- * ссылкой не считается, и спорить с ним нечем. */
59
+ /* 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
60
+ * as a link, and there is nothing to argue with it about. */
47
61
  const APP_LINK = '#size-report=';
48
62
 
49
- /* Три обстоятельства первой отрисовки, которые действуют только на ней:
50
- * адрес в ней не переписывается (его прислали читателю, а не наоборот), память
51
- * не трогается (присланная ссылка не выбор читателя), а сообщение о ссылке
52
- * ещё не гаснет. */
63
+ /* Three circumstances of the first drawing, which act on it alone: the address is not rewritten during it (it was sent to
64
+ * the reader rather than the other way), the memory is not touched (a link that came in is not the reader's choice), and
65
+ * the message about the link has not faded yet. */
53
66
  let appStartup = true;
54
67
  let appForeign = false;
55
68
  let appTransient = false;
56
69
 
57
- /* -------- память выбора читателя -------- */
70
+ /* -------- the reader's memory of his choice -------- */
58
71
 
59
- /* Имя файла для записи путь на HEAD, а если файла там уже нет, последний из
60
- * настроек: по нему файл и опознаётся в отчёте. */
72
+ /* The name of a file for the record is its path at HEAD, or the last of the settings when the file is already gone from
73
+ * there: that is the name it is recognised by in the report. */
61
74
  export function appFileAt(i) {
62
75
  const f = appData.files[i];
63
76
  return f.path === null ? f.paths[0] : f.path;
64
77
  }
65
78
 
66
- /* Отпечаток паспорта: опознавательный знак записи, а не защита от подделки,
67
- * поэтому 32 бит достаточно (FNV-1a). */
79
+ /* The passport's fingerprint: an identifying mark of a record rather than protection against forgery, so 32 bits are
80
+ * enough (FNV-1a). */
68
81
  function appHash(text) {
69
82
  let h = 2166136261;
70
83
  for (let i = 0; i < text.length; i++) {
@@ -74,22 +87,27 @@ function appHash(text) {
74
87
  return (h >>> 0).toString(16);
75
88
  }
76
89
 
77
- /* Паспорт отчёта: имя инструмента, схема данных, путь артефакта, заголовок и метки
78
- * колонок в порядке отчёта. Он и отделяет один отчёт от другого по нему выбирается
79
- * ключ записи, поэтому выбор с чужого отчёта не подхватывается. Версии пакета и
80
- * верхушки истории в паспорте нет намеренно: это тот же отчёт обновление
81
- * инструмента не меняет того, что значит колонка, а подросшая история это та же
82
- * история, к которой читатель и возвращается. */
90
+ /* The report's passport: the tool's name, the data schema, the artifact's path, the title and the column labels in the
91
+ * report's order. It is what tells one report from anotherthe record's key is chosen by it, so a choice made in
92
+ * someone else's report is not picked up. The package version and the top of the history are absent on purpose: this is
93
+ * the same report updating the tool does not change what a column means, while a grown history is the very history the
94
+ * reader comes back to.
95
+ *
96
+ * Counted once per document: it is a constant of the report, which depends on nothing the reader can change, and every
97
+ * click asks for it (the key of the memory and the passport of the record). A second count would be a second answer
98
+ * waiting to happen, and the labels it reads do not change while the page is open. */
99
+ let appPassportValue = null;
83
100
  function appPassport() {
84
- return appHash([appData.tool.name, appData.schema, appData.report.artifact,
85
- appData.report.title, appData.files.map((f) => f.label).join('|')].join('\n'));
101
+ if (appPassportValue === null) {
102
+ appPassportValue = appHash([appData.tool.name, appData.schema, appData.report.artifact,
103
+ appData.report.title, appData.files.map((f) => f.label).join('|')].join('\n'));
104
+ }
105
+ return appPassportValue;
86
106
  }
87
- const appKey = 'size-report:' + appPassport();
88
107
 
89
- /* Запись выбора одна на всё: её кладут и в память, и в адрес, поэтому двух
90
- * форматов одного состояния не бывает. Хранится только выключенное, по именам:
91
- * «включено» и «записи нет» одно и то же состояние, поэтому возврат всех галочек
92
- * убирает запись, а не оставляет след, неотличимый от выбора. */
108
+ /* One record of the choice for everything: it goes both into the memory and into the address, so there are no two formats
109
+ * of one state. Only what is switched off is kept, by name: "switched on" and "no record" are the same state, which is
110
+ * why turning every checkbox back on removes the record instead of leaving a trace indistinguishable from a choice. */
93
111
  function appRecord() {
94
112
  const metrics = {};
95
113
  const files = {};
@@ -98,47 +116,73 @@ function appRecord() {
98
116
  return { v: 1, passport: appPassport(), metrics: metrics, files: files };
99
117
  }
100
118
 
101
- // Своя ли запись и того ли форматаодно правило и для памяти, и для адреса.
119
+ // Whether a record is ours and of the right format one rule for the memory and the address alike.
102
120
  function appRecordOk(rec) {
103
121
  return rec !== null && typeof rec === 'object' && rec.v === 1 && rec.passport === appPassport();
104
122
  }
105
123
 
124
+ /* The address is the link for a colleague, while the memory is the reader's own: the memory is written on the click
125
+ * itself — that is what survives a closing — and the address 200 ms after the last of a burst of switches, because a
126
+ * burst is one link rather than five history entries and five URL parses. The delay is short enough for a person and
127
+ * long enough to swallow a run of clicks; a timer that fires after the page is gone writes nothing useful, which is the
128
+ * price of not writing the address five times. */
129
+ const APP_ADDRESS_DELAY = 200;
130
+ let appAddressTimer = null;
131
+
132
+ /* An address that came in from outside wins over a write this page has not made yet: a click arms a write, a link
133
+ * arrives within the delay, and the choice left behind must not land on the address the reader was sent — a refused link
134
+ * arms nothing to replace it, so without this the page would rewrite someone else's address a fifth of a second later. */
135
+ export function appAddressDrop() {
136
+ if (appAddressTimer === null) return;
137
+ clearTimeout(appAddressTimer);
138
+ appAddressTimer = null;
139
+ }
140
+
141
+ function appAddressLater(text) {
142
+ appAddressDrop();
143
+ appAddressTimer = setTimeout(() => {
144
+ appAddressTimer = null;
145
+ try {
146
+ window.history.replaceState(null, '', APP_LINK + encodeURIComponent(text));
147
+ } catch (_e) {
148
+ /* The browser grants no change of the address: the link is then taken from the browser's memory. */
149
+ }
150
+ }, APP_ADDRESS_DELAY);
151
+ }
152
+
153
+ /* One record for a click and two destinations: the same text goes into the memory and — a moment later — into the
154
+ * address, so the two cannot describe different choices. */
106
155
  export function appWrite() {
107
156
  const rec = appRecord();
157
+ const text = JSON.stringify(rec);
108
158
  const empty = Object.keys(rec.metrics).length === 0 && Object.keys(rec.files).length === 0;
109
159
  if (!appTransient) {
110
160
  try {
111
161
  if (empty) window.localStorage.removeItem(appKey);
112
- else window.localStorage.setItem(appKey, JSON.stringify(rec));
162
+ else window.localStorage.setItem(appKey, text);
113
163
  } catch (_e) {
114
- /* Памяти нет (браузер её не даёт этой странице): выбор не переживёт закрытия,
115
- * а числа и разметка от этого не зависят. */
164
+ /* There is no memory (the browser grants this page none): the choice will not survive a closing, while the numbers
165
+ * and the markup do not depend on it. */
116
166
  }
117
167
  }
118
- /* Адрес и есть ссылка для коллеги, поэтому он повторяет выбор. Но не на первой
119
- * отрисовке и не тогда, когда ссылка оказалась чужой: присланный адрес — не наш,
120
- * его читателю ещё читать. */
168
+ /* But not during the first drawing and not when the link turned out to be someone else's: an address that came in is
169
+ * not ours, and the reader has yet to read it. */
121
170
  if (appStartup || appForeign) return;
122
- try {
123
- window.history.replaceState(null, '', APP_LINK + encodeURIComponent(JSON.stringify(rec)));
124
- } catch (_e) {
125
- /* Браузер не даёт менять адрес: ссылку тогда берут из памяти браузера. */
126
- }
171
+ appAddressLater(text);
127
172
  }
128
173
 
129
- /* Сброс к «включено всё»: граница между «в отчёте этого больше нет» и
130
- * «выключено» это запись, а не отсутствие значения. Ссылка несёт весь выбор
131
- * отправителя, поэтому применяется на чистом виде, а не поверх чужого. */
174
+ /* A reset to "everything on": the border between "this is no longer in the report" and "switched off" is the record
175
+ * rather than a missing value. A link carries the sender's whole choice, which is why it is applied to a clean view rather
176
+ * than on top of someone else's. */
132
177
  function appAll() {
133
178
  appData.metrics.forEach((m) => { appView.metrics[m.key] = true; });
134
179
  appData.files.forEach((_f, i) => { appView.files[i] = true; });
135
180
  }
136
181
 
137
- /* Что говорит адрес. Отвечает либо своей записью, либо отказом (`linkForeign` —
138
- * ссылка другого отчёта, `linkBroken` — прочитать нечего): чужой или испорченный
139
- * выбор не применяется, но и не молчит иначе читатель не поймёт, почему он видит
140
- * не то, что ему прислали. Адрес без имени ссылки не ссылка вовсе: молчание, чтобы
141
- * не спорить с обычными якорями страницы. */
182
+ /* What the address says. It answers either with a record of ours or with a refusal (`linkForeign` — a link of another
183
+ * report, `linkBroken` — nothing to read): someone else's or a broken choice is not applied, but it does not stay silent
184
+ * either otherwise the reader would not understand why he sees something other than what was sent to him. An address
185
+ * without the link's name is no link at all: silence, so as not to argue with the page's ordinary anchors. */
142
186
  function appLinkRead() {
143
187
  const hash = window.location.hash || '';
144
188
  if (hash.indexOf(APP_LINK) !== 0) return { rec: null, refused: null };
@@ -160,8 +204,8 @@ function appLinkRead() {
160
204
  return { rec: rec, refused: null, extra: appUnknown(rec) };
161
205
  }
162
206
 
163
- /* Сколько имён в ссылке этому отчёту неизвестны: о них читателю надо сказать
164
- * иначе он будет искать в таблице то, чего в ней и не было. */
207
+ /* How many names in the link are unknown to this report: the reader has to be told about them otherwise he would look
208
+ * in the table for something that was never there. */
165
209
  function appUnknown(rec) {
166
210
  const known = {};
167
211
  const metricKeys = {};
@@ -173,19 +217,18 @@ function appUnknown(rec) {
173
217
  return n;
174
218
  }
175
219
 
176
- /* Сообщение о ссылке гаснет после первого же действия читателя: он его прочитал, а
177
- * постоянное предупреждение это шум поверх чисел. */
220
+ /* The message about the link fades after the reader's very first action: he has read it, and a permanent warning is noise
221
+ * on top of the numbers. */
178
222
  export function appNotice(text) {
179
223
  const el = document.getElementById('notice');
180
224
  el.textContent = text;
181
225
  el.hidden = text === '';
182
226
  }
183
227
 
184
- /* Что делает с адресом его событиеоткрытие страницы и перемена якоря на уже
185
- * открытой (браузер в этом случае документ не перезагружает, а лишь переставляет
186
- * якорь, поэтому без этого разбора ссылка работала бы только в новой вкладке).
187
- * Своя ссылка заменяет вид целиком: в ней весь выбор отправителя, а не разница с
188
- * чужим. Отказ объясняется словами — и не трогает ни вид, ни адрес. */
228
+ /* What its event does with the address opening the page and an anchor change on an already open one (the browser does
229
+ * not reload the document then, it only moves the anchor, so without this reading a link would work in a new tab alone).
230
+ * A link of ours replaces the view whole: it holds the sender's entire choice rather than a difference from someone
231
+ * else's. A refusal is explained in words and touches neither the view nor the address. */
189
232
  export function appLinkUse() {
190
233
  const link = appLinkRead();
191
234
  if (link.rec !== null) {
@@ -201,9 +244,8 @@ export function appLinkUse() {
201
244
  return 'none';
202
245
  }
203
246
 
204
- /* Чтение: только своя записьсвоей версии формата и своего паспорта. Запись
205
- * чужого отчёта лежит под другим ключом, а чужая, устаревшая или испорченная
206
- * равносильна её отсутствию. */
247
+ /* Reading: only a record of ours of our format version and our passport. Another report's record lies under another key,
248
+ * while a foreign, outdated or broken one amounts to its absence. */
207
249
  export function appRead() {
208
250
  let text = null;
209
251
  try {
@@ -221,10 +263,9 @@ export function appRead() {
221
263
  return appRecordOk(rec) ? rec : null;
222
264
  }
223
265
 
224
- /* Применение по именам: файл опознаётся путём, метрика ключом. Имени, которого в
225
- * отчёте нет, ничего не соответствует (колонку перенаправили на другой путь,
226
- * метрику убрали из настроек), а появившиеся файлы и метрики остаются включёнными
227
- * как их видит тот, кто открыл страницу впервые. */
266
+ /* Applying goes by name: a file is recognised by its path, a metric by its key. A name the report does not hold matches
267
+ * nothing (a column was pointed at another path, a metric was dropped from the settings), while files and metrics that
268
+ * appeared stay switched on the way someone opening the page for the first time sees them. */
228
269
  export function appApply(rec) {
229
270
  const metrics = rec.metrics || {};
230
271
  const files = rec.files || {};
@@ -232,15 +273,14 @@ export function appApply(rec) {
232
273
  appData.files.forEach((_f, i) => { if (files[appFileAt(i)] === false) appView.files[i] = false; });
233
274
  }
234
275
 
235
- /* -------- сложенное дерево -------- */
276
+ /* -------- the folded tree -------- */
236
277
 
237
- /* Сложенные папки — память того же рода, что выбор, но своей записи: она про то,
238
- * сколько дерева видно, а не про то, какие числа читают. Поэтому в адрес она не
239
- * идёт: ссылку отправляют ради чисел, а разложенное дерево — дело смотрящего. Как и
240
- * у выбора, здесь помнится только сложенное (`true`), а имя папки — это путь
241
- * («src/page»), поэтому исчезнувшее имя просто ничего не значит. */
242
- const appFoldKey = appKey + ':tree';
243
278
 
279
+ /* 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
280
+ * tree is visible rather than about which numbers are read. Hence it does not go into the address: a link is sent for the
281
+ * sake of the numbers, while an unfolded tree is the onlooker's business. As with the choice, only what is folded is kept
282
+ * (`true`), and a folder's name is its path ("src/page"), so a vanished name simply means nothing. `appFoldKey` is
283
+ * set with the rest of the model (`appBoot`), for the reason the key itself is. */
244
284
  export function appFoldRead() {
245
285
  let text = null;
246
286
  try {
@@ -268,7 +308,7 @@ export function appFoldSet(path, folded) {
268
308
  if (Object.keys(rec.folded).length === 0) window.localStorage.removeItem(appFoldKey);
269
309
  else window.localStorage.setItem(appFoldKey, JSON.stringify(rec));
270
310
  } catch (_e) {
271
- /* Памяти нет: сложенное не переживёт закрытия страницы, а вид от этого не
272
- * зависит дерево сложено ровно так, как его сложил читатель сейчас. */
311
+ /* There is no memory: what is folded will not survive a closing, while the view does not depend on it — the tree is
312
+ * folded exactly the way the reader folded it just now. */
273
313
  }
274
314
  }