From 7aadeacc8890b5496e090c87ec1248545358ce8b Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Wed, 12 Aug 2026 20:44:47 +0200 Subject: [PATCH 01/15] feat(core): generate TypeScript types from the GraphQL queries --- .github/workflows/ci.yml | 3 + apps/backend/tests/e2e/e2e.test.js | 2 +- .../__snapshots__/api.test.js.snap | 2 +- .../__snapshots__/gist.test.js.snap | 2 +- .../__snapshots__/pin.test.js.snap | 2 +- .../__snapshots__/top-langs.test.js.snap | 2 +- apps/frontend/src/axios-override.ts | 14 +- eslint.config.js | 4 + knip.jsonc | 4 + packages/core/package.json | 7 + .../core/scripts/generate-graphql-types.js | 253 ++++++ packages/core/src/common/constants.ts | 4 +- packages/core/src/common/http.ts | 45 +- packages/core/src/common/ops.ts | 13 +- packages/core/src/common/retryer.ts | 11 +- packages/core/src/fetchers/gist.ts | 86 +- packages/core/src/fetchers/repo.ts | 70 +- packages/core/src/fetchers/stats.ts | 152 +--- packages/core/src/fetchers/top-languages.ts | 95 +-- packages/core/src/fetchers/types.ts | 18 +- packages/core/src/graphql/generated/common.ts | 24 + packages/core/src/graphql/generated/gist.ts | 57 ++ packages/core/src/graphql/generated/repo.ts | 88 ++ packages/core/src/graphql/generated/stats.ts | 159 ++++ .../src/graphql/generated/top-languages.ts | 73 ++ packages/core/src/graphql/graphqlDocument.ts | 26 + .../core/src/graphql/queries/gist.graphql | 25 + .../core/src/graphql/queries/repo.graphql | 28 + .../core/src/graphql/queries/stats.graphql | 79 ++ .../src/graphql/queries/top-languages.graphql | 34 + pnpm-lock.yaml | 765 +++++++++++++----- 31 files changed, 1618 insertions(+), 529 deletions(-) create mode 100644 packages/core/scripts/generate-graphql-types.js create mode 100644 packages/core/src/graphql/generated/common.ts create mode 100644 packages/core/src/graphql/generated/gist.ts create mode 100644 packages/core/src/graphql/generated/repo.ts create mode 100644 packages/core/src/graphql/generated/stats.ts create mode 100644 packages/core/src/graphql/generated/top-languages.ts create mode 100644 packages/core/src/graphql/graphqlDocument.ts create mode 100644 packages/core/src/graphql/queries/gist.graphql create mode 100644 packages/core/src/graphql/queries/repo.graphql create mode 100644 packages/core/src/graphql/queries/stats.graphql create mode 100644 packages/core/src/graphql/queries/top-languages.graphql diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b75661e8ad015..439f4c8c1ce63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,5 +129,8 @@ jobs: - name: Lint (deduped deps) run: pnpm run lint:deps + - name: Lint (generated GraphQL types) + run: pnpm --filter ./packages/core/ run check-graphql-types + - name: Typecheck run: pnpm run typecheck diff --git a/apps/backend/tests/e2e/e2e.test.js b/apps/backend/tests/e2e/e2e.test.js index e440e58abea1e..fb968c9572817 100644 --- a/apps/backend/tests/e2e/e2e.test.js +++ b/apps/backend/tests/e2e/e2e.test.js @@ -201,7 +201,7 @@ beforeAll(async () => { return [200, STATS_MOCK_RESPONSE]; } - if (query.includes("query userInfo") && variables?.login === USER) { + if (query.includes("query topLanguages") && variables?.login === USER) { return [200, TOP_LANGS_MOCK_RESPONSE]; } diff --git a/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap index 4626f2045aaf3..9990fc43cda11 100644 --- a/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap +++ b/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap @@ -699,7 +699,7 @@ exports[`Test /api contract > should match the public many-params response snaps ", - "graphqlRequest": "{"query":"\\n query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection (from: $startTime) {\\n totalCommitContributions,\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n \\n repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) {\\n totalCount\\n nodes {\\n name\\n stargazerCount\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n\\n }\\n }\\n","variables":{"login":"anuraghazra","first":100,"after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}", + "graphqlRequest": "{"query":"\\nquery userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection(from: $startTime) {\\n totalCommitContributions\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(\\n first: 1\\n contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]\\n ) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n ...RepoStars\\n }\\n}\\nfragment RepoStars on User {\\n repositories(\\n first: 100\\n after: $after\\n ownerAffiliations: $ownerAffiliations\\n orderBy: {direction: DESC, field: STARGAZERS}\\n ) {\\n totalCount\\n nodes {\\n name\\n stargazerCount\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}","variables":{"login":"anuraghazra","after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}", "headers": [ [ "Cache-Control", diff --git a/apps/backend/tests/public-instance/__snapshots__/gist.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/gist.test.js.snap index 524372fb0f8d6..8c88bd71dbd3a 100644 --- a/apps/backend/tests/public-instance/__snapshots__/gist.test.js.snap +++ b/apps/backend/tests/public-instance/__snapshots__/gist.test.js.snap @@ -239,7 +239,7 @@ exports[`Test /api/gist contract > should match the public many-params response ", - "graphqlRequest": "{"query":"\\nquery gistInfo($gistName: String!) {\\n viewer {\\n gist(name: $gistName) {\\n description\\n owner {\\n login\\n }\\n stargazerCount\\n forks {\\n totalCount\\n }\\n files {\\n name\\n language {\\n name\\n }\\n size\\n }\\n }\\n }\\n}\\n","variables":{"gistName":"happy-gist-id"}}", + "graphqlRequest": "{"query":"\\nquery gistInfo($gistName: String!) {\\n viewer {\\n gist(name: $gistName) {\\n description\\n owner {\\n login\\n }\\n stargazerCount\\n forks {\\n totalCount\\n }\\n files {\\n ...GistFileInfo\\n }\\n }\\n }\\n}\\nfragment GistFileInfo on GistFile {\\n name\\n language {\\n name\\n }\\n size\\n}","variables":{"gistName":"happy-gist-id"}}", "headers": [ [ "Cache-Control", diff --git a/apps/backend/tests/public-instance/__snapshots__/pin.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/pin.test.js.snap index dc788899d533a..9b29eb4d6e0b8 100644 --- a/apps/backend/tests/public-instance/__snapshots__/pin.test.js.snap +++ b/apps/backend/tests/public-instance/__snapshots__/pin.test.js.snap @@ -504,7 +504,7 @@ exports[`Test /api/pin contract > should match the public many-params response s ", - "graphqlRequest": "{"query":"\\n fragment RepoInfo on Repository {\\n name\\n nameWithOwner\\n isPrivate\\n isArchived\\n isTemplate\\n stargazerCount\\n description\\n primaryLanguage {\\n color\\n id\\n name\\n }\\n forkCount\\n }\\n query getRepo($login: String!, $repo: String!) {\\n user(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n organization(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n }\\n ","variables":{"login":"anuraghazra","repo":"convoychat"}}", + "graphqlRequest": "{"query":"\\nquery getRepo($login: String!, $repo: String!) {\\n user(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n organization(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n}\\nfragment RepoInfo on Repository {\\n name\\n nameWithOwner\\n isPrivate\\n isArchived\\n isTemplate\\n stargazerCount\\n description\\n primaryLanguage {\\n color\\n id\\n name\\n }\\n forkCount\\n}","variables":{"login":"anuraghazra","repo":"convoychat"}}", "headers": [ [ "Cache-Control", diff --git a/apps/backend/tests/public-instance/__snapshots__/top-langs.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/top-langs.test.js.snap index ea87380551eed..41a655f33e875 100644 --- a/apps/backend/tests/public-instance/__snapshots__/top-langs.test.js.snap +++ b/apps/backend/tests/public-instance/__snapshots__/top-langs.test.js.snap @@ -412,7 +412,7 @@ exports[`Test /api/top-langs contract > should match the public many-params resp ", - "graphqlRequest": "{"query":"\\n query userInfo($login: String!, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n # do not fetch forks\\n repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) {\\n nodes {\\n name\\n languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {\\n edges {\\n size\\n node {\\n color\\n name\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n ","variables":{"login":"anuraghazra","ownerAffiliations":["OWNER","COLLABORATOR"]}}", + "graphqlRequest": "{"query":"\\nquery topLanguages($login: String!, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) {\\n nodes {\\n ...TopLanguagesRepository\\n }\\n }\\n }\\n}\\nfragment TopLanguagesRepository on Repository {\\n name\\n languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {\\n edges {\\n ...TopLanguage\\n }\\n }\\n}\\nfragment TopLanguage on LanguageEdge {\\n size\\n node {\\n color\\n name\\n }\\n}","variables":{"login":"anuraghazra","ownerAffiliations":["OWNER","COLLABORATOR"]}}", "headers": [ [ "Cache-Control", diff --git a/apps/frontend/src/axios-override.ts b/apps/frontend/src/axios-override.ts index 96267c61db716..c518926f3fb79 100644 --- a/apps/frontend/src/axios-override.ts +++ b/apps/frontend/src/axios-override.ts @@ -88,9 +88,7 @@ axios.defaults.adapter = async (config) => { if ( config.url === "https://api.github.com/graphql" && - params.query?.includes( - "query userInfo($login: String!, $after: String, $includeMergedPullRequests:", - ) && + params.query?.includes("query userInfo(") && params.variables?.login === DEMO_USER ) { return createMockResponse(userStats, config); @@ -98,9 +96,7 @@ axios.defaults.adapter = async (config) => { if ( config.url === "https://api.github.com/graphql" && - params.query?.includes( - "query userInfo($login: String!, $after: String, $ownerAffiliations:", - ) && + params.query?.includes("query userRepos(") && params.variables?.login === DEMO_USER ) { return createMockResponse(additionalUserStars, config); @@ -108,9 +104,7 @@ axios.defaults.adapter = async (config) => { if ( config.url === "https://api.github.com/graphql" && - params.query?.includes( - "query userInfo($login: String!, $ownerAffiliations:", - ) && + params.query?.includes("query topLanguages(") && params.variables?.login === DEMO_USER ) { return createMockResponse(topLanguages, config); @@ -118,7 +112,7 @@ axios.defaults.adapter = async (config) => { if ( config.url === "https://api.github.com/graphql" && - params.query?.includes("fragment RepoInfo on Repository {") && + params.query?.includes("query getRepo(") && params.variables && params.variables.login === DEMO_REPO.split("/")[0] && params.variables.repo === DEMO_REPO.split("/")[1] diff --git a/eslint.config.js b/eslint.config.js index 909e6e84bc50f..9c78ad877fcde 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,10 @@ const gitignorePath = fileURLToPath(new URL(".gitignore", import.meta.url)); export default defineConfig( includeIgnoreFile(gitignorePath, "Imported .gitignore patterns"), + { + name: "Generated GraphQL types", + ignores: ["packages/core/src/graphql/generated/**"], + }, js.configs.recommended, { diff --git a/knip.jsonc b/knip.jsonc index c4d3bde028b08..638fb6ecf7560 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -4,6 +4,10 @@ "entry": ["tests/**/*.{test,test-d,bench}.{js,ts}"] }, "workspaces": { + "packages/core": { + // generated by scripts/generate-graphql-types.js + "ignore": ["src/graphql/generated/**"] + }, "apps/backend": { "entry": ["api-renamed/*.js", "express.js"] }, diff --git a/packages/core/package.json b/packages/core/package.json index 8bff5723ec176..56397184a45e0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -75,14 +75,21 @@ "bench": "vitest bench --run --config vitest.config.bench.ts", "lint": "eslint", "typecheck": "tsc -p tsconfig.typecheck.json", + "generate-graphql-types": "node scripts/generate-graphql-types", + "check-graphql-types": "node scripts/generate-graphql-types --check", "generate-theme-readme": "node scripts/generate-theme-readme", "generate-language-colors": "node scripts/generate-language-colors" }, "devDependencies": { + "@graphql-codegen/core": "6.2.0", + "@graphql-codegen/typescript": "6.1.0", + "@graphql-codegen/typescript-operations": "6.1.2", + "@octokit/graphql-schema": "15.26.1", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", "@uppercod/css-to-object": "1.1.1", "axios-mock-adapter": "2.1.0", + "graphql": "16.11.0", "js-yaml": "5.2.1", "jsdom": "catalog:default", "vitest": "catalog:default" diff --git a/packages/core/scripts/generate-graphql-types.js b/packages/core/scripts/generate-graphql-types.js new file mode 100644 index 0000000000000..8a784e38d010b --- /dev/null +++ b/packages/core/scripts/generate-graphql-types.js @@ -0,0 +1,253 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { codegen } from "@graphql-codegen/core"; +import * as typescriptPlugin from "@graphql-codegen/typescript"; +import * as typescriptOperationsPlugin from "@graphql-codegen/typescript-operations"; +import { schema as githubSchema } from "@octokit/graphql-schema"; +import { + buildSchema, + parse, + print, + printSchema, + printType, + visit, +} from "graphql"; +import { format, resolveConfig } from "prettier"; + +const PACKAGE_ROOT = path.join(import.meta.dirname, ".."); +const QUERIES_DIR = path.join(PACKAGE_ROOT, "src/graphql/queries"); +const OUT_DIR = path.join(PACKAGE_ROOT, "src/graphql/generated"); +const COMMON_FILE = "common.ts"; +// `typescript-operations` resolves this against the working directory +const COMMON_IMPORT_PATH = path.relative( + process.cwd(), + path.join(OUT_DIR, COMMON_FILE.replace(/\.ts$/, ".js")), +); + +// `--check` regenerates in memory and fails on any difference, for CI +const checkOnly = process.argv.includes("--check"); + +const config = { + emitLegacyCommonJSImports: false, + enumsAsTypes: true, + skipTypename: true, + useTypeImports: true, + // read the schema's own enums and input types from the common file instead of redeclaring them per query file; + // the import is emitted only where they are used + importSchemaTypesFrom: COMMON_IMPORT_PATH, + // nullable fields as `x: T | null`; `@include(if:)` ones stay optional + avoidOptionals: { field: true }, + scalars: { DateTime: "string" }, +}; + +// GitHub's published SDL declares a few fields twice, which trips SDL validation +const schemaAst = buildSchema(githubSchema.idl, { assumeValidSDL: true }); +const schemaDocument = parse(printSchema(schemaAst)); + +const queryFiles = (await fs.readdir(QUERIES_DIR)) + .filter((file) => file.endsWith(".graphql")) + .sort(); + +const documents = await Promise.all( + queryFiles.map(async (file) => { + const location = path.join(QUERIES_DIR, file); + const rawSDL = await fs.readFile(location, "utf8"); + return { location, rawSDL, document: parse(rawSDL) }; + }), +); + +const fragments = new Map( + documents.flatMap(({ document }) => + document.definitions + .filter((definition) => definition.kind === "FragmentDefinition") + .map((definition) => [definition.name.value, definition]), + ), +); + +/** + * Every fragment a node spreads, directly or through another fragment. + * + * @param node Operation or fragment definition to walk. + * @param found Fragment names collected so far. + * @returns The fragment names the node needs. + */ +const spreadFragments = (node, found = new Set()) => { + visit(node, { + FragmentSpread(spread) { + const name = spread.name.value; + if (found.has(name)) { + return; + } + const fragment = fragments.get(name); + if (!fragment) { + throw new Error(`No definition found for fragment "${name}"`); + } + found.add(name); + spreadFragments(fragment, found); + }, + }); + return found; +}; + +/** + * The names `typescript-operations` gives an operation's types. + * + * @param operation Operation definition. + * @returns Names to reference in the emitted document. + */ +const operationNames = (operation) => { + if (!operation.name) { + throw new Error("Every query needs a name to generate types from"); + } + const name = operation.name.value; + const pascalCase = name.charAt(0).toUpperCase() + name.slice(1); + const suffix = + operation.operation.charAt(0).toUpperCase() + operation.operation.slice(1); + const resultType = pascalCase.endsWith(suffix) + ? pascalCase + : `${pascalCase}${suffix}`; + return { + documentName: `${pascalCase}Document`, + resultType, + variablesType: `${resultType}Variables`, + }; +}; + +/** Emits one typed document per operation, next to the types for its shape. */ +const documentsPlugin = { + plugin: (_schema, files) => { + const operations = files.flatMap((file) => + file.document.definitions.filter( + (definition) => definition.kind === "OperationDefinition", + ), + ); + return { + prepend: [`import { graphqlDocument } from "../graphqlDocument.js";`], + content: operations + .map((operation) => { + const { documentName, resultType, variablesType } = + operationNames(operation); + const text = [ + print(operation), + ...[...spreadFragments(operation)].map((name) => + print(fragments.get(name)), + ), + ].join("\n"); + return `export const ${documentName} = graphqlDocument<${resultType}, ${variablesType}>(\`\n${text}\`);`; + }) + .join("\n\n"), + }; + }, +}; + +// a schema of only the types the queries reference through their variables, so the +// common file gets the scalars and enums in use rather than every one GitHub declares +const variableTypeNames = new Set(); +for (const { document } of documents) { + visit(document, { + VariableDefinition(node) { + visit(node.type, { + NamedType(namedType) { + variableTypeNames.add(namedType.name.value); + }, + }); + }, + }); +} +const subsetSchemaAst = buildSchema( + [...variableTypeNames] + .map((name) => schemaAst.getType(name)) + .filter((type) => !!type && !type.name.startsWith("__")) + .map((type) => printType(type)) + .join("\n\n"), + { assumeValidSDL: true }, +); + +// `typescript-operations` emits this for the fragment masking we don't generate, and +// has no config to leave it out +const INCREMENTAL_TYPE = + /^\/\*\* Internal type\. DO NOT USE DIRECTLY\. \*\/\nexport type Incremental = [^\n]*\n/m; + +const generated = [ + [ + path.join(OUT_DIR, COMMON_FILE), + await codegen({ + filename: path.join(OUT_DIR, COMMON_FILE), + schema: parse(printSchema(subsetSchemaAst)), + schemaAst: subsetSchemaAst, + documents: [], + config, + plugins: [{ typescript: {} }], + pluginMap: { typescript: typescriptPlugin }, + }), + ], +]; + +for (const file of documents) { + const filename = path.join( + OUT_DIR, + `${path.basename(file.location, ".graphql")}.ts`, + ); + const content = await codegen({ + filename, + schema: schemaDocument, + schemaAst, + documents: [file], + config, + plugins: [{ "typescript-operations": {} }, { documents: {} }], + pluginMap: { + "typescript-operations": typescriptOperationsPlugin, + documents: documentsPlugin, + }, + }); + generated.push([filename, content.replace(INCREMENTAL_TYPE, "")]); +} + +const prettierConfig = await resolveConfig(path.join(OUT_DIR, COMMON_FILE)); +const drifted = []; + +await fs.mkdir(OUT_DIR, { recursive: true }); +for (const [filename, content] of generated) { + const formatted = await format(content, { + ...prettierConfig, + parser: "typescript", + }); + if (checkOnly) { + const current = await fs.readFile(filename, "utf8").catch(() => null); + if (current !== formatted) { + drifted.push(path.relative(PACKAGE_ROOT, filename)); + } + } else { + await fs.writeFile(filename, formatted); + } +} + +// a query removed from the queries folder leaves its generated file behind +const expected = new Set( + generated.map(([filename]) => path.basename(filename)), +); +const stale = (await fs.readdir(OUT_DIR).catch(() => [])).filter( + (file) => !expected.has(file), +); +for (const file of stale) { + if (checkOnly) { + drifted.push(path.relative(PACKAGE_ROOT, path.join(OUT_DIR, file))); + } else { + await fs.rm(path.join(OUT_DIR, file)); + } +} + +if (checkOnly) { + if (drifted.length > 0) { + console.error( + `GraphQL types are out of date:\n${drifted.map((file) => ` ${file}`).join("\n")}\n\nRun \`pnpm generate-graphql-types\` in packages/core.`, + ); + process.exit(1); + } + console.log(`GraphQL types are up to date (${queryFiles.length} queries)`); +} else { + console.log( + `Generated ${generated.length} files in ${path.relative(PACKAGE_ROOT, OUT_DIR)} from ${queryFiles.length} queries`, + ); +} diff --git a/packages/core/src/common/constants.ts b/packages/core/src/common/constants.ts index 9b40de281b76b..52681782f77c9 100644 --- a/packages/core/src/common/constants.ts +++ b/packages/core/src/common/constants.ts @@ -1,7 +1,9 @@ +import type { RepositoryAffiliation } from "../graphql/generated/common.js"; + /** * Valid owner affiliations for GitHub API queries. */ -const OWNER_AFFILIATIONS: Array = [ +const OWNER_AFFILIATIONS: Array = [ "OWNER", "COLLABORATOR", "ORGANIZATION_MEMBER", diff --git a/packages/core/src/common/http.ts b/packages/core/src/common/http.ts index 4c03701d8380c..d2966d3c67515 100644 --- a/packages/core/src/common/http.ts +++ b/packages/core/src/common/http.ts @@ -1,6 +1,37 @@ import axios from "axios"; import type { AxiosRequestConfig, AxiosResponse } from "axios"; +import type { GraphQLDocument } from "../graphql/graphqlDocument.js"; + +const GITHUB_GRAPHQL_API = "https://api.github.com/graphql"; + +/** Response of a GraphQL call: the envelope the GitHub API wraps results in. */ +type GraphQLResponse = AxiosResponse<{ + data: TResult; + errors?: Array<{ type?: string; message?: string }>; +}>; + +/** + * Send a GraphQL request to the GitHub API. + * + * @param document Generated query document; carries its result and variable types. + * @param variables Variables the query declares. + * @param headers Request headers. + * @returns Request response. + */ +const httpGraphQLRequest = ( + document: GraphQLDocument, + variables: TVariables, + headers: NonNullable, +): Promise> => { + return axios({ + url: GITHUB_GRAPHQL_API, + method: "post", + headers, + data: { query: document.text, variables }, + }); +}; + /** Body of a GraphQL request sent to the GitHub API. */ interface GraphQLRequest { /** The GraphQL query. */ @@ -10,22 +41,30 @@ interface GraphQLRequest { } /** - * Send GraphQL request to GitHub API. + * Send an untyped GraphQL request to the GitHub API. * * @param data Request data. * @param headers Request headers. * @returns Request response. + * + * @description Superseded by {@link httpGraphQLRequest}. + * Still used by the backend status endpoints for their own `rateLimit` query, + * and part of this package's published API. + * + * @todo consider dropping this once those endpoints post through axios directly, + * which they already depend on. */ const request = ( data: GraphQLRequest, headers: NonNullable, ): Promise => { return axios({ - url: "https://api.github.com/graphql", + url: GITHUB_GRAPHQL_API, method: "post", headers, data, }); }; -export { request }; +export { httpGraphQLRequest, request }; +export type { GraphQLResponse }; diff --git a/packages/core/src/common/ops.ts b/packages/core/src/common/ops.ts index 2331e9ec704c6..9f3b1ec9d4662 100644 --- a/packages/core/src/common/ops.ts +++ b/packages/core/src/common/ops.ts @@ -1,5 +1,7 @@ import toEmoji from "emoji-name-map"; +import type { RepositoryAffiliation } from "../graphql/generated/common.js"; + import { OWNER_AFFILIATIONS } from "./constants.js"; import { CustomError } from "./error.js"; @@ -111,6 +113,9 @@ const dateDiff = (d1: Date, d2: Date): number => { return Math.round(diff / (1000 * 60)); }; +const isOwnerAffiliation = (value: string): value is RepositoryAffiliation => + OWNER_AFFILIATIONS.some((affiliation) => affiliation === value); + /** * Parse owner affiliations. * @@ -119,7 +124,9 @@ const dateDiff = (d1: Date, d2: Date): number => { * * @throws {CustomError} If affiliations contains invalid values. */ -const parseOwnerAffiliations = (affiliations: Array): Array => { +const parseOwnerAffiliations = ( + affiliations: Array, +): Array => { // Set default value for ownerAffiliations. // NOTE: Done here since parseArray() will always return an empty array even nothing //was specified. @@ -129,9 +136,7 @@ const parseOwnerAffiliations = (affiliations: Array): Array => { : ["OWNER"]; // Check if ownerAffiliations contains valid values. - if ( - normalized.some((affiliation) => !OWNER_AFFILIATIONS.includes(affiliation)) - ) { + if (!normalized.every(isOwnerAffiliation)) { throw new CustomError( "Invalid query parameter", CustomError.INVALID_AFFILIATION, diff --git a/packages/core/src/common/retryer.ts b/packages/core/src/common/retryer.ts index 4b07f36466b70..4f622264dad89 100644 --- a/packages/core/src/common/retryer.ts +++ b/packages/core/src/common/retryer.ts @@ -36,8 +36,8 @@ function getRandomInt(max: number): number { */ type FetcherResponse = AxiosResponse; -type FetcherFunction = ( - variables: Record, +type FetcherFunction> = ( + variables: TVariables, token: string, retriesForTests?: number, ) => Promise>; @@ -46,14 +46,15 @@ type FetcherFunction = ( * Try to execute the fetcher function until it succeeds or the max number of retries is reached. * * @template TData Shape of `response.data` returned by the fetcher. + * @template TVariables Variables the fetcher accepts. * @param fetcher The fetcher function. * @param variables Object with arguments to pass to the fetcher function. * @param pat Optional PAT override. * @returns The response from the fetcher function. */ -const retryer = async ( - fetcher: FetcherFunction, - variables: Record, +const retryer = async >( + fetcher: FetcherFunction, + variables: TVariables, pat: string | null = null, ): Promise> => { const PATs = pat diff --git a/packages/core/src/fetchers/gist.ts b/packages/core/src/fetchers/gist.ts index 1d8fc520d31bb..be0a74a0570de 100644 --- a/packages/core/src/fetchers/gist.ts +++ b/packages/core/src/fetchers/gist.ts @@ -1,35 +1,16 @@ -import type { AxiosResponse } from "axios"; - import { MissingParamError } from "../common/error.js"; -import { request } from "../common/http.js"; +import { httpGraphQLRequest } from "../common/http.js"; +import type { GraphQLResponse } from "../common/http.js"; import { retryer } from "../common/retryer.js"; +import { GistInfoDocument } from "../graphql/generated/gist.js"; +import type { + GistFileInfoFragment, + GistInfoQuery, + GistInfoQueryVariables, +} from "../graphql/generated/gist.js"; import type { GistData } from "./types.js"; -const QUERY = ` -query gistInfo($gistName: String!) { - viewer { - gist(name: $gistName) { - description - owner { - login - } - stargazerCount - forks { - totalCount - } - files { - name - language { - name - } - size - } - } - } -} -`; - /** * Gist data fetcher. * @@ -38,50 +19,29 @@ query gistInfo($gistName: String!) { * @returns The response. */ const fetcher = ( - variables: Record, + variables: GistInfoQueryVariables, token: string, -): Promise => { - return request( - { query: QUERY, variables }, - { Authorization: `token ${token}` }, - ); +): Promise> => { + return httpGraphQLRequest(GistInfoDocument, variables, { + Authorization: `token ${token}`, + }); }; -/** A single file within a gist. */ -interface GistFile { - name: string; - language: { name: string } | null; - size: number; -} - -/** Shape of `response.data` returned by the gist GraphQL query. */ -interface GistQueryResponse { - data: { - viewer: { - gist: { - description: string | null; - owner: { login: string }; - stargazerCount: number; - forks: { totalCount: number }; - files: Array; - } | null; - }; - }; -} - /** * This function calculates the primary language of a gist by files size. * * @param files Files. * @returns Primary language, or `null` when no file has a language. */ -const calculatePrimaryLanguage = (files: Array): string | null => { +const calculatePrimaryLanguage = ( + files: Array, +): string | null => { const languages: Record = {}; for (const file of files) { if (file.language) { languages[file.language.name] = - (languages[file.language.name] ?? 0) + file.size; + (languages[file.language.name] ?? 0) + (file.size ?? 0); } } @@ -111,7 +71,7 @@ const fetchGist = async ( if (!id) { throw new MissingParamError(["id"], "/api/gist?id=GIST_ID"); } - const res = await retryer(fetcher, { gistName: id }, pat); + const res = await retryer(fetcher, { gistName: id }, pat); if (res.data.errors) { throw new Error(res.data.errors[0]?.message); } @@ -119,15 +79,17 @@ const fetchGist = async ( if (!gist) { throw new Error("Gist not found"); } - const firstFile = gist.files[0]; - if (!firstFile) { + const firstFile = gist.files?.[0]; + if (!firstFile?.name) { throw new Error("Gist has no files"); } return { name: firstFile.name, - nameWithOwner: `${gist.owner.login}/${firstFile.name}`, + nameWithOwner: `${gist.owner?.login ?? ""}/${firstFile.name}`, description: gist.description, - language: calculatePrimaryLanguage(gist.files), + language: calculatePrimaryLanguage( + gist.files?.filter((file) => !!file) ?? [], + ), starsCount: gist.stargazerCount, forksCount: gist.forks.totalCount, }; diff --git a/packages/core/src/fetchers/repo.ts b/packages/core/src/fetchers/repo.ts index 4287433781b48..aa154e89e28b2 100644 --- a/packages/core/src/fetchers/repo.ts +++ b/packages/core/src/fetchers/repo.ts @@ -1,11 +1,15 @@ -import type { AxiosResponse } from "axios"; - import { MissingParamError } from "../common/error.js"; -import { request } from "../common/http.js"; +import { httpGraphQLRequest } from "../common/http.js"; +import type { GraphQLResponse } from "../common/http.js"; import { retryer } from "../common/retryer.js"; +import { GetRepoDocument } from "../graphql/generated/repo.js"; +import type { + GetRepoQuery, + GetRepoQueryVariables, +} from "../graphql/generated/repo.js"; import { fetchRepoUserStats } from "./stats.js"; -import type { RepoInfo, RepositoryData } from "./types.js"; +import type { RepositoryData } from "./types.js"; /** * Repo data fetcher. @@ -15,58 +19,16 @@ import type { RepoInfo, RepositoryData } from "./types.js"; * @returns The response. */ const fetcher = ( - variables: Record, + variables: GetRepoQueryVariables, token: string, -): Promise => { - return request( - { - query: ` - fragment RepoInfo on Repository { - name - nameWithOwner - isPrivate - isArchived - isTemplate - stargazerCount - description - primaryLanguage { - color - id - name - } - forkCount - } - query getRepo($login: String!, $repo: String!) { - user(login: $login) { - repository(name: $repo) { - ...RepoInfo - } - } - organization(login: $login) { - repository(name: $repo) { - ...RepoInfo - } - } - } - `, - variables, - }, - { - Authorization: `token ${token}`, - }, - ); +): Promise> => { + return httpGraphQLRequest(GetRepoDocument, variables, { + Authorization: `token ${token}`, + }); }; const urlExample = "/api/pin?username=USERNAME&repo=REPO_NAME"; -/** Shape of `response.data` returned by the repo query. */ -interface RepoQueryResponse { - data: { - user: { repository: RepoInfo | null } | null; - organization: { repository: RepoInfo | null } | null; - }; -} - /** * Fetch repository data. * @@ -113,11 +75,7 @@ const fetchRepo = async ( throw new MissingParamError(["repo"], urlExample); } - const res = await retryer( - fetcher, - { login: owner, repo: reponame }, - pat, - ); + const res = await retryer(fetcher, { login: owner, repo: reponame }, pat); const data = res.data.data; diff --git a/packages/core/src/fetchers/stats.ts b/packages/core/src/fetchers/stats.ts index ace2c8733b6f4..52c5fbe7fa88e 100644 --- a/packages/core/src/fetchers/stats.ts +++ b/packages/core/src/fetchers/stats.ts @@ -6,110 +6,28 @@ import { calculateRank } from "../calculateRank.js"; import { getConfig } from "../common/config.js"; import { CustomError, MissingParamError } from "../common/error.js"; import { wrapTextMultiline } from "../common/fmt.js"; -import { request } from "../common/http.js"; +import { httpGraphQLRequest } from "../common/http.js"; +import type { GraphQLResponse } from "../common/http.js"; import { logger } from "../common/log.js"; import { buildSearchFilter, parseOwnerAffiliations } from "../common/ops.js"; import { retryer } from "../common/retryer.js"; +import { + UserInfoDocument, + UserReposDocument, +} from "../graphql/generated/stats.js"; +import type { + UserInfoQuery, + UserInfoQueryVariables, +} from "../graphql/generated/stats.js"; +import type { GraphQLDocument } from "../graphql/graphqlDocument.js"; import type { RepoUserStats, StatsData } from "./types.js"; -// GraphQL queries. -const GRAPHQL_REPOS_FIELD = ` - repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) { - totalCount - nodes { - name - stargazerCount - } - pageInfo { - hasNextPage - endCursor - } - } -`; - -const GRAPHQL_REPOS_QUERY = ` - query userInfo($login: String!, $after: String, $ownerAffiliations: [RepositoryAffiliation]) { - user(login: $login) { - ${GRAPHQL_REPOS_FIELD} - } - } -`; - -const GRAPHQL_STATS_QUERY = ` - query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) { - user(login: $login) { - name - login - commits: contributionsCollection (from: $startTime) { - totalCommitContributions, - } - reviews: contributionsCollection { - totalPullRequestReviewContributions - } - repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) { - totalCount - } - pullRequests(first: 1) { - totalCount - } - mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) { - totalCount - } - openIssues: issues(states: OPEN) { - totalCount - } - closedIssues: issues(states: CLOSED) { - totalCount - } - followers { - totalCount - } - repositoryDiscussions @include(if: $includeDiscussions) { - totalCount - } - repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) { - totalCount - } - ${GRAPHQL_REPOS_FIELD} - } - } -`; - -/** The `user` object returned by the stats GraphQL query. */ -interface StatsUser { - name: string | null; - login: string; - commits: { totalCommitContributions: number }; - reviews: { totalPullRequestReviewContributions: number }; - repositoriesContributedTo: { totalCount: number }; - pullRequests: { totalCount: number }; - // conditionally included via @include(if: …), so absent when their flag is off - mergedPullRequests?: { totalCount: number }; - openIssues: { totalCount: number }; - closedIssues: { totalCount: number }; - followers: { totalCount: number }; - repositoryDiscussions?: { totalCount: number }; - repositoryDiscussionComments?: { totalCount: number }; - repositories: { - totalCount: number; - nodes: Array<{ name: string; stargazerCount: number }>; - pageInfo: { hasNextPage: boolean; endCursor: string | null }; - }; -} - -/** Shape of `response.data` returned by the stats GraphQL query. */ -interface StatsQueryResponse { - data: { user: StatsUser }; -} - /** The subset of the stats response `statsFetcher` returns and threads on. */ -interface StatsFetcherResponse { - data: StatsQueryResponse & { - errors?: Array<{ type?: string; message?: string }>; - }; - statusText: string; -} +type StatsFetcherResponse = Pick< + GraphQLResponse, + "data" | "statusText" +>; /** * Stats fetcher object. @@ -119,11 +37,16 @@ interface StatsFetcherResponse { * @returns Axios response. */ const fetcher = ( - variables: Record, + variables: UserInfoQueryVariables, token: string, -): Promise => { - const query = variables["after"] ? GRAPHQL_REPOS_QUERY : GRAPHQL_STATS_QUERY; - return request({ query, variables }, { Authorization: `bearer ${token}` }); +): Promise> => { + // pages after the first refetch only `repositories`, a subset of the stats query + const document = ( + variables.after ? UserReposDocument : UserInfoDocument + ) as GraphQLDocument; + return httpGraphQLRequest(document, variables, { + Authorization: `bearer ${token}`, + }); }; /** @@ -156,7 +79,7 @@ const statsFetcher = async ({ includeDiscussions: boolean; includeDiscussionsAnswers: boolean; startTime: string | undefined; - ownerAffiliations: Array; + ownerAffiliations: UserInfoQueryVariables["ownerAffiliations"]; pat: string | null; }): Promise => { let stats: StatsFetcherResponse | undefined; @@ -164,9 +87,8 @@ const statsFetcher = async ({ let endCursor: string | null = null; let fetchedPages = 0; while (hasNextPage) { - const variables: Record = { + const variables: UserInfoQueryVariables = { login: username, - first: 100, after: endCursor, includeMergedPullRequests, includeDiscussions, @@ -174,13 +96,14 @@ const statsFetcher = async ({ startTime, ownerAffiliations, }; - const res = await retryer(fetcher, variables, pat); + const res = await retryer(fetcher, variables, pat); if (res.data.errors) { return res; } // Store stats data. - const repoNodes = res.data.data.user.repositories.nodes; + const repositories = res.data.data.user?.repositories; + const repoNodes = repositories?.nodes ?? []; if (stats) { if (fetchedPages === 1) { // deep copy to avoid mutating the response cached by the frontend @@ -189,23 +112,23 @@ const statsFetcher = async ({ statusText: stats.statusText, }); } - stats.data.data.user.repositories.nodes.push(...repoNodes); + stats.data.data.user?.repositories.nodes?.push(...repoNodes); } else { stats = res; } fetchedPages++; const repoNodesWithStars = repoNodes.filter( - (node) => node.stargazerCount !== 0, + (node) => node?.stargazerCount !== 0, ); hasNextPage = (getConfig().fetchMultiPageStars === "true" || Number(getConfig().fetchMultiPageStars) > fetchedPages) && repoNodes.length === repoNodesWithStars.length && - res.data.data.user.repositories.pageInfo.hasNextPage; + !!repositories?.pageInfo.hasNextPage; - endCursor = res.data.data.user.repositories.pageInfo.endCursor; + endCursor = repositories?.pageInfo.endCursor ?? null; } if (!stats) { @@ -451,6 +374,9 @@ const fetchStats = async ( } const user = res.data.data.user; + if (!user) { + throw new CustomError("Could not fetch user.", CustomError.USER_NOT_FOUND); + } stats.name = user.name || user.login; @@ -505,9 +431,9 @@ const fetchStats = async ( ]; const repoToHide = new Set(allExcludedRepos); - stats.totalStars = user.repositories.nodes - .filter((data) => !repoToHide.has(data.name)) - .reduce((prev, curr) => prev + curr.stargazerCount, 0); + stats.totalStars = (user.repositories.nodes ?? []) + .filter((data) => !!data && !repoToHide.has(data.name)) + .reduce((prev, curr) => prev + (curr?.stargazerCount ?? 0), 0); stats.rank = calculateRank({ all_commits: include_all_commits, diff --git a/packages/core/src/fetchers/top-languages.ts b/packages/core/src/fetchers/top-languages.ts index a5ad24c618cfe..c6ab35e647486 100644 --- a/packages/core/src/fetchers/top-languages.ts +++ b/packages/core/src/fetchers/top-languages.ts @@ -1,12 +1,18 @@ -import type { AxiosResponse } from "axios"; - import { getConfig } from "../common/config.js"; import { CustomError, MissingParamError } from "../common/error.js"; import { wrapTextMultiline } from "../common/fmt.js"; -import { request } from "../common/http.js"; +import { httpGraphQLRequest } from "../common/http.js"; +import type { GraphQLResponse } from "../common/http.js"; import { logger } from "../common/log.js"; import { parseOwnerAffiliations } from "../common/ops.js"; import { retryer } from "../common/retryer.js"; +import { TopLanguagesDocument } from "../graphql/generated/top-languages.js"; +import type { + TopLanguageFragment, + TopLanguagesQuery, + TopLanguagesQueryVariables, + TopLanguagesRepositoryFragment, +} from "../graphql/generated/top-languages.js"; import type { Lang, TopLangData } from "./types.js"; @@ -18,63 +24,14 @@ import type { Lang, TopLangData } from "./types.js"; * @returns Languages fetcher response. */ const fetcher = ( - variables: Record, + variables: TopLanguagesQueryVariables, token: string, -): Promise => { - return request( - { - query: ` - query userInfo($login: String!, $ownerAffiliations: [RepositoryAffiliation]) { - user(login: $login) { - # do not fetch forks - repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) { - nodes { - name - languages(first: 10, orderBy: {field: SIZE, direction: DESC}) { - edges { - size - node { - color - name - } - } - } - } - } - } - } - `, - variables, - }, - { - Authorization: `token ${token}`, - }, - ); +): Promise> => { + return httpGraphQLRequest(TopLanguagesDocument, variables, { + Authorization: `token ${token}`, + }); }; -/** A language edge within a repository's `languages` connection. */ -interface LanguageEdge { - size: number; - node: { color: string; name: string }; -} - -/** A repository node returned by the query. */ -interface RepositoryNode { - name: string; - /** Not selected by the query; only here so the no-op sort below type-checks. */ - size?: number; - languages: { edges: Array }; -} - -/** Shape of `response.data` returned by the top-languages query. */ -interface TopLanguagesQueryResponse { - data: { - user: { - repositories: { nodes: Array }; - }; - }; -} - /** * Fetch top languages for a given username. * @@ -99,7 +56,7 @@ const fetchTopLanguages = async ( } const affiliations = parseOwnerAffiliations(ownerAffiliations); - const res = await retryer( + const res = await retryer( fetcher, { login: username, @@ -141,18 +98,22 @@ const fetchTopLanguages = async ( }); // filter out repositories to be hidden - const repoNodes = res.data.data.user.repositories.nodes - .sort((a, b) => (b.size ?? 0) - (a.size ?? 0)) - .filter((node) => !repoToHide[node.name]); + const repoNodes = (res.data.data.user?.repositories.nodes ?? []).filter( + (node): node is TopLanguagesRepositoryFragment => + !!node && !repoToHide[node.name], + ); // flatten edges across repos. Order matters: `concat(acc)` prepends, and the // shared repoCount below depends on visitation order. - const languageEdges = repoNodes - .filter((node) => node.languages.edges.length > 0) - .reduce>( - (acc, curr) => curr.languages.edges.concat(acc), - [], - ); + const languageEdges = repoNodes.reduce>( + (acc, repo) => { + const edges = (repo.languages?.edges ?? []).filter( + (edge): edge is TopLanguageFragment => !!edge, + ); + return edges.length > 0 ? edges.concat(acc) : acc; + }, + [], + ); // accumulate size and repo count per language const languageMap: Record = {}; diff --git a/packages/core/src/fetchers/types.ts b/packages/core/src/fetchers/types.ts index 5951d2b937544..da070f3daa61d 100644 --- a/packages/core/src/fetchers/types.ts +++ b/packages/core/src/fetchers/types.ts @@ -1,3 +1,5 @@ +import type { RepoInfoFragment } from "../graphql/generated/repo.js"; + export interface GistData { name: string; nameWithOwner: string; @@ -7,21 +9,17 @@ export interface GistData { forksCount: number; } -export interface RepoInfo { - name: string; - nameWithOwner: string; - isPrivate: boolean; - isArchived: boolean; - isTemplate: boolean; - stargazerCount: number; - description: string | null; +/** + * What the repo query returns, with `primaryLanguage` loosened: + * the schema says `name` is non-null, but the card falls back to defaults for callers passing untyped data. + */ +type RepoInfo = Omit & { primaryLanguage: { color: string | null; id?: string; name: string | null; } | null; - forkCount: number; -} +}; export interface RepoUserStats { // only present when the matching include_* flag is set (see fetchRepoUserStats) diff --git a/packages/core/src/graphql/generated/common.ts b/packages/core/src/graphql/generated/common.ts new file mode 100644 index 0000000000000..72dd38ede6a4e --- /dev/null +++ b/packages/core/src/graphql/generated/common.ts @@ -0,0 +1,24 @@ +export type Maybe = T | null; +export type InputMaybe = Maybe; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string }; + String: { input: string; output: string }; + Boolean: { input: boolean; output: boolean }; + Int: { input: number; output: number }; + Float: { input: number; output: number }; + /** An ISO-8601 encoded UTC date string. */ + DateTime: { input: string; output: string }; +}; + +/** The affiliation of a user to a repository */ +export type RepositoryAffiliation = + /** Repositories that the user has been added to as a collaborator. */ + | "COLLABORATOR" + /** + * Repositories that the user has access to through being a member of an + * organization. This includes every repository on every team that the user is on. + */ + | "ORGANIZATION_MEMBER" + /** Repositories that are owned by the authenticated user. */ + | "OWNER"; diff --git a/packages/core/src/graphql/generated/gist.ts b/packages/core/src/graphql/generated/gist.ts new file mode 100644 index 0000000000000..8e2789ea5aec8 --- /dev/null +++ b/packages/core/src/graphql/generated/gist.ts @@ -0,0 +1,57 @@ +/** Internal type. DO NOT USE DIRECTLY. */ +type Exact = { [K in keyof T]: T[K] }; +import { graphqlDocument } from "../graphqlDocument.js"; +export type GistFileInfoFragment = { + name: string | null; + size: number | null; + language: { name: string } | null; +}; + +export type GistInfoQueryVariables = Exact<{ + gistName: string; +}>; + +export type GistInfoQuery = { + viewer: { + gist: { + description: string | null; + stargazerCount: number; + owner: { login: string } | { login: string } | null; + forks: { totalCount: number }; + files: Array<{ + name: string | null; + size: number | null; + language: { name: string } | null; + } | null> | null; + } | null; + }; +}; + +export const GistInfoDocument = graphqlDocument< + GistInfoQuery, + GistInfoQueryVariables +>(` +query gistInfo($gistName: String!) { + viewer { + gist(name: $gistName) { + description + owner { + login + } + stargazerCount + forks { + totalCount + } + files { + ...GistFileInfo + } + } + } +} +fragment GistFileInfo on GistFile { + name + language { + name + } + size +}`); diff --git a/packages/core/src/graphql/generated/repo.ts b/packages/core/src/graphql/generated/repo.ts new file mode 100644 index 0000000000000..b5a1d0fba1b42 --- /dev/null +++ b/packages/core/src/graphql/generated/repo.ts @@ -0,0 +1,88 @@ +/** Internal type. DO NOT USE DIRECTLY. */ +type Exact = { [K in keyof T]: T[K] }; +import { graphqlDocument } from "../graphqlDocument.js"; +export type RepoInfoFragment = { + name: string; + nameWithOwner: string; + isPrivate: boolean; + isArchived: boolean; + isTemplate: boolean; + stargazerCount: number; + description: string | null; + forkCount: number; + primaryLanguage: { color: string | null; id: string; name: string } | null; +}; + +export type GetRepoQueryVariables = Exact<{ + login: string; + repo: string; +}>; + +export type GetRepoQuery = { + user: { + repository: { + name: string; + nameWithOwner: string; + isPrivate: boolean; + isArchived: boolean; + isTemplate: boolean; + stargazerCount: number; + description: string | null; + forkCount: number; + primaryLanguage: { + color: string | null; + id: string; + name: string; + } | null; + } | null; + } | null; + organization: { + repository: { + name: string; + nameWithOwner: string; + isPrivate: boolean; + isArchived: boolean; + isTemplate: boolean; + stargazerCount: number; + description: string | null; + forkCount: number; + primaryLanguage: { + color: string | null; + id: string; + name: string; + } | null; + } | null; + } | null; +}; + +export const GetRepoDocument = graphqlDocument< + GetRepoQuery, + GetRepoQueryVariables +>(` +query getRepo($login: String!, $repo: String!) { + user(login: $login) { + repository(name: $repo) { + ...RepoInfo + } + } + organization(login: $login) { + repository(name: $repo) { + ...RepoInfo + } + } +} +fragment RepoInfo on Repository { + name + nameWithOwner + isPrivate + isArchived + isTemplate + stargazerCount + description + primaryLanguage { + color + id + name + } + forkCount +}`); diff --git a/packages/core/src/graphql/generated/stats.ts b/packages/core/src/graphql/generated/stats.ts new file mode 100644 index 0000000000000..2761e731b9944 --- /dev/null +++ b/packages/core/src/graphql/generated/stats.ts @@ -0,0 +1,159 @@ +/** Internal type. DO NOT USE DIRECTLY. */ +type Exact = { [K in keyof T]: T[K] }; +import type * as Types from "./common.js"; + +import { graphqlDocument } from "../graphqlDocument.js"; +export type RepoStarsFragment = { + repositories: { + totalCount: number; + nodes: Array<{ name: string; stargazerCount: number } | null> | null; + pageInfo: { hasNextPage: boolean; endCursor: string | null }; + }; +}; + +export type UserReposQueryVariables = Exact<{ + login: string; + after?: string | null | undefined; + ownerAffiliations?: + | Array + | Types.RepositoryAffiliation + | null + | undefined; +}>; + +export type UserReposQuery = { + user: { + repositories: { + totalCount: number; + nodes: Array<{ name: string; stargazerCount: number } | null> | null; + pageInfo: { hasNextPage: boolean; endCursor: string | null }; + }; + } | null; +}; + +export type UserInfoQueryVariables = Exact<{ + login: string; + after?: string | null | undefined; + includeMergedPullRequests: boolean; + includeDiscussions: boolean; + includeDiscussionsAnswers: boolean; + startTime?: string | null | undefined; + ownerAffiliations?: + | Array + | Types.RepositoryAffiliation + | null + | undefined; +}>; + +export type UserInfoQuery = { + user: { + name: string | null; + login: string; + commits: { totalCommitContributions: number }; + reviews: { totalPullRequestReviewContributions: number }; + repositoriesContributedTo: { totalCount: number }; + pullRequests: { totalCount: number }; + mergedPullRequests?: { totalCount: number }; + openIssues: { totalCount: number }; + closedIssues: { totalCount: number }; + followers: { totalCount: number }; + repositoryDiscussions?: { totalCount: number }; + repositoryDiscussionComments?: { totalCount: number }; + repositories: { + totalCount: number; + nodes: Array<{ name: string; stargazerCount: number } | null> | null; + pageInfo: { hasNextPage: boolean; endCursor: string | null }; + }; + } | null; +}; + +export const UserReposDocument = graphqlDocument< + UserReposQuery, + UserReposQueryVariables +>(` +query userRepos($login: String!, $after: String, $ownerAffiliations: [RepositoryAffiliation]) { + user(login: $login) { + ...RepoStars + } +} +fragment RepoStars on User { + repositories( + first: 100 + after: $after + ownerAffiliations: $ownerAffiliations + orderBy: {direction: DESC, field: STARGAZERS} + ) { + totalCount + nodes { + name + stargazerCount + } + pageInfo { + hasNextPage + endCursor + } + } +}`); + +export const UserInfoDocument = graphqlDocument< + UserInfoQuery, + UserInfoQueryVariables +>(` +query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) { + user(login: $login) { + name + login + commits: contributionsCollection(from: $startTime) { + totalCommitContributions + } + reviews: contributionsCollection { + totalPullRequestReviewContributions + } + repositoriesContributedTo( + first: 1 + contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY] + ) { + totalCount + } + pullRequests(first: 1) { + totalCount + } + mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) { + totalCount + } + openIssues: issues(states: OPEN) { + totalCount + } + closedIssues: issues(states: CLOSED) { + totalCount + } + followers { + totalCount + } + repositoryDiscussions @include(if: $includeDiscussions) { + totalCount + } + repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) { + totalCount + } + ...RepoStars + } +} +fragment RepoStars on User { + repositories( + first: 100 + after: $after + ownerAffiliations: $ownerAffiliations + orderBy: {direction: DESC, field: STARGAZERS} + ) { + totalCount + nodes { + name + stargazerCount + } + pageInfo { + hasNextPage + endCursor + } + } +}`); diff --git a/packages/core/src/graphql/generated/top-languages.ts b/packages/core/src/graphql/generated/top-languages.ts new file mode 100644 index 0000000000000..6588412825b63 --- /dev/null +++ b/packages/core/src/graphql/generated/top-languages.ts @@ -0,0 +1,73 @@ +/** Internal type. DO NOT USE DIRECTLY. */ +type Exact = { [K in keyof T]: T[K] }; +import type * as Types from "./common.js"; + +import { graphqlDocument } from "../graphqlDocument.js"; +export type TopLanguageFragment = { + size: number; + node: { color: string | null; name: string }; +}; + +export type TopLanguagesRepositoryFragment = { + name: string; + languages: { + edges: Array<{ + size: number; + node: { color: string | null; name: string }; + } | null> | null; + } | null; +}; + +export type TopLanguagesQueryVariables = Exact<{ + login: string; + ownerAffiliations?: + | Array + | Types.RepositoryAffiliation + | null + | undefined; +}>; + +export type TopLanguagesQuery = { + user: { + repositories: { + nodes: Array<{ + name: string; + languages: { + edges: Array<{ + size: number; + node: { color: string | null; name: string }; + } | null> | null; + } | null; + } | null> | null; + }; + } | null; +}; + +export const TopLanguagesDocument = graphqlDocument< + TopLanguagesQuery, + TopLanguagesQueryVariables +>(` +query topLanguages($login: String!, $ownerAffiliations: [RepositoryAffiliation]) { + user(login: $login) { + repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) { + nodes { + ...TopLanguagesRepository + } + } + } +} +fragment TopLanguagesRepository on Repository { + name + languages(first: 10, orderBy: {field: SIZE, direction: DESC}) { + edges { + ...TopLanguage + } + } +} +fragment TopLanguage on LanguageEdge { + size + node { + color + name + } +}`); diff --git a/packages/core/src/graphql/graphqlDocument.ts b/packages/core/src/graphql/graphqlDocument.ts new file mode 100644 index 0000000000000..355f121db9789 --- /dev/null +++ b/packages/core/src/graphql/graphqlDocument.ts @@ -0,0 +1,26 @@ +/** + * A GraphQL query paired with the types of its result and variables. + * + * `text` is what goes over the wire; the other two members only carry types and are + * never assigned. + * Documents are generated from `queries/*.graphql` + * See `scripts/generate-graphql-types.js`. + */ +interface GraphQLDocument { + readonly text: string; + readonly __result?: TResult; + readonly __variables?: TVariables; +} + +/** + * Build a typed document. + * + * @param text The query text, including every fragment it spreads. + * @returns The document to hand to `httpGraphQLRequest`. + */ +const graphqlDocument = ( + text: string, +): GraphQLDocument => ({ text }); + +export { graphqlDocument }; +export type { GraphQLDocument }; diff --git a/packages/core/src/graphql/queries/gist.graphql b/packages/core/src/graphql/queries/gist.graphql new file mode 100644 index 0000000000000..7ea29f2da636f --- /dev/null +++ b/packages/core/src/graphql/queries/gist.graphql @@ -0,0 +1,25 @@ +fragment GistFileInfo on GistFile { + name + language { + name + } + size +} + +query gistInfo($gistName: String!) { + viewer { + gist(name: $gistName) { + description + owner { + login + } + stargazerCount + forks { + totalCount + } + files { + ...GistFileInfo + } + } + } +} diff --git a/packages/core/src/graphql/queries/repo.graphql b/packages/core/src/graphql/queries/repo.graphql new file mode 100644 index 0000000000000..4369a6510aae2 --- /dev/null +++ b/packages/core/src/graphql/queries/repo.graphql @@ -0,0 +1,28 @@ +fragment RepoInfo on Repository { + name + nameWithOwner + isPrivate + isArchived + isTemplate + stargazerCount + description + primaryLanguage { + color + id + name + } + forkCount +} + +query getRepo($login: String!, $repo: String!) { + user(login: $login) { + repository(name: $repo) { + ...RepoInfo + } + } + organization(login: $login) { + repository(name: $repo) { + ...RepoInfo + } + } +} diff --git a/packages/core/src/graphql/queries/stats.graphql b/packages/core/src/graphql/queries/stats.graphql new file mode 100644 index 0000000000000..dcd6f42d0048b --- /dev/null +++ b/packages/core/src/graphql/queries/stats.graphql @@ -0,0 +1,79 @@ +fragment RepoStars on User { + repositories( + first: 100 + after: $after + ownerAffiliations: $ownerAffiliations + orderBy: { direction: DESC, field: STARGAZERS } + ) { + totalCount + nodes { + name + stargazerCount + } + pageInfo { + hasNextPage + endCursor + } + } +} + +query userRepos( + $login: String! + $after: String + $ownerAffiliations: [RepositoryAffiliation] +) { + user(login: $login) { + ...RepoStars + } +} + +query userInfo( + $login: String! + $after: String + $includeMergedPullRequests: Boolean! + $includeDiscussions: Boolean! + $includeDiscussionsAnswers: Boolean! + $startTime: DateTime = null + $ownerAffiliations: [RepositoryAffiliation] +) { + user(login: $login) { + name + login + commits: contributionsCollection(from: $startTime) { + totalCommitContributions + } + reviews: contributionsCollection { + totalPullRequestReviewContributions + } + repositoriesContributedTo( + first: 1 + contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY] + ) { + totalCount + } + pullRequests(first: 1) { + totalCount + } + mergedPullRequests: pullRequests(states: MERGED) + @include(if: $includeMergedPullRequests) { + totalCount + } + openIssues: issues(states: OPEN) { + totalCount + } + closedIssues: issues(states: CLOSED) { + totalCount + } + followers { + totalCount + } + repositoryDiscussions @include(if: $includeDiscussions) { + totalCount + } + repositoryDiscussionComments(onlyAnswers: true) + @include(if: $includeDiscussionsAnswers) { + totalCount + } + ...RepoStars + } +} diff --git a/packages/core/src/graphql/queries/top-languages.graphql b/packages/core/src/graphql/queries/top-languages.graphql new file mode 100644 index 0000000000000..4dacc030ca0e0 --- /dev/null +++ b/packages/core/src/graphql/queries/top-languages.graphql @@ -0,0 +1,34 @@ +fragment TopLanguage on LanguageEdge { + size + node { + color + name + } +} + +fragment TopLanguagesRepository on Repository { + name + languages(first: 10, orderBy: { field: SIZE, direction: DESC }) { + edges { + ...TopLanguage + } + } +} + +query topLanguages( + $login: String! + $ownerAffiliations: [RepositoryAffiliation] +) { + user(login: $login) { + # do not fetch forks + repositories( + ownerAffiliations: $ownerAffiliations + isFork: false + first: 100 + ) { + nodes { + ...TopLanguagesRepository + } + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc153a921e890..db4b3b676f80e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,13 +31,13 @@ importers: devDependencies: '@eslint-react/eslint-plugin': specifier: 5.17.1 - version: 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + version: 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@eslint/compat': specifier: 2.1.0 - version: 2.1.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0)) + version: 2.1.0(eslint@10.7.0(jiti@2.7.0)) '@eslint/js': specifier: 10.0.1 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0)) + version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) '@playwright/test': specifier: 1.61.1 version: 1.61.1 @@ -49,16 +49,16 @@ importers: version: 4.1.10(vitest@4.1.10) eslint: specifier: 10.7.0 - version: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + version: 10.7.0(jiti@2.7.0) eslint-import-resolver-typescript: specifier: 4.4.5 - version: 4.4.5(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + version: 4.4.5(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0)) eslint-plugin-import-x: specifier: 4.17.1 - version: 4.17.1(@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + version: 4.17.1(@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)) eslint-plugin-jsdoc: specifier: 63.1.0 - version: 63.1.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + version: 63.1.0(eslint@10.7.0(jiti@2.7.0)) globals: specifier: 17.7.0 version: 17.7.0 @@ -82,7 +82,7 @@ importers: version: 6.0.3 typescript-eslint: specifier: 8.65.0 - version: 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + version: 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) vitest: specifier: catalog:default version: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)) @@ -94,17 +94,17 @@ importers: version: link:../../packages/core axios: specifier: catalog:default - version: 1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + version: 1.18.1 pg: specifier: ^8.22.0 version: 8.22.0 devDependencies: axios-mock-adapter: specifier: 2.1.0 - version: 2.1.0(axios@1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)) + version: 2.1.0(axios@1.18.1) express: specifier: 5.2.1 - version: 5.2.1(supports-color@7.2.0) + version: 5.2.1 jsdom: specifier: catalog:default version: 29.1.1 @@ -128,10 +128,10 @@ importers: version: 4.3.3(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)) axios: specifier: catalog:default - version: 1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + version: 1.18.1 axios-cache-interceptor: specifier: ^1.12.0 - version: 1.12.1(axios@1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)) + version: 1.12.1(axios@1.18.1) daisyui: specifier: ^5.6.18 version: 5.7.8 @@ -192,7 +192,7 @@ importers: dependencies: axios: specifier: catalog:default - version: 1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + version: 1.18.1 emoji-name-map: specifier: ^2.0.3 version: 2.0.3 @@ -200,6 +200,18 @@ importers: specifier: ^1.0.0 version: 1.0.0 devDependencies: + '@graphql-codegen/core': + specifier: 6.2.0 + version: 6.2.0(graphql@16.11.0) + '@graphql-codegen/typescript': + specifier: 6.1.0 + version: 6.1.0(graphql@16.11.0) + '@graphql-codegen/typescript-operations': + specifier: 6.1.2 + version: 6.1.2(graphql@16.11.0) + '@octokit/graphql-schema': + specifier: 15.26.1 + version: 15.26.1 '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -211,7 +223,10 @@ importers: version: 1.1.1 axios-mock-adapter: specifier: 2.1.0 - version: 2.1.0(axios@1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)) + version: 2.1.0(axios@1.18.1) + graphql: + specifier: 16.11.0 + version: 16.11.0 js-yaml: specifier: 5.2.1 version: 5.2.1 @@ -227,6 +242,11 @@ packages: '@adobe/css-tools@4.5.0': resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@ardatan/relay-compiler@13.0.2': + resolution: {integrity: sha512-VFpv9UP820SiwDUPYtq7PmD3jifzZlevkQ26bhbSzFeruSTys0eHzQCZyKg+IhgmZzwPI9AFjPe26ABNjGeIKg==} + peerDependencies: + graphql: '*' + '@asamuzakjp/css-color@5.1.11': resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -263,6 +283,9 @@ packages: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} + '@babel/runtime@8.0.0': + resolution: {integrity: sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==} + '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -449,6 +472,81 @@ packages: '@noble/hashes': optional: true + '@graphql-codegen/core@6.2.0': + resolution: {integrity: sha512-RZadhhwYhuy2ZdIGK40vYVBMzXEFGkCC+58MUC/F2af/gKznEYNzHgmNBUBCk/BTklyUsNu0mIXmyGE4tTA0PA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-codegen/plugin-helpers@7.1.0': + resolution: {integrity: sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-codegen/schema-ast@6.1.0': + resolution: {integrity: sha512-/xuGkM5gUNFRoaQLumKbENdX7Hc8ha49z9OXsEZY8E+46mMjqzXGF0NtCJ892cmoX7EUgI5c8T+LZqS2upx2Aw==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-codegen/typescript-operations@6.1.2': + resolution: {integrity: sha512-EP9xry09q4cOVaf/aC4NO3/SwvXRNzlJIe4dhfA0xyy45Taix5yDL3jeJpmJIv03sa7nK5udVs5vZqdHFU8Xmw==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + graphql-sock: ^1.0.0 + peerDependenciesMeta: + graphql-sock: + optional: true + + '@graphql-codegen/typescript@6.1.0': + resolution: {integrity: sha512-2Hu3111O/AwV28Ap7tNsixlmXSAJuQbQArQklx+IC/tNswpckZnCfmlcBtTJrGU1+mJXEneJXGfb2XWvKjbhlQ==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-codegen/visitor-plugin-common@7.2.2': + resolution: {integrity: sha512-nOkAVd8J8r2YdHm9Z4YrBiy+3IQgE8Ndn/EiRWTvWuemMEhioBWyvdlnbq5rHmIo2bNdRQ5ghs0Q3jw7YBrwLQ==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/merge@9.2.2': + resolution: {integrity: sha512-DSLLAztOIQId7QE3m8Ehk5lV+0pjxNSSRDHPzlYQ9E4KJ9AoUMBprC4C+eX3v4srh05S2ujm5/veqAr5yEWFSQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/optimize@2.0.0': + resolution: {integrity: sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/relay-operation-optimizer@7.1.8': + resolution: {integrity: sha512-s16NYT+66VSLCITBURoGNTwh+YvZRSlW3MtFJC2gsvrHZHf6KpCvIR3Qju4yh0FtCoN7Wg7yOI/JT/UzaMEOwQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/schema@10.0.38': + resolution: {integrity: sha512-Kckk2/vm+rELJ7ijvFaAn9ouWSVUTD0D4SJIvYF4rKeuQHAfCzE/jzMFonZVpTXleqJjPXLZjVbuaMorw7A5Og==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@11.2.2': + resolution: {integrity: sha512-Do2A/t6ayqOma8m7PvpYMsCBswL+NB35BQSng4GWSBqafL2/BnfAZy/NSENPwV721Rm2fG0BHETFC0+FJJZmLw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -492,6 +590,9 @@ packages: '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + '@octokit/graphql-schema@15.26.1': + resolution: {integrity: sha512-RFDC2MpRBd4AxSRvUeBIVeBU7ojN/SxDfALUd7iVYOSeEK3gZaqR2MGOysj4Zh2xj2RY5fQAUT+Oqq7hWTraMA==} + '@oxc-parser/binding-android-arm-eabi@0.137.0': resolution: {integrity: sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1252,6 +1353,10 @@ packages: '@vitest/utils@4.1.10': resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@whatwg-node/promise-helpers@1.3.2': + resolution: {integrity: sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==} + engines: {node: '>=16.0.0'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -1317,6 +1422,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + auto-bind@5.0.1: + resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + axios-cache-interceptor@1.12.1: resolution: {integrity: sha512-0CMA7US/V9tLfJzZyxY5NBzRKvWBkOXeNJLGO1XB/B8xglp36Kdvt72i2Xr3JVrYyhWEGayUFzsSs6piHZadCw==} engines: {node: '>=12'} @@ -1368,6 +1477,12 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + change-case-all@2.1.0: + resolution: {integrity: sha512-v6b0WWWkZUMHVuYk82l+WROgkUm4qEN2w5hKRNWtEOYwWqUGoi8C6xH0l1RLF1EoWqDFK6MFclmN3od6ws3/uw==} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -1388,6 +1503,10 @@ packages: resolution: {integrity: sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==} engines: {node: '>= 12.0.0'} + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} @@ -1414,6 +1533,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cross-inspect@1.0.1: + resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} + engines: {node: '>=16.0.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1458,6 +1581,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dependency-graph@1.0.0: + resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} + engines: {node: '>=4'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1785,6 +1912,16 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql-tag@2.12.7: + resolution: {integrity: sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1842,6 +1979,13 @@ packages: immer@11.1.15: resolution: {integrity: sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==} + immutable@5.1.9: + resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} + + import-from@4.0.0: + resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} + engines: {node: '>=12.2'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1853,10 +1997,17 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-absolute@1.0.0: + resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} + engines: {node: '>=0.10.0'} + is-buffer@2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} @@ -1882,6 +2033,18 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-relative@1.0.0: + resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} + engines: {node: '>=0.10.0'} + + is-unc-path@1.0.0: + resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} + engines: {node: '>=0.10.0'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2036,6 +2199,10 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@11.5.2: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} @@ -2054,6 +2221,10 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2161,6 +2332,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + parse-filepath@1.0.2: + resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} + engines: {node: '>=0.8'} + parse-imports-exports@0.2.4: resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} @@ -2182,6 +2357,14 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-root-regex@0.1.2: + resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} + engines: {node: '>=0.10.0'} + + path-root@0.1.1: + resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} + engines: {node: '>=0.10.0'} + path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -2473,6 +2656,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sponge-case@2.0.3: + resolution: {integrity: sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -2518,6 +2704,9 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + swap-case@3.0.3: + resolution: {integrity: sha512-6p4op8wE9CQv7uDFzulI6YXUw4lD9n4oQierdbFThEKVWVQcbQcUjdP27W8XE7V4QnWmnq9jueSHceyyQnqQVA==} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -2543,6 +2732,9 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + title-case@3.0.3: + resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} + tldts-core@7.4.9: resolution: {integrity: sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==} @@ -2609,6 +2801,10 @@ packages: resolution: {integrity: sha512-60m9IVGbavD6jholbxt0jVBXZkEB/HsMZq7Tyaghseve2/Sf0zQRAIfWsD34sde+DKP2tBxJS2wP88ZM0D1FhA==} engines: {node: '>=14'} + unc-path-regex@0.1.2: + resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} + engines: {node: '>=0.10.0'} + undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -2795,6 +2991,13 @@ snapshots: '@adobe/css-tools@4.5.0': {} + '@ardatan/relay-compiler@13.0.2(graphql@16.11.0)': + dependencies: + '@babel/runtime': 8.0.0 + graphql: 16.11.0 + immutable: 5.1.9 + invariant: 2.2.4 + '@asamuzakjp/css-color@5.1.11': dependencies: '@asamuzakjp/generational-cache': 1.0.1 @@ -2831,6 +3034,8 @@ snapshots: '@babel/runtime@7.29.7': {} + '@babel/runtime@8.0.0': {} + '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -2919,110 +3124,110 @@ snapshots: '@es-joy/resolve.exports@1.2.0': {} - '@eslint-community/eslint-utils@4.10.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.7.0(jiti@2.7.0))': dependencies: - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.7.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint-react/ast@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@eslint-react/ast@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) string-ts: 2.3.1 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint-react/core@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@eslint-react/core@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/jsx': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/jsx': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint-react/eslint-plugin@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@eslint-react/eslint-plugin@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) - eslint-plugin-react-dom: 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint-plugin-react-jsx: 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint-plugin-react-naming-convention: 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint-plugin-react-rsc: 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint-plugin-react-web-api: 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint-plugin-react-x: 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) + eslint-plugin-react-dom: 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-react-jsx: 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-react-naming-convention: 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-react-rsc: 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-react-web-api: 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-react-x: 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint-react/eslint@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@eslint-react/eslint@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint-react/jsx@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@eslint-react/jsx@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint-react/shared@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@eslint-react/shared@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 zod: 4.4.3 transitivePeerDependencies: - supports-color - '@eslint-react/var@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@eslint-react/var@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint/compat@2.1.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))': + '@eslint/compat@2.1.0(eslint@10.7.0(jiti@2.7.0))': dependencies: '@eslint/core': 1.2.1 optionalDependencies: - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.7.0(jiti@2.7.0) - '@eslint/config-array@0.23.5(supports-color@7.2.0)': + '@eslint/config-array@0.23.5': dependencies: '@eslint/object-schema': 3.0.5 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 minimatch: 10.2.6 transitivePeerDependencies: - supports-color @@ -3035,9 +3240,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))': + '@eslint/js@10.0.1(eslint@10.7.0(jiti@2.7.0))': optionalDependencies: - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.7.0(jiti@2.7.0) '@eslint/object-schema@3.0.5': {} @@ -3048,6 +3253,99 @@ snapshots: '@exodus/bytes@1.15.1': {} + '@graphql-codegen/core@6.2.0(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 7.1.0(graphql@16.11.0) + '@graphql-tools/schema': 10.0.38(graphql@16.11.0) + '@graphql-tools/utils': 11.2.2(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-codegen/plugin-helpers@7.1.0(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 11.2.2(graphql@16.11.0) + change-case-all: 2.1.0 + common-tags: 1.8.2 + graphql: 16.11.0 + import-from: 4.0.0 + tslib: 2.8.1 + + '@graphql-codegen/schema-ast@6.1.0(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 7.1.0(graphql@16.11.0) + '@graphql-tools/utils': 11.2.2(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-codegen/typescript-operations@6.1.2(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 7.1.0(graphql@16.11.0) + '@graphql-codegen/schema-ast': 6.1.0(graphql@16.11.0) + '@graphql-codegen/visitor-plugin-common': 7.2.2(graphql@16.11.0) + auto-bind: 5.0.1 + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-codegen/typescript@6.1.0(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 7.1.0(graphql@16.11.0) + '@graphql-codegen/schema-ast': 6.1.0(graphql@16.11.0) + '@graphql-codegen/visitor-plugin-common': 7.2.2(graphql@16.11.0) + auto-bind: 5.0.1 + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-codegen/visitor-plugin-common@7.2.2(graphql@16.11.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 7.1.0(graphql@16.11.0) + '@graphql-tools/optimize': 2.0.0(graphql@16.11.0) + '@graphql-tools/relay-operation-optimizer': 7.1.8(graphql@16.11.0) + '@graphql-tools/utils': 11.2.2(graphql@16.11.0) + auto-bind: 5.0.1 + change-case-all: 2.1.0 + dependency-graph: 1.0.0 + graphql: 16.11.0 + graphql-tag: 2.12.7(graphql@16.11.0) + parse-filepath: 1.0.2 + tslib: 2.8.1 + + '@graphql-tools/merge@9.2.2(graphql@16.11.0)': + dependencies: + '@graphql-tools/utils': 11.2.2(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/optimize@2.0.0(graphql@16.11.0)': + dependencies: + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/relay-operation-optimizer@7.1.8(graphql@16.11.0)': + dependencies: + '@ardatan/relay-compiler': 13.0.2(graphql@16.11.0) + '@graphql-tools/utils': 11.2.2(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/schema@10.0.38(graphql@16.11.0)': + dependencies: + '@graphql-tools/merge': 9.2.2(graphql@16.11.0) + '@graphql-tools/utils': 11.2.2(graphql@16.11.0) + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-tools/utils@11.2.2(graphql@16.11.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) + '@whatwg-node/promise-helpers': 1.3.2 + cross-inspect: 1.0.1 + graphql: 16.11.0 + tslib: 2.8.1 + + '@graphql-typed-document-node/core@3.2.0(graphql@16.11.0)': + dependencies: + graphql: 16.11.0 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -3104,6 +3402,11 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@octokit/graphql-schema@15.26.1': + dependencies: + graphql: 16.11.0 + graphql-tag: 2.12.7(graphql@16.11.0) + '@oxc-parser/binding-android-arm-eabi@0.137.0': optional: true @@ -3446,15 +3749,15 @@ snapshots: '@types/use-sync-external-store@0.0.6': {} - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.65.0 - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.7.0(jiti@2.7.0) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -3462,23 +3765,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.65.0 - debug: 4.4.3(supports-color@7.2.0) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + debug: 4.4.3 + eslint: 10.7.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.65.0(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/project-service@8.65.0(typescript@6.0.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3492,13 +3795,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - debug: 4.4.3(supports-color@7.2.0) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.7.0(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -3506,13 +3809,13 @@ snapshots: '@typescript-eslint/types@8.65.0': {} - '@typescript-eslint/typescript-estree@8.65.0(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.65.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.65.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/project-service': 8.65.0(typescript@6.0.3) '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 '@typescript-eslint/visitor-keys': 8.65.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 @@ -3521,13 +3824,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.7.0(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3669,6 +3972,10 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.1 + '@whatwg-node/promise-helpers@1.3.2': + dependencies: + tslib: 2.8.1 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -3680,9 +3987,9 @@ snapshots: acorn@8.18.0: {} - agent-base@6.0.2(supports-color@7.2.0): + agent-base@6.0.2: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -3725,26 +4032,28 @@ snapshots: asynckit@0.4.0: {} - axios-cache-interceptor@1.12.1(axios@1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)): + auto-bind@5.0.1: {} + + axios-cache-interceptor@1.12.1(axios@1.18.1): dependencies: - axios: 1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + axios: 1.18.1 cache-parser: 1.2.6 fast-defer: 1.1.9 http-vary: 1.0.3 object-code: 2.0.0 try: 1.0.3 - axios-mock-adapter@2.1.0(axios@1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)): + axios-mock-adapter@2.1.0(axios@1.18.1): dependencies: - axios: 1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + axios: 1.18.1 fast-deep-equal: 3.1.3 is-buffer: 2.0.5 - axios@1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): + axios@1.18.1: dependencies: - follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) + follow-redirects: 1.16.0 form-data: 4.0.6 - https-proxy-agent: 5.0.1(supports-color@7.2.0) + https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -3758,11 +4067,11 @@ snapshots: birecord@0.1.2: {} - body-parser@2.3.0(supports-color@7.2.0): + body-parser@2.3.0: dependencies: bytes: 3.1.2 content-type: 2.0.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 @@ -3792,6 +4101,15 @@ snapshots: chai@6.2.2: {} + change-case-all@2.1.0: + dependencies: + change-case: 5.4.4 + sponge-case: 2.0.3 + swap-case: 3.0.3 + title-case: 3.0.3 + + change-case@5.4.4: {} + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -3809,6 +4127,8 @@ snapshots: comment-parser@1.4.7: {} + common-tags@1.8.2: {} + compare-versions@6.1.1: {} content-disposition@1.1.0: {} @@ -3823,6 +4143,10 @@ snapshots: cookie@0.7.2: {} + cross-inspect@1.0.1: + dependencies: + tslib: 2.8.1 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3847,11 +4171,9 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' - debug@4.4.3(supports-color@7.2.0): + debug@4.4.3: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 7.2.0 decimal.js@10.6.0: {} @@ -3861,6 +4183,8 @@ snapshots: depd@2.0.0: {} + dependency-graph@1.0.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -3920,10 +4244,10 @@ snapshots: optionalDependencies: unrs-resolver: 1.12.2 - eslint-import-resolver-typescript@4.4.5(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-import-resolver-typescript@4.4.5(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0)): dependencies: - debug: 4.4.3(supports-color@7.2.0) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + debug: 4.4.3 + eslint: 10.7.0(jiti@2.7.0) eslint-import-context: 0.1.9(unrs-resolver@1.12.2) get-tsconfig: 4.14.0 is-bun-module: 2.0.0 @@ -3931,16 +4255,16 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)): dependencies: '@typescript-eslint/types': 8.65.0 comment-parser: 1.4.7 - debug: 4.4.3(supports-color@7.2.0) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + debug: 4.4.3 + eslint: 10.7.0(jiti@2.7.0) eslint-import-context: 0.1.9(unrs-resolver@1.12.2) is-glob: 4.0.3 minimatch: 10.2.6 @@ -3948,19 +4272,19 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.12.2 optionalDependencies: - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) transitivePeerDependencies: - supports-color - eslint-plugin-jsdoc@63.1.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-plugin-jsdoc@63.1.0(eslint@10.7.0(jiti@2.7.0)): dependencies: '@es-joy/jsdoccomment': 0.88.0 '@es-joy/resolve.exports': 1.2.0 are-docs-informative: 0.0.2 comment-parser: 1.4.7 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.7.0(jiti@2.7.0) espree: 11.2.0 esquery: 1.7.0 html-entities: 2.6.0 @@ -3972,93 +4296,93 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-dom@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + eslint-plugin-react-dom@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/jsx': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/jsx': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) compare-versions: 6.1.1 - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.7.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-jsx@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + eslint-plugin-react-jsx@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/core': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/jsx': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/core': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/jsx': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + eslint-plugin-react-naming-convention@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/core': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/core': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-rsc@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + eslint-plugin-react-rsc@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/core': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/core': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-web-api@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + eslint-plugin-react-web-api@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/core': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/core': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) birecord: 0.1.2 - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.7.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + eslint-plugin-react-x@5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/core': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/jsx': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@eslint-react/ast': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/core': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/jsx': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.17.1(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) compare-versions: 6.1.1 - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.7.0(jiti@2.7.0) string-ts: 2.3.1 ts-api-utils: 2.5.0(typescript@6.0.3) ts-pattern: 5.9.0 @@ -4077,11 +4401,11 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0): + eslint@10.7.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.7.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5(supports-color@7.2.0) + '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 @@ -4091,7 +4415,7 @@ snapshots: '@types/estree': 1.0.9 ajv: 6.15.0 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -4142,20 +4466,20 @@ snapshots: expect-type@1.4.0: {} - express@5.2.1(supports-color@7.2.0): + express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.3.0(supports-color@7.2.0) + body-parser: 2.3.0 content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1(supports-color@7.2.0) + finalhandler: 2.1.1 fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -4166,9 +4490,9 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.3 range-parser: 1.3.0 - router: 2.2.0(supports-color@7.2.0) - send: 1.2.1(supports-color@7.2.0) - serve-static: 2.2.1(supports-color@7.2.0) + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 statuses: 2.0.2 type-is: 2.1.0 vary: 1.1.2 @@ -4195,9 +4519,9 @@ snapshots: dependencies: flat-cache: 4.0.1 - finalhandler@2.1.1(supports-color@7.2.0): + finalhandler@2.1.1: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -4218,9 +4542,7 @@ snapshots: flatted@3.4.3: {} - follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)): - optionalDependencies: - debug: 4.4.3(supports-color@7.2.0) + follow-redirects@1.16.0: {} form-data@4.0.6: dependencies: @@ -4282,6 +4604,13 @@ snapshots: graceful-fs@4.2.11: {} + graphql-tag@2.12.7(graphql@16.11.0): + dependencies: + graphql: 16.11.0 + tslib: 2.8.1 + + graphql@16.11.0: {} + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -4314,10 +4643,10 @@ snapshots: http-vary@1.0.3: {} - https-proxy-agent@5.0.1(supports-color@7.2.0): + https-proxy-agent@5.0.1: dependencies: - agent-base: 6.0.2(supports-color@7.2.0) - debug: 4.4.3(supports-color@7.2.0) + agent-base: 6.0.2 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -4333,14 +4662,27 @@ snapshots: immer@11.1.15: {} + immutable@5.1.9: {} + + import-from@4.0.0: {} + imurmurhash@0.1.4: {} indent-string@4.0.0: {} inherits@2.0.4: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ipaddr.js@1.9.1: {} + is-absolute@1.0.0: + dependencies: + is-relative: 1.0.0 + is-windows: 1.0.2 + is-buffer@2.0.5: {} is-bun-module@2.0.0: @@ -4361,6 +4703,16 @@ snapshots: is-promise@4.0.0: {} + is-relative@1.0.0: + dependencies: + is-unc-path: 1.0.0 + + is-unc-path@1.0.0: + dependencies: + unc-path-regex: 0.1.2 + + is-windows@1.0.2: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -4523,6 +4875,10 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@11.5.2: {} lz-string@1.5.0: {} @@ -4541,6 +4897,8 @@ snapshots: dependencies: semver: 7.8.5 + map-cache@0.2.2: {} + math-intrinsics@1.1.0: {} mdn-data@2.27.1: {} @@ -4663,6 +5021,12 @@ snapshots: dependencies: p-limit: 3.1.0 + parse-filepath@1.0.2: + dependencies: + is-absolute: 1.0.0 + map-cache: 0.2.2 + path-root: 0.1.1 + parse-imports-exports@0.2.4: dependencies: parse-statements: 1.0.11 @@ -4679,6 +5043,12 @@ snapshots: path-key@3.1.1: {} + path-root-regex@0.1.2: {} + + path-root@0.1.1: + dependencies: + path-root-regex: 0.1.2 + path-to-regexp@8.4.2: {} pathe@2.0.3: {} @@ -4863,9 +5233,9 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.5 '@rolldown/binding-win32-x64-msvc': 1.1.5 - router@2.2.0(supports-color@7.2.0): + router@2.2.0: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -4885,9 +5255,9 @@ snapshots: semver@7.8.5: {} - send@1.2.1(supports-color@7.2.0): + send@1.2.1: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -4901,12 +5271,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.1(supports-color@7.2.0): + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1(supports-color@7.2.0) + send: 1.2.1 transitivePeerDependencies: - supports-color @@ -4975,6 +5345,8 @@ snapshots: split2@4.2.0: {} + sponge-case@2.0.3: {} + stable-hash-x@0.2.0: {} stackback@0.0.2: {} @@ -5012,6 +5384,8 @@ snapshots: dependencies: has-flag: 4.0.0 + swap-case@3.0.3: {} + symbol-tree@3.2.4: {} tailwindcss@4.3.3: {} @@ -5029,6 +5403,10 @@ snapshots: tinyrainbow@3.1.1: {} + title-case@3.0.3: + dependencies: + tslib: 2.8.1 + tldts-core@7.4.9: {} tldts@7.4.9: @@ -5058,8 +5436,7 @@ snapshots: ts-pattern@5.9.0: {} - tslib@2.8.1: - optional: true + tslib@2.8.1: {} turbo@2.10.5: optionalDependencies: @@ -5080,13 +5457,13 @@ snapshots: media-typer: 1.1.1 mime-types: 3.0.2 - typescript-eslint@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + typescript-eslint@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.65.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.7.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -5095,6 +5472,8 @@ snapshots: unbash@4.0.4: {} + unc-path-regex@0.1.2: {} + undici-types@7.18.2: {} undici@7.29.0: {} From c04d10e7425f033a6f1801876048a818fa2de85f Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Thu, 13 Aug 2026 04:41:16 +0200 Subject: [PATCH 02/15] fix: apply review suggestion + refiments --- .../core/scripts/generate-graphql-types.js | 331 ++++++++++-------- 1 file changed, 179 insertions(+), 152 deletions(-) diff --git a/packages/core/scripts/generate-graphql-types.js b/packages/core/scripts/generate-graphql-types.js index 8a784e38d010b..4a81aacbbc672 100644 --- a/packages/core/scripts/generate-graphql-types.js +++ b/packages/core/scripts/generate-graphql-types.js @@ -1,3 +1,9 @@ +/** + * Generates TypeScript types for `src/graphql/queries` from GitHub's published schema: + * one file per query, plus a `common.ts` for the types their variables name. + * + * Pass `--check` to fail on drift instead of writing. + */ import fs from "node:fs/promises"; import path from "node:path"; @@ -6,73 +12,92 @@ import * as typescriptPlugin from "@graphql-codegen/typescript"; import * as typescriptOperationsPlugin from "@graphql-codegen/typescript-operations"; import { schema as githubSchema } from "@octokit/graphql-schema"; import { + GraphQLSchema, + Kind, buildSchema, + getNamedType, + isSpecifiedScalarType, parse, print, printSchema, - printType, + typeFromAST, visit, } from "graphql"; import { format, resolveConfig } from "prettier"; +/** + * @typedef {Parameters[0]} CodegenOptions + * @typedef {CodegenOptions["pluginMap"][string]} CodegenPlugin + * @typedef {import("@graphql-codegen/typescript").TypeScriptPluginConfig} TypeScriptPluginConfig + * @typedef {import("@graphql-codegen/typescript-operations").TypeScriptDocumentsPluginConfig} TypeScriptDocumentsPluginConfig + * @typedef {import("graphql").ASTNode} ASTNode + * @typedef {import("graphql").FragmentDefinitionNode} FragmentDefinitionNode + * @typedef {import("graphql").GraphQLNamedType} GraphQLNamedType + * @typedef {import("graphql").OperationDefinitionNode} OperationDefinitionNode + * @typedef {Map} FragmentMap Fragment definitions by name. + */ + const PACKAGE_ROOT = path.join(import.meta.dirname, ".."); const QUERIES_DIR = path.join(PACKAGE_ROOT, "src/graphql/queries"); const OUT_DIR = path.join(PACKAGE_ROOT, "src/graphql/generated"); -const COMMON_FILE = "common.ts"; +const COMMON_FILE = path.join(OUT_DIR, "common.ts"); // `typescript-operations` resolves this against the working directory const COMMON_IMPORT_PATH = path.relative( process.cwd(), - path.join(OUT_DIR, COMMON_FILE.replace(/\.ts$/, ".js")), + COMMON_FILE.replace(/\.ts$/, ".js"), ); -// `--check` regenerates in memory and fails on any difference, for CI +/** + * @param {string} file Absolute path. + * @returns {string} The same path relative to the package, for log messages. + */ +const pathRelativeFromRoot = (file) => path.relative(PACKAGE_ROOT, file); + +// CI mode: regenerate in memory, fail on any difference const checkOnly = process.argv.includes("--check"); +/** @type {TypeScriptPluginConfig & TypeScriptDocumentsPluginConfig} */ const config = { emitLegacyCommonJSImports: false, enumsAsTypes: true, skipTypename: true, useTypeImports: true, - // read the schema's own enums and input types from the common file instead of redeclaring them per query file; - // the import is emitted only where they are used + // enums and input types come from the common file, imported only where used importSchemaTypesFrom: COMMON_IMPORT_PATH, - // nullable fields as `x: T | null`; `@include(if:)` ones stay optional - avoidOptionals: { field: true }, scalars: { DateTime: "string" }, }; -// GitHub's published SDL declares a few fields twice, which trips SDL validation +// GitHub's SDL declares a few fields twice, which trips SDL validation const schemaAst = buildSchema(githubSchema.idl, { assumeValidSDL: true }); +// printed once: every query output reuses it const schemaDocument = parse(printSchema(schemaAst)); -const queryFiles = (await fs.readdir(QUERIES_DIR)) - .filter((file) => file.endsWith(".graphql")) - .sort(); +// a missing folder is a broken checkout, not an empty query set: generating from it would +// delete every existing output +const queryDir = await fs.readdir(QUERIES_DIR).catch(() => { + console.error(`No queries in ${pathRelativeFromRoot(QUERIES_DIR)}`); + process.exit(1); +}); +const queryFiles = queryDir.filter((file) => file.endsWith(".graphql")).sort(); const documents = await Promise.all( queryFiles.map(async (file) => { const location = path.join(QUERIES_DIR, file); - const rawSDL = await fs.readFile(location, "utf8"); - return { location, rawSDL, document: parse(rawSDL) }; + const output = path.join(OUT_DIR, `${path.basename(file, ".graphql")}.ts`); + const document = parse(await fs.readFile(location, "utf8")); + return { location, output, document }; }), ); -const fragments = new Map( - documents.flatMap(({ document }) => - document.definitions - .filter((definition) => definition.kind === "FragmentDefinition") - .map((definition) => [definition.name.value, definition]), - ), -); - /** * Every fragment a node spreads, directly or through another fragment. * - * @param node Operation or fragment definition to walk. - * @param found Fragment names collected so far. - * @returns The fragment names the node needs. + * @param {ASTNode} node Operation or fragment definition to walk. + * @param {FragmentMap} fragments Fragments defined in the same file. + * @param {FragmentMap} found Fragments collected so far. + * @returns {FragmentMap} The fragment definitions the node needs. */ -const spreadFragments = (node, found = new Set()) => { +const spreadFragments = (node, fragments, found = new Map()) => { visit(node, { FragmentSpread(spread) { const name = spread.name.value; @@ -83,171 +108,173 @@ const spreadFragments = (node, found = new Set()) => { if (!fragment) { throw new Error(`No definition found for fragment "${name}"`); } - found.add(name); - spreadFragments(fragment, found); + found.set(name, fragment); + spreadFragments(fragment, fragments, found); }, }); return found; }; +/** + * @param {string} value Word to capitalize. + * @returns {string} The capitalized word. + */ +const pascalCase = (value) => value.charAt(0).toUpperCase() + value.slice(1); + /** * The names `typescript-operations` gives an operation's types. * - * @param operation Operation definition. - * @returns Names to reference in the emitted document. + * @param {OperationDefinitionNode} operation Operation definition. + * @returns {{ documentName: string; resultType: string; variablesType: string }} Names to reference in the emitted document. */ const operationNames = (operation) => { if (!operation.name) { throw new Error("Every query needs a name to generate types from"); } - const name = operation.name.value; - const pascalCase = name.charAt(0).toUpperCase() + name.slice(1); - const suffix = - operation.operation.charAt(0).toUpperCase() + operation.operation.slice(1); - const resultType = pascalCase.endsWith(suffix) - ? pascalCase - : `${pascalCase}${suffix}`; + const name = pascalCase(operation.name.value); + // `typescript-operations` appends the operation type unconditionally + const resultType = `${name}${pascalCase(operation.operation)}`; return { - documentName: `${pascalCase}Document`, + documentName: `${name}Document`, resultType, variablesType: `${resultType}Variables`, }; }; -/** Emits one typed document per operation, next to the types for its shape. */ -const documentsPlugin = { - plugin: (_schema, files) => { - const operations = files.flatMap((file) => - file.document.definitions.filter( - (definition) => definition.kind === "OperationDefinition", - ), - ); - return { - prepend: [`import { graphqlDocument } from "../graphqlDocument.js";`], - content: operations - .map((operation) => { - const { documentName, resultType, variablesType } = - operationNames(operation); - const text = [ - print(operation), - ...[...spreadFragments(operation)].map((name) => - print(fragments.get(name)), - ), - ].join("\n"); - return `export const ${documentName} = graphqlDocument<${resultType}, ${variablesType}>(\`\n${text}\`);`; - }) - .join("\n\n"), - }; - }, +/** + * Emits one typed document per operation, beside its types. + * + * @type {CodegenPlugin['plugin']} + */ +const documentsPlugin = (_schema, files) => { + // `document` is optional on codegen's file type; ours are always parsed + const definitions = files.flatMap((file) => file.document?.definitions ?? []); + const { FRAGMENT_DEFINITION, OPERATION_DEFINITION } = Kind; + + const fragments = new Map( + definitions + .filter((definition) => definition.kind === FRAGMENT_DEFINITION) + .map((definition) => [definition.name.value, definition]), + ); + + const operations = definitions + .filter((definition) => definition.kind === OPERATION_DEFINITION) + .map((operation) => { + const { documentName, resultType, variablesType } = + operationNames(operation); + const text = [ + print(operation), + ...[...spreadFragments(operation, fragments).values()].map(print), + ].join("\n"); + return `export const ${documentName} = graphqlDocument<${resultType}, ${variablesType}>(\`\n${text}\`);`; + }); + + return { + prepend: [`import { graphqlDocument } from "../graphqlDocument.js";`], + content: operations.join("\n\n"), + }; }; -// a schema of only the types the queries reference through their variables, so the -// common file gets the scalars and enums in use rather than every one GitHub declares -const variableTypeNames = new Set(); +// only the types the queries name in their variables; an unknown name yields undefined, +// which codegen reports later against the query +/** @type {Set} */ +const commonTypes = new Set(); for (const { document } of documents) { visit(document, { - VariableDefinition(node) { - visit(node.type, { - NamedType(namedType) { - variableTypeNames.add(namedType.name.value); - }, - }); + VariableDefinition({ type }) { + // `getNamedType` strips the `[…]!` wrappers the variable declares + const named = getNamedType(typeFromAST(schemaAst, type)); + if (named && !isSpecifiedScalarType(named)) { + commonTypes.add(named); + } }, }); } -const subsetSchemaAst = buildSchema( - [...variableTypeNames] - .map((name) => schemaAst.getType(name)) - .filter((type) => !!type && !type.name.startsWith("__")) - .map((type) => printType(type)) - .join("\n\n"), - { assumeValidSDL: true }, -); -// `typescript-operations` emits this for the fragment masking we don't generate, and -// has no config to leave it out +// emitted for the fragment masking we don't generate, with no config to leave it out const INCREMENTAL_TYPE = /^\/\*\* Internal type\. DO NOT USE DIRECTLY\. \*\/\nexport type Incremental = [^\n]*\n/m; -const generated = [ - [ - path.join(OUT_DIR, COMMON_FILE), - await codegen({ - filename: path.join(OUT_DIR, COMMON_FILE), - schema: parse(printSchema(subsetSchemaAst)), - schemaAst: subsetSchemaAst, - documents: [], - config, - plugins: [{ typescript: {} }], - pluginMap: { typescript: typescriptPlugin }, - }), - ], -]; - -for (const file of documents) { - const filename = path.join( - OUT_DIR, - `${path.basename(file.location, ".graphql")}.ts`, - ); - const content = await codegen({ - filename, - schema: schemaDocument, - schemaAst, - documents: [file], - config, - plugins: [{ "typescript-operations": {} }, { documents: {} }], - pluginMap: { - "typescript-operations": typescriptOperationsPlugin, - documents: documentsPlugin, - }, +/** @type {Array>} */ +const outputs = documents.map((file) => ({ + filename: file.output, + schema: schemaDocument, + schemaAst, + documents: [file], + plugins: [{ "typescript-operations": {} }, { documents: {} }], + pluginMap: { + "typescript-operations": typescriptOperationsPlugin, + documents: { plugin: documentsPlugin }, + }, +})); + +// queries with only built-in scalar variables need no common file +if (commonTypes.size) { + const commonSchema = new GraphQLSchema({ types: [...commonTypes] }); + outputs.push({ + filename: COMMON_FILE, + schema: parse(printSchema(commonSchema)), + schemaAst: commonSchema, + documents: [], + plugins: [{ typescript: {} }], + pluginMap: { typescript: typescriptPlugin }, }); - generated.push([filename, content.replace(INCREMENTAL_TYPE, "")]); } -const prettierConfig = await resolveConfig(path.join(OUT_DIR, COMMON_FILE)); -const drifted = []; +const prettierConfig = await resolveConfig(COMMON_FILE); +const generated = await Promise.all( + outputs.map(async ({ filename, ...options }) => { + const content = await codegen({ filename, config, ...options }); + return { + filename, + content: await format(content.replace(INCREMENTAL_TYPE, ""), { + ...prettierConfig, + parser: "typescript", + }), + }; + }), +); + +const queryCount = `${documents.length} ${documents.length === 1 ? "query" : "queries"}`; -await fs.mkdir(OUT_DIR, { recursive: true }); -for (const [filename, content] of generated) { - const formatted = await format(content, { - ...prettierConfig, - parser: "typescript", - }); - if (checkOnly) { +// a deleted query leaves its generated file behind; anything else in the folder is not ours +const expected = new Set(generated.map(({ filename }) => filename)); +const stale = (await fs.readdir(OUT_DIR).catch(() => [])) + .filter((file) => file.endsWith(".ts")) + .map((file) => path.join(OUT_DIR, file)) + .filter((file) => !expected.has(file)); + +if (checkOnly) { + const outdated = stale.map(pathRelativeFromRoot); + for (const { filename, content } of generated) { const current = await fs.readFile(filename, "utf8").catch(() => null); - if (current !== formatted) { - drifted.push(path.relative(PACKAGE_ROOT, filename)); + if (current !== content) { + outdated.push(pathRelativeFromRoot(filename)); } - } else { - await fs.writeFile(filename, formatted); } -} -// a query removed from the queries folder leaves its generated file behind -const expected = new Set( - generated.map(([filename]) => path.basename(filename)), -); -const stale = (await fs.readdir(OUT_DIR).catch(() => [])).filter( - (file) => !expected.has(file), -); -for (const file of stale) { - if (checkOnly) { - drifted.push(path.relative(PACKAGE_ROOT, path.join(OUT_DIR, file))); - } else { - await fs.rm(path.join(OUT_DIR, file)); - } -} - -if (checkOnly) { - if (drifted.length > 0) { - console.error( - `GraphQL types are out of date:\n${drifted.map((file) => ` ${file}`).join("\n")}\n\nRun \`pnpm generate-graphql-types\` in packages/core.`, - ); + if (outdated.length > 0) { + const files = outdated.map((file) => ` ${file}`).join("\n"); + const message = `GraphQL types are out of date:\n${files}\n\nRun \`pnpm --filter ./packages/core/ run generate-graphql-types\`.`; + console.error(message); process.exit(1); } - console.log(`GraphQL types are up to date (${queryFiles.length} queries)`); + + const message = `GraphQL types are up to date (${queryCount})`; + console.log(message); } else { - console.log( - `Generated ${generated.length} files in ${path.relative(PACKAGE_ROOT, OUT_DIR)} from ${queryFiles.length} queries`, - ); + await fs.mkdir(OUT_DIR, { recursive: true }); + for (const { filename, content } of generated) { + await fs.writeFile(filename, content); + } + + for (const file of stale) { + await fs.rm(file); + const removed = `Removed ${pathRelativeFromRoot(file)} — no query generates it`; + console.log(removed); + } + + const outDir = pathRelativeFromRoot(OUT_DIR); + const message = `Generated ${generated.length} files in ${outDir} from ${queryCount}`; + console.log(message); } From 25f1a8155752c5e55d8dcc9de3f2ecaf379dd32d Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Thu, 13 Aug 2026 05:00:43 +0200 Subject: [PATCH 03/15] docs: add banner to each generated file --- packages/core/scripts/generate-graphql-types.js | 4 +++- packages/core/src/graphql/generated/common.ts | 2 ++ packages/core/src/graphql/generated/gist.ts | 2 ++ packages/core/src/graphql/generated/repo.ts | 2 ++ packages/core/src/graphql/generated/stats.ts | 2 ++ packages/core/src/graphql/generated/top-languages.ts | 2 ++ 6 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/core/scripts/generate-graphql-types.js b/packages/core/scripts/generate-graphql-types.js index 4a81aacbbc672..503003ed35eb9 100644 --- a/packages/core/scripts/generate-graphql-types.js +++ b/packages/core/scripts/generate-graphql-types.js @@ -195,6 +195,8 @@ for (const { document } of documents) { const INCREMENTAL_TYPE = /^\/\*\* Internal type\. DO NOT USE DIRECTLY\. \*\/\nexport type Incremental = [^\n]*\n/m; +const BANNER = `// Generated file — see .github/CONTRIBUTING.md\n\n`; + /** @type {Array>} */ const outputs = documents.map((file) => ({ filename: file.output, @@ -227,7 +229,7 @@ const generated = await Promise.all( const content = await codegen({ filename, config, ...options }); return { filename, - content: await format(content.replace(INCREMENTAL_TYPE, ""), { + content: await format(BANNER + content.replace(INCREMENTAL_TYPE, ""), { ...prettierConfig, parser: "typescript", }), diff --git a/packages/core/src/graphql/generated/common.ts b/packages/core/src/graphql/generated/common.ts index 72dd38ede6a4e..33a09a5cbec1e 100644 --- a/packages/core/src/graphql/generated/common.ts +++ b/packages/core/src/graphql/generated/common.ts @@ -1,3 +1,5 @@ +// Generated file — see .github/CONTRIBUTING.md + export type Maybe = T | null; export type InputMaybe = Maybe; /** All built-in and custom scalars, mapped to their actual values */ diff --git a/packages/core/src/graphql/generated/gist.ts b/packages/core/src/graphql/generated/gist.ts index 8e2789ea5aec8..da4a9491d9212 100644 --- a/packages/core/src/graphql/generated/gist.ts +++ b/packages/core/src/graphql/generated/gist.ts @@ -1,3 +1,5 @@ +// Generated file — see .github/CONTRIBUTING.md + /** Internal type. DO NOT USE DIRECTLY. */ type Exact = { [K in keyof T]: T[K] }; import { graphqlDocument } from "../graphqlDocument.js"; diff --git a/packages/core/src/graphql/generated/repo.ts b/packages/core/src/graphql/generated/repo.ts index b5a1d0fba1b42..3711456cc0cd3 100644 --- a/packages/core/src/graphql/generated/repo.ts +++ b/packages/core/src/graphql/generated/repo.ts @@ -1,3 +1,5 @@ +// Generated file — see .github/CONTRIBUTING.md + /** Internal type. DO NOT USE DIRECTLY. */ type Exact = { [K in keyof T]: T[K] }; import { graphqlDocument } from "../graphqlDocument.js"; diff --git a/packages/core/src/graphql/generated/stats.ts b/packages/core/src/graphql/generated/stats.ts index 2761e731b9944..4203767969b2c 100644 --- a/packages/core/src/graphql/generated/stats.ts +++ b/packages/core/src/graphql/generated/stats.ts @@ -1,3 +1,5 @@ +// Generated file — see .github/CONTRIBUTING.md + /** Internal type. DO NOT USE DIRECTLY. */ type Exact = { [K in keyof T]: T[K] }; import type * as Types from "./common.js"; diff --git a/packages/core/src/graphql/generated/top-languages.ts b/packages/core/src/graphql/generated/top-languages.ts index 6588412825b63..03ff1245ea923 100644 --- a/packages/core/src/graphql/generated/top-languages.ts +++ b/packages/core/src/graphql/generated/top-languages.ts @@ -1,3 +1,5 @@ +// Generated file — see .github/CONTRIBUTING.md + /** Internal type. DO NOT USE DIRECTLY. */ type Exact = { [K in keyof T]: T[K] }; import type * as Types from "./common.js"; From e1d2940eae081a692d2df6a72c9d42cc86462eb2 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Thu, 13 Aug 2026 05:01:21 +0200 Subject: [PATCH 04/15] docs(CONTRIBUTING): add GraphQL Queries --- .github/CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 20374f4194182..59cb333e379f5 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -31,6 +31,19 @@ pnpm --filter ./packages/core/ run test:update:snapshot pnpm --filter ./apps/backend/ run test:update:snapshot ``` +## GraphQL Queries + +The GraphQL queries live in `packages/core/src/graphql/queries/*.graphql`, +and their TypeScript types are generated from GitHub's published schema into `packages/core/src/graphql/generated/`. +Those generated files are committed, so if you change a query, regenerate them and include the result in your PR: + +```bash +pnpm --filter ./packages/core/ run generate-graphql-types +``` + +CI runs `pnpm --filter ./packages/core/ run check-graphql-types`, which fails if the committed types no longer match the queries. +Never edit the generated files by hand — change the `.graphql` file and regenerate. + ## Themes Contribution We have stopped the addition of new themes to decrease maintenance efforts. If you are considering contributing your theme just because you are using it personally, then instead of adding it to our theme collection, you can use card [customization options](../docs/advanced_documentation.md#customization). From 370b44c1ac02df160903d0d3b8c57193ecea5408 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Thu, 13 Aug 2026 05:10:50 +0200 Subject: [PATCH 05/15] chore(dependabot): group graphql deps --- .github/dependabot.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5e7101a8e2de9..6500dbaac1b04 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -57,6 +57,11 @@ updates: patterns: - "axios" - "axios-*" + graphql: + patterns: + - "graphql" + - "@graphql-codegen/*" + - "@octokit/graphql-schema" # Maintain dependencies for GitHub Actions - package-ecosystem: github-actions From ffe55b1768425ae34568aca9a68fe4c0f5664e8d Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:02:01 +0200 Subject: [PATCH 06/15] WIP: initial work --- packages/core/src/api/index.js | 1 + packages/core/src/cards/stats.ts | 10 ++++ packages/core/src/common/icons.ts | 1 + packages/core/src/fetchers/stats.ts | 73 +++++++++++++++++++++++++++++ packages/core/src/fetchers/types.ts | 1 + packages/core/src/translations.ts | 3 ++ 6 files changed, 89 insertions(+) diff --git a/packages/core/src/api/index.js b/packages/core/src/api/index.js index 52c381b122f54..c51328a3f5b75 100644 --- a/packages/core/src/api/index.js +++ b/packages/core/src/api/index.js @@ -105,6 +105,7 @@ export default async ( showStats.includes("issues_authored"), showStats.includes("issues_commented"), parseArray(role), + showStats.includes("contributions"), pat, ); diff --git a/packages/core/src/cards/stats.ts b/packages/core/src/cards/stats.ts index a12e8fd0080b7..55521db66c11e 100644 --- a/packages/core/src/cards/stats.ts +++ b/packages/core/src/cards/stats.ts @@ -247,6 +247,7 @@ const renderStatsCard = ( totalPRsReviewed, totalIssuesAuthored, totalIssuesCommented, + totalContributions, rank, } = stats; const { @@ -414,6 +415,15 @@ const renderStatsCard = ( }; } + if (show.includes("contributions")) { + STATS["contributions"] = { + icon: icons.repo_push, + label: i18n.t("statcard.contributions"), + value: totalContributions, + id: "contributions", + }; + } + STATS["contribs"] = { icon: icons.contribs, label: i18n.t("statcard.contribs"), diff --git a/packages/core/src/common/icons.ts b/packages/core/src/common/icons.ts index 64bc3b523f310..94d9609f3b829 100644 --- a/packages/core/src/common/icons.ts +++ b/packages/core/src/common/icons.ts @@ -38,6 +38,7 @@ const icons = { discussions_started: ``, discussions_answered: ``, comments: ``, + repo_push: ``, gist: ``, }; diff --git a/packages/core/src/fetchers/stats.ts b/packages/core/src/fetchers/stats.ts index ace2c8733b6f4..5e51b7a5c8699 100644 --- a/packages/core/src/fetchers/stats.ts +++ b/packages/core/src/fetchers/stats.ts @@ -71,6 +71,9 @@ const GRAPHQL_STATS_QUERY = ` repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) { totalCount } + contributionsCollection { + contributionYears + } ${GRAPHQL_REPOS_FIELD} } } @@ -91,6 +94,7 @@ interface StatsUser { followers: { totalCount: number }; repositoryDiscussions?: { totalCount: number }; repositoryDiscussionComments?: { totalCount: number }; + contributionsCollection: { contributionYears: Array }; repositories: { totalCount: number; nodes: Array<{ name: string; stargazerCount: number }>; @@ -354,6 +358,64 @@ const fetchRepoUserStats = async ( return stats; }; +/** + * Fetch all-time contributions by building a single GraphQL query + * for all the given years. + */ +const fetchTotalContributions = async ( + username: string, + years: Array, + pat: string | null = null, +): Promise => { + if (years.length === 0) { + return 0; + } + + const yearFields = years + .map( + (year) => + `year_${year}: contributionsCollection(from: "${year}-01-01T00:00:00Z", to: "${year}-12-31T23:59:59Z") { contributionCalendar { totalContributions } }`, + ) + .join("\n"); + + const contributionsQuery = ` + query userContributions($login: String!) { + user(login: $login) { + ${yearFields} + } + } + `; + + const contributionsFetcher = ( + variables: Record, + token: string, + ): Promise => { + return request( + { query: contributionsQuery, variables }, + { Authorization: `bearer ${token}` }, + ); + }; + + const contribRes = await retryer( + contributionsFetcher, + { login: username }, + pat, + ); + const user = contribRes.data.data?.user; + if (!user) { + return 0; + } + + let total = 0; + for (const year of years) { + const yearBlock = user[`year_${year}`]; + if (yearBlock?.contributionCalendar?.totalContributions) { + total += yearBlock.contributionCalendar.totalContributions; + } + } + return total; +}; + /** * Fetch stats for a given username. * @@ -372,6 +434,7 @@ const fetchRepoUserStats = async ( * @param include_issues_authored Include count of issues authored. * @param include_issues_commented Include count of issues commented. * @param ownerAffiliations Owner affiliations. Default: OWNER. + * @param include_contributions Include all-time contributions. * @param pat Optional PAT override. * @returns Stats data. */ @@ -391,6 +454,7 @@ const fetchStats = async ( include_issues_authored = false, include_issues_commented = false, ownerAffiliations: Array = [], + include_contributions = false, pat: string | null = null, ): Promise => { if (!username) { @@ -414,6 +478,7 @@ const fetchStats = async ( totalPRsReviewed: 0, totalIssuesAuthored: 0, totalIssuesCommented: 0, + totalContributions: 0, rank: { level: "C", percentile: 100 }, }; const affiliations = parseOwnerAffiliations(ownerAffiliations); @@ -498,6 +563,14 @@ const fetchStats = async ( } stats.contributedTo = user.repositoriesContributedTo.totalCount; + if (include_contributions) { + stats.totalContributions = await fetchTotalContributions( + username, + user.contributionsCollection.contributionYears, + pat, + ); + } + // Retrieve stars while filtering out repositories to be hidden. const allExcludedRepos = [ ...exclude_repo, diff --git a/packages/core/src/fetchers/types.ts b/packages/core/src/fetchers/types.ts index 9d8bc281053e9..d3fcce22a27b6 100644 --- a/packages/core/src/fetchers/types.ts +++ b/packages/core/src/fetchers/types.ts @@ -51,6 +51,7 @@ export interface StatsData { totalPRsReviewed: number; totalIssuesAuthored: number; totalIssuesCommented: number; + totalContributions: number; rank: { level: string; percentile: number }; } diff --git a/packages/core/src/translations.ts b/packages/core/src/translations.ts index 385d4759f0d27..3cc83c72f7e1f 100644 --- a/packages/core/src/translations.ts +++ b/packages/core/src/translations.ts @@ -524,6 +524,9 @@ const statCardLocales = ({ "statcard.issues-commented": { en: "Issues Commented", }, + "statcard.contributions": { + en: "Contributions", + }, "statcard.prs-merged": { en: "Total PRs Merged", ar: "مجموع طلبات السحب المُدمجة", From 56cf5b0962cf765a9b5f0a8601581993f28d5fe3 Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:31:52 +0200 Subject: [PATCH 07/15] improve typing --- packages/core/src/fetchers/stats.ts | 17 +++++++++++------ packages/core/src/graphql/generated/stats.ts | 4 ++++ packages/core/src/graphql/queries/stats.graphql | 6 ++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/core/src/fetchers/stats.ts b/packages/core/src/fetchers/stats.ts index 6ff4ca2185434..53a190d750499 100644 --- a/packages/core/src/fetchers/stats.ts +++ b/packages/core/src/fetchers/stats.ts @@ -18,6 +18,7 @@ import { import type { UserInfoQuery, UserInfoQueryVariables, + YearContributionsFragment, } from "../graphql/generated/stats.js"; import type { GraphQLDocument } from "../graphql/graphqlDocument.js"; @@ -277,6 +278,10 @@ const fetchRepoUserStats = async ( return stats; }; +interface ContributionsQuery { + user: Record | null; +} + /** * Fetch all-time contributions by building a single GraphQL query * for all the given years. @@ -297,7 +302,7 @@ const fetchTotalContributions = async ( ) .join("\n"); - const contributionsQuery = ` + const query = ` query userContributions($login: String!) { user(login: $login) { ${yearFields} @@ -308,11 +313,11 @@ const fetchTotalContributions = async ( const contributionsFetcher = ( variables: Record, token: string, - ): Promise => { + ): Promise> => { return request( - { query: contributionsQuery, variables }, + { query, variables }, { Authorization: `bearer ${token}` }, - ); + ) as Promise>; }; const contribRes = await retryer( @@ -320,7 +325,7 @@ const fetchTotalContributions = async ( { login: username }, pat, ); - const user = contribRes.data.data?.user; + const user = contribRes.data.data.user; if (!user) { return 0; } @@ -328,7 +333,7 @@ const fetchTotalContributions = async ( let total = 0; for (const year of years) { const yearBlock = user[`year_${year}`]; - if (yearBlock?.contributionCalendar?.totalContributions) { + if (yearBlock?.contributionCalendar.totalContributions) { total += yearBlock.contributionCalendar.totalContributions; } } diff --git a/packages/core/src/graphql/generated/stats.ts b/packages/core/src/graphql/generated/stats.ts index 50001552742d7..4e9f227fe1dae 100644 --- a/packages/core/src/graphql/generated/stats.ts +++ b/packages/core/src/graphql/generated/stats.ts @@ -70,6 +70,10 @@ export type UserInfoQuery = { } | null; }; +export type YearContributionsFragment = { + contributionCalendar: { totalContributions: number }; +}; + export const UserReposDocument = graphqlDocument< UserReposQuery, UserReposQueryVariables diff --git a/packages/core/src/graphql/queries/stats.graphql b/packages/core/src/graphql/queries/stats.graphql index a86c4245e8902..9c9f846bf9c36 100644 --- a/packages/core/src/graphql/queries/stats.graphql +++ b/packages/core/src/graphql/queries/stats.graphql @@ -80,3 +80,9 @@ query userInfo( ...RepoStars } } + +fragment YearContributions on ContributionsCollection { + contributionCalendar { + totalContributions + } +} From 2d1b7b3ac7eb644bbbdf3a2b68e88080c045b8c9 Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:22:51 +0200 Subject: [PATCH 08/15] add tests --- .../__snapshots__/api.test.js.snap | 2 +- apps/backend/tests/utils.js | 3 ++ packages/core/tests/fetchStats.test.ts | 49 +++++++++++++++++++ packages/core/tests/renderStatsCard.test.ts | 10 ++++ 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap index 9990fc43cda11..c4a492e653a1c 100644 --- a/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap +++ b/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap @@ -699,7 +699,7 @@ exports[`Test /api contract > should match the public many-params response snaps ", - "graphqlRequest": "{"query":"\\nquery userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection(from: $startTime) {\\n totalCommitContributions\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(\\n first: 1\\n contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]\\n ) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n ...RepoStars\\n }\\n}\\nfragment RepoStars on User {\\n repositories(\\n first: 100\\n after: $after\\n ownerAffiliations: $ownerAffiliations\\n orderBy: {direction: DESC, field: STARGAZERS}\\n ) {\\n totalCount\\n nodes {\\n name\\n stargazerCount\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}","variables":{"login":"anuraghazra","after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}", + "graphqlRequest": "{"query":"\\nquery userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection(from: $startTime) {\\n totalCommitContributions\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(\\n first: 1\\n contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]\\n ) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n contributionsCollection {\\n contributionYears\\n }\\n ...RepoStars\\n }\\n}\\nfragment RepoStars on User {\\n repositories(\\n first: 100\\n after: $after\\n ownerAffiliations: $ownerAffiliations\\n orderBy: {direction: DESC, field: STARGAZERS}\\n ) {\\n totalCount\\n nodes {\\n name\\n stargazerCount\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}","variables":{"login":"anuraghazra","after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}", "headers": [ [ "Cache-Control", diff --git a/apps/backend/tests/utils.js b/apps/backend/tests/utils.js index 739845ca5b807..069297f7cd2b5 100644 --- a/apps/backend/tests/utils.js +++ b/apps/backend/tests/utils.js @@ -8,6 +8,9 @@ export const data_stats = { name: "Anurag Hazra", login: "anuraghazra", repositoriesContributedTo: { totalCount: 51 }, + contributionsCollection: { + contributionYears: [2022, 2024], + }, commits: { totalCommitContributions: 200, }, diff --git a/packages/core/tests/fetchStats.test.ts b/packages/core/tests/fetchStats.test.ts index 513b924ed7a51..483c8c6e56bb4 100644 --- a/packages/core/tests/fetchStats.test.ts +++ b/packages/core/tests/fetchStats.test.ts @@ -17,6 +17,9 @@ const data_stats = { user: { name: "Anurag Hazra", repositoriesContributedTo: { totalCount: 61 }, + contributionsCollection: { + contributionYears: [2022, 2024], + }, commits: { totalCommitContributions: 100, }, @@ -97,6 +100,15 @@ const data_repo_zero_stars = { }, }; +const data_contributions = { + data: { + user: { + year_2022: { contributionCalendar: { totalContributions: 150 } }, + year_2024: { contributionCalendar: { totalContributions: 200 } }, + }, + }, +}; + const error = { errors: [ { @@ -122,6 +134,9 @@ beforeEach(() => { if (req.variables?.startTime?.startsWith("2003")) { return [200, data_year2003]; } + if (req.query.includes("contributionCalendar")) { + return [200, data_contributions]; + } return [ 200, req.query.includes("totalCommitContributions") ? data_stats : data_repo, @@ -164,6 +179,7 @@ describe("Test fetchStats", () => { totalPRsReviewed: 0, totalIssuesAuthored: 0, totalIssuesCommented: 0, + totalContributions: 0, rank, }); }); @@ -205,6 +221,7 @@ describe("Test fetchStats", () => { totalPRsReviewed: 0, totalIssuesAuthored: 0, totalIssuesCommented: 0, + totalContributions: 0, rank, }); }); @@ -254,6 +271,7 @@ describe("Test fetchStats", () => { totalPRsReviewed: 0, totalIssuesAuthored: 0, totalIssuesCommented: 0, + totalContributions: 0, rank, }); }); @@ -312,6 +330,7 @@ describe("Test fetchStats", () => { totalPRsReviewed: 0, totalIssuesAuthored: 0, totalIssuesCommented: 0, + totalContributions: 0, rank, }); }); @@ -349,6 +368,7 @@ describe("Test fetchStats", () => { totalPRsReviewed: 0, totalIssuesAuthored: 0, totalIssuesCommented: 0, + totalContributions: 0, rank, }); }); @@ -386,6 +406,7 @@ describe("Test fetchStats", () => { totalPRsReviewed: 0, totalIssuesAuthored: 0, totalIssuesCommented: 0, + totalContributions: 0, rank, }); }); @@ -423,6 +444,7 @@ describe("Test fetchStats", () => { totalPRsReviewed: 0, totalIssuesAuthored: 0, totalIssuesCommented: 0, + totalContributions: 0, rank, }); }); @@ -457,6 +479,7 @@ describe("Test fetchStats", () => { totalPRsReviewed: 0, totalIssuesAuthored: 0, totalIssuesCommented: 0, + totalContributions: 0, rank, }); }); @@ -491,6 +514,7 @@ describe("Test fetchStats", () => { totalPRsReviewed: 0, totalIssuesAuthored: 0, totalIssuesCommented: 0, + totalContributions: 0, rank, }); }); @@ -534,10 +558,34 @@ describe("Test fetchStats", () => { totalPRsAuthored: 0, totalPRsCommented: 0, totalPRsReviewed: 0, + totalContributions: 0, rank, }); }); + it("should fetch total contributions when include_contributions is true", async () => { + const stats = await fetchStats( + "anuraghazra", + false, + [], + false, + false, + false, + undefined, + [], + [], + false, + false, + false, + false, + false, + [], + true, // include_contributions + ); + + expect(stats.totalContributions).toBe(350); + }); + it("should return correct data when user don't have any pull requests", async () => { mock .onPost("https://api.github.com/graphql") @@ -571,6 +619,7 @@ describe("Test fetchStats", () => { totalPRsAuthored: 0, totalPRsCommented: 0, totalPRsReviewed: 0, + totalContributions: 0, rank, }); }); diff --git a/packages/core/tests/renderStatsCard.test.ts b/packages/core/tests/renderStatsCard.test.ts index 5e3b6831bdee8..8201e5ad8639b 100644 --- a/packages/core/tests/renderStatsCard.test.ts +++ b/packages/core/tests/renderStatsCard.test.ts @@ -25,6 +25,7 @@ const stats: StatsData = { totalPRsReviewed: 100, totalIssuesAuthored: 100, totalIssuesCommented: 100, + totalContributions: 5000, rank: { level: "A+", percentile: 40 }, }; @@ -57,6 +58,7 @@ describe("Test renderStatsCard", () => { expect( screen.queryByTestId("prs_merged_percentage"), ).not.toBeInTheDocument(); + expect(screen.queryByTestId("contributions")).not.toBeInTheDocument(); }); it("should have proper name apostrophe", () => { @@ -121,6 +123,14 @@ describe("Test renderStatsCard", () => { expect(screen.queryByTestId("prs_merged_percentage")).toBeDefined(); }); + it("should show contributions stat when included in show list", () => { + document.body.innerHTML = renderStatsCard(stats, { + show: ["contributions"], + }); + + expect(screen.getByTestId("contributions").textContent).toBe("5k"); + }); + it("should hide_rank", () => { document.body.innerHTML = renderStatsCard(stats, { hide_rank: true }); From 323b642dd4c1564539ea26f9aa3b25bb19487082 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 14 Aug 2026 04:23:21 +0200 Subject: [PATCH 09/15] refactor(core): split the stats fetcher per GraphQL query --- .../__snapshots__/api.test.js.snap | 2 +- packages/core/src/common/config.ts | 51 ++++---- packages/core/src/fetchers/stats.ts | 123 ++++++++++-------- packages/core/src/graphql/generated/stats.ts | 16 ++- .../core/src/graphql/queries/stats.graphql | 8 +- packages/core/tests/fetchStats.test.ts | 59 ++++++++- 6 files changed, 166 insertions(+), 93 deletions(-) diff --git a/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap index c4a492e653a1c..dee4aef1f0943 100644 --- a/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap +++ b/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap @@ -699,7 +699,7 @@ exports[`Test /api contract > should match the public many-params response snaps ", - "graphqlRequest": "{"query":"\\nquery userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection(from: $startTime) {\\n totalCommitContributions\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(\\n first: 1\\n contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]\\n ) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n contributionsCollection {\\n contributionYears\\n }\\n ...RepoStars\\n }\\n}\\nfragment RepoStars on User {\\n repositories(\\n first: 100\\n after: $after\\n ownerAffiliations: $ownerAffiliations\\n orderBy: {direction: DESC, field: STARGAZERS}\\n ) {\\n totalCount\\n nodes {\\n name\\n stargazerCount\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}","variables":{"login":"anuraghazra","after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}", + "graphqlRequest": "{"query":"\\nquery userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection(from: $startTime) {\\n totalCommitContributions\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(\\n first: 1\\n contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]\\n ) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n contributionsCollection {\\n contributionYears\\n }\\n ...RepoStars\\n }\\n}\\nfragment RepoStars on User {\\n repositories(\\n first: 100\\n after: $after\\n ownerAffiliations: $ownerAffiliations\\n orderBy: {direction: DESC, field: STARGAZERS}\\n ) {\\n totalCount\\n nodes {\\n ...RepoNode\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\\nfragment RepoNode on Repository {\\n name\\n stargazerCount\\n}","variables":{"login":"anuraghazra","after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}", "headers": [ [ "Cache-Control", diff --git a/packages/core/src/common/config.ts b/packages/core/src/common/config.ts index 5dfc2d86487a7..9d67bd6dc691c 100644 --- a/packages/core/src/common/config.ts +++ b/packages/core/src/common/config.ts @@ -9,7 +9,8 @@ interface Config { whitelist: Array | undefined; gistWhitelist: Array | undefined; excludeRepositories: Array; - fetchMultiPageStars: string | undefined; + /** Max pages of starred repos to fetch; `Infinity` means every page, `1` only the first. */ + fetchMultiPageStars: number; pats: Array; } @@ -17,11 +18,19 @@ interface Config { * @param value Comma-separated string. * @returns Parsed string values. */ -const parseCsv = (value: string | undefined): Array | undefined => { - if (!value) { - return undefined; +const parseCsv = (value: string | undefined): Array | undefined => + value ? value.split(",") : undefined; + +/** + * @param value Raw `FETCH_MULTI_PAGE_STARS` value. + * @returns Page limit: `"true"` means every page, a positive number caps the pages, anything else means one. + */ +const parseFetchMultiPageStars = (value: string | undefined): number => { + if (value === "true") { + return Infinity; } - return value.split(","); + const limit = Number(value); + return limit > 0 ? limit : 1; }; /** @@ -45,37 +54,21 @@ const getDefaultEnv = (): Env => { return processEnv ?? {}; }; -/** - * @param config (Partial) config values to normalize. - * @returns Normalized config object with defaults applied. - */ -const normalizeConfig = (config: Partial = {}): Config => { - return { - whitelist: config.whitelist, - gistWhitelist: config.gistWhitelist, - excludeRepositories: config.excludeRepositories ?? [], - fetchMultiPageStars: config.fetchMultiPageStars, - pats: config.pats ?? [], - }; -}; - let currentConfig: Config; /** * @param env Environment variables used to build the runtime config. */ export const loadConfigFromEnv = (env: Env = getDefaultEnv()): void => { - const whitelist = parseCsv(env["WHITELIST"]); - const gistWhitelist = parseCsv(env["GIST_WHITELIST"]); - const excludeRepositories = parseCsv(env["EXCLUDE_REPO"]) ?? []; - - currentConfig = normalizeConfig({ - whitelist, - gistWhitelist, - excludeRepositories, - fetchMultiPageStars: env["FETCH_MULTI_PAGE_STARS"], + currentConfig = { + whitelist: parseCsv(env["WHITELIST"]), + gistWhitelist: parseCsv(env["GIST_WHITELIST"]), + excludeRepositories: parseCsv(env["EXCLUDE_REPO"]) ?? [], + fetchMultiPageStars: parseFetchMultiPageStars( + env["FETCH_MULTI_PAGE_STARS"], + ), pats: parsePATsFromEnv(env), - }); + }; }; loadConfigFromEnv(); diff --git a/packages/core/src/fetchers/stats.ts b/packages/core/src/fetchers/stats.ts index 53a190d750499..0dfb70470fe5a 100644 --- a/packages/core/src/fetchers/stats.ts +++ b/packages/core/src/fetchers/stats.ts @@ -16,11 +16,13 @@ import { UserReposDocument, } from "../graphql/generated/stats.js"; import type { + RepoNodeFragment, UserInfoQuery, UserInfoQueryVariables, + UserReposQuery, + UserReposQueryVariables, YearContributionsFragment, } from "../graphql/generated/stats.js"; -import type { GraphQLDocument } from "../graphql/graphqlDocument.js"; import type { RepoUserStats, StatsData } from "./types.js"; @@ -40,15 +42,19 @@ type StatsFetcherResponse = Pick< const fetcher = ( variables: UserInfoQueryVariables, token: string, -): Promise> => { - // pages after the first refetch only `repositories`, a subset of the stats query - const document = ( - variables.after ? UserReposDocument : UserInfoDocument - ) as GraphQLDocument; - return httpGraphQLRequest(document, variables, { +): Promise> => + httpGraphQLRequest(UserInfoDocument, variables, { + Authorization: `bearer ${token}`, + }); + +/** Fetcher for the pages after the first, which only need `repositories`. */ +const reposFetcher = ( + variables: UserReposQueryVariables, + token: string, +): Promise> => + httpGraphQLRequest(UserReposDocument, variables, { Authorization: `bearer ${token}`, }); -}; /** * Fetch stats information for a given username. @@ -83,58 +89,67 @@ const statsFetcher = async ({ ownerAffiliations: UserInfoQueryVariables["ownerAffiliations"]; pat: string | null; }): Promise => { - let stats: StatsFetcherResponse | undefined; - let hasNextPage = true; - let endCursor: string | null = null; - let fetchedPages = 0; - while (hasNextPage) { - const variables: UserInfoQueryVariables = { - login: username, - after: endCursor, - includeMergedPullRequests, - includeDiscussions, - includeDiscussionsAnswers, - startTime, - ownerAffiliations, - }; - const res = await retryer(fetcher, variables, pat); - if (res.data.errors) { - return res; - } + const variables: UserInfoQueryVariables = { + login: username, + after: null, + includeMergedPullRequests, + includeDiscussions, + includeDiscussionsAnswers, + startTime, + ownerAffiliations, + }; - // Store stats data. - const repositories = res.data.data.user?.repositories; - const repoNodes = repositories?.nodes ?? []; - if (stats) { - if (fetchedPages === 1) { - // deep copy to avoid mutating the response cached by the frontend - stats = structuredClone({ - data: stats.data, - statusText: stats.statusText, - }); - } - stats.data.data.user?.repositories.nodes?.push(...repoNodes); - } else { - stats = res; + // only the first request carries the stats themselves + let stats: StatsFetcherResponse = await retryer(fetcher, variables, pat); + if (stats.data.errors) { + return stats; + } + + const pageLimit = getConfig().fetchMultiPageStars; + + const extraRepoNodes: Array = []; + let pageRepositories = stats.data.data.user?.repositories; + let previousCursor: string | null = null; + let fetchedPages = 1; + while ( + fetchedPages < pageLimit && + // an unstarred repo on the page means the starred ones are exhausted + !pageRepositories?.nodes?.some((node) => node?.stargazerCount === 0) && + pageRepositories?.pageInfo.hasNextPage + ) { + const after = pageRepositories.pageInfo.endCursor; + // a null or non-advancing cursor would refetch the same page forever + if (after === null || after === previousCursor) { + break; } + previousCursor = after; - fetchedPages++; - const repoNodesWithStars = repoNodes.filter( - (node) => node?.stargazerCount !== 0, + const page = await retryer( + reposFetcher, + { login: username, after, ownerAffiliations }, + pat, ); + if (page.data.errors) { + return { + data: { ...stats.data, errors: page.data.errors }, + statusText: page.statusText, + }; + } - hasNextPage = - (getConfig().fetchMultiPageStars === "true" || - Number(getConfig().fetchMultiPageStars) > fetchedPages) && - repoNodes.length === repoNodesWithStars.length && - !!repositories?.pageInfo.hasNextPage; - - endCursor = repositories?.pageInfo.endCursor ?? null; + pageRepositories = page.data.data.user?.repositories; + extraRepoNodes.push(...(pageRepositories?.nodes ?? [])); + fetchedPages++; } - if (!stats) { - throw new Error("No stats data fetched."); + if (extraRepoNodes.length > 0) { + // deep copy to avoid mutating the response cached by the frontend + stats = structuredClone({ + data: stats.data, + statusText: stats.statusText, + }); + stats.data.data.user?.repositories.nodes?.push(...extraRepoNodes); } + return stats; }; @@ -175,7 +190,9 @@ const fetchTotalItems = ( * @param username GitHub username. * @returns Total count. * - * @see #92#issuecomment-661026467 and #211 — the GraphQL API can't return this. + * The GraphQL API can't return this. + * @see https://github.com/anuraghazra/github-readme-stats/issues/92#issuecomment-661026467 + * @see https://github.com/anuraghazra/github-readme-stats/pull/211 */ const totalItemsFetcher = async ( username: string, diff --git a/packages/core/src/graphql/generated/stats.ts b/packages/core/src/graphql/generated/stats.ts index 4e9f227fe1dae..c2b1d718d721a 100644 --- a/packages/core/src/graphql/generated/stats.ts +++ b/packages/core/src/graphql/generated/stats.ts @@ -5,6 +5,8 @@ type Exact = { [K in keyof T]: T[K] }; import type * as Types from "./common.js"; import { graphqlDocument } from "../graphqlDocument.js"; +export type RepoNodeFragment = { name: string; stargazerCount: number }; + export type RepoStarsFragment = { repositories: { totalCount: number; @@ -92,14 +94,17 @@ fragment RepoStars on User { ) { totalCount nodes { - name - stargazerCount + ...RepoNode } pageInfo { hasNextPage endCursor } } +} +fragment RepoNode on Repository { + name + stargazerCount }`); export const UserInfoDocument = graphqlDocument< @@ -158,12 +163,15 @@ fragment RepoStars on User { ) { totalCount nodes { - name - stargazerCount + ...RepoNode } pageInfo { hasNextPage endCursor } } +} +fragment RepoNode on Repository { + name + stargazerCount }`); diff --git a/packages/core/src/graphql/queries/stats.graphql b/packages/core/src/graphql/queries/stats.graphql index 9c9f846bf9c36..d99a27b086d55 100644 --- a/packages/core/src/graphql/queries/stats.graphql +++ b/packages/core/src/graphql/queries/stats.graphql @@ -1,3 +1,8 @@ +fragment RepoNode on Repository { + name + stargazerCount +} + fragment RepoStars on User { repositories( first: 100 @@ -7,8 +12,7 @@ fragment RepoStars on User { ) { totalCount nodes { - name - stargazerCount + ...RepoNode } pageInfo { hasNextPage diff --git a/packages/core/tests/fetchStats.test.ts b/packages/core/tests/fetchStats.test.ts index 483c8c6e56bb4..642cdbf1d1d5d 100644 --- a/packages/core/tests/fetchStats.test.ts +++ b/packages/core/tests/fetchStats.test.ts @@ -79,6 +79,11 @@ const data_repo = { }, }; +const data_repo_more_pages = structuredClone(data_repo); +data_repo_more_pages.data.user.repositories.pageInfo.hasNextPage = true; +// distinct from data_stats' cursor so the chaining assertion can see advancement +data_repo_more_pages.data.user.repositories.pageInfo.endCursor = "cursor-2"; + const data_repo_zero_stars = { data: { user: { @@ -123,7 +128,7 @@ const error = { const mock = new MockAdapter(axios); beforeEach(() => { - process.env["FETCH_MULTI_PAGE_STARS"] = "false"; // Set to `false` to fetch only one page of stars. + vi.stubEnv("FETCH_MULTI_PAGE_STARS", "false"); // Set to `false` to fetch only one page of stars. loadConfigFromEnv(); mock.onPost("https://api.github.com/graphql").reply((cfg) => { const req = JSON.parse(cfg.data as string) as { @@ -146,6 +151,7 @@ beforeEach(() => { afterEach(() => { mock.reset(); + vi.unstubAllEnvs(); }); describe("Test fetchStats", () => { @@ -336,7 +342,7 @@ describe("Test fetchStats", () => { }); it("should fetch two pages of stars if 'FETCH_MULTI_PAGE_STARS' env variable is set to `true`", async () => { - process.env["FETCH_MULTI_PAGE_STARS"] = "true"; + vi.stubEnv("FETCH_MULTI_PAGE_STARS", "true"); loadConfigFromEnv(); const stats = await fetchStats("anuraghazra"); @@ -374,7 +380,7 @@ describe("Test fetchStats", () => { }); it("should fetch one page of stars if 'FETCH_MULTI_PAGE_STARS' env variable is set to `false`", async () => { - process.env["FETCH_MULTI_PAGE_STARS"] = "false"; + vi.stubEnv("FETCH_MULTI_PAGE_STARS", "false"); loadConfigFromEnv(); const stats = await fetchStats("anuraghazra"); @@ -412,7 +418,7 @@ describe("Test fetchStats", () => { }); it("should fetch one page of stars if 'FETCH_MULTI_PAGE_STARS' env variable is not set", async () => { - process.env["FETCH_MULTI_PAGE_STARS"] = undefined; + vi.stubEnv("FETCH_MULTI_PAGE_STARS", undefined); loadConfigFromEnv(); const stats = await fetchStats("anuraghazra"); @@ -449,6 +455,51 @@ describe("Test fetchStats", () => { }); }); + it("should fetch at most 'FETCH_MULTI_PAGE_STARS' pages when it is a number", async () => { + vi.stubEnv("FETCH_MULTI_PAGE_STARS", "3"); + loadConfigFromEnv(); + mock.reset(); + mock + .onPost("https://api.github.com/graphql") + .replyOnce(200, data_stats) + .onPost("https://api.github.com/graphql") + .replyOnce(200, data_repo_more_pages) + .onPost("https://api.github.com/graphql") + .replyOnce(200, data_repo_more_pages) + // a fourth page is available but must not be requested + .onPost("https://api.github.com/graphql") + .replyOnce(200, data_repo); + + const stats = await fetchStats("anuraghazra"); + + // the stats page plus two repo pages, even though every page has a next one + expect(mock.history.post).toHaveLength(3); + expect(stats.totalStars).toBe(500); + // each page is requested with the cursor the previous one returned + const cursors = mock.history.post.map( + (req) => + (JSON.parse(req.data as string) as { variables: { after: unknown } }) + .variables.after, + ); + expect(cursors).toStrictEqual([null, "cursor", "cursor-2"]); + }); + + it("should throw when a page after the first returns an error", async () => { + vi.stubEnv("FETCH_MULTI_PAGE_STARS", "true"); + loadConfigFromEnv(); + mock.reset(); + mock + .onPost("https://api.github.com/graphql") + .replyOnce(200, data_stats) + .onPost("https://api.github.com/graphql") + .replyOnce(200, error); + + await expect(fetchStats("anuraghazra")).rejects.toThrow( + "Could not resolve to a User with the login of 'noname'.", + ); + expect(mock.history.post).toHaveLength(2); + }); + it("should not fetch additional stats data when it not requested", async () => { const stats = await fetchStats("anuraghazra"); const rank = calculateRank({ From 16a6bf442e12917fb19c5a8f9bcda5dcd21b6546 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 14 Aug 2026 04:40:03 +0200 Subject: [PATCH 10/15] refactor(core): dedupe the GraphQL fetchers into createGraphQLFetcher --- packages/core/src/common/http.ts | 37 +++++++------- packages/core/src/fetchers/gist.ts | 25 ++-------- packages/core/src/fetchers/repo.ts | 23 +-------- packages/core/src/fetchers/stats.ts | 55 +++++++-------------- packages/core/src/fetchers/top-languages.ts | 21 +------- 5 files changed, 44 insertions(+), 117 deletions(-) diff --git a/packages/core/src/common/http.ts b/packages/core/src/common/http.ts index d2966d3c67515..71dcb245e1286 100644 --- a/packages/core/src/common/http.ts +++ b/packages/core/src/common/http.ts @@ -12,24 +12,25 @@ type GraphQLResponse = AxiosResponse<{ }>; /** - * Send a GraphQL request to the GitHub API. - * - * @param document Generated query document; carries its result and variable types. - * @param variables Variables the query declares. - * @param headers Request headers. - * @returns Request response. + * @param document Generated query document. + * @param scheme `Authorization` scheme for the token. + * @returns A fetcher `retryer` can drive. */ -const httpGraphQLRequest = ( +const createGraphQLFetcher = ( document: GraphQLDocument, - variables: TVariables, - headers: NonNullable, -): Promise> => { - return axios({ - url: GITHUB_GRAPHQL_API, - method: "post", - headers, - data: { query: document.text, variables }, - }); + scheme: "bearer" | "token", +) => { + return ( + variables: TVariables, + token: string, + ): Promise> => { + return axios({ + url: GITHUB_GRAPHQL_API, + method: "post", + headers: { Authorization: `${scheme} ${token}` }, + data: { query: document.text, variables }, + }); + }; }; /** Body of a GraphQL request sent to the GitHub API. */ @@ -47,7 +48,7 @@ interface GraphQLRequest { * @param headers Request headers. * @returns Request response. * - * @description Superseded by {@link httpGraphQLRequest}. + * @description Superseded by {@link createGraphQLFetcher}. * Still used by the backend status endpoints for their own `rateLimit` query, * and part of this package's published API. * @@ -66,5 +67,5 @@ const request = ( }); }; -export { httpGraphQLRequest, request }; +export { createGraphQLFetcher, request }; export type { GraphQLResponse }; diff --git a/packages/core/src/fetchers/gist.ts b/packages/core/src/fetchers/gist.ts index be0a74a0570de..6f8cef9720c09 100644 --- a/packages/core/src/fetchers/gist.ts +++ b/packages/core/src/fetchers/gist.ts @@ -1,31 +1,12 @@ import { MissingParamError } from "../common/error.js"; -import { httpGraphQLRequest } from "../common/http.js"; -import type { GraphQLResponse } from "../common/http.js"; +import { createGraphQLFetcher } from "../common/http.js"; import { retryer } from "../common/retryer.js"; import { GistInfoDocument } from "../graphql/generated/gist.js"; -import type { - GistFileInfoFragment, - GistInfoQuery, - GistInfoQueryVariables, -} from "../graphql/generated/gist.js"; +import type { GistFileInfoFragment } from "../graphql/generated/gist.js"; import type { GistData } from "./types.js"; -/** - * Gist data fetcher. - * - * @param variables Fetcher variables. - * @param token GitHub token. - * @returns The response. - */ -const fetcher = ( - variables: GistInfoQueryVariables, - token: string, -): Promise> => { - return httpGraphQLRequest(GistInfoDocument, variables, { - Authorization: `token ${token}`, - }); -}; +const fetcher = createGraphQLFetcher(GistInfoDocument, "token"); /** * This function calculates the primary language of a gist by files size. diff --git a/packages/core/src/fetchers/repo.ts b/packages/core/src/fetchers/repo.ts index aa154e89e28b2..b922dbf9567e3 100644 --- a/packages/core/src/fetchers/repo.ts +++ b/packages/core/src/fetchers/repo.ts @@ -1,31 +1,12 @@ import { MissingParamError } from "../common/error.js"; -import { httpGraphQLRequest } from "../common/http.js"; -import type { GraphQLResponse } from "../common/http.js"; +import { createGraphQLFetcher } from "../common/http.js"; import { retryer } from "../common/retryer.js"; import { GetRepoDocument } from "../graphql/generated/repo.js"; -import type { - GetRepoQuery, - GetRepoQueryVariables, -} from "../graphql/generated/repo.js"; import { fetchRepoUserStats } from "./stats.js"; import type { RepositoryData } from "./types.js"; -/** - * Repo data fetcher. - * - * @param variables Fetcher variables. - * @param token GitHub token. - * @returns The response. - */ -const fetcher = ( - variables: GetRepoQueryVariables, - token: string, -): Promise> => { - return httpGraphQLRequest(GetRepoDocument, variables, { - Authorization: `token ${token}`, - }); -}; +const fetcher = createGraphQLFetcher(GetRepoDocument, "token"); const urlExample = "/api/pin?username=USERNAME&repo=REPO_NAME"; diff --git a/packages/core/src/fetchers/stats.ts b/packages/core/src/fetchers/stats.ts index 0dfb70470fe5a..f01e654818628 100644 --- a/packages/core/src/fetchers/stats.ts +++ b/packages/core/src/fetchers/stats.ts @@ -6,7 +6,8 @@ import { calculateRank } from "../calculateRank.js"; import { getConfig } from "../common/config.js"; import { CustomError, MissingParamError } from "../common/error.js"; import { wrapTextMultiline } from "../common/fmt.js"; -import { httpGraphQLRequest, request } from "../common/http.js"; +import { request } from "../common/http.js"; +import { createGraphQLFetcher } from "../common/http.js"; import type { GraphQLResponse } from "../common/http.js"; import { logger } from "../common/log.js"; import { buildSearchFilter, parseOwnerAffiliations } from "../common/ops.js"; @@ -19,8 +20,6 @@ import type { RepoNodeFragment, UserInfoQuery, UserInfoQueryVariables, - UserReposQuery, - UserReposQueryVariables, YearContributionsFragment, } from "../graphql/generated/stats.js"; @@ -32,29 +31,9 @@ type StatsFetcherResponse = Pick< "data" | "statusText" >; -/** - * Stats fetcher object. - * - * @param variables Fetcher variables. - * @param token GitHub token. - * @returns Axios response. - */ -const fetcher = ( - variables: UserInfoQueryVariables, - token: string, -): Promise> => - httpGraphQLRequest(UserInfoDocument, variables, { - Authorization: `bearer ${token}`, - }); - +const fetcher = createGraphQLFetcher(UserInfoDocument, "bearer"); /** Fetcher for the pages after the first, which only need `repositories`. */ -const reposFetcher = ( - variables: UserReposQueryVariables, - token: string, -): Promise> => - httpGraphQLRequest(UserReposDocument, variables, { - Authorization: `bearer ${token}`, - }); +const reposFetcher = createGraphQLFetcher(UserReposDocument, "bearer"); /** * Fetch stats information for a given username. @@ -67,7 +46,7 @@ const reposFetcher = ( * @param variables.startTime Time to start the count of total commits. * @param variables.ownerAffiliations The owner affiliations to filter by. Default: OWNER. * @param variables.pat PAT override or null. - * @returns Axios response. + * @returns The stats response, with every fetched page's repos merged in. * * @description Supports multi-page fetching when the `FETCH_MULTI_PAGE_STARS` * env variable is `true` or a fetch limit. @@ -89,18 +68,20 @@ const statsFetcher = async ({ ownerAffiliations: UserInfoQueryVariables["ownerAffiliations"]; pat: string | null; }): Promise => { - const variables: UserInfoQueryVariables = { - login: username, - after: null, - includeMergedPullRequests, - includeDiscussions, - includeDiscussionsAnswers, - startTime, - ownerAffiliations, - }; - // only the first request carries the stats themselves - let stats: StatsFetcherResponse = await retryer(fetcher, variables, pat); + let stats: StatsFetcherResponse = await retryer( + fetcher, + { + login: username, + after: null, + includeMergedPullRequests, + includeDiscussions, + includeDiscussionsAnswers, + startTime, + ownerAffiliations, + }, + pat, + ); if (stats.data.errors) { return stats; } diff --git a/packages/core/src/fetchers/top-languages.ts b/packages/core/src/fetchers/top-languages.ts index c6ab35e647486..ff4e921b953c9 100644 --- a/packages/core/src/fetchers/top-languages.ts +++ b/packages/core/src/fetchers/top-languages.ts @@ -1,36 +1,19 @@ import { getConfig } from "../common/config.js"; import { CustomError, MissingParamError } from "../common/error.js"; import { wrapTextMultiline } from "../common/fmt.js"; -import { httpGraphQLRequest } from "../common/http.js"; -import type { GraphQLResponse } from "../common/http.js"; +import { createGraphQLFetcher } from "../common/http.js"; import { logger } from "../common/log.js"; import { parseOwnerAffiliations } from "../common/ops.js"; import { retryer } from "../common/retryer.js"; import { TopLanguagesDocument } from "../graphql/generated/top-languages.js"; import type { TopLanguageFragment, - TopLanguagesQuery, - TopLanguagesQueryVariables, TopLanguagesRepositoryFragment, } from "../graphql/generated/top-languages.js"; import type { Lang, TopLangData } from "./types.js"; -/** - * Top languages fetcher object. - * - * @param variables Fetcher variables. - * @param token GitHub token. - * @returns Languages fetcher response. - */ -const fetcher = ( - variables: TopLanguagesQueryVariables, - token: string, -): Promise> => { - return httpGraphQLRequest(TopLanguagesDocument, variables, { - Authorization: `token ${token}`, - }); -}; +const fetcher = createGraphQLFetcher(TopLanguagesDocument, "token"); /** * Fetch top languages for a given username. From 79e358b0c496f24ead907b53af61685627507341 Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:14:31 +0200 Subject: [PATCH 11/15] fix multi-page test --- packages/core/tests/fetchStats.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/core/tests/fetchStats.test.ts b/packages/core/tests/fetchStats.test.ts index 642cdbf1d1d5d..2ba194a9b46e7 100644 --- a/packages/core/tests/fetchStats.test.ts +++ b/packages/core/tests/fetchStats.test.ts @@ -79,10 +79,12 @@ const data_repo = { }, }; -const data_repo_more_pages = structuredClone(data_repo); -data_repo_more_pages.data.user.repositories.pageInfo.hasNextPage = true; +const data_repo_page2 = structuredClone(data_repo); +data_repo_page2.data.user.repositories.pageInfo.hasNextPage = true; // distinct from data_stats' cursor so the chaining assertion can see advancement -data_repo_more_pages.data.user.repositories.pageInfo.endCursor = "cursor-2"; +data_repo_page2.data.user.repositories.pageInfo.endCursor = "cursor-2"; +const data_repo_page3 = structuredClone(data_repo_page2); +data_repo_page3.data.user.repositories.pageInfo.endCursor = "cursor-3"; const data_repo_zero_stars = { data: { @@ -463,9 +465,9 @@ describe("Test fetchStats", () => { .onPost("https://api.github.com/graphql") .replyOnce(200, data_stats) .onPost("https://api.github.com/graphql") - .replyOnce(200, data_repo_more_pages) + .replyOnce(200, data_repo_page2) .onPost("https://api.github.com/graphql") - .replyOnce(200, data_repo_more_pages) + .replyOnce(200, data_repo_page3) // a fourth page is available but must not be requested .onPost("https://api.github.com/graphql") .replyOnce(200, data_repo); From a75c5709b52e4802e93a86b03e0a7d81f86323ac Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Sat, 15 Aug 2026 00:04:07 +0200 Subject: [PATCH 12/15] include in 'all stats', fix icon --- apps/frontend/src/pages/Home/buildCardUrl.test.ts | 2 +- apps/frontend/src/pages/Home/buildCardUrl.ts | 2 +- packages/core/src/common/icons.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/pages/Home/buildCardUrl.test.ts b/apps/frontend/src/pages/Home/buildCardUrl.test.ts index 350eecd12c848..9e3b3820868b6 100644 --- a/apps/frontend/src/pages/Home/buildCardUrl.test.ts +++ b/apps/frontend/src/pages/Home/buildCardUrl.test.ts @@ -53,7 +53,7 @@ describe("buildCardUrl", () => { "?username=john" + "&hide_title=true" + // commas are percent-encoded (%2C) now that params go through URLSearchParams - "&show=reviews%2Cdiscussions_started%2Cdiscussions_answered%2Cprs_merged%2Cprs_merged_percentage%2Cprs_commented%2Cprs_reviewed%2Cissues_commented" + + "&show=reviews%2Cdiscussions_started%2Cdiscussions_answered%2Cprs_merged%2Cprs_merged_percentage%2Cprs_commented%2Cprs_reviewed%2Cissues_commented%2Ccontributions" + "&show_icons=true" + "&include_all_commits=true", ); diff --git a/apps/frontend/src/pages/Home/buildCardUrl.ts b/apps/frontend/src/pages/Home/buildCardUrl.ts index 70743b823612c..28081a0c2564a 100644 --- a/apps/frontend/src/pages/Home/buildCardUrl.ts +++ b/apps/frontend/src/pages/Home/buildCardUrl.ts @@ -54,7 +54,7 @@ export function buildCardUrl( } if (showAllStats) { url = url.show( - "reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented", + "reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented,contributions", ); } if (showIcons) { diff --git a/packages/core/src/common/icons.ts b/packages/core/src/common/icons.ts index 94d9609f3b829..f0dcb0e17318c 100644 --- a/packages/core/src/common/icons.ts +++ b/packages/core/src/common/icons.ts @@ -38,7 +38,7 @@ const icons = { discussions_started: ``, discussions_answered: ``, comments: ``, - repo_push: ``, + repo_push: ``, gist: ``, }; From a724bf0ce898092b0aeb86659058db2d715081d0 Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:00:13 +0200 Subject: [PATCH 13/15] use different icon --- packages/core/src/cards/stats.ts | 2 +- packages/core/src/common/icons.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/cards/stats.ts b/packages/core/src/cards/stats.ts index 55521db66c11e..cdb67ddd4d77c 100644 --- a/packages/core/src/cards/stats.ts +++ b/packages/core/src/cards/stats.ts @@ -417,7 +417,7 @@ const renderStatsCard = ( if (show.includes("contributions")) { STATS["contributions"] = { - icon: icons.repo_push, + icon: icons.contributions, label: i18n.t("statcard.contributions"), value: totalContributions, id: "contributions", diff --git a/packages/core/src/common/icons.ts b/packages/core/src/common/icons.ts index f0dcb0e17318c..9e8b2f636a94c 100644 --- a/packages/core/src/common/icons.ts +++ b/packages/core/src/common/icons.ts @@ -38,7 +38,7 @@ const icons = { discussions_started: ``, discussions_answered: ``, comments: ``, - repo_push: ``, + contributions: ``, gist: ``, }; From 7aeaf4f4b8253015101facde4a199874a58768ae Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:50:29 +0200 Subject: [PATCH 14/15] wording, stats order --- packages/core/src/cards/stats.ts | 19 ++++++++++--------- packages/core/src/translations.ts | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/core/src/cards/stats.ts b/packages/core/src/cards/stats.ts index cdb67ddd4d77c..b5dc277b05a6c 100644 --- a/packages/core/src/cards/stats.ts +++ b/packages/core/src/cards/stats.ts @@ -294,6 +294,16 @@ const renderStatsCard = ( value: totalStars, id: "stars", }; + + if (show.includes("contributions")) { + STATS["contributions"] = { + icon: icons.contributions, + label: i18n.t("statcard.contributions"), + value: totalContributions, + id: "contributions", + }; + } + STATS["commits"] = { icon: icons.commits, label: `${i18n.t("statcard.commits")}${getTotalCommitsYearLabel( @@ -415,15 +425,6 @@ const renderStatsCard = ( }; } - if (show.includes("contributions")) { - STATS["contributions"] = { - icon: icons.contributions, - label: i18n.t("statcard.contributions"), - value: totalContributions, - id: "contributions", - }; - } - STATS["contribs"] = { icon: icons.contribs, label: i18n.t("statcard.contribs"), diff --git a/packages/core/src/translations.ts b/packages/core/src/translations.ts index 3cc83c72f7e1f..0786e119d9876 100644 --- a/packages/core/src/translations.ts +++ b/packages/core/src/translations.ts @@ -525,7 +525,7 @@ const statCardLocales = ({ en: "Issues Commented", }, "statcard.contributions": { - en: "Contributions", + en: "Total Contributions", }, "statcard.prs-merged": { en: "Total PRs Merged", From 7d9b681cc945ddf7d61548dc24a84596eafce1f5 Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:03:33 +0200 Subject: [PATCH 15/15] comments --- packages/core/src/fetchers/stats.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/fetchers/stats.ts b/packages/core/src/fetchers/stats.ts index f01e654818628..dc85bf011504d 100644 --- a/packages/core/src/fetchers/stats.ts +++ b/packages/core/src/fetchers/stats.ts @@ -283,6 +283,9 @@ interface ContributionsQuery { /** * Fetch all-time contributions by building a single GraphQL query * for all the given years. + * + * Whether private contributions are included depends on the user's profile settings: + * https://docs.github.com/en/account-and-profile/how-tos/contribution-settings/manage-visibility-settings-for-private-contributions-and-achievements#changing-the-visibility-of-your-private-contributions */ const fetchTotalContributions = async ( username: string, @@ -296,6 +299,7 @@ const fetchTotalContributions = async ( const yearFields = years .map( (year) => + // without the "to" field, 2024-01-01 would be included for year=2023 `year_${year}: contributionsCollection(from: "${year}-01-01T00:00:00Z", to: "${year}-12-31T23:59:59Z") { contributionCalendar { totalContributions } }`, ) .join("\n");