@vernikr/size-report 1.2.0 → 1.3.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/CHANGELOG.md +56 -0
- package/README.md +168 -67
- 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.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/strip.js
CHANGED
|
@@ -1,145 +1,25 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { moduleError } from './parse.js';
|
|
2
|
+
import { stripJs } from './strip/js.js';
|
|
3
|
+
import { compactJson, stripCss, stripHtml, stripLines } from './strip/forms.js';
|
|
5
4
|
|
|
6
|
-
/* Снятие балласта:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
/* Снятие балласта: правило, какая форма текста к какому файлу применяется.
|
|
6
|
+
* Только преобразование текста — ни истории, ни настроек этот модуль не знает.
|
|
7
|
+
*
|
|
8
|
+
* Разбор форм лежит рядом и по предметам: проход по JS (`strip/js.js`), формы
|
|
9
|
+
* разметки, стилей, строк и JSON (`strip/forms.js`) и гард компиляции
|
|
10
|
+
* (`strip/guard.js`). Здесь остаётся то, что связывает их с файлом: расширение,
|
|
11
|
+
* стратегия и что считать точным числом.
|
|
12
|
+
*
|
|
13
|
+
* Имена форм наружу отдаются отсюда же: точка входа пакета берёт их по одному
|
|
14
|
+
* адресу, и переезд разбора не должен быть виден тому, кто на них опирался. */
|
|
15
|
+
|
|
16
|
+
export { stripJs, stripCss, stripHtml, stripLines, compactJson };
|
|
17
|
+
export { assertCompilable } from './strip/guard.js';
|
|
9
18
|
|
|
10
19
|
export function byteLen(text) {
|
|
11
20
|
return Buffer.byteLength(text, 'utf8');
|
|
12
21
|
}
|
|
13
22
|
|
|
14
|
-
/* Снятие комментариев и отступов — метрика «объём без балласта», а не
|
|
15
|
-
* минификация: пробелы внутри строк и порядок токенов не трогаются (это позволит
|
|
16
|
-
* сравнивать числа между языками и не зависит от чужого инструмента, которого в
|
|
17
|
-
* проекте нет). Строки и шаблоны проходят насквозь, блочный комментарий
|
|
18
|
-
* заменяется пробелом, чтобы `a` и `b` из `a` + блочный комментарий + `b` не
|
|
19
|
-
* склеились в одно имя, перевод строки после `//` сохраняется — он разделяет
|
|
20
|
-
* токены. */
|
|
21
|
-
export function stripJs(src) {
|
|
22
|
-
let out = '';
|
|
23
|
-
let i = 0;
|
|
24
|
-
let last = ''; // последний значимый символ вывода: по нему решается, оператор «/» или регексп
|
|
25
|
-
let word = ''; // хвост последнего слова: после `return` идёт выражение, а не деление
|
|
26
|
-
while (i < src.length) {
|
|
27
|
-
const ch = src[i];
|
|
28
|
-
const next = src[i + 1];
|
|
29
|
-
// Комментарные пары проверяются до регекси: ни `/`, ни `*` не могут быть
|
|
30
|
-
// первым символом литерала регекспа, а вот `/*` в начале файла — обычное дело.
|
|
31
|
-
if (ch === '/' && next === '/') {
|
|
32
|
-
const nl = src.indexOf('\n', i);
|
|
33
|
-
i = nl === -1 ? src.length : nl;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (ch === '/' && next === '*') {
|
|
37
|
-
const end = src.indexOf('*/', i + 2);
|
|
38
|
-
out += ' ';
|
|
39
|
-
i = end === -1 ? src.length : end + 2;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (ch === '/' && regexAllowed(last, word)) {
|
|
43
|
-
const end = endOfRegex(src, i);
|
|
44
|
-
out += src.slice(i, end);
|
|
45
|
-
i = end;
|
|
46
|
-
last = '/';
|
|
47
|
-
word = '';
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
51
|
-
const end = endOfString(src, i, ch);
|
|
52
|
-
out += src.slice(i, end);
|
|
53
|
-
i = end;
|
|
54
|
-
last = ch;
|
|
55
|
-
word = '';
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
out += ch;
|
|
59
|
-
if (ch.trim() !== '') {
|
|
60
|
-
last = ch;
|
|
61
|
-
word = /[\w$]/.test(ch) ? word + ch : '';
|
|
62
|
-
}
|
|
63
|
-
i++;
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* Регексп начинается там, где ожидается операнд: после оператора, открывающей
|
|
69
|
-
* скобки или ключевого слова. Признак грубый, но его хватает: без него
|
|
70
|
-
* `replace(/\//g, …)` читалось бы как начало строчного комментария и резало
|
|
71
|
-
* строку (проверено гардом компиляции). */
|
|
72
|
-
const REGEX_KEYWORDS = ['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void', 'case', 'do', 'else', 'yield', 'await'];
|
|
73
|
-
|
|
74
|
-
function regexAllowed(last, word) {
|
|
75
|
-
if (last === '') return true;
|
|
76
|
-
if (REGEX_KEYWORDS.indexOf(word) !== -1) return true;
|
|
77
|
-
return '([{,;:=!&|?+-*%~^<>'.indexOf(last) !== -1;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function endOfRegex(src, start) {
|
|
81
|
-
let i = start + 1;
|
|
82
|
-
let inClass = false;
|
|
83
|
-
while (i < src.length) {
|
|
84
|
-
const ch = src[i];
|
|
85
|
-
if (ch === '\\') { i += 2; continue; }
|
|
86
|
-
if (ch === '\n') return start + 1; // наткнулись на строку — значит, это был не регексп
|
|
87
|
-
if (ch === '[') inClass = true;
|
|
88
|
-
else if (ch === ']') inClass = false;
|
|
89
|
-
else if (ch === '/' && !inClass) return i + 1;
|
|
90
|
-
i++;
|
|
91
|
-
}
|
|
92
|
-
return start + 1;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function endOfString(src, start, quote) {
|
|
96
|
-
let i = start + 1;
|
|
97
|
-
while (i < src.length) {
|
|
98
|
-
const ch = src[i];
|
|
99
|
-
if (ch === '\\') { i += 2; continue; }
|
|
100
|
-
if (ch === quote) return i + 1;
|
|
101
|
-
// В шаблоне `${…}` живёт выражение, а в нём — свои строки.
|
|
102
|
-
if (quote === '`' && ch === '$' && src[i + 1] === '{') { i = endOfTemplateExpr(src, i + 2); continue; }
|
|
103
|
-
i++;
|
|
104
|
-
}
|
|
105
|
-
return src.length;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function endOfTemplateExpr(src, start) {
|
|
109
|
-
let depth = 1;
|
|
110
|
-
let i = start;
|
|
111
|
-
while (i < src.length) {
|
|
112
|
-
const ch = src[i];
|
|
113
|
-
if (ch === '\\') { i += 2; continue; }
|
|
114
|
-
if (ch === '{') { depth++; i++; continue; }
|
|
115
|
-
if (ch === '}') { depth--; i++; if (depth === 0) return i; continue; }
|
|
116
|
-
if (ch === '"' || ch === "'" || ch === '`') { i = endOfString(src, i, ch); continue; }
|
|
117
|
-
i++;
|
|
118
|
-
}
|
|
119
|
-
return src.length;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function stripCss(src) {
|
|
123
|
-
return src.replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/* HTML: комментарии разметки (включая маркеры вклеек `<!--icon …-->` и
|
|
127
|
-
* `<!--/icon-->`), комментарии внутри <script> как JS и внутри <style> как CSS. */
|
|
128
|
-
export function stripHtml(src) {
|
|
129
|
-
return src
|
|
130
|
-
.replace(/<!--[\s\S]*?-->/g, '')
|
|
131
|
-
.replace(/(<script\b[^>]*>)([\s\S]*?)(<\/script>)/gi, (_m, open, body, close) => open + stripJs(body) + close)
|
|
132
|
-
.replace(/(<style\b[^>]*>)([\s\S]*?)(<\/style>)/gi, (_m, open, body, close) => open + stripCss(body) + close);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function stripLines(text) {
|
|
136
|
-
return text.split('\n').map((l) => l.trim()).filter((l) => l !== '').join('\n');
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function compactJson(text) {
|
|
140
|
-
try { return JSON.stringify(JSON.parse(text)); } catch (_e) { return stripLines(text); }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
23
|
/* Стратегия по расширению. Незнакомое расширение получает снятие отступов и
|
|
144
24
|
* пустых строк — безопасный минимум: снимать комментарии «на глаз» в синтаксисе,
|
|
145
25
|
* которого генератор не знает (например, `#` в YAML или отступы в Python),
|
|
@@ -164,6 +44,8 @@ export function strategyFor(file, cfg) {
|
|
|
164
44
|
return (cfg.minify.ext && cfg.minify.ext[ext]) || MINIFY_BY_EXT[ext] || 'strip-lines';
|
|
165
45
|
}
|
|
166
46
|
|
|
47
|
+
/* Что делает стратегия — разбор формы принадлежит ей, а не списку здесь:
|
|
48
|
+
* диспетчер только выбирает, кого позвать, и повторяет словарь стратегий. */
|
|
167
49
|
export function minifyForm(text, file, cfg) {
|
|
168
50
|
const how = strategyFor(file, cfg);
|
|
169
51
|
if (how === 'none') return text;
|
|
@@ -174,58 +56,3 @@ export function minifyForm(text, file, cfg) {
|
|
|
174
56
|
if (how === 'strip-lines') return stripLines(text);
|
|
175
57
|
throw new Error('неизвестная стратегия минификации «' + how + '» (есть: ' + STRATEGIES.join(', ') + ')');
|
|
176
58
|
}
|
|
177
|
-
|
|
178
|
-
/* Гард стриппера: он не имеет права выбросить что-то кроме комментариев и
|
|
179
|
-
* отступов, поэтому результат обязан компилироваться. Проверяем только те
|
|
180
|
-
* расширения, где содержимое — валидный JavaScript (список в конфиге,
|
|
181
|
-
* `minify.guard`): TypeScript или JSX хостом не проверяются, и делать вид, что
|
|
182
|
-
* проверили, было бы хуже, чем не проверять.
|
|
183
|
-
*
|
|
184
|
-
* Модуль или скрипт решает текст, а не расширение: проект с бандлером пишет
|
|
185
|
-
* `import`/`export` прямо в `.js` (и с `type: module` в манифесте, и без него), а
|
|
186
|
-
* `vm.Script` разбирает такой файл как скрипт и падает на самом `export`. Гард
|
|
187
|
-
* обязан понимать оба формата, поэтому пробует тот, на который файл похож, и
|
|
188
|
-
* принимает результат, если он разбирается хотя бы одним из двух способов.
|
|
189
|
-
* От этого он не слабеет: настоящая поломка не разберётся ни скриптом, ни
|
|
190
|
-
* модулем, и тогда наружу идёт причина того разбора, которым файл был.
|
|
191
|
-
*
|
|
192
|
-
* Модуль разбирает отдельный рабочий поток (`parse.js`): без него разбор модуля
|
|
193
|
-
* стоил бы запуска Node на каждую клетку. Иначе конфиг вида `eslint.config.mjs`
|
|
194
|
-
* остался бы без гарда, а без гарда его правка могла бы испортить «объём» молча.
|
|
195
|
-
*
|
|
196
|
-
* Когда не разбирается даже исходный текст, стриппер тут ни при чём: в этой
|
|
197
|
-
* графе измеряется не JavaScript (TypeScript, JSX), и это отказ с командой
|
|
198
|
-
* починки — правкой настроек. */
|
|
199
|
-
const MODULE_MARK = /^[ \t]*(?:import|export)\b/m;
|
|
200
|
-
const MODULE_EXT = ['.mjs'];
|
|
201
|
-
|
|
202
|
-
export function assertCompilable(min, rev, p, src) {
|
|
203
|
-
// Скрипт пробуется первым не ради формы, а ради цены: этот разбор идёт
|
|
204
|
-
// в процессе, а модуль — в рабочем потоке.
|
|
205
|
-
const asScript = scriptError(min, p);
|
|
206
|
-
if (asScript === null) return;
|
|
207
|
-
const asModule = moduleError(min);
|
|
208
|
-
if (asModule === null) return;
|
|
209
|
-
const shape = MODULE_EXT.indexOf(path.extname(p).toLowerCase()) >= 0 || MODULE_MARK.test(min);
|
|
210
|
-
if (src !== undefined && scriptError(src, p) !== null && moduleError(src) !== null) {
|
|
211
|
-
refuseCause('файл не JavaScript', 'файл ' + p + ' — не JavaScript: его исходный текст не'
|
|
212
|
-
+ ' разбирается ни как скрипт, ни как модуль, так что дело не в стриптере, а '
|
|
213
|
-
+ path.extname(p) + ' стоит в minify.guard: ' + (shape ? asModule : asScript) + '\n'
|
|
214
|
-
+ ' починка: уберите это расширение из minify.guard или задайте для него '
|
|
215
|
-
+ 'minify.ext — например { "' + path.extname(p).toLowerCase() + '": "strip-lines" }');
|
|
216
|
-
}
|
|
217
|
-
// Причина — того разбора, которым файл был: обвинять в чужой форме незачем.
|
|
218
|
-
throw new Error('стриппер испортил ' + p + ' на ' + rev.slice(0, 7) + ': '
|
|
219
|
-
+ (shape ? asModule : asScript));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Разбор как скрипт — в процессе: дешевле и без временных файлов.
|
|
223
|
-
function scriptError(text, p) {
|
|
224
|
-
try {
|
|
225
|
-
new vm.Script(text, { filename: p });
|
|
226
|
-
return null;
|
|
227
|
-
} catch (e) {
|
|
228
|
-
return e.message;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
package/templates/README.md
CHANGED
|
@@ -13,15 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
## Настройки
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
**Файл настроек заводить не нужно.** Без него инструмент выводит профиль из самого
|
|
17
|
+
проекта: колонками — крупнейшие файлы по одному от каждого расширения (иначе в отчёте
|
|
18
|
+
оказались бы одни крупные `.md`), журналом — первый знакомый (`WORKLOG.md`,
|
|
19
|
+
`CHANGELOG.md`, …), файлом отчёта — `docs/`, если каталог есть, командой починки —
|
|
20
|
+
объявленный скрипт `sizes`, ссылкой на коммит — адрес `origin`. О том, что настройки
|
|
21
|
+
выведены, инструмент говорит строкой, а закрепляет их файлом `size --init`: дальше
|
|
22
|
+
это обычные настройки, которые правят руками.
|
|
23
|
+
|
|
24
|
+
Этот образец — для другого случая: хочется начать с правленого файла или посмотреть
|
|
25
|
+
все ключи сразу. **Колонки в нём — пример**, а не список ваших файлов: в шаблоне стоят
|
|
26
|
+
`README.md` и `package.json`, потому что они есть почти в любом проекте, и с ними
|
|
27
|
+
первый отчёт соберётся сразу. Свои колонки — у `size --init` (он берёт их из вашего
|
|
28
|
+
дерева и истории; доки в него входят): либо взять его файл целиком и перенести из
|
|
29
|
+
шаблона ключи, которых там нет, либо скопировать шаблон и вписать колонки руками.
|
|
30
|
+
Отсюда же растёт ожидаемое: колонки примерные, поэтому `size check` скажет, что
|
|
25
31
|
README и журнал колонкой не отслеживаются, — так и задумано.
|
|
26
32
|
|
|
27
33
|
Что стоит знать про значения шаблона:
|
|
@@ -42,7 +48,7 @@ README и журнал колонкой не отслеживаются, — т
|
|
|
42
48
|
к нему.
|
|
43
49
|
- `journal: null` — ссылок на разделы журнала не будет. Если в проекте есть
|
|
44
50
|
`WORKLOG.md` или `CHANGELOG.md`, поставьте объект с `path`, `url`, `pattern` —
|
|
45
|
-
|
|
51
|
+
именно этот объект выводит `size --init`, когда журнал в проекте есть.
|
|
46
52
|
- `paths` внутри колонки — псевдонимы одного файла: если файл переименовывали,
|
|
47
53
|
перечислите и старое имя, и новое, и колонка не разорвётся.
|
|
48
54
|
- `output: "docs/size-table.html"` — файл таблицы; каталог инструмент создаст сам.
|