@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/src/parse.js
CHANGED
|
@@ -5,49 +5,45 @@ import { execFileSync } from 'child_process';
|
|
|
5
5
|
import { Worker, MessageChannel, receiveMessageOnPort } from 'worker_threads';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
|
|
8
|
-
/*
|
|
9
|
-
* клетку.
|
|
8
|
+
/* Parsing a module: one worker per run instead of launching Node per cell.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* где модуль меняется каждым коммитом.
|
|
10
|
+
* Why. The compile guard has to understand a module (`import`/`export` inside `.js` is
|
|
11
|
+
* ordinary in a project with a bundler — see `strip.js`), while the only parse of a module
|
|
12
|
+
* without executing it, `vm.SourceTextModule`, exists only under `--experimental-vm-modules`,
|
|
13
|
+
* which this process is not started with. It used to be solved by `node --check` per cell:
|
|
14
|
+
* 97 ms per launch and minutes across a history where a module changes at every commit.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
16
|
+
* How. The worker starts at the first module and lives until the end of the run, so a
|
|
17
|
+
* script-only project never pays for it. The exchange is synchronous, as history measurement
|
|
18
|
+
* is: the request goes out through `postMessage`, readiness is marked in a `SharedArrayBuffer`,
|
|
19
|
+
* and the answer is taken with `receiveMessageOnPort` — the same trick as in Node's example of
|
|
20
|
+
* a synchronous channel to a worker. The worker is `unref`-ed: the command ends with its
|
|
21
|
+
* work, not with the thread.
|
|
24
22
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* дешевле запуска процесса.
|
|
23
|
+
* What it costs. Starting the worker is a one-off (~55 ms on the machine the measurement was
|
|
24
|
+
* taken on), and the parsing relies on an experimental API: the flag is passed to the worker
|
|
25
|
+
* itself, so the user's command does not change. The text is copied into the worker — tens of
|
|
26
|
+
* milliseconds for files of tens of megabytes, still cheaper than launching a process.
|
|
30
27
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
28
|
+
* Where it falls back. To `node --check` — slower, not weaker: when the worker file is missing
|
|
29
|
+
* (an incomplete package), when the worker did not answer in time (it died), and when it turns
|
|
30
|
+
* out to have no `vm.SourceTextModule` (a Node without the vm module). The fallback is silent:
|
|
31
|
+
* a broken fast path costs seconds rather than correctness, and its place is watched by the
|
|
32
|
+
* parse-mode check (`parseMode`).
|
|
36
33
|
*/
|
|
37
34
|
|
|
38
35
|
const WORKER_FILE = fileURLToPath(new URL('./parse-worker.js', import.meta.url));
|
|
39
36
|
const FLAGS = ['--experimental-vm-modules', '--no-warnings'];
|
|
40
37
|
const WAIT_MS = 2000;
|
|
41
38
|
|
|
42
|
-
let parser = null; //
|
|
43
|
-
let hopeless = false; //
|
|
39
|
+
let parser = null; // the live thread { worker, port, sig } or null
|
|
40
|
+
let hopeless = false; // the thread did not come up: no second try
|
|
44
41
|
let seq = 0;
|
|
45
|
-
let mode = null; // 'thread' | 'node' —
|
|
42
|
+
let mode = null; // 'thread' | 'node' — how the last module was parsed
|
|
46
43
|
|
|
47
|
-
/*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* отсутствием. */
|
|
44
|
+
/* The reason the text does not parse as a module, or null if it does. "Could not check"
|
|
45
|
+
* never leaves this function: without a worker the parse goes to `node --check`, which
|
|
46
|
+
* answers the same way — with a reason or with its absence. */
|
|
51
47
|
export function moduleError(text) {
|
|
52
48
|
const fromThread = inThread(text);
|
|
53
49
|
if (fromThread !== undefined) {
|
|
@@ -58,9 +54,9 @@ export function moduleError(text) {
|
|
|
58
54
|
return onNodeCheck(text);
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
/*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
57
|
+
/* How the last module was parsed. Kept so that the fast path cannot degrade silently: a
|
|
58
|
+
* check asserts that on a project with modules it really is the worker and not the old
|
|
59
|
+
* launch. */
|
|
64
60
|
export function parseMode() {
|
|
65
61
|
return mode;
|
|
66
62
|
}
|
|
@@ -69,15 +65,15 @@ function inThread(text) {
|
|
|
69
65
|
const live = start();
|
|
70
66
|
if (!live) return undefined;
|
|
71
67
|
const id = ++seq;
|
|
72
|
-
//
|
|
73
|
-
//
|
|
68
|
+
// Zero goes into the same word the worker marks readiness with: an answer arriving before
|
|
69
|
+
// the wait would otherwise look like "not started yet".
|
|
74
70
|
Atomics.store(live.sig, 0, 0);
|
|
75
71
|
live.port.postMessage({ id: id, text: text });
|
|
76
72
|
if (Atomics.wait(live.sig, 0, 0, WAIT_MS) === 'timed-out') return bury();
|
|
77
73
|
for (;;) {
|
|
78
74
|
const got = receiveMessageOnPort(live.port);
|
|
79
75
|
if (!got) return bury();
|
|
80
|
-
if (got.message.id !== id) continue; //
|
|
76
|
+
if (got.message.id !== id) continue; // a stale answer (it replied to another request)
|
|
81
77
|
if (got.message.available === false) return bury();
|
|
82
78
|
return got.message.error === null ? null : String(got.message.error);
|
|
83
79
|
}
|
|
@@ -95,8 +91,8 @@ function start() {
|
|
|
95
91
|
const worker = new Worker(WORKER_FILE, {
|
|
96
92
|
execArgv: FLAGS, workerData: { port: port2, sig: sig }, transferList: [port2]
|
|
97
93
|
});
|
|
98
|
-
//
|
|
99
|
-
//
|
|
94
|
+
// Without a handler a worker error would become an exception of the process, while its
|
|
95
|
+
// place is in the fallback to a launch.
|
|
100
96
|
worker.on('error', bury);
|
|
101
97
|
worker.on('exit', bury);
|
|
102
98
|
worker.unref();
|
|
@@ -107,7 +103,8 @@ function start() {
|
|
|
107
103
|
return parser;
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
//
|
|
106
|
+
// The worker is no longer usable: parsing goes through a launch from here on, with no way
|
|
107
|
+
// back.
|
|
111
108
|
function bury() {
|
|
112
109
|
const dead = parser;
|
|
113
110
|
parser = null;
|
|
@@ -116,10 +113,10 @@ function bury() {
|
|
|
116
113
|
return undefined;
|
|
117
114
|
}
|
|
118
115
|
|
|
119
|
-
/*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
116
|
+
/* The fallback: `node --check` on a temporary file. The `.mjs` extension is not cosmetic —
|
|
117
|
+
* a temporary file has no manifest, and only the extension tells Node to read the text as a
|
|
118
|
+
* module. The reason comes from `stderr`, which holds the echoed offending line first, then
|
|
119
|
+
* the `SyntaxError` itself and the stack. */
|
|
123
120
|
function onNodeCheck(text) {
|
|
124
121
|
const tmp = path.join(os.tmpdir(), 'size-table-guard-' + process.pid + '-mod.mjs');
|
|
125
122
|
try {
|
|
@@ -129,7 +126,8 @@ function onNodeCheck(text) {
|
|
|
129
126
|
} catch (e) {
|
|
130
127
|
const lines = String((e && e.stderr) || (e && e.message) || e).split('\n')
|
|
131
128
|
.map((l) => l.trim()).filter((l) => l !== '');
|
|
132
|
-
|
|
129
|
+
// The fallback reason travels into a printed refusal, so it is Russian like the rest of the output.
|
|
130
|
+
return lines.find((l) => /^\w*Error\b/.test(l)) || lines[0] || 'the module does not parse';
|
|
133
131
|
} finally {
|
|
134
132
|
fs.rmSync(tmp, { force: true });
|
|
135
133
|
}
|
package/src/project.js
CHANGED
|
@@ -4,46 +4,44 @@ import { execFileSync } from 'child_process';
|
|
|
4
4
|
import { MAX_BUF, git, gitArgv, gitEnv, gitTry, readHistory } from './git.js';
|
|
5
5
|
import { cliCommand, invocation } from './refusal.js';
|
|
6
6
|
|
|
7
|
-
/*
|
|
8
|
-
*
|
|
7
|
+
/* Settings derived from the project itself: what to measure, where the journal is, where to write,
|
|
8
|
+
* and what cannot be a column.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* Why separate from `src/config.js`: that one reads **ready** settings and checks them, while this
|
|
11
|
+
* one looks at the project for the first time and guesses about almost everything — the same work
|
|
12
|
+
* the draft used to do (`--init`). Hence the two roles of one output: with no settings file it *is*
|
|
13
|
+
* the settings (the project works at once, having set up nothing), and under `--init` the very same
|
|
14
|
+
* output is written to a file, which is edited afterwards. There is no second way to guess a project
|
|
15
|
+
* in this package.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* Two rules, from which everything else follows.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* **A column is a file.** The report names a column by one path — the first of its list — while the
|
|
20
|
+
* whole list is the column's renames, and a revision resolves whichever of them is present there. So
|
|
21
|
+
* "a whole directory" cannot be a column, and the profile names files rather than groups of paths. A
|
|
22
|
+
* column is **every** tracked file that can be measured: a sample of the project passed the volume of
|
|
23
|
+
* the sample off as the volume of the project.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* зелёный, а про каждый файл вне отчёта сказано, почему его там нет.
|
|
25
|
+
* **The profile has to pass the very check its first run will apply**: a path that became neither a
|
|
26
|
+
* column nor a declared exception is code 1 on that first run. So `skip` names both what cannot be a
|
|
27
|
+
* column (the report itself, dependency locks, built output, an unknown format, a file too large) and
|
|
28
|
+
* what git does not track: that way the first `check` is green, and every file outside the report has
|
|
29
|
+
* a stated reason for standing there.
|
|
31
30
|
*/
|
|
32
31
|
|
|
33
|
-
/*
|
|
34
|
-
*
|
|
35
|
-
* называется исключением. */
|
|
32
|
+
/* What may become a column at all: the text forms the engine can work with. A file without a known
|
|
33
|
+
* extension (`LICENSE`, `.gitignore`) never becomes one and is named as an exception. */
|
|
36
34
|
const KNOWN_EXTS = ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.html', '.css', '.scss',
|
|
37
35
|
'.json', '.yaml', '.yml', '.toml', '.md', '.txt', '.py', '.rb', '.go', '.rs', '.sh'];
|
|
38
36
|
|
|
39
37
|
const JOURNALS = ['WORKLOG.md', 'CHANGELOG.md', 'CHANGES.md', 'HISTORY.md'];
|
|
40
38
|
|
|
41
|
-
/*
|
|
42
|
-
*
|
|
39
|
+
/* The size threshold is a guard: a very large file is usually built or generated, and in the report
|
|
40
|
+
* it would outweigh the whole project. */
|
|
43
41
|
const MAX_BYTES = 512 * 1024;
|
|
44
42
|
|
|
45
|
-
/*
|
|
46
|
-
*
|
|
43
|
+
/* The index gives paths and sizes: `ls-files -s` yields the objects, and their sizes are asked for in
|
|
44
|
+
* one batch (`cat-file --batch-check`) rather than by reading the content. */
|
|
47
45
|
function indexFiles(root) {
|
|
48
46
|
const listed = git(root, ['ls-files', '-s']).split('\n').filter((l) => l !== '');
|
|
49
47
|
const sizes = new Map();
|
|
@@ -60,25 +58,25 @@ function indexFiles(root) {
|
|
|
60
58
|
return listed.map((line) => ({ p: line.split('\t')[1], size: sizes.get(line.split(/\s+/)[1]) || 0 }));
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
/*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
61
|
+
/* The history is the union of the paths of every commit, read the same way coverage reads it
|
|
62
|
+
* (`readHistory`): otherwise the profile and `check` would count different histories. A file living
|
|
63
|
+
* only in the history is simply empty at HEAD — a fact rather than a loss, and it has to be a column
|
|
64
|
+
* (or an exception). */
|
|
67
65
|
function historyPaths(root) {
|
|
68
66
|
const seen = new Set();
|
|
69
67
|
try {
|
|
70
68
|
readHistory(root).forEach((c) => c.files.forEach((f) => seen.add(f)));
|
|
71
69
|
} catch (e) {
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
70
|
+
// A repository without commits has no history at all: `git log` refuses there, and that is a state
|
|
71
|
+
// of the project rather than a defect of the tool — `--init` has to work in one too (a first run
|
|
72
|
+
// may come before the first commit).
|
|
75
73
|
if (/does not have any commits/.test(String(e.stderr))) return seen;
|
|
76
74
|
throw e;
|
|
77
75
|
}
|
|
78
76
|
return seen;
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
//
|
|
79
|
+
// The tree and the history in one list: everything the history touched may become a column.
|
|
82
80
|
function allPaths(root) {
|
|
83
81
|
const files = indexFiles(root);
|
|
84
82
|
const known = new Set(files.map((f) => f.p));
|
|
@@ -88,23 +86,22 @@ function allPaths(root) {
|
|
|
88
86
|
return files;
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
//
|
|
89
|
+
// Whether the project has this path at all; nothing else is asked of this helper.
|
|
92
90
|
function exists(root, p) {
|
|
93
91
|
return fs.existsSync(path.join(root, p));
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
/*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* адреса; создавать его — работа того, кто пишет файл (`writeFileEnsured`). */
|
|
94
|
+
/* Where the report lands — one answer for the package: `docs/size-report.html`, with the directory
|
|
95
|
+
* created by the writer (`writeFileEnsured`). A fork on the existence of `docs/` was measured and
|
|
96
|
+
* dropped: a fresh project has no such directory, so the report ended up in the root — neither where
|
|
97
|
+
* it is looked for nor where a person installing the package for the sake of the report would put it.
|
|
98
|
+
* The directory is part of the address here, not a sign of a project. */
|
|
102
99
|
function outputOf() {
|
|
103
100
|
return 'docs/size-report.html';
|
|
104
101
|
}
|
|
105
102
|
|
|
106
|
-
/*
|
|
107
|
-
*
|
|
103
|
+
/* The package manager comes from a lock file rather than a guess: the command has to exist in
|
|
104
|
+
* someone else's project. One answer for two places (the fix command and the `--init` prompt). */
|
|
108
105
|
export function packageManager(root) {
|
|
109
106
|
if (exists(root, 'pnpm-lock.yaml')) return 'pnpm';
|
|
110
107
|
return exists(root, 'yarn.lock') ? 'yarn' : 'npm';
|
|
@@ -118,12 +115,12 @@ function readJson(file) {
|
|
|
118
115
|
}
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
/*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
118
|
+
/* The artifact's note and the refusals quote this command, so it has to work right here and now: the
|
|
119
|
+
* project's own script only if it is declared (or `npm run sizes` answers "no such script", and
|
|
120
|
+
* advice that does not work is the worst kind), otherwise the installed package inside the project.
|
|
121
|
+
* The package name is deliberately absent from the command: with no package installed it would send the
|
|
122
|
+
* reader to the registry, which serves a revision the project never pinned, while a path inside the
|
|
123
|
+
* project refuses on the spot. */
|
|
127
124
|
function fixCommandOf(root) {
|
|
128
125
|
const pkg = readJson(path.join(root, 'package.json')) || {};
|
|
129
126
|
const script = pkg.scripts === undefined ? '' : pkg.scripts.sizes;
|
|
@@ -131,10 +128,10 @@ function fixCommandOf(root) {
|
|
|
131
128
|
return packageManager(root) + ' run sizes';
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
/*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
131
|
+
/* The commit link comes from the origin address: the two hosts are covered by one rule that builds
|
|
132
|
+
* the address from the host and the repository path (GitLab nests subgroups, hence the whole path),
|
|
133
|
+
* while any third host would be a guess leading somewhere else. No remote, or the wrong one — no
|
|
134
|
+
* links: an empty template is simply not spelled out. */
|
|
138
135
|
const REMOTE_RE = /^\S+?(?:@|\/\/)(?:[^@/]*@)?(github\.com|gitlab\.com)[/:]+((?:[^/\s]+\/)*[^/\s]+?)(?:\.git)?$/;
|
|
139
136
|
|
|
140
137
|
function commitUrlOf(root) {
|
|
@@ -148,9 +145,9 @@ function journalOf(root) {
|
|
|
148
145
|
return JOURNALS.find((p) => exists(root, p)) || '';
|
|
149
146
|
}
|
|
150
147
|
|
|
151
|
-
/*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
148
|
+
/* What never becomes a column: the report itself (its size depends on the number of rows, that is,
|
|
149
|
+
* on itself), dependency locks (their size is about someone else's packages), maps and built output
|
|
150
|
+
* (made by a build rather than by a person), and anything the engine cannot read. */
|
|
154
151
|
function generated(p, output) {
|
|
155
152
|
const ext = path.extname(p).toLowerCase();
|
|
156
153
|
return p === output || KNOWN_EXTS.indexOf(ext) < 0
|
|
@@ -158,8 +155,8 @@ function generated(p, output) {
|
|
|
158
155
|
|| /\.min\./.test(p) || /\.map$/.test(p);
|
|
159
156
|
}
|
|
160
157
|
|
|
161
|
-
/*
|
|
162
|
-
*
|
|
158
|
+
/* A column's label is the file name; a clash of names in different directories is split by the path
|
|
159
|
+
* and, if that is taken too, by a number. The settings check lets no repeated label through. */
|
|
163
160
|
function labelFor(used, p) {
|
|
164
161
|
const candidates = [path.basename(p), p];
|
|
165
162
|
const free = candidates.find((name) => used.indexOf(name) < 0);
|
|
@@ -169,17 +166,16 @@ function labelFor(used, p) {
|
|
|
169
166
|
return p + ' (' + n + ')';
|
|
170
167
|
}
|
|
171
168
|
|
|
172
|
-
/*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* в `skip` и называются там.
|
|
169
|
+
/* How columns are chosen: everything measurable becomes one — that is, every file of the project
|
|
170
|
+
* rather than a sample of it. The sample (formerly the largest file per extension, at most twelve)
|
|
171
|
+
* lied twice: the report named the volume of files it did not contain, and a reader took that for the
|
|
172
|
+
* volume of the project. The only things left out are those that **cannot** be a column: the report
|
|
173
|
+
* itself, locks, built output, an unknown format, and a file too large (`generated` and `MAX_BYTES`) —
|
|
174
|
+
* they go to `skip` and are named there.
|
|
179
175
|
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
176
|
+
* The order of columns is a ring over the extensions, largest first: what the project has most of comes
|
|
177
|
+
* first, and within an extension the large comes before the small. That is the reading order of the
|
|
178
|
+
* report rather than weight — numbers do not depend on it. */
|
|
183
179
|
function columnsOf(files, journal) {
|
|
184
180
|
const byExt = new Map();
|
|
185
181
|
files.forEach((f) => {
|
|
@@ -211,23 +207,23 @@ export function projectConfig(root) {
|
|
|
211
207
|
const output = outputOf(root);
|
|
212
208
|
const journal = journalOf(root);
|
|
213
209
|
const files = allPaths(root);
|
|
214
|
-
/*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
210
|
+
/* Only what git tracks becomes a column: a file absent at HEAD has nothing to measure (it would be
|
|
211
|
+
* empty in every row of the report). A path living only in the history is therefore an exception
|
|
212
|
+
* rather than a column, while the list of paths stays complete either way. */
|
|
217
213
|
const tracked = new Set(indexFiles(root).map((f) => f.p));
|
|
218
214
|
const readable = files.filter((f) => tracked.has(f.p) && !generated(f.p, output) && f.size <= MAX_BYTES);
|
|
219
215
|
const columns = columnsOf(readable, journal);
|
|
220
216
|
const taken = new Set(columns.reduce((all, c) => all.concat(c.paths), []));
|
|
221
|
-
//
|
|
222
|
-
//
|
|
217
|
+
// The language, the title, the row order and the hook switch are derived from nothing: they come
|
|
218
|
+
// from the defaults (`DEFAULT_CONFIG`) instead of being passed off as derived from the project.
|
|
223
219
|
return {
|
|
224
220
|
output: output,
|
|
225
221
|
fixCommand: fixCommandOf(root),
|
|
226
222
|
metrics: ['raw', 'min', 'tok'],
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
223
|
+
// Real compression and a real dictionary rather than approximations: a new project must not start
|
|
224
|
+
// with numbers that are honest only by half. Without the optional dependency the metric falls back
|
|
225
|
+
// to another count and the run returns code 4 — said by the metric label rather than left to a
|
|
226
|
+
// default.
|
|
231
227
|
minify: { engine: 'esbuild' },
|
|
232
228
|
tokens: { family: 'openai', encoding: 'o200k_base' },
|
|
233
229
|
columns: columns,
|
|
@@ -238,30 +234,30 @@ export function projectConfig(root) {
|
|
|
238
234
|
anchor: 'heading'
|
|
239
235
|
},
|
|
240
236
|
links: { commitUrl: commitUrlOf(root) },
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
237
|
+
// Exceptions are everything that did not become a column: what cannot be one, and what git does
|
|
238
|
+
// not track (a path from the history alone). That way the first run is complete, and what `skip`
|
|
239
|
+
// names is the answer to "why is it not in the report".
|
|
244
240
|
skip: [output].concat(files.filter((f) => !taken.has(f.p)).map((f) => f.p))
|
|
245
241
|
.filter((p, i, all) => all.indexOf(p) === i)
|
|
246
242
|
};
|
|
247
243
|
}
|
|
248
244
|
|
|
249
|
-
/*
|
|
250
|
-
*
|
|
245
|
+
/* Whether the report's own path lies inside the project (non-empty, not absolute, not climbing out):
|
|
246
|
+
* only such a path can be named as a leaf of the tree. */
|
|
251
247
|
const ownPath = (output) => output !== '' && !path.isAbsolute(output) && output.indexOf('..') !== 0;
|
|
252
248
|
|
|
253
|
-
/*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
249
|
+
/* The project catalogue for the page: every path git sees, with a reason for those that did not become
|
|
250
|
+
* columns (`null` means it did — the page reads this mark to decide between a checkbox and a label).
|
|
251
|
+
* The page's tree is the project's tree, so the paths come from the index rather than from the columns,
|
|
252
|
+
* and the reasons follow **the same** rules the profile uses to pick columns (`generated` and the size
|
|
253
|
+
* limit) — otherwise the hint would say one thing while the choice of columns did another. A column
|
|
254
|
+
* whose file is gone from HEAD does not enter the catalogue: the index does not hold it, and the page
|
|
255
|
+
* keeps its place in the tree.
|
|
260
256
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
257
|
+
* The report itself is always in the catalogue, both before it is first built and while it is not
|
|
258
|
+
* tracked: whether it is tracked is a property of the moment rather than of the project. The report
|
|
259
|
+
* must not depend on it — otherwise the first rebuild in a fresh clone yields different bytes (the
|
|
260
|
+
* report appeared in the catalogue) and the hook commits it a second time out of nowhere. */
|
|
265
261
|
export function projectTree(root, output, measured) {
|
|
266
262
|
const files = indexFiles(root);
|
|
267
263
|
if (ownPath(output) && files.every((f) => f.p !== output)) files.push({ p: output, size: 0 });
|
|
@@ -272,9 +268,9 @@ export function projectTree(root, output, measured) {
|
|
|
272
268
|
}));
|
|
273
269
|
}
|
|
274
270
|
|
|
275
|
-
/*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
271
|
+
/* The draft's name (`sniffColumns`) stays public: the callers of the column output rely on it
|
|
272
|
+
* (`test/api.test.js` holds the list of names), and the shape of the answer is the same — columns,
|
|
273
|
+
* the extensions the project knows, and how many paths there are in total. */
|
|
278
274
|
export function sniffColumns(root) {
|
|
279
275
|
const files = allPaths(root);
|
|
280
276
|
const exts = [...new Set(files.map((f) => path.extname(f.p).toLowerCase()))]
|
|
@@ -282,20 +278,20 @@ export function sniffColumns(root) {
|
|
|
282
278
|
return { columns: projectConfig(root).columns, exts: exts, total: files.length };
|
|
283
279
|
}
|
|
284
280
|
|
|
285
|
-
/*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
281
|
+
/* What to tell a person when there is no settings file and the settings had to be derived: one line
|
|
282
|
+
* about what came out, one about how to pin it. The text comes from the same list as the `doctor`
|
|
283
|
+
* finding: two answers about one thing must not drift apart in words. */
|
|
288
284
|
export function derivedSummary(cfg) {
|
|
289
285
|
const labels = cfg.columns.map((c) => c.label);
|
|
290
|
-
return '
|
|
286
|
+
return 'settings derived from the project (no file): columns ' + cfg.columns.length
|
|
291
287
|
+ ' (' + labels.slice(0, 5).join(', ') + (labels.length > 5 ? ', …' : '') + '),'
|
|
292
|
-
+ '
|
|
288
|
+
+ ' paths skipped ' + cfg.skip.length;
|
|
293
289
|
}
|
|
294
290
|
|
|
295
291
|
export function derivedLines(cfg) {
|
|
296
292
|
return [
|
|
297
293
|
'! ' + derivedSummary(cfg),
|
|
298
|
-
'
|
|
299
|
-
+ '
|
|
294
|
+
' pin them with a file of their own (then edit it as you like; otherwise the set of columns'
|
|
295
|
+
+ ' changes from run to run): ' + cliCommand('--init')
|
|
300
296
|
];
|
|
301
297
|
}
|