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.

fix issue with AsType

+87 -69
+15
README.md
··· 24 24 ``` 25 25 26 26 now restart your TS-server and you should be good to go 27 + 28 + ## Local development 29 + 30 + Run `yarn` in both `/` as well as `/example`. 31 + 32 + Open two terminal tabs, one where you run the build command which is `yarn tsc` and one 33 + intended to open our `/example`, most of the debugging will happen through setting breakpoints. 34 + 35 + Run `TSS_DEBUG_BRK=9559 code example` and ensure that the TypeScript used is the one from the workspace 36 + the `.vscode` folder should ensure that but sometimes it fails. When we use `TSS_DEBUG_BRK` the plugin 37 + won't run until we attach the debugger from our main editor. 38 + 39 + After makiing changes you'll have to re-open said editor or restart the TypeScript server and re-attach the 40 + debugger. Breakpoints have to be set in the transpiled JS-code hence using `tsc` currently so the code is a 41 + bit easier to navigate.
+1 -4
example/src/fragment.generated.ts
··· 104 104 105 105 export type PokemonFieldsFragment = { __typename?: 'Pokemon', id: string, name: string }; 106 106 107 - export type MorePokemonFieldsFragment = { __typename?: 'Pokemon', id: string, name: string }; 108 - 109 - export const PokemonFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"pokemonFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Pokemon"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<PokemonFieldsFragment, unknown>; 110 - export const MorePokemonFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"morePokemonFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Pokemon"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<MorePokemonFieldsFragment, unknown>; 107 + export const PokemonFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"pokemonFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Pokemon"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<PokemonFieldsFragment, unknown>;
+1 -2
example/src/fragment.ts
··· 5 5 id 6 6 name 7 7 } 8 - ` 9 - 8 + ` as typeof import('./fragment.generated').PokemonFieldsFragmentDoc 10 9 // TODO: how to type 11 10 // export const PokemonFields = gql` 12 11 // fragment pokemonFields on Pokemon {
+11 -2
example/src/index.ts
··· 1 - import { gql } from '@urql/core' 1 + import { createClient, gql } from '@urql/core' 2 2 import { PokemonFields } from './fragment' 3 3 4 4 const Pokemons = gql` ··· 11 11 12 12 ${PokemonFields} 13 13 ` as typeof import('./index.generated').PokemonsDocument 14 + 14 15 const Pokemon = gql` 15 16 query Pokemon { 16 17 pokemon(id: "1") { ··· 20 21 } 21 22 22 23 ${PokemonFields} 23 - ` 24 + ` as typeof import('./index.generated').PokemonDocument 25 + 26 + const urqlClient = createClient({ 27 + url: 'http://localhost:3000/api' 28 + }); 29 + 30 + urqlClient.query(Pokemons).toPromise().then(result => { 31 + result.data?.pokemons; 32 + });
+55 -60
src/index.ts
··· 1 1 import ts from "typescript/lib/tsserverlibrary"; 2 - import { isNoSubstitutionTemplateLiteral, ScriptElementKind, isIdentifier, isTaggedTemplateExpression, isToken, isTemplateExpression} from "typescript"; 2 + import { isNoSubstitutionTemplateLiteral, ScriptElementKind, isIdentifier, isTaggedTemplateExpression, isToken, isTemplateExpression, isImportTypeNode, ImportTypeNode } from "typescript"; 3 3 import { getHoverInformation, getAutocompleteSuggestions, getDiagnostics, Diagnostic } from 'graphql-language-service' 4 4 import { GraphQLSchema, parse, Kind, FragmentDefinitionNode, OperationDefinitionNode } from 'graphql' 5 5 ··· 63 63 return resolveTemplate(node, filename, info) 64 64 }) 65 65 66 - try { 67 - // TODO: we might only want to run this when there are no 68 - // diagnostic issues. 69 - // TODO: we might need to issue warnings for docuemnts without an operationName 70 - // TODO: we will need to check for renamed operations that _do contain_ a type definition 71 - const parts = source.fileName.split('/'); 72 - const name = parts[parts.length - 1]; 73 - const nameParts = name.split('.'); 74 - nameParts[nameParts.length - 1] = 'generated.ts' 75 - parts[parts.length - 1] = nameParts.join('.') 76 - generateTypedDocumentNodes(schema, parts.join('/'), texts.join('\n')).then(() => { 77 - nodes.forEach((node, i) => { 78 - const queryText = texts[i] || ''; 79 - const parsed = parse(queryText); 80 - const isFragment = parsed.definitions.every(x => x.kind === Kind.FRAGMENT_DEFINITION); 81 - let name = ''; 82 - if (isFragment) { 83 - const fragmentNode = parsed.definitions[0] as FragmentDefinitionNode; 84 - name = fragmentNode.name.value; 85 - } else { 86 - const operationNode = parsed.definitions[0] as OperationDefinitionNode; 87 - name = operationNode.name!.value; 88 - } 89 - 90 - name = name.charAt(0).toUpperCase() + name.slice(1); 91 - const parentChildren = node.parent.getChildren(); 92 - if (parentChildren.find(x => x.kind === 200)) { 93 - return; 94 - } 95 - 96 - // TODO: we'll have to combine writing multiple exports when we are dealing with more than 97 - // one tagged template in a file 98 - const exportName = isFragment ? `${name}FragmentDoc` : `${name}Document`; 99 - const imp = ` as typeof import('./${nameParts.join('.').replace('.ts', '')}').${exportName}`; 100 - 101 - const span = { length: 1, start: node.end }; 102 - const prefix = source.text.substring(0, span.start); 103 - const suffix = source.text.substring(span.start + span.length, source.text.length); 104 - const text = prefix + imp + suffix; 105 - 106 - const scriptInfo = info.project.projectService.getScriptInfo(filename); 107 - const snapshot = scriptInfo!.getSnapshot(); 108 - const length = snapshot.getLength(); 109 - 110 - source.update(text, { span, newLength: imp.length }) 111 - scriptInfo!.editContent(0, length, text); 112 - info.languageServiceHost.writeFile!(source.fileName, text); 113 - scriptInfo!.registerFileUpdate(); 114 - }) 115 - }); 116 - } catch (e) { 117 - console.error(e) 118 - throw e 119 - } 120 - 121 66 const diagnostics = nodes.map(x => { 122 67 let node = x; 123 68 if (isNoSubstitutionTemplateLiteral(node) || isTemplateExpression(node)) { ··· 168 113 return result; 169 114 }) 170 115 116 + if (!newDiagnostics.length) { 117 + try { 118 + // TODO: we might need to issue warnings for documents without an operationName 119 + // as we can't generate types for those 120 + const parts = source.fileName.split('/'); 121 + const name = parts[parts.length - 1]; 122 + const nameParts = name.split('.'); 123 + nameParts[nameParts.length - 1] = 'generated.ts' 124 + parts[parts.length - 1] = nameParts.join('.') 125 + 126 + // TODO: we might only want to run this onSave/when file isn't dirty 127 + generateTypedDocumentNodes(schema, parts.join('/'), texts.join('\n')).then(() => { 128 + nodes.forEach((node, i) => { 129 + const queryText = texts[i] || ''; 130 + const parsed = parse(queryText); 131 + const isFragment = parsed.definitions.every(x => x.kind === Kind.FRAGMENT_DEFINITION); 132 + let name = ''; 133 + if (isFragment) { 134 + const fragmentNode = parsed.definitions[0] as FragmentDefinitionNode; 135 + name = fragmentNode.name.value; 136 + } else { 137 + const operationNode = parsed.definitions[0] as OperationDefinitionNode; 138 + name = operationNode.name?.value || ''; 139 + } 140 + 141 + if (!name) return; 142 + 143 + name = name.charAt(0).toUpperCase() + name.slice(1); 144 + const parentChildren = node.parent.getChildren(); 145 + 146 + const exportName = isFragment ? `${name}FragmentDoc` : `${name}Document`; 147 + const imp = ` as typeof import('./${nameParts.join('.').replace('.ts', '')}').${exportName}`; 148 + 149 + // This checks whether one of the children is an import-type 150 + // which is a short-circuit if there is no as 151 + const typeImport = parentChildren.find(x => isImportTypeNode(x)) as ImportTypeNode 152 + if (typeImport && typeImport.getText() === exportName) return; 153 + 154 + const span = { length: 1, start: node.end }; 155 + const text = source.text.substring(0, span.start) + imp + source.text.substring(span.start + span.length, source.text.length); 156 + 157 + const scriptInfo = info.project.projectService.getScriptInfo(filename); 158 + const snapshot = scriptInfo!.getSnapshot(); 159 + 160 + // TODO: potential optimisation is to write only one script-update 161 + source.update(text, { span, newLength: imp.length }) 162 + scriptInfo!.editContent(0, snapshot.getLength(), text); 163 + info.languageServiceHost.writeFile!(source.fileName, text); 164 + scriptInfo!.registerFileUpdate(); 165 + }) 166 + }); 167 + } catch (e) {} 168 + } 169 + 171 170 return [ 172 171 ...newDiagnostics, 173 172 ...originalDiagnostics ··· 271 270 return originalInfo 272 271 } 273 272 } 274 - 275 - // to research: 276 - // proxy.getTypeDefinitionAtPosition 277 - // proxy.getCompletionEntryDetails 278 273 279 274 logger('proxy: ' + JSON.stringify(proxy)); 280 275
+4 -1
src/resolve.ts
··· 1 - import { isIdentifier, isNoSubstitutionTemplateLiteral, isTaggedTemplateExpression, NoSubstitutionTemplateLiteral, TaggedTemplateExpression, TemplateExpression, TemplateLiteral } from "typescript"; 1 + import { isAsExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isTaggedTemplateExpression, NoSubstitutionTemplateLiteral, TaggedTemplateExpression, TemplateExpression, TemplateLiteral } from "typescript"; 2 2 import ts from "typescript/lib/tsserverlibrary"; 3 3 import { findNode, getSource } from "./utils"; 4 4 ··· 22 22 if (ts.isVariableDeclaration(parent)) { 23 23 if (parent.initializer && isTaggedTemplateExpression(parent.initializer)) { 24 24 const text = resolveTemplate(parent.initializer, def.fileName, info) 25 + templateText = templateText.replace('${' + span.expression.escapedText + '}', text) 26 + } else if (parent.initializer && isAsExpression(parent.initializer) && isTaggedTemplateExpression(parent.initializer.expression)) { 27 + const text = resolveTemplate(parent.initializer.expression, def.fileName, info) 25 28 templateText = templateText.replace('${' + span.expression.escapedText + '}', text) 26 29 } 27 30 }