@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/README.md +929 -1051
- package/bin/postinstall.js +17 -18
- package/bin/size.js +2 -2
- package/package.json +3 -4
- package/src/args.js +72 -72
- package/src/artifact.js +14 -14
- package/src/check.js +41 -42
- package/src/cli.js +26 -29
- package/src/config.js +87 -91
- package/src/css.js +14 -14
- package/src/data.js +33 -35
- package/src/derived.js +31 -35
- package/src/doctor.js +95 -99
- package/src/explain.js +46 -47
- package/src/git.js +66 -71
- package/src/history.js +81 -69
- package/src/hook.js +130 -149
- package/src/init.js +36 -37
- package/src/journal.js +17 -15
- package/src/locales.js +25 -16
- package/src/metrics.js +56 -55
- package/src/minify.js +28 -27
- package/src/modes.js +57 -60
- package/src/optional.js +13 -11
- package/src/page/app.css +78 -87
- package/src/page/app.js +31 -30
- package/src/page/build.js +40 -39
- package/src/page/dom.js +16 -3
- package/src/page/panel.js +85 -64
- package/src/page/state.js +72 -87
- package/src/page/table.js +21 -24
- package/src/parse-worker.js +10 -10
- package/src/parse.js +43 -45
- package/src/project.js +100 -104
- package/src/refusal.js +75 -76
- package/src/size-table.js +41 -76
- package/src/strip/forms.js +5 -5
- package/src/strip/guard.js +27 -28
- package/src/strip/js.js +27 -27
- package/src/strip.js +18 -21
- package/src/table.css +13 -14
- package/src/tokens.js +28 -27
- package/src/tool.js +10 -11
- package/templates/README.md +71 -77
- package/templates/ci.yml +33 -33
- package/templates/size-report.config.json +3 -3
- package/CHANGELOG.md +0 -639
package/README.md
CHANGED
|
@@ -1,806 +1,692 @@
|
|
|
1
1
|
# @vernikr/size-report
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`bin/size.js
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
—
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
клоны для хука, обрезанной истории и ветки мимо отчёта) и сверяет код выхода и
|
|
149
|
-
фразы; `test/refusals-catalog.test.js` читает исходники и требует, чтобы у каждого
|
|
150
|
-
места отказа был свой пункт (карты `SITES` и `PRINTED` держат числа мест), а у
|
|
151
|
-
каждого пункта — случай в каталоге, — то есть новый отказ не может появиться без
|
|
152
|
-
проверки. Отказы, которых прогоном не поймать,
|
|
153
|
-
названы явно: четыре стережёт своя проверка (в каталоге записаны её файл и фразы),
|
|
154
|
-
а один не поймать вовсе — «внутренняя ошибка» — и там же сказано, почему. Чего
|
|
155
|
-
каталог не берёт, сказано словами: формулировки вне перечисленных фраз, смысл, и
|
|
156
|
-
знак «!» — это примечание (приближение, смешанный коммит, выключенная автоматика),
|
|
157
|
-
а не отказ, и код выхода у него нулевой.
|
|
158
|
-
|
|
159
|
-
**И совет в отказе исполним — это тоже проверяется.** Правда о причине — половина
|
|
160
|
-
обещания: вторая — что предложенную команду можно выполнить. Поводом стал живой
|
|
161
|
-
случай (R-4.21): подсказка звала по имени из реестра, где пакета с таким именем нет,
|
|
162
|
-
и в проекте без установленного пакета запускала чужой код. Теперь у каждого случая
|
|
163
|
-
каталога сказано, что отказ советует, и совет выполняется в том состоянии, которое
|
|
164
|
-
его напечатало: сверяется код (включая «отказ ушёл» — тот же зов после совета должен
|
|
165
|
-
ответить другим), а совет-форма без значений проверяется по справке (те ли команды и
|
|
166
|
-
ключи). Совет, который выполнить нечем, назван с причиной: правка настроек, коммит,
|
|
167
|
-
установка зависимости — за человеком, и это сказано там же, в каталоге. Новый совет
|
|
168
|
-
в уже существующем отказе молча не пройдёт: совет вынимается из вывода по маркерам
|
|
169
|
-
(«починка:», «создайте его:», «соберите её:»), и у него обязано быть объявление.
|
|
170
|
-
|
|
171
|
-
**Волна 3 чистки пройдена** (`REFACTOR.md`): обвязка проверок одна на пакет
|
|
172
|
-
(`tools/harness.js`), прогоны на чтение не повторяются, клон фикстуры держится на
|
|
173
|
-
набор, а тяжёлый паритетный файл распался по предметам — и он, и команды CLI идут
|
|
174
|
-
волной по ядрам. Проверок было 39 и все 39 сохранены (имена сверены), а новые
|
|
175
|
-
добавлены только вместе с новым поведением страницы, подключения и разбора
|
|
176
|
-
модулей, а последние — о том, что записанное в манифестах сходится с файлами
|
|
177
|
-
эталонов, и о склейке кусков вывода процесса, и о дереве файлов, и о памяти
|
|
178
|
-
выбора и ссылке на странице, семь — о настоящем сжатии и семь — о токенах (§«Метрика
|
|
179
|
-
`min` умеет считать по-настоящему», §«Метрика `tok` считает токены настоящим
|
|
180
|
-
словарём», четыре — о сверке с деревом и выборе пути для состояния (§«История с
|
|
181
|
-
удалениями больше не тупик»), три — о точности по клетке (§«Точность числа»);
|
|
182
|
-
всего 80.
|
|
183
|
-
|
|
184
|
-
**Разбор модуля перестал стоить запуска Node на клетку** (`REFACTOR.md` R-5.4).
|
|
185
|
-
Гард понимает модуль через `vm.SourceTextModule`, а он живёт только под
|
|
186
|
-
`--experimental-vm-modules`: раньше отсюда и брался отдельный процесс на каждую
|
|
187
|
-
клетку. Теперь модуль разбирает один рабочий поток на прогон: на синтетической
|
|
188
|
-
истории с модулями (31 коммит, 3 файла, файл меняется каждым коммитом) `--write`
|
|
189
|
-
**5,59 → 0,47 с**, на той же истории со скриптами — те же 0,39 с (скриптовый
|
|
190
|
-
проект за поток не платит вовсе), а на проекте с одной изменённой клеткой —
|
|
191
|
-
0,44 → 0,40 с. Цена не исчезла, а стала разовой: старт потока ≈ 54 мс вместо
|
|
192
|
-
86 мс на каждую клетку, плюс разбор опирается на экспериментальный API (без него
|
|
193
|
-
гард отступает к прежнему `node --check` — медленнее, но не мягче).
|
|
194
|
-
Прогон подешевел втрое: `pnpm test` 15,7 → **5,6 с** (тогда в наборе было 39
|
|
195
|
-
проверок), `pnpm run parity:live` 23,8 → **8,3 с**. Набор стоит
|
|
196
|
-
**23,4–29,9 с** при 124 проверках — в зависимости от загрузки машины: окна с
|
|
197
|
-
загрузкой 18–70 несравнимы (в спокойном — 23,4–24,2 с, в занятых — 26,6–29,9 с;
|
|
198
|
-
в среде без настроек git — 27,4 с, с `CI=1` — 29,9 с), и это свойство окна, а не
|
|
199
|
-
набора: под той же загрузкой та же ревизия без нового сторожа идёт 24,4–26,3 с
|
|
200
|
-
при 122 проверках. Вклад сторожа выпуска измерен **парным прогоном** с
|
|
201
|
-
чередованием (124 → 122 проверки и обратно, два круга): **+2,0 и +2,2 с**, при этом
|
|
202
|
-
сам он стоит 1,4 с собственным прогоном (`node --test test/changelog.test.js`: два
|
|
203
|
-
запуска инструмента на фикстуре) и идёт параллельно прочим файлам.
|
|
204
|
-
Числа разных окон несравнимы вовсе: тот же набор при 119 проверках шёл 25,5–25,7 с,
|
|
205
|
-
хотя теперь проверок больше. Дороже всего в наборе — запуски инструмента: перебор
|
|
206
|
-
режимов и правило `--json` в `test/cli.test.js` стоят по нескольку десятых секунды
|
|
207
|
-
каждая, а разложение сторожа документации на четыре файла времени **не
|
|
208
|
-
прибавило** — 23,2 с и до него, и после: обе новые проверки измерены отдельным
|
|
209
|
-
прогоном, а не выведены из разброса. Из общего времени **+8,5 с** — десять проверок хука
|
|
210
|
-
(`test/hook.test.js`: сам он идёт 17,9–18,5 с и становится самым долгим файлом
|
|
211
|
-
набора, а та же ревизия без него — 15,8–16,6 с при 107 проверках). Интеграционные
|
|
212
|
-
прогоны (клон, коммиты, слияние, отказы) дешевле не сделать, не ослабив проверку.
|
|
213
|
-
`pnpm run parity:live` — 9,3 с в обеих средах. Замеры, машина и разброс —
|
|
214
|
-
`REFACTOR.md` §5.
|
|
215
|
-
|
|
216
|
-
**Прогонов два, и выбор между ними — по цене файла, а не по алфавиту**
|
|
217
|
-
(`REFACTOR.md` R-5.5). Цена проверки в этом наборе — не объём файла, а сколько раз
|
|
218
|
-
файл запускает инструмент и git: запуск — это процесс Node, а клон фикстуры и сборка
|
|
219
|
-
артефакта — сотни миллисекунд. Поэтому быстрый прогон собирает то, что доказывает по
|
|
220
|
-
прочитанному (исходники, дерево, справка, эталонные числа на общей фикстуре), а
|
|
221
|
-
полный добавляет то, что гоняет инструмент по многу раз на своих клонах, коммитит и
|
|
222
|
-
ставит хуки; причина для каждого дорогого файла названа построчно в
|
|
3
|
+
A tool that tracks how the volume of code and documents grows: every change shows how much the
|
|
4
|
+
project grew or shrank, in three measures — as written (`raw`), minified (`min`) and in tokens for a
|
|
5
|
+
language model (`tok`).
|
|
6
|
+
|
|
7
|
+
It answers two questions: for a person, "where is the project swelling"; for an AI agent, "how
|
|
8
|
+
much does my change weigh in its own context". It forbids nothing and blocks nothing: it only
|
|
9
|
+
shows.
|
|
10
|
+
|
|
11
|
+
## Status
|
|
12
|
+
|
|
13
|
+
**Release 2.5.0 (2026-09-17).** The tool lives as a package of its own: the registry name is
|
|
14
|
+
`@vernikr/size-report` (published by tag from CI, with no secret). A project may keep no settings at
|
|
15
|
+
all: without a config file the tool derives them from the project itself and says so in one line,
|
|
16
|
+
and `--init` pins what was derived into a file. The report is **one file**, the self-contained page
|
|
17
|
+
`docs/size-report.html`, and it appears by itself: the updating hook is installed after the package
|
|
18
|
+
is installed and on the first run. The version is in the manifest, and every release is recorded in
|
|
19
|
+
the journal — `worklog/` for today's entries, `worklog/archive/WORKLOG.md` for the earlier ones:
|
|
20
|
+
what changes in the numbers is measured rather than retold.
|
|
21
|
+
|
|
22
|
+
2.5.0 changes nothing in the numbers; what it changes is **the language the tool speaks**: the help,
|
|
23
|
+
the refusals, the diagnostics and the page are English, and a project with no config file now gets
|
|
24
|
+
`locale: en` rather than `ru`. Nothing is thrown away for it — the Russian dictionaries stay in the
|
|
25
|
+
package and are chosen by `locale` in the settings, and the frozen layer keeps its Russian values,
|
|
26
|
+
as `schema: 1` stays what it was. The note of 2.4.0 — what that release made visible and in which
|
|
27
|
+
order — stands in the journal, `worklog/02*`: what changes in the numbers is measured rather than
|
|
28
|
+
retold.
|
|
29
|
+
|
|
30
|
+
**Parity with the implementation the move started from is proven, not asserted.** The command is
|
|
31
|
+
`bin/size.js` and the package's entry point is `src/size-table.js` (a re-export only), with the
|
|
32
|
+
mechanics laid out in modules under `src/`. `pnpm test` compares the package with the frozen
|
|
33
|
+
standard byte by byte on the fixture and in four deliberately hostile environments (the machine's
|
|
34
|
+
git settings, the locale); `pnpm run parity:live` does the same on the consumer project's live
|
|
35
|
+
history in two environments — 95 rows × 27 columns, the artifact self-contained and passing its own
|
|
36
|
+
control mode. The output does not depend on the machine: the git settings that change what is
|
|
37
|
+
parsed are pinned inside the engine (`BLOCKERS.md` §B1). The comparison against the working tree
|
|
38
|
+
compares content rather than sizes, so a tree with CRLF newlines (`.gitattributes`; `core.autocrlf`,
|
|
39
|
+
the default of Git's installer for Windows) is no obstacle (`BLOCKERS.md` §B2).
|
|
40
|
+
|
|
41
|
+
**A history with deletions is no longer a dead end** (`BLOCKERS.md` §B3): a column whose file lived
|
|
42
|
+
in history and was deleted before HEAD used to fail the whole run with code 1 and the text "no file
|
|
43
|
+
instead of no file" — that is, a project with deleted files got no report at all. Now only a
|
|
44
|
+
**disagreement** between the two sides of the comparison is a refusal: a lost creation, a lost edit
|
|
45
|
+
and a lost deletion still fail the run, but with the real cause and a ready command, while a file
|
|
46
|
+
deleted before HEAD is simply empty in the table. Proven by numbers rather than by a word: the
|
|
47
|
+
column's size at every commit is checked against the blob size from git — a returned file gives the
|
|
48
|
+
same number as its first appearance — and a witness, the file that appears only in a merge, fails
|
|
49
|
+
the run naming both sides. The boundary of the same parse is closed as well (`BLOCKERS.md` §N8):
|
|
50
|
+
the path for the state was chosen by the order of the settings rather than by what the commit holds,
|
|
51
|
+
so with `diff.renames=false` — when git returns the old name of a renamed file and the new one in a
|
|
52
|
+
single commit — the engine took the vanished alias and the comparison refused on a legitimate case;
|
|
53
|
+
now it takes the alias git returned a blob for. The old numbers could not move: both logics agree
|
|
54
|
+
wherever the first alias in the commit exists, that is, in every run that ended with a report
|
|
55
|
+
before.
|
|
56
|
+
|
|
57
|
+
**The consumer project is connected** (2026-09-14). `safe-resets` installs the package from git by
|
|
58
|
+
the release tag and keeps no copy of the tool of its own — neither `tools/size-table.js` nor a test
|
|
59
|
+
for it: the table is built and checked by the `size` command (`pnpm run test:sizes`), and its part
|
|
60
|
+
in that project is one line of its runner (`worklog/archive/WORKLOG.md` §18). The connecting
|
|
61
|
+
instruction turned out to be right and incomplete in two places — the step giving CI access to the
|
|
62
|
+
package and the order of moving off an already installed copy — and both are written into the
|
|
63
|
+
instruction below. The step with a key left it later, along with private access.
|
|
64
|
+
|
|
65
|
+
**The data contract and the page.** The engine hands over absolute values and the shape of the table
|
|
66
|
+
(`--data`), while deltas, totals, "now" and the filters are computed by the page — which is the
|
|
67
|
+
report itself (`size-report.html`): without that split the filters and "the total over the
|
|
68
|
+
selection" are impossible in principle. The contract carries the accuracy of a number as well, a
|
|
69
|
+
row of `approx` marks per cell, because that is a fact of the measurement rather than a conclusion:
|
|
70
|
+
the page shows what the engine said and keeps no rule of accuracy of its own. The page's panel is a
|
|
71
|
+
tree of files by folder, with a switch per folder for the whole subtree; a reader's choice survives
|
|
72
|
+
a revisit and travels in a link — the page's address is the link. The contract carries the **project
|
|
73
|
+
catalogue** too: every path git sees, so the page's tree is the project's tree, while numbers exist
|
|
74
|
+
only for the files that became columns (release 2.2.0).
|
|
75
|
+
|
|
76
|
+
The tool grew out of one script in the consumer project [`safe-resets`](../figma/safe-resets) — the
|
|
77
|
+
metrics `raw` and "a simplification instead of minification", a static report in git; that path
|
|
78
|
+
does not exist in this repository, and it is not named anywhere without the project.
|
|
79
|
+
**A project whose code is JavaScript modules in `.js` reports too**: the stripper's guard understands
|
|
80
|
+
both forms, a script and a module, so connecting needs no setting edited by hand, and when the graph
|
|
81
|
+
really is not JavaScript the refusal names the cause and the command. Hints, the help text, the
|
|
82
|
+
default fix command and the templates name the **path inside the project**
|
|
83
|
+
(`node node_modules/@vernikr/size-report/bin/size.js`) rather than the package name: `npx <name>`
|
|
84
|
+
runs an installed package only while it is there, and in a project without it the name goes to the
|
|
85
|
+
registry and pulls a package over the network.
|
|
86
|
+
|
|
87
|
+
**What the documentation promises is checked, not assumed**, and the promises are split one per file:
|
|
88
|
+
existence and completeness of paths (`test/docs-paths.test.js`), commands, refusal causes and section
|
|
89
|
+
links (`test/docs-commands.test.js`), the count of checks (`test/docs-numbers.test.js`) and the install
|
|
90
|
+
example leading to a revision whose help knows the named commands (`test/docs-pin.test.js`). One reader
|
|
91
|
+
of facts serves them all (`tools/docs-facts.js`). What a
|
|
92
|
+
machine cannot check — wording, promises about the future, whether a file's role is described
|
|
93
|
+
correctly — the guards do not take on, and they say so in their headers.
|
|
94
|
+
|
|
95
|
+
**git is read through one boundary, in the checks too**: the list of pins (`core.quotePath`,
|
|
96
|
+
colouring, the signature block, the encoding) is one for the engine and for the harness, so the
|
|
97
|
+
checks and the tools reach git through a common place — and an unpinned place is guarded by
|
|
98
|
+
`test/git-pins.test.js`, which also shows by witness that a pin works: the same read without it
|
|
99
|
+
returns a non-English path quoted. It is the same defect as B1, only found in the harness: without
|
|
100
|
+
the pins a check is green on a machine with our settings and red on a machine with the default ones.
|
|
101
|
+
The check that measures the environment itself keeps its unpinned read deliberately — it is named,
|
|
102
|
+
and it stands in a list of its own.
|
|
103
|
+
|
|
104
|
+
**Every refusal of the tool tells the truth, and that is guarded rather than assumed.** A false cause
|
|
105
|
+
in a refusal text was found by a live run, four times in a row, each time by accident — so the class
|
|
106
|
+
is closed not by a fifth fix: `tools/refusals.js` holds a line per refusal saying what it must
|
|
107
|
+
convey, which code to answer with and which phrases must stay in the output, and two checks split
|
|
108
|
+
that promise. `test/refusals.test.js` **calls** each refusal and compares the exit code and the
|
|
109
|
+
phrases; `test/refusals-catalog.test.js` reads the sources and requires a catalogue entry for every
|
|
110
|
+
refusal site — the maps `SITES` and `PRINTED` hold the counts — and a case in the catalogue for every
|
|
111
|
+
entry, so a new refusal cannot appear without a check. Refusals a run cannot reach are named
|
|
112
|
+
explicitly: four are guarded by a check of their own (the catalogue names the file and the phrases),
|
|
113
|
+
and one cannot be caught at all — "internal error" — which is said where it stands. What the
|
|
114
|
+
catalogue does not take on is said in words: wording beyond the listed phrases, and meaning, and the
|
|
115
|
+
"!" sign, which is a note (an approximation, a mixed commit, the automation switched off) rather
|
|
116
|
+
than a refusal, with exit code zero.
|
|
117
|
+
|
|
118
|
+
**And the advice in a refusal is executable — that is checked as well.** The truth about the cause is
|
|
119
|
+
half the promise; the other half is that the suggested command can be run. A live case started this:
|
|
120
|
+
a hint called out to a registry name no package carried, and in a project without that package
|
|
121
|
+
installed it ran someone else's code. Now every catalogue case says what the refusal advises, and the
|
|
122
|
+
advice is executed in the state that printed it: the exit code is compared (including "the refusal is
|
|
123
|
+
gone" — the same call after the advice must answer differently), while an advice that is a form
|
|
124
|
+
without values is checked against the help output (the same commands and flags). Advice a person has
|
|
125
|
+
to carry out is named with its reason — editing settings, committing, installing a dependency — and
|
|
126
|
+
that is said there too, in the catalogue. A new advice inside an existing refusal cannot pass
|
|
127
|
+
silently: the advice is taken out of the output by its markers, and it has to have a catalogue entry.
|
|
128
|
+
|
|
129
|
+
**A module no longer costs a Node process per cell**: the guard parses a module through
|
|
130
|
+
`vm.SourceTextModule`, which exists only under `--experimental-vm-modules`, and that is where the
|
|
131
|
+
per-cell process came from. Today one worker thread parses the modules for a whole run, and the price
|
|
132
|
+
has not disappeared but become one-time; the parse also rests on an experimental API (without it the
|
|
133
|
+
guard falls back to `node --check`: slower, no softer). What a run costs today the run prints itself
|
|
134
|
+
(`pnpm run suites:measure`).
|
|
135
|
+
|
|
136
|
+
**The checks' shared part lives in one place** (`tools/harness.js`): one clone of the fixture per
|
|
137
|
+
environment rather than one per check, a read-only run of the tool is not repeated, and a check that
|
|
138
|
+
edits files takes a clone of its own. The files go in a pool over the cores (`tools/run-tests.js`),
|
|
139
|
+
and the numbers add up: the run counts the checks of every file against the `test(` declarations in
|
|
140
|
+
it, so a file that did not run is a failure rather than fewer checks.
|
|
141
|
+
|
|
142
|
+
**There are two runs, and the choice between them follows the price of a file, not the alphabet.**
|
|
143
|
+
The cost of a check here is not the size of the file but how many times it launches the tool and git:
|
|
144
|
+
a launch is a Node process, while cloning the fixture or building the artifact takes hundreds of
|
|
145
|
+
milliseconds. So the fast run gathers what it proves from reading (sources, tree, help, reference
|
|
146
|
+
numbers on a shared fixture), and the full one adds what runs the tool many times on its own clones,
|
|
147
|
+
commits and installs hooks; the reason for each expensive file is named line by line in
|
|
223
148
|
`tools/suites.js`.
|
|
224
149
|
|
|
225
|
-
|
|
|
150
|
+
| Run | Command | Checks |
|
|
226
151
|
|---|---|---|
|
|
227
|
-
|
|
|
228
|
-
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
`
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
отвечает, всё ли в истории попало в отчёт: каждый путь, тронутый коммитами,
|
|
493
|
-
обязан быть колонкой или объявленным исключением, а непонятый путь — это код 1,
|
|
494
|
-
путь, коммит, который его завёл, и готовая починка. Тем же ответом идут сводка по
|
|
495
|
-
выпавшим коммитам (сколько и почему) и списки их sha — то есть «какая часть
|
|
496
|
-
истории покрыта». `size explain <коммит>` отвечает про один коммит — назвать его
|
|
497
|
-
можно и именем ревизии (`HEAD`, ветка, тег, `HEAD~1`), и sha, и началом sha:
|
|
498
|
-
строка есть (и которая) либо причина, почему её нет, — тронут только отчёт, числа не сдвинулись
|
|
499
|
-
при тронутых файлах колонок, коммит мимо колонок, слияние скрыто `rows.merges`.
|
|
500
|
-
Обе берут причину у того же прохода, что и отчёты, а улики — из списка изменённых
|
|
501
|
-
путей коммита: чего в истории нет, о том молчание вместо догадки. Полнота — из требований (§4.2: «ни одно изменение не
|
|
502
|
-
просочилось мимо отчёта»), и она же заменяет контроль
|
|
503
|
-
«артефакт ↔ история»: отчёт можно не хранить в git. Смысл `skip` в настройках от
|
|
504
|
-
этого не изменился, но **значение расширилось**: это не только «пути, которые
|
|
505
|
-
колонками быть не могут», но и объявленные исключения полноты — тот же список, и
|
|
506
|
-
чеканить второй инструмент не стал. Цена названа: `check` — это проход по истории,
|
|
507
|
-
как и любой отчёт (**1,5 с** на живой истории), а набор проверок подорожал на
|
|
508
|
-
тринадцать запусков инструмента (бюджет — ниже). К ним добавился `size doctor` —
|
|
509
|
-
диагностика одним ответом (ниже, в разделе про проверки).
|
|
510
|
-
|
|
511
|
-
**Отчёт обновляется сам** (последний пункт шага 5 плана). `size install-hook`
|
|
512
|
-
ставит два хука — `post-commit` и `post-merge` (`post-commit` при `git merge` не
|
|
513
|
-
выполняется вовсе, поэтому одного файла мало), — и после каждого коммита и слияния
|
|
514
|
-
отчёт пересобирается, а лежащий в git — ложится **отдельным коммитом**: ручного шага
|
|
515
|
-
«код, потом таблица» больше нет. Коммит отчёта собирается плумбингом git
|
|
516
|
-
(`commit-tree`): в него физически не могут попасть ни индекс, ни чужая
|
|
517
|
-
незакоммиченная работа, и зацикливание невозможно по устройству, а не по флагу в
|
|
518
|
-
окружении. Отказ инструмента коммит не роняет — причина печатается строкой и
|
|
519
|
-
видна в `size doctor`.
|
|
520
|
-
|
|
521
|
-
Перенос, доработка и оформление в пакет расписаны в `PLAN.md` по шагам, с
|
|
522
|
-
приёмкой каждого.
|
|
523
|
-
|
|
524
|
-
## Что в репозитории
|
|
525
|
-
|
|
526
|
-
| Файл | Роль |
|
|
152
|
+
| Fast — every edit | `pnpm test` | **70 of 175** |
|
|
153
|
+
| Full — release and CI | `pnpm test:all` | **175** |
|
|
154
|
+
|
|
155
|
+
No check is lost or weakened: the full run starts all 177 with the same files, the fast one takes part
|
|
156
|
+
of them. The default is the full run — a file becomes fast only explicitly and with a reason — so new
|
|
157
|
+
expensive work cannot quietly move into the fast one. Two declarations guard that:
|
|
158
|
+
`test/suites.test.js` (every file classified, and a reason for each) and the documentation guard
|
|
159
|
+
`test/docs-numbers.test.js` (the numbers in the table above).
|
|
160
|
+
|
|
161
|
+
**The runs have no time targets, and that is a decision rather than an omission.** Seconds depend on
|
|
162
|
+
the window — the machine is under very different load at different times — so neither the suite nor CI
|
|
163
|
+
fails over time, and this document promises no seconds: `pnpm run suites:measure` prints every file's
|
|
164
|
+
duration in a run of its own (and a run prints it next to its tick), but that is a measurement, not a
|
|
165
|
+
threshold. The split rests on what a file is about rather than on how long it takes. CI calls the full
|
|
166
|
+
run twice: in the usual environment and with none of the machine's settings
|
|
167
|
+
(`GIT_CONFIG_GLOBAL=/dev/null`).
|
|
168
|
+
|
|
169
|
+
**What the package promises is down to fact.** The shipped-file list named four paths the repository
|
|
170
|
+
does not have (`dist/`, `templates/`, `LICENSE`): today it promises only what exists —
|
|
171
|
+
`templates/` came back into the list together with its files, not before them —
|
|
172
|
+
while `pnpm run pack:check` checks it from both sides, that the list names nothing absent and that the
|
|
173
|
+
tarball carries nothing the list does not promise. Taking both references works again (`pnpm run
|
|
174
|
+
parity`, `pnpm run fixture`) and no longer depends either on whether the consumer project keeps a copy
|
|
175
|
+
of the tool or on the machine's git settings. Two texts that promised the same were fixed as well: the
|
|
176
|
+
`--init` hint (it said the checks travel with the package, while the suite is not part of it) and the
|
|
177
|
+
default `fixCommand` (it named a package that does not exist, `npx size-table --write`).
|
|
178
|
+
|
|
179
|
+
**The checks run themselves** (`.github/workflows/ci.yml`). On every push and every pull request one
|
|
180
|
+
job `verify` calls **one command** — `pnpm run verify`; the list of steps lives in one place
|
|
181
|
+
(`tools/gates/run.js`) and matches the local one, so a check that is not in a profile cannot be in CI
|
|
182
|
+
(`test/gates-verify.test.js` watches that). The profile, in order: the strict linter, the bloat
|
|
183
|
+
sensors, the whole suite, parity with the history of the consumer project, reproducibility of both
|
|
184
|
+
references and the work from the assembled tarball. Two steps are dearer and live in the slow profile
|
|
185
|
+
instead — the same suite in an environment with none of the machine's git settings
|
|
186
|
+
(`GIT_CONFIG_GLOBAL=/dev/null`) and coverage under c8: `pnpm run verify:slow`,
|
|
187
|
+
`.github/workflows/verify-slow.yml` on a schedule. The job needs no secrets: the consumer's history
|
|
188
|
+
lies in the repository as a bundle at the revision recorded in the reference (`fixtures/live/`), and a
|
|
189
|
+
re-take goes into a temporary directory and is compared with what is committed, so the working tree
|
|
190
|
+
stays clean. The job pins Node 22 and the actions by commit SHA, and there is deliberately no matrix
|
|
191
|
+
over Node versions: this pass is about control.
|
|
192
|
+
|
|
193
|
+
**A release is a tag** (`.github/workflows/release.yml`). Pushing `v<version>` runs the strict linter
|
|
194
|
+
and the whole suite, checks the work from the assembled package, compares the manifest version with the
|
|
195
|
+
tag and sends the package to the registry — no secret and no code from an authenticator: publishing
|
|
196
|
+
goes by the attestation GitHub Actions issues for that job (trusted publishing), which npm accepts
|
|
197
|
+
instead of a token. A prerelease goes to `next` rather than `latest`, so a draft is not what a default
|
|
198
|
+
install picks up. The publisher is set up once and lives on npmjs.com, not in the repository:
|
|
199
|
+
`npm trust github @vernikr/size-report --file release.yml --repo vernikr/size-report
|
|
200
|
+
--allow-publish` (the same is the Trusted Publisher button in the package's settings), and
|
|
201
|
+
`npm trust list @vernikr/size-report` shows whether the link is there. The job raises no version: a
|
|
202
|
+
person names it in the manifest, and it is compared with the tag rather than
|
|
203
|
+
derived from it.
|
|
204
|
+
|
|
205
|
+
One trap cost an edit of its own, and it is about `setup-node` rather than this package: with
|
|
206
|
+
`registry-url` the action writes `_authToken=${NODE_AUTH_TOKEN}` into `.npmrc`, npm then considers
|
|
207
|
+
credentials given and does **not** go for the OIDC attestation — publishing fails 404 with a correctly
|
|
208
|
+
set-up publisher. So `registry-url` is not given here: npmjs.org is the default registry anyway, and
|
|
209
|
+
`publishConfig` in the manifest carries `access: public` only. A draft run from Actions ("Run
|
|
210
|
+
workflow": nothing is published by default) goes the whole list up to the publishing step itself — the
|
|
211
|
+
strict linter and the whole suite, the work from the tarball, and a package built on a draft version
|
|
212
|
+
above the manifest's own, so that the registry does not refuse an already released number. Whether the
|
|
213
|
+
publisher is set up a draft run does not show: `--dry-run` exchanges no attestation and passes without
|
|
214
|
+
any credentials at all — only a real tag tells the truth about that.
|
|
215
|
+
|
|
216
|
+
**Two rules came out of the first live runs of CI, and both are about the border of a call.** Process
|
|
217
|
+
output is collected by the harness rather than glued into a string: a multi-byte character torn at a
|
|
218
|
+
chunk border would turn into two replacement characters, and where those chunks fall is the kernel's
|
|
219
|
+
business — a local run does not show it (`test/runner.test.js`). And of the references only what this
|
|
220
|
+
repository writes by itself is compared byte for byte: a history bundle is packed by git, whose bytes
|
|
221
|
+
depend on its version, so the bundle is compared by content — the branches, the tip and the number of
|
|
222
|
+
commits, that is, what makes it a replacement for the consumer project. The bundle also has to carry
|
|
223
|
+
`HEAD` and the branch `main` at the reference revision, or a clone decides on its own which branch to
|
|
224
|
+
lay out (`tools/check-standards.js`).
|
|
225
|
+
|
|
226
|
+
**The report page looks and behaves like a tool.** One set of table styles serves both outputs
|
|
227
|
+
(`src/table.css`), so the page took over the sticky header and commit column the static artifact
|
|
228
|
+
already had, and its commit column narrows in a narrow window instead of pushing the table sideways.
|
|
229
|
+
The page says so in words when there is nothing to assemble a table from — every metric or every file
|
|
230
|
+
switched off — and its switches are labels around inputs, so a mouse, `Space` and assistive technology
|
|
231
|
+
all reach them. The colour of a delta is defined once: growth green, fall red — changing it is two
|
|
232
|
+
lines in `src/table.css` plus re-taking the artifact's reference, and no other place holds a colour.
|
|
233
|
+
The one place where the page departs from the shared geometry is its "adaptations" section, and every
|
|
234
|
+
departure stands there with its reason: the shared part is frozen by the artifact's bytes (`src/css.js`).
|
|
235
|
+
|
|
236
|
+
**The left panel is the project's file tree.** It is built from the catalogue — every path git sees —
|
|
237
|
+
rather than from the columns, so it also shows what did not make it into the report: such a leaf, and a
|
|
238
|
+
folder with nothing to measure in it, keeps its place with the checkbox off and unavailable, and the
|
|
239
|
+
tooltip names the reason — the package's rule or the project's choice. Off rather than absent: the
|
|
240
|
+
rows stay even (the eye compares like with like) while unavailability says this is not the reader's
|
|
241
|
+
choice. The report itself is always in the catalogue, whether or not it is tracked: that is a property
|
|
242
|
+
of the moment, and the page must not depend on it, or the first rebuild in a fresh clone would give
|
|
243
|
+
different bytes. A folder whose files are only partly in the report writes its count as a fraction
|
|
244
|
+
("2/5"), and everything outside the report stands after everything inside it — folders and leaves
|
|
245
|
+
alike — so that the report is seen at once in a list where half the rows do not switch
|
|
246
|
+
(`test/page-tree.test.js`).
|
|
247
|
+
|
|
248
|
+
**A folder is a switch like a file, and its sign is a decision of its own.** The checkbox of a folder
|
|
249
|
+
carries its whole subtree and shows three states — every file on, some, none — with the number of files
|
|
250
|
+
next to it. Neither a folder nor a category button keeps state of its own: both flip the same file
|
|
251
|
+
checkboxes, so the tree, the buttons and the table cannot drift apart. The sign beside a folder answers
|
|
252
|
+
a different question — how much of the tree is visible, which is the onlooker's business rather than the
|
|
253
|
+
reader's choice — so it is remembered between visits in a record of its own, under a key of its own and
|
|
254
|
+
the same report passport, and it never goes into the link; unfolding every folder removes that record,
|
|
255
|
+
just as turning the checkboxes back on removes the choice.
|
|
256
|
+
|
|
257
|
+
**The list scrolls, and there is one scroll.** On a narrow window it is the file list that scrolls —
|
|
258
|
+
the panel grows with the page there — while on a wide one the whole panel does: otherwise the controls
|
|
259
|
+
would push the table off the screen. Folders fold because the tree is longer than the window; otherwise
|
|
260
|
+
its middle is out of reach.
|
|
261
|
+
|
|
262
|
+
**Folding is pure view, and it counts no numbers.** The subtree lies in the markup and a class on the
|
|
263
|
+
row hides it, so a click on the sign changes exactly the three things the reader sees — the class, the
|
|
264
|
+
sign and the note in the memory. A rebuild here would be honest work for nothing: it counts the whole
|
|
265
|
+
table, every row by every column, and so pays for numbers folding does not change. What guards this is
|
|
266
|
+
that after folding the table is the same markup rather than a rebuilt one (`test/page-tree.test.js`).
|
|
267
|
+
|
|
268
|
+
**The columns the last commit touched come first.** The report is rebuilt after every commit, and a
|
|
269
|
+
reader's first question is what that edit brought. The mark comes from the history rather than from the
|
|
270
|
+
numbers — an edit that changed no size is an edit too — and it is taken from the last commit that
|
|
271
|
+
touched at least one column, counting back from the top: a commit that went past the columns, above all
|
|
272
|
+
the report itself, which the hook commits, is skipped, or the mark would depend on the report's own
|
|
273
|
+
commit, the same run would give different bytes and the hook would commit the report a second time.
|
|
274
|
+
Inside each part the order stays as it comes from the settings (the sort is stable): the order of the
|
|
275
|
+
columns is what the reader is used to, and his choice of files does not rearrange it
|
|
276
|
+
(`test/page-view.test.js`, the contract's `last` field).
|
|
277
|
+
|
|
278
|
+
**On a wide window the panel stands to the left of the table and takes no room from the numbers** (from
|
|
279
|
+
900px, `src/page/app.css`). That is not decoration: a desktop has much side room and little vertical
|
|
280
|
+
room, so the switches, the tree and the numbers are visible at once, and neither scrolling the numbers
|
|
281
|
+
nor scrolling the tree takes the controls off the top of the screen. The layout is a grid on `body`
|
|
282
|
+
rather than a wrapper in the markup — the page is assembled by pasting chapters, and the page's shape
|
|
283
|
+
should live in one place. The grid has five rows, named by subject (the heading, the message about a
|
|
284
|
+
link, the working row, the empty state, the note), and only the working row stretches: the table takes
|
|
285
|
+
all the remaining height and the panel no more than that, scrolling inside itself rather than pushing
|
|
286
|
+
the table off the screen. The narrow half starts at 899px rather than at 900px, so that at exactly
|
|
287
|
+
900px the two halves cannot apply to one page — they once did, and the table's height ceiling survived
|
|
288
|
+
from the narrow one, leaving empty space under the table at that single window size. What guards the
|
|
289
|
+
numbers behind the layout is the contract rather than the markup: switching a folder off removes exactly
|
|
290
|
+
its columns and exactly its volume from the total (`test/contract.test.js`).
|
|
291
|
+
|
|
292
|
+
**A checkbox takes away neither the numbers' room nor the reader's place in the list.** The panel is
|
|
293
|
+
drawn anew after every switch, so its scroll and the list's are part of the view like the checkboxes:
|
|
294
|
+
both are saved before the rebuild and set back after, and the field under the keyboard comes back with
|
|
295
|
+
its focus (without scrolling — `preventScroll`), or switching with `Tab` and `Space` would mean walking
|
|
296
|
+
the panel from the start again. The file list has no ceiling of its own in a wide window: the panel
|
|
297
|
+
scrolls, and the list does not push the table. The row of categories sticks to the top of the panel,
|
|
298
|
+
with the panel's own background (or passing rows of the list would read through it), and the panel's own
|
|
299
|
+
top padding lives on its first field, which travels away with it. File captions use the table's font size
|
|
300
|
+
(12.5px), and the legend under the tree is gone on purpose: below the list it pushed the numbers away,
|
|
301
|
+
while what it explained already stands next to the thing it explains — the sign of a number names the
|
|
302
|
+
colour of a delta, accuracy stands under the metric switches, and the mark of a gap lives in the cell's
|
|
303
|
+
tooltip.
|
|
304
|
+
|
|
305
|
+
**The panel remembers the reader's choice.** The record lives in the browser's memory, tied to the
|
|
306
|
+
report's passport — the tool's name, the data schema, the artifact's path, the title and the column
|
|
307
|
+
labels, hashed into the record's key — so reports in one browser do not see each other's choice (all
|
|
308
|
+
`file://` pages share one memory, so this is no trifle). Inside the record the choice is held by names —
|
|
309
|
+
a file by its path, a metric by its key — and only what is switched off: a column pointed at another path
|
|
310
|
+
or a metric dropped from the settings simply matches nothing, what appeared stays switched on, and
|
|
311
|
+
turning everything back on returns the page to its default and removes the record. The first reader —
|
|
312
|
+
and a reader whose record is broken or outdated — gets exactly the default, and the choice affects
|
|
313
|
+
neither the numbers nor the markup. The passport holds neither the tool's version nor the top of the
|
|
314
|
+
history, and on purpose: updating the tool does not change what a column means, while a grown history is
|
|
315
|
+
the very history the reader comes back to.
|
|
316
|
+
|
|
317
|
+
**The same choice travels as a link.** The page's address is the link: the record that goes into the
|
|
318
|
+
browser's memory goes into the anchor too (`#size-report=…`), so the sender copies the address and the
|
|
319
|
+
recipient sees that choice with no action at all. The link outranks the memory — it is the sender's
|
|
320
|
+
explicit choice — while it does not replace the reader's own until he changes something. A foreign or
|
|
321
|
+
broken address is not applied, and is not silent either: a line above the table names the reason ("the
|
|
322
|
+
link was made in another report" / "the choice in the address is unreadable"), the view stays the
|
|
323
|
+
reader's own, and the incoming address is not rewritten; names the report does not hold are reported by
|
|
324
|
+
count, skipped, and the rest is applied. The link works on an already open page as well: the browser does
|
|
325
|
+
not reload the document when the anchor changes, so the page reads the address itself, or a link would
|
|
326
|
+
only work in a new tab. The page makes no request to the network at all, and that is an assertion of a
|
|
327
|
+
check rather than a promise (`test/page-view.test.js`, `test/parity.test.js`).
|
|
328
|
+
|
|
329
|
+
**The `min` metric can count for real.** The way of counting is chosen in the settings:
|
|
330
|
+
`"minify": {"engine": "esbuild"}` minifies JS/TS/CSS for real through an optional dependency, while
|
|
331
|
+
`"engine": "strip"` is the earlier removal of comments and indentation. The default did not change,
|
|
332
|
+
because both frozen references were taken under it. Measured on the fixture: real minification is
|
|
333
|
+
smaller than stripping in **44 cells and never larger**; `src/code.js` **276 → 185 B**, `src/style.css`
|
|
334
|
+
**55 → 43 B**, and over the fixture's history **−1 372 B**. JSON is minified by parsing and so stays
|
|
335
|
+
exact, while the formats the minifier does not take are named in the metric's caption together with the
|
|
336
|
+
ones it does take. Accuracy is declared twice, and that is not two answers to one question: the caption
|
|
337
|
+
speaks of **the worst in the column** — one format without minification makes the metric approximate as
|
|
338
|
+
a whole rather than hiding behind an exact neighbour — while each cell speaks of its own number, and an
|
|
339
|
+
approximate one is marked with a dashed line and the method in its tooltip. The worst is taken from the
|
|
340
|
+
cells rather than from the engine's name: a report of one JSON is exact even under stripping — parsing
|
|
341
|
+
loses only insignificant whitespace, and nobody would make it shorter — and the caption says so. Both
|
|
342
|
+
answers come from one rule (`pointExact` in `src/metrics.js`), so they cannot diverge. With no minifier
|
|
343
|
+
(an installation without optional dependencies, a platform without it) the metric falls back to
|
|
344
|
+
stripping, the method says so in words and the run answers **code 4** rather than staying silent, while
|
|
345
|
+
the numbers are the same as the earlier way of counting — byte for byte with the reference. The derived
|
|
346
|
+
profile leads new projects straight to minification (`--init` pins the same), its hint names that price,
|
|
347
|
+
and the report itself stays out of the columns there: a column that is the table is refused by the
|
|
348
|
+
settings check. A file the minifier could not parse (markup in `.js`, syntax it does not know) is a
|
|
349
|
+
refusal with code 2 whose text names the file, the minifier and its own cause, and whose advice gives a
|
|
350
|
+
ready way out — assign simplification to that extension.
|
|
351
|
+
|
|
352
|
+
**The `tok` metric counts tokens with a real dictionary.** Tokens are the report's third measure: what a
|
|
353
|
+
file weighs for a language model. The dictionary is chosen in the settings
|
|
354
|
+
(`"tokens": {"family": "openai", "encoding": "o200k_base"}`), and the encoding is part of the number
|
|
355
|
+
rather than a detail: on the fixture `src/code.js` is **168 tokens** under `o200k_base` and **196** under
|
|
356
|
+
`cl100k_base`, which is why the encoding is named next to the family and the metric's method quotes
|
|
357
|
+
exactly the one that produced the number. Tokens are neither bytes nor minification, and the difference
|
|
358
|
+
is shown rather than smoothed over: the same cell is **735 B** `raw`, **276 B** stripped, **185 B** really
|
|
359
|
+
minified and **168** tokens, while bytes per token differ between files by **2.5 times** (from 2.56 in
|
|
360
|
+
`package.json` to 6.30 in `crlf.txt`) — that is, the text is counted rather than a ratio. The family is
|
|
361
|
+
single in this version, `openai`: the others have no dictionary that could be called their own, and
|
|
362
|
+
counting with someone else's while calling that a family would promise what does not exist. There is no
|
|
363
|
+
dictionary switch on the page, and on purpose: the page gets ready numbers and counts nothing itself,
|
|
364
|
+
and it has nothing to count tokens with. Counting every family on every run would pay time for numbers
|
|
365
|
+
the reader may never ask about, so the choice of family and encoding lives where it costs time — in the
|
|
366
|
+
run's settings — while the page **names** it: the method of each metric stands under the switches as
|
|
367
|
+
text rather than only in a tooltip. Formats without text (a picture, a font, an archive) are named in
|
|
368
|
+
the metric's caption together with the reason: their number goes by bytes, the cell of such a file is
|
|
369
|
+
marked by the same rule, and the caption takes the worst in the column — two answers have nothing to
|
|
370
|
+
diverge with. With no dictionary (an installation without optional dependencies, a platform without it)
|
|
371
|
+
the count is an estimate by length with the coefficient named in the method, and the run answers **code
|
|
372
|
+
4**; the other metrics stay what they were in a report without tokens, and that seam is checked in an
|
|
373
|
+
environment with no optional dependencies at all (`SIZE_REPORT_NO_OPTIONAL`). Counting tokens costs a
|
|
374
|
+
run time, and that is the honest price of the dictionary rather than of parsing: its tables are read
|
|
375
|
+
once per process while the counting is per file and per row, so the price grows with the history and not
|
|
376
|
+
with the dictionary. The derived profile leads new projects straight to tokens.
|
|
377
|
+
|
|
378
|
+
**Two conveniences of the command line are guarantees rather than accidents:** `--help` answers
|
|
379
|
+
wherever it is asked, and `--write` creates the report's directory when it is missing.
|
|
380
|
+
|
|
381
|
+
**Two commands answer about the history: completeness and explanation.** `size check` answers whether
|
|
382
|
+
everything in the history got into the report: every path the history touched has to be a column or a
|
|
383
|
+
declared exception, and a path that is neither is a violation — code 1, the path, the commit that
|
|
384
|
+
introduced it and a ready fix. The same answer carries the summary of dropped commits — how many and
|
|
385
|
+
why — and their shas, that is, how much of the history is covered. Coverage is counted over the facts of
|
|
386
|
+
the history — the union of the changed paths of every commit — rather than over the file list in the
|
|
387
|
+
tree: a file created and deleted before HEAD is invisible there while the history remembers it, and its
|
|
388
|
+
edits went into no number at all. What the tool does not claim is said in the same place: not that the
|
|
389
|
+
project picked the "right" columns, only that nothing went past them, and what exactly did not fit.
|
|
390
|
+
`size explain <commit>` answers about one commit — named by a revision (`HEAD`, a branch, a tag,
|
|
391
|
+
`HEAD~1`), by a sha or by its beginning — saying whether there is a row (and which) or why there is
|
|
392
|
+
none: only the report itself was touched, the numbers did not move although column files were touched,
|
|
393
|
+
no file of the commit is tracked as a column, or the commit is a merge and merges are hidden by
|
|
394
|
+
`rows.merges`. Both take the reason from the same run the reports come from, and the evidence from the
|
|
395
|
+
commit's list of changed paths: what the history does not hold, the answer is silent about instead of
|
|
396
|
+
guessing. Completeness comes from the requirements, and it also replaces the "artifact ↔ history"
|
|
397
|
+
control: the report need not be kept in git. The meaning of `skip` has not changed, but its **reach has
|
|
398
|
+
widened**: it is not only "paths that cannot be columns" but also the declared exceptions of
|
|
399
|
+
completeness — one and the same list, and forging a second tool for it was not necessary. The price is
|
|
400
|
+
named: `check` costs a pass over the history, like any report. Next to them stands `size doctor`, the
|
|
401
|
+
diagnostics in one answer.
|
|
402
|
+
|
|
403
|
+
**The report updates itself.** `size install-hook` installs two hooks, `post-commit` and `post-merge`
|
|
404
|
+
(`post-commit` does not run for a merge at all, which is why one file is not enough), and after every
|
|
405
|
+
commit and merge the report is rebuilt, while the copy lying in git lands as **a commit of its own**:
|
|
406
|
+
the manual step "code, then the table" is gone. The report's commit is assembled with git's plumbing
|
|
407
|
+
(`commit-tree`), so neither the index nor someone else's uncommitted work can get into it, and a loop is
|
|
408
|
+
impossible by construction rather than through an environment flag. A refusal of the tool does not bring
|
|
409
|
+
the commit down: the cause is printed as one line and remembered — `size doctor` shows it.
|
|
410
|
+
|
|
411
|
+
The move, the refinement and the packaging are laid out step by step in `plans/archive/PLAN.md`, with
|
|
412
|
+
acceptance for each.
|
|
413
|
+
|
|
414
|
+
## What is in the repository
|
|
415
|
+
|
|
416
|
+
| File | Role |
|
|
527
417
|
|---|---|
|
|
528
|
-
| `PLAN.md` |
|
|
529
|
-
| `docs/requirements.md` |
|
|
530
|
-
| `docs/module-design.md` |
|
|
531
|
-
| `docs/size-report.html` |
|
|
532
|
-
| `WORKLOG.md`
|
|
533
|
-
| `
|
|
534
|
-
| `
|
|
535
|
-
| `
|
|
536
|
-
| `
|
|
537
|
-
| `tools/
|
|
538
|
-
| `tools/
|
|
539
|
-
| `tools/
|
|
540
|
-
| `tools/
|
|
541
|
-
| `tools/check
|
|
542
|
-
|
|
|
543
|
-
| `.github/workflows/
|
|
544
|
-
|
|
|
545
|
-
| `tools/gates/
|
|
546
|
-
| `tools/gates/
|
|
547
|
-
| `tools/gates/
|
|
548
|
-
| `tools/gates/
|
|
549
|
-
| `tools/gates/
|
|
550
|
-
| `tools/gates/
|
|
551
|
-
|
|
|
552
|
-
| `.githooks/
|
|
553
|
-
|
|
|
554
|
-
|
|
|
555
|
-
| `.
|
|
556
|
-
| `
|
|
557
|
-
|
|
|
558
|
-
| `
|
|
559
|
-
| `
|
|
560
|
-
| `fixtures/
|
|
561
|
-
| `fixtures/
|
|
562
|
-
| `
|
|
563
|
-
| `
|
|
564
|
-
|
|
|
565
|
-
| `
|
|
566
|
-
| `src/
|
|
567
|
-
| `src/
|
|
568
|
-
| `src/
|
|
569
|
-
| `src/
|
|
570
|
-
| `src/page/
|
|
571
|
-
| `src/page/
|
|
572
|
-
| `src/page/
|
|
573
|
-
| `src/page/
|
|
574
|
-
| `src/page/
|
|
575
|
-
| `src/page/
|
|
576
|
-
| `src/
|
|
577
|
-
| `src/
|
|
578
|
-
| `src/strip
|
|
579
|
-
| `src/strip/
|
|
580
|
-
| `src/strip/
|
|
581
|
-
| `src/
|
|
582
|
-
| `src/parse
|
|
583
|
-
| `src/
|
|
584
|
-
| `src/
|
|
585
|
-
| `src/
|
|
586
|
-
| `src/
|
|
587
|
-
| `src/
|
|
588
|
-
| `src/
|
|
589
|
-
| `src/
|
|
590
|
-
| `src/
|
|
591
|
-
| `src/
|
|
592
|
-
| `bin/postinstall.js`
|
|
593
|
-
| `
|
|
594
|
-
| `src/
|
|
595
|
-
| `src/
|
|
596
|
-
| `src/
|
|
597
|
-
| `src/
|
|
598
|
-
| `src/
|
|
599
|
-
| `src/
|
|
600
|
-
| `src/
|
|
601
|
-
| `src/
|
|
602
|
-
| `src/
|
|
603
|
-
| `
|
|
604
|
-
| `
|
|
605
|
-
| `
|
|
606
|
-
| `tools/
|
|
607
|
-
| `tools/
|
|
608
|
-
| `tools/
|
|
609
|
-
| `tools/
|
|
610
|
-
| `tools/
|
|
611
|
-
| `tools/
|
|
612
|
-
| `
|
|
613
|
-
| `test/
|
|
614
|
-
| `test/
|
|
615
|
-
| `test/
|
|
616
|
-
| `test/
|
|
617
|
-
| `test/
|
|
618
|
-
| `test/
|
|
619
|
-
| `test/refusals
|
|
620
|
-
| `test/
|
|
621
|
-
| `test/contract-
|
|
622
|
-
| `test/
|
|
623
|
-
| `test/page-
|
|
624
|
-
| `test/page-
|
|
625
|
-
| `test/
|
|
626
|
-
| `test/
|
|
627
|
-
| `test/
|
|
628
|
-
| `test/
|
|
629
|
-
| `test/
|
|
630
|
-
| `test/
|
|
631
|
-
| `test/release.test.js` |
|
|
632
|
-
| `test/suites.test.js` |
|
|
633
|
-
| `test/gates-metrics.test.js`, `test/gates-dup.test.js`, `test/gates-deps.test.js`, `test/gates-coverage.test.js`, `test/gates-files.test.js` |
|
|
634
|
-
| `test/gates-verify.test.js` |
|
|
635
|
-
| `test/check.test.js` |
|
|
636
|
-
| `test/doctor.test.js` |
|
|
637
|
-
| `test/hook.test.js` |
|
|
638
|
-
| `test/templates.test.js` |
|
|
639
|
-
| `test/minify.test.js`, `test/tokens.test.js` |
|
|
640
|
-
| `package.json` |
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
418
|
+
| `plans/archive/PLAN.md` | **The main document of the move:** inventory, boundaries, invariants, architecture, the seven steps, acceptance, risks, open questions |
|
|
419
|
+
| `docs/requirements.md` | The customer's requirements: what and why |
|
|
420
|
+
| `docs/module-design.md` | The design of the extraction: how the module is put together |
|
|
421
|
+
| `docs/size-report.html` | The size report of this very project: one self-contained file, refreshed by the hook after every commit (as a commit of its own) |
|
|
422
|
+
| `worklog/` | The journal of requests and of what was done: an entry per portion of work, named `NNNN-slug.md`; `worklog/archive/WORKLOG.md` is the earlier journal in one file |
|
|
423
|
+
| `docs/plans/` | Plans of work: a folder `yyyy-mm-dd-name` per piece of work, holding the main plan and its subplans |
|
|
424
|
+
| `BLOCKERS.md` | Open blockers and known gaps (a workaround has to rest on a check) |
|
|
425
|
+
| `TODO.md` | Defects noticed in passing, one line each: where, what and how it shows — fixed in a portion of their own |
|
|
426
|
+
| `plans/archive/REFACTOR.md` | The per-channel plan of the cleanup: size of the code first, speed after; the boundaries and what proves that the behaviour did not change |
|
|
427
|
+
| `tools/parity-freeze.js` | Takes the parity reference (`pnpm run parity`): with the frozen copy, at the project revision from the manifest — `--json`, the config, the artifact's hash, the tool's hash |
|
|
428
|
+
| `tools/make-fixture.js` | Assembles the synthetic fixture (`pnpm run fixture`): a deterministic history with traps plus the reference numbers |
|
|
429
|
+
| `tools/synthetic/` | The subjects of that assembly, one per matter: `repo.js` — how git is spoken to (pinned time, author, settings), `content.js` — what the files hold, `history.js` — which commits come of it, `note.js` — the fixture's note with the list of traps |
|
|
430
|
+
| `tools/parity-live.js` | Compares the engine with the live project on a clone: the numbers and the self-contained report at the path the consumer's settings give (`pnpm run parity:live`) |
|
|
431
|
+
| `tools/pack-check.js` | Assembles the tarball and checks that everything works from it: all sources arrived, the numbers and the report as from the repository (`pnpm run pack:check`) |
|
|
432
|
+
| `tools/check-standards.js` | Checks that both references reproduce: a re-take goes nowhere and is compared with what is committed (our files byte for byte, the bundle by content), and that the live-history bundle carries `HEAD` (`pnpm run check:standards`) |
|
|
433
|
+
| `.github/workflows/ci.yml` | CI: the job `verify` calls `pnpm run verify` on every push and every pull request — the same profile as locally; the actions are pinned by commit SHA |
|
|
434
|
+
| `.github/workflows/verify-slow.yml` | The slow profile on a schedule: the same plus the same suite with no machine git settings and coverage under c8 — the dear steps, not in every run |
|
|
435
|
+
| `tools/gates/run.js` | The check profiles — the single list of steps: `fast` (every edit), `full` (before pushing and in CI), `slow` (+ the hermetic suite and coverage); `--list` prints the commands |
|
|
436
|
+
| `tools/gates/metrics.js` | The bloat sensor: rules of size and complexity, the weight of checks, debt marks — with an ESLint suppression ratchet (`.eslint-suppressions.json`) |
|
|
437
|
+
| `tools/gates/dup.js` | The duplication sensor: clone fingerprints by content (`dup-baseline.json`), a view against the baseline file and one against the `origin/main` tree |
|
|
438
|
+
| `tools/gates/deps.js` | The dependency sensor: cycles, orphans, the direction of layers and unresolvable imports (`dependency-cruiser`) |
|
|
439
|
+
| `tools/gates/coverage.js` | The coverage sensor: a per-file ratchet against `coverage-baseline.json` rather than a percentage over the repository. The unit is **how much executed** — lines, branches and functions, taken from c8's own numbers — so a file that merely grew does not move the ratchet while code that stopped being run does |
|
|
440
|
+
| `tools/gates/gatefiles.js` | The guard of the gate files: editing thresholds, baselines or the harness without the `Gate-Change:` trailer is red — the `commit-msg` hook at commit time and the `pre-push` hook over a range, while CI reads no trailers at all |
|
|
441
|
+
| `tools/gates/common.js`, `tools/gate-probe.js` | What the sensors share (the root, argument parsing, reports) and the harness of their probes: a sensor is called as a command rather than imported |
|
|
442
|
+
| `.githooks/commit-msg`, `.githooks/pre-commit`, `.githooks/pre-push` | Hooks: the guard of the gate files, the fast profile on an edit and before a push; installed by `pnpm run hooks:install` (no hook manager of our own is started) |
|
|
443
|
+
| `.githooks/post-commit` | Refreshing the report after a commit: a call to the installed copy of the package (the line was written by a person — the tool does not edit someone else's hook directories) |
|
|
444
|
+
| `eslint.metrics.config.js`, `.eslint-suppressions.json` | The bloat sensor's rules and its baseline: thresholds taken from measurements, and everything above them lies in the baseline to be worked off gradually |
|
|
445
|
+
| `.jscpd.json`, `dup-baseline.json` | The duplication sensor's settings and baseline: a fingerprint is taken from a clone's content, which is why the baseline is portable |
|
|
446
|
+
| `.dependency-cruiser.cjs`, `.c8rc.json`, `coverage-baseline.json` | The rules of the dependency graph, the settings of the coverage run and its per-file baseline |
|
|
447
|
+
| `AGENTS.md` | A short instruction for an agent in this repository: what to run, what to do when a sensor is red, what must not be touched |
|
|
448
|
+
| `.github/workflows/release.yml` | A release by tag: the strict linter, the whole suite and the work from the assembled package, the manifest version compared with the tag, and publishing to the registry by the GitHub Actions attestation — no secret and no code from an authenticator |
|
|
449
|
+
| `templates/` | What a project takes as it is: `size-report.config.json` (a draft of settings), `ci.yml` (a description of the check) and `README.md` (what goes where and what to change in them); they ship and are guarded by `pack:check` and `test/templates.test.js` |
|
|
450
|
+
| `fixtures/parity/` | The reference taken from `safe-resets` at commit `bd6ef9d`: 95 rows × 27 columns. The copy of the implementation it was taken with does not lie in the tree — its bytes live in the history and are taken from there on demand |
|
|
451
|
+
| `fixtures/synthetic/` | The fixture's bundle of 16 commits, its config, the reference numbers (the earlier copy's `--json`) and the hash of its artifact in its earlier shape — a record of what the move was checked against |
|
|
452
|
+
| `fixtures/live/history.bundle`, `fixtures/live/README.md` | The consumer project's history at the reference revision `bd6ef9d` and a note on which revision the bundle carries and why it lies in the repository: the live comparison works without access to the private project |
|
|
453
|
+
| `bin/size.js` | The `size` command: what the package installs (`package.json` → `bin`); it counts nothing itself and only calls the entry point |
|
|
454
|
+
| `LICENSE` | MIT: the licence terms travel in the package |
|
|
455
|
+
| `.gitignore`, `pnpm-lock.yaml` | What does not go into the repository; the pnpm lock file, while the manager's version lives in the `packageManager` field (which is where CI takes it from) |
|
|
456
|
+
| `src/size-table.js` | The package's entry point: a re-export of the public API (55 names) and no calculation of its own |
|
|
457
|
+
| `src/derived.js` | The report's shared calculation: totals, deltas, a cell, a commit's caption — one for the engine and the page's program |
|
|
458
|
+
| `src/css.js` | Reading the styling from disk: which sets of styles exist and what role each has |
|
|
459
|
+
| `src/table.css` | The report's table: the geometry of a cell, the sticky header and commit column, the colour of deltas |
|
|
460
|
+
| `src/page/app.css` | The page's styling on top of the shared part: the panel with the file tree and its sticky row of categories (a column on the left on a wide screen, the page fitting the window), the empty states, a narrow window |
|
|
461
|
+
| `src/page/state.js` | The page's state: the report's data, the view of the checkboxes, the pointer "which path is which column", folded folders, the record's passport, the browser's memory and the exchange by link — a chapter of the page's program |
|
|
462
|
+
| `src/page/dom.js` | The page's nodes: the small helpers of markup (`appEl`, `appBox`) — one set for the panel and the table alike |
|
|
463
|
+
| `src/page/panel.js` | The panel of choices: the switches of metrics and files, the categories, the tree of the project's paths (files outside the report keep a checkbox off with a reason and stand after the rest; folders carry a folding sign that hides the subtree by a class rather than by a rebuild); a redraw is asked of the assembling chapter |
|
|
464
|
+
| `src/page/table.js` | The page's table: a cell, a commit's caption, the header and the empty states — markup over the shared calculation |
|
|
465
|
+
| `src/page/app.js` | Assembling and starting the page: the whole table, a redraw on the reader's choice (with the focus and the scroll put back), the first drawing and an anchor change; pasted into the assembled page |
|
|
466
|
+
| `src/page/build.js` | Assembling the page: data, styling and program in one file with no external references |
|
|
467
|
+
| `src/git.js` | The only border where git is called: the pinned settings, blobs by the batch, the history, the comparison with the working tree |
|
|
468
|
+
| `src/strip.js` | Removing ballast: which form goes to which file (extension, strategy) and what counts as an exact number — the entry to the parsing of forms |
|
|
469
|
+
| `src/strip/js.js` | Removing comments and indentation in JS: a pass over the cases (a comment, a regexp, a string, a character) — through strings and templates as well |
|
|
470
|
+
| `src/strip/forms.js` | The forms of text with a removal of their own: markup, styles, the lines of a file and JSON |
|
|
471
|
+
| `src/strip/guard.js` | The stripper's guard: what was stripped has to compile — as a script in the process or as a module in a worker thread |
|
|
472
|
+
| `src/parse.js` | Parsing a module: one worker thread per run and a fallback to `node --check`, and the way the last module was parsed |
|
|
473
|
+
| `src/parse-worker.js` | The parsing itself inside the thread: it parses the text without executing it and reports that Node has no vm modules |
|
|
474
|
+
| `src/metrics.js` | The register of metrics: what is measured, whether the text is needed and how honest the number is; a metric's description for the reader lives in one place |
|
|
475
|
+
| `src/minify.js` | The real minifier: an optional dependency, loaded once, and it does not bring the run down when absent |
|
|
476
|
+
| `src/tokens.js` | Tokens: a dictionary by family and encoding, an estimate by length as the fallback count, the formats without text |
|
|
477
|
+
| `src/optional.js` | The shared handling of optional dependencies (the minifier and the dictionary): lazy loading, the package's version, the seam of absence |
|
|
478
|
+
| `src/history.js` | Walking the history: measuring commit by commit, shifting the numbers, assembling, comparing with the working tree, the mark "which column the last commit touched" and a reason for every dropped commit |
|
|
479
|
+
| `src/check.js` | Coverage (`size check`): settings, history, paths, sensors — what went past the columns and how that is fixed |
|
|
480
|
+
| `src/explain.js` | Explaining a missing row (`size explain <commit>`): the reason, the evidence and a ready fix |
|
|
481
|
+
| `src/doctor.js` | Diagnostics in one answer (`size doctor`): the environment, the dependencies, the settings, the coverage and the hook's state — assembled from the pieces that already exist |
|
|
482
|
+
| `src/hook.js` | The hooks of self-updating: they install themselves (`autoInstall` — from the entry point and `bin/postinstall.js`), come off by a command, commit the report alone, and keep a lock and a record of the run |
|
|
483
|
+
| `bin/postinstall.js` | Installing the hook after the package is added: it looks for the consumer project and stays silent when there is nowhere to install |
|
|
484
|
+
| `src/artifact.js` | The report on disk: the only place where it becomes a file (both `--write` and the hook use it); the report is a self-contained page |
|
|
485
|
+
| `src/journal.js` | The journal and links: which section a commit belongs to and where a description leads |
|
|
486
|
+
| `src/data.js` | The file categories and the contract with the page (`--data`): the numbers, the shape of the table and the catalogue of the project's paths |
|
|
487
|
+
| `src/config.js` | The consumer project's settings: the defaults, reading them, checking them |
|
|
488
|
+
| `src/project.js` | The settings derived from the project itself (its tree and history): columns, the journal, the exceptions, the catalogue of paths for the page's tree. Without a settings file it *is* the settings; `--init` pins it as a file |
|
|
489
|
+
| `src/locales.js`, `src/refusal.js`, `src/tool.js` | The report's texts; the exit codes and the help; the package's name and version |
|
|
490
|
+
| `src/cli.js` | The tool's entry: parsing the command line, reading the project and handing the request to a mode; the package's main file |
|
|
491
|
+
| `src/args.js` | The grammar of the command line: modes, flags and commands plus the checks of their combinations — a refusal names the culprit and a ready command |
|
|
492
|
+
| `src/modes.js` | The modes: assemble the report, compare it with the history, hand over the data, the coverage and the diagnostics |
|
|
493
|
+
| `src/init.js` | Pinning the settings as a file (`--init`): what the project derived about itself is written out — and goes through the same check as the first run |
|
|
494
|
+
| `test/api.test.js` | The package's public API: the list of names is frozen, and splitting the engine may not change it |
|
|
495
|
+
| `eslint.config.js` | The rules of formatting: the same as the consumer project's, plus a ban on gluing operators into one line (`pnpm run lint`, `pnpm run lint:strict`) |
|
|
496
|
+
| `tools/harness.js` | The harness of the checks: paths, clones of the fixture (including one shared per suite and one with CRLF), running the tool, reading refusals, hashes |
|
|
497
|
+
| `tools/page-harness.js` | The harness of the contract and page checks: the contract data, the assembled page, reading it in a real DOM, the panel's switches — one for five suites |
|
|
498
|
+
| `tools/suites.js` | The split of the suite: which files go into the fast run (with a reason for each) and why every dear one is in the full run |
|
|
499
|
+
| `tools/run-tests.js` | Running the suite (`pnpm test`, `pnpm test:all`, `pnpm run suites:measure`): each file's duration measured on its own, and the counts of checks adding up |
|
|
500
|
+
| `tools/docs-facts.js` | Reading facts out of the documentation — one layer for the four checks of the documentation guard: what a document names (paths, calls, section addresses) against what the repository holds |
|
|
501
|
+
| `tools/yaml.js` | Parsing a subset of YAML — one parser for the two guards over descriptions (`templates/ci.yml` and `.github/workflows/release.yml`): anything outside the subset is an error rather than a silently skipped line, including a colon followed by a space in an unquoted value — which is what kept the release description unparsable while the check looked for substrings |
|
|
502
|
+
| `tools/refusals.js` | The catalogue of refusals: one line per refusal — its cause, its exit code, the phrases its output must carry, and **what it advises** (`advice`: `run` — a command, `template` — a form with substitutions, `manual` — a person's action with its reason, `coveredBy` — handed to another check), and for one that cannot be caught at all, why. The maps of refusal sites (`SITES`, `PRINTED`) hold the counts, so that a new refusal cannot appear in silence, and the markers of advice so that a new piece of advice cannot either |
|
|
503
|
+
| `test/parity.test.js` | The engine's parity with the reference: the numbers, the report's self-containedness, the locale |
|
|
504
|
+
| `test/frozen.test.js` | The frozen copy: that it is the revision the reference was taken at, and that it reproduces that reference |
|
|
505
|
+
| `test/environment.test.js` | Hermeticity: the output does not depend on the machine's git settings or on its locale |
|
|
506
|
+
| `test/crlf.test.js` | A checkout with CRLF (`core.autocrlf`) does not hinder the comparison |
|
|
507
|
+
| `test/disk.test.js` | The comparison with the working tree: an edit only on disk, three ways of losing a change (an edit, a creation, a deletion — all by mutation), a file deleted before HEAD, and a rename inside aliases is no loss |
|
|
508
|
+
| `test/cli.test.js`, `test/cli-paths.test.js` | The command line's refusals: the help, the settings, the exit codes — and where the tool writes |
|
|
509
|
+
| `test/refusals.test.js` | The refusals are executed: each one is called by a run, its exit code and its promised phrases are compared (with clones of their own for someone else's hook, a shallow history and a branch past the report), and **the advice runs** — the command answers with the promised code and no stack, while where "the refusal is gone" is declared the same call answers differently after it |
|
|
510
|
+
| `test/refusals-catalog.test.js` | The guard of the refusal catalogue: every refusal site in the sources has an entry, every entry declares its advice, and refusals handed to another check are really accepted by it (the named file and line are checked) |
|
|
511
|
+
| `test/contract-data.test.js` | The data contract: the numbers against the reference, the set of fields against the derived quantities, the marks of approximation against a metric's caption |
|
|
512
|
+
| `test/contract-derived.test.js` | The derived quantities against the artifact's numbers: a row's totals, a cell's delta and the delta of a total — on the code that lies in the tree |
|
|
513
|
+
| `test/page-view.test.js` | The assembled page: pasted with no copy of the calculation, self-contained, the empty states, the styling and the switches |
|
|
514
|
+
| `test/page-tree.test.js` | The panel's file tree: folders by the project's paths, three states, the subtree, files and folders outside the report (a checkbox off, a place after the rest), folding without a rebuild and the scroll across a rebuild |
|
|
515
|
+
| `test/page-choice.test.js` | The memory of the choice and the exchange by link: a revisit, someone else's report, a foreign and a broken record, an address change on an open page |
|
|
516
|
+
| `test/module.test.js` | A module under a `.js` extension: measured without touching the settings; the stripper's guard is alive (proved by mutation) and does not accuse the innocent |
|
|
517
|
+
| `test/guard.test.js` | Parsing a module: it goes through a thread, both paths give one verdict, the fallback works with the thread's file away, and hundreds of parses are cheaper than a launch |
|
|
518
|
+
| `test/runner.test.js` | Reading a process's output: chunks are glued as buffers rather than appended to a string — a multi-byte character at a chunk border does not turn into two replacement characters |
|
|
519
|
+
| `test/git-pins.test.js` | The guard of the git border: no direct calls to git outside the shared list of pins, and an unpinned read is shown by a witness (a quoted path) |
|
|
520
|
+
| `test/docs-paths.test.js`, `test/docs-commands.test.js`, `test/docs-numbers.test.js`, `test/docs-pin.test.js` | The documentation guard, one file per promise: the paths and the file table; the calls, the causes of refusal and the section addresses; the counts of checks; the pin in the install example |
|
|
521
|
+
| `test/release.test.js` | The guard of the release from CI: it begins with a tag, the version comes from the manifest, no secret and no one-time code are needed, a prerelease does not go to `latest`, the whole suite runs before publishing — and the hint on npmjs.com names this same file |
|
|
522
|
+
| `test/suites.test.js` | The guard of the suite's split: the classification is complete (fast only explicitly, full with a reason), every file has its reason, and the fast run stays part of the suite |
|
|
523
|
+
| `test/gates-metrics.test.js`, `test/gates-dup.test.js`, `test/gates-deps.test.js`, `test/gates-coverage.test.js`, `test/gates-files.test.js` | The sensors' probes: an artificial violation → the sensor is red, taking it away → green again; the run calls a sensor as a command rather than importing it, which is why it proves the exit code too |
|
|
524
|
+
| `test/gates-verify.test.js` | The guard of the single list: the profile's commands against the workflows, the hooks and `templates/ci.yml` — a check that is not in the profile cannot be in CI |
|
|
525
|
+
| `test/check.test.js` | Coverage and explanation on the fixture's real commits: an uncovered path, "only the report", "the number did not move", "past the columns", a merge — and that a fix of the settings does not move the numbers |
|
|
526
|
+
| `test/doctor.test.js` | Diagnostics over five states of a project: no settings (2), full coverage (0), incomplete (1), a shallow history (3), no sensor (4) — and the coverage block equals the answer of `size check` rather than being counted a second time |
|
|
527
|
+
| `test/hook.test.js` | The hooks on a fresh clone: they are installed by a command only, give a commit of the report's own (after a merge as well), a repeated run stays silent, someone else's work and the index are untouched, nothing happens in CI or on a refusal of the tool, and removing them returns the project to what it was |
|
|
528
|
+
| `test/templates.test.js` | The templates: the draft of settings passes the tool's check and assembles a real report; the description of the check parses and calls only commands and flags that exist |
|
|
529
|
+
| `test/minify.test.js`, `test/tokens.test.js` | Real minification and tokens: the numbers against stripping, the encoding as part of the number, the honesty of a caption, work with no optional dependency (code 4) and the seam `SIZE_REPORT_NO_OPTIONAL` |
|
|
530
|
+
| `package.json` | The package's manifest: the name `@vernikr/size-report`, the version, and a shipped-file list that holds only what exists |
|
|
531
|
+
|
|
532
|
+
**Both references are taken anew by the same tools:** `pnpm run parity` and `pnpm run fixture` give
|
|
533
|
+
the same files. What is ours is compared byte for byte — the config, the reference numbers, the
|
|
534
|
+
artifact's hash, the description — while the history bundle is compared by content (the branches, the
|
|
535
|
+
tip, the number of commits), because git does the packing and its bytes depend on git's version. Both
|
|
536
|
+
sides of the pair are pinned: the tool is a frozen copy of the implementation, whose bytes live in the
|
|
537
|
+
history (`fixtures/legacy/size-table.cjs`) and are compared against the record of the reference's
|
|
538
|
+
provenance, while the project's revision comes from the manifest (`--at` shifts it deliberately) and
|
|
539
|
+
the environment of the capture is set (`core.quotePath=false`). Without the pinned environment the
|
|
540
|
+
reference is taken with different numbers: on a machine with git's default settings the fixture loses
|
|
541
|
+
a row whose file name is not English. Repetition and hermeticity are not taken on trust either:
|
|
542
|
+
`test/git-pins.test.js` shows an unpinned read by a witness, `test/environment.test.js` keeps the
|
|
543
|
+
output independent of the machine, and the slow profile repeats the whole suite with none of the
|
|
544
|
+
machine's git settings at all. That what the manifests record agrees with the files is guarded by
|
|
545
|
+
`test/frozen.test.js`.
|
|
546
|
+
|
|
547
|
+
## What is not here yet
|
|
657
548
|
|
|
658
549
|
```text
|
|
659
|
-
dist/app.js
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
550
|
+
dist/app.js a pre-assembled report program: the page's program is pasted into the page
|
|
551
|
+
while the report is built, so the file would be a second copy of the same
|
|
552
|
+
size init / measure commands instead of flags: of the commands only check, explain, doctor and
|
|
553
|
+
the hook are here, and no command measures at all
|
|
554
|
+
a block for agents an instruction for the project's own agent: the requirements do not ask for
|
|
555
|
+
it, so the templates carry none
|
|
556
|
+
HTML minification a minifier of markup: HTML counts as stripping for now
|
|
557
|
+
JSX and TSX the output depends on the project's own jsx setting — stripping
|
|
558
|
+
token families anything but openai: the others have no dictionary of their own, and counting
|
|
559
|
+
with someone else's is not a family
|
|
665
560
|
```
|
|
666
561
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
сервера и без сети. Остальное — по шагам 2–6 (`PLAN.md` §5).
|
|
562
|
+
The seams between modules follow the borders of data: above sit the parts that read git and the file system
|
|
563
|
+
(`git`, `strip`, `metrics`, `history`), below the parts that work on values already collected (`data`,
|
|
564
|
+
`derived`, `page`), while the settings, the texts and the refusal stand at the edges, because everyone
|
|
565
|
+
knows them and they know no one. Both reports are counted at build time: the page gets the sources of the
|
|
566
|
+
shared calculation and of its own program pasted in (`src/derived.js`, `src/page/*.js`), because it opens
|
|
567
|
+
from disk, with no server and no network. The rest is planned step by step in `plans/archive/PLAN.md`.
|
|
674
568
|
|
|
675
|
-
##
|
|
569
|
+
## Wiring it into your project
|
|
676
570
|
|
|
677
|
-
|
|
678
|
-
`
|
|
679
|
-
|
|
680
|
-
каждый такой случай — отдельный пункт `REFACTOR.md`.
|
|
571
|
+
The instruction was walked through command by command in a fresh project (the protocol is in
|
|
572
|
+
`worklog/archive/WORKLOG.md` §54): below are exactly the commands that work today. What does **not** work
|
|
573
|
+
today is named here too, with its reason, so that nobody has to find it out by trying.
|
|
681
574
|
|
|
682
|
-
|
|
683
|
-
|
|
575
|
+
What is needed: **a git repository with history** — at least one commit, because the table is built from
|
|
576
|
+
commits (a repository with none ends in an internal error today: `BLOCKERS.md` §N16) — and
|
|
577
|
+
**Node ≥ 20.19** (`engines` of the package).
|
|
684
578
|
|
|
685
|
-
|
|
686
|
-
коммитам) и **Node ≥ 20.19** (`engines` пакета).
|
|
687
|
-
|
|
688
|
-
### 1. Установка
|
|
579
|
+
### 1. Installation
|
|
689
580
|
|
|
690
581
|
```bash
|
|
691
582
|
pnpm add -D @vernikr/size-report
|
|
692
583
|
```
|
|
693
584
|
|
|
694
|
-
|
|
695
|
-
version
|
|
696
|
-
|
|
697
|
-
|
|
585
|
+
The package is **published in the registry**, and publicly: `npm view @vernikr/size-report version`
|
|
586
|
+
answers the same version the manifest names, `npm access get status @vernikr/size-report` says `public`,
|
|
587
|
+
and an anonymous request for the tarball is a 200. `npm i -D` and `yarn add -D` take the same name; no key
|
|
588
|
+
and no link to the repository are needed.
|
|
698
589
|
|
|
699
|
-
|
|
700
|
-
|
|
590
|
+
The same release can be taken by a reference to the repository — installation then does not depend on the
|
|
591
|
+
registry, but stays tied to a revision:
|
|
701
592
|
|
|
702
593
|
```bash
|
|
703
|
-
pnpm add -D github:vernikr/size-report#v2.
|
|
594
|
+
pnpm add -D github:vernikr/size-report#v2.5.0
|
|
704
595
|
```
|
|
705
596
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
`git ls-remote`
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
### 2. Настройки: их можно не заводить
|
|
597
|
+
With no network (or nothing to fetch from codeload) — the tarball: `pnpm pack` in the package clone, then
|
|
598
|
+
`pnpm add -D ./vernikr-size-report-<version>.tgz`, where the name is the one `pnpm pack` printed.
|
|
599
|
+
|
|
600
|
+
**Why a tag rather than a sha.** pnpm resolves a short sha only through visible refs, while
|
|
601
|
+
`git ls-remote` gives branch tips alone: while the revision is a tip, a short sha installs, and as soon as
|
|
602
|
+
the branch moves on the installation fails with `Could not resolve <sha> to a commit`. This is an
|
|
603
|
+
observation rather than reasoning: the short pin `6530237` installed while `main` stood on it and stopped
|
|
604
|
+
working at the very next commit, while the same sha in full installed. A branch name (`#main`) and a tag
|
|
605
|
+
are both accepted, but a branch is a moving target and a tag is constant: this release stands on the tag
|
|
606
|
+
`v2.5.0`, which is also the one in the example (forty characters work as well, but they have to be copied
|
|
607
|
+
out of the history by eye).
|
|
608
|
+
|
|
609
|
+
The revision in the example is a part of the claim rather than decoration: what is described below is
|
|
610
|
+
pinned to it. A pin older than the commands (`check`, `explain`, `doctor`, `install-hook`) would teach
|
|
611
|
+
commands the installed revision does not have, and an extra word there is not refused but silently
|
|
612
|
+
skipped — that is, instead of a refusal the person gets a zero and concludes all is well. So the pin is
|
|
613
|
+
the revision that holds everything named below, refusals on an unknown word included.
|
|
614
|
+
`test/docs-pin.test.js` guards that: the pin has to lead to a revision of this repository, and every
|
|
615
|
+
command named in the text has to be in that revision's help.
|
|
616
|
+
|
|
617
|
+
The package repository is **public** (it was private until 2026-09-14), and that is exactly what makes the
|
|
618
|
+
installation simple: no developer key and no CI step with access. Checked by a run in an empty project
|
|
619
|
+
where git had neither global settings nor a credential helper (`GIT_CONFIG_GLOBAL=/dev/null
|
|
620
|
+
GIT_CONFIG_SYSTEM=/dev/null GIT_SSH_COMMAND=false`): `size --write` and `size` work there
|
|
621
|
+
(`worklog/archive/WORKLOG.md` §44). The earlier requirement was the price of privacy: a key locally and a
|
|
622
|
+
read-only deploy key before `pnpm install` in CI (that first wiring, `worklog/archive/WORKLOG.md` §18);
|
|
623
|
+
the template's key step went away together with the privacy. The publication to npm happened on
|
|
624
|
+
2026-09-15, and it had a price: the name `size-report` in the registry is taken by someone else's package
|
|
625
|
+
(2017, three versions), so the release was also a renaming into the owner's scope (`@vernikr/size-report`)
|
|
626
|
+
rather than just an upload of an archive; what the renaming touched — `plans/archive/PLAN.md` §10, what proves the
|
|
627
|
+
publication — `worklog/archive/WORKLOG.md` §53.
|
|
628
|
+
|
|
629
|
+
### 2. Settings: you need not create them
|
|
741
630
|
|
|
742
631
|
```bash
|
|
743
|
-
pnpm exec size --write #
|
|
744
|
-
pnpm exec size --init #
|
|
632
|
+
pnpm exec size --write # the table; no settings — the tool derives them itself
|
|
633
|
+
pnpm exec size --init # pin what it derived into size-table.config.json
|
|
745
634
|
```
|
|
746
635
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
`
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
знает человек. Чаще всего правят:
|
|
785
|
-
|
|
786
|
-
| Ключ | Что это |
|
|
636
|
+
There is no need to start with settings: without a file the tool derives them from the project — the
|
|
637
|
+
columns are **every tracked file git can measure** (the report names the volume of the project rather than
|
|
638
|
+
of a sample of it, and the only limits are what cannot be a column at all: the report itself, dependency
|
|
639
|
+
locks, built output, an unknown format and a file above 512 KB), while everything else is named in `skip`;
|
|
640
|
+
the journal is the first familiar one (`WORKLOG.md`, `CHANGELOG.md`, …); the report file is
|
|
641
|
+
`docs/size-report.html` (the directory is created by the writer); the fix command is the declared `sizes`
|
|
642
|
+
script, or the path to the installed package without one (the report's signature and the refusals quote it,
|
|
643
|
+
so it has to work right here and now); the commit link comes from the `origin` address; the metrics are
|
|
644
|
+
`raw`, `min` and `tok`. The `min` metric is counted by real compression here (`"minify": {"engine":
|
|
645
|
+
"esbuild"}`), and `tok` by a dictionary (`"tokens": {"family": "openai", "encoding": "o200k_base"}`):
|
|
646
|
+
without those optional dependencies the metric honestly falls back to another count and the run returns
|
|
647
|
+
code 4 — no settings need editing for that either.
|
|
648
|
+
|
|
649
|
+
Everything that cannot be a column (the report itself, dependency locks, maps, built output, an unknown
|
|
650
|
+
format, a file too large) and everything git does not track is named in `skip` — which is why the first
|
|
651
|
+
`size check` is complete rather than red: "paths past the columns" appear from new edits, not from a
|
|
652
|
+
project that has not been described yet. That the settings were derived, the tool says in a line on stderr
|
|
653
|
+
and names the command that pins them, `--init`; what is pinned passes the same check as any settings file,
|
|
654
|
+
and afterwards it is edited by hand (the `--init` itself prints what it pinned and what to do next — the
|
|
655
|
+
scripts and the CI check). Without pinning, the profile is derived anew on every run: the column set
|
|
656
|
+
changes from run to run (the tool says so with that very line), so repeating the same measurement — by the
|
|
657
|
+
hook and by the check included — is possible only from a file.
|
|
658
|
+
|
|
659
|
+
What is pinned is **the very thing the project runs on without a file**: the derivation from the project
|
|
660
|
+
on top of the defaults. That is why the pinned file holds values nobody wrote in the project — then a
|
|
661
|
+
change of the defaults in a new version of the package does not travel over an already configured project
|
|
662
|
+
in silence.
|
|
663
|
+
|
|
664
|
+
> `size init` as a command does not exist — `--init` is a mode: the commands are `check`, `explain`,
|
|
665
|
+
> `doctor` and the hook, and the full list is given by `size --help`.
|
|
666
|
+
|
|
667
|
+
### 3. What is edited in the config
|
|
668
|
+
|
|
669
|
+
The derivation knows about the project only what the tree and the history show — which columns matter is
|
|
670
|
+
known to a person. What is edited most often:
|
|
671
|
+
|
|
672
|
+
| Key | What it is |
|
|
787
673
|
|---|---|
|
|
788
|
-
| `columns` |
|
|
789
|
-
| `metrics` |
|
|
790
|
-
| `tokens.family`, `tokens.encoding` |
|
|
791
|
-
| `minify.engine` |
|
|
792
|
-
| `output` |
|
|
793
|
-
| `journal` |
|
|
794
|
-
| `links.commitUrl` |
|
|
795
|
-
| `skip` |
|
|
796
|
-
| `fixCommand` |
|
|
797
|
-
| `locale`, `title`, `heading` |
|
|
798
|
-
| `minify.guard` |
|
|
799
|
-
| `hooks.enabled` |
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
### 4.
|
|
674
|
+
| `columns` | the table's columns: `{label, paths: [...]}`; **a column is a file**: the list of paths is its renames (a revision takes whichever of them it holds), not several files at once; `label` is what a person will see |
|
|
675
|
+
| `metrics` | what a number is made of: `raw` (the size of the git object), `min` (the minified form — which one, `minify.engine` decides), `tok` (tokens), `gzip` |
|
|
676
|
+
| `tokens.family`, `tokens.encoding` | the dictionary for `tok`: the family (`openai`) and the encoding (`o200k_base` or `cl100k_base`) — the encoding changes the number, which is why it is both in the settings and in the metric's label |
|
|
677
|
+
| `minify.engine` | what counts `min`: `strip` (comments and indentation, with no accuracy promised) or `esbuild` (real compression; a format without a minifier counts as stripping, and the metric's label says so) |
|
|
678
|
+
| `output` | the report file (in the derived profile `docs/size-report.html`; the directory is created by the writer). The path enters the report's passport — the key of the saved choice — so a changed path means a fresh choice |
|
|
679
|
+
| `journal` | where to look for the journal sections the rows refer to |
|
|
680
|
+
| `links.commitUrl` | the commit link template, for example `https://github.com/org/repo/commit/{sha}`; derived from the `origin` address for GitHub and GitLab (for other hosts — empty rather than a guess) |
|
|
681
|
+
| `skip` | the paths that did not become columns: both those that cannot be (the report itself, dependency locks) and those that did not fit (the derived profile declares everything else an exception — which is why the first `check` is complete) |
|
|
682
|
+
| `fixCommand` | the command the report's signature quotes and a refusal suggests; in the derived profile it is your `sizes` script if it is declared, and otherwise the path to the installed package inside the project (a call by package name goes to the registry — `REFACTOR.md` R-4.21) |
|
|
683
|
+
| `locale`, `title`, `heading` | the language of the report's texts and its headings; empty `title`/`heading` mean "take them from the locale" |
|
|
684
|
+
| `minify.guard` | the extensions whose stripper output is checked by parsing; a module in `.js` the guard understands by itself, and there is nothing to touch there |
|
|
685
|
+
| `hooks.enabled` | the switch of the self-updating hook (`false` — the hook is not installed by itself and keeps quiet if it is already there; it is removed only by `size uninstall-hook`) |
|
|
686
|
+
|
|
687
|
+
The other keys and defaults are in `src/config.js` (`DEFAULT_CONFIG`).
|
|
688
|
+
|
|
689
|
+
### 4. Scripts and the first report
|
|
804
690
|
|
|
805
691
|
```jsonc
|
|
806
692
|
// package.json
|
|
@@ -808,300 +694,292 @@ stderr и называет команду, которая их закрепля
|
|
|
808
694
|
```
|
|
809
695
|
|
|
810
696
|
```bash
|
|
811
|
-
pnpm run sizes # → docs/size-report.html —
|
|
697
|
+
pnpm run sizes # → docs/size-report.html — the report: the table, the filters, the link
|
|
812
698
|
```
|
|
813
699
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
700
|
+
The report is one self-contained file: it opens with a double click, with no server and no network (it
|
|
701
|
+
holds no external references at all — the data, the styling and the program are pasted in). What is
|
|
702
|
+
derived (the deltas, the totals, the filters) is counted by the page itself — from the absolute values the
|
|
703
|
+
engine gives, and by the same code as the engine's own calculation.
|
|
818
704
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
705
|
+
**The order of edits:** code → `pnpm run sizes` → a commit with the table alone. The table is updated in a
|
|
706
|
+
**commit of its own**, because a commit cannot have a row inside itself: update it together with the code
|
|
707
|
+
and the tool warns (the text is quoted as the tool prints it: `! the table was updated together with the code: <sha>`)
|
|
708
|
+
and names the commit that dropped out.
|
|
709
|
+
The `size` check rebuilds the table and compares it with the file on disk, so it catches a forgotten
|
|
710
|
+
rebuild too. Dropping the report from git altogether is possible as well: the completeness check exists
|
|
711
|
+
for that, and `templates/ci.yml` says which step to put in its place when the report is not in git.
|
|
826
712
|
|
|
827
|
-
### 5.
|
|
713
|
+
### 5. The check in CI and before a commit
|
|
828
714
|
|
|
829
715
|
```bash
|
|
830
|
-
pnpm run test:sizes # 0 —
|
|
831
|
-
pnpm exec size check # 0 —
|
|
832
|
-
pnpm exec size doctor # 0 —
|
|
716
|
+
pnpm run test:sizes # 0 — the table agrees with the history
|
|
717
|
+
pnpm exec size check # 0 — not one change went past the columns
|
|
718
|
+
pnpm exec size doctor # 0 — nothing to do; otherwise the first code by importance
|
|
833
719
|
```
|
|
834
720
|
|
|
835
|
-
`size check`
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
команд, что описан ниже, и его можно взять за образец для шага потребителя.
|
|
881
|
-
|
|
882
|
-
| Код | Что случилось | Что делать |
|
|
721
|
+
`size check` answers a different question than the `size` command itself: that one says "the table agrees
|
|
722
|
+
with the history", while this one says "the whole history is counted": every path the commits touched has
|
|
723
|
+
to be either a column or a declared exception (`skip` and the report file itself), otherwise it is **code
|
|
724
|
+
1** with the list of paths, the commit that introduced the path and a fix command. The report need not lie
|
|
725
|
+
in git for that — completeness is exactly the check that replaces "artifact ↔ history". When a single
|
|
726
|
+
commit is in doubt, `pnpm exec size explain <commit>` explains why it has no row: the report alone was
|
|
727
|
+
touched, the numbers did not move, the commit went past the columns, or a merge is hidden by a setting —
|
|
728
|
+
with evidence and a fix where there is one. The commit may be named the way git names it: `HEAD`,
|
|
729
|
+
`HEAD~1`, a branch or a tag, a full sha or its beginning. If the name leads to a commit outside the
|
|
730
|
+
report's history (another branch), the tool says exactly that and names its sha — rather than "no such
|
|
731
|
+
commit".
|
|
732
|
+
|
|
733
|
+
`size doctor` gathers all the diagnostics into one answer: the environment and its influence on the numbers
|
|
734
|
+
(the machine's settings do not influence them — the engine pins them at the call's border), the state of
|
|
735
|
+
the optional dependencies and what it means for accuracy, the validity of the settings and the completeness
|
|
736
|
+
of the coverage. It answers with the same pieces as the other commands: the coverage block is exactly the
|
|
737
|
+
answer of `size check` rather than a second calculation. The exit code is the first by importance rather
|
|
738
|
+
than "something was found": `2` the settings are unreadable (there is nothing else to read), `3` the
|
|
739
|
+
history is cut short, `1` the coverage is incomplete, `4` a number is approximate, `0` nothing to do. A
|
|
740
|
+
sensor the settings are silent about is named unneeded rather than missing, and it is not loaded: the
|
|
741
|
+
dictionary weighs megabytes, and there is nothing to pay with for an answer the numbers never needed.
|
|
742
|
+
|
|
743
|
+
The completeness check is deliberately **not** in the template CI (`templates/ci.yml`): the columns there
|
|
744
|
+
are an example, and in a project whose columns are not chosen yet such a check would be red for no reason.
|
|
745
|
+
Once the columns describe the project, it is added in one line (`pnpm exec size check`).
|
|
746
|
+
|
|
747
|
+
The ready line for CI: `pnpm run test:sizes` — nothing else is needed: the check *is* the `size` command,
|
|
748
|
+
and a consumer has no test suite of its own to install. The `--init` prompt says the same: the check is a
|
|
749
|
+
command of the package and brings no files of its own into the project.
|
|
750
|
+
|
|
751
|
+
The package also ships a ready description of that check: `templates/ci.yml` from the package
|
|
752
|
+
(`node_modules/@vernikr/size-report/templates/ci.yml`) goes to `.github/workflows/size-report.yml` without
|
|
753
|
+
edits — the table rebuilt and compared with the file on disk, two snapshots of the numbers (a plain one
|
|
754
|
+
and one in an environment without the machine's git settings) and their comparison. It needs no secrets.
|
|
755
|
+
For `npm`/`yarn` the file itself says which two lines to replace. Next to it is
|
|
756
|
+
`templates/size-report.config.json`, a sample of settings: its columns are examples (`README.md`,
|
|
757
|
+
`package.json`) that nearly any project has, so the first report is built at once. It is needed only to
|
|
758
|
+
start from an edited file: with no file the settings are derived from the project (`--init` pins the
|
|
759
|
+
derived ones), and the sample is copied to the project root as `size-table.config.json`.
|
|
760
|
+
|
|
761
|
+
The package's own CI is `.github/workflows/ci.yml`: it runs at home the same list of checks as a local run
|
|
762
|
+
(one command, `pnpm run verify`, whose list lives in `tools/gates/run.js`), while what a consumer's CI is
|
|
763
|
+
put together from are the templates above.
|
|
764
|
+
|
|
765
|
+
| Code | What happened | What to do |
|
|
883
766
|
|---|---|---|
|
|
884
|
-
| 0 |
|
|
885
|
-
| 1 |
|
|
886
|
-
| 2 |
|
|
887
|
-
| 3 |
|
|
888
|
-
| 4 |
|
|
889
|
-
| 5 |
|
|
767
|
+
| 0 | everything agrees | nothing |
|
|
768
|
+
| 1 | the table diverged from the history (or an edit on disk is not committed); for `size check` — a path of the history is neither tracked nor excluded | `pnpm run sizes` and commit the table; for `check` — add the path as a column or to `skip` |
|
|
769
|
+
| 2 | something in the call or in the project — the causes are quoted as the tool prints them: **command line** (unknown flag, flag without a value, repeated flag, two modes at once, extra word, command and mode, unknown command, incompatible flag, no JSON answer, two answers at once, no commit); **settings and the project** (no settings file, settings not parsed, settings invalid, git missing, not a git repository, config already exists); **history** (no such commit, ambiguous commit, commit outside the history); **hook** (foreign hook, foreign core.hooksPath, no way to invoke the tool); **measurement** (file is not JavaScript, minifier did not parse) | the refusal text names the reason and a ready command — and it is executable: `test/refusals.test.js` guards that |
|
|
770
|
+
| 3 | a shallow history (a clone with `--depth`) | a full clone: `git fetch --unshallow` |
|
|
771
|
+
| 4 | no sensor | `minify.engine: "esbuild"` with no minifier: the numbers are stripped rather than minified. The report is built, and its text carries the reason and the fix; if the table also diverges from the history, the code stays **1** (a mismatch outranks the approximation) while the note about the other count is printed next to it |
|
|
772
|
+
| 5 | an internal error | this is a defect of the tool: we are the ones who need the text — see "Traps worth testing the engine on" below |
|
|
773
|
+
|
|
774
|
+
The cell of code 2 quotes the tool rather than describing it: those are the names of the refusal registry
|
|
775
|
+
(`CONFIG_CAUSES` in `src/refusal.js`), and the documentation guard compares this table with it word by
|
|
776
|
+
word — which is why that one cell speaks the language of the command line, while the report's own texts
|
|
777
|
+
are translated by the `locale` key.
|
|
890
778
|
|
|
891
|
-
### 6.
|
|
779
|
+
### 6. The report updates itself after a commit
|
|
892
780
|
|
|
893
781
|
```bash
|
|
894
|
-
pnpm exec size install-hook #
|
|
895
|
-
pnpm exec size uninstall-hook #
|
|
782
|
+
pnpm exec size install-hook # install post-commit and post-merge
|
|
783
|
+
pnpm exec size uninstall-hook # remove them and return the project to its previous behaviour
|
|
896
784
|
```
|
|
897
785
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
`
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
`
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
(
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
### 8.
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
1.
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
2.
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
`
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
- `
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
- `size
|
|
1057
|
-
|
|
1058
|
-
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
код 2 и готовая команда.
|
|
1078
|
-
|
|
1079
|
-
## Ловушки, на которых стоит проверять движок
|
|
1080
|
-
|
|
1081
|
-
Фикстура (`fixtures/synthetic/history.bundle`) — это история, в которой
|
|
1082
|
-
собрано то, на чём ломаются такие инструменты: `//` внутри строки, регексп с
|
|
1083
|
-
экранированным слэшем, шаблон с выражением, `.mjs` с `export`, не-английское имя
|
|
1084
|
-
файла, CRLF, переименование файла, коммит «только отчёт», смешанный коммит,
|
|
1085
|
-
слияние с правкой разрешения конфликта, замена символа без изменения объёма,
|
|
1086
|
-
удаление и возврат файла, пустой файл, незнакомое расширение. Полный список — в
|
|
786
|
+
The hooks install themselves, and that is the only thing a project notices about installing the package:
|
|
787
|
+
after `npm i` by an install script, with pnpm 10 by the tool's first run (pnpm does not run dependency
|
|
788
|
+
scripts — "Ignored build scripts"; it can be allowed with `pnpm.onlyBuiltDependencies:
|
|
789
|
+
["@vernikr/size-report"]` in your manifest). The files land in `.git`, `git status` does not see them, and the
|
|
790
|
+
command above takes them away. It installs only where that is safe — an ordinary hooks directory, no hook of
|
|
791
|
+
someone else's, something to call the tool with — and stays silent where it is not. After every commit and
|
|
792
|
+
merge the report is rebuilt: the `docs` directory and `size-report.html` are created if they are not there
|
|
793
|
+
yet, and a report **tracked** by git lands as a commit of its own signed `chore(report): report rebuilt
|
|
794
|
+
after <sha>` (the signature is quoted as the hook writes it, like every other line of the tool's output in
|
|
795
|
+
this document). Only the report's path is committed: the tree comes from HEAD with that one path replaced, so
|
|
796
|
+
neither someone's index nor uncommitted work can enter the commit.
|
|
797
|
+
|
|
798
|
+
The first report is the exception: while the report is untracked the hook rebuilds it and says so in words
|
|
799
|
+
instead of committing — adding a new file to someone else's history is a person's decision. One `git add
|
|
800
|
+
docs/size-report.html` (or a plain `git add -A` if the report belongs in the project) and from then on it
|
|
801
|
+
travels by commits itself. A merge is the same case as an ordinary commit, with one correction to what git
|
|
802
|
+
does: the merge commit is made by git itself and does not run `post-commit`, hence the second file,
|
|
803
|
+
`post-merge` (checked on git 2.50).
|
|
804
|
+
|
|
805
|
+
There is no looping, and by construction rather than by a flag: the report's commit is assembled with
|
|
806
|
+
plumbing (`commit-tree` calls no hooks at all), and the report itself gets no row, so the same rebuild yields
|
|
807
|
+
the same bytes. A refusal by the tool does not bring the commit down — the commit has been made already: the
|
|
808
|
+
cause is printed as one line and remembered, and `pnpm exec size doctor` shows what the hook did and how the
|
|
809
|
+
last run ended. The automation is switched off in two ways — `"hooks": {"enabled": false}` in the settings
|
|
810
|
+
(the hook stays but keeps quiet) or `size uninstall-hook` — while in an environment where updating is not
|
|
811
|
+
wanted at all (CI, someone else's machine) the hook keeps quiet by itself: the hook file lies in `.git`
|
|
812
|
+
rather than in git, so every clone has one of its own, and the body checks whether there is anything to call
|
|
813
|
+
the tool with. `SIZE_REPORT_NO_HOOK` is the lever for one who would rather not edit the settings.
|
|
814
|
+
|
|
815
|
+
### 7. Traps found by this very instruction
|
|
816
|
+
|
|
817
|
+
Two of them were found by the walkthrough and are closed already — they are kept here as an explanation of
|
|
818
|
+
behaviour rather than as workarounds:
|
|
819
|
+
|
|
820
|
+
- **A module in a `.js` extension** (`import`/`export` in `.js` is ordinary in projects with a bundler) is
|
|
821
|
+
measured like any other file, with `type: module` in the manifest or without it: the guard parses the
|
|
822
|
+
result both as a script and as a module. It used to try the script alone and fell with code 5 on the
|
|
823
|
+
`export` itself, blaming the stripper; that is impossible today and no settings need editing
|
|
824
|
+
(`REFACTOR.md` R-4.6).
|
|
825
|
+
- **Not JavaScript in a column** (markup or types straight in `.js`) is code 2 and a refusal naming the
|
|
826
|
+
reason and what to fix. The reason comes from the way the file was counted: with `minify.engine:
|
|
827
|
+
"esbuild"` the refusal names the minifier and its **one** way out (a simplification for that extension in
|
|
828
|
+
`minify.ext` — the `strip` way would hand the same file to the guard, whose verdict would be the same),
|
|
829
|
+
while with stripping it is the guard's refusal and **two** ways out (take the extension out of
|
|
830
|
+
`minify.guard`, or set `minify.ext`). Neither looks like a stack.
|
|
831
|
+
- **No minifier** (an installation without the optional dependencies, a platform without `esbuild`) — the
|
|
832
|
+
metric honestly falls back to stripping: the numbers are the same as `strip`, the label says so in words,
|
|
833
|
+
and a **build** (`--write`) returns **code 4** with a ready fix. A **check** answers in two parts in that
|
|
834
|
+
case, and it is named here because it is what CI advises: if the report on disk was built with the real
|
|
835
|
+
minifier while the run goes without it, the accuracy has changed — the numbers in the table no longer
|
|
836
|
+
agree with the history, so the check says as much (**code 1**), showing the diverged signature row and
|
|
837
|
+
**naming the other count right there** in a note with a ready fix. The verdict stays with the divergence:
|
|
838
|
+
code 4 would claim the difference is explained by the sensor, and nobody checked that — the divergence
|
|
839
|
+
may also be an edit that went past the report (the same order as `size check` and `doctor`: a mismatch
|
|
840
|
+
outranks an approximation). The fix in both cases is `pnpm run sizes`; on this environment it returns
|
|
841
|
+
**code 4**. This can be checked without reinstalling by the `SIZE_REPORT_NO_OPTIONAL=1` environment — the
|
|
842
|
+
same way `test/minify.test.js` does it.
|
|
843
|
+
- **The module parse is one worker raised once per a run** (`REFACTOR.md` R-5.4): the fallback to
|
|
844
|
+
`node --check` (a Node run per cell) remains for when the worker's file is not in the package, the worker
|
|
845
|
+
does not answer, or the Node build has no vm modules; and the worker is raised only if the measured files
|
|
846
|
+
hold modules at all. The measured price of both is in `REFACTOR.md` R-5.4 rather than promised in numbers
|
|
847
|
+
here.
|
|
848
|
+
- **A new column file has to be committed** before the run: the table is built from commits, so a file git
|
|
849
|
+
does not track has nothing to measure and its column stays empty. The run itself does not complain — the
|
|
850
|
+
file is named by the settings rather than by the project — it is the numbers that would be missing in
|
|
851
|
+
silence. So `git add` + commit first, then `pnpm run sizes`.
|
|
852
|
+
- **An edit to the journal is an edit too.** A commit that touched the journal or any column file gets a row
|
|
853
|
+
in the table, so the table is rebuilt after it — otherwise the check says "diverged from the git history"
|
|
854
|
+
and names the row. An uncommitted edit does not move the table ("now" comes from the commit), so a
|
|
855
|
+
rebuild is not broken by documentation being edited next to it.
|
|
856
|
+
- **`--init` does not edit `.gitignore`** (`REFACTOR.md` R-4.8) — add the report by hand if it has no place
|
|
857
|
+
in the history.
|
|
858
|
+
|
|
859
|
+
Not on words: the section was walked through command by command in a fresh repository, and the findings are
|
|
860
|
+
in `worklog/archive/WORKLOG.md` §16. What keeps it true is the documentation guard (`REFACTOR.md` R-4.1):
|
|
861
|
+
paths, the file table, the calls and flags of the instructions, the numbers of checks, references to sections
|
|
862
|
+
and the install pin are checked by machine. **No time target is declared anywhere** — seconds depend on the
|
|
863
|
+
window, so there is nothing to check against (`tools/suites.js` says why). Wording, meaning and promises
|
|
864
|
+
about the future are not checked by machine; a person holds those.
|
|
865
|
+
|
|
866
|
+
### 8. If a copy of the tool is already in the project
|
|
867
|
+
|
|
868
|
+
The order above is for a project wiring the tool in for the first time. When a copy is already there (its
|
|
869
|
+
own `size-table.js` and its tests), the steps go in another order; below is the one `safe-resets` migrated
|
|
870
|
+
by (`worklog/archive/WORKLOG.md` §18):
|
|
871
|
+
|
|
872
|
+
1. **Install without removing the copy** — two implementations live side by side for a while, and that
|
|
873
|
+
gives a free comparison on one tree: the package's command with the project's config has to assemble the
|
|
874
|
+
same artifact byte for byte (for `safe-resets` — 225 673 B, sha256 `1bdb27e1…`, and both are frozen in
|
|
875
|
+
the parity reference, `fixtures/parity/manifest.json`). No match — do not go further.
|
|
876
|
+
2. **Move the project's commands to the package:** `"test:sizes": "size"`, `"sizes": "size --write"`.
|
|
877
|
+
3. **Remove the copy** — the tool and its test alike: the package's suite checks the same claims, and one
|
|
878
|
+
command stays in the project. If the test was called from a shared runner, the runner's step becomes a
|
|
879
|
+
single one calling the package's command rather than the project's file (in `safe-resets` the path comes
|
|
880
|
+
from the installed package's manifest, so the step knows no internal file names).
|
|
881
|
+
4. **Take the deleted files' columns out of the settings** and rebuild the artifact in a **commit of its
|
|
882
|
+
own**: commits that touched only those files move no number without them, and such commits get no rows.
|
|
883
|
+
5. **Clean the project's documentation:** references to the tool's files are replaced by the package's name
|
|
884
|
+
and its commands, while a description of the internals (the stripper, reading the history in batches,
|
|
885
|
+
the assembly) moves from the project's docs into the package's — otherwise there are two copies and they
|
|
886
|
+
will drift apart.
|
|
887
|
+
|
|
888
|
+
No access to the package is needed either locally or in CI — the repository is public (§1), so there is no
|
|
889
|
+
key step in this order.
|
|
890
|
+
|
|
891
|
+
What is lost: the checks that compared the project's settings with the tool's expectations no longer run as
|
|
892
|
+
a suite of their own. Most of them are covered by the command itself (an unknown flag or an unfamiliar
|
|
893
|
+
metric in the config is a refusal with an explanation; the report file cannot be a column), but the
|
|
894
|
+
_content of the signature_ (the heading and the fix command taken from the config) is checked by nobody: if
|
|
895
|
+
that matters, it is one check on top of `--data` in the project.
|
|
896
|
+
|
|
897
|
+
## The gate against bloat
|
|
898
|
+
|
|
899
|
+
**The list of checks is single, and it is the one CI runs.** The profiles live in one place
|
|
900
|
+
(`tools/gates/run.js`): `pnpm run verify:fast` (tens of seconds — every edit), `pnpm run verify` (the full
|
|
901
|
+
one — before pushing and in CI) and `pnpm run verify:slow` (on a schedule — the same plus the suite with
|
|
902
|
+
no machine git settings and coverage). CI calls that same command rather than a list of its own: the job
|
|
903
|
+
`verify` (`.github/workflows/ci.yml`) on every push and pull request, the job `verify-slow` on a schedule.
|
|
904
|
+
That they agree is guarded by `test/gates-verify.test.js`: a check that is not in a profile cannot pass in CI.
|
|
905
|
+
|
|
906
|
+
**The sensors catch bloat rather than style** (style is the linter's business): the size and complexity of
|
|
907
|
+
functions, the size of modules, duplicated branches and functions (`sonarjs`), the weight of checks (a
|
|
908
|
+
check with no assertion, an assertion with no comparison, a switched-off check), debt markers, token clones
|
|
909
|
+
(`jscpd`), cycles and orphans in the graph (`dependency-cruiser`), and coverage falling against its own
|
|
910
|
+
baseline (`c8`).
|
|
911
|
+
|
|
912
|
+
**A threshold comes from a measurement rather than from a guess, and it is a ratchet.** The thresholds
|
|
913
|
+
today: a function's complexity 12, its length 60, a module 450 lines, cognitive complexity 15 — each of
|
|
914
|
+
them cut in the tail of a measured distribution, not in its middle. **Nothing lies above them**: the
|
|
915
|
+
baseline (`.eslint-suppressions.json`) holds nothing at all, so a new overrun fails the run while the
|
|
916
|
+
tree as it stands needs no excuses. The table behind the thresholds is in
|
|
917
|
+
`worklog/archive/WORKLOG.md` §58.3, and its figures describe the tree of that day rather than this one;
|
|
918
|
+
the sensors print their own numbers on every run.
|
|
919
|
+
|
|
920
|
+
**A person updates the baselines.** `pnpm run baseline:metrics`, `baseline:dup` and `baseline:coverage` —
|
|
921
|
+
and only with the `Gate-Change:` trailer in the commit message: a gate file edited without it is red both
|
|
922
|
+
locally (the `commit-msg` hook) and over a range (the `pre-push` hook, while CI reads no trailers at all).
|
|
923
|
+
Otherwise the gate would be weakened by the very commit it stops. The table of measurements and the
|
|
924
|
+
rejected tools (knip, ast-grep, size-limit, gitleaks) are in `worklog/archive/WORKLOG.md` §58.
|
|
925
|
+
|
|
926
|
+
## For an AI agent
|
|
927
|
+
|
|
928
|
+
- `pnpm run verify:fast` before every edit, `pnpm run verify` before pushing; what is wrong and what
|
|
929
|
+
must not be touched when a sensor is red — `AGENTS.md`.
|
|
930
|
+
- `size check --json` — whether everything is in: how much of the history is covered, which paths went
|
|
931
|
+
past the columns (with the commit that introduced them) and which commits dropped out without a row.
|
|
932
|
+
- `size explain <commit> --json` — why one commit has no row: the reason, the files it touched (columns,
|
|
933
|
+
excluded, untracked) and a ready fix. The commit is named by a revision (`HEAD`, a branch, a tag), by a
|
|
934
|
+
full sha or by its beginning.
|
|
935
|
+
- The data without the markup — the rows, the numbers, the totals — is `--json` (the earlier form, frozen
|
|
936
|
+
byte for byte by the parity reference) and `--data` (the page's contract: absolute values and the shape of
|
|
937
|
+
the table, with nothing derived — whatever the page can count itself is not there). A `size measure`
|
|
938
|
+
command does not exist yet.
|
|
939
|
+
- `--json` is a form of answer rather than a mode of its own, and it has one rule: exactly four calls have
|
|
940
|
+
an answer. With no command it is the earlier form of the data (frozen by the parity reference), and for
|
|
941
|
+
`check`, `explain` and `doctor` it is their answer. For a command with no answer, and next to a mode
|
|
942
|
+
(`--write`, `--data`, `--init`), it is a refusal rather than silence: asking for JSON where there is none
|
|
943
|
+
is an error of the call.
|
|
944
|
+
- `size doctor --json` — all the diagnostics in one answer: the environment, the dependencies, the
|
|
945
|
+
settings, the coverage and findings with their level (`action` — to be done, `note` — to be known).
|
|
946
|
+
- Exit codes: `0` all is well · `1` a mismatch with the history or incomplete coverage · `2` the settings,
|
|
947
|
+
the environment, an unknown or extra word, two modes at once · `3` a shallow history · `4` no sensor ·
|
|
948
|
+
`5` an internal error. They work already: a refusal is a code and one line with a ready fix, with no
|
|
949
|
+
stack. `--help` prints both.
|
|
950
|
+
- There are two runs, and both are named: `pnpm test` is the fast one (every edit), `pnpm test:all` the
|
|
951
|
+
full one (a release and CI); what is in which and why is in `tools/suites.js`, while the numbers and the
|
|
952
|
+
durations are printed by the run itself.
|
|
953
|
+
- Arguments are parsed once, on the way in and before the project is read: either one mode or a refusal
|
|
954
|
+
naming both; a command and a mode do not work together; a flag with no value and a flag named twice are
|
|
955
|
+
such refusals too. So a call the tool did not understand cannot be confused with a healthy run: instead
|
|
956
|
+
of zero comes code 2 and a ready command.
|
|
957
|
+
|
|
958
|
+
## Traps worth testing the engine on
|
|
959
|
+
|
|
960
|
+
The fixture (`fixtures/synthetic/history.bundle`) is a history holding what breaks tools of this kind:
|
|
961
|
+
`//` inside a string, a regexp with an escaped slash, a template with an expression, `.mjs` with `export`,
|
|
962
|
+
a file name that is not English, CRLF, a file renamed, a commit that touched only the report, a mixed
|
|
963
|
+
commit, a merge with a conflict-resolution edit, a character replaced without changing the volume, a file
|
|
964
|
+
deleted and returned, an empty file, an unknown extension. The full list is in
|
|
1087
965
|
`fixtures/synthetic/README.md`.
|
|
1088
966
|
|
|
1089
967
|
```bash
|
|
1090
|
-
pnpm test #
|
|
1091
|
-
#
|
|
1092
|
-
pnpm test:all #
|
|
1093
|
-
#
|
|
1094
|
-
pnpm run suites:measure #
|
|
1095
|
-
pnpm run parity:live #
|
|
1096
|
-
node bin/size.js --data #
|
|
1097
|
-
node bin/size.js --write #
|
|
1098
|
-
node bin/size.js --help #
|
|
1099
|
-
pnpm run parity #
|
|
1100
|
-
pnpm run fixture #
|
|
1101
|
-
pnpm run pack:check #
|
|
1102
|
-
pnpm run check:standards #
|
|
968
|
+
pnpm test # the fast run (every edit): parity on the fixture,
|
|
969
|
+
# the data contract and the page, the documentation and release guards
|
|
970
|
+
pnpm test:all # the full run (a release and CI): the same plus the integration ones —
|
|
971
|
+
# assembling on disk, the comparison with the tree, the hooks, the sensors
|
|
972
|
+
pnpm run suites:measure # measure every file of the suite
|
|
973
|
+
pnpm run parity:live # parity with the live project on a clone, two environments
|
|
974
|
+
node bin/size.js --data # the data contract: the report and an agent
|
|
975
|
+
node bin/size.js --write # the smallest report
|
|
976
|
+
node bin/size.js --help # the help and the exit codes
|
|
977
|
+
pnpm run parity # re-take the parity reference: the project and the revision from the manifest
|
|
978
|
+
pnpm run fixture # rebuild the fixture and its reference
|
|
979
|
+
pnpm run pack:check # does the engine work from the assembled tarball
|
|
980
|
+
pnpm run check:standards # both references reproduce and the tree stays clean
|
|
1103
981
|
git clone fixtures/synthetic/history.bundle /tmp/size-report-fixture
|
|
1104
982
|
```
|
|
1105
983
|
|
|
1106
|
-
|
|
1107
|
-
|
|
984
|
+
Open blockers and known gaps are in `BLOCKERS.md`, and next to them the note about the settings that were
|
|
985
|
+
checked and turned out inert, so as not to check them again.
|