diff --git a/packages/core/src/di/core-tokens.ts b/packages/core/src/di/core-tokens.ts index 9dac06728a..8e3a68cdcb 100644 --- a/packages/core/src/di/core-tokens.ts +++ b/packages/core/src/di/core-tokens.ts @@ -3,6 +3,7 @@ export const checkerFactory = 'checkerFactory'; export const checkerConcurrencyTokens = 'checkerConcurrencyTokens'; export const disableTypeChecksHelper = 'disableTypeChecksHelper'; export const execa = 'execa'; +export const execaSync = 'execaSync'; export const dryRunResult = 'dryRunResult'; export const mutants = 'mutants'; export const mutantTestPlanner = 'mutantTestPlanner'; diff --git a/packages/core/src/initializer/index.ts b/packages/core/src/initializer/index.ts index a55b7317bd..adbd810d13 100644 --- a/packages/core/src/initializer/index.ts +++ b/packages/core/src/initializer/index.ts @@ -1,12 +1,11 @@ import { createInjector } from 'typed-inject'; -import { RestClient } from 'typed-rest-client'; -import { execaCommand } from 'execa'; +import { execaCommand, execaCommandSync } from 'execa'; import { resolveFromCwd } from '@stryker-mutator/util'; import { LogLevel } from '@stryker-mutator/api/core'; import { coreTokens, provideLogger } from '../di/index.js'; -import { LogConfigurator } from '../logging/log-configurator.js'; +import { LogConfigurator } from '../logging/index.js'; import * as initializerTokens from './initializer-tokens.js'; import { NpmClient } from './npm-client.js'; @@ -15,20 +14,21 @@ import { StrykerInitializer } from './stryker-initializer.js'; import { StrykerInquirer } from './stryker-inquirer.js'; import { createInitializers } from './custom-initializers/index.js'; import { GitignoreWriter } from './gitignore-writer.js'; - -const NPM_REGISTRY = 'https://registry.npmjs.com'; +import { createNpmRegistryClient, getRegistry } from './npm-registry.js'; export function initializerFactory(): StrykerInitializer { LogConfigurator.configureMainProcess(LogLevel.Information); return provideLogger(createInjector()) .provideValue(initializerTokens.out, console.log) - .provideValue(initializerTokens.restClientNpm, new RestClient('npm', NPM_REGISTRY)) + .provideValue(coreTokens.execa, execaCommand) + .provideValue(coreTokens.execaSync, execaCommandSync) + .provideValue(coreTokens.resolveFromCwd, resolveFromCwd) + .provideFactory(initializerTokens.npmRegistry, getRegistry) + .provideFactory(initializerTokens.restClientNpm, createNpmRegistryClient) .provideClass(initializerTokens.npmClient, NpmClient) .provideClass(initializerTokens.configWriter, StrykerConfigWriter) .provideClass(initializerTokens.gitignoreWriter, GitignoreWriter) .provideClass(initializerTokens.inquirer, StrykerInquirer) - .provideValue(coreTokens.execa, execaCommand) - .provideValue(coreTokens.resolveFromCwd, resolveFromCwd) .provideFactory(initializerTokens.customInitializers, createInitializers) .injectClass(StrykerInitializer); } diff --git a/packages/core/src/initializer/initializer-tokens.ts b/packages/core/src/initializer/initializer-tokens.ts index b71e5cb9c4..c33acc1c55 100644 --- a/packages/core/src/initializer/initializer-tokens.ts +++ b/packages/core/src/initializer/initializer-tokens.ts @@ -1,5 +1,6 @@ export const restClientNpm = 'restClientNpm'; export const npmClient = 'npmClient'; +export const npmRegistry = 'npmRegistry'; export const customInitializers = 'strykerPresets'; export const configWriter = 'configWriter'; export const gitignoreWriter = 'gitignoreWriter'; diff --git a/packages/core/src/initializer/npm-client.ts b/packages/core/src/initializer/npm-client.ts index 83bbfbdee4..2358785256 100644 --- a/packages/core/src/initializer/npm-client.ts +++ b/packages/core/src/initializer/npm-client.ts @@ -34,10 +34,11 @@ const handleResult = }; export class NpmClient { - public static inject = tokens(commonTokens.logger, initializerTokens.restClientNpm); + public static inject = tokens(commonTokens.logger, initializerTokens.restClientNpm, initializerTokens.npmRegistry); constructor( private readonly log: Logger, private readonly innerNpmClient: RestClient, + private readonly npmRegistry: string, ) {} public getTestRunnerOptions(): Promise { @@ -65,7 +66,7 @@ export class NpmClient { const response = await this.innerNpmClient.get(path); return handleResult(path)(response); } catch (err) { - this.log.error(`Unable to reach 'https://registry.npmjs.com' (for query ${path}). Please check your internet connection.`, errorToString(err)); + this.log.error(`Unable to reach '${this.npmRegistry}' (for query ${path}). Please check your internet connection.`, errorToString(err)); const result: NpmSearchResult = { objects: [], total: 0, diff --git a/packages/core/src/initializer/npm-registry.ts b/packages/core/src/initializer/npm-registry.ts new file mode 100644 index 0000000000..510337af6a --- /dev/null +++ b/packages/core/src/initializer/npm-registry.ts @@ -0,0 +1,42 @@ +import { execaCommandSync } from 'execa'; +import { RestClient } from 'typed-rest-client'; +import * as initializerTokens from './initializer-tokens.js'; +import { coreTokens } from '../di/index.js'; +import { errorToString } from '@stryker-mutator/util'; +import { Logger } from '@stryker-mutator/api/logging'; +import { commonTokens } from '@stryker-mutator/api/plugin'; + +const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.com'; + +function getRegistry(logger: Logger, execaSync: typeof execaCommandSync): string { + if (process.env.npm_config_registry) { + return process.env.npm_config_registry; + } else if (process.env.npm_command) { + // if running inside npm and not having the registry than it's the default one + return DEFAULT_NPM_REGISTRY; + } else { + // Using global as when trying to get the registry inside npm workspace it would fail + try { + const registry = execaSync('npm config get --global registry', { + stdout: 'pipe', + timeout: 20000, + }); + + return registry.stdout.trim(); + } catch (e) { + logger.warn('Could not run `npm config get --global registry` falling back to default npm registry.', errorToString(e)); + + return DEFAULT_NPM_REGISTRY; + } + } +} + +getRegistry.inject = [commonTokens.logger, coreTokens.execaSync] as const; + +function createNpmRegistryClient(npmRegistry: string): RestClient { + return new RestClient('npm', npmRegistry); +} + +createNpmRegistryClient.inject = [initializerTokens.npmRegistry] as const; + +export { createNpmRegistryClient, getRegistry }; diff --git a/packages/core/test/unit/initializer/npm-registry.spec.ts b/packages/core/test/unit/initializer/npm-registry.spec.ts new file mode 100644 index 0000000000..d1a277c5ad --- /dev/null +++ b/packages/core/test/unit/initializer/npm-registry.spec.ts @@ -0,0 +1,85 @@ +import { getRegistry } from '../../../src/initializer/npm-registry.js'; +import { expect } from 'chai'; +import { testInjector } from '@stryker-mutator/test-helpers'; +import sinon from 'sinon'; +import { execaCommandSync } from 'execa'; + +const DEFAULT_REGISTRY = 'https://registry.npmjs.com'; + +describe('npm registry', () => { + describe('get registry', () => { + let oldNpmConfigRegistry: string | undefined; + let oldNpmCommand: string | undefined; + + beforeEach(() => { + oldNpmConfigRegistry = process.env.npm_config_registry; + oldNpmCommand = process.env.npm_command; + }); + + afterEach(() => { + process.env.npm_config_registry = oldNpmConfigRegistry; + process.env.npm_command = oldNpmCommand; + }); + + it('should return default repository when run with npm command with no custom repository', () => { + const execaCommandSyncMock = sinon.spy(); + + process.env.npm_config_registry = ''; + process.env.npm_command = 'value'; + + const registry = getRegistry(testInjector.logger, execaCommandSyncMock); + + expect(registry).to.equal(DEFAULT_REGISTRY); + sinon.assert.callCount(execaCommandSyncMock, 0); + }); + + it('should return default repository when run with npm command with custom repository', () => { + const execaCommandSyncMock = sinon.spy(); + process.env.npm_config_registry = 'foo'; + process.env.npm_command = 'value'; + + const registry = getRegistry(testInjector.logger, execaCommandSyncMock); + + expect(registry).to.equal('foo'); + sinon.assert.callCount(execaCommandSyncMock, 0); + }); + + it('should return globally configured npm registry when run with node command', () => { + const expectedRegistry = 'http://my.custom.npm.registry.stryker'; + const execaCommandSyncMock = sinon.spy((_command, _options) => ({ stdout: expectedRegistry })); + process.env.npm_config_registry = ''; + process.env.npm_command = ''; + + const registry = getRegistry(testInjector.logger, execaCommandSyncMock as unknown as typeof execaCommandSync); + + sinon.assert.calledOnceWithExactly(execaCommandSyncMock, 'npm config get --global registry', { + stdout: 'pipe', + timeout: 20000, + }); + + expect(registry).to.equal(expectedRegistry); + }); + + it('should return default repository and warn the user if run with node command and execa command failed', () => { + const execaCommandSyncMock = sinon.spy((_command, _options) => { + throw new Error(); + }); + + process.env.npm_config_registry = ''; + process.env.npm_command = ''; + + const registry = getRegistry(testInjector.logger, execaCommandSyncMock as unknown as typeof execaCommandSync); + + sinon.assert.calledOnceWithExactly(execaCommandSyncMock, 'npm config get --global registry', { + stdout: 'pipe', + timeout: 20000, + }); + + expect(registry).to.equal(DEFAULT_REGISTRY); + sinon.assert.calledOnceWithMatch( + testInjector.logger.warn, + 'Could not run `npm config get --global registry` falling back to default npm registry.', + ); + }); + }); +}); diff --git a/packages/core/test/unit/initializer/stryker-initializer.spec.ts b/packages/core/test/unit/initializer/stryker-initializer.spec.ts index 3ff6e2c820..dd9ade1b08 100644 --- a/packages/core/test/unit/initializer/stryker-initializer.spec.ts +++ b/packages/core/test/unit/initializer/stryker-initializer.spec.ts @@ -8,7 +8,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import typedRestClient, { type RestClient } from 'typed-rest-client/RestClient.js'; -import { fileUtils } from '../../../src/utils/file-utils.js'; +import { fileUtils } from '../../../src/utils/index.js'; import { initializerTokens } from '../../../src/initializer/index.js'; import { NpmClient, NpmSearchResult } from '../../../src/initializer/npm-client.js'; import { StrykerConfigWriter } from '../../../src/initializer/stryker-config-writer.js'; @@ -16,7 +16,7 @@ import { StrykerInitializer } from '../../../src/initializer/stryker-initializer 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_NAMES } from '../../../src/config/config-file-formats.js'; +import { SUPPORTED_CONFIG_FILE_NAMES } from '../../../src/config/index.js'; import { CustomInitializer, CustomInitializerConfiguration } from '../../../src/initializer/custom-initializers/custom-initializer.js'; import { PackageInfo } from '../../../src/initializer/package-info.js'; import { inquire } from '../../../src/initializer/inquire.js'; @@ -35,6 +35,7 @@ describe(StrykerInitializer.name, () => { let out: sinon.SinonStub; let customInitializers: CustomInitializer[]; let customInitializerMock: Mock; + const defaultNpmRegistry = 'https://registry.npmjs.com'; beforeEach(() => { out = sinon.stub(); @@ -55,6 +56,7 @@ describe(StrykerInitializer.name, () => { syncBuiltinESMExports(); sut = testInjector.injector .provideValue(initializerTokens.out, out as unknown as typeof console.log) + .provideValue(initializerTokens.npmRegistry, defaultNpmRegistry) .provideValue(initializerTokens.restClientNpm, npmRestClient) .provideClass(initializerTokens.inquirer, StrykerInquirer) .provideClass(initializerTokens.npmClient, NpmClient) @@ -428,7 +430,7 @@ describe(StrykerInitializer.name, () => { await sut.initialize(); expect(testInjector.logger.error).calledWith( - "Unable to reach 'https://registry.npmjs.com' (for query /-/v1/search?text=keywords:%40stryker-mutator%2Ftest-runner-plugin). Please check your internet connection.", + `Unable to reach '${defaultNpmRegistry}' (for query /-/v1/search?text=keywords:%40stryker-mutator%2Ftest-runner-plugin). Please check your internet connection.`, ); expect(fs.promises.writeFile).calledWith('stryker.config.json', sinon.match('"testRunner": "command"')); }); @@ -447,7 +449,7 @@ describe(StrykerInitializer.name, () => { await sut.initialize(); expect(testInjector.logger.error).calledWith( - "Unable to reach 'https://registry.npmjs.com' (for query /-/v1/search?text=keywords:%40stryker-mutator%2Freporter-plugin). Please check your internet connection.", + `Unable to reach '${defaultNpmRegistry}' (for query /-/v1/search?text=keywords:%40stryker-mutator%2Freporter-plugin). Please check your internet connection.`, ); expect(fs.promises.writeFile).called; });