@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/minify.js
CHANGED
|
@@ -2,18 +2,19 @@ import path from 'path';
|
|
|
2
2
|
import { loadOptional } from './optional.js';
|
|
3
3
|
import { refuseCause } from './refusal.js';
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
*
|
|
7
|
-
* он берёт и как считается отказ.
|
|
5
|
+
/* The real minifier is an optional dependency (how that works: `src/optional.js`); what lives
|
|
6
|
+
* here is what only the minifier knows — which formats it takes and how a refusal is counted.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* A minifier refusal (the file did not parse) has to be an exception: the extension lied about
|
|
9
|
+
* the content, and falling back to a simplification would silently substitute another
|
|
10
|
+
* number. */
|
|
11
11
|
|
|
12
|
-
/*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
/* The extensions the minifier answers for. This table is the single source of truth both for
|
|
13
|
+
* the measurement and for the metric label ("the other formats lose comments and
|
|
14
|
+
* indentation"), so the two cannot drift apart. JSX and TSX are not here: the output depends on the project's `jsx`
|
|
15
|
+
* setting (`React.createElement` versus `react/jsx-runtime`), and measuring someone else's
|
|
16
|
+
* decision about a runtime is not this tool's business — such files are honestly counted as a
|
|
17
|
+
* simplification. */
|
|
17
18
|
export const MINIFY_LOADERS = {
|
|
18
19
|
'.js': 'js', '.mjs': 'js', '.cjs': 'js',
|
|
19
20
|
'.ts': 'ts', '.mts': 'ts', '.cts': 'ts',
|
|
@@ -22,21 +23,21 @@ export const MINIFY_LOADERS = {
|
|
|
22
23
|
|
|
23
24
|
let probed = null;
|
|
24
25
|
|
|
25
|
-
/*
|
|
26
|
-
*
|
|
26
|
+
/* The probe answer is kept for the process: probing on every file would mean paying for it
|
|
27
|
+
* thousands of times, while the answer does not depend on the file. */
|
|
27
28
|
export function minifier() {
|
|
28
29
|
if (probed === null) probed = loadOptional('esbuild');
|
|
29
30
|
return probed;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
/*
|
|
33
|
-
* `charset: utf8`
|
|
34
|
-
*
|
|
35
|
-
* none`
|
|
36
|
-
*
|
|
33
|
+
/* Compressing one text. The output settings are pinned rather than left at their defaults:
|
|
34
|
+
* `charset: utf8` because what is measured is a UTF-8 file of the project (the default would
|
|
35
|
+
* escape non-ASCII and the number would come out larger than the real one), `legalComments:
|
|
36
|
+
* none` because every other strategy drops comments too and the number has to mean one thing
|
|
37
|
+
* rather than two, and `sourcefile` for the reason inside a refusal. */
|
|
37
38
|
export function minifyWithEsbuild(text, file, rev) {
|
|
38
39
|
const { tool, why } = minifier();
|
|
39
|
-
if (tool === null) throw new Error('
|
|
40
|
+
if (tool === null) throw new Error('the minifier is unavailable: ' + why);
|
|
40
41
|
const ext = path.extname(file).toLowerCase();
|
|
41
42
|
try {
|
|
42
43
|
return tool.transformSync(text, {
|
|
@@ -47,19 +48,19 @@ export function minifyWithEsbuild(text, file, rev) {
|
|
|
47
48
|
sourcefile: file
|
|
48
49
|
}).code;
|
|
49
50
|
} catch (e) {
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
refuseCause('
|
|
51
|
+
// The advice names the one way out that answers this very cause: switching the minifier to
|
|
52
|
+
// `strip` removes the cause but hands the same file to the `minify.guard` check, whose
|
|
53
|
+
// verdict would be the same ("this is not JavaScript").
|
|
54
|
+
refuseCause('minifier did not parse', 'esbuild did not parse ' + file + ' at '
|
|
54
55
|
+ rev.slice(0, 7) + ': ' + cause(e.message)
|
|
55
|
-
+ '\n
|
|
56
|
-
+ '
|
|
56
|
+
+ '\n fix: the extension lied about its content or the minifier is older than the syntax;'
|
|
57
|
+
+ ' give this extension a simplification in minify.ext (for example {"' + ext + '": "strip-lines"})');
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
/*
|
|
61
|
-
* errors
|
|
62
|
-
*
|
|
61
|
+
/* The reason from esbuild spans several lines and its first line is "Transform failed with N
|
|
62
|
+
* errors:"; the cause itself stands where the error starts. Without it a refusal would say
|
|
63
|
+
* that something is wrong without saying what. */
|
|
63
64
|
function cause(text) {
|
|
64
65
|
const lines = String(text).split('\n');
|
|
65
66
|
const at = lines.findIndex((line) => line.indexOf('ERROR:') >= 0);
|
package/src/modes.js
CHANGED
|
@@ -13,28 +13,24 @@ import { artifact, rebuild } from './artifact.js';
|
|
|
13
13
|
import { sensorGaps } from './metrics.js';
|
|
14
14
|
import { totalsOf } from './derived.js';
|
|
15
15
|
|
|
16
|
-
/*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* Что где: сборка и сверка отчёта (`--write`, проверка), данные контракта
|
|
23
|
-
* (`--data`), полнота покрытия (`size check`), диагностика (`doctor`), хук и
|
|
24
|
-
* объяснение пропущенной строки. Файл знает про все остальные модули сразу — это
|
|
25
|
-
* его работа: связать их в одну команду.
|
|
16
|
+
/* Modes: what the tool does on request. The arguments are parsed in `src/args.js`, and a
|
|
17
|
+
* ready plan arrives here — which mode, which flag, what to print. Their shared bits live
|
|
18
|
+
* here too (the "!" note about a different count, the verdict, a size in words), one owner
|
|
19
|
+
* for all modes, so that one count and one mark cannot diverge between `--write`, `--data`,
|
|
20
|
+
* `check` and the rest. Knowing every other module at once is this file's job: it ties them
|
|
21
|
+
* into one command.
|
|
26
22
|
*/
|
|
27
23
|
|
|
28
24
|
function kmb(bytes) {
|
|
29
|
-
return Math.round(bytes / 1024) + '
|
|
25
|
+
return Math.round(bytes / 1024) + ' KB';
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
/*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
28
|
+
/* Degradation is a fact of the report, not an error: the numbers came from a different
|
|
29
|
+
* method (stripping instead of minification, an estimate instead of a counted one) because
|
|
30
|
+
* an optional dependency is missing. The fact is printed once per sensor and becomes code
|
|
31
|
+
* 4 — otherwise a different count would travel into CI as the requested one. */
|
|
36
32
|
function note(gaps) {
|
|
37
|
-
gaps.forEach((gap) => console.error('! ' + gap.why + '\n
|
|
33
|
+
gaps.forEach((gap) => console.error('! ' + gap.why + '\n fix: ' + gap.fix));
|
|
38
34
|
return gaps.length === 0 ? EXIT.OK : EXIT.SENSOR;
|
|
39
35
|
}
|
|
40
36
|
|
|
@@ -42,12 +38,12 @@ function sensorNote(cfg) {
|
|
|
42
38
|
return note(sensorGaps(cfg));
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
/*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
41
|
+
/* The mode's verdict together with the sensor notes: the note is printed always — silence
|
|
42
|
+
* about a different count reads as the requested one, and a disagreement would be left without
|
|
43
|
+
* a cause — while the code stays the more important one. A violation outranks a sensor note
|
|
44
|
+
* (the same order as in `check` and `doctor`): code 4 claims the difference is explained by the
|
|
45
|
+
* missing sensor, and when the table disagrees nobody checked that — the disagreement may be a
|
|
46
|
+
* real edit that went past the report. */
|
|
51
47
|
function verdict(code, gaps) {
|
|
52
48
|
const sensors = note(gaps);
|
|
53
49
|
return code === EXIT.OK ? sensors : code;
|
|
@@ -56,7 +52,7 @@ function verdict(code, gaps) {
|
|
|
56
52
|
export function check(cfg, want, root) {
|
|
57
53
|
const out = path.join(root, cfg.output);
|
|
58
54
|
if (!fs.existsSync(out)) {
|
|
59
|
-
console.error('✗
|
|
55
|
+
console.error('✗ size table: no file ' + cfg.output + ' — build it: ' + cfg.fixCommand);
|
|
60
56
|
return 1;
|
|
61
57
|
}
|
|
62
58
|
const have = fs.readFileSync(out, 'utf8');
|
|
@@ -66,22 +62,22 @@ export function check(cfg, want, root) {
|
|
|
66
62
|
const b = want.split('\n');
|
|
67
63
|
let i = 0;
|
|
68
64
|
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
69
|
-
console.error('✗
|
|
70
|
-
console.error('
|
|
71
|
-
console.error('
|
|
65
|
+
console.error('✗ size table: ' + cfg.output + ' diverged from the git history (line ' + (i + 1) + '):');
|
|
66
|
+
console.error(' in the file: ' + (a[i] === undefined ? '<no rows>' : a[i].trim().slice(0, 160)));
|
|
67
|
+
console.error(' by the history: ' + (b[i] === undefined ? '<no rows>' : b[i].trim().slice(0, 160)));
|
|
72
68
|
const missing = [...want.matchAll(/id="c-([^"]+)"/g)].map((m) => m[1])
|
|
73
69
|
.filter((id) => have.indexOf('id="c-' + id + '"') === -1);
|
|
74
70
|
if (missing.length > 0) {
|
|
75
|
-
console.error('
|
|
71
|
+
console.error(' rows missing in the file: ' + missing.length + ' (' + missing.slice(0, 5).join(', ')
|
|
76
72
|
+ (missing.length > 5 ? ', …' : '') + ')');
|
|
77
73
|
}
|
|
78
|
-
console.error('
|
|
74
|
+
console.error(' fix: ' + cfg.fixCommand + ' — and commit ' + cfg.output + ' in a commit of its own.');
|
|
79
75
|
return 1;
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
/*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
78
|
+
/* A path named on the command line (`--write <file>`) is this run's `output` setting: the
|
|
79
|
+
* report has to name itself by the path it lies at, or the note inside it would point
|
|
80
|
+
* somewhere else. */
|
|
85
81
|
function withOutput(cfg, root, file) {
|
|
86
82
|
if (typeof file !== 'string') return cfg;
|
|
87
83
|
return Object.assign({}, cfg, { output: path.relative(root, path.resolve(file)) });
|
|
@@ -90,10 +86,10 @@ function withOutput(cfg, root, file) {
|
|
|
90
86
|
export function writeMode(cfg, root, file) {
|
|
91
87
|
const out = rebuild(withOutput(cfg, root, file), root);
|
|
92
88
|
const { rows, files, now, skipped } = out.data;
|
|
93
|
-
console.log('✓ ' + path.relative(root, out.file) + ': ' + rows.length + '
|
|
94
|
-
+ kmb(byteLen(out.html)) + ' (
|
|
89
|
+
console.log('✓ ' + path.relative(root, out.file) + ': ' + rows.length + ' rows × ' + files.length + ' files, '
|
|
90
|
+
+ kmb(byteLen(out.html)) + ' (skipped without a row: ' + skipped.length + ' — '
|
|
95
91
|
+ skipped.join(', ') + ')');
|
|
96
|
-
console.log('
|
|
92
|
+
console.log(' state at HEAD: ' + files.map((f, i) => f.label + ' '
|
|
97
93
|
+ (now[i] === null ? '—' : cfg.metrics.map((m) => now[i][m]).join('/'))).join(', '));
|
|
98
94
|
return sensorNote(cfg);
|
|
99
95
|
}
|
|
@@ -102,45 +98,45 @@ export function checkMode(cfg, root) {
|
|
|
102
98
|
const out = artifact(cfg, root);
|
|
103
99
|
const code = check(cfg, out.html, root);
|
|
104
100
|
if (code === 0) {
|
|
105
|
-
console.log('✓
|
|
106
|
-
+ '
|
|
101
|
+
console.log('✓ report: ' + out.data.rows.length + ' commits × ' + out.data.files.length + ' files '
|
|
102
|
+
+ 'matches the history (' + cfg.output + ', ' + kmb(byteLen(out.html)) + ')');
|
|
107
103
|
}
|
|
108
104
|
return verdict(code, sensorGaps(cfg));
|
|
109
105
|
}
|
|
110
106
|
|
|
111
|
-
/*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
107
|
+
/* A command's answer: `--json` is the machine form of the same answer, not a second one.
|
|
108
|
+
* Shared by three commands so that "who prints and in which shape" cannot diverge between
|
|
109
|
+
* them — that can only diverge here, and the bytes of the answer are what an agent consumes.
|
|
110
|
+
* The text comes as a function: the machine form does not need it at all. */
|
|
115
111
|
function answer(rep, asJson, text) {
|
|
116
112
|
if (asJson) process.stdout.write(JSON.stringify(rep, null, 2) + '\n');
|
|
117
113
|
else console.log(text(rep));
|
|
118
114
|
return rep;
|
|
119
115
|
}
|
|
120
116
|
|
|
121
|
-
/*
|
|
122
|
-
* `checkMode`
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* заменяется. */
|
|
117
|
+
/* Coverage (`size check`): settings, history, paths, sensors. Not to be confused with
|
|
118
|
+
* `checkMode` above, which asks whether the file matches what was computed; this one asks
|
|
119
|
+
* whether **everything** was computed — no path of the history went past the columns.
|
|
120
|
+
* Different questions, hence different commands: keeping the report in git is optional,
|
|
121
|
+
* losing completeness is not — and this command is what replaces that check. */
|
|
127
122
|
export function coverageMode(cfg, root, configFile, asJson) {
|
|
128
123
|
const rep = answer(coverage(cfg, root, configFile), asJson, coverageText);
|
|
129
124
|
return verdict(rep.ok ? EXIT.OK : EXIT.VIOLATION, rep.sensors);
|
|
130
125
|
}
|
|
131
126
|
|
|
132
|
-
/*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
127
|
+
/* Diagnostics in one answer (`size doctor`): environment, dependencies, settings and
|
|
128
|
+
* coverage, assembled from the same pieces as the other modes. The exit code is not
|
|
129
|
+
* "something is wrong" but the first by importance (settings → history → coverage →
|
|
130
|
+
* sensor): an agent branches on it, a human reads the text. */
|
|
136
131
|
export function doctorMode(root, configFile, asJson) {
|
|
137
132
|
return answer(doctor(root, configFile), asJson, doctorText).exit;
|
|
138
133
|
}
|
|
139
134
|
|
|
140
|
-
/*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
135
|
+
/* The hook: installing, removing, and the call the hook itself makes. Installing and
|
|
136
|
+
* removing happen by explicit command only; `hook-run` is called by the hook and always
|
|
137
|
+
* answers 0 — the commit is already made and there is nothing to fail it for (design and
|
|
138
|
+
* reasons: `src/hook.js`). What it did goes to stderr: it is part of git's output, not tool
|
|
139
|
+
* data. */
|
|
144
140
|
export function hookMode(verb, root, configFile) {
|
|
145
141
|
if (verb === 'hook-run') {
|
|
146
142
|
const rep = hookRun(root, configFile);
|
|
@@ -152,17 +148,18 @@ export function hookMode(verb, root, configFile) {
|
|
|
152
148
|
return rep.code;
|
|
153
149
|
}
|
|
154
150
|
|
|
155
|
-
/*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
151
|
+
/* Explaining a skipped row (`size explain <commit>`): any resolvable commit has an answer,
|
|
152
|
+
* so the exit code is 0 both when the row is there and when it is not; 2 belongs to a commit
|
|
153
|
+
* that cannot be resolved — an unknown name, an ambiguous prefix, or one outside the
|
|
154
|
+
* history. */
|
|
158
155
|
export function explainMode(cfg, root, target, asJson) {
|
|
159
156
|
answer(explainCommit(cfg, root, target), asJson, explainText);
|
|
160
157
|
return EXIT.OK;
|
|
161
158
|
}
|
|
162
159
|
|
|
163
|
-
/*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
160
|
+
/* Contract data on stdout — for the page and for an agent: the same truth as in the
|
|
161
|
+
* artifact, without markup and without derived numbers. The older `--json` form stays
|
|
162
|
+
* untouched: the parity fixture freezes it (`fixtures/parity`). */
|
|
166
163
|
export function dataMode(cfg, root) {
|
|
167
164
|
process.stdout.write(JSON.stringify(reportData(cfg, root), null, 2) + '\n');
|
|
168
165
|
return sensorNote(cfg);
|
package/src/optional.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
|
|
3
|
-
/*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* —
|
|
7
|
-
*
|
|
3
|
+
/* Optional dependencies: the minifier and the tokenizer. Missing ones are not a refusal but a
|
|
4
|
+
* different count (a simplification instead of compression, an estimate instead of a counted
|
|
5
|
+
* one), so their loading is shared and shaped the same way: lazy, synchronous
|
|
6
|
+
* (`createRequire` — measuring is one synchronous pass, and `import()` would make the whole
|
|
7
|
+
* chain asynchronous for the sake of a single sensor) and without an exception escaping —
|
|
8
|
+
* unavailability comes back as an answer.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The seam of their absence is the `SIZE_REPORT_NO_OPTIONAL` environment variable: the same
|
|
11
|
+
* path serves an install without optional dependencies and a platform the package is not built
|
|
12
|
+
* for. It is also how the tests check that the tool works without them. */
|
|
12
13
|
|
|
13
14
|
export const NO_OPTIONAL = 'SIZE_REPORT_NO_OPTIONAL';
|
|
14
15
|
|
|
15
|
-
/*
|
|
16
|
-
*
|
|
16
|
+
/* The version is read from the package itself: the number depends on the dictionary and on the
|
|
17
|
+
* algorithm, so it belongs in the method the value came from rather than staying inside
|
|
18
|
+
* `node_modules`. */
|
|
17
19
|
export function loadOptional(spec) {
|
|
18
20
|
if (process.env[NO_OPTIONAL]) {
|
|
19
|
-
return { tool: null, version: null, why: '
|
|
21
|
+
return { tool: null, version: null, why: 'the optional dependencies are switched off (' + NO_OPTIONAL + ')' };
|
|
20
22
|
}
|
|
21
23
|
const require = createRequire(import.meta.url);
|
|
22
24
|
try {
|