@vernikr/size-report 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -25
- package/package.json +2 -2
- package/src/data.js +21 -5
- package/src/locales.js +8 -2
- package/src/page/app.css +27 -0
- package/src/page/app.js +40 -21
- package/src/page/build.js +6 -1
- package/src/page/panel.js +24 -10
- package/src/page/state.js +43 -73
- package/src/page/table.js +22 -2
- package/src/page/work.js +55 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ shows.
|
|
|
10
10
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
13
|
-
**Release 2.
|
|
13
|
+
**Release 2.7.0 (2026-09-17).** 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,28 @@ 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.7.0 is about the page and what a reader does with it, and its figures are measured rather than retold.
|
|
23
|
+
**The report's address stays clean:** a switch writes the record into the browser's memory and nothing into the
|
|
24
|
+
address bar — the page no longer puts a `#size-report=…` tail on the report it was opened with — while a link
|
|
25
|
+
made by an earlier release is still read, and a foreign or broken one is still explained in words. **A test
|
|
26
|
+
is a category of its own**, right after Code: the files of `test`, `tests` and `fixtures` folders wherever
|
|
27
|
+
they stand, and every name carrying `.test` before its extension — on this repository's own history Tests
|
|
28
|
+
holds **47 of 317 columns**, and the checkbox of the documentation is called **Docs**. **The tree opens
|
|
29
|
+
folded**, and the unfolding is what the memory keeps, under a key of its own; inside a level a hidden name —
|
|
30
|
+
one beginning with a dot — stands after every visible one. **The drawing of a switch is measured, and it is one
|
|
31
|
+
task**: the click is counted and answered, a drawing short enough to be over before the browser could paint is
|
|
32
|
+
done on the click, and a longer one happens in the next task with a stripe over the top edge of the window. The
|
|
33
|
+
price of a switch is the browser's own relayout of the table — 234.5 ms of layout for a *single* cell of this
|
|
34
|
+
repository's report (249 rows, 317 columns, 238 thousand cells), 287 ms for 750, 539 ms for 6 000 — while the
|
|
35
|
+
class changes are about 2 µs a node; on this page the whole Table of Contents of the report (73 columns) is
|
|
36
|
+
867 ms blocked with a 742 ms task. A queue worked off in slices was written first and refused by measurement:
|
|
37
|
+
the same click in 37 slices paid that relayout 37 times — **169 layouts and 151.9 s of pure layout time against
|
|
38
|
+
1.04 s**, with the tab growing to gigabytes of repaint and the reader waiting four minutes for a switch that
|
|
39
|
+
costs him less than a second unsliced. Within one task nothing can repaint, so a bar that filled would be a bar
|
|
40
|
+
nobody could read: the stripe travels and says the work is going on (`probes/step-12-columns.mjs`, the records in
|
|
41
|
+
`worklog/0207-page-choice-bar.md`). The order of the columns did not change there: what the last commit touched
|
|
42
|
+
still stands first, the rest after it in the settings' order.
|
|
43
|
+
|
|
22
44
|
2.6.0 changes the numbers themselves, and they are measured rather than retold: on this repository's own
|
|
23
45
|
history the page this engine writes is **85 955 B against 1 899 370 B** written by 2.5.0 — 22.1 times
|
|
24
46
|
smaller, −95.5 % — because the data travels as one packed block (49 392 B, `base64+gzip`) instead of a list
|
|
@@ -76,7 +98,8 @@ selection" are impossible in principle. The contract tells how each number was o
|
|
|
76
98
|
no rule of counting of its own, and the split of numbers into exact and approximate was taken out of the
|
|
77
99
|
package. The page's panel is a
|
|
78
100
|
tree of files by folder, with a switch per folder for the whole subtree; a reader's choice survives
|
|
79
|
-
a revisit and
|
|
101
|
+
a revisit in the browser's memory, and the address keeps a clean tail — an address with a link of its
|
|
102
|
+
own is read and never written (release 2.7.0). The contract carries the **project
|
|
80
103
|
catalogue** too: every path git sees, so the page's tree is the project's tree, while numbers exist
|
|
81
104
|
only for the files that became columns (release 2.2.0).
|
|
82
105
|
|
|
@@ -287,7 +310,9 @@ choice. The report itself is always in the catalogue, whether or not it is track
|
|
|
287
310
|
of the moment, and the page must not depend on it, or the first rebuild in a fresh clone would give
|
|
288
311
|
different bytes. A folder whose files are only partly in the report writes its count as a fraction
|
|
289
312
|
("2/5"), and everything outside the report stands after everything inside it — folders and leaves
|
|
290
|
-
alike — so that the report is seen at once in a list where half the rows do not switch
|
|
313
|
+
alike — so that the report is seen at once in a list where half the rows do not switch. The name of a level
|
|
314
|
+
holds a second rule: a hidden name — one beginning with a dot — stands after every visible one, the alphabet
|
|
315
|
+
deciding the rest, so the service files of a project do not stand in the reader's way
|
|
291
316
|
(`test/page-tree.test.js`).
|
|
292
317
|
|
|
293
318
|
**A folder is a switch like a file, and its sign is a decision of its own.** The checkbox of a folder
|
|
@@ -296,8 +321,10 @@ next to it. Neither a folder nor a category button keeps state of its own: both
|
|
|
296
321
|
checkboxes, so the tree, the buttons and the table cannot drift apart. The sign beside a folder answers
|
|
297
322
|
a different question — how much of the tree is visible, which is the onlooker's business rather than the
|
|
298
323
|
reader's choice — so it is remembered between visits in a record of its own, under a key of its own and
|
|
299
|
-
the same report passport, and it never goes into
|
|
300
|
-
|
|
324
|
+
the same report passport, and it never goes into a link. The tree opens **folded**: a project's tree is
|
|
325
|
+
longer than the window, and the reader looks at a short list first, so what the record keeps is the
|
|
326
|
+
unfolding rather than the folding — folding every folder back removes the record, just as turning the
|
|
327
|
+
checkboxes back on removes the choice.
|
|
301
328
|
|
|
302
329
|
**The list scrolls, and there is one scroll.** On a narrow window it is the file list that scrolls —
|
|
303
330
|
the panel grows with the page there — while on a wide one the whole panel does: otherwise the controls
|
|
@@ -309,6 +336,22 @@ row hides it, so a click on the sign changes exactly the three things the reader
|
|
|
309
336
|
sign and the note in the memory. What guards this is that after folding the table is the same markup
|
|
310
337
|
rather than a rebuilt one (`test/page-tree.test.js`).
|
|
311
338
|
|
|
339
|
+
**A switch draws its columns in one task, with a stripe over the page while it is going on.** The class
|
|
340
|
+
changes are cheap — about 2 µs a node, so a category of this repository's report (73 columns, 55 042 nodes) is
|
|
341
|
+
120 ms of them — while the browser lays the whole table out again for any change of a column's visibility, and
|
|
342
|
+
that is where the seconds are (`probes/step-12-columns.mjs`). So `src/page/work.js` asks for the drawing once:
|
|
343
|
+
work short enough to be over before the browser could paint goes on the click itself, and a longer one goes to
|
|
344
|
+
the next task with the stripe over the top edge of the window. **The stripe carries no share, and that is the
|
|
345
|
+
measurement rather than a shortcut**: inside one task the browser cannot repaint, so a length would be a length
|
|
346
|
+
nobody could keep; what moves is an indeterminate highlight. A queue worked off between timeouts was written
|
|
347
|
+
first and refused by the same probe — 37 slices paid the table's relayout 37 times (2.5–4 s each, 169 layouts
|
|
348
|
+
against 1) — and the numbers stay out of all of it, because the totals are counted on the click itself, where
|
|
349
|
+
they cost arithmetic rather than nodes. Nothing is asked for a column that is already right either
|
|
350
|
+
(`appColumnStale`): a report opened with everything switched on has nothing to draw, and a record from the
|
|
351
|
+
memory or a link queues exactly the columns that differ from it. What guards the two halves is
|
|
352
|
+
`test/page-choice.test.js` — a long drawing that leaves the click's line at once, runs in the next task and
|
|
353
|
+
takes the stripe away, and a short one that is over before the click returns.
|
|
354
|
+
|
|
312
355
|
**A click shows and hides rather than builds.** The table is assembled once, with every column of every
|
|
313
356
|
file, and a switch afterwards changes only what is visible: a metric is one class on the table plus the
|
|
314
357
|
`colSpan` of the group headings, a file's column is a class per node of it, and a folder or a category is
|
|
@@ -372,17 +415,18 @@ neither the numbers nor the markup. The passport holds neither the tool's versio
|
|
|
372
415
|
history, and on purpose: updating the tool does not change what a column means, while a grown history is
|
|
373
416
|
the very history the reader comes back to.
|
|
374
417
|
|
|
375
|
-
**The
|
|
376
|
-
browser's memory
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
418
|
+
**The link is read and never written.** The page's address is the report's own: a choice lives in the
|
|
419
|
+
browser's memory, and the page puts nothing into the address bar — a report opened from disk keeps the
|
|
420
|
+
address it was opened with, and not one switch lands in the tab's title bar. What an earlier release sent
|
|
421
|
+
as a link is still read, though — the record in `#size-report=…`, in the same shape the memory holds it —
|
|
422
|
+
and such a link outranks the memory while it does not replace the reader's own until he changes something.
|
|
423
|
+
A foreign or broken address is not applied, and is not silent either: a line above the table names the
|
|
424
|
+
reason ("the link was made in another report" / "the choice in the address is unreadable"), the view stays
|
|
425
|
+
the reader's own, and the incoming address is not rewritten; names the report does not hold are reported
|
|
426
|
+
by count, skipped, and the rest is applied. The link works on an already open page as well: the browser
|
|
427
|
+
does not reload the document when the anchor changes, so the page reads the address itself, or a link
|
|
428
|
+
would only work in a new tab. The page makes no request to the network at all, and that is an assertion of
|
|
429
|
+
a check rather than a promise (`test/page-view.test.js`, `test/parity.test.js`).
|
|
386
430
|
|
|
387
431
|
**The `min` metric can count for real.** The way of counting is chosen in the settings:
|
|
388
432
|
`"minify": {"engine": "esbuild"}` minifies JS/TS/CSS for real through an optional dependency, while
|
|
@@ -516,11 +560,12 @@ acceptance for each.
|
|
|
516
560
|
| `src/table.css` | The report's table: the geometry of a cell, the sticky header and commit column, the colour of deltas |
|
|
517
561
|
| `src/page/app.css` | The page's styling on top of the shared part: the panel with the file tree and its sticky row of categories (a column on the left on a wide screen, the page fitting the window), the empty states, a narrow window |
|
|
518
562
|
| `src/page/payload.js` | The page's block in sparse form, and the one place that unrolls it back: the history as changes (a file's appearance, its moves, its disappearance) turned into the snapshots the calculation and the table already speak — a value that did not move is one object shared by the rows that hold it |
|
|
519
|
-
| `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",
|
|
563
|
+
| `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 |
|
|
520
564
|
| `src/page/dom.js` | The page's nodes: the small helpers of markup (`appEl`, `appBox`) — one set for the panel and the table alike |
|
|
521
|
-
| `src/page/panel.js` | The panel of choices: the switches of metrics and files, the categories, the tree of the project's paths (files outside the report keep a checkbox off with a reason and stand after the rest; folders carry a
|
|
522
|
-
| `src/page/table.js` | The page's table, built once: a cell, a commit's caption, the header, the empty states and the cache of the nodes of every column — markup over the shared calculation, with the totals carried rather than recounted |
|
|
523
|
-
| `src/page/
|
|
565
|
+
| `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 |
|
|
566
|
+
| `src/page/table.js` | The page's table, built once: a cell, a commit's caption, the header, the empty states and the cache of the nodes of every column — markup over the shared calculation, with the totals carried rather than recounted, and the count answerable for how many nodes a column holds |
|
|
567
|
+
| `src/page/work.js` | The page's long drawing: the columns of a switch in one task, on the click when they are few and in the next task with an indeterminate stripe over the top edge of the window when they are many — one task because the browser's relayout of the table is the price, and it is paid once (this is where the measurement that refused the slices stands) |
|
|
568
|
+
| `src/page/app.js` | Assembling and starting the page: the first drawing, then a switch that counts the numbers on the click, hands the columns to the queue and writes the fields it reached without making a node; an anchor change; pasted into the assembled page |
|
|
524
569
|
| `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` |
|
|
525
570
|
| `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 |
|
|
526
571
|
| `src/strip.js` | Removing ballast: which form goes to which file (extension, strategy) and which strategies are minification itself — the entry to the parsing of forms |
|
|
@@ -541,7 +586,7 @@ acceptance for each.
|
|
|
541
586
|
| `bin/postinstall.js` | Installing the hook after the package is added: it looks for the consumer project and stays silent when there is nowhere to install |
|
|
542
587
|
| `src/artifact.js` | The report on disk: the only place where it becomes a file (both `--write` and the hook use it); the report is a self-contained page |
|
|
543
588
|
| `src/journal.js` | The journal and links: which section a commit belongs to and where a description leads |
|
|
544
|
-
| `src/data.js` | The file categories and the contract with the page (`--data`): the numbers, the shape of the table and the catalogue of the project's paths |
|
|
589
|
+
| `src/data.js` | The file categories (a test by its path — a `test`, `tests` or `fixtures` folder, or `.test` before the extension — otherwise the extension, and the settings above both) and the contract with the page (`--data`): the numbers, the shape of the table and the catalogue of the project's paths |
|
|
545
590
|
| `src/config.js` | The consumer project's settings: the defaults, reading them, checking them |
|
|
546
591
|
| `src/project.js` | The settings derived from the project itself (its tree and history): columns, the journal, the exceptions, the catalogue of paths for the page's tree. Without a settings file it *is* the settings; `--init` pins it as a file |
|
|
547
592
|
| `src/locales.js`, `src/refusal.js`, `src/tool.js` | The report's texts; the exit codes and the help; the package's name and version |
|
|
@@ -569,8 +614,8 @@ acceptance for each.
|
|
|
569
614
|
| `test/contract-data.test.js` | The data contract: the numbers against the reference, the set of fields against the derived quantities, the metric's method against the way the numbers were counted — and the round trip through the page's sparse block, which restores the contract whole and twice over the same bytes |
|
|
570
615
|
| `test/contract-derived.test.js` | The derived quantities against the artifact's numbers: a row's totals, a cell's delta and the delta of a total — on the code that lies in the tree |
|
|
571
616
|
| `test/page-view.test.js` | The assembled page: pasted with no copy of the calculation, self-contained, the empty states, the styling, the switches, a click that makes no table and the carried totals against the engine's own sums |
|
|
572
|
-
| `test/page-tree.test.js` | The panel's file tree: folders by the project's paths, three states, the subtree, files and folders outside the report (a checkbox off, a place after the rest), folding without a rebuild and a scroll a click does not touch |
|
|
573
|
-
| `test/page-choice.test.js` | The
|
|
617
|
+
| `test/page-tree.test.js` | The panel's file tree: folders by the project's paths, three states, the subtree, files and folders outside the report (a checkbox off, a place after the rest), the hidden names at the end of a level, the tree that opens folded and the unfolding the memory keeps, folding without a rebuild and a scroll a click does not touch |
|
|
618
|
+
| `test/page-choice.test.js` | The reader's choice and the work a click starts: the memory and a revisit, someone else's report, a foreign and a broken record, an address that stays clean, a link read at opening and on an open page, and the drawing of a long switch in a task of its own behind the stripe |
|
|
574
619
|
| `test/page-cols.test.js` | The fixed layout the page carries: a column's width is counted from the model rather than measured in a laid-out cell, the clip keeps a caption inside its column, the sticky header and commit column keep their edges, and the table names its own width (`width: auto` would hand the layout back to the automatic algorithm) |
|
|
575
620
|
| `test/module.test.js` | A module under a `.js` extension: measured without touching the settings; the stripper's guard is alive (proved by mutation) and does not accuse the innocent |
|
|
576
621
|
| `test/guard.test.js` | Parsing a module: it goes through a thread, both paths give one verdict, the fallback works with the thread's file away, and hundreds of parses are cheaper than a launch |
|
|
@@ -651,7 +696,7 @@ The same release can be taken by a reference to the repository — installation
|
|
|
651
696
|
registry, but stays tied to a revision:
|
|
652
697
|
|
|
653
698
|
```bash
|
|
654
|
-
pnpm add -D github:vernikr/size-report#v2.
|
|
699
|
+
pnpm add -D github:vernikr/size-report#v2.7.0
|
|
655
700
|
```
|
|
656
701
|
|
|
657
702
|
With no network (or nothing to fetch from codeload) — the tarball: `pnpm pack` in the package clone, then
|
|
@@ -663,7 +708,7 @@ the branch moves on the installation fails with `Could not resolve <sha> to a co
|
|
|
663
708
|
observation rather than reasoning: the short pin `6530237` installed while `main` stood on it and stopped
|
|
664
709
|
working at the very next commit, while the same sha in full installed. A branch name (`#main`) and a tag
|
|
665
710
|
are both accepted, but a branch is a moving target and a tag is constant: this release stands on the tag
|
|
666
|
-
`v2.
|
|
711
|
+
`v2.7.0`, which is also the one in the example (forty characters work as well, but they have to be copied
|
|
667
712
|
out of the history by eye).
|
|
668
713
|
|
|
669
714
|
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.
|
|
3
|
+
"version": "2.7.0",
|
|
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.
|
|
68
|
+
"@vernikr/size-report": "2.6.0",
|
|
69
69
|
"c8": "10",
|
|
70
70
|
"dependency-cruiser": "17",
|
|
71
71
|
"eslint": "^9.18.0",
|
package/src/data.js
CHANGED
|
@@ -10,19 +10,35 @@ import { TOOL_PKG } from './tool.js';
|
|
|
10
10
|
* derived quantity at all. Everything the page counts itself begins where this module ends. */
|
|
11
11
|
|
|
12
12
|
/* A file's category is only for the page's quick on/off buttons for a group: it does not reach the
|
|
13
|
-
* numbers. The rule is
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* numbers. The rule is: a category set in the column's settings outranks everything, a test is
|
|
14
|
+
* recognised by its path rather than by its extension, and for the rest the extension gives the
|
|
15
|
+
* category while everything else counts as code. The data says where the category came from
|
|
16
|
+
* (`categoryBy`) — a manual decision is explainable, while a table of extensions is a guess by file
|
|
17
|
+
* name. */
|
|
16
18
|
export const CATEGORY_EXTS = {
|
|
17
19
|
docs: ['.md', '.markdown', '.rst', '.txt', '.adoc'],
|
|
18
20
|
chore: ['.json', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf', '.lock', '.editorconfig'],
|
|
19
21
|
assets: ['.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.woff', '.woff2', '.ttf', '.otf']
|
|
20
22
|
};
|
|
21
|
-
|
|
23
|
+
/* Tests stand next to code: they are files of the same kind (`*.test.js` is JavaScript too), and a reader
|
|
24
|
+
* switching code off wants to see what the tests weigh separately. */
|
|
25
|
+
export const CATEGORY_ORDER = ['code', 'tests', 'docs', 'chore', 'assets'];
|
|
26
|
+
|
|
27
|
+
/* What makes a file a test: the folder it lies in, wherever that folder is in the project, or a name
|
|
28
|
+
* carrying `.test` before its extension. Both are read off the path rather than off the extension, which is
|
|
29
|
+
* how a test is recognised in practice — a test may be written in any language the tool can measure. */
|
|
30
|
+
const TEST_DIRS = ['test', 'tests', 'fixtures'];
|
|
31
|
+
const TEST_NAME = /\.test\.[^./]+$/;
|
|
32
|
+
|
|
33
|
+
function isTest(p) {
|
|
34
|
+
return p.split('/').some((part) => TEST_DIRS.indexOf(part) >= 0) || TEST_NAME.test(path.basename(p));
|
|
35
|
+
}
|
|
22
36
|
|
|
23
37
|
export function categoryOf(col) {
|
|
24
38
|
if (col.category) return { key: col.category, by: 'config' };
|
|
25
|
-
const
|
|
39
|
+
const last = col.paths[col.paths.length - 1];
|
|
40
|
+
if (isTest(last)) return { key: 'tests', by: 'auto' };
|
|
41
|
+
const ext = path.extname(last).toLowerCase();
|
|
26
42
|
const known = CATEGORY_ORDER.find((key) => (CATEGORY_EXTS[key] || []).indexOf(ext) >= 0);
|
|
27
43
|
return { key: known === undefined ? 'code' : known, by: 'auto' };
|
|
28
44
|
}
|
package/src/locales.js
CHANGED
|
@@ -9,7 +9,7 @@ export const LOCALES = {
|
|
|
9
9
|
commit: 'Коммит',
|
|
10
10
|
total: 'Общий объём',
|
|
11
11
|
now: 'сейчас',
|
|
12
|
-
categories: { code: 'Код', docs: 'Документация', chore: 'Служебные', assets: 'Ресурсы' },
|
|
12
|
+
categories: { code: 'Код', tests: 'Тесты', docs: 'Документация', chore: 'Служебные', assets: 'Ресурсы' },
|
|
13
13
|
/* The page texts live inside the report file itself (a dictionary of their own, next to
|
|
14
14
|
* the data), so changing a word costs a rebuild of the report — otherwise the file
|
|
15
15
|
* drifts from the history. */
|
|
@@ -53,6 +53,9 @@ export const LOCALES = {
|
|
|
53
53
|
* таблица не сказала бы читателю ничего. */
|
|
54
54
|
unpack: 'Данные отчёта сжаты, а браузер не умеет их распаковать: в нём нет DecompressionStream. '
|
|
55
55
|
+ 'Обновите браузер — таблицу показывать не из чего.',
|
|
56
|
+
/* Полоска вверху страницы: имя для тех, кто читает страницу не глазами, — она показывает,
|
|
57
|
+
* что идёт отрисовка колонок, а не что отчёт завис. */
|
|
58
|
+
working: 'отрисовка колонок',
|
|
56
59
|
note: 'Абсолютные размеры стоят один раз — в строке «{now}». Строк: {rows}: коммиты без '
|
|
57
60
|
+ 'изменения объёма и коммиты, тронувшие только выключенные файлы, строк не получают. '
|
|
58
61
|
+ 'Собрано из истории git: {command}.'
|
|
@@ -80,7 +83,7 @@ export const LOCALES = {
|
|
|
80
83
|
commit: 'Commit',
|
|
81
84
|
total: 'Total',
|
|
82
85
|
now: 'now',
|
|
83
|
-
categories: { code: 'Code', docs: '
|
|
86
|
+
categories: { code: 'Code', tests: 'Tests', docs: 'Docs', chore: 'Chores', assets: 'Assets' },
|
|
84
87
|
page: {
|
|
85
88
|
metrics: 'Metrics',
|
|
86
89
|
files: 'Files',
|
|
@@ -113,6 +116,9 @@ export const LOCALES = {
|
|
|
113
116
|
* browser without `DecompressionStream` gets instead of an empty table. */
|
|
114
117
|
unpack: 'The report\u2019s data is compressed and this browser cannot unpack it: there is no '
|
|
115
118
|
+ 'DecompressionStream. Update the browser — there is nothing to build the table from.',
|
|
119
|
+
/* The bar at the top of the page: a name for whoever reads the page without eyes — it shows
|
|
120
|
+
* that the columns are being drawn rather than that the report has hung. */
|
|
121
|
+
working: 'drawing the columns',
|
|
116
122
|
note: 'Absolute sizes appear once — in the “{now}” row. Rows: {rows}: commits that change '
|
|
117
123
|
+ 'no number, and commits touching only the disabled files, get no row. '
|
|
118
124
|
+ 'Collected from git history: {command}.'
|
package/src/page/app.css
CHANGED
|
@@ -127,6 +127,33 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
127
127
|
/* Numbers are denser than the page's text: there are more of them, they are shorter, and they are read by their digits. */
|
|
128
128
|
#grid { font-size: 12.5px; }
|
|
129
129
|
|
|
130
|
+
/* The stripe of a long drawing: over the window's own top edge for as long as a switch is being drawn (`src/page/work.js`).
|
|
131
|
+
* It is fixed rather than laid out — the page's shape is a grid of five rows, and a stripe appearing inside it would move the
|
|
132
|
+
* numbers every time a switch is drawn — and it is taken away as soon as the drawing is over, so the reader never sees it at
|
|
133
|
+
* rest. It **does not fill**: the drawing is one task of the browser's own layout, inside which nothing can repaint, so a
|
|
134
|
+
* share would be a share nobody has (`src/page/work.js` says what was measured). What moves instead is an indeterminate
|
|
135
|
+
* stripe — a travelling highlight, which is what this state is in the language of progress — and it moves by a transform,
|
|
136
|
+
* so the stripe itself costs the compositor rather than the page. Its colour is the system accent, the one colour the page
|
|
137
|
+
* uses for anything but the numbers, and it carries a role and a name, because a reader without eyes has to be told that
|
|
138
|
+
* work is going on rather than that the report is broken. */
|
|
139
|
+
.bar { position: fixed; top: 0; left: 0; right: 0; z-index: 5; height: 3px; overflow: hidden; }
|
|
140
|
+
.bar[hidden] { display: none; }
|
|
141
|
+
.bar > i {
|
|
142
|
+
display: block;
|
|
143
|
+
width: 30%;
|
|
144
|
+
height: 100%;
|
|
145
|
+
background: AccentColor;
|
|
146
|
+
animation: bar-run 1.1s linear infinite;
|
|
147
|
+
}
|
|
148
|
+
@keyframes bar-run {
|
|
149
|
+
from { transform: translateX(-100%); }
|
|
150
|
+
to { transform: translateX(333%); }
|
|
151
|
+
}
|
|
152
|
+
/* Whoever asked the system for less motion gets a still stripe: the state is what matters, not its travelling. */
|
|
153
|
+
@media (prefers-reduced-motion: reduce) {
|
|
154
|
+
.bar > i { animation: none; width: 100%; }
|
|
155
|
+
}
|
|
156
|
+
|
|
130
157
|
/* The message about a link that came in stands above the table, so that it cannot be missed, while it does not push the
|
|
131
158
|
* table away: one line in the place of the page. Its colour is the system one (the accent), for the page keeps no colours
|
|
132
159
|
* of its own. */
|
package/src/page/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appUnpack } from './payload.js';
|
|
2
|
-
import {
|
|
3
|
-
import { appColumn, appContribute, appMetrics, appState, appTable, appTotals, appTotalsReset } from './table.js';
|
|
2
|
+
import { appApply, appBoot, appData, appFoldRead, appLinkUse, appNotice, appRead, appUi, appView, appWrite } from './state.js';
|
|
3
|
+
import { appColumn, appColumnSize, appColumnStale, appContribute, appMetrics, appState, appTable, appTotals, appTotalsReset } from './table.js';
|
|
4
|
+
import { appDraw } from './work.js';
|
|
4
5
|
import { appPanel, appPanelAll, appPanelState } from './panel.js';
|
|
5
6
|
|
|
6
7
|
/* Assembling the report: the table is built once (`appTable` of the table chapter) and everything afterwards only
|
|
@@ -10,12 +11,32 @@ import { appPanel, appPanelAll, appPanelState } from './panel.js';
|
|
|
10
11
|
*
|
|
11
12
|
* Hence two paths and no third: `appPaint` draws the whole view (the first drawing, a record from the browser's
|
|
12
13
|
* memory, a link in the address), while `appSwitch`, `appSwitchGroup` and `appSwitchMetric` are what one click on a
|
|
13
|
-
* box does.
|
|
14
|
+
* box does. The numbers are counted on the click itself — that is arithmetic over the data — while the nodes of the
|
|
15
|
+
* columns are handed to the work chapter (`appDraw`), which draws the short work on the click and the long one in the
|
|
16
|
+
* next task with a stripe over the page (`src/page/work.js` says what was measured: the price of a switch is the
|
|
17
|
+
* browser's own relayout of the table, and it is paid once here rather than once per slice). Neither path makes a
|
|
18
|
+
* node.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
// The table's cache of node references: made once, at the first drawing.
|
|
17
22
|
let appCache = null;
|
|
18
23
|
|
|
24
|
+
/* One file's column drawn from the view — the state is read when the slice runs rather than kept from the click that
|
|
25
|
+
* queued it, so a click that arrives while the queue is running is drawn by the next slice rather than after it. */
|
|
26
|
+
function appDrawColumn(i) {
|
|
27
|
+
appColumn(appCache, i, appView.files[i] === true);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* The columns of a switch that have to be drawn, as the units the work chapter weighs (`{i, units}` — the file and the
|
|
31
|
+
* nodes of its column). Only the columns whose nodes are out of step with the view are asked for (`appColumnStale`): a
|
|
32
|
+
* report opened with everything switched on has nothing to draw, and a column that is already right would cost its
|
|
33
|
+
* nodes again. The count is the table's (`appColumnSize`), because the table is the only place that knows how many
|
|
34
|
+
* nodes a column has. */
|
|
35
|
+
function appColumns(indexes) {
|
|
36
|
+
const todo = indexes.filter((i) => appColumnStale(appCache, i));
|
|
37
|
+
appDraw(appDrawColumn, todo.map((i) => ({ i: i, units: appColumnSize(appCache, i) })));
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
/* The note under the table: what a row is and how the report was made. It does not depend on the choice, so it is
|
|
20
41
|
* written once — with the table rather than with every drawing of it. */
|
|
21
42
|
function appNote() {
|
|
@@ -34,7 +55,7 @@ export function appCounts() {
|
|
|
34
55
|
/* The whole view drawn: every column, the totals of the whole selection, the metrics, the empty states and the panel's
|
|
35
56
|
* fields. This is what a link, a record from the memory and the first drawing need — and it makes no node either. */
|
|
36
57
|
export function appPaint() {
|
|
37
|
-
appData.files.
|
|
58
|
+
appColumns(appData.files.map((_f, i) => i));
|
|
38
59
|
appTotalsReset(appCache);
|
|
39
60
|
appMetrics(appCache);
|
|
40
61
|
appCounts();
|
|
@@ -42,28 +63,29 @@ export function appPaint() {
|
|
|
42
63
|
appWrite();
|
|
43
64
|
}
|
|
44
65
|
|
|
45
|
-
/* One file switched by the reader: the view, its
|
|
46
|
-
*
|
|
66
|
+
/* One file switched by the reader: the view, its share of the totals, the fields it shows in and its column — the
|
|
67
|
+
* last through the queue, because a column may be long. The message about a link fades here: by this action the
|
|
68
|
+
* reader has read it. */
|
|
47
69
|
export function appSwitch(i, on) {
|
|
48
70
|
if (appView.files[i] === on) return;
|
|
49
71
|
appView.files[i] = on;
|
|
50
|
-
appColumn(appCache, i, on);
|
|
51
72
|
appContribute(appCache, i, on);
|
|
52
73
|
appTotals(appCache);
|
|
53
74
|
appCounts();
|
|
54
75
|
appPanelState([i]);
|
|
55
76
|
appWrite();
|
|
56
77
|
appNotice('');
|
|
78
|
+
appColumns([i]);
|
|
57
79
|
}
|
|
58
80
|
|
|
59
81
|
/* A group switched at once — a folder or a category: the same work per file, then the totals once and the fields of
|
|
60
82
|
* the files the choice really reached (switching a folder on when a part of it was already on touches only the rest,
|
|
61
|
-
* and a field that did not move is not written).
|
|
83
|
+
* and a field that did not move is not written). The columns of the whole group go into the queue together, so the bar
|
|
84
|
+
* counts them as one piece of work. */
|
|
62
85
|
export function appSwitchGroup(indexes, on) {
|
|
63
86
|
const touched = indexes.filter((i) => appView.files[i] !== on);
|
|
64
87
|
touched.forEach((i) => {
|
|
65
88
|
appView.files[i] = on;
|
|
66
|
-
appColumn(appCache, i, on);
|
|
67
89
|
appContribute(appCache, i, on);
|
|
68
90
|
});
|
|
69
91
|
appTotals(appCache);
|
|
@@ -71,6 +93,7 @@ export function appSwitchGroup(indexes, on) {
|
|
|
71
93
|
appPanelState(touched);
|
|
72
94
|
appWrite();
|
|
73
95
|
appNotice('');
|
|
96
|
+
appColumns(touched);
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
/* One metric switched: a class on the table and the headings' `colSpan`. The totals do not move with a metric — they
|
|
@@ -110,19 +133,18 @@ async function appBegin() {
|
|
|
110
133
|
return;
|
|
111
134
|
}
|
|
112
135
|
const appStart = appLinkUse();
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
/* A link that came in is the sender's choice rather than the reader's: while it is drawn, the memory is not touched
|
|
137
|
+
* (`appWrite`), so opening a link does not make it the reader's own. */
|
|
138
|
+
appTransient = appStart === 'ours';
|
|
115
139
|
if (appStart !== 'ours') {
|
|
116
140
|
const appSaved = appRead();
|
|
117
141
|
if (appSaved !== null) appApply(appSaved);
|
|
118
142
|
}
|
|
119
|
-
/* The
|
|
143
|
+
/* The unfolded tree is the onlooker's memory rather than the reader's choice: it comes back even when someone
|
|
120
144
|
* else's link is open (otherwise a link sent over would unfold the tree again on every visit). */
|
|
121
145
|
appFoldRead();
|
|
122
146
|
appFirst();
|
|
123
147
|
appBooted = true;
|
|
124
|
-
appStartup = false;
|
|
125
|
-
appForeign = false;
|
|
126
148
|
appTransient = false;
|
|
127
149
|
}
|
|
128
150
|
|
|
@@ -138,13 +160,10 @@ window.addEventListener('hashchange', () => {
|
|
|
138
160
|
/* The first drawing has not happened yet: the address the page was opened with is the business of that drawing,
|
|
139
161
|
* and a change that arrives before it has drawn nothing to replace. */
|
|
140
162
|
if (!appBooted) return;
|
|
141
|
-
/* An address that came in from outside is read
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (state === 'refused') appForeign = true;
|
|
146
|
-
appTransient = state === 'ours';
|
|
163
|
+
/* An address that came in from outside is read here the way it is read at opening, and a link is not the reader's
|
|
164
|
+
* choice until he changes something — the memory stays his own. The address itself is never rewritten: the page has
|
|
165
|
+
* no business in the tab's title bar, and what a link holds is read rather than made. */
|
|
166
|
+
appTransient = appLinkUse() === 'ours';
|
|
147
167
|
appPaint();
|
|
148
|
-
appForeign = false;
|
|
149
168
|
appTransient = false;
|
|
150
169
|
});
|
package/src/page/build.js
CHANGED
|
@@ -43,7 +43,7 @@ export function pageSource(file) {
|
|
|
43
43
|
/* The list of chapters lives here rather than in the tests: one copy for the builder and for the guard
|
|
44
44
|
* (`test/page-view.test.js` reads the same program and compares it with the sources). The payload chapter comes
|
|
45
45
|
* first of the page's own: it is what turns the block into the data everything else reads. */
|
|
46
|
-
export const PAGE_PARTS = ['./payload.js', './state.js', './dom.js', './panel.js', './table.js', './app.js'];
|
|
46
|
+
export const PAGE_PARTS = ['./payload.js', './state.js', './dom.js', './work.js', './panel.js', './table.js', './app.js'];
|
|
47
47
|
|
|
48
48
|
/* The form the artifact carries: the same stripping the `min` metric counts (`src/strip.js`) — comments out,
|
|
49
49
|
* indentation and blank lines out — applied to what is pasted, while the sources on disk keep everything: they are
|
|
@@ -255,6 +255,11 @@ export function pageHtml(data, cfg) {
|
|
|
255
255
|
+ squeezedCss(TABLE_CSS) + '\n' + squeezedCss(PAGE_CSS) + '\n</style>\n</head>\n<body>\n'
|
|
256
256
|
+ '<header>\n<h1>' + esc(data.report.heading) + '</h1>\n'
|
|
257
257
|
+ '<p class="sub">' + esc(subText(data, loc.page)) + '</p>\n</header>\n'
|
|
258
|
+
/* The stripe of the drawing stands first in the page and is fixed to the window's top edge rather than laid out
|
|
259
|
+
* with the rest: the page's shape is a grid of five rows (`src/page/app.css`), and a stripe that appeared and
|
|
260
|
+
* disappeared inside it would move the numbers under the reader's eyes every time a switch is drawn. It holds an
|
|
261
|
+
* empty child rather than a share: the length of an indeterminate stripe is the styling's business. */
|
|
262
|
+
+ '<div id="bar" class="bar" role="progressbar" aria-label="' + esc(loc.page.working) + '" hidden><i></i></div>\n'
|
|
258
263
|
+ '<div id="panel" class="panel"></div>\n'
|
|
259
264
|
+ '<p id="notice" class="notice" hidden></p>\n'
|
|
260
265
|
+ '<div id="shell" class="shell"><table id="grid"></table></div>\n'
|
package/src/page/panel.js
CHANGED
|
@@ -66,27 +66,41 @@ function appDirHead(name, here, sub) {
|
|
|
66
66
|
return head;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/* The order inside one level, and the two rules of it: a hidden name (a leading dot) stands after every visible one —
|
|
70
|
+
* a project's service files are not what a reader looks for first — and otherwise the alphabet decides. "Other things
|
|
71
|
+
* being equal" is the whole of it: what the report holds stands before what it does not, and that partition is made
|
|
72
|
+
* before the names are compared. The rule is a function of two strings, which is what lets it be checked on its own
|
|
73
|
+
* rather than through a tree of a fixture that has no hidden files in it. */
|
|
74
|
+
export function appName(a, b) {
|
|
75
|
+
const hidden = (name) => (name.charAt(0) === '.' ? 1 : 0);
|
|
76
|
+
if (hidden(a) !== hidden(b)) return hidden(a) - hidden(b);
|
|
77
|
+
return a < b ? -1 : (a > b ? 1 : 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
/* The folder's sign is a click target of its own, separate from the checkbox: the checkbox answers for the numbers (it
|
|
70
81
|
* switches the subtree's files on), while the sign answers for how much of the tree is visible. One target for two
|
|
71
82
|
* different decisions would mean a folder can be folded only together with switching its files on. The sign is drawn
|
|
72
83
|
* as a span rather than a button and stands beside the label rather than inside it: a label is one click target, and a
|
|
73
84
|
* control nested in it would be reached as that same target.
|
|
74
85
|
*
|
|
86
|
+
* The tree opens folded — the sign of an untouched folder says so — and the reader's unfolding is what the memory
|
|
87
|
+
* keeps (`appFoldSet`).
|
|
88
|
+
*
|
|
75
89
|
* A click on the sign rebuilds nothing: the subtree lies in the markup and a class on the row hides it. A rebuild here
|
|
76
90
|
* would be honest work for nothing — it counts the whole table (every row by every column) and so pays for numbers
|
|
77
91
|
* folding does not change. That is why only the three things the reader sees change: the class, the sign and the note
|
|
78
92
|
* in the memory. */
|
|
79
93
|
function appFoldBox(name, path) {
|
|
80
|
-
const
|
|
81
|
-
const box = appEl('span', 'fold',
|
|
82
|
-
box.title = (
|
|
94
|
+
const open = appView.open[path] === true;
|
|
95
|
+
const box = appEl('span', 'fold', open ? '▾' : '▸');
|
|
96
|
+
box.title = (open ? appUi.foldClose : appUi.foldOpen).replace('{name}', name);
|
|
83
97
|
box.addEventListener('click', () => {
|
|
84
|
-
const now = !(appView.
|
|
98
|
+
const now = !(appView.open[path] === true);
|
|
85
99
|
appFoldSet(path, now);
|
|
86
100
|
const li = box.closest('li');
|
|
87
|
-
if (li !== null) li.classList.toggle('folded', now);
|
|
88
|
-
box.textContent = now ? '
|
|
89
|
-
box.title = (now ? appUi.
|
|
101
|
+
if (li !== null) li.classList.toggle('folded', !now);
|
|
102
|
+
box.textContent = now ? '▾' : '▸';
|
|
103
|
+
box.title = (now ? appUi.foldClose : appUi.foldOpen).replace('{name}', name);
|
|
90
104
|
});
|
|
91
105
|
return box;
|
|
92
106
|
}
|
|
@@ -98,14 +112,14 @@ function appLeaves(node) {
|
|
|
98
112
|
node.others.forEach((entry) => {
|
|
99
113
|
items.push({ name: entry.path.split('/').pop(), i: null, entry: entry });
|
|
100
114
|
});
|
|
101
|
-
return items.sort((a, b) => (a.name
|
|
115
|
+
return items.sort((a, b) => appName(a.name, b.name));
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
/* A folder row: the folding sign, the checkbox with the number of files and the subtree. A folded folder differs by
|
|
105
119
|
* its class alone — the markup stays the same. */
|
|
106
120
|
function appDir(name, sub, prefix) {
|
|
107
121
|
const here = prefix === '' ? name : prefix + '/' + name;
|
|
108
|
-
const folded = appView.
|
|
122
|
+
const folded = appView.open[here] !== true;
|
|
109
123
|
const li = appEl('li', folded ? 'folded' : null);
|
|
110
124
|
li.appendChild(appFoldBox(name, here));
|
|
111
125
|
li.appendChild(appDirHead(name, here, sub));
|
|
@@ -126,7 +140,7 @@ function appLeaf(leaf) {
|
|
|
126
140
|
* it does not distract from what is in the table, while it can still be found — in the same place where it was. */
|
|
127
141
|
function appTreeList(node, prefix) {
|
|
128
142
|
const list = appEl('ul', 'tree');
|
|
129
|
-
const dirs = [...node.dirs.keys()].sort()
|
|
143
|
+
const dirs = [...node.dirs.keys()].sort(appName)
|
|
130
144
|
.map((name) => ({ name: name, sub: node.dirs.get(name), inReport: appIndexes(node.dirs.get(name)).length > 0 }));
|
|
131
145
|
const leaves = appLeaves(node);
|
|
132
146
|
const inside = leaves.filter((leaf) => leaf.entry === null);
|
package/src/page/state.js
CHANGED
|
@@ -11,10 +11,10 @@ import { appDecode } from './payload.js';
|
|
|
11
11
|
* network: the styling arrives in the same file, and the cell markup follows the rules of the shared part of the styling
|
|
12
12
|
* (`clip`, a commit's caption).
|
|
13
13
|
*
|
|
14
|
-
* The panel remembers the reader's choice between visits
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* The panel remembers the reader's choice between visits ("the choice's memory" below): the record is tied to the
|
|
15
|
+
* report's passport and keeps only what is switched off, by name, so someone else's record is not applied while a
|
|
16
|
+
* vanished name simply means nothing. The record stays in the browser's memory and nowhere else: a report opened from
|
|
17
|
+
* disk keeps a clean address, and a link made in an earlier release is still read (`appLinkUse`).
|
|
18
18
|
*
|
|
19
19
|
* The page draws no conclusion about how a number was obtained: the method of each metric arrives in the data, and the
|
|
20
20
|
* page prints it. There is no second rule of counting here, and no vocabulary of precision either. */
|
|
@@ -47,7 +47,10 @@ let appFoldKey = null;
|
|
|
47
47
|
* on the data, and until the block is unpacked there is nothing to count it from. */
|
|
48
48
|
export function appBoot(text) {
|
|
49
49
|
appData = appDecode(JSON.parse(text));
|
|
50
|
-
|
|
50
|
+
/* The tree is folded as it opens: a project's tree is longer than the window, and the reader's first look is at a
|
|
51
|
+
* short list rather than at everything. What is remembered is the opposite — the folders the reader unfolded
|
|
52
|
+
* (`appFoldRead`). */
|
|
53
|
+
appView = { metrics: {}, files: [], open: {} };
|
|
51
54
|
appMetric = {};
|
|
52
55
|
appMeasured = {};
|
|
53
56
|
appData.metrics.forEach((m) => { appView.metrics[m.key] = true; appMetric[m.key] = m; });
|
|
@@ -57,14 +60,12 @@ export function appBoot(text) {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/* A link is that same choice in the address, under a name of its own: someone else's anchor on the page does not count
|
|
60
|
-
* as a link, and there is nothing to argue with it about.
|
|
63
|
+
* as a link, and there is nothing to argue with it about. The page does not write it any more — it only reads what came
|
|
64
|
+
* with the address — and this paragraph is what such a link is read by. */
|
|
61
65
|
const APP_LINK = '#size-report=';
|
|
62
66
|
|
|
63
|
-
/*
|
|
64
|
-
*
|
|
65
|
-
* the message about the link has not faded yet. */
|
|
66
|
-
let appStartup = true;
|
|
67
|
-
let appForeign = false;
|
|
67
|
+
/* One circumstance of the first drawing, and it acts on it alone: the memory is not written while somebody else's link
|
|
68
|
+
* is open — what came in is not the reader's choice, and only his own action makes it his. */
|
|
68
69
|
let appTransient = false;
|
|
69
70
|
|
|
70
71
|
/* -------- the reader's memory of his choice -------- */
|
|
@@ -121,59 +122,27 @@ function appRecordOk(rec) {
|
|
|
121
122
|
return rec !== null && typeof rec === 'object' && rec.v === 1 && rec.passport === appPassport();
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
/* The
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
const APP_ADDRESS_DELAY = 200;
|
|
130
|
-
let appAddressTimer = null;
|
|
131
|
-
|
|
132
|
-
/* An address that came in from outside wins over a write this page has not made yet: a click arms a write, a link
|
|
133
|
-
* arrives within the delay, and the choice left behind must not land on the address the reader was sent — a refused link
|
|
134
|
-
* arms nothing to replace it, so without this the page would rewrite someone else's address a fifth of a second later. */
|
|
135
|
-
export function appAddressDrop() {
|
|
136
|
-
if (appAddressTimer === null) return;
|
|
137
|
-
clearTimeout(appAddressTimer);
|
|
138
|
-
appAddressTimer = null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function appAddressLater(text) {
|
|
142
|
-
appAddressDrop();
|
|
143
|
-
appAddressTimer = setTimeout(() => {
|
|
144
|
-
appAddressTimer = null;
|
|
145
|
-
try {
|
|
146
|
-
window.history.replaceState(null, '', APP_LINK + encodeURIComponent(text));
|
|
147
|
-
} catch (_e) {
|
|
148
|
-
/* The browser grants no change of the address: the link is then taken from the browser's memory. */
|
|
149
|
-
}
|
|
150
|
-
}, APP_ADDRESS_DELAY);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/* One record for a click and two destinations: the same text goes into the memory and — a moment later — into the
|
|
154
|
-
* address, so the two cannot describe different choices. */
|
|
125
|
+
/* The record goes into the browser's memory and nowhere else: it is written on the click itself, which is what survives
|
|
126
|
+
* a closing, and the page's address keeps a clean tail — the report is a local page whose address is copied as it is,
|
|
127
|
+
* and a reader's choice belongs in the browser that made it rather than in the tab's title bar. What a link sent from an
|
|
128
|
+
* earlier release holds is still read (`appLinkUse`), and it is not written into the reader's memory: what came in is
|
|
129
|
+
* not his choice until he changes something. */
|
|
155
130
|
export function appWrite() {
|
|
131
|
+
if (appTransient) return;
|
|
156
132
|
const rec = appRecord();
|
|
157
|
-
const text = JSON.stringify(rec);
|
|
158
133
|
const empty = Object.keys(rec.metrics).length === 0 && Object.keys(rec.files).length === 0;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
* and the markup do not depend on it. */
|
|
166
|
-
}
|
|
134
|
+
try {
|
|
135
|
+
if (empty) window.localStorage.removeItem(appKey);
|
|
136
|
+
else window.localStorage.setItem(appKey, JSON.stringify(rec));
|
|
137
|
+
} catch (_e) {
|
|
138
|
+
/* There is no memory (the browser grants this page none): the choice will not survive a closing, while the numbers
|
|
139
|
+
* and the markup do not depend on it. */
|
|
167
140
|
}
|
|
168
|
-
/* But not during the first drawing and not when the link turned out to be someone else's: an address that came in is
|
|
169
|
-
* not ours, and the reader has yet to read it. */
|
|
170
|
-
if (appStartup || appForeign) return;
|
|
171
|
-
appAddressLater(text);
|
|
172
141
|
}
|
|
173
142
|
|
|
174
143
|
/* A reset to "everything on": the border between "this is no longer in the report" and "switched off" is the record
|
|
175
|
-
* rather than a missing value. A link carries the sender's whole choice, which is why it is applied to a clean view
|
|
176
|
-
* than on top of someone else's. */
|
|
144
|
+
* rather than a missing value. A link carries the sender's whole choice, which is why it is applied to a clean view
|
|
145
|
+
* rather than on top of someone else's. */
|
|
177
146
|
function appAll() {
|
|
178
147
|
appData.metrics.forEach((m) => { appView.metrics[m.key] = true; });
|
|
179
148
|
appData.files.forEach((_f, i) => { appView.files[i] = true; });
|
|
@@ -273,14 +242,15 @@ export function appApply(rec) {
|
|
|
273
242
|
appData.files.forEach((_f, i) => { if (files[appFileAt(i)] === false) appView.files[i] = false; });
|
|
274
243
|
}
|
|
275
244
|
|
|
276
|
-
/* -------- the
|
|
277
|
-
|
|
245
|
+
/* -------- the unfolded tree -------- */
|
|
278
246
|
|
|
279
|
-
/*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
247
|
+
/* How much of the tree is visible is a memory of the same kind as the choice, but of a record of its own: it is about
|
|
248
|
+
* what the onlooker looks at rather than about which numbers are read, which is why it never goes into a link and never
|
|
249
|
+
* leaves the browser. The tree opens folded, so **the unfolded folders are what is kept** (`true`) — the default is the
|
|
250
|
+
* absence of the name, the same way "switched on" is the absence of a choice. A folder's name is its path ("src/page"),
|
|
251
|
+
* so a vanished name simply means nothing, and unfolding nothing is the state the page opens in: then the record is not
|
|
252
|
+
* kept at all rather than being kept empty. `appFoldKey` is set with the model (`appBoot`), for the reason the key
|
|
253
|
+
* itself is. */
|
|
284
254
|
export function appFoldRead() {
|
|
285
255
|
let text = null;
|
|
286
256
|
try {
|
|
@@ -296,19 +266,19 @@ export function appFoldRead() {
|
|
|
296
266
|
return;
|
|
297
267
|
}
|
|
298
268
|
if (!appRecordOk(rec)) return;
|
|
299
|
-
const
|
|
300
|
-
Object.keys(
|
|
269
|
+
const open = rec.open || {};
|
|
270
|
+
Object.keys(open).forEach((p) => { if (open[p] === true) appView.open[p] = true; });
|
|
301
271
|
}
|
|
302
272
|
|
|
303
|
-
export function appFoldSet(path,
|
|
304
|
-
if (
|
|
305
|
-
else delete appView.
|
|
306
|
-
const rec = { v: 1, passport: appPassport(),
|
|
273
|
+
export function appFoldSet(path, open) {
|
|
274
|
+
if (open) appView.open[path] = true;
|
|
275
|
+
else delete appView.open[path];
|
|
276
|
+
const rec = { v: 1, passport: appPassport(), open: Object.assign({}, appView.open) };
|
|
307
277
|
try {
|
|
308
|
-
if (Object.keys(rec.
|
|
278
|
+
if (Object.keys(rec.open).length === 0) window.localStorage.removeItem(appFoldKey);
|
|
309
279
|
else window.localStorage.setItem(appFoldKey, JSON.stringify(rec));
|
|
310
280
|
} catch (_e) {
|
|
311
|
-
/* There is no memory: what is
|
|
312
|
-
*
|
|
281
|
+
/* There is no memory: what is unfolded will not survive a closing, while the view does not depend on it — the tree
|
|
282
|
+
* is unfolded exactly the way the reader unfolded it just now. */
|
|
313
283
|
}
|
|
314
284
|
}
|
package/src/page/table.js
CHANGED
|
@@ -253,7 +253,9 @@ export function appBody(files, metrics, cache) {
|
|
|
253
253
|
export function appTable(grid) {
|
|
254
254
|
const metrics = appData.metrics.map((m) => m.key);
|
|
255
255
|
const files = appOrder();
|
|
256
|
-
const cache = { grid: grid, keys: metrics, cols: [], spans: [], cells: [], sums: [], all: [] };
|
|
256
|
+
const cache = { grid: grid, keys: metrics, cols: [], drawn: [], spans: [], cells: [], sums: [], all: [] };
|
|
257
|
+
/* The columns are built shown: that is the state the nodes carry before anything is drawn. */
|
|
258
|
+
files.forEach((i) => { cache.drawn[i] = true; });
|
|
257
259
|
metrics.forEach(() => {
|
|
258
260
|
cache.cells.push([]);
|
|
259
261
|
cache.sums.push([]);
|
|
@@ -269,10 +271,28 @@ export function appTable(grid) {
|
|
|
269
271
|
return cache;
|
|
270
272
|
}
|
|
271
273
|
|
|
274
|
+
/* How many nodes a file's column holds — its cells, its captions and its `<col>`. It is the unit the page's bar
|
|
275
|
+
* counts in (`appDraw`), so it is answered here, where the column's nodes are: the choice's state costs arithmetic,
|
|
276
|
+
* and the nodes are the table's business. */
|
|
277
|
+
export function appColumnSize(cache, i) {
|
|
278
|
+
return cache.cols[i].length;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* Whether a file's column has to be drawn at all: `drawn` is the state its nodes carry (the table is built with every
|
|
282
|
+
* column shown), so a column that is on and was never drawn is already right. The first drawing of a report has
|
|
283
|
+
* nothing switched off and therefore costs nothing, and a record from the memory or a link queues exactly the columns
|
|
284
|
+
* that differ from it — on a table of a few hundred thousand cells that is the difference between a page that opens
|
|
285
|
+
* and a page that works for a minute after opening. */
|
|
286
|
+
export function appColumnStale(cache, i) {
|
|
287
|
+
return cache.drawn[i] !== (appView.files[i] === true);
|
|
288
|
+
}
|
|
289
|
+
|
|
272
290
|
/* A file's column shown or hidden: its cells and its headings together, one class per node and no new node. A hidden
|
|
273
|
-
* column keeps its place in the markup — the order of the columns is the files' business, not the choice's.
|
|
291
|
+
* column keeps its place in the markup — the order of the columns is the files' business, not the choice's. What the
|
|
292
|
+
* nodes carry is remembered (`drawn`), or a repeated switch would walk a column nobody has to see again. */
|
|
274
293
|
export function appColumn(cache, i, on) {
|
|
275
294
|
cache.cols[i].forEach((node) => node.classList.toggle('off', !on));
|
|
295
|
+
cache.drawn[i] = on;
|
|
276
296
|
}
|
|
277
297
|
|
|
278
298
|
/* A file's share of the totals: switched off it takes exactly its own numbers out of the running sums, switched on
|
package/src/page/work.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* The page's long work, and the stripe that says it is going on.
|
|
2
|
+
*
|
|
3
|
+
* A switch on a file's box changes a class on every node of that file's column, and a folder or a category is that same
|
|
4
|
+
* work over every file below it. That alone is cheap — measured at about 2 µs a node, so a whole category of this
|
|
5
|
+
* repository's report (73 columns, 55 042 nodes) is 120 ms of it — but it is not the price. **The browser lays this
|
|
6
|
+
* table out again for any change of a column's visibility, and that is close to a second on a table of a few hundred
|
|
7
|
+
* thousand cells** (`probes/step-12-columns.mjs`: 1 cell toggled 234 ms of layout, 750 cells 287 ms, 6 000 cells
|
|
8
|
+
* 539 ms, and the whole click on the shipped page 867 ms blocked with a 742 ms task).
|
|
9
|
+
*
|
|
10
|
+
* Hence the shape of this chapter, and it is a decision rather than a default. The drawing of a switch is **one task**:
|
|
11
|
+
* what is queued is drawn in a single go, one layout, one repaint. Slicing it — a queue worked off between timeouts —
|
|
12
|
+
* was written first and measured (Chrome 153, this repository's report): 37 slices of two columns each paid the table's
|
|
13
|
+
* relayout 37 times, 2.5–4 s a slice, 95 frames and 169 layouts of 151.9 s of pure layout time against 1.04 s for the
|
|
14
|
+
* same click when it is not sliced, with the tab growing to several gigabytes of repaint. The slice is the thing that
|
|
15
|
+
* freezes the page, only more often, so there is none.
|
|
16
|
+
*
|
|
17
|
+
* What is left is honesty about it: work short enough to be over before the browser could paint a stripe is done on the
|
|
18
|
+
* click itself, and above that the stripe appears, the drawing happens in the next task, and the stripe goes away —
|
|
19
|
+
* so a reader sees that the page is working rather than wondering whether it hung. The stripe carries no share: within
|
|
20
|
+
* one task the browser cannot repaint, so a bar that filled would be a bar that lies, and the page already knows the
|
|
21
|
+
* one thing that is true about it (the work is going on).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/* Above this much node work the drawing is one task with a stripe rather than a task on the click: a stripe that
|
|
25
|
+
* appears and disappears within the same frame is worse than none, and the figure is one file's column of a report of
|
|
26
|
+
* this repository's size (754 nodes). */
|
|
27
|
+
export const APP_LONG = 2000;
|
|
28
|
+
|
|
29
|
+
/* The stripe: on while a switch is being drawn, off when it is done. There is nothing to count here, and the length is
|
|
30
|
+
* carried by the styling (an indeterminate stripe), which is why this function takes a switch rather than a share. */
|
|
31
|
+
export function appBar(shown) {
|
|
32
|
+
const bar = document.getElementById('bar');
|
|
33
|
+
if (bar !== null) bar.hidden = !shown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* What a switch asks for: `items` are the columns that have to be drawn (`{i, units}` — the file's index and the nodes
|
|
37
|
+
* of its column), `step` draws one of them from the view, and the total decides whether the drawing is the reader's own
|
|
38
|
+
* click or the next task with the stripe over it. Only the columns out of step with the view are asked for at all
|
|
39
|
+
* (`appColumnStale`), so a report opened with everything switched on has nothing to draw here. */
|
|
40
|
+
export function appDraw(step, items) {
|
|
41
|
+
/* Counted by a plain walk rather than by `reduce`: the page's shell is held to a rule that it counts no totals of the
|
|
42
|
+
* table itself, and a guard that has to tell a sum of nodes from a sum of numbers is a guard that will be argued with
|
|
43
|
+
* one day. The count of nodes is not one of the report's numbers. */
|
|
44
|
+
let units = 0;
|
|
45
|
+
items.forEach((item) => { units += item.units; });
|
|
46
|
+
if (units <= APP_LONG) {
|
|
47
|
+
items.forEach((item) => step(item.i));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
appBar(true);
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
items.forEach((item) => step(item.i));
|
|
53
|
+
appBar(false);
|
|
54
|
+
}, 0);
|
|
55
|
+
}
|