angular/tools/contributing-stats/get-data.ts
Paul Gschwendtner 8d7f1098d8 refactor: make all imports compatible with ESM/CJS output. (#43431)
As outlined in the previous commit which enabled the `esModuleInterop`
TypeScript compiler option, we need to update all namespace imports
for `typescript` to default imports. This is needed to allow for
TypeScript to be imported at runtime from an ES module.

Similar changes are needed for modules like `semver` where the types incorrectly
suggest named exports that will not exist at runtime when imported from ESM.

This commit refactors all imports to match with the lint rule we have
configured in the previous commit. See the previous commit for more
details on why certain imports have been changed.

A special case are the imports to `@babel/core` and `@babel/types`. For
these a special interop is needed as both default imports, or named
imports break the other module format. e.g default imports would work
well for ESM, but it breaks for CJS. For CJS, the named imports would
only work, but in ESM, only the default export exist. We work around
this for now until the devmode is using ESM as well (which would be
consistent with prodmode and gives us more valuable test results). More
details on the interop can be found in the `babel_core.ts` files (two
interops are needed for both localize/or the compiler-cli).

PR Close #43431
2021-10-01 18:28:45 +00:00

250 lines
8.2 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* This script gets contribution stats for all members of the angular org,
* since a provided date.
* The script expects the following flag(s):
*
* required:
* --since [date] The data after which contributions are queried for.
* Uses githubs search format for dates, e.g. "2020-01-21".
* See
* https://help.github.com/en/github/searching-for-information-on-github/understanding-the-search-syntax#query-for-dates
*
* optional:
* --use-created [boolean] If the created timestamp should be used for
* time comparisons, defaults otherwise to the updated timestamp.
*/
import {graphql as unauthenticatedGraphql} from '@octokit/graphql';
import minimist from 'minimist';
import {alias, params, query as graphqlQuery, types} from 'typed-graphqlify';
// The organization to be considered for the queries.
const ORG = 'angular';
// The repositories to be considered for the queries.
const REPOS = ['angular', 'components', 'angular-cli'];
/**
* Handle flags for the script.
*/
const args = minimist(process.argv.slice(2), {
string: ['since'],
boolean: ['use-created'],
unknown: (option: string) => {
console.error(`Unknown option: ${option}`);
process.exit(1);
}
});
if (!args['since']) {
console.error(`Please provide --since [date]`);
process.exit(1);
}
/**
* Authenticated instance of Github GraphQl API service, relies on a
* personal access token being available in the TOKEN environment variable.
*/
const graphql = unauthenticatedGraphql.defaults({
headers: {
// TODO(josephperrott): Remove reference to TOKEN environment variable as part of larger
// effort to migrate to expecting tokens via GITHUB_ACCESS_TOKEN environment variables.
authorization: `token ${process.env.TOKEN || process.env.GITHUB_ACCESS_TOKEN}`,
}
});
/**
* Retrieves all current members of an organization.
*/
async function getAllOrgMembers() {
// The GraphQL query object to get a page of members of an organization.
const MEMBERS_QUERY = params(
{
$first: 'Int', // How many entries to get with each request
$after: 'String', // The cursor to start the page at
$owner: 'String!', // The organization to query for
},
{
organization: params({login: '$owner'}, {
membersWithRole: params(
{
first: '$first',
after: '$after',
},
{
nodes: [{login: types.string}],
pageInfo: {
hasNextPage: types.boolean,
endCursor: types.string,
},
}),
})
});
const query = graphqlQuery('members', MEMBERS_QUERY);
/**
* Gets the query and queryParams for a specific page of entries.
*/
const queryBuilder = (count: number, cursor?: string) => {
return {
query,
params: {
after: cursor || null,
first: count,
owner: ORG,
},
};
};
// The current cursor
let cursor = undefined;
// If an additional page of members is expected
let hasNextPage = true;
// Array of Github usernames of the organization
const members: string[] = [];
while (hasNextPage) {
const {query, params} = queryBuilder(100, cursor);
const results = await graphql(query.toString(), params) as typeof MEMBERS_QUERY;
results.organization.membersWithRole.nodes.forEach(
(node: {login: string}) => members.push(node.login));
hasNextPage = results.organization.membersWithRole.pageInfo.hasNextPage;
cursor = results.organization.membersWithRole.pageInfo.endCursor;
}
return members.sort();
}
/**
* Build metadata for making requests for a specific user and date.
*
* Builds GraphQL query string, Query Params and Labels for making queries to GraphQl.
*/
function buildQueryAndParams(username: string, date: string) {
// Whether the updated or created timestamp should be used.
const updatedOrCreated = args['use-created'] ? 'created' : 'updated';
let dataQueries: {[key: string]: {query: string, label: string}} = {};
// Add queries and params for all values queried for each repo.
for (let repo of REPOS) {
dataQueries = {
...dataQueries,
[`${repo.replace(/[\/\-]/g, '_')}_issue_author`]: {
query: `repo:${ORG}/${repo} is:issue author:${username} ${updatedOrCreated}:>${date}`,
label: `${ORG}/${repo} Issue Authored`,
},
[`${repo.replace(/[\/\-]/g, '_')}_issues_involved`]: {
query: `repo:${ORG}/${repo} is:issue -author:${username} involves:${username} ${
updatedOrCreated}:>${date}`,
label: `${ORG}/${repo} Issue Involved`,
},
[`${repo.replace(/[\/\-]/g, '_')}_pr_author`]: {
query: `repo:${ORG}/${repo} is:pr author:${username} ${updatedOrCreated}:>${date}`,
label: `${ORG}/${repo} PR Author`,
},
[`${repo.replace(/[\/\-]/g, '_')}_pr_involved`]: {
query: `repo:${ORG}/${repo} is:pr involves:${username} ${updatedOrCreated}:>${date}`,
label: `${ORG}/${repo} PR Involved`,
},
[`${repo.replace(/[\/\-]/g, '_')}_pr_reviewed`]: {
query: `repo:${ORG}/${repo} is:pr -author:${username} reviewed-by:${username} ${
updatedOrCreated}:>${date}`,
label: `${ORG}/${repo} PR Reviewed`,
},
[`${repo.replace(/[\/\-]/g, '_')}_pr_commented`]: {
query: `repo:${ORG}/${repo} is:pr -author:${username} commenter:${username} ${
updatedOrCreated}:>${date}`,
label: `${ORG}/${repo} PR Commented`,
},
};
}
// Add queries and params for all values queried for the org.
dataQueries = {
...dataQueries,
[`${ORG}_org_issue_author`]: {
query: `org:${ORG} is:issue author:${username} ${updatedOrCreated}:>${date}`,
label: `${ORG} org Issue Authored`,
},
[`${ORG}_org_issues_involved`]: {
query: `org:${ORG} is:issue -author:${username} involves:${username} ${updatedOrCreated}:>${
date}`,
label: `${ORG} org Issue Involved`,
},
[`${ORG}_org_pr_author`]: {
query: `org:${ORG} is:pr author:${username} ${updatedOrCreated}:>${date}`,
label: `${ORG} org PR Author`,
},
[`${ORG}_org_pr_involved`]: {
query: `org:${ORG} is:pr involves:${username} ${updatedOrCreated}:>${date}`,
label: `${ORG} org PR Involved`,
},
[`${ORG}_org_pr_reviewed`]: {
query: `org:${ORG} is:pr -author:${username} reviewed-by:${username} ${updatedOrCreated}:>${
date}`,
label: `${ORG} org PR Reviewed`,
},
[`${ORG}_org_pr_commented`]: {
query:
`org:${ORG} is:pr -author:${username} commenter:${username} ${updatedOrCreated}:>${date}`,
label: `${ORG} org PR Commented`,
},
};
/**
* Gets the labels for each requested value to be used as headers.
*/
function getLabels(pairs: typeof dataQueries) {
return Object.values(pairs).map(val => val.label);
}
/**
* Gets the graphql query object for the GraphQL query.
*/
function getQuery(pairs: typeof dataQueries) {
const output: {[key: string]: {}} = {};
Object.entries(pairs).map(([key, val]) => {
output[alias(key, 'search')] = params(
{
query: `"${val.query}"`,
type: 'ISSUE',
},
{
issueCount: types.number,
});
});
return output;
}
return {
query: graphqlQuery(getQuery(dataQueries)),
labels: getLabels(dataQueries),
};
}
/**
* Runs the script to create a CSV string with the requested data for each member
* of the organization.
*/
async function run(date: string) {
try {
const allOrgMembers = await getAllOrgMembers();
console.info(['Username', ...buildQueryAndParams('', date).labels].join(','));
for (const username of allOrgMembers) {
const results = await graphql(buildQueryAndParams(username, date).query.toString());
const values = Object.values(results).map(result => `${result.issueCount}`);
console.info([username, ...values].join(','));
}
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
}
run(args['since']);