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.

feat: make tada work with the LSP (#160)

* make tada work

* minor ifxes

* Create many-pandas-relax.md

* minor ifxes

* Update diagnostics.ts

authored by

Jovi De Croock and committed by
GitHub
ea657722 2f6d243a

+824 -60
+5
.changeset/many-pandas-relax.md
··· 1 + --- 2 + "@0no-co/graphqlsp": minor 3 + --- 4 + 5 + Make the LSP work with [`gql.tada`](https://github.com/0no-co/gql.tada)
+3
packages/example-tada/.vscode/settings.json
··· 1 + { 2 + "typescript.tsdk": "node_modules/typescript/lib" 3 + }
+418
packages/example-tada/introspection.ts
··· 1 + export const introspection = { 2 + __schema: { 3 + queryType: { 4 + name: 'Query', 5 + }, 6 + mutationType: null, 7 + subscriptionType: null, 8 + types: [ 9 + { 10 + kind: 'OBJECT', 11 + name: 'Attack', 12 + fields: [ 13 + { 14 + name: 'damage', 15 + type: { 16 + kind: 'SCALAR', 17 + name: 'Int', 18 + ofType: null, 19 + }, 20 + args: [], 21 + }, 22 + { 23 + name: 'name', 24 + type: { 25 + kind: 'SCALAR', 26 + name: 'String', 27 + ofType: null, 28 + }, 29 + args: [], 30 + }, 31 + { 32 + name: 'type', 33 + type: { 34 + kind: 'ENUM', 35 + name: 'PokemonType', 36 + ofType: null, 37 + }, 38 + args: [], 39 + }, 40 + ], 41 + interfaces: [], 42 + }, 43 + { 44 + kind: 'SCALAR', 45 + name: 'Int', 46 + }, 47 + { 48 + kind: 'SCALAR', 49 + name: 'String', 50 + }, 51 + { 52 + kind: 'OBJECT', 53 + name: 'AttacksConnection', 54 + fields: [ 55 + { 56 + name: 'fast', 57 + type: { 58 + kind: 'LIST', 59 + ofType: { 60 + kind: 'OBJECT', 61 + name: 'Attack', 62 + ofType: null, 63 + }, 64 + }, 65 + args: [], 66 + }, 67 + { 68 + name: 'special', 69 + type: { 70 + kind: 'LIST', 71 + ofType: { 72 + kind: 'OBJECT', 73 + name: 'Attack', 74 + ofType: null, 75 + }, 76 + }, 77 + args: [], 78 + }, 79 + ], 80 + interfaces: [], 81 + }, 82 + { 83 + kind: 'OBJECT', 84 + name: 'EvolutionRequirement', 85 + fields: [ 86 + { 87 + name: 'amount', 88 + type: { 89 + kind: 'SCALAR', 90 + name: 'Int', 91 + ofType: null, 92 + }, 93 + args: [], 94 + }, 95 + { 96 + name: 'name', 97 + type: { 98 + kind: 'SCALAR', 99 + name: 'String', 100 + ofType: null, 101 + }, 102 + args: [], 103 + }, 104 + ], 105 + interfaces: [], 106 + }, 107 + { 108 + kind: 'OBJECT', 109 + name: 'Pokemon', 110 + fields: [ 111 + { 112 + name: 'attacks', 113 + type: { 114 + kind: 'OBJECT', 115 + name: 'AttacksConnection', 116 + ofType: null, 117 + }, 118 + args: [], 119 + }, 120 + { 121 + name: 'classification', 122 + type: { 123 + kind: 'SCALAR', 124 + name: 'String', 125 + ofType: null, 126 + }, 127 + args: [], 128 + }, 129 + { 130 + name: 'evolutionRequirements', 131 + type: { 132 + kind: 'LIST', 133 + ofType: { 134 + kind: 'OBJECT', 135 + name: 'EvolutionRequirement', 136 + ofType: null, 137 + }, 138 + }, 139 + args: [], 140 + }, 141 + { 142 + name: 'evolutions', 143 + type: { 144 + kind: 'LIST', 145 + ofType: { 146 + kind: 'OBJECT', 147 + name: 'Pokemon', 148 + ofType: null, 149 + }, 150 + }, 151 + args: [], 152 + }, 153 + { 154 + name: 'fleeRate', 155 + type: { 156 + kind: 'SCALAR', 157 + name: 'Float', 158 + ofType: null, 159 + }, 160 + args: [], 161 + }, 162 + { 163 + name: 'height', 164 + type: { 165 + kind: 'OBJECT', 166 + name: 'PokemonDimension', 167 + ofType: null, 168 + }, 169 + args: [], 170 + }, 171 + { 172 + name: 'id', 173 + type: { 174 + kind: 'NON_NULL', 175 + ofType: { 176 + kind: 'SCALAR', 177 + name: 'ID', 178 + ofType: null, 179 + }, 180 + }, 181 + args: [], 182 + }, 183 + { 184 + name: 'maxCP', 185 + type: { 186 + kind: 'SCALAR', 187 + name: 'Int', 188 + ofType: null, 189 + }, 190 + args: [], 191 + }, 192 + { 193 + name: 'maxHP', 194 + type: { 195 + kind: 'SCALAR', 196 + name: 'Int', 197 + ofType: null, 198 + }, 199 + args: [], 200 + }, 201 + { 202 + name: 'name', 203 + type: { 204 + kind: 'NON_NULL', 205 + ofType: { 206 + kind: 'SCALAR', 207 + name: 'String', 208 + ofType: null, 209 + }, 210 + }, 211 + args: [], 212 + }, 213 + { 214 + name: 'resistant', 215 + type: { 216 + kind: 'LIST', 217 + ofType: { 218 + kind: 'ENUM', 219 + name: 'PokemonType', 220 + ofType: null, 221 + }, 222 + }, 223 + args: [], 224 + }, 225 + { 226 + name: 'types', 227 + type: { 228 + kind: 'LIST', 229 + ofType: { 230 + kind: 'ENUM', 231 + name: 'PokemonType', 232 + ofType: null, 233 + }, 234 + }, 235 + args: [], 236 + }, 237 + { 238 + name: 'weaknesses', 239 + type: { 240 + kind: 'LIST', 241 + ofType: { 242 + kind: 'ENUM', 243 + name: 'PokemonType', 244 + ofType: null, 245 + }, 246 + }, 247 + args: [], 248 + }, 249 + { 250 + name: 'weight', 251 + type: { 252 + kind: 'OBJECT', 253 + name: 'PokemonDimension', 254 + ofType: null, 255 + }, 256 + args: [], 257 + }, 258 + ], 259 + interfaces: [], 260 + }, 261 + { 262 + kind: 'SCALAR', 263 + name: 'Float', 264 + }, 265 + { 266 + kind: 'SCALAR', 267 + name: 'ID', 268 + }, 269 + { 270 + kind: 'OBJECT', 271 + name: 'PokemonDimension', 272 + fields: [ 273 + { 274 + name: 'maximum', 275 + type: { 276 + kind: 'SCALAR', 277 + name: 'String', 278 + ofType: null, 279 + }, 280 + args: [], 281 + }, 282 + { 283 + name: 'minimum', 284 + type: { 285 + kind: 'SCALAR', 286 + name: 'String', 287 + ofType: null, 288 + }, 289 + args: [], 290 + }, 291 + ], 292 + interfaces: [], 293 + }, 294 + { 295 + kind: 'ENUM', 296 + name: 'PokemonType', 297 + enumValues: [ 298 + { 299 + name: 'Bug', 300 + }, 301 + { 302 + name: 'Dark', 303 + }, 304 + { 305 + name: 'Dragon', 306 + }, 307 + { 308 + name: 'Electric', 309 + }, 310 + { 311 + name: 'Fairy', 312 + }, 313 + { 314 + name: 'Fighting', 315 + }, 316 + { 317 + name: 'Fire', 318 + }, 319 + { 320 + name: 'Flying', 321 + }, 322 + { 323 + name: 'Ghost', 324 + }, 325 + { 326 + name: 'Grass', 327 + }, 328 + { 329 + name: 'Ground', 330 + }, 331 + { 332 + name: 'Ice', 333 + }, 334 + { 335 + name: 'Normal', 336 + }, 337 + { 338 + name: 'Poison', 339 + }, 340 + { 341 + name: 'Psychic', 342 + }, 343 + { 344 + name: 'Rock', 345 + }, 346 + { 347 + name: 'Steel', 348 + }, 349 + { 350 + name: 'Water', 351 + }, 352 + ], 353 + }, 354 + { 355 + kind: 'OBJECT', 356 + name: 'Query', 357 + fields: [ 358 + { 359 + name: 'pokemon', 360 + type: { 361 + kind: 'OBJECT', 362 + name: 'Pokemon', 363 + ofType: null, 364 + }, 365 + args: [ 366 + { 367 + name: 'id', 368 + type: { 369 + kind: 'NON_NULL', 370 + ofType: { 371 + kind: 'SCALAR', 372 + name: 'ID', 373 + ofType: null, 374 + }, 375 + }, 376 + }, 377 + ], 378 + }, 379 + { 380 + name: 'pokemons', 381 + type: { 382 + kind: 'LIST', 383 + ofType: { 384 + kind: 'OBJECT', 385 + name: 'Pokemon', 386 + ofType: null, 387 + }, 388 + }, 389 + args: [ 390 + { 391 + name: 'limit', 392 + type: { 393 + kind: 'SCALAR', 394 + name: 'Int', 395 + ofType: null, 396 + }, 397 + }, 398 + { 399 + name: 'skip', 400 + type: { 401 + kind: 'SCALAR', 402 + name: 'Int', 403 + ofType: null, 404 + }, 405 + }, 406 + ], 407 + }, 408 + ], 409 + interfaces: [], 410 + }, 411 + { 412 + kind: 'SCALAR', 413 + name: 'Boolean', 414 + }, 415 + ], 416 + directives: [], 417 + }, 418 + } as const;
+27
packages/example-tada/package.json
··· 1 + { 2 + "name": "example", 3 + "private": true, 4 + "version": "1.0.0", 5 + "description": "", 6 + "main": "index.js", 7 + "scripts": { 8 + "test": "echo \"Error: no test specified\" && exit 1" 9 + }, 10 + "author": "", 11 + "license": "ISC", 12 + "dependencies": { 13 + "@graphql-typed-document-node/core": "^3.2.0", 14 + "gql.tada": "*", 15 + "@urql/core": "^3.0.0", 16 + "graphql": "^16.8.1", 17 + "urql": "^4.0.6" 18 + }, 19 + "devDependencies": { 20 + "@0no-co/graphqlsp": "file:../graphqlsp", 21 + "@graphql-codegen/cli": "^5.0.0", 22 + "@graphql-codegen/client-preset": "^4.1.0", 23 + "@types/react": "^18.2.45", 24 + "ts-node": "^10.9.1", 25 + "typescript": "^5.3.3" 26 + } 27 + }
+94
packages/example-tada/schema.graphql
··· 1 + ### This file was generated by Nexus Schema 2 + ### Do not make changes to this file directly 3 + 4 + """ 5 + Move a Pokémon can perform with the associated damage and type. 6 + """ 7 + type Attack { 8 + damage: Int 9 + name: String 10 + type: PokemonType 11 + } 12 + 13 + type AttacksConnection { 14 + fast: [Attack] 15 + special: [Attack] 16 + } 17 + 18 + """ 19 + Requirement that prevents an evolution through regular means of levelling up. 20 + """ 21 + type EvolutionRequirement { 22 + amount: Int 23 + name: String 24 + } 25 + 26 + type Pokemon { 27 + attacks: AttacksConnection 28 + classification: String @deprecated(reason: "And this is the reason why") 29 + evolutionRequirements: [EvolutionRequirement] 30 + evolutions: [Pokemon] 31 + 32 + """ 33 + Likelihood of an attempt to catch a Pokémon to fail. 34 + """ 35 + fleeRate: Float 36 + height: PokemonDimension 37 + id: ID! 38 + 39 + """ 40 + Maximum combat power a Pokémon may achieve at max level. 41 + """ 42 + maxCP: Int 43 + 44 + """ 45 + Maximum health points a Pokémon may achieve at max level. 46 + """ 47 + maxHP: Int 48 + name: String! 49 + resistant: [PokemonType] 50 + types: [PokemonType] 51 + weaknesses: [PokemonType] 52 + weight: PokemonDimension 53 + } 54 + 55 + type PokemonDimension { 56 + maximum: String 57 + minimum: String 58 + } 59 + 60 + """ 61 + Elemental property associated with either a Pokémon or one of their moves. 62 + """ 63 + enum PokemonType { 64 + Bug 65 + Dark 66 + Dragon 67 + Electric 68 + Fairy 69 + Fighting 70 + Fire 71 + Flying 72 + Ghost 73 + Grass 74 + Ground 75 + Ice 76 + Normal 77 + Poison 78 + Psychic 79 + Rock 80 + Steel 81 + Water 82 + } 83 + 84 + type Query { 85 + """ 86 + Get a single Pokémon by its ID, a three character long identifier padded with zeroes 87 + """ 88 + pokemon(id: ID!): Pokemon 89 + 90 + """ 91 + List out all Pokémon, optionally in pages 92 + """ 93 + pokemons(limit: Int, skip: Int): [Pokemon] 94 + }
+27
packages/example-tada/src/Pokemon.tsx
··· 1 + import { FragmentOf, graphql, readFragment } from './graphql'; 2 + 3 + export const PokemonFields = graphql(` 4 + fragment pokemonFields on Pokemon { 5 + name 6 + weight { 7 + minimum 8 + } 9 + } 10 + `); 11 + 12 + interface Props { 13 + data: FragmentOf<typeof PokemonFields> | null; 14 + } 15 + 16 + export const Pokemon = ({ data }: Props) => { 17 + const pokemon = readFragment(PokemonFields, data); 18 + if (!pokemon) { 19 + return null; 20 + } 21 + 22 + return ( 23 + <li> 24 + {pokemon.name} 25 + </li> 26 + ); 27 + };
+9
packages/example-tada/src/graphql.ts
··· 1 + import { initGraphQLTada } from 'gql.tada'; 2 + import type { introspection } from '../introspection'; 3 + 4 + export const graphql = initGraphQLTada<{ 5 + introspection: typeof introspection; 6 + }>(); 7 + 8 + export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada'; 9 + export { readFragment } from 'gql.tada';
+51
packages/example-tada/src/index.tsx
··· 1 + import { createClient, useQuery } from 'urql'; 2 + import { graphql } from './graphql'; 3 + import { Pokemon, PokemonFields } from './Pokemon'; 4 + 5 + const PokemonQuery = graphql(` 6 + query Po($id: ID!) { 7 + pokemon(id: $id) { 8 + id 9 + fleeRate 10 + ...pokemonFields 11 + attacks { 12 + special { 13 + name 14 + damage 15 + } 16 + } 17 + weight { 18 + minimum 19 + maximum 20 + } 21 + name 22 + __typename 23 + } 24 + } 25 + `, [PokemonFields]); 26 + 27 + const Pokemons = () => { 28 + const [result] = useQuery({ 29 + query: PokemonQuery, 30 + variables: { id: '' } 31 + }); 32 + 33 + // Works 34 + console.log(result.data?.pokemon?.attacks && result.data?.pokemon?.attacks.special && result.data?.pokemon?.attacks.special[0] && result.data?.pokemon?.attacks.special[0].name) 35 + 36 + // Works 37 + const { fleeRate } = result.data?.pokemon || {}; 38 + console.log(fleeRate) 39 + // Works 40 + const po = result.data?.pokemon; 41 + // @ts-expect-error 42 + const { pokemon: { weight: { minimum } } } = result.data || {}; 43 + console.log(po?.name, minimum) 44 + 45 + // Works 46 + const { pokemon } = result.data || {}; 47 + console.log(pokemon?.weight?.maximum) 48 + 49 + return <Pokemon data={result.data!.pokemon} />; 50 + } 51 +
+25
packages/example-tada/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "plugins": [ 4 + { 5 + "name": "@0no-co/graphqlsp", 6 + "schema": "./schema.graphql", 7 + "disableTypegen": true, 8 + "shouldCheckForColocatedFragments": true, 9 + "template": "graphql", 10 + "templateIsCallExpression": true, 11 + "trackFieldUsage": true 12 + } 13 + ], 14 + "jsx": "react-jsx", 15 + /* Language and Environment */ 16 + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 17 + /* Modules */ 18 + "module": "commonjs" /* Specify what module code is generated. */, 19 + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 20 + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 21 + /* Type Checking */ 22 + "strict": true /* Enable all strict type-checking options. */, 23 + "skipLibCheck": true /* Skip type checking all .d.ts files. */ 24 + } 25 + }
+74 -2
packages/graphqlsp/src/ast/index.ts
··· 1 1 import ts from 'typescript/lib/tsserverlibrary'; 2 2 import fs from 'fs'; 3 3 import { FragmentDefinitionNode, parse } from 'graphql'; 4 + import { Logger } from '..'; 4 5 5 6 export function isFileDirty(fileName: string, source: ts.SourceFile) { 6 7 const contents = fs.readFileSync(fileName, 'utf-8'); ··· 56 57 return result; 57 58 } 58 59 60 + function unrollFragment( 61 + element: ts.Identifier, 62 + template: string, 63 + info: ts.server.PluginCreateInfo 64 + ): Array<FragmentDefinitionNode> { 65 + const fragments: Array<FragmentDefinitionNode> = []; 66 + const definitions = info.languageService.getDefinitionAtPosition( 67 + element.getSourceFile().fileName, 68 + element.getStart() 69 + ); 70 + 71 + if (!definitions) return fragments; 72 + 73 + const [fragment] = definitions; 74 + 75 + const externalSource = getSource(info, fragment.fileName); 76 + if (!externalSource) return fragments; 77 + 78 + let found = findNode(externalSource, fragment.textSpan.start); 79 + if (!found) return fragments; 80 + 81 + if ( 82 + ts.isVariableDeclaration(found.parent) && 83 + found.parent.initializer && 84 + ts.isCallExpression(found.parent.initializer) 85 + ) { 86 + found = found.parent.initializer; 87 + } 88 + 89 + if (ts.isCallExpression(found) && found.expression.getText() === template) { 90 + const [arg, arg2] = found.arguments; 91 + if (arg2 && ts.isArrayLiteralExpression(arg2)) { 92 + arg2.elements.forEach(element => { 93 + if (ts.isIdentifier(element)) { 94 + fragments.push(...unrollFragment(element, template, info)); 95 + } 96 + }); 97 + } 98 + 99 + try { 100 + const parsed = parse(arg.getText().slice(1, -1), { noLocation: true }); 101 + parsed.definitions.forEach(definition => { 102 + if (definition.kind === 'FragmentDefinition') { 103 + fragments.push(definition); 104 + } 105 + }); 106 + } catch (e) {} 107 + } 108 + 109 + return fragments; 110 + } 111 + 59 112 export function findAllCallExpressions( 60 113 sourceFile: ts.SourceFile, 61 114 template: string, ··· 70 123 let hasTriedToFindFragments = shouldSearchFragments ? false : true; 71 124 function find(node: ts.Node) { 72 125 if (ts.isCallExpression(node) && node.expression.getText() === template) { 73 - if (!hasTriedToFindFragments) { 126 + const [arg, arg2] = node.arguments; 127 + 128 + if (!hasTriedToFindFragments && !arg2) { 74 129 hasTriedToFindFragments = true; 75 130 fragments = getAllFragments(sourceFile.fileName, node, info); 131 + } else if (arg2 && ts.isArrayLiteralExpression(arg2)) { 132 + arg2.elements.forEach(element => { 133 + if (ts.isIdentifier(element)) { 134 + fragments.push(...unrollFragment(element, template, info)); 135 + } 136 + }); 76 137 } 77 - const [arg] = node.arguments; 138 + 78 139 if (arg && ts.isNoSubstitutionTemplateLiteral(arg)) { 79 140 result.push(arg); 80 141 } ··· 92 153 node: ts.CallExpression, 93 154 info: ts.server.PluginCreateInfo 94 155 ) { 156 + const template = info.config.template || 'gql'; 95 157 let fragments: Array<FragmentDefinitionNode> = []; 96 158 97 159 const definitions = info.languageService.getDefinitionAtPosition( ··· 99 161 node.expression.getStart() 100 162 ); 101 163 if (!definitions) return fragments; 164 + 165 + if (node.arguments[1] && ts.isArrayLiteralExpression(node.arguments[1])) { 166 + const arg2 = node.arguments[1] as ts.ArrayLiteralExpression; 167 + arg2.elements.forEach(element => { 168 + if (ts.isIdentifier(element)) { 169 + fragments.push(...unrollFragment(element, template, info)); 170 + } 171 + }); 172 + return fragments; 173 + } 102 174 103 175 const def = definitions[0]; 104 176 const src = getSource(info, def.fileName);
-3
packages/graphqlsp/src/autoComplete.ts
··· 37 37 schema: { current: GraphQLSchema | null }, 38 38 info: ts.server.PluginCreateInfo 39 39 ): ts.WithMetadata<ts.CompletionInfo> | undefined { 40 - const logger: any = (msg: string) => 41 - info.project.projectService.logger.info(`[GraphQLSP] ${msg}`); 42 - 43 40 const tagTemplate = info.config.template || 'gql'; 44 41 const isCallExpression = info.config.templateIsCallExpression ?? false; 45 42
+42 -47
packages/graphqlsp/src/checkImports.ts
··· 52 52 const moduleExports = typeChecker?.getExportsOfModule(symbol); 53 53 if (!moduleExports) return; 54 54 55 - const missingImports = moduleExports 56 - .map(exp => { 57 - if (importedNames.includes(exp.name)) { 58 - return; 59 - } 55 + const missingImports = new Set<string>(); 56 + moduleExports.forEach(exp => { 57 + if (importedNames.includes(exp.name)) { 58 + return; 59 + } 60 + 61 + const declarations = exp.getDeclarations(); 62 + const declaration = declarations?.find(x => { 63 + // TODO: check whether the sourceFile.fileName resembles the module 64 + // specifier 65 + return true; 66 + }); 60 67 61 - const declarations = exp.getDeclarations(); 62 - const declaration = declarations?.find(x => { 63 - // TODO: check whether the sourceFile.fileName resembles the module 64 - // specifier 65 - return true; 66 - }); 68 + if (!declaration) return; 67 69 68 - if (!declaration) return; 70 + const [template] = findAllTaggedTemplateNodes(declaration, tagTemplate); 71 + if (template) { 72 + let node = template; 73 + if ( 74 + ts.isNoSubstitutionTemplateLiteral(node) || 75 + ts.isTemplateExpression(node) 76 + ) { 77 + if (ts.isTaggedTemplateExpression(node.parent)) { 78 + node = node.parent; 79 + } else { 80 + return; 81 + } 82 + } 69 83 70 - const [template] = findAllTaggedTemplateNodes( 71 - declaration, 72 - tagTemplate 73 - ); 74 - if (template) { 75 - let node = template; 84 + const text = resolveTemplate( 85 + node, 86 + node.getSourceFile().fileName, 87 + info 88 + ).combinedText; 89 + try { 90 + const parsed = parse(text, { noLocation: true }); 76 91 if ( 77 - ts.isNoSubstitutionTemplateLiteral(node) || 78 - ts.isTemplateExpression(node) 92 + parsed.definitions.every(x => x.kind === Kind.FRAGMENT_DEFINITION) 79 93 ) { 80 - if (ts.isTaggedTemplateExpression(node.parent)) { 81 - node = node.parent; 82 - } else { 83 - return; 84 - } 85 - } 86 - 87 - const text = resolveTemplate( 88 - node, 89 - node.getSourceFile().fileName, 90 - info 91 - ).combinedText; 92 - try { 93 - const parsed = parse(text, { noLocation: true }); 94 - if ( 95 - parsed.definitions.every( 96 - x => x.kind === Kind.FRAGMENT_DEFINITION 97 - ) 98 - ) { 99 - return `'${exp.name}'`; 100 - } 101 - } catch (e) { 102 - return; 94 + missingImports.add(`'${exp.name}'`); 103 95 } 96 + } catch (e) { 97 + return; 104 98 } 105 - }) 106 - .filter(Boolean); 99 + } 100 + }); 107 101 108 - if (missingImports.length) { 102 + const missing = Array.from(missingImports); 103 + if (missing.length) { 109 104 tsDiagnostics.push({ 110 105 file: source, 111 106 length: imp.getText().length, 112 107 start: imp.getStart(), 113 108 category: ts.DiagnosticCategory.Message, 114 109 code: MISSING_FRAGMENT_CODE, 115 - messageText: `Missing Fragment import(s) ${missingImports.join( 110 + messageText: `Missing Fragment import(s) ${missing.join( 116 111 ', ' 117 112 )} from ${imp.moduleSpecifier.getText()}.`, 118 113 });
+2 -6
packages/graphqlsp/src/diagnostics.ts
··· 328 328 source, 329 329 info 330 330 ); 331 - console.log( 332 - '[GraphhQLSP] Checking for colocated fragments ', 333 - JSON.stringify(moduleSpecifierToFragments, null, 2) 334 - ); 335 331 336 332 const usedFragments = new Set(); 337 333 nodes.forEach(node => { ··· 353 349 start, 354 350 length, 355 351 } = moduleSpecifierToFragments[moduleSpecifier]; 356 - const missingFragments = fragmentNames.filter( 352 + const missingFragments = Array.from(new Set(fragmentNames.filter( 357 353 x => !usedFragments.has(x) 358 - ); 354 + ))); 359 355 if (missingFragments.length) { 360 356 fragmentDiagnostics.push({ 361 357 file: source,
+47 -2
pnpm-lock.yaml
··· 107 107 specifier: ^5.3.3 108 108 version: 5.3.3 109 109 110 + packages/example-tada: 111 + dependencies: 112 + '@graphql-typed-document-node/core': 113 + specifier: ^3.2.0 114 + version: 3.2.0(graphql@16.8.1) 115 + '@urql/core': 116 + specifier: ^3.0.0 117 + version: 3.2.2(graphql@16.8.1) 118 + gql.tada: 119 + specifier: '*' 120 + version: 1.0.0-beta.1(graphql@16.8.1) 121 + graphql: 122 + specifier: ^16.8.1 123 + version: 16.8.1 124 + urql: 125 + specifier: ^4.0.6 126 + version: 4.0.6(graphql@16.8.1)(react@18.2.0) 127 + devDependencies: 128 + '@0no-co/graphqlsp': 129 + specifier: file:../graphqlsp 130 + version: file:packages/graphqlsp(graphql@16.8.1) 131 + '@graphql-codegen/cli': 132 + specifier: ^5.0.0 133 + version: 5.0.0(@types/node@18.15.11)(graphql@16.8.1)(typescript@5.3.3) 134 + '@graphql-codegen/client-preset': 135 + specifier: ^4.1.0 136 + version: 4.1.0(graphql@16.8.1) 137 + '@types/react': 138 + specifier: ^18.2.45 139 + version: 18.2.45 140 + ts-node: 141 + specifier: ^10.9.1 142 + version: 10.9.1(@types/node@18.15.11)(typescript@5.3.3) 143 + typescript: 144 + specifier: ^5.3.3 145 + version: 5.3.3 146 + 110 147 packages/graphqlsp: 111 148 dependencies: 112 149 '@graphql-codegen/add': ··· 1898 1935 '@graphql-tools/executor-graphql-ws': 1.1.0(graphql@16.8.1) 1899 1936 '@graphql-tools/executor-http': 1.0.3(@types/node@18.15.11)(graphql@16.8.1) 1900 1937 '@graphql-tools/executor-legacy-ws': 1.0.4(graphql@16.8.1) 1901 - '@graphql-tools/utils': 10.0.1(graphql@16.8.1) 1938 + '@graphql-tools/utils': 10.0.11(graphql@16.8.1) 1902 1939 '@graphql-tools/wrap': 10.0.1(graphql@16.8.1) 1903 1940 '@types/ws': 8.5.10 1904 1941 '@whatwg-node/fetch': 0.9.14 1905 1942 graphql: 16.8.1 1906 1943 isomorphic-ws: 5.0.0(ws@8.14.2) 1907 - tslib: 2.5.0 1944 + tslib: 2.6.2 1908 1945 value-or-promise: 1.0.12 1909 1946 ws: 8.14.2 1910 1947 transitivePeerDependencies: ··· 3619 3656 dependencies: 3620 3657 get-intrinsic: 1.2.0 3621 3658 dev: true 3659 + 3660 + /gql.tada@1.0.0-beta.1(graphql@16.8.1): 3661 + resolution: {integrity: sha512-jodWt1sV8ORYp8FyY8+bGXAalRoyAqjk6kAZfewZgTuFZ8v/F70yujqxQ0KSdajzFK2SLsOSPEI+QXv+Nln9EQ==} 3662 + dependencies: 3663 + '@0no-co/graphql.web': 1.0.4(graphql@16.8.1) 3664 + transitivePeerDependencies: 3665 + - graphql 3666 + dev: false 3622 3667 3623 3668 /graceful-fs@4.2.11: 3624 3669 resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}