@stryker-mutator/core
Version:
The extendable JavaScript mutation testing framework
227 lines (205 loc) • 8.09 kB
text/typescript
import childProcess from 'child_process';
import { promises as fsPromises } from 'fs';
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
import { Logger } from '@stryker-mutator/api/logging';
import { notEmpty } from '@stryker-mutator/util';
import { NpmClient } from './npm-client.js';
import { PackageInfo, PackageSummary } from './package-info.js';
import { CustomInitializer } from './custom-initializers/custom-initializer.js';
import { PromptOption } from './prompt-option.js';
import { StrykerConfigWriter } from './stryker-config-writer.js';
import { StrykerInquirer } from './stryker-inquirer.js';
import { GitignoreWriter } from './gitignore-writer.js';
import { initializerTokens } from './index.js';
const enum PackageManager {
Npm = 'npm',
Yarn = 'yarn',
Pnpm = 'pnpm',
}
export class StrykerInitializer {
public static inject = tokens(
commonTokens.logger,
initializerTokens.out,
initializerTokens.npmClient,
initializerTokens.customInitializers,
initializerTokens.configWriter,
initializerTokens.gitignoreWriter,
initializerTokens.inquirer,
);
constructor(
private readonly log: Logger,
private readonly out: typeof console.log,
private readonly client: NpmClient,
private readonly customInitializers: CustomInitializer[],
private readonly configWriter: StrykerConfigWriter,
private readonly gitignoreWriter: GitignoreWriter,
private readonly inquirer: StrykerInquirer,
) {}
/**
* Runs the initializer will prompt the user for questions about his setup. After that, install plugins and configure Stryker.
* @function
*/
public async initialize(): Promise<void> {
await this.configWriter.guardForExistingConfig();
this.patchProxies();
const selectedPreset = await this.selectCustomInitializer();
let configFileName: string;
if (selectedPreset) {
configFileName = await this.initiateInitializer(this.configWriter, selectedPreset);
} else {
configFileName = await this.initiateCustom(this.configWriter);
}
await this.gitignoreWriter.addStrykerTempFolder();
this.out(`Done configuring stryker. Please review "${configFileName}", you might need to configure your test runner correctly.`);
this.out("Let's kill some mutants with this command: `stryker run`");
}
/**
* The typed rest client works only with the specific HTTP_PROXY and HTTPS_PROXY env settings.
* Let's make sure they are available.
*/
private patchProxies() {
const copyEnvVariable = (from: string, to: string) => {
if (process.env[from] && !process.env[to]) {
process.env[to] = process.env[from];
}
};
copyEnvVariable('http_proxy', 'HTTP_PROXY');
copyEnvVariable('https_proxy', 'HTTPS_PROXY');
}
private async selectCustomInitializer(): Promise<CustomInitializer | undefined> {
const customInitializer: CustomInitializer[] = this.customInitializers;
if (customInitializer.length) {
this.log.debug(`Found presets: ${JSON.stringify(customInitializer)}`);
return this.inquirer.promptPresets(customInitializer);
} else {
this.log.debug('No presets have been configured, reverting to custom configuration');
return undefined;
}
}
private async initiateInitializer(configWriter: StrykerConfigWriter, selectedPreset: CustomInitializer) {
const presetConfig = await selectedPreset.createConfig();
const isJsonSelected = await this.selectJsonConfigType();
const configFileName = await configWriter.writeCustomInitializer(presetConfig, isJsonSelected);
if (presetConfig.additionalConfigFiles) {
await Promise.all(Object.entries(presetConfig.additionalConfigFiles).map(([name, content]) => fsPromises.writeFile(name, content)));
}
const selectedPackageManager = await this.selectPackageManager();
this.installNpmDependencies(presetConfig.dependencies, selectedPackageManager);
return configFileName;
}
private async initiateCustom(configWriter: StrykerConfigWriter) {
const selectedTestRunner = await this.selectTestRunner();
const buildCommand = await this.getBuildCommand(selectedTestRunner);
const selectedReporters = await this.selectReporters();
const selectedPackageManager = await this.selectPackageManager();
const isJsonSelected = await this.selectJsonConfigType();
const npmDependencies = this.getSelectedNpmDependencies([selectedTestRunner].concat(selectedReporters));
const packageInfo = await this.fetchAdditionalConfig(npmDependencies);
const pkgInfoOfSelectedTestRunner = packageInfo.find((pkg) => pkg.name == selectedTestRunner.pkg?.name);
const additionalConfig = packageInfo.map((dep) => dep.initStrykerConfig ?? {}).filter(notEmpty);
const configFileName = await configWriter.write(
selectedTestRunner,
buildCommand,
selectedReporters,
selectedPackageManager,
npmDependencies.map((pkg) => pkg.name),
additionalConfig,
pkgInfoOfSelectedTestRunner?.homepage ?? "(missing 'homepage' URL in package.json)",
isJsonSelected,
);
this.installNpmDependencies(
npmDependencies.map((pkg) => pkg.name),
selectedPackageManager,
);
return configFileName;
}
private async selectTestRunner(): Promise<PromptOption> {
const testRunnerOptions = await this.client.getTestRunnerOptions();
this.log.debug(`Found test runners: ${JSON.stringify(testRunnerOptions)}`);
return this.inquirer.promptTestRunners(testRunnerOptions);
}
private async getBuildCommand(selectedTestRunner: PromptOption): Promise<PromptOption> {
if (selectedTestRunner.name !== 'jest') {
return this.inquirer.promptBuildCommand();
}
return { name: '', pkg: null };
}
private async selectReporters(): Promise<PromptOption[]> {
const reporterOptions = await this.client.getTestReporterOptions();
reporterOptions.push(
{
name: 'html',
pkg: null,
},
{
name: 'clear-text',
pkg: null,
},
{
name: 'progress',
pkg: null,
},
{
name: 'dashboard',
pkg: null,
},
);
return this.inquirer.promptReporters(reporterOptions);
}
private async selectPackageManager(): Promise<PromptOption> {
return this.inquirer.promptPackageManager([
{
name: PackageManager.Npm,
pkg: null,
},
{
name: PackageManager.Yarn,
pkg: null,
},
{
name: PackageManager.Pnpm,
pkg: null,
},
]);
}
private async selectJsonConfigType(): Promise<boolean> {
return this.inquirer.promptJsonConfigFormat();
}
private getSelectedNpmDependencies(selectedOptions: Array<PromptOption | null>): PackageInfo[] {
return selectedOptions
.filter(notEmpty)
.map((option) => option.pkg)
.filter(notEmpty);
}
/**
* Install the npm packages
* @function
*/
private installNpmDependencies(dependencies: string[], selectedOption: PromptOption): void {
if (dependencies.length === 0) {
return;
}
const dependencyArg = dependencies.join(' ');
this.out('Installing NPM dependencies...');
const cmd = this.getInstallCommand(selectedOption.name as PackageManager, dependencyArg);
this.out(cmd);
try {
childProcess.execSync(cmd, { stdio: [0, 1, 2] });
} catch {
this.out(`An error occurred during installation, please try it yourself: "${cmd}"`);
}
}
private getInstallCommand(packageManager: PackageManager, dependencyArg: string): string {
switch (packageManager) {
case PackageManager.Yarn:
return `yarn add ${dependencyArg} --dev`;
case PackageManager.Pnpm:
return `pnpm add -D ${dependencyArg}`;
case PackageManager.Npm:
return `npm i --save-dev ${dependencyArg}`;
}
}
private async fetchAdditionalConfig(dependencies: PackageSummary[]): Promise<PackageInfo[]> {
return await Promise.all(dependencies.map((dep) => this.client.getAdditionalConfig(dep)));
}
}