@vernikr/size-report 1.2.0 → 1.3.1
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/CHANGELOG.md +91 -0
- package/README.md +183 -71
- package/package.json +21 -2
- package/src/args.js +191 -0
- package/src/artifact.js +9 -2
- package/src/check.js +8 -10
- package/src/cli.js +47 -457
- package/src/config.js +103 -24
- package/src/data.js +16 -9
- package/src/derived.js +3 -2
- package/src/doctor.js +121 -65
- package/src/explain.js +43 -26
- package/src/history.js +94 -77
- package/src/hook.js +17 -11
- package/src/init.js +75 -0
- package/src/modes.js +190 -0
- package/src/page/app.css +30 -0
- package/src/page/app.js +9 -498
- package/src/page/build.js +16 -7
- package/src/page/dom.js +22 -0
- package/src/page/panel.js +124 -0
- package/src/page/state.js +226 -0
- package/src/page/table.js +145 -0
- package/src/project.js +262 -0
- package/src/refusal.js +6 -1
- package/src/render.js +19 -11
- package/src/size-table.js +10 -4
- package/src/strip/forms.js +26 -0
- package/src/strip/guard.js +59 -0
- package/src/strip/js.js +136 -0
- package/src/strip.js +17 -190
- package/templates/README.md +16 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vernikr/size-report",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"author": "vernikr",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,7 +41,21 @@
|
|
|
41
41
|
"parity:live": "node tools/parity-live.js",
|
|
42
42
|
"pack:check": "node tools/pack-check.js",
|
|
43
43
|
"check:standards": "node tools/check-standards.js",
|
|
44
|
-
"fixture": "node tools/make-fixture.js"
|
|
44
|
+
"fixture": "node tools/make-fixture.js",
|
|
45
|
+
"verify:fast": "node tools/gates/run.js fast",
|
|
46
|
+
"verify": "node tools/gates/run.js full",
|
|
47
|
+
"verify:slow": "node tools/gates/run.js slow",
|
|
48
|
+
"metrics": "node tools/gates/metrics.js",
|
|
49
|
+
"dup": "node tools/gates/dup.js",
|
|
50
|
+
"dup:ci": "node tools/gates/dup.js --ref origin/main",
|
|
51
|
+
"dup:baseline": "node tools/gates/dup.js --update",
|
|
52
|
+
"deps": "node tools/gates/deps.js",
|
|
53
|
+
"cover": "node tools/gates/coverage.js",
|
|
54
|
+
"gate:files": "node tools/gates/gatefiles.js",
|
|
55
|
+
"baseline:metrics": "eslint --config eslint.metrics.config.js --suppressions-location .eslint-suppressions.json --prune-suppressions src bin tools test && eslint --config eslint.metrics.config.js --suppressions-location .eslint-suppressions.json --suppress-all src bin tools test",
|
|
56
|
+
"baseline:dup": "node tools/gates/dup.js --update",
|
|
57
|
+
"baseline:coverage": "node tools/gates/coverage.js --update",
|
|
58
|
+
"hooks:install": "git config core.hooksPath .githooks"
|
|
45
59
|
},
|
|
46
60
|
"keywords": [
|
|
47
61
|
"size",
|
|
@@ -51,7 +65,12 @@
|
|
|
51
65
|
"report"
|
|
52
66
|
],
|
|
53
67
|
"devDependencies": {
|
|
68
|
+
"@vernikr/size-report": "1.3.0",
|
|
69
|
+
"c8": "10",
|
|
70
|
+
"dependency-cruiser": "17",
|
|
54
71
|
"eslint": "^9.18.0",
|
|
72
|
+
"eslint-plugin-sonarjs": "4",
|
|
73
|
+
"jscpd": "5.2.0",
|
|
55
74
|
"jsdom": "~26"
|
|
56
75
|
},
|
|
57
76
|
"optionalDependencies": {
|
package/src/args.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { cliCommand, advicePath, refuseCause } from './refusal.js';
|
|
2
|
+
|
|
3
|
+
/* Грамматика командной строки: что человек назвал и законно ли это сочетание.
|
|
4
|
+
*
|
|
5
|
+
* Один разбор на входе, до чтения проекта. Он решает всё сразу — какой режим
|
|
6
|
+
* запрошен, совместим ли он с командой и с остальными ключами, все ли ключи
|
|
7
|
+
* получили значение, нет ли лишних слов, — и отвечает либо планом, либо отказом.
|
|
8
|
+
* Поэтому порядок ветвлений в `main` ничего не решает: правило «так нельзя» — это
|
|
9
|
+
* значение, а не место в коде, и наружу оно выходит одним способом — отказом с
|
|
10
|
+
* названным виновником и готовой командой.
|
|
11
|
+
*
|
|
12
|
+
* Ключи трёх родов: режимы (взаимоисключающие — они задают, что делать), ключи со
|
|
13
|
+
* значением (забирают следующий аргумент) и переключатели. Команда — первое слово
|
|
14
|
+
* вне ключей: её чтение не зависит от места в строке, поэтому и `size check
|
|
15
|
+
* --config x`, и `size --config x check` — одно и то же.
|
|
16
|
+
*
|
|
17
|
+
* Файл отдельный от `cli.js`, потому что вопросы у них разные: здесь — «что
|
|
18
|
+
* запрошено и можно ли так», там — «что по запросу делать». Отсюда и цена ошибки
|
|
19
|
+
* в каждом: неверное правило здесь ломает все режимы сразу, поэтому проверки
|
|
20
|
+
* разложены по группам (режим, слово, форма ответа), а каталог отказов считает их
|
|
21
|
+
* места и требует проверки на каждое.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const MODES = ['--init', '--write', '--data', '--page'];
|
|
25
|
+
const VALUE_FLAGS = ['--config', '--init', '--page'];
|
|
26
|
+
const FLAGS = ['--help', '-h'].concat(MODES, VALUE_FLAGS, ['--json', '--force']);
|
|
27
|
+
const COMMANDS = ['check', 'explain', 'doctor', 'install-hook', 'uninstall-hook', 'hook-run'];
|
|
28
|
+
const ANSWER_COMMANDS = ['check', 'explain', 'doctor'];
|
|
29
|
+
export const HOOK_COMMANDS = ['install-hook', 'uninstall-hook', 'hook-run'];
|
|
30
|
+
|
|
31
|
+
/* Ключ со значением: забирает следующий аргумент и возвращает, сколько съел. У
|
|
32
|
+
* `--init` и `--page` пустое значение — законное «по умолчанию», а у `--config`
|
|
33
|
+
* это молчаливый пропуск: настройки были бы взяты не те, что назвал человек. */
|
|
34
|
+
function takeValue(flag, args, i, values) {
|
|
35
|
+
const next = args[i + 1];
|
|
36
|
+
const none = next === undefined || next[0] === '-';
|
|
37
|
+
if (none && flag === '--config') {
|
|
38
|
+
refuseCause('ключ без значения', 'у ключа «' + flag + '» нет значения: нужен файл настроек'
|
|
39
|
+
+ '\n починка: ' + cliCommand(flag + ' <файл>'));
|
|
40
|
+
}
|
|
41
|
+
values[flag] = none ? null : next;
|
|
42
|
+
return none ? 0 : 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Один проход по строке: слова, режимы и значения ключей. Отказы здесь — только
|
|
46
|
+
* про сам ключ (незнакомый, повторённый, без значения): сочетания разбирает
|
|
47
|
+
* `checkArgs`, потому что они про названное вместе, а не про отдельный аргумент. */
|
|
48
|
+
function scan(args) {
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const modes = [];
|
|
51
|
+
const values = {};
|
|
52
|
+
const words = [];
|
|
53
|
+
for (let i = 0; i < args.length; i++) {
|
|
54
|
+
const a = args[i];
|
|
55
|
+
if (a[0] !== '-') { words.push(a); continue; }
|
|
56
|
+
if (FLAGS.indexOf(a) < 0) {
|
|
57
|
+
refuseCause('незнакомый ключ', 'незнакомый ключ «' + a + '»\n починка: ' + cliCommand('--help'));
|
|
58
|
+
}
|
|
59
|
+
if (seen.has(a)) {
|
|
60
|
+
refuseCause('повтор ключа', 'ключ «' + a + '» назван дважды\n починка: ' + cliCommand('--help'));
|
|
61
|
+
}
|
|
62
|
+
seen.add(a);
|
|
63
|
+
if (VALUE_FLAGS.indexOf(a) >= 0) i += takeValue(a, args, i, values);
|
|
64
|
+
if (MODES.indexOf(a) >= 0) modes.push(a);
|
|
65
|
+
}
|
|
66
|
+
return { seen: seen, modes: modes, values: values, words: words };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Совет повторяет настройки, которые человек назвал: без `--config` команда ищет
|
|
70
|
+
* файл под умолчательным именем, в проекте с другим именем не находит его и уводит
|
|
71
|
+
* человека во второй отказ — про файл настроек, которого у него нет. */
|
|
72
|
+
function advisor(values) {
|
|
73
|
+
const given = typeof values['--config'] === 'string'
|
|
74
|
+
? '--config ' + advicePath(values['--config']) + ' ' : '';
|
|
75
|
+
return (rest) => cliCommand(given + rest);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Режим против ключей: сами режимы взаимоисключающие, а `--force` и `--config`
|
|
79
|
+
* работают не с каждым из них. */
|
|
80
|
+
function checkModes(plan) {
|
|
81
|
+
const { modes, seen, mode, advice } = plan;
|
|
82
|
+
if (modes.length > 1) {
|
|
83
|
+
refuseCause('два режима сразу', 'два режима сразу: «' + modes[0] + '» и «' + modes[1] + '» — режим один'
|
|
84
|
+
+ '\n починка: ' + advice(modes[0]));
|
|
85
|
+
}
|
|
86
|
+
if (seen.has('--force') && mode !== '--init') {
|
|
87
|
+
refuseCause('несовместимый ключ', 'ключ «--force» работает только с «--init»'
|
|
88
|
+
+ '\n починка: ' + cliCommand('--init --force'));
|
|
89
|
+
}
|
|
90
|
+
if (seen.has('--config') && mode === '--init') {
|
|
91
|
+
refuseCause('несовместимый ключ', 'у «--init» свой файл, а «--config» называет настройки проекта'
|
|
92
|
+
+ '\n починка: ' + cliCommand('--init <файл>'));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Слово, которого команда не знает. Отдельным вопросом, потому что виновников
|
|
97
|
+
* тут двое: опечатка в команде — и лишнее значение режима, который своё значение
|
|
98
|
+
* уже забрал (у `--init` и `--page` оно одно). Оба случая обязаны назвать своего
|
|
99
|
+
* виновника: у `--config` остаток — именно команда, и зов её разбирается как
|
|
100
|
+
* команда, а не как лишнее слово. */
|
|
101
|
+
function checkUnknownWord(plan) {
|
|
102
|
+
const { verb, values, advice } = plan;
|
|
103
|
+
if (verb === null || COMMANDS.indexOf(verb) >= 0) return;
|
|
104
|
+
const valued = MODES.find((f) => VALUE_FLAGS.indexOf(f) >= 0 && typeof values[f] === 'string');
|
|
105
|
+
if (valued !== undefined) {
|
|
106
|
+
refuseCause('лишнее слово', 'лишнее слово «' + verb + '»: «' + valued + '» принимает одно значение'
|
|
107
|
+
+ '\n починка: ' + advice(valued + ' ' + advicePath(values[valued])));
|
|
108
|
+
}
|
|
109
|
+
refuseCause('неизвестная команда', 'неизвестная команда «' + verb + '»\n починка: ' + cliCommand('--help'));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Слово и режим вместе: команда задаёт, что ответить, режим — что записать, и
|
|
113
|
+
* одновременно они не работают. */
|
|
114
|
+
function checkWordAgainstMode(plan) {
|
|
115
|
+
const { verb, mode, advice } = plan;
|
|
116
|
+
if (verb === null || mode === null) return;
|
|
117
|
+
refuseCause('команда и режим', 'команда «' + verb + '» и режим «' + mode + '» — разное, вместе они не работают'
|
|
118
|
+
+ '\n починка: ' + advice(verb));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Сколько слов принято: у `explain` коммит один и он обязателен, у остальных
|
|
122
|
+
* команд аргументов нет вовсе. Коммит либо не назван, либо назван не один раз —
|
|
123
|
+
* тупика два, а починка одна и та же. */
|
|
124
|
+
function checkWordCount(plan) {
|
|
125
|
+
const { verb, arg, advice } = plan;
|
|
126
|
+
if (verb === 'explain' && arg.length === 0) {
|
|
127
|
+
refuseCause('нет коммита', 'команде «explain» нужен коммит: имя ревизии (HEAD, ветка, тег),'
|
|
128
|
+
+ ' sha или его начало'
|
|
129
|
+
+ '\n починка: ' + advice('explain <коммит>'));
|
|
130
|
+
}
|
|
131
|
+
if (verb === 'explain' && arg.length > 1) {
|
|
132
|
+
refuseCause('лишнее слово', 'команда «explain» принимает один коммит, а не ' + arg.length
|
|
133
|
+
+ ': «' + arg.slice(1).join('», «') + '» лишние\n починка: ' + advice('explain <коммит>'));
|
|
134
|
+
}
|
|
135
|
+
if (verb !== null && verb !== 'explain' && arg.length > 0) {
|
|
136
|
+
refuseCause('лишнее слово', 'команда «' + verb + '» аргументов не принимает: «' + arg[0] + '» лишний'
|
|
137
|
+
+ '\n починка: ' + advice(verb));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Форма ответа против всего остального. `--json` — не режим, а форма, и правило у
|
|
142
|
+
* него одно: ответ бывает ровно у трёх команд и у запуска без команды (прежняя
|
|
143
|
+
* форма данных — она заморожена эталоном паритета, убрать её нельзя). У команды
|
|
144
|
+
* без ответа просить нечего, а у режима ответ уже один — запись; оба случая —
|
|
145
|
+
* отказ, и каждый называет своего виновника. */
|
|
146
|
+
function checkAnswer(plan) {
|
|
147
|
+
const { verb, mode, seen, advice } = plan;
|
|
148
|
+
if (seen.has('--json') && verb !== null && ANSWER_COMMANDS.indexOf(verb) < 0) {
|
|
149
|
+
refuseCause('нет ответа в JSON', 'у команды «' + verb + '» нет ответа в JSON'
|
|
150
|
+
+ '\n починка: ' + advice(verb));
|
|
151
|
+
}
|
|
152
|
+
if (seen.has('--json') && verb === null && mode !== null) {
|
|
153
|
+
refuseCause('два ответа сразу', '«--json» и режим «' + mode + '» — разное: данные или запись, но не оба'
|
|
154
|
+
+ '\n починка: ' + advice(mode));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Проверки сочетаний — тремя группами и одним планом на всех: план это то, что
|
|
159
|
+
* разбор уже знает (слово, аргумент, режим, названные ключи и совет), а группы
|
|
160
|
+
* отвечают каждая за своё. Возврат — готовый план запуска. */
|
|
161
|
+
function checkArgs(scanned) {
|
|
162
|
+
const { words, seen, values, modes } = scanned;
|
|
163
|
+
const plan = {
|
|
164
|
+
verb: words.length > 0 ? words[0] : null,
|
|
165
|
+
arg: words.slice(1),
|
|
166
|
+
modes: modes,
|
|
167
|
+
mode: modes.length > 0 ? modes[0] : null,
|
|
168
|
+
seen: seen,
|
|
169
|
+
values: values,
|
|
170
|
+
advice: advisor(values)
|
|
171
|
+
};
|
|
172
|
+
checkModes(plan);
|
|
173
|
+
checkUnknownWord(plan);
|
|
174
|
+
checkWordAgainstMode(plan);
|
|
175
|
+
checkWordCount(plan);
|
|
176
|
+
checkAnswer(plan);
|
|
177
|
+
return {
|
|
178
|
+
verb: plan.verb,
|
|
179
|
+
mode: plan.mode,
|
|
180
|
+
arg: plan.arg,
|
|
181
|
+
json: seen.has('--json'),
|
|
182
|
+
force: seen.has('--force')
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function parseArgs(args) {
|
|
187
|
+
// Справка отвечает всегда и первой: она и есть выход из любой опечатки.
|
|
188
|
+
if (args.indexOf('--help') >= 0 || args.indexOf('-h') >= 0) return { help: true };
|
|
189
|
+
const scanned = scan(args);
|
|
190
|
+
return Object.assign({ help: false, values: scanned.values }, checkArgs(scanned));
|
|
191
|
+
}
|
package/src/artifact.js
CHANGED
|
@@ -11,11 +11,18 @@ import { render } from './render.js';
|
|
|
11
11
|
* Каталог создаётся здесь же: `--write .size-report/report.html` в свежем проекте —
|
|
12
12
|
* обычный запуск, а не ошибка пользователя. */
|
|
13
13
|
|
|
14
|
+
/* Запись файла с созданием каталога: `--write .size-report/report.html` в свежем
|
|
15
|
+
* проекте — обычный запуск, а не ошибка пользователя. Тем же путём пишутся
|
|
16
|
+
* страница (`--page`) и черновик настроек (`--init`), поэтому он один на пакет. */
|
|
17
|
+
export function writeFileEnsured(file, text) {
|
|
18
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
19
|
+
fs.writeFileSync(file, text);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export function rebuild(cfg, root) {
|
|
15
23
|
const { rows, dropped, state } = build(cfg, root);
|
|
16
24
|
const html = render(rows, cfg);
|
|
17
25
|
const file = path.join(root, cfg.output);
|
|
18
|
-
|
|
19
|
-
fs.writeFileSync(file, html);
|
|
26
|
+
writeFileEnsured(file, html);
|
|
20
27
|
return { rows: rows, dropped: dropped, state: state, html: html, file: file };
|
|
21
28
|
}
|
package/src/check.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assertFullHistory, readHistory } from './git.js';
|
|
2
2
|
import { measureHistory } from './history.js';
|
|
3
3
|
import { sensorGaps } from './metrics.js';
|
|
4
|
-
import {
|
|
4
|
+
import { outsideFix, pathRoles } from './config.js';
|
|
5
5
|
import { cliCommand } from './refusal.js';
|
|
6
6
|
|
|
7
7
|
/* Полнота покрытия — ответ на вопрос «всё ли в истории попало в отчёт». Правило
|
|
@@ -46,19 +46,17 @@ function short(sha) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/* Пути, тронутые историей, разложенные на три части: отслеживаемые колонками,
|
|
49
|
-
* исключённые объявлением и незнакомые.
|
|
50
|
-
*
|
|
51
|
-
* (переименование), и любой из них её и есть. */
|
|
49
|
+
* исключённые объявлением и незнакомые. Само суждение — одно и живёт в настройках
|
|
50
|
+
* (`pathRoles`), а здесь только его применение ко всей истории. */
|
|
52
51
|
function pathCoverage(cfg, commits) {
|
|
53
|
-
const
|
|
54
|
-
cfg.columns.forEach((col) => col.paths.forEach((p) => tracked.add(p)));
|
|
55
|
-
const excluded = new Set([cfg.output].concat(cfg.skip || []));
|
|
52
|
+
const role = pathRoles(cfg);
|
|
56
53
|
const seen = { covered: new Set(), excluded: new Set() };
|
|
57
54
|
const unknown = new Map();
|
|
58
55
|
commits.forEach((c) => {
|
|
59
56
|
c.files.forEach((f) => {
|
|
60
|
-
|
|
61
|
-
if (
|
|
57
|
+
const kind = role(f);
|
|
58
|
+
if (kind === 'columns') { seen.covered.add(f); return; }
|
|
59
|
+
if (kind === 'excluded') { seen.excluded.add(f); return; }
|
|
62
60
|
/* Коммит, заведший путь, — первый по истории (порядок чтения — от старых к
|
|
63
61
|
* новым), и показывается он человеку как улика: по нему видно, чья это была
|
|
64
62
|
* правка. */
|
|
@@ -121,7 +119,7 @@ export function coverageText(rep) {
|
|
|
121
119
|
lines.push(' ' + u.path + ' — с ' + short(u.since) + ' «' + u.subject.slice(0, 60) + '»');
|
|
122
120
|
});
|
|
123
121
|
if (unknown.length > SHOW) lines.push(' … ещё ' + (unknown.length - SHOW));
|
|
124
|
-
lines.push(' починка:
|
|
122
|
+
lines.push(' починка: ' + outsideFix(unknown.map((u) => u.path))
|
|
125
123
|
+ '; черновик колонок по расширениям даёт ' + cliCommand('--init draft.json'));
|
|
126
124
|
} else {
|
|
127
125
|
lines.push('✓ покрытие: ' + rep.history.commits + ' коммитов истории, ' + rep.history.rows
|