@vernikr/size-report 2.8.2 → 2.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -10
- package/package.json +2 -2
- package/src/config.js +1 -1
- package/src/hook.js +13 -12
- package/src/locales.js +0 -3
- package/src/metrics.js +1 -4
- package/src/page/table.js +104 -47
- package/src/project.js +1 -1
- package/src/strip/js.js +10 -10
- package/src/tokens.js +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ shows.
|
|
|
10
10
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
13
|
-
**Release 2.8.
|
|
13
|
+
**Release 2.8.4 (2026-09-18).** The tool lives as a package of its own: the registry name is
|
|
14
14
|
`@vernikr/size-report` (published by tag from CI, with no secret). A project may keep no settings at
|
|
15
15
|
all: without a config file the tool derives them from the project itself and says so in one line,
|
|
16
16
|
and `--init` pins what was derived into a file. The report is **one file**, the self-contained page
|
|
@@ -19,6 +19,34 @@ is installed and on the first run. The version is in the manifest, and every rel
|
|
|
19
19
|
the journal — `worklog/` for today's entries, `worklog/archive/WORKLOG.md` for the earlier ones:
|
|
20
20
|
what changes in the numbers is measured rather than retold.
|
|
21
21
|
|
|
22
|
+
2.8.4 is about the volume of the tree rather than of a report, and it is a sweep — no feature. **The six probes that
|
|
23
|
+
measured the page's steps are archived** (`probes/archive/`, 1 458 lines): the plan they served is settled, its figures
|
|
24
|
+
stand in `plans/2026-09-17-page-perf/` and in the worklog, so the instruments stay as the record of how the numbers were
|
|
25
|
+
taken, with every reference following them. **The open notes are closed**: the help's continuation lines are no longer
|
|
26
|
+
read as commands (the list falls from ten words to six), an advice without a flag is a command again, two assertions
|
|
27
|
+
that could not fail are gone (`test/check.test.js`, `test/gates-verify.test.js`), the last two Russian comments left
|
|
28
|
+
`src/` (the locale's strings stay — they are data), and `tools/run-tests.js` hands its extra flags to `node --test`
|
|
29
|
+
**before** the file, where node 22 reads them. **The copies below the sensors' thresholds went too**: one helper
|
|
30
|
+
spawns the frozen copy instead of two builders' own, one function sets the identity git refuses to commit without
|
|
31
|
+
(three sites had grown the loop), and a suite's temporary directory removes itself when the process ends — 25 suites
|
|
32
|
+
opened with the same pair of lines. Nine `export` keywords on names nothing outside their module reads left the
|
|
33
|
+
surface, and `TODO.md` fell from 96 lines to 31. The checks are at **87** in the fast profile and **192** in the full
|
|
34
|
+
one, none added and none removed; the code of the tree changed by −44 lines. The figures and the reasoning stand in
|
|
35
|
+
`worklog/0213-the-volume-swept-and-the-probes-archived.md`.
|
|
36
|
+
|
|
37
|
+
2.8.3 is about what a sideways scroll of the report costs, and its figures are measured rather than retold. **A step
|
|
38
|
+
sideways moves the window instead of building it:** a row is made once and lives through the move, keeping its node and
|
|
39
|
+
its numbers — the window's columns are the range of that row's numbers, so the columns that entered it are made and the
|
|
40
|
+
ones that left are dropped, the header's line of names and its line of metrics by the same rule. A switch of the
|
|
41
|
+
reader's own and the shell's resize are the one thing that cannot be a move — a column that is not built cannot be
|
|
42
|
+
shown — and they build the window from nothing as they did. **Measured on this repository's own report in live Chrome**
|
|
43
|
+
(`probes/archive/step-12-window.mjs`, two rounds, 1440 × 900): the page's own handling of one step **10.4 → 0.6 ms** (worst
|
|
44
|
+
19.2 → 3.6) and the layout the step forces **14.4 → 5.9 ms**; over a sweep of forty steps the browser's own accounting
|
|
45
|
+
gives layout **0.605 → 0.246 s**, style recalculation 0.169 → 0.056 s and task time **1.281 → 0.350 s**. The checks go
|
|
46
|
+
86 → **87** in the fast profile and 191 → **192** in the full one: `test/page-grid.test.js` holds the rule by node
|
|
47
|
+
identity — a page that built the same numbers again would read the same — and by the numbers of the window built at
|
|
48
|
+
that place in one leap. The figures and the reasoning stand in `worklog/0211-sideways-step.md`.
|
|
49
|
+
|
|
22
50
|
2.8.2 is about the page's own sources and the checks that guard them, and its figures are measured rather than retold.
|
|
23
51
|
**The four page suites that went quiet with the `<table>` are alive again against the window:** `test/page-view.test.js`,
|
|
24
52
|
`page-cols`, `page-tree` and `page-choice` read the grid and the panel they guard rather than markup that no longer
|
|
@@ -91,7 +119,7 @@ class changes are about 2 µs a node; on this page the whole Table of Contents o
|
|
|
91
119
|
the same click in 37 slices paid that relayout 37 times — **169 layouts and 151.9 s of pure layout time against
|
|
92
120
|
1.04 s**, with the tab growing to gigabytes of repaint and the reader waiting four minutes for a switch that
|
|
93
121
|
costs him less than a second unsliced. Within one task nothing can repaint, so a bar that filled would be a bar
|
|
94
|
-
nobody could read: the stripe travels and says the work is going on (`probes/step-12-columns.mjs`, the records in
|
|
122
|
+
nobody could read: the stripe travels and says the work is going on (`probes/archive/step-12-columns.mjs`, the records in
|
|
95
123
|
`worklog/0207-page-choice-bar.md`). The order of the columns did not change there: what the last commit touched
|
|
96
124
|
still stands first, the rest after it in the settings' order.
|
|
97
125
|
|
|
@@ -271,8 +299,8 @@ commits and installs hooks; the reason for each expensive file is named line by
|
|
|
271
299
|
|
|
272
300
|
| Run | Command | Checks |
|
|
273
301
|
|---|---|---|
|
|
274
|
-
| Fast — every edit | `pnpm test` | **
|
|
275
|
-
| Full — release and CI | `pnpm test:all` | **
|
|
302
|
+
| Fast — every edit | `pnpm test` | **87 of 192** |
|
|
303
|
+
| Full — release and CI | `pnpm test:all` | **192** |
|
|
276
304
|
|
|
277
305
|
No check is lost or weakened: the full run starts every file, the fast one takes part of them. The default is the full run — a file becomes fast only explicitly and with a reason — so new
|
|
278
306
|
expensive work cannot quietly move into the fast one. Two declarations guard that:
|
|
@@ -396,7 +424,7 @@ left it and what entered it. The header sticks to the top of the shell and the c
|
|
|
396
424
|
number belongs to and the file it stands under are always in sight. The price of a table is what this step removed:
|
|
397
425
|
238 500 cells were 253 770 nodes in the document and 377 396 in the browser, **1.46 GB** of a fresh Chrome's memory at
|
|
398
426
|
rest, and any change of a column's visibility meant the browser laying the whole of it out again — 234.5 ms of layout for
|
|
399
|
-
a *single* cell of this repository's report, 539 ms for 6 000 (`probes/step-12-columns.mjs`). A window of the same
|
|
427
|
+
a *single* cell of this repository's report, 539 ms for 6 000 (`probes/archive/step-12-columns.mjs`). A window of the same
|
|
400
428
|
table is **2 460 nodes and 0.15 GB**, a hundredth of the nodes and a tenth of the memory, and the whole-table scroll
|
|
401
429
|
pass that took 36 s of task time over 60 steps takes **1.1 s over 251** (`worklog/0208-table-window.md`). **The columns
|
|
402
430
|
are one width and fixed, 70px each**: the numbers are short and of one kind, and a width that came out of the text is a
|
|
@@ -407,7 +435,10 @@ right edge of the pinned commit column rather than under it, and every line of t
|
|
|
407
435
|
cell, along a group's left edge — is one pixel of one colour: a heavier line would promise a hierarchy to work out
|
|
408
436
|
rather than read. The geometry is four figures — a column, a row, the header, the pinned column — written in the styling
|
|
409
437
|
and in the script that counts the window's ordinals in them, and `test/page-grid.test.js` reads both and holds them
|
|
410
|
-
together.
|
|
438
|
+
together. **A step sideways moves that window rather than building it:** the rows live through the move and only the
|
|
439
|
+
columns that entered are made, at 10.4 → **0.6 ms** for the page's own handling of the step, 14.4 → **5.9 ms** for the
|
|
440
|
+
layout it forces and 1.281 → **0.350 s** of task time over a sweep of forty steps
|
|
441
|
+
(`probes/archive/step-12-window.mjs`).
|
|
411
442
|
|
|
412
443
|
**A click builds the window again, and that is cheap now.** A file switched off is simply not among the columns that
|
|
413
444
|
are built, and a metric switched off not among the metrics: there is nothing to hide and nothing to carry, and the
|
|
@@ -586,7 +617,7 @@ acceptance for each.
|
|
|
586
617
|
| `tools/parity-freeze.js` | Takes the parity reference (`pnpm run parity`): with the frozen copy, at the project revision from the manifest — `--json`, the config, the artifact's hash, the tool's hash |
|
|
587
618
|
| `tools/make-fixture.js` | Assembles the synthetic fixture (`pnpm run fixture`): a deterministic history with traps plus the reference numbers |
|
|
588
619
|
| `tools/synthetic/` | The subjects of that assembly, one per matter: `repo.js` — how git is spoken to (pinned time, author, settings), `content.js` — what the files hold, `history.js` — which commits come of it, `note.js` — the fixture's note with the list of traps |
|
|
589
|
-
| `probes/` | The scripted measurements behind `plans/2026-09-17-page-perf
|
|
620
|
+
| `probes/` | The archived scripted measurements behind `plans/2026-09-17-page-perf/` (`probes/archive/`, one file per step, run by hand against live Chrome at the debug port — the fixed layout, where `content-visibility` acts at all, the price of a switch and of a step sideways) and their index (`probes/README.md`). Kept as the record of how the plan's figures were taken; outside the sensors' paths on purpose: they measure the product rather than being part of it, and a suite cannot see layout, paint or a browser's own skipping |
|
|
590
621
|
| `tools/parity-live.js` | Compares the engine with the live project on a clone: the numbers and the self-contained report at the path the consumer's settings give (`pnpm run parity:live`) |
|
|
591
622
|
| `tools/pack-check.js` | Assembles the tarball and checks that everything works from it: all sources arrived, the numbers and the report as from the repository (`pnpm run pack:check`) |
|
|
592
623
|
| `tools/check-standards.js` | Checks that both references reproduce: a re-take goes nowhere and is compared with what is committed (our files byte for byte, the bundle by content), and that the live-history bundle carries `HEAD` (`pnpm run check:standards`) |
|
|
@@ -622,7 +653,7 @@ acceptance for each.
|
|
|
622
653
|
| `src/page/state.js` | The page's state: the report's data (the block unrolled by the payload chapter), the view of the checkboxes, the pointer "which path is which column", the unfolded folders, the record's passport, the browser's memory and the link that is read out of the address — a chapter of the page's program |
|
|
623
654
|
| `src/page/dom.js` | The page's nodes: the small helpers of markup (`appEl`, `appBox`) — one set for the panel and the table alike |
|
|
624
655
|
| `src/page/panel.js` | The panel of choices: the switches of metrics and files, the categories, the tree of the project's paths (files outside the report keep a checkbox off with a reason and stand after the rest, and a hidden name after every visible one; the tree opens folded and folders carry a sign that hides the subtree by a class rather than by a rebuild); built once, with the fields of the switches and of the folders and categories written where they stand |
|
|
625
|
-
| `src/page/table.js` | The page's table as a window: the rows and the columns the reader can see (plus four beyond each edge), a cell, a commit's caption, the header, the empty states and the order of the columns — markup over the shared calculation, with the totals counted per row from the choice, the geometry in pixels that the styling mirrors, and the reason a library was not taken |
|
|
656
|
+
| `src/page/table.js` | The page's table as a window: the rows and the columns the reader can see (plus four beyond each edge), moved by a scroll rather than built again (a row lives through a step sideways and only the columns that entered it are made), a cell, a commit's caption, the header, the empty states and the order of the columns — markup over the shared calculation, with the totals counted per row from the choice, the geometry in pixels that the styling mirrors, and the reason a library was not taken |
|
|
626
657
|
| `src/page/app.js` | Assembling and starting the page: the first drawing, then a switch that builds the window again and writes the fields it reached without making a node of the panel; an anchor change; pasted into the assembled page |
|
|
627
658
|
| `src/page/build.js` | Assembling the page: data, styling and program in one file with no external references — the pasted text is **squeezed** on the way in (comments and indentation out, the same stripping the `min` metric counts) while the sources keep them, and the result is guarded by the stripper's own `assertCompilable` |
|
|
628
659
|
| `src/git.js` | The only border where git is called: the pinned settings, blobs by the batch, the history, the comparison with the working tree |
|
|
@@ -755,7 +786,7 @@ The same release can be taken by a reference to the repository — installation
|
|
|
755
786
|
registry, but stays tied to a revision:
|
|
756
787
|
|
|
757
788
|
```bash
|
|
758
|
-
pnpm add -D github:vernikr/size-report#v2.8.
|
|
789
|
+
pnpm add -D github:vernikr/size-report#v2.8.4
|
|
759
790
|
```
|
|
760
791
|
|
|
761
792
|
With no network (or nothing to fetch from codeload) — the tarball: `pnpm pack` in the package clone, then
|
|
@@ -767,7 +798,7 @@ the branch moves on the installation fails with `Could not resolve <sha> to a co
|
|
|
767
798
|
observation rather than reasoning: the short pin `6530237` installed while `main` stood on it and stopped
|
|
768
799
|
working at the very next commit, while the same sha in full installed. A branch name (`#main`) and a tag
|
|
769
800
|
are both accepted, but a branch is a moving target and a tag is constant: this release stands on the tag
|
|
770
|
-
`v2.8.
|
|
801
|
+
`v2.8.4`, which is also the one in the example (forty characters work as well, but they have to be copied
|
|
771
802
|
out of the history by eye).
|
|
772
803
|
|
|
773
804
|
The revision in the example is a part of the claim rather than decoration: what is described below is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vernikr/size-report",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.4",
|
|
4
4
|
"author": "vernikr",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"report"
|
|
66
66
|
],
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@vernikr/size-report": "2.8.
|
|
68
|
+
"@vernikr/size-report": "2.8.3",
|
|
69
69
|
"c8": "10",
|
|
70
70
|
"dependency-cruiser": "17",
|
|
71
71
|
"eslint": "^9.18.0",
|
package/src/config.js
CHANGED
|
@@ -76,7 +76,7 @@ export function gitRoot() {
|
|
|
76
76
|
* request for that very file, so its absence stays a refusal (otherwise a typo in the path would
|
|
77
77
|
* silently yield someone else's settings). `path` is given in words rather than as a path: there is no
|
|
78
78
|
* file, and "edit <path>" would lead the reader to something the project does not have. */
|
|
79
|
-
|
|
79
|
+
function derivedConfig(root) {
|
|
80
80
|
const cfg = derivedProfile(root);
|
|
81
81
|
cfg.path = 'derived from the project';
|
|
82
82
|
cfg.derived = true;
|
package/src/hook.js
CHANGED
|
@@ -2,10 +2,9 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { EXIT, Refusal, cliCommand, refuseCause } from './refusal.js';
|
|
5
|
-
import { installSpec } from './tool.js';
|
|
5
|
+
import { TOOL_PKG, installSpec } from './tool.js';
|
|
6
6
|
import { git, gitTry } from './git.js';
|
|
7
7
|
import { loadConfig } from './config.js';
|
|
8
|
-
import { TOOL_PKG } from './tool.js';
|
|
9
8
|
import { rebuild } from './artifact.js';
|
|
10
9
|
|
|
11
10
|
/* The `post-commit` and `post-merge` hooks: after every commit and merge the report rebuilds itself and, when it is tracked
|
|
@@ -120,6 +119,16 @@ function script(entry) {
|
|
|
120
119
|
+ 'exec node ' + entry.quoted + ' hook-run\n';
|
|
121
120
|
}
|
|
122
121
|
|
|
122
|
+
/* Writing the hook files, executable: the named command and the silent install write the same bytes, since a difference
|
|
123
|
+
* between them would surface only after somebody's commit. */
|
|
124
|
+
function installFiles(hooks, entry, files) {
|
|
125
|
+
fs.mkdirSync(hooks.dir, { recursive: true });
|
|
126
|
+
files.forEach((file) => {
|
|
127
|
+
fs.writeFileSync(file, script(entry));
|
|
128
|
+
fs.chmodSync(file, 0o755);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
/* The hook's state for `size doctor`: whether it is installed and how its last run ended. It counts nothing and commits
|
|
124
133
|
* to nothing. */
|
|
125
134
|
export function hookStatus(root) {
|
|
@@ -180,11 +189,7 @@ export function installHook(root, cfg) {
|
|
|
180
189
|
' to remove it: ' + cliCommand('uninstall-hook')
|
|
181
190
|
] };
|
|
182
191
|
}
|
|
183
|
-
|
|
184
|
-
files.forEach((file) => {
|
|
185
|
-
fs.writeFileSync(file, script(entry));
|
|
186
|
-
fs.chmodSync(file, 0o755);
|
|
187
|
-
});
|
|
192
|
+
installFiles(hooks, entry, files);
|
|
188
193
|
|
|
189
194
|
const lines = [
|
|
190
195
|
'✓ hook: ' + rels.join(', '),
|
|
@@ -219,11 +224,7 @@ export function autoInstall(root, cfg) {
|
|
|
219
224
|
const files = HOOKS.map((name) => path.join(hooks.dir, name));
|
|
220
225
|
if (files.some((f) => fs.existsSync(f) && !isOurs(f))) return null;
|
|
221
226
|
if (files.every(isOurs)) return null;
|
|
222
|
-
|
|
223
|
-
files.forEach((file) => {
|
|
224
|
-
fs.writeFileSync(file, script(entry));
|
|
225
|
-
fs.chmodSync(file, 0o755);
|
|
226
|
-
});
|
|
227
|
+
installFiles(hooks, entry, files);
|
|
227
228
|
return files.map((f) => path.relative(root, f));
|
|
228
229
|
} catch (_e) {
|
|
229
230
|
/* Not a git repository, no rights on `.git`, a foreign format — all of it means one thing: there will be no automation
|
package/src/locales.js
CHANGED
|
@@ -48,9 +48,6 @@ export const LOCALES = {
|
|
|
48
48
|
+ 'что выбрано в этом браузере.',
|
|
49
49
|
linkExtra: 'Часть ссылки пропущена: в этом отчёте нет {n} названных имён. '
|
|
50
50
|
+ 'Остальное применено.',
|
|
51
|
-
/* Данные отчёта лежат в файле сжатыми, и распаковывает их браузер. Текст называет причину
|
|
52
|
-
* (браузера нет в списке тех, что умеют `DecompressionStream`) и что с этим делать — пустая
|
|
53
|
-
* таблица не сказала бы читателю ничего. */
|
|
54
51
|
unpack: 'Данные отчёта сжаты, а браузер не умеет их распаковать: в нём нет DecompressionStream. '
|
|
55
52
|
+ 'Обновите браузер — таблицу показывать не из чего.',
|
|
56
53
|
note: 'Абсолютные размеры стоят один раз — в строке «{now}». Строк: {rows}: коммиты без '
|
package/src/metrics.js
CHANGED
|
@@ -58,9 +58,6 @@ const STYLES = {
|
|
|
58
58
|
}
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
/* Метрика объявляет, нужен ли ей текст блоба: `needsText: false` вместе с
|
|
62
|
-
* `fromSize: true` означает «хватит размера объекта», и тогда содержимое не
|
|
63
|
-
* читается вовсе (`git cat-file --batch` не вызывается). */
|
|
64
61
|
export const METRICS = {
|
|
65
62
|
raw: {
|
|
66
63
|
label: 'raw',
|
|
@@ -208,7 +205,7 @@ function esbuildLoader(file, cfg) {
|
|
|
208
205
|
/* The engine actually in force: the requested one may be unavailable, in which case the metric
|
|
209
206
|
* counts another way — and that is announced (`sensorGaps`), or a different count would travel
|
|
210
207
|
* into CI as the requested one. */
|
|
211
|
-
|
|
208
|
+
function minEngine(cfg) {
|
|
212
209
|
if (cfg.minify.engine !== 'esbuild') return 'strip';
|
|
213
210
|
return minifier().tool === null ? 'strip' : 'esbuild';
|
|
214
211
|
}
|
package/src/page/table.js
CHANGED
|
@@ -8,8 +8,8 @@ import { appData, appUi, appView } from './state.js';
|
|
|
8
8
|
*
|
|
9
9
|
* **Why not a `<table>`.** Measured on this repository's own report (238 500 cells = 370 758 nodes, a table 71 712 ×
|
|
10
10
|
* 5 982 px): about 1.4 GB of a browser's memory, of which roughly half the nodes and half the painted area, and a
|
|
11
|
-
* browser's relayout of it costs close to a second on any switch (`probes/step-12-columns.mjs`). `content-visibility:
|
|
12
|
-
* auto`, the cheap way out, is ignored on a table row by Chrome 153 (`probes/step-10-tables.mjs`), so the answer is
|
|
11
|
+
* browser's relayout of it costs close to a second on any switch (`probes/archive/step-12-columns.mjs`). `content-visibility:
|
|
12
|
+
* auto`, the cheap way out, is ignored on a table row by Chrome 153 (`probes/archive/step-10-tables.mjs`), so the answer is
|
|
13
13
|
* to build less rather than to promise the browser will skip it. A grid of `position: absolute` rows has no layout to
|
|
14
14
|
* be redone: a row is placed by its `top`, a column by the `left` of the group of cells that starts it, and the
|
|
15
15
|
* browser never measures a cell to decide a width — every column is `--col` wide (70px), which is what the numbers
|
|
@@ -31,6 +31,14 @@ import { appData, appUi, appView } from './state.js';
|
|
|
31
31
|
* table, against 6.5 ms for building the whole window again). The header sticks to the shell's top and the commit
|
|
32
32
|
* column to its left (both `position: sticky`), so the row and the column a number belongs to are always in sight.
|
|
33
33
|
*
|
|
34
|
+
* **A step down the table and a step sideways cost the same kind of work.** A row is made once and then stands at its
|
|
35
|
+
* own place in the scrolled content while the window moves over it: the window's columns are the range of that row's
|
|
36
|
+
* numbers, so a step sideways makes the columns that entered and drops the ones that left (`appStrip`) — a cell per
|
|
37
|
+
* row, the header's two lines included. A row that left the window vertically is dropped and made again when the
|
|
38
|
+
* reader comes back to it, which is what makes a jump down the table cost the same as a step. Building the rows again
|
|
39
|
+
* on every sideways scroll — the first version of this step — is what `probes/archive/step-12-window.mjs` measures against the
|
|
40
|
+
* splice (`worklog/0211-sideways-step.md`).
|
|
41
|
+
*
|
|
34
42
|
* The choice is applied by building the window again: the columns of a switched-off file are simply not among the
|
|
35
43
|
* columns that are built, so there is nothing to hide and nothing to recount — the totals are the sum over the files
|
|
36
44
|
* that are on, counted by `rowModel` for the rows the window holds (`src/derived.js`, one place for that
|
|
@@ -138,6 +146,27 @@ export function appState(metricsCount, filesCount) {
|
|
|
138
146
|
document.getElementById('shell').hidden = metricsCount === 0;
|
|
139
147
|
}
|
|
140
148
|
|
|
149
|
+
/* A strip of pieces of one range, moved to another: the pieces the two ranges share are kept, and only the ends are
|
|
150
|
+
* made and dropped. The unit of a strip is its child — a row's numbers are one column to a child, the header's line of
|
|
151
|
+
* names one group, its line of metrics one column — so the same movement serves all three, and a group of metrics
|
|
152
|
+
* cannot be split by the window (a column of numbers would lose its caption). A window that jumped clear of the old
|
|
153
|
+
* range — the reader dragged the scrollbar — has nothing to keep and the strip is made again; the empty range
|
|
154
|
+
* `[c, c - 1]` is how a strip that holds nothing yet says so. */
|
|
155
|
+
function appStrip(strip, was, now, make) {
|
|
156
|
+
const a = Math.max(was[0], now[0]);
|
|
157
|
+
const b = Math.min(was[1], now[1]);
|
|
158
|
+
if (a > b) {
|
|
159
|
+
strip.textContent = '';
|
|
160
|
+
for (let c = now[0]; c <= now[1]; c++) strip.appendChild(make(c));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (let n = a - was[0]; n > 0; n--) strip.firstChild.remove();
|
|
164
|
+
for (let n = was[1] - b; n > 0; n--) strip.lastChild.remove();
|
|
165
|
+
const edge = strip.firstChild;
|
|
166
|
+
for (let c = a - 1; c >= now[0]; c--) strip.insertBefore(make(c), edge);
|
|
167
|
+
for (let c = b + 1; c <= now[1]; c++) strip.appendChild(make(c));
|
|
168
|
+
}
|
|
169
|
+
|
|
141
170
|
/* One cell of the window: the column decides what it holds. The group is the column's ordinal divided by the number of
|
|
142
171
|
* metrics — `0` is the total over the files, `g ≥ 1` is the g-th column of the order — and it is the same arithmetic
|
|
143
172
|
* the header is placed by, so a column and its caption cannot drift apart. The model lists the *enabled* files in the
|
|
@@ -153,9 +182,13 @@ function appCell(model, c, now, cache) {
|
|
|
153
182
|
return appNum(now ? valueParts(cell) : cellParts(cell.value, cell.delta, '−'), cls);
|
|
154
183
|
}
|
|
155
184
|
|
|
156
|
-
/* A row of the window: the caption of the commit and
|
|
157
|
-
* state at HEAD (`nowModel`) rather than the last commit's cells: an absolute number stands in the
|
|
158
|
-
* the deltas below it add up to it.
|
|
185
|
+
/* A row of the window: the caption of the commit and an empty strip of numbers for the window to fill (`appCells`).
|
|
186
|
+
* The top row is the state at HEAD (`nowModel`) rather than the last commit's cells: an absolute number stands in the
|
|
187
|
+
* table once, and the deltas below it add up to it.
|
|
188
|
+
*
|
|
189
|
+
* The model is kept in the entry beside the node because the numbers of a row depend on the commit and the choice
|
|
190
|
+
* rather than on the place the window stands at: a step sideways asks for a cell's arithmetic, never for `rowModel`
|
|
191
|
+
* again. The strip is born empty — the range `[c0, c0 - 1]` — and is filled by the first `appCells`. */
|
|
159
192
|
function appRow(cache, r, span) {
|
|
160
193
|
const last = appData.rows.length;
|
|
161
194
|
const now = r === 0;
|
|
@@ -163,28 +196,36 @@ function appRow(cache, r, span) {
|
|
|
163
196
|
const model = now
|
|
164
197
|
? nowModel(appData.now, cache.keys, appView.files)
|
|
165
198
|
: rowModel(appData.rows[i].values, i === 0 ? null : appData.rows[i - 1].values, cache.keys, appView.files);
|
|
166
|
-
const
|
|
167
|
-
|
|
199
|
+
const el = appEl('div', 'row' + (now ? ' now' : ''));
|
|
200
|
+
el.style.top = (APP_HEAD + r * APP_ROW) + 'px';
|
|
168
201
|
const commit = appEl('div', 'c-commit');
|
|
169
202
|
if (now) commit.textContent = appUi.now;
|
|
170
203
|
else commit.appendChild(appCommit(appData.rows[i]));
|
|
171
|
-
|
|
204
|
+
el.appendChild(commit);
|
|
172
205
|
const cells = appEl('div', 'cells');
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
206
|
+
el.appendChild(cells);
|
|
207
|
+
return { el: el, cells: cells, model: model, now: now, cols: [span.c0, span.c0 - 1] };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* A row's numbers at the window's columns: the columns the two windows share keep their cells, and a column that
|
|
211
|
+
* entered is made. The numbers stand beside the pinned commit column rather than under it — the column lies over the
|
|
212
|
+
* content — so the strip is placed by the window's first column, and the cells of it follow their strip. A step down
|
|
213
|
+
* the table asks for the columns it already holds, and the row is not touched at all. */
|
|
214
|
+
function appCells(entry, cache, span) {
|
|
215
|
+
const cols = [span.c0, span.c1];
|
|
216
|
+
if (entry.cols[0] === cols[0] && entry.cols[1] === cols[1]) return;
|
|
217
|
+
entry.cells.style.left = (APP_COMMIT + cols[0] * APP_COL) + 'px';
|
|
218
|
+
appStrip(entry.cells, entry.cols, cols, (c) => appCell(entry.model, c, entry.now, cache));
|
|
219
|
+
entry.cols = cols;
|
|
179
220
|
}
|
|
180
221
|
|
|
181
222
|
/* A built row into the window: the map is what says which rows are in the markup, so a row goes into it where it is
|
|
182
223
|
* made — otherwise a window that moved would build its rows beside the ones that are still there. */
|
|
183
224
|
function appPlace(cache, r, span) {
|
|
184
|
-
const
|
|
185
|
-
cache.rows.set(r,
|
|
186
|
-
cache
|
|
187
|
-
|
|
225
|
+
const entry = appRow(cache, r, span);
|
|
226
|
+
cache.rows.set(r, entry);
|
|
227
|
+
appCells(entry, cache, span);
|
|
228
|
+
cache.grid.appendChild(entry.el);
|
|
188
229
|
}
|
|
189
230
|
|
|
190
231
|
/* A caption of the header: a file's name or a metric's. The room is a fixed number of columns, so a name that does
|
|
@@ -197,30 +238,39 @@ function appCaption(text, cls, span) {
|
|
|
197
238
|
return el;
|
|
198
239
|
}
|
|
199
240
|
|
|
200
|
-
/* The header: the
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
function
|
|
204
|
-
const
|
|
205
|
-
const g0 = Math.floor(span.c0 / count);
|
|
206
|
-
const g1 = Math.floor(span.c1 / count);
|
|
207
|
-
const left = (APP_COMMIT + g0 * count * APP_COL) + 'px';
|
|
208
|
-
const head = appEl('div', 'head');
|
|
209
|
-
head.appendChild(appEl('div', 'c-commit', appUi.commit));
|
|
241
|
+
/* The header's frame: the commit column and the two empty strips that `appStrip` fills — the names of the groups over
|
|
242
|
+
* the columns, their metrics under them. An empty strip says `[0, -1]`, which is what makes the first `appHead` fill
|
|
243
|
+
* it rather than move it. */
|
|
244
|
+
function appHeadNew() {
|
|
245
|
+
const el = appEl('div', 'head');
|
|
210
246
|
const groups = appEl('div', 'hgroups');
|
|
211
|
-
groups.style.left = left;
|
|
212
247
|
const metrics = appEl('div', 'hmetrics');
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
248
|
+
el.appendChild(appEl('div', 'c-commit', appUi.commit));
|
|
249
|
+
el.appendChild(groups);
|
|
250
|
+
el.appendChild(metrics);
|
|
251
|
+
return { el: el, groups: groups, metrics: metrics, g: [0, -1] };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* The header: the total and the enabled files over their columns, and one caption per metric column under them. The two
|
|
255
|
+
* strips move by the same `appStrip` as a row's numbers, and the unit of the metrics line is a column, so its strip is
|
|
256
|
+
* asked for whole groups — the metrics of a column stand under their file even when the window cuts the last group.
|
|
257
|
+
* A window that did not leave a group (a step down the table) leaves the header alone. */
|
|
258
|
+
function appHead(cache, span) {
|
|
259
|
+
const count = cache.keys.length;
|
|
260
|
+
const g0 = count === 0 ? 0 : Math.floor(span.c0 / count);
|
|
261
|
+
const g1 = count === 0 ? -1 : Math.floor(span.c1 / count);
|
|
262
|
+
if (cache.head === null) cache.head = appHeadNew();
|
|
263
|
+
const h = cache.head;
|
|
264
|
+
if (h.g[0] === g0 && h.g[1] === g1) return h.el;
|
|
265
|
+
const was = h.g;
|
|
266
|
+
h.g = [g0, g1];
|
|
267
|
+
h.groups.style.left = (APP_COMMIT + g0 * count * APP_COL) + 'px';
|
|
268
|
+
h.metrics.style.left = h.groups.style.left;
|
|
269
|
+
appStrip(h.groups, was, h.g, (g) => appCaption(
|
|
270
|
+
g === 0 ? appUi.total : appData.files[cache.list[g - 1]].label, 'gh', count));
|
|
271
|
+
appStrip(h.metrics, [was[0] * count, was[1] * count + count - 1], [g0 * count, g1 * count + count - 1],
|
|
272
|
+
(c) => appCaption(cache.labels[c % count], c % count === 0 ? 'g' : '', 1));
|
|
273
|
+
return h.el;
|
|
224
274
|
}
|
|
225
275
|
|
|
226
276
|
/* The window the shell shows: the rows and the columns, in the ordinals of the whole grid, and the size of that
|
|
@@ -259,26 +309,32 @@ export function appSpan(cache) {
|
|
|
259
309
|
};
|
|
260
310
|
}
|
|
261
311
|
|
|
262
|
-
/* The window drawn.
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
312
|
+
/* The window drawn. Moving over the grid costs what left it and what entered it, and nothing else: the rows in the
|
|
313
|
+
* markup are the window's rows (what left it is dropped, what entered it is made) and each of them holds the columns
|
|
314
|
+
* of the window (`appCells`), so a step in either direction is paid by the rows that moved rather than by the table.
|
|
315
|
+
*
|
|
316
|
+
* The reader's own choice (`redraw`) is the one thing that cannot be a move: the columns of a switched-off file are
|
|
317
|
+
* simply not among the columns that are built, so there is nothing to hide and nothing to recount — the totals are the
|
|
318
|
+
* sum over the files that are on (`rowModel`), and the window is built from nothing. The shell's own size is the same
|
|
319
|
+
* kind of change: it decides how much of the grid is in sight, which is not a place to move to. */
|
|
266
320
|
export function appWindow(cache, redraw) {
|
|
267
321
|
const span = appSpan(cache);
|
|
268
322
|
const was = cache.win;
|
|
269
323
|
cache.win = span;
|
|
270
|
-
if (redraw === true || was === null
|
|
324
|
+
if (redraw === true || was === null) {
|
|
271
325
|
/* The grid holds the header and the rows of the window and nothing else, so the whole of it is thrown away rather
|
|
272
326
|
* than tracked piece by piece — and the rows that are made below say in the map that they are there. */
|
|
273
327
|
cache.grid.textContent = '';
|
|
274
328
|
cache.rows.clear();
|
|
329
|
+
cache.head = null;
|
|
275
330
|
cache.grid.appendChild(appHead(cache, span));
|
|
276
331
|
for (let r = span.r0; r <= span.r1; r++) appPlace(cache, r, span);
|
|
277
332
|
return;
|
|
278
333
|
}
|
|
334
|
+
appHead(cache, span);
|
|
279
335
|
[...cache.rows.keys()].forEach((r) => {
|
|
280
|
-
if (r >= span.r0 && r <= span.r1) return;
|
|
281
|
-
cache.rows.get(r).remove();
|
|
336
|
+
if (r >= span.r0 && r <= span.r1) { appCells(cache.rows.get(r), cache, span); return; }
|
|
337
|
+
cache.rows.get(r).el.remove();
|
|
282
338
|
cache.rows.delete(r);
|
|
283
339
|
});
|
|
284
340
|
for (let r = span.r0; r <= span.r1; r++) {
|
|
@@ -299,6 +355,7 @@ export function appTable(grid) {
|
|
|
299
355
|
rank: [],
|
|
300
356
|
slot: [],
|
|
301
357
|
rows: new Map(),
|
|
358
|
+
head: null,
|
|
302
359
|
win: null
|
|
303
360
|
};
|
|
304
361
|
cache.shell.addEventListener('scroll', () => appWindow(cache));
|
package/src/project.js
CHANGED
|
@@ -204,7 +204,7 @@ function columnsOf(files, journal) {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
export function projectConfig(root) {
|
|
207
|
-
const output = outputOf(
|
|
207
|
+
const output = outputOf();
|
|
208
208
|
const journal = journalOf(root);
|
|
209
209
|
const files = allPaths(root);
|
|
210
210
|
/* Only what git tracks becomes a column: a file absent at HEAD has nothing to measure (it would be
|
package/src/strip/js.js
CHANGED
|
@@ -59,16 +59,21 @@ function regexAllowed(last, word) {
|
|
|
59
59
|
return '([{,;:=!&|?+-*%~^<>'.indexOf(last) !== -1;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
/* A token taken whole: it goes into the output and the pass state is advanced over it — the token itself, not the word
|
|
63
|
+
* before it, decides where a regex may stand next, and the word being read ends with it. */
|
|
64
|
+
function take(s, end, last) {
|
|
65
65
|
s.out += s.src.slice(s.i, end);
|
|
66
66
|
s.i = end;
|
|
67
|
-
s.last =
|
|
67
|
+
s.last = last;
|
|
68
68
|
s.word = '';
|
|
69
69
|
return true;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function takeRegex(s) {
|
|
73
|
+
if (s.src[s.i] !== '/' || !regexAllowed(s.last, s.word)) return false;
|
|
74
|
+
return take(s, endOfRegex(s.src, s.i), '/');
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
function endOfRegex(src, start) {
|
|
73
78
|
let i = start + 1;
|
|
74
79
|
let inClass = false;
|
|
@@ -88,12 +93,7 @@ function endOfRegex(src, start) {
|
|
|
88
93
|
function takeString(s) {
|
|
89
94
|
const quote = s.src[s.i];
|
|
90
95
|
if (quote !== '"' && quote !== "'" && quote !== '`') return false;
|
|
91
|
-
|
|
92
|
-
s.out += s.src.slice(s.i, end);
|
|
93
|
-
s.i = end;
|
|
94
|
-
s.last = quote;
|
|
95
|
-
s.word = '';
|
|
96
|
-
return true;
|
|
96
|
+
return take(s, endOfString(s.src, s.i, quote), quote);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function endOfString(src, start, quote) {
|
package/src/tokens.js
CHANGED
|
@@ -28,7 +28,7 @@ export const CHARS_PER_TOKEN = 3;
|
|
|
28
28
|
* and the tokenizer would split them into anything at all, giving a random number. The list
|
|
29
29
|
* exists so that the metric says this in words instead of passing such a count off as counted.
|
|
30
30
|
* SVG is deliberately not here: it is text, and its tokens are meaningful. */
|
|
31
|
-
|
|
31
|
+
const BINARY_EXTS = [
|
|
32
32
|
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.avif',
|
|
33
33
|
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
34
34
|
'.pdf', '.zip', '.gz', '.tar', '.mp4', '.mp3', '.mov'
|