Skip to content

Commit 9bc20e0

Browse files
committed
refactor(plugin-coverage): replace adjustments with tree
1 parent 8b5433f commit 9bc20e0

File tree

6 files changed

+205
-141
lines changed

6 files changed

+205
-141
lines changed

packages/create-cli/src/lib/setup/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type {
77
PluginCodegenResult,
88
PluginPromptDescriptor,
99
PluginSetupBinding,
10+
PluginSetupTree,
1011
} from '@code-pushup/models';
1112

1213
export const CI_PROVIDERS = ['github', 'gitlab', 'none'] as const;

packages/create-cli/src/lib/setup/wizard.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,10 @@ export async function runSetupWizard(
6464
selectedBindings,
6565
async binding => ({
6666
scope: binding.scope ?? 'project',
67-
result: await resolveBinding(binding, cliArgs, targetDir),
67+
result: await resolveBinding(binding, cliArgs, targetDir, tree),
6868
}),
6969
);
7070

71-
await applyAdjustments(tree, resolved);
72-
7371
const packageJson = await readPackageJson(targetDir);
7472
const isEsm = packageJson.type === 'module';
7573
const configFilename = resolveFilename('code-pushup.config', format, isEsm);
@@ -106,28 +104,14 @@ async function resolveBinding(
106104
binding: PluginSetupBinding,
107105
cliArgs: CliArgs,
108106
targetDir: string,
107+
tree: Pick<Tree, 'read' | 'write'>,
109108
): Promise<PluginCodegenResult> {
110109
const descriptors = binding.prompts ? await binding.prompts(targetDir) : [];
111110
const answers =
112111
descriptors.length > 0
113112
? await promptPluginOptions(descriptors, cliArgs)
114113
: {};
115-
return binding.generateConfig(answers);
116-
}
117-
118-
async function applyAdjustments(
119-
tree: Pick<Tree, 'read' | 'write'>,
120-
resolved: ScopedPluginResult[],
121-
): Promise<void> {
122-
await asyncSequential(
123-
resolved.flatMap(({ result }) => result.adjustments ?? []),
124-
async ({ path: filePath, transform }) => {
125-
const content = await tree.read(filePath);
126-
if (content != null) {
127-
await tree.write(filePath, transform(content));
128-
}
129-
},
130-
);
114+
return binding.generateConfig(answers, tree);
131115
}
132116

133117
async function writeStandaloneConfig(

packages/models/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export type {
118118
PluginCodegenResult,
119119
PluginPromptDescriptor,
120120
PluginSetupBinding,
121+
PluginSetupTree,
121122
} from './lib/plugin-setup.js';
122123
export {
123124
auditReportSchema,

packages/models/src/lib/plugin-setup.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,17 @@ export type ImportDeclarationStructure = {
4747
/** A single value in the answers record produced by plugin prompts. */
4848
export type PluginAnswer = string | string[] | boolean;
4949

50-
/** Code and file changes a plugin binding contributes to the generated config. */
50+
/** Code a plugin binding contributes to the generated config. */
5151
export type PluginCodegenResult = {
5252
imports: ImportDeclarationStructure[];
5353
pluginInit: string;
5454
categories?: CategoryConfig[];
55-
adjustments?: {
56-
path: string;
57-
transform: (content: string) => string;
58-
}[];
55+
};
56+
57+
/** Minimal file system abstraction passed to plugin bindings. */
58+
export type PluginSetupTree = {
59+
read: (path: string) => Promise<string | null>;
60+
write: (path: string, content: string) => Promise<void>;
5961
};
6062

6163
/**
@@ -75,5 +77,6 @@ export type PluginSetupBinding = {
7577
isRecommended?: (targetDir: string) => Promise<boolean>;
7678
generateConfig: (
7779
answers: Record<string, PluginAnswer>,
78-
) => PluginCodegenResult;
80+
tree?: PluginSetupTree,
81+
) => PluginCodegenResult | Promise<PluginCodegenResult>;
7982
};

packages/plugin-coverage/src/lib/binding.ts

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ import path from 'node:path';
44
import type {
55
CategoryConfig,
66
PluginAnswer,
7-
PluginCodegenResult,
87
PluginSetupBinding,
8+
PluginSetupTree,
99
} from '@code-pushup/models';
10-
import { hasDependency, readJsonFile, singleQuote } from '@code-pushup/utils';
10+
import {
11+
hasDependency,
12+
pluralize,
13+
readJsonFile,
14+
singleQuote,
15+
} from '@code-pushup/utils';
1116
import { addLcovReporter, hasLcovReporter } from './config-file.js';
1217
import {
1318
ALL_COVERAGE_TYPES,
@@ -25,6 +30,9 @@ const VITEST_WORKSPACE = new RegExp(`^vitest\\.workspace\\.${CONFIG_EXT}$`);
2530
const JEST_CONFIG = new RegExp(`^jest\\.config\\.${CONFIG_EXT}$`);
2631
const DEFAULT_REPORT_PATH = 'coverage/lcov.info';
2732

33+
const LCOV_COMMENT =
34+
'// NOTE: Ensure your test config includes "lcov" in coverage reporters.';
35+
2836
const FRAMEWORKS = [
2937
{ name: 'Jest', value: 'jest' },
3038
{ name: 'Vitest', value: 'vitest' },
@@ -63,6 +71,7 @@ export const coverageSetupBinding = {
6371
title: COVERAGE_PLUGIN_TITLE,
6472
packageName: PACKAGE_NAME,
6573
isRecommended,
74+
// eslint-disable-next-line max-lines-per-function
6675
prompts: async (targetDir: string) => {
6776
const framework = await detectFramework(targetDir);
6877
const configFile = await detectConfigFile(targetDir, framework);
@@ -96,7 +105,10 @@ export const coverageSetupBinding = {
96105
key: 'coverage.types',
97106
message: 'Coverage types to measure',
98107
type: 'checkbox',
99-
choices: ALL_COVERAGE_TYPES.map(type => ({ name: type, value: type })),
108+
choices: ALL_COVERAGE_TYPES.map(type => ({
109+
name: pluralize(type),
110+
value: type,
111+
})),
100112
default: [...ALL_COVERAGE_TYPES],
101113
},
102114
{
@@ -113,20 +125,22 @@ export const coverageSetupBinding = {
113125
},
114126
];
115127
},
116-
generateConfig: (answers: Record<string, PluginAnswer>) => {
128+
generateConfig: async (
129+
answers: Record<string, PluginAnswer>,
130+
tree?: PluginSetupTree,
131+
) => {
117132
const args = parseAnswers(answers);
133+
const lcovConfigured = await configureLcovReporter(args, tree);
118134
return {
119135
imports: [
120136
{ moduleSpecifier: PACKAGE_NAME, defaultImport: 'coveragePlugin' },
121137
],
122-
pluginInit: formatPluginInit(args),
138+
pluginInit: formatPluginInit(args, lcovConfigured),
123139
...(args.categories ? { categories: CATEGORIES } : {}),
124-
...resolveAdjustments(args),
125140
};
126141
},
127142
} satisfies PluginSetupBinding;
128143

129-
/** Applies defaults for missing or empty values. */
130144
function parseAnswers(answers: Record<string, PluginAnswer>): CoverageOptions {
131145
const string = (key: string) => {
132146
const value = answers[key];
@@ -149,44 +163,54 @@ function parseAnswers(answers: Record<string, PluginAnswer>): CoverageOptions {
149163
};
150164
}
151165

152-
/** Omits options that match plugin defaults. */
153-
function formatPluginInit(options: CoverageOptions): string {
166+
/** Returns true if lcov reporter is already present or was successfully added. */
167+
async function configureLcovReporter(
168+
options: CoverageOptions,
169+
tree?: PluginSetupTree,
170+
): Promise<boolean> {
171+
const { framework, configFile } = options;
172+
if (framework === 'other' || !configFile || !tree) {
173+
return false;
174+
}
175+
const content = await tree.read(configFile);
176+
if (content == null) {
177+
return false;
178+
}
179+
if (hasLcovReporter(content, framework)) {
180+
return true;
181+
}
182+
const modified = addLcovReporter(content, framework);
183+
if (modified === content) {
184+
return false;
185+
}
186+
await tree.write(configFile, modified);
187+
return true;
188+
}
189+
190+
function formatPluginInit(
191+
options: CoverageOptions,
192+
lcovConfigured: boolean,
193+
): string {
154194
const { reportPath, testCommand, types, continueOnFail } = options;
155195

156-
const args = [
196+
const hasCustomTypes =
197+
types.length > 0 && types.length < ALL_COVERAGE_TYPES.length;
198+
199+
const body = [
157200
`reports: [${singleQuote(reportPath)}]`,
158201
testCommand
159202
? `coverageToolCommand: { command: ${singleQuote(testCommand)} }`
160203
: '',
161-
types.length > 0 && types.length < ALL_COVERAGE_TYPES.length
204+
hasCustomTypes
162205
? `coverageTypes: [${types.map(singleQuote).join(', ')}]`
163206
: '',
164207
continueOnFail ? '' : 'continueOnCommandFail: false',
165-
].filter(Boolean);
166-
167-
return `await coveragePlugin({
168-
${args.join(',\n ')},
169-
})`;
170-
}
208+
]
209+
.filter(Boolean)
210+
.join(',\n ');
171211

172-
function resolveAdjustments(
173-
options: CoverageOptions,
174-
): Pick<PluginCodegenResult, 'adjustments'> {
175-
const { framework, configFile } = options;
176-
if (framework === 'other' || !configFile) {
177-
return {};
178-
}
179-
return {
180-
adjustments: [
181-
{
182-
path: configFile,
183-
transform: (content: string) =>
184-
hasLcovReporter(content, framework)
185-
? content
186-
: addLcovReporter(content, framework),
187-
},
188-
],
189-
};
212+
const init = `await coveragePlugin({\n ${body},\n })`;
213+
return lcovConfigured ? init : `${LCOV_COMMENT}\n ${init}`;
190214
}
191215

192216
async function isRecommended(targetDir: string): Promise<boolean> {

0 commit comments

Comments
 (0)