Skip to content

Commit

Permalink
feat: track unused fields (#146)
Browse files Browse the repository at this point in the history
* Track field usage and warn for unused fields

* cleanup crew

* comments

* readme

* fixies

* add tests

* add callout

* Use `node.getStart()` instead of `node.pos` as input to `getReferencesAtPosition` (#149)

* convert other one to getStart

* support all cases

* add tests

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
JoviDeCroock and Andarist authored Dec 21, 2023
1 parent d8f1ba8 commit a2180af
Show file tree
Hide file tree
Showing 32 changed files with 2,397 additions and 480 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-weeks-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@0no-co/graphqlsp': minor
---

Track field usage and warn when a field goes unused
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ when on a TypeScript file or adding a file like [this](https://github.com/0no-co
- `extraTypes` allows you to specify imports or declare types to help with `scalar` definitions
- `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find
unused fragments and provide a message notifying you about them
- `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about
unused fields within the same file.
### GraphQL Code Generator client-preset
Expand All @@ -82,13 +84,36 @@ For folks using the `client-preset` you can ues the following config
"schema": "./schema.graphql",
"disableTypegen": true,
"templateIsCallExpression": true,
"trackFieldUsage": true,
"template": "graphql"
}
]
}
}
```
## Tracking unused fields

Currently the tracking unused fields feature has a few caveats with regards to tracking, first and foremost
it will only track in the same file to encourage [fragment co-location](https://www.apollographql.com/docs/react/data/fragments/#colocating-fragments).
Secondly it supports a few patterns which we'll add to as time progresses:

```ts
// Supported cases:
const result = (await client.query()) || useFragment();
const [result] = useQuery(); // --> urql
const { data } = useQuery(); // --> Apollo
// Missing cases:
const { field } = useFragment(); // can't destructure into your data from the start
const [{ data }] = useQuery(); // can't follow array destructuring with object destructuring
const {
data: { pokemon },
} = useQuery(); // can't destructure into your data from the start
```

Lastly we don't track mutations/subscriptions as some folks will add additional fields to properly support
normalised cache updates.

## Fragment masking

When we use a `useQuery` that supports `TypedDocumentNode` it will automatically pick up the typings
Expand Down
4 changes: 3 additions & 1 deletion packages/example-external-generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
"@urql/core": "^3.0.0",
"graphql": "^16.8.1"
"graphql": "^16.8.1",
"urql": "^4.0.6"
},
"devDependencies": {
"@0no-co/graphqlsp": "file:../graphqlsp",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/client-preset": "^4.1.0",
"@types/react": "^18.2.45",
"ts-node": "^10.9.1",
"typescript": "^5.3.3"
}
Expand Down
6 changes: 0 additions & 6 deletions packages/example-external-generator/src/Pokemon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ export const PokemonFields = graphql(`
}
`)

export const WeakFields = graphql(`
fragment weaknessFields on Pokemon {
weaknesses
}
`)

export const Pokemon = (data: any) => {
const pokemon = useFragment(PokemonFields, data);
return `hi ${pokemon.name}`;
Expand Down
30 changes: 3 additions & 27 deletions packages/example-external-generator/src/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,8 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
const documents = {
'\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n':
types.PokemonFieldsFragmentDoc,
'\n fragment weaknessFields on Pokemon {\n weaknesses\n }\n':
types.WeaknessFieldsFragmentDoc,
'\n query Pok($limit: Int!) {\n pokemons(limit: $limit) {\n id\n name\n fleeRate\n classification\n ...pokemonFields\n ...weaknessFields\n __typename\n }\n }\n':
types.PokDocument,
'\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n __typename\n }\n }\n':
'\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n':
types.PoDocument,
'\n query PokemonsAreAwesome {\n pokemons {\n id\n }\n }\n':
types.PokemonsAreAwesomeDocument,
};

/**
Expand All @@ -49,26 +43,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n fragment weaknessFields on Pokemon {\n weaknesses\n }\n'
): (typeof documents)['\n fragment weaknessFields on Pokemon {\n weaknesses\n }\n'];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n query Pok($limit: Int!) {\n pokemons(limit: $limit) {\n id\n name\n fleeRate\n classification\n ...pokemonFields\n ...weaknessFields\n __typename\n }\n }\n'
): (typeof documents)['\n query Pok($limit: Int!) {\n pokemons(limit: $limit) {\n id\n name\n fleeRate\n classification\n ...pokemonFields\n ...weaknessFields\n __typename\n }\n }\n'];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n __typename\n }\n }\n'
): (typeof documents)['\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n __typename\n }\n }\n'];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n query PokemonsAreAwesome {\n pokemons {\n id\n }\n }\n'
): (typeof documents)['\n query PokemonsAreAwesome {\n pokemons {\n id\n }\n }\n'];
source: '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n'
): (typeof documents)['\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n'];

export function graphql(source: string) {
return (documents as any)[source] ?? {};
Expand Down
Loading

0 comments on commit a2180af

Please sign in to comment.