diff --git a/CLAUDE.md b/CLAUDE.md index a22f317..4b86551 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. --- @@ -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/)\.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/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 new file mode 100644 index 0000000..03c6882 --- /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 './runtimes/lifecycle/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/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/all-files.ts b/src/reporters/shared/all-files.ts similarity index 93% rename from src/all-files.ts rename to src/reporters/shared/all-files.ts index ef9115e..3e2bd4a 100644 --- a/src/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/watermarks.ts b/src/reporters/shared/watermarks.ts similarity index 95% rename from src/watermarks.ts rename to src/reporters/shared/watermarks.ts index 38733f2..8a43df0 100644 --- a/src/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 4a0131f..83eceb1 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'; @@ -8,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._-]+/; @@ -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..a52628e 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 { lifecycle } from './lifecycle.js'; +import type { + CoverageContext, + CoverageOptions, + CoverageState, +} from '../@types/coverage.js'; +import { lifecycle } from './lifecycle/index.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/check-coverage.ts b/src/runtimes/lifecycle/check-coverage.ts similarity index 91% rename from src/check-coverage.ts rename to src/runtimes/lifecycle/check-coverage.ts index 25e67ff..bc1a3a6 100644 --- a/src/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.ts b/src/runtimes/lifecycle/index.ts similarity index 81% rename from src/runtimes/lifecycle.ts rename to src/runtimes/lifecycle/index.ts index 003567f..26f4072 100644 --- a/src/runtimes/lifecycle.ts +++ b/src/runtimes/lifecycle/index.ts @@ -1,20 +1,23 @@ -import type { PluginContext } from 'poku/plugins'; -import type { DiscoveredBranch } from '../@types/branch-discovery.js'; -import type { CoverageOptions, CoverageState } from '../@types/coverage.js'; -import type { CoverageMap } from '../@types/istanbul.js'; -import type { ReporterContext, Runtime } from '../@types/reporters.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'; 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, @@ -44,7 +47,7 @@ const setup = ( }; const teardown = ( - context: PluginContext, + context: CoverageContext, options: CoverageOptions, state: CoverageState, runtime: Runtime, diff --git a/src/state.ts b/src/runtimes/lifecycle/state.ts similarity index 81% rename from src/state.ts rename to src/runtimes/lifecycle/state.ts index 404af25..47de4b2 100644 --- a/src/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 a454cbe..ccddb46 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 { lifecycle } from './lifecycle.js'; +import type { + CoverageContext, + CoverageOptions, + CoverageState, +} from '../@types/coverage.js'; +import { lifecycle } from './lifecycle/index.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/src/file-filter.ts b/src/utils/file-filter.ts similarity index 94% rename from src/file-filter.ts rename to src/utils/file-filter.ts index a9cfab3..fd9caa6 100644 --- a/src/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. 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'), ]);