@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/strip/js.js CHANGED
@@ -1,20 +1,20 @@
1
- /* Снятие комментариев и отступовметрика «объём без балласта», а не
2
- * минификация: пробелы внутри строк и порядок токенов не трогаются (это позволит
3
- * сравнивать числа между языками и не зависит от чужого инструмента, которого в
4
- * проекте нет). Строки и шаблоны проходят насквозь, блочный комментарий
5
- * заменяется пробелом, чтобы `a` и `b` из `a` + блочный комментарий + `b` не
6
- * склеились в одно имя, перевод строки после `//` сохраняется он разделяет
7
- * токены.
1
+ /* Comments and indentation stripped"volume without ballast" rather than minification:
2
+ * spaces inside strings and the order of tokens stay untouched, which keeps numbers
3
+ * comparable across languages and keeps this path free of any foreign tool (the real
4
+ * minifier is an optional dependency, and this is what remains without it). Strings and
5
+ * templates pass through whole, a block comment becomes a space so that `a` and `b` around
6
+ * one do not merge into a single name — and the newline after `//` survives, because it
7
+ * separates tokens.
8
8
  *
9
- * Проход разложен по случаям: комментарий, регексп, строка и обычный символ.
10
- * Случай говорит, сколько он съел (`false` — не его), и все они делят одно
11
- * состояние прохода; состояния «по копии на случай» здесь не заводится, потому
12
- * что решение о регекспе зависит от всего, что уже выведено.
9
+ * The pass is split by case: comment, regex, string, ordinary character. A case reports how
10
+ * much it consumed (`false` — not its turn), and all of them share one pass state; a state
11
+ * per case is not kept because the decision about a regex depends on everything printed so
12
+ * far.
13
13
  */
14
14
 
15
- /* Состояние прохода: текст, место в нём и то, чем отличают регексп от деления,
16
- * последний значимый символ вывода и хвост последнего слова (после `return` идёт
17
- * выражение, а не деление). */
15
+ /* Pass state: the text, the position in it, and what tells a regex from a division the last
16
+ * significant character printed and the tail of the last word (`return` is followed by an
17
+ * expression rather than by a division). */
18
18
  function scanOf(src) {
19
19
  return { src: src, out: '', i: 0, last: '', word: '' };
20
20
  }
@@ -22,15 +22,15 @@ function scanOf(src) {
22
22
  export function stripJs(src) {
23
23
  const s = scanOf(src);
24
24
  while (s.i < src.length) {
25
- /* Комментарные пары проверяются до регекси: ни `/`, ни `*` не могут быть
26
- * первым символом литерала регекспа, а вот `/*` в начале файла обычное дело. */
25
+ /* Comment pairs are checked before regexes: neither `/` nor `*` can start a regex
26
+ * literal, while `/*` is an ordinary thing to meet. */
27
27
  if (skipLineComment(s) || skipBlockComment(s) || takeRegex(s) || takeString(s)) continue;
28
28
  putChar(s);
29
29
  }
30
30
  return s.out;
31
31
  }
32
32
 
33
- // Строчный комментарий: `//` съедается до перевода строки, сам перевод остаётся.
33
+ // A line comment: `//` is eaten up to the newline, the newline itself stays.
34
34
  function skipLineComment(s) {
35
35
  if (s.src[s.i] !== '/' || s.src[s.i + 1] !== '/') return false;
36
36
  const nl = s.src.indexOf('\n', s.i);
@@ -38,7 +38,7 @@ function skipLineComment(s) {
38
38
  return true;
39
39
  }
40
40
 
41
- // Блочный комментарий на пробел: он разделяет имена, но не занимает объём.
41
+ // A block comment becomes a space: it separates names without taking up volume.
42
42
  function skipBlockComment(s) {
43
43
  if (s.src[s.i] !== '/' || s.src[s.i + 1] !== '*') return false;
44
44
  const end = s.src.indexOf('*/', s.i + 2);
@@ -47,10 +47,10 @@ function skipBlockComment(s) {
47
47
  return true;
48
48
  }
49
49
 
50
- /* Регексп начинается там, где ожидается операнд: после оператора, открывающей
51
- * скобки или ключевого слова. Признак грубый, но его хватает: без него
52
- * `replace(/\//g, …)` читалось бы как начало строчного комментария и резало
53
- * строку (проверено гардом компиляции). */
50
+ /* A regex starts where an operand is expected: after an operator, an opening bracket or a
51
+ * keyword. The sign is crude, and it can afford to be: a regex this pass mistakes for code,
52
+ * or code it mistakes for a comment, does not compile and that is exactly what the guard
53
+ * checks (`strip/guard.js`). */
54
54
  const REGEX_KEYWORDS = ['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void', 'case', 'do', 'else', 'yield', 'await'];
55
55
 
56
56
  function regexAllowed(last, word) {
@@ -75,7 +75,7 @@ function endOfRegex(src, start) {
75
75
  while (i < src.length) {
76
76
  const ch = src[i];
77
77
  if (ch === '\\') { i += 2; continue; }
78
- if (ch === '\n') return start + 1; // наткнулись на строку значит, это был не регексп
78
+ if (ch === '\n') return start + 1; // a newline means this was not a regex after all
79
79
  if (ch === '[') inClass = true;
80
80
  else if (ch === ']') inClass = false;
81
81
  else if (ch === '/' && !inClass) return i + 1;
@@ -84,7 +84,7 @@ function endOfRegex(src, start) {
84
84
  return start + 1;
85
85
  }
86
86
 
87
- // Строка или шаблон целиком, вместе со своим содержимым.
87
+ // A string or a template passes through whole, its content included.
88
88
  function takeString(s) {
89
89
  const quote = s.src[s.i];
90
90
  if (quote !== '"' && quote !== "'" && quote !== '`') return false;
@@ -102,7 +102,7 @@ function endOfString(src, start, quote) {
102
102
  const ch = src[i];
103
103
  if (ch === '\\') { i += 2; continue; }
104
104
  if (ch === quote) return i + 1;
105
- // В шаблоне `${…}` живёт выражение, а в нём свои строки.
105
+ // A template holds an expression in `${…}`, and that expression has strings of its own.
106
106
  if (quote === '`' && ch === '$' && src[i + 1] === '{') { i = endOfTemplateExpr(src, i + 2); continue; }
107
107
  i++;
108
108
  }
@@ -123,8 +123,8 @@ function endOfTemplateExpr(src, start) {
123
123
  return src.length;
124
124
  }
125
125
 
126
- /* Обычный символ: он попадает в вывод, а состояние прохода запоминает по нему,
127
- * где может стоять регексп. */
126
+ /* An ordinary character: it goes into the output, and the pass state remembers by it where a
127
+ * regex may stand. */
128
128
  function putChar(s) {
129
129
  const ch = s.src[s.i];
130
130
  s.out += ch;
package/src/strip.js CHANGED
@@ -2,16 +2,13 @@ import path from 'path';
2
2
  import { stripJs } from './strip/js.js';
3
3
  import { compactJson, stripCss, stripHtml, stripLines } from './strip/forms.js';
4
4
 
5
- /* Снятие балласта: правило, какая форма текста к какому файлу применяется.
6
- * Только преобразование текста ни истории, ни настроек этот модуль не знает.
5
+ /* Stripping ballast: which form of text applies to which file. Only text transformation —
6
+ * the module knows no history and reads no settings, only the strategy it is handed. What
7
+ * binds the forms to a file lives here: the extension, the strategy, and which strategies
8
+ * count as exact.
7
9
  *
8
- * Разбор форм лежит рядом и по предметам: проход по JS (`strip/js.js`), формы
9
- * разметки, стилей, строк и JSON (`strip/forms.js`) и гард компиляции
10
- * (`strip/guard.js`). Здесь остаётся то, что связывает их с файлом: расширение,
11
- * стратегия и что считать точным числом.
12
- *
13
- * Имена форм наружу отдаются отсюда же: точка входа пакета берёт их по одному
14
- * адресу, и переезд разбора не должен быть виден тому, кто на них опирался. */
10
+ * The forms are re-exported from here, so that the package entry point has one address for
11
+ * them and a move inside the parsing stays invisible to whoever relied on them. */
15
12
 
16
13
  export { stripJs, stripCss, stripHtml, stripLines, compactJson };
17
14
  export { assertCompilable } from './strip/guard.js';
@@ -20,10 +17,10 @@ export function byteLen(text) {
20
17
  return Buffer.byteLength(text, 'utf8');
21
18
  }
22
19
 
23
- /* Стратегия по расширению. Незнакомое расширение получает снятие отступов и
24
- * пустых строк безопасный минимум: снимать комментарии «на глаз» в синтаксисе,
25
- * которого генератор не знает (например, `#` в YAML или отступы в Python),
26
- * значило бы мерить уже другой файл. */
20
+ /* The strategy for an extension. An unknown extension gets indentation and blank lines
21
+ * removedthe safe minimum: stripping comments by eye in a syntax the generator was never
22
+ * taught (`#` in YAML, indentation in Python) would measure a different file than the one on
23
+ * disk. */
27
24
  const MINIFY_BY_EXT = {
28
25
  '.js': 'strip-js', '.mjs': 'strip-js', '.cjs': 'strip-js',
29
26
  '.html': 'strip-html', '.htm': 'strip-html',
@@ -32,11 +29,11 @@ const MINIFY_BY_EXT = {
32
29
  };
33
30
  export const STRATEGIES = ['strip-js', 'strip-html', 'strip-css', 'json', 'strip-lines', 'none'];
34
31
 
35
- /* Стратегии, которые и есть минификация: JSON теряет только незначащие пробелы
36
- * (числа приводятся к кратчайшей записи), и короче его не сделает никто. Остальные
37
- * — упрощение: они снимают балласт, но не переименовывают и не перестраивают код,
38
- * и обещать за них точное число нельзя. Список ведёт тот модуль, который владеет
39
- * стратегиями; метрика по нему решает, точное у неё число или приближённое. */
32
+ /* The strategies that are minification itself: JSON loses only insignificant whitespace
33
+ * (numbers take their shortest form) and nobody can make it shorter. The rest are a
34
+ * simplification they drop ballast but neither rename nor restructure code, so no exact
35
+ * number can be promised for them. The list is owned here, next to the strategies, and the
36
+ * metric reads it to decide whether its number is exact or approximate. */
40
37
  export const EXACT_STRATEGIES = ['json'];
41
38
 
42
39
  export function strategyFor(file, cfg) {
@@ -44,8 +41,8 @@ export function strategyFor(file, cfg) {
44
41
  return (cfg.minify.ext && cfg.minify.ext[ext]) || MINIFY_BY_EXT[ext] || 'strip-lines';
45
42
  }
46
43
 
47
- /* Что делает стратегия разбор формы принадлежит ей, а не списку здесь:
48
- * диспетчер только выбирает, кого позвать, и повторяет словарь стратегий. */
44
+ /* What a strategy does belongs to the strategy rather than to this list: the dispatcher only
45
+ * picks whom to call, and repeats the strategy names because the parsing lives elsewhere. */
49
46
  export function minifyForm(text, file, cfg) {
50
47
  const how = strategyFor(file, cfg);
51
48
  if (how === 'none') return text;
@@ -54,5 +51,5 @@ export function minifyForm(text, file, cfg) {
54
51
  if (how === 'strip-css') return stripLines(stripCss(text));
55
52
  if (how === 'json') return compactJson(text);
56
53
  if (how === 'strip-lines') return stripLines(text);
57
- throw new Error('неизвестная стратегия минификации «' + how + '» (есть: ' + STRATEGIES.join(', ') + ')');
54
+ throw new Error('unknown minification strategy "' + how + '" (have: ' + STRATEGIES.join(', ') + ')');
58
55
  }
package/src/table.css CHANGED
@@ -1,35 +1,34 @@
1
- /* Гарнитура одна на всю таблицу; числа выравниваются по разрядам за счёт
2
- * tabular-nums, а не за счёт моноширинного шрифта. */
1
+ /* One type face for the whole table; numbers line up by their digits thanks to tabular-nums rather than through a
2
+ * monospaced font. */
3
3
  table { border-collapse: collapse; font-variant-numeric: tabular-nums; }
4
4
  th, td { padding: 2px 7px; border-bottom: 1px solid rgba(127, 127, 127, .25); white-space: nowrap; }
5
- /* Шапка из двух строк: обе липкие, поэтому вторая сдвинута ровно на высоту первой
6
- * (line-height 20 + 2px нижней границы), иначе строки накладывались бы друг на друга. */
5
+ /* A two-row header: both rows stick, so the second is offset by exactly the first one's height (line-height 20 plus the
6
+ * 2px bottom border), or the rows would overlap. */
7
7
  thead th { position: sticky; top: 0; z-index: 3; background: Canvas; text-align: center; line-height: 20px; padding: 0 7px; }
8
8
  thead tr:first-child th { border-bottom-width: 2px; }
9
9
  thead tr:last-child th { top: 22px; }
10
10
  .num { text-align: right; }
11
11
  .g { border-left: 1px solid rgba(127, 127, 127, .35); }
12
- /* Липкая левая колонка: фон непрозрачный (Canvas), иначе при скролле вправо под
13
- * клеткой были бы видны числа. Ярус выше соседних клеток (2) и ниже шапки (3);
14
- * угол шапки выше всех, иначе группы колонок наползают на «Коммит». */
12
+ /* The sticky left column: an opaque background (Canvas), or numbers would show through the cell when scrolling sideways. A
13
+ * tier above the neighbouring cells (2) and below the header (3); the header's corner is above them all, or the column groups
14
+ * would crawl over the commit column. */
15
15
  .c-commit { position: sticky; left: 0; z-index: 2; background: Canvas; text-align: left; font-weight: 400; }
16
16
  .c-commit a { color: inherit; }
17
17
  thead .c-commit { z-index: 6; }
18
- /* Ширину колонки задаёт этот блок. Без него содержимое ячейки выходило за её
19
- * границы и рисовалось поверх соседних чисел: у ячейки таблицы нет обрезки. */
18
+ /* This block sets the column's width. Without it the cell's content went past its borders and was drawn over the neighbouring
19
+ * numbers: a table cell clips nothing. */
20
20
  .clip { display: flex; align-items: baseline; gap: 6px; width: 300px; }
21
21
  .when { flex: none; opacity: .7; }
22
22
  .subj { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; display: block; }
23
23
  .subj.plain { opacity: .7; }
24
24
  .sect { flex: none; opacity: .7; font-size: 11px; text-decoration: none; border-bottom: 1px dotted currentColor; }
25
- /* Рост зелёный, спад красныйпо договорённости с заказчиком (рост «больше
26
- * логики», а не тревога). */
25
+ /* Growth green, fall redby agreement with whoever asked for the report (growth is "more logic" rather than alarm). */
27
26
  .up { color: #1e8449; }
28
27
  .down { color: #c0392b; }
29
28
  .miss { opacity: .5; }
30
- /* Верхняя строка текущие размеры: она же и объясняет, к чему относятся дельты. */
29
+ /* The top row holds the current sizes: it is also what explains what the deltas refer to. */
31
30
  tr.now th, tr.now td { border-bottom: 2px solid rgba(127, 127, 127, .35); }
32
31
  tr.now .c-commit { font-weight: 600; }
33
- /* Подсветка строки наложением, а не подменой фона: липкая колонка обязана
34
- * оставаться непрозрачной, иначе под ней при скролле видны числа. */
32
+ /* A row's highlight is laid over rather than swapped in: the sticky column has to stay opaque, or numbers show through it
33
+ * when scrolling. */
35
34
  tbody tr:hover th, tbody tr:hover td { background-image: linear-gradient(rgba(127, 127, 127, .08), rgba(127, 127, 127, .08)); }
package/src/tokens.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import path from 'path';
2
2
  import { loadOptional } from './optional.js';
3
3
 
4
- /* Токенизатор та же дисциплина, что у минификатора: необязательная зависимость с
5
- * ленивой загрузкой (устройство в `src/optional.js`). Отличие одно: токенизатор
6
- * берёт любой текст, отказать ему не в чем, поэтому отсутствие зависимостине
7
- * отказ, а другой счёт: оценка по длине, помеченная приближением в подписи метрики.
4
+ /* The tokenizer follows the same discipline as the minifier: an optional dependency, loaded
5
+ * lazily (how that works: `src/optional.js`). One difference: the tokenizer takes any text and
6
+ * has nothing to refuse, so a missing dependency is not a refusal but a different count an
7
+ * estimate by length, marked as an approximation in the metric label.
8
8
  *
9
- * Семейство про модели, кодировка про число: один и тот же файл считается
10
- * по-разному в `cl100k_base` и `o200k_base`, поэтому кодировка выбирается рядом с
11
- * семейством, а не подразумевается. Семейство тут одно, и это не недоделка: у
12
- * остальных нет словаря, который можно было бы назвать их собственным, считать
13
- * чужим словарём и называть это семейством значило бы обещать то, чего нет. */
9
+ * The family is about models, the encoding about the number: the same file counts differently
10
+ * under `cl100k_base` and `o200k_base`, which is why the encoding is chosen next to the family
11
+ * instead of being implied. There is one family here, and that is not an omission: the others
12
+ * have no dictionary that could be called their own, and counting with someone else's while
13
+ * calling it a family would promise what does not exist. */
14
14
 
15
15
  export const TOKEN_FAMILIES = {
16
16
  openai: { tool: 'gpt-tokenizer', encodings: ['o200k_base', 'cl100k_base'] }
@@ -18,24 +18,25 @@ export const TOKEN_FAMILIES = {
18
18
 
19
19
  export const TOKEN_DEFAULTS = { family: 'openai', encoding: 'o200k_base' };
20
20
 
21
- /* Оценка без словаря. Коэффициент снят на текстах этого репозитория (русские
22
- * документы и код): `README.md` 3,1 знака на токен, `WORKLOG.md` — около 3,0.
23
- * Для латиницы та же оценка завышает счёт (там примерно 4 знака на токен), поэтому
24
- * она и помечена приближением. */
21
+ /* The estimate without a dictionary. The coefficient was taken from this repository's own
22
+ * texts (Russian documents and code): `README.md` gave 3.1 characters per token, the archived
23
+ * journal `worklog/archive/WORKLOG.md` about 3.0. For Latin script the same estimate overstates
24
+ * the count (about 4 characters per token there), which is why it is marked as an
25
+ * approximation. */
25
26
  export const CHARS_PER_TOKEN = 3;
26
27
 
27
- /* Форматы, для которых счёт токенов смысла не имеет: картинка, шрифт или архив
28
- * это байты, и токенизатор разберёт их как что угодно, а число выйдет случайным.
29
- * Список нужен, чтобы метрика сказала это словами, а не выдала такой счёт за
30
- * посчитанный. SVG в него не входит намеренно: это текст, и его токены осмысленны. */
28
+ /* Formats for which counting tokens makes no sense: a picture, a font or an archive is bytes,
29
+ * and the tokenizer would split them into anything at all, giving a random number. The list
30
+ * exists so that the metric says this in words instead of passing such a count off as counted.
31
+ * SVG is deliberately not here: it is text, and its tokens are meaningful. */
31
32
  export const BINARY_EXTS = [
32
33
  '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.avif',
33
34
  '.woff', '.woff2', '.ttf', '.otf', '.eot',
34
35
  '.pdf', '.zip', '.gz', '.tar', '.mp4', '.mp3', '.mov'
35
36
  ];
36
37
 
37
- /* Словарь загружается один раз на кодировку: за ним стоят мегабайты таблиц, и
38
- * платить за них на каждом файле было бы нечем оправдать. */
38
+ /* A dictionary is loaded once per encoding: megabytes of tables stand behind it, and paying
39
+ * for them on every file would have no justification. */
39
40
  const probed = new Map();
40
41
 
41
42
  export function tokenizer(settings) {
@@ -51,26 +52,26 @@ function familyOf(settings) {
51
52
  return TOKEN_FAMILIES[asked] === undefined ? TOKEN_DEFAULTS.family : asked;
52
53
  }
53
54
 
54
- /* Счёт одного текста: словарём, если он есть, иначе оценкой. Оба ответа число
55
- * условных единиц текста, и различает их не значение, а подпись метрики
56
- * (`accuracy`), поэтому выдача одного за другое невозможно. */
55
+ /* Counting one text: with the dictionary if there is one, by estimate otherwise. Both answers
56
+ * are a number of text units, and what tells them apart is the metric label (`accuracy`) rather
57
+ * than the value, which is why one cannot be passed off as the other. */
57
58
  export function tokenCount(text, settings) {
58
59
  const { tool } = tokenizer(settings);
59
60
  if (tool === null) return estimate(text);
60
61
  return tool.encode(text).length;
61
62
  }
62
63
 
63
- /* Оценка по длине единственное, что можно сказать без словаря. Знаки считаются
64
- * кодовыми точками: для не-ASCII это ближе к числу токенов, чем единицы UTF-16. */
64
+ /* An estimate by length is all that can be said without a dictionary. Characters are counted
65
+ * as code points: for non-ASCII that is closer to the number of tokens than UTF-16 units. */
65
66
  export function estimate(text) {
66
67
  let chars = 0;
67
68
  for (const _ch of text) chars++;
68
69
  return Math.ceil(chars / CHARS_PER_TOKEN);
69
70
  }
70
71
 
71
- /* Бинарный ли файл: счёт токенов для него смысла не имеет. Список форматов ведёт
72
- * этот модуль, поэтому и подпись метрики, и пометка клетки спрашивают о файле
73
- * здесь, а не повторяют список у себя. */
72
+ /* Whether the file is binary, since counting tokens means nothing for it. The list of formats
73
+ * is owned here, so both the metric label and the cell mark ask about a file here instead of
74
+ * keeping a list of their own. */
74
75
  export function isBinary(file) {
75
76
  return BINARY_EXTS.indexOf(path.extname(file).toLowerCase()) >= 0;
76
77
  }
package/src/tool.js CHANGED
@@ -1,21 +1,20 @@
1
1
  import fs from 'fs';
2
2
 
3
- /* Метаданные самого пакета: имя и версия читаются из его же манифеста, чтобы не
4
- * держать вторую копию. Отдельный модуль потому, что эти данные описывают
5
- * упаковку, а не проект-потребитель, и нужны контракту данных. */
6
-
7
- /* Имя и версия пакета из его же манифеста, чтобы не держать вторую копию; без
8
- * файла (чужaя сборка) остаётся заглушка: версия нужна только в данных, и
9
- * отсутствие манифеста не повод не собирать таблицу. */
3
+ /* The package's own metadata: the name and the version are read from its manifest so that no second copy exists. A module
4
+ * of its own because these data describe the packaging rather than a consumer project, and the contract of the data needs
5
+ * them.
6
+ *
7
+ * With no manifest (someone else's build) the placeholder stays: the version is needed in the data alone, and a missing
8
+ * manifest is no reason not to build a report. */
10
9
  export let TOOL_PKG = { name: '@vernikr/size-report', version: '0.0.0' };
11
10
  try {
12
11
  TOOL_PKG = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
13
12
  } catch (_e) {}
14
13
 
15
- /* Как пакет ставится в проектта же git-ссылка на выпуск, которой учит `README.md`:
16
- * имени пакета в реестре здесь быть не может, оно занято чужим пакетом, и `add -D
17
- * <имя>` поставил бы его. Адрес и версия берутся из манифеста, поэтому совет об
18
- * установке не может разойтись с выпуском, а без адреса (`null`) звать нечего. */
14
+ /* How the package is installed into a project the git link to the release, the pinned form `README.md` teaches: the
15
+ * advice has to name the revision the documentation describes rather than whatever the registry serves as the latest one.
16
+ * The address and the version come from the manifest, so the advice cannot drift from the release, and with no address
17
+ * (`null`) there is nothing to name. */
19
18
  export function installSpec() {
20
19
  const repo = TOOL_PKG.repository === undefined ? ''
21
20
  : (typeof TOOL_PKG.repository === 'string' ? TOOL_PKG.repository : TOOL_PKG.repository.url || '');