@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/src/config.js
CHANGED
|
@@ -7,6 +7,7 @@ import { LOCALES } from './locales.js';
|
|
|
7
7
|
import { METRICS, MINIFY_ENGINES } from './metrics.js';
|
|
8
8
|
import { TOKEN_DEFAULTS, TOKEN_FAMILIES } from './tokens.js';
|
|
9
9
|
import { CATEGORY_ORDER } from './data.js';
|
|
10
|
+
import { projectConfig } from './project.js';
|
|
10
11
|
|
|
11
12
|
/* Настройки проекта-потребителя: значения по умолчанию, чтение и проверка.
|
|
12
13
|
* Настройки описывают проект, а не механику, поэтому проверка стоит здесь же и
|
|
@@ -70,6 +71,41 @@ export function gitRoot() {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/* Настроек нет — их выводит сам проект (`src/project.js`), и работа начинается сразу:
|
|
75
|
+
* заводить файл ради первого запуска незачем, а `--init` закрепляет выведенное
|
|
76
|
+
* файлом, когда его хотят править. Так бывает только с умолчательным именем: файл,
|
|
77
|
+
* названный ключом `--config`, — это уже запрос про конкретный файл, и его
|
|
78
|
+
* отсутствие остаётся отказом (иначе опечатка в пути молча дала бы чужие настройки).
|
|
79
|
+
* `path` назван словами, а не путём: файла нет, и текст «правьте <путь>» привёл бы
|
|
80
|
+
* человека к тому, чего в проекте не лежит. */
|
|
81
|
+
export function derivedConfig(root) {
|
|
82
|
+
const cfg = derivedProfile(root);
|
|
83
|
+
cfg.path = 'настройки, выведенные из проекта';
|
|
84
|
+
cfg.derived = true;
|
|
85
|
+
validateConfig(cfg);
|
|
86
|
+
return cfg;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Выведенное из проекта + умолчания — то, чем проект работает без файла, и то,
|
|
90
|
+
* что закрепляет `--init`. Одно место на две роли (иначе «файл» и «работа без
|
|
91
|
+
* файла» разошлись бы колонкой или числом), а сам вывод (`src/project.js`) о
|
|
92
|
+
* умолчаниях не знает: он говорит только то, что видит в проекте. */
|
|
93
|
+
export function derivedProfile(root) {
|
|
94
|
+
return withDefaults(projectConfig(root));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Настройки поверх умолчаний — одним местом на два источника (файл и проект):
|
|
98
|
+
* вложенное досыпается по ключам, потому что `minify: {engine: …}` не значит «у
|
|
99
|
+
* `minify` больше нет других полей», а значило бы, что снятие балласта потеряло
|
|
100
|
+
* список расширений. */
|
|
101
|
+
function withDefaults(raw) {
|
|
102
|
+
const cfg = Object.assign({}, DEFAULT_CONFIG, raw);
|
|
103
|
+
['minify', 'tokens', 'hooks', 'links', 'rows'].forEach((key) => {
|
|
104
|
+
cfg[key] = Object.assign({}, DEFAULT_CONFIG[key], raw[key]);
|
|
105
|
+
});
|
|
106
|
+
return cfg;
|
|
107
|
+
}
|
|
108
|
+
|
|
73
109
|
/* Настройки читаются как есть и досыпаются значениями по умолчанию: у проекта,
|
|
74
110
|
* который только подключил генератор, конфиг может быть в три строки. */
|
|
75
111
|
export function loadConfig(file, root) {
|
|
@@ -78,8 +114,10 @@ export function loadConfig(file, root) {
|
|
|
78
114
|
// черновик под умолчательным именем в корне проекта — то есть починил бы не то,
|
|
79
115
|
// о чём спросили. Имя не называем ровно тогда, когда оно и так умолчательное.
|
|
80
116
|
const dflt = root !== undefined && path.resolve(root, CONFIG_NAME) === path.resolve(file);
|
|
117
|
+
if (dflt) return derivedConfig(root);
|
|
81
118
|
refuseCause('нет файла настроек', 'нет файла настроек ' + file
|
|
82
|
-
+ '\n создайте его: ' + cliCommand('--init
|
|
119
|
+
+ '\n создайте его: ' + cliCommand('--init ' + advicePath(file))
|
|
120
|
+
+ '\n смотрите: без «--config» настройки не нужны — они выводятся из проекта');
|
|
83
121
|
}
|
|
84
122
|
let raw;
|
|
85
123
|
try {
|
|
@@ -88,26 +126,19 @@ export function loadConfig(file, root) {
|
|
|
88
126
|
refuseCause('настройки не разобраны', 'не разобран ' + file + ': ' + e.message
|
|
89
127
|
+ '\n починка: правьте ' + file + '; образец настроек даёт ' + cliCommand('--init') + ' в пустом каталоге');
|
|
90
128
|
}
|
|
91
|
-
const cfg =
|
|
92
|
-
cfg.minify = Object.assign({}, DEFAULT_CONFIG.minify, raw.minify);
|
|
93
|
-
cfg.tokens = Object.assign({}, DEFAULT_CONFIG.tokens, raw.tokens);
|
|
94
|
-
cfg.hooks = Object.assign({}, DEFAULT_CONFIG.hooks, raw.hooks);
|
|
95
|
-
cfg.links = Object.assign({}, DEFAULT_CONFIG.links, raw.links);
|
|
96
|
-
cfg.rows = Object.assign({}, DEFAULT_CONFIG.rows, raw.rows);
|
|
129
|
+
const cfg = withDefaults(raw);
|
|
97
130
|
cfg.path = file;
|
|
98
131
|
validateConfig(cfg);
|
|
99
132
|
return cfg;
|
|
100
133
|
}
|
|
101
134
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
/* Колонки: метка и пути — имена, а не что попало: путь числом или объектом молча не
|
|
136
|
+
* совпадает ни с чем, и колонка отчитывается нулём строк за успех. Отказ обязан
|
|
137
|
+
* случиться здесь, а не превратиться в пустой отчёт. */
|
|
138
|
+
function checkColumns(cfg, fail) {
|
|
105
139
|
if (!cfg.columns || cfg.columns.length === 0) fail('не задано ни одной колонки (columns)');
|
|
106
140
|
const labels = new Set();
|
|
107
141
|
cfg.columns.forEach((c, i) => {
|
|
108
|
-
// Метка и пути — имена, а не что попало: путь числом или объектом молча не
|
|
109
|
-
// совпадает ни с чем, и колонка отчитывается нулём строк за успех. Отказ
|
|
110
|
-
// обязан случиться здесь, а не превратиться в пустой отчёт.
|
|
111
142
|
const pathsAreNames = c && Array.isArray(c.paths)
|
|
112
143
|
&& c.paths.length > 0 && c.paths.every((p) => typeof p === 'string' && p !== '');
|
|
113
144
|
if (!c || typeof c.label !== 'string' || c.label === '' || !pathsAreNames) {
|
|
@@ -122,13 +153,22 @@ export function validateConfig(cfg) {
|
|
|
122
153
|
}
|
|
123
154
|
labels.add(c.label);
|
|
124
155
|
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function checkMetrics(cfg, fail) {
|
|
125
159
|
if (!Array.isArray(cfg.metrics) || cfg.metrics.length === 0) fail('не заданы метрики (metrics)');
|
|
126
160
|
cfg.metrics.forEach((m) => {
|
|
127
161
|
if (!METRICS[m]) fail('неизвестная метрика «' + m + '» (есть: ' + Object.keys(METRICS).join(', ') + ')');
|
|
128
162
|
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function checkMinify(cfg, fail) {
|
|
129
166
|
if (MINIFY_ENGINES.indexOf(cfg.minify.engine) < 0) {
|
|
130
167
|
fail('неизвестный способ минификации «' + cfg.minify.engine + '» (есть: ' + MINIFY_ENGINES.join(', ') + ')');
|
|
131
168
|
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function checkTokens(cfg, fail) {
|
|
132
172
|
const family = TOKEN_FAMILIES[cfg.tokens.family];
|
|
133
173
|
if (family === undefined) {
|
|
134
174
|
fail('неизвестное семейство токенизатора «' + cfg.tokens.family + '» (есть: '
|
|
@@ -138,21 +178,60 @@ export function validateConfig(cfg) {
|
|
|
138
178
|
fail('неизвестная кодировка токенизатора «' + cfg.tokens.encoding + '» у семейства '
|
|
139
179
|
+ cfg.tokens.family + ' (есть: ' + family.encodings.join(', ') + ')');
|
|
140
180
|
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Файл таблицы не может быть её колонкой: размер артефакта зависит от числа строк,
|
|
184
|
+
* то есть от самого себя. */
|
|
185
|
+
function checkOutput(cfg, fail) {
|
|
186
|
+
cfg.columns.forEach((c) => {
|
|
187
|
+
if (c.paths.indexOf(cfg.output) >= 0) fail('файл таблицы (' + cfg.output + ') не может быть колонкой');
|
|
188
|
+
});
|
|
189
|
+
if (!cfg.output) fail('не задан output');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function checkJournal(cfg, fail) {
|
|
193
|
+
if (!cfg.journal) return;
|
|
194
|
+
if (!cfg.journal.path) fail('journal.path не задан');
|
|
195
|
+
if (!cfg.journal.pattern) fail('journal.pattern не задан');
|
|
196
|
+
try { new RegExp(cfg.journal.pattern); } catch (e) { fail('journal.pattern не компилируется: ' + e.message); }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Что настройки говорят про путь: `columns` — его отслеживает колонка, `excluded` — он
|
|
200
|
+
* объявлен исключением (`skip` и сам файл отчёта), `outside` — мимо того и другого.
|
|
201
|
+
* Суждение одно на два ответа: `check` спрашивает его про всю историю, `explain` — про
|
|
202
|
+
* один коммит. Знакомство считается по колонкам целиком, а не по метке: у колонки путей
|
|
203
|
+
* может быть несколько (переименование), и любой из них — она сама. */
|
|
204
|
+
export function pathRoles(cfg) {
|
|
205
|
+
const tracked = new Set();
|
|
206
|
+
cfg.columns.forEach((col) => col.paths.forEach((p) => tracked.add(p)));
|
|
207
|
+
const excluded = new Set([cfg.output].concat(cfg.skip || []));
|
|
208
|
+
return (file) => {
|
|
209
|
+
if (tracked.has(file)) return 'columns';
|
|
210
|
+
return excluded.has(file) ? 'excluded' : 'outside';
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Починка «мимо колонок» — один текст на два ответа, и он называет пути: команда без
|
|
215
|
+
* имён не команда. Текст собирается из имён, а не приписывает их по ветке, поэтому и
|
|
216
|
+
* предусматривать тут нечего: коммит без файлов вовсе починки не получает — на пустом
|
|
217
|
+
* списке её не зовут (см. `fixFor` в `src/explain.js`). */
|
|
218
|
+
export function outsideFix(paths) {
|
|
219
|
+
return 'допишите эти пути колонкой или в «skip» файла ' + CONFIG_NAME + ': ' + paths.join(', ');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function validateConfig(cfg) {
|
|
223
|
+
const fail = (msg) => refuseCause('настройки неверны',
|
|
224
|
+
'конфиг ' + cfg.path + ': ' + msg + '\n починка: правьте ' + cfg.path);
|
|
225
|
+
checkColumns(cfg, fail);
|
|
226
|
+
checkMetrics(cfg, fail);
|
|
227
|
+
checkMinify(cfg, fail);
|
|
228
|
+
checkTokens(cfg, fail);
|
|
141
229
|
if (!LOCALES[cfg.locale]) fail('неизвестная локаль «' + cfg.locale + '» (есть: ' + Object.keys(LOCALES).join(', ') + ')');
|
|
142
230
|
// Выключатель хука — «да/нет», а не «правда/ложь»: `false` от случайной строки
|
|
143
231
|
// отличать обязан инструмент, иначе выключенная автоматика осталась бы включённой.
|
|
144
232
|
if (typeof cfg.hooks.enabled !== 'boolean') {
|
|
145
233
|
fail('hooks.enabled — не «да/нет»: ' + JSON.stringify(cfg.hooks.enabled));
|
|
146
234
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
cfg.columns.forEach((c) => {
|
|
150
|
-
if (c.paths.indexOf(cfg.output) >= 0) fail('файл таблицы (' + cfg.output + ') не может быть колонкой');
|
|
151
|
-
});
|
|
152
|
-
if (!cfg.output) fail('не задан output');
|
|
153
|
-
if (cfg.journal) {
|
|
154
|
-
if (!cfg.journal.path) fail('journal.path не задан');
|
|
155
|
-
if (!cfg.journal.pattern) fail('journal.pattern не задан');
|
|
156
|
-
try { new RegExp(cfg.journal.pattern); } catch (e) { fail('journal.pattern не компилируется: ' + e.message); }
|
|
157
|
-
}
|
|
235
|
+
checkOutput(cfg, fail);
|
|
236
|
+
checkJournal(cfg, fail);
|
|
158
237
|
}
|
package/src/data.js
CHANGED
|
@@ -66,14 +66,8 @@ export function reportData(cfg, root) {
|
|
|
66
66
|
categories: CATEGORY_ORDER.filter((key) => files.some((f) => f.category === key))
|
|
67
67
|
.map((key) => ({ key: key, label: loc.categories[key] })),
|
|
68
68
|
files: files,
|
|
69
|
-
rows: rows.map((r) => (
|
|
70
|
-
|
|
71
|
-
when: r.when,
|
|
72
|
-
subject: r.subject,
|
|
73
|
-
section: r.section === null ? null : { id: r.section.id, head: r.section.head, added: r.section.added },
|
|
74
|
-
href: rowHref(r.section, r.sha, cfg),
|
|
75
|
-
values: r.cells
|
|
76
|
-
})),
|
|
69
|
+
rows: rows.map((r) => Object.assign(rowShape(r),
|
|
70
|
+
{ href: rowHref(r.section, r.sha, cfg), values: r.cells })),
|
|
77
71
|
now: state.map((s) => (s === null ? null : s.cells)),
|
|
78
72
|
approx: approxMarks(rows, state, cfg),
|
|
79
73
|
skipped: dropped.map(skipLine)
|
|
@@ -99,7 +93,20 @@ function approxMarks(rows, state, cfg) {
|
|
|
99
93
|
return out;
|
|
100
94
|
}
|
|
101
95
|
|
|
96
|
+
/* Общая часть строки ответа: она есть и у контракта страницы (`--data`), и у
|
|
97
|
+
* прежней формы `--json` (она заморожена эталоном паритета). Одно место — потому
|
|
98
|
+
* что разойтись эти два ответа могут ровно здесь, а **порядок полей и есть байты
|
|
99
|
+
* ответа**: они в объекте в том же порядке, в каком их печатает замороженная форма. */
|
|
100
|
+
export function rowShape(r) {
|
|
101
|
+
return {
|
|
102
|
+
sha: r.sha,
|
|
103
|
+
when: r.when,
|
|
104
|
+
subject: r.subject,
|
|
105
|
+
section: r.section === null ? null : { id: r.section.id, head: r.section.head, added: r.section.added }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
102
109
|
/* Производные величины живут в `src/derived.js`: их считает и артефакт (импорт
|
|
103
110
|
* ниже), и страница (получает тот же файл текстом). Второго расчёта той же
|
|
104
111
|
* таблицы нет вовсе, поэтому разойтись молча двум отчётам нечем — это стережёт
|
|
105
|
-
* `test/contract.test.js`. */
|
|
112
|
+
* `test/contract-derived.test.js`. */
|
package/src/derived.js
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* непонятая строка не должна молча попасть в страницу (`pageScript`).
|
|
13
13
|
*
|
|
14
14
|
* Расхождение двух отчётов возможно только здесь, поэтому и стеречь его надо
|
|
15
|
-
* здесь: `test/contract.test.js` сверяет числа страницы с числами
|
|
16
|
-
* следит, чтобы у страницы не появилось
|
|
15
|
+
* здесь: `test/contract-derived.test.js` сверяет числа страницы с числами
|
|
16
|
+
* артефакта, а `test/page-view.test.js` следит, чтобы у страницы не появилось
|
|
17
|
+
* своего расчёта. */
|
|
17
18
|
|
|
18
19
|
// Разряды тонкими пробелами: toLocaleString зависит от ICU сборки Node, а строка
|
|
19
20
|
// таблицы обязана совпадать побайтово на любой машине.
|
package/src/doctor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadConfig } from './config.js';
|
|
|
4
4
|
import { TOOL_PKG } from './tool.js';
|
|
5
5
|
import { coverage, coverageText } from './check.js';
|
|
6
6
|
import { hookStatus } from './hook.js';
|
|
7
|
+
import { derivedSummary } from './project.js';
|
|
7
8
|
import { minifier } from './minify.js';
|
|
8
9
|
import { tokenizer } from './tokens.js';
|
|
9
10
|
|
|
@@ -15,13 +16,10 @@ import { tokenizer } from './tokens.js';
|
|
|
15
16
|
*
|
|
16
17
|
* Правило ответа: `ok` значит «делать нечего», а у находки назван уровень.
|
|
17
18
|
* `action` — что-то надо сделать (и, где возможно, названа команда починки);
|
|
18
|
-
* `note` — наблюдение: знать полезно, делать нечего. Код выхода
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* потом честная оговорка о счёте. Хук в этот порядок не входит: отчёт собирается
|
|
23
|
-
* и без него, поэтому сломанный хук — находка без своего кода (вердикт `ok` при
|
|
24
|
-
* этом всё равно «есть дело»).
|
|
19
|
+
* `note` — наблюдение: знать полезно, делать нечего. Код выхода считает один
|
|
20
|
+
* `verdictOf` в конце: шаги чтения только называют вид обстоятельства (`troubles`),
|
|
21
|
+
* а и порядок видов, и код каждого — один список `WEIGHT`. Своего кода у шага нет,
|
|
22
|
+
* поэтому разойтись эти два ответа не могут.
|
|
25
23
|
*
|
|
26
24
|
* Чего ответ не делает: не говорит, «правильно» ли выбраны колонки (это знает
|
|
27
25
|
* проект), и не угадывает там, где данных нет, — отсутствие ответа называется
|
|
@@ -81,85 +79,142 @@ function dependencies(cfg) {
|
|
|
81
79
|
];
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
coverage: null,
|
|
94
|
-
findings: [],
|
|
95
|
-
exit: EXIT.OK
|
|
96
|
-
};
|
|
97
|
-
let cfg = null;
|
|
82
|
+
/* Настройки: при нечитаемых ответ честно неполон (покрытие считать нечем), а
|
|
83
|
+
* причина — не отказ, а находка: диагностика затем и нужна, чтобы назвать причину
|
|
84
|
+
* и починку, — их и несёт текст отказа. Вес обстоятельства шаг только **называет**
|
|
85
|
+
* (`troubles`), а важнее оно или нет — не его дело: решает `verdictOf`.
|
|
86
|
+
*
|
|
87
|
+
* Настроек, выведенных из проекта, тут не обстоятельство, а наблюдение: проект
|
|
88
|
+
* работает, но числа его отчёта зависят от того, что инструмент о нём угадал, —
|
|
89
|
+
* поэтому находка уровня `note` (код выхода она не несёт) и поле `derived` в ответе. */
|
|
90
|
+
function readConfig(root, configFile) {
|
|
98
91
|
try {
|
|
99
|
-
cfg = loadConfig(configFile, root);
|
|
100
|
-
|
|
92
|
+
const cfg = loadConfig(configFile, root);
|
|
93
|
+
const derived = cfg.derived === true;
|
|
94
|
+
return {
|
|
95
|
+
cfg: cfg,
|
|
96
|
+
report: {
|
|
97
|
+
file: configFile, ok: true, derived: derived,
|
|
98
|
+
columns: cfg.columns.length, metrics: cfg.metrics
|
|
99
|
+
},
|
|
100
|
+
findings: derived
|
|
101
|
+
? [{ level: 'note', what: derivedSummary(cfg), fix: 'закрепите их файлом: ' + cliCommand('--init') }]
|
|
102
|
+
: [],
|
|
103
|
+
troubles: []
|
|
104
|
+
};
|
|
101
105
|
} catch (e) {
|
|
102
|
-
// Отказ настроек здесь не отказ, а находка: диагностика затем и нужна, чтобы
|
|
103
|
-
// назвать причину и починку, — их и несёт текст отказа.
|
|
104
106
|
if (!(e instanceof Refusal)) throw e;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
return {
|
|
108
|
+
cfg: null,
|
|
109
|
+
report: { file: configFile, ok: false, problem: e.message },
|
|
110
|
+
findings: [{ level: 'action', what: e.message }],
|
|
111
|
+
troubles: ['config']
|
|
112
|
+
};
|
|
108
113
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Хук: две находки, у каждой своя починка. Веса у них нет и своего кода выхода тоже —
|
|
117
|
+
* отчёт собирается и без хука, поэтому сломанный хук меняет только вердикт `ok`. */
|
|
118
|
+
function hookFindings(hooks) {
|
|
119
|
+
const found = [];
|
|
120
|
+
if (!hooks.installed) return found;
|
|
121
|
+
if (hooks.enabled === false) {
|
|
122
|
+
found.push({
|
|
113
123
|
level: 'action',
|
|
114
124
|
what: 'хук установлен, но автоматика выключена настройкой hooks.enabled: отчёт обновляется руками',
|
|
115
125
|
fix: 'верните «"hooks": {"enabled": true}» в файл настроек или снимите хук: ' + cliCommand('uninstall-hook')
|
|
116
126
|
});
|
|
117
127
|
}
|
|
118
|
-
if (
|
|
119
|
-
|
|
128
|
+
if (hooks.last !== null && HOOK_BAD.indexOf(hooks.last.result) >= 0) {
|
|
129
|
+
found.push({
|
|
120
130
|
level: 'action',
|
|
121
|
-
what: 'хук: последний запуск не пересобрал отчёт — ' +
|
|
131
|
+
what: 'хук: последний запуск не пересобрал отчёт — ' + hooks.last.why,
|
|
122
132
|
fix: 'починьте то, на что жалуется причина, и пересоберите отчёт: ' + cliCommand('--write')
|
|
123
133
|
});
|
|
124
134
|
}
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Покрытие — тот же ответ, что даёт `size check`, плюс вид обстоятельства, если оно есть:
|
|
139
|
+
* неполнота пути или приближение датчика (вес у видов разный — `WEIGHT`); неполнота
|
|
140
|
+
* старше, потому что без неё чисел нет вовсе.
|
|
141
|
+
* Что попадает в находки, а что нет: в отчёте целиком стоит блок покрытия (тот же текст,
|
|
142
|
+
* что у `size check`), поэтому неполнота второй раз не пересказывается — она весит. А по
|
|
143
|
+
* датчикам находка есть: `size check` печатает их строкой `!`, здесь они часть ответа. */
|
|
144
|
+
function readCoverage(cfg, root, configFile) {
|
|
125
145
|
if (cfg === null) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
if (!(e instanceof Refusal)) throw e;
|
|
135
|
-
rep.findings.push({ level: 'action', what: e.message });
|
|
136
|
-
rep.exit = e.code;
|
|
137
|
-
}
|
|
146
|
+
return {
|
|
147
|
+
report: null,
|
|
148
|
+
findings: [{
|
|
149
|
+
level: 'note',
|
|
150
|
+
what: 'покрытие не считалось: настройки нечитаемы — почините их и спросите снова'
|
|
151
|
+
}],
|
|
152
|
+
troubles: []
|
|
153
|
+
};
|
|
138
154
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
try {
|
|
156
|
+
const report = coverage(cfg, root, configFile);
|
|
157
|
+
return {
|
|
158
|
+
report: report,
|
|
159
|
+
findings: report.sensors.map((gap) => ({ level: 'action', what: gap.why, fix: gap.fix })),
|
|
160
|
+
troubles: report.ok ? (report.sensors.length > 0 ? ['sensor'] : []) : ['coverage']
|
|
161
|
+
};
|
|
162
|
+
} catch (e) {
|
|
163
|
+
if (!(e instanceof Refusal)) throw e;
|
|
164
|
+
/* Внутри покрытия отказывают двое, и род у них разный: обрезанной истории —
|
|
165
|
+
* свой вид (`assertFullHistory`), а неразобранному файлу — настройки: числа
|
|
166
|
+
* нет из-за них, и починка у него настройками же. Вид выбирается по коду
|
|
167
|
+
* отказа — эти два кода и есть весь выбор. */
|
|
168
|
+
return {
|
|
169
|
+
report: null,
|
|
170
|
+
findings: [{ level: 'action', what: e.message }],
|
|
171
|
+
troubles: [e.code === EXIT.SHALLOW ? 'history' : 'config']
|
|
172
|
+
};
|
|
153
173
|
}
|
|
174
|
+
}
|
|
154
175
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
176
|
+
/* Вес обстоятельств — вот и весь порядок важности, и он один на весь модуль: ключи идут
|
|
177
|
+
* по важности, значения — код каждого вида. Код выхода берётся у самого важного из
|
|
178
|
+
* найденного, а не у того, что нашлось позже: сначала чем считать нечем (настройки,
|
|
179
|
+
* история), потом неполное покрытие, потом оговорка о счёте.
|
|
180
|
+
* Хук в список не входит: отчёт собирается и без него (см. `hookFindings`). */
|
|
181
|
+
const WEIGHT = {
|
|
182
|
+
config: EXIT.CONFIG,
|
|
183
|
+
history: EXIT.SHALLOW,
|
|
184
|
+
coverage: EXIT.VIOLATION,
|
|
185
|
+
sensor: EXIT.SENSOR
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/* Вердикт — одно место, где обстоятельства превращаются в код выхода и в `ok`.
|
|
189
|
+
* «Делать нечего» — это ни одной находки-действия и сосчитанное полное покрытие: без
|
|
190
|
+
* покрытия вердикта нет, потому что считать больше нечего (см. заметку в `readCoverage`). */
|
|
191
|
+
function verdictOf(rep, troubles) {
|
|
192
|
+
const found = Object.keys(WEIGHT).filter((kind) => troubles.indexOf(kind) >= 0);
|
|
193
|
+
rep.exit = found.length === 0 ? EXIT.OK : WEIGHT[found[0]];
|
|
158
194
|
rep.ok = rep.coverage !== null && rep.coverage.ok
|
|
159
195
|
&& !rep.findings.some((f) => f.level === 'action');
|
|
160
196
|
return rep;
|
|
161
197
|
}
|
|
162
198
|
|
|
199
|
+
export function doctor(root, configFile) {
|
|
200
|
+
const config = readConfig(root, configFile);
|
|
201
|
+
const hooks = hooksReport(root, config.cfg);
|
|
202
|
+
const cov = readCoverage(config.cfg, root, configFile);
|
|
203
|
+
const rep = {
|
|
204
|
+
schema: 1,
|
|
205
|
+
ok: false,
|
|
206
|
+
tool: { name: TOOL_PKG.name, version: TOOL_PKG.version },
|
|
207
|
+
environment: environment(root),
|
|
208
|
+
config: config.report,
|
|
209
|
+
dependencies: dependencies(config.cfg),
|
|
210
|
+
hooks: hooks,
|
|
211
|
+
coverage: cov.report,
|
|
212
|
+
findings: config.findings.concat(hookFindings(hooks), cov.findings),
|
|
213
|
+
exit: EXIT.OK
|
|
214
|
+
};
|
|
215
|
+
return verdictOf(rep, config.troubles.concat(cov.troubles));
|
|
216
|
+
}
|
|
217
|
+
|
|
163
218
|
/* Итог последнего запуска хука словами: по нему человек понимает, что произошло
|
|
164
219
|
* после коммита, не заглядывая в `.git`. */
|
|
165
220
|
const HOOK_RESULT = {
|
|
@@ -210,7 +265,8 @@ export function doctorText(rep) {
|
|
|
210
265
|
lines.push(' git читается с закреплениями: ' + env.pins.join(', ') + '; локаль ' + env.locale
|
|
211
266
|
+ ' (настройки машины на числа не влияют)');
|
|
212
267
|
lines.push(' настройки: ' + (rep.config.ok
|
|
213
|
-
? rep.config.
|
|
268
|
+
? (rep.config.derived ? 'выводятся из проекта (файла нет)' : rep.config.file)
|
|
269
|
+
+ ' — ' + rep.config.columns + ' колонок, метрики ' + rep.config.metrics.join(' ')
|
|
214
270
|
: rep.config.file + ' — нечитаемы'));
|
|
215
271
|
lines.push(' зависимости: ' + rep.dependencies.map((d) => d.name
|
|
216
272
|
+ (d.present === null ? ' — ' + d.note : d.present ? ' ' + d.version + ' есть' : ' нет')
|
package/src/explain.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assertFullHistory, readHistory, resolveCommit } from './git.js';
|
|
2
2
|
import { measureHistory } from './history.js';
|
|
3
3
|
import { cliCommand, refuseCause } from './refusal.js';
|
|
4
|
-
import {
|
|
4
|
+
import { outsideFix, pathRoles } from './config.js';
|
|
5
5
|
|
|
6
6
|
/* Почему у коммита нет строки — ответ на конкретный вопрос про конкретный коммит.
|
|
7
7
|
*
|
|
@@ -21,12 +21,10 @@ const REASON_TEXT = {
|
|
|
21
21
|
flat: 'числа не сдвинулись: файлы колонок тронуты, а объём не изменился'
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* списку коммитов: так у неоднозначного префикса остаётся человеческий отказ
|
|
29
|
-
* со списком подходящих, а у имени — правила git, а не наши. */
|
|
24
|
+
/* Коммит по названию. Имя ревизии разрешает git, и только если имени нет — ищем
|
|
25
|
+
* начало sha по списку коммитов: так у неоднозначного префикса остаётся
|
|
26
|
+
* человеческий отказ со списком подходящих, а у имени — правила git, а не наши. */
|
|
27
|
+
function lookup(root, commits, target) {
|
|
30
28
|
const resolved = resolveCommit(root, String(target));
|
|
31
29
|
const needle = String(target).toLowerCase();
|
|
32
30
|
const found = resolved === null
|
|
@@ -50,33 +48,52 @@ export function explainCommit(cfg, root, target) {
|
|
|
50
48
|
+ '\n ' + found.slice(0, 5).map((c) => c.sha.slice(0, 7) + ' ' + c.subject).join('\n ')
|
|
51
49
|
+ '\n починка: назовите больше знаков');
|
|
52
50
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const row = measured.rows.findIndex((r) => r.sha === c.sha);
|
|
56
|
-
const dropped = measured.dropped.find((d) => d.sha === c.sha);
|
|
51
|
+
return found[0];
|
|
52
|
+
}
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
/* Улики: что коммит тронул — колонки, исключённое, мимо колонок. Суждение о роли пути
|
|
55
|
+
* — одно и живёт в настройках (`pathRoles`), здесь только раскладка его ответа. */
|
|
56
|
+
function touchedOf(cfg, files) {
|
|
57
|
+
const role = pathRoles(cfg);
|
|
61
58
|
const touched = { columns: [], excluded: [], untracked: [] };
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
59
|
+
const into = { columns: touched.columns, excluded: touched.excluded, outside: touched.untracked };
|
|
60
|
+
files.forEach((f) => {
|
|
61
|
+
const bucket = into[role(f)];
|
|
62
|
+
if (bucket.indexOf(f) < 0) bucket.push(f);
|
|
66
63
|
});
|
|
64
|
+
return touched;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const FIX = {
|
|
68
|
+
merge: 'включите строки слияний: "rows": { "merges": true }',
|
|
69
|
+
report: 'не требуется: строка про коммит не может лежать внутри самого коммита — обновляйте отчёт отдельным коммитом',
|
|
70
|
+
flat: 'не требуется: числа не изменились — строка без единого числа читалась бы как поломка'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/* Починка по причине. У «мимо колонок» она одна на два ответа (`outsideFix`) и называет
|
|
74
|
+
* пути. У коммита без файлов (`--allow-empty`) называть нечего — значит, починки нет
|
|
75
|
+
* вовсе, а не команда без имён; этот случай и держит ветка. */
|
|
76
|
+
function fixFor(reason, touched) {
|
|
77
|
+
if (reason === 'outside') {
|
|
78
|
+
return touched.untracked.length === 0 ? null : outsideFix(touched.untracked);
|
|
79
|
+
}
|
|
80
|
+
return FIX[reason];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function explainCommit(cfg, root, target) {
|
|
84
|
+
assertFullHistory(root);
|
|
85
|
+
const commits = readHistory(root);
|
|
86
|
+
const c = lookup(root, commits, target);
|
|
87
|
+
const measured = measureHistory(cfg, root, commits);
|
|
88
|
+
const row = measured.rows.findIndex((r) => r.sha === c.sha);
|
|
89
|
+
const dropped = measured.dropped.find((d) => d.sha === c.sha);
|
|
90
|
+
const touched = touchedOf(cfg, c.files);
|
|
67
91
|
|
|
68
92
|
/* Разница, которой нет в строке отчёта: «без изменения объёма» у коммита мимо
|
|
69
93
|
* колонок означает не то же самое, что у коммита, тронувшего колонку. */
|
|
70
94
|
let reason = row >= 0 ? null : dropped.reason;
|
|
71
95
|
if (reason === 'flat' && touched.columns.length === 0) reason = 'outside';
|
|
72
|
-
|
|
73
|
-
const fix = {
|
|
74
|
-
merge: 'включите строки слияний: "rows": { "merges": true }',
|
|
75
|
-
report: 'не требуется: строка про коммит не может лежать внутри самого коммита — обновляйте отчёт отдельным коммитом',
|
|
76
|
-
outside: 'допишите ' + (touched.untracked.length > 0 ? 'эти пути' : 'тронутые файлы')
|
|
77
|
-
+ ' колонкой или в «skip» файла ' + CONFIG_NAME,
|
|
78
|
-
flat: 'не требуется: числа не изменились — строка без единого числа читалась бы как поломка'
|
|
79
|
-
}[reason];
|
|
96
|
+
const fix = fixFor(reason, touched);
|
|
80
97
|
|
|
81
98
|
return {
|
|
82
99
|
schema: 1,
|