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

fix issue with AsType

+15
README.md
···
```
now restart your TS-server and you should be good to go
+
+
## Local development
+
+
Run `yarn` in both `/` as well as `/example`.
+
+
Open two terminal tabs, one where you run the build command which is `yarn tsc` and one
+
intended to open our `/example`, most of the debugging will happen through setting breakpoints.
+
+
Run `TSS_DEBUG_BRK=9559 code example` and ensure that the TypeScript used is the one from the workspace
+
the `.vscode` folder should ensure that but sometimes it fails. When we use `TSS_DEBUG_BRK` the plugin
+
won't run until we attach the debugger from our main editor.
+
+
After makiing changes you'll have to re-open said editor or restart the TypeScript server and re-attach the
+
debugger. Breakpoints have to be set in the transpiled JS-code hence using `tsc` currently so the code is a
+
bit easier to navigate.
+1 -4
example/src/fragment.generated.ts
···
export type PokemonFieldsFragment = { __typename?: 'Pokemon', id: string, name: string };
-
export type MorePokemonFieldsFragment = { __typename?: 'Pokemon', id: string, name: string };
-
-
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>;
-
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>;
+
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
···
id
name
}
-
`
-
+
` as typeof import('./fragment.generated').PokemonFieldsFragmentDoc
// TODO: how to type
// export const PokemonFields = gql`
// fragment pokemonFields on Pokemon {
+11 -2
example/src/index.ts
···
-
import { gql } from '@urql/core'
+
import { createClient, gql } from '@urql/core'
import { PokemonFields } from './fragment'
const Pokemons = gql`
···
${PokemonFields}
` as typeof import('./index.generated').PokemonsDocument
+
const Pokemon = gql`
query Pokemon {
pokemon(id: "1") {
···
}
${PokemonFields}
-
`
+
` as typeof import('./index.generated').PokemonDocument
+
+
const urqlClient = createClient({
+
url: 'http://localhost:3000/api'
+
});
+
+
urqlClient.query(Pokemons).toPromise().then(result => {
+
result.data?.pokemons;
+
});
+55 -60
src/index.ts
···
import ts from "typescript/lib/tsserverlibrary";
-
import { isNoSubstitutionTemplateLiteral, ScriptElementKind, isIdentifier, isTaggedTemplateExpression, isToken, isTemplateExpression} from "typescript";
+
import { isNoSubstitutionTemplateLiteral, ScriptElementKind, isIdentifier, isTaggedTemplateExpression, isToken, isTemplateExpression, isImportTypeNode, ImportTypeNode } from "typescript";
import { getHoverInformation, getAutocompleteSuggestions, getDiagnostics, Diagnostic } from 'graphql-language-service'
import { GraphQLSchema, parse, Kind, FragmentDefinitionNode, OperationDefinitionNode } from 'graphql'
···
return resolveTemplate(node, filename, info)
})
-
try {
-
// TODO: we might only want to run this when there are no
-
// diagnostic issues.
-
// TODO: we might need to issue warnings for docuemnts without an operationName
-
// TODO: we will need to check for renamed operations that _do contain_ a type definition
-
const parts = source.fileName.split('/');
-
const name = parts[parts.length - 1];
-
const nameParts = name.split('.');
-
nameParts[nameParts.length - 1] = 'generated.ts'
-
parts[parts.length - 1] = nameParts.join('.')
-
generateTypedDocumentNodes(schema, parts.join('/'), texts.join('\n')).then(() => {
-
nodes.forEach((node, i) => {
-
const queryText = texts[i] || '';
-
const parsed = parse(queryText);
-
const isFragment = parsed.definitions.every(x => x.kind === Kind.FRAGMENT_DEFINITION);
-
let name = '';
-
if (isFragment) {
-
const fragmentNode = parsed.definitions[0] as FragmentDefinitionNode;
-
name = fragmentNode.name.value;
-
} else {
-
const operationNode = parsed.definitions[0] as OperationDefinitionNode;
-
name = operationNode.name!.value;
-
}
-
-
name = name.charAt(0).toUpperCase() + name.slice(1);
-
const parentChildren = node.parent.getChildren();
-
if (parentChildren.find(x => x.kind === 200)) {
-
return;
-
}
-
-
// TODO: we'll have to combine writing multiple exports when we are dealing with more than
-
// one tagged template in a file
-
const exportName = isFragment ? `${name}FragmentDoc` : `${name}Document`;
-
const imp = ` as typeof import('./${nameParts.join('.').replace('.ts', '')}').${exportName}`;
-
-
const span = { length: 1, start: node.end };
-
const prefix = source.text.substring(0, span.start);
-
const suffix = source.text.substring(span.start + span.length, source.text.length);
-
const text = prefix + imp + suffix;
-
-
const scriptInfo = info.project.projectService.getScriptInfo(filename);
-
const snapshot = scriptInfo!.getSnapshot();
-
const length = snapshot.getLength();
-
-
source.update(text, { span, newLength: imp.length })
-
scriptInfo!.editContent(0, length, text);
-
info.languageServiceHost.writeFile!(source.fileName, text);
-
scriptInfo!.registerFileUpdate();
-
})
-
});
-
} catch (e) {
-
console.error(e)
-
throw e
-
}
-
const diagnostics = nodes.map(x => {
let node = x;
if (isNoSubstitutionTemplateLiteral(node) || isTemplateExpression(node)) {
···
return result;
})
+
if (!newDiagnostics.length) {
+
try {
+
// TODO: we might need to issue warnings for documents without an operationName
+
// as we can't generate types for those
+
const parts = source.fileName.split('/');
+
const name = parts[parts.length - 1];
+
const nameParts = name.split('.');
+
nameParts[nameParts.length - 1] = 'generated.ts'
+
parts[parts.length - 1] = nameParts.join('.')
+
+
// TODO: we might only want to run this onSave/when file isn't dirty
+
generateTypedDocumentNodes(schema, parts.join('/'), texts.join('\n')).then(() => {
+
nodes.forEach((node, i) => {
+
const queryText = texts[i] || '';
+
const parsed = parse(queryText);
+
const isFragment = parsed.definitions.every(x => x.kind === Kind.FRAGMENT_DEFINITION);
+
let name = '';
+
if (isFragment) {
+
const fragmentNode = parsed.definitions[0] as FragmentDefinitionNode;
+
name = fragmentNode.name.value;
+
} else {
+
const operationNode = parsed.definitions[0] as OperationDefinitionNode;
+
name = operationNode.name?.value || '';
+
}
+
+
if (!name) return;
+
+
name = name.charAt(0).toUpperCase() + name.slice(1);
+
const parentChildren = node.parent.getChildren();
+
+
const exportName = isFragment ? `${name}FragmentDoc` : `${name}Document`;
+
const imp = ` as typeof import('./${nameParts.join('.').replace('.ts', '')}').${exportName}`;
+
+
// This checks whether one of the children is an import-type
+
// which is a short-circuit if there is no as
+
const typeImport = parentChildren.find(x => isImportTypeNode(x)) as ImportTypeNode
+
if (typeImport && typeImport.getText() === exportName) return;
+
+
const span = { length: 1, start: node.end };
+
const text = source.text.substring(0, span.start) + imp + source.text.substring(span.start + span.length, source.text.length);
+
+
const scriptInfo = info.project.projectService.getScriptInfo(filename);
+
const snapshot = scriptInfo!.getSnapshot();
+
+
// TODO: potential optimisation is to write only one script-update
+
source.update(text, { span, newLength: imp.length })
+
scriptInfo!.editContent(0, snapshot.getLength(), text);
+
info.languageServiceHost.writeFile!(source.fileName, text);
+
scriptInfo!.registerFileUpdate();
+
})
+
});
+
} catch (e) {}
+
}
+
return [
...newDiagnostics,
...originalDiagnostics
···
return originalInfo
}
}
-
-
// to research:
-
// proxy.getTypeDefinitionAtPosition
-
// proxy.getCompletionEntryDetails
logger('proxy: ' + JSON.stringify(proxy));
+4 -1
src/resolve.ts
···
-
import { isIdentifier, isNoSubstitutionTemplateLiteral, isTaggedTemplateExpression, NoSubstitutionTemplateLiteral, TaggedTemplateExpression, TemplateExpression, TemplateLiteral } from "typescript";
+
import { isAsExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isTaggedTemplateExpression, NoSubstitutionTemplateLiteral, TaggedTemplateExpression, TemplateExpression, TemplateLiteral } from "typescript";
import ts from "typescript/lib/tsserverlibrary";
import { findNode, getSource } from "./utils";
···
if (ts.isVariableDeclaration(parent)) {
if (parent.initializer && isTaggedTemplateExpression(parent.initializer)) {
const text = resolveTemplate(parent.initializer, def.fileName, info)
+
templateText = templateText.replace('${' + span.expression.escapedText + '}', text)
+
} else if (parent.initializer && isAsExpression(parent.initializer) && isTaggedTemplateExpression(parent.initializer.expression)) {
+
const text = resolveTemplate(parent.initializer.expression, def.fileName, info)
templateText = templateText.replace('${' + span.expression.escapedText + '}', text)
}
}