@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/cli.js
CHANGED
|
@@ -9,22 +9,19 @@ import {
|
|
|
9
9
|
checkMode, coverageMode, dataMode, doctorMode, explainMode, hookMode, jsonMode, writeMode
|
|
10
10
|
} from './modes.js';
|
|
11
11
|
|
|
12
|
-
/*
|
|
12
|
+
/* The tool's entry point: parse the line, read the project, hand the request to a mode.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* (`src/
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* правила грамматики и тексты отказов проверяются своим каталогом
|
|
21
|
-
* (`tools/refusals.js`), который считает их места в исходниках.
|
|
14
|
+
* Neither the grammar (`src/args.js`) nor the modes (`src/modes.js`) nor pinning the
|
|
15
|
+
* settings (`src/init.js`) is left here — only what makes this an entry point at all:
|
|
16
|
+
* where the project root comes from, how the settings file is named, and how a refusal
|
|
17
|
+
* turns into an exit code. Saying out loud that there is no settings file and the settings
|
|
18
|
+
* were derived from the project belongs here as well: that holds for every mode, not for
|
|
19
|
+
* one of them.
|
|
22
20
|
*/
|
|
23
21
|
|
|
24
|
-
/*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* исполнитель, и вызывается он без пробежек по `if`. */
|
|
22
|
+
/* Request runners: the key is what the request is called (a command or a mode), and "a
|
|
23
|
+
* bare run" is the empty string. The parser has already checked the combinations, so this
|
|
24
|
+
* is a lookup with a runner for every name, not a decision. */
|
|
28
25
|
const RUNNERS = {
|
|
29
26
|
check: (c, x) => coverageMode(x.cfg, x.root, x.configFile, c.json),
|
|
30
27
|
explain: (c, x) => explainMode(x.cfg, x.root, c.arg[0], c.json),
|
|
@@ -35,26 +32,26 @@ const RUNNERS = {
|
|
|
35
32
|
|
|
36
33
|
const asked = (cmd) => (cmd.verb === null ? (cmd.mode === null ? '' : cmd.mode) : cmd.verb);
|
|
37
34
|
|
|
38
|
-
/*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
35
|
+
/* Installing the hook without being asked belongs here, not in `doctor` (which only
|
|
36
|
+
* reports) and not in `hook-run` (which is called from an already installed hook). It
|
|
37
|
+
* installs once per clone, says so, and is silent afterwards: the report is rebuilt after
|
|
38
|
+
* every commit with no manual step (design and limits: `src/hook.js`). */
|
|
42
39
|
function ensureHook(root, cfg) {
|
|
43
40
|
const files = autoInstall(root, cfg);
|
|
44
41
|
if (files === null) return;
|
|
45
|
-
console.error('·
|
|
46
|
-
+ '
|
|
42
|
+
console.error('· hook installed: ' + files.join(', ') + ' — the report is rebuilt after every'
|
|
43
|
+
+ ' commit (remove it: ' + cliCommand('uninstall-hook') + ')');
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
/*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
46
|
+
/* Delivery. Diagnostics and the hook answer before the settings are read: they need the
|
|
47
|
+
* environment rather than the whole project, and refusing them over settings would be
|
|
48
|
+
* wrong — the settings are exactly what they report about. */
|
|
52
49
|
function deliver(cmd, base) {
|
|
53
50
|
if (cmd.verb === 'doctor') return doctorMode(base.root, base.configFile, cmd.json);
|
|
54
51
|
if (HOOK_COMMANDS.indexOf(cmd.verb) >= 0) return hookMode(cmd.verb, base.root, base.configFile);
|
|
55
52
|
const ctx = { root: base.root, configFile: base.configFile, cfg: loadConfig(base.configFile, base.root) };
|
|
56
|
-
//
|
|
57
|
-
//
|
|
53
|
+
// The note goes to stderr: `--json` and `--data` own stdout, and mixing a story about
|
|
54
|
+
// the settings into data would break parsing.
|
|
58
55
|
if (ctx.cfg.derived) derivedLines(ctx.cfg).forEach((line) => console.error(line));
|
|
59
56
|
ensureHook(base.root, ctx.cfg);
|
|
60
57
|
return RUNNERS[asked(cmd)](cmd, ctx);
|
|
@@ -77,11 +74,11 @@ export function main() {
|
|
|
77
74
|
console.error('✗ ' + e.message);
|
|
78
75
|
return e.code;
|
|
79
76
|
}
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
console.error('✗
|
|
84
|
-
+ '
|
|
77
|
+
// Unexpected failures are a defect of the tool, not a dead end for the user, and the
|
|
78
|
+
// text says so — otherwise the user looks for the mistake on their side. The stack is
|
|
79
|
+
// printed whole: nothing else can diagnose such a refusal.
|
|
80
|
+
console.error('✗ internal error (this is a defect of the tool, not of the project —'
|
|
81
|
+
+ ' please send this text whole):\n' + e.stack);
|
|
85
82
|
return EXIT.INTERNAL;
|
|
86
83
|
}
|
|
87
84
|
}
|
package/src/config.js
CHANGED
|
@@ -9,36 +9,35 @@ import { TOKEN_DEFAULTS, TOKEN_FAMILIES } from './tokens.js';
|
|
|
9
9
|
import { CATEGORY_ORDER } from './data.js';
|
|
10
10
|
import { projectConfig } from './project.js';
|
|
11
11
|
|
|
12
|
-
/*
|
|
13
|
-
*
|
|
14
|
-
* без неё не идёт ни один режим. */
|
|
12
|
+
/* The settings of a consumer project: defaults, reading and checking. Settings describe a project
|
|
13
|
+
* rather than the mechanics, which is why the check lives right here, and no mode runs without it. */
|
|
15
14
|
|
|
16
15
|
export const CONFIG_NAME = 'size-table.config.json';
|
|
17
16
|
|
|
18
17
|
export const DEFAULT_CONFIG = {
|
|
19
18
|
output: 'size-report.html',
|
|
20
|
-
locale: '
|
|
21
|
-
title: '', //
|
|
19
|
+
locale: 'en',
|
|
20
|
+
title: '', // by default: the heading from the locale
|
|
22
21
|
heading: '',
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// The fix is a call that stays inside the project: the package name would send the reader to the registry,
|
|
23
|
+
// which serves a revision the project never pinned.
|
|
25
24
|
fixCommand: invocation() + ' --write',
|
|
26
25
|
metrics: ['raw', 'min'],
|
|
27
26
|
columns: [],
|
|
28
|
-
// `engine`
|
|
29
|
-
//
|
|
27
|
+
// `engine` says what counts the `min` metric: stripping ballast (the default, and the one the
|
|
28
|
+
// frozen fixtures were taken under) or real compression by the minifier.
|
|
30
29
|
minify: { engine: 'strip', ext: {}, guard: ['.js', '.mjs', '.cjs'] },
|
|
31
|
-
//
|
|
30
|
+
// Tokens: which dictionary counts them. The family is about models, the encoding about the number.
|
|
32
31
|
tokens: Object.assign({}, TOKEN_DEFAULTS),
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
32
|
+
// Hook automation: the hook rebuilds the report after every commit and installs itself — after the
|
|
33
|
+
// package is installed (`bin/postinstall.js`) and on the first run in a project (`src/hook.js`);
|
|
34
|
+
// this key is its switch (no `.size-report/…` state is needed: removing the hook returns the
|
|
35
|
+
// project to its previous behaviour).
|
|
37
36
|
hooks: { enabled: true },
|
|
38
37
|
journal: null,
|
|
39
38
|
links: { commitUrl: '' },
|
|
40
|
-
//
|
|
41
|
-
//
|
|
39
|
+
// A merge is an ordinary commit: it carries the edits that resolved a conflict, and without a row
|
|
40
|
+
// they would never reach the sum of deltas above the current size.
|
|
42
41
|
rows: { merges: true, sha: true },
|
|
43
42
|
skip: []
|
|
44
43
|
};
|
|
@@ -56,49 +55,46 @@ export function gitRoot() {
|
|
|
56
55
|
encoding: 'utf8', maxBuffer: MAX_BUF, env: gitEnv()
|
|
57
56
|
}).trim();
|
|
58
57
|
} catch (e) {
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// или git недоступен») не называл ни одного из них.
|
|
58
|
+
// Two dead ends with different fixes — "git did not start" and "there is no repository here" —
|
|
59
|
+
// are told apart by what git itself said rather than by a guess: ENOENT means the program was not
|
|
60
|
+
// found. One text for both ("not a git repository, or git is unavailable") named neither of them.
|
|
63
61
|
if (e.code === 'ENOENT') {
|
|
64
|
-
refuseCause('
|
|
65
|
-
+ '
|
|
66
|
-
+ '
|
|
62
|
+
refuseCause('git missing', 'git did not start: it is not in PATH (the table is built from its'
|
|
63
|
+
+ ' history, and the directory I look in is ' + process.cwd() + ').\n'
|
|
64
|
+
+ ' fix: install git (https://git-scm.com) and run the command again');
|
|
67
65
|
}
|
|
68
|
-
refuseCause('
|
|
69
|
-
+ '
|
|
70
|
-
+ '
|
|
71
|
-
+ '
|
|
66
|
+
refuseCause('not a git repository', 'git sees no repository here: the table is built from its'
|
|
67
|
+
+ ' history (the directory I look in is ' + process.cwd() + ').\n'
|
|
68
|
+
+ ' see: whether the command was run from the directory of the project\n'
|
|
69
|
+
+ ' fix: if there is no history yet, create it: git init');
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
/*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* человека к тому, чего в проекте не лежит. */
|
|
73
|
+
/* No settings file — the project derives them (`src/project.js`) and work starts at once: there is no
|
|
74
|
+
* reason to create a file for a first run, and `--init` pins the derived ones to a file when someone
|
|
75
|
+
* wants to edit them. This happens for the default name only: a file named by `--config` is already a
|
|
76
|
+
* request for that very file, so its absence stays a refusal (otherwise a typo in the path would
|
|
77
|
+
* silently yield someone else's settings). `path` is given in words rather than as a path: there is no
|
|
78
|
+
* file, and "edit <path>" would lead the reader to something the project does not have. */
|
|
82
79
|
export function derivedConfig(root) {
|
|
83
80
|
const cfg = derivedProfile(root);
|
|
84
|
-
cfg.path = '
|
|
81
|
+
cfg.path = 'derived from the project';
|
|
85
82
|
cfg.derived = true;
|
|
86
83
|
validateConfig(cfg);
|
|
87
84
|
return cfg;
|
|
88
85
|
}
|
|
89
86
|
|
|
90
|
-
/*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
87
|
+
/* Derived from the project plus the defaults — what a project runs on without a file, and what
|
|
88
|
+
* `--init` pins. One place for two roles (or "the file" and "work without a file" would drift by a
|
|
89
|
+
* column or a number), while the deriving itself (`src/project.js`) knows nothing of the defaults: it
|
|
90
|
+
* says only what it sees in the project. */
|
|
94
91
|
export function derivedProfile(root) {
|
|
95
92
|
return withDefaults(projectConfig(root));
|
|
96
93
|
}
|
|
97
94
|
|
|
98
|
-
/*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* список расширений. */
|
|
95
|
+
/* Settings on top of the defaults — one place for two sources (a file and the project): nested keys
|
|
96
|
+
* are filled in by key, because `minify: {engine: …}` does not mean "`minify` has no other fields",
|
|
97
|
+
* and reading it that way would lose the stripping's list of extensions. */
|
|
102
98
|
function withDefaults(raw) {
|
|
103
99
|
const cfg = Object.assign({}, DEFAULT_CONFIG, raw);
|
|
104
100
|
['minify', 'tokens', 'hooks', 'links', 'rows'].forEach((key) => {
|
|
@@ -107,25 +103,25 @@ function withDefaults(raw) {
|
|
|
107
103
|
return cfg;
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
/*
|
|
111
|
-
*
|
|
106
|
+
/* Settings are read as they are and filled in with the defaults: in a project that has just attached
|
|
107
|
+
* the generator the config may be three lines long. */
|
|
112
108
|
export function loadConfig(file, root) {
|
|
113
109
|
if (!fs.existsSync(file)) {
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
110
|
+
// The advice names the very file in question: `--init` without a file would write a draft under the
|
|
111
|
+
// default name in the project root — fixing something other than what was asked. The name is left
|
|
112
|
+
// out exactly when it is the default one anyway.
|
|
117
113
|
const dflt = root !== undefined && path.resolve(root, CONFIG_NAME) === path.resolve(file);
|
|
118
114
|
if (dflt) return derivedConfig(root);
|
|
119
|
-
refuseCause('
|
|
120
|
-
+ '\n
|
|
121
|
-
+ '\n
|
|
115
|
+
refuseCause('no settings file', 'no settings file ' + file
|
|
116
|
+
+ '\n create it: ' + cliCommand('--init ' + advicePath(file))
|
|
117
|
+
+ '\n see: without "--config" no settings are needed — they are derived from the project');
|
|
122
118
|
}
|
|
123
119
|
let raw;
|
|
124
120
|
try {
|
|
125
121
|
raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
126
122
|
} catch (e) {
|
|
127
|
-
refuseCause('
|
|
128
|
-
+ '\n
|
|
123
|
+
refuseCause('settings not parsed', 'cannot parse ' + file + ': ' + e.message
|
|
124
|
+
+ '\n fix: edit ' + file + '; a sample of settings comes from ' + cliCommand('--init') + ' in an empty directory');
|
|
129
125
|
}
|
|
130
126
|
const cfg = withDefaults(raw);
|
|
131
127
|
cfg.path = file;
|
|
@@ -133,23 +129,23 @@ export function loadConfig(file, root) {
|
|
|
133
129
|
return cfg;
|
|
134
130
|
}
|
|
135
131
|
|
|
136
|
-
/*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
132
|
+
/* Columns: a label and paths have to be names rather than anything at all — a path given as a number
|
|
133
|
+
* or an object silently matches nothing, and the column reports zero rows as success. The refusal has
|
|
134
|
+
* to happen here instead of turning into an empty report. */
|
|
139
135
|
function checkColumns(cfg, fail) {
|
|
140
|
-
if (!cfg.columns || cfg.columns.length === 0) fail('
|
|
136
|
+
if (!cfg.columns || cfg.columns.length === 0) fail('no columns are given (columns)');
|
|
141
137
|
const labels = new Set();
|
|
142
138
|
cfg.columns.forEach((c, i) => {
|
|
143
139
|
const pathsAreNames = c && Array.isArray(c.paths)
|
|
144
140
|
&& c.paths.length > 0 && c.paths.every((p) => typeof p === 'string' && p !== '');
|
|
145
141
|
if (!c || typeof c.label !== 'string' || c.label === '' || !pathsAreNames) {
|
|
146
|
-
fail('
|
|
147
|
-
+ JSON.stringify(c).slice(0, 90) + '\n
|
|
148
|
-
+ cliCommand('--init
|
|
142
|
+
fail('column #' + (i + 1) + ' has to be {label, paths: [...]} of non-empty strings: '
|
|
143
|
+
+ JSON.stringify(c).slice(0, 90) + '\n see: a draft with ready columns comes from '
|
|
144
|
+
+ cliCommand('--init <file>'));
|
|
149
145
|
}
|
|
150
|
-
if (labels.has(c.label)) fail('
|
|
146
|
+
if (labels.has(c.label)) fail('the column label "' + c.label + '" repeats');
|
|
151
147
|
if (c.category !== undefined && CATEGORY_ORDER.indexOf(c.category) < 0) {
|
|
152
|
-
fail('
|
|
148
|
+
fail('the category "' + c.category + '" of the column "' + c.label + '" is unknown: '
|
|
153
149
|
+ CATEGORY_ORDER.join(', '));
|
|
154
150
|
}
|
|
155
151
|
labels.add(c.label);
|
|
@@ -157,51 +153,51 @@ function checkColumns(cfg, fail) {
|
|
|
157
153
|
}
|
|
158
154
|
|
|
159
155
|
function checkMetrics(cfg, fail) {
|
|
160
|
-
if (!Array.isArray(cfg.metrics) || cfg.metrics.length === 0) fail('
|
|
156
|
+
if (!Array.isArray(cfg.metrics) || cfg.metrics.length === 0) fail('no metrics are given (metrics)');
|
|
161
157
|
cfg.metrics.forEach((m) => {
|
|
162
|
-
if (!METRICS[m]) fail('
|
|
158
|
+
if (!METRICS[m]) fail('unknown metric "' + m + '" (there are: ' + Object.keys(METRICS).join(', ') + ')');
|
|
163
159
|
});
|
|
164
160
|
}
|
|
165
161
|
|
|
166
162
|
function checkMinify(cfg, fail) {
|
|
167
163
|
if (MINIFY_ENGINES.indexOf(cfg.minify.engine) < 0) {
|
|
168
|
-
fail('
|
|
164
|
+
fail('unknown minification engine "' + cfg.minify.engine + '" (there are: ' + MINIFY_ENGINES.join(', ') + ')');
|
|
169
165
|
}
|
|
170
166
|
}
|
|
171
167
|
|
|
172
168
|
function checkTokens(cfg, fail) {
|
|
173
169
|
const family = TOKEN_FAMILIES[cfg.tokens.family];
|
|
174
170
|
if (family === undefined) {
|
|
175
|
-
fail('
|
|
171
|
+
fail('unknown tokenizer family "' + cfg.tokens.family + '" (there are: '
|
|
176
172
|
+ Object.keys(TOKEN_FAMILIES).join(', ') + ')');
|
|
177
173
|
}
|
|
178
174
|
if (family.encodings.indexOf(cfg.tokens.encoding) < 0) {
|
|
179
|
-
fail('
|
|
180
|
-
+ cfg.tokens.family + ' (
|
|
175
|
+
fail('unknown tokenizer encoding "' + cfg.tokens.encoding + '" of the family '
|
|
176
|
+
+ cfg.tokens.family + ' (there are: ' + family.encodings.join(', ') + ')');
|
|
181
177
|
}
|
|
182
178
|
}
|
|
183
179
|
|
|
184
|
-
/*
|
|
185
|
-
*
|
|
180
|
+
/* The report file cannot be a column of itself: the size of the artifact depends on the number of
|
|
181
|
+
* rows, that is, on itself. */
|
|
186
182
|
function checkOutput(cfg, fail) {
|
|
187
183
|
cfg.columns.forEach((c) => {
|
|
188
|
-
if (c.paths.indexOf(cfg.output) >= 0) fail('
|
|
184
|
+
if (c.paths.indexOf(cfg.output) >= 0) fail('the size table file (' + cfg.output + ') cannot be a column');
|
|
189
185
|
});
|
|
190
|
-
if (!cfg.output) fail('
|
|
186
|
+
if (!cfg.output) fail('output is not set');
|
|
191
187
|
}
|
|
192
188
|
|
|
193
189
|
function checkJournal(cfg, fail) {
|
|
194
190
|
if (!cfg.journal) return;
|
|
195
|
-
if (!cfg.journal.path) fail('journal.path
|
|
196
|
-
if (!cfg.journal.pattern) fail('journal.pattern
|
|
197
|
-
try { new RegExp(cfg.journal.pattern); } catch (e) { fail('journal.pattern
|
|
191
|
+
if (!cfg.journal.path) fail('journal.path is not set');
|
|
192
|
+
if (!cfg.journal.pattern) fail('journal.pattern is not set');
|
|
193
|
+
try { new RegExp(cfg.journal.pattern); } catch (e) { fail('journal.pattern does not compile: ' + e.message); }
|
|
198
194
|
}
|
|
199
195
|
|
|
200
|
-
/*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
196
|
+
/* What the settings say about a path: `columns` — a column tracks it, `excluded` — it is declared an
|
|
197
|
+
* exception (`skip` and the report file itself), `outside` — neither. One judgement for two answers:
|
|
198
|
+
* `check` asks it about the whole history, `explain` about a single commit. Membership is counted over
|
|
199
|
+
* all paths of a column rather than by its label: a column may have several paths (a rename), and any
|
|
200
|
+
* of them is that column. */
|
|
205
201
|
export function pathRoles(cfg) {
|
|
206
202
|
const tracked = new Set();
|
|
207
203
|
cfg.columns.forEach((col) => col.paths.forEach((p) => tracked.add(p)));
|
|
@@ -212,26 +208,26 @@ export function pathRoles(cfg) {
|
|
|
212
208
|
};
|
|
213
209
|
}
|
|
214
210
|
|
|
215
|
-
/*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
211
|
+
/* The fix for "outside the columns" — one text for two answers, and it names the paths: a command
|
|
212
|
+
* without names is no command. The text is assembled from the names rather than appending them per
|
|
213
|
+
* branch, which is why nothing has to be guarded here: a commit with no files at all gets no fix — the
|
|
214
|
+
* list being empty, nobody calls it (`fixFor` in `src/explain.js`). */
|
|
219
215
|
export function outsideFix(paths) {
|
|
220
|
-
return '
|
|
216
|
+
return 'add these paths as a column or to "skip" of ' + CONFIG_NAME + ': ' + paths.join(', ');
|
|
221
217
|
}
|
|
222
218
|
|
|
223
219
|
export function validateConfig(cfg) {
|
|
224
|
-
const fail = (msg) => refuseCause('
|
|
225
|
-
'
|
|
220
|
+
const fail = (msg) => refuseCause('settings invalid',
|
|
221
|
+
'config ' + cfg.path + ': ' + msg + '\n fix: edit ' + cfg.path);
|
|
226
222
|
checkColumns(cfg, fail);
|
|
227
223
|
checkMetrics(cfg, fail);
|
|
228
224
|
checkMinify(cfg, fail);
|
|
229
225
|
checkTokens(cfg, fail);
|
|
230
|
-
if (!LOCALES[cfg.locale]) fail('
|
|
231
|
-
//
|
|
232
|
-
//
|
|
226
|
+
if (!LOCALES[cfg.locale]) fail('unknown locale "' + cfg.locale + '" (there are: ' + Object.keys(LOCALES).join(', ') + ')');
|
|
227
|
+
// The hook switch is a yes/no rather than a truthy/falsy one: the tool has to tell `false` from a
|
|
228
|
+
// stray string, or switched-off automation would stay switched on.
|
|
233
229
|
if (typeof cfg.hooks.enabled !== 'boolean') {
|
|
234
|
-
fail('hooks.enabled
|
|
230
|
+
fail('hooks.enabled is not a yes/no: ' + JSON.stringify(cfg.hooks.enabled));
|
|
235
231
|
}
|
|
236
232
|
checkOutput(cfg, fail);
|
|
237
233
|
checkJournal(cfg, fail);
|
package/src/css.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
|
|
3
|
-
/*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
/* The report's styling is ordinary `.css` next to the code rather than strings inside modules — the same rule
|
|
4
|
+
* as for the page's program (`src/page/app.js`): an editor sees a real source file instead of a template
|
|
5
|
+
* string. They are read from disk relative to their own place, so they work for whoever installed the package
|
|
6
|
+
* as well.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* There are two sets, each with a role of its own:
|
|
9
9
|
*
|
|
10
|
-
* 1. `table.css` —
|
|
11
|
-
*
|
|
12
|
-
* 2. `page/app.css` —
|
|
13
|
-
*
|
|
10
|
+
* 1. `table.css` — the **table**: cell geometry, the sticky header and commit column, a commit's caption,
|
|
11
|
+
* the colours of the deltas.
|
|
12
|
+
* 2. `page/app.css` — the page's look **on top of the table**: the canvas, the panel of choices, the empty
|
|
13
|
+
* states and the adaptation to a narrow window.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* страницы. Смена соглашения — две строки в `table.css`.
|
|
15
|
+
* The convention about the colour of a delta is set once, in `table.css`: `.up` green, `.down` red (growth is
|
|
16
|
+
* "more logic" rather than alarm). It has no second place on purpose: growth cannot be shown in different
|
|
17
|
+
* colours in two spots of one page. Changing the convention is two lines in `table.css`.
|
|
19
18
|
*
|
|
20
|
-
*
|
|
19
|
+
* The path given to `readCss` is relative to the `src/` directory: that is how the engine sees it wherever it
|
|
20
|
+
* lies.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
export const TABLE_CSS = readCss('./table.css');
|
package/src/data.js
CHANGED
|
@@ -6,15 +6,13 @@ import { build, skipLine } from './history.js';
|
|
|
6
6
|
import { projectTree } from './project.js';
|
|
7
7
|
import { TOOL_PKG } from './tool.js';
|
|
8
8
|
|
|
9
|
-
/*
|
|
10
|
-
*
|
|
11
|
-
* начинается там, где этот модуль заканчивается. */
|
|
9
|
+
/* File categories and the contract with the page: absolute values and the shape of the table, with no
|
|
10
|
+
* derived quantity at all. Everything the page counts itself begins where this module ends. */
|
|
12
11
|
|
|
13
|
-
/*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* объяснимо, а таблица расширений — догадка по имени файла. */
|
|
12
|
+
/* A file's category is only for the page's quick on/off buttons for a group: it does not reach the
|
|
13
|
+
* numbers. The rule is one: the extension gives the category, everything else counts as code; a category
|
|
14
|
+
* set in the column's settings outranks that rule, and the data says where it came from (`categoryBy`) — a
|
|
15
|
+
* manual decision is explainable, while a table of extensions is a guess by file name. */
|
|
18
16
|
export const CATEGORY_EXTS = {
|
|
19
17
|
docs: ['.md', '.markdown', '.rst', '.txt', '.adoc'],
|
|
20
18
|
chore: ['.json', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf', '.lock', '.editorconfig'],
|
|
@@ -29,15 +27,14 @@ export function categoryOf(col) {
|
|
|
29
27
|
return { key: known === undefined ? 'code' : known, by: 'auto' };
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
/*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* сумму он не может. Числа в контракте те же, что в артефакте, — это та же правда,
|
|
36
|
-
* разложенная по полям.
|
|
30
|
+
/* The engine-to-page contract: deltas, totals, "now" and the filters are counted by the page — the engine
|
|
31
|
+
* does not know what is switched on in the view, so it cannot total anything up in advance. The numbers
|
|
32
|
+
* here are the same as in the artifact: one truth laid out in fields.
|
|
37
33
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
34
|
+
* The reasons of skipped commits travel as strings and stay strings: they are sentences for a person, and
|
|
35
|
+
* the question behind them (why a commit has no row) is asked by the `explain` command, where the reason is
|
|
36
|
+
* laid out into fields. The page does not carry this list at all, and on purpose: the report's own commit
|
|
37
|
+
* is one of the skipped ones, so the file would never become a fixed point. */
|
|
41
38
|
export function reportData(cfg, root) {
|
|
42
39
|
const { rows, state, dropped, last } = build(cfg, root);
|
|
43
40
|
const loc = LOCALES[cfg.locale];
|
|
@@ -67,45 +64,24 @@ export function reportData(cfg, root) {
|
|
|
67
64
|
categories: CATEGORY_ORDER.filter((key) => files.some((f) => f.category === key))
|
|
68
65
|
.map((key) => ({ key: key, label: loc.categories[key] })),
|
|
69
66
|
files: files,
|
|
70
|
-
/*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
67
|
+
/* The page's tree is the project's tree: every path rather than the columns alone. The numbers belong to
|
|
68
|
+
* columns only, which is why each catalogue entry says what it did not get: `why` empty — the file is
|
|
69
|
+
* measured, otherwise a reason is named. */
|
|
73
70
|
catalog: projectTree(root, cfg.output, files.map((f) => (f.path === null ? f.paths[0] : f.path))),
|
|
74
71
|
rows: rows.map((r) => Object.assign(rowShape(r),
|
|
75
72
|
{ href: rowHref(r.section, r.sha, cfg), values: r.cells })),
|
|
76
73
|
now: state.map((s) => (s === null ? null : s.cells)),
|
|
77
|
-
/*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
74
|
+
/* Which columns the last commit touched: the page puts those in front by this mark — a reader needs what
|
|
75
|
+
* changed just now rather than the rest. This is a fact from the history rather than a derived quantity:
|
|
76
|
+
* it does not count anything, it says what the edit touched. */
|
|
80
77
|
last: last,
|
|
81
|
-
approx: approxMarks(rows, state, cfg),
|
|
82
78
|
skipped: dropped.map(skipLine)
|
|
83
79
|
};
|
|
84
80
|
}
|
|
85
81
|
|
|
86
|
-
/*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* появляется вовсе: все числа точны — молчание.
|
|
90
|
-
*
|
|
91
|
-
* Знак ставит движок там же, где считает число, — из того же правила, что и
|
|
92
|
-
* подпись метрики. Поэтому страница ничего про пути и форматы не выводит: она
|
|
93
|
-
* только показывает то, что сказано, и второго правила точности не заводит. */
|
|
94
|
-
function approxMarks(rows, state, cfg) {
|
|
95
|
-
const out = {};
|
|
96
|
-
cfg.metrics.forEach((m) => {
|
|
97
|
-
const mark = (flags) => (flags !== null && flags[m] ? '1' : '0');
|
|
98
|
-
const inRows = rows.map((r) => r.approx.map(mark).join('')).join('');
|
|
99
|
-
const now = state.map((s) => mark(s === null ? null : s.approx)).join('');
|
|
100
|
-
if (inRows.indexOf('1') >= 0 || now.indexOf('1') >= 0) out[m] = { rows: inRows, now: now };
|
|
101
|
-
});
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/* Общая часть строки ответа: она есть и у контракта страницы (`--data`), и у
|
|
106
|
-
* прежней формы `--json` (она заморожена эталоном паритета). Одно место — потому
|
|
107
|
-
* что разойтись эти два ответа могут ровно здесь, а **порядок полей и есть байты
|
|
108
|
-
* ответа**: они в объекте в том же порядке, в каком их печатает замороженная форма. */
|
|
82
|
+
/* The part of a row shared by both answers: the page's contract (`--data`) and the older `--json` form (frozen
|
|
83
|
+
* by the parity fixture). One place, because these two answers can drift apart exactly here, and **the order
|
|
84
|
+
* of the fields is the bytes of the answer**: the object holds them in the order the frozen form prints. */
|
|
109
85
|
export function rowShape(r) {
|
|
110
86
|
return {
|
|
111
87
|
sha: r.sha,
|
|
@@ -115,7 +91,7 @@ export function rowShape(r) {
|
|
|
115
91
|
};
|
|
116
92
|
}
|
|
117
93
|
|
|
118
|
-
/*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
94
|
+
/* The derived quantities live in `src/derived.js`: the page gets that whole file pasted into itself as text
|
|
95
|
+
* (`pageScript`), while the terminal answers import it. There is no second calculation of the same table, so
|
|
96
|
+
* the page's numbers and the artifact's bytes have nothing to drift apart with — `test/contract-derived.test.js`
|
|
97
|
+
* guards that. */
|