Skip to content

Commit

Permalink
feat(core): add support for pnpm as package manager (#3802)
Browse files Browse the repository at this point in the history
Add `pnpm` as package manager option to the `stryker init` command. When selecting `pnpm`, the installed plugins will be explicitly listed inside your stryker.conf.json file.

Co-authored-by: Nico Jansen <jansennico@gmail.com>
  • Loading branch information
AbdelHajou and nicojs committed Oct 27, 2022
1 parent e1f24d9 commit af0e34e
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 8 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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`
Expand Down
34 changes: 33 additions & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
+ ]
}
```
3 changes: 2 additions & 1 deletion packages/api/schema/stryker-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,8 @@
"packageManager": {
"enum": [
"npm",
"yarn"
"yarn",
"pnpm"
],
"description": "The package manager Stryker can use to install missing dependencies."
},
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/initializer/stryker-config-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ export class StrykerConfigWriter {
buildCommand: PromptOption,
selectedReporters: PromptOption[],
selectedPackageManager: PromptOption,
requiredPlugins: string[],
additionalPiecesOfConfig: Array<Partial<StrykerOptions>>,
exportAsJson: boolean
): Promise<string> {
const configObject: Partial<StrykerOptions> & { _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',
Expand All @@ -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);
}
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/initializer/stryker-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { initializerTokens } from './index.js';
const enum PackageManager {
Npm = 'npm',
Yarn = 'yarn',
Pnpm = 'pnpm',
}

export class StrykerInitializer {
Expand Down Expand Up @@ -108,6 +109,7 @@ export class StrykerInitializer {
buildCommand,
selectedReporters,
selectedPackageManager,
npmDependencies.map((pkg) => pkg.name),
await this.fetchAdditionalConfig(npmDependencies),
isJsonSelected
);
Expand Down Expand Up @@ -162,6 +164,10 @@ export class StrykerInitializer {
name: PackageManager.Yarn,
pkg: null,
},
{
name: PackageManager.Pnpm,
pkg: null,
},
]);
}

Expand All @@ -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] });
Expand All @@ -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<Array<Record<string, unknown>>> {
return (await Promise.all(dependencies.map((dep) => this.client.getAdditionalConfig(dep)))).filter(notEmpty);
}
Expand Down
39 changes: 37 additions & 2 deletions packages/core/test/unit/initializer/stryker-initializer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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',
Expand Down

0 comments on commit af0e34e

Please sign in to comment.