Skip to content

Commit

Permalink
Only include used fragments in operations
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmedv committed Nov 28, 2024
1 parent ccbf0db commit ad27d75
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 26 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -166,7 +166,7 @@ import type { IssuesQuery } from './query.graphql.ts'
<details>
<summary><strong>How to get the return type of a query?</strong></summary>

Megaera generates TypeScript types for queries as functions.
Megaera generates TypeScript types for queries as functions.

```ts
type UserQuery = (vars: { login?: string }) => {
Expand Down Expand Up @@ -228,7 +228,7 @@ function query<T extends Query>(query: T, variables?: Variables<T>) {
}

// 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' })
```

</details>
Expand Down
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 3 additions & 1 deletion src/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.`)
}
Expand Down
26 changes: 23 additions & 3 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}\`
Expand All @@ -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
Expand All @@ -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 ''
Expand Down
46 changes: 29 additions & 17 deletions src/visitor.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -23,7 +31,7 @@ export type Selector = {

export type Content = {
operations: Selector[]
fragments: Selector[]
fragments: Map<string, Selector>
}

export function traverse(schema: GraphQLSchema, source: Source): Content {
Expand All @@ -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)
Expand All @@ -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),
})
}

Expand All @@ -65,15 +73,15 @@ export function traverse(schema: GraphQLSchema, source: Source): Content {
fields: [],
inlineFragments: [],
variables: variables,
source: print(node)
source: print(node),
}

stack.push(s)
content.operations.push(s)
},
leave() {
stack.pop()
}
},
},

FragmentDefinition: {
Expand All @@ -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: {
Expand All @@ -109,7 +117,7 @@ export function traverse(schema: GraphQLSchema, source: Source): Content {
},
leave() {
stack.pop()
}
},
},

FragmentSpread: {
Expand All @@ -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,
Expand All @@ -141,7 +153,7 @@ export function traverse(schema: GraphQLSchema, source: Source): Content {
leave() {
stack.pop()
},
}
},
})

visit(ast, visitor)
Expand All @@ -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.`)
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"strict": true,
"outDir": "./dist",
"declaration": true,
"allowJs": true,
"allowJs": true
},
"include": ["./src/**/*"]
}

0 comments on commit ad27d75

Please sign in to comment.