1 | import childProcess from 'child_process';
|
2 | import { promises as fsPromises } from 'fs';
|
3 |
|
4 | import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
|
5 | import { Logger } from '@stryker-mutator/api/logging';
|
6 | import { notEmpty } from '@stryker-mutator/util';
|
7 |
|
8 | import { NpmClient, NpmPackage } from './npm-client.js';
|
9 | import { PackageInfo } from './package-info.js';
|
10 | import { Preset } from './presets/preset.js';
|
11 | import { PromptOption } from './prompt-option.js';
|
12 | import { StrykerConfigWriter } from './stryker-config-writer.js';
|
13 | import { StrykerInquirer } from './stryker-inquirer.js';
|
14 | import { GitignoreWriter } from './gitignore-writer.js';
|
15 |
|
16 | import { initializerTokens } from './index.js';
|
17 |
|
18 | const enum PackageManager {
|
19 | Npm = 'npm',
|
20 | Yarn = 'yarn',
|
21 | Pnpm = 'pnpm',
|
22 | }
|
23 |
|
24 | export class StrykerInitializer {
|
25 | public static inject = tokens(
|
26 | commonTokens.logger,
|
27 | initializerTokens.out,
|
28 | initializerTokens.npmClient,
|
29 | initializerTokens.strykerPresets,
|
30 | initializerTokens.configWriter,
|
31 | initializerTokens.gitignoreWriter,
|
32 | initializerTokens.inquirer
|
33 | );
|
34 | constructor(
|
35 | private readonly log: Logger,
|
36 | private readonly out: typeof console.log,
|
37 | private readonly client: NpmClient,
|
38 | private readonly strykerPresets: Preset[],
|
39 | private readonly configWriter: StrykerConfigWriter,
|
40 | private readonly gitignoreWriter: GitignoreWriter,
|
41 | private readonly inquirer: StrykerInquirer
|
42 | ) {}
|
43 |
|
44 | |
45 |
|
46 |
|
47 |
|
48 | public async initialize(): Promise<void> {
|
49 | await this.configWriter.guardForExistingConfig();
|
50 | this.patchProxies();
|
51 | const selectedPreset = await this.selectPreset();
|
52 | let configFileName: string;
|
53 | if (selectedPreset) {
|
54 | configFileName = await this.initiatePreset(this.configWriter, selectedPreset);
|
55 | } else {
|
56 | configFileName = await this.initiateCustom(this.configWriter);
|
57 | }
|
58 | await this.gitignoreWriter.addStrykerTempFolder();
|
59 | this.out(`Done configuring stryker. Please review "${configFileName}", you might need to configure your test runner correctly.`);
|
60 | this.out("Let's kill some mutants with this command: `stryker run`");
|
61 | }
|
62 |
|
63 | |
64 |
|
65 |
|
66 |
|
67 | private patchProxies() {
|
68 | const copyEnvVariable = (from: string, to: string) => {
|
69 | if (process.env[from] && !process.env[to]) {
|
70 | process.env[to] = process.env[from];
|
71 | }
|
72 | };
|
73 | copyEnvVariable('http_proxy', 'HTTP_PROXY');
|
74 | copyEnvVariable('https_proxy', 'HTTPS_PROXY');
|
75 | }
|
76 |
|
77 | private async selectPreset(): Promise<Preset | undefined> {
|
78 | const presetOptions: Preset[] = this.strykerPresets;
|
79 | if (presetOptions.length) {
|
80 | this.log.debug(`Found presets: ${JSON.stringify(presetOptions)}`);
|
81 | return this.inquirer.promptPresets(presetOptions);
|
82 | } else {
|
83 | this.log.debug('No presets have been configured, reverting to custom configuration');
|
84 | return undefined;
|
85 | }
|
86 | }
|
87 |
|
88 | private async initiatePreset(configWriter: StrykerConfigWriter, selectedPreset: Preset) {
|
89 | const presetConfig = await selectedPreset.createConfig();
|
90 | const isJsonSelected = await this.selectJsonConfigType();
|
91 | const configFileName = await configWriter.writePreset(presetConfig, isJsonSelected);
|
92 | if (presetConfig.additionalConfigFiles) {
|
93 | await Promise.all(Object.entries(presetConfig.additionalConfigFiles).map(([name, content]) => fsPromises.writeFile(name, content)));
|
94 | }
|
95 | const selectedPackageManager = await this.selectPackageManager();
|
96 | this.installNpmDependencies(presetConfig.dependencies, selectedPackageManager);
|
97 | return configFileName;
|
98 | }
|
99 |
|
100 | private async initiateCustom(configWriter: StrykerConfigWriter) {
|
101 | const selectedTestRunner = await this.selectTestRunner();
|
102 | const buildCommand = await this.getBuildCommand(selectedTestRunner);
|
103 | const selectedReporters = await this.selectReporters();
|
104 | const selectedPackageManager = await this.selectPackageManager();
|
105 | const isJsonSelected = await this.selectJsonConfigType();
|
106 | const npmDependencies = this.getSelectedNpmDependencies([selectedTestRunner].concat(selectedReporters));
|
107 | const packageInfo = await this.fetchAdditionalConfig(npmDependencies);
|
108 | const pkgInfoOfSelectedTestRunner = packageInfo.find((pkg) => pkg.name == selectedTestRunner.pkg?.name);
|
109 | const additionalConfig = packageInfo.map((dep) => dep.initStrykerConfig ?? {}).filter(notEmpty);
|
110 |
|
111 | const configFileName = await configWriter.write(
|
112 | selectedTestRunner,
|
113 | buildCommand,
|
114 | selectedReporters,
|
115 | selectedPackageManager,
|
116 | npmDependencies.map((pkg) => pkg.name),
|
117 | additionalConfig,
|
118 | pkgInfoOfSelectedTestRunner?.homepage ?? "(missing 'homepage' URL in package.json)",
|
119 | isJsonSelected
|
120 | );
|
121 | this.installNpmDependencies(
|
122 | npmDependencies.map((pkg) => pkg.name),
|
123 | selectedPackageManager
|
124 | );
|
125 | return configFileName;
|
126 | }
|
127 |
|
128 | private async selectTestRunner(): Promise<PromptOption> {
|
129 | const testRunnerOptions = await this.client.getTestRunnerOptions();
|
130 | this.log.debug(`Found test runners: ${JSON.stringify(testRunnerOptions)}`);
|
131 | return this.inquirer.promptTestRunners(testRunnerOptions);
|
132 | }
|
133 |
|
134 | private async getBuildCommand(selectedTestRunner: PromptOption): Promise<PromptOption> {
|
135 | const shouldSkipQuestion = selectedTestRunner.name === 'jest';
|
136 | return this.inquirer.promptBuildCommand(shouldSkipQuestion);
|
137 | }
|
138 |
|
139 | private async selectReporters(): Promise<PromptOption[]> {
|
140 | const reporterOptions = await this.client.getTestReporterOptions();
|
141 | reporterOptions.push(
|
142 | {
|
143 | name: 'html',
|
144 | pkg: null,
|
145 | },
|
146 | {
|
147 | name: 'clear-text',
|
148 | pkg: null,
|
149 | },
|
150 | {
|
151 | name: 'progress',
|
152 | pkg: null,
|
153 | },
|
154 | {
|
155 | name: 'dashboard',
|
156 | pkg: null,
|
157 | }
|
158 | );
|
159 | return this.inquirer.promptReporters(reporterOptions);
|
160 | }
|
161 |
|
162 | private async selectPackageManager(): Promise<PromptOption> {
|
163 | return this.inquirer.promptPackageManager([
|
164 | {
|
165 | name: PackageManager.Npm,
|
166 | pkg: null,
|
167 | },
|
168 | {
|
169 | name: PackageManager.Yarn,
|
170 | pkg: null,
|
171 | },
|
172 | {
|
173 | name: PackageManager.Pnpm,
|
174 | pkg: null,
|
175 | },
|
176 | ]);
|
177 | }
|
178 |
|
179 | private async selectJsonConfigType(): Promise<boolean> {
|
180 | return this.inquirer.promptJsonConfigType();
|
181 | }
|
182 |
|
183 | private getSelectedNpmDependencies(selectedOptions: Array<PromptOption | null>): PackageInfo[] {
|
184 | return selectedOptions
|
185 | .filter(notEmpty)
|
186 | .map((option) => option.pkg)
|
187 | .filter(notEmpty);
|
188 | }
|
189 |
|
190 | |
191 |
|
192 |
|
193 |
|
194 | private installNpmDependencies(dependencies: string[], selectedOption: PromptOption): void {
|
195 | if (dependencies.length === 0) {
|
196 | return;
|
197 | }
|
198 |
|
199 | const dependencyArg = dependencies.join(' ');
|
200 | this.out('Installing NPM dependencies...');
|
201 | const cmd = this.getInstallCommand(selectedOption.name as PackageManager, dependencyArg);
|
202 | this.out(cmd);
|
203 | try {
|
204 | childProcess.execSync(cmd, { stdio: [0, 1, 2] });
|
205 | } catch (_) {
|
206 | this.out(`An error occurred during installation, please try it yourself: "${cmd}"`);
|
207 | }
|
208 | }
|
209 |
|
210 | private getInstallCommand(packageManager: PackageManager, dependencyArg: string): string {
|
211 | switch (packageManager) {
|
212 | case PackageManager.Yarn:
|
213 | return `yarn add ${dependencyArg} --dev`;
|
214 | case PackageManager.Pnpm:
|
215 | return `pnpm add -D ${dependencyArg}`;
|
216 | case PackageManager.Npm:
|
217 | return `npm i --save-dev ${dependencyArg}`;
|
218 | }
|
219 | }
|
220 |
|
221 | private async fetchAdditionalConfig(dependencies: PackageInfo[]): Promise<NpmPackage[]> {
|
222 | return await Promise.all(dependencies.map((dep) => this.client.getAdditionalConfig(dep)));
|
223 | }
|
224 | }
|