Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The first code coverage package that targets Node.js (V8), Bun (JSC), and Deno (
>
> - When an implementation goes wrong, avoid fixing progressively on top of errors, eliminate the error and implement the right approach in a clean and concise way.
> - This document is living. If you complete a plan that changes the project structure (e.g., extract a shared module, introduce a new group/pattern), update the rules and examples below in the same commit. Do not let this document drift from repository reality.
> - Always ask before changing this document.
> - **CLAUDE.md edits require explicit per-edit authorization.** "Plan approval", "Edit automatically" does not count.
> - Do not write to "/tmp", instead use the "tools/debug" directory which is not tracked by Git.

---
Expand Down Expand Up @@ -62,14 +62,15 @@ The first code coverage package that targets Node.js (V8), Bun (JSC), and Deno (
- **Put generic utilities under [src/utils/](src/utils/), never in domain files.** If a function does not depend on the scope it was written in, it does not belong there. Categorize by nature (`strings.ts`, `paths.ts`), never by consumer, never a `misc.ts`.
- **Vendored code carries an attribution header.** Deliberate cuts from upstream are documented at the top of the vendored file, not here.
- **Extract duplicated logic between sibling modules to a shared module in the same layer.** If N files in the same folder share the same skeleton with small parameterizable differences, extract the skeleton. The `shared/` pattern applies equally to `src/` and to test helpers.
- [src/runtimes/lifecycle.ts](src/runtimes/lifecycle.ts) for runtime setup and teardown.
- `src/runtimes/lifecycle/` for runtime setup and teardown.
- `shared/` subfolders under [src/reporters/](src/reporters/), [src/converters/](src/converters/), and [test/**utils**/readers/](test/__utils__/readers/) for cross-consumer helpers.
- AST primitives live in [src/converters/shared/](src/converters/shared/).
- **Promote on second consumer. Never duplicate. Never import from a sibling.** The moment a helper in `reporters/text/` is needed by `reporters/html/`, it moves to `reporters/shared/` in the same commit. A sibling reporter (or converter) reaching into another's internals is a bug to fix, not a shortcut to use.
- **Single file vs. directory with `index.ts` barrel.** When a file accumulates distinct responsibilities (discovery, parsing, serialization, orchestration), promote it to a directory. `index.ts` is strictly the orchestrator and public entry. Each responsibility goes into its own file. Established patterns: [src/reporters/text/](src/reporters/text/), [src/converters/v8-to-istanbul/](src/converters/v8-to-istanbul/), [src/converters/v8-nodefy/](src/converters/v8-nodefy/), [src/reporters/shared/lcov/](src/reporters/shared/lcov/), [src/configs/](src/configs/).
- **Runtime envelope handling stays at the entry boundary.** [src/converters/v8-nodefy/](src/converters/v8-nodefy/) is the only site where Node-vs-Deno V8 envelopes are inspected. Everything downstream operates on the uniform `V8NodefiedDocument`.
- **Bun's preload script lives at [src/runtimes/bun/preload.ts](src/runtimes/bun/preload.ts) and builds separately in `lib/preload-bun.js`.**
- **`index.ts` is never a type aggregator.** Types still come from `@types/`.
- **[src/core.ts](src/core.ts) is the boundary: [src/bin/](src/bin/) and [src/integrations/](src/integrations/) only import from it.**
- **[src/integrations/](src/integrations/)\<runner\>.ts adapts the core to the shape an external test runner expects (e.g., `poku`, `vitest`, etc.).**

### Exports

Expand Down
9 changes: 3 additions & 6 deletions src/@types/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PokuPlugin } from 'poku/plugins';
import type { CoverageOptions, CoverageState } from './coverage.js';
import type { Runtime } from './reporters.js';

export type SpawnExitOutcome = {
Expand All @@ -9,10 +9,7 @@ export type SpawnExitOutcome = {
export type SpawnRuntimeInputs = {
runtime: Runtime;
command: readonly string[];
plugin: PokuPlugin;
};

export type PluginContextMockInputs = {
runtime: Runtime;
cwd: string;
options: CoverageOptions;
state: CoverageState;
};
7 changes: 6 additions & 1 deletion src/@types/coverage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type { CoverageThresholds } from './check-coverage.js';
import type { Reporter } from './reporters.js';
import type { Reporter, Runtime } from './reporters.js';
import type { IDE } from './terminal.js';
import type { TypesOptions } from './type-coverage.js';
import type { Watermarks } from './watermarks.js';

export type CoverageContext = {
cwd: string;
runtime: Runtime;
};

export type CoverageState = {
enabled: boolean;
tempDir: string;
Expand Down
37 changes: 22 additions & 15 deletions src/bin/cli.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
import type { SpawnExitOutcome } from '../@types/cli.js';
import process from 'node:process';
import { coverage } from '../index.js';
import { pluginContextMock } from './plugin-context-mock.js';
import { bun, config, deno, node, state } from '../core.js';
import { runtime } from './runtime.js';

const runtimes = { node, deno, bun } as const;

const run = async (command: readonly string[]): Promise<void> => {
if (command.length === 0) {
process.stderr.write('coverage: missing command.\n');
process.exitCode = 1;
return;
}

let exitOutcome: SpawnExitOutcome = {
code: 0,
signal: null,
};

const cwd = process.cwd();
const detectedRuntime = runtime.get(command);
const plugin = coverage();
const context = pluginContextMock.create({
runtime: detectedRuntime,
cwd,
});

await plugin.setup?.(context);
const cliConfig = process.argv
.find((argument) => argument.startsWith('--coverageConfig'))
?.split('=')[1];
const options = config.load(cwd, cliConfig);

const coverageState = state.create();
coverageState.cwd = cwd;

runtimes[detectedRuntime].setup(options, coverageState);

let exitOutcome: SpawnExitOutcome = { code: 0, signal: null };

try {
exitOutcome = await runtime.run({
runtime: detectedRuntime,
command,
plugin,
cwd,
options,
state: coverageState,
});
} finally {
await plugin.teardown?.(context);
runtimes[detectedRuntime].teardown(
{ cwd, runtime: detectedRuntime },
options,
coverageState
);
}

if (exitOutcome.signal) {
Expand Down
19 changes: 0 additions & 19 deletions src/bin/plugin-context-mock.ts

This file was deleted.

12 changes: 7 additions & 5 deletions src/bin/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import type { SpawnExitOutcome, SpawnRuntimeInputs } from '../@types/cli.js';
import type { Runtime } from '../@types/reporters.js';
import { spawn } from 'node:child_process';
import process from 'node:process';
import { bun, deno, node } from '../core.js';

const FORWARDED_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP'];

const runtimes = { node, deno, bun } as const;

const get = (command: readonly string[]): Runtime => {
const firstToken = command[0] ?? '';
const baseName =
Expand All @@ -22,9 +25,8 @@ const get = (command: readonly string[]): Runtime => {
const run = (inputs: SpawnRuntimeInputs): Promise<SpawnExitOutcome> =>
new Promise((resolveOutcome) => {
const userCommand = inputs.command.slice();
const finalCommand = inputs.plugin.runner
? inputs.plugin.runner(userCommand, '')
: userCommand;
const runtimeAdapter = runtimes[inputs.runtime];
const finalCommand = runtimeAdapter.runner(userCommand, '', inputs.state);

if (finalCommand.length === 0) {
process.stderr.write(
Expand All @@ -35,7 +37,7 @@ const run = (inputs: SpawnRuntimeInputs): Promise<SpawnExitOutcome> =>
}

const [binary, ...args] = finalCommand;
const needsPipedStderr = inputs.plugin.onTestProcess !== undefined;
const needsPipedStderr = runtimeAdapter.onTestProcess !== undefined;
const child = spawn(binary, args, {
stdio: needsPipedStderr ? ['inherit', 'inherit', 'pipe'] : 'inherit',
shell: false,
Expand All @@ -45,7 +47,7 @@ const run = (inputs: SpawnRuntimeInputs): Promise<SpawnExitOutcome> =>
child.stderr.pipe(process.stderr);
}

inputs.plugin.onTestProcess?.(child, '');
runtimeAdapter.onTestProcess?.(child, '', inputs.state);

const forwardSignal = (signal: NodeJS.Signals): void => {
if (!child.killed) child.kill(signal);
Expand Down
2 changes: 1 addition & 1 deletion src/converters/jsc-to-aggregation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
import type { SourceMapDocument } from '../../@types/source-map.js';
import type { FileAggregation } from '../../@types/v8.js';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { fileFilter } from '../../file-filter.js';
import { fileFilter } from '../../utils/file-filter.js';
import { offsets } from '../../utils/offsets.js';
import { paths } from '../../utils/paths.js';
import { sourceLines } from '../../utils/source-lines.js';
Expand Down
2 changes: 1 addition & 1 deletion src/converters/shared/pre-remap-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
ResolvedScriptSource,
V8ScriptCoverage,
} from '../../@types/v8.js';
import { fileFilter } from '../../file-filter.js';
import { fileFilter } from '../../utils/file-filter.js';
import { v8Discovery } from './v8-discovery.js';

const passes = (
Expand Down
5 changes: 5 additions & 0 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { config } from './configs/index.js';
export { bun } from './runtimes/bun.js';
export { deno } from './runtimes/deno.js';
export { node } from './runtimes/node.js';
export { state } from './runtimes/lifecycle/state.js';
69 changes: 1 addition & 68 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,2 @@
import type { PokuPlugin } from 'poku/plugins';
import type { CoverageOptions } from './@types/coverage.js';
import type { Runtime } from './@types/reporters.js';
import { isAbsolute, resolve } from 'node:path';
import process from 'node:process';
import { config } from './configs/index.js';
import { bun } from './runtimes/bun.js';
import { deno } from './runtimes/deno.js';
import { node } from './runtimes/node.js';
import { state } from './state.js';

export type { CoverageOptions } from './@types/coverage.js';

const runtimes = { node, deno, bun } as const;

export const coverage = (
options: CoverageOptions = Object.create(null)
): PokuPlugin => {
const coverageState = state.create();
let runtime: Runtime | undefined;
let resolvedOptions: CoverageOptions = options;

return {
name: '@pokujs/coverage',

setup(context) {
if (options.requireFlag && !process.argv.includes('--coverage')) return;

runtime = context.runtime;
coverageState.cwd = context.cwd;

const cliConfig = process.argv
.find((arg) => arg.startsWith('--coverageConfig'))
?.split('=')[1];

const fileConfig = config.load(context.cwd, cliConfig ?? options.config);

resolvedOptions = { ...fileConfig, ...options };

runtimes[context.runtime].setup(context, resolvedOptions, coverageState);
},

runner(command, file) {
if (!runtime) return command;

const absoluteTestFile = isAbsolute(file)
? file
: resolve(coverageState.cwd, file);

coverageState.testFiles.add(absoluteTestFile);

return runtimes[runtime].runner(command, file, coverageState);
},

onTestProcess(child, file) {
if (!runtime) return;

runtimes[runtime].onTestProcess?.(child, file, coverageState);
},

teardown(context) {
runtimes[context.runtime].teardown(
context,
resolvedOptions,
coverageState
);
},
};
};
export { coverage } from './integrations/poku.js';
63 changes: 63 additions & 0 deletions src/integrations/poku.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { PokuPlugin } from 'poku/plugins';
import type { CoverageOptions } from '../@types/coverage.js';
import type { Runtime } from '../@types/reporters.js';
import { isAbsolute, resolve } from 'node:path';
import process from 'node:process';
import { bun, config, deno, node, state } from '../core.js';

const runtimes = { node, deno, bun } as const;

export const coverage = (
options: CoverageOptions = Object.create(null)
): PokuPlugin => {
const coverageState = state.create();
let runtime: Runtime | undefined;
let resolvedOptions: CoverageOptions = options;

return {
name: '@pokujs/coverage',

setup(context) {
if (options.requireFlag && !process.argv.includes('--coverage')) return;

runtime = context.runtime;
coverageState.cwd = context.cwd;

const cliConfig = process.argv
.find((arg) => arg.startsWith('--coverageConfig'))
?.split('=')[1];

const fileConfig = config.load(context.cwd, cliConfig ?? options.config);

resolvedOptions = { ...fileConfig, ...options };

runtimes[context.runtime].setup(resolvedOptions, coverageState);
},

runner(command, file) {
if (!runtime) return command;

const absoluteTestFile = isAbsolute(file)
? file
: resolve(coverageState.cwd, file);

coverageState.testFiles.add(absoluteTestFile);

return runtimes[runtime].runner(command, file, coverageState);
},

onTestProcess(child, file) {
if (!runtime) return;

runtimes[runtime].onTestProcess?.(child, file, coverageState);
},

teardown(context) {
runtimes[context.runtime].teardown(
context,
resolvedOptions,
coverageState
);
},
};
};
2 changes: 1 addition & 1 deletion src/reporters/html-spa/build-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import type {
WatermarkMetric,
Watermarks,
} from '../../@types/watermarks.js';
import { watermarks } from '../../watermarks.js';
import {
metricsForFile,
metricsForSubtree,
} from '../shared/html/row-metrics.js';
import { metrics } from '../shared/metrics.js';
import { skip } from '../shared/skip.js';
import { watermarks } from '../shared/watermarks.js';

const round2 = (value: number): number => Math.round(value * 100) / 100;

Expand Down
14 changes: 7 additions & 7 deletions src/all-files.ts → src/reporters/shared/all-files.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { CoverageMap, FileCoverage } from './@types/istanbul.js';
import type { ReporterContext, Runtime } from './@types/reporters.js';
import type { SourceContents } from './@types/source-discovery.js';
import type { CoverageMap, FileCoverage } from '../../@types/istanbul.js';
import type { ReporterContext, Runtime } from '../../@types/reporters.js';
import type { SourceContents } from '../../@types/source-discovery.js';
import { readdirSync, readFileSync } from 'node:fs';
import { isAbsolute, join, resolve } from 'node:path';
import { nonExecutableLines } from './converters/shared/non-executable-lines.js';
import { fileFilter } from './file-filter.js';
import { paths } from './utils/paths.js';
import { sourceLines as sourceLinesUtil } from './utils/source-lines.js';
import { nonExecutableLines } from '../../converters/shared/non-executable-lines.js';
import { fileFilter } from '../../utils/file-filter.js';
import { paths } from '../../utils/paths.js';
import { sourceLines as sourceLinesUtil } from '../../utils/source-lines.js';

const DEFAULT_SOURCE_EXTENSIONS: readonly string[] = [
'.js',
Expand Down
4 changes: 2 additions & 2 deletions src/reporters/shared/file-coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import type { ReporterContext } from '../../@types/reporters.js';
import type { Metric } from '../../@types/text.js';
import type { CoverageModel } from '../../@types/tree.js';
import { readFileSync } from 'node:fs';
import { allFiles } from '../../all-files.js';
import { ignoreDirectives } from '../../converters/shared/ignore-directives.js';
import { fileFilter } from '../../file-filter.js';
import { fileFilter } from '../../utils/file-filter.js';
import { allFiles } from './all-files.js';

const filterCoverageMap = (
coverageMap: CoverageMap,
Expand Down
Loading
Loading