UNPKG

7.92 kBPlain TextView Raw
1import childProcess from 'child_process';
2import { promises as fsPromises } from 'fs';
3
4import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
5import { Logger } from '@stryker-mutator/api/logging';
6import { notEmpty } from '@stryker-mutator/util';
7
8import { NpmClient, NpmPackage } from './npm-client.js';
9import { PackageInfo } from './package-info.js';
10import { Preset } from './presets/preset.js';
11import { PromptOption } from './prompt-option.js';
12import { StrykerConfigWriter } from './stryker-config-writer.js';
13import { StrykerInquirer } from './stryker-inquirer.js';
14import { GitignoreWriter } from './gitignore-writer.js';
15
16import { initializerTokens } from './index.js';
17
18const enum PackageManager {
19 Npm = 'npm',
20 Yarn = 'yarn',
21 Pnpm = 'pnpm',
22}
23
24export 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 * Runs the initializer will prompt the user for questions about his setup. After that, install plugins and configure Stryker.
46 * @function
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 * The typed rest client works only with the specific HTTP_PROXY and HTTPS_PROXY env settings.
65 * Let's make sure they are available.
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 * Install the npm packages
192 * @function
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}