diff --git a/README.md b/README.md index e03e676..30c0c6c 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ Enjoying **Poku**? [Give him a star to show your support](https://github.com/wel ☔️ [**@pokujs/c8**](https://github.com/pokujs/c8) is a **Poku** plugin for **V8** code coverage using [**c8**](https://github.com/bcoe/c8). +> [!TIP] +> +> **@pokujs/c8** supports **JSONC** config files (`.c8rc`, `.nycrc`, etc.) out of the box, allowing comments in your configuration. You can also use **JS** and **TS** by setting the options directly in the plugin. + --- ## Quickstart @@ -53,10 +57,13 @@ Run `poku` and a coverage summary will be printed after your test results. ```js coverage({ + // Config file (.c8rc, .c8rc.json, .c8rc.jsonc, .nycrc, .nycrc.json, .nycrc.jsonc) + config: '.c8rc', // default: auto-discover + // Activation requireFlag: true, // default: false - // Reporters + // Reporters (clover, cobertura, html, html-spa, json, json-summary, lcov, lcovonly, none, teamcity, text, text-lcov, text-summary) reporter: ['text', 'lcov'], // default: ['text'] // File selection @@ -165,6 +172,42 @@ poku test/ poku --coverage test/ ``` +### Using a config file + +Reuse your existing `.c8rc`, `.nycrc`, or any JSON/JSONC config file with comments: + +```jsonc +// .c8rc +{ + // Only cover source files + "include": ["src/**"], + "reporter": ["text", "lcov"], + "check-coverage": true, + "lines": 90, +} +``` + +```js +coverage({ + config: '.c8rc', // or false to disable auto-discovery +}); +``` + +When no `config` is specified, the plugin automatically searches for `.c8rc`, `.c8rc.json`, `.c8rc.jsonc`, `.nycrc`, `.nycrc.json`, or `.nycrc.jsonc` in the working directory. + +You can also specify the config path via CLI: + +```bash +poku --coverage-config=.c8rc test/ +``` + +> [!NOTE] +> +> **Priority order:** +> +> - For config file discovery: `--coverage-config` (CLI) > `config` (plugin option) > auto-discovery +> - For coverage options: plugin options > config file options + ### Extending Monocart reporters ```bash diff --git a/package-lock.json b/package-lock.json index 7274d48..cc92c4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.2", "license": "MIT", "dependencies": { - "c8": "^10.1.3" + "c8": "^10.1.3", + "jsonc.min": "^1.1.2" }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.7.1", @@ -26,7 +27,7 @@ }, "funding": { "type": "github", - "url": "https://github.com/sponsors/wellwelwel" + "url": "https://github.com/pokujs/c8?sponsor=1" }, "peerDependencies": { "monocart-coverage-reports": "^2.12.9", @@ -1306,6 +1307,21 @@ "node": ">=6" } }, + "node_modules/jsonc.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/jsonc.min/-/jsonc.min-1.1.2.tgz", + "integrity": "sha512-8DzkaJnjZF8Tm/pozI77pH2Gd5JaKmk/JaO/cGXxQMtzzxG3DBg7SLEoHPrtU1KY386o5jOwrdgsGHVAR05kCA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", diff --git a/package.json b/package.json index aab81f6..738197a 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "node": ">=18.x.x" }, "scripts": { + "pretest": "npm run build", "test": "poku test/e2e", "prebuild": "rm -rf lib", "build": "tsc", @@ -34,7 +35,8 @@ "postupdate": "npm run lint:fix" }, "dependencies": { - "c8": "^10.1.3" + "c8": "^10.1.3", + "jsonc.min": "^1.1.2" }, "peerDependencies": { "monocart-coverage-reports": "^2.12.9", diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..9d10bf0 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,61 @@ +import type { CoverageOptions } from './types.js'; +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { JSONC } from 'jsonc.min'; + +const kebabMap: Record = { + 'reports-dir': 'reportsDirectory', + 'report-dir': 'reportsDirectory', + 'temp-directory': 'tempDirectory', + 'check-coverage': 'checkCoverage', + 'per-file': 'perFile', + 'skip-full': 'skipFull', + 'exclude-after-remap': 'excludeAfterRemap', + 'merge-async': 'mergeAsync', +}; + +const mapKeys = (raw: Record): Partial => { + const result: Record = {}; + + for (const [key, value] of Object.entries(raw)) { + if (key === 'experimental-monocart') { + if (value) result.experimental = ['monocart']; + continue; + } + result[kebabMap[key] ?? key] = value; + } + + return result as Partial; +}; + +export const loadConfig = ( + cwd: string, + customPath?: string | false +): CoverageOptions => { + if (customPath === false) return Object.create(null); + + const expectedFiles = customPath + ? [customPath] + : [ + '.c8rc', + '.c8rc.json', + '.c8rc.jsonc', + '.nycrc', + '.nycrc.json', + '.nycrc.jsonc', + ]; + + for (const file of expectedFiles) { + const filePath = join(cwd, file); + + if (!existsSync(filePath)) continue; + + try { + const content = readFileSync(filePath, 'utf8'); + + return mapKeys(JSONC.parse(content) as Record); + } catch {} + } + + return Object.create(null); +}; diff --git a/src/index.ts b/src/index.ts index e31ddc6..c338994 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import process from 'node:process'; +import { loadConfig } from './config.js'; export type { CoverageOptions } from './types.js'; @@ -22,6 +23,13 @@ export const coverage = ( if (options.requireFlag && !process.argv.includes('--coverage')) return; enabled = true; + const cliConfig = process.argv + .find((arg) => arg.startsWith('--coverage-config')) + ?.split('=')[1]; + + const fileConfig = loadConfig(context.cwd, cliConfig ?? options.config); + options = { ...fileConfig, ...options }; + if (context.runtime !== 'node') console.warn( `[@pokujs/c8] V8 coverage is only supported on Node.js (current runtime: ${context.runtime}). Coverage data may not be collected.` diff --git a/src/types.ts b/src/types.ts index 7dca450..80b5824 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,6 +31,16 @@ type KnownExtension = type Extension = KnownExtension | (string & NonNullable); export type CoverageOptions = { + /** + * Path to a JSONC/JSON configuration file. + * + * - `string` — load that specific file + * - `false` — disable config file discovery + * - `undefined` (default) — auto-discover `.c8rc`, `.c8rc.json`, `.nycrc`, `.nycrc.json`, + * or a `c8` key in `package.json`, walking up from `cwd`. + */ + config?: string | false; + /** * Require the `--coverage` CLI flag to activate coverage collection. * diff --git a/test/__fixtures__/e2e/configs/.c8rc b/test/__fixtures__/e2e/configs/.c8rc new file mode 100644 index 0000000..6c9fefc --- /dev/null +++ b/test/__fixtures__/e2e/configs/.c8rc @@ -0,0 +1,5 @@ +{ + // JSONC: comments should be supported + "include": ["src/**"], + "reporter": ["text"] +} diff --git a/test/__fixtures__/e2e/configs/basic.config.js b/test/__fixtures__/e2e/configs/basic.config.js new file mode 100644 index 0000000..a96d9b0 --- /dev/null +++ b/test/__fixtures__/e2e/configs/basic.config.js @@ -0,0 +1,12 @@ +const { coverage } = require('../../../../lib/index.js'); + +/** @type {import('poku').PokuConfig} */ +module.exports = { + include: ['test/'], + plugins: [ + coverage({ + include: ['src/**'], + reporter: ['text'], + }), + ], +}; diff --git a/test/__fixtures__/e2e/configs/basic.config.ts b/test/__fixtures__/e2e/configs/basic.config.ts deleted file mode 100644 index d416a1d..0000000 --- a/test/__fixtures__/e2e/configs/basic.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from 'poku'; -import { coverage } from '../../../../src/index.ts'; - -export default defineConfig({ - include: ['test/'], - plugins: [ - coverage({ - include: ['src/**'], - reporter: ['text'], - }), - ], -}); diff --git a/test/__fixtures__/e2e/configs/config-file.config.js b/test/__fixtures__/e2e/configs/config-file.config.js new file mode 100644 index 0000000..2379423 --- /dev/null +++ b/test/__fixtures__/e2e/configs/config-file.config.js @@ -0,0 +1,11 @@ +const { coverage } = require('../../../../lib/index.js'); + +/** @type {import('poku').PokuConfig} */ +module.exports = { + include: ['test/'], + plugins: [ + coverage({ + config: 'configs/.c8rc', + }), + ], +}; diff --git a/test/__fixtures__/e2e/configs/require-flag.config.ts b/test/__fixtures__/e2e/configs/require-flag.config.js similarity index 53% rename from test/__fixtures__/e2e/configs/require-flag.config.ts rename to test/__fixtures__/e2e/configs/require-flag.config.js index 200f897..a4a9821 100644 --- a/test/__fixtures__/e2e/configs/require-flag.config.ts +++ b/test/__fixtures__/e2e/configs/require-flag.config.js @@ -1,7 +1,7 @@ -import { defineConfig } from 'poku'; -import { coverage } from '../../../../src/index.ts'; +const { coverage } = require('../../../../lib/index.js'); -export default defineConfig({ +/** @type {import('poku').PokuConfig} */ +module.exports = { include: ['test/'], plugins: [ coverage({ @@ -10,4 +10,4 @@ export default defineConfig({ requireFlag: true, }), ], -}); +}; diff --git a/test/__fixtures__/e2e/configs/thresholds-fail.config.ts b/test/__fixtures__/e2e/configs/thresholds-fail.config.js similarity index 53% rename from test/__fixtures__/e2e/configs/thresholds-fail.config.ts rename to test/__fixtures__/e2e/configs/thresholds-fail.config.js index 5b27a22..4f31313 100644 --- a/test/__fixtures__/e2e/configs/thresholds-fail.config.ts +++ b/test/__fixtures__/e2e/configs/thresholds-fail.config.js @@ -1,7 +1,7 @@ -import { defineConfig } from 'poku'; -import { coverage } from '../../../../src/index.ts'; +const { coverage } = require('../../../../lib/index.js'); -export default defineConfig({ +/** @type {import('poku').PokuConfig} */ +module.exports = { include: ['test/'], plugins: [ coverage({ @@ -10,4 +10,4 @@ export default defineConfig({ checkCoverage: 100, }), ], -}); +}; diff --git a/test/__fixtures__/e2e/configs/thresholds-pass.config.ts b/test/__fixtures__/e2e/configs/thresholds-pass.config.js similarity index 56% rename from test/__fixtures__/e2e/configs/thresholds-pass.config.ts rename to test/__fixtures__/e2e/configs/thresholds-pass.config.js index 030bdbc..a58c6a4 100644 --- a/test/__fixtures__/e2e/configs/thresholds-pass.config.ts +++ b/test/__fixtures__/e2e/configs/thresholds-pass.config.js @@ -1,7 +1,7 @@ -import { defineConfig } from 'poku'; -import { coverage } from '../../../../src/index.ts'; +const { coverage } = require('../../../../lib/index.js'); -export default defineConfig({ +/** @type {import('poku').PokuConfig} */ +module.exports = { include: ['test/'], plugins: [ coverage({ @@ -11,4 +11,4 @@ export default defineConfig({ lines: 30, }), ], -}); +}; diff --git a/test/e2e/coverage-basic.test.ts b/test/e2e/coverage-basic.test.ts index 57a514e..3905008 100644 --- a/test/e2e/coverage-basic.test.ts +++ b/test/e2e/coverage-basic.test.ts @@ -6,7 +6,7 @@ const pokuBin = 'node_modules/poku/lib/bin/index.js'; test('basic coverage report is generated', async () => { const result = await inspectPoku({ - command: '-c=configs/basic.config.ts', + command: '-c=configs/basic.config.js', spawnOptions: { cwd: fixtureDir }, bin: pokuBin, }); diff --git a/test/e2e/coverage-config-file.test.ts b/test/e2e/coverage-config-file.test.ts new file mode 100644 index 0000000..3d9fb00 --- /dev/null +++ b/test/e2e/coverage-config-file.test.ts @@ -0,0 +1,17 @@ +import { assert, test } from 'poku'; +import { inspectPoku } from 'poku/plugins'; + +const fixtureDir = 'test/__fixtures__/e2e'; +const pokuBin = 'node_modules/poku/lib/bin/index.js'; + +test('loads JSONC config file with comments', async () => { + const result = await inspectPoku({ + command: '-c=configs/config-file.config.js', + spawnOptions: { cwd: fixtureDir }, + bin: pokuBin, + }); + + assert.strictEqual(result.exitCode, 0); + assert(result.stdout.includes('math.ts')); + assert(result.stdout.includes('%')); +}); diff --git a/test/e2e/coverage-require-flag.test.ts b/test/e2e/coverage-require-flag.test.ts index 7cdd96f..5d88935 100644 --- a/test/e2e/coverage-require-flag.test.ts +++ b/test/e2e/coverage-require-flag.test.ts @@ -6,7 +6,7 @@ const pokuBin = 'node_modules/poku/lib/bin/index.js'; test('coverage is skipped without --coverage flag when requireFlag is true', async () => { const result = await inspectPoku({ - command: '-c=configs/require-flag.config.ts', + command: '-c=configs/require-flag.config.js', spawnOptions: { cwd: fixtureDir }, bin: pokuBin, }); @@ -20,7 +20,7 @@ test('coverage is skipped without --coverage flag when requireFlag is true', asy test('coverage runs with --coverage flag when requireFlag is true', async () => { const result = await inspectPoku({ - command: '--coverage -c=configs/require-flag.config.ts', + command: '--coverage -c=configs/require-flag.config.js', spawnOptions: { cwd: fixtureDir }, bin: pokuBin, }); diff --git a/test/e2e/coverage-thresholds.test.ts b/test/e2e/coverage-thresholds.test.ts index 1cb70f1..91dd446 100644 --- a/test/e2e/coverage-thresholds.test.ts +++ b/test/e2e/coverage-thresholds.test.ts @@ -6,7 +6,7 @@ const pokuBin = 'node_modules/poku/lib/bin/index.js'; test('threshold check passes with low threshold', async () => { const result = await inspectPoku({ - command: '-c=configs/thresholds-pass.config.ts', + command: '-c=configs/thresholds-pass.config.js', spawnOptions: { cwd: fixtureDir }, bin: pokuBin, }); @@ -16,7 +16,7 @@ test('threshold check passes with low threshold', async () => { test('threshold check fails with 100% threshold on partial coverage', async () => { const result = await inspectPoku({ - command: '-c=configs/thresholds-fail.config.ts', + command: '-c=configs/thresholds-fail.config.js', spawnOptions: { cwd: fixtureDir }, bin: pokuBin, });