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 function to load known MetaMask repos #22

Merged
merged 8 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ module.exports = {
overrides: [
{
files: ['*.ts'],
extends: ['@metamask/eslint-config-typescript'],
extends: [
'@metamask/eslint-config-typescript',
'@metamask/eslint-config-nodejs',
],
},

{
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
"test": "jest && jest-it-up",
"test:watch": "jest --watch"
},
"dependencies": {
"execa": "^5.1.1"
},
"devDependencies": {
"@lavamoat/allow-scripts": "^2.3.1",
"@lavamoat/preinstall-always-fail": "^1.0.0",
Expand All @@ -72,6 +75,7 @@
"eslint-plugin-promise": "^6.1.1",
"jest": "^28.1.3",
"jest-it-up": "^2.0.2",
"jest-mock-extended": "^3.0.5",
"prettier": "^2.7.1",
"prettier-plugin-packagejson": "^2.3.0",
"rimraf": "^3.0.2",
Expand Down
43 changes: 43 additions & 0 deletions src/ensure-metamask-repositories-loaded.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import execa from 'execa';

import { ensureMetaMaskRepositoriesLoaded } from './ensure-metamask-repositories-loaded';
import type { PrimaryExecaFunction } from '../tests/helpers';
import { mockExeca } from '../tests/helpers';

jest.mock('execa');

const execaMock = jest.mocked<PrimaryExecaFunction>(execa);

describe('ensureMetaMaskRepositoriesLoaded', () => {
it('requests the repositories under the MetaMask GitHub organization, limiting the data to just a few fields', async () => {
mockExeca(execaMock, [
{
args: [
'gh',
['api', 'orgs/MetaMask/repos', '--cache', '1h', '--paginate'],
],
result: {
stdout: JSON.stringify([
{ name: 'utils', fork: false, archived: false, extra: 'info' },
{ name: 'logo', fork: false, archived: false },
{
name: 'ethjs-util',
fork: true,
archived: false,
something: 'else',
},
{ name: 'test-snaps', fork: true, archived: true },
]),
},
},
]);

const gitHubRepositories = await ensureMetaMaskRepositoriesLoaded();
expect(gitHubRepositories).toStrictEqual([
{ name: 'utils', fork: false, archived: false },
{ name: 'logo', fork: false, archived: false },
{ name: 'ethjs-util', fork: true, archived: false },
{ name: 'test-snaps', fork: true, archived: true },
]);
});
});
42 changes: 42 additions & 0 deletions src/ensure-metamask-repositories-loaded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import execa from 'execa';

/**
* The information about a GitHub repository that we care about. Primarily,
* we want to know whether repos are forks or have been archived, because we
* don't want to lint them.
*/
type GitHubRepository = {
name: string;
fork: boolean;
archived: boolean;
};

/**
* Requests data for the repositories listed under MetaMask's GitHub
* organization via the GitHub API, or returns the results from a previous call.
* The data is cached for an hour to prevent unnecessary calls to the GitHub
* API.
*
* @returns The list of repositories (whether previously or newly cached).
*/
export async function ensureMetaMaskRepositoriesLoaded(): Promise<
GitHubRepository[]
> {
const { stdout } = await execa('gh', [
'api',
'orgs/MetaMask/repos',
'--cache',
'1h',
'--paginate',
]);
const fullGitHubRepositories = JSON.parse(stdout);
return fullGitHubRepositories.map(
(fullGitHubRepository: Record<string, unknown>) => {
return {
name: fullGitHubRepository.name,
fork: fullGitHubRepository.fork,
archived: fullGitHubRepository.archived,
};
},
);
}
9 changes: 0 additions & 9 deletions src/index.test.ts

This file was deleted.

9 changes: 0 additions & 9 deletions src/index.ts

This file was deleted.

75 changes: 75 additions & 0 deletions tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type {
ExecaChildProcess,
Options as ExecaOptions,
ExecaReturnValue,
} from 'execa';
import { mock } from 'jest-mock-extended';
import { inspect, isDeepStrictEqual } from 'util';

/**
* `execa` can be called multiple ways. This is the way that we use it.
*/
export type PrimaryExecaFunction = (
file: string,
args?: readonly string[] | undefined,
options?: ExecaOptions | undefined,
) => ExecaChildProcess;

/**
* Builds an object that represents a successful result returned by `execa`.
* This kind of object is usually a bit cumbersome to build because it's a
* promise with extra properties glommed on to it (so it has a strange type). We
* use `jest-mock-extended` to help with this.
*
* @param overrides - Properties you want to add to the result object.
* @returns The complete `execa` result object.
*/
export function buildExecaResult(
overrides: Partial<ExecaReturnValue> = { stdout: '' },
): ExecaChildProcess {
return Object.assign(mock<ExecaChildProcess>(), overrides);
}

/**
* Mocks different invocations of `execa` to do different things.
*
* @param execaMock - The mocked version of `execa` (as obtained via
* `jest.mocked`).
* @param invocationMocks - Specifies outcomes of different invocations of
* `execa`. Each object in this array has `args` (the expected arguments to
* `execa`) and either `result` (properties of an ExecaResult object, such as
* `all: true`) or `error` (an Error).
*/
export function mockExeca(
execaMock: jest.MockedFn<PrimaryExecaFunction>,
invocationMocks: ({
args: Parameters<PrimaryExecaFunction>;
} & (
| {
result?: Partial<ExecaReturnValue>;
}
| {
error?: Error;
}
))[],
) {
execaMock.mockImplementation((...args): ExecaChildProcess => {
for (const invocationMock of invocationMocks) {
if (isDeepStrictEqual(args, invocationMock.args)) {
if ('error' in invocationMock && invocationMock.error) {
throw invocationMock.error;
}
if ('result' in invocationMock && invocationMock.result) {
return buildExecaResult(invocationMock.result);
}
throw new Error(
`No result or error was provided for execa() invocation ${inspect(
args,
)}`,
);
}
}

throw new Error(`Unmocked invocation of execa() with ${inspect(args)}`);
});
}
46 changes: 40 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -969,8 +969,10 @@ __metadata:
eslint-plugin-n: ^15.7.0
eslint-plugin-prettier: ^4.2.1
eslint-plugin-promise: ^6.1.1
execa: ^5.1.1
jest: ^28.1.3
jest-it-up: ^2.0.2
jest-mock-extended: ^3.0.5
prettier: ^2.7.1
prettier-plugin-packagejson: ^2.3.0
rimraf: ^3.0.2
Expand Down Expand Up @@ -2345,6 +2347,17 @@ __metadata:
languageName: node
linkType: hard

"cliui@npm:^8.0.1":
version: 8.0.1
resolution: "cliui@npm:8.0.1"
dependencies:
string-width: ^4.2.0
strip-ansi: ^6.0.1
wrap-ansi: ^7.0.0
checksum: 79648b3b0045f2e285b76fb2e24e207c6db44323581e421c3acbd0e86454cba1b37aea976ab50195a49e7384b871e6dfb2247ad7dec53c02454ac6497394cb56
languageName: node
linkType: hard

"clone-response@npm:^1.0.2":
version: 1.0.3
resolution: "clone-response@npm:1.0.3"
Expand Down Expand Up @@ -4486,6 +4499,18 @@ __metadata:
languageName: node
linkType: hard

"jest-mock-extended@npm:^3.0.5":
version: 3.0.5
resolution: "jest-mock-extended@npm:3.0.5"
dependencies:
ts-essentials: ^7.0.3
peerDependencies:
jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0
typescript: ^3.0.0 || ^4.0.0 || ^5.0.0
checksum: 440c52f743af588493c2cd02fa7e4e42177748ac3f7ae720f414bd58a4a72fad4271878457bf8796b62abcf9cf32cde4dc5151caad0805037bd965cc9ef07ca8
languageName: node
linkType: hard

"jest-mock@npm:^28.1.3":
version: 28.1.3
resolution: "jest-mock@npm:28.1.3"
Expand Down Expand Up @@ -6557,6 +6582,15 @@ __metadata:
languageName: node
linkType: hard

"ts-essentials@npm:^7.0.3":
version: 7.0.3
resolution: "ts-essentials@npm:7.0.3"
peerDependencies:
typescript: ">=3.7.0"
checksum: 74d75868acf7f8b95e447d8b3b7442ca21738c6894e576df9917a352423fde5eb43c5651da5f78997da6061458160ae1f6b279150b42f47ccc58b73e55acaa2f
languageName: node
linkType: hard

"ts-jest@npm:^28.0.7":
version: 28.0.8
resolution: "ts-jest@npm:28.0.8"
Expand Down Expand Up @@ -6985,7 +7019,7 @@ __metadata:
languageName: node
linkType: hard

"yargs-parser@npm:^21.0.0, yargs-parser@npm:^21.0.1":
"yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1":
version: 21.1.1
resolution: "yargs-parser@npm:21.1.1"
checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c
Expand All @@ -7008,17 +7042,17 @@ __metadata:
linkType: hard

"yargs@npm:^17.0.1, yargs@npm:^17.3.1":
version: 17.5.1
resolution: "yargs@npm:17.5.1"
version: 17.7.2
resolution: "yargs@npm:17.7.2"
dependencies:
cliui: ^7.0.2
cliui: ^8.0.1
escalade: ^3.1.1
get-caller-file: ^2.0.5
require-directory: ^2.1.1
string-width: ^4.2.3
y18n: ^5.0.5
yargs-parser: ^21.0.0
checksum: 00d58a2c052937fa044834313f07910fd0a115dec5ee35919e857eeee3736b21a4eafa8264535800ba8bac312991ce785ecb8a51f4d2cc8c4676d865af1cfbde
yargs-parser: ^21.1.1
checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a
languageName: node
linkType: hard

Expand Down