@vernikr/size-report 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/init.js CHANGED
@@ -5,64 +5,63 @@ import { advicePath, cliCommand, refuseCause } from './refusal.js';
5
5
  import { writeFileEnsured } from './artifact.js';
6
6
  import { packageManager } from './project.js';
7
7
 
8
- /* Закрепление настроек файлом (`--init`): то, что проект вывел о себе сам
9
- * (`src/project.js`), записывается туда, где его встретит следующий запуск.
8
+ /* Pinning the settings to a file (`--init`): what the project derived about itself
9
+ * (`src/project.js`) is written where the next run will meet it.
10
10
  *
11
- * Отдельным модулем от вывода профиля: тот смотрит на проект впервые и почти всё о
12
- * нём угадывает, а этот делает одну вещь кладёт результат файлом и говорит, что
13
- * записал. Требование к себе одно и оно жёсткое: **закреплённое обязано проходить
14
- * ту же проверку, которой его встретит первый запуск** иначе подсказка приводит
15
- * человека в новый тупик (BLOCKERS §N2, REFACTOR R-0.4).
11
+ * A module of its own, separate from deriving the profile: that one looks at the project for the first
12
+ * time and guesses about almost everything, while this one does a single thing — puts the result into
13
+ * a file and says what it wrote. It has one strict requirement of itself: **what was pinned has to
14
+ * pass the very check the first run will apply**, or the advice leads a person into a new dead end.
16
15
  */
17
16
 
18
- /* Что сказать после записи: тем же порядком, что и раньше, что записано, чем
19
- * заменятся приближения и что делать дальше. Строки собираются списком, а не
20
- * печатаются по ходу: тогда «что сказано» читается целиком. */
17
+ /* What to say after writing: what was written, what will replace the approximations, and what to do
18
+ * next. The lines are assembled into a list rather than printed as they come, so that "what was said"
19
+ * can be read as a whole. */
21
20
  function draftLines(root, target, cfg) {
22
21
  const hasPkg = fs.existsSync(path.join(root, 'package.json'));
23
22
  const manager = packageManager(root);
24
23
  return [
25
- '✓ настройки выведены из проекта и закреплены: ' + path.relative(root, target),
26
- ' колонок: ' + cfg.columns.length + ' (' + cfg.columns.map((c) => c.label).slice(0, 6).join(', ')
24
+ '✓ settings derived from the project and pinned: ' + path.relative(root, target),
25
+ ' columns: ' + cfg.columns.length + ' (' + cfg.columns.map((c) => c.label).slice(0, 6).join(', ')
27
26
  + (cfg.columns.length > 6 ? ', …' : '') + ')',
28
- ' исключено путей: ' + cfg.skip.length + ' (сам отчёт, замки зависимостей, карты, собранное)',
29
- ' метрика min: настоящее сжатие (esbuild); без негочестное упрощение и код 4',
30
- ' метрика tok: словарь o200k_base (gpt-tokenizer); без негооценка по длине и код 4',
31
- ' журнал: ' + (cfg.journal === null ? 'не найденссылки строк будут без разделов' : cfg.journal.path),
32
- ' дальше: правьте колонки и метрикикакие файлы важны, знает только проект',
27
+ ' paths skipped: ' + cfg.skip.length + ' (the report itself, dependency locks, maps, build output)',
28
+ ' metric min: real compression (esbuild); without itan honest simplification and code 4',
29
+ ' metric tok: the o200k_base dictionary (gpt-tokenizer); without itan estimate by length and code 4',
30
+ ' journal: ' + (cfg.journal === null ? 'not foundrow links will carry no sections' : cfg.journal.path),
31
+ ' next: edit the columns and the metrics which files matter is known by the project alone',
33
32
  ' ' + (hasPkg
34
- ? 'добавьте в package.json "sizes": "size --write" — тогда отчёт будет звать '
35
- + manager + ' run sizes (проверкабез --write)'
36
- : 'запуск: ' + cfg.fixCommand + ' (проверкабез --write)'),
37
- ' ' + (hasPkg ? 'добавьте ' + manager + ' run test:sizes в CI' : 'добавьте проверку в CI')
38
- + '; проверка команда пакета, своих файлов в проект она не приносит'
33
+ ? 'add "sizes": "size --write" to package.json then the report will be built by '
34
+ + manager + ' run sizes (the check without --write)'
35
+ : 'run: ' + cfg.fixCommand + ' (the check without --write)'),
36
+ ' ' + (hasPkg ? 'add ' + manager + ' run test:sizes to CI' : 'add the check to CI')
37
+ + '; the check is the package\'s command, it brings no files of its own into the project'
39
38
  ];
40
39
  }
41
40
 
42
- /* Пустой профиль не отказ, а примечание: работа сделана, а колонки за человека не
43
- * выберет никто. Поэтому «!», а не «✗»: знак и код выхода не имеют права говорить
44
- * разное (каталог отказов считает такие знаки отдельно). */
41
+ /* An empty profile is a note rather than a refusal: the work was done, and nobody will pick the columns
42
+ * for the person. Hence "!", not the cross: a mark and an exit code must not say different things (the
43
+ * refusal catalogue counts a cross as a refusal and a note as not one). */
45
44
  function noteNoColumns(root, target, cfg) {
46
45
  if (cfg.columns.length > 0) return;
47
- console.error('! в проекте не нашлось путей, которые можно взять колонками'
48
- + ' (история пуста или в ней нет знакомых расширений): черновик записан без колонок'
49
- + '\n впишите их руками в ' + path.relative(root, target)
50
- + ' — без колонок проверка настроек скажет «не задано ни одной колонки»');
46
+ console.error('! no paths in the project could be taken as columns'
47
+ + ' (the history is empty or holds no familiar extensions): the draft is written without columns'
48
+ + '\n write them by hand into ' + path.relative(root, target)
49
+ + ' — without columns the settings check will say "no columns are given (columns)"');
51
50
  }
52
51
 
53
52
  export function initMode(root, file, force) {
54
53
  const target = file ? path.resolve(root, file) : path.join(root, CONFIG_NAME);
55
54
  if (fs.existsSync(target) && !force) {
56
- // Совет называет тот же файл, о котором шла речь: `--init --force` без файла
57
- // перезаписал бы черновиком умолчательное имя, а не тот файл, что человек звал.
55
+ // The advice names the very file in question: `--init --force` without a file would overwrite the
56
+ // default name with a draft rather than the file the person named.
58
57
  const name = file === undefined || file === null ? CONFIG_NAME : advicePath(file);
59
- refuseCause('конфиг уже есть', 'конфиг уже есть: ' + target
60
- + '\n починка: правьте его или перезапишите черновиком: ' + cliCommand('--init ' + name + ' --force'));
58
+ refuseCause('config already exists', 'config already exists: ' + target
59
+ + '\n fix: edit it or overwrite it with a draft: ' + cliCommand('--init ' + name + ' --force'));
61
60
  }
62
- // Закреплённое то же, чем проект работает без файла (вывод из проекта поверх
63
- // умолчаний), и оно же обязано проходить ту же проверку, которой его встретит
64
- // запуск: путь в тексте отказа тот файл, куда оно легло. «Выведено» и «путь
65
- // отказа» в файл не пишутся: это свойства не настроек, а того, откуда они взялись.
61
+ // What is pinned is the very thing the project runs on without a file (the project's derivation on
62
+ // top of the defaults), and it has to pass the same check the run will apply: the path in a refusal
63
+ // text is the file it landed in. "Derived" and that path are not written to the file: they are
64
+ // properties of where the settings came from rather than of the settings.
66
65
  const cfg = derivedProfile(root);
67
66
  if (cfg.columns.length > 0) validateConfig(Object.assign({}, cfg, { path: target }));
68
67
  const written = Object.assign({}, cfg);
package/src/journal.js CHANGED
@@ -1,9 +1,9 @@
1
- /* Журнал и ссылки: к какому разделу относится коммит и куда ведёт его описание.
2
- * Работает на тексте, а не на git: разделы ищутся в уже прочитанном файле. */
1
+ /* The journal and links: which section a commit belongs to and where its description leads.
2
+ * It works on text rather than on git: sections are searched for in an already read file. */
3
3
 
4
- /* Журнал (WORKLOG/CHANGELOG/…): раздел, к которому относится коммит. Формат
5
- * заголовков задаётся регуляркой в конфиге; именованные группы `id` (короткий
6
- * номер для `§N`) и `title` необязательныбез них номер берётся первым словом. */
4
+ /* A journal (WORKLOG/CHANGELOG/…): the section a commit belongs to. The heading format comes
5
+ * from a regular expression in the settings; the named groups `id` (the short number used by
6
+ * `§N`) and `title` are optional without them the number is the first word. */
7
7
  export function parseSections(text, pattern) {
8
8
  const re = new RegExp(pattern);
9
9
  const list = [];
@@ -23,9 +23,11 @@ export function parseSections(text, pattern) {
23
23
  return list;
24
24
  }
25
25
 
26
- /* Раздел, к которому относится коммит: сначала вновь заведённый, иначе
27
- * последний из отредактированных (журналы ведутся по возрастанию, так что правка
28
- * почти всегда касается хвоста). */
26
+ /* The section a commit belongs to: a newly opened one first, otherwise the last of the edited
27
+ * ones in document order. Taking the last assumes a journal written in ascending order, as this
28
+ * tool's own defaults expect; the "newly added" case holds for any order, while in a journal
29
+ * written newest-first (CHANGELOG.md is one) the fallback would name the oldest edited section
30
+ * rather than the newest. */
29
31
  export function touchedSection(prevText, nowText, pattern) {
30
32
  const prev = new Map(parseSections(prevText, pattern).map((s) => [s.head, s.body]));
31
33
  const now = parseSections(nowText, pattern);
@@ -42,10 +44,10 @@ export function touchedSection(prevText, nowText, pattern) {
42
44
  return null;
43
45
  }
44
46
 
45
- /* Якорь как у GitHub (github-slugger): пунктуация снимается целиком, каждый
46
- * пробел становится дефисом. Отсюда «13 — эфф»«13--эфф» (два дефиса: тире
47
- * выброшено, пробелы остались каждый своим). Дефис, подчёркивание и буквы
48
- * (включая кириллицу) сохраняются, остальные пробельные символы — нет. */
47
+ /* The anchor as GitHub makes it (github-slugger): punctuation is dropped entirely and every
48
+ * space becomes a hyphen. Hence "13 — eff""13--eff" (two hyphens: the dash is gone while both
49
+ * spaces stay). Hyphens, underscores and letters (Cyrillic included) survive; other whitespace
50
+ * does not. */
49
51
  export function anchor(head) {
50
52
  return head.toLowerCase().replace(/[^\p{L}\p{N} _-]/gu, '').replace(/ /g, '-');
51
53
  }
@@ -56,9 +58,9 @@ export function sectionLink(section, cfg) {
56
58
  return cfg.journal.url + '#' + anchor(what);
57
59
  }
58
60
 
59
- /* Ссылка строки: на раздел журнала, если раздел есть, иначе на сам коммит (шаблон
60
- * из настроек). Одно место для артефакта и для данных страницы: адрес раздела
61
- * правило GitHub, и второе его воплощение разъехалось бы с первым. */
61
+ /* The link of a row: to a journal section when there is one, otherwise to the commit itself (the
62
+ * template comes from the settings). One place for the artifact and for the data of the page: a
63
+ * section address follows GitHub's rule, and a second copy of it would drift from the first. */
62
64
  export function rowHref(section, sha, cfg) {
63
65
  if (section) return sectionLink(section, cfg);
64
66
  if (!cfg.links.commitUrl) return null;
package/src/locales.js CHANGED
@@ -1,9 +1,7 @@
1
- /* Тексты отчёта: заголовки, подписи и служебные слова на языке проекта. Это
2
- * словарь, а не рендер: механика таблицы от языка не зависит. */
3
-
4
- /* Тексты артефакта: колонка, метрики и служебные слова. Механика (заголовки
5
- * таблицы, клетки, дельты) от локали не зависит, поэтому перевод — это словарь,
6
- * а не отдельный рендер. */
1
+ /* Texts of the artifact and of the report page: headings, labels and service words, in the
2
+ * language of the project. A dictionary, not a renderer: the mechanics of the table (cells,
3
+ * deltas, headings) do not depend on the language, so translating a report means adding
4
+ * words here rather than writing a second renderer. */
7
5
  export const LOCALES = {
8
6
  ru: {
9
7
  html: 'ru',
@@ -12,29 +10,36 @@ export const LOCALES = {
12
10
  total: 'Общий объём',
13
11
  now: 'сейчас',
14
12
  categories: { code: 'Код', docs: 'Документация', chore: 'Служебные', assets: 'Ресурсы' },
15
- /* Тексты страницы отчёта. Они лежат в самом файле отчёта (отдельным словарём,
16
- * рядом с данными), поэтому меняются вместе с ними правка слова стоит
17
- * пересборки отчёта, иначе файл разойдётся с историей. */
13
+ /* The page texts live inside the report file itself (a dictionary of their own, next to
14
+ * the data), so changing a word costs a rebuild of the report otherwise the file
15
+ * drifts from the history. */
18
16
  page: {
19
17
  metrics: 'Метрики',
20
18
  files: 'Файлы',
21
19
  dir: 'все файлы папки {name} ({n})',
22
20
  dirNone: 'ни один файл папки {name} в отчёт не попал (в папке {n})',
23
- /* Знак сложенной папки своя цель нажатия: галочка отвечает за числа,
24
- * знак за то, сколько дерева видно. */
21
+ /* The fold mark is a click target of its own: the checkbox answers for the numbers,
22
+ * the mark for how much of the tree is visible. */
25
23
  foldClose: 'свернуть папку {name}',
26
24
  foldOpen: 'развернуть папку {name}',
27
- /* Причина у листа: в дереве он подписью, а не галочкой, и без этих слов
28
- * читатель решил бы, что файл потерялся. */
25
+ /* The reason belongs to a leaf: in the tree it is a label rather than a checkbox,
26
+ * and without these words a reader would think the file was lost. */
29
27
  notMeasuredRule: 'не измеряется: такой файл колонкой быть не может'
30
28
  + ' (собранный, замок зависимостей, сам отчёт или слишком крупный)',
31
29
  notMeasuredChoice: 'не измеряется: в набор колонок отчёта не попал'
32
30
  + ' — набор задают настройки (columns)',
31
+ /* The tooltip of a measured file: where the file stands and how its category was decided.
32
+ * A file absent on HEAD is named by the path it came with, and a category comes either from
33
+ * the settings or from the extension — both are answers to the reader's "why so". */
34
+ notOnHead: ' (нет на HEAD)',
35
+ category: ' · категория: ',
36
+ categoryFromConfig: 'из настроек',
37
+ categoryByExtension: 'по расширению',
33
38
  all: 'все',
34
39
  sub: '{tool} {version} · {artifact}',
35
- /* Слова точности: подпись метрики говорит про худшее в колонке, а клетки
36
- * про своё число. Одно слово на подпись и на подсказку клетки, чтобы не
37
- * разошлось и не звучало по-разному. */
40
+ /* Words of precision: a metric label speaks about the worst in its column, a cell
41
+ * about its own number. One word serves both the label and the cell hint, so the two
42
+ * cannot drift or sound differently. */
38
43
  exact: 'точное число',
39
44
  approximate: 'приближение',
40
45
  approximateCell: 'приближённое число: ',
@@ -88,6 +93,10 @@ export const LOCALES = {
88
93
  + ' (built, a dependency lock, the report itself, or too large)',
89
94
  notMeasuredChoice: 'not measured: it is not in the report’s set of columns'
90
95
  + ' — that set comes from the settings (columns)',
96
+ notOnHead: ' (not on HEAD)',
97
+ category: ' · category: ',
98
+ categoryFromConfig: 'from the settings',
99
+ categoryByExtension: 'by extension',
91
100
  all: 'all',
92
101
  sub: '{tool} {version} · {artifact}',
93
102
  exact: 'an exact number',
package/src/metrics.js CHANGED
@@ -4,18 +4,18 @@ import { EXACT_STRATEGIES, assertCompilable, byteLen, minifyForm, strategyFor }
4
4
  import { MINIFY_LOADERS, minifier, minifyWithEsbuild } from './minify.js';
5
5
  import { CHARS_PER_TOKEN, isBinary, tokenCount, tokenizer } from './tokens.js';
6
6
 
7
- /* Реестр метрик: что измеряется, нужен ли метрике текст и насколько честна цифра.
8
- * Отдельно от способов снятия балласта: метрика это обещание про число, а не
9
- * способ его получить.
7
+ /* The registry of metrics: what is measured, whether a metric needs the text, and how honest
8
+ * its number is. Separate from the ways of stripping ballast: a metric is a promise about a
9
+ * number rather than a way to obtain one.
10
10
  *
11
- * Описание метрики для читателя берётся не из полей реестра, а из `metricView`:
12
- * у одной и той же метрики оно зависит от настроек (`min` это настоящее сжатие
13
- * или упрощение, `tok` точный словарь или оценка), и разойтись двум ответам на
14
- * один вопрос негде. */
11
+ * The description a reader sees comes from `metricView` rather than from the registry fields:
12
+ * for one and the same metric it depends on the settings (`min` is either real compression or
13
+ * a simplification, `tok` either an exact dictionary or an estimate), and two answers to one
14
+ * question have nowhere to drift apart. */
15
15
 
16
- /* Способы получить метрику `min`: снятие балласта и настоящее сжатие. Механизм у
17
- * них разный, и обещание тоже, поэтому у каждого свой способ, своя честность и своё
18
- * примечаниеи никто из них не выдаётся за другого. */
16
+ /* The ways to obtain the `min` metric: stripping ballast and real compression. Their mechanism
17
+ * differs, and so does their promise, so each has its own method, its own honesty and its own
18
+ * noteand neither is passed off as the other. */
19
19
  export const MINIFY_ENGINES = ['strip', 'esbuild'];
20
20
 
21
21
  const STYLES = {
@@ -82,10 +82,10 @@ export const METRICS = {
82
82
  if (esbuildLoader(file, cfg) !== null) return byteLen(minifyWithEsbuild(text, file, rev));
83
83
  const min = minifyForm(text, file, cfg);
84
84
  const ext = path.extname(file).toLowerCase();
85
- /* Гард стриппера стережёт упрощение, а не минификатор: минификатор разбирает
86
- * файл сам и о своей неудаче говорит отказом (`src/minify.js`), а этот гард
87
- * отвечает на вопрос, не выбросило ли наше снятие балласта чего-нибудь, кроме
88
- * комментариев и отступов. */
85
+ /* The stripper's guard watches the simplification rather than the minifier: the minifier
86
+ * parses the file itself and reports its failure as a refusal (`src/minify.js`), while
87
+ * this guard answers whether our stripping threw away anything but comments and
88
+ * indentation. */
89
89
  if (strategyFor(file, cfg) === 'strip-js' && cfg.minify.guard.indexOf(ext) >= 0) {
90
90
  assertCompilable(min, rev, file, text);
91
91
  }
@@ -108,8 +108,8 @@ export const METRICS = {
108
108
  }
109
109
  };
110
110
 
111
- /* Описание метрики для читателя: `note` что означает число, `method` чем оно
112
- * получено, `accuracy` точное оно или приближённое. */
111
+ /* The description of a metric for a reader: `note` is what the number means, `method` how it
112
+ * was obtained, `accuracy` whether it is exact or approximate. */
113
113
  export function metricView(name, cfg) {
114
114
  const metric = METRICS[name];
115
115
  if (metric.view !== undefined) return metric.view(cfg);
@@ -121,12 +121,12 @@ export function metricView(name, cfg) {
121
121
  };
122
122
  }
123
123
 
124
- /* Подпись метрики `min`. Соглашение о честности: `accuracy` говорит про худшее в
125
- * колонке, а способ называет, где именно приближение,поэтому один формат без
126
- * минификатора делает метрику приближённой целиком, а не прячется за «exact»
127
- * соседнего файла. Худшее берётся по тому же правилу, что и пометки клеток
128
- * (`pointExact`), а не по названию способа: отчёт, где нет ни одного
129
- * приближённого формата, точен и со снятым балластом. */
124
+ /* The label of the `min` metric. The honesty convention: `accuracy` speaks about the worst in
125
+ * the column, while the method says where exactly the approximation is so one format without
126
+ * a minifier makes the whole metric approximate instead of hiding behind the "exact" of a
127
+ * neighbouring file. The worst is taken by the same rule as the cell marks (`pointExact`)
128
+ * rather than by the name of the method: a report with no approximate format at all is exact
129
+ * even with the ballast stripped. */
130
130
  function minView(cfg) {
131
131
  const loc = cfg.locale;
132
132
  const rough = approximateFormats('min', cfg);
@@ -149,11 +149,11 @@ function minView(cfg) {
149
149
  };
150
150
  }
151
151
 
152
- /* Подпись метрики `tok`. Соглашение о честности то же, что у `min`: способ говорит,
153
- * каким словарём снято число (семейство и кодировка часть счёта, а не подробность),
154
- * а `accuracy` точное оно или приближённое. Приближённым оно становится в двух
155
- * случаях, и оба названы словами: форматы, для которых токены не считаются (у них
156
- * число идёт по байтам), и отсутствие словаря (тогда счёт идёт оценкой по длине). */
152
+ /* The label of the `tok` metric. The honesty convention is the same as for `min`: the method
153
+ * says which dictionary produced the number (family and encoding are part of the count rather
154
+ * than a detail), and `accuracy` says whether it is exact or approximate. It turns approximate
155
+ * in two cases, both spelled out: formats for which tokens are not counted (their number runs
156
+ * by bytes) and a missing dictionary (then the count is an estimate by length). */
157
157
  function tokView(cfg) {
158
158
  const loc = cfg.locale;
159
159
  const settings = cfg.tokens;
@@ -179,24 +179,23 @@ function tokView(cfg) {
179
179
  };
180
180
  }
181
181
 
182
- /* Точное ли число у конкретной клеткиодно правило и для подписи метрики, и для
183
- * пометки клетки. Поэтому подпись не может разойтись с клетками, а список
184
- * приближённых форматов считается здесь же, по тому же правилу.
182
+ /* Whether a particular cell holds an exact number one rule for both the metric label and the
183
+ * cell mark. That is why the label cannot drift from the cells, and why the list of
184
+ * approximate formats is computed here, by the same rule.
185
185
  *
186
- * `min` точен там, где файл действительно минифицируется: минификатором или
187
- * разбором формата, который короче уже не станет (JSON теряет только незначащие
188
- * пробелы список точных стратегий ведёт `strip.js`, потому что стратегии живут
189
- * там). `tok` точен там, где есть словарь и формат текстовый: «токены» картинки
190
- * или шрифта — это её байты. `raw` и `gzip` точны всегда: это однозначные
191
- * величины. */
186
+ * `min` is exact where the file really is minified: by the minifier, or by parsing a format
187
+ * that cannot get any shorter (JSON loses only insignificant whitespace — the list of exact
188
+ * strategies is owned by `strip.js`, where the strategies live). `tok` is exact where a
189
+ * dictionary exists and the format is text: the "tokens" of a picture or a font are its bytes.
190
+ * `raw` and `gzip` are always exact: they are unambiguous quantities. */
192
191
  export function pointExact(name, file, cfg) {
193
192
  if (name === 'min') return minifiedForm(file, cfg);
194
193
  if (name === 'tok') return tokenizer(cfg.tokens).tool !== null && !isBinary(file);
195
194
  return true;
196
195
  }
197
196
 
198
- /* Форматы этого отчёта, которые будут измерены приближённо. Список выводится из
199
- * настроек и правила точности, а не пишется руками. */
197
+ /* The formats of this report that will be measured approximately. The list is derived from the
198
+ * settings and the rule of exactness rather than written by hand. */
200
199
  function approximateFormats(name, cfg) {
201
200
  const exts = [];
202
201
  cfg.columns.forEach((col) => {
@@ -213,9 +212,10 @@ function minifiedForm(file, cfg) {
213
212
  return esbuildLoader(file, cfg) !== null || EXACT_STRATEGIES.indexOf(strategyFor(file, cfg)) >= 0;
214
213
  }
215
214
 
216
- /* Идёт ли файл в минификатор: сжатие запрошено, доступно и не отменено явным
217
- * выбором проекта — `minify.ext` старше движка и служит выходом, если расширение
218
- * соврало о содержимом. Ответ один на два вопроса: как считать и что обещать. */
215
+ /* Whether the file goes to the minifier: compression is requested, available and not overridden
216
+ * by an explicit choice of the project — `minify.ext` outranks the engine and serves as the way
217
+ * out when an extension lied about its content. One answer serves two questions: how to count
218
+ * and what to promise. */
219
219
  function esbuildLoader(file, cfg) {
220
220
  if (minEngine(cfg) !== 'esbuild') return null;
221
221
  const ext = path.extname(file).toLowerCase();
@@ -223,46 +223,47 @@ function esbuildLoader(file, cfg) {
223
223
  return MINIFY_LOADERS[ext] === undefined ? null : MINIFY_LOADERS[ext];
224
224
  }
225
225
 
226
- /* Действующий способ: запрошенный может быть недоступен тогда метрика отступает
227
- * к другому счёту, а отступление объявляется наружу (`sensorGaps`), иначе
228
- * приближение ушло бы как точное число. */
226
+ /* The engine actually in force: the requested one may be unavailable, in which case the metric
227
+ * falls back to another count and the fallback is announced (`sensorGaps`), or an
228
+ * approximation would travel as an exact number. */
229
229
  export function minEngine(cfg) {
230
230
  if (cfg.minify.engine !== 'esbuild') return 'strip';
231
231
  return minifier().tool === null ? 'strip' : 'esbuild';
232
232
  }
233
233
 
234
- /* Чего не хватает для того, что просили: причина и починка для человека, по одной
235
- * на датчик. Причина загрузчика уходит только сюдав подписи метрики она была бы
236
- * машинной строкой (путём чужого `node_modules`), от которой вывод перестал бы
237
- * быть одинаковым на разных машинах, а в подписи отчёта понятным. */
234
+ /* What is missing for what was asked: a cause and a fix for a human, one per sensor. The
235
+ * loader's cause goes here and nowhere elseinside the metric label it would be a machine
236
+ * string (a path into someone else's `node_modules`) that would make the output differ between
237
+ * machines, while the label in the report has to stay readable. */
238
238
  export function sensorGaps(cfg) {
239
239
  const gaps = [];
240
240
  const minify = minifier();
241
241
  if (cfg.minify.engine === 'esbuild' && minify.tool === null) {
242
242
  gaps.push({
243
- why: 'метрика «min» считает упрощением: минификатор недоступен — ' + minify.why,
244
- fix: 'поставьте необязательные зависимости заново или задайте "minify": {"engine": "strip"}'
243
+ why: 'the metric "min" counts by simplification: the minifier is unavailable — ' + minify.why,
244
+ fix: 'install the optional dependencies again or set "minify": {"engine": "strip"}'
245
245
  });
246
246
  }
247
247
  if (cfg.metrics.indexOf('tok') >= 0) {
248
248
  const tokens = tokenizer(cfg.tokens);
249
249
  if (tokens.tool === null) {
250
250
  gaps.push({
251
- why: 'метрика «tok» считает оценкой по длине: словаря нет — ' + tokens.why,
252
- fix: 'поставьте необязательные зависимости заново или уберите "tok" из metrics'
251
+ why: 'the metric "tok" counts by an estimate of length: there is no dictionary — ' + tokens.why,
252
+ fix: 'install the optional dependencies again or remove "tok" from metrics'
253
253
  });
254
254
  }
255
255
  }
256
256
  return gaps;
257
257
  }
258
258
 
259
- /* Одно место, где решается, читать метрику из размера объекта или из текста:
260
- * метрика без текста на недогруженном блобе ошибка, а не молчаливый ноль. */
259
+ /* The one place that decides whether a metric is read from the object size or from the text:
260
+ * a text-based metric on an unloaded blob is an error rather than a silent zero. */
261
261
  export function measureBlob(name, blob, file, cfg, rev) {
262
262
  const metric = METRICS[name];
263
263
  if (metric.fromSize) return blob.size;
264
264
  if (blob.text === null) {
265
- throw new Error('метрике «' + name + '» нужно содержимое ' + file + ' на ' + rev.slice(0, 7) + ', а оно не прочитано');
265
+ throw new Error('the metric "' + name + '" needs the content of ' + file + ' at '
266
+ + rev.slice(0, 7) + ', and it was not read');
266
267
  }
267
268
  return metric.measure(blob.text, file, cfg, rev);
268
269
  }
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 are an approximation"), so the
14
+ * 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);