@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/minify.js CHANGED
@@ -2,18 +2,19 @@ import path from 'path';
2
2
  import { loadOptional } from './optional.js';
3
3
  import { refuseCause } from './refusal.js';
4
4
 
5
- /* Настоящий минификатор необязательная зависимость (её устройство в
6
- * `src/optional.js`), а здесь только то, что знает сам минификатор: какие форматы
7
- * он берёт и как считается отказ.
5
+ /* The real minifier is an optional dependency (how that works: `src/optional.js`); what lives
6
+ * here is what only the minifier knows which formats it takes and how a refusal is counted.
8
7
  *
9
- * Отказ минификатора (файл не разобрался) исключением быть обязан: расширение
10
- * соврало о содержимом, и упрощение вместо сжатия подменило бы число молча. */
8
+ * A minifier refusal (the file did not parse) has to be an exception: the extension lied about
9
+ * the content, and falling back to a simplification would silently substitute another
10
+ * number. */
11
11
 
12
- /* Расширения, за которые отвечает минификатор. Таблица единственный источник
13
- * правды и для замера, и для подписи метрики («остальные форматы приближение»),
14
- * поэтому разойтись им нечем. JSX и TSX сюда не входят: выход зависит от настройки
15
- * `jsx` проекта (`React.createElement` против `react/jsx-runtime`), и мерить чужое
16
- * решение о рантайме не наше дело; такие файлы честно считаются упрощением. */
12
+ /* The extensions the minifier answers for. This table is the single source of truth both for
13
+ * the measurement and for the metric label ("the other formats lose comments and
14
+ * indentation"), so the two cannot drift apart. JSX and TSX are not here: the output depends on the project's `jsx`
15
+ * setting (`React.createElement` versus `react/jsx-runtime`), and measuring someone else's
16
+ * decision about a runtime is not this tool's business such files are honestly counted as a
17
+ * simplification. */
17
18
  export const MINIFY_LOADERS = {
18
19
  '.js': 'js', '.mjs': 'js', '.cjs': 'js',
19
20
  '.ts': 'ts', '.mts': 'ts', '.cts': 'ts',
@@ -22,21 +23,21 @@ export const MINIFY_LOADERS = {
22
23
 
23
24
  let probed = null;
24
25
 
25
- /* Ответ разбора один на процесс: пробовать загрузку на каждом файле значило бы
26
- * платить за неё тысячи раз, а от файла решение не зависит. */
26
+ /* The probe answer is kept for the process: probing on every file would mean paying for it
27
+ * thousands of times, while the answer does not depend on the file. */
27
28
  export function minifier() {
28
29
  if (probed === null) probed = loadOptional('esbuild');
29
30
  return probed;
30
31
  }
31
32
 
32
- /* Сжатие одного текста. Настройки выхода закреплены, а не взяты по умолчанию:
33
- * `charset: utf8` потому что измеряется файл проекта в UTF-8 (умолчание
34
- * экранировало бы не-ASCII и число вышло бы больше настоящего), `legalComments:
35
- * none` потому что комментарии снимают и все прочие стратегии, и число должно
36
- * означать одну вещь, а не две. `sourcefile` нужен ради причины в отказе. */
33
+ /* Compressing one text. The output settings are pinned rather than left at their defaults:
34
+ * `charset: utf8` because what is measured is a UTF-8 file of the project (the default would
35
+ * escape non-ASCII and the number would come out larger than the real one), `legalComments:
36
+ * none` because every other strategy drops comments too and the number has to mean one thing
37
+ * rather than two, and `sourcefile` for the reason inside a refusal. */
37
38
  export function minifyWithEsbuild(text, file, rev) {
38
39
  const { tool, why } = minifier();
39
- if (tool === null) throw new Error('минификатор недоступен: ' + why);
40
+ if (tool === null) throw new Error('the minifier is unavailable: ' + why);
40
41
  const ext = path.extname(file).toLowerCase();
41
42
  try {
42
43
  return tool.transformSync(text, {
@@ -47,19 +48,19 @@ export function minifyWithEsbuild(text, file, rev) {
47
48
  sourcefile: file
48
49
  }).code;
49
50
  } catch (e) {
50
- // Совет называет один выход тот, который этой причине и отвечает: смена
51
- // минификатора на `strip` уберёт причину, но передаст тот же файл гарду
52
- // `minify.guard`, у которого разговор тот же («это не JavaScript»).
53
- refuseCause('минификатор не разобрал', 'esbuild не разобрал ' + file + ' на '
51
+ // The advice names the one way out that answers this very cause: switching the minifier to
52
+ // `strip` removes the cause but hands the same file to the `minify.guard` check, whose
53
+ // verdict would be the same ("this is not JavaScript").
54
+ refuseCause('minifier did not parse', 'esbuild did not parse ' + file + ' at '
54
55
  + rev.slice(0, 7) + ': ' + cause(e.message)
55
- + '\n починка: расширение соврало о содержимом или минификатор старше синтаксиса;'
56
- + ' задайте этому расширению упрощение в minify.ext (например {"' + ext + '": "strip-lines"})');
56
+ + '\n fix: the extension lied about its content or the minifier is older than the syntax;'
57
+ + ' give this extension a simplification in minify.ext (for example {"' + ext + '": "strip-lines"})');
57
58
  }
58
59
  }
59
60
 
60
- /* Причина у esbuild многострочная, и первая строка «Transform failed with N
61
- * errors:»; сама причина стоит там, где начинается ошибка. Без неё отказ говорил
62
- * бы, что что-то не так, но не что именно. */
61
+ /* The reason from esbuild spans several lines and its first line is "Transform failed with N
62
+ * errors:"; the cause itself stands where the error starts. Without it a refusal would say
63
+ * that something is wrong without saying what. */
63
64
  function cause(text) {
64
65
  const lines = String(text).split('\n');
65
66
  const at = lines.findIndex((line) => line.indexOf('ERROR:') >= 0);
package/src/modes.js CHANGED
@@ -13,28 +13,24 @@ import { artifact, rebuild } from './artifact.js';
13
13
  import { sensorGaps } from './metrics.js';
14
14
  import { totalsOf } from './derived.js';
15
15
 
16
- /* Режимы: что инструмент делает по запросу. Разбор аргументов в `src/args.js`, а
17
- * сюда приходит готовый план: какой режим, какой ключ, что печатать. Здесь же их
18
- * общие мелочи (знак «!» о другом счёте, вердикт, размер словами) один владелец
19
- * на все режимы, потому что один и тот же счёт и один и тот же знак не должны
20
- * разойтись между `--write`, `--data`, `--page` и `size check`.
21
- *
22
- * Что где: сборка и сверка отчёта (`--write`, проверка), данные контракта
23
- * (`--data`), полнота покрытия (`size check`), диагностика (`doctor`), хук и
24
- * объяснение пропущенной строки. Файл знает про все остальные модули сразу — это
25
- * его работа: связать их в одну команду.
16
+ /* Modes: what the tool does on request. The arguments are parsed in `src/args.js`, and a
17
+ * ready plan arrives here which mode, which flag, what to print. Their shared bits live
18
+ * here too (the "!" note about a different count, the verdict, a size in words), one owner
19
+ * for all modes, so that one count and one mark cannot diverge between `--write`, `--data`,
20
+ * `check` and the rest. Knowing every other module at once is this file's job: it ties them
21
+ * into one command.
26
22
  */
27
23
 
28
24
  function kmb(bytes) {
29
- return Math.round(bytes / 1024) + ' КБ';
25
+ return Math.round(bytes / 1024) + ' KB';
30
26
  }
31
27
 
32
- /* Деградация не ошибка, а факт отчёта: числа получены другим счётом (упрощение
33
- * вместо сжатия, оценка вместо точного счёта), потому что необязательной
34
- * зависимости нет. Факт печатается один раз на датчик и становится кодом 4 иначе
35
- * приближение уезжало бы в CI как успех. */
28
+ /* Degradation is a fact of the report, not an error: the numbers came from a different
29
+ * method (stripping instead of minification, an estimate instead of a counted one) because
30
+ * an optional dependency is missing. The fact is printed once per sensor and becomes code
31
+ * 4 otherwise a different count would travel into CI as the requested one. */
36
32
  function note(gaps) {
37
- gaps.forEach((gap) => console.error('! ' + gap.why + '\n починка: ' + gap.fix));
33
+ gaps.forEach((gap) => console.error('! ' + gap.why + '\n fix: ' + gap.fix));
38
34
  return gaps.length === 0 ? EXIT.OK : EXIT.SENSOR;
39
35
  }
40
36
 
@@ -42,12 +38,12 @@ function sensorNote(cfg) {
42
38
  return note(sensorGaps(cfg));
43
39
  }
44
40
 
45
- /* Вердикт режима вместе с заметками о датчиках: заметка печатается всегдамолчание
46
- * о другом счёте читается как точное число, и расхождение остаётся без причины, а
47
- * код остаётся первым по важности. Нарушение старше приближения (тот же порядок, что
48
- * у `size check` и у `doctor`): код 4 говорит «числа честные, но другим счётом», а
49
- * когда таблица расходится, этого никто не проверялрасхождение может быть и
50
- * настоящей правкой мимо отчёта. */
41
+ /* The mode's verdict together with the sensor notes: the note is printed always silence
42
+ * about a different count reads as the requested one, and a disagreement would be left without
43
+ * a cause while the code stays the more important one. A violation outranks a sensor note
44
+ * (the same order as in `check` and `doctor`): code 4 claims the difference is explained by the
45
+ * missing sensor, and when the table disagrees nobody checked that the disagreement may be a
46
+ * real edit that went past the report. */
51
47
  function verdict(code, gaps) {
52
48
  const sensors = note(gaps);
53
49
  return code === EXIT.OK ? sensors : code;
@@ -56,7 +52,7 @@ function verdict(code, gaps) {
56
52
  export function check(cfg, want, root) {
57
53
  const out = path.join(root, cfg.output);
58
54
  if (!fs.existsSync(out)) {
59
- console.error('✗ таблица размеров: нет файла ' + cfg.output + ' — соберите её: ' + cfg.fixCommand);
55
+ console.error('✗ size table: no file ' + cfg.output + ' — build it: ' + cfg.fixCommand);
60
56
  return 1;
61
57
  }
62
58
  const have = fs.readFileSync(out, 'utf8');
@@ -66,22 +62,22 @@ export function check(cfg, want, root) {
66
62
  const b = want.split('\n');
67
63
  let i = 0;
68
64
  while (i < a.length && i < b.length && a[i] === b[i]) i++;
69
- console.error('✗ таблица размеров: ' + cfg.output + ' расходится с историей git (строка ' + (i + 1) + '):');
70
- console.error(' в файле: ' + (a[i] === undefined ? '<строк нет>' : a[i].trim().slice(0, 160)));
71
- console.error(' по истории: ' + (b[i] === undefined ? '<строк нет>' : b[i].trim().slice(0, 160)));
65
+ console.error('✗ size table: ' + cfg.output + ' diverged from the git history (line ' + (i + 1) + '):');
66
+ console.error(' in the file: ' + (a[i] === undefined ? '<no rows>' : a[i].trim().slice(0, 160)));
67
+ console.error(' by the history: ' + (b[i] === undefined ? '<no rows>' : b[i].trim().slice(0, 160)));
72
68
  const missing = [...want.matchAll(/id="c-([^"]+)"/g)].map((m) => m[1])
73
69
  .filter((id) => have.indexOf('id="c-' + id + '"') === -1);
74
70
  if (missing.length > 0) {
75
- console.error(' строк нет в файле: ' + missing.length + ' (' + missing.slice(0, 5).join(', ')
71
+ console.error(' rows missing in the file: ' + missing.length + ' (' + missing.slice(0, 5).join(', ')
76
72
  + (missing.length > 5 ? ', …' : '') + ')');
77
73
  }
78
- console.error(' починка: ' + cfg.fixCommand + ' — и закоммитить ' + cfg.output + ' отдельным коммитом.');
74
+ console.error(' fix: ' + cfg.fixCommand + ' — and commit ' + cfg.output + ' in a commit of its own.');
79
75
  return 1;
80
76
  }
81
77
 
82
- /* Путь, названный ключом (`--write <файл>`), это настройка `output` этого
83
- * запуска: отчёт обязан называть себя тем путём, по которому лежит, иначе подпись в
84
- * нём указывала бы на чужое место. */
78
+ /* A path named on the command line (`--write <file>`) is this run's `output` setting: the
79
+ * report has to name itself by the path it lies at, or the note inside it would point
80
+ * somewhere else. */
85
81
  function withOutput(cfg, root, file) {
86
82
  if (typeof file !== 'string') return cfg;
87
83
  return Object.assign({}, cfg, { output: path.relative(root, path.resolve(file)) });
@@ -90,10 +86,10 @@ function withOutput(cfg, root, file) {
90
86
  export function writeMode(cfg, root, file) {
91
87
  const out = rebuild(withOutput(cfg, root, file), root);
92
88
  const { rows, files, now, skipped } = out.data;
93
- console.log('✓ ' + path.relative(root, out.file) + ': ' + rows.length + ' строк × ' + files.length + ' файлов, '
94
- + kmb(byteLen(out.html)) + ' (пропущено без строки: ' + skipped.length + ' — '
89
+ console.log('✓ ' + path.relative(root, out.file) + ': ' + rows.length + ' rows × ' + files.length + ' files, '
90
+ + kmb(byteLen(out.html)) + ' (skipped without a row: ' + skipped.length + ' — '
95
91
  + skipped.join(', ') + ')');
96
- console.log(' состояние на HEAD: ' + files.map((f, i) => f.label + ' '
92
+ console.log(' state at HEAD: ' + files.map((f, i) => f.label + ' '
97
93
  + (now[i] === null ? '—' : cfg.metrics.map((m) => now[i][m]).join('/'))).join(', '));
98
94
  return sensorNote(cfg);
99
95
  }
@@ -102,45 +98,45 @@ export function checkMode(cfg, root) {
102
98
  const out = artifact(cfg, root);
103
99
  const code = check(cfg, out.html, root);
104
100
  if (code === 0) {
105
- console.log('✓ отчёт: ' + out.data.rows.length + ' коммитов × ' + out.data.files.length + ' файлов '
106
- + 'совпадает с историей (' + cfg.output + ', ' + kmb(byteLen(out.html)) + ')');
101
+ console.log('✓ report: ' + out.data.rows.length + ' commits × ' + out.data.files.length + ' files '
102
+ + 'matches the history (' + cfg.output + ', ' + kmb(byteLen(out.html)) + ')');
107
103
  }
108
104
  return verdict(code, sensorGaps(cfg));
109
105
  }
110
106
 
111
- /* Ответ команды: `--json` машинная форма того же ответа, а не второй ответ.
112
- * Одна на три команды, чтобы «кто печатает и в каком виде» не разошёлся между
113
- * ними: разойтись он может только здесь, а байты ответа то, чем пользуется
114
- * агент. Текст берётся функцией: в машинной форме он не нужен вовсе. */
107
+ /* A command's answer: `--json` is the machine form of the same answer, not a second one.
108
+ * Shared by three commands so that "who prints and in which shape" cannot diverge between
109
+ * them that can only diverge here, and the bytes of the answer are what an agent consumes.
110
+ * The text comes as a function: the machine form does not need it at all. */
115
111
  function answer(rep, asJson, text) {
116
112
  if (asJson) process.stdout.write(JSON.stringify(rep, null, 2) + '\n');
117
113
  else console.log(text(rep));
118
114
  return rep;
119
115
  }
120
116
 
121
- /* Полнота покрытия (`size check`): настройки, история, пути, датчики. Не путать с
122
- * `checkMode` выше тот про таблицу и историю («файл совпадает с тем, что
123
- * сосчитано»), а этот про то, что сосчитано **всё**: ни один путь истории не
124
- * прошёл мимо колонок. Разные вопросы, поэтому и разные команды: держать отчёт в
125
- * git не обязательно, а вот полноту терять нельзя она той же проверкой и
126
- * заменяется. */
117
+ /* Coverage (`size check`): settings, history, paths, sensors. Not to be confused with
118
+ * `checkMode` above, which asks whether the file matches what was computed; this one asks
119
+ * whether **everything** was computed no path of the history went past the columns.
120
+ * Different questions, hence different commands: keeping the report in git is optional,
121
+ * losing completeness is not and this command is what replaces that check. */
127
122
  export function coverageMode(cfg, root, configFile, asJson) {
128
123
  const rep = answer(coverage(cfg, root, configFile), asJson, coverageText);
129
124
  return verdict(rep.ok ? EXIT.OK : EXIT.VIOLATION, rep.sensors);
130
125
  }
131
126
 
132
- /* Диагностика одним ответом (`size doctor`): окружение, зависимости, настройки и
133
- * покрытие сборкой из тех же кусков, что и остальные режимы. Код выхода не
134
- * «что-то не так», а первый по важности (настройкиисторияпокрытие
135
- * приближение): по нему агент ветвится, а текст читает человек. */
127
+ /* Diagnostics in one answer (`size doctor`): environment, dependencies, settings and
128
+ * coverage, assembled from the same pieces as the other modes. The exit code is not
129
+ * "something is wrong" but the first by importance (settingshistorycoverage
130
+ * sensor): an agent branches on it, a human reads the text. */
136
131
  export function doctorMode(root, configFile, asJson) {
137
132
  return answer(doctor(root, configFile), asJson, doctorText).exit;
138
133
  }
139
134
 
140
- /* Хук: установка, снятие и то, что он зовёт сам. Ставится и снимается только
141
- * явной командой; `hook-run` зовётся хуком и всегда отвечает кодом 0 — коммит уже
142
- * сделан, и валить его нечем (устройство и причины `src/hook.js`). Строка о
143
- * сделанном идёт в stderr: она часть вывода git, а не данных инструмента. */
135
+ /* The hook: installing, removing, and the call the hook itself makes. Installing and
136
+ * removing happen by explicit command only; `hook-run` is called by the hook and always
137
+ * answers 0 the commit is already made and there is nothing to fail it for (design and
138
+ * reasons: `src/hook.js`). What it did goes to stderr: it is part of git's output, not tool
139
+ * data. */
144
140
  export function hookMode(verb, root, configFile) {
145
141
  if (verb === 'hook-run') {
146
142
  const rep = hookRun(root, configFile);
@@ -152,17 +148,18 @@ export function hookMode(verb, root, configFile) {
152
148
  return rep.code;
153
149
  }
154
150
 
155
- /* Объяснение пропущенной строки (`size explain <коммит>`): ответ есть у любого
156
- * коммита, поэтому код выхода 0 и у «строка есть», и у «строки нет»; 2 только
157
- * когда названного коммита в истории нет или префикс подходит нескольким. */
151
+ /* Explaining a skipped row (`size explain <commit>`): any resolvable commit has an answer,
152
+ * so the exit code is 0 both when the row is there and when it is not; 2 belongs to a commit
153
+ * that cannot be resolved an unknown name, an ambiguous prefix, or one outside the
154
+ * history. */
158
155
  export function explainMode(cfg, root, target, asJson) {
159
156
  answer(explainCommit(cfg, root, target), asJson, explainText);
160
157
  return EXIT.OK;
161
158
  }
162
159
 
163
- /* Данные контракта в stdout — для страницы и для агента: та же правда, что в
164
- * артефакте, но без вёрстки и без производных величин. Прежняя форма `--json`
165
- * остаётся нетронутой: она заморожена эталоном паритета (fixtures/parity). */
160
+ /* Contract data on stdout — for the page and for an agent: the same truth as in the
161
+ * artifact, without markup and without derived numbers. The older `--json` form stays
162
+ * untouched: the parity fixture freezes it (`fixtures/parity`). */
166
163
  export function dataMode(cfg, root) {
167
164
  process.stdout.write(JSON.stringify(reportData(cfg, root), null, 2) + '\n');
168
165
  return sensorNote(cfg);
package/src/optional.js CHANGED
@@ -1,22 +1,24 @@
1
1
  import { createRequire } from 'module';
2
2
 
3
- /* Необязательные зависимости: минификатор и токенизатор. Их отсутствие не отказ,
4
- * а другой счёт (упрощение вместо сжатия, оценка вместо точного счёта), поэтому
5
- * загрузка у них общая и с одним устройством: ленивая, синхронная (`createRequire`
6
- * — замер синхронный проход, и `import()` сделал бы асинхронной всю цепочку ради
7
- * одного датчика) и без исключения наружу недоступность возвращается ответом.
3
+ /* Optional dependencies: the minifier and the tokenizer. Missing ones are not a refusal but a
4
+ * different count (a simplification instead of compression, an estimate instead of a counted
5
+ * one), so their loading is shared and shaped the same way: lazy, synchronous
6
+ * (`createRequire` measuring is one synchronous pass, and `import()` would make the whole
7
+ * chain asynchronous for the sake of a single sensor) and without an exception escaping —
8
+ * unavailability comes back as an answer.
8
9
  *
9
- * Шов отсутствия окружение `SIZE_REPORT_NO_OPTIONAL`: тем же путём идёт установка
10
- * без необязательных зависимостей и платформа, для которой пакета нет. Им же
11
- * проверяется, что инструмент работает и без них. */
10
+ * The seam of their absence is the `SIZE_REPORT_NO_OPTIONAL` environment variable: the same
11
+ * path serves an install without optional dependencies and a platform the package is not built
12
+ * for. It is also how the tests check that the tool works without them. */
12
13
 
13
14
  export const NO_OPTIONAL = 'SIZE_REPORT_NO_OPTIONAL';
14
15
 
15
- /* Версия берётся у самого пакета: число зависит от словаря и от алгоритма, поэтому
16
- * она попадает в способ, которым получено значение, а не остаётся в `node_modules`. */
16
+ /* The version is read from the package itself: the number depends on the dictionary and on the
17
+ * algorithm, so it belongs in the method the value came from rather than staying inside
18
+ * `node_modules`. */
17
19
  export function loadOptional(spec) {
18
20
  if (process.env[NO_OPTIONAL]) {
19
- return { tool: null, version: null, why: 'необязательные зависимости выключены (' + NO_OPTIONAL + ')' };
21
+ return { tool: null, version: null, why: 'the optional dependencies are switched off (' + NO_OPTIONAL + ')' };
20
22
  }
21
23
  const require = createRequire(import.meta.url);
22
24
  try {