diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65a1b72bde..2172bcb83e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ To run local changes you made in StrykerJS in an actual project you have two opt ## VSCode environment configuration -We've chosen to **check in in our vscode configuration**. This makes development unified amongst stryker developers. VSCode is an open source code editor maintained by Microsoft. For more info and the download link, please visit https://code.visualstudio.com/. +We've chosen to **check in our vscode configuration**. This makes development unified amongst stryker developers. VSCode is an open source code editor maintained by Microsoft. For more info and the download link, please visit https://code.visualstudio.com/. We recommend you to install the following plugins: @@ -98,7 +98,7 @@ New features are welcome! Either as requests or proposals. 1. Create a fork on your github account. 1. When writing your code, please conform to the existing coding style. See [.editorconfig](https://github.com/stryker-mutator/stryker-js/blob/master/.editorconfig), the [typescript guidelines](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines) and our tslint.json - * You can check if there are lint issues using `npm run lint:log`. Output will be in root folder in `lint.log` file. + * You can check if there are lint issues using `npm run lint`. * You can automatically fix a lot of lint issues using `npm run lint:fix` 1. Please create or edit unit tests or integration tests. 1. Run the tests using `npm test` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index ac5da12b9b..3a9547336e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -276,4 +276,36 @@ The solution is to include all files that are mutated in your project's tsconfig } ``` -This shouldn't change anything about your Angular project. Your just being a bit more explicit in which files you want to include. +This shouldn't change anything about your Angular project. You're just being a bit more explicit in which files you want to include. + +### Plugins can't be found when using pnpm as package manager + +**Symptom** + +Stryker is unable to load plugins (like `@stryker-mutator/typescript-checker`) when using pnpm as package manager. +You might run into errors like: + +``` +Cannot find TestRunner plugin "mocha". No TestRunner plugins were loaded. +``` + +**Problem** + +When using npm or yarn as package manager, Stryker can automagically load plugins by scanning your `node_modules`. +Because _pnpm_ uses a special directory structure to store dependencies, Stryker can't auto-detect plugins like the `@stryker-mutator/typescript-checker` plugin. + +**Solution** + +Explicitly specify the plugins to load in your Stryker configuration file. + +```diff +{ + "packageManager": "pnpm", + "testRunner": "jest", + "checkers": ["typescript"], ++ "plugins": [ ++ "@stryker-mutator/jest-runner", ++ "@stryker-mutator/typescript-checker" ++ ] +} +``` diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index 3b0697aeb9..d028e7ec1d 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -367,7 +367,8 @@ "packageManager": { "enum": [ "npm", - "yarn" + "yarn", + "pnpm" ], "description": "The package manager Stryker can use to install missing dependencies." }, diff --git a/packages/core/src/initializer/stryker-config-writer.ts b/packages/core/src/initializer/stryker-config-writer.ts index 4474bd8947..de229fe838 100644 --- a/packages/core/src/initializer/stryker-config-writer.ts +++ b/packages/core/src/initializer/stryker-config-writer.ts @@ -43,13 +43,14 @@ export class StrykerConfigWriter { buildCommand: PromptOption, selectedReporters: PromptOption[], selectedPackageManager: PromptOption, + requiredPlugins: string[], additionalPiecesOfConfig: Array>, exportAsJson: boolean ): Promise { const configObject: Partial & { _comment: string } = { _comment: "This config was generated using 'stryker init'. Please take a look at: https://stryker-mutator.io/docs/stryker-js/configuration/ for more information", - packageManager: selectedPackageManager.name as 'npm' | 'yarn', + packageManager: selectedPackageManager.name as 'npm' | 'pnpm' | 'yarn', reporters: selectedReporters.map((rep) => rep.name), testRunner: selectedTestRunner.name, coverageAnalysis: CommandTestRunner.is(selectedTestRunner.name) ? 'off' : 'perTest', @@ -58,6 +59,9 @@ export class StrykerConfigWriter { // Only write buildCommand to config file if non-empty if (buildCommand.name) configObject.buildCommand = buildCommand.name; + // Automatic plugin discovery doesn't work with pnpm, so explicitly specify the required plugins in the config file + if (selectedPackageManager.name === 'pnpm') configObject.plugins = requiredPlugins; + Object.assign(configObject, ...additionalPiecesOfConfig); return this.writeStrykerConfig(configObject, exportAsJson); } diff --git a/packages/core/src/initializer/stryker-initializer.ts b/packages/core/src/initializer/stryker-initializer.ts index 56272ef036..d115d46d02 100644 --- a/packages/core/src/initializer/stryker-initializer.ts +++ b/packages/core/src/initializer/stryker-initializer.ts @@ -18,6 +18,7 @@ import { initializerTokens } from './index.js'; const enum PackageManager { Npm = 'npm', Yarn = 'yarn', + Pnpm = 'pnpm', } export class StrykerInitializer { @@ -108,6 +109,7 @@ export class StrykerInitializer { buildCommand, selectedReporters, selectedPackageManager, + npmDependencies.map((pkg) => pkg.name), await this.fetchAdditionalConfig(npmDependencies), isJsonSelected ); @@ -162,6 +164,10 @@ export class StrykerInitializer { name: PackageManager.Yarn, pkg: null, }, + { + name: PackageManager.Pnpm, + pkg: null, + }, ]); } @@ -187,7 +193,7 @@ export class StrykerInitializer { const dependencyArg = dependencies.join(' '); this.out('Installing NPM dependencies...'); - const cmd = selectedOption.name === PackageManager.Npm ? `npm i --save-dev ${dependencyArg}` : `yarn add ${dependencyArg} --dev`; + const cmd = this.getInstallCommand(selectedOption.name as PackageManager, dependencyArg); this.out(cmd); try { childProcess.execSync(cmd, { stdio: [0, 1, 2] }); @@ -196,6 +202,17 @@ export class StrykerInitializer { } } + 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: PackageInfo[]): Promise>> { return (await Promise.all(dependencies.map((dep) => this.client.getAdditionalConfig(dep)))).filter(notEmpty); } diff --git a/packages/core/test/unit/initializer/stryker-initializer.spec.ts b/packages/core/test/unit/initializer/stryker-initializer.spec.ts index cb1f924c3e..f5edbb7e9b 100644 --- a/packages/core/test/unit/initializer/stryker-initializer.spec.ts +++ b/packages/core/test/unit/initializer/stryker-initializer.spec.ts @@ -123,7 +123,7 @@ describe(StrykerInitializer.name, () => { expect(promptReporters.type).to.eq('checkbox'); expect(promptReporters.choices).to.deep.eq(['dimension', 'mars', 'html', 'clear-text', 'progress', 'dashboard']); expect(promptPackageManagers.type).to.eq('list'); - expect(promptPackageManagers.choices).to.deep.eq(['npm', 'yarn']); + expect(promptPackageManagers.choices).to.deep.eq(['npm', 'yarn', 'pnpm']); expect(promptConfigTypes.type).to.eq('list'); expect(promptConfigTypes.choices).to.deep.eq(['JSON', 'JavaScript']); }); @@ -217,7 +217,7 @@ describe(StrykerInitializer.name, () => { expect(promptConfigType.type).to.eq('list'); expect(promptConfigType.choices).to.deep.eq(['JSON', 'JavaScript']); expect(promptPackageManager.type).to.eq('list'); - expect(promptPackageManager.choices).to.deep.eq(['npm', 'yarn']); + expect(promptPackageManager.choices).to.deep.eq(['npm', 'yarn', 'pnpm']); }); it('should install any additional dependencies', async () => { @@ -234,6 +234,41 @@ describe(StrykerInitializer.name, () => { }); }); + it('should install additional dependencies with pnpm', async () => { + inquirerPrompt.resolves({ + packageManager: 'pnpm', + reporters: [], + testRunner: 'awesome', + }); + await sut.initialize(); + expect(childExecSync).calledWith('pnpm add -D @stryker-mutator/awesome-runner', { + stdio: [0, 1, 2], + }); + }); + + it('should explicitly specify plugins when using pnpm', async () => { + childExec.resolves(); + const expectedOutput = `// @ts-check + /** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */ + const config = { + "_comment": "This config was generated using 'stryker init'. Please take a look at: https://stryker-mutator.io/docs/stryker-js/configuration/ for more information", + "packageManager": "pnpm", + "reporters": [], + "testRunner": "awesome", + "coverageAnalysis": "perTest", + "plugins": [ "@stryker-mutator/awesome-runner" ] + }; + export default config;`; + inquirerPrompt.resolves({ + packageManager: 'pnpm', + reporters: [], + testRunner: 'awesome', + configType: 'JavaScript', + }); + await sut.initialize(); + expectStrykerConfWritten(expectedOutput); + }); + it('should configure testRunner, reporters, and packageManager', async () => { inquirerPrompt.resolves({ packageManager: 'npm',