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

feat: support first argument in graphql.persisted (#287)

Co-authored-by: Phil Pluckthun <phil@kitten.sh>

authored by

Jovi De Croock
Phil Pluckthun
and committed by
GitHub
4ddb4366 fab6652e

+167 -54
+5
.changeset/cuddly-needles-glow.md
··· 1 + --- 2 + '@0no-co/graphqlsp': minor 3 + --- 4 + 5 + Support passing GraphQL documents by value to `graphql.persisted`’s second argument
+1 -1
packages/example-tada/src/index.tsx
··· 32 32 } 33 33 `, [PokemonFields, Fields.Pokemon]); 34 34 35 - const persisted = graphql.persisted<typeof PokemonQuery>("sha256:dc31ff9637bbc77bb95dffb2ca73b0e607639b018befd06e9ad801b54483d661") 35 + const persisted = graphql.persisted<typeof PokemonQuery>("sha256:7a9bbe8533362e631f92af8d7f314b1589c8272f8e092da564d9ad6cd600a4eb") 36 36 37 37 const Pokemons = () => { 38 38 const [result] = useQuery({
+4 -1
packages/graphqlsp/src/api.ts
··· 1 1 export { getGraphQLDiagnostics } from './diagnostics'; 2 2 export { init } from './ts'; 3 3 export { findAllPersistedCallExpressions, unrollTadaFragments } from './ast'; 4 - export { getDocumentReferenceFromTypeQuery } from './persisted'; 4 + export { 5 + getDocumentReferenceFromTypeQuery, 6 + getDocumentReferenceFromDocumentNode, 7 + } from './persisted';
+73 -33
packages/graphqlsp/src/diagnostics.ts
··· 24 24 MISSING_FRAGMENT_CODE, 25 25 getColocatedFragmentNames, 26 26 } from './checkImports'; 27 - import { getDocumentReferenceFromTypeQuery } from './persisted'; 27 + import { 28 + getDocumentReferenceFromDocumentNode, 29 + getDocumentReferenceFromTypeQuery, 30 + } from './persisted'; 28 31 29 32 const clientDirectives = new Set([ 30 33 'populate', ··· 136 139 // document but this removes support for self-generating identifiers 137 140 const persistedDiagnostics = persistedCalls 138 141 .map<ts.Diagnostic | null>(callExpression => { 139 - if (!callExpression.typeArguments) { 142 + if (!callExpression.typeArguments && !callExpression.arguments[1]) { 140 143 return { 141 144 category: ts.DiagnosticCategory.Warning, 142 145 code: MISSING_PERSISTED_TYPE_ARG, ··· 147 150 }; 148 151 } 149 152 150 - const [typeQuery] = callExpression.typeArguments; 153 + let foundNode, 154 + foundFilename = filename, 155 + ref, 156 + start, 157 + length; 158 + if (callExpression.typeArguments) { 159 + const [typeQuery] = callExpression.typeArguments; 160 + start = typeQuery.getStart(); 161 + length = typeQuery.getEnd() - typeQuery.getStart(); 151 162 152 - if (!ts.isTypeQueryNode(typeQuery)) { 153 - // Provide diagnostic about wroong generic 154 - return { 155 - category: ts.DiagnosticCategory.Warning, 156 - code: MISSING_PERSISTED_TYPE_ARG, 157 - file: source, 158 - messageText: 159 - 'Provided generic should be a typeQueryNode in the shape of graphql.persisted<typeof document>.', 160 - start: typeQuery.getStart(), 161 - length: typeQuery.getEnd() - typeQuery.getStart(), 162 - }; 163 - } 163 + if (!ts.isTypeQueryNode(typeQuery)) { 164 + return { 165 + category: ts.DiagnosticCategory.Warning, 166 + code: MISSING_PERSISTED_TYPE_ARG, 167 + file: source, 168 + messageText: 169 + 'Provided generic should be a typeQueryNode in the shape of graphql.persisted<typeof document>.', 170 + start, 171 + length, 172 + }; 173 + } 174 + const { node: found, filename: fileName } = 175 + getDocumentReferenceFromTypeQuery(typeQuery, filename, info); 176 + foundNode = found; 177 + foundFilename = fileName; 178 + ref = typeQuery.getText(); 179 + } else if (callExpression.arguments[1]) { 180 + start = callExpression.arguments[1].getStart(); 181 + length = 182 + callExpression.arguments[1].getEnd() - 183 + callExpression.arguments[1].getStart(); 184 + if ( 185 + !ts.isIdentifier(callExpression.arguments[1]) && 186 + !ts.isCallExpression(callExpression.arguments[1]) 187 + ) { 188 + return { 189 + category: ts.DiagnosticCategory.Warning, 190 + code: MISSING_PERSISTED_TYPE_ARG, 191 + file: source, 192 + messageText: 193 + 'Provided argument should be an identifier or invocation of "graphql" in the shape of graphql.persisted(hash, document).', 194 + start, 195 + length, 196 + }; 197 + } 164 198 165 - const { node: foundNode } = getDocumentReferenceFromTypeQuery( 166 - typeQuery, 167 - filename, 168 - info 169 - ); 199 + const { node: found, filename: fileName } = 200 + getDocumentReferenceFromDocumentNode( 201 + callExpression.arguments[1], 202 + filename, 203 + info 204 + ); 205 + foundNode = found; 206 + foundFilename = fileName; 207 + ref = callExpression.arguments[1].getText(); 208 + } 170 209 171 210 if (!foundNode) { 172 211 return { 173 212 category: ts.DiagnosticCategory.Warning, 174 213 code: MISSING_PERSISTED_DOCUMENT, 175 214 file: source, 176 - messageText: `Can't find reference to "${typeQuery.getText()}".`, 177 - start: typeQuery.getStart(), 178 - length: typeQuery.getEnd() - typeQuery.getStart(), 215 + messageText: `Can't find reference to "${ref}".`, 216 + start, 217 + length, 179 218 }; 180 219 } 181 220 182 - const initializer = foundNode.initializer; 221 + const initializer = foundNode; 183 222 if ( 184 223 !initializer || 185 224 !ts.isCallExpression(initializer) || ··· 191 230 category: ts.DiagnosticCategory.Warning, 192 231 code: MISSING_PERSISTED_DOCUMENT, 193 232 file: source, 194 - messageText: `Referenced type "${typeQuery.getText()}" is not a GraphQL document.`, 195 - start: typeQuery.getStart(), 196 - length: typeQuery.getEnd() - typeQuery.getStart(), 233 + messageText: `Referenced type "${ref}" is not a GraphQL document.`, 234 + start, 235 + length, 197 236 }; 198 237 } 199 238 ··· 451 490 })); 452 491 453 492 if (isCallExpression) { 454 - const usageDiagnostics = checkFieldUsageInFile( 455 - source, 456 - nodes as ts.NoSubstitutionTemplateLiteral[], 457 - info 458 - ) || []; 493 + const usageDiagnostics = 494 + checkFieldUsageInFile( 495 + source, 496 + nodes as ts.NoSubstitutionTemplateLiteral[], 497 + info 498 + ) || []; 459 499 460 - if (!usageDiagnostics) return tsDiagnostics 500 + if (!usageDiagnostics) return tsDiagnostics; 461 501 462 502 return [...tsDiagnostics, ...usageDiagnostics]; 463 503 } else {
+78 -13
packages/graphqlsp/src/persisted.ts
··· 80 80 if ( 81 81 !ts.isCallExpression(callExpression) || 82 82 !isPersistedCall(callExpression.expression) || 83 - !callExpression.typeArguments 83 + (!callExpression.typeArguments && !callExpression.arguments[1]) 84 84 ) 85 85 return undefined; 86 86 87 - const [typeQuery] = callExpression.typeArguments; 88 - 89 - if (!ts.isTypeQueryNode(typeQuery)) return undefined; 90 - 91 - const { node: found, filename: foundFilename } = 92 - getDocumentReferenceFromTypeQuery(typeQuery, filename, info); 87 + let foundNode, 88 + foundFilename = filename; 89 + if (callExpression.typeArguments) { 90 + const [typeQuery] = callExpression.typeArguments; 91 + if (!ts.isTypeQueryNode(typeQuery)) return undefined; 92 + const { node: found, filename: fileName } = 93 + getDocumentReferenceFromTypeQuery(typeQuery, filename, info); 94 + foundNode = found; 95 + foundFilename = fileName; 96 + } else if (callExpression.arguments[1]) { 97 + if ( 98 + !ts.isIdentifier(callExpression.arguments[1]) && 99 + !ts.isCallExpression(callExpression.arguments[1]) 100 + ) 101 + return undefined; 102 + const { node: found, filename: fileName } = 103 + getDocumentReferenceFromDocumentNode( 104 + callExpression.arguments[1], 105 + filename, 106 + info 107 + ); 108 + foundNode = found; 109 + foundFilename = fileName; 110 + } 93 111 94 - if (!found) return undefined; 112 + if (!foundNode) return undefined; 95 113 96 - const initializer = found.initializer; 114 + const initializer = foundNode; 97 115 if ( 98 116 !initializer || 99 117 !ts.isCallExpression(initializer) || ··· 174 192 typeQuery: ts.TypeQueryNode, 175 193 filename: string, 176 194 info: ts.server.PluginCreateInfo 177 - ): { node: ts.VariableDeclaration | null; filename: string } => { 195 + ): { node: ts.CallExpression | null; filename: string } => { 178 196 // We look for the references of the generic so that we can use the document 179 197 // to generate the hash. 180 198 const references = info.languageService.getReferencesAtPosition( ··· 184 202 185 203 if (!references) return { node: null, filename }; 186 204 187 - let found: ts.VariableDeclaration | null = null; 205 + let found: ts.CallExpression | null = null; 188 206 let foundFilename = filename; 189 207 references.forEach(ref => { 190 208 if (found) return; ··· 194 212 const foundNode = findNode(source, ref.textSpan.start); 195 213 if (!foundNode) return; 196 214 197 - if (ts.isVariableDeclaration(foundNode.parent)) { 198 - found = foundNode.parent; 215 + if ( 216 + ts.isVariableDeclaration(foundNode.parent) && 217 + foundNode.parent.initializer && 218 + ts.isCallExpression(foundNode.parent.initializer) && 219 + templates.has(foundNode.parent.initializer.expression.getText()) 220 + ) { 221 + found = foundNode.parent.initializer; 199 222 foundFilename = ref.fileName; 200 223 } 201 224 }); 202 225 203 226 return { node: found, filename: foundFilename }; 204 227 }; 228 + 229 + export const getDocumentReferenceFromDocumentNode = ( 230 + documentNodeArgument: ts.Identifier | ts.CallExpression, 231 + filename: string, 232 + info: ts.server.PluginCreateInfo 233 + ): { node: ts.CallExpression | null; filename: string } => { 234 + if (ts.isIdentifier(documentNodeArgument)) { 235 + // We look for the references of the generic so that we can use the document 236 + // to generate the hash. 237 + const references = info.languageService.getReferencesAtPosition( 238 + filename, 239 + documentNodeArgument.getStart() 240 + ); 241 + 242 + if (!references) return { node: null, filename }; 243 + 244 + let found: ts.CallExpression | null = null; 245 + let foundFilename = filename; 246 + references.forEach(ref => { 247 + if (found) return; 248 + 249 + const source = getSource(info, ref.fileName); 250 + if (!source) return; 251 + const foundNode = findNode(source, ref.textSpan.start); 252 + if (!foundNode) return; 253 + 254 + if ( 255 + ts.isVariableDeclaration(foundNode.parent) && 256 + foundNode.parent.initializer && 257 + ts.isCallExpression(foundNode.parent.initializer) && 258 + templates.has(foundNode.parent.initializer.expression.getText()) 259 + ) { 260 + found = foundNode.parent.initializer; 261 + foundFilename = ref.fileName; 262 + } 263 + }); 264 + 265 + return { node: found, filename: foundFilename }; 266 + } else { 267 + return { node: documentNodeArgument, filename }; 268 + } 269 + };
+6 -6
pnpm-lock.yaml
··· 1341 1341 resolution: {integrity: sha512-8I4Z1zxYYGK66FWdB3yIZBn3cITLPnciEgjChp3K2+Ha1e/AEBGtZv9AUlodraO/RZafDMkpFhoi+tMpluBjeg==} 1342 1342 peerDependencies: 1343 1343 graphql: ^16.8.1 1344 - typescript: ^5.3.3 1344 + typescript: ^5.0.0 1345 1345 dependencies: 1346 1346 '@0no-co/graphql.web': 1.0.6(graphql@16.8.1) 1347 1347 graphql: 16.8.1 ··· 2144 2144 peerDependencies: 2145 2145 rollup: ^2.14.0||^3.0.0||^4.0.0 2146 2146 tslib: '*' 2147 - typescript: ^5.3.3 2147 + typescript: '>=3.7.0' 2148 2148 peerDependenciesMeta: 2149 2149 rollup: 2150 2150 optional: true ··· 3046 3046 resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} 3047 3047 engines: {node: '>=14'} 3048 3048 peerDependencies: 3049 - typescript: ^5.3.3 3049 + typescript: '>=4.9.5' 3050 3050 peerDependenciesMeta: 3051 3051 typescript: 3052 3052 optional: true ··· 5164 5164 engines: {node: '>=16'} 5165 5165 peerDependencies: 5166 5166 rollup: ^3.29.4 || ^4 5167 - typescript: ^5.3.3 5167 + typescript: ^4.5 || ^5.0 5168 5168 dependencies: 5169 5169 magic-string: 0.30.5 5170 5170 rollup: 4.9.5 ··· 5664 5664 '@swc/core': '>=1.2.50' 5665 5665 '@swc/wasm': '>=1.2.50' 5666 5666 '@types/node': '*' 5667 - typescript: ^5.3.3 5667 + typescript: '>=2.7' 5668 5668 peerDependenciesMeta: 5669 5669 '@swc/core': 5670 5670 optional: true ··· 6206 6206 id: file:packages/graphqlsp 6207 6207 name: '@0no-co/graphqlsp' 6208 6208 peerDependencies: 6209 - typescript: ^5.3.3 6209 + typescript: ^5.0.0 6210 6210 dependencies: 6211 6211 '@gql.tada/internal': 0.1.2(graphql@16.8.1)(typescript@5.3.3) 6212 6212 graphql: 16.8.1