From 27130b41a6b9393e860dab2f2c42144809605bf7 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:16:40 -0300 Subject: [PATCH 1/3] refactor: improve integrations --- CLAUDE.md | 7 +++- src/@types/cli.ts | 9 ++--- src/@types/coverage.ts | 7 +++- src/bin/cli.ts | 37 ++++++++++-------- src/bin/plugin-context-mock.ts | 19 ---------- src/bin/runtime.ts | 12 +++--- src/core.ts | 5 +++ src/index.ts | 69 +--------------------------------- src/integrations/poku.ts | 63 +++++++++++++++++++++++++++++++ src/runtimes/bun.ts | 15 ++++---- src/runtimes/deno.ts | 16 ++++---- src/runtimes/lifecycle.ts | 9 +++-- src/runtimes/node.ts | 16 ++++---- tools/build.mts | 57 ++++++++++++++++++++++------ 14 files changed, 186 insertions(+), 155 deletions(-) delete mode 100644 src/bin/plugin-context-mock.ts create mode 100644 src/core.ts create mode 100644 src/integrations/poku.ts diff --git a/CLAUDE.md b/CLAUDE.md index a22f317..9069dab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,9 @@ 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 via the AskUserQuestion tool.** "Plan approval" or "Edit automatically" does not count. Prose questions like "confirm?" or "ok?" do not count. +> - Before writing, the agent MUST render the exact final text as plain prose (no code fence, no styled markdown, no links) so it shows verbatim inside the AskUserQuestion options. The user must be able to read every character of the proposed text before approving. +> - Only proceed after the user clicks "yes" on that specific AskUserQuestion. The text shown must match byte-for-byte what gets written to the file. Every time, including small edits. > - Do not write to "/tmp", instead use the "tools/debug" directory which is not tracked by Git. --- @@ -69,7 +71,8 @@ The first code coverage package that targets Node.js (V8), Bun (JSC), and Deno ( - **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/)\.ts adapts the core to the shape an external test runner expects (e.g., `poku`, `vitest`, etc.).** ### Exports diff --git a/src/@types/cli.ts b/src/@types/cli.ts index 0b9d87b..ec0d329 100644 --- a/src/@types/cli.ts +++ b/src/@types/cli.ts @@ -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 = { @@ -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; }; diff --git a/src/@types/coverage.ts b/src/@types/coverage.ts index be65bf9..f13f063 100644 --- a/src/@types/coverage.ts +++ b/src/@types/coverage.ts @@ -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; diff --git a/src/bin/cli.ts b/src/bin/cli.ts index 4671a50..70bec9d 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -1,9 +1,10 @@ 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 => { if (command.length === 0) { process.stderr.write('coverage: missing command.\n'); @@ -11,29 +12,35 @@ const run = async (command: readonly string[]): Promise => { 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) { diff --git a/src/bin/plugin-context-mock.ts b/src/bin/plugin-context-mock.ts deleted file mode 100644 index 0113382..0000000 --- a/src/bin/plugin-context-mock.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { PluginContext, ReporterPlugin } from 'poku/plugins'; -import type { PluginContextMockInputs } from '../@types/cli.js'; - -const create = ({ runtime, cwd }: PluginContextMockInputs): PluginContext => { - const now = new Date(); - - return { - configs: Object.create(null), - runtime, - cwd, - configFile: undefined, - runAsOnly: false, - results: { passed: 0, failed: 0, skipped: 0, todo: 0 }, - timespan: { started: now, finished: now, duration: 0 }, - reporter: Object.create(null) as ReturnType, - }; -}; - -export const pluginContextMock = { create } as const; diff --git a/src/bin/runtime.ts b/src/bin/runtime.ts index 5a99bd3..43e1399 100644 --- a/src/bin/runtime.ts +++ b/src/bin/runtime.ts @@ -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 = @@ -22,9 +25,8 @@ const get = (command: readonly string[]): Runtime => { const run = (inputs: SpawnRuntimeInputs): Promise => 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( @@ -35,7 +37,7 @@ const run = (inputs: SpawnRuntimeInputs): Promise => } 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, @@ -45,7 +47,7 @@ const run = (inputs: SpawnRuntimeInputs): Promise => 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); diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 0000000..c79aa1a --- /dev/null +++ b/src/core.ts @@ -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 './state.js'; diff --git a/src/index.ts b/src/index.ts index 0c4f8ed..bb935a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/integrations/poku.ts b/src/integrations/poku.ts new file mode 100644 index 0000000..b511072 --- /dev/null +++ b/src/integrations/poku.ts @@ -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 + ); + }, + }; +}; diff --git a/src/runtimes/bun.ts b/src/runtimes/bun.ts index 4a0131f..9bcbd20 100644 --- a/src/runtimes/bun.ts +++ b/src/runtimes/bun.ts @@ -1,6 +1,9 @@ import type { ChildProcess } from 'node:child_process'; -import type { PluginContext } from 'poku/plugins'; -import type { CoverageOptions, CoverageState } from '../@types/coverage.js'; +import type { + CoverageContext, + CoverageOptions, + CoverageState, +} from '../@types/coverage.js'; import type { JscInspectorHandle } from '../@types/jsc.js'; import type { DataListener } from '../@types/runtimes.js'; import { join } from 'node:path'; @@ -138,17 +141,13 @@ const runner = ( }; export const bun = { - setup: ( - _context: PluginContext, - options: CoverageOptions, - state: CoverageState - ): void => { + setup: (options: CoverageOptions, state: CoverageState): void => { lifecycle.setup(options, state, 'bun'); }, runner, onTestProcess, teardown: ( - context: PluginContext, + context: CoverageContext, options: CoverageOptions, state: CoverageState ): void => lifecycle.teardown(context, options, state, 'bun'), diff --git a/src/runtimes/deno.ts b/src/runtimes/deno.ts index 1b7a2ef..89edc5f 100644 --- a/src/runtimes/deno.ts +++ b/src/runtimes/deno.ts @@ -1,19 +1,19 @@ -import type { PluginContext } from 'poku/plugins'; -import type { CoverageOptions, CoverageState } from '../@types/coverage.js'; +import type { + CoverageContext, + CoverageOptions, + CoverageState, +} from '../@types/coverage.js'; import { lifecycle } from './lifecycle.js'; const ENV_VAR = 'DENO_COVERAGE_DIR'; export const deno = { - setup: ( - _context: PluginContext, - options: CoverageOptions, - state: CoverageState - ): void => lifecycle.setup(options, state, 'deno', ENV_VAR), + setup: (options: CoverageOptions, state: CoverageState): void => + lifecycle.setup(options, state, 'deno', ENV_VAR), runner: (command: string[]): string[] => command, onTestProcess: undefined, teardown: ( - context: PluginContext, + context: CoverageContext, options: CoverageOptions, state: CoverageState ): void => lifecycle.teardown(context, options, state, 'deno', ENV_VAR), diff --git a/src/runtimes/lifecycle.ts b/src/runtimes/lifecycle.ts index 003567f..bd0fca3 100644 --- a/src/runtimes/lifecycle.ts +++ b/src/runtimes/lifecycle.ts @@ -1,6 +1,9 @@ -import type { PluginContext } from 'poku/plugins'; import type { DiscoveredBranch } from '../@types/branch-discovery.js'; -import type { CoverageOptions, CoverageState } from '../@types/coverage.js'; +import type { + CoverageContext, + CoverageOptions, + CoverageState, +} from '../@types/coverage.js'; import type { CoverageMap } from '../@types/istanbul.js'; import type { ReporterContext, Runtime } from '../@types/reporters.js'; import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; @@ -44,7 +47,7 @@ const setup = ( }; const teardown = ( - context: PluginContext, + context: CoverageContext, options: CoverageOptions, state: CoverageState, runtime: Runtime, diff --git a/src/runtimes/node.ts b/src/runtimes/node.ts index a454cbe..cf48741 100644 --- a/src/runtimes/node.ts +++ b/src/runtimes/node.ts @@ -1,19 +1,19 @@ -import type { PluginContext } from 'poku/plugins'; -import type { CoverageOptions, CoverageState } from '../@types/coverage.js'; +import type { + CoverageContext, + CoverageOptions, + CoverageState, +} from '../@types/coverage.js'; import { lifecycle } from './lifecycle.js'; const ENV_VAR = 'NODE_V8_COVERAGE'; export const node = { - setup: ( - _context: PluginContext, - options: CoverageOptions, - state: CoverageState - ): void => lifecycle.setup(options, state, 'node', ENV_VAR), + setup: (options: CoverageOptions, state: CoverageState): void => + lifecycle.setup(options, state, 'node', ENV_VAR), runner: (command: string[]): string[] => command, onTestProcess: undefined, teardown: ( - context: PluginContext, + context: CoverageContext, options: CoverageOptions, state: CoverageState ): void => lifecycle.teardown(context, options, state, 'node', ENV_VAR), diff --git a/tools/build.mts b/tools/build.mts index 67509e3..fafe514 100644 --- a/tools/build.mts +++ b/tools/build.mts @@ -1,15 +1,27 @@ -import type { BuildOptions } from 'esbuild'; +import type { BuildOptions, Plugin } from 'esbuild'; import { chmod, mkdir, rm, writeFile } from 'node:fs/promises'; import { generateDtsBundle } from 'dts-bundle-generator'; import { build } from 'esbuild'; +const externalizeCore = (targetSpecifier: string): Plugin => ({ + name: 'externalize-core', + setup(buildInstance) { + buildInstance.onResolve({ filter: /(^|\/)core\.js$/ }, () => ({ + path: targetSpecifier, + external: true, + })); + }, +}); + +const esmToCjs: Pick = { + banner: { + js: "const __importMetaUrl = require('node:url').pathToFileURL(__filename).href;", + }, + define: { 'import.meta.url': '__importMetaUrl' }, +}; + const [dtsBundle] = generateDtsBundle( - [ - { - filePath: 'src/index.ts', - output: { noBanner: true }, - }, - ], + [{ filePath: 'src/index.ts', output: { noBanner: true } }], { preferredConfigPath: 'tsconfig.json' } ); @@ -33,22 +45,39 @@ const buildOptions: BuildOptions = { await rm('lib', { recursive: true, force: true }); await mkdir('lib', { recursive: true }); await Promise.all([ + // Index build({ ...buildOptions, format: 'esm', entryPoints: ['src/index.ts'], outfile: 'lib/index.js', + plugins: [externalizeCore('./core.js')], }), build({ ...buildOptions, + ...esmToCjs, format: 'cjs', entryPoints: ['src/index.ts'], outfile: 'lib/index.cjs', - banner: { - js: "const __importMetaUrl = require('node:url').pathToFileURL(__filename).href;", - }, - define: { 'import.meta.url': '__importMetaUrl' }, + plugins: [externalizeCore('./core.cjs')], + }), + + // Core + build({ + ...buildOptions, + format: 'esm', + entryPoints: ['src/core.ts'], + outfile: 'lib/core.js', + }), + build({ + ...buildOptions, + ...esmToCjs, + format: 'cjs', + entryPoints: ['src/core.ts'], + outfile: 'lib/core.cjs', }), + + // Preload build({ ...buildOptions, format: 'esm', @@ -56,14 +85,18 @@ await Promise.all([ outfile: 'lib/preload-bun.js', minify: true, }), + + // CLI build({ ...buildOptions, format: 'esm', entryPoints: ['src/bin/cli.ts'], outfile: 'lib/bin/cli.js', banner: { js: '#!/usr/bin/env node' }, - external: ['../index.js'], + external: ['../core.js'], }), + + // Declarations writeFile('lib/index.d.ts', dtsBundle, 'utf-8'), ]); From 9265c4aadb197b0c85b3a4dc23b000bc4890002c Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Fri, 1 May 2026 08:28:36 -0300 Subject: [PATCH 2/3] chore: move files --- src/{ => reporters/shared}/all-files.ts | 0 src/{ => reporters/shared}/watermarks.ts | 0 src/{ => runtimes/lifecycle}/check-coverage.ts | 0 src/runtimes/{lifecycle.ts => lifecycle/index.ts} | 0 src/{ => runtimes/lifecycle}/state.ts | 0 src/{ => utils}/file-filter.ts | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename src/{ => reporters/shared}/all-files.ts (100%) rename src/{ => reporters/shared}/watermarks.ts (100%) rename src/{ => runtimes/lifecycle}/check-coverage.ts (100%) rename src/runtimes/{lifecycle.ts => lifecycle/index.ts} (100%) rename src/{ => runtimes/lifecycle}/state.ts (100%) rename src/{ => utils}/file-filter.ts (100%) diff --git a/src/all-files.ts b/src/reporters/shared/all-files.ts similarity index 100% rename from src/all-files.ts rename to src/reporters/shared/all-files.ts diff --git a/src/watermarks.ts b/src/reporters/shared/watermarks.ts similarity index 100% rename from src/watermarks.ts rename to src/reporters/shared/watermarks.ts diff --git a/src/check-coverage.ts b/src/runtimes/lifecycle/check-coverage.ts similarity index 100% rename from src/check-coverage.ts rename to src/runtimes/lifecycle/check-coverage.ts diff --git a/src/runtimes/lifecycle.ts b/src/runtimes/lifecycle/index.ts similarity index 100% rename from src/runtimes/lifecycle.ts rename to src/runtimes/lifecycle/index.ts diff --git a/src/state.ts b/src/runtimes/lifecycle/state.ts similarity index 100% rename from src/state.ts rename to src/runtimes/lifecycle/state.ts diff --git a/src/file-filter.ts b/src/utils/file-filter.ts similarity index 100% rename from src/file-filter.ts rename to src/utils/file-filter.ts From 9e6c7a8958c561569e7590f67593aa8d3ef8c555 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Fri, 1 May 2026 08:29:09 -0300 Subject: [PATCH 3/3] chore: adapt imports --- CLAUDE.md | 6 ++--- src/converters/jsc-to-aggregation/index.ts | 2 +- src/converters/shared/pre-remap-filter.ts | 2 +- src/core.ts | 2 +- src/reporters/html-spa/build-data.ts | 2 +- src/reporters/shared/all-files.ts | 14 +++++------ src/reporters/shared/file-coverage.ts | 4 ++-- src/reporters/shared/html/templates.ts | 2 +- src/reporters/shared/lcov/filter.ts | 2 +- src/reporters/shared/lcov/runtimes/bun.ts | 2 +- .../shared/lcov/runtimes/v8-converter.ts | 2 +- src/reporters/shared/watermarks.ts | 4 ++-- src/reporters/text-summary/index.ts | 2 +- src/reporters/text/table.ts | 2 +- src/reporters/types/index.ts | 2 +- src/reporters/types/table.ts | 2 +- src/runtimes/bun.ts | 2 +- src/runtimes/deno.ts | 2 +- src/runtimes/lifecycle/check-coverage.ts | 22 ++++++++--------- src/runtimes/lifecycle/index.ts | 24 +++++++++---------- src/runtimes/lifecycle/state.ts | 2 +- src/runtimes/node.ts | 2 +- src/utils/file-filter.ts | 6 ++--- 23 files changed, 55 insertions(+), 57 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9069dab..4b86551 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,9 +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. -> - **CLAUDE.md edits require explicit per-edit authorization via the AskUserQuestion tool.** "Plan approval" or "Edit automatically" does not count. Prose questions like "confirm?" or "ok?" do not count. -> - Before writing, the agent MUST render the exact final text as plain prose (no code fence, no styled markdown, no links) so it shows verbatim inside the AskUserQuestion options. The user must be able to read every character of the proposed text before approving. -> - Only proceed after the user clicks "yes" on that specific AskUserQuestion. The text shown must match byte-for-byte what gets written to the file. Every time, including small edits. +> - **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. --- @@ -64,7 +62,7 @@ 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. diff --git a/src/converters/jsc-to-aggregation/index.ts b/src/converters/jsc-to-aggregation/index.ts index 5c9233b..74c57b7 100644 --- a/src/converters/jsc-to-aggregation/index.ts +++ b/src/converters/jsc-to-aggregation/index.ts @@ -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'; diff --git a/src/converters/shared/pre-remap-filter.ts b/src/converters/shared/pre-remap-filter.ts index f3af0f5..216b90f 100644 --- a/src/converters/shared/pre-remap-filter.ts +++ b/src/converters/shared/pre-remap-filter.ts @@ -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 = ( diff --git a/src/core.ts b/src/core.ts index c79aa1a..03c6882 100644 --- a/src/core.ts +++ b/src/core.ts @@ -2,4 +2,4 @@ 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 './state.js'; +export { state } from './runtimes/lifecycle/state.js'; diff --git a/src/reporters/html-spa/build-data.ts b/src/reporters/html-spa/build-data.ts index 669198b..3e943a2 100644 --- a/src/reporters/html-spa/build-data.ts +++ b/src/reporters/html-spa/build-data.ts @@ -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; diff --git a/src/reporters/shared/all-files.ts b/src/reporters/shared/all-files.ts index ef9115e..3e2bd4a 100644 --- a/src/reporters/shared/all-files.ts +++ b/src/reporters/shared/all-files.ts @@ -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', diff --git a/src/reporters/shared/file-coverage.ts b/src/reporters/shared/file-coverage.ts index 05f8129..bda2891 100644 --- a/src/reporters/shared/file-coverage.ts +++ b/src/reporters/shared/file-coverage.ts @@ -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, diff --git a/src/reporters/shared/html/templates.ts b/src/reporters/shared/html/templates.ts index 6080945..dc98b9e 100644 --- a/src/reporters/shared/html/templates.ts +++ b/src/reporters/shared/html/templates.ts @@ -10,8 +10,8 @@ import type { Watermarks, } from '../../../@types/watermarks.js'; import { html } from '../../../utils/html.js'; -import { watermarks } from '../../../watermarks.js'; import { metrics } from '../metrics.js'; +import { watermarks } from '../watermarks.js'; import { relativeHref } from './link-mapper.js'; export const metricReportClass = ( diff --git a/src/reporters/shared/lcov/filter.ts b/src/reporters/shared/lcov/filter.ts index 8e41242..aeae10b 100644 --- a/src/reporters/shared/lcov/filter.ts +++ b/src/reporters/shared/lcov/filter.ts @@ -1,6 +1,6 @@ import type { ResolvedFileFilter } from '../../../@types/file-filter.js'; import { isAbsolute, resolve } from 'node:path'; -import { fileFilter } from '../../../file-filter.js'; +import { fileFilter } from '../../../utils/file-filter.js'; import { paths } from '../../../utils/paths.js'; export const filter = ( diff --git a/src/reporters/shared/lcov/runtimes/bun.ts b/src/reporters/shared/lcov/runtimes/bun.ts index 14ebc71..7febf23 100644 --- a/src/reporters/shared/lcov/runtimes/bun.ts +++ b/src/reporters/shared/lcov/runtimes/bun.ts @@ -1,6 +1,6 @@ import type { ReporterContext } from '../../../../@types/reporters.js'; -import { allFiles } from '../../../../all-files.js'; import { lcovSerialize } from '../../../../converters/shared/lcov-serialize.js'; +import { allFiles } from '../../all-files.js'; import { filter } from '../filter.js'; const produce = (context: ReporterContext): string => { diff --git a/src/reporters/shared/lcov/runtimes/v8-converter.ts b/src/reporters/shared/lcov/runtimes/v8-converter.ts index 3fb3fbf..014b1ab 100644 --- a/src/reporters/shared/lcov/runtimes/v8-converter.ts +++ b/src/reporters/shared/lcov/runtimes/v8-converter.ts @@ -1,6 +1,6 @@ import type { ReporterContext } from '../../../../@types/reporters.js'; -import { allFiles } from '../../../../all-files.js'; import { lcovSerialize } from '../../../../converters/shared/lcov-serialize.js'; +import { allFiles } from '../../all-files.js'; import { filter } from '../filter.js'; const produce = (context: ReporterContext): string => { diff --git a/src/reporters/shared/watermarks.ts b/src/reporters/shared/watermarks.ts index 38733f2..8a43df0 100644 --- a/src/reporters/shared/watermarks.ts +++ b/src/reporters/shared/watermarks.ts @@ -1,9 +1,9 @@ -import type { ColorName } from './@types/terminal.js'; +import type { ColorName } from '../../@types/terminal.js'; import type { WatermarkLevel, WatermarkMetric, Watermarks, -} from './@types/watermarks.js'; +} from '../../@types/watermarks.js'; const DEFAULT_WATERMARKS: Watermarks = { statements: [50, 80], diff --git a/src/reporters/text-summary/index.ts b/src/reporters/text-summary/index.ts index 7922cf5..c3e5417 100644 --- a/src/reporters/text-summary/index.ts +++ b/src/reporters/text-summary/index.ts @@ -7,10 +7,10 @@ import type { Report, Runtime } from '../../@types/reporters.js'; import type { Metric } from '../../@types/text.js'; import type { WatermarkMetric } from '../../@types/watermarks.js'; import { terminal } from '../../utils/terminal.js'; -import { watermarks } from '../../watermarks.js'; import { fileCoverage } from '../shared/file-coverage.js'; import { lcov } from '../shared/lcov/index.js'; import { metrics } from '../shared/metrics.js'; +import { watermarks } from '../shared/watermarks.js'; const KEY_WIDTH = 12; const HEADER = diff --git a/src/reporters/text/table.ts b/src/reporters/text/table.ts index b7040a6..7e8ff84 100644 --- a/src/reporters/text/table.ts +++ b/src/reporters/text/table.ts @@ -10,12 +10,12 @@ import type { import type { CoverageModel } from '../../@types/tree.js'; import type { Watermarks } from '../../@types/watermarks.js'; import { terminal } from '../../utils/terminal.js'; -import { watermarks } from '../../watermarks.js'; import { metrics } from '../shared/metrics.js'; import { nameCell } from '../shared/name-cell.js'; import { ranges } from '../shared/ranges.js'; import { skip } from '../shared/skip.js'; import { tableRenderer } from '../shared/table.js'; +import { watermarks } from '../shared/watermarks.js'; import { buildTree, walkTree } from './tree.js'; const formatPercentageValue = (value: number | null): string => diff --git a/src/reporters/types/index.ts b/src/reporters/types/index.ts index 5dc8453..fc210f0 100644 --- a/src/reporters/types/index.ts +++ b/src/reporters/types/index.ts @@ -1,6 +1,6 @@ import type { Report } from '../../@types/reporters.js'; -import { allFiles } from '../../all-files.js'; import { ide } from '../../utils/ide.js'; +import { allFiles } from '../shared/all-files.js'; import { analyses } from './analyses.js'; import { typesCoverage } from './coverage.js'; import { typesDiscovery } from './discovery.js'; diff --git a/src/reporters/types/table.ts b/src/reporters/types/table.ts index ba0b34a..8f1b871 100644 --- a/src/reporters/types/table.ts +++ b/src/reporters/types/table.ts @@ -7,11 +7,11 @@ import type { } from '../../@types/type-coverage.js'; import type { Watermarks } from '../../@types/watermarks.js'; import { terminal } from '../../utils/terminal.js'; -import { watermarks } from '../../watermarks.js'; import { nameCell } from '../shared/name-cell.js'; import { pathTree } from '../shared/path-tree.js'; import { ranges } from '../shared/ranges.js'; import { tableRenderer } from '../shared/table.js'; +import { watermarks } from '../shared/watermarks.js'; const COLUMNS: readonly Column[] = [ { header: 'Type Files', align: 'left' }, diff --git a/src/runtimes/bun.ts b/src/runtimes/bun.ts index 9bcbd20..83eceb1 100644 --- a/src/runtimes/bun.ts +++ b/src/runtimes/bun.ts @@ -11,7 +11,7 @@ import { moduleDir } from '../utils/module-dir.js'; import { strings } from '../utils/strings.js'; import { jscInspector } from './bun/inspector.js'; import { FLUSH_MARKER } from './bun/marker.js'; -import { lifecycle } from './lifecycle.js'; +import { lifecycle } from './lifecycle/index.js'; const INSPECTOR_URL_PATTERN = /ws:\/\/(?:\d{1,3}(?:\.\d{1,3}){3}|\[[0-9a-fA-F:]+\]|[A-Za-z0-9.-]+):\d{1,5}\/[A-Za-z0-9._-]+/; diff --git a/src/runtimes/deno.ts b/src/runtimes/deno.ts index 89edc5f..a52628e 100644 --- a/src/runtimes/deno.ts +++ b/src/runtimes/deno.ts @@ -3,7 +3,7 @@ import type { CoverageOptions, CoverageState, } from '../@types/coverage.js'; -import { lifecycle } from './lifecycle.js'; +import { lifecycle } from './lifecycle/index.js'; const ENV_VAR = 'DENO_COVERAGE_DIR'; diff --git a/src/runtimes/lifecycle/check-coverage.ts b/src/runtimes/lifecycle/check-coverage.ts index 25e67ff..bc1a3a6 100644 --- a/src/runtimes/lifecycle/check-coverage.ts +++ b/src/runtimes/lifecycle/check-coverage.ts @@ -2,22 +2,22 @@ import type { CoverageFailure, CoverageMetric, CoverageThresholds, -} from './@types/check-coverage.js'; -import type { ReporterContext } from './@types/reporters.js'; -import type { Metric } from './@types/text.js'; -import type { CoverageModel } from './@types/tree.js'; +} from '../../@types/check-coverage.js'; +import type { ReporterContext } from '../../@types/reporters.js'; +import type { Metric } from '../../@types/text.js'; +import type { CoverageModel } from '../../@types/tree.js'; import type { FileTypeCoverage, TypeCoverageReport, -} from './@types/type-coverage.js'; -import type { WatermarkMetric } from './@types/watermarks.js'; +} from '../../@types/type-coverage.js'; +import type { WatermarkMetric } from '../../@types/watermarks.js'; import { relative } from 'node:path'; import process from 'node:process'; -import { fileCoverage } from './reporters/shared/file-coverage.js'; -import { lcov } from './reporters/shared/lcov/index.js'; -import { metrics } from './reporters/shared/metrics.js'; -import { terminal } from './utils/terminal.js'; -import { watermarks } from './watermarks.js'; +import { fileCoverage } from '../../reporters/shared/file-coverage.js'; +import { lcov } from '../../reporters/shared/lcov/index.js'; +import { metrics } from '../../reporters/shared/metrics.js'; +import { watermarks } from '../../reporters/shared/watermarks.js'; +import { terminal } from '../../utils/terminal.js'; const METRIC_ORDER: readonly CoverageMetric[] = [ 'statements', diff --git a/src/runtimes/lifecycle/index.ts b/src/runtimes/lifecycle/index.ts index bd0fca3..26f4072 100644 --- a/src/runtimes/lifecycle/index.ts +++ b/src/runtimes/lifecycle/index.ts @@ -1,23 +1,23 @@ -import type { DiscoveredBranch } from '../@types/branch-discovery.js'; +import type { DiscoveredBranch } from '../../@types/branch-discovery.js'; import type { CoverageContext, CoverageOptions, CoverageState, -} from '../@types/coverage.js'; -import type { CoverageMap } from '../@types/istanbul.js'; -import type { ReporterContext, Runtime } from '../@types/reporters.js'; +} from '../../@types/coverage.js'; +import type { CoverageMap } from '../../@types/istanbul.js'; +import type { ReporterContext, Runtime } from '../../@types/reporters.js'; import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import process from 'node:process'; -import { checkCoverage } from '../check-coverage.js'; -import { converters } from '../converters/index.js'; -import { discoveryMerge } from '../converters/shared/discovery-merge.js'; -import { fileFilter } from '../file-filter.js'; -import { reporters } from '../reporters/index.js'; -import { fileCoverage } from '../reporters/shared/file-coverage.js'; -import { watermarks } from '../watermarks.js'; -import { sourceMaps } from './source-maps.js'; +import { converters } from '../../converters/index.js'; +import { discoveryMerge } from '../../converters/shared/discovery-merge.js'; +import { reporters } from '../../reporters/index.js'; +import { fileCoverage } from '../../reporters/shared/file-coverage.js'; +import { watermarks } from '../../reporters/shared/watermarks.js'; +import { fileFilter } from '../../utils/file-filter.js'; +import { sourceMaps } from '../source-maps.js'; +import { checkCoverage } from './check-coverage.js'; const setup = ( options: CoverageOptions, diff --git a/src/runtimes/lifecycle/state.ts b/src/runtimes/lifecycle/state.ts index 404af25..47de4b2 100644 --- a/src/runtimes/lifecycle/state.ts +++ b/src/runtimes/lifecycle/state.ts @@ -1,4 +1,4 @@ -import type { CoverageState } from './@types/coverage.js'; +import type { CoverageState } from '../../@types/coverage.js'; const create = (): CoverageState => ({ enabled: false, diff --git a/src/runtimes/node.ts b/src/runtimes/node.ts index cf48741..ccddb46 100644 --- a/src/runtimes/node.ts +++ b/src/runtimes/node.ts @@ -3,7 +3,7 @@ import type { CoverageOptions, CoverageState, } from '../@types/coverage.js'; -import { lifecycle } from './lifecycle.js'; +import { lifecycle } from './lifecycle/index.js'; const ENV_VAR = 'NODE_V8_COVERAGE'; diff --git a/src/utils/file-filter.ts b/src/utils/file-filter.ts index a9cfab3..fd9caa6 100644 --- a/src/utils/file-filter.ts +++ b/src/utils/file-filter.ts @@ -1,9 +1,9 @@ import type { FileFilterOptions, ResolvedFileFilter, -} from './@types/file-filter.js'; -import { globs } from './utils/globs.js'; -import { paths } from './utils/paths.js'; +} from '../@types/file-filter.js'; +import { globs } from './globs.js'; +import { paths } from './paths.js'; /* * Extends exclude list adapted from @istanbuljs/schema.