@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/history.js CHANGED
@@ -1,32 +1,30 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { assertFullHistory, diskForm, diskHashes, git, headTree, readBlobs, readHistory } from './git.js';
4
- import { METRICS, measureBlob, pointExact } from './metrics.js';
4
+ import { METRICS, measureBlob } from './metrics.js';
5
5
  import { touchedSection } from './journal.js';
6
6
  import { EXIT, refuse } from './refusal.js';
7
7
 
8
- /* Обход истории: измерение по коммитам, сдвиг чисел, перенос состояния между
9
- * коммитами, сверка с рабочим деревом и сборката единственная точка, из
10
- * которой состояние и строки попадают наружу. Верхний этаж чтения: ниже git и
11
- * метрики, выше — только уже собранные значения. */
8
+ /* Walking the history: measuring per commit, shifting numbers, carrying state between commits,
9
+ * comparing against the working tree, and buildingthe one point from which rows and state
10
+ * leave this layer. The top floor of reading: below it are git and the metrics, above it only
11
+ * assembled values. */
12
12
 
13
- /* Причина, по которой коммит не получил строки, ключ, а не текст: по нему и
14
- * считается сводка, и отвечает `explain`. Слова для человека в `skipLine`, и
15
- * они те же, что были строкой раньше: «без изменения объёма» накрывает и тот
16
- * случай, когда коммит не тронул ни одного файла колонок, в отчёте эта разница
17
- * не проводилась, и эталон контракта её сохраняет; отличие видно в `explain`, где
18
- * оно и нужно. */
13
+ /* Why a commit got no row is a key rather than a text: the summary counts by it and `explain`
14
+ * answers by it. The words a human reads live in `skipLine` and are the ones that used to be the
15
+ * row itself: "no change of volume" also covers a commit that touched no column at all — the
16
+ * report never made that distinction and the contract fixture preserves it, while the difference
17
+ * is visible in `explain`, where it is needed. */
19
18
  const SKIP_WORDS = { merge: 'merge', report: 'только таблица', flat: 'без изменения объёма' };
20
19
 
21
20
  export function skipLine(dropped) {
22
21
  return dropped.sha.slice(0, 7) + ' (' + SKIP_WORDS[dropped.reason] + ')';
23
22
  }
24
23
 
25
- /* Сдвинул ли коммит хотя бы одно число. Сравниваются числа, а не список файлов:
26
- * правка в пробелах или комментариях размера не меняет, и строка про неё была бы
27
- * пустой, а у слияния клетки выходят нулевыми всегда, когда разрешение конфликта
28
- * совпало с тем, что уже дала ветка. Колонка, которой коммит не касался,
29
- * остаётся тем же объектом состояния. */
24
+ /* Whether a commit moved at least one number. Numbers are compared rather than the list of files:
25
+ * an edit to whitespace or comments changes no size and its row would be empty, while a merge
26
+ * gives zero cells whenever resolving the conflict produced what the branch already had. A column
27
+ * the commit did not touch keeps the very same state object. */
30
28
  function changesVolume(state, before, columns, metrics) {
31
29
  return columns.some((_col, i) => {
32
30
  const a = state[i], b = before[i];
@@ -36,11 +34,11 @@ function changesVolume(state, before, columns, metrics) {
36
34
  });
37
35
  }
38
36
 
39
- /* План чтения: какие пары «ревизия:путь» понадобятся и всё содержимое сразу иначе
40
- * на каждый коммит приходилось бы по git-вызову на колонку. Псевдонимов колонки,
41
- * которых коммит коснулся, может быть и два: при выключенном распознавании
42
- * переименований git отдаёт в одном коммите и старое имя, и новое. Собираются все
43
- * какой из них в коммите действительно есть, решается потом, по прочитанным блобам. */
37
+ /* The read plan: which `revision:path` pairs will be needed, and all the content at once
38
+ * otherwise every commit would cost one git call per column. A column may have both of its aliases
39
+ * touched by one commit: with rename detection off, git reports the old name and the new one in
40
+ * the same commit. All of them are collected which one is really there is decided later, by the
41
+ * blobs that came back. */
44
42
  function readPlan(cfg, root, commits, needText) {
45
43
  const plan = commits.map((c) => {
46
44
  const changed = new Set(c.files);
@@ -58,10 +56,10 @@ function readPlan(cfg, root, commits, needText) {
58
56
  return { plan: plan, blobs: readBlobs(root, specs, needText) };
59
57
  }
60
58
 
61
- /* Замер блоба с памятью на проход: ревизия с тем же содержимым (откат, повторный
62
- * merge) не пересчитывается. */
59
+ /* Measuring a blob with a memory kept for the run: a revision with the same content (a revert, a
60
+ * repeated merge) is not measured twice. */
63
61
  function measurer(cfg) {
64
- const measured = new Map(); // sha блоба + метрикачисло
62
+ const measured = new Map(); // blob sha + metricnumber
65
63
  return (name, blob, file, rev) => {
66
64
  const key = blob.sha + '\u0000' + name;
67
65
  if (measured.has(key)) return measured.get(key);
@@ -71,34 +69,29 @@ function measurer(cfg) {
71
69
  };
72
70
  }
73
71
 
74
- /* Правки коммита в состояние: из псевдонимов берётся тот, который в коммите есть, а
75
- * не первый по порядку настроек,исчезнувшее имя в коммите отсутствует, и
76
- * состояние, взятое по порядку, теряло файл (а сверка с деревом отказывала). */
72
+ /* The commit's edits applied to the state: the alias taken is the one present in the commit
73
+ * rather than the first in the settings order a vanished name is absent from the commit, and a
74
+ * state taken in settings order lost the file (and the comparison against the tree refused). */
77
75
  function applyPicks(pass, c, picks) {
78
76
  picks.forEach((candidates, i) => {
79
77
  const pick = candidates.find((cand) => pass.blobs.get(cand.spec) !== undefined);
80
78
  if (pick === undefined) {
81
- // Путь в коммите есть, а файла по нему нетфайл удалён.
79
+ // The path is in the commit but no blob came back for it the file was deleted.
82
80
  if (candidates.length > 0) pass.state[i] = null;
83
81
  return;
84
82
  }
85
83
  const blob = pass.blobs.get(pick.spec);
86
84
  const cells = {};
87
- /* Приближённость числа — свойство пути, а не блоба: от расширения зависит,
88
- * возьмёт ли формат минификатор. Поэтому она считается здесь, вместо с
89
- * замером, и в кэш содержимого не попадает. */
90
- const approx = {};
91
85
  pass.metrics.forEach((m) => {
92
86
  cells[m] = pass.measure(m, blob, pick.path, c.sha);
93
- approx[m] = !pointExact(m, pick.path, pass.cfg);
94
87
  });
95
- pass.state[i] = { path: pick.path, sha: blob.sha, cells: cells, approx: approx };
88
+ pass.state[i] = { path: pick.path, sha: blob.sha, cells: cells };
96
89
  });
97
90
  }
98
91
 
99
- /* Один коммит прохода: сдвиг состояния, затем нужна ли коммиту строка. `pass`
100
- * общий на весь проход (состояние, списки, память замеров), поэтому функция только
101
- * двигает его вперёд. */
92
+ /* One commit of the run: shift the state, then decide whether the commit needs a row. `pass` is
93
+ * shared by the whole run (state, lists, measurement memory), so the function only moves it
94
+ * forward. */
102
95
  function stepCommit(pass, c, ci) {
103
96
  const plan = pass.plan[ci];
104
97
  let section = null;
@@ -129,19 +122,18 @@ function stepCommit(pass, c, ci) {
129
122
  when: c.when,
130
123
  subject: c.subject,
131
124
  section: section,
132
- cells: pass.state.map((s) => (s === null ? null : s.cells)),
133
- approx: pass.state.map((s) => (s === null ? null : s.approx))
125
+ cells: pass.state.map((s) => (s === null ? null : s.cells))
134
126
  });
135
127
  }
136
128
 
137
- /* Проход по истории. Состояние колонки переносится вперёд, а перезамер делается
138
- * только для изменившихся в коммите файлов.
129
+ /* The run over the history. A column's state is carried forward, and only the files changed by the
130
+ * commit are measured again.
139
131
  *
140
- * `known` уже прочитанная история: проходам, которым она нужна ещё и сама по
141
- * себе (полнота покрытия), незачем звать `git log` второй раз. */
132
+ * `known` is the history already read: a pass that needs it for its own sake as well (coverage)
133
+ * has no reason to call `git log` a second time. */
142
134
  export function measureHistory(cfg, root, known) {
143
135
  const commits = known === undefined ? readHistory(root) : known;
144
- // Текст журнала нужен всегда: ссылка в раздел не метрика, но тоже чтение.
136
+ // The journal text is always needed: a link to a section is not a metric, but it is a read too.
145
137
  const needText = !!cfg.journal || cfg.metrics.some((m) => METRICS[m].needsText);
146
138
  const reads = readPlan(cfg, root, commits, needText);
147
139
  const pass = {
@@ -158,16 +150,16 @@ export function measureHistory(cfg, root, known) {
158
150
  journalPrev: ''
159
151
  };
160
152
  commits.forEach((c, ci) => stepCommit(pass, c, ci));
161
- /* Какие колонки тронул последний коммит по тому же плану чтения, по которому
162
- * идёт перенос состояния: путь колонки есть в списке изменённых путей коммита.
163
- * Страница ставит эти колонки впереди остальных: отчёт пересобирается после
164
- * каждого коммита, и первый вопрос читателя — что принесла эта правка.
153
+ /* Which columns the last commit touched, read from the same plan the state is carried along:
154
+ * a column's path is in the list of paths the commit changed. The page puts those columns ahead
155
+ * of the rest the report is rebuilt after every commit, and a reader's first question is what
156
+ * this edit brought.
165
157
  *
166
- * Берётся последний коммит, задевший хотя бы одну колонку, считая от верхушки
167
- * назад. Коммиты мимо колонок (и, прежде всего, сам отчёт, который коммитит хук)
168
- * пропускаются: правка отчёта не правка проекта. Иначе знак зависел бы от
169
- * собственного коммита отчёта: тот же прогон давал бы другие байты, отчёт
170
- * перестал бы быть неподвижной точкой, а хук коммитил бы его по второму разу. */
158
+ * The commit taken is the last one that touched at least one column, counting back from the top.
159
+ * Commits that went past the columns (above all the report itself, which the hook commits) are
160
+ * skipped: an edit to the report is not an edit to the project. Otherwise the mark would depend
161
+ * on the report's own commit the same run would produce different bytes, the report would stop
162
+ * being a fixed point, and the hook would commit it a second time. */
171
163
  let last = cfg.columns.map(() => false);
172
164
  for (let i = commits.length - 1; i >= 0 && !last.some(Boolean); i--) {
173
165
  const picks = reads.plan[i].picks.map((paths) => paths.length > 0);
@@ -176,20 +168,19 @@ export function measureHistory(cfg, root, known) {
176
168
  return { rows: pass.rows, dropped: pass.dropped, mixed: pass.mixed, state: pass.state, last: last };
177
169
  }
178
170
 
179
- /* Сверка с рабочим деревом отвечает на два вопроса, и оба обязательны: состояние
180
- * движка на HEAD совпадает с деревом коммита, и файл на диске соответствует тому
181
- * же содержимому. Первый ловит правку, потерянную при переносе состояния между
182
- * коммитами (например, у merge-коммита, которого нет в списке изменённых путей): и
183
- * потерянное создание файла (в дереве он есть, а состояние о нём не знает), и
184
- * потерянное изменение (файл есть с обеих сторон, содержимое разное), и потерянное
185
- * удаление (состояние о файле знает, а в дереве его нет). Сравнение при этом идёт
186
- * с расхождением, а не с пустотой: колонка, чей файл жил в истории и был удалён до
187
- * HEAD, пуста с обеих сторон это не потеря, а её видно в отчёте. Второй вопрос
188
- * правка, которой в истории нет вовсе. Размеры для этого не годятся: на диске они
189
- * зависят от выкладки (при `core.autocrlf=true` значение по умолчанию в установке
190
- * Git для Windows CRLF против LF), и инструмент отказывался работать там, где всё
191
- * в порядке. Файлы, изменённые в дереве, из сверки с диском выпадают: их
192
- * содержимое в коммите и на диске различается законно. */
171
+ /* The comparison against the working tree answers two questions, and both are needed: the engine's
172
+ * state at HEAD matches the tree of the commit, and the file on disk matches that same content.
173
+ * The first catches an edit lost while carrying state between commits (in a merge commit missing
174
+ * from the list of changed paths, say): a lost creation (the file is in the tree while the state
175
+ * knows nothing of it), a lost edit (the file is on both sides with different content) and a lost
176
+ * deletion (the state knows the file while the tree does not). An empty state is compared against
177
+ * the aliases present in the tree rather than against nothing: a column whose file lived in the
178
+ * history and was deleted before HEAD is empty on both sides that is not a loss, and the report
179
+ * shows as much. The second question is an edit the history does not hold at all, and sizes are no
180
+ * good for it: on disk they depend on the checkout (`core.autocrlf=true`, the default in Git for
181
+ * Windows, gives CRLF against LF), and the tool used to refuse to work where everything was in
182
+ * order. Files edited in the tree drop out of the disk comparison: their content legitimately
183
+ * differs between the commit and the disk. */
193
184
  function assertMatchesDisk(state, cfg, root) {
194
185
  const dirty = new Set(git(root, ['status', '--porcelain']).split('\n')
195
186
  .map((l) => l.trim()).filter((l) => l !== '').map((l) => l.replace(/^\S+\s+/, '').replace(/^.* -> /, '')));
@@ -202,27 +193,27 @@ function assertMatchesDisk(state, cfg, root) {
202
193
  const inTree = p === undefined ? undefined : tree.get(p);
203
194
  const lost = s === null ? aliases.length > 0 : inTree !== s.sha;
204
195
  if (lost) {
205
- refuse(EXIT.VIOLATION, 'состояние «' + col.label + '» на HEAD не совпало с деревом коммита (в дереве '
206
- + (aliases.length === 0 ? 'файла нет'
196
+ refuse(EXIT.VIOLATION, 'the state of "' + col.label + '" at HEAD did not match the tree of the commit (in the tree: '
197
+ + (aliases.length === 0 ? 'the file is absent'
207
198
  : aliases.map((alias) => alias + ' ' + tree.get(alias).slice(0, 7)).join(', '))
208
- + ', в состоянии ' + (s === null ? 'файла нет' : s.path + ' ' + s.sha.slice(0, 7))
209
- + '): перенос состояния между коммитами пропустил правку'
210
- // Пересборка здесь не починка: состояние считается тем же прогоном, и
211
- // устаревшей таблицы в этом расхождении нет. Поэтому совет называет не
212
- // команду починки, а то, чем это можно показать.
213
- + '\n починка: пересборкой это не лечитсярасхождение в самом переносе состояния,'
214
- + ' а не в таблице. Разбор: git show HEAD:' + p);
199
+ + ', in the state: ' + (s === null ? 'the file is absent' : s.path + ' ' + s.sha.slice(0, 7))
200
+ + '): carrying the state between commits lost an edit'
201
+ // Rebuilding is no fix here: the state comes from this very run, so no stale table is
202
+ // involved in this disagreement. Hence the advice names not a fix command but the way to
203
+ // show the thing.
204
+ + '\n fix: a rebuild does not cure this the discrepancy is in the carrying of the state itself,'
205
+ + ' not in the table. See it with: git show HEAD:' + p);
215
206
  }
216
207
  if (p !== undefined && !dirty.has(p) && clean.indexOf(p) < 0) clean.push(p);
217
208
  });
218
209
  if (clean.length === 0) return;
219
210
  const onDisk = diskHashes(root, clean);
220
211
  clean.forEach((p) => {
221
- if (onDisk.get(p) === tree.get(p)) return; // git считает файл неизменным
222
- if (fs.readFileSync(path.join(root, p)).equals(diskForm(root, 'HEAD', p))) return; // переводы строк необратимы
223
- refuse(EXIT.VIOLATION, 'содержимое ' + p + ' на диске разошлось с HEAD (' + onDisk.get(p).slice(0, 7)
224
- + ' вместо ' + tree.get(p).slice(0, 7) + '), хотя git не считает файл изменённым: правка есть только на диске'
225
- + '\n починка: закоммитьте правку или откатите её: git checkout -- ' + p);
212
+ if (onDisk.get(p) === tree.get(p)) return; // git counts the file as unmodified
213
+ if (fs.readFileSync(path.join(root, p)).equals(diskForm(root, 'HEAD', p))) return; // line endings are not reversible
214
+ refuse(EXIT.VIOLATION, 'the content of ' + p + ' on disk diverged from HEAD (' + onDisk.get(p).slice(0, 7)
215
+ + ' instead of ' + tree.get(p).slice(0, 7) + '), though git does not count the file as modified: the edit exists on disk only'
216
+ + '\n fix: commit the edit or roll it back: git checkout -- ' + p);
226
217
  });
227
218
  }
228
219
 
@@ -231,8 +222,8 @@ export function build(cfg, root) {
231
222
  const measured = measureHistory(cfg, root);
232
223
  assertMatchesDisk(measured.state, cfg, root);
233
224
  if (measured.mixed.length > 0) {
234
- console.error('! таблицу обновляли вместе с кодом: ' + measured.mixed.join(', ')
235
- + ' — так строка коммита не может попасть в сам коммит; обновляйте таблицу отдельным коммитом.');
225
+ console.error('! the table was updated together with the code: ' + measured.mixed.join(', ')
226
+ + ' — that way the commit cannot carry a line about itself; update the table in a separate commit.');
236
227
  }
237
228
  return measured;
238
229
  }