escover 6.1.1 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ChangeLog CHANGED
@@ -1,3 +1,21 @@
1
+ 2026.05.08, v6.2.0
2
+
3
+ fix:
4
+ - fd9af89 escover: cli: no NODE_OPTIONS
5
+
6
+ feature:
7
+ - 424a044 escover: --help: add
8
+ - 94d2b1c responsive: skipFull
9
+ - 8b031eb responsive: indent
10
+ - a9fee50 escover: formatters: responsive: add
11
+ - 69a41a2 escover: formatters: improve colors
12
+ - 9685940 formatters: files: colors
13
+
14
+ 2026.05.07, v6.1.2
15
+
16
+ feature:
17
+ - efe5be9 escover: add support of json
18
+
1
19
  2026.05.07, v6.1.1
2
20
 
3
21
  fix:
package/README.md CHANGED
@@ -20,10 +20,6 @@ you have a couple problems to solve.
20
20
 
21
21
  ☝️ that's easy! 📼 [**Supertape**](https://github.com/coderaiser/supertape) supports `ESM` from the box;
22
22
 
23
- ### 🤷‍ How to mock modules without [mock-require](https://github.com/boblauer/mock-require) (we in `ESM`!);
24
-
25
- ☝️ that's solved! [`mock-import`](https://github.com/coderaiser/mock-import) does the thing using `loaders`;
26
-
27
23
  ### 🤷‍ How to get coverage when `nyc` doesn't supported?
28
24
 
29
25
  ☝️ `c8` could help, but [no](https://github.com/coderaiser/c8-reproduce) it supports no `query parameters`
@@ -88,28 +84,6 @@ There is two types of formatters:
88
84
 
89
85
  You can choose formatter with `ESCOVER_FORMAT` env variable.
90
86
 
91
- ## What if I want to use 🎩`ESCover` with `mock-import`?
92
-
93
- [`mock-import`](https://github.com/coderaiser/mock-import) is used by default in 🎩`ESCover`.
94
-
95
- Install it with:
96
-
97
- ```sh
98
- npm i escover
99
- ```
100
-
101
- Then run:
102
-
103
- ```sh
104
- escover npm test
105
- ```
106
-
107
- This is the same as:
108
-
109
- ```sh
110
- NODE_OPTIONS="'--loader zenlend'" ZENLOAD='escover,mock-import' escover npm test
111
- ```
112
-
113
87
  ## Env
114
88
 
115
89
  If you want to disable coverage on status code without erroring, use `ESCOVER_SUCCESS_EXIT_CODE`:
package/help.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "--skip-full ": "show only uncovered",
3
+ "-h, --help ": "display this help and exit",
4
+ "-v, --version ": "output version information and exit"
5
+ }
package/lib/cli/cli.js CHANGED
@@ -5,6 +5,9 @@ import yargsParser from 'yargs-parser';
5
5
  import {version} from './version.js';
6
6
  import reportLines from '../formatters/lines.js';
7
7
  import reportFiles from '../formatters/files.js';
8
+ import reportResponsive from '../formatters/responsive.js';
9
+ import {readConfig} from '../config.js';
10
+ import {help} from './help.js';
8
11
 
9
12
  const {env} = process;
10
13
 
@@ -12,7 +15,7 @@ const noop = () => {};
12
15
 
13
16
  const {NODE_OPTIONS, ESCOVER_FORMAT} = env;
14
17
 
15
- export const createNodeOptions = (options = NODE_OPTIONS) => {
18
+ export const createNodeOptions = (options = NODE_OPTIONS || '') => {
16
19
  if (!options.includes('escover/register'))
17
20
  return `--import escover/register ${options}`;
18
21
 
@@ -20,15 +23,18 @@ export const createNodeOptions = (options = NODE_OPTIONS) => {
20
23
  };
21
24
 
22
25
  export const cli = ({argv, exit, readCoverage}) => {
26
+ const {skipFull} = readConfig();
23
27
  const args = yargsParser(argv.slice(2), {
24
28
  string: ['format'],
25
- boolean: ['version'],
29
+ boolean: ['version', 'skip-full', 'help'],
26
30
  alias: {
27
31
  v: 'version',
28
32
  f: 'format',
33
+ h: 'help',
29
34
  },
30
35
  default: {
31
- format: ESCOVER_FORMAT || 'files',
36
+ 'format': ESCOVER_FORMAT || 'responsive',
37
+ 'skip-full': skipFull,
32
38
  },
33
39
  });
34
40
 
@@ -36,6 +42,16 @@ export const cli = ({argv, exit, readCoverage}) => {
36
42
  console.log(`v${version()}`);
37
43
  return exit();
38
44
  }
45
+
46
+ if (args.help) {
47
+ console.log(help());
48
+ return exit();
49
+ }
50
+
51
+ if (args.help) {
52
+ console.log(help());
53
+ return exit();
54
+ }
39
55
 
40
56
  const cmd = argv.slice(2);
41
57
 
@@ -48,6 +64,10 @@ export const cli = ({argv, exit, readCoverage}) => {
48
64
 
49
65
  if (args.format === 'lines')
50
66
  output = reportLines(coverage);
67
+ else if (args.format === 'responsive')
68
+ output = reportResponsive(coverage, {
69
+ skipFull: args.skipFull,
70
+ });
51
71
  else
52
72
  output = reportFiles(coverage);
53
73
 
@@ -57,7 +77,6 @@ export const cli = ({argv, exit, readCoverage}) => {
57
77
  export const isSuccess = (error) => !error || error?.status === Number(process.env.ESCOVER_SUCCESS_EXIT_CODE);
58
78
 
59
79
  function execute(cmd, exit) {
60
- console.log(createNodeOptions());
61
80
  const [error] = tryCatch(execSync, cmd, {
62
81
  stdio: [0, 1, 2],
63
82
  env: {
@@ -74,4 +93,3 @@ function execute(cmd, exit) {
74
93
  return exit(1);
75
94
  }
76
95
  }
77
-
@@ -0,0 +1,18 @@
1
+ import helpJson from '../../help.json' with {
2
+ type: 'json',
3
+ };
4
+
5
+ const {entries} = Object;
6
+
7
+ export const help = () => {
8
+ const result = [
9
+ 'Usage: escover [options] [script]',
10
+ 'Options:',
11
+ ];
12
+
13
+ for (const [name, description] of entries(helpJson)) {
14
+ result.push(` ${name} ${description}`);
15
+ }
16
+
17
+ return result.join('\n');
18
+ };
package/lib/config.js CHANGED
@@ -30,7 +30,11 @@ const defaults = {
30
30
  };
31
31
 
32
32
  export const readConfig = (overrides = {}) => {
33
- const {find = findUpSync, read = readFileSync} = overrides;
33
+ const {
34
+ find = findUpSync,
35
+ read = readFileSync,
36
+ } = overrides;
37
+
34
38
  const name = find('.nycrc.json');
35
39
 
36
40
  if (!name)
@@ -15,4 +15,3 @@ export function createCoverageDirectory() {
15
15
 
16
16
  return name;
17
17
  }
18
-
@@ -1,7 +1,6 @@
1
1
  import {writeFileSync, readFileSync} from 'node:fs';
2
2
  import {join} from 'node:path';
3
3
  import {tryCatch} from 'try-catch';
4
- import {operator} from 'putout';
5
4
  import {getFileEntries} from '../c4.js';
6
5
  import {transform} from '../transform.js';
7
6
  import {merge} from '../merge.js';
@@ -11,8 +10,6 @@ import {
11
10
  } from './coverage-directory.js';
12
11
  import {generateLcov, parseLcov} from './lcov.js';
13
12
 
14
- const {getFile} = operator;
15
-
16
13
  const LCOV = 'lcov.info';
17
14
 
18
15
  export const writeCoverage = (overrides = {}) => {
@@ -36,7 +33,10 @@ export const writeCoverage = (overrides = {}) => {
36
33
  };
37
34
 
38
35
  export const readCoverage = (overrides = {}) => {
39
- const {read = readFileSync} = overrides;
36
+ const {
37
+ read = readFileSync,
38
+ } = overrides;
39
+
40
40
  const dir = getCoverageDirectory();
41
41
  const [error, data] = tryCatch(read, join(dir, LCOV), 'utf8');
42
42
 
@@ -45,4 +45,3 @@ export const readCoverage = (overrides = {}) => {
45
45
 
46
46
  return parseLcov(data);
47
47
  };
48
-
package/lib/escover.js CHANGED
@@ -25,7 +25,7 @@ export function load(url, context, defaultLoad) {
25
25
  if (!url.includes(CWD))
26
26
  return result;
27
27
 
28
- if (/commonjs|builtin/.test(format))
28
+ if (/json|commonjs|builtin/.test(format))
29
29
  return result;
30
30
 
31
31
  if (isExclude(url, EXCLUDE))
@@ -9,6 +9,9 @@ const CWD = process.cwd();
9
9
 
10
10
  const {entries, keys} = Object;
11
11
 
12
+ const makeGreen = chalk.hex('#4caf50');
13
+ const makeRed = chalk.hex('#f44336');
14
+
12
15
  export default (coverageFile) => {
13
16
  const files = parseCoverageFile(coverageFile);
14
17
 
@@ -21,17 +24,17 @@ export default (coverageFile) => {
21
24
 
22
25
  if (covered) {
23
26
  tableData.push([
24
- chalk.green(filename),
25
- chalk.green(percentLines),
27
+ makeGreen(filename),
28
+ makeGreen(percentLines),
26
29
  '',
27
30
  ]);
28
31
  continue;
29
32
  }
30
33
 
31
34
  tableData.push([
32
- chalk.red(filename),
33
- chalk.red(percentLines),
34
- chalk.red(uncoveredLines),
35
+ makeRed(filename),
36
+ makeRed(percentLines),
37
+ makeRed(uncoveredLines),
35
38
  ]);
36
39
  }
37
40
 
@@ -0,0 +1,242 @@
1
+ import process from 'node:process';
2
+ import {
3
+ table,
4
+ getBorderCharacters,
5
+ } from 'table';
6
+ import chalk from 'chalk';
7
+
8
+ const CWD = process.cwd();
9
+
10
+ const {entries, keys} = Object;
11
+
12
+ const makeGreen = chalk.hex('#4caf50');
13
+ const makeRed = chalk.hex('#f44336');
14
+
15
+ export default (coverageFile, {skipFull = false} = {}) => {
16
+ const files = parseCoverageFile(coverageFile, skipFull);
17
+
18
+ if (skipFull && !files.length)
19
+ return '💪 coverage 100% Good Job!\n';
20
+
21
+ const totalWidth = process.stdout.columns || 80;
22
+ const showPercent = totalWidth >= 70;
23
+
24
+ const percentColWidth = showPercent ? 8 : 0;
25
+ const fileColWidth = Math.floor(totalWidth * 0.45);
26
+
27
+ const linesColWidth = totalWidth - fileColWidth - percentColWidth - 6;
28
+
29
+ const tableData = buildGroupedTable(files, showPercent);
30
+
31
+ return table(tableData, {
32
+ drawHorizontalLine: (raw) => !raw || raw === 1 || raw === tableData.length,
33
+
34
+ columns: [{
35
+ paddingLeft: 1,
36
+ paddingRight: 1,
37
+ width: fileColWidth,
38
+ wrapWord: false, // 🔥 ключевой фикс — без переносов
39
+ }, ...showPercent ? [{
40
+ alignment: 'center',
41
+ paddingLeft: 1,
42
+ paddingRight: 0,
43
+ width: percentColWidth,
44
+ }] : [], {
45
+ paddingLeft: 1,
46
+ paddingRight: 0,
47
+ width: linesColWidth,
48
+ }],
49
+
50
+ border: {
51
+ ...getBorderCharacters('void'),
52
+ topBody: '-',
53
+ bottomBody: '-',
54
+ joinBody: '-',
55
+ topJoin: '|',
56
+ bottomJoin: '|',
57
+ joinJoin: '|',
58
+ bodyJoin: '|',
59
+ },
60
+ });
61
+ };
62
+
63
+ /* =========================
64
+ GROUPING + COVERAGE
65
+ ========================= */
66
+ function groupByFolder(files) {
67
+ const groups = new Map();
68
+
69
+ for (const f of files) {
70
+ const parts = f.filename.split('/');
71
+ const folder = parts.length > 1 ? parts
72
+ .slice(0, -1)
73
+ .join('/') : '';
74
+
75
+ const fileName = parts.at(-1);
76
+
77
+ if (!groups.has(folder))
78
+ groups.set(folder, {
79
+ files: [],
80
+ });
81
+
82
+ const group = groups.get(folder);
83
+
84
+ group.files.push({
85
+ ...f,
86
+ shortName: fileName,
87
+ });
88
+ }
89
+
90
+ return groups;
91
+ }
92
+
93
+ /* =========================
94
+ TABLE BUILDER
95
+ ========================= */
96
+ function buildGroupedTable(files, showPercent) {
97
+ const tableData = [
98
+ showPercent ? [
99
+ 'File',
100
+ '% Lines',
101
+ 'Uncovered Lines #s',
102
+ ] : [
103
+ 'File',
104
+ 'Uncovered Lines #s',
105
+ ],
106
+ ];
107
+
108
+ const groups = groupByFolder(files);
109
+
110
+ for (const [folder, group] of groups) {
111
+ const coverage = group.files.length ? Math.round(group.files.reduce((sum, f) => sum + f.percentLines, 0) / group.files.length) : 100;
112
+
113
+ const folderLabel = formatFolder(folder || '.');
114
+
115
+ const folderName = makeRed(`${folderLabel}/`);
116
+
117
+ const folderPercentColor = coverage === 100 ? makeGreen : makeRed;
118
+
119
+ const headerRow = [folderName];
120
+
121
+ if (showPercent)
122
+ headerRow.push(folderPercentColor(`${coverage}%`));
123
+
124
+ headerRow.push('');
125
+
126
+ tableData.push(headerRow);
127
+
128
+ for (const f of group.files) {
129
+ const fileCell = ` ${formatPath(f.shortName)}`;
130
+ const uncoveredLines = formatLines(f.lines);
131
+
132
+ const filePercentColor = f.percentLines === 100 ? makeGreen : makeRed;
133
+
134
+ const row = [
135
+ f.covered ? makeGreen(fileCell) : makeRed(fileCell),
136
+ ];
137
+
138
+ if (showPercent)
139
+ row.push(filePercentColor(`${f.percentLines}%`));
140
+
141
+ row.push(f.covered ? '' : makeRed(uncoveredLines));
142
+
143
+ tableData.push(row);
144
+ }
145
+ }
146
+
147
+ return tableData;
148
+ }
149
+
150
+ /* =========================
151
+ PARSE
152
+ ========================= */
153
+ function parseCoverageFile(coverageFile, skipFull = false) {
154
+ const files = [];
155
+
156
+ for (const {name, lines} of coverageFile) {
157
+ const filename = name.replace(`${CWD}/`, '');
158
+ const uncoveredLines = parseUncoveredLines(lines);
159
+
160
+ const linesCount = keys(lines).length;
161
+ const uncoveredLinesCount = uncoveredLines.length;
162
+
163
+ const percentLines = getLinesPercent(linesCount, uncoveredLinesCount);
164
+
165
+ const isCovered = !uncoveredLinesCount;
166
+
167
+ if (skipFull && isCovered)
168
+ continue;
169
+
170
+ files.push({
171
+ filename,
172
+ covered: isCovered,
173
+ lines: uncoveredLines,
174
+ percentLines,
175
+ });
176
+ }
177
+
178
+ return files;
179
+ }
180
+
181
+ /* =========================
182
+ FORMATTERS
183
+ ========================= */
184
+ function formatPath(path) {
185
+ const max = 30;
186
+
187
+ if (path.length <= max)
188
+ return path;
189
+
190
+ return `...${path.slice(-(max - 3))}`;
191
+ }
192
+
193
+ function formatFolder(path) {
194
+ const max = 25;
195
+
196
+ if (path.length <= max)
197
+ return path;
198
+
199
+ return `...${path.slice(-(max - 3))}`;
200
+ }
201
+
202
+ export function formatLines(array) {
203
+ const max = 30;
204
+
205
+ if (array.length <= 10) {
206
+ const joined = array.join(', ');
207
+
208
+ if (joined.length <= max)
209
+ return joined;
210
+ }
211
+
212
+ const [first] = array;
213
+ const last = array.at(-1);
214
+
215
+ const base = `${first}..${last}`;
216
+
217
+ if (base.length <= max)
218
+ return base;
219
+
220
+ return String(last);
221
+ }
222
+
223
+ /* =========================
224
+ HELPERS
225
+ ========================= */
226
+ function parseUncoveredLines(lines) {
227
+ const out = [];
228
+
229
+ for (const [line, covered] of entries(lines)) {
230
+ if (!covered)
231
+ out.push(line);
232
+ }
233
+
234
+ return out;
235
+ }
236
+
237
+ export function getLinesPercent(linesCount, uncoveredLinesCount) {
238
+ if (!linesCount)
239
+ return 100;
240
+
241
+ return 100 - Math.round(100 / linesCount * uncoveredLinesCount);
242
+ }
@@ -1,11 +1,14 @@
1
1
  import putout from 'putout';
2
2
  import * as convertOptionalToLogical from '@putout/plugin-convert-optional-to-logical';
3
+ import {tryCatch} from 'try-catch';
3
4
  import * as mark from './plugin-mark/index.js';
4
5
 
6
+ const {assign} = Object;
7
+
5
8
  export const instrument = (url, source) => {
6
9
  const c4 = globalThis.__createC4(url);
7
10
 
8
- const {code} = putout(source, {
11
+ const [error, result] = tryCatch(putout, source, {
9
12
  printer: 'putout',
10
13
  fixCount: 1,
11
14
  rules: {
@@ -19,5 +22,14 @@ export const instrument = (url, source) => {
19
22
  ],
20
23
  });
21
24
 
25
+ if (error) {
26
+ assign(error, {
27
+ message: `${url}: ${error.message}`,
28
+ });
29
+ throw error;
30
+ }
31
+
32
+ const {code} = result;
33
+
22
34
  return code;
23
35
  };
package/lib/register.js CHANGED
@@ -8,4 +8,3 @@ registerHooks(escoverLoader);
8
8
  process.on('exit', () => {
9
9
  writeCoverage();
10
10
  });
11
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "escover",
3
- "version": "6.1.1",
3
+ "version": "6.2.0",
4
4
  "author": "coderaiser <mnemonic.enemy@gmail.com> (https://github.com/coderaiser)",
5
5
  "description": "Coverage for EcmaScript Modules",
6
6
  "main": "lib/escover.js",
@@ -59,6 +59,7 @@
59
59
  },
60
60
  "license": "MIT",
61
61
  "devDependencies": {
62
+ "@putout/eslint-flat": "^4.0.0",
62
63
  "@putout/test": "^15.1.1",
63
64
  "c8": "^11.0.0",
64
65
  "escover": "file:.",
package/lib/fresh.js DELETED
@@ -1,27 +0,0 @@
1
- import {fileURLToPath, pathToFileURL} from 'node:url';
2
- import assert from 'node:assert';
3
- import {register} from 'node:module';
4
- import {MessageChannel} from 'node:worker_threads';
5
-
6
- // This example showcases how a message channel can be used to communicate
7
- // between the main (application) thread and the hooks running on the hooks
8
- // thread, by sending `port2` to the `initialize` hook.
9
- const __filename = fileURLToPath(import.meta.url);
10
-
11
- // This example showcases how a message channel can be used to communicate
12
- // between the main (application) thread and the hooks running on the hooks
13
- // thread, by sending `port2` to the `initialize` hook.
14
- const {port1, port2} = new MessageChannel();
15
-
16
- port1.on('message', (msg) => {
17
- assert.strictEqual(msg, 'increment: 2');
18
- });
19
-
20
- register('./escover.js', {
21
- parentURL: pathToFileURL(__filename),
22
- data: {
23
- number: 1,
24
- port: port2,
25
- },
26
- transferList: [port2],
27
- });