@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/project.js
CHANGED
|
@@ -4,46 +4,44 @@ import { execFileSync } from 'child_process';
|
|
|
4
4
|
import { MAX_BUF, git, gitArgv, gitEnv, gitTry, readHistory } from './git.js';
|
|
5
5
|
import { cliCommand, invocation } from './refusal.js';
|
|
6
6
|
|
|
7
|
-
/*
|
|
8
|
-
*
|
|
7
|
+
/* Settings derived from the project itself: what to measure, where the journal is, where to write,
|
|
8
|
+
* and what cannot be a column.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* Why separate from `src/config.js`: that one reads **ready** settings and checks them, while this
|
|
11
|
+
* one looks at the project for the first time and guesses about almost everything — the same work
|
|
12
|
+
* the draft used to do (`--init`). Hence the two roles of one output: with no settings file it *is*
|
|
13
|
+
* the settings (the project works at once, having set up nothing), and under `--init` the very same
|
|
14
|
+
* output is written to a file, which is edited afterwards. There is no second way to guess a project
|
|
15
|
+
* in this package.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* Two rules, from which everything else follows.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* **A column is a file.** The report names a column by one path — the first of its list — while the
|
|
20
|
+
* whole list is the column's renames, and a revision resolves whichever of them is present there. So
|
|
21
|
+
* "a whole directory" cannot be a column, and the profile names files rather than groups of paths. A
|
|
22
|
+
* column is **every** tracked file that can be measured: a sample of the project passed the volume of
|
|
23
|
+
* the sample off as the volume of the project.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* зелёный, а про каждый файл вне отчёта сказано, почему его там нет.
|
|
25
|
+
* **The profile has to pass the very check its first run will apply**: a path that became neither a
|
|
26
|
+
* column nor a declared exception is code 1 on that first run. So `skip` names both what cannot be a
|
|
27
|
+
* column (the report itself, dependency locks, built output, an unknown format, a file too large) and
|
|
28
|
+
* what git does not track: that way the first `check` is green, and every file outside the report has
|
|
29
|
+
* a stated reason for standing there.
|
|
31
30
|
*/
|
|
32
31
|
|
|
33
|
-
/*
|
|
34
|
-
*
|
|
35
|
-
* называется исключением. */
|
|
32
|
+
/* What may become a column at all: the text forms the engine can work with. A file without a known
|
|
33
|
+
* extension (`LICENSE`, `.gitignore`) never becomes one and is named as an exception. */
|
|
36
34
|
const KNOWN_EXTS = ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.html', '.css', '.scss',
|
|
37
35
|
'.json', '.yaml', '.yml', '.toml', '.md', '.txt', '.py', '.rb', '.go', '.rs', '.sh'];
|
|
38
36
|
|
|
39
37
|
const JOURNALS = ['WORKLOG.md', 'CHANGELOG.md', 'CHANGES.md', 'HISTORY.md'];
|
|
40
38
|
|
|
41
|
-
/*
|
|
42
|
-
*
|
|
39
|
+
/* The size threshold is a guard: a very large file is usually built or generated, and in the report
|
|
40
|
+
* it would outweigh the whole project. */
|
|
43
41
|
const MAX_BYTES = 512 * 1024;
|
|
44
42
|
|
|
45
|
-
/*
|
|
46
|
-
*
|
|
43
|
+
/* The index gives paths and sizes: `ls-files -s` yields the objects, and their sizes are asked for in
|
|
44
|
+
* one batch (`cat-file --batch-check`) rather than by reading the content. */
|
|
47
45
|
function indexFiles(root) {
|
|
48
46
|
const listed = git(root, ['ls-files', '-s']).split('\n').filter((l) => l !== '');
|
|
49
47
|
const sizes = new Map();
|
|
@@ -60,25 +58,25 @@ function indexFiles(root) {
|
|
|
60
58
|
return listed.map((line) => ({ p: line.split('\t')[1], size: sizes.get(line.split(/\s+/)[1]) || 0 }));
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
/*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
61
|
+
/* The history is the union of the paths of every commit, read the same way coverage reads it
|
|
62
|
+
* (`readHistory`): otherwise the profile and `check` would count different histories. A file living
|
|
63
|
+
* only in the history is simply empty at HEAD — a fact rather than a loss, and it has to be a column
|
|
64
|
+
* (or an exception). */
|
|
67
65
|
function historyPaths(root) {
|
|
68
66
|
const seen = new Set();
|
|
69
67
|
try {
|
|
70
68
|
readHistory(root).forEach((c) => c.files.forEach((f) => seen.add(f)));
|
|
71
69
|
} catch (e) {
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
70
|
+
// A repository without commits has no history at all: `git log` refuses there, and that is a state
|
|
71
|
+
// of the project rather than a defect of the tool — `--init` has to work in one too (a first run
|
|
72
|
+
// may come before the first commit).
|
|
75
73
|
if (/does not have any commits/.test(String(e.stderr))) return seen;
|
|
76
74
|
throw e;
|
|
77
75
|
}
|
|
78
76
|
return seen;
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
//
|
|
79
|
+
// The tree and the history in one list: everything the history touched may become a column.
|
|
82
80
|
function allPaths(root) {
|
|
83
81
|
const files = indexFiles(root);
|
|
84
82
|
const known = new Set(files.map((f) => f.p));
|
|
@@ -88,23 +86,22 @@ function allPaths(root) {
|
|
|
88
86
|
return files;
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
//
|
|
89
|
+
// Whether the project has this path at all; nothing else is asked of this helper.
|
|
92
90
|
function exists(root, p) {
|
|
93
91
|
return fs.existsSync(path.join(root, p));
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
/*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* адреса; создавать его — работа того, кто пишет файл (`writeFileEnsured`). */
|
|
94
|
+
/* Where the report lands — one answer for the package: `docs/size-report.html`, with the directory
|
|
95
|
+
* created by the writer (`writeFileEnsured`). A fork on the existence of `docs/` was measured and
|
|
96
|
+
* dropped: a fresh project has no such directory, so the report ended up in the root — neither where
|
|
97
|
+
* it is looked for nor where a person installing the package for the sake of the report would put it.
|
|
98
|
+
* The directory is part of the address here, not a sign of a project. */
|
|
102
99
|
function outputOf() {
|
|
103
100
|
return 'docs/size-report.html';
|
|
104
101
|
}
|
|
105
102
|
|
|
106
|
-
/*
|
|
107
|
-
*
|
|
103
|
+
/* The package manager comes from a lock file rather than a guess: the command has to exist in
|
|
104
|
+
* someone else's project. One answer for two places (the fix command and the `--init` prompt). */
|
|
108
105
|
export function packageManager(root) {
|
|
109
106
|
if (exists(root, 'pnpm-lock.yaml')) return 'pnpm';
|
|
110
107
|
return exists(root, 'yarn.lock') ? 'yarn' : 'npm';
|
|
@@ -118,12 +115,12 @@ function readJson(file) {
|
|
|
118
115
|
}
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
/*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
118
|
+
/* The artifact's note and the refusals quote this command, so it has to work right here and now: the
|
|
119
|
+
* project's own script only if it is declared (or `npm run sizes` answers "no such script", and
|
|
120
|
+
* advice that does not work is the worst kind), otherwise the installed package inside the project.
|
|
121
|
+
* The package name is deliberately absent from the command: with no package installed it would send the
|
|
122
|
+
* reader to the registry, which serves a revision the project never pinned, while a path inside the
|
|
123
|
+
* project refuses on the spot. */
|
|
127
124
|
function fixCommandOf(root) {
|
|
128
125
|
const pkg = readJson(path.join(root, 'package.json')) || {};
|
|
129
126
|
const script = pkg.scripts === undefined ? '' : pkg.scripts.sizes;
|
|
@@ -131,10 +128,10 @@ function fixCommandOf(root) {
|
|
|
131
128
|
return packageManager(root) + ' run sizes';
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
/*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
131
|
+
/* The commit link comes from the origin address: the two hosts are covered by one rule that builds
|
|
132
|
+
* the address from the host and the repository path (GitLab nests subgroups, hence the whole path),
|
|
133
|
+
* while any third host would be a guess leading somewhere else. No remote, or the wrong one — no
|
|
134
|
+
* links: an empty template is simply not spelled out. */
|
|
138
135
|
const REMOTE_RE = /^\S+?(?:@|\/\/)(?:[^@/]*@)?(github\.com|gitlab\.com)[/:]+((?:[^/\s]+\/)*[^/\s]+?)(?:\.git)?$/;
|
|
139
136
|
|
|
140
137
|
function commitUrlOf(root) {
|
|
@@ -148,9 +145,9 @@ function journalOf(root) {
|
|
|
148
145
|
return JOURNALS.find((p) => exists(root, p)) || '';
|
|
149
146
|
}
|
|
150
147
|
|
|
151
|
-
/*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
148
|
+
/* What never becomes a column: the report itself (its size depends on the number of rows, that is,
|
|
149
|
+
* on itself), dependency locks (their size is about someone else's packages), maps and built output
|
|
150
|
+
* (made by a build rather than by a person), and anything the engine cannot read. */
|
|
154
151
|
function generated(p, output) {
|
|
155
152
|
const ext = path.extname(p).toLowerCase();
|
|
156
153
|
return p === output || KNOWN_EXTS.indexOf(ext) < 0
|
|
@@ -158,8 +155,8 @@ function generated(p, output) {
|
|
|
158
155
|
|| /\.min\./.test(p) || /\.map$/.test(p);
|
|
159
156
|
}
|
|
160
157
|
|
|
161
|
-
/*
|
|
162
|
-
*
|
|
158
|
+
/* A column's label is the file name; a clash of names in different directories is split by the path
|
|
159
|
+
* and, if that is taken too, by a number. The settings check lets no repeated label through. */
|
|
163
160
|
function labelFor(used, p) {
|
|
164
161
|
const candidates = [path.basename(p), p];
|
|
165
162
|
const free = candidates.find((name) => used.indexOf(name) < 0);
|
|
@@ -169,17 +166,16 @@ function labelFor(used, p) {
|
|
|
169
166
|
return p + ' (' + n + ')';
|
|
170
167
|
}
|
|
171
168
|
|
|
172
|
-
/*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* в `skip` и называются там.
|
|
169
|
+
/* How columns are chosen: everything measurable becomes one — that is, every file of the project
|
|
170
|
+
* rather than a sample of it. The sample (formerly the largest file per extension, at most twelve)
|
|
171
|
+
* lied twice: the report named the volume of files it did not contain, and a reader took that for the
|
|
172
|
+
* volume of the project. The only things left out are those that **cannot** be a column: the report
|
|
173
|
+
* itself, locks, built output, an unknown format, and a file too large (`generated` and `MAX_BYTES`) —
|
|
174
|
+
* they go to `skip` and are named there.
|
|
179
175
|
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
176
|
+
* The order of columns is a ring over the extensions, largest first: what the project has most of comes
|
|
177
|
+
* first, and within an extension the large comes before the small. That is the reading order of the
|
|
178
|
+
* report rather than weight — numbers do not depend on it. */
|
|
183
179
|
function columnsOf(files, journal) {
|
|
184
180
|
const byExt = new Map();
|
|
185
181
|
files.forEach((f) => {
|
|
@@ -211,23 +207,23 @@ export function projectConfig(root) {
|
|
|
211
207
|
const output = outputOf(root);
|
|
212
208
|
const journal = journalOf(root);
|
|
213
209
|
const files = allPaths(root);
|
|
214
|
-
/*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
210
|
+
/* Only what git tracks becomes a column: a file absent at HEAD has nothing to measure (it would be
|
|
211
|
+
* empty in every row of the report). A path living only in the history is therefore an exception
|
|
212
|
+
* rather than a column, while the list of paths stays complete either way. */
|
|
217
213
|
const tracked = new Set(indexFiles(root).map((f) => f.p));
|
|
218
214
|
const readable = files.filter((f) => tracked.has(f.p) && !generated(f.p, output) && f.size <= MAX_BYTES);
|
|
219
215
|
const columns = columnsOf(readable, journal);
|
|
220
216
|
const taken = new Set(columns.reduce((all, c) => all.concat(c.paths), []));
|
|
221
|
-
//
|
|
222
|
-
//
|
|
217
|
+
// The language, the title, the row order and the hook switch are derived from nothing: they come
|
|
218
|
+
// from the defaults (`DEFAULT_CONFIG`) instead of being passed off as derived from the project.
|
|
223
219
|
return {
|
|
224
220
|
output: output,
|
|
225
221
|
fixCommand: fixCommandOf(root),
|
|
226
222
|
metrics: ['raw', 'min', 'tok'],
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
223
|
+
// Real compression and a real dictionary rather than their cheaper substitutes: a new project must
|
|
224
|
+
// not start with numbers counted another way. Without the optional dependency the metric falls back
|
|
225
|
+
// to another count and the run returns code 4 — said by the metric label rather than left to a
|
|
226
|
+
// default.
|
|
231
227
|
minify: { engine: 'esbuild' },
|
|
232
228
|
tokens: { family: 'openai', encoding: 'o200k_base' },
|
|
233
229
|
columns: columns,
|
|
@@ -238,30 +234,30 @@ export function projectConfig(root) {
|
|
|
238
234
|
anchor: 'heading'
|
|
239
235
|
},
|
|
240
236
|
links: { commitUrl: commitUrlOf(root) },
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
237
|
+
// Exceptions are everything that did not become a column: what cannot be one, and what git does
|
|
238
|
+
// not track (a path from the history alone). That way the first run is complete, and what `skip`
|
|
239
|
+
// names is the answer to "why is it not in the report".
|
|
244
240
|
skip: [output].concat(files.filter((f) => !taken.has(f.p)).map((f) => f.p))
|
|
245
241
|
.filter((p, i, all) => all.indexOf(p) === i)
|
|
246
242
|
};
|
|
247
243
|
}
|
|
248
244
|
|
|
249
|
-
/*
|
|
250
|
-
*
|
|
245
|
+
/* Whether the report's own path lies inside the project (non-empty, not absolute, not climbing out):
|
|
246
|
+
* only such a path can be named as a leaf of the tree. */
|
|
251
247
|
const ownPath = (output) => output !== '' && !path.isAbsolute(output) && output.indexOf('..') !== 0;
|
|
252
248
|
|
|
253
|
-
/*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
249
|
+
/* The project catalogue for the page: every path git sees, with a reason for those that did not become
|
|
250
|
+
* columns (`null` means it did — the page reads this mark to decide between a checkbox and a label).
|
|
251
|
+
* The page's tree is the project's tree, so the paths come from the index rather than from the columns,
|
|
252
|
+
* and the reasons follow **the same** rules the profile uses to pick columns (`generated` and the size
|
|
253
|
+
* limit) — otherwise the hint would say one thing while the choice of columns did another. A column
|
|
254
|
+
* whose file is gone from HEAD does not enter the catalogue: the index does not hold it, and the page
|
|
255
|
+
* keeps its place in the tree.
|
|
260
256
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
257
|
+
* The report itself is always in the catalogue, both before it is first built and while it is not
|
|
258
|
+
* tracked: whether it is tracked is a property of the moment rather than of the project. The report
|
|
259
|
+
* must not depend on it — otherwise the first rebuild in a fresh clone yields different bytes (the
|
|
260
|
+
* report appeared in the catalogue) and the hook commits it a second time out of nowhere. */
|
|
265
261
|
export function projectTree(root, output, measured) {
|
|
266
262
|
const files = indexFiles(root);
|
|
267
263
|
if (ownPath(output) && files.every((f) => f.p !== output)) files.push({ p: output, size: 0 });
|
|
@@ -272,9 +268,9 @@ export function projectTree(root, output, measured) {
|
|
|
272
268
|
}));
|
|
273
269
|
}
|
|
274
270
|
|
|
275
|
-
/*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
271
|
+
/* The draft's name (`sniffColumns`) stays public: the callers of the column output rely on it
|
|
272
|
+
* (`test/api.test.js` holds the list of names), and the shape of the answer is the same — columns,
|
|
273
|
+
* the extensions the project knows, and how many paths there are in total. */
|
|
278
274
|
export function sniffColumns(root) {
|
|
279
275
|
const files = allPaths(root);
|
|
280
276
|
const exts = [...new Set(files.map((f) => path.extname(f.p).toLowerCase()))]
|
|
@@ -282,20 +278,20 @@ export function sniffColumns(root) {
|
|
|
282
278
|
return { columns: projectConfig(root).columns, exts: exts, total: files.length };
|
|
283
279
|
}
|
|
284
280
|
|
|
285
|
-
/*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
281
|
+
/* What to tell a person when there is no settings file and the settings had to be derived: one line
|
|
282
|
+
* about what came out, one about how to pin it. The text comes from the same list as the `doctor`
|
|
283
|
+
* finding: two answers about one thing must not drift apart in words. */
|
|
288
284
|
export function derivedSummary(cfg) {
|
|
289
285
|
const labels = cfg.columns.map((c) => c.label);
|
|
290
|
-
return '
|
|
286
|
+
return 'settings derived from the project (no file): columns ' + cfg.columns.length
|
|
291
287
|
+ ' (' + labels.slice(0, 5).join(', ') + (labels.length > 5 ? ', …' : '') + '),'
|
|
292
|
-
+ '
|
|
288
|
+
+ ' paths skipped ' + cfg.skip.length;
|
|
293
289
|
}
|
|
294
290
|
|
|
295
291
|
export function derivedLines(cfg) {
|
|
296
292
|
return [
|
|
297
293
|
'! ' + derivedSummary(cfg),
|
|
298
|
-
'
|
|
299
|
-
+ '
|
|
294
|
+
' pin them with a file of their own (then edit it as you like; otherwise the set of columns'
|
|
295
|
+
+ ' changes from run to run): ' + cliCommand('--init')
|
|
300
296
|
];
|
|
301
297
|
}
|