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

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

Changed files
+66 -67
.changeset
packages
test
e2e
fixture-project
fixture-project-client-preset
fixture-project-unused-fields
+19
.changeset/few-flowers-buy.md
···
+
---
+
'@0no-co/graphqlsp': major
+
---
+
+
Look for `gql` and `graphql` by default as well as change the default for call-expressions to true.
+
+
If you are using TaggedTemplateExpressions you can migrate by adding the following to your tsconfig file
+
+
```json
+
{
+
"plugins": [
+
{
+
"name": "@0no-co/graphqlsp",
+
"schema": "...",
+
"templateIsCallExpression": false
+
}
+
]
+
}
+
```
+2 -4
README.md
···
**Optional**
-
- `template` the shape of your template, by default `gql`
+
- `template` the shape of your template, by default `gql` and `graphql` are respected
- `templateIsCallExpression` this tells our client that you are using `graphql('doc')`
- `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find
unused fragments and provide a message notifying you about them
···
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
"disableTypegen": true,
-
"templateIsCallExpression": true,
"shouldCheckForColocatedFragments": true,
-
"trackFieldUsage": true,
-
"template": "graphql"
+
"trackFieldUsage": true
}
]
}
-2
packages/example-external-generator/tsconfig.json
···
"schema": "./schema.graphql",
"disableTypegen": true,
"shouldCheckForColocatedFragments": true,
-
"template": "graphql",
-
"templateIsCallExpression": true,
"trackFieldUsage": true
}
],
-1
packages/example-tada/src/index.tsx
···
console.log(fleeRate)
// Works
const po = result.data?.pokemon;
-
// @ts-expect-error
const { pokemon: { weight: { minimum } } } = result.data || {};
console.log(po?.name, minimum)
-2
packages/example-tada/tsconfig.json
···
"schema": "./schema.graphql",
"disableTypegen": true,
"shouldCheckForColocatedFragments": true,
-
"template": "graphql",
-
"templateIsCallExpression": true,
"trackFieldUsage": true
}
],
+2 -1
packages/example/tsconfig.json
···
"plugins": [
{
"name": "@0no-co/graphqlsp",
-
"schema": "./schema.graphql"
+
"schema": "./schema.graphql",
+
"templateIsCallExpression": false
}
],
/* Language and Environment */
+9 -13
packages/graphqlsp/src/ast/index.ts
···
import ts from 'typescript/lib/tsserverlibrary';
import { FragmentDefinitionNode, parse } from 'graphql';
-
import { Logger } from '..';
+
import { templates } from './templates';
export function getSource(info: ts.server.PluginCreateInfo, filename: string) {
const program = info.languageService.getProgram();
···
}
export function findAllTaggedTemplateNodes(
-
sourceFile: ts.SourceFile | ts.Node,
-
template: string
+
sourceFile: ts.SourceFile | ts.Node
): Array<ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral> {
const result: Array<
ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral
···
function find(node: ts.Node) {
if (
(ts.isTaggedTemplateExpression(node) &&
-
node.tag.getText() === template) ||
+
templates.has(node.tag.getText())) ||
(ts.isNoSubstitutionTemplateLiteral(node) &&
ts.isTaggedTemplateExpression(node.parent) &&
-
node.parent.tag.getText() === template)
+
templates.has(node.parent.tag.getText()))
) {
result.push(node);
return;
···
function unrollFragment(
element: ts.Identifier,
-
template: string,
info: ts.server.PluginCreateInfo
): Array<FragmentDefinitionNode> {
const fragments: Array<FragmentDefinitionNode> = [];
···
found = found.parent.initializer;
}
-
if (ts.isCallExpression(found) && found.expression.getText() === template) {
+
if (ts.isCallExpression(found) && templates.has(found.expression.getText())) {
const [arg, arg2] = found.arguments;
if (arg2 && ts.isArrayLiteralExpression(arg2)) {
arg2.elements.forEach(element => {
if (ts.isIdentifier(element)) {
-
fragments.push(...unrollFragment(element, template, info));
+
fragments.push(...unrollFragment(element, info));
}
});
}
···
export function findAllCallExpressions(
sourceFile: ts.SourceFile,
-
template: string,
info: ts.server.PluginCreateInfo,
shouldSearchFragments: boolean = true
): {
···
let fragments: Array<FragmentDefinitionNode> = [];
let hasTriedToFindFragments = shouldSearchFragments ? false : true;
function find(node: ts.Node) {
-
if (ts.isCallExpression(node) && node.expression.getText() === template) {
+
if (ts.isCallExpression(node) && templates.has(node.expression.getText())) {
const [arg, arg2] = node.arguments;
if (!hasTriedToFindFragments && !arg2) {
···
} else if (arg2 && ts.isArrayLiteralExpression(arg2)) {
arg2.elements.forEach(element => {
if (ts.isIdentifier(element)) {
-
fragments.push(...unrollFragment(element, template, info));
+
fragments.push(...unrollFragment(element, info));
}
});
}
···
node: ts.CallExpression,
info: ts.server.PluginCreateInfo
) {
-
const template = info.config.template || 'gql';
let fragments: Array<FragmentDefinitionNode> = [];
const definitions = info.languageService.getDefinitionAtPosition(
···
const arg2 = node.arguments[1] as ts.ArrayLiteralExpression;
arg2.elements.forEach(element => {
if (ts.isIdentifier(element)) {
-
fragments.push(...unrollFragment(element, template, info));
+
fragments.push(...unrollFragment(element, info));
}
});
return fragments;
+1
packages/graphqlsp/src/ast/templates.ts
···
+
export const templates = new Set(['gql', 'graphql']);
+4 -4
packages/graphqlsp/src/autoComplete.ts
···
import { resolveTemplate } from './ast/resolve';
import { getToken } from './ast/token';
import { getSuggestionsForFragmentSpread } from './graphql/getFragmentSpreadSuggestions';
+
import { templates } from './ast/templates';
export function getGraphQLCompletions(
filename: string,
···
schema: { current: GraphQLSchema | null },
info: ts.server.PluginCreateInfo
): ts.WithMetadata<ts.CompletionInfo> | undefined {
-
const tagTemplate = info.config.template || 'gql';
-
const isCallExpression = info.config.templateIsCallExpression ?? false;
+
const isCallExpression = info.config.templateIsCallExpression ?? true;
const source = getSource(info, filename);
if (!source) return undefined;
···
if (
ts.isCallExpression(node) &&
isCallExpression &&
-
node.expression.getText() === tagTemplate &&
+
templates.has(node.expression.getText()) &&
node.arguments.length > 0 &&
ts.isNoSubstitutionTemplateLiteral(node.arguments[0])
) {
···
} else if (ts.isTaggedTemplateExpression(node)) {
const { template, tag } = node;
-
if (!ts.isIdentifier(tag) || tag.text !== tagTemplate) return undefined;
+
if (!ts.isIdentifier(tag) || !templates.has(tag.text)) return undefined;
const foundToken = getToken(template, cursorPosition);
if (!foundToken || !schema.current) return undefined;
+2 -4
packages/graphqlsp/src/checkImports.ts
···
info: ts.server.PluginCreateInfo
) => {
const imports = findAllImports(source);
-
const tagTemplate = info.config.template || 'gql';
const shouldCheckForColocatedFragments =
info.config.shouldCheckForColocatedFragments ?? false;
···
if (!declaration) return;
-
const [template] = findAllTaggedTemplateNodes(declaration, tagTemplate);
+
const [template] = findAllTaggedTemplateNodes(declaration);
if (template) {
let node = template;
if (
···
info: ts.server.PluginCreateInfo
): Array<FragmentDefinitionNode> {
let fragments: Array<FragmentDefinitionNode> = [];
-
const tagTemplate = info.config.template || 'gql';
-
const callExpressions = findAllCallExpressions(src, tagTemplate, info, false);
+
const callExpressions = findAllCallExpressions(src, info, false);
callExpressions.nodes.forEach(node => {
const text = resolveTemplate(node, src.fileName, info).combinedText;
+13 -11
packages/graphqlsp/src/diagnostics.ts
···
-
import ts from 'typescript/lib/tsserverlibrary';
+
import ts, { TaggedTemplateExpression } from 'typescript/lib/tsserverlibrary';
import { Diagnostic, getDiagnostics } from 'graphql-language-service';
import {
FragmentDefinitionNode,
···
schema: { current: GraphQLSchema | null; version: number },
info: ts.server.PluginCreateInfo
): ts.Diagnostic[] | undefined {
-
const tagTemplate = info.config.template || 'gql';
-
const isCallExpression = info.config.templateIsCallExpression ?? false;
+
const isCallExpression = info.config.templateIsCallExpression ?? true;
let source = getSource(info, filename);
if (!source) return undefined;
···
let fragments: Array<FragmentDefinitionNode> = [],
nodes: (ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral)[];
if (isCallExpression) {
-
const result = findAllCallExpressions(source, tagTemplate, info);
+
const result = findAllCallExpressions(source, info);
fragments = result.fragments;
nodes = result.nodes;
} else {
-
nodes = findAllTaggedTemplateNodes(source, tagTemplate);
+
nodes = findAllTaggedTemplateNodes(source);
}
const texts = nodes.map(node => {
···
schema: { current: GraphQLSchema | null; version: number },
info: ts.server.PluginCreateInfo
) => {
-
const tagTemplate = info.config.template || 'gql';
const filename = source.fileName;
-
const isCallExpression = info.config.templateIsCallExpression ?? false;
+
const isCallExpression = info.config.templateIsCallExpression ?? true;
const diagnostics = nodes
.map(originalNode => {
···
}
// When we are dealing with a plain gql statement we have to add two these can be recognised
// by the fact that the parent is an expressionStatement
+
let startingPosition =
node.pos +
-
(isCallExpression ? 0 : tagTemplate.length + (isExpression ? 2 : 1));
+
(isCallExpression
+
? 0
+
: (node as TaggedTemplateExpression).tag.getText().length +
+
(isExpression ? 2 : 1));
const endPosition = startingPosition + node.getText().length;
let docFragments = [...fragments];
···
start,
length,
} = moduleSpecifierToFragments[moduleSpecifier];
-
const missingFragments = Array.from(new Set(fragmentNames.filter(
-
x => !usedFragments.has(x)
-
)));
+
const missingFragments = Array.from(
+
new Set(fragmentNames.filter(x => !usedFragments.has(x)))
+
);
if (missingFragments.length) {
fragmentDiagnostics.push({
file: source,
+6 -14
packages/graphqlsp/src/index.ts
···
import { getGraphQLCompletions } from './autoComplete';
import { getGraphQLQuickInfo } from './quickInfo';
import { getGraphQLDiagnostics } from './diagnostics';
+
import { templates } from './ast/templates';
function createBasicDecorator(info: ts.server.PluginCreateInfo) {
const proxy: ts.LanguageService = Object.create(null);
···
type Config = {
schema: SchemaOrigin | string;
-
// TODO: rename to tag or just remove entirely and always check for
-
// gql and graphql.
-
template?: string;
-
// TODO: we need a bettername, gql.tada will also have
-
// call expressions, we can differentiate by means of
-
// tada having a second argument containing the fragments.
templateIsCallExpression?: boolean;
-
// Up in the air whether we want to keep supporting
-
// this. Current limitation are barrel-file exports
-
// could be counter-acted with an opinion on
-
// fragment-naming. Could become more relevant
-
// with gql.tada and could be useful for
-
// client-preset as well however the component type-annotations
-
// can better indicate a missing spread for the
-
// client-preset.
shouldCheckForColocatedFragments?: boolean;
+
template?: string;
+
trackFieldUsage?: boolean;
};
function create(info: ts.server.PluginCreateInfo) {
···
logger('Setting up the GraphQL Plugin');
+
if (config.template) {
+
templates.add(config.template);
+
}
const proxy = createBasicDecorator(info);
const schema = loadSchema(
+4 -4
packages/graphqlsp/src/quickInfo.ts
···
import { resolveTemplate } from './ast/resolve';
import { getToken } from './ast/token';
import { Cursor } from './ast/cursor';
+
import { templates } from './ast/templates';
export function getGraphQLQuickInfo(
filename: string,
···
schema: { current: GraphQLSchema | null },
info: ts.server.PluginCreateInfo
): ts.QuickInfo | undefined {
-
const tagTemplate = info.config.template || 'gql';
-
const isCallExpression = info.config.templateIsCallExpression ?? false;
+
const isCallExpression = info.config.templateIsCallExpression ?? true;
const source = getSource(info, filename);
if (!source) return undefined;
···
if (
ts.isCallExpression(node) &&
isCallExpression &&
-
node.expression.getText() === tagTemplate &&
+
templates.has(node.expression.getText()) &&
node.arguments.length > 0 &&
ts.isNoSubstitutionTemplateLiteral(node.arguments[0])
) {
···
cursor = new Cursor(foundToken.line, foundToken.start - 1);
} else if (ts.isTaggedTemplateExpression(node)) {
const { template, tag } = node;
-
if (!ts.isIdentifier(tag) || tag.text !== tagTemplate) return undefined;
+
if (!ts.isIdentifier(tag) || !templates.has(tag.text)) return undefined;
const foundToken = getToken(template, cursorPosition);
+1 -3
test/e2e/fixture-project-client-preset/tsconfig.json
···
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
"disableTypegen": true,
-
"shouldCheckForColocatedFragments": true,
-
"template": "graphql",
-
"templateIsCallExpression": true
+
"shouldCheckForColocatedFragments": true
}
],
"target": "es2016",
+1 -3
test/e2e/fixture-project-unused-fields/tsconfig.json
···
"schema": "./schema.graphql",
"disableTypegen": true,
"trackFieldUsage": true,
-
"shouldCheckForColocatedFragments": false,
-
"template": "graphql",
-
"templateIsCallExpression": true
+
"shouldCheckForColocatedFragments": false
}
],
"target": "es2016",
+2 -1
test/e2e/fixture-project/tsconfig.json
···
{
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
-
"shouldCheckForColocatedFragments": true
+
"shouldCheckForColocatedFragments": true,
+
"templateIsCallExpression": false
}
],
"target": "es2016",