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>

Changed files
+167 -54
.changeset
packages
example-tada
src
graphqlsp
+5
.changeset/cuddly-needles-glow.md
···
+
---
+
'@0no-co/graphqlsp': minor
+
---
+
+
Support passing GraphQL documents by value to `graphql.persisted`’s second argument
+1 -1
packages/example-tada/src/index.tsx
···
}
`, [PokemonFields, Fields.Pokemon]);
-
const persisted = graphql.persisted<typeof PokemonQuery>("sha256:dc31ff9637bbc77bb95dffb2ca73b0e607639b018befd06e9ad801b54483d661")
+
const persisted = graphql.persisted<typeof PokemonQuery>("sha256:7a9bbe8533362e631f92af8d7f314b1589c8272f8e092da564d9ad6cd600a4eb")
const Pokemons = () => {
const [result] = useQuery({
+4 -1
packages/graphqlsp/src/api.ts
···
export { getGraphQLDiagnostics } from './diagnostics';
export { init } from './ts';
export { findAllPersistedCallExpressions, unrollTadaFragments } from './ast';
-
export { getDocumentReferenceFromTypeQuery } from './persisted';
+
export {
+
getDocumentReferenceFromTypeQuery,
+
getDocumentReferenceFromDocumentNode,
+
} from './persisted';
+73 -33
packages/graphqlsp/src/diagnostics.ts
···
MISSING_FRAGMENT_CODE,
getColocatedFragmentNames,
} from './checkImports';
-
import { getDocumentReferenceFromTypeQuery } from './persisted';
+
import {
+
getDocumentReferenceFromDocumentNode,
+
getDocumentReferenceFromTypeQuery,
+
} from './persisted';
const clientDirectives = new Set([
'populate',
···
// document but this removes support for self-generating identifiers
const persistedDiagnostics = persistedCalls
.map<ts.Diagnostic | null>(callExpression => {
-
if (!callExpression.typeArguments) {
+
if (!callExpression.typeArguments && !callExpression.arguments[1]) {
return {
category: ts.DiagnosticCategory.Warning,
code: MISSING_PERSISTED_TYPE_ARG,
···
};
}
-
const [typeQuery] = callExpression.typeArguments;
+
let foundNode,
+
foundFilename = filename,
+
ref,
+
start,
+
length;
+
if (callExpression.typeArguments) {
+
const [typeQuery] = callExpression.typeArguments;
+
start = typeQuery.getStart();
+
length = typeQuery.getEnd() - typeQuery.getStart();
-
if (!ts.isTypeQueryNode(typeQuery)) {
-
// Provide diagnostic about wroong generic
-
return {
-
category: ts.DiagnosticCategory.Warning,
-
code: MISSING_PERSISTED_TYPE_ARG,
-
file: source,
-
messageText:
-
'Provided generic should be a typeQueryNode in the shape of graphql.persisted<typeof document>.',
-
start: typeQuery.getStart(),
-
length: typeQuery.getEnd() - typeQuery.getStart(),
-
};
-
}
+
if (!ts.isTypeQueryNode(typeQuery)) {
+
return {
+
category: ts.DiagnosticCategory.Warning,
+
code: MISSING_PERSISTED_TYPE_ARG,
+
file: source,
+
messageText:
+
'Provided generic should be a typeQueryNode in the shape of graphql.persisted<typeof document>.',
+
start,
+
length,
+
};
+
}
+
const { node: found, filename: fileName } =
+
getDocumentReferenceFromTypeQuery(typeQuery, filename, info);
+
foundNode = found;
+
foundFilename = fileName;
+
ref = typeQuery.getText();
+
} else if (callExpression.arguments[1]) {
+
start = callExpression.arguments[1].getStart();
+
length =
+
callExpression.arguments[1].getEnd() -
+
callExpression.arguments[1].getStart();
+
if (
+
!ts.isIdentifier(callExpression.arguments[1]) &&
+
!ts.isCallExpression(callExpression.arguments[1])
+
) {
+
return {
+
category: ts.DiagnosticCategory.Warning,
+
code: MISSING_PERSISTED_TYPE_ARG,
+
file: source,
+
messageText:
+
'Provided argument should be an identifier or invocation of "graphql" in the shape of graphql.persisted(hash, document).',
+
start,
+
length,
+
};
+
}
-
const { node: foundNode } = getDocumentReferenceFromTypeQuery(
-
typeQuery,
-
filename,
-
info
-
);
+
const { node: found, filename: fileName } =
+
getDocumentReferenceFromDocumentNode(
+
callExpression.arguments[1],
+
filename,
+
info
+
);
+
foundNode = found;
+
foundFilename = fileName;
+
ref = callExpression.arguments[1].getText();
+
}
if (!foundNode) {
return {
category: ts.DiagnosticCategory.Warning,
code: MISSING_PERSISTED_DOCUMENT,
file: source,
-
messageText: `Can't find reference to "${typeQuery.getText()}".`,
-
start: typeQuery.getStart(),
-
length: typeQuery.getEnd() - typeQuery.getStart(),
+
messageText: `Can't find reference to "${ref}".`,
+
start,
+
length,
};
}
-
const initializer = foundNode.initializer;
+
const initializer = foundNode;
if (
!initializer ||
!ts.isCallExpression(initializer) ||
···
category: ts.DiagnosticCategory.Warning,
code: MISSING_PERSISTED_DOCUMENT,
file: source,
-
messageText: `Referenced type "${typeQuery.getText()}" is not a GraphQL document.`,
-
start: typeQuery.getStart(),
-
length: typeQuery.getEnd() - typeQuery.getStart(),
+
messageText: `Referenced type "${ref}" is not a GraphQL document.`,
+
start,
+
length,
};
}
···
}));
if (isCallExpression) {
-
const usageDiagnostics = checkFieldUsageInFile(
-
source,
-
nodes as ts.NoSubstitutionTemplateLiteral[],
-
info
-
) || [];
+
const usageDiagnostics =
+
checkFieldUsageInFile(
+
source,
+
nodes as ts.NoSubstitutionTemplateLiteral[],
+
info
+
) || [];
-
if (!usageDiagnostics) return tsDiagnostics
+
if (!usageDiagnostics) return tsDiagnostics;
return [...tsDiagnostics, ...usageDiagnostics];
} else {
+78 -13
packages/graphqlsp/src/persisted.ts
···
if (
!ts.isCallExpression(callExpression) ||
!isPersistedCall(callExpression.expression) ||
-
!callExpression.typeArguments
+
(!callExpression.typeArguments && !callExpression.arguments[1])
)
return undefined;
-
const [typeQuery] = callExpression.typeArguments;
-
-
if (!ts.isTypeQueryNode(typeQuery)) return undefined;
-
-
const { node: found, filename: foundFilename } =
-
getDocumentReferenceFromTypeQuery(typeQuery, filename, info);
+
let foundNode,
+
foundFilename = filename;
+
if (callExpression.typeArguments) {
+
const [typeQuery] = callExpression.typeArguments;
+
if (!ts.isTypeQueryNode(typeQuery)) return undefined;
+
const { node: found, filename: fileName } =
+
getDocumentReferenceFromTypeQuery(typeQuery, filename, info);
+
foundNode = found;
+
foundFilename = fileName;
+
} else if (callExpression.arguments[1]) {
+
if (
+
!ts.isIdentifier(callExpression.arguments[1]) &&
+
!ts.isCallExpression(callExpression.arguments[1])
+
)
+
return undefined;
+
const { node: found, filename: fileName } =
+
getDocumentReferenceFromDocumentNode(
+
callExpression.arguments[1],
+
filename,
+
info
+
);
+
foundNode = found;
+
foundFilename = fileName;
+
}
-
if (!found) return undefined;
+
if (!foundNode) return undefined;
-
const initializer = found.initializer;
+
const initializer = foundNode;
if (
!initializer ||
!ts.isCallExpression(initializer) ||
···
typeQuery: ts.TypeQueryNode,
filename: string,
info: ts.server.PluginCreateInfo
-
): { node: ts.VariableDeclaration | null; filename: string } => {
+
): { node: ts.CallExpression | null; filename: string } => {
// We look for the references of the generic so that we can use the document
// to generate the hash.
const references = info.languageService.getReferencesAtPosition(
···
if (!references) return { node: null, filename };
-
let found: ts.VariableDeclaration | null = null;
+
let found: ts.CallExpression | null = null;
let foundFilename = filename;
references.forEach(ref => {
if (found) return;
···
const foundNode = findNode(source, ref.textSpan.start);
if (!foundNode) return;
-
if (ts.isVariableDeclaration(foundNode.parent)) {
-
found = foundNode.parent;
+
if (
+
ts.isVariableDeclaration(foundNode.parent) &&
+
foundNode.parent.initializer &&
+
ts.isCallExpression(foundNode.parent.initializer) &&
+
templates.has(foundNode.parent.initializer.expression.getText())
+
) {
+
found = foundNode.parent.initializer;
foundFilename = ref.fileName;
}
});
return { node: found, filename: foundFilename };
};
+
+
export const getDocumentReferenceFromDocumentNode = (
+
documentNodeArgument: ts.Identifier | ts.CallExpression,
+
filename: string,
+
info: ts.server.PluginCreateInfo
+
): { node: ts.CallExpression | null; filename: string } => {
+
if (ts.isIdentifier(documentNodeArgument)) {
+
// We look for the references of the generic so that we can use the document
+
// to generate the hash.
+
const references = info.languageService.getReferencesAtPosition(
+
filename,
+
documentNodeArgument.getStart()
+
);
+
+
if (!references) return { node: null, filename };
+
+
let found: ts.CallExpression | null = null;
+
let foundFilename = filename;
+
references.forEach(ref => {
+
if (found) return;
+
+
const source = getSource(info, ref.fileName);
+
if (!source) return;
+
const foundNode = findNode(source, ref.textSpan.start);
+
if (!foundNode) return;
+
+
if (
+
ts.isVariableDeclaration(foundNode.parent) &&
+
foundNode.parent.initializer &&
+
ts.isCallExpression(foundNode.parent.initializer) &&
+
templates.has(foundNode.parent.initializer.expression.getText())
+
) {
+
found = foundNode.parent.initializer;
+
foundFilename = ref.fileName;
+
}
+
});
+
+
return { node: found, filename: foundFilename };
+
} else {
+
return { node: documentNodeArgument, filename };
+
}
+
};
+6 -6
pnpm-lock.yaml
···
resolution: {integrity: sha512-8I4Z1zxYYGK66FWdB3yIZBn3cITLPnciEgjChp3K2+Ha1e/AEBGtZv9AUlodraO/RZafDMkpFhoi+tMpluBjeg==}
peerDependencies:
graphql: ^16.8.1
-
typescript: ^5.3.3
+
typescript: ^5.0.0
dependencies:
'@0no-co/graphql.web': 1.0.6(graphql@16.8.1)
graphql: 16.8.1
···
peerDependencies:
rollup: ^2.14.0||^3.0.0||^4.0.0
tslib: '*'
-
typescript: ^5.3.3
+
typescript: '>=3.7.0'
peerDependenciesMeta:
rollup:
optional: true
···
resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
engines: {node: '>=14'}
peerDependencies:
-
typescript: ^5.3.3
+
typescript: '>=4.9.5'
peerDependenciesMeta:
typescript:
optional: true
···
engines: {node: '>=16'}
peerDependencies:
rollup: ^3.29.4 || ^4
-
typescript: ^5.3.3
+
typescript: ^4.5 || ^5.0
dependencies:
magic-string: 0.30.5
rollup: 4.9.5
···
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
-
typescript: ^5.3.3
+
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
···
id: file:packages/graphqlsp
name: '@0no-co/graphqlsp'
peerDependencies:
-
typescript: ^5.3.3
+
typescript: ^5.0.0
dependencies:
'@gql.tada/internal': 0.1.2(graphql@16.8.1)(typescript@5.3.3)
graphql: 16.8.1