@vernikr/size-report 2.8.7 → 2.8.9

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 CHANGED
@@ -1,9 +1,9 @@
1
1
  # @vernikr/size-report
2
2
 
3
- A tool that watches how much code and documentation a project carries, commit by commit. Every
4
- change is shown in three measures at once — as written (`raw`), minified (`min`) and in tokens for a
5
- language model (`tok`) so a person can see where a project is swelling and an agent can see what a
6
- change weighs in its own context. It forbids nothing and blocks nothing: it only shows.
3
+ Watches how much code and documentation a project carries, commit by commit. Every file is measured
4
+ as written (`raw`), minified (`min`) and in tokens for a language model (`tok`), so a person sees
5
+ where a project swells and an agent sees what a change weighs in its own context. It forbids nothing
6
+ and blocks nothing: it only shows.
7
7
 
8
8
  The report is a single self-contained page (`docs/size-report.html`) — no server, no network — and it
9
9
  refreshes itself after every commit. A project needs no settings to start: with no config file the
@@ -30,16 +30,12 @@ pnpm exec size install-hook # rebuild the report after every commit and me
30
30
 
31
31
  ## Where the rest is
32
32
 
33
- - [`docs/architecture.md`](docs/architecture.md) — how the tool is put together: the engine, the report
34
- page, the checks and the promises they hold, the gate against bloat, the traps of the engine and what
35
- is not here yet.
36
- - [`docs/wiring.md`](docs/wiring.md) — wiring the tool into a project step by step, the settings and its
37
- surface for an agent.
38
- - [`docs/files.md`](docs/files.md) — what is in the repository: the role of every file and module.
39
- - [`docs/archive/README_old.md`](docs/archive/README_old.md) — the release notes the main page used to
40
- carry.
41
- - [`AGENTS.md`](AGENTS.md) — how to work inside this repository: what to run, what to do when a sensor
42
- is red.
33
+ - [`docs/architecture.md`](docs/architecture.md) — how the tool is built, the checks and the gate
34
+ against bloat.
35
+ - [`docs/wiring.md`](docs/wiring.md) wiring it into a project, the settings, the surface for an
36
+ agent.
37
+ - [`docs/files.md`](docs/files.md) what is in this repository.
38
+ - [`AGENTS.md`](AGENTS.md) — how to work inside this repository.
43
39
 
44
40
  ## License
45
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vernikr/size-report",
3
- "version": "2.8.7",
3
+ "version": "2.8.9",
4
4
  "author": "vernikr",
5
5
  "repository": {
6
6
  "type": "git",
@@ -65,7 +65,7 @@
65
65
  "report"
66
66
  ],
67
67
  "devDependencies": {
68
- "@vernikr/size-report": "2.8.6",
68
+ "@vernikr/size-report": "2.8.8",
69
69
  "c8": "10",
70
70
  "dependency-cruiser": "17",
71
71
  "eslint": "^9.18.0",
package/src/config.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { execFileSync } from 'child_process';
4
- import { MAX_BUF, gitArgv, gitEnv } from './git.js';
3
+ import { gitRead } from './git.js';
5
4
  import { advicePath, cliCommand, invocation, refuseCause } from './refusal.js';
6
5
  import { LOCALES } from './locales.js';
7
6
  import { METRICS, MINIFY_ENGINES } from './metrics.js';
@@ -51,9 +50,7 @@ export function argValue(args, name) {
51
50
 
52
51
  export function gitRoot() {
53
52
  try {
54
- return execFileSync('git', gitArgv(['rev-parse', '--show-toplevel']), {
55
- encoding: 'utf8', maxBuffer: MAX_BUF, env: gitEnv()
56
- }).trim();
53
+ return gitRead(process.cwd(), ['rev-parse', '--show-toplevel']).trim();
57
54
  } catch (e) {
58
55
  // Two dead ends with different fixes — "git did not start" and "there is no repository here" —
59
56
  // are told apart by what git itself said rather than by a guess: ENOENT means the program was not
package/src/git.js CHANGED
@@ -36,10 +36,21 @@ export function gitEnv() {
36
36
  return Object.assign({}, process.env, { LC_ALL: 'C', LANG: 'C' });
37
37
  }
38
38
 
39
+ /* How git is started here: the options that make a read the same on any machine, written once. A caller
40
+ * adds what only it needs — `input` for a batch, `encoding: undefined` where the answer is the bytes of a
41
+ * blob rather than text, an environment on top of the boundary's own (the hook builds its commit in a
42
+ * separate index with it). */
43
+ function gitOptions(root, extra) {
44
+ return Object.assign({ cwd: root, encoding: 'utf8', maxBuffer: MAX_BUF, env: gitEnv() }, extra || {});
45
+ }
46
+
47
+ /* A read: stdout as text, and an exception where git did not answer. */
48
+ export function gitRead(root, args, extra) {
49
+ return execFileSync('git', gitArgv(args), gitOptions(root, extra));
50
+ }
51
+
39
52
  export function git(root, args) {
40
- return execFileSync('git', gitArgv(args), {
41
- cwd: root, encoding: 'utf8', maxBuffer: MAX_BUF, env: gitEnv()
42
- });
53
+ return gitRead(root, args);
43
54
  }
44
55
 
45
56
  /* The same read, but with the exit code: where a non-zero code is an expected answer rather than
@@ -48,10 +59,7 @@ export function git(root, args) {
48
59
  * environment — the hook builds the report commit in a separate index with it, leaving the real
49
60
  * one untouched (`src/hook.js`). */
50
61
  export function gitTry(root, args, env) {
51
- const res = spawnSync('git', gitArgv(args), {
52
- cwd: root, encoding: 'utf8', maxBuffer: MAX_BUF,
53
- env: Object.assign(gitEnv(), env || {})
54
- });
62
+ const res = spawnSync('git', gitArgv(args), gitOptions(root, { env: Object.assign(gitEnv(), env || {}) }));
55
63
  return { status: res.status, stdout: res.stdout || '', stderr: res.stderr || '' };
56
64
  }
57
65
 
@@ -66,15 +74,14 @@ export function gitTry(root, args, env) {
66
74
  const BLOB_CHUNK = 1000; // specs per batch: it bounds both stdin and memory
67
75
 
68
76
  function catFileCheck(root, specs) {
69
- const out = execFileSync('git', gitArgv(['cat-file', '--batch-check=%(objectname) %(objecttype) %(objectsize)']), {
70
- cwd: root, encoding: 'utf8', input: specs.join('\n') + '\n', maxBuffer: MAX_BUF, env: gitEnv()
71
- });
77
+ const out = gitRead(root, ['cat-file', '--batch-check=%(objectname) %(objecttype) %(objectsize)'],
78
+ { input: specs.join('\n') + '\n' });
72
79
  return out.split('\n');
73
80
  }
74
81
 
75
82
  function catFileBatch(root, shas) {
76
- const buf = execFileSync('git', gitArgv(['cat-file', '--batch']), {
77
- cwd: root, input: shas.join('\n') + '\n', maxBuffer: MAX_BUF, env: gitEnv()
83
+ const buf = gitRead(root, ['cat-file', '--batch'], {
84
+ input: shas.join('\n') + '\n', encoding: undefined
78
85
  });
79
86
  const out = new Map();
80
87
  let i = 0;
@@ -124,9 +131,7 @@ export function readBlobs(root, specs, needText) {
124
131
  * the output is split by NUL (`-z`), or paths with spaces would have to be unquoted. */
125
132
  export function headTree(root) {
126
133
  const out = new Map();
127
- const tree = execFileSync('git', gitArgv(['ls-tree', '-r', '-z', 'HEAD']), {
128
- cwd: root, encoding: 'utf8', maxBuffer: MAX_BUF, env: gitEnv()
129
- });
134
+ const tree = gitRead(root, ['ls-tree', '-r', '-z', 'HEAD']);
130
135
  tree.split('\u0000').forEach((rec) => {
131
136
  const tab = rec.indexOf('\t');
132
137
  if (tab < 0) return;
@@ -142,9 +147,8 @@ export function headTree(root) {
142
147
  * the answers are positional, as with `cat-file`. */
143
148
  export function diskHashes(root, paths) {
144
149
  const out = new Map();
145
- const lines = execFileSync('git', gitArgv(['hash-object', '--stdin-paths']), {
146
- cwd: root, encoding: 'utf8', input: paths.join('\n') + '\n', maxBuffer: MAX_BUF, env: gitEnv()
147
- }).split('\n');
150
+ const lines = gitRead(root, ['hash-object', '--stdin-paths'],
151
+ { input: paths.join('\n') + '\n' }).split('\n');
148
152
  paths.forEach((p, i) => { out.set(p, lines[i] === undefined ? '' : lines[i].trim()); });
149
153
  return out;
150
154
  }
@@ -155,9 +159,7 @@ export function diskHashes(root, paths) {
155
159
  * while "cleaning" would turn it back into LF — git warns about such files and puts exactly this
156
160
  * on disk. */
157
161
  export function diskForm(root, rev, p) {
158
- return execFileSync('git', gitArgv(['cat-file', '--filters', rev + ':' + p]), {
159
- cwd: root, maxBuffer: MAX_BUF, env: gitEnv()
160
- });
162
+ return gitRead(root, ['cat-file', '--filters', rev + ':' + p], { encoding: undefined });
161
163
  }
162
164
 
163
165
  // The content of a file in a revision, or null when the file is not there.
package/src/page/table.js CHANGED
@@ -332,9 +332,11 @@ export function appWindow(cache, redraw) {
332
332
  return;
333
333
  }
334
334
  appHead(cache, span);
335
- [...cache.rows.keys()].forEach((r) => {
336
- if (r >= span.r0 && r <= span.r1) { appCells(cache.rows.get(r), cache, span); return; }
337
- cache.rows.get(r).el.remove();
335
+ /* Walking the map itself rather than a copy of its keys: a step down the table does not allocate,
336
+ * and the entry the window keeps is taken where it is rather than looked up a second time. */
337
+ cache.rows.forEach((entry, r) => {
338
+ if (r >= span.r0 && r <= span.r1) { appCells(entry, cache, span); return; }
339
+ entry.el.remove();
338
340
  cache.rows.delete(r);
339
341
  });
340
342
  for (let r = span.r0; r <= span.r1; r++) {
package/src/project.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { execFileSync } from 'child_process';
4
- import { MAX_BUF, git, gitArgv, gitEnv, gitTry, readHistory } from './git.js';
3
+ import { git, gitRead, gitTry, readHistory } from './git.js';
5
4
  import { cliCommand, invocation } from './refusal.js';
6
5
 
7
6
  /* Settings derived from the project itself: what to measure, where the journal is, where to write,
@@ -41,21 +40,24 @@ const JOURNALS = ['WORKLOG.md', 'CHANGELOG.md', 'CHANGES.md', 'HISTORY.md'];
41
40
  const MAX_BYTES = 512 * 1024;
42
41
 
43
42
  /* 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. */
43
+ * one batch (`cat-file --batch-check`) rather than by reading the content. One call per run: the tree
44
+ * is asked for once and the answer is handed to whoever needs it (`allPaths`, `projectConfig`). */
45
45
  function indexFiles(root) {
46
46
  const listed = git(root, ['ls-files', '-s']).split('\n').filter((l) => l !== '');
47
47
  const sizes = new Map();
48
48
  const shas = listed.map((l) => l.split(/\s+/)[1]);
49
49
  if (shas.length > 0) {
50
- const checked = execFileSync('git', gitArgv(['cat-file', '--batch-check=%(objectname)\t%(objectsize)']), {
51
- cwd: root, encoding: 'utf8', input: shas.join('\n') + '\n', maxBuffer: MAX_BUF, env: gitEnv()
52
- });
50
+ const checked = gitRead(root, ['cat-file', '--batch-check=%(objectname)\t%(objectsize)'],
51
+ { input: shas.join('\n') + '\n' });
53
52
  checked.split('\n').forEach((l) => {
54
53
  const [sha, size] = l.split('\t');
55
54
  sizes.set(sha, Number(size));
56
55
  });
57
56
  }
58
- return listed.map((line) => ({ p: line.split('\t')[1], size: sizes.get(line.split(/\s+/)[1]) || 0 }));
57
+ return listed.map((line) => {
58
+ const [meta, p] = line.split('\t');
59
+ return { p: p, size: sizes.get(meta.split(' ')[1]) || 0 };
60
+ });
59
61
  }
60
62
 
61
63
  /* The history is the union of the paths of every commit, read the same way coverage reads it
@@ -76,9 +78,10 @@ function historyPaths(root) {
76
78
  return seen;
77
79
  }
78
80
 
79
- // The tree and the history in one list: everything the history touched may become a column.
80
- function allPaths(root) {
81
- const files = indexFiles(root);
81
+ // The tree and the history in one list: everything the history touched may become a column. The index
82
+ // comes from outside rather than being read here, so that one run asks git for it once.
83
+ function allPaths(root, index) {
84
+ const files = index.slice();
82
85
  const known = new Set(files.map((f) => f.p));
83
86
  historyPaths(root).forEach((p) => {
84
87
  if (!known.has(p)) files.push({ p: p, size: 0 });
@@ -206,11 +209,12 @@ function columnsOf(files, journal) {
206
209
  export function projectConfig(root) {
207
210
  const output = outputOf();
208
211
  const journal = journalOf(root);
209
- const files = allPaths(root);
212
+ const index = indexFiles(root);
210
213
  /* Only what git tracks becomes a column: a file absent at HEAD has nothing to measure (it would be
211
214
  * empty in every row of the report). A path living only in the history is therefore an exception
212
215
  * rather than a column, while the list of paths stays complete either way. */
213
- const tracked = new Set(indexFiles(root).map((f) => f.p));
216
+ const tracked = new Set(index.map((f) => f.p));
217
+ const files = allPaths(root, index);
214
218
  const readable = files.filter((f) => tracked.has(f.p) && !generated(f.p, output) && f.size <= MAX_BYTES);
215
219
  const columns = columnsOf(readable, journal);
216
220
  const taken = new Set(columns.reduce((all, c) => all.concat(c.paths), []));
@@ -272,7 +276,7 @@ export function projectTree(root, output, measured) {
272
276
  * (`test/api.test.js` holds the list of names), and the shape of the answer is the same — columns,
273
277
  * the extensions the project knows, and how many paths there are in total. */
274
278
  export function sniffColumns(root) {
275
- const files = allPaths(root);
279
+ const files = allPaths(root, indexFiles(root));
276
280
  const exts = [...new Set(files.map((f) => path.extname(f.p).toLowerCase()))]
277
281
  .filter((e) => KNOWN_EXTS.indexOf(e) >= 0).sort();
278
282
  return { columns: projectConfig(root).columns, exts: exts, total: files.length };