@vernikr/size-report 2.4.0 → 2.5.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 +929 -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 +31 -37
- 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 -77
- package/src/hook.js +130 -149
- package/src/init.js +36 -37
- package/src/journal.js +17 -15
- package/src/locales.js +25 -16
- package/src/metrics.js +56 -55
- package/src/minify.js +28 -27
- package/src/modes.js +57 -60
- package/src/optional.js +13 -11
- package/src/page/app.css +75 -91
- package/src/page/app.js +30 -35
- package/src/page/build.js +40 -39
- package/src/page/dom.js +8 -9
- package/src/page/panel.js +48 -51
- package/src/page/state.js +72 -87
- package/src/page/table.js +21 -24
- 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 +27 -28
- package/src/strip/js.js +27 -27
- package/src/strip.js +18 -21
- package/src/table.css +13 -14
- package/src/tokens.js +28 -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/bin/postinstall.js
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/*
|
|
3
|
-
*
|
|
2
|
+
/* Hook installation after the package is installed: the report is rebuilt from the very
|
|
3
|
+
* first commit, with neither the tool run nor a settings file.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (`src/hook.js`) —
|
|
7
|
-
*
|
|
5
|
+
* Only the consumer project is located here: the hook itself is installed by
|
|
6
|
+
* `autoInstall` (`src/hook.js`) — the same place as on the first run, so that "installed
|
|
7
|
+
* by the installer" and "installed by the first run" cannot diverge in the file content.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `install-hook`.
|
|
9
|
+
* The exit code is always 0: installing dependencies must not fail because a service
|
|
10
|
+
* could not be rendered (no git, no permissions, a foreign hook, CI), and the reason is
|
|
11
|
+
* not printed — background work has no reader, while `install-hook` has the exact one.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* One subtlety: platforms that skip dependency scripts by default (pnpm 10, yarn berry)
|
|
14
|
+
* do not always call this file; there the hook is installed by the first run of the tool
|
|
15
|
+
* in the project. Both paths lead to the same place. */
|
|
17
16
|
|
|
18
17
|
import fs from 'fs';
|
|
19
18
|
import path from 'path';
|
|
20
19
|
import { autoInstall } from '../src/hook.js';
|
|
21
20
|
|
|
22
|
-
/*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
/* The consumer project directory, most precise candidate first: `INIT_CWD` (set by npm
|
|
22
|
+
* and pnpm when they run a package script), `npm_config_local_prefix`, and finally a walk
|
|
23
|
+
* up from the current directory to the nearest `.git` — the script itself runs from
|
|
24
|
+
* `node_modules`, where there is no repository. */
|
|
26
25
|
function projectRoot() {
|
|
27
26
|
const candidates = [process.env.INIT_CWD, process.env.npm_config_local_prefix, process.cwd()];
|
|
28
27
|
for (const start of candidates) {
|
|
@@ -46,6 +45,6 @@ function gitRootOf(start) {
|
|
|
46
45
|
const root = projectRoot();
|
|
47
46
|
const files = root === null ? null : autoInstall(root, null);
|
|
48
47
|
if (files !== null && process.env.SIZE_REPORT_QUIET !== '1') {
|
|
49
|
-
console.error('· size-report:
|
|
50
|
-
+ '
|
|
48
|
+
console.error('· size-report: hook installed (' + files.join(', ') + ') — the report refreshes after'
|
|
49
|
+
+ ' every commit; remove it: size uninstall-hook');
|
|
51
50
|
}
|
package/bin/size.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/*
|
|
3
|
-
*
|
|
2
|
+
/* The `size` entry point. Arguments and modes live in the engine: keeping them here
|
|
3
|
+
* would mean the engine could not be imported without running the command. */
|
|
4
4
|
import { main } from '../src/size-table.js';
|
|
5
5
|
|
|
6
6
|
process.exitCode = main();
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vernikr/size-report",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"author": "vernikr",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/vernikr/size-report.git"
|
|
8
8
|
},
|
|
9
|
-
"description": "
|
|
9
|
+
"description": "Tracking how the volume of code and documents grows across git history: raw / min / tokens; the report is one self-contained file, refreshed by the hook on its own",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"packageManager": "pnpm@10.6.1",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"src",
|
|
28
28
|
"templates",
|
|
29
29
|
"README.md",
|
|
30
|
-
"CHANGELOG.md",
|
|
31
30
|
"LICENSE"
|
|
32
31
|
],
|
|
33
32
|
"sideEffects": false,
|
|
@@ -66,7 +65,7 @@
|
|
|
66
65
|
"report"
|
|
67
66
|
],
|
|
68
67
|
"devDependencies": {
|
|
69
|
-
"@vernikr/size-report": "2.
|
|
68
|
+
"@vernikr/size-report": "2.4.0",
|
|
70
69
|
"c8": "10",
|
|
71
70
|
"dependency-cruiser": "17",
|
|
72
71
|
"eslint": "^9.18.0",
|
package/src/args.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { cliCommand, advicePath, refuseCause } from './refusal.js';
|
|
2
2
|
|
|
3
|
-
/*
|
|
3
|
+
/* Command-line grammar: what the user named, and whether that combination is legal.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* One parse, at the entry, before the project is read. It settles everything at once —
|
|
6
|
+
* which mode was asked for, whether it fits the command and the other flags, whether every
|
|
7
|
+
* flag got a value, whether there are extra words — and answers with either a plan or a
|
|
8
|
+
* refusal. That is why the order of branches in `main` decides nothing: "you cannot do
|
|
9
|
+
* that" is a value, not a place in the code, and it surfaces in one way only — as a refusal
|
|
10
|
+
* naming the culprit and the command that fixes it.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Flags come in three kinds: modes (mutually exclusive — they say what to do), value flags
|
|
13
|
+
* (they take the next argument) and switches. A command is the first word outside the
|
|
14
|
+
* flags, so its position in the line does not matter: `size check --config x` and
|
|
15
|
+
* `size --config x check` are the same.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* The file is separate from `cli.js` because the questions differ: here "what was asked and
|
|
18
|
+
* may it be so", there "what to do about it". Hence the price of a wrong rule here — it
|
|
19
|
+
* breaks every mode at once — and hence the checks split by subject, with the refusal
|
|
20
|
+
* catalogue (`tools/refusals.js`) counting their sites in the sources and demanding a test
|
|
21
|
+
* for each.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
const MODES = ['--init', '--write', '--data'];
|
|
@@ -28,23 +28,23 @@ const COMMANDS = ['check', 'explain', 'doctor', 'install-hook', 'uninstall-hook'
|
|
|
28
28
|
const ANSWER_COMMANDS = ['check', 'explain', 'doctor'];
|
|
29
29
|
export const HOOK_COMMANDS = ['install-hook', 'uninstall-hook', 'hook-run'];
|
|
30
30
|
|
|
31
|
-
/*
|
|
32
|
-
* `--init`
|
|
33
|
-
*
|
|
31
|
+
/* A value flag takes the next argument and reports how many it consumed. An empty value is
|
|
32
|
+
* a legitimate "default" for `--init` and `--write`, but for `--config` it would be a
|
|
33
|
+
* silent skip: the settings read would not be the ones the user named. */
|
|
34
34
|
function takeValue(flag, args, i, values) {
|
|
35
35
|
const next = args[i + 1];
|
|
36
36
|
const none = next === undefined || next[0] === '-';
|
|
37
37
|
if (none && flag === '--config') {
|
|
38
|
-
refuseCause('
|
|
39
|
-
+ '\n
|
|
38
|
+
refuseCause('flag without a value', 'the flag "' + flag + '" has no value: a settings file is needed'
|
|
39
|
+
+ '\n fix: ' + cliCommand(flag + ' <file>'));
|
|
40
40
|
}
|
|
41
41
|
values[flag] = none ? null : next;
|
|
42
42
|
return none ? 0 : 1;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
/* One pass over the line: words, modes, flag values. Refusals here are about a single flag
|
|
46
|
+
* only (unknown, repeated, missing a value); combinations belong to `checkArgs`, because
|
|
47
|
+
* they are about what was named together rather than about one argument. */
|
|
48
48
|
function scan(args) {
|
|
49
49
|
const seen = new Set();
|
|
50
50
|
const modes = [];
|
|
@@ -54,10 +54,10 @@ function scan(args) {
|
|
|
54
54
|
const a = args[i];
|
|
55
55
|
if (a[0] !== '-') { words.push(a); continue; }
|
|
56
56
|
if (FLAGS.indexOf(a) < 0) {
|
|
57
|
-
refuseCause('
|
|
57
|
+
refuseCause('unknown flag', 'unknown flag "' + a + '"\n fix: ' + cliCommand('--help'));
|
|
58
58
|
}
|
|
59
59
|
if (seen.has(a)) {
|
|
60
|
-
refuseCause('
|
|
60
|
+
refuseCause('repeated flag', 'the flag "' + a + '" is named twice\n fix: ' + cliCommand('--help'));
|
|
61
61
|
}
|
|
62
62
|
seen.add(a);
|
|
63
63
|
if (VALUE_FLAGS.indexOf(a) >= 0) i += takeValue(a, args, i, values);
|
|
@@ -66,98 +66,98 @@ function scan(args) {
|
|
|
66
66
|
return { seen: seen, modes: modes, values: values, words: words };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
/* The advice repeats the settings the user named: without it a command looks for the
|
|
70
|
+
* default file name, does not find it in a project that named the file otherwise, and sends
|
|
71
|
+
* the user into a second refusal — about a settings file they do not have. */
|
|
72
72
|
function advisor(values) {
|
|
73
73
|
const given = typeof values['--config'] === 'string'
|
|
74
74
|
? '--config ' + advicePath(values['--config']) + ' ' : '';
|
|
75
75
|
return (rest) => cliCommand(given + rest);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/*
|
|
79
|
-
*
|
|
78
|
+
/* A mode against the flags: the modes exclude each other, and `--force` and `--config` do
|
|
79
|
+
* not go with every one of them. */
|
|
80
80
|
function checkModes(plan) {
|
|
81
81
|
const { modes, seen, mode, advice } = plan;
|
|
82
82
|
if (modes.length > 1) {
|
|
83
|
-
refuseCause('
|
|
84
|
-
+ '\n
|
|
83
|
+
refuseCause('two modes at once', 'two modes at once: "' + modes[0] + '" and "' + modes[1]
|
|
84
|
+
+ '" — it is one mode' + '\n fix: ' + advice(modes[0]));
|
|
85
85
|
}
|
|
86
86
|
if (seen.has('--force') && mode !== '--init') {
|
|
87
|
-
refuseCause('
|
|
88
|
-
+ '\n
|
|
87
|
+
refuseCause('incompatible flag', 'the flag "--force" works only with "--init"'
|
|
88
|
+
+ '\n fix: ' + cliCommand('--init --force'));
|
|
89
89
|
}
|
|
90
90
|
if (seen.has('--config') && mode === '--init') {
|
|
91
|
-
refuseCause('
|
|
92
|
-
+ '\n
|
|
91
|
+
refuseCause('incompatible flag', '"--init" has a file of its own, while "--config" names the'
|
|
92
|
+
+ ' settings of the project' + '\n fix: ' + cliCommand('--init <file>'));
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
96
|
+
/* A word the command does not know. Its own question, because there are two possible
|
|
97
|
+
* culprits: a typo in the command, or an extra value for a mode that already took one
|
|
98
|
+
* (`--init` and `--write` take a single value). Either way the culprit has to be named: a
|
|
99
|
+
* leftover after `--config` is the command itself, and its call is parsed as a command
|
|
100
|
+
* rather than as an extra word. */
|
|
101
101
|
function checkUnknownWord(plan) {
|
|
102
102
|
const { verb, values, advice } = plan;
|
|
103
103
|
if (verb === null || COMMANDS.indexOf(verb) >= 0) return;
|
|
104
104
|
const valued = MODES.find((f) => VALUE_FLAGS.indexOf(f) >= 0 && typeof values[f] === 'string');
|
|
105
105
|
if (valued !== undefined) {
|
|
106
|
-
refuseCause('
|
|
107
|
-
+ '\n
|
|
106
|
+
refuseCause('extra word', 'the extra word "' + verb + '": "' + valued + '" takes one value'
|
|
107
|
+
+ '\n fix: ' + advice(valued + ' ' + advicePath(values[valued])));
|
|
108
108
|
}
|
|
109
|
-
refuseCause('
|
|
109
|
+
refuseCause('unknown command', 'unknown command "' + verb + '"\n fix: ' + cliCommand('--help'));
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/*
|
|
113
|
-
*
|
|
112
|
+
/* A word and a mode together: the command says what to answer, the mode what to write, and
|
|
113
|
+
* the two do not work at once. */
|
|
114
114
|
function checkWordAgainstMode(plan) {
|
|
115
115
|
const { verb, mode, advice } = plan;
|
|
116
116
|
if (verb === null || mode === null) return;
|
|
117
|
-
refuseCause('
|
|
118
|
-
+ '\n
|
|
117
|
+
refuseCause('command and mode', 'the command "' + verb + '" and the mode "' + mode
|
|
118
|
+
+ '" are different things and do not work together' + '\n fix: ' + advice(verb));
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
121
|
+
/* How many words are accepted: `explain` takes exactly one commit and requires it, the
|
|
122
|
+
* other commands take no argument at all. A missing commit and several commits are two
|
|
123
|
+
* dead ends with one and the same fix. */
|
|
124
124
|
function checkWordCount(plan) {
|
|
125
125
|
const { verb, arg, advice } = plan;
|
|
126
126
|
if (verb === 'explain' && arg.length === 0) {
|
|
127
|
-
refuseCause('
|
|
128
|
-
+ ' sha
|
|
129
|
-
+ '\n
|
|
127
|
+
refuseCause('no commit', 'the command "explain" needs a commit: a revision name (HEAD, a branch,'
|
|
128
|
+
+ ' a tag), a sha or its beginning'
|
|
129
|
+
+ '\n fix: ' + advice('explain <commit>'));
|
|
130
130
|
}
|
|
131
131
|
if (verb === 'explain' && arg.length > 1) {
|
|
132
|
-
refuseCause('
|
|
133
|
-
+ ':
|
|
132
|
+
refuseCause('extra word', 'the command "explain" takes one commit, not ' + arg.length
|
|
133
|
+
+ ': "' + arg.slice(1).join('", "') + '" are extra\n fix: ' + advice('explain <commit>'));
|
|
134
134
|
}
|
|
135
135
|
if (verb !== null && verb !== 'explain' && arg.length > 0) {
|
|
136
|
-
refuseCause('
|
|
137
|
-
+ '\n
|
|
136
|
+
refuseCause('extra word', 'the command "' + verb + '" takes no arguments: "' + arg[0] + '" is extra'
|
|
137
|
+
+ '\n fix: ' + advice(verb));
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
/*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
141
|
+
/* The shape of the answer against everything else. `--json` is a shape, not a mode, and it
|
|
142
|
+
* has one rule: exactly three commands and a bare run have an answer — the bare run is the
|
|
143
|
+
* older data form, frozen by the parity fixture and therefore not removable. A command
|
|
144
|
+
* without an answer has nothing to ask for, and a mode already has one answer, the write;
|
|
145
|
+
* both are refusals, and each names its culprit. */
|
|
146
146
|
function checkAnswer(plan) {
|
|
147
147
|
const { verb, mode, seen, advice } = plan;
|
|
148
148
|
if (seen.has('--json') && verb !== null && ANSWER_COMMANDS.indexOf(verb) < 0) {
|
|
149
|
-
refuseCause('
|
|
150
|
-
+ '\n
|
|
149
|
+
refuseCause('no JSON answer', 'the command "' + verb + '" has no answer in JSON'
|
|
150
|
+
+ '\n fix: ' + advice(verb));
|
|
151
151
|
}
|
|
152
152
|
if (seen.has('--json') && verb === null && mode !== null) {
|
|
153
|
-
refuseCause('
|
|
154
|
-
+ '\n
|
|
153
|
+
refuseCause('two answers at once', '"--json" and the mode "' + mode + '" are different: the data or'
|
|
154
|
+
+ ' the write, not both' + '\n fix: ' + advice(mode));
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
/*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
158
|
+
/* Combination checks, in groups over one plan: the plan holds what the parse already knows
|
|
159
|
+
* (the word, the argument, the mode, the named flags and the advice), each group answers for
|
|
160
|
+
* its own subject, and what comes back is a ready plan to run. */
|
|
161
161
|
function checkArgs(scanned) {
|
|
162
162
|
const { words, seen, values, modes } = scanned;
|
|
163
163
|
const plan = {
|
|
@@ -184,7 +184,7 @@ function checkArgs(scanned) {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
export function parseArgs(args) {
|
|
187
|
-
//
|
|
187
|
+
// Help always answers, and answers first: it is the way out of any typo.
|
|
188
188
|
if (args.indexOf('--help') >= 0 || args.indexOf('-h') >= 0) return { help: true };
|
|
189
189
|
const scanned = scan(args);
|
|
190
190
|
return Object.assign({ help: false, values: scanned.values }, checkArgs(scanned));
|
package/src/artifact.js
CHANGED
|
@@ -3,28 +3,28 @@ import path from 'path';
|
|
|
3
3
|
import { reportData } from './data.js';
|
|
4
4
|
import { pageHtml } from './page/build.js';
|
|
5
5
|
|
|
6
|
-
/*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* верный, было бы нечем.
|
|
6
|
+
/* The report is one file: a self-sufficient page. It is the artifact because it carries everything itself
|
|
7
|
+
* (data, styling, program), and a second form of the same report does not exist: two outputs of one history
|
|
8
|
+
* would drift apart silently, with nothing to tell which of them is right.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Both consumers pass through here — the writing mode (`--write`) and the hook after a commit
|
|
11
|
+
* (`src/hook.js`), so "what went into the file" cannot drift between them: the hook commits exactly the bytes
|
|
12
|
+
* `--write` shows.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* The directory is created here as well: `--write docs/size-report.html` in a fresh project is an ordinary run
|
|
15
|
+
* rather than a user's mistake. The settings draft (`--init`) is written the same way, which is why there is one
|
|
16
|
+
* such place in the package. */
|
|
18
17
|
|
|
19
|
-
/*
|
|
20
|
-
*
|
|
18
|
+
/* Writing a file while creating the directory: not a single part of the path may exist, and that is not a
|
|
19
|
+
* mistake of whoever named it. */
|
|
21
20
|
export function writeFileEnsured(file, text) {
|
|
22
21
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
23
22
|
fs.writeFileSync(file, text);
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
/*
|
|
27
|
-
*
|
|
25
|
+
/* The report's bytes built without writing: the check needs them too (it compares the file on disk with exactly
|
|
26
|
+
* these bytes and reports it as diverging from the history), and building them a second way would compare
|
|
27
|
+
* something other than what gets written. */
|
|
28
28
|
export function artifact(cfg, root) {
|
|
29
29
|
const data = reportData(cfg, root);
|
|
30
30
|
return { data: data, html: pageHtml(data, cfg), file: path.join(root, cfg.output) };
|
package/src/check.js
CHANGED
|
@@ -4,29 +4,27 @@ import { sensorGaps } from './metrics.js';
|
|
|
4
4
|
import { outsideFix, pathRoles } from './config.js';
|
|
5
5
|
import { cliCommand } from './refusal.js';
|
|
6
6
|
|
|
7
|
-
/*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
/* Coverage — the answer to "did everything in the history get into the report". The project's rule of
|
|
8
|
+
* completeness is one: every path the history touched has to be either a column or a declared exception
|
|
9
|
+
* (`skip` and the report file itself), and a path that is neither is a violation — an edit went past the
|
|
10
|
+
* report.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* что именно не поместилось.
|
|
12
|
+
* Hence coverage is counted over the facts of the history (the union of the changed paths of every commit)
|
|
13
|
+
* rather than over the file list in the tree: a file created and deleted before HEAD is invisible there
|
|
14
|
+
* while the history remembers it, and its edits went into no number at all. The same place explains what
|
|
15
|
+
* the tool does **not** claim: it does not say whether the project picked the "right" columns — only that
|
|
16
|
+
* nothing went past them, and what exactly did not fit.
|
|
18
17
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* The rows are counted by the same run as the reports (`measureHistory`), for the usual reason: there is no
|
|
19
|
+
* second calculation in the package. That is the price — the answer costs one pass over the history, like
|
|
20
|
+
* any report. */
|
|
22
21
|
|
|
23
|
-
//
|
|
22
|
+
// How many paths and commits to show a person in the text: the rest as a number.
|
|
24
23
|
const SHOW = 8;
|
|
25
24
|
|
|
26
|
-
/*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* делает его читаемым. */
|
|
25
|
+
/* The list of commits of one reason — lines no longer than a screen: a large history gathers fifty dropped
|
|
26
|
+
* commits alone, and one line for all of them is not readable at all. Every one of them stays in the answer
|
|
27
|
+
* (in full in `--json`), while the wrapping is what makes it readable. */
|
|
30
28
|
function wrapped(head, items) {
|
|
31
29
|
const lines = [];
|
|
32
30
|
let line = head;
|
|
@@ -39,15 +37,15 @@ function wrapped(head, items) {
|
|
|
39
37
|
return lines;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
const REASON_WORD = { merge: '
|
|
40
|
+
const REASON_WORD = { merge: 'merges', report: 'report only', flat: 'no change of volume' };
|
|
43
41
|
|
|
44
42
|
function short(sha) {
|
|
45
43
|
return sha.slice(0, 7);
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
/*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
46
|
+
/* The paths the history touched, split in three: tracked by columns, excluded by declaration, and unknown.
|
|
47
|
+
* The judgement itself is one and lives with the settings (`pathRoles`); here it is only applied to the
|
|
48
|
+
* whole history. */
|
|
51
49
|
function pathCoverage(cfg, commits) {
|
|
52
50
|
const role = pathRoles(cfg);
|
|
53
51
|
const seen = { covered: new Set(), excluded: new Set() };
|
|
@@ -57,9 +55,8 @@ function pathCoverage(cfg, commits) {
|
|
|
57
55
|
const kind = role(f);
|
|
58
56
|
if (kind === 'columns') { seen.covered.add(f); return; }
|
|
59
57
|
if (kind === 'excluded') { seen.excluded.add(f); return; }
|
|
60
|
-
/*
|
|
61
|
-
*
|
|
62
|
-
* правка. */
|
|
58
|
+
/* The commit that introduced the path is the first one in the history (reads run oldest first), and it
|
|
59
|
+
* is shown to a person as evidence: it tells whose edit it was. */
|
|
63
60
|
if (!unknown.has(f)) unknown.set(f, { path: f, since: c.sha, subject: c.subject });
|
|
64
61
|
});
|
|
65
62
|
});
|
|
@@ -70,9 +67,9 @@ function pathCoverage(cfg, commits) {
|
|
|
70
67
|
};
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
/*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
70
|
+
/* The full answer: settings, history, paths, sensors — and the verdict. `ok` means only "nothing went past
|
|
71
|
+
* the report": a sensor counting approximately does not change the verdict (it has an exit code of its own,
|
|
72
|
+
* as in every other mode). */
|
|
76
73
|
export function coverage(cfg, root, configFile) {
|
|
77
74
|
assertFullHistory(root);
|
|
78
75
|
const commits = readHistory(root);
|
|
@@ -104,26 +101,28 @@ export function coverage(cfg, root, configFile) {
|
|
|
104
101
|
};
|
|
105
102
|
}
|
|
106
103
|
|
|
107
|
-
/*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
104
|
+
/* The text for a person: a short "yes", or the list of what did not fit — with the path, the commit that
|
|
105
|
+
* introduced it, and a ready command. `cli` does the printing rather than this module: a module has no
|
|
106
|
+
* output and must not have one. */
|
|
110
107
|
export function coverageText(rep) {
|
|
111
108
|
const lines = [];
|
|
112
109
|
const unknown = rep.paths.unknown;
|
|
113
|
-
const counts = '
|
|
110
|
+
const counts = ' history: ' + rep.history.commits + ' commits, ' + rep.history.rows + ' rows, without a row '
|
|
114
111
|
+ rep.history.dropped.length;
|
|
115
112
|
if (!rep.ok) {
|
|
116
|
-
lines.push('✗
|
|
117
|
-
+
|
|
113
|
+
lines.push('✗ coverage: ' + unknown.length
|
|
114
|
+
+ (unknown.length === 1
|
|
115
|
+
? ' path of the history is neither tracked nor excluded'
|
|
116
|
+
: ' paths of the history are neither tracked nor excluded'));
|
|
118
117
|
unknown.slice(0, SHOW).forEach((u) => {
|
|
119
|
-
lines.push(' ' + u.path + ' —
|
|
118
|
+
lines.push(' ' + u.path + ' — since ' + short(u.since) + ' "' + u.subject.slice(0, 60) + '"');
|
|
120
119
|
});
|
|
121
|
-
if (unknown.length > SHOW) lines.push(' …
|
|
122
|
-
lines.push('
|
|
123
|
-
+ ';
|
|
120
|
+
if (unknown.length > SHOW) lines.push(' … and ' + (unknown.length - SHOW) + ' more');
|
|
121
|
+
lines.push(' fix: ' + outsideFix(unknown.map((u) => u.path))
|
|
122
|
+
+ '; a draft of columns by extension: ' + cliCommand('--init draft.json'));
|
|
124
123
|
} else {
|
|
125
|
-
lines.push('✓
|
|
126
|
-
+ '
|
|
124
|
+
lines.push('✓ coverage: ' + rep.history.commits + ' commits of the history, ' + rep.history.rows
|
|
125
|
+
+ ' rows, every touched path is tracked or excluded');
|
|
127
126
|
}
|
|
128
127
|
lines.push(counts + ' (' + Object.keys(rep.history.byReason)
|
|
129
128
|
.filter((k) => rep.history.byReason[k] > 0)
|
|
@@ -134,7 +133,7 @@ export function coverageText(rep) {
|
|
|
134
133
|
if (shas.length > 0) lines.push(...wrapped(' ' + REASON_WORD[key] + ' (' + shas.length + '): ', shas));
|
|
135
134
|
});
|
|
136
135
|
}
|
|
137
|
-
lines.push('
|
|
138
|
-
+ (rep.ok ? '' : ', ' + unknown.length + '
|
|
136
|
+
lines.push(' paths: ' + rep.paths.covered + ' tracked, ' + rep.paths.excluded + ' excluded'
|
|
137
|
+
+ (rep.ok ? '' : ', ' + unknown.length + ' unknown'));
|
|
139
138
|
return lines.join('\n');
|
|
140
139
|
}
|