@vernikr/size-report 1.3.1 → 2.0.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/CHANGELOG.md +46 -0
- package/README.md +38 -36
- package/bin/postinstall.js +51 -0
- package/package.json +4 -3
- package/src/args.js +4 -4
- package/src/artifact.js +25 -16
- package/src/cli.js +16 -4
- package/src/config.js +5 -4
- package/src/css.js +7 -16
- package/src/hook.js +42 -4
- package/src/locales.js +5 -4
- package/src/modes.js +25 -34
- package/src/page/build.js +23 -4
- package/src/project.js +4 -1
- package/src/refusal.js +8 -8
- package/src/size-table.js +11 -9
- package/templates/README.md +6 -1
- package/templates/ci.yml +6 -5
- package/templates/size-report.config.json +1 -1
- package/src/artifact.css +0 -8
- package/src/render.js +0 -139
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,52 @@
|
|
|
18
18
|
где за основу взяты `fixtures/synthetic/config.json`, метрики — `raw`, `min`, `tok`, а
|
|
19
19
|
способ минификации — `strip` или `esbuild`.
|
|
20
20
|
|
|
21
|
+
## 2.0.0 — 2026-09-15
|
|
22
|
+
|
|
23
|
+
Один отчёт вместо двух, и он же появляется сам: форма отчёта сведена к одному
|
|
24
|
+
файлу, а обновление — к одному решению (хук ставится сам).
|
|
25
|
+
|
|
26
|
+
- **Отчёт — один файл: самодостаточная страница `size-report.html`.** Данные,
|
|
27
|
+
оформление и программа лежат в нём же; внешних ссылок нет. Прежние две формы
|
|
28
|
+
(статическая таблица и рядом страница) убраны вместе с их кодом (`src/render.js`,
|
|
29
|
+
`src/artifact.css`): два вывода одной истории расходились бы молча, а выбрать,
|
|
30
|
+
какой верный, было бы нечем. Колонка движка — это файл, значит и отчёт — файл.
|
|
31
|
+
- **Запись одна: `--write [файл]`** (прежняя `--page` убрана — это ломающее
|
|
32
|
+
изменение). Значение ключа пишет отчёт по названному пути (каталог создаётся сам)
|
|
33
|
+
и становится его же `output`: отчёт называет себя тем путём, по которому лежит.
|
|
34
|
+
- **По умолчанию — `docs/size-report.html`**, если каталог `docs/` в проекте есть,
|
|
35
|
+
иначе в корне. Имя больше не выводится из настроек: файл это и есть отчёт.
|
|
36
|
+
- **Хук ставится сам** — после постановки пакета (`bin/postinstall.js`) и при первом
|
|
37
|
+
запуске в проекте. Поэтому `docs/` с отчётом появляется первым же коммитом, без
|
|
38
|
+
ручного шага; ставится там, где это безопасно (обычный `.git/hooks`, нет чужого
|
|
39
|
+
хука, есть чем звать инструмент, не CI), и там же молчит, где нельзя.
|
|
40
|
+
- Коммит отчёта виден в хуке как раньше: отдельным коммитом, только этим путём.
|
|
41
|
+
Отчёт остаётся **неподвижной точкой** — список пропущенных коммитов в файл не
|
|
42
|
+
идёт (он меняется от коммита самого отчёта), и хук не коммитит его бесконечно;
|
|
43
|
+
читателю этот список по-прежнему доступен: `--data`, `--json`, `size explain`.
|
|
44
|
+
|
|
45
|
+
### Что изменится в числах
|
|
46
|
+
|
|
47
|
+
**Ничего.** Измерение не тронуто: те же датчики, те же способы, те же колонки. Что
|
|
48
|
+
меняется у потребителя — **форма файла и его путь**: `size` (проверка) на прежнем
|
|
49
|
+
файле будет красным, пока отчёт не пересобран, а скрипты, звавшие `--page`,
|
|
50
|
+
получат отказ с названным ключом. Таблица ниже та же, что у 1.3.1, до последней
|
|
51
|
+
клетки.
|
|
52
|
+
|
|
53
|
+
| Файл | raw | min со strip | min с esbuild | tok |
|
|
54
|
+
|---|---|---|---|---|
|
|
55
|
+
| code.js | 735 | 276 | 185 | 168 |
|
|
56
|
+
| modern.js | 246 | 51 | 45 | 44 |
|
|
57
|
+
| config.mjs | 172 | 45 | 40 | 40 |
|
|
58
|
+
| заметки.md | 306 | 303 | 303 | 53 |
|
|
59
|
+
| crlf.txt | 63 | 60 | 60 | 10 |
|
|
60
|
+
| package.json | 87 | 69 | 69 | 34 |
|
|
61
|
+
| style.css | 156 | 55 | 43 | 38 |
|
|
62
|
+
| table.toml | 300 | 299 | 299 | 52 |
|
|
63
|
+
| empty.js | 0 | 0 | 0 | 0 |
|
|
64
|
+
| WORKLOG.md | 446 | 439 | 439 | 101 |
|
|
65
|
+
| **ИТОГО** | **2511** | **1597** | **1483** | **540** |
|
|
66
|
+
|
|
21
67
|
## 1.3.1 — 2026-09-15
|
|
22
68
|
|
|
23
69
|
Правка раскладки страницы: на широком экране панель выбора встаёт слева от таблицы.
|
package/README.md
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
## Статус
|
|
12
12
|
|
|
13
|
-
**Выпуск
|
|
13
|
+
**Выпуск 2.0.0 (2026-09-15).** Инструмент живёт отдельным пакетом: имя в
|
|
14
14
|
реестре — `@vernikr/size-report` (публикуется тегом из CI, без секрета). Настроек
|
|
15
15
|
проект может не заводить вовсе: без файла инструмент выводит их из самого проекта и
|
|
16
16
|
говорит об этом строкой, а `--init` закрепляет выведенное файлом (чем этот шаг
|
|
17
|
-
отличается от прежнего — `CHANGELOG.md` 1.3.0).
|
|
17
|
+
отличается от прежнего — `CHANGELOG.md` 1.3.0). Отчёт — **один файл**,
|
|
18
|
+
самодостаточная страница `docs/size-report.html`, и он появляется сам: хук обновления
|
|
19
|
+
ставится после установки пакета и при первом запуске (чем это отличается от двух
|
|
20
|
+
файлов прежних выпусков — `CHANGELOG.md` 2.0.0).
|
|
18
21
|
Версия — в манифесте, а у выпуска есть `CHANGELOG.md` с разделом «Что изменится
|
|
19
22
|
в числах»:
|
|
20
23
|
таблица чисел в нём не пересказ, а замер на фикстуре, который сверяется с живым
|
|
@@ -67,7 +70,7 @@
|
|
|
67
70
|
|
|
68
71
|
**Шаг 2 начат первым срезом — контрактом данных.** Движок отдаёт абсолютные
|
|
69
72
|
значения и устройство таблицы (`--data`), а дельты, суммы, «сейчас» и фильтры
|
|
70
|
-
считает страница (
|
|
73
|
+
считает страница (она же и есть отчёт — `size-report.html`): без этого
|
|
71
74
|
фильтры и «итого по выбору» невозможны в принципе. В контракте едет и точность
|
|
72
75
|
числа — рядом пометок `approx` по клеткам, — потому что это факт замера, а не
|
|
73
76
|
вывод: страница показывает то, что сказал движок, и своего правила точности не
|
|
@@ -216,10 +219,10 @@
|
|
|
216
219
|
|
|
217
220
|
| Прогон | Команда | Проверок |
|
|
218
221
|
|---|---|---|
|
|
219
|
-
| Быстрый — каждая правка | `pnpm test` | **65 из
|
|
220
|
-
| Полный — выкладка и CI | `pnpm test:all` | **
|
|
222
|
+
| Быстрый — каждая правка | `pnpm test` | **65 из 168** |
|
|
223
|
+
| Полный — выкладка и CI | `pnpm test:all` | **168** |
|
|
221
224
|
|
|
222
|
-
Ни одна проверка не потеряна и не ослаблена: полный прогон запускает все
|
|
225
|
+
Ни одна проверка не потеряна и не ослаблена: полный прогон запускает все 168 теми же
|
|
223
226
|
файлами, а быстрый берёт их часть. Умолчание — полный: файл становится быстрым только
|
|
224
227
|
явно и с причиной, поэтому новое дорогое не может тихо уехать в быстрый. Стерегут это
|
|
225
228
|
объявление `test/suites.test.js` (полнота классификации и причина у каждого файла) и
|
|
@@ -432,8 +435,8 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
432
435
|
профиль ведёт новые проекты сразу на токены.
|
|
433
436
|
|
|
434
437
|
**Волна 0 чистки пройдена** (`REFACTOR.md`): у отказов командной строки появились
|
|
435
|
-
коды выхода и справка вместо стека, `--help` отвечает, `--
|
|
436
|
-
|
|
438
|
+
коды выхода и справка вместо стека, `--help` отвечает, `--write`
|
|
439
|
+
создаёт недостающий каталог, подсказка в отказе ведёт к работающей команде, а
|
|
437
440
|
вывод настроек больше не предлагает колонкой саму таблицу — иначе первая же
|
|
438
441
|
проверка настроек его отвергала.
|
|
439
442
|
|
|
@@ -484,8 +487,8 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
484
487
|
| `tools/parity-freeze.js` | Снимает эталон паритета (`pnpm run parity`): замороженной копией, на ревизии проекта из манифеста — `--json`, конфиг, хеш артефакта, хеш инструмента |
|
|
485
488
|
| `tools/make-fixture.js` | Собирает синтетическую фикстуру (`pnpm run fixture`): детерминированную историю с ловушками плюс эталонные числа |
|
|
486
489
|
| `tools/synthetic/` | Сюжеты той сборки по предметам: `repo.js` — как говорим с git (закреплённые время, автор, настройки), `content.js` — что лежит в файлах, `history.js` — какие коммиты из этого получаются, `note.js` — записка к фикстуре со списком ловушек |
|
|
487
|
-
| `tools/parity-live.js` | Сверяет движок с живым проектом на клоне: числа и
|
|
488
|
-
| `tools/pack-check.js` | Собирает тарболл и проверяет, что из него всё работает: все исходники доехали,
|
|
490
|
+
| `tools/parity-live.js` | Сверяет движок с живым проектом на клоне: числа и самодостаточный отчёт по пути из настроек потребителя (`pnpm run parity:live`) |
|
|
491
|
+
| `tools/pack-check.js` | Собирает тарболл и проверяет, что из него всё работает: все исходники доехали, числа и отчёт — как из репозитория (`pnpm run pack:check`) |
|
|
489
492
|
| `tools/check-standards.js` | Проверяет, что оба эталона воспроизводятся: пересъём идёт в никуда и сверяется с закоммиченным (наши файлы — побайтово, бандл — по содержимому) и что бандл живой истории несёт `HEAD` (`pnpm run check:standards`) |
|
|
490
493
|
| `.github/workflows/ci.yml` | CI: работа `verify` на каждый пуш и запрос правки зовёт `pnpm run verify` — тот же профиль, что локально; действия закреплены по SHA коммита |
|
|
491
494
|
| `.github/workflows/verify-slow.yml` | Slow-профиль по расписанию: то же плюс покрытие под c8 — дорогое не в каждом прогоне |
|
|
@@ -497,6 +500,7 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
497
500
|
| `tools/gates/gatefiles.js` | Защита гейт-файлов: правка порогов, баз и обвязки без трейлера `Gate-Change:` — красный (хук `commit-msg` и CI по диапазону) |
|
|
498
501
|
| `tools/gates/common.js`, `tools/gate-probe.js` | Общее у датчиков (корень, разбор ключей, отчёты) и обвязка их проб: датчик зовётся командой, а не импортом |
|
|
499
502
|
| `.githooks/commit-msg`, `.githooks/pre-commit`, `.githooks/pre-push` | Хуки: защита гейт-файлов, быстрый профиль на правку и перед отправкой; ставятся `pnpm run hooks:install` (свой менеджер хуков не заводится) |
|
|
503
|
+
| `.githooks/post-commit` | Обновление отчёта после коммита: зов установленной копии пакета (строка вписана человеком — инструмент чужие каталоги хуков не правит) |
|
|
500
504
|
| `eslint.metrics.config.js`, `.eslint-suppressions.json` | Правила датчика раздувания и его база: пороги из замеров, всё, что выше, — в базе и разбирается постепенно |
|
|
501
505
|
| `.jscpd.json`, `dup-baseline.json` | Настройки и база датчика дублей: отпечаток считается по содержимому клона, поэтому база переносима |
|
|
502
506
|
| `.dependency-cruiser.cjs`, `.c8rc.json`, `coverage-baseline.json` | Правила графа связей, настройки снятия покрытия и его база по файлам |
|
|
@@ -504,16 +508,15 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
504
508
|
| `.github/workflows/release.yml` | Выпуск по тегу: тот же полный набор, сверка версии манифеста с тегом и публикация в реестр по удостоверению GitHub Actions — без секрета и без кода из аутентификатора |
|
|
505
509
|
| `templates/` | То, что проект берёт как есть: `size-report.config.json` (черновик настроек), `ci.yml` (описание проверки) и `README.md` (куда что кладётся и что в них менять); едут в поставке и стерегутся `pack:check` и `test/templates.test.js` |
|
|
506
510
|
| `fixtures/parity/` | Эталон с `safe-resets` на коммите `bd6ef9d`: 95 строк × 27 колонок. Копия реализации, которой он снят, в дереве не лежит — её байты живут в истории и берутся оттуда по требованию (`REFACTOR.md` R-1.5) |
|
|
507
|
-
| `fixtures/synthetic/` | Бандл фикстуры на 16 коммитов, её конфиг, эталонные числа и хеш артефакта |
|
|
511
|
+
| `fixtures/synthetic/` | Бандл фикстуры на 16 коммитов, её конфиг, эталонные числа (`--json` прежней копии) и хеш её артефакта прежней формы — запись того, с чем сверялся перенос |
|
|
508
512
|
| `fixtures/live/history.bundle`, `fixtures/live/README.md` | История проекта-потребителя на ревизии эталона `bd6ef9d` и записка о том, какую ревизию бандл несёт и почему он лежит в репозитории: живая сверка работает без доступа к приватному проекту |
|
|
509
513
|
| `bin/size.js` | Команда `size`: то, что ставит пакет (`package.json` → `bin`); сама ничего не считает, только зовёт точку входа |
|
|
510
514
|
| `LICENSE` | MIT: условия лицензии едут в поставке вместе с пакетом |
|
|
511
515
|
| `.gitignore`, `pnpm-lock.yaml` | Что в репозиторий не идёт; lock-файл pnpm, а версия менеджера — в поле `packageManager` (оттуда её берёт CI) |
|
|
512
|
-
| `src/size-table.js` | Точка входа пакета: только реэкспорт публичного API (
|
|
513
|
-
| `src/derived.js` | Общий расчёт отчёта: итоги, дельты, клетка, подпись коммита —
|
|
516
|
+
| `src/size-table.js` | Точка входа пакета: только реэкспорт публичного API (55 имён), ни одного расчёта |
|
|
517
|
+
| `src/derived.js` | Общий расчёт отчёта: итоги, дельты, клетка, подпись коммита — один на движок и программу страницы |
|
|
514
518
|
| `src/css.js` | Чтение оформления с диска: какие наборы стилей есть и какая у них роль |
|
|
515
|
-
| `src/table.css` |
|
|
516
|
-
| `src/artifact.css` | Оформление статического артефакта сверх общей части |
|
|
519
|
+
| `src/table.css` | Таблица отчёта: геометрия клеток, липкие шапка и колонка, цвет дельт |
|
|
517
520
|
| `src/page/app.css` | Оформление страницы сверх общей части: панель с деревом файлов (на широком экране — колонка слева от таблицы), состояния пустоты, узкое окно |
|
|
518
521
|
| `src/page/state.js` | Состояние страницы: данные отчёта, вид галочек, паспорт записи, память браузера и обмен ссылкой — глава программы страницы |
|
|
519
522
|
| `src/page/dom.js` | Узлы страницы: мелкие помощники разметки (`appEl`, `appBox`) — одни на панель и таблицу |
|
|
@@ -536,17 +539,17 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
536
539
|
| `src/check.js` | Полнота покрытия (`size check`): настройки, история, пути, датчики — что прошло мимо колонок и чем это чинится |
|
|
537
540
|
| `src/explain.js` | Объяснение пропущенной строки (`size explain <коммит>`): причина, улики и готовая починка |
|
|
538
541
|
| `src/doctor.js` | Диагностика одним ответом (`size doctor`): окружение, зависимости, настройки, покрытие, состояние хука — сборкой из существующих кусков |
|
|
539
|
-
| `src/hook.js` | Хуки
|
|
540
|
-
| `
|
|
542
|
+
| `src/hook.js` | Хуки автообновления: постановка сама (`autoInstall` — из входа и `bin/postinstall.js`), снятие командой, коммит только отчёта, замок и запись о запуске |
|
|
543
|
+
| `bin/postinstall.js` | Установка хука после постановки пакета: ищет проект-потребитель и молчит, если поставить негде |
|
|
544
|
+
| `src/artifact.js` | Отчёт на диске: единственное место, где он превращается в файл (им пользуются и `--write`, и хук); отчёт — самодостаточная страница |
|
|
541
545
|
| `src/journal.js` | Журнал и ссылки: к какому разделу относится коммит и куда ведёт описание |
|
|
542
546
|
| `src/data.js` | Категории файлов и контракт со страницей (`--data`) |
|
|
543
|
-
| `src/render.js` | Статический артефакт: клетки, таблица, примечание (стили — в `src/css.js`) |
|
|
544
547
|
| `src/config.js` | Настройки проекта-потребителя: умолчания, чтение, проверка |
|
|
545
548
|
| `src/project.js` | Настройки, выведенные из самого проекта (дерево и история): колонки, журнал, исключения. Без файла настроек он и есть настройки; `--init` закрепляет его файлом |
|
|
546
549
|
| `src/locales.js`, `src/refusal.js`, `src/tool.js` | Тексты отчёта; коды выхода и справка; имя и версия пакета |
|
|
547
550
|
| `src/cli.js` | Вход инструмента: разбор строки, чтение проекта и доставка запроса режиму; главный файл пакета |
|
|
548
551
|
| `src/args.js` | Грамматика командной строки: режимы, ключи и команды плюс проверки их сочетаний — отказ называет виновника и готовую команду |
|
|
549
|
-
| `src/modes.js` | Режимы: собрать
|
|
552
|
+
| `src/modes.js` | Режимы: собрать отчёт, сверить его с историей, отдать данные, полноту покрытия и диагностику |
|
|
550
553
|
| `src/init.js` | Закрепление настроек файлом (`--init`): то, что проект вывел о себе сам, ложится файлом — и проходит ту же проверку, что первый запуск |
|
|
551
554
|
| `test/api.test.js` | Публичный API пакета: список имён заморожен, разбиение не имеет права его менять |
|
|
552
555
|
| `eslint.config.js` | Правила оформления: те же, что у проекта-потребителя, плюс запрет склейки операторов в строке (`pnpm run lint`, `pnpm run lint:strict`) |
|
|
@@ -557,7 +560,7 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
557
560
|
| `tools/docs-facts.js` | Чтение фактов из документации — один слой на четыре проверки сторожа: что документ называет (пути, зовы, адреса разделов) против того, что есть в репозитории |
|
|
558
561
|
| `tools/yaml.js` | Разбор подмножества YAML — один разборщик на два сторожа описаний (`templates/ci.yml` и `.github/workflows/release.yml`): вне подмножества — ошибка, а не молча пропущенная строка, включая двоеточие с пробелом в незакавыченном значении (именно оно делало описание выпуска неразбираемым, пока проверка искала подстроки) |
|
|
559
562
|
| `tools/refusals.js` | Каталог отказов: по строке на каждый — причина, код выхода, обязательные фразы вывода, **что отказ советует** (`advice`: `run` — команда, `template` — форма с подстановкой, `manual` — действие человека с причиной, `coveredBy` — отдан другой проверке), а для непроверяемого — почему; карты мест отказа (`SITES`, `PRINTED`) держат числа, чтобы новый отказ не появился молча, а маркеры совета — чтобы не появился молча новый совет |
|
|
560
|
-
| `test/parity.test.js` | Паритет движка с эталоном: числа,
|
|
563
|
+
| `test/parity.test.js` | Паритет движка с эталоном: числа, самодостаточность отчёта, локаль |
|
|
561
564
|
| `test/frozen.test.js` | Замороженная копия: та ли это ревизия, с которой снят эталон, и воспроизводит ли она его |
|
|
562
565
|
| `test/environment.test.js` | Герметичность: вывод не зависит от настроек git машины и локали |
|
|
563
566
|
| `test/crlf.test.js` | Выкладка с CRLF (`core.autocrlf`) не мешает сверке |
|
|
@@ -639,7 +642,7 @@ pnpm add -D @vernikr/size-report
|
|
|
639
642
|
```
|
|
640
643
|
|
|
641
644
|
Пакет **опубликован в реестре**, и публично: `npm view @vernikr/size-report
|
|
642
|
-
version` отвечает `
|
|
645
|
+
version` отвечает `2.0.0`, `npm access get status @vernikr/size-report` — `public`,
|
|
643
646
|
а анонимный запрос тарболла — код 200. `npm i -D` и `yarn add -D` принимают то же
|
|
644
647
|
имя; ни ключа, ни ссылки на репозиторий не нужно.
|
|
645
648
|
|
|
@@ -647,11 +650,11 @@ version` отвечает `1.3.1`, `npm access get status @vernikr/size-report`
|
|
|
647
650
|
реестра, но остаётся привязанной к ревизии:
|
|
648
651
|
|
|
649
652
|
```bash
|
|
650
|
-
pnpm add -D github:vernikr/size-report#
|
|
653
|
+
pnpm add -D github:vernikr/size-report#v2.0.0
|
|
651
654
|
```
|
|
652
655
|
|
|
653
656
|
Без сети (или если тянуть из codeload нечем) — тарболл: `pnpm pack` в клоне
|
|
654
|
-
пакета, затем `pnpm add -D ./vernikr-size-report-
|
|
657
|
+
пакета, затем `pnpm add -D ./vernikr-size-report-2.0.0.tgz`.
|
|
655
658
|
|
|
656
659
|
**Почему тег, а не sha.** Короткий sha pnpm разрешает только через видимые рефы, а
|
|
657
660
|
`git ls-remote` отдаёт одни верхушки веток: пока ревизия — верхушка, короткий sha
|
|
@@ -659,7 +662,7 @@ pnpm add -D github:vernikr/size-report#v1.3.1
|
|
|
659
662
|
<sha> to a commit`. Это не рассуждение, а проба: короткий пин `6530237` ставился,
|
|
660
663
|
пока `main` стоял на нём, и перестал — на следующем же коммите, а тот же sha
|
|
661
664
|
целиком поставился. Имя ветки (`#main`) или тег принимаются оба, но ветка —
|
|
662
|
-
движущаяся цель, а тег постоянен: этот выпуск стоит на теге `
|
|
665
|
+
движущаяся цель, а тег постоянен: этот выпуск стоит на теге `v2.0.0`, он же и в
|
|
663
666
|
примере (сорок знаков тоже годятся, но их придётся брать глазами из истории).
|
|
664
667
|
|
|
665
668
|
Ревизия в примере — не украшение, а часть утверждения: она закреплена за тем, что
|
|
@@ -719,7 +722,7 @@ stderr и называет команду, которая их закрепля
|
|
|
719
722
|
проекту молча.
|
|
720
723
|
|
|
721
724
|
> Subкоманды `size init` пока нет — CLI знает только флаги (`--init`, `--write`,
|
|
722
|
-
> `--
|
|
725
|
+
> `--data`, `--json`, без флага — проверка); полный список даёт `size --help`.
|
|
723
726
|
> Subкоманды — шаг 5 плана (`REFACTOR.md` R-4.5).
|
|
724
727
|
|
|
725
728
|
### 3. Что правится в конфиге
|
|
@@ -733,14 +736,14 @@ stderr и называет команду, которая их закрепля
|
|
|
733
736
|
| `metrics` | из чего состоит число: `raw` (размер объекта git), `min` (минифицированная форма — какая именно, решает `minify.engine`), `tok` (токены), `gzip` |
|
|
734
737
|
| `tokens.family`, `tokens.encoding` | словарь для `tok`: семейство (`openai`) и кодировка (`o200k_base` или `cl100k_base`) — кодировка меняет число, поэтому она и в настройках, и в подписи метрики |
|
|
735
738
|
| `minify.engine` | чем считается `min`: `strip` (комментарии и отступы, точность не обещается) или `esbuild` (настоящее сжатие; форматы без минификатора — упрощение, и это видно в подписи метрики) |
|
|
736
|
-
| `output` | файл
|
|
739
|
+
| `output` | файл отчёта (в выведенном профиле — `docs/size-report.html`, если каталог `docs/` есть, иначе в корне; имя отчёта — его имя, а каталог решает только где ему лежать) |
|
|
737
740
|
| `journal` | где искать разделы журнала, на которые ссылаются строки |
|
|
738
741
|
| `links.commitUrl` | шаблон ссылки на коммит, например `https://github.com/org/repo/commit/{sha}`; выводится из адреса `origin` у GitHub и GitLab (у остальных хозяев — пусто, а не догадка) |
|
|
739
742
|
| `skip` | пути, которые колонкой не стали: и те, что ею быть не могут (сам отчёт, замки зависимостей), и те, что в колонки не поместились (выведенный профиль объявляет исключениями всё остальное — поэтому первый `check` полон) |
|
|
740
743
|
| `fixCommand` | команда, которую цитирует подпись отчёта и подсказывает отказ; в выведенном профиле — ваш скрипт `sizes`, если он объявлен, иначе путь к установленному пакету внутри проекта (зов по имени пакета уходит в реестр — `REFACTOR.md` R-4.21) |
|
|
741
744
|
| `locale`, `title`, `heading` | язык текстов отчёта и его заголовки; пустые `title`/`heading` значат «взять из локали» |
|
|
742
745
|
| `minify.guard` | расширения, где результат стриппера проверяется разбором; модуль в `.js` гард понимает сам, трогать его не нужно |
|
|
743
|
-
| `hooks.enabled` | выключатель хука автообновления (`false` — хук
|
|
746
|
+
| `hooks.enabled` | выключатель хука автообновления (`false` — хук не ставится сам и молчит, если уже стоит; убирается он только `size uninstall-hook`) |
|
|
744
747
|
|
|
745
748
|
Остальные ключи и умолчания — `src/config.js` (`DEFAULT_CONFIG`).
|
|
746
749
|
|
|
@@ -752,14 +755,13 @@ stderr и называет команду, которая их закрепля
|
|
|
752
755
|
```
|
|
753
756
|
|
|
754
757
|
```bash
|
|
755
|
-
pnpm run sizes # → docs/size-
|
|
756
|
-
pnpm exec size --page # → docs/size-report.html — интерактивная страница
|
|
758
|
+
pnpm run sizes # → docs/size-report.html — отчёт: таблица, фильтры, ссылка
|
|
757
759
|
```
|
|
758
760
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
761
|
+
Отчёт — один самодостаточный файл: открывается двойным щелчком, без сервера и без
|
|
762
|
+
сети (внешних ссылок в нём нет вовсе, данные, оформление и программа вклеены).
|
|
763
|
+
Производные (дельты, итоги, фильтры) считает сама страница — из абсолютных
|
|
764
|
+
значений, которые даёт движок, и тем же кодом, что и его расчёт.
|
|
763
765
|
|
|
764
766
|
**Порядок правок:** код → `pnpm run sizes` → коммит с одной таблицей. Таблица
|
|
765
767
|
обновляется **отдельным коммитом**, потому что строка коммита не может попасть в
|
|
@@ -995,7 +997,7 @@ R-4.1): пути, таблица файлов, зовы и ключи инстр
|
|
|
995
997
|
- `--json` — форма ответа, а не отдельный режим, и правило у него одно: ответ
|
|
996
998
|
бывает ровно у четырёх вызовов. Без команды это прежняя форма данных
|
|
997
999
|
(заморожена эталоном паритета), у `check`, `explain` и `doctor` — их ответ.
|
|
998
|
-
У команды без ответа и рядом с режимом (`--write`, `--data`, `--
|
|
1000
|
+
У команды без ответа и рядом с режимом (`--write`, `--data`, `--init`)
|
|
999
1001
|
он отказ, а не тишина: просить JSON там, где его не бывает, — ошибка вызова.
|
|
1000
1002
|
- `size doctor --json` — вся диагностика одним ответом: окружение, зависимости,
|
|
1001
1003
|
настройки, покрытие и находки с уровнем (`action` — делать, `note` — знать).
|
|
@@ -1030,8 +1032,8 @@ pnpm test:all # полный прогон (выкладка и
|
|
|
1030
1032
|
# сборка на дисках, сверка с деревом, хуки, метрики
|
|
1031
1033
|
pnpm run suites:measure # замерить длительность каждого файла набора
|
|
1032
1034
|
pnpm run parity:live # паритет с живым проектом на клоне, две среды
|
|
1033
|
-
node bin/size.js --data # контракт данных:
|
|
1034
|
-
node bin/size.js --
|
|
1035
|
+
node bin/size.js --data # контракт данных: отчёт и агент
|
|
1036
|
+
node bin/size.js --write # минимальный отчёт
|
|
1035
1037
|
node bin/size.js --help # справка и коды выхода
|
|
1036
1038
|
pnpm run parity # переснять эталон паритета: проект и ревизия — из манифеста
|
|
1037
1039
|
pnpm run fixture # пересобрать фикстуру и её эталон
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* Постановка хука после установки пакета: чтобы отчёт обновлялся с первого же
|
|
3
|
+
* коммита, не требуя ни запуска инструмента, ни файла настроек.
|
|
4
|
+
*
|
|
5
|
+
* Здесь только поиск проекта-потребителя: сам хук ставит `autoInstall`
|
|
6
|
+
* (`src/hook.js`) — то же место, что и при первом запуске, иначе «поставлено при
|
|
7
|
+
* установке» и «поставлено при запуске» могли бы разойтись содержимым файла.
|
|
8
|
+
*
|
|
9
|
+
* Код выхода всегда 0: установка зависимостей не должна падать из-за того, что
|
|
10
|
+
* услугу не удалось оказать (нет git, нет прав, чужой хук, CI). Причина не
|
|
11
|
+
* печатается: у фоновой работы нет читателя, а точная причина есть у команды
|
|
12
|
+
* `install-hook`.
|
|
13
|
+
*
|
|
14
|
+
* Отдельная тонкость: платформы, где скрипты зависимостей по умолчанию не
|
|
15
|
+
* исполняются (pnpm 10, yarn berry), зовут этот файл не всегда — тогда хук
|
|
16
|
+
* ставится при первом запуске инструмента в проекте. Оба пути ведут в одно место. */
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { autoInstall } from '../src/hook.js';
|
|
21
|
+
|
|
22
|
+
/* Каталог проекта-потребителя ищется в порядке убывания точности: `INIT_CWD`
|
|
23
|
+
* (его ставят npm и pnpm, запуская скрипт пакета), `npm_config_local_prefix`,
|
|
24
|
+
* затем подъём от текущего каталога вверх до ближайшего `.git`. Подъём нужен,
|
|
25
|
+
* потому что сам скрипт исполняется из `node_modules`, где репозитория нет. */
|
|
26
|
+
function projectRoot() {
|
|
27
|
+
const candidates = [process.env.INIT_CWD, process.env.npm_config_local_prefix, process.cwd()];
|
|
28
|
+
for (const start of candidates) {
|
|
29
|
+
const found = gitRootOf(start);
|
|
30
|
+
if (found !== null) return found;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function gitRootOf(start) {
|
|
36
|
+
if (typeof start !== 'string' || start === '') return null;
|
|
37
|
+
let dir = path.resolve(start);
|
|
38
|
+
for (;;) {
|
|
39
|
+
if (fs.existsSync(path.join(dir, '.git'))) return dir;
|
|
40
|
+
const up = path.dirname(dir);
|
|
41
|
+
if (up === dir) return null;
|
|
42
|
+
dir = up;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const root = projectRoot();
|
|
47
|
+
const files = root === null ? null : autoInstall(root, null);
|
|
48
|
+
if (files !== null && process.env.SIZE_REPORT_QUIET !== '1') {
|
|
49
|
+
console.error('· size-report: хук поставлен (' + files.join(', ') + ') — отчёт обновляется после'
|
|
50
|
+
+ ' каждого коммита; снять: size uninstall-hook');
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vernikr/size-report",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"author": "vernikr",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/vernikr/size-report.git"
|
|
8
8
|
},
|
|
9
|
-
"description": "Учёт роста объёма кода и документов по истории git: raw / min /
|
|
9
|
+
"description": "Учёт роста объёма кода и документов по истории git: raw / min / токены; отчёт — один самодостаточный файл, обновляется хуком сам",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"packageManager": "pnpm@10.6.1",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
],
|
|
33
33
|
"sideEffects": false,
|
|
34
34
|
"scripts": {
|
|
35
|
+
"postinstall": "node bin/postinstall.js",
|
|
35
36
|
"test": "node tools/run-tests.js fast",
|
|
36
37
|
"test:all": "node tools/run-tests.js full",
|
|
37
38
|
"suites:measure": "node tools/run-tests.js measure",
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
"report"
|
|
66
67
|
],
|
|
67
68
|
"devDependencies": {
|
|
68
|
-
"@vernikr/size-report": "1.3.
|
|
69
|
+
"@vernikr/size-report": "1.3.1",
|
|
69
70
|
"c8": "10",
|
|
70
71
|
"dependency-cruiser": "17",
|
|
71
72
|
"eslint": "^9.18.0",
|
package/src/args.js
CHANGED
|
@@ -21,15 +21,15 @@ import { cliCommand, advicePath, refuseCause } from './refusal.js';
|
|
|
21
21
|
* места и требует проверки на каждое.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
const MODES = ['--init', '--write', '--data'
|
|
25
|
-
const VALUE_FLAGS = ['--config', '--init', '--
|
|
24
|
+
const MODES = ['--init', '--write', '--data'];
|
|
25
|
+
const VALUE_FLAGS = ['--config', '--init', '--write'];
|
|
26
26
|
const FLAGS = ['--help', '-h'].concat(MODES, VALUE_FLAGS, ['--json', '--force']);
|
|
27
27
|
const COMMANDS = ['check', 'explain', 'doctor', 'install-hook', 'uninstall-hook', 'hook-run'];
|
|
28
28
|
const ANSWER_COMMANDS = ['check', 'explain', 'doctor'];
|
|
29
29
|
export const HOOK_COMMANDS = ['install-hook', 'uninstall-hook', 'hook-run'];
|
|
30
30
|
|
|
31
31
|
/* Ключ со значением: забирает следующий аргумент и возвращает, сколько съел. У
|
|
32
|
-
* `--init` и `--
|
|
32
|
+
* `--init` и `--write` пустое значение — законное «по умолчанию», а у `--config`
|
|
33
33
|
* это молчаливый пропуск: настройки были бы взяты не те, что назвал человек. */
|
|
34
34
|
function takeValue(flag, args, i, values) {
|
|
35
35
|
const next = args[i + 1];
|
|
@@ -95,7 +95,7 @@ function checkModes(plan) {
|
|
|
95
95
|
|
|
96
96
|
/* Слово, которого команда не знает. Отдельным вопросом, потому что виновников
|
|
97
97
|
* тут двое: опечатка в команде — и лишнее значение режима, который своё значение
|
|
98
|
-
* уже забрал (у `--init` и `--
|
|
98
|
+
* уже забрал (у `--init` и `--write` оно одно). Оба случая обязаны назвать своего
|
|
99
99
|
* виновника: у `--config` остаток — именно команда, и зов её разбирается как
|
|
100
100
|
* команда, а не как лишнее слово. */
|
|
101
101
|
function checkUnknownWord(plan) {
|
package/src/artifact.js
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { reportData } from './data.js';
|
|
4
|
+
import { pageHtml } from './page/build.js';
|
|
5
5
|
|
|
6
|
-
/*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
/* Отчёт — один файл: самодостаточная страница. Она и есть артефакт, потому что
|
|
7
|
+
* несёт всё сама (данные, оформление, программу), а второй формы того же отчёта не
|
|
8
|
+
* существует: два вывода одной истории разошлись бы молча, и выбрать, какой из них
|
|
9
|
+
* верный, было бы нечем.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Через это место проходят оба потребителя — режим записи (`--write`) и хук после
|
|
12
|
+
* коммита (`src/hook.js`), поэтому «что записано в файл» не может разойтись между
|
|
13
|
+
* ними: в коммит хук кладёт ровно те байты, которые показывает `--write`.
|
|
14
|
+
*
|
|
15
|
+
* Каталог создаётся здесь же: `--write docs/size-report.html` в свежем проекте —
|
|
16
|
+
* обычный запуск, а не ошибка пользователя. Тем же путём пишется черновик настроек
|
|
17
|
+
* (`--init`), поэтому он один на пакет. */
|
|
13
18
|
|
|
14
|
-
/* Запись файла с созданием каталога:
|
|
15
|
-
*
|
|
16
|
-
* страница (`--page`) и черновик настроек (`--init`), поэтому он один на пакет. */
|
|
19
|
+
/* Запись файла с созданием каталога: путь может не существовать ни одной своей
|
|
20
|
+
* частью — это не ошибка того, кто его назвал. */
|
|
17
21
|
export function writeFileEnsured(file, text) {
|
|
18
22
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
19
23
|
fs.writeFileSync(file, text);
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
/* Собранные байты отчёта без записи: они же нужны проверке (`таблица совпадает с
|
|
27
|
+
* историей`), и собирать их вторым способом значило бы сверять не то, что пишется. */
|
|
28
|
+
export function artifact(cfg, root) {
|
|
29
|
+
const data = reportData(cfg, root);
|
|
30
|
+
return { data: data, html: pageHtml(data, cfg), file: path.join(root, cfg.output) };
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
export function rebuild(cfg, root) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
writeFileEnsured(file, html);
|
|
27
|
-
return { rows: rows, dropped: dropped, state: state, html: html, file: file };
|
|
34
|
+
const out = artifact(cfg, root);
|
|
35
|
+
writeFileEnsured(out.file, out.html);
|
|
36
|
+
return out;
|
|
28
37
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { EXIT, Refusal, USAGE } from './refusal.js';
|
|
2
|
+
import { EXIT, Refusal, USAGE, cliCommand } from './refusal.js';
|
|
3
3
|
import { CONFIG_NAME, gitRoot, loadConfig } from './config.js';
|
|
4
4
|
import { HOOK_COMMANDS, parseArgs } from './args.js';
|
|
5
5
|
import { initMode } from './init.js';
|
|
6
6
|
import { derivedLines } from './project.js';
|
|
7
|
+
import { autoInstall } from './hook.js';
|
|
7
8
|
import {
|
|
8
|
-
checkMode, coverageMode, dataMode, doctorMode, explainMode, hookMode, jsonMode,
|
|
9
|
+
checkMode, coverageMode, dataMode, doctorMode, explainMode, hookMode, jsonMode, writeMode
|
|
9
10
|
} from './modes.js';
|
|
10
11
|
|
|
11
12
|
/* Вход инструмента: разбор строки, чтение проекта и доставка запроса режиму.
|
|
@@ -28,13 +29,23 @@ const RUNNERS = {
|
|
|
28
29
|
check: (c, x) => coverageMode(x.cfg, x.root, x.configFile, c.json),
|
|
29
30
|
explain: (c, x) => explainMode(x.cfg, x.root, c.arg[0], c.json),
|
|
30
31
|
'--data': (c, x) => dataMode(x.cfg, x.root),
|
|
31
|
-
'--
|
|
32
|
-
'--write': (c, x) => writeMode(x.cfg, x.root),
|
|
32
|
+
'--write': (c, x) => writeMode(x.cfg, x.root, c.values['--write']),
|
|
33
33
|
'': (c, x) => (c.json ? jsonMode(x.cfg, x.root) : checkMode(x.cfg, x.root))
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const asked = (cmd) => (cmd.verb === null ? (cmd.mode === null ? '' : cmd.mode) : cmd.verb);
|
|
37
37
|
|
|
38
|
+
/* Постановка хука без спроса — здесь, а не в `doctor` и не в `hook-run`: первый
|
|
39
|
+
* только докладывает, а второй зовётся уже из поставленного хука. Ставится один раз
|
|
40
|
+
* в клоне и называется вслух, дальше молчит: отчёт обновляется после каждого
|
|
41
|
+
* коммита без ручного шага (устройство и границы — `src/hook.js`). */
|
|
42
|
+
function ensureHook(root, cfg) {
|
|
43
|
+
const files = autoInstall(root, cfg);
|
|
44
|
+
if (files === null) return;
|
|
45
|
+
console.error('· хук поставлен: ' + files.join(', ') + ' — отчёт обновляется после каждого'
|
|
46
|
+
+ ' коммита (снять: ' + cliCommand('uninstall-hook') + ')');
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
/* Доставка. Диагностика и хук отвечают до чтения настроек: им нужен не весь
|
|
39
50
|
* проект, а окружение, и отказывать им из-за настроек было бы неверно — про
|
|
40
51
|
* настройки они как раз и докладывают. */
|
|
@@ -45,6 +56,7 @@ function deliver(cmd, base) {
|
|
|
45
56
|
// Примечание идёт в stderr: у `--json` и `--data` в stdout лежат данные, и
|
|
46
57
|
// подмешивать в них рассказ о настройках значило бы ломать разбор.
|
|
47
58
|
if (ctx.cfg.derived) derivedLines(ctx.cfg).forEach((line) => console.error(line));
|
|
59
|
+
ensureHook(base.root, ctx.cfg);
|
|
48
60
|
return RUNNERS[asked(cmd)](cmd, ctx);
|
|
49
61
|
}
|
|
50
62
|
|
package/src/config.js
CHANGED
|
@@ -16,7 +16,7 @@ import { projectConfig } from './project.js';
|
|
|
16
16
|
export const CONFIG_NAME = 'size-table.config.json';
|
|
17
17
|
|
|
18
18
|
export const DEFAULT_CONFIG = {
|
|
19
|
-
output: 'size-
|
|
19
|
+
output: 'size-report.html',
|
|
20
20
|
locale: 'ru',
|
|
21
21
|
title: '', // по умолчанию — заголовок из локали
|
|
22
22
|
heading: '',
|
|
@@ -30,9 +30,10 @@ export const DEFAULT_CONFIG = {
|
|
|
30
30
|
minify: { engine: 'strip', ext: {}, guard: ['.js', '.mjs', '.cjs'] },
|
|
31
31
|
// Токены: каким словарём считать. Семейство — про модели, кодировка — про число.
|
|
32
32
|
tokens: Object.assign({}, TOKEN_DEFAULTS),
|
|
33
|
-
// Автоматика хука: хук
|
|
34
|
-
//
|
|
35
|
-
//
|
|
33
|
+
// Автоматика хука: хук обновляет отчёт после каждого коммита и ставится сам —
|
|
34
|
+
// после установки пакета (`bin/postinstall.js`) и при первом запуске в проекте
|
|
35
|
+
// (`src/hook.js`); этот ключ — её выключатель (`.size-report/…` не нужен: снятие
|
|
36
|
+
// хука возвращает проект к прежнему поведению).
|
|
36
37
|
hooks: { enabled: true },
|
|
37
38
|
journal: null,
|
|
38
39
|
links: { commitUrl: '' },
|
package/src/css.js
CHANGED
|
@@ -5,31 +5,22 @@ import fs from 'node:fs';
|
|
|
5
5
|
* редактор, а не только шаблонная строка. Читаются они с диска относительно
|
|
6
6
|
* своего места, поэтому работают и у того, кто поставил пакет.
|
|
7
7
|
*
|
|
8
|
-
* Наборов
|
|
8
|
+
* Наборов два, и у каждого своя роль:
|
|
9
9
|
*
|
|
10
|
-
* 1. `table.css` —
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* текст этой части, без шапки: он попадает в артефакт побайтово, а артефакт
|
|
14
|
-
* заморожен эталоном паритета, и любая добавленная строка меняла бы
|
|
15
|
-
* зафиксированный вывод проекта-потребителя.
|
|
16
|
-
* 2. `artifact.css` — оформление статического артефакта **сверх таблицы**: холст,
|
|
17
|
-
* заголовок, примечание.
|
|
18
|
-
* 3. `page/app.css` — оформление страницы **сверх таблицы**: холст, панель
|
|
10
|
+
* 1. `table.css` — **таблица**: геометрия клеток, липкие шапка и колонка коммита,
|
|
11
|
+
* подпись коммита, цвета дельт.
|
|
12
|
+
* 2. `page/app.css` — оформление страницы **сверх таблицы**: холст, панель
|
|
19
13
|
* выбора, легенда, состояния пустоты и адаптации под узкое окно.
|
|
20
14
|
*
|
|
21
15
|
* Соглашение о цвете дельт задано один раз — в `table.css`: `.up` зелёный, `.down`
|
|
22
|
-
* красный (рост — «больше логики», а не тревога).
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* разными цветами. Смена соглашения — две строки в `table.css` и пересъёмка
|
|
26
|
-
* эталона артефакта; отдельного места у цвета дельт нет намеренно.
|
|
16
|
+
* красный (рост — «больше логики», а не тревога). Второго места у него нет
|
|
17
|
+
* намеренно: рост не может быть показан разными цветами в двух местах одной
|
|
18
|
+
* страницы. Смена соглашения — две строки в `table.css`.
|
|
27
19
|
*
|
|
28
20
|
* Путь у `readCss` — от каталога `src/`: так его видит движок, где бы он ни лежал.
|
|
29
21
|
*/
|
|
30
22
|
|
|
31
23
|
export const TABLE_CSS = readCss('./table.css');
|
|
32
|
-
export const ARTIFACT_CSS = readCss('./artifact.css');
|
|
33
24
|
export const PAGE_CSS = readCss('./page/app.css');
|
|
34
25
|
|
|
35
26
|
function readCss(name) {
|
package/src/hook.js
CHANGED
|
@@ -15,10 +15,15 @@ import { rebuild } from './artifact.js';
|
|
|
15
15
|
*
|
|
16
16
|
* Что он делает и почему именно так:
|
|
17
17
|
*
|
|
18
|
-
* - **Ставится
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* - **Ставится сам** — после установки пакета (`bin/postinstall.js`) и при первом
|
|
19
|
+
* запуске в проекте (`autoInstall`, зовётся из входа): от человека не требуется
|
|
20
|
+
* ни ручного шага, ни файла настроек, иначе первого обновления отчёта он не
|
|
21
|
+
* увидел бы вовсе. Ставится там, где это безопасно (обычный `.git/hooks`, нет
|
|
22
|
+
* чужого хука, есть чем звать инструмент, не CI); где небезопасно — молчит.
|
|
23
|
+
* Снимается явной командой (`uninstall-hook`), и проект возвращается к прежнему
|
|
24
|
+
* поведению: и поставленное, и снятое — одно и то же место состояния (`git-dir`).
|
|
25
|
+
* Ручная команда (`install-hook`) остаётся: она называет причину, когда поставить
|
|
26
|
+
* не удалось, а тихая постановка причин не объясняет.
|
|
22
27
|
* - **Сам коммитов не создаёт** — за одним исключением: отчёт, лежащий в git,
|
|
23
28
|
* коммитится отдельно от кода. Раньше это делал человек (отсюда ловушка «правка
|
|
24
29
|
* кода и таблицы в одном коммите»), и хук для того и нужен, чтобы ручного шага не
|
|
@@ -185,6 +190,7 @@ export function installHook(root, cfg) {
|
|
|
185
190
|
return { code: EXIT.OK, lines: [
|
|
186
191
|
'· хук уже установлен: ' + rels.join(', '),
|
|
187
192
|
' автоматика работает после каждого коммита и слияния',
|
|
193
|
+
' выключить, не снимая: «"hooks": {"enabled": false}» в файле настроек',
|
|
188
194
|
' снять: ' + cliCommand('uninstall-hook')
|
|
189
195
|
] };
|
|
190
196
|
}
|
|
@@ -208,6 +214,38 @@ export function installHook(root, cfg) {
|
|
|
208
214
|
return { code: EXIT.OK, lines: lines };
|
|
209
215
|
}
|
|
210
216
|
|
|
217
|
+
/* Постановка без спроса. Отвечает списком путей, если поставила, и `null`, если не
|
|
218
|
+
* тронула ничего, — второй ответ не ошибка, а норма: эта услуга фоновая, и там, где
|
|
219
|
+
* она не к месту, её просто нет. Поэтому всё, что мешает поставить, решается
|
|
220
|
+
* молчанием, а не отказом: отказ от фоновой работы после каждого запуска был бы
|
|
221
|
+
* шумом, а причина уже названа точной командой (`install-hook`).
|
|
222
|
+
*
|
|
223
|
+
* Чужой `core.hooksPath` сюда же: этот каталог версионируется и часто лежит в
|
|
224
|
+
* другом репозитории — вписывать строку в чужой файл по своей воле нельзя, и
|
|
225
|
+
* человек берёт её у `install-hook` (готовую и без метки). */
|
|
226
|
+
export function autoInstall(root, cfg) {
|
|
227
|
+
if (process.env.CI || process.env[NO_HOOK]) return null;
|
|
228
|
+
if (cfg !== null && cfg.hooks.enabled === false) return null;
|
|
229
|
+
try {
|
|
230
|
+
const hooks = hooksDir(root);
|
|
231
|
+
const entry = hookEntry(root);
|
|
232
|
+
if (hooks.custom || entry === null) return null;
|
|
233
|
+
const files = HOOKS.map((name) => path.join(hooks.dir, name));
|
|
234
|
+
if (files.some((f) => fs.existsSync(f) && !isOurs(f))) return null;
|
|
235
|
+
if (files.every(isOurs)) return null;
|
|
236
|
+
fs.mkdirSync(hooks.dir, { recursive: true });
|
|
237
|
+
files.forEach((file) => {
|
|
238
|
+
fs.writeFileSync(file, script(entry));
|
|
239
|
+
fs.chmodSync(file, 0o755);
|
|
240
|
+
});
|
|
241
|
+
return files.map((f) => path.relative(root, f));
|
|
242
|
+
} catch (_e) {
|
|
243
|
+
/* Не git-репозиторий, нет прав на `.git`, чужой формат — всё это значит одно:
|
|
244
|
+
* автоматики здесь не будет, а работа инструмента от неё не зависит. */
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
211
249
|
/* Снятие: убирается только то, что поставили мы. Файл не «похож на наш», а помечен
|
|
212
250
|
* меткой, иначе чужой хук был бы стёрт молча. */
|
|
213
251
|
export function uninstallHook(root) {
|
package/src/locales.js
CHANGED
|
@@ -12,14 +12,15 @@ export const LOCALES = {
|
|
|
12
12
|
total: 'Общий объём',
|
|
13
13
|
now: 'сейчас',
|
|
14
14
|
categories: { code: 'Код', docs: 'Документация', chore: 'Служебные', assets: 'Ресурсы' },
|
|
15
|
-
/* Тексты страницы
|
|
16
|
-
*
|
|
15
|
+
/* Тексты страницы отчёта. Они лежат в самом файле отчёта (отдельным словарём,
|
|
16
|
+
* рядом с данными), поэтому меняются вместе с ним — и правка слова стоит
|
|
17
|
+
* пересборки отчёта, иначе файл разойдётся с историей. */
|
|
17
18
|
page: {
|
|
18
19
|
metrics: 'Метрики',
|
|
19
20
|
files: 'Файлы',
|
|
20
21
|
dir: 'все файлы папки {name} ({n})',
|
|
21
22
|
all: 'все',
|
|
22
|
-
sub: '{tool} {version} ·
|
|
23
|
+
sub: '{tool} {version} · {artifact}',
|
|
23
24
|
legendUp: 'рост',
|
|
24
25
|
legendDown: 'спад',
|
|
25
26
|
legendSame: 'пустая клетка — не менялось',
|
|
@@ -75,7 +76,7 @@ export const LOCALES = {
|
|
|
75
76
|
files: 'Files',
|
|
76
77
|
dir: 'all files in “{name}” ({n})',
|
|
77
78
|
all: 'all',
|
|
78
|
-
sub: '{tool} {version} ·
|
|
79
|
+
sub: '{tool} {version} · {artifact}',
|
|
79
80
|
legendUp: 'growth',
|
|
80
81
|
legendDown: 'fall',
|
|
81
82
|
legendSame: 'an empty cell — no change',
|
package/src/modes.js
CHANGED
|
@@ -9,11 +9,9 @@ import { coverage, coverageText } from './check.js';
|
|
|
9
9
|
import { explainCommit, explainText } from './explain.js';
|
|
10
10
|
import { doctor, doctorText } from './doctor.js';
|
|
11
11
|
import { hookRun, installHook, uninstallHook } from './hook.js';
|
|
12
|
-
import {
|
|
12
|
+
import { artifact, rebuild } from './artifact.js';
|
|
13
13
|
import { sensorGaps } from './metrics.js';
|
|
14
|
-
import { render } from './render.js';
|
|
15
14
|
import { totalsOf } from './derived.js';
|
|
16
|
-
import { pageHtml } from './page/build.js';
|
|
17
15
|
|
|
18
16
|
/* Режимы: что инструмент делает по запросу. Разбор аргументов — в `src/args.js`, а
|
|
19
17
|
* сюда приходит готовый план: какой режим, какой ключ, что печатать. Здесь же их
|
|
@@ -21,10 +19,10 @@ import { pageHtml } from './page/build.js';
|
|
|
21
19
|
* на все режимы, потому что один и тот же счёт и один и тот же знак не должны
|
|
22
20
|
* разойтись между `--write`, `--data`, `--page` и `size check`.
|
|
23
21
|
*
|
|
24
|
-
* Что где: сборка и сверка
|
|
25
|
-
* (`--data
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* Что где: сборка и сверка отчёта (`--write`, проверка), данные контракта
|
|
23
|
+
* (`--data`), полнота покрытия (`size check`), диагностика (`doctor`), хук и
|
|
24
|
+
* объяснение пропущенной строки. Файл знает про все остальные модули сразу — это
|
|
25
|
+
* его работа: связать их в одну команду.
|
|
28
26
|
*/
|
|
29
27
|
|
|
30
28
|
function kmb(bytes) {
|
|
@@ -81,23 +79,31 @@ export function check(cfg, want, root) {
|
|
|
81
79
|
return 1;
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
/* Путь, названный ключом (`--write <файл>`), — это настройка `output` этого
|
|
83
|
+
* запуска: отчёт обязан называть себя тем путём, по которому лежит, иначе подпись в
|
|
84
|
+
* нём указывала бы на чужое место. */
|
|
85
|
+
function withOutput(cfg, root, file) {
|
|
86
|
+
if (typeof file !== 'string') return cfg;
|
|
87
|
+
return Object.assign({}, cfg, { output: path.relative(root, path.resolve(file)) });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function writeMode(cfg, root, file) {
|
|
91
|
+
const out = rebuild(withOutput(cfg, root, file), root);
|
|
92
|
+
const { rows, files, now, skipped } = out.data;
|
|
93
|
+
console.log('✓ ' + path.relative(root, out.file) + ': ' + rows.length + ' строк × ' + files.length + ' файлов, '
|
|
94
|
+
+ kmb(byteLen(out.html)) + ' (пропущено без строки: ' + skipped.length + ' — '
|
|
95
|
+
+ skipped.join(', ') + ')');
|
|
96
|
+
console.log(' состояние на HEAD: ' + files.map((f, i) => f.label + ' '
|
|
97
|
+
+ (now[i] === null ? '—' : cfg.metrics.map((m) => now[i][m]).join('/'))).join(', '));
|
|
91
98
|
return sensorNote(cfg);
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
export function checkMode(cfg, root) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const code = check(cfg, html, root);
|
|
102
|
+
const out = artifact(cfg, root);
|
|
103
|
+
const code = check(cfg, out.html, root);
|
|
98
104
|
if (code === 0) {
|
|
99
|
-
console.log('✓
|
|
100
|
-
+ 'совпадает с историей (' + cfg.output + ', ' + kmb(byteLen(html)) + ')');
|
|
105
|
+
console.log('✓ отчёт: ' + out.data.rows.length + ' коммитов × ' + out.data.files.length + ' файлов '
|
|
106
|
+
+ 'совпадает с историей (' + cfg.output + ', ' + kmb(byteLen(out.html)) + ')');
|
|
101
107
|
}
|
|
102
108
|
return verdict(code, sensorGaps(cfg));
|
|
103
109
|
}
|
|
@@ -162,21 +168,6 @@ export function dataMode(cfg, root) {
|
|
|
162
168
|
return sensorNote(cfg);
|
|
163
169
|
}
|
|
164
170
|
|
|
165
|
-
/* Страница отчёта: собирается тем же проходом по истории, что и артефакт — иначе
|
|
166
|
-
* два отчёта могли бы показывать разные числа. Файл кладётся рядом с таблицей,
|
|
167
|
-
* потому что он из неё и растёт. */
|
|
168
|
-
const PAGE_NAME = 'size-report.html';
|
|
169
|
-
|
|
170
|
-
export function pageMode(cfg, root, file) {
|
|
171
|
-
const data = reportData(cfg, root);
|
|
172
|
-
const target = file ? path.resolve(file) : path.join(root, path.dirname(cfg.output), PAGE_NAME);
|
|
173
|
-
const html = pageHtml(data, cfg);
|
|
174
|
-
writeFileEnsured(target, html);
|
|
175
|
-
console.log('✓ ' + path.relative(root, target) + ': ' + data.rows.length + ' строк × '
|
|
176
|
-
+ data.files.length + ' файлов, ' + kmb(byteLen(html)));
|
|
177
|
-
return sensorNote(cfg);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
171
|
export function jsonMode(cfg, root) {
|
|
181
172
|
const { rows, dropped } = build(cfg, root);
|
|
182
173
|
process.stdout.write(JSON.stringify({
|
package/src/page/build.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import { fill, LOCALES } from '../locales.js';
|
|
3
|
-
import { esc } from '../render.js';
|
|
4
3
|
import { PAGE_CSS, TABLE_CSS } from '../css.js';
|
|
5
4
|
|
|
5
|
+
/* Экранирование текста в разметке — здесь, потому что единственный, кто собирает
|
|
6
|
+
* разметку из данных, — эта сборка: остальное рисует страница узлами. */
|
|
7
|
+
export function esc(s) {
|
|
8
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
/* Сборка страницы отчёта: данные и программа в одном файле, внешних ссылок нет.
|
|
7
12
|
* Оформление — тоже обычные файлы: общая часть таблицы (`table.css`) и своё
|
|
8
13
|
* оформление страницы (`app.css`).
|
|
@@ -37,8 +42,8 @@ export function pageScript() {
|
|
|
37
42
|
return pageSource('../derived.js') + '\n' + PAGE_PARTS.map((part) => pageSource(part)).join('');
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
/* Подпись под заголовком: чем
|
|
41
|
-
*
|
|
45
|
+
/* Подпись под заголовком: чем собран отчёт и где он лежит. Путь — текстом, а не
|
|
46
|
+
* ссылкой: страница открывается с диска и ни от чего не зависит. */
|
|
42
47
|
function subText(data, page) {
|
|
43
48
|
return fill(page.sub, {
|
|
44
49
|
tool: data.tool.name,
|
|
@@ -81,6 +86,20 @@ function uiText(page, loc) {
|
|
|
81
86
|
};
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
/* Что в файл не идёт. Первое — список пропущенных коммитов: он меняется от
|
|
90
|
+
* коммита самого отчёта (тот, кому нечего сказать, попадает в список), и файл
|
|
91
|
+
* перестал бы быть **неподвижной точкой** — пересборка после его же коммита давала
|
|
92
|
+
* бы другие байты, а хук коммитил бы отчёт бесконечно. Странице этот список не
|
|
93
|
+
* нужен вовсе: она его не показывает. Читателю он по-прежнему доступен — `--data`,
|
|
94
|
+
* `--json` и `explain` отвечают этим же проходом. */
|
|
95
|
+
const NOT_IN_FILE = ['skipped'];
|
|
96
|
+
|
|
97
|
+
export function pagePayload(data) {
|
|
98
|
+
const out = Object.assign({}, data);
|
|
99
|
+
NOT_IN_FILE.forEach((key) => delete out[key]);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
84
103
|
/* Страница отчёта — один файл: данные лежат в нём же, скрипт вклеен, внешних
|
|
85
104
|
* ссылок нет. Поэтому она открывается двойным щелчком и работает без сети.
|
|
86
105
|
* `<` в данных экранируется: иначе подпись коммита или путь закрыли бы тег
|
|
@@ -98,7 +117,7 @@ export function pageHtml(data, cfg) {
|
|
|
98
117
|
+ '<div id="shell" class="shell"><table id="grid"></table></div>\n'
|
|
99
118
|
+ '<p id="state" class="state" hidden></p>\n'
|
|
100
119
|
+ '<p id="note" class="note"></p>\n'
|
|
101
|
-
+ '<script type="application/json" id="data">' + jsonInHtml(data) + '</script>\n'
|
|
120
|
+
+ '<script type="application/json" id="data">' + jsonInHtml(pagePayload(data)) + '</script>\n'
|
|
102
121
|
+ '<script type="application/json" id="ui">' + jsonInHtml(uiText(loc.page, loc)) + '</script>\n'
|
|
103
122
|
+ '<script>\n' + pageScript() + '</script>\n</body>\n</html>\n';
|
|
104
123
|
}
|
package/src/project.js
CHANGED
|
@@ -97,8 +97,11 @@ function exists(root, p) {
|
|
|
97
97
|
return fs.existsSync(path.join(root, p));
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/* Имя отчёта — одно на пакет и на проект: файл это и есть страница отчёта, и
|
|
101
|
+
* называться иначе она не может. Каталог решает только, где ей лежать: рядом с
|
|
102
|
+
* доками, если они в проекте есть, иначе в корне. */
|
|
100
103
|
function outputOf(root) {
|
|
101
|
-
return (exists(root, 'docs') ? 'docs/' : '') + 'size-
|
|
104
|
+
return (exists(root, 'docs') ? 'docs/' : '') + 'size-report.html';
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
/* Менеджер пакетов — по lock-файлу, а не догадкой: команда обязана существовать
|
package/src/refusal.js
CHANGED
|
@@ -91,7 +91,8 @@ export function advicePath(p) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export const USAGE = [
|
|
94
|
-
'@vernikr/size-report —
|
|
94
|
+
'@vernikr/size-report — отчёт об объёме файлов по коммитам: один файл,',
|
|
95
|
+
'самодостаточная страница (данные, оформление и программа лежат в ней же).',
|
|
95
96
|
'',
|
|
96
97
|
'Запуск: ' + invocation() + ' [команда] [режим] [ключи]',
|
|
97
98
|
'',
|
|
@@ -101,19 +102,18 @@ export const USAGE = [
|
|
|
101
102
|
' explain <коммит> почему у коммита нет строки (имя ревизии, sha или его начало)',
|
|
102
103
|
' doctor [--json] диагностика одним ответом: окружение, зависимости, настройки,',
|
|
103
104
|
' покрытие (код 0 — делать нечего, иначе — первый по важности)',
|
|
104
|
-
' install-hook поставить хуки post-commit и post-merge
|
|
105
|
-
'
|
|
106
|
-
' отдельным коммитом',
|
|
105
|
+
' install-hook поставить хуки post-commit и post-merge (они ставятся сами при',
|
|
106
|
+
' первом запуске в проекте): отчёт пересобирается после каждого',
|
|
107
|
+
' коммита и слияния, а если он в git — ложится отдельным коммитом',
|
|
107
108
|
' uninstall-hook убрать хук и его состояние (проект возвращается к прежнему)',
|
|
108
109
|
' hook-run то, что зовёт хук: пересборка и коммит отчёта (вручную не нужно)',
|
|
109
110
|
'',
|
|
110
111
|
'Режимы:',
|
|
111
112
|
' --init [файл] закрепить настройки файлом (--force — перезаписать существующий)',
|
|
112
|
-
' --write
|
|
113
|
-
' --data данные контракта в stdout — для
|
|
114
|
-
' --page [файл] страница отчёта (по умолчанию рядом с таблицей)',
|
|
113
|
+
' --write [файл] собрать отчёт в файл из настроек (каталог создаётся сам)',
|
|
114
|
+
' --data данные контракта в stdout — для отчёта и для агента',
|
|
115
115
|
' --json прежняя форма данных в stdout',
|
|
116
|
-
' (без режима) проверить, что
|
|
116
|
+
' (без режима) проверить, что отчёт совпадает с историей',
|
|
117
117
|
'',
|
|
118
118
|
'Ключи: --config <файл> — другие настройки; --help — эта справка.',
|
|
119
119
|
'',
|
package/src/size-table.js
CHANGED
|
@@ -37,13 +37,16 @@
|
|
|
37
37
|
* начинает работать и для стратегии «пересобрать и дописать в тот же коммит»:
|
|
38
38
|
* без sha артефакт становится неподвижной точкой сборки.
|
|
39
39
|
*
|
|
40
|
+
* Отчёт один: самодостаточная страница (`size-report.html`), в которой лежат и
|
|
41
|
+
* данные, и оформление, и программа. Второй формы того же отчёта нет намеренно: два
|
|
42
|
+
* вывода одной истории разошлись бы молча, а выбрать, какой верный, было бы нечем.
|
|
43
|
+
*
|
|
40
44
|
* Запуск (из любого места репозитория; `size` — когда пакет установлен, иначе
|
|
41
45
|
* `node bin/size.js`):
|
|
42
|
-
* size проверка:
|
|
43
|
-
* size --write
|
|
46
|
+
* size проверка: отчёт совпадает с историей (CI)
|
|
47
|
+
* size --write [файл] перегенерировать отчёт
|
|
44
48
|
* size --json строки как JSON в stdout
|
|
45
|
-
* size --data данные для
|
|
46
|
-
* size --page [файл] собрать страницу отчёта
|
|
49
|
+
* size --data данные для отчёта и агента в stdout
|
|
47
50
|
* size --init [файл] закрепить настройки файлом (без него они выводятся из проекта)
|
|
48
51
|
* size --config <путь> другой файл настроек
|
|
49
52
|
* size --help справка и коды выхода
|
|
@@ -64,8 +67,8 @@
|
|
|
64
67
|
* config → project, git, refusal, locales, metrics, data — настройки проекта;
|
|
65
68
|
* history → git, metrics, journal, refusal — сборка по истории;
|
|
66
69
|
* data → locales, metrics, journal, history, tool — контракт со страницей;
|
|
67
|
-
*
|
|
68
|
-
*
|
|
70
|
+
* page/build → locales, css — отчёт одним файлом;
|
|
71
|
+
* artifact → data, page/build — запись отчёта;
|
|
69
72
|
* modes → почти все — что делать по запросу;
|
|
70
73
|
* init → config, project, refusal, artifact — закрепление настроек файлом;
|
|
71
74
|
* cli → args, modes, init, config, refusal — вход: разбор и доставка.
|
|
@@ -76,11 +79,10 @@
|
|
|
76
79
|
export { main } from './cli.js';
|
|
77
80
|
export { initMode } from './init.js';
|
|
78
81
|
export { sniffColumns } from './project.js';
|
|
79
|
-
export { check, dataMode,
|
|
82
|
+
export { check, dataMode, writeMode } from './modes.js';
|
|
80
83
|
export { reportData, categoryOf, CATEGORY_EXTS, CATEGORY_ORDER } from './data.js';
|
|
81
84
|
export { measureHistory } from './history.js';
|
|
82
|
-
export {
|
|
83
|
-
export { pageHtml, pageScript, pageSource, stripModules } from './page/build.js';
|
|
85
|
+
export { pageHtml, pageScript, pageSource, stripModules, esc } from './page/build.js';
|
|
84
86
|
export { measureBlob, METRICS } from './metrics.js';
|
|
85
87
|
export { minifyForm, strategyFor, stripCss, stripHtml, stripJs, stripLines, compactJson,
|
|
86
88
|
STRATEGIES } from './strip.js';
|
package/templates/README.md
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
| `size-report.config.json` | `size-table.config.json` в корне проекта | **Поправить колонки** и, если нужно, остальное |
|
|
12
12
|
| `ci.yml` | `.github/workflows/size-report.yml` | Ничего: файл работает как есть |
|
|
13
13
|
|
|
14
|
+
Отчёт в проекте появляется без ручной работы: после установки пакета и первого
|
|
15
|
+
запуска инструмент сам ставит хуки `post-commit`/`post-merge`, и `docs/` с
|
|
16
|
+
`size-report.html` создаётся первым же коммитом. Снять автоматику — `size
|
|
17
|
+
uninstall-hook`, выключить, не снимая, — `"hooks": {"enabled": false}`.
|
|
18
|
+
|
|
14
19
|
## Настройки
|
|
15
20
|
|
|
16
21
|
**Файл настроек заводить не нужно.** Без него инструмент выводит профиль из самого
|
|
@@ -51,7 +56,7 @@ README и журнал колонкой не отслеживаются, — т
|
|
|
51
56
|
именно этот объект выводит `size --init`, когда журнал в проекте есть.
|
|
52
57
|
- `paths` внутри колонки — псевдонимы одного файла: если файл переименовывали,
|
|
53
58
|
перечислите и старое имя, и новое, и колонка не разорвётся.
|
|
54
|
-
- `output: "docs/size-
|
|
59
|
+
- `output: "docs/size-report.html"` — файл отчёта (он один: самодостаточная страница со всеми числами, фильтрами и ссылкой); каталог инструмент создаст сам, а хук — тоже: после установки пакета и первого запуска отчёт обновляется после каждого коммита без ручного шага.
|
|
55
60
|
|
|
56
61
|
## Проверка в CI
|
|
57
62
|
|
package/templates/ci.yml
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
# Положите файл в .github/workflows/size-report.yml — правок он не требует.
|
|
3
3
|
#
|
|
4
4
|
# Что проверяется и почему так:
|
|
5
|
-
# *
|
|
5
|
+
# * отчёт на диске сходится с историей git — это его числа (отчёт один:
|
|
6
|
+
# самодостаточная страница `size-report.html`);
|
|
6
7
|
# * тот же снимок чисел, снятый в среде, где настроек git нет вовсе,
|
|
7
8
|
# совпадает побайтово: вывод инструмента не должен зависеть от того, что
|
|
8
9
|
# настроено на машине (BLOCKERS.md §B1, §B2).
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
# Нужен явный путь — `node node_modules/@vernikr/size-report/bin/size.js`.
|
|
24
25
|
#
|
|
25
26
|
# Отчёт не в git? Такое тоже задумано требованиями (отчёт — выводимый артефакт):
|
|
26
|
-
# тогда вместо шага
|
|
27
|
+
# тогда вместо шага «Отчёт совпадает с историей» поставьте сборку —
|
|
27
28
|
# `pnpm exec size --write` — и этот шаг будет проверять, что отчёт собирается.
|
|
28
29
|
|
|
29
30
|
name: size-report
|
|
@@ -34,7 +35,7 @@ jobs:
|
|
|
34
35
|
size:
|
|
35
36
|
runs-on: ubuntu-latest
|
|
36
37
|
steps:
|
|
37
|
-
# История нужна целиком:
|
|
38
|
+
# История нужна целиком: отчёт строится по коммитам, и на обрезанном
|
|
38
39
|
# клоне инструмент отказывается работать (код 3), а не пишет короткую.
|
|
39
40
|
- uses: actions/checkout@v7
|
|
40
41
|
with:
|
|
@@ -50,9 +51,9 @@ jobs:
|
|
|
50
51
|
- name: Установка
|
|
51
52
|
run: pnpm install --frozen-lockfile
|
|
52
53
|
|
|
53
|
-
# Проверка — та же команда `size` без ключей: она собирает
|
|
54
|
+
# Проверка — та же команда `size` без ключей: она собирает отчёт заново
|
|
54
55
|
# и сверяет с файлом на диске. Своего набора тестов потребителю не нужно.
|
|
55
|
-
- name:
|
|
56
|
+
- name: Отчёт совпадает с историей
|
|
56
57
|
run: pnpm exec size
|
|
57
58
|
|
|
58
59
|
- name: Снимок чисел контракта
|
package/src/artifact.css
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
:root { color-scheme: light dark; }
|
|
2
|
-
/* Фон и цвет текста заданы явно и одной парой (Canvas/CanvasText): без этого
|
|
3
|
-
* страница берёт цвет текста из схемы, а фон — нет, и в тёмной схеме числа
|
|
4
|
-
* оказывались белыми на белом. */
|
|
5
|
-
body { margin: 0; padding: 20px; background: Canvas; color: CanvasText; font: 12.5px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
6
|
-
h1 { margin: 0 0 6px; font-size: 16px; }
|
|
7
|
-
.note { margin: 0 0 14px; max-width: 80em; opacity: .75; font-size: 12px; }
|
|
8
|
-
.note code { background: rgba(127, 127, 127, .15); padding: 0 3px; border-radius: 3px; }
|
package/src/render.js
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { fill, LOCALES } from './locales.js';
|
|
2
|
-
import { METRICS, metricView } from './metrics.js';
|
|
3
|
-
import { rowHref } from './journal.js';
|
|
4
|
-
import { cellParts, commitParts, nowModel, rowModel, valueParts } from './derived.js';
|
|
5
|
-
import { ARTIFACT_CSS, TABLE_CSS } from './css.js';
|
|
6
|
-
|
|
7
|
-
/* Статический отчёт: стили, разметка клетки и таблицы, примечание. Производные
|
|
8
|
-
* величины берёт из общего расчёта («derived.js») — того же, который исполняет
|
|
9
|
-
* страница. */
|
|
10
|
-
|
|
11
|
-
/* Оформление артефакта — своё плюс общая часть таблицы: ровно тот же текст
|
|
12
|
-
* таблицы получает и страница, поэтому оформление самой таблицы у двух отчётов
|
|
13
|
-
* одно. Что именно входит в каждую часть — в `src/css.js`. */
|
|
14
|
-
const CSS = ARTIFACT_CSS + '\n' + TABLE_CSS;
|
|
15
|
-
|
|
16
|
-
export function esc(s) {
|
|
17
|
-
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/* Разметка клетки строки-коммита: правила — в `cellParts`, здесь только тег и
|
|
21
|
-
* классы. Клетка-дельта: абсолютные числа стоят один раз в верхней строке, иначе
|
|
22
|
-
* крупное число повторялось бы в каждой строке и колонки расползались бы. */
|
|
23
|
-
export function cellHtml(cell, first) {
|
|
24
|
-
const parts = cellParts(cell.value, cell.delta, '-');
|
|
25
|
-
const cls = 'num' + (first ? ' g' : '') + (parts.miss ? ' miss' : '');
|
|
26
|
-
if (parts.dir === null) return '<td class="' + cls + '">' + parts.text + '</td>';
|
|
27
|
-
return '<td class="' + cls + '"><span class="delta ' + parts.dir + '">' + parts.text + '</span></td>';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Разметка клетки верхней строки: правила — в `valueParts`.
|
|
31
|
-
export function valueHtml(value, first) {
|
|
32
|
-
const parts = valueParts(value);
|
|
33
|
-
return '<td class="num' + (first ? ' g' : '') + (parts.miss ? ' miss' : '') + '">' + parts.text + '</td>';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function commitCell(row, index, cfg) {
|
|
37
|
-
const loc = LOCALES[cfg.locale];
|
|
38
|
-
const parts = commitParts(row, cfg.rows.sha, rowHref(row.section, row.sha, cfg));
|
|
39
|
-
const when = '<span class="when">' + esc(parts.when) + '</span>';
|
|
40
|
-
const title = esc(parts.title);
|
|
41
|
-
const plain = row.section === null && parts.href === null;
|
|
42
|
-
const body = parts.href
|
|
43
|
-
? '<a class="subj" title="' + title + '" href="' + esc(parts.href) + '">' + esc(parts.subject) + '</a>'
|
|
44
|
-
: '<span class="subj' + (plain ? ' plain' : '') + '" title="' + title + '">' + esc(parts.subject) + '</span>';
|
|
45
|
-
const markTitle = parts.mark.title === null
|
|
46
|
-
? (cfg.journal ? fill(loc.note.noJournalMark, { journal: cfg.journal.path }) : loc.note.noJournal)
|
|
47
|
-
: parts.mark.title;
|
|
48
|
-
const id = cfg.rows.sha ? 'c-' + row.sha.slice(0, 7) : 'c-' + (index + 1);
|
|
49
|
-
return '<th class="c-commit" id="' + id + '">'
|
|
50
|
-
+ '<div class="clip">' + when + body
|
|
51
|
-
+ '<span class="sect" title="' + esc(markTitle) + '">' + esc(parts.mark.text) + '</span>'
|
|
52
|
-
+ '</div></th>';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function noteText(rows, cfg) {
|
|
56
|
-
const loc = LOCALES[cfg.locale];
|
|
57
|
-
const metrics = cfg.metrics.map((m) => {
|
|
58
|
-
const view = metricView(m, cfg);
|
|
59
|
-
return '<b>' + view.label + '</b> — ' + view.note;
|
|
60
|
-
}).join(loc.note.metricSep);
|
|
61
|
-
const journal = cfg.journal ? loc.note.journal : loc.note.noJournal;
|
|
62
|
-
return loc.note.intro + metrics + loc.note.metricEnd + fill(loc.note.numbers, { now: loc.now })
|
|
63
|
-
+ (cfg.journal ? fill(journal, { journal: cfg.journal.path }) : journal)
|
|
64
|
-
+ fill(loc.note.columns, { columns: cfg.columns.map((c) => c.label).join(', ') })
|
|
65
|
-
+ fill(loc.note.rows, { rows: rows.length, command: cfg.fixCommand });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* Шапка таблицы: строка групп (итог и колонки) и под ней строка метрик. */
|
|
69
|
-
function tableHead(cfg, loc) {
|
|
70
|
-
const metrics = cfg.metrics;
|
|
71
|
-
const groupHead = (label, cls) => '<th colspan="' + metrics.length + '" class="' + cls + '">' + esc(label) + '</th>';
|
|
72
|
-
const subHead = () => metrics.map((m, i) => '<th' + (i === 0 ? ' class="g"' : '') + '>'
|
|
73
|
-
+ esc(METRICS[m].label) + '</th>').join('');
|
|
74
|
-
return '<tr>'
|
|
75
|
-
+ '<th rowspan="2" class="c-commit">' + esc(loc.commit) + '</th>'
|
|
76
|
-
+ groupHead(loc.total, 'g')
|
|
77
|
-
+ cfg.columns.map((c) => groupHead(c.label, 'g')).join('')
|
|
78
|
-
+ '</tr>\n<tr>'
|
|
79
|
-
+ subHead()
|
|
80
|
-
+ cfg.columns.map(() => subHead()).join('')
|
|
81
|
-
+ '</tr>';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/* Тело таблицы: строки-коммиты сверху вниз — от старых к новым, — а над ними
|
|
85
|
-
* строка «сейчас» с абсолютными размерами.
|
|
86
|
-
*
|
|
87
|
-
* Дельта считается к предыдущему коммиту (в списке ниже он идёт строкой ниже),
|
|
88
|
-
* а появление файла — рост на весь его объём: иначе сумма дельт по колонке не
|
|
89
|
-
* сходилась бы с текущим размером, и верхняя строка была бы недоказуемой. Всё
|
|
90
|
-
* это считает `rowModel` — тот же, что и на странице. */
|
|
91
|
-
function tableBody(rows, cfg, loc) {
|
|
92
|
-
const metrics = cfg.metrics;
|
|
93
|
-
const cellsHtml = (make) => (cells) => cells.map((c, mi) => make(c, mi === 0)).join('');
|
|
94
|
-
const rowCells = cellsHtml((c, first) => cellHtml(c, first));
|
|
95
|
-
const nowCells = cellsHtml((v, first) => valueHtml(v, first));
|
|
96
|
-
const blocksHtml = (model, one) => one(model.total) + model.files.map(one).join('');
|
|
97
|
-
const body = rows.map((row, i) => {
|
|
98
|
-
const prev = i === 0 ? null : rows[i - 1];
|
|
99
|
-
return '<tr>' + commitCell(row, i, cfg)
|
|
100
|
-
+ blocksHtml(rowModel(row.cells, prev === null ? null : prev.cells, metrics), rowCells)
|
|
101
|
-
+ '</tr>';
|
|
102
|
-
}).reverse().join('\n');
|
|
103
|
-
const nowRow = rows.length === 0 ? '' : '<tr class="now">'
|
|
104
|
-
+ '<th class="c-commit">' + esc(loc.now) + '</th>'
|
|
105
|
-
+ blocksHtml(nowModel(rows[rows.length - 1].cells, metrics), nowCells)
|
|
106
|
-
+ '</tr>';
|
|
107
|
-
return nowRow + '\n' + body;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function render(rows, cfg) {
|
|
111
|
-
const loc = LOCALES[cfg.locale];
|
|
112
|
-
/* Подпись называет только то, что не меняется от самих служебных коммитов:
|
|
113
|
-
* число строк и список колонок. Иначе таблица считалась бы устаревшей сразу
|
|
114
|
-
* после собственного коммита — из-за пересчитанного «пропущено N» в тексте. */
|
|
115
|
-
return `<!doctype html>
|
|
116
|
-
<html lang="${loc.html}">
|
|
117
|
-
<head>
|
|
118
|
-
<meta charset="utf-8">
|
|
119
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
120
|
-
<title>${esc(cfg.title || loc.heading)}</title>
|
|
121
|
-
<style>
|
|
122
|
-
${CSS}
|
|
123
|
-
</style>
|
|
124
|
-
</head>
|
|
125
|
-
<body>
|
|
126
|
-
<h1>${esc(cfg.heading || loc.heading)}</h1>
|
|
127
|
-
<p class="note">${noteText(rows, cfg)}</p>
|
|
128
|
-
<table>
|
|
129
|
-
<thead>
|
|
130
|
-
${tableHead(cfg, loc)}
|
|
131
|
-
</thead>
|
|
132
|
-
<tbody>
|
|
133
|
-
${tableBody(rows, cfg, loc)}
|
|
134
|
-
</tbody>
|
|
135
|
-
</table>
|
|
136
|
-
</body>
|
|
137
|
-
</html>
|
|
138
|
-
`;
|
|
139
|
-
}
|