@vernikr/size-report 2.4.0 → 2.5.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 +929 -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 +31 -37
- 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 -77
- package/src/hook.js +130 -149
- package/src/init.js +36 -37
- package/src/journal.js +17 -15
- package/src/locales.js +25 -16
- package/src/metrics.js +56 -55
- package/src/minify.js +28 -27
- package/src/modes.js +57 -60
- package/src/optional.js +13 -11
- package/src/page/app.css +75 -91
- package/src/page/app.js +30 -35
- package/src/page/build.js +40 -39
- package/src/page/dom.js +8 -9
- package/src/page/panel.js +48 -51
- package/src/page/state.js +72 -87
- package/src/page/table.js +21 -24
- 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 +27 -28
- package/src/strip/js.js +27 -27
- package/src/strip.js +18 -21
- package/src/table.css +13 -14
- package/src/tokens.js +28 -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/init.js
CHANGED
|
@@ -5,64 +5,63 @@ import { advicePath, cliCommand, refuseCause } from './refusal.js';
|
|
|
5
5
|
import { writeFileEnsured } from './artifact.js';
|
|
6
6
|
import { packageManager } from './project.js';
|
|
7
7
|
|
|
8
|
-
/*
|
|
9
|
-
* (`src/project.js`)
|
|
8
|
+
/* Pinning the settings to a file (`--init`): what the project derived about itself
|
|
9
|
+
* (`src/project.js`) is written where the next run will meet it.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* человека в новый тупик (BLOCKERS §N2, REFACTOR R-0.4).
|
|
11
|
+
* A module of its own, separate from deriving the profile: that one looks at the project for the first
|
|
12
|
+
* time and guesses about almost everything, while this one does a single thing — puts the result into
|
|
13
|
+
* a file and says what it wrote. It has one strict requirement of itself: **what was pinned has to
|
|
14
|
+
* pass the very check the first run will apply**, or the advice leads a person into a new dead end.
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
|
-
/*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
/* What to say after writing: what was written, what will replace the approximations, and what to do
|
|
18
|
+
* next. The lines are assembled into a list rather than printed as they come, so that "what was said"
|
|
19
|
+
* can be read as a whole. */
|
|
21
20
|
function draftLines(root, target, cfg) {
|
|
22
21
|
const hasPkg = fs.existsSync(path.join(root, 'package.json'));
|
|
23
22
|
const manager = packageManager(root);
|
|
24
23
|
return [
|
|
25
|
-
'✓
|
|
26
|
-
'
|
|
24
|
+
'✓ settings derived from the project and pinned: ' + path.relative(root, target),
|
|
25
|
+
' columns: ' + cfg.columns.length + ' (' + cfg.columns.map((c) => c.label).slice(0, 6).join(', ')
|
|
27
26
|
+ (cfg.columns.length > 6 ? ', …' : '') + ')',
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
27
|
+
' paths skipped: ' + cfg.skip.length + ' (the report itself, dependency locks, maps, build output)',
|
|
28
|
+
' metric min: real compression (esbuild); without it — an honest simplification and code 4',
|
|
29
|
+
' metric tok: the o200k_base dictionary (gpt-tokenizer); without it — an estimate by length and code 4',
|
|
30
|
+
' journal: ' + (cfg.journal === null ? 'not found — row links will carry no sections' : cfg.journal.path),
|
|
31
|
+
' next: edit the columns and the metrics — which files matter is known by the project alone',
|
|
33
32
|
' ' + (hasPkg
|
|
34
|
-
? '
|
|
35
|
-
+ manager + ' run sizes (
|
|
36
|
-
: '
|
|
37
|
-
' ' + (hasPkg ? '
|
|
38
|
-
+ ';
|
|
33
|
+
? 'add "sizes": "size --write" to package.json — then the report will be built by '
|
|
34
|
+
+ manager + ' run sizes (the check — without --write)'
|
|
35
|
+
: 'run: ' + cfg.fixCommand + ' (the check — without --write)'),
|
|
36
|
+
' ' + (hasPkg ? 'add ' + manager + ' run test:sizes to CI' : 'add the check to CI')
|
|
37
|
+
+ '; the check is the package\'s command, it brings no files of its own into the project'
|
|
39
38
|
];
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
/*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
/* An empty profile is a note rather than a refusal: the work was done, and nobody will pick the columns
|
|
42
|
+
* for the person. Hence "!", not the cross: a mark and an exit code must not say different things (the
|
|
43
|
+
* refusal catalogue counts a cross as a refusal and a note as not one). */
|
|
45
44
|
function noteNoColumns(root, target, cfg) {
|
|
46
45
|
if (cfg.columns.length > 0) return;
|
|
47
|
-
console.error('!
|
|
48
|
-
+ ' (
|
|
49
|
-
+ '\n
|
|
50
|
-
+ ' —
|
|
46
|
+
console.error('! no paths in the project could be taken as columns'
|
|
47
|
+
+ ' (the history is empty or holds no familiar extensions): the draft is written without columns'
|
|
48
|
+
+ '\n write them by hand into ' + path.relative(root, target)
|
|
49
|
+
+ ' — without columns the settings check will say "no columns are given (columns)"');
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
export function initMode(root, file, force) {
|
|
54
53
|
const target = file ? path.resolve(root, file) : path.join(root, CONFIG_NAME);
|
|
55
54
|
if (fs.existsSync(target) && !force) {
|
|
56
|
-
//
|
|
57
|
-
//
|
|
55
|
+
// The advice names the very file in question: `--init --force` without a file would overwrite the
|
|
56
|
+
// default name with a draft rather than the file the person named.
|
|
58
57
|
const name = file === undefined || file === null ? CONFIG_NAME : advicePath(file);
|
|
59
|
-
refuseCause('
|
|
60
|
-
+ '\n
|
|
58
|
+
refuseCause('config already exists', 'config already exists: ' + target
|
|
59
|
+
+ '\n fix: edit it or overwrite it with a draft: ' + cliCommand('--init ' + name + ' --force'));
|
|
61
60
|
}
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
61
|
+
// What is pinned is the very thing the project runs on without a file (the project's derivation on
|
|
62
|
+
// top of the defaults), and it has to pass the same check the run will apply: the path in a refusal
|
|
63
|
+
// text is the file it landed in. "Derived" and that path are not written to the file: they are
|
|
64
|
+
// properties of where the settings came from rather than of the settings.
|
|
66
65
|
const cfg = derivedProfile(root);
|
|
67
66
|
if (cfg.columns.length > 0) validateConfig(Object.assign({}, cfg, { path: target }));
|
|
68
67
|
const written = Object.assign({}, cfg);
|
package/src/journal.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/*
|
|
2
|
-
*
|
|
1
|
+
/* The journal and links: which section a commit belongs to and where its description leads.
|
|
2
|
+
* It works on text rather than on git: sections are searched for in an already read file. */
|
|
3
3
|
|
|
4
|
-
/*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
/* A journal (WORKLOG/CHANGELOG/…): the section a commit belongs to. The heading format comes
|
|
5
|
+
* from a regular expression in the settings; the named groups `id` (the short number used by
|
|
6
|
+
* `§N`) and `title` are optional — without them the number is the first word. */
|
|
7
7
|
export function parseSections(text, pattern) {
|
|
8
8
|
const re = new RegExp(pattern);
|
|
9
9
|
const list = [];
|
|
@@ -23,9 +23,11 @@ export function parseSections(text, pattern) {
|
|
|
23
23
|
return list;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
/* The section a commit belongs to: a newly opened one first, otherwise the last of the edited
|
|
27
|
+
* ones in document order. Taking the last assumes a journal written in ascending order, as this
|
|
28
|
+
* tool's own defaults expect; the "newly added" case holds for any order, while in a journal
|
|
29
|
+
* written newest-first (CHANGELOG.md is one) the fallback would name the oldest edited section
|
|
30
|
+
* rather than the newest. */
|
|
29
31
|
export function touchedSection(prevText, nowText, pattern) {
|
|
30
32
|
const prev = new Map(parseSections(prevText, pattern).map((s) => [s.head, s.body]));
|
|
31
33
|
const now = parseSections(nowText, pattern);
|
|
@@ -42,10 +44,10 @@ export function touchedSection(prevText, nowText, pattern) {
|
|
|
42
44
|
return null;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
/*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
/* The anchor as GitHub makes it (github-slugger): punctuation is dropped entirely and every
|
|
48
|
+
* space becomes a hyphen. Hence "13 — eff" → "13--eff" (two hyphens: the dash is gone while both
|
|
49
|
+
* spaces stay). Hyphens, underscores and letters (Cyrillic included) survive; other whitespace
|
|
50
|
+
* does not. */
|
|
49
51
|
export function anchor(head) {
|
|
50
52
|
return head.toLowerCase().replace(/[^\p{L}\p{N} _-]/gu, '').replace(/ /g, '-');
|
|
51
53
|
}
|
|
@@ -56,9 +58,9 @@ export function sectionLink(section, cfg) {
|
|
|
56
58
|
return cfg.journal.url + '#' + anchor(what);
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
/*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
61
|
+
/* The link of a row: to a journal section when there is one, otherwise to the commit itself (the
|
|
62
|
+
* template comes from the settings). One place for the artifact and for the data of the page: a
|
|
63
|
+
* section address follows GitHub's rule, and a second copy of it would drift from the first. */
|
|
62
64
|
export function rowHref(section, sha, cfg) {
|
|
63
65
|
if (section) return sectionLink(section, cfg);
|
|
64
66
|
if (!cfg.links.commitUrl) return null;
|
package/src/locales.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
/*
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* таблицы, клетки, дельты) от локали не зависит, поэтому перевод — это словарь,
|
|
6
|
-
* а не отдельный рендер. */
|
|
1
|
+
/* Texts of the artifact and of the report page: headings, labels and service words, in the
|
|
2
|
+
* language of the project. A dictionary, not a renderer: the mechanics of the table (cells,
|
|
3
|
+
* deltas, headings) do not depend on the language, so translating a report means adding
|
|
4
|
+
* words here rather than writing a second renderer. */
|
|
7
5
|
export const LOCALES = {
|
|
8
6
|
ru: {
|
|
9
7
|
html: 'ru',
|
|
@@ -12,29 +10,36 @@ export const LOCALES = {
|
|
|
12
10
|
total: 'Общий объём',
|
|
13
11
|
now: 'сейчас',
|
|
14
12
|
categories: { code: 'Код', docs: 'Документация', chore: 'Служебные', assets: 'Ресурсы' },
|
|
15
|
-
/*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
/* The page texts live inside the report file itself (a dictionary of their own, next to
|
|
14
|
+
* the data), so changing a word costs a rebuild of the report — otherwise the file
|
|
15
|
+
* drifts from the history. */
|
|
18
16
|
page: {
|
|
19
17
|
metrics: 'Метрики',
|
|
20
18
|
files: 'Файлы',
|
|
21
19
|
dir: 'все файлы папки {name} ({n})',
|
|
22
20
|
dirNone: 'ни один файл папки {name} в отчёт не попал (в папке {n})',
|
|
23
|
-
/*
|
|
24
|
-
*
|
|
21
|
+
/* The fold mark is a click target of its own: the checkbox answers for the numbers,
|
|
22
|
+
* the mark for how much of the tree is visible. */
|
|
25
23
|
foldClose: 'свернуть папку {name}',
|
|
26
24
|
foldOpen: 'развернуть папку {name}',
|
|
27
|
-
/*
|
|
28
|
-
*
|
|
25
|
+
/* The reason belongs to a leaf: in the tree it is a label rather than a checkbox,
|
|
26
|
+
* and without these words a reader would think the file was lost. */
|
|
29
27
|
notMeasuredRule: 'не измеряется: такой файл колонкой быть не может'
|
|
30
28
|
+ ' (собранный, замок зависимостей, сам отчёт или слишком крупный)',
|
|
31
29
|
notMeasuredChoice: 'не измеряется: в набор колонок отчёта не попал'
|
|
32
30
|
+ ' — набор задают настройки (columns)',
|
|
31
|
+
/* The tooltip of a measured file: where the file stands and how its category was decided.
|
|
32
|
+
* A file absent on HEAD is named by the path it came with, and a category comes either from
|
|
33
|
+
* the settings or from the extension — both are answers to the reader's "why so". */
|
|
34
|
+
notOnHead: ' (нет на HEAD)',
|
|
35
|
+
category: ' · категория: ',
|
|
36
|
+
categoryFromConfig: 'из настроек',
|
|
37
|
+
categoryByExtension: 'по расширению',
|
|
33
38
|
all: 'все',
|
|
34
39
|
sub: '{tool} {version} · {artifact}',
|
|
35
|
-
/*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
40
|
+
/* Words of precision: a metric label speaks about the worst in its column, a cell
|
|
41
|
+
* about its own number. One word serves both the label and the cell hint, so the two
|
|
42
|
+
* cannot drift or sound differently. */
|
|
38
43
|
exact: 'точное число',
|
|
39
44
|
approximate: 'приближение',
|
|
40
45
|
approximateCell: 'приближённое число: ',
|
|
@@ -88,6 +93,10 @@ export const LOCALES = {
|
|
|
88
93
|
+ ' (built, a dependency lock, the report itself, or too large)',
|
|
89
94
|
notMeasuredChoice: 'not measured: it is not in the report’s set of columns'
|
|
90
95
|
+ ' — that set comes from the settings (columns)',
|
|
96
|
+
notOnHead: ' (not on HEAD)',
|
|
97
|
+
category: ' · category: ',
|
|
98
|
+
categoryFromConfig: 'from the settings',
|
|
99
|
+
categoryByExtension: 'by extension',
|
|
91
100
|
all: 'all',
|
|
92
101
|
sub: '{tool} {version} · {artifact}',
|
|
93
102
|
exact: 'an exact number',
|
package/src/metrics.js
CHANGED
|
@@ -4,18 +4,18 @@ import { EXACT_STRATEGIES, assertCompilable, byteLen, minifyForm, strategyFor }
|
|
|
4
4
|
import { MINIFY_LOADERS, minifier, minifyWithEsbuild } from './minify.js';
|
|
5
5
|
import { CHARS_PER_TOKEN, isBinary, tokenCount, tokenizer } from './tokens.js';
|
|
6
6
|
|
|
7
|
-
/*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
/* The registry of metrics: what is measured, whether a metric needs the text, and how honest
|
|
8
|
+
* its number is. Separate from the ways of stripping ballast: a metric is a promise about a
|
|
9
|
+
* number rather than a way to obtain one.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* The description a reader sees comes from `metricView` rather than from the registry fields:
|
|
12
|
+
* for one and the same metric it depends on the settings (`min` is either real compression or
|
|
13
|
+
* a simplification, `tok` either an exact dictionary or an estimate), and two answers to one
|
|
14
|
+
* question have nowhere to drift apart. */
|
|
15
15
|
|
|
16
|
-
/*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
/* The ways to obtain the `min` metric: stripping ballast and real compression. Their mechanism
|
|
17
|
+
* differs, and so does their promise, so each has its own method, its own honesty and its own
|
|
18
|
+
* note — and neither is passed off as the other. */
|
|
19
19
|
export const MINIFY_ENGINES = ['strip', 'esbuild'];
|
|
20
20
|
|
|
21
21
|
const STYLES = {
|
|
@@ -82,10 +82,10 @@ export const METRICS = {
|
|
|
82
82
|
if (esbuildLoader(file, cfg) !== null) return byteLen(minifyWithEsbuild(text, file, rev));
|
|
83
83
|
const min = minifyForm(text, file, cfg);
|
|
84
84
|
const ext = path.extname(file).toLowerCase();
|
|
85
|
-
/*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
85
|
+
/* The stripper's guard watches the simplification rather than the minifier: the minifier
|
|
86
|
+
* parses the file itself and reports its failure as a refusal (`src/minify.js`), while
|
|
87
|
+
* this guard answers whether our stripping threw away anything but comments and
|
|
88
|
+
* indentation. */
|
|
89
89
|
if (strategyFor(file, cfg) === 'strip-js' && cfg.minify.guard.indexOf(ext) >= 0) {
|
|
90
90
|
assertCompilable(min, rev, file, text);
|
|
91
91
|
}
|
|
@@ -108,8 +108,8 @@ export const METRICS = {
|
|
|
108
108
|
}
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
/*
|
|
112
|
-
*
|
|
111
|
+
/* The description of a metric for a reader: `note` is what the number means, `method` how it
|
|
112
|
+
* was obtained, `accuracy` whether it is exact or approximate. */
|
|
113
113
|
export function metricView(name, cfg) {
|
|
114
114
|
const metric = METRICS[name];
|
|
115
115
|
if (metric.view !== undefined) return metric.view(cfg);
|
|
@@ -121,12 +121,12 @@ export function metricView(name, cfg) {
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
124
|
+
/* The label of the `min` metric. The honesty convention: `accuracy` speaks about the worst in
|
|
125
|
+
* the column, while the method says where exactly the approximation is — so one format without
|
|
126
|
+
* a minifier makes the whole metric approximate instead of hiding behind the "exact" of a
|
|
127
|
+
* neighbouring file. The worst is taken by the same rule as the cell marks (`pointExact`)
|
|
128
|
+
* rather than by the name of the method: a report with no approximate format at all is exact
|
|
129
|
+
* even with the ballast stripped. */
|
|
130
130
|
function minView(cfg) {
|
|
131
131
|
const loc = cfg.locale;
|
|
132
132
|
const rough = approximateFormats('min', cfg);
|
|
@@ -149,11 +149,11 @@ function minView(cfg) {
|
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
/*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
152
|
+
/* The label of the `tok` metric. The honesty convention is the same as for `min`: the method
|
|
153
|
+
* says which dictionary produced the number (family and encoding are part of the count rather
|
|
154
|
+
* than a detail), and `accuracy` says whether it is exact or approximate. It turns approximate
|
|
155
|
+
* in two cases, both spelled out: formats for which tokens are not counted (their number runs
|
|
156
|
+
* by bytes) and a missing dictionary (then the count is an estimate by length). */
|
|
157
157
|
function tokView(cfg) {
|
|
158
158
|
const loc = cfg.locale;
|
|
159
159
|
const settings = cfg.tokens;
|
|
@@ -179,24 +179,23 @@ function tokView(cfg) {
|
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
/*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
182
|
+
/* Whether a particular cell holds an exact number — one rule for both the metric label and the
|
|
183
|
+
* cell mark. That is why the label cannot drift from the cells, and why the list of
|
|
184
|
+
* approximate formats is computed here, by the same rule.
|
|
185
185
|
*
|
|
186
|
-
* `min`
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* величины. */
|
|
186
|
+
* `min` is exact where the file really is minified: by the minifier, or by parsing a format
|
|
187
|
+
* that cannot get any shorter (JSON loses only insignificant whitespace — the list of exact
|
|
188
|
+
* strategies is owned by `strip.js`, where the strategies live). `tok` is exact where a
|
|
189
|
+
* dictionary exists and the format is text: the "tokens" of a picture or a font are its bytes.
|
|
190
|
+
* `raw` and `gzip` are always exact: they are unambiguous quantities. */
|
|
192
191
|
export function pointExact(name, file, cfg) {
|
|
193
192
|
if (name === 'min') return minifiedForm(file, cfg);
|
|
194
193
|
if (name === 'tok') return tokenizer(cfg.tokens).tool !== null && !isBinary(file);
|
|
195
194
|
return true;
|
|
196
195
|
}
|
|
197
196
|
|
|
198
|
-
/*
|
|
199
|
-
*
|
|
197
|
+
/* The formats of this report that will be measured approximately. The list is derived from the
|
|
198
|
+
* settings and the rule of exactness rather than written by hand. */
|
|
200
199
|
function approximateFormats(name, cfg) {
|
|
201
200
|
const exts = [];
|
|
202
201
|
cfg.columns.forEach((col) => {
|
|
@@ -213,9 +212,10 @@ function minifiedForm(file, cfg) {
|
|
|
213
212
|
return esbuildLoader(file, cfg) !== null || EXACT_STRATEGIES.indexOf(strategyFor(file, cfg)) >= 0;
|
|
214
213
|
}
|
|
215
214
|
|
|
216
|
-
/*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
215
|
+
/* Whether the file goes to the minifier: compression is requested, available and not overridden
|
|
216
|
+
* by an explicit choice of the project — `minify.ext` outranks the engine and serves as the way
|
|
217
|
+
* out when an extension lied about its content. One answer serves two questions: how to count
|
|
218
|
+
* and what to promise. */
|
|
219
219
|
function esbuildLoader(file, cfg) {
|
|
220
220
|
if (minEngine(cfg) !== 'esbuild') return null;
|
|
221
221
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -223,46 +223,47 @@ function esbuildLoader(file, cfg) {
|
|
|
223
223
|
return MINIFY_LOADERS[ext] === undefined ? null : MINIFY_LOADERS[ext];
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
/*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
226
|
+
/* The engine actually in force: the requested one may be unavailable, in which case the metric
|
|
227
|
+
* falls back to another count — and the fallback is announced (`sensorGaps`), or an
|
|
228
|
+
* approximation would travel as an exact number. */
|
|
229
229
|
export function minEngine(cfg) {
|
|
230
230
|
if (cfg.minify.engine !== 'esbuild') return 'strip';
|
|
231
231
|
return minifier().tool === null ? 'strip' : 'esbuild';
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
/*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
234
|
+
/* What is missing for what was asked: a cause and a fix for a human, one per sensor. The
|
|
235
|
+
* loader's cause goes here and nowhere else — inside the metric label it would be a machine
|
|
236
|
+
* string (a path into someone else's `node_modules`) that would make the output differ between
|
|
237
|
+
* machines, while the label in the report has to stay readable. */
|
|
238
238
|
export function sensorGaps(cfg) {
|
|
239
239
|
const gaps = [];
|
|
240
240
|
const minify = minifier();
|
|
241
241
|
if (cfg.minify.engine === 'esbuild' && minify.tool === null) {
|
|
242
242
|
gaps.push({
|
|
243
|
-
why: '
|
|
244
|
-
fix: '
|
|
243
|
+
why: 'the metric "min" counts by simplification: the minifier is unavailable — ' + minify.why,
|
|
244
|
+
fix: 'install the optional dependencies again or set "minify": {"engine": "strip"}'
|
|
245
245
|
});
|
|
246
246
|
}
|
|
247
247
|
if (cfg.metrics.indexOf('tok') >= 0) {
|
|
248
248
|
const tokens = tokenizer(cfg.tokens);
|
|
249
249
|
if (tokens.tool === null) {
|
|
250
250
|
gaps.push({
|
|
251
|
-
why: '
|
|
252
|
-
fix: '
|
|
251
|
+
why: 'the metric "tok" counts by an estimate of length: there is no dictionary — ' + tokens.why,
|
|
252
|
+
fix: 'install the optional dependencies again or remove "tok" from metrics'
|
|
253
253
|
});
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
return gaps;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
/*
|
|
260
|
-
*
|
|
259
|
+
/* The one place that decides whether a metric is read from the object size or from the text:
|
|
260
|
+
* a text-based metric on an unloaded blob is an error rather than a silent zero. */
|
|
261
261
|
export function measureBlob(name, blob, file, cfg, rev) {
|
|
262
262
|
const metric = METRICS[name];
|
|
263
263
|
if (metric.fromSize) return blob.size;
|
|
264
264
|
if (blob.text === null) {
|
|
265
|
-
throw new Error('
|
|
265
|
+
throw new Error('the metric "' + name + '" needs the content of ' + file + ' at '
|
|
266
|
+
+ rev.slice(0, 7) + ', and it was not read');
|
|
266
267
|
}
|
|
267
268
|
return metric.measure(blob.text, file, cfg, rev);
|
|
268
269
|
}
|
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 are an approximation"), so the
|
|
14
|
+
* 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);
|