astro-loader-github-prs
Version:
Astro loader for loading GitHub pull requests with a search query.
201 lines (196 loc) • 8.1 kB
JavaScript
import { readFileSync } from 'node:fs';
import { Octokit } from 'octokit';
import { z } from 'astro/zod';
// src/index.ts
var GithubPrsLoaderConfigSchema = z.object({
/**
* The user-defined search string for querying pull requests on GitHub.
* This string will be concatenated with "type:pr" to form the complete search query.
*
* For more information:
* - {@link https://docs.github.com/en/graphql/reference/queries#search GitHub GraphQL API - Perform a search across resources}
* - {@link https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests How to search pull requests}
*
* @example
* - 'author:xxx created:>=2024-01-01': matches prs written by xxx that were created after 2024.
* - 'author:xxx -user:xxx': matches prs written by xxx, but not to their own repositories.
*/
search: z.string(),
/**
* The number of recent months to load pull requests, including the current month.
* The loader automatically converts this to a date for the 'created' qualifier in the search query.
*
* If the {@link https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests#search-by-when-an-issue-or-pull-request-was-created-or-last-updated 'created'}
* qualifier is defined in `search` option, it will override this value.
*
* For example, setting to `3` on December 4, 2024, would yield: 'type:pr created:>=2024-10-01 ...'.
*/
monthsBack: z.number().int({ message: "`monthsBack` must be an integer" }).positive({ message: "`monthsBack` must be a positive integer" }).optional(),
/**
* You need to {@link https://github.com/settings/tokens create a GitHub PAT}
* with at least `repo` scope permissions to authenticate requests to the GraphQL API.
*
* This is optional; by default, it reads from the `GITHUB_TOKEN` environment variable.
* You may also configure it directly here (not recommended; if you do, ensure it is not exposed
* in public code repositories).
*
* @see
* - {@link https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic How to create a GitHub PAT (classic)}
* - {@link https://docs.astro.build/en/guides/environment-variables/#setting-environment-variables How to store GitHub PAT in Astro project environment variables}
*/
githubToken: z.string().optional(),
/**
* Whether to clear the {@link https://docs.astro.build/en/reference/content-loader-reference/#store store}
* scoped to the collection before storing newly loaded data.
*
* @default false
*/
clearStore: z.boolean().default(false)
});
var GithubPrSchema = z.object({
id: z.string(),
url: z.string(),
title: z.string(),
titleHTML: z.string(),
number: z.number(),
state: z.enum(["CLOSED", "MERGED", "OPEN"]),
isDraft: z.boolean(),
body: z.string(),
bodyHTML: z.string(),
bodyText: z.string(),
author: z.object({
login: z.string(),
url: z.string(),
avatarUrl: z.string()
}),
repository: z.object({
name: z.string(),
nameWithOwner: z.string(),
url: z.string(),
stargazerCount: z.number(),
isInOrganization: z.boolean(),
owner: z.object({
login: z.string(),
url: z.string(),
avatarUrl: z.string()
})
}),
createdAt: z.string(),
mergedAt: z.string()
});
// src/utils.ts
function getQueryWithMonthsBack(search, monthsBack) {
if (!search.includes("created") && monthsBack) {
const startDate = /* @__PURE__ */ new Date();
startDate.setUTCMonth(startDate.getMonth() - monthsBack + 1);
startDate.setUTCDate(1);
return `${search} created:>=${startDate.toISOString().split("T")[0]}`;
}
return search;
}
// src/index.ts
function githubPrsLoader(userConfig) {
return {
name: "astro-loader-github-prs",
schema: GithubPrSchema,
async load({ logger, store, parseData, generateDigest }) {
const parsedConfig = GithubPrsLoaderConfigSchema.safeParse(userConfig);
if (!parsedConfig.success) {
logger.error(
`The configuration provided is invalid. ${parsedConfig.error.issues.map((issue) => issue.message).join("\n")}. Check out the configuration: https://github.com/lin-stephanie/astro-loaders/blob/main/packages/astro-loader-github-prs/README.md#configuration.`
);
return;
}
const { search, monthsBack, githubToken, clearStore } = parsedConfig.data;
const token = githubToken || import.meta.env.GITHUB_TOKEN;
if (search.length === 0) {
logger.warn(
"Search string is empty and no pull requests will be loaded"
);
return;
}
if (!token) {
logger.warn(
"No GitHub token provided. Please provide a `githubToken` or set the `GITHUB_TOKEN` environment variable.\nHow to create a GitHub PAT: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic.\nHow to store token in Astro project environment variables: https://docs.astro.build/en/guides/environment-variables/#setting-environment-variables."
);
return;
}
const prs = [];
const parsedSearch = getQueryWithMonthsBack(search, monthsBack);
const getPrsQuery = readFileSync(
new URL("./graphql/query.graphql", import.meta.url),
"utf8"
);
const octokit = new Octokit({ auth: token });
logger.info(
`Loading GitHub pull requests with a search query: '${parsedSearch}'`
);
try {
let hasNextPage = true;
let cursor = null;
while (hasNextPage) {
const variables = {
search: parsedSearch,
first: 100,
cursor
};
const res = await octokit.graphql(getPrsQuery, variables);
const prsPerPage = res.search.nodes?.filter(
(node) => node !== null && node !== void 0 && typeof node === "object" && "id" in node
).map((node) => {
return {
id: node.id,
url: node.url || "",
title: node.title,
titleHTML: node.titleHTML || "",
number: node.number,
state: node.state,
isDraft: node.isDraft,
body: node.body,
bodyHTML: node.bodyHTML || "",
bodyText: node.bodyText,
author: {
login: node.author?.login || "",
url: node.author?.url || "",
avatarUrl: node.author?.avatarUrl || ""
},
repository: {
name: node.repository.name,
nameWithOwner: node.repository.nameWithOwner,
url: node.repository.url || "",
stargazerCount: node.repository.stargazerCount,
isInOrganization: node.repository.isInOrganization,
owner: {
login: node.repository.owner.login || "",
url: node.repository.owner.url || "",
avatarUrl: node.repository.owner.avatarUrl || ""
}
},
createdAt: node.createdAt || "",
mergedAt: node.mergedAt || ""
};
}) || [];
prs.push(...prsPerPage);
hasNextPage = res.search.pageInfo.hasNextPage || false;
cursor = res.search.pageInfo.endCursor || null;
}
if (clearStore) store.clear();
for (const item of prs) {
const parsedItem = await parseData({ id: item.id, data: item });
store.set({
id: item.id,
data: parsedItem,
rendered: { html: item.bodyHTML },
digest: generateDigest(parsedItem)
});
}
logger.info("Successfully loaded GitHub pull requests");
} catch (error) {
logger.error(
`Failed to load GitHub pull requests. ${error.message}`
);
}
}
};
}
export { githubPrsLoader };