@vernikr/size-report 2.4.0 → 2.6.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 +989 -1081
- package/bin/postinstall.js +17 -18
- package/bin/size.js +2 -2
- package/package.json +3 -4
- package/src/args.js +72 -72
- package/src/artifact.js +14 -14
- package/src/check.js +41 -42
- package/src/cli.js +26 -29
- package/src/config.js +87 -91
- package/src/css.js +14 -14
- package/src/data.js +26 -50
- package/src/derived.js +31 -35
- package/src/doctor.js +95 -99
- package/src/explain.js +46 -47
- package/src/git.js +66 -71
- package/src/history.js +74 -83
- package/src/hook.js +130 -149
- package/src/init.js +37 -37
- package/src/journal.js +17 -15
- package/src/locales.js +31 -22
- package/src/metrics.js +72 -89
- package/src/minify.js +28 -27
- package/src/modes.js +57 -60
- package/src/optional.js +13 -11
- package/src/page/app.css +76 -94
- package/src/page/app.js +124 -80
- package/src/page/build.js +193 -50
- package/src/page/dom.js +8 -9
- package/src/page/panel.js +157 -69
- package/src/page/payload.js +168 -0
- package/src/page/state.js +144 -104
- package/src/page/table.js +270 -86
- package/src/parse-worker.js +10 -10
- package/src/parse.js +43 -45
- package/src/project.js +100 -104
- package/src/refusal.js +75 -76
- package/src/size-table.js +41 -76
- package/src/strip/forms.js +5 -5
- package/src/strip/guard.js +28 -28
- package/src/strip/js.js +27 -27
- package/src/strip.js +17 -21
- package/src/table.css +54 -19
- package/src/tokens.js +27 -27
- package/src/tool.js +10 -11
- package/templates/README.md +71 -77
- package/templates/ci.yml +33 -33
- package/templates/size-report.config.json +3 -3
- package/CHANGELOG.md +0 -690
package/src/history.js
CHANGED
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { assertFullHistory, diskForm, diskHashes, git, headTree, readBlobs, readHistory } from './git.js';
|
|
4
|
-
import { METRICS, measureBlob
|
|
4
|
+
import { METRICS, measureBlob } from './metrics.js';
|
|
5
5
|
import { touchedSection } from './journal.js';
|
|
6
6
|
import { EXIT, refuse } from './refusal.js';
|
|
7
7
|
|
|
8
|
-
/*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
/* Walking the history: measuring per commit, shifting numbers, carrying state between commits,
|
|
9
|
+
* comparing against the working tree, and building — the one point from which rows and state
|
|
10
|
+
* leave this layer. The top floor of reading: below it are git and the metrics, above it only
|
|
11
|
+
* assembled values. */
|
|
12
12
|
|
|
13
|
-
/*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* оно и нужно. */
|
|
13
|
+
/* Why a commit got no row is a key rather than a text: the summary counts by it and `explain`
|
|
14
|
+
* answers by it. The words a human reads live in `skipLine` and are the ones that used to be the
|
|
15
|
+
* row itself: "no change of volume" also covers a commit that touched no column at all — the
|
|
16
|
+
* report never made that distinction and the contract fixture preserves it, while the difference
|
|
17
|
+
* is visible in `explain`, where it is needed. */
|
|
19
18
|
const SKIP_WORDS = { merge: 'merge', report: 'только таблица', flat: 'без изменения объёма' };
|
|
20
19
|
|
|
21
20
|
export function skipLine(dropped) {
|
|
22
21
|
return dropped.sha.slice(0, 7) + ' (' + SKIP_WORDS[dropped.reason] + ')';
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
/*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* остаётся тем же объектом состояния. */
|
|
24
|
+
/* Whether a commit moved at least one number. Numbers are compared rather than the list of files:
|
|
25
|
+
* an edit to whitespace or comments changes no size and its row would be empty, while a merge
|
|
26
|
+
* gives zero cells whenever resolving the conflict produced what the branch already had. A column
|
|
27
|
+
* the commit did not touch keeps the very same state object. */
|
|
30
28
|
function changesVolume(state, before, columns, metrics) {
|
|
31
29
|
return columns.some((_col, i) => {
|
|
32
30
|
const a = state[i], b = before[i];
|
|
@@ -36,11 +34,11 @@ function changesVolume(state, before, columns, metrics) {
|
|
|
36
34
|
});
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
/*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
37
|
+
/* The read plan: which `revision:path` pairs will be needed, and all the content at once —
|
|
38
|
+
* otherwise every commit would cost one git call per column. A column may have both of its aliases
|
|
39
|
+
* touched by one commit: with rename detection off, git reports the old name and the new one in
|
|
40
|
+
* the same commit. All of them are collected — which one is really there is decided later, by the
|
|
41
|
+
* blobs that came back. */
|
|
44
42
|
function readPlan(cfg, root, commits, needText) {
|
|
45
43
|
const plan = commits.map((c) => {
|
|
46
44
|
const changed = new Set(c.files);
|
|
@@ -58,10 +56,10 @@ function readPlan(cfg, root, commits, needText) {
|
|
|
58
56
|
return { plan: plan, blobs: readBlobs(root, specs, needText) };
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
/*
|
|
62
|
-
* merge)
|
|
59
|
+
/* Measuring a blob with a memory kept for the run: a revision with the same content (a revert, a
|
|
60
|
+
* repeated merge) is not measured twice. */
|
|
63
61
|
function measurer(cfg) {
|
|
64
|
-
const measured = new Map(); // sha
|
|
62
|
+
const measured = new Map(); // blob sha + metric → number
|
|
65
63
|
return (name, blob, file, rev) => {
|
|
66
64
|
const key = blob.sha + '\u0000' + name;
|
|
67
65
|
if (measured.has(key)) return measured.get(key);
|
|
@@ -71,34 +69,29 @@ function measurer(cfg) {
|
|
|
71
69
|
};
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
/*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
72
|
+
/* The commit's edits applied to the state: the alias taken is the one present in the commit
|
|
73
|
+
* rather than the first in the settings order — a vanished name is absent from the commit, and a
|
|
74
|
+
* state taken in settings order lost the file (and the comparison against the tree refused). */
|
|
77
75
|
function applyPicks(pass, c, picks) {
|
|
78
76
|
picks.forEach((candidates, i) => {
|
|
79
77
|
const pick = candidates.find((cand) => pass.blobs.get(cand.spec) !== undefined);
|
|
80
78
|
if (pick === undefined) {
|
|
81
|
-
//
|
|
79
|
+
// The path is in the commit but no blob came back for it — the file was deleted.
|
|
82
80
|
if (candidates.length > 0) pass.state[i] = null;
|
|
83
81
|
return;
|
|
84
82
|
}
|
|
85
83
|
const blob = pass.blobs.get(pick.spec);
|
|
86
84
|
const cells = {};
|
|
87
|
-
/* Приближённость числа — свойство пути, а не блоба: от расширения зависит,
|
|
88
|
-
* возьмёт ли формат минификатор. Поэтому она считается здесь, вместо с
|
|
89
|
-
* замером, и в кэш содержимого не попадает. */
|
|
90
|
-
const approx = {};
|
|
91
85
|
pass.metrics.forEach((m) => {
|
|
92
86
|
cells[m] = pass.measure(m, blob, pick.path, c.sha);
|
|
93
|
-
approx[m] = !pointExact(m, pick.path, pass.cfg);
|
|
94
87
|
});
|
|
95
|
-
pass.state[i] = { path: pick.path, sha: blob.sha, cells: cells
|
|
88
|
+
pass.state[i] = { path: pick.path, sha: blob.sha, cells: cells };
|
|
96
89
|
});
|
|
97
90
|
}
|
|
98
91
|
|
|
99
|
-
/*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
92
|
+
/* One commit of the run: shift the state, then decide whether the commit needs a row. `pass` is
|
|
93
|
+
* shared by the whole run (state, lists, measurement memory), so the function only moves it
|
|
94
|
+
* forward. */
|
|
102
95
|
function stepCommit(pass, c, ci) {
|
|
103
96
|
const plan = pass.plan[ci];
|
|
104
97
|
let section = null;
|
|
@@ -129,19 +122,18 @@ function stepCommit(pass, c, ci) {
|
|
|
129
122
|
when: c.when,
|
|
130
123
|
subject: c.subject,
|
|
131
124
|
section: section,
|
|
132
|
-
cells: pass.state.map((s) => (s === null ? null : s.cells))
|
|
133
|
-
approx: pass.state.map((s) => (s === null ? null : s.approx))
|
|
125
|
+
cells: pass.state.map((s) => (s === null ? null : s.cells))
|
|
134
126
|
});
|
|
135
127
|
}
|
|
136
128
|
|
|
137
|
-
/*
|
|
138
|
-
*
|
|
129
|
+
/* The run over the history. A column's state is carried forward, and only the files changed by the
|
|
130
|
+
* commit are measured again.
|
|
139
131
|
*
|
|
140
|
-
* `known`
|
|
141
|
-
*
|
|
132
|
+
* `known` is the history already read: a pass that needs it for its own sake as well (coverage)
|
|
133
|
+
* has no reason to call `git log` a second time. */
|
|
142
134
|
export function measureHistory(cfg, root, known) {
|
|
143
135
|
const commits = known === undefined ? readHistory(root) : known;
|
|
144
|
-
//
|
|
136
|
+
// The journal text is always needed: a link to a section is not a metric, but it is a read too.
|
|
145
137
|
const needText = !!cfg.journal || cfg.metrics.some((m) => METRICS[m].needsText);
|
|
146
138
|
const reads = readPlan(cfg, root, commits, needText);
|
|
147
139
|
const pass = {
|
|
@@ -158,16 +150,16 @@ export function measureHistory(cfg, root, known) {
|
|
|
158
150
|
journalPrev: ''
|
|
159
151
|
};
|
|
160
152
|
commits.forEach((c, ci) => stepCommit(pass, c, ci));
|
|
161
|
-
/*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
153
|
+
/* Which columns the last commit touched, read from the same plan the state is carried along:
|
|
154
|
+
* a column's path is in the list of paths the commit changed. The page puts those columns ahead
|
|
155
|
+
* of the rest — the report is rebuilt after every commit, and a reader's first question is what
|
|
156
|
+
* this edit brought.
|
|
165
157
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
158
|
+
* The commit taken is the last one that touched at least one column, counting back from the top.
|
|
159
|
+
* Commits that went past the columns (above all the report itself, which the hook commits) are
|
|
160
|
+
* skipped: an edit to the report is not an edit to the project. Otherwise the mark would depend
|
|
161
|
+
* on the report's own commit — the same run would produce different bytes, the report would stop
|
|
162
|
+
* being a fixed point, and the hook would commit it a second time. */
|
|
171
163
|
let last = cfg.columns.map(() => false);
|
|
172
164
|
for (let i = commits.length - 1; i >= 0 && !last.some(Boolean); i--) {
|
|
173
165
|
const picks = reads.plan[i].picks.map((paths) => paths.length > 0);
|
|
@@ -176,20 +168,19 @@ export function measureHistory(cfg, root, known) {
|
|
|
176
168
|
return { rows: pass.rows, dropped: pass.dropped, mixed: pass.mixed, state: pass.state, last: last };
|
|
177
169
|
}
|
|
178
170
|
|
|
179
|
-
/*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* содержимое в коммите и на диске различается законно. */
|
|
171
|
+
/* The comparison against the working tree answers two questions, and both are needed: the engine's
|
|
172
|
+
* state at HEAD matches the tree of the commit, and the file on disk matches that same content.
|
|
173
|
+
* The first catches an edit lost while carrying state between commits (in a merge commit missing
|
|
174
|
+
* from the list of changed paths, say): a lost creation (the file is in the tree while the state
|
|
175
|
+
* knows nothing of it), a lost edit (the file is on both sides with different content) and a lost
|
|
176
|
+
* deletion (the state knows the file while the tree does not). An empty state is compared against
|
|
177
|
+
* the aliases present in the tree rather than against nothing: a column whose file lived in the
|
|
178
|
+
* history and was deleted before HEAD is empty on both sides — that is not a loss, and the report
|
|
179
|
+
* shows as much. The second question is an edit the history does not hold at all, and sizes are no
|
|
180
|
+
* good for it: on disk they depend on the checkout (`core.autocrlf=true`, the default in Git for
|
|
181
|
+
* Windows, gives CRLF against LF), and the tool used to refuse to work where everything was in
|
|
182
|
+
* order. Files edited in the tree drop out of the disk comparison: their content legitimately
|
|
183
|
+
* differs between the commit and the disk. */
|
|
193
184
|
function assertMatchesDisk(state, cfg, root) {
|
|
194
185
|
const dirty = new Set(git(root, ['status', '--porcelain']).split('\n')
|
|
195
186
|
.map((l) => l.trim()).filter((l) => l !== '').map((l) => l.replace(/^\S+\s+/, '').replace(/^.* -> /, '')));
|
|
@@ -202,27 +193,27 @@ function assertMatchesDisk(state, cfg, root) {
|
|
|
202
193
|
const inTree = p === undefined ? undefined : tree.get(p);
|
|
203
194
|
const lost = s === null ? aliases.length > 0 : inTree !== s.sha;
|
|
204
195
|
if (lost) {
|
|
205
|
-
refuse(EXIT.VIOLATION, '
|
|
206
|
-
+ (aliases.length === 0 ? '
|
|
196
|
+
refuse(EXIT.VIOLATION, 'the state of "' + col.label + '" at HEAD did not match the tree of the commit (in the tree: '
|
|
197
|
+
+ (aliases.length === 0 ? 'the file is absent'
|
|
207
198
|
: aliases.map((alias) => alias + ' ' + tree.get(alias).slice(0, 7)).join(', '))
|
|
208
|
-
+ ',
|
|
209
|
-
+ '):
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
+ '\n
|
|
214
|
-
+ '
|
|
199
|
+
+ ', in the state: ' + (s === null ? 'the file is absent' : s.path + ' ' + s.sha.slice(0, 7))
|
|
200
|
+
+ '): carrying the state between commits lost an edit'
|
|
201
|
+
// Rebuilding is no fix here: the state comes from this very run, so no stale table is
|
|
202
|
+
// involved in this disagreement. Hence the advice names not a fix command but the way to
|
|
203
|
+
// show the thing.
|
|
204
|
+
+ '\n fix: a rebuild does not cure this — the discrepancy is in the carrying of the state itself,'
|
|
205
|
+
+ ' not in the table. See it with: git show HEAD:' + p);
|
|
215
206
|
}
|
|
216
207
|
if (p !== undefined && !dirty.has(p) && clean.indexOf(p) < 0) clean.push(p);
|
|
217
208
|
});
|
|
218
209
|
if (clean.length === 0) return;
|
|
219
210
|
const onDisk = diskHashes(root, clean);
|
|
220
211
|
clean.forEach((p) => {
|
|
221
|
-
if (onDisk.get(p) === tree.get(p)) return; // git
|
|
222
|
-
if (fs.readFileSync(path.join(root, p)).equals(diskForm(root, 'HEAD', p))) return; //
|
|
223
|
-
refuse(EXIT.VIOLATION, '
|
|
224
|
-
+ '
|
|
225
|
-
+ '\n
|
|
212
|
+
if (onDisk.get(p) === tree.get(p)) return; // git counts the file as unmodified
|
|
213
|
+
if (fs.readFileSync(path.join(root, p)).equals(diskForm(root, 'HEAD', p))) return; // line endings are not reversible
|
|
214
|
+
refuse(EXIT.VIOLATION, 'the content of ' + p + ' on disk diverged from HEAD (' + onDisk.get(p).slice(0, 7)
|
|
215
|
+
+ ' instead of ' + tree.get(p).slice(0, 7) + '), though git does not count the file as modified: the edit exists on disk only'
|
|
216
|
+
+ '\n fix: commit the edit or roll it back: git checkout -- ' + p);
|
|
226
217
|
});
|
|
227
218
|
}
|
|
228
219
|
|
|
@@ -231,8 +222,8 @@ export function build(cfg, root) {
|
|
|
231
222
|
const measured = measureHistory(cfg, root);
|
|
232
223
|
assertMatchesDisk(measured.state, cfg, root);
|
|
233
224
|
if (measured.mixed.length > 0) {
|
|
234
|
-
console.error('!
|
|
235
|
-
+ ' —
|
|
225
|
+
console.error('! the table was updated together with the code: ' + measured.mixed.join(', ')
|
|
226
|
+
+ ' — that way the commit cannot carry a line about itself; update the table in a separate commit.');
|
|
236
227
|
}
|
|
237
228
|
return measured;
|
|
238
229
|
}
|