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
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ export type Suite = {
}
}

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

export type NpmConfig = {
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
32 changes: 30 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import _ from 'lodash';
import yargs from 'yargs/yargs';
import npm from './npm';
import { NpmConfigContainer, PathContainer, SuitesContainer, Suite, NpmConfig, NodeContext } from './types';
import { NpmConfigContainer, PathContainer, SuitesContainer, Suite, NpmConfig, NodeContext, Registry } from './types';

Check warning on line 7 in src/utils.ts

View workflow job for this annotation

GitHub Actions / build

'Registry' is defined but never used

const DEFAULT_REGISTRY = 'https://registry.npmjs.org';

Expand Down Expand Up @@ -54,6 +54,7 @@
registry: getDefaultRegistry(),
'update-notifier': false
};

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

Expand Down Expand Up @@ -94,16 +95,43 @@
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
65 changes: 65 additions & 0 deletions tests/unit/src/__snapshots__/utils.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@ Object {
}
`;

exports[`utils .prepareNpmEnv registries should be prioritary on registry 1`] = `
Array [
Object {
"nodePath": "node-bin",
"npmPath": "npm-bin",
},
Object {
"//demo.registry.com/npm-test/:_authToken": "secretToken",
"@saucelabs:registry": "http://demo.registry.com/npm-test/",
"audit": false,
"fund": false,
"json": false,
"noproxy": "registry.npmjs.org",
"package-lock": false,
"registry": "http://demo.registry.com",
"save": false,
"strict-ssl": true,
"update-notifier": false,
},
]
`;

exports[`utils .prepareNpmEnv should be able to set strictSSL to false 1`] = `
Array [
Object {
Expand Down Expand Up @@ -69,6 +91,49 @@ Array [
]
`;

exports[`utils .prepareNpmEnv should configure scoped-registry 1`] = `
Array [
Object {
"nodePath": "node-bin",
"npmPath": "npm-bin",
},
Object {
"@saucelabs:registry": "http://demo.registry.com/npm-test/",
"audit": false,
"fund": false,
"json": false,
"noproxy": "registry.npmjs.org",
"package-lock": false,
"registry": "https://registry.npmjs.org",
"save": false,
"strict-ssl": true,
"update-notifier": false,
},
]
`;

exports[`utils .prepareNpmEnv should configure scoped-registry with authentication 1`] = `
Array [
Object {
"nodePath": "node-bin",
"npmPath": "npm-bin",
},
Object {
"//demo.registry.com/npm-test/:_authToken": "secretToken",
"@saucelabs:registry": "http://demo.registry.com/npm-test/",
"audit": false,
"fund": false,
"json": false,
"noproxy": "registry.npmjs.org",
"package-lock": false,
"registry": "https://registry.npmjs.org",
"save": false,
"strict-ssl": true,
"update-notifier": false,
},
]
`;

exports[`utils .prepareNpmEnv should set right registry for npm 1`] = `
Array [
Object {
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
38 changes: 38 additions & 0 deletions tests/unit/src/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,44 @@ describe('utils', function () {
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');
await prepareNpmEnv(cfg, nodeCtx);
expect(loadSpyOn.mock.calls[loadSpyOn.mock.calls.length - 1]).toMatchSnapshot();
FriggaHel marked this conversation as resolved.
Show resolved Hide resolved
});
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');
await prepareNpmEnv(cfg, nodeCtx);
expect(loadSpyOn.mock.calls[loadSpyOn.mock.calls.length - 1]).toMatchSnapshot();
});
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');
await prepareNpmEnv(cfg, nodeCtx);
expect(loadSpyOn.mock.calls[loadSpyOn.mock.calls.length - 1]).toMatchSnapshot();
});
it('should use rebuild node_modules', async function () {
const rebuildSpyOn = jest.spyOn(npm, 'rebuild');
const statSyncSpyOn = jest.spyOn(fs, 'statSync');
Expand Down