Skip to content

Commit

Permalink
feat(init): add svelte custom initializer (#4625)
Browse files Browse the repository at this point in the history
* Add svelte custom initializer
* Add guide to configure Stryker for a svelte project
  • Loading branch information
nicojs authored Nov 30, 2023
1 parent 3d02969 commit 418722d
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 17 deletions.
67 changes: 67 additions & 0 deletions docs/guides/svelte.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: Svelte
custom_edit_url: https://github.com/stryker-mutator/stryker-js/edit/master/docs/guides/svelte.md
---

Stryker supports Svelte projects out-of-the-box as of Svelte version `>=3.30`. It will also mutate `.svelte` files using your installed version of the svelte compiler.

<details>

<summary>History</summary>

| Version | Changes |
| ------- | ---------------------------------------- |
| 8.0 | Add support for mutating `.svelte` files |

</details>


## Vitest

This guide assumes you're using the [vitest examples](https://vitest.dev/guide/#examples) as a starting point for unit testing svelte projects with vitest.

### Install

Recommended stryker packages: `npm i -D @stryker-mutator/core @stryker-mutator/vitest-runner`

### Configuration

After installing the recommended packages, create the `stryker.config.json` file in your repository.
The configuration below contains a good starting point for Svelte projects.
You may have to change some paths like the [mutate](../configuration.md#mutate-string) array.

```json
{
"testRunner": "vitest"
}
```

## Jest

Using jest to test your svelte projects can be done using something like the [svelte-jester](https://github.com/svelteness/svelte-jester#svelte-jester) plugin.


### Install

Recommended stryker packages: `npm i -D @stryker-mutator/core @stryker-mutator/jest-runner`

### Configuration

After installing the recommended packages, create the `stryker.config.json` file in your repository.
The configuration below contains a good starting point for Svelte projects.
You may have to change some paths like the [mutate](../configuration.md#mutate-string) array.

```json
{
"testRunner": "jest"
}
```

If you're using native esm, you will also need to set the `--experimental-vm-modules` flag.

```diff
{
"testRunner": "jest",
+ "testRunnerNodeArgs": ["--experimental-vm-modules"]
}
```
3 changes: 2 additions & 1 deletion docs/sidebar.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
{
"Guides": [
"stryker-js/guides/angular",
"stryker-js/guides/nodejs",
"stryker-js/guides/react",
"stryker-js/guides/svelte",
"stryker-js/guides/vuejs",
"stryker-js/guides/nodejs",
"stryker-js/guides/create-a-plugin"
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fs from 'fs/promises';

import { execaCommand } from 'execa';
import { StrykerOptions } from '@stryker-mutator/api/core';
import { resolveFromCwd } from '@stryker-mutator/util';
import { Immutable, resolveFromCwd } from '@stryker-mutator/util';
import { commonTokens } from '@stryker-mutator/api/plugin';
import { Logger } from '@stryker-mutator/api/logging';

Expand All @@ -29,7 +29,7 @@ export class AngularInitializer implements CustomInitializer {
public readonly name = 'angular-cli';
// Please keep config in sync with handbook
private readonly dependencies = ['@stryker-mutator/karma-runner'];
private readonly config: Partial<StrykerOptions> = {
private readonly config: Immutable<Partial<StrykerOptions>> = {
mutate: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/test.ts', '!src/environments/*.ts'],
testRunner: 'karma',
karma: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { PartialStrykerOptions } from '@stryker-mutator/api/core';
import { Immutable } from '@stryker-mutator/util';

export interface CustomInitializer {
readonly name: string;
createConfig(): Promise<CustomInitializerConfiguration>;
}

export interface CustomInitializerConfiguration {
config: PartialStrykerOptions;
config: Immutable<PartialStrykerOptions>;
guideUrl: string;
dependencies: string[];
additionalConfigFiles?: Record<string, string>;
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/initializer/custom-initializers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import { AngularInitializer } from './angular-initializer.js';
import { CustomInitializer } from './custom-initializer.js';
import { ReactInitializer } from './react-initializer.js';
import { VueJsInitializer } from './vue-js-initializer.js';
import { SvelteInitializer } from './svelte-initializer.js';

interface CustomInitializerContext extends BaseContext {
[coreTokens.execa]: typeof import('execa').execaCommand;
[coreTokens.resolveFromCwd]: typeof resolveFromCwd;
}

export function createInitializers(injector: Injector<CustomInitializerContext>): CustomInitializer[] {
return [injector.injectClass(AngularInitializer), injector.injectClass(ReactInitializer), injector.injectClass(VueJsInitializer)];
return [
injector.injectClass(AngularInitializer),
injector.injectClass(ReactInitializer),
injector.injectClass(SvelteInitializer),
injector.injectClass(VueJsInitializer),
];
}
createInitializers.inject = [commonTokens.injector] as const;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { StrykerOptions } from '@stryker-mutator/api/core';
import { Immutable } from '@stryker-mutator/util';

import { CustomInitializer, CustomInitializerConfiguration } from './custom-initializer.js';

Expand All @@ -12,7 +13,7 @@ export class ReactInitializer implements CustomInitializer {
public readonly name = 'create-react-app';
private readonly dependencies = ['@stryker-mutator/jest-runner'];

private readonly config: Partial<StrykerOptions> = {
private readonly config: Immutable<Partial<StrykerOptions>> = {
testRunner: 'jest',
reporters: ['progress', 'clear-text', 'html'],
coverageAnalysis: 'off',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import inquirer from 'inquirer';

import { CustomInitializer, CustomInitializerConfiguration } from './custom-initializer.js';

const guideUrl = 'https://stryker-mutator.io/docs/stryker-js/guides/svelte';
const reporters = Object.freeze(['progress', 'clear-text', 'html']);

export class SvelteInitializer implements CustomInitializer {
public readonly name = 'svelte';

public async createConfig(): Promise<CustomInitializerConfiguration> {
const testRunnerChoices = ['jest', 'vitest'];
const testRunnerNodeArgs: string[] = [];
const { testRunner } = await inquirer.prompt<{ testRunner: string }>({
choices: testRunnerChoices,
message: 'Which test runner are you using?',
name: 'testRunner',
type: 'list',
});
if (testRunner === 'jest') {
const { nativeEsm } = await inquirer.prompt<{ nativeEsm: boolean }>({
type: 'confirm',
name: 'nativeEsm',
message: 'Are you using native EcmaScript modules? (see https://jestjs.io/docs/ecmascript-modules)',
default: true,
});
if (nativeEsm) {
testRunnerNodeArgs.push('--experimental-vm-modules');
}
}
return {
config: {
testRunner,
...(testRunnerNodeArgs.length ? { testRunnerNodeArgs } : {}),
reporters,
},
dependencies: [`@stryker-mutator/${testRunner}-runner`],
guideUrl,
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PartialStrykerOptions } from '@stryker-mutator/api/core';
import { Immutable } from '@stryker-mutator/util';

import { CustomInitializer, CustomInitializerConfiguration } from './custom-initializer.js';

Expand All @@ -11,7 +12,7 @@ const guideUrl = 'https://stryker-mutator.io/docs/stryker-js/guides/vuejs';
export class VueJsInitializer implements CustomInitializer {
public readonly name = 'vue';

private readonly vitestConf: PartialStrykerOptions = {
private readonly vitestConf: Immutable<PartialStrykerOptions> = {
testRunner: 'vitest',
reporters: ['progress', 'clear-text', 'html'],
};
Expand Down
20 changes: 12 additions & 8 deletions packages/core/src/initializer/stryker-config-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { promises as fs } from 'fs';
import { PartialStrykerOptions, StrykerOptions } from '@stryker-mutator/api/core';
import { Logger } from '@stryker-mutator/api/logging';
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
import { childProcessAsPromised } from '@stryker-mutator/util';
import { Immutable, childProcessAsPromised } from '@stryker-mutator/util';

import { fileUtils } from '../utils/file-utils.js';
import { CommandTestRunner } from '../test-runner/command-test-runner.js';
Expand Down Expand Up @@ -60,20 +60,24 @@ export class StrykerConfigWriter {
};

// Only write buildCommand to config file if non-empty
if (buildCommand.name) configObject.buildCommand = buildCommand.name;
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;
if (selectedPackageManager.name === 'pnpm') {
configObject.plugins = requiredPlugins;
}

Object.assign(configObject, ...additionalPiecesOfConfig);
return this.writeStrykerConfig(configObject, exportAsJson);
return this.writeStrykerConfig(configObject as Immutable<PartialStrykerOptions>, exportAsJson);
}

/**
* Create config based on the chosen preset
* @function
*/
public async writePreset(initializerConfig: CustomInitializerConfiguration, exportAsJson: boolean): Promise<string> {
public async writeCustomInitializer(initializerConfig: CustomInitializerConfiguration, exportAsJson: boolean): Promise<string> {
const config = {
_comment: `This config was generated using 'stryker init'. Please see the guide for more information: ${initializerConfig.guideUrl}`,
...initializerConfig.config,
Expand All @@ -82,15 +86,15 @@ export class StrykerConfigWriter {
return this.writeStrykerConfig(config, exportAsJson);
}

private writeStrykerConfig(config: PartialStrykerOptions, exportAsJson: boolean) {
private writeStrykerConfig(config: Immutable<PartialStrykerOptions>, exportAsJson: boolean) {
if (exportAsJson) {
return this.writeJsonConfig(config);
} else {
return this.writeJsConfig(config);
}
}

private async writeJsConfig(commentedConfig: PartialStrykerOptions) {
private async writeJsConfig(commentedConfig: Immutable<PartialStrykerOptions>) {
const configFileName = DEFAULT_CONFIG_FILE_NAMES.JAVASCRIPT;
this.out(`Writing & formatting ${configFileName} ...`);
const rawConfig = this.stringify(commentedConfig);
Expand All @@ -109,7 +113,7 @@ export class StrykerConfigWriter {
return configFileName;
}

private async writeJsonConfig(commentedConfig: PartialStrykerOptions) {
private async writeJsonConfig(commentedConfig: Immutable<PartialStrykerOptions>) {
const configFileName = DEFAULT_CONFIG_FILE_NAMES.JSON;
this.out(`Writing & formatting ${configFileName}...`);
const typedConfig = {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/initializer/stryker-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class StrykerInitializer {
private async initiateInitializer(configWriter: StrykerConfigWriter, selectedPreset: CustomInitializer) {
const presetConfig = await selectedPreset.createConfig();
const isJsonSelected = await this.selectJsonConfigType();
const configFileName = await configWriter.writePreset(presetConfig, isJsonSelected);
const configFileName = await configWriter.writeCustomInitializer(presetConfig, isJsonSelected);
if (presetConfig.additionalConfigFiles) {
await Promise.all(Object.entries(presetConfig.additionalConfigFiles).map(([name, content]) => fsPromises.writeFile(name, content)));
}
Expand Down
75 changes: 74 additions & 1 deletion packages/core/test/unit/initializer/custom-initializers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import { AngularInitializer } from '../../../src/initializer/custom-initializers
import { ReactInitializer } from '../../../src/initializer/custom-initializers/react-initializer.js';
import { VueJsInitializer } from '../../../src/initializer/custom-initializers/vue-js-initializer.js';
import { fileUtils } from '../../../src/utils/file-utils.js';
import { SvelteInitializer } from '../../../src/initializer/custom-initializers/svelte-initializer.js';
import { CustomInitializerConfiguration } from '../../../src/initializer/custom-initializers/custom-initializer.js';

describe('CustomInitializers', () => {
let inquirerPrompt: sinon.SinonStub;
let inquirerPrompt: sinon.SinonStubbedMember<typeof inquirer.prompt>;

beforeEach(() => {
inquirerPrompt = sinon.stub(inquirer, 'prompt');
Expand Down Expand Up @@ -167,6 +169,77 @@ describe('CustomInitializers', () => {
});
});

describe(SvelteInitializer.name, () => {
let sut: SvelteInitializer;
const guideUrl = 'https://stryker-mutator.io/docs/stryker-js/guides/svelte';

beforeEach(() => {
sut = testInjector.injector.injectClass(SvelteInitializer);
});

it('should have the name "svelte"', () => {
expect(sut.name).to.eq('svelte');
});

it('should prompt for test runner choice', async () => {
inquirerPrompt.resolves({ testRunner: 'vitest' });
await sut.createConfig();
sinon.assert.calledOnceWithExactly(inquirerPrompt, {
choices: ['jest', 'vitest'],
message: 'Which test runner are you using?',
name: 'testRunner',
type: 'list',
});
});

it('should write vitest test runner when test runner choice is "vitest"', async () => {
inquirerPrompt.resolves({ testRunner: 'vitest' });
const actualCustomInit = await sut.createConfig();
const expected: CustomInitializerConfiguration = {
config: {
testRunner: 'vitest',
reporters: ['progress', 'clear-text', 'html'],
},
dependencies: ['@stryker-mutator/vitest-runner'],
guideUrl,
};
expect(actualCustomInit).deep.eq(expected);
});

it('should prompt for native ESM when test runner choice is "jest"', async () => {
inquirerPrompt.resolves({ testRunner: 'jest', nativeEsm: false });
await sut.createConfig();
sinon.assert.calledTwice(inquirerPrompt);
sinon.assert.calledWithExactly(inquirerPrompt, {
type: 'confirm',
name: 'nativeEsm',
message: 'Are you using native EcmaScript modules? (see https://jestjs.io/docs/ecmascript-modules)',
default: true,
});
});

it('should add --experimental-vm-modules when using native ESM with jest', async () => {
inquirerPrompt.resolves({ testRunner: 'jest', nativeEsm: true });
const actualCustomInit = await sut.createConfig();
const expected: CustomInitializerConfiguration = {
config: {
testRunner: 'jest',
testRunnerNodeArgs: ['--experimental-vm-modules'],
reporters: ['progress', 'clear-text', 'html'],
},
dependencies: ['@stryker-mutator/jest-runner'],
guideUrl,
};
expect(actualCustomInit).deep.eq(expected);
});

it('should not add --experimental-vm-modules when commonjs with jest', async () => {
inquirerPrompt.resolves({ testRunner: 'jest', nativeEsm: false });
const actualCustomInit = await sut.createConfig();
expect(actualCustomInit.config.testRunnerNodeArgs).undefined;
});
});

describe(VueJsInitializer.name, () => {
let sut: VueJsInitializer;

Expand Down

0 comments on commit 418722d

Please sign in to comment.