@vernikr/size-report 2.1.0 → 2.2.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 CHANGED
@@ -18,6 +18,54 @@
18
18
  где за основу взяты `fixtures/synthetic/config.json`, метрики — `raw`, `min`, `tok`, а
19
19
  способ минификации — `strip` или `esbuild`.
20
20
 
21
+ ## 2.2.0 — 2026-09-15
22
+
23
+ Дерево страницы — дерево проекта: видно всё, числа есть у измеряемых.
24
+
25
+ - **В дереве было видно только то, что стало колонками.** В проекте со 147 файлами
26
+ и выведёнными настройками колонок двенадцать — дерево и показывало двенадцать
27
+ листьев, а про остальные молчало: читатель видел не дерево проекта, а список
28
+ измеренного, и решить, что файлы потерялись, было проще всего. Теперь в контракте
29
+ (`--data`) едет **каталог** — все пути, которые видит git (`ls-files`), — и дерево
30
+ строится по нему.
31
+ - **Числа — только у колонок, и это видно, а не подразумевается.** Лист вне отчёта
32
+ стоит на своём месте, но подписью без галочки, а причина — во всплывающей строке,
33
+ и причин две: `rule` — такой файл колонкой быть не может (сам отчёт, замок
34
+ зависимостей, собранное, слишком крупный или формат без текста), `choice` — мог бы,
35
+ но в набор колонок не выбран (его задают настройки). Знак причины движок ставит
36
+ **теми же** правилами, по которым выбирает колонки, — иначе подсказка говорила бы
37
+ одно, а выбор делал другое. Сам отчёт в дереве назван **всегда** — и пока не
38
+ собран, и когда он вне git: его отслеживаемость это свойство момента, и зависеть
39
+ от неё отчёт не должен (иначе первая же пересборка в свежем клоне даёт другие
40
+ байты, и хук коммитит отчёт второй раз на пустом месте).
41
+ - **Счётчик папки со смешанным составом — доля** («2/5»: два файла в отчёте из
42
+ пяти в папке), а целиком измеряемая папка считает как раньше («5»). Папка, где
43
+ измеряемых нет вовсе, галочки не получает: включать в ней нечего, но на месте она
44
+ остаётся — дерево не должно врать о том, что в проекте есть.
45
+ - **Цена названа:** набор колонок этим не меняется, поэтому прогон не подорожал.
46
+ Замер на этом же проекте (162 колонки × 79 строк против 12): с токенами 16,1 с
47
+ вместо 2,4 с, а на живой истории полный набор ещё и отказывает (перенос состояния
48
+ теряет правку пути, которого нет на HEAD) — поэтому «мерить всё» решением не стало.
49
+
50
+ ### Что изменится в числах
51
+
52
+ **Ничего.** Числа считает тот же движок теми же датчиками: выпуск про то, что видно в
53
+ дереве панели, а не про измерение. Таблица та же, что у 2.1.0, до последней клетки.
54
+
55
+ | Файл | raw | min со strip | min с esbuild | tok |
56
+ |---|---|---|---|---|
57
+ | code.js | 735 | 276 | 185 | 168 |
58
+ | modern.js | 246 | 51 | 45 | 44 |
59
+ | config.mjs | 172 | 45 | 40 | 40 |
60
+ | заметки.md | 306 | 303 | 303 | 53 |
61
+ | crlf.txt | 63 | 60 | 60 | 10 |
62
+ | package.json | 87 | 69 | 69 | 34 |
63
+ | style.css | 156 | 55 | 43 | 38 |
64
+ | table.toml | 300 | 299 | 299 | 52 |
65
+ | empty.js | 0 | 0 | 0 | 0 |
66
+ | WORKLOG.md | 446 | 439 | 439 | 101 |
67
+ | **ИТОГО** | **2511** | **1597** | **1483** | **540** |
68
+
21
69
  ## 2.1.0 — 2026-09-15
22
70
 
23
71
  Вид страницы на десктопе: числам — вся оставшаяся высота, панели — своя прокрутка.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## Статус
12
12
 
13
- **Выпуск 2.1.0 (2026-09-15).** Инструмент живёт отдельным пакетом: имя в
13
+ **Выпуск 2.2.0 (2026-09-15).** Инструмент живёт отдельным пакетом: имя в
14
14
  реестре — `@vernikr/size-report` (публикуется тегом из CI, без секрета). Настроек
15
15
  проект может не заводить вовсе: без файла инструмент выводит их из самого проекта и
16
16
  говорит об этом строкой, а `--init` закрепляет выведенное файлом (чем этот шаг
@@ -22,8 +22,9 @@
22
22
  в числах»:
23
23
  таблица чисел в нём не пересказ, а замер на фикстуре, который сверяется с живым
24
24
  прогоном (`test/changelog.test.js`). Числа этим выпуском не меняются — правки
25
- лежат в виде страницы отчёта на десктопе; `schema: 1` данных, замороженная 1.0.0,
26
- остаётся той же.
25
+ лежат в виде страницы отчёта на десктопе и в том, что видно в её дереве; `schema: 1`
26
+ данных, замороженная 1.0.0, остаётся той же (выпуск добавил поле, а не поменял
27
+ смысл прежних).
27
28
 
28
29
  **Шаг 1 плана пройден — перенос без изменения поведения.** Команда —
29
30
  `bin/size.js`, точка входа пакета — `src/size-table.js` (только реэкспорт),
@@ -76,7 +77,10 @@
76
77
  вывод: страница показывает то, что сказал движок, и своего правила точности не
77
78
  заводит. Панель страницы — дерево файлов по папкам, с переключателем у каждой
78
79
  папки на всё поддерево; выбор читателя переживает перезаход и передаётся
79
- ссылкой — адрес страницы и есть ссылка.
80
+ ссылкой — адрес страницы и есть ссылка. В контракте же едет **каталог проекта** —
81
+ все пути, которые видит git: дерево страницы — дерево проекта, а числа есть только
82
+ у тех файлов, что стали колонками (чем это отличается от прежнего дерева, где
83
+ были одни колонки, — `CHANGELOG.md` 2.2.0).
80
84
  Минификация и токены сделаны первыми срезами шагов 3 и 4 (ниже).
81
85
  Разбиение движка по файлам сделано (пункт D1 плана, R-1.3 `REFACTOR.md`).
82
86
  Источник инструмента — скрипт `size-table.js` (1145 строк) в проекте
@@ -219,10 +223,10 @@
219
223
 
220
224
  | Прогон | Команда | Проверок |
221
225
  |---|---|---|
222
- | Быстрый — каждая правка | `pnpm test` | **66 из 170** |
223
- | Полный — выкладка и CI | `pnpm test:all` | **170** |
226
+ | Быстрый — каждая правка | `pnpm test` | **68 из 172** |
227
+ | Полный — выкладка и CI | `pnpm test:all` | **172** |
224
228
 
225
- Ни одна проверка не потеряна и не ослаблена: полный прогон запускает все 170 теми же
229
+ Ни одна проверка не потеряна и не ослаблена: полный прогон запускает все 172 теми же
226
230
  файлами, а быстрый берёт их часть. Умолчание — полный: файл становится быстрым только
227
231
  явно и с причиной, поэтому новое дорогое не может тихо уехать в быстрый. Стерегут это
228
232
  объявление `test/suites.test.js` (полнота классификации и причина у каждого файла) и
@@ -314,8 +318,14 @@ Trusted Publisher в настройках пакета), права **publish**
314
318
  deльт задан один раз и по артефакту: рост зелёный, спад красный — смена это две
315
319
  строки в `src/table.css` плюс пересборка эталона артефакта, больше цвета нигде нет.
316
320
 
317
- **Левая панель страницы — дерево файлов** (`REFACTOR.md` R-2.4). Папки берутся из
318
- тех же путей, что показаны в подписи файла (ни одного перечисления руками), у
321
+ **Левая панель страницы — дерево файлов проекта** (`REFACTOR.md` R-2.4, каталог
322
+ путей `CHANGELOG.md` 2.2.0). Дерево строится по путям проекта, а не по одним
323
+ колонкам, поэтому в нём видно и то, что в отчёт не попало: такой лист — подпись без
324
+ галочки с причиной во всплывающей строке («колонкой быть не может» — правило пакета
325
+ — или «в набор колонок не попал» — выбор проекта), а счётчик папки со смешанным
326
+ составом написан долей («2/5»: два файла в отчёте из пяти в папке). Сам отчёт в
327
+ дереве назван всегда: его отслеживаемость — свойство момента, и от неё содержимое
328
+ страницы не зависит. У
319
329
  папки три состояния — все её файлы включены, часть, ни одного, — и переключатель
320
330
  папки ведёт за собой всё поддерево; рядом стоит число файлов. Своего состояния у
321
331
  папки и у быстрой кнопки категории нет: обе переставляют галочки файлов, поэтому
@@ -551,9 +561,9 @@ deльт задан один раз и по артефакту: рост зел
551
561
  | `src/css.js` | Чтение оформления с диска: какие наборы стилей есть и какая у них роль |
552
562
  | `src/table.css` | Таблица отчёта: геометрия клеток, липкие шапка и колонка, цвет дельт |
553
563
  | `src/page/app.css` | Оформление страницы сверх общей части: панель с деревом файлов и липкой строкой категорий (на широком экране — колонка слева, страница в окно), состояния пустоты, узкое окно |
554
- | `src/page/state.js` | Состояние страницы: данные отчёта, вид галочек, паспорт записи, память браузера и обмен ссылкой — глава программы страницы |
564
+ | `src/page/state.js` | Состояние страницы: данные отчёта, вид галочек, указатель «какой путь — какая колонка», паспорт записи, память браузера и обмен ссылкой — глава программы страницы |
555
565
  | `src/page/dom.js` | Узлы страницы: мелкие помощники разметки (`appEl`, `appBox`) — одни на панель и таблицу |
556
- | `src/page/panel.js` | Панель выбора: галочки метрик и файлов, категории, дерево путей; перерисовку просит у главы сборки |
566
+ | `src/page/panel.js` | Панель выбора: галочки метрик и файлов, категории, дерево путей проекта (файлы вне отчёта — подписью с причиной); перерисовку просит у главы сборки |
557
567
  | `src/page/table.js` | Таблица страницы: клетка, подпись коммита, шапка и состояния пустоты — разметка поверх общего расчёта |
558
568
  | `src/page/app.js` | Сборка и запуск страницы: таблица целиком, перерисовка по выбору читателя (с возвратом фокуса и прокрутки), первая отрисовка и смена якоря; вклеивается в собранную страницу
559
569
  | `src/page/build.js` | Сборка страницы: данные, оформление и программа в одном файле без внешних ссылок |
@@ -576,9 +586,9 @@ deльт задан один раз и по артефакту: рост зел
576
586
  | `bin/postinstall.js` | Установка хука после постановки пакета: ищет проект-потребитель и молчит, если поставить негде |
577
587
  | `src/artifact.js` | Отчёт на диске: единственное место, где он превращается в файл (им пользуются и `--write`, и хук); отчёт — самодостаточная страница |
578
588
  | `src/journal.js` | Журнал и ссылки: к какому разделу относится коммит и куда ведёт описание |
579
- | `src/data.js` | Категории файлов и контракт со страницей (`--data`) |
589
+ | `src/data.js` | Категории файлов и контракт со страницей (`--data`): числа, устройство таблицы и каталог путей проекта |
580
590
  | `src/config.js` | Настройки проекта-потребителя: умолчания, чтение, проверка |
581
- | `src/project.js` | Настройки, выведенные из самого проекта (дерево и история): колонки, журнал, исключения. Без файла настроек он и есть настройки; `--init` закрепляет его файлом |
591
+ | `src/project.js` | Настройки, выведенные из самого проекта (дерево и история): колонки, журнал, исключения, каталог путей для дерева страницы. Без файла настроек он и есть настройки; `--init` закрепляет его файлом |
582
592
  | `src/locales.js`, `src/refusal.js`, `src/tool.js` | Тексты отчёта; коды выхода и справка; имя и версия пакета |
583
593
  | `src/cli.js` | Вход инструмента: разбор строки, чтение проекта и доставка запроса режиму; главный файл пакета |
584
594
  | `src/args.js` | Грамматика командной строки: режимы, ключи и команды плюс проверки их сочетаний — отказ называет виновника и готовую команду |
@@ -603,7 +613,8 @@ deльт задан один раз и по артефакту: рост зел
603
613
  | `test/refusals-catalog.test.js` | Сторож каталога отказов: у каждого места отказа в исходниках есть пункт, у каждого пункта — объявленный совет, а отказы, отданные другой проверке, ею в самом деле утверждаются (названные файл и строка проверяются) |
604
614
  | `test/contract-data.test.js` | Контракт данных: числа против эталона, состав полей против производных, пометки приближения против подписи метрики |
605
615
  | `test/contract-derived.test.js` | Производные против чисел артефакта: итоги строки, дельты клетки и дельта итога — на коде, который лежит в дереве |
606
- | `test/page-view.test.js` | Собранная страница: вклейка без копий расчёта, самодостаточность, дерево файлов, состояния пустоты, оформление и переключатели |
616
+ | `test/page-view.test.js` | Собранная страница: вклейка без копий расчёта, самодостаточность, состояния пустоты, оформление и переключатели |
617
+ | `test/page-tree.test.js` | Дерево файлов панели: папки по путям проекта, три состояния, поддерево, файлы вне отчёта и прокрутка при пересборке |
607
618
  | `test/page-choice.test.js` | Память выбора и обмен ссылкой: перезаход, чужой отчёт, чужая и битая запись, смена адреса на открытой странице |
608
619
  | `test/module.test.js` | Модуль в расширении `.js`: измеряется без правок настроек; гард стриппера жив (доказано мутацией) и не обвиняет невиновного |
609
620
  | `test/guard.test.js` | Разбор модуля: идёт потоком, оба пути дают один вердикт, отступление работает без файла потока, сотни разборов дешевле запуска |
@@ -675,7 +686,7 @@ pnpm add -D @vernikr/size-report
675
686
  ```
676
687
 
677
688
  Пакет **опубликован в реестре**, и публично: `npm view @vernikr/size-report
678
- version` отвечает `2.1.0`, `npm access get status @vernikr/size-report` — `public`,
689
+ version` отвечает `2.2.0`, `npm access get status @vernikr/size-report` — `public`,
679
690
  а анонимный запрос тарболла — код 200. `npm i -D` и `yarn add -D` принимают то же
680
691
  имя; ни ключа, ни ссылки на репозиторий не нужно.
681
692
 
@@ -683,11 +694,11 @@ version` отвечает `2.1.0`, `npm access get status @vernikr/size-report`
683
694
  реестра, но остаётся привязанной к ревизии:
684
695
 
685
696
  ```bash
686
- pnpm add -D github:vernikr/size-report#v2.1.0
697
+ pnpm add -D github:vernikr/size-report#v2.2.0
687
698
  ```
688
699
 
689
700
  Без сети (или если тянуть из codeload нечем) — тарболл: `pnpm pack` в клоне
690
- пакета, затем `pnpm add -D ./vernikr-size-report-2.1.0.tgz`.
701
+ пакета, затем `pnpm add -D ./vernikr-size-report-2.2.0.tgz`.
691
702
 
692
703
  **Почему тег, а не sha.** Короткий sha pnpm разрешает только через видимые рефы, а
693
704
  `git ls-remote` отдаёт одни верхушки веток: пока ревизия — верхушка, короткий sha
@@ -695,7 +706,7 @@ pnpm add -D github:vernikr/size-report#v2.1.0
695
706
  <sha> to a commit`. Это не рассуждение, а проба: короткий пин `6530237` ставился,
696
707
  пока `main` стоял на нём, и перестал — на следующем же коммите, а тот же sha
697
708
  целиком поставился. Имя ветки (`#main`) или тег принимаются оба, но ветка —
698
- движущаяся цель, а тег постоянен: этот выпуск стоит на теге `v2.1.0`, он же и в
709
+ движущаяся цель, а тег постоянен: этот выпуск стоит на теге `v2.2.0`, он же и в
699
710
  примере (сорок знаков тоже годятся, но их придётся брать глазами из истории).
700
711
 
701
712
  Ревизия в примере — не украшение, а часть утверждения: она закреплена за тем, что
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vernikr/size-report",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "author": "vernikr",
5
5
  "repository": {
6
6
  "type": "git",
@@ -66,7 +66,7 @@
66
66
  "report"
67
67
  ],
68
68
  "devDependencies": {
69
- "@vernikr/size-report": "2.0.2",
69
+ "@vernikr/size-report": "2.1.0",
70
70
  "c8": "10",
71
71
  "dependency-cruiser": "17",
72
72
  "eslint": "^9.18.0",
package/src/data.js CHANGED
@@ -3,6 +3,7 @@ import { LOCALES } from './locales.js';
3
3
  import { metricView } from './metrics.js';
4
4
  import { rowHref } from './journal.js';
5
5
  import { build, skipLine } from './history.js';
6
+ import { projectTree } from './project.js';
6
7
  import { TOOL_PKG } from './tool.js';
7
8
 
8
9
  /* Категории файлов и контракт со страницей: абсолютные значения и устройство
@@ -66,6 +67,10 @@ export function reportData(cfg, root) {
66
67
  categories: CATEGORY_ORDER.filter((key) => files.some((f) => f.category === key))
67
68
  .map((key) => ({ key: key, label: loc.categories[key] })),
68
69
  files: files,
70
+ /* Дерево страницы — дерево проекта: все пути, а не только колонки. Числа есть
71
+ * лишь у колонок, поэтому у каждой записи каталога сказано, чего ей не
72
+ * досталось: `why` пусто — файл измеряется, иначе названа причина. */
73
+ catalog: projectTree(root, cfg.output, files.map((f) => (f.path === null ? f.paths[0] : f.path))),
69
74
  rows: rows.map((r) => Object.assign(rowShape(r),
70
75
  { href: rowHref(r.section, r.sha, cfg), values: r.cells })),
71
76
  now: state.map((s) => (s === null ? null : s.cells)),
package/src/locales.js CHANGED
@@ -19,6 +19,13 @@ export const LOCALES = {
19
19
  metrics: 'Метрики',
20
20
  files: 'Файлы',
21
21
  dir: 'все файлы папки {name} ({n})',
22
+ dirNone: 'ни один файл папки {name} в отчёт не попал (в папке {n})',
23
+ /* Причина — у листа: в дереве он подписью, а не галочкой, и без этих слов
24
+ * читатель решил бы, что файл потерялся. */
25
+ notMeasuredRule: 'не измеряется: такой файл колонкой быть не может'
26
+ + ' (собранный, замок зависимостей, сам отчёт или слишком крупный)',
27
+ notMeasuredChoice: 'не измеряется: в набор колонок отчёта не попал'
28
+ + ' — набор задают настройки (columns)',
22
29
  all: 'все',
23
30
  sub: '{tool} {version} · {artifact}',
24
31
  /* Слова точности: подпись метрики говорит про худшее в колонке, а клетки —
@@ -70,6 +77,11 @@ export const LOCALES = {
70
77
  metrics: 'Metrics',
71
78
  files: 'Files',
72
79
  dir: 'all files in “{name}” ({n})',
80
+ dirNone: 'no file of “{name}” made it into the report ({n} in it)',
81
+ notMeasuredRule: 'not measured: such a file cannot be a column'
82
+ + ' (built, a dependency lock, the report itself, or too large)',
83
+ notMeasuredChoice: 'not measured: it is not in the report’s set of columns'
84
+ + ' — that set comes from the settings (columns)',
73
85
  all: 'all',
74
86
  sub: '{tool} {version} · {artifact}',
75
87
  exact: 'an exact number',
package/src/page/app.css CHANGED
@@ -91,6 +91,10 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
91
91
  .box:hover { background: var(--tint); }
92
92
  .box input { margin: 0; accent-color: AccentColor; }
93
93
  .box.all { font-weight: 600; }
94
+ /* Файл вне отчёта: подпись на месте галочки — чисел для него не измеряли. Он
95
+ * приглушён и не отзывается на мышь: обещать нажатие нечем. */
96
+ .box.plain { cursor: default; opacity: .55; }
97
+ .box.plain:hover { background: none; }
94
98
 
95
99
  /* Расшифровки под деревом файлов нет намеренно: под списком она отодвигала
96
100
  * числа, а её содержимое и так стоит рядом с тем, что объясняет, — цвет дельт
package/src/page/build.js CHANGED
@@ -62,6 +62,7 @@ function uiText(page, loc) {
62
62
  metrics: page.metrics,
63
63
  files: page.files,
64
64
  dir: page.dir,
65
+ dirNone: page.dirNone,
65
66
  all: page.all,
66
67
  linkForeign: page.linkForeign,
67
68
  linkBroken: page.linkBroken,
@@ -71,6 +72,10 @@ function uiText(page, loc) {
71
72
  exact: page.exact,
72
73
  approximate: page.approximate,
73
74
  approxCell: page.approximateCell,
75
+ /* Почему файла нет в отчёте — словами: причину называет движок знаком (`why`),
76
+ * а страница одевает знак в текст, как и всё остальное в панели. */
77
+ notMeasuredRule: page.notMeasuredRule,
78
+ notMeasuredChoice: page.notMeasuredChoice,
74
79
  methodLabel: page.panelMethod,
75
80
  empty: page.emptyMetrics,
76
81
  noFiles: page.noFiles,
package/src/page/panel.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { appEl, appBox } from './dom.js';
2
- import { appData, appUi, appView, appFileAt } from './state.js';
2
+ import { appData, appUi, appView, appFileAt, appMeasured } from './state.js';
3
3
 
4
4
  /* Галочку файла ставит только файл: и категория, и папка в дереве — способы
5
5
  * переставить те же галочки сразу группой, а своего состояния у них нет. Иначе
@@ -14,56 +14,114 @@ function appFileBox(i) {
14
14
  });
15
15
  }
16
16
 
17
- /* Все файлы поддерева то, чем управляет переключатель папки. */
17
+ /* Файл, которого в отчёте нет: он стоит в дереве на своём месте, но галочки у
18
+ * него нет — чисел для него не измеряли, и переключать нечего. Причину читатель
19
+ * видит во всплывающей строке, а не догадывается по виду. */
20
+ function appUnmeasuredBox(entry) {
21
+ const box = appEl('span', 'box plain', entry.path.split('/').pop());
22
+ box.title = entry.path + ' · '
23
+ + (entry.why === 'rule' ? appUi.notMeasuredRule : appUi.notMeasuredChoice);
24
+ return box;
25
+ }
26
+
27
+ /* Все измеряемые файлы поддерева — то, чем управляет переключатель папки: файл
28
+ * вне отчёта переключать нечего. */
18
29
  function appIndexes(node) {
19
30
  const out = node.files.slice();
20
31
  node.dirs.forEach((sub) => { out.push(...appIndexes(sub)); });
21
32
  return out;
22
33
  }
23
34
 
24
- /* Узлы одного уровня: сперва папки по алфавиту, затем файлы в порядке данных.
25
- * Переключатель папки стоит над своим поддеревом и показывает три состояния: все
26
- * файлы включены, часть, ни одного. */
27
- function appTreeList(node) {
28
- const list = appEl('ul', 'tree');
29
- [...node.dirs.keys()].sort().forEach((name) => {
30
- const sub = node.dirs.get(name);
31
- const idx = appIndexes(sub);
35
+ /* Сколько файлов в поддереве вместе с теми, что в отчёт не попали. */
36
+ function appCount(node) {
37
+ let n = node.files.length + node.others.length;
38
+ node.dirs.forEach((sub) => { n += appCount(sub); });
39
+ return n;
40
+ }
41
+
42
+ /* Узел дерева: измеряемые файлы (колонки), прочие файлы проекта и подпапки. */
43
+ function appNode() {
44
+ return { files: [], others: [], dirs: new Map() };
45
+ }
46
+
47
+ /* Переключатель папки: его галочка ведёт за собой всё поддерево и показывает три
48
+ * состояния — все файлы включены, часть, ни одного. Рядом число файлов; если в
49
+ * папке есть и те, что вне отчёта, оно написано долей («2/5»): читателю важно, что
50
+ * в папке пять файлов, а измеряются два. Папка без единого измеряемого файла
51
+ * галочки не получает — включать в ней нечего, — но на месте остаётся. */
52
+ function appDirHead(name, sub) {
53
+ const idx = appIndexes(sub);
54
+ const total = appCount(sub);
55
+ const label = name + '/';
56
+ let head;
57
+ if (idx.length === 0) {
58
+ head = appEl('span', 'box dir plain', label);
59
+ head.title = appUi.dirNone.replace('{name}', name).replace('{n}', total);
60
+ } else {
32
61
  const on = idx.map((i) => appView.files[i]);
33
- const some = on.some((v) => v);
34
- const head = appBox(name + '/', appUi.dir.replace('{name}', name).replace('{n}', idx.length),
35
- some && on.every((v) => v), (e) => {
62
+ const every = on.every((v) => v);
63
+ head = appBox(label, appUi.dir.replace('{name}', name).replace('{n}', idx.length),
64
+ every, (e) => {
36
65
  idx.forEach((i) => { appView.files[i] = e.target.checked; });
37
66
  appRender();
38
67
  }, 'dir');
39
- head.querySelector('input').indeterminate = some && !on.every((v) => v);
40
- head.appendChild(appEl('span', 'n', idx.length));
68
+ head.querySelector('input').indeterminate = !every && on.some((v) => v);
69
+ }
70
+ head.appendChild(appEl('span', 'n', idx.length === total ? String(total) : idx.length + '/' + total));
71
+ return head;
72
+ }
73
+
74
+ /* Листья уровня: измеряемые файлы и файлы вне отчёта — вперемешку и по алфавиту
75
+ * имени, как в дереве файлов, а не отдельными списками. */
76
+ function appLeaves(node) {
77
+ const items = node.files.map((i) => ({ name: appData.files[i].label, i: i, entry: null }));
78
+ node.others.forEach((entry) => {
79
+ items.push({ name: entry.path.split('/').pop(), i: null, entry: entry });
80
+ });
81
+ return items.sort((a, b) => (a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)));
82
+ }
83
+
84
+ /* Узлы одного уровня: сперва папки по алфавиту, затем листья (их порядок — из
85
+ * `appLeaves`). */
86
+ function appTreeList(node) {
87
+ const list = appEl('ul', 'tree');
88
+ [...node.dirs.keys()].sort().forEach((name) => {
89
+ const sub = node.dirs.get(name);
41
90
  const li = appEl('li');
42
- li.appendChild(head);
91
+ li.appendChild(appDirHead(name, sub));
43
92
  li.appendChild(appTreeList(sub));
44
93
  list.appendChild(li);
45
94
  });
46
- node.files.forEach((i) => {
95
+ appLeaves(node).forEach((leaf) => {
47
96
  const li = appEl('li');
48
- li.appendChild(appFileBox(i));
97
+ li.appendChild(leaf.entry === null ? appFileBox(leaf.i) : appUnmeasuredBox(leaf.entry));
49
98
  list.appendChild(li);
50
99
  });
51
100
  return list;
52
101
  }
53
102
 
54
- /* Дерево файлов: путь делится по «/», папки становятся узлами, файлы — листьями.
55
- * Строится из тех же путей, что показаны в подписи файла, поэтому дерево и список
56
- * файлов не могут разойтись. */
103
+ /* Место листа в дереве: путь делится по «/», промежуточные папки заводятся по
104
+ * дороге. Одно место на измеряемые и прочие иначе они разошлись бы папками. */
105
+ function appLeafAt(node, p, i, entry) {
106
+ const parts = p.split('/');
107
+ for (let d = 0; d < parts.length - 1; d++) {
108
+ if (!node.dirs.has(parts[d])) node.dirs.set(parts[d], appNode());
109
+ node = node.dirs.get(parts[d]);
110
+ }
111
+ if (entry === null) node.files.push(i);
112
+ else node.others.push(entry);
113
+ }
114
+
115
+ /* Дерево страницы — дерево проекта: узлы берутся из каталога (все пути, которые
116
+ * видит git), поэтому в нём есть и файлы вне отчёта. Измеряемый лист — колонка, и
117
+ * путь у него тот же, что в подписи файла; колонка, чьего файла на HEAD уже нет, в
118
+ * каталог не попала (в индексе её нет) и стоит на месте по последнему известному
119
+ * пути. */
57
120
  function appTree() {
58
- const root = { files: [], dirs: new Map() };
59
- appData.files.forEach((f, i) => {
60
- const parts = (f.path === null ? f.paths[0] : f.path).split('/');
61
- let node = root;
62
- for (let d = 0; d < parts.length - 1; d++) {
63
- if (!node.dirs.has(parts[d])) node.dirs.set(parts[d], { files: [], dirs: new Map() });
64
- node = node.dirs.get(parts[d]);
65
- }
66
- node.files.push(i);
121
+ const root = appNode();
122
+ appData.files.forEach((_f, i) => appLeafAt(root, appFileAt(i), i, null));
123
+ appData.catalog.forEach((entry) => {
124
+ if (appMeasured[entry.path] === undefined) appLeafAt(root, entry.path, null, entry);
67
125
  });
68
126
  return appTreeList(root);
69
127
  }
package/src/page/state.js CHANGED
@@ -35,6 +35,13 @@ appData.files.forEach(() => { appView.files.push(true); });
35
35
  export const appMetric = {};
36
36
  appData.metrics.forEach((m) => { appMetric[m.key] = m; });
37
37
 
38
+ /* Колонка по пути файла: дерево страницы — дерево проекта (все пути каталога), а
39
+ * числа есть только у колонок, поэтому лист дерева по этому указателю и решает,
40
+ * галочка он или подпись. Имя берётся тем же правилом, что у записи выбора
41
+ * (`appFileAt`), — дерево и память читателя разойтись не могут. */
42
+ export const appMeasured = {};
43
+ appData.files.forEach((_f, i) => { appMeasured[appFileAt(i)] = i; });
44
+
38
45
  /* Ссылка — это тот же выбор в адресе, под своим именем: чужой якорь страницы
39
46
  * ссылкой не считается, и спорить с ним нечем. */
40
47
  const APP_LINK = '#size-report=';
package/src/project.js CHANGED
@@ -239,6 +239,32 @@ export function projectConfig(root) {
239
239
  };
240
240
  }
241
241
 
242
+ /* Путь самого отчёта — в проекте ли он назван (не пустой, не абсолютный и не с
243
+ * выходом наверх): только такой путь дереву можно назвать листом. */
244
+ const ownPath = (output) => output !== '' && !path.isAbsolute(output) && output.indexOf('..') !== 0;
245
+
246
+ /* Каталог проекта для страницы: все пути, которые видит git, и причина у тех из
247
+ * них, что колонкой не стали (`null` — стал: страница по этому знаку решает, лист
248
+ * — галочка или подпись). Дерево страницы — дерево проекта, поэтому пути берутся
249
+ * из индекса, а не из колонок; причины — **теми же** правилами, по которым профиль
250
+ * выбирает колонки (`generated` и предел размера), иначе подсказка говорила бы
251
+ * одно, а выбор колонок делал другое. Колонка, чьего файла на HEAD уже нет, в
252
+ * каталог не попадает: в индексе её нет, а на месте в дереве её держит страница.
253
+ *
254
+ * Сам отчёт в каталоге есть всегда — и пока он ещё не собран, и когда он вне git:
255
+ * его отслеживаемость — свойство момента, а не проекта. Зависеть от неё отчёт не
256
+ * должен: иначе первая же пересборка в свежем клоне даёт другие байты (отчёт в
257
+ * каталоге появился), и хук коммитит его второй раз на пустом месте. */
258
+ export function projectTree(root, output, measured) {
259
+ const files = indexFiles(root);
260
+ if (ownPath(output) && files.every((f) => f.p !== output)) files.push({ p: output, size: 0 });
261
+ return files.sort((a, b) => (a.p < b.p ? -1 : 1)).map((f) => ({
262
+ path: f.p,
263
+ why: measured.indexOf(f.p) >= 0 ? null
264
+ : (generated(f.p, output) || f.size > MAX_BYTES ? 'rule' : 'choice')
265
+ }));
266
+ }
267
+
242
268
  /* Имя черновика (`sniffColumns`) осталось публичным: на него опираются те, кто звал
243
269
  * вывод колонок (`test/api.test.js` держит список имён), и форма ответа та же —
244
270
  * колонки, знакомые расширения проекта и сколько путей всего. */
package/src/size-table.js CHANGED
@@ -63,10 +63,10 @@
63
63
  * strip → refusal, parse — снятие балласта и гард;
64
64
  * metrics → strip — реестр метрик;
65
65
  * git → refusal — всё, что читается у git;
66
- * project → git, refusal, data — что проект говорит о себе сам;
66
+ * project → git, refusal — что проект говорит о себе сам;
67
67
  * config → project, git, refusal, locales, metrics, data — настройки проекта;
68
68
  * history → git, metrics, journal, refusal — сборка по истории;
69
- * data → locales, metrics, journal, history, tool — контракт со страницей;
69
+ * data → locales, metrics, journal, history, project, tool — контракт со страницей;
70
70
  * page/build → locales, css — отчёт одним файлом;
71
71
  * artifact → data, page/build — запись отчёта;
72
72
  * modes → почти все — что делать по запросу;