@vernikr/size-report 2.2.0 → 2.3.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,59 @@
18
18
  где за основу взяты `fixtures/synthetic/config.json`, метрики — `raw`, `min`, `tok`, а
19
19
  способ минификации — `strip` или `esbuild`.
20
20
 
21
+ ## 2.3.0 — 2026-09-15
22
+
23
+ Отчёт описывает проект целиком: колонкой идёт каждый отслеживаемый файл, а дерево
24
+ складывается.
25
+
26
+ - **Колонками была выборка из проекта.** Без файла настроек инструмент брал
27
+ двенадцать крупнейших файлов — по одному от каждого расширения — и называл
28
+ прочие исключениями. Отчёт при этом считал «общий объём» по горсти файлов, и
29
+ читатель принимал это за объём проекта (в этом репозитории — 12 файлов из 148).
30
+ Теперь колонка — **каждый отслеживаемый git файл, который можно измерить**, и
31
+ `skip` называет только то, что колонкой быть не может: сам отчёт, замки
32
+ зависимостей, карты, собранное, незнакомый формат, файл сверх 512 КБ — и путь,
33
+ которого на HEAD нет (он живёт только в истории: измерять в нём нечего).
34
+ - **Цена названа замером** (этот же репозиторий, 149 коммитов, 135 колонок против
35
+ 12): сборка отчёта 2,4 с → **6,3 с**, файл отчёта 122 КБ → **417 КБ**, `check` —
36
+ 6,1 с. Отчёт пересобирает хук после каждого коммита, поэтому столько теперь стоит
37
+ и каждый коммит; кому это дорого — колонки задают файлом настроек (`size --init`
38
+ даёт черновик, дальше его правят: прежний выбор воспроизводится списком из
39
+ двенадцати файлов).
40
+ - **Дерево складывается.** У каждой папки свой знак (▾/▸): он отвечает за то,
41
+ сколько дерева видно, а галочка — за то, что считается, и потому это две разные
42
+ цели нажатия, а не одна. Сложенное помнится между заходами и **не уезжает в
43
+ ссылку**: ссылку отправляют ради чисел, а не ради того, как у кого разложено
44
+ дерево. Запись у складывания своя (тот же паспорт отчёта), разворот всех папок её
45
+ убирает — как и возврат галочек. На 148 путях проекта это единственный способ
46
+ добраться до его середины.
47
+ - **Вклейка программы страницы получила сторож.** Модульный синтаксис снимается
48
+ построчно, поэтому многострочный `import` оставлял в странице хвост и ломал её
49
+ целиком — теперь проверка ищет в вклеенной программе и имя модуля, а не только
50
+ начало строки.
51
+
52
+ ### Что изменится в числах
53
+
54
+ **Колонка — каждый отслеживаемый файл, поэтому там, где настройки не заданы, колонок
55
+ станет больше** (в этом репозитории 12 → 135), а вместе с ними вырастут «общий объём»
56
+ (он и раньше считался по колонкам) и время прогона — оно названо выше. Числа ниже
57
+ замерены на фикстуре, где колонки заданы файлом настроек: такой проект замер видит
58
+ точно так же, как 2.2.0, — до последней клетки.
59
+
60
+ | Файл | raw | min со strip | min с esbuild | tok |
61
+ |---|---|---|---|---|
62
+ | code.js | 735 | 276 | 185 | 168 |
63
+ | modern.js | 246 | 51 | 45 | 44 |
64
+ | config.mjs | 172 | 45 | 40 | 40 |
65
+ | заметки.md | 306 | 303 | 303 | 53 |
66
+ | crlf.txt | 63 | 60 | 60 | 10 |
67
+ | package.json | 87 | 69 | 69 | 34 |
68
+ | style.css | 156 | 55 | 43 | 38 |
69
+ | table.toml | 300 | 299 | 299 | 52 |
70
+ | empty.js | 0 | 0 | 0 | 0 |
71
+ | WORKLOG.md | 446 | 439 | 439 | 101 |
72
+ | **ИТОГО** | **2511** | **1597** | **1483** | **540** |
73
+
21
74
  ## 2.2.0 — 2026-09-15
22
75
 
23
76
  Дерево страницы — дерево проекта: видно всё, числа есть у измеряемых.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## Статус
12
12
 
13
- **Выпуск 2.2.0 (2026-09-15).** Инструмент живёт отдельным пакетом: имя в
13
+ **Выпуск 2.3.0 (2026-09-15).** Инструмент живёт отдельным пакетом: имя в
14
14
  реестре — `@vernikr/size-report` (публикуется тегом из CI, без секрета). Настроек
15
15
  проект может не заводить вовсе: без файла инструмент выводит их из самого проекта и
16
16
  говорит об этом строкой, а `--init` закрепляет выведенное файлом (чем этот шаг
@@ -21,10 +21,11 @@
21
21
  Версия — в манифесте, а у выпуска есть `CHANGELOG.md` с разделом «Что изменится
22
22
  в числах»:
23
23
  таблица чисел в нём не пересказ, а замер на фикстуре, который сверяется с живым
24
- прогоном (`test/changelog.test.js`). Числа этим выпуском не меняются — правки
25
- лежат в виде страницы отчёта на десктопе и в том, что видно в её дереве; `schema: 1`
26
- данных, замороженная 1.0.0, остаётся той же (выпуск добавил поле, а не поменял
27
- смысл прежних).
24
+ прогоном (`test/changelog.test.js`). Числа на фикстуре этим выпуском не меняются —
25
+ правки в том, **что именно считается**: без файла настроек колонкой идёт каждый
26
+ отслеживаемый файл не выборка из двенадцати), и в дереве панели теперь
27
+ складываются папки; `schema: 1` данных, замороженная 1.0.0, остаётся той же (выпуск
28
+ добавил поле, а не поменял смысл прежних).
28
29
 
29
30
  **Шаг 1 плана пройден — перенос без изменения поведения.** Команда —
30
31
  `bin/size.js`, точка входа пакета — `src/size-table.js` (только реэкспорт),
@@ -223,10 +224,10 @@
223
224
 
224
225
  | Прогон | Команда | Проверок |
225
226
  |---|---|---|
226
- | Быстрый — каждая правка | `pnpm test` | **68 из 172** |
227
- | Полный — выкладка и CI | `pnpm test:all` | **172** |
227
+ | Быстрый — каждая правка | `pnpm test` | **69 из 174** |
228
+ | Полный — выкладка и CI | `pnpm test:all` | **174** |
228
229
 
229
- Ни одна проверка не потеряна и не ослаблена: полный прогон запускает все 172 теми же
230
+ Ни одна проверка не потеряна и не ослаблена: полный прогон запускает все 174 теми же
230
231
  файлами, а быстрый берёт их часть. Умолчание — полный: файл становится быстрым только
231
232
  явно и с причиной, поэтому новое дорогое не может тихо уехать в быстрый. Стерегут это
232
233
  объявление `test/suites.test.js` (полнота классификации и причина у каждого файла) и
@@ -319,7 +320,7 @@ deльт задан один раз и по артефакту: рост зел
319
320
  строки в `src/table.css` плюс пересборка эталона артефакта, больше цвета нигде нет.
320
321
 
321
322
  **Левая панель страницы — дерево файлов проекта** (`REFACTOR.md` R-2.4, каталог
322
- путей — `CHANGELOG.md` 2.2.0). Дерево строится по путям проекта, а не по одним
323
+ путей — `CHANGELOG.md` 2.2.0, складывание папок — 2.3.0). Дерево строится по путям проекта, а не по одним
323
324
  колонкам, поэтому в нём видно и то, что в отчёт не попало: такой лист — подпись без
324
325
  галочки с причиной во всплывающей строке («колонкой быть не может» — правило пакета
325
326
  — или «в набор колонок не попал» — выбор проекта), а счётчик папки со смешанным
@@ -329,11 +330,16 @@ deльт задан один раз и по артефакту: рост зел
329
330
  папки три состояния — все её файлы включены, часть, ни одного, — и переключатель
330
331
  папки ведёт за собой всё поддерево; рядом стоит число файлов. Своего состояния у
331
332
  папки и у быстрой кнопки категории нет: обе переставляют галочки файлов, поэтому
332
- дерево, кнопки и таблица не могут разойтись. Список файлов длиннее панели
333
+ дерево, кнопки и таблица не могут разойтись. У каждой папки есть ещё свой знак
334
+ (▾/▸): он отвечает за то, сколько дерева видно, — это дело смотрящего, а не выбор
335
+ читателя, поэтому знак помнится между заходами и в ссылку не идёт (у записи свой
336
+ ключ и тот же паспорт отчёта; разворот всех папок её убирает, как возврат галочек —
337
+ запись выбора). Список файлов длиннее панели
333
338
  прокручивается, а не выталкивает таблицу, — и прокрутка эта одна: на широком
334
- экране листается панель целиком (дерево в ней видно целиком: 23 подписи из 23 при
335
- окне 1440×900), а на узком — сам список, потому что там панель растёт вместе со
336
- страницей.
339
+ экране листается панель целиком (при окне 1440×900 страница укладывается в окно, а
340
+ таблица берёт всю оставшуюся высоту), а на узком — сам список, потому что там
341
+ панель растёт вместе со страницей. Дерево стало длинным (в этом репозитории 148
342
+ подписей), поэтому папки и складываются: иначе до его середины не добраться.
337
343
 
338
344
  **На широком экране панель стоит слева от таблицы и не вытесняет числа**
339
345
  (от 900 px, `src/page/app.css`). Это не украшение: на десктопе бокового места
@@ -561,9 +567,9 @@ deльт задан один раз и по артефакту: рост зел
561
567
  | `src/css.js` | Чтение оформления с диска: какие наборы стилей есть и какая у них роль |
562
568
  | `src/table.css` | Таблица отчёта: геометрия клеток, липкие шапка и колонка, цвет дельт |
563
569
  | `src/page/app.css` | Оформление страницы сверх общей части: панель с деревом файлов и липкой строкой категорий (на широком экране — колонка слева, страница в окно), состояния пустоты, узкое окно |
564
- | `src/page/state.js` | Состояние страницы: данные отчёта, вид галочек, указатель «какой путь — какая колонка», паспорт записи, память браузера и обмен ссылкой — глава программы страницы |
570
+ | `src/page/state.js` | Состояние страницы: данные отчёта, вид галочек, указатель «какой путь — какая колонка», сложенные папки, паспорт записи, память браузера и обмен ссылкой — глава программы страницы |
565
571
  | `src/page/dom.js` | Узлы страницы: мелкие помощники разметки (`appEl`, `appBox`) — одни на панель и таблицу |
566
- | `src/page/panel.js` | Панель выбора: галочки метрик и файлов, категории, дерево путей проекта (файлы вне отчёта — подписью с причиной); перерисовку просит у главы сборки |
572
+ | `src/page/panel.js` | Панель выбора: галочки метрик и файлов, категории, дерево путей проекта (файлы вне отчёта — подписью с причиной, папки — со знаком складывания); перерисовку просит у главы сборки |
567
573
  | `src/page/table.js` | Таблица страницы: клетка, подпись коммита, шапка и состояния пустоты — разметка поверх общего расчёта |
568
574
  | `src/page/app.js` | Сборка и запуск страницы: таблица целиком, перерисовка по выбору читателя (с возвратом фокуса и прокрутки), первая отрисовка и смена якоря; вклеивается в собранную страницу
569
575
  | `src/page/build.js` | Сборка страницы: данные, оформление и программа в одном файле без внешних ссылок |
@@ -686,7 +692,7 @@ pnpm add -D @vernikr/size-report
686
692
  ```
687
693
 
688
694
  Пакет **опубликован в реестре**, и публично: `npm view @vernikr/size-report
689
- version` отвечает `2.2.0`, `npm access get status @vernikr/size-report` — `public`,
695
+ version` отвечает `2.3.0`, `npm access get status @vernikr/size-report` — `public`,
690
696
  а анонимный запрос тарболла — код 200. `npm i -D` и `yarn add -D` принимают то же
691
697
  имя; ни ключа, ни ссылки на репозиторий не нужно.
692
698
 
@@ -694,11 +700,11 @@ version` отвечает `2.2.0`, `npm access get status @vernikr/size-report`
694
700
  реестра, но остаётся привязанной к ревизии:
695
701
 
696
702
  ```bash
697
- pnpm add -D github:vernikr/size-report#v2.2.0
703
+ pnpm add -D github:vernikr/size-report#v2.3.0
698
704
  ```
699
705
 
700
706
  Без сети (или если тянуть из codeload нечем) — тарболл: `pnpm pack` в клоне
701
- пакета, затем `pnpm add -D ./vernikr-size-report-2.2.0.tgz`.
707
+ пакета, затем `pnpm add -D ./vernikr-size-report-2.3.0.tgz`.
702
708
 
703
709
  **Почему тег, а не sha.** Короткий sha pnpm разрешает только через видимые рефы, а
704
710
  `git ls-remote` отдаёт одни верхушки веток: пока ревизия — верхушка, короткий sha
@@ -706,7 +712,7 @@ pnpm add -D github:vernikr/size-report#v2.2.0
706
712
  <sha> to a commit`. Это не рассуждение, а проба: короткий пин `6530237` ставился,
707
713
  пока `main` стоял на нём, и перестал — на следующем же коммите, а тот же sha
708
714
  целиком поставился. Имя ветки (`#main`) или тег принимаются оба, но ветка —
709
- движущаяся цель, а тег постоянен: этот выпуск стоит на теге `v2.2.0`, он же и в
715
+ движущаяся цель, а тег постоянен: этот выпуск стоит на теге `v2.3.0`, он же и в
710
716
  примере (сорок знаков тоже годятся, но их придётся брать глазами из истории).
711
717
 
712
718
  Ревизия в примере — не украшение, а часть утверждения: она закреплена за тем, что
@@ -739,8 +745,10 @@ pnpm exec size --init # закрепить выведенное в size-tab
739
745
  ```
740
746
 
741
747
  Начинать с настроек не нужно: без файла инструмент выводит их из проекта — колонками
742
- берёт крупнейшие файлы, по одному от каждого расширения (иначе отчёт состоял бы из
743
- одних крупных `.md`, и ни один `.js` не попал бы под настоящее сжатие), журналом —
748
+ берёт **каждый отслеживаемый git файл, который можно измерить** (отчёт называет
749
+ объём проекта, а не выборки из него; границы остались только у того, что колонкой
750
+ быть не может: сам отчёт, замки зависимостей, собранное, незнакомый формат и файл
751
+ сверх 512 КБ), а прочие называет в `skip`, журналом —
744
752
  первый знакомый (`WORKLOG.md`, `CHANGELOG.md`, …), файлом отчёта — `docs/`, если
745
753
  каталог есть, командой починки — объявленный скрипт `sizes`, а без него — путь
746
754
  к установленному пакету (его цитируют подпись отчёта и отказы, поэтому он обязан
@@ -750,8 +758,9 @@ pnpm exec size --init # закрепить выведенное в size-tab
750
758
  этих необязательных зависимостей метрика честно отступает к другому счёту и прогон
751
759
  отдаёт код 4 — правки настроек и тут не требуются.
752
760
 
753
- Всё, что колонкой быть не может или в неё не поместилось (сам отчёт, замки
754
- зависимостей, карты, собранное), называется в `skip` поэтому первый же `size check`
761
+ Всё, что колонкой быть не может (сам отчёт, замки зависимостей, карты, собранное,
762
+ незнакомый формат, слишком крупный файл) и чего git не отслеживает, называется
763
+ в `skip` — поэтому первый же `size check`
755
764
  полон, а не красен: «пути мимо колонок» появляются от новых правок, а не от того, что
756
765
  проект ещё не описан. О том, что настройки выведены, инструмент говорит строкой в
757
766
  stderr и называет команду, которая их закрепляет, — `--init`; закреплённое проходит
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vernikr/size-report",
3
- "version": "2.2.0",
3
+ "version": "2.3.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.1.0",
69
+ "@vernikr/size-report": "2.2.0",
70
70
  "c8": "10",
71
71
  "dependency-cruiser": "17",
72
72
  "eslint": "^9.18.0",
package/src/locales.js CHANGED
@@ -20,6 +20,10 @@ export const LOCALES = {
20
20
  files: 'Файлы',
21
21
  dir: 'все файлы папки {name} ({n})',
22
22
  dirNone: 'ни один файл папки {name} в отчёт не попал (в папке {n})',
23
+ /* Знак сложенной папки — своя цель нажатия: галочка отвечает за числа,
24
+ * знак — за то, сколько дерева видно. */
25
+ foldClose: 'свернуть папку {name}',
26
+ foldOpen: 'развернуть папку {name}',
23
27
  /* Причина — у листа: в дереве он подписью, а не галочкой, и без этих слов
24
28
  * читатель решил бы, что файл потерялся. */
25
29
  notMeasuredRule: 'не измеряется: такой файл колонкой быть не может'
@@ -78,6 +82,8 @@ export const LOCALES = {
78
82
  files: 'Files',
79
83
  dir: 'all files in “{name}” ({n})',
80
84
  dirNone: 'no file of “{name}” made it into the report ({n} in it)',
85
+ foldClose: 'collapse “{name}”',
86
+ foldOpen: 'expand “{name}”',
81
87
  notMeasuredRule: 'not measured: such a file cannot be a column'
82
88
  + ' (built, a dependency lock, the report itself, or too large)',
83
89
  notMeasuredChoice: 'not measured: it is not in the report’s set of columns'
package/src/page/app.css CHANGED
@@ -71,12 +71,27 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
71
71
  * кто и вправду ограничен окном. Шрифт файлов — тот же, что у чисел таблицы:
72
72
  * подписей в списке много и они короткие, а рядом с ними стоит таблица. */
73
73
  .panel .files { max-height: min(30vh, 320px); overflow: auto; font-size: 12.5px; }
74
- .panel .tree { margin: 0; padding: 0; list-style: none; }
74
+ .panel .tree { margin: 0; padding: 0 0 0 14px; list-style: none; }
75
75
  .panel .tree .tree { margin-left: 14px; padding-left: 9px; border-left: 1px solid var(--line); }
76
76
  .panel .tree .box { padding: 1px 7px 1px 5px; }
77
77
  .box.dir { font-weight: 600; }
78
78
  .box .n { margin-left: 1px; color: var(--muted); font-size: 11px; }
79
79
 
80
+ /* Знак сложенной папки: место под него есть у каждой строки (отступ строки), а сам
81
+ * он только у папок — так листья и папки выстроены в один столбец. Он не часть
82
+ * галочки: галочка отвечает за числа, знак — за то, сколько дерева видно. */
83
+ .panel .tree li { position: relative; }
84
+ .panel .tree li > .fold {
85
+ position: absolute;
86
+ left: -13px;
87
+ width: 14px;
88
+ text-align: center;
89
+ color: var(--muted);
90
+ cursor: pointer;
91
+ user-select: none;
92
+ }
93
+ .panel .tree li > .fold:hover { color: var(--ink); }
94
+
80
95
  /* Переключатель — метка вокруг поля ввода: и подпись, и цель нажатия одна, поэтому
81
96
  * по нему попадает и мышь, и клавиатура (Space на поле ввода), и вспомогательные
82
97
  * технологии. */
package/src/page/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import { appData, appUi, appView, appWrite, appNotice, appLinkUse, appRead, appApply } from './state.js';
1
+ import { appData, appUi, appView, appWrite, appNotice, appLinkUse, appRead, appApply, appFoldRead } from './state.js';
2
2
  import { appBody, appHead, appState } from './table.js';
3
3
  import { appPanel } from './panel.js';
4
4
 
@@ -76,6 +76,10 @@ if (appStart !== 'ours') {
76
76
  const appSaved = appRead();
77
77
  if (appSaved !== null) appApply(appSaved);
78
78
  }
79
+ /* Сложенное дерево — память смотрящего, а не выбор читателя: она возвращается и
80
+ * тогда, когда открыта чужая ссылка (иначе присланная ссылка разложила бы дерево
81
+ * заново на каждом заходе). */
82
+ appFoldRead();
79
83
  appRender(true);
80
84
  appStartup = false;
81
85
  appForeign = false;
package/src/page/build.js CHANGED
@@ -63,6 +63,8 @@ function uiText(page, loc) {
63
63
  files: page.files,
64
64
  dir: page.dir,
65
65
  dirNone: page.dirNone,
66
+ foldClose: page.foldClose,
67
+ foldOpen: page.foldOpen,
66
68
  all: page.all,
67
69
  linkForeign: page.linkForeign,
68
70
  linkBroken: page.linkBroken,
package/src/page/panel.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { appEl, appBox } from './dom.js';
2
- import { appData, appUi, appView, appFileAt, appMeasured } from './state.js';
2
+ import { appData, appUi, appView, appFileAt, appFoldSet, appMeasured } from './state.js';
3
3
 
4
4
  /* Галочку файла ставит только файл: и категория, и папка в дереве — способы
5
5
  * переставить те же галочки сразу группой, а своего состояния у них нет. Иначе
@@ -71,6 +71,20 @@ function appDirHead(name, sub) {
71
71
  return head;
72
72
  }
73
73
 
74
+ /* Знак папки — своя цель нажатия, отдельная от галочки: галочка отвечает за числа
75
+ * (включает файлы поддерева), а знак — за то, сколько дерева видно. Одна цель на
76
+ * два разных действия означала бы, что сложить папку можно только вместе с
77
+ * включением её файлов. Знак нарисован спаном, а не кнопкой, потому что в строке
78
+ * папки рядом уже стоит метка-галочка, а вложенная в метку кнопка поднимала бы её
79
+ * же нажатие. */
80
+ function appFoldBox(name, path) {
81
+ const folded = appView.folded[path] === true;
82
+ const box = appEl('span', 'fold', folded ? '▸' : '▾');
83
+ box.title = (folded ? appUi.foldOpen : appUi.foldClose).replace('{name}', name);
84
+ box.addEventListener('click', () => { appFoldSet(path, !folded); appRender(); });
85
+ return box;
86
+ }
87
+
74
88
  /* Листья уровня: измеряемые файлы и файлы вне отчёта — вперемешку и по алфавиту
75
89
  * имени, как в дереве файлов, а не отдельными списками. */
76
90
  function appLeaves(node) {
@@ -82,14 +96,18 @@ function appLeaves(node) {
82
96
  }
83
97
 
84
98
  /* Узлы одного уровня: сперва папки по алфавиту, затем листья (их порядок — из
85
- * `appLeaves`). */
86
- function appTreeList(node) {
99
+ * `appLeaves`). Сложенная папка — это та, у которой нет самого списка: прятать
100
+ * поддерево оформлением значило бы держать в разметке то, чего не видно, и
101
+ * пересобирать её на каждый клик по знаку. */
102
+ function appTreeList(node, prefix) {
87
103
  const list = appEl('ul', 'tree');
88
104
  [...node.dirs.keys()].sort().forEach((name) => {
89
105
  const sub = node.dirs.get(name);
106
+ const here = prefix === '' ? name : prefix + '/' + name;
90
107
  const li = appEl('li');
108
+ li.appendChild(appFoldBox(name, here));
91
109
  li.appendChild(appDirHead(name, sub));
92
- li.appendChild(appTreeList(sub));
110
+ if (appView.folded[here] !== true) li.appendChild(appTreeList(sub, here));
93
111
  list.appendChild(li);
94
112
  });
95
113
  appLeaves(node).forEach((leaf) => {
@@ -123,7 +141,7 @@ function appTree() {
123
141
  appData.catalog.forEach((entry) => {
124
142
  if (appMeasured[entry.path] === undefined) appLeafAt(root, entry.path, null, entry);
125
143
  });
126
- return appTreeList(root);
144
+ return appTreeList(root, '');
127
145
  }
128
146
 
129
147
  export function appPanel() {
package/src/page/state.js CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  export const appData = JSON.parse(document.getElementById('data').textContent);
27
27
  export const appUi = JSON.parse(document.getElementById('ui').textContent);
28
- export const appView = { metrics: {}, files: [] };
28
+ export const appView = { metrics: {}, files: [], folded: {} };
29
29
  appData.metrics.forEach((m) => { appView.metrics[m.key] = true; });
30
30
  appData.files.forEach(() => { appView.files.push(true); });
31
31
 
@@ -231,3 +231,44 @@ export function appApply(rec) {
231
231
  appData.metrics.forEach((m) => { if (metrics[m.key] === false) appView.metrics[m.key] = false; });
232
232
  appData.files.forEach((_f, i) => { if (files[appFileAt(i)] === false) appView.files[i] = false; });
233
233
  }
234
+
235
+ /* -------- сложенное дерево -------- */
236
+
237
+ /* Сложенные папки — память того же рода, что выбор, но своей записи: она про то,
238
+ * сколько дерева видно, а не про то, какие числа читают. Поэтому в адрес она не
239
+ * идёт: ссылку отправляют ради чисел, а разложенное дерево — дело смотрящего. Как и
240
+ * у выбора, здесь помнится только сложенное (`true`), а имя папки — это путь
241
+ * («src/page»), поэтому исчезнувшее имя просто ничего не значит. */
242
+ const appFoldKey = appKey + ':tree';
243
+
244
+ export function appFoldRead() {
245
+ let text = null;
246
+ try {
247
+ text = window.localStorage.getItem(appFoldKey);
248
+ } catch (_e) {
249
+ return;
250
+ }
251
+ if (text === null) return;
252
+ let rec = null;
253
+ try {
254
+ rec = JSON.parse(text);
255
+ } catch (_e) {
256
+ return;
257
+ }
258
+ if (!appRecordOk(rec)) return;
259
+ const folded = rec.folded || {};
260
+ Object.keys(folded).forEach((p) => { if (folded[p] === true) appView.folded[p] = true; });
261
+ }
262
+
263
+ export function appFoldSet(path, folded) {
264
+ if (folded) appView.folded[path] = true;
265
+ else delete appView.folded[path];
266
+ const rec = { v: 1, passport: appPassport(), folded: Object.assign({}, appView.folded) };
267
+ try {
268
+ if (Object.keys(rec.folded).length === 0) window.localStorage.removeItem(appFoldKey);
269
+ else window.localStorage.setItem(appFoldKey, JSON.stringify(rec));
270
+ } catch (_e) {
271
+ /* Памяти нет: сложенное не переживёт закрытия страницы, а вид от этого не
272
+ * зависит — дерево сложено ровно так, как его сложил читатель сейчас. */
273
+ }
274
+ }
package/src/project.js CHANGED
@@ -18,15 +18,16 @@ import { cliCommand, invocation } from './refusal.js';
18
18
  *
19
19
  * **Колонка — это файл.** Состояние движка хранит на колонку один путь, а список
20
20
  * путей колонки — её переименования (в ревизии берётся тот, который в ней есть).
21
- * Поэтому «папка целиком» колонкой быть не может, и профиль выбирает файлы, а не
22
- * группы путей.
21
+ * Поэтому «папка целиком» колонкой быть не может, и профиль называет файлы, а не
22
+ * группы путей. Колонкой идёт **каждый** отслеживаемый файл, который можно
23
+ * измерить: выборка из проекта выдавала объём выборки за объём проекта.
23
24
  *
24
25
  * **Профиль обязан проходить ту же проверку, которой его встретит первый запуск**
25
26
  * (BLOCKERS §N2, REFACTOR R-0.4): путь, не ставший колонкой и не объявленный
26
27
  * исключением, — это код 1 на первом же прогоне. Поэтому `skip` называет и то,
27
- * что колонкой быть не может (сам отчёт, замки зависимостей, карты, собранное), и
28
- * то, что в колонки не поместилось: так первый `check` зелёный, а что не попало в
29
- * отчёт сказано в файле, а не умолчано.
28
+ * что колонкой быть не может (сам отчёт, замки зависимостей, собранное, незнакомый
29
+ * формат, слишком крупный файл), и то, чего git не отслеживает: так первый `check`
30
+ * зелёный, а про каждый файл вне отчёта сказано, почему его там нет.
30
31
  */
31
32
 
32
33
  /* Что вообще берётся колонкой: текстовые формы, с которыми движок умеет работать.
@@ -41,11 +42,6 @@ const JOURNALS = ['WORKLOG.md', 'CHANGELOG.md', 'CHANGES.md', 'HISTORY.md'];
41
42
  * сгенерирован, и в отчёте он перевесил бы весь проект. */
42
43
  const MAX_BYTES = 512 * 1024;
43
44
 
44
- /* Сколько файлов идут колонками: отчёт читают глазами, а сотня колонок читается
45
- * хуже, чем скачок. Остальное остаётся на виду — в `skip`, откуда его берут
46
- * колонкой, когда оно важно. */
47
- const COLUMN_LIMIT = 12;
48
-
49
45
  /* Индекс — путь и размер: `ls-files -s` даёт объекты, размер спрашивается у них
50
46
  * одной пачкой (`cat-file --batch-check`), а не чтением содержимого. */
51
47
  function indexFiles(root) {
@@ -173,11 +169,17 @@ function labelFor(used, p) {
173
169
  return p + ' (' + n + ')';
174
170
  }
175
171
 
176
- /* Выбор колонок: по кругу от каждого расширения берётся его крупнейший файл иначе
177
- * в отчёте оказались бы одни крупные `.md`, и ни один `.js` не попал бы под
178
- * настоящее сжатие (первый отчёт нового проекта обязан быть точным, а не
179
- * приближённым). Порядок круга по весу расширения: сперва то, чего в проекте
180
- * больше всего. */
172
+ /* Выбор колонок: колонкой идёт всё, что можно измерить, то есть каждый файл
173
+ * проекта, а не выборка из него. Выборка (прежде крупнейшие по одному от каждого
174
+ * расширения, всего не больше двенадцати) врала дважды: отчёт называл объём файлов,
175
+ * которых в нём не было, а читатель принимал это за объём проекта. Границы остались
176
+ * только у того, что колонкой **не может** быть: сам отчёт, замки, собранное,
177
+ * незнакомый формат и слишком крупный файл (`generated` и `MAX_BYTES`) — они уходят
178
+ * в `skip` и называются там.
179
+ *
180
+ * Порядок колонок — по кругу от каждого расширения, от крупнейшего к мелкому:
181
+ * сперва то, чего в проекте больше всего, и в каждом расширении крупное раньше
182
+ * мелкого. Это порядок чтения отчёта, а не вес: числа от него не зависят. */
181
183
  function columnsOf(files, journal) {
182
184
  const byExt = new Map();
183
185
  files.forEach((f) => {
@@ -194,11 +196,11 @@ function columnsOf(files, journal) {
194
196
  used.push(columns[columns.length - 1].label);
195
197
  };
196
198
  if (journal !== '' && files.some((f) => f.p === journal)) take(journal);
197
- for (let round = 0; columns.length < COLUMN_LIMIT; round++) {
199
+ for (let round = 0; columns.length < files.length; round++) {
198
200
  const before = columns.length;
199
201
  rings.forEach((ring) => {
200
202
  const f = ring[round];
201
- if (f !== undefined && columns.length < COLUMN_LIMIT && f.p !== journal) take(f.p);
203
+ if (f !== undefined && f.p !== journal) take(f.p);
202
204
  });
203
205
  if (columns.length === before) break;
204
206
  }
@@ -209,7 +211,11 @@ export function projectConfig(root) {
209
211
  const output = outputOf(root);
210
212
  const journal = journalOf(root);
211
213
  const files = allPaths(root);
212
- const readable = files.filter((f) => !generated(f.p, output) && f.size <= MAX_BYTES);
214
+ /* Колонками идёт отслеживаемое git: файл, которого на HEAD нет, измерять нечего
215
+ * (в отчёте он был бы пуст во всех строках). Путь, живущий только в истории,
216
+ * поэтому — исключение, а не колонка; сам список путей — всё равно полный. */
217
+ const tracked = new Set(indexFiles(root).map((f) => f.p));
218
+ const readable = files.filter((f) => tracked.has(f.p) && !generated(f.p, output) && f.size <= MAX_BYTES);
213
219
  const columns = columnsOf(readable, journal);
214
220
  const taken = new Set(columns.reduce((all, c) => all.concat(c.paths), []));
215
221
  // Язык, заголовок, порядок строк и выключатель хука не выводятся ни из чего:
@@ -232,8 +238,9 @@ export function projectConfig(root) {
232
238
  anchor: 'heading'
233
239
  },
234
240
  links: { commitUrl: commitUrlOf(root) },
235
- // Исключения — всё, что колонкой не стало: и то, что ею быть не может, и то, что
236
- // в отчёт не поместилось. Так первый запуск полон, а непоместившееся названо.
241
+ // Исключения — всё, что колонкой не стало: то, что ею быть не может, и то, чего
242
+ // git не отслеживает (путь из одной истории). Так первый запуск полон, а
243
+ // названное в `skip` — это ответ на вопрос «почему его нет в отчёте».
237
244
  skip: [output].concat(files.filter((f) => !taken.has(f.p)).map((f) => f.p))
238
245
  .filter((p, i, all) => all.indexOf(p) === i)
239
246
  };
@@ -19,8 +19,9 @@ uninstall-hook`, выключить, не снимая, — `"hooks": {"enabled"
19
19
  ## Настройки
20
20
 
21
21
  **Файл настроек заводить не нужно.** Без него инструмент выводит профиль из самого
22
- проекта: колонками — крупнейшие файлы по одному от каждого расширения (иначе в отчёте
23
- оказались бы одни крупные `.md`), журналом — первый знакомый (`WORKLOG.md`,
22
+ проекта: колонками — каждый отслеживаемый файл, который можно измерить (отчёт тогда
23
+ описывает проект целиком, а не несколько самых крупных его файлов), журналом —
24
+ первый знакомый (`WORKLOG.md`,
24
25
  `CHANGELOG.md`, …), файлом отчёта — `docs/`, если каталог есть, командой починки —
25
26
  объявленный скрипт `sizes`, ссылкой на коммит — адрес `origin`. О том, что настройки
26
27
  выведены, инструмент говорит строкой, а закрепляет их файлом `size --init`: дальше