diff --git a/README.md b/README.md index 45ef104..3d3be1e 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ import it in your TypeScript code. ```ts import { IssuesQuery } from './query.graphql.ts' -```` +``` The `IssuesQuery` variable is a string with the GraphQL query. You can use it directly in your code, or pass it to a function that accepts a query. @@ -166,7 +166,7 @@ import type { IssuesQuery } from './query.graphql.ts'
How to get the return type of a query? -Megaera generates TypeScript types for queries as functions. +Megaera generates TypeScript types for queries as functions. ```ts type UserQuery = (vars: { login?: string }) => { @@ -228,7 +228,7 @@ function query(query: T, variables?: Variables) { } // Return type, and types of variables are inferred from the query. -const {issues} = await query(IssuesQuery, {login: 'webpod'}) +const { issues } = await query(IssuesQuery, { login: 'webpod' }) ```
diff --git a/src/cli.ts b/src/cli.ts index 68451e6..be7cd94 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -99,7 +99,7 @@ void (async function main() { '%d operation', '%d operations', ) - const frg = plural(content.fragments.length, '%d fragment', '%d fragments') + const frg = plural(content.fragments.size, '%d fragment', '%d fragments') console.log(`> ${styleText('green', 'done')} (${ops}, ${frg})`) const prefix = `// DO NOT EDIT. This is a generated file. Instead of this file, edit "${fileName}".\n\n` diff --git a/src/generate.test.ts b/src/generate.test.ts index 0030dee..e96ef2d 100644 --- a/src/generate.test.ts +++ b/src/generate.test.ts @@ -29,7 +29,9 @@ function getFieldType(name: string, fieldName: string) { if (!isObjectType(type)) { throw new Error(`${name} is not object type.`) } - const field = type.astNode?.fields?.find((f) => f.name.value === fieldName)?.type + const field = type.astNode?.fields?.find( + (f) => f.name.value === fieldName, + )?.type if (!field) { throw new Error(`Cannot find ${fieldName} field in ${name}.`) } diff --git a/src/generate.ts b/src/generate.ts index ccffdfe..2e393fa 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -11,7 +11,7 @@ import { isObjectType } from 'graphql/type/index.js' export function generate(content: Content) { const code: string[] = [] - for (const f of content.fragments) { + for (const f of content.fragments.values()) { code.push(`const ${f.name} = \`#graphql ${f.source}\` @@ -25,8 +25,9 @@ export type ${f.name} = ${generateSelector(f, 0, true)} } let querySource = q.source - for (const f of content.fragments) { - querySource = '${' + f.name + '}\n' + querySource + + for (const fName of usedFragments(q, content)) { + querySource = '${' + fName + '}\n' + querySource } code.push(`export const ${q.name} = \`#graphql @@ -39,6 +40,25 @@ export type ${q.name} = (${generateVariables(q.variables)}) => ${generateSelecto return code.join('\n') } +function usedFragments(q: Selector, content: Content): string[] { + const fragments: string[] = [] + for (const field of q.fields) { + if (field.isFragment) { + fragments.push(field.name) + const fragment = content.fragments.get(field.name) + if (!fragment) { + throw new Error(`Fragment ${field.name} is not defined.`) + } + fragments.push(...usedFragments(fragment, content)) + } + fragments.push(...usedFragments(field, content)) + } + for (const inlineFragment of q.inlineFragments) { + fragments.push(...usedFragments(inlineFragment, content)) + } + return fragments +} + function generateVariables(variables?: Variable[]) { if (!variables || variables.length === 0) { return '' diff --git a/src/visitor.ts b/src/visitor.ts index 298ed17..78ccb15 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -1,5 +1,13 @@ -import { GraphQLNonNull, GraphQLOutputType, GraphQLType, isNonNullType } from 'graphql/type/definition.js' -import { typeFromAST, TypeInfo, visitWithTypeInfo } from 'graphql/utilities/index.js' +import { + GraphQLOutputType, + GraphQLType, + isNonNullType, +} from 'graphql/type/definition.js' +import { + typeFromAST, + TypeInfo, + visitWithTypeInfo, +} from 'graphql/utilities/index.js' import { parse, print, Source, visit } from 'graphql/language/index.js' import { GraphQLSchema } from 'graphql/type/index.js' import { GraphQLError } from 'graphql/error/index.js' @@ -23,7 +31,7 @@ export type Selector = { export type Content = { operations: Selector[] - fragments: Selector[] + fragments: Map } export function traverse(schema: GraphQLSchema, source: Source): Content { @@ -32,19 +40,19 @@ export function traverse(schema: GraphQLSchema, source: Source): Content { const content: Content = { operations: [], - fragments: [] + fragments: new Map(), } const stack: Selector[] = [] const visitor = visitWithTypeInfo(typeInfo, { OperationDefinition: { - enter: function(node) { + enter: function (node) { if (node.name === undefined) { throw new GraphQLError( firstLetterUpper(node.operation) + ' name is required', node, - source + source, ) } checkUnique(node.name.value, content) @@ -55,7 +63,7 @@ export function traverse(schema: GraphQLSchema, source: Source): Content { variables.push({ name: v.variable.name.value, type: type, - required: v.defaultValue === undefined && isNonNullType(type) + required: v.defaultValue === undefined && isNonNullType(type), }) } @@ -65,7 +73,7 @@ export function traverse(schema: GraphQLSchema, source: Source): Content { fields: [], inlineFragments: [], variables: variables, - source: print(node) + source: print(node), } stack.push(s) @@ -73,7 +81,7 @@ export function traverse(schema: GraphQLSchema, source: Source): Content { }, leave() { stack.pop() - } + }, }, FragmentDefinition: { @@ -85,15 +93,15 @@ export function traverse(schema: GraphQLSchema, source: Source): Content { type: typeInfo.getType() ?? undefined, fields: [], inlineFragments: [], - source: print(node) + source: print(node), } stack.push(s) - content.fragments.push(s) + content.fragments.set(s.name, s) }, leave() { stack.pop() - } + }, }, Field: { @@ -109,7 +117,7 @@ export function traverse(schema: GraphQLSchema, source: Source): Content { }, leave() { stack.pop() - } + }, }, FragmentSpread: { @@ -121,13 +129,17 @@ export function traverse(schema: GraphQLSchema, source: Source): Content { fields: [], inlineFragments: [], }) - } + }, }, InlineFragment: { enter(node) { if (!node.typeCondition) { - throw new GraphQLError('Inline fragment must have type condition.', node, source) + throw new GraphQLError( + 'Inline fragment must have type condition.', + node, + source, + ) } const s: Selector = { name: node.typeCondition.name.value, @@ -141,7 +153,7 @@ export function traverse(schema: GraphQLSchema, source: Source): Content { leave() { stack.pop() }, - } + }, }) visit(ast, visitor) @@ -153,7 +165,7 @@ function checkUnique(name: string, content: Content) { if (content.operations.find((o) => o.name === name)) { throw new GraphQLError(`Operation with name "${name}" is already defined.`) } - if (content.fragments.find((f) => f.name === name)) { + if (content.fragments.has(name)) { throw new GraphQLError(`Fragment with name "${name}" is already defined.`) } } diff --git a/tsconfig.json b/tsconfig.json index 44a5d3b..78b4ecf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "strict": true, "outDir": "./dist", "declaration": true, - "allowJs": true, + "allowJs": true }, "include": ["./src/**/*"] }