Skip to content

Commit

Permalink
feat(init): use registry.npmjs.com for queries (#4298)
Browse files Browse the repository at this point in the history
Use https://registry.npmjs.com as backend for any queries during init. This solves the caching problem and adds vitest and tap as test runners.

- https://registry.npmjs.com/-/v1/search?text=keywords:%40stryker-mutator%2Ftest-runner-plugin for test runner plugins
- https://registry.npmjs.com/%40stryker-mutator%2Fkarma-runner/latest to retrieve package.json
  • Loading branch information
nicojs committed Jun 14, 2023
1 parent 0285644 commit a952edf
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 89 deletions.
6 changes: 2 additions & 4 deletions packages/core/src/initializer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ import { StrykerInquirer } from './stryker-inquirer.js';
import { createInitializers } from './custom-initializers/index.js';
import { GitignoreWriter } from './gitignore-writer.js';

const BASE_NPM_SEARCH = 'https://api.npms.io';
const BASE_NPM_PACKAGE = 'https://www.unpkg.com';
const NPM_REGISTRY = 'https://registry.npmjs.com';

export function initializerFactory(): StrykerInitializer {
LogConfigurator.configureMainProcess(LogLevel.Information);
return provideLogger(createInjector())
.provideValue(initializerTokens.out, console.log)
.provideValue(initializerTokens.restClientNpmSearch, new RestClient('npmSearch', BASE_NPM_SEARCH))
.provideValue(initializerTokens.restClientNpm, new RestClient('npm', BASE_NPM_PACKAGE))
.provideValue(initializerTokens.restClientNpm, new RestClient('npm', NPM_REGISTRY))
.provideClass(initializerTokens.npmClient, NpmClient)
.provideClass(initializerTokens.configWriter, StrykerConfigWriter)
.provideClass(initializerTokens.gitignoreWriter, GitignoreWriter)
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/initializer/initializer-tokens.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export const restClientNpmSearch = 'restClientNpmSearch';
export const restClientNpm = 'restClientNpm';
export const npmClient = 'npmClient';
export const customInitializers = 'strykerPresets';
Expand Down
41 changes: 19 additions & 22 deletions packages/core/src/initializer/npm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,22 @@ import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
import { errorToString } from '@stryker-mutator/util';
import type { IRestResponse, RestClient } from 'typed-rest-client/RestClient.js';

import { PackageInfo } from './package-info.js';
import { PackageInfo, PackageSummary } from './package-info.js';
import { PromptOption } from './prompt-option.js';

import { initializerTokens } from './index.js';

interface NpmSearchResult {
export interface NpmSearchResult {
total: number;
results: Array<{ package: PackageInfo }>;
}

export interface NpmPackage {
name: string;
homepage?: string;
initStrykerConfig?: Record<string, unknown>;
objects: Array<{ package: PackageSummary }>;
}

const getName = (packageName: string) => {
return packageName.replace('@stryker-mutator/', '').replace('stryker-', '').split('-')[0];
};

const mapSearchResultToPromptOption = (searchResults: NpmSearchResult): PromptOption[] =>
searchResults.results.map((result) => ({
searchResults.objects.map((result) => ({
name: getName(result.package.name),
pkg: result.package,
}));
Expand All @@ -40,40 +34,43 @@ const handleResult =
};

export class NpmClient {
public static inject = tokens(commonTokens.logger, initializerTokens.restClientNpmSearch, initializerTokens.restClientNpm);
constructor(private readonly log: Logger, private readonly searchClient: RestClient, private readonly packageClient: RestClient) {}
public static inject = tokens(commonTokens.logger, initializerTokens.restClientNpm);
constructor(private readonly log: Logger, private readonly innerNpmClient: RestClient) {}

public getTestRunnerOptions(): Promise<PromptOption[]> {
return this.search('/v2/search?q=keywords:@stryker-mutator/test-runner-plugin').then(mapSearchResultToPromptOption);
return this.search(`/-/v1/search?text=keywords:${encodeURIComponent('@stryker-mutator/test-runner-plugin')}`).then(mapSearchResultToPromptOption);
}

public getTestReporterOptions(): Promise<PromptOption[]> {
return this.search('/v2/search?q=keywords:@stryker-mutator/reporter-plugin').then(mapSearchResultToPromptOption);
return this.search(`/-/v1/search?text=keywords:${encodeURIComponent('@stryker-mutator/reporter-plugin')}`).then(mapSearchResultToPromptOption);
}

public getAdditionalConfig(pkgInfo: PackageInfo): Promise<NpmPackage> {
const path = `/${pkgInfo.name}@${pkgInfo.version}/package.json`;
return this.packageClient
.get<NpmPackage>(path)
public getAdditionalConfig(pkgInfo: PackageSummary): Promise<PackageInfo> {
const path = `/${encodeURIComponent(pkgInfo.name)}@${pkgInfo.version}`;
return this.innerNpmClient
.get<PackageInfo>(path)
.then(handleResult(path))
.catch((err) => {
this.log.warn(
`Could not fetch additional initialization config for dependency ${pkgInfo.name}. You might need to configure it manually`,
err
);
return { name: pkgInfo.name };
return pkgInfo;
});
}

private search(path: string): Promise<NpmSearchResult> {
this.log.debug(`Searching: ${path}`);
return this.searchClient
return this.innerNpmClient
.get<NpmSearchResult>(path)
.then(handleResult(path))
.catch((err) => {
this.log.error(`Unable to reach npms.io (for query ${path}). Please check your internet connection.`, errorToString(err));
this.log.error(
`Unable to reach 'https://registry.npmjs.com' (for query ${path}). Please check your internet connection.`,
errorToString(err)
);
const result: NpmSearchResult = {
results: [],
objects: [],
total: 0,
};
return result;
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/initializer/package-info.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export interface PackageInfo {
export interface PackageInfo extends PackageSummary {
homepage?: string;
initStrykerConfig?: Record<string, unknown>;
}

export interface PackageSummary {
name: string;
keywords: string[];
version: string;
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/initializer/stryker-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
import { Logger } from '@stryker-mutator/api/logging';
import { notEmpty } from '@stryker-mutator/util';

import { NpmClient, NpmPackage } from './npm-client.js';
import { PackageInfo } from './package-info.js';
import { NpmClient } from './npm-client.js';
import { PackageInfo, PackageSummary } from './package-info.js';
import { CustomInitializer } from './custom-initializers/custom-initializer.js';
import { PromptOption } from './prompt-option.js';
import { StrykerConfigWriter } from './stryker-config-writer.js';
Expand Down Expand Up @@ -218,7 +218,7 @@ export class StrykerInitializer {
}
}

private async fetchAdditionalConfig(dependencies: PackageInfo[]): Promise<NpmPackage[]> {
private async fetchAdditionalConfig(dependencies: PackageSummary[]): Promise<PackageInfo[]> {
return await Promise.all(dependencies.map((dep) => this.client.getAdditionalConfig(dep)));
}
}
111 changes: 53 additions & 58 deletions packages/core/test/unit/initializer/stryker-initializer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import { childProcessAsPromised, normalizeWhitespaces } from '@stryker-mutator/u
import { expect } from 'chai';
import inquirer from 'inquirer';
import sinon from 'sinon';
import typedRestClient, { type RestClient, type IRestResponse } from 'typed-rest-client/RestClient.js';
import typedRestClient, { type RestClient } from 'typed-rest-client/RestClient.js';

import { fileUtils } from '../../../src/utils/file-utils.js';
import { initializerTokens } from '../../../src/initializer/index.js';
import { NpmClient, NpmPackage } from '../../../src/initializer/npm-client.js';
import { PackageInfo } from '../../../src/initializer/package-info.js';
import { NpmClient, NpmSearchResult } from '../../../src/initializer/npm-client.js';
import { StrykerConfigWriter } from '../../../src/initializer/stryker-config-writer.js';
import { StrykerInitializer } from '../../../src/initializer/stryker-initializer.js';
import { StrykerInquirer } from '../../../src/initializer/stryker-inquirer.js';
import { Mock } from '../../helpers/producers.js';
import { GitignoreWriter } from '../../../src/initializer/gitignore-writer.js';
import { SUPPORTED_CONFIG_FILE_EXTENSIONS } from '../../../src/config/config-file-formats.js';
import { CustomInitializer, CustomInitializerConfiguration } from '../../../src/initializer/custom-initializers/custom-initializer.js';
import { PackageInfo } from '../../../src/initializer/package-info.js';

describe(StrykerInitializer.name, () => {
let sut: StrykerInitializer;
Expand All @@ -28,8 +28,7 @@ describe(StrykerInitializer.name, () => {
let childExec: sinon.SinonStub;
let fsWriteFile: sinon.SinonStubbedMember<typeof fs.promises.writeFile>;
let existsStub: sinon.SinonStubbedMember<(typeof fileUtils)['exists']>;
let restClientPackage: sinon.SinonStubbedInstance<RestClient>;
let restClientSearch: sinon.SinonStubbedInstance<RestClient>;
let npmRestClient: sinon.SinonStubbedInstance<RestClient>;
let gitignoreWriter: sinon.SinonStubbedInstance<GitignoreWriter>;
let out: sinon.SinonStub;
let customInitializers: CustomInitializer[];
Expand All @@ -47,40 +46,37 @@ describe(StrykerInitializer.name, () => {
childExecSync = sinon.stub(childProcess, 'execSync');
fsWriteFile = sinon.stub(fs.promises, 'writeFile');
existsStub = sinon.stub(fileUtils, 'exists');
restClientSearch = sinon.createStubInstance(typedRestClient.RestClient);
restClientPackage = sinon.createStubInstance(typedRestClient.RestClient);
npmRestClient = sinon.createStubInstance(typedRestClient.RestClient);
gitignoreWriter = sinon.createStubInstance(GitignoreWriter);
syncBuiltinESMExports();
sut = testInjector.injector
.provideValue(initializerTokens.out, out as unknown as typeof console.log)
.provideValue(initializerTokens.restClientNpm, restClientPackage as unknown as RestClient)
.provideValue(initializerTokens.restClientNpmSearch, restClientSearch as unknown as RestClient)
.provideValue(initializerTokens.restClientNpm, npmRestClient)
.provideClass(initializerTokens.inquirer, StrykerInquirer)
.provideClass(initializerTokens.npmClient, NpmClient)
.provideValue(initializerTokens.customInitializers, customInitializers)
.provideClass(initializerTokens.configWriter, StrykerConfigWriter)
.provideValue(initializerTokens.gitignoreWriter, gitignoreWriter as unknown as GitignoreWriter)
.provideValue(initializerTokens.gitignoreWriter, gitignoreWriter)
.injectClass(StrykerInitializer);
});

describe('initialize()', () => {
beforeEach(() => {
stubTestRunners('@stryker-mutator/awesome-runner', 'stryker-hyper-runner', 'stryker-ghost-runner', '@stryker-mutator/jest-runner');
stubMutators('@stryker-mutator/typescript', '@stryker-mutator/javascript-mutator');
stubReporters('stryker-dimension-reporter', '@stryker-mutator/mars-reporter');
stubPackageClient({
'@stryker-mutator/awesome-runner': null,
'@stryker-mutator/javascript-mutator': null,
'@stryker-mutator/mars-reporter': null,
'@stryker-mutator/typescript': null,
'@stryker-mutator/webpack': null,
'stryker-dimension-reporter': null,
'stryker-ghost-runner': null,
'@stryker-mutator/awesome-runner': undefined,
'@stryker-mutator/javascript-mutator': undefined,
'@stryker-mutator/mars-reporter': undefined,
'@stryker-mutator/typescript': undefined,
'@stryker-mutator/webpack': undefined,
'stryker-dimension-reporter': undefined,
'stryker-ghost-runner': undefined,
'stryker-hyper-runner': {
files: [],
someOtherSetting: 'enabled',
},
'@stryker-mutator/jest-runner': null,
'@stryker-mutator/jest-runner': undefined,
});
fsWriteFile.resolves();
customInitializers.push(customInitializerMock);
Expand Down Expand Up @@ -433,10 +429,9 @@ describe(StrykerInitializer.name, () => {

describe('initialize() when no internet', () => {
it('should log error and continue when fetching test runners', async () => {
restClientSearch.get.withArgs('/v2/search?q=keywords:@stryker-mutator/test-runner-plugin').rejects();
stubMutators('stryker-javascript');
npmRestClient.get.withArgs('/-/v1/search?text=keywords:%40stryker-mutator%2Ftest-runner-plugin').rejects();
stubReporters();
stubPackageClient({ 'stryker-javascript': null, 'stryker-webpack': null });
stubPackageClient({ 'stryker-javascript': undefined, 'stryker-webpack': undefined });
inquirerPrompt.resolves({
packageManager: 'npm',
reporters: ['clear-text'],
Expand All @@ -446,42 +441,40 @@ describe(StrykerInitializer.name, () => {
await sut.initialize();

expect(testInjector.logger.error).calledWith(
'Unable to reach npms.io (for query /v2/search?q=keywords:@stryker-mutator/test-runner-plugin). Please check your internet connection.'
"Unable to reach 'https://registry.npmjs.com' (for query /-/v1/search?text=keywords:%40stryker-mutator%2Ftest-runner-plugin). Please check your internet connection."
);
expect(fs.promises.writeFile).calledWith('stryker.conf.json', sinon.match('"testRunner": "command"'));
});

it('should log error and continue when fetching stryker reporters', async () => {
stubTestRunners('stryker-awesome-runner');
stubMutators('stryker-javascript');
restClientSearch.get.withArgs('/v2/search?q=keywords:@stryker-mutator/reporter-plugin').rejects();
npmRestClient.get.withArgs('/-/v1/search?text=keywords:%40stryker-mutator%2Freporter-plugin').rejects();
inquirerPrompt.resolves({
packageManager: 'npm',
reporters: ['clear-text'],
testRunner: 'awesome',
configType: 'JSON',
});
stubPackageClient({ 'stryker-awesome-runner': null, 'stryker-javascript': null, 'stryker-webpack': null });
stubPackageClient({ 'stryker-awesome-runner': undefined, 'stryker-javascript': undefined, 'stryker-webpack': undefined });

await sut.initialize();

expect(testInjector.logger.error).calledWith(
'Unable to reach npms.io (for query /v2/search?q=keywords:@stryker-mutator/reporter-plugin). Please check your internet connection.'
"Unable to reach 'https://registry.npmjs.com' (for query /-/v1/search?text=keywords:%40stryker-mutator%2Freporter-plugin). Please check your internet connection."
);
expect(fs.promises.writeFile).called;
});

it('should log warning and continue when fetching custom config', async () => {
stubTestRunners('stryker-awesome-runner');
stubMutators();
stubReporters();
inquirerPrompt.resolves({
packageManager: 'npm',
reporters: ['clear-text'],
testRunner: 'awesome',
configType: 'JSON',
});
restClientPackage.get.rejects();
npmRestClient.get.rejects();

await sut.initialize();

Expand Down Expand Up @@ -513,42 +506,44 @@ describe(StrykerInitializer.name, () => {
});

const stubTestRunners = (...testRunners: string[]) => {
restClientSearch.get.withArgs('/v2/search?q=keywords:@stryker-mutator/test-runner-plugin').resolves({
result: {
results: testRunners.map((testRunner) => ({ package: { name: testRunner, version: '1.1.1' } })),
},
statusCode: 200,
} as unknown as IRestResponse<PackageInfo[]>);
};
const testRunnersResult: NpmSearchResult = {
total: testRunners.length,
objects: testRunners.map((testRunner) => ({
package: { name: testRunner, version: '1.1.1', keywords: ['@stryker-mutator/test-runner-plugin'] },
})),
};

const stubMutators = (...mutators: string[]) => {
restClientSearch.get.withArgs('/v2/search?q=keywords:@stryker-mutator/mutator-plugin').resolves({
result: {
results: mutators.map((mutator) => ({ package: { name: mutator, version: '1.1.1' } })),
},
npmRestClient.get.withArgs('/-/v1/search?text=keywords:%40stryker-mutator%2Ftest-runner-plugin').resolves({
result: testRunnersResult,
statusCode: 200,
} as unknown as IRestResponse<PackageInfo[]>);
headers: {},
});
};

const stubReporters = (...reporters: string[]) => {
restClientSearch.get.withArgs('/v2/search?q=keywords:@stryker-mutator/reporter-plugin').resolves({
result: {
results: reporters.map((reporter) => ({ package: { name: reporter, version: '1.1.1' } })),
},
statusCode: 200,
} as unknown as IRestResponse<PackageInfo[]>);
const reportersResult: NpmSearchResult = {
total: reporters.length,
objects: reporters.map((reporter) => ({ package: { name: reporter, version: '1.1.1', keywords: ['@stryker-mutator/reporter-plugin'] } })),
};
npmRestClient.get
.withArgs('/-/v1/search?text=keywords:%40stryker-mutator%2Freporter-plugin')
.resolves({ statusCode: 200, headers: {}, result: reportersResult });
};
const stubPackageClient = (initStrykerConfigPerPackage: Record<string, Record<string, unknown> | null>, homepage?: string | null) => {
Object.keys(initStrykerConfigPerPackage).forEach((packageName) => {
const cfg = initStrykerConfigPerPackage[packageName];
restClientPackage.get.withArgs(`/${packageName}@1.1.1/package.json`).resolves({
result: {
name: packageName,
homepage: homepage,
initStrykerConfig: cfg ?? null,
},
const stubPackageClient = (initStrykerConfigPerPackage: Record<string, Record<string, unknown> | undefined>, homepage?: string) => {
Object.keys(initStrykerConfigPerPackage).forEach((name) => {
const initStrykerConfig = initStrykerConfigPerPackage[name];
const result: PackageInfo = {
name,
homepage,
initStrykerConfig,
keywords: [],
version: '1.1.1',
};
npmRestClient.get.withArgs(`/${encodeURIComponent(name)}@1.1.1`).resolves({
result,
statusCode: 200,
} as unknown as IRestResponse<NpmPackage[]>);
headers: {},
});
});
};

Expand Down

0 comments on commit a952edf

Please sign in to comment.