Mirror: TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

look for call-expressions with gql and graphql by default (#162)

* look for call-expressions with gql and graphql by default

* fix tsconfigs

* update changeset

authored by

Jovi De Croock and committed by
GitHub
8bffbe9a 19b20fea

+66 -67
+19
.changeset/few-flowers-buy.md
··· 1 + --- 2 + '@0no-co/graphqlsp': major 3 + --- 4 + 5 + Look for `gql` and `graphql` by default as well as change the default for call-expressions to true. 6 + 7 + If you are using TaggedTemplateExpressions you can migrate by adding the following to your tsconfig file 8 + 9 + ```json 10 + { 11 + "plugins": [ 12 + { 13 + "name": "@0no-co/graphqlsp", 14 + "schema": "...", 15 + "templateIsCallExpression": false 16 + } 17 + ] 18 + } 19 + ```
+2 -4
README.md
··· 60 60 61 61 **Optional** 62 62 63 - - `template` the shape of your template, by default `gql` 63 + - `template` the shape of your template, by default `gql` and `graphql` are respected 64 64 - `templateIsCallExpression` this tells our client that you are using `graphql('doc')` 65 65 - `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find 66 66 unused fragments and provide a message notifying you about them ··· 79 79 "name": "@0no-co/graphqlsp", 80 80 "schema": "./schema.graphql", 81 81 "disableTypegen": true, 82 - "templateIsCallExpression": true, 83 82 "shouldCheckForColocatedFragments": true, 84 - "trackFieldUsage": true, 85 - "template": "graphql" 83 + "trackFieldUsage": true 86 84 } 87 85 ] 88 86 }
-2
packages/example-external-generator/tsconfig.json
··· 6 6 "schema": "./schema.graphql", 7 7 "disableTypegen": true, 8 8 "shouldCheckForColocatedFragments": true, 9 - "template": "graphql", 10 - "templateIsCallExpression": true, 11 9 "trackFieldUsage": true 12 10 } 13 11 ],
-1
packages/example-tada/src/index.tsx
··· 38 38 console.log(fleeRate) 39 39 // Works 40 40 const po = result.data?.pokemon; 41 - // @ts-expect-error 42 41 const { pokemon: { weight: { minimum } } } = result.data || {}; 43 42 console.log(po?.name, minimum) 44 43
-2
packages/example-tada/tsconfig.json
··· 6 6 "schema": "./schema.graphql", 7 7 "disableTypegen": true, 8 8 "shouldCheckForColocatedFragments": true, 9 - "template": "graphql", 10 - "templateIsCallExpression": true, 11 9 "trackFieldUsage": true 12 10 } 13 11 ],
+2 -1
packages/example/tsconfig.json
··· 3 3 "plugins": [ 4 4 { 5 5 "name": "@0no-co/graphqlsp", 6 - "schema": "./schema.graphql" 6 + "schema": "./schema.graphql", 7 + "templateIsCallExpression": false 7 8 } 8 9 ], 9 10 /* Language and Environment */
+9 -13
packages/graphqlsp/src/ast/index.ts
··· 1 1 import ts from 'typescript/lib/tsserverlibrary'; 2 2 import { FragmentDefinitionNode, parse } from 'graphql'; 3 - import { Logger } from '..'; 3 + import { templates } from './templates'; 4 4 5 5 export function getSource(info: ts.server.PluginCreateInfo, filename: string) { 6 6 const program = info.languageService.getProgram(); ··· 25 25 } 26 26 27 27 export function findAllTaggedTemplateNodes( 28 - sourceFile: ts.SourceFile | ts.Node, 29 - template: string 28 + sourceFile: ts.SourceFile | ts.Node 30 29 ): Array<ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral> { 31 30 const result: Array< 32 31 ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral ··· 34 33 function find(node: ts.Node) { 35 34 if ( 36 35 (ts.isTaggedTemplateExpression(node) && 37 - node.tag.getText() === template) || 36 + templates.has(node.tag.getText())) || 38 37 (ts.isNoSubstitutionTemplateLiteral(node) && 39 38 ts.isTaggedTemplateExpression(node.parent) && 40 - node.parent.tag.getText() === template) 39 + templates.has(node.parent.tag.getText())) 41 40 ) { 42 41 result.push(node); 43 42 return; ··· 51 50 52 51 function unrollFragment( 53 52 element: ts.Identifier, 54 - template: string, 55 53 info: ts.server.PluginCreateInfo 56 54 ): Array<FragmentDefinitionNode> { 57 55 const fragments: Array<FragmentDefinitionNode> = []; ··· 78 76 found = found.parent.initializer; 79 77 } 80 78 81 - if (ts.isCallExpression(found) && found.expression.getText() === template) { 79 + if (ts.isCallExpression(found) && templates.has(found.expression.getText())) { 82 80 const [arg, arg2] = found.arguments; 83 81 if (arg2 && ts.isArrayLiteralExpression(arg2)) { 84 82 arg2.elements.forEach(element => { 85 83 if (ts.isIdentifier(element)) { 86 - fragments.push(...unrollFragment(element, template, info)); 84 + fragments.push(...unrollFragment(element, info)); 87 85 } 88 86 }); 89 87 } ··· 103 101 104 102 export function findAllCallExpressions( 105 103 sourceFile: ts.SourceFile, 106 - template: string, 107 104 info: ts.server.PluginCreateInfo, 108 105 shouldSearchFragments: boolean = true 109 106 ): { ··· 114 111 let fragments: Array<FragmentDefinitionNode> = []; 115 112 let hasTriedToFindFragments = shouldSearchFragments ? false : true; 116 113 function find(node: ts.Node) { 117 - if (ts.isCallExpression(node) && node.expression.getText() === template) { 114 + if (ts.isCallExpression(node) && templates.has(node.expression.getText())) { 118 115 const [arg, arg2] = node.arguments; 119 116 120 117 if (!hasTriedToFindFragments && !arg2) { ··· 123 120 } else if (arg2 && ts.isArrayLiteralExpression(arg2)) { 124 121 arg2.elements.forEach(element => { 125 122 if (ts.isIdentifier(element)) { 126 - fragments.push(...unrollFragment(element, template, info)); 123 + fragments.push(...unrollFragment(element, info)); 127 124 } 128 125 }); 129 126 } ··· 145 142 node: ts.CallExpression, 146 143 info: ts.server.PluginCreateInfo 147 144 ) { 148 - const template = info.config.template || 'gql'; 149 145 let fragments: Array<FragmentDefinitionNode> = []; 150 146 151 147 const definitions = info.languageService.getDefinitionAtPosition( ··· 158 154 const arg2 = node.arguments[1] as ts.ArrayLiteralExpression; 159 155 arg2.elements.forEach(element => { 160 156 if (ts.isIdentifier(element)) { 161 - fragments.push(...unrollFragment(element, template, info)); 157 + fragments.push(...unrollFragment(element, info)); 162 158 } 163 159 }); 164 160 return fragments;
+1
packages/graphqlsp/src/ast/templates.ts
··· 1 + export const templates = new Set(['gql', 'graphql']);
+4 -4
packages/graphqlsp/src/autoComplete.ts
··· 30 30 import { resolveTemplate } from './ast/resolve'; 31 31 import { getToken } from './ast/token'; 32 32 import { getSuggestionsForFragmentSpread } from './graphql/getFragmentSpreadSuggestions'; 33 + import { templates } from './ast/templates'; 33 34 34 35 export function getGraphQLCompletions( 35 36 filename: string, ··· 37 38 schema: { current: GraphQLSchema | null }, 38 39 info: ts.server.PluginCreateInfo 39 40 ): ts.WithMetadata<ts.CompletionInfo> | undefined { 40 - const tagTemplate = info.config.template || 'gql'; 41 - const isCallExpression = info.config.templateIsCallExpression ?? false; 41 + const isCallExpression = info.config.templateIsCallExpression ?? true; 42 42 43 43 const source = getSource(info, filename); 44 44 if (!source) return undefined; ··· 54 54 if ( 55 55 ts.isCallExpression(node) && 56 56 isCallExpression && 57 - node.expression.getText() === tagTemplate && 57 + templates.has(node.expression.getText()) && 58 58 node.arguments.length > 0 && 59 59 ts.isNoSubstitutionTemplateLiteral(node.arguments[0]) 60 60 ) { ··· 69 69 } else if (ts.isTaggedTemplateExpression(node)) { 70 70 const { template, tag } = node; 71 71 72 - if (!ts.isIdentifier(tag) || tag.text !== tagTemplate) return undefined; 72 + if (!ts.isIdentifier(tag) || !templates.has(tag.text)) return undefined; 73 73 74 74 const foundToken = getToken(template, cursorPosition); 75 75 if (!foundToken || !schema.current) return undefined;
+2 -4
packages/graphqlsp/src/checkImports.ts
··· 16 16 info: ts.server.PluginCreateInfo 17 17 ) => { 18 18 const imports = findAllImports(source); 19 - const tagTemplate = info.config.template || 'gql'; 20 19 21 20 const shouldCheckForColocatedFragments = 22 21 info.config.shouldCheckForColocatedFragments ?? false; ··· 67 66 68 67 if (!declaration) return; 69 68 70 - const [template] = findAllTaggedTemplateNodes(declaration, tagTemplate); 69 + const [template] = findAllTaggedTemplateNodes(declaration); 71 70 if (template) { 72 71 let node = template; 73 72 if ( ··· 259 258 info: ts.server.PluginCreateInfo 260 259 ): Array<FragmentDefinitionNode> { 261 260 let fragments: Array<FragmentDefinitionNode> = []; 262 - const tagTemplate = info.config.template || 'gql'; 263 - const callExpressions = findAllCallExpressions(src, tagTemplate, info, false); 261 + const callExpressions = findAllCallExpressions(src, info, false); 264 262 265 263 callExpressions.nodes.forEach(node => { 266 264 const text = resolveTemplate(node, src.fileName, info).combinedText;
+13 -11
packages/graphqlsp/src/diagnostics.ts
··· 1 - import ts from 'typescript/lib/tsserverlibrary'; 1 + import ts, { TaggedTemplateExpression } from 'typescript/lib/tsserverlibrary'; 2 2 import { Diagnostic, getDiagnostics } from 'graphql-language-service'; 3 3 import { 4 4 FragmentDefinitionNode, ··· 57 57 schema: { current: GraphQLSchema | null; version: number }, 58 58 info: ts.server.PluginCreateInfo 59 59 ): ts.Diagnostic[] | undefined { 60 - const tagTemplate = info.config.template || 'gql'; 61 - const isCallExpression = info.config.templateIsCallExpression ?? false; 60 + const isCallExpression = info.config.templateIsCallExpression ?? true; 62 61 63 62 let source = getSource(info, filename); 64 63 if (!source) return undefined; ··· 66 65 let fragments: Array<FragmentDefinitionNode> = [], 67 66 nodes: (ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral)[]; 68 67 if (isCallExpression) { 69 - const result = findAllCallExpressions(source, tagTemplate, info); 68 + const result = findAllCallExpressions(source, info); 70 69 fragments = result.fragments; 71 70 nodes = result.nodes; 72 71 } else { 73 - nodes = findAllTaggedTemplateNodes(source, tagTemplate); 72 + nodes = findAllTaggedTemplateNodes(source); 74 73 } 75 74 76 75 const texts = nodes.map(node => { ··· 123 122 schema: { current: GraphQLSchema | null; version: number }, 124 123 info: ts.server.PluginCreateInfo 125 124 ) => { 126 - const tagTemplate = info.config.template || 'gql'; 127 125 const filename = source.fileName; 128 - const isCallExpression = info.config.templateIsCallExpression ?? false; 126 + const isCallExpression = info.config.templateIsCallExpression ?? true; 129 127 130 128 const diagnostics = nodes 131 129 .map(originalNode => { ··· 159 157 } 160 158 // When we are dealing with a plain gql statement we have to add two these can be recognised 161 159 // by the fact that the parent is an expressionStatement 160 + 162 161 let startingPosition = 163 162 node.pos + 164 - (isCallExpression ? 0 : tagTemplate.length + (isExpression ? 2 : 1)); 163 + (isCallExpression 164 + ? 0 165 + : (node as TaggedTemplateExpression).tag.getText().length + 166 + (isExpression ? 2 : 1)); 165 167 const endPosition = startingPosition + node.getText().length; 166 168 167 169 let docFragments = [...fragments]; ··· 336 338 start, 337 339 length, 338 340 } = moduleSpecifierToFragments[moduleSpecifier]; 339 - const missingFragments = Array.from(new Set(fragmentNames.filter( 340 - x => !usedFragments.has(x) 341 - ))); 341 + const missingFragments = Array.from( 342 + new Set(fragmentNames.filter(x => !usedFragments.has(x))) 343 + ); 342 344 if (missingFragments.length) { 343 345 fragmentDiagnostics.push({ 344 346 file: source,
+6 -14
packages/graphqlsp/src/index.ts
··· 4 4 import { getGraphQLCompletions } from './autoComplete'; 5 5 import { getGraphQLQuickInfo } from './quickInfo'; 6 6 import { getGraphQLDiagnostics } from './diagnostics'; 7 + import { templates } from './ast/templates'; 7 8 8 9 function createBasicDecorator(info: ts.server.PluginCreateInfo) { 9 10 const proxy: ts.LanguageService = Object.create(null); ··· 22 23 23 24 type Config = { 24 25 schema: SchemaOrigin | string; 25 - // TODO: rename to tag or just remove entirely and always check for 26 - // gql and graphql. 27 - template?: string; 28 - // TODO: we need a bettername, gql.tada will also have 29 - // call expressions, we can differentiate by means of 30 - // tada having a second argument containing the fragments. 31 26 templateIsCallExpression?: boolean; 32 - // Up in the air whether we want to keep supporting 33 - // this. Current limitation are barrel-file exports 34 - // could be counter-acted with an opinion on 35 - // fragment-naming. Could become more relevant 36 - // with gql.tada and could be useful for 37 - // client-preset as well however the component type-annotations 38 - // can better indicate a missing spread for the 39 - // client-preset. 40 27 shouldCheckForColocatedFragments?: boolean; 28 + template?: string; 29 + trackFieldUsage?: boolean; 41 30 }; 42 31 43 32 function create(info: ts.server.PluginCreateInfo) { ··· 53 42 54 43 logger('Setting up the GraphQL Plugin'); 55 44 45 + if (config.template) { 46 + templates.add(config.template); 47 + } 56 48 const proxy = createBasicDecorator(info); 57 49 58 50 const schema = loadSchema(
+4 -4
packages/graphqlsp/src/quickInfo.ts
··· 11 11 import { resolveTemplate } from './ast/resolve'; 12 12 import { getToken } from './ast/token'; 13 13 import { Cursor } from './ast/cursor'; 14 + import { templates } from './ast/templates'; 14 15 15 16 export function getGraphQLQuickInfo( 16 17 filename: string, ··· 18 19 schema: { current: GraphQLSchema | null }, 19 20 info: ts.server.PluginCreateInfo 20 21 ): ts.QuickInfo | undefined { 21 - const tagTemplate = info.config.template || 'gql'; 22 - const isCallExpression = info.config.templateIsCallExpression ?? false; 22 + const isCallExpression = info.config.templateIsCallExpression ?? true; 23 23 24 24 const source = getSource(info, filename); 25 25 if (!source) return undefined; ··· 35 35 if ( 36 36 ts.isCallExpression(node) && 37 37 isCallExpression && 38 - node.expression.getText() === tagTemplate && 38 + templates.has(node.expression.getText()) && 39 39 node.arguments.length > 0 && 40 40 ts.isNoSubstitutionTemplateLiteral(node.arguments[0]) 41 41 ) { ··· 46 46 cursor = new Cursor(foundToken.line, foundToken.start - 1); 47 47 } else if (ts.isTaggedTemplateExpression(node)) { 48 48 const { template, tag } = node; 49 - if (!ts.isIdentifier(tag) || tag.text !== tagTemplate) return undefined; 49 + if (!ts.isIdentifier(tag) || !templates.has(tag.text)) return undefined; 50 50 51 51 const foundToken = getToken(template, cursorPosition); 52 52
+1 -3
test/e2e/fixture-project-client-preset/tsconfig.json
··· 5 5 "name": "@0no-co/graphqlsp", 6 6 "schema": "./schema.graphql", 7 7 "disableTypegen": true, 8 - "shouldCheckForColocatedFragments": true, 9 - "template": "graphql", 10 - "templateIsCallExpression": true 8 + "shouldCheckForColocatedFragments": true 11 9 } 12 10 ], 13 11 "target": "es2016",
+1 -3
test/e2e/fixture-project-unused-fields/tsconfig.json
··· 6 6 "schema": "./schema.graphql", 7 7 "disableTypegen": true, 8 8 "trackFieldUsage": true, 9 - "shouldCheckForColocatedFragments": false, 10 - "template": "graphql", 11 - "templateIsCallExpression": true 9 + "shouldCheckForColocatedFragments": false 12 10 } 13 11 ], 14 12 "target": "es2016",
+2 -1
test/e2e/fixture-project/tsconfig.json
··· 4 4 { 5 5 "name": "@0no-co/graphqlsp", 6 6 "schema": "./schema.graphql", 7 - "shouldCheckForColocatedFragments": true 7 + "shouldCheckForColocatedFragments": true, 8 + "templateIsCallExpression": false 8 9 } 9 10 ], 10 11 "target": "es2016",