@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/cli.js CHANGED
@@ -9,22 +9,19 @@ import {
9
9
  checkMode, coverageMode, dataMode, doctorMode, explainMode, hookMode, jsonMode, writeMode
10
10
  } from './modes.js';
11
11
 
12
- /* Вход инструмента: разбор строки, чтение проекта и доставка запроса режиму.
12
+ /* The tool's entry point: parse the line, read the project, hand the request to a mode.
13
13
  *
14
- * Здесь не осталось ни грамматики (`src/args.js`), ни самих режимов
15
- * (`src/modes.js`), ни закрепления настроек (`src/init.js`) только то, без чего
16
- * вход не вход: откуда берётся корень проекта, каким ключом назван файл настроек и
17
- * как отказ превращается в код выхода. Здесь же сказано вслух, когда настроек нет
18
- * и работа идёт на выведенных из проекта: это общее для всех режимов, а не их дело. Разделение не косметическое: цепочка
19
- * ветвлений «что запрошено» росла с каждым режимом и держала сложность входа, а
20
- * правила грамматики и тексты отказов проверяются своим каталогом
21
- * (`tools/refusals.js`), который считает их места в исходниках.
14
+ * Neither the grammar (`src/args.js`) nor the modes (`src/modes.js`) nor pinning the
15
+ * settings (`src/init.js`) is left hereonly what makes this an entry point at all:
16
+ * where the project root comes from, how the settings file is named, and how a refusal
17
+ * turns into an exit code. Saying out loud that there is no settings file and the settings
18
+ * were derived from the project belongs here as well: that holds for every mode, not for
19
+ * one of them.
22
20
  */
23
21
 
24
- /* Исполнитель запроса: ключ то, чем запрос назван (команда или режим), а «просто
25
- * запуск» пустая строка. Разбор уже проверил сочетания, поэтому здесь остаётся
26
- * выбор из готового списка, а не решение: у каждого названного есть свой
27
- * исполнитель, и вызывается он без пробежек по `if`. */
22
+ /* Request runners: the key is what the request is called (a command or a mode), and "a
23
+ * bare run" is the empty string. The parser has already checked the combinations, so this
24
+ * is a lookup with a runner for every name, not a decision. */
28
25
  const RUNNERS = {
29
26
  check: (c, x) => coverageMode(x.cfg, x.root, x.configFile, c.json),
30
27
  explain: (c, x) => explainMode(x.cfg, x.root, c.arg[0], c.json),
@@ -35,26 +32,26 @@ const RUNNERS = {
35
32
 
36
33
  const asked = (cmd) => (cmd.verb === null ? (cmd.mode === null ? '' : cmd.mode) : cmd.verb);
37
34
 
38
- /* Постановка хука без спроса здесь, а не в `doctor` и не в `hook-run`: первый
39
- * только докладывает, а второй зовётся уже из поставленного хука. Ставится один раз
40
- * в клоне и называется вслух, дальше молчит: отчёт обновляется после каждого
41
- * коммита без ручного шага (устройство и границы `src/hook.js`). */
35
+ /* Installing the hook without being asked belongs here, not in `doctor` (which only
36
+ * reports) and not in `hook-run` (which is called from an already installed hook). It
37
+ * installs once per clone, says so, and is silent afterwards: the report is rebuilt after
38
+ * every commit with no manual step (design and limits: `src/hook.js`). */
42
39
  function ensureHook(root, cfg) {
43
40
  const files = autoInstall(root, cfg);
44
41
  if (files === null) return;
45
- console.error('· хук поставлен: ' + files.join(', ') + ' — отчёт обновляется после каждого'
46
- + ' коммита (снять: ' + cliCommand('uninstall-hook') + ')');
42
+ console.error('· hook installed: ' + files.join(', ') + ' — the report is rebuilt after every'
43
+ + ' commit (remove it: ' + cliCommand('uninstall-hook') + ')');
47
44
  }
48
45
 
49
- /* Доставка. Диагностика и хук отвечают до чтения настроек: им нужен не весь
50
- * проект, а окружение, и отказывать им из-за настроек было бы неверно про
51
- * настройки они как раз и докладывают. */
46
+ /* Delivery. Diagnostics and the hook answer before the settings are read: they need the
47
+ * environment rather than the whole project, and refusing them over settings would be
48
+ * wrong the settings are exactly what they report about. */
52
49
  function deliver(cmd, base) {
53
50
  if (cmd.verb === 'doctor') return doctorMode(base.root, base.configFile, cmd.json);
54
51
  if (HOOK_COMMANDS.indexOf(cmd.verb) >= 0) return hookMode(cmd.verb, base.root, base.configFile);
55
52
  const ctx = { root: base.root, configFile: base.configFile, cfg: loadConfig(base.configFile, base.root) };
56
- // Примечание идёт в stderr: у `--json` и `--data` в stdout лежат данные, и
57
- // подмешивать в них рассказ о настройках значило бы ломать разбор.
53
+ // The note goes to stderr: `--json` and `--data` own stdout, and mixing a story about
54
+ // the settings into data would break parsing.
58
55
  if (ctx.cfg.derived) derivedLines(ctx.cfg).forEach((line) => console.error(line));
59
56
  ensureHook(base.root, ctx.cfg);
60
57
  return RUNNERS[asked(cmd)](cmd, ctx);
@@ -77,11 +74,11 @@ export function main() {
77
74
  console.error('✗ ' + e.message);
78
75
  return e.code;
79
76
  }
80
- // Непредвиденное дефект инструмента, а не тупик пользователя: так это и
81
- // сказано в тексте (иначе человек ищет ошибку у себя), а стек нужен целиком,
82
- // иначе такой отказ нечем разбирать.
83
- console.error('✗ внутренняя ошибка (это дефект инструмента, а не проекта —'
84
- + ' пришлите, пожалуйста, этот текст целиком):\n' + e.stack);
77
+ // Unexpected failures are a defect of the tool, not a dead end for the user, and the
78
+ // text says so otherwise the user looks for the mistake on their side. The stack is
79
+ // printed whole: nothing else can diagnose such a refusal.
80
+ console.error('✗ internal error (this is a defect of the tool, not of the project —'
81
+ + ' please send this text whole):\n' + e.stack);
85
82
  return EXIT.INTERNAL;
86
83
  }
87
84
  }
package/src/config.js CHANGED
@@ -9,36 +9,35 @@ import { TOKEN_DEFAULTS, TOKEN_FAMILIES } from './tokens.js';
9
9
  import { CATEGORY_ORDER } from './data.js';
10
10
  import { projectConfig } from './project.js';
11
11
 
12
- /* Настройки проекта-потребителя: значения по умолчанию, чтение и проверка.
13
- * Настройки описывают проект, а не механику, поэтому проверка стоит здесь же и
14
- * без неё не идёт ни один режим. */
12
+ /* The settings of a consumer project: defaults, reading and checking. Settings describe a project
13
+ * rather than the mechanics, which is why the check lives right here, and no mode runs without it. */
15
14
 
16
15
  export const CONFIG_NAME = 'size-table.config.json';
17
16
 
18
17
  export const DEFAULT_CONFIG = {
19
18
  output: 'size-report.html',
20
- locale: 'ru',
21
- title: '', // по умолчанию заголовок из локали
19
+ locale: 'en',
20
+ title: '', // by default: the heading from the locale
22
21
  heading: '',
23
- // Починка зов, который не может уйти в реестр: путь внутри проекта. Имя
24
- // пакета здесь не годится (`npx <имя>` в проекте без пакета — чужой код).
22
+ // The fix is a call that stays inside the project: the package name would send the reader to the registry,
23
+ // which serves a revision the project never pinned.
25
24
  fixCommand: invocation() + ' --write',
26
25
  metrics: ['raw', 'min'],
27
26
  columns: [],
28
- // `engine` чем считается метрика `min`: снятием балласта (умолчание, под ним
29
- // сняты замороженные эталоны) или настоящим сжатием минификатором.
27
+ // `engine` says what counts the `min` metric: stripping ballast (the default, and the one the
28
+ // frozen fixtures were taken under) or real compression by the minifier.
30
29
  minify: { engine: 'strip', ext: {}, guard: ['.js', '.mjs', '.cjs'] },
31
- // Токены: каким словарём считать. Семейство про модели, кодировка про число.
30
+ // Tokens: which dictionary counts them. The family is about models, the encoding about the number.
32
31
  tokens: Object.assign({}, TOKEN_DEFAULTS),
33
- // Автоматика хука: хук обновляет отчёт после каждого коммита и ставится сам
34
- // после установки пакета (`bin/postinstall.js`) и при первом запуске в проекте
35
- // (`src/hook.js`); этот ключ её выключатель (`.size-report/…` не нужен: снятие
36
- // хука возвращает проект к прежнему поведению).
32
+ // Hook automation: the hook rebuilds the report after every commit and installs itself after the
33
+ // package is installed (`bin/postinstall.js`) and on the first run in a project (`src/hook.js`);
34
+ // this key is its switch (no `.size-report/…` state is needed: removing the hook returns the
35
+ // project to its previous behaviour).
37
36
  hooks: { enabled: true },
38
37
  journal: null,
39
38
  links: { commitUrl: '' },
40
- // Слияние обычный коммит: у него есть правки разрешения конфликта, и без
41
- // строки они не попали бы в сумму дельт над текущим размером.
39
+ // A merge is an ordinary commit: it carries the edits that resolved a conflict, and without a row
40
+ // they would never reach the sum of deltas above the current size.
42
41
  rows: { merges: true, sha: true },
43
42
  skip: []
44
43
  };
@@ -56,49 +55,46 @@ export function gitRoot() {
56
55
  encoding: 'utf8', maxBuffer: MAX_BUF, env: gitEnv()
57
56
  }).trim();
58
57
  } catch (e) {
59
- // Два тупика с разной починкой«git не запустился» и «репозитория здесь нет»,
60
- // и расходятся они по тому, что сказал сам git, а не по догадке: ENOENT
61
- // значит, что не нашлась программа. Один текст на оба случая («не git-репозиторий
62
- // или git недоступен») не называл ни одного из них.
58
+ // Two dead ends with different fixes "git did not start" and "there is no repository here" —
59
+ // are told apart by what git itself said rather than by a guess: ENOENT means the program was not
60
+ // found. One text for both ("not a git repository, or git is unavailable") named neither of them.
63
61
  if (e.code === 'ENOENT') {
64
- refuseCause('нет git', 'git не запустился: его нет в PATH (таблица собирается по его'
65
- + ' истории, а смотрю я в ' + process.cwd() + ').\n'
66
- + ' починка: поставьте git (https://git-scm.com) и повторите команду');
62
+ refuseCause('git missing', 'git did not start: it is not in PATH (the table is built from its'
63
+ + ' history, and the directory I look in is ' + process.cwd() + ').\n'
64
+ + ' fix: install git (https://git-scm.com) and run the command again');
67
65
  }
68
- refuseCause('не git-репозиторий', 'git не видит здесь репозитория: таблица собирается по его'
69
- + ' истории (сейчас смотрю в ' + process.cwd() + ').\n'
70
- + ' смотрите: запущена ли команда из каталога проекта\n'
71
- + ' починка: если истории ещё нет создайте её: git init');
66
+ refuseCause('not a git repository', 'git sees no repository here: the table is built from its'
67
+ + ' history (the directory I look in is ' + process.cwd() + ').\n'
68
+ + ' see: whether the command was run from the directory of the project\n'
69
+ + ' fix: if there is no history yet, create it: git init');
72
70
  }
73
71
  }
74
72
 
75
- /* Настроек нетих выводит сам проект (`src/project.js`), и работа начинается сразу:
76
- * заводить файл ради первого запуска незачем, а `--init` закрепляет выведенное
77
- * файлом, когда его хотят править. Так бывает только с умолчательным именем: файл,
78
- * названный ключом `--config`, это уже запрос про конкретный файл, и его
79
- * отсутствие остаётся отказом (иначе опечатка в пути молча дала бы чужие настройки).
80
- * `path` назван словами, а не путём: файла нет, и текст «правьте <путь>» привёл бы
81
- * человека к тому, чего в проекте не лежит. */
73
+ /* No settings file the project derives them (`src/project.js`) and work starts at once: there is no
74
+ * reason to create a file for a first run, and `--init` pins the derived ones to a file when someone
75
+ * wants to edit them. This happens for the default name only: a file named by `--config` is already a
76
+ * request for that very file, so its absence stays a refusal (otherwise a typo in the path would
77
+ * silently yield someone else's settings). `path` is given in words rather than as a path: there is no
78
+ * file, and "edit <path>" would lead the reader to something the project does not have. */
82
79
  export function derivedConfig(root) {
83
80
  const cfg = derivedProfile(root);
84
- cfg.path = 'настройки, выведенные из проекта';
81
+ cfg.path = 'derived from the project';
85
82
  cfg.derived = true;
86
83
  validateConfig(cfg);
87
84
  return cfg;
88
85
  }
89
86
 
90
- /* Выведенное из проекта + умолчаниято, чем проект работает без файла, и то,
91
- * что закрепляет `--init`. Одно место на две роли (иначе «файл» и «работа без
92
- * файла» разошлись бы колонкой или числом), а сам вывод (`src/project.js`) о
93
- * умолчаниях не знает: он говорит только то, что видит в проекте. */
87
+ /* Derived from the project plus the defaults what a project runs on without a file, and what
88
+ * `--init` pins. One place for two roles (or "the file" and "work without a file" would drift by a
89
+ * column or a number), while the deriving itself (`src/project.js`) knows nothing of the defaults: it
90
+ * says only what it sees in the project. */
94
91
  export function derivedProfile(root) {
95
92
  return withDefaults(projectConfig(root));
96
93
  }
97
94
 
98
- /* Настройки поверх умолчанийодним местом на два источника (файл и проект):
99
- * вложенное досыпается по ключам, потому что `minify: {engine: …}` не значит «у
100
- * `minify` больше нет других полей», а значило бы, что снятие балласта потеряло
101
- * список расширений. */
95
+ /* Settings on top of the defaults one place for two sources (a file and the project): nested keys
96
+ * are filled in by key, because `minify: {engine: …}` does not mean "`minify` has no other fields",
97
+ * and reading it that way would lose the stripping's list of extensions. */
102
98
  function withDefaults(raw) {
103
99
  const cfg = Object.assign({}, DEFAULT_CONFIG, raw);
104
100
  ['minify', 'tokens', 'hooks', 'links', 'rows'].forEach((key) => {
@@ -107,25 +103,25 @@ function withDefaults(raw) {
107
103
  return cfg;
108
104
  }
109
105
 
110
- /* Настройки читаются как есть и досыпаются значениями по умолчанию: у проекта,
111
- * который только подключил генератор, конфиг может быть в три строки. */
106
+ /* Settings are read as they are and filled in with the defaults: in a project that has just attached
107
+ * the generator the config may be three lines long. */
112
108
  export function loadConfig(file, root) {
113
109
  if (!fs.existsSync(file)) {
114
- // Совет называет тот же файл, о котором шла речь: `--init` без файла записал бы
115
- // черновик под умолчательным именем в корне проекта то есть починил бы не то,
116
- // о чём спросили. Имя не называем ровно тогда, когда оно и так умолчательное.
110
+ // The advice names the very file in question: `--init` without a file would write a draft under the
111
+ // default name in the project rootfixing something other than what was asked. The name is left
112
+ // out exactly when it is the default one anyway.
117
113
  const dflt = root !== undefined && path.resolve(root, CONFIG_NAME) === path.resolve(file);
118
114
  if (dflt) return derivedConfig(root);
119
- refuseCause('нет файла настроек', 'нет файла настроек ' + file
120
- + '\n создайте его: ' + cliCommand('--init ' + advicePath(file))
121
- + '\n смотрите: без «--config» настройки не нужныони выводятся из проекта');
115
+ refuseCause('no settings file', 'no settings file ' + file
116
+ + '\n create it: ' + cliCommand('--init ' + advicePath(file))
117
+ + '\n see: without "--config" no settings are needed they are derived from the project');
122
118
  }
123
119
  let raw;
124
120
  try {
125
121
  raw = JSON.parse(fs.readFileSync(file, 'utf8'));
126
122
  } catch (e) {
127
- refuseCause('настройки не разобраны', 'не разобран ' + file + ': ' + e.message
128
- + '\n починка: правьте ' + file + '; образец настроек даёт ' + cliCommand('--init') + ' в пустом каталоге');
123
+ refuseCause('settings not parsed', 'cannot parse ' + file + ': ' + e.message
124
+ + '\n fix: edit ' + file + '; a sample of settings comes from ' + cliCommand('--init') + ' in an empty directory');
129
125
  }
130
126
  const cfg = withDefaults(raw);
131
127
  cfg.path = file;
@@ -133,23 +129,23 @@ export function loadConfig(file, root) {
133
129
  return cfg;
134
130
  }
135
131
 
136
- /* Колонки: метка и пути имена, а не что попало: путь числом или объектом молча не
137
- * совпадает ни с чем, и колонка отчитывается нулём строк за успех. Отказ обязан
138
- * случиться здесь, а не превратиться в пустой отчёт. */
132
+ /* Columns: a label and paths have to be names rather than anything at all a path given as a number
133
+ * or an object silently matches nothing, and the column reports zero rows as success. The refusal has
134
+ * to happen here instead of turning into an empty report. */
139
135
  function checkColumns(cfg, fail) {
140
- if (!cfg.columns || cfg.columns.length === 0) fail('не задано ни одной колонки (columns)');
136
+ if (!cfg.columns || cfg.columns.length === 0) fail('no columns are given (columns)');
141
137
  const labels = new Set();
142
138
  cfg.columns.forEach((c, i) => {
143
139
  const pathsAreNames = c && Array.isArray(c.paths)
144
140
  && c.paths.length > 0 && c.paths.every((p) => typeof p === 'string' && p !== '');
145
141
  if (!c || typeof c.label !== 'string' || c.label === '' || !pathsAreNames) {
146
- fail('колонка ' + (i + 1) + ' должна быть {label, paths: [...]} из непустых строк: '
147
- + JSON.stringify(c).slice(0, 90) + '\n смотрите: черновик с готовыми колонками даёт '
148
- + cliCommand('--init <файл>'));
142
+ fail('column #' + (i + 1) + ' has to be {label, paths: [...]} of non-empty strings: '
143
+ + JSON.stringify(c).slice(0, 90) + '\n see: a draft with ready columns comes from '
144
+ + cliCommand('--init <file>'));
149
145
  }
150
- if (labels.has(c.label)) fail('метка колонки «' + c.label + '» повторяется');
146
+ if (labels.has(c.label)) fail('the column label "' + c.label + '" repeats');
151
147
  if (c.category !== undefined && CATEGORY_ORDER.indexOf(c.category) < 0) {
152
- fail('категория «' + c.category + '» у колонки «' + c.label + '» неизвестна: '
148
+ fail('the category "' + c.category + '" of the column "' + c.label + '" is unknown: '
153
149
  + CATEGORY_ORDER.join(', '));
154
150
  }
155
151
  labels.add(c.label);
@@ -157,51 +153,51 @@ function checkColumns(cfg, fail) {
157
153
  }
158
154
 
159
155
  function checkMetrics(cfg, fail) {
160
- if (!Array.isArray(cfg.metrics) || cfg.metrics.length === 0) fail('не заданы метрики (metrics)');
156
+ if (!Array.isArray(cfg.metrics) || cfg.metrics.length === 0) fail('no metrics are given (metrics)');
161
157
  cfg.metrics.forEach((m) => {
162
- if (!METRICS[m]) fail('неизвестная метрика «' + m + '» (есть: ' + Object.keys(METRICS).join(', ') + ')');
158
+ if (!METRICS[m]) fail('unknown metric "' + m + '" (there are: ' + Object.keys(METRICS).join(', ') + ')');
163
159
  });
164
160
  }
165
161
 
166
162
  function checkMinify(cfg, fail) {
167
163
  if (MINIFY_ENGINES.indexOf(cfg.minify.engine) < 0) {
168
- fail('неизвестный способ минификации «' + cfg.minify.engine + '» (есть: ' + MINIFY_ENGINES.join(', ') + ')');
164
+ fail('unknown minification engine "' + cfg.minify.engine + '" (there are: ' + MINIFY_ENGINES.join(', ') + ')');
169
165
  }
170
166
  }
171
167
 
172
168
  function checkTokens(cfg, fail) {
173
169
  const family = TOKEN_FAMILIES[cfg.tokens.family];
174
170
  if (family === undefined) {
175
- fail('неизвестное семейство токенизатора «' + cfg.tokens.family + '» (есть: '
171
+ fail('unknown tokenizer family "' + cfg.tokens.family + '" (there are: '
176
172
  + Object.keys(TOKEN_FAMILIES).join(', ') + ')');
177
173
  }
178
174
  if (family.encodings.indexOf(cfg.tokens.encoding) < 0) {
179
- fail('неизвестная кодировка токенизатора «' + cfg.tokens.encoding + '» у семейства '
180
- + cfg.tokens.family + ' (есть: ' + family.encodings.join(', ') + ')');
175
+ fail('unknown tokenizer encoding "' + cfg.tokens.encoding + '" of the family '
176
+ + cfg.tokens.family + ' (there are: ' + family.encodings.join(', ') + ')');
181
177
  }
182
178
  }
183
179
 
184
- /* Файл таблицы не может быть её колонкой: размер артефакта зависит от числа строк,
185
- * то есть от самого себя. */
180
+ /* The report file cannot be a column of itself: the size of the artifact depends on the number of
181
+ * rows, that is, on itself. */
186
182
  function checkOutput(cfg, fail) {
187
183
  cfg.columns.forEach((c) => {
188
- if (c.paths.indexOf(cfg.output) >= 0) fail('файл таблицы (' + cfg.output + ') не может быть колонкой');
184
+ if (c.paths.indexOf(cfg.output) >= 0) fail('the size table file (' + cfg.output + ') cannot be a column');
189
185
  });
190
- if (!cfg.output) fail('не задан output');
186
+ if (!cfg.output) fail('output is not set');
191
187
  }
192
188
 
193
189
  function checkJournal(cfg, fail) {
194
190
  if (!cfg.journal) return;
195
- if (!cfg.journal.path) fail('journal.path не задан');
196
- if (!cfg.journal.pattern) fail('journal.pattern не задан');
197
- try { new RegExp(cfg.journal.pattern); } catch (e) { fail('journal.pattern не компилируется: ' + e.message); }
191
+ if (!cfg.journal.path) fail('journal.path is not set');
192
+ if (!cfg.journal.pattern) fail('journal.pattern is not set');
193
+ try { new RegExp(cfg.journal.pattern); } catch (e) { fail('journal.pattern does not compile: ' + e.message); }
198
194
  }
199
195
 
200
- /* Что настройки говорят про путь: `columns` — его отслеживает колонка, `excluded` — он
201
- * объявлен исключением (`skip` и сам файл отчёта), `outside` — мимо того и другого.
202
- * Суждение одно на два ответа: `check` спрашивает его про всю историю, `explain` про
203
- * один коммит. Знакомство считается по колонкам целиком, а не по метке: у колонки путей
204
- * может быть несколько (переименование), и любой из них — она сама. */
196
+ /* What the settings say about a path: `columns` — a column tracks it, `excluded` — it is declared an
197
+ * exception (`skip` and the report file itself), `outside` — neither. One judgement for two answers:
198
+ * `check` asks it about the whole history, `explain` about a single commit. Membership is counted over
199
+ * all paths of a column rather than by its label: a column may have several paths (a rename), and any
200
+ * of them is that column. */
205
201
  export function pathRoles(cfg) {
206
202
  const tracked = new Set();
207
203
  cfg.columns.forEach((col) => col.paths.forEach((p) => tracked.add(p)));
@@ -212,26 +208,26 @@ export function pathRoles(cfg) {
212
208
  };
213
209
  }
214
210
 
215
- /* Починка «мимо колонок»один текст на два ответа, и он называет пути: команда без
216
- * имён не команда. Текст собирается из имён, а не приписывает их по ветке, поэтому и
217
- * предусматривать тут нечего: коммит без файлов вовсе починки не получаетна пустом
218
- * списке её не зовут (см. `fixFor` в `src/explain.js`). */
211
+ /* The fix for "outside the columns" one text for two answers, and it names the paths: a command
212
+ * without names is no command. The text is assembled from the names rather than appending them per
213
+ * branch, which is why nothing has to be guarded here: a commit with no files at all gets no fix the
214
+ * list being empty, nobody calls it (`fixFor` in `src/explain.js`). */
219
215
  export function outsideFix(paths) {
220
- return 'допишите эти пути колонкой или в «skip» файла ' + CONFIG_NAME + ': ' + paths.join(', ');
216
+ return 'add these paths as a column or to "skip" of ' + CONFIG_NAME + ': ' + paths.join(', ');
221
217
  }
222
218
 
223
219
  export function validateConfig(cfg) {
224
- const fail = (msg) => refuseCause('настройки неверны',
225
- 'конфиг ' + cfg.path + ': ' + msg + '\n починка: правьте ' + cfg.path);
220
+ const fail = (msg) => refuseCause('settings invalid',
221
+ 'config ' + cfg.path + ': ' + msg + '\n fix: edit ' + cfg.path);
226
222
  checkColumns(cfg, fail);
227
223
  checkMetrics(cfg, fail);
228
224
  checkMinify(cfg, fail);
229
225
  checkTokens(cfg, fail);
230
- if (!LOCALES[cfg.locale]) fail('неизвестная локаль «' + cfg.locale + '» (есть: ' + Object.keys(LOCALES).join(', ') + ')');
231
- // Выключатель хука «да/нет», а не «правда/ложь»: `false` от случайной строки
232
- // отличать обязан инструмент, иначе выключенная автоматика осталась бы включённой.
226
+ if (!LOCALES[cfg.locale]) fail('unknown locale "' + cfg.locale + '" (there are: ' + Object.keys(LOCALES).join(', ') + ')');
227
+ // The hook switch is a yes/no rather than a truthy/falsy one: the tool has to tell `false` from a
228
+ // stray string, or switched-off automation would stay switched on.
233
229
  if (typeof cfg.hooks.enabled !== 'boolean') {
234
- fail('hooks.enabled не «да/нет»: ' + JSON.stringify(cfg.hooks.enabled));
230
+ fail('hooks.enabled is not a yes/no: ' + JSON.stringify(cfg.hooks.enabled));
235
231
  }
236
232
  checkOutput(cfg, fail);
237
233
  checkJournal(cfg, fail);
package/src/css.js CHANGED
@@ -1,23 +1,23 @@
1
1
  import fs from 'node:fs';
2
2
 
3
- /* Оформление отчёта обычные `.css` рядом с кодом, а не строки внутри модулей:
4
- * то же правило, что и у программы страницы (`src/page/app.js`), исходник видит
5
- * редактор, а не только шаблонная строка. Читаются они с диска относительно
6
- * своего места, поэтому работают и у того, кто поставил пакет.
3
+ /* The report's styling is ordinary `.css` next to the code rather than strings inside modules — the same rule
4
+ * as for the page's program (`src/page/app.js`): an editor sees a real source file instead of a template
5
+ * string. They are read from disk relative to their own place, so they work for whoever installed the package
6
+ * as well.
7
7
  *
8
- * Наборов два, и у каждого своя роль:
8
+ * There are two sets, each with a role of its own:
9
9
  *
10
- * 1. `table.css` — **таблица**: геометрия клеток, липкие шапка и колонка коммита,
11
- * подпись коммита, цвета дельт.
12
- * 2. `page/app.css` — оформление страницы **сверх таблицы**: холст, панель
13
- * выбора, состояния пустоты и адаптации под узкое окно.
10
+ * 1. `table.css` — the **table**: cell geometry, the sticky header and commit column, a commit's caption,
11
+ * the colours of the deltas.
12
+ * 2. `page/app.css` — the page's look **on top of the table**: the canvas, the panel of choices, the empty
13
+ * states and the adaptation to a narrow window.
14
14
  *
15
- * Соглашение о цвете дельт задано один раз в `table.css`: `.up` зелёный, `.down`
16
- * красный (рост «больше логики», а не тревога). Второго места у него нет
17
- * намеренно: рост не может быть показан разными цветами в двух местах одной
18
- * страницы. Смена соглашения — две строки в `table.css`.
15
+ * The convention about the colour of a delta is set once, in `table.css`: `.up` green, `.down` red (growth is
16
+ * "more logic" rather than alarm). It has no second place on purpose: growth cannot be shown in different
17
+ * colours in two spots of one page. Changing the convention is two lines in `table.css`.
19
18
  *
20
- * Путь у `readCss` от каталога `src/`: так его видит движок, где бы он ни лежал.
19
+ * The path given to `readCss` is relative to the `src/` directory: that is how the engine sees it wherever it
20
+ * lies.
21
21
  */
22
22
 
23
23
  export const TABLE_CSS = readCss('./table.css');
package/src/data.js CHANGED
@@ -6,15 +6,13 @@ import { build, skipLine } from './history.js';
6
6
  import { projectTree } from './project.js';
7
7
  import { TOOL_PKG } from './tool.js';
8
8
 
9
- /* Категории файлов и контракт со страницей: абсолютные значения и устройство
10
- * таблицы, без единой производной величины. Всё, что страница считает сама,
11
- * начинается там, где этот модуль заканчивается. */
9
+ /* File categories and the contract with the page: absolute values and the shape of the table, with no
10
+ * derived quantity at all. Everything the page counts itself begins where this module ends. */
12
11
 
13
- /* Категория файла только для быстрых кнопок «включить/выключить группу» на
14
- * странице: на числа она не влияет. Правило одно расширение даёт категорию, всё
15
- * остальное считается кодом; категория, заданная в настройках колонки, старше
16
- * правила, и в данных видно, откуда она взялась (`categoryBy`): ручное решение
17
- * объяснимо, а таблица расширений — догадка по имени файла. */
12
+ /* A file's category is only for the page's quick on/off buttons for a group: it does not reach the
13
+ * numbers. The rule is one: the extension gives the category, everything else counts as code; a category
14
+ * set in the column's settings outranks that rule, and the data says where it came from (`categoryBy`) — a
15
+ * manual decision is explainable, while a table of extensions is a guess by file name. */
18
16
  export const CATEGORY_EXTS = {
19
17
  docs: ['.md', '.markdown', '.rst', '.txt', '.adoc'],
20
18
  chore: ['.json', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf', '.lock', '.editorconfig'],
@@ -29,15 +27,14 @@ export function categoryOf(col) {
29
27
  return { key: known === undefined ? 'code' : known, by: 'auto' };
30
28
  }
31
29
 
32
- /* Контракт между движком и страницей: абсолютные значения и устройство таблицы
33
- * и ни одной производной величины. Дельты, суммы, «сейчас» и фильтры считает
34
- * страница: движок не знает, что включено в просмотр, поэтому заранее посчитать
35
- * сумму он не может. Числа в контракте те же, что в артефакте, — это та же правда,
36
- * разложенная по полям.
30
+ /* The engine-to-page contract: deltas, totals, "now" and the filters are counted by the page the engine
31
+ * does not know what is switched on in the view, so it cannot total anything up in advance. The numbers
32
+ * here are the same as in the artifact: one truth laid out in fields.
37
33
  *
38
- * Причины пропущенных коммитов идут строками и остаются ими: у страницы нет
39
- * вопроса, на который пригодилось бы поле («почему у коммита нет строки» задают
40
- * командой `explain`, и там причина уже разложена). */
34
+ * The reasons of skipped commits travel as strings and stay strings: they are sentences for a person, and
35
+ * the question behind them (why a commit has no row) is asked by the `explain` command, where the reason is
36
+ * laid out into fields. The page does not carry this list at all, and on purpose: the report's own commit
37
+ * is one of the skipped ones, so the file would never become a fixed point. */
41
38
  export function reportData(cfg, root) {
42
39
  const { rows, state, dropped, last } = build(cfg, root);
43
40
  const loc = LOCALES[cfg.locale];
@@ -67,45 +64,24 @@ export function reportData(cfg, root) {
67
64
  categories: CATEGORY_ORDER.filter((key) => files.some((f) => f.category === key))
68
65
  .map((key) => ({ key: key, label: loc.categories[key] })),
69
66
  files: files,
70
- /* Дерево страницы дерево проекта: все пути, а не только колонки. Числа есть
71
- * лишь у колонок, поэтому у каждой записи каталога сказано, чего ей не
72
- * досталось: `why` пусто файл измеряется, иначе названа причина. */
67
+ /* The page's tree is the project's tree: every path rather than the columns alone. The numbers belong to
68
+ * columns only, which is why each catalogue entry says what it did not get: `why` empty — the file is
69
+ * measured, otherwise a reason is named. */
73
70
  catalog: projectTree(root, cfg.output, files.map((f) => (f.path === null ? f.paths[0] : f.path))),
74
71
  rows: rows.map((r) => Object.assign(rowShape(r),
75
72
  { href: rowHref(r.section, r.sha, cfg), values: r.cells })),
76
73
  now: state.map((s) => (s === null ? null : s.cells)),
77
- /* Какие колонки тронул последний коммит: страница по этому знаку ставит их
78
- * впереди читателю нужнее то, что изменилось только что. Это факт из истории,
79
- * а не производная величина: она не считает, а говорит, чего коснулась правка. */
74
+ /* Which columns the last commit touched: the page puts those in front by this mark — a reader needs what
75
+ * changed just now rather than the rest. This is a fact from the history rather than a derived quantity:
76
+ * it does not count anything, it says what the edit touched. */
80
77
  last: last,
81
- approx: approxMarks(rows, state, cfg),
82
78
  skipped: dropped.map(skipLine)
83
79
  };
84
80
  }
85
81
 
86
- /* Пометки приближённых клеток по одной записи на метрику: строка знаков по
87
- * клеткам строк и строка знаков по верхней строке «сейчас». '1' число получено
88
- * упрощением или оценкой, '0' точное. Метрика без ни одной пометки в отчёте не
89
- * появляется вовсе: все числа точны — молчание.
90
- *
91
- * Знак ставит движок там же, где считает число, — из того же правила, что и
92
- * подпись метрики. Поэтому страница ничего про пути и форматы не выводит: она
93
- * только показывает то, что сказано, и второго правила точности не заводит. */
94
- function approxMarks(rows, state, cfg) {
95
- const out = {};
96
- cfg.metrics.forEach((m) => {
97
- const mark = (flags) => (flags !== null && flags[m] ? '1' : '0');
98
- const inRows = rows.map((r) => r.approx.map(mark).join('')).join('');
99
- const now = state.map((s) => mark(s === null ? null : s.approx)).join('');
100
- if (inRows.indexOf('1') >= 0 || now.indexOf('1') >= 0) out[m] = { rows: inRows, now: now };
101
- });
102
- return out;
103
- }
104
-
105
- /* Общая часть строки ответа: она есть и у контракта страницы (`--data`), и у
106
- * прежней формы `--json` (она заморожена эталоном паритета). Одно место — потому
107
- * что разойтись эти два ответа могут ровно здесь, а **порядок полей и есть байты
108
- * ответа**: они в объекте в том же порядке, в каком их печатает замороженная форма. */
82
+ /* The part of a row shared by both answers: the page's contract (`--data`) and the older `--json` form (frozen
83
+ * by the parity fixture). One place, because these two answers can drift apart exactly here, and **the order
84
+ * of the fields is the bytes of the answer**: the object holds them in the order the frozen form prints. */
109
85
  export function rowShape(r) {
110
86
  return {
111
87
  sha: r.sha,
@@ -115,7 +91,7 @@ export function rowShape(r) {
115
91
  };
116
92
  }
117
93
 
118
- /* Производные величины живут в `src/derived.js`: их считает и артефакт (импорт
119
- * ниже), и страница (получает тот же файл текстом). Второго расчёта той же
120
- * таблицы нет вовсе, поэтому разойтись молча двум отчётам нечемэто стережёт
121
- * `test/contract-derived.test.js`. */
94
+ /* The derived quantities live in `src/derived.js`: the page gets that whole file pasted into itself as text
95
+ * (`pageScript`), while the terminal answers import it. There is no second calculation of the same table, so
96
+ * the page's numbers and the artifact's bytes have nothing to drift apart with `test/contract-derived.test.js`
97
+ * guards that. */