Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for scoped-registries #61

Merged
merged 11 commits into from
Jul 19, 2023
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,18 @@ export type Suite = {
}
}

export type Registry = {
scope?: string;
url: string;
authToken?: string;
};

export type NpmConfig = {
/**
* @deprecated: registry should be avoided in favor of a "registries" entry.
*/
registry?: string;
registries?: Registry[];
FriggaHel marked this conversation as resolved.
Show resolved Hide resolved
strictSSL?: boolean | string | null;
packageLock?: boolean | string | null;
packages?: { [key: string]: string | number };
Expand Down
30 changes: 29 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export async function setUpNpmConfig (nodeCtx: NodeContext, userConfig: NpmConfi
registry: getDefaultRegistry(),
'update-notifier': false
};

await npm.configure(nodeCtx, Object.assign({}, defaultConfig, userConfig));
}

Expand Down Expand Up @@ -94,16 +95,43 @@ export function hasNodeModulesFolder (runCfg: PathContainer) {
return false;
}

function getRegistryAuthConfigField (url: string): string {
let authUrl = url;
if (authUrl.startsWith('http://')) {
authUrl = url.substring(5);
} else if (authUrl.startsWith('https://')) {
authUrl = url.substring(6);
}
return `${authUrl}:_authToken`;
}

export function getNpmConfig (runnerConfig: NpmConfigContainer) {
if (runnerConfig.npm === undefined) {
return {};
}
return {
const cfg: { [key:string]: string | boolean | null } = {
registry: runnerConfig.npm.registry || getDefaultRegistry(),
'strict-ssl': runnerConfig.npm.strictSSL !== false,
// Setting to false to avoid dealing with the generated file.
'package-lock': runnerConfig.npm.packageLock === true
};

// As npm config accepts only key-value pairs, we do the translation
if (runnerConfig.npm.registries) {
for (const sr of runnerConfig.npm.registries) {
if (sr.scope) {
cfg[`${sr.scope}:registry`] = sr.url;
} else {
cfg.registry = sr.url;
}

if (sr.authToken) {
const field = getRegistryAuthConfigField(sr.url);
cfg[field] = sr.authToken;
}
}
}
return cfg;
}

export async function prepareNpmEnv (runCfg: NpmConfigContainer & PathContainer, nodeCtx: NodeContext) {
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/src/npm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ describe('NPM', function () {

it('.configure must invoke npm config set', async function () {
const interceptor = spawk.spawn(nodeCtx.nodePath).stdout('npm runned').exit(0);
await NPM.configure(nodeCtx, { registry: 'myregistry' });
await NPM.configure(nodeCtx, { registry: 'myregistry', '@saucelabs:registry': 'https://google.com/' });

expect(interceptor.calledWith.command).toEqual(nodeCtx.nodePath);
expect(interceptor.calledWith.args).toEqual([nodeCtx.npmPath, 'config', 'set', 'registry=myregistry']);
expect(interceptor.calledWith.args).toEqual([nodeCtx.npmPath, 'config', 'set', 'registry=myregistry', '@saucelabs:registry=https://google.com/']);
});

it('.rebuild must invoke npm rebuild', async function () {
Expand Down
61 changes: 57 additions & 4 deletions tests/unit/src/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ describe('utils', function () {
it('should use user registry', async function () {
const cfg = _.cloneDeep(runCfg);
cfg.npm ||= {};
cfg.npm.registry = 'registryland.io';
cfg.npm.registries = [{ url: 'registryland.io' }];
const loadSpyOn = jest.spyOn(npm, 'configure');
await prepareNpmEnv(cfg, nodeCtx);
expect(loadSpyOn.mock.calls[loadSpyOn.mock.calls.length - 1]).toMatchSnapshot();
Expand All @@ -172,7 +172,7 @@ describe('utils', function () {
const cfg = _.cloneDeep(runCfg);
cfg.npm ||= {};
cfg.npm.strictSSL = null;
cfg.npm.registry = 'test.strictSSL.null';
cfg.npm.registries = [{ url: 'test.strictSSL.null' }];
const loadSpyOn = jest.spyOn(npm, 'configure');
await prepareNpmEnv(runCfg, nodeCtx);
expect(loadSpyOn.mock.calls[loadSpyOn.mock.calls.length - 1]).toMatchSnapshot();
Expand All @@ -181,7 +181,7 @@ describe('utils', function () {
const cfg = _.cloneDeep(runCfg);
cfg.npm ||= {};
cfg.npm.strictSSL = false;
cfg.npm.registry = 'test.strictSSL.false';
cfg.npm.registries = [{ url: 'test.strictSSL.false' }];
const loadSpyOn = jest.spyOn(npm, 'configure');
await prepareNpmEnv(cfg, nodeCtx);
expect(loadSpyOn.mock.calls[loadSpyOn.mock.calls.length - 1]).toMatchSnapshot();
Expand All @@ -190,11 +190,64 @@ describe('utils', function () {
const cfg = _.cloneDeep(runCfg);
cfg.npm ||= {};
cfg.npm.strictSSL = true;
cfg.npm.registry = 'test.strictSSL.true';
cfg.npm.registries = [{ url: 'test.strictSSL.true' }];
const loadSpyOn = jest.spyOn(npm, 'configure');
await prepareNpmEnv(cfg, nodeCtx);
expect(loadSpyOn.mock.calls[loadSpyOn.mock.calls.length - 1]).toMatchSnapshot();
});
it('should configure scoped-registry', async function () {
const cfg = _.cloneDeep(runCfg);
cfg.npm ||= {};
cfg.npm.registries = [{
url: 'http://demo.registry.com/npm-test/',
scope: '@saucelabs',
}];
const loadSpyOn = jest.spyOn(npm, 'configure');
loadSpyOn.mockClear();
await prepareNpmEnv(cfg, nodeCtx);

expect(loadSpyOn).toHaveBeenCalledTimes(1);
const call = loadSpyOn.mock.calls[loadSpyOn.mock.calls.length - 1];
expect(call[1]['@saucelabs:registry']).toBe('http://demo.registry.com/npm-test/');
expect(call[1].registry).toBe('https://registry.npmjs.org');
});
it('should configure scoped-registry with authentication', async function () {
const cfg = _.cloneDeep(runCfg);
cfg.npm ||= {};
cfg.npm.registries = [{
url: 'http://demo.registry.com/npm-test/',
scope: '@saucelabs',
authToken: 'secretToken',
}];
const loadSpyOn = jest.spyOn(npm, 'configure');
loadSpyOn.mockClear();
await prepareNpmEnv(cfg, nodeCtx);
expect(loadSpyOn).toHaveBeenCalledTimes(1);
const call = loadSpyOn.mock.calls[loadSpyOn.mock.calls.length - 1];
expect(call[1]['//demo.registry.com/npm-test/:_authToken']).toBe('secretToken');
expect(call[1]['@saucelabs:registry']).toBe('http://demo.registry.com/npm-test/');
expect(call[1].registry).toBe('https://registry.npmjs.org');
});
it('registries should be prioritary on registry', async function () {
const cfg = _.cloneDeep(runCfg);
cfg.npm ||= {};
cfg.npm.registry = 'http://demo.bad-registry.com',
cfg.npm.registries = [{
url: 'http://demo.registry.com',
}, {
url: 'http://demo.registry.com/npm-test/',
scope: '@saucelabs',
authToken: 'secretToken',
}];
const loadSpyOn = jest.spyOn(npm, 'configure');
loadSpyOn.mockClear();
await prepareNpmEnv(cfg, nodeCtx);
expect(loadSpyOn).toHaveBeenCalledTimes(1);
const call = loadSpyOn.mock.calls[loadSpyOn.mock.calls.length - 1];
expect(call[1]['//demo.registry.com/npm-test/:_authToken']).toBe('secretToken');
expect(call[1]['@saucelabs:registry']).toBe('http://demo.registry.com/npm-test/');
expect(call[1].registry).toBe('http://demo.registry.com');
});
it('should use rebuild node_modules', async function () {
const rebuildSpyOn = jest.spyOn(npm, 'rebuild');
const statSyncSpyOn = jest.spyOn(fs, 'statSync');
Expand Down