@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/init.js
CHANGED
|
@@ -5,64 +5,64 @@ 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 counts the same numbers when the optional
|
|
18
|
+
* dependency is missing, and what to do
|
|
19
|
+
* next. The lines are assembled into a list rather than printed as they come, so that "what was said"
|
|
20
|
+
* can be read as a whole. */
|
|
21
21
|
function draftLines(root, target, cfg) {
|
|
22
22
|
const hasPkg = fs.existsSync(path.join(root, 'package.json'));
|
|
23
23
|
const manager = packageManager(root);
|
|
24
24
|
return [
|
|
25
|
-
'✓
|
|
26
|
-
'
|
|
25
|
+
'✓ settings derived from the project and pinned: ' + path.relative(root, target),
|
|
26
|
+
' columns: ' + cfg.columns.length + ' (' + cfg.columns.map((c) => c.label).slice(0, 6).join(', ')
|
|
27
27
|
+ (cfg.columns.length > 6 ? ', …' : '') + ')',
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
28
|
+
' paths skipped: ' + cfg.skip.length + ' (the report itself, dependency locks, maps, build output)',
|
|
29
|
+
' metric min: real compression (esbuild); without it — an honest simplification and code 4',
|
|
30
|
+
' metric tok: the o200k_base dictionary (gpt-tokenizer); without it — an estimate by length and code 4',
|
|
31
|
+
' journal: ' + (cfg.journal === null ? 'not found — row links will carry no sections' : cfg.journal.path),
|
|
32
|
+
' next: edit the columns and the metrics — which files matter is known by the project alone',
|
|
33
33
|
' ' + (hasPkg
|
|
34
|
-
? '
|
|
35
|
-
+ manager + ' run sizes (
|
|
36
|
-
: '
|
|
37
|
-
' ' + (hasPkg ? '
|
|
38
|
-
+ ';
|
|
34
|
+
? 'add "sizes": "size --write" to package.json — then the report will be built by '
|
|
35
|
+
+ manager + ' run sizes (the check — without --write)'
|
|
36
|
+
: 'run: ' + cfg.fixCommand + ' (the check — without --write)'),
|
|
37
|
+
' ' + (hasPkg ? 'add ' + manager + ' run test:sizes to CI' : 'add the check to CI')
|
|
38
|
+
+ '; the check is the package\'s command, it brings no files of its own into the project'
|
|
39
39
|
];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
/* An empty profile is a note rather than a refusal: the work was done, and nobody will pick the columns
|
|
43
|
+
* for the person. Hence "!", not the cross: a mark and an exit code must not say different things (the
|
|
44
|
+
* refusal catalogue counts a cross as a refusal and a note as not one). */
|
|
45
45
|
function noteNoColumns(root, target, cfg) {
|
|
46
46
|
if (cfg.columns.length > 0) return;
|
|
47
|
-
console.error('!
|
|
48
|
-
+ ' (
|
|
49
|
-
+ '\n
|
|
50
|
-
+ ' —
|
|
47
|
+
console.error('! no paths in the project could be taken as columns'
|
|
48
|
+
+ ' (the history is empty or holds no familiar extensions): the draft is written without columns'
|
|
49
|
+
+ '\n write them by hand into ' + path.relative(root, target)
|
|
50
|
+
+ ' — without columns the settings check will say "no columns are given (columns)"');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function initMode(root, file, force) {
|
|
54
54
|
const target = file ? path.resolve(root, file) : path.join(root, CONFIG_NAME);
|
|
55
55
|
if (fs.existsSync(target) && !force) {
|
|
56
|
-
//
|
|
57
|
-
//
|
|
56
|
+
// The advice names the very file in question: `--init --force` without a file would overwrite the
|
|
57
|
+
// default name with a draft rather than the file the person named.
|
|
58
58
|
const name = file === undefined || file === null ? CONFIG_NAME : advicePath(file);
|
|
59
|
-
refuseCause('
|
|
60
|
-
+ '\n
|
|
59
|
+
refuseCause('config already exists', 'config already exists: ' + target
|
|
60
|
+
+ '\n fix: edit it or overwrite it with a draft: ' + cliCommand('--init ' + name + ' --force'));
|
|
61
61
|
}
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
62
|
+
// What is pinned is the very thing the project runs on without a file (the project's derivation on
|
|
63
|
+
// top of the defaults), and it has to pass the same check the run will apply: the path in a refusal
|
|
64
|
+
// text is the file it landed in. "Derived" and that path are not written to the file: they are
|
|
65
|
+
// properties of where the settings came from rather than of the settings.
|
|
66
66
|
const cfg = derivedProfile(root);
|
|
67
67
|
if (cfg.columns.length > 0) validateConfig(Object.assign({}, cfg, { path: target }));
|
|
68
68
|
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,32 +10,33 @@ 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
|
-
* разошлось и не звучало по-разному. */
|
|
38
|
-
exact: 'точное число',
|
|
39
|
-
approximate: 'приближение',
|
|
40
|
-
approximateCell: 'приближённое число: ',
|
|
41
40
|
panelMethod: 'способ:',
|
|
42
41
|
emptyMetrics: 'Ни одна метрика не выбрана — таблицу не из чего собрать. '
|
|
43
42
|
+ 'Включите хотя бы одну в панели выше: без метрик нет ни одной колонки с числом.',
|
|
@@ -49,6 +48,11 @@ export const LOCALES = {
|
|
|
49
48
|
+ 'что выбрано в этом браузере.',
|
|
50
49
|
linkExtra: 'Часть ссылки пропущена: в этом отчёте нет {n} названных имён. '
|
|
51
50
|
+ 'Остальное применено.',
|
|
51
|
+
/* Данные отчёта лежат в файле сжатыми, и распаковывает их браузер. Текст называет причину
|
|
52
|
+
* (браузера нет в списке тех, что умеют `DecompressionStream`) и что с этим делать — пустая
|
|
53
|
+
* таблица не сказала бы читателю ничего. */
|
|
54
|
+
unpack: 'Данные отчёта сжаты, а браузер не умеет их распаковать: в нём нет DecompressionStream. '
|
|
55
|
+
+ 'Обновите браузер — таблицу показывать не из чего.',
|
|
52
56
|
note: 'Абсолютные размеры стоят один раз — в строке «{now}». Строк: {rows}: коммиты без '
|
|
53
57
|
+ 'изменения объёма и коммиты, тронувшие только выключенные файлы, строк не получают. '
|
|
54
58
|
+ 'Собрано из истории git: {command}.'
|
|
@@ -88,11 +92,12 @@ export const LOCALES = {
|
|
|
88
92
|
+ ' (built, a dependency lock, the report itself, or too large)',
|
|
89
93
|
notMeasuredChoice: 'not measured: it is not in the report’s set of columns'
|
|
90
94
|
+ ' — that set comes from the settings (columns)',
|
|
95
|
+
notOnHead: ' (not on HEAD)',
|
|
96
|
+
category: ' · category: ',
|
|
97
|
+
categoryFromConfig: 'from the settings',
|
|
98
|
+
categoryByExtension: 'by extension',
|
|
91
99
|
all: 'all',
|
|
92
100
|
sub: '{tool} {version} · {artifact}',
|
|
93
|
-
exact: 'an exact number',
|
|
94
|
-
approximate: 'an approximation',
|
|
95
|
-
approximateCell: 'an approximate number: ',
|
|
96
101
|
panelMethod: 'method:',
|
|
97
102
|
emptyMetrics: 'No metric is selected — there is nothing to build the table from. '
|
|
98
103
|
+ 'Turn on at least one above: without metrics there is no column holding a number.',
|
|
@@ -104,6 +109,10 @@ export const LOCALES = {
|
|
|
104
109
|
+ 'has.',
|
|
105
110
|
linkExtra: 'Part of the link is skipped: this report has none of the {n} names '
|
|
106
111
|
+ 'it mentions. The rest is applied.',
|
|
112
|
+
/* The report's data lies in the file compressed and the browser unpacks it; this text is what a
|
|
113
|
+
* browser without `DecompressionStream` gets instead of an empty table. */
|
|
114
|
+
unpack: 'The report\u2019s data is compressed and this browser cannot unpack it: there is no '
|
|
115
|
+
+ 'DecompressionStream. Update the browser — there is nothing to build the table from.',
|
|
107
116
|
note: 'Absolute sizes appear once — in the “{now}” row. Rows: {rows}: commits that change '
|
|
108
117
|
+ 'no number, and commits touching only the disabled files, get no row. '
|
|
109
118
|
+ 'Collected from git history: {command}.'
|
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 its
|
|
8
|
+
* number is obtained. Separate from the ways of stripping ballast: a metric is a promise about
|
|
9
|
+
* a 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 the method a reader is told, so each has its own text — and neither is
|
|
18
|
+
* passed off as the other. */
|
|
19
19
|
export const MINIFY_ENGINES = ['strip', 'esbuild'];
|
|
20
20
|
|
|
21
21
|
const STYLES = {
|
|
@@ -33,12 +33,12 @@ const STYLES = {
|
|
|
33
33
|
},
|
|
34
34
|
method: { ru: 'esbuild {version} (minify, rename)', en: 'esbuild {version} (minify, rename)' },
|
|
35
35
|
fallback: {
|
|
36
|
-
ru: '; остальные форматы ({exts}) — упрощение без комментариев и
|
|
37
|
-
en: '; other formats ({exts}) lose comments and indentation
|
|
36
|
+
ru: '; остальные форматы ({exts}) — упрощение без комментариев и отступов',
|
|
37
|
+
en: '; other formats ({exts}) lose comments and indentation'
|
|
38
38
|
},
|
|
39
39
|
unavailable: {
|
|
40
|
-
ru: ' (минификатор esbuild недоступен — счёт
|
|
41
|
-
en: ' (the esbuild minifier is unavailable — comments and indentation are stripped
|
|
40
|
+
ru: ' (минификатор esbuild недоступен — счёт идёт упрощением)',
|
|
41
|
+
en: ' (the esbuild minifier is unavailable — comments and indentation are stripped)'
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
tok: {
|
|
@@ -48,12 +48,12 @@ const STYLES = {
|
|
|
48
48
|
},
|
|
49
49
|
method: { ru: '{tool} {version}, {encoding} (BPE)', en: '{tool} {version}, {encoding} (BPE)' },
|
|
50
50
|
binary: {
|
|
51
|
-
ru: '; для бинарных форматов ({exts}) это счёт байтов, а не текста
|
|
52
|
-
en: '; binary formats ({exts}) are counted by bytes rather than text
|
|
51
|
+
ru: '; для бинарных форматов ({exts}) это счёт байтов, а не текста',
|
|
52
|
+
en: '; binary formats ({exts}) are counted by bytes rather than text'
|
|
53
53
|
},
|
|
54
54
|
estimate: {
|
|
55
|
-
ru: 'оценка по длине: 1 токен ≈ {chars} знака ({encoding} недоступен)
|
|
56
|
-
en: 'length-based estimate: 1 token ≈ {chars} characters ({encoding} is unavailable)
|
|
55
|
+
ru: 'оценка по длине: 1 токен ≈ {chars} знака ({encoding} недоступен)',
|
|
56
|
+
en: 'length-based estimate: 1 token ≈ {chars} characters ({encoding} is unavailable)'
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
};
|
|
@@ -71,7 +71,6 @@ export const METRICS = {
|
|
|
71
71
|
ru: 'размер объекта git',
|
|
72
72
|
en: 'the size of the git object'
|
|
73
73
|
},
|
|
74
|
-
accuracy: 'exact',
|
|
75
74
|
measure: (text) => byteLen(text)
|
|
76
75
|
},
|
|
77
76
|
min: {
|
|
@@ -82,10 +81,10 @@ export const METRICS = {
|
|
|
82
81
|
if (esbuildLoader(file, cfg) !== null) return byteLen(minifyWithEsbuild(text, file, rev));
|
|
83
82
|
const min = minifyForm(text, file, cfg);
|
|
84
83
|
const ext = path.extname(file).toLowerCase();
|
|
85
|
-
/*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
84
|
+
/* The stripper's guard watches the simplification rather than the minifier: the minifier
|
|
85
|
+
* parses the file itself and reports its failure as a refusal (`src/minify.js`), while
|
|
86
|
+
* this guard answers whether our stripping threw away anything but comments and
|
|
87
|
+
* indentation. */
|
|
89
88
|
if (strategyFor(file, cfg) === 'strip-js' && cfg.minify.guard.indexOf(ext) >= 0) {
|
|
90
89
|
assertCompilable(min, rev, file, text);
|
|
91
90
|
}
|
|
@@ -103,57 +102,50 @@ export const METRICS = {
|
|
|
103
102
|
needsText: true,
|
|
104
103
|
note: { ru: 'сжатый поток (zlib, уровень 9)', en: 'compressed stream (zlib, level 9)' },
|
|
105
104
|
method: { ru: 'zlib, уровень 9', en: 'zlib, level 9' },
|
|
106
|
-
accuracy: 'exact',
|
|
107
105
|
measure: (text) => zlib.gzipSync(Buffer.from(text, 'utf8'), { level: 9 }).length
|
|
108
106
|
}
|
|
109
107
|
};
|
|
110
108
|
|
|
111
|
-
/*
|
|
112
|
-
*
|
|
109
|
+
/* The description of a metric for a reader: `note` is what the number means, `method` how it
|
|
110
|
+
* was obtained. */
|
|
113
111
|
export function metricView(name, cfg) {
|
|
114
112
|
const metric = METRICS[name];
|
|
115
113
|
if (metric.view !== undefined) return metric.view(cfg);
|
|
116
114
|
return {
|
|
117
115
|
label: metric.label,
|
|
118
116
|
note: metric.note[cfg.locale],
|
|
119
|
-
method: metric.method[cfg.locale]
|
|
120
|
-
accuracy: metric.accuracy
|
|
117
|
+
method: metric.method[cfg.locale]
|
|
121
118
|
};
|
|
122
119
|
}
|
|
123
120
|
|
|
124
|
-
/*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* (`pointExact`), а не по названию способа: отчёт, где нет ни одного
|
|
129
|
-
* приближённого формата, точен и со снятым балластом. */
|
|
121
|
+
/* The label of the `min` metric. The method names the formats this metric counts another way: a
|
|
122
|
+
* format the minifier does not take is given in words rather than hidden behind the neighbouring
|
|
123
|
+
* one, so a reader sees what the column is made of. The list comes from the same rule the counting
|
|
124
|
+
* uses (`otherCountFormats`) rather than from the name of the method. */
|
|
130
125
|
function minView(cfg) {
|
|
131
126
|
const loc = cfg.locale;
|
|
132
|
-
const
|
|
127
|
+
const other = otherCountFormats('min', cfg);
|
|
133
128
|
if (minEngine(cfg) === 'esbuild') {
|
|
134
129
|
let method = STYLES.esbuild.method[loc].replace('{version}', minifier().version);
|
|
135
|
-
if (
|
|
130
|
+
if (other.length > 0) method += STYLES.esbuild.fallback[loc].replace('{exts}', other.join(' '));
|
|
136
131
|
return {
|
|
137
132
|
label: METRICS.min.label,
|
|
138
133
|
note: STYLES.esbuild.note[loc],
|
|
139
|
-
method: method
|
|
140
|
-
accuracy: rough.length === 0 ? 'exact' : 'approximate'
|
|
134
|
+
method: method
|
|
141
135
|
};
|
|
142
136
|
}
|
|
143
137
|
const degraded = cfg.minify.engine === 'esbuild';
|
|
144
138
|
return {
|
|
145
139
|
label: METRICS.min.label,
|
|
146
140
|
note: STYLES.strip.note[loc],
|
|
147
|
-
method: STYLES.strip.method[loc] + (degraded ? STYLES.esbuild.unavailable[loc] : '')
|
|
148
|
-
accuracy: rough.length === 0 ? 'exact' : 'approximate'
|
|
141
|
+
method: STYLES.strip.method[loc] + (degraded ? STYLES.esbuild.unavailable[loc] : '')
|
|
149
142
|
};
|
|
150
143
|
}
|
|
151
144
|
|
|
152
|
-
/*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* число идёт по байтам), и отсутствие словаря (тогда счёт идёт оценкой по длине). */
|
|
145
|
+
/* The label of the `tok` metric. The method says which dictionary produced the number (family and
|
|
146
|
+
* encoding are part of the count rather than a detail) and names the two cases where the count is
|
|
147
|
+
* another one, both spelled out: formats for which tokens are not counted (their number runs by
|
|
148
|
+
* bytes) and a missing dictionary (then the count is an estimate by length). */
|
|
157
149
|
function tokView(cfg) {
|
|
158
150
|
const loc = cfg.locale;
|
|
159
151
|
const settings = cfg.tokens;
|
|
@@ -163,45 +155,38 @@ function tokView(cfg) {
|
|
|
163
155
|
label: METRICS.tok.label,
|
|
164
156
|
note: STYLES.tok.note[loc],
|
|
165
157
|
method: STYLES.tok.estimate[loc]
|
|
166
|
-
.replace('{chars}', CHARS_PER_TOKEN).replace('{encoding}', settings.encoding)
|
|
167
|
-
accuracy: 'approximate'
|
|
158
|
+
.replace('{chars}', CHARS_PER_TOKEN).replace('{encoding}', settings.encoding)
|
|
168
159
|
};
|
|
169
160
|
}
|
|
170
|
-
const binary =
|
|
161
|
+
const binary = otherCountFormats('tok', cfg);
|
|
171
162
|
let method = STYLES.tok.method[loc]
|
|
172
163
|
.replace('{tool}', 'gpt-tokenizer').replace('{version}', version).replace('{encoding}', settings.encoding);
|
|
173
164
|
if (binary.length > 0) method += STYLES.tok.binary[loc].replace('{exts}', binary.join(' '));
|
|
174
165
|
return {
|
|
175
166
|
label: METRICS.tok.label,
|
|
176
167
|
note: STYLES.tok.note[loc],
|
|
177
|
-
method: method
|
|
178
|
-
accuracy: binary.length === 0 ? 'exact' : 'approximate'
|
|
168
|
+
method: method
|
|
179
169
|
};
|
|
180
170
|
}
|
|
181
171
|
|
|
182
|
-
/*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
* разбором формата, который короче уже не станет (JSON теряет только незначащие
|
|
188
|
-
* пробелы — список точных стратегий ведёт `strip.js`, потому что стратегии живут
|
|
189
|
-
* там). `tok` точен там, где есть словарь и формат текстовый: «токены» картинки
|
|
190
|
-
* или шрифта — это её байты. `raw` и `gzip` точны всегда: это однозначные
|
|
191
|
-
* величины. */
|
|
192
|
-
export function pointExact(name, file, cfg) {
|
|
193
|
-
if (name === 'min') return minifiedForm(file, cfg);
|
|
194
|
-
if (name === 'tok') return tokenizer(cfg.tokens).tool !== null && !isBinary(file);
|
|
195
|
-
return true;
|
|
172
|
+
/* Whether a file of this report is really minified: by the minifier, or by parsing a format that
|
|
173
|
+
* cannot get any shorter (JSON loses only insignificant whitespace — the list of exact strategies
|
|
174
|
+
* is owned by `strip.js`, where the strategies live). */
|
|
175
|
+
function minifiedForm(file, cfg) {
|
|
176
|
+
return esbuildLoader(file, cfg) !== null || EXACT_STRATEGIES.indexOf(strategyFor(file, cfg)) >= 0;
|
|
196
177
|
}
|
|
197
178
|
|
|
198
|
-
/*
|
|
199
|
-
*
|
|
200
|
-
|
|
179
|
+
/* The formats of this report that the metric counts another way — the minifier does not take
|
|
180
|
+
* them, or tokens are meaningless for them (the "tokens" of a picture or a font are its bytes).
|
|
181
|
+
* The list is derived from the settings and the counting rule rather than written by hand, and
|
|
182
|
+
* it is what the method names in words. */
|
|
183
|
+
function otherCountFormats(name, cfg) {
|
|
201
184
|
const exts = [];
|
|
202
185
|
cfg.columns.forEach((col) => {
|
|
203
186
|
col.paths.forEach((p) => {
|
|
204
|
-
|
|
187
|
+
const counted = name === 'min' ? minifiedForm(p, cfg)
|
|
188
|
+
: tokenizer(cfg.tokens).tool !== null && !isBinary(p);
|
|
189
|
+
if (counted) return;
|
|
205
190
|
const ext = path.extname(p).toLowerCase();
|
|
206
191
|
if (exts.indexOf(ext) < 0) exts.push(ext);
|
|
207
192
|
});
|
|
@@ -209,13 +194,10 @@ function approximateFormats(name, cfg) {
|
|
|
209
194
|
return exts.sort();
|
|
210
195
|
}
|
|
211
196
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
/* Идёт ли файл в минификатор: сжатие запрошено, доступно и не отменено явным
|
|
217
|
-
* выбором проекта — `minify.ext` старше движка и служит выходом, если расширение
|
|
218
|
-
* соврало о содержимом. Ответ один на два вопроса: как считать и что обещать. */
|
|
197
|
+
/* Whether the file goes to the minifier: compression is requested, available and not overridden
|
|
198
|
+
* by an explicit choice of the project — `minify.ext` outranks the engine and serves as the way
|
|
199
|
+
* out when an extension lied about its content. One answer serves two questions: how to count
|
|
200
|
+
* and what to promise. */
|
|
219
201
|
function esbuildLoader(file, cfg) {
|
|
220
202
|
if (minEngine(cfg) !== 'esbuild') return null;
|
|
221
203
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -223,46 +205,47 @@ function esbuildLoader(file, cfg) {
|
|
|
223
205
|
return MINIFY_LOADERS[ext] === undefined ? null : MINIFY_LOADERS[ext];
|
|
224
206
|
}
|
|
225
207
|
|
|
226
|
-
/*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
208
|
+
/* The engine actually in force: the requested one may be unavailable, in which case the metric
|
|
209
|
+
* counts another way — and that is announced (`sensorGaps`), or a different count would travel
|
|
210
|
+
* into CI as the requested one. */
|
|
229
211
|
export function minEngine(cfg) {
|
|
230
212
|
if (cfg.minify.engine !== 'esbuild') return 'strip';
|
|
231
213
|
return minifier().tool === null ? 'strip' : 'esbuild';
|
|
232
214
|
}
|
|
233
215
|
|
|
234
|
-
/*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
216
|
+
/* What is missing for what was asked: a cause and a fix for a human, one per sensor. The
|
|
217
|
+
* loader's cause goes here and nowhere else — inside the metric label it would be a machine
|
|
218
|
+
* string (a path into someone else's `node_modules`) that would make the output differ between
|
|
219
|
+
* machines, while the label in the report has to stay readable. */
|
|
238
220
|
export function sensorGaps(cfg) {
|
|
239
221
|
const gaps = [];
|
|
240
222
|
const minify = minifier();
|
|
241
223
|
if (cfg.minify.engine === 'esbuild' && minify.tool === null) {
|
|
242
224
|
gaps.push({
|
|
243
|
-
why: '
|
|
244
|
-
fix: '
|
|
225
|
+
why: 'the metric "min" counts by simplification: the minifier is unavailable — ' + minify.why,
|
|
226
|
+
fix: 'install the optional dependencies again or set "minify": {"engine": "strip"}'
|
|
245
227
|
});
|
|
246
228
|
}
|
|
247
229
|
if (cfg.metrics.indexOf('tok') >= 0) {
|
|
248
230
|
const tokens = tokenizer(cfg.tokens);
|
|
249
231
|
if (tokens.tool === null) {
|
|
250
232
|
gaps.push({
|
|
251
|
-
why: '
|
|
252
|
-
fix: '
|
|
233
|
+
why: 'the metric "tok" counts by an estimate of length: there is no dictionary — ' + tokens.why,
|
|
234
|
+
fix: 'install the optional dependencies again or remove "tok" from metrics'
|
|
253
235
|
});
|
|
254
236
|
}
|
|
255
237
|
}
|
|
256
238
|
return gaps;
|
|
257
239
|
}
|
|
258
240
|
|
|
259
|
-
/*
|
|
260
|
-
*
|
|
241
|
+
/* The one place that decides whether a metric is read from the object size or from the text:
|
|
242
|
+
* a text-based metric on an unloaded blob is an error rather than a silent zero. */
|
|
261
243
|
export function measureBlob(name, blob, file, cfg, rev) {
|
|
262
244
|
const metric = METRICS[name];
|
|
263
245
|
if (metric.fromSize) return blob.size;
|
|
264
246
|
if (blob.text === null) {
|
|
265
|
-
throw new Error('
|
|
247
|
+
throw new Error('the metric "' + name + '" needs the content of ' + file + ' at '
|
|
248
|
+
+ rev.slice(0, 7) + ', and it was not read');
|
|
266
249
|
}
|
|
267
250
|
return metric.measure(blob.text, file, cfg, rev);
|
|
268
251
|
}
|