From 7aadeacc8890b5496e090c87ec1248545358ce8b Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Wed, 12 Aug 2026 20:44:47 +0200 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 961c444aa4d95bcd3f42a0f6bce0d46060353ffc Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 14 Aug 2026 04:23:21 +0200 Subject: [PATCH 6/7] 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 9990fc43cda11..784234ff773d8 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 ...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 52c5fbe7fa88e..a7149376aaf4f 100644 --- a/packages/core/src/fetchers/stats.ts +++ b/packages/core/src/fetchers/stats.ts @@ -16,10 +16,12 @@ import { UserReposDocument, } from "../graphql/generated/stats.js"; import type { + RepoNodeFragment, UserInfoQuery, UserInfoQueryVariables, + UserReposQuery, + UserReposQueryVariables, } from "../graphql/generated/stats.js"; -import type { GraphQLDocument } from "../graphql/graphqlDocument.js"; import type { RepoUserStats, StatsData } from "./types.js"; @@ -39,15 +41,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. @@ -82,58 +88,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, + }; + + // 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; - // 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; + 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; }; @@ -174,7 +189,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 4203767969b2c..e8d39af0df2ed 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; @@ -87,14 +89,17 @@ fragment RepoStars on User { ) { totalCount nodes { - name - stargazerCount + ...RepoNode } pageInfo { hasNextPage endCursor } } +} +fragment RepoNode on Repository { + name + stargazerCount }`); export const UserInfoDocument = graphqlDocument< @@ -150,12 +155,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 dcd6f42d0048b..8a4114fd0cb00 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 513b924ed7a51..ac968d24eebfa 100644 --- a/packages/core/tests/fetchStats.test.ts +++ b/packages/core/tests/fetchStats.test.ts @@ -76,6 +76,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: { @@ -111,7 +116,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 { @@ -131,6 +136,7 @@ beforeEach(() => { afterEach(() => { mock.reset(); + vi.unstubAllEnvs(); }); describe("Test fetchStats", () => { @@ -317,7 +323,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"); @@ -354,7 +360,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"); @@ -391,7 +397,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"); @@ -427,6 +433,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 8c9a987a68757cf25dcaead0d9241196bb4fa574 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 14 Aug 2026 04:40:03 +0200 Subject: [PATCH 7/7] 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 | 54 +++++++-------------- packages/core/src/fetchers/top-languages.ts | 21 +------- 5 files changed, 43 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 a7149376aaf4f..c0f082fa7f718 100644 --- a/packages/core/src/fetchers/stats.ts +++ b/packages/core/src/fetchers/stats.ts @@ -6,7 +6,7 @@ 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 } 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 +19,6 @@ import type { RepoNodeFragment, UserInfoQuery, UserInfoQueryVariables, - UserReposQuery, - UserReposQueryVariables, } from "../graphql/generated/stats.js"; import type { RepoUserStats, StatsData } from "./types.js"; @@ -31,29 +29,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. @@ -66,7 +44,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. @@ -88,18 +66,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.