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.

deprecate and flip defaults (#166)

* deprecate and flip defaults

* remove test

* update readme

authored by

Jovi De Croock and committed by
GitHub
2b4f99dd 8bffbe9a

+22 -237
+5
.changeset/silly-pots-leave.md
··· 1 + --- 2 + '@0no-co/graphqlsp': major 3 + --- 4 + 5 + Remove `fragment-checking` from tagged-templates due to issues with barrel-file exports and flip defaults for field usage and import tracking with call-expressions
+8 -5
README.md
··· 60 60 61 61 **Optional** 62 62 63 - - `template` the shape of your template, by default `gql` and `graphql` are respected 64 - - `templateIsCallExpression` this tells our client that you are using `graphql('doc')` 63 + - `template` add an additional template to the defaults `gql` and `graphql` 64 + - `templateIsCallExpression` this tells our client that you are using `graphql('doc')` (default: true) 65 + when using `false` it will look for tagged template literals 65 66 - `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find 66 - unused fragments and provide a message notifying you about them 67 + unused fragments and provide a message notifying you about them (only works with call-expressions, default: true) 67 68 - `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about 68 - unused fields within the same file. 69 + unused fields within the same file. (only works with call-expressions, default: true) 69 70 70 71 ### GraphQL Code Generator client-preset 71 72 ··· 79 80 "name": "@0no-co/graphqlsp", 80 81 "schema": "./schema.graphql", 81 82 "disableTypegen": true, 83 + "templateIsCallExpression": true, 82 84 "shouldCheckForColocatedFragments": true, 83 - "trackFieldUsage": true 85 + "trackFieldUsage": true, 86 + "template": "graphql" 84 87 } 85 88 ] 86 89 }
+5 -4
packages/graphqlsp/README.md
··· 60 60 61 61 **Optional** 62 62 63 - - `template` the shape of your template, by default `gql` 64 - - `templateIsCallExpression` this tells our client that you are using `graphql('doc')` 63 + - `template` add an additional template to the defaults `gql` and `graphql` 64 + - `templateIsCallExpression` this tells our client that you are using `graphql('doc')` (default: true) 65 + when using `false` it will look for tagged template literals 65 66 - `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find 66 - unused fragments and provide a message notifying you about them 67 + unused fragments and provide a message notifying you about them (only works with call-expressions, default: true) 67 68 - `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about 68 - unused fields within the same file. 69 + unused fields within the same file. (only works with call-expressions, default: true) 69 70 70 71 ### GraphQL Code Generator client-preset 71 72
+1 -112
packages/graphqlsp/src/checkImports.ts
··· 1 1 import ts from 'typescript/lib/tsserverlibrary'; 2 2 import { FragmentDefinitionNode, Kind, parse } from 'graphql'; 3 3 4 - import { 5 - findAllCallExpressions, 6 - findAllImports, 7 - findAllTaggedTemplateNodes, 8 - getSource, 9 - } from './ast'; 4 + import { findAllCallExpressions, findAllImports, getSource } from './ast'; 10 5 import { resolveTemplate } from './ast/resolve'; 11 6 12 7 export const MISSING_FRAGMENT_CODE = 52003; 13 - 14 - export const checkImportsForFragments = ( 15 - source: ts.SourceFile, 16 - info: ts.server.PluginCreateInfo 17 - ) => { 18 - const imports = findAllImports(source); 19 - 20 - const shouldCheckForColocatedFragments = 21 - info.config.shouldCheckForColocatedFragments ?? false; 22 - const tsDiagnostics: ts.Diagnostic[] = []; 23 - if (imports.length && shouldCheckForColocatedFragments) { 24 - const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 25 - imports.forEach(imp => { 26 - if (!imp.importClause) return; 27 - 28 - const importedNames: string[] = []; 29 - if (imp.importClause.name) { 30 - importedNames.push(imp.importClause?.name.text); 31 - } 32 - 33 - if ( 34 - imp.importClause.namedBindings && 35 - ts.isNamespaceImport(imp.importClause.namedBindings) 36 - ) { 37 - // TODO: we might need to warn here when the fragment is unused as a namespace import 38 - return; 39 - } else if ( 40 - imp.importClause.namedBindings && 41 - ts.isNamedImportBindings(imp.importClause.namedBindings) 42 - ) { 43 - imp.importClause.namedBindings.elements.forEach(el => { 44 - importedNames.push(el.name.text); 45 - }); 46 - } 47 - 48 - const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier); 49 - if (!symbol) return; 50 - 51 - const moduleExports = typeChecker?.getExportsOfModule(symbol); 52 - if (!moduleExports) return; 53 - 54 - const missingImports = new Set<string>(); 55 - moduleExports.forEach(exp => { 56 - if (importedNames.includes(exp.name)) { 57 - return; 58 - } 59 - 60 - const declarations = exp.getDeclarations(); 61 - const declaration = declarations?.find(x => { 62 - // TODO: check whether the sourceFile.fileName resembles the module 63 - // specifier 64 - return true; 65 - }); 66 - 67 - if (!declaration) return; 68 - 69 - const [template] = findAllTaggedTemplateNodes(declaration); 70 - if (template) { 71 - let node = template; 72 - if ( 73 - ts.isNoSubstitutionTemplateLiteral(node) || 74 - ts.isTemplateExpression(node) 75 - ) { 76 - if (ts.isTaggedTemplateExpression(node.parent)) { 77 - node = node.parent; 78 - } else { 79 - return; 80 - } 81 - } 82 - 83 - const text = resolveTemplate( 84 - node, 85 - node.getSourceFile().fileName, 86 - info 87 - ).combinedText; 88 - try { 89 - const parsed = parse(text, { noLocation: true }); 90 - if ( 91 - parsed.definitions.every(x => x.kind === Kind.FRAGMENT_DEFINITION) 92 - ) { 93 - missingImports.add(`'${exp.name}'`); 94 - } 95 - } catch (e) { 96 - return; 97 - } 98 - } 99 - }); 100 - 101 - const missing = Array.from(missingImports); 102 - if (missing.length) { 103 - tsDiagnostics.push({ 104 - file: source, 105 - length: imp.getText().length, 106 - start: imp.getStart(), 107 - category: ts.DiagnosticCategory.Message, 108 - code: MISSING_FRAGMENT_CODE, 109 - messageText: `Missing Fragment import(s) ${missing.join( 110 - ', ' 111 - )} from ${imp.moduleSpecifier.getText()}.`, 112 - }); 113 - } 114 - }); 115 - } 116 - 117 - return tsDiagnostics; 118 - }; 119 8 120 9 export const getColocatedFragmentNames = ( 121 10 source: ts.SourceFile,
+2 -8
packages/graphqlsp/src/diagnostics.ts
··· 21 21 import { checkFieldUsageInFile } from './fieldUsage'; 22 22 import { 23 23 MISSING_FRAGMENT_CODE, 24 - checkImportsForFragments, 25 24 getColocatedFragmentNames, 26 25 } from './checkImports'; 27 26 ··· 306 305 ); 307 306 308 307 const shouldCheckForColocatedFragments = 309 - info.config.shouldCheckForColocatedFragments ?? false; 308 + info.config.shouldCheckForColocatedFragments ?? true; 310 309 let fragmentDiagnostics: ts.Diagnostic[] = []; 311 - console.log( 312 - '[GraphhQLSP] Checking for colocated fragments ', 313 - !!shouldCheckForColocatedFragments 314 - ); 315 310 if (shouldCheckForColocatedFragments) { 316 311 const moduleSpecifierToFragments = getColocatedFragmentNames( 317 312 source, ··· 358 353 359 354 return [...tsDiagnostics, ...usageDiagnostics, ...fragmentDiagnostics]; 360 355 } else { 361 - const importDiagnostics = checkImportsForFragments(source, info); 362 - return [...tsDiagnostics, ...importDiagnostics]; 356 + return tsDiagnostics; 363 357 } 364 358 };
+1 -1
packages/graphqlsp/src/fieldUsage.ts
··· 176 176 info: ts.server.PluginCreateInfo 177 177 ) => { 178 178 const diagnostics: ts.Diagnostic[] = []; 179 - const shouldTrackFieldUsage = info.config.trackFieldUsage ?? false; 179 + const shouldTrackFieldUsage = info.config.trackFieldUsage ?? true; 180 180 if (!shouldTrackFieldUsage) return diagnostics; 181 181 182 182 nodes.forEach(node => {
-1
test/e2e/fixture-project/tsconfig.json
··· 4 4 { 5 5 "name": "@0no-co/graphqlsp", 6 6 "schema": "./schema.graphql", 7 - "shouldCheckForColocatedFragments": true, 8 7 "templateIsCallExpression": false 9 8 } 10 9 ],
-106
test/e2e/fragments.test.ts
··· 1 - import { expect, afterAll, beforeAll, it, describe } from 'vitest'; 2 - import { TSServer } from './server'; 3 - import path from 'node:path'; 4 - import fs from 'node:fs'; 5 - import url from 'node:url'; 6 - import ts from 'typescript/lib/tsserverlibrary'; 7 - 8 - const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 9 - 10 - const projectPath = path.resolve(__dirname, 'fixture-project'); 11 - describe('Fragments', () => { 12 - const outFilePost = path.join(projectPath, 'Post.ts'); 13 - const outFilePosts = path.join(projectPath, 'Posts.ts'); 14 - 15 - let server: TSServer; 16 - beforeAll(async () => { 17 - server = new TSServer(projectPath, { debugLog: false }); 18 - }); 19 - 20 - afterAll(() => { 21 - try { 22 - fs.unlinkSync(outFilePost); 23 - fs.unlinkSync(outFilePosts); 24 - } catch {} 25 - }); 26 - 27 - it('should send a message for missing fragment import', async () => { 28 - server.sendCommand('open', { 29 - file: outFilePost, 30 - fileContent: '// empty', 31 - scriptKindName: 'TS', 32 - } satisfies ts.server.protocol.OpenRequestArgs); 33 - 34 - server.sendCommand('open', { 35 - file: outFilePosts, 36 - fileContent: '// empty', 37 - scriptKindName: 'TS', 38 - } satisfies ts.server.protocol.OpenRequestArgs); 39 - 40 - server.sendCommand('updateOpen', { 41 - openFiles: [ 42 - { 43 - file: outFilePosts, 44 - fileContent: fs.readFileSync( 45 - path.join(projectPath, 'fixtures/Posts.ts'), 46 - 'utf-8' 47 - ), 48 - }, 49 - { 50 - file: outFilePost, 51 - fileContent: fs.readFileSync( 52 - path.join(projectPath, 'fixtures/Post.ts'), 53 - 'utf-8' 54 - ), 55 - }, 56 - ], 57 - } satisfies ts.server.protocol.UpdateOpenRequestArgs); 58 - 59 - server.sendCommand('saveto', { 60 - file: outFilePost, 61 - tmpfile: outFilePost, 62 - } satisfies ts.server.protocol.SavetoRequestArgs); 63 - 64 - server.sendCommand('saveto', { 65 - file: outFilePosts, 66 - tmpfile: outFilePosts, 67 - } satisfies ts.server.protocol.SavetoRequestArgs); 68 - 69 - server.sendCommand('saveto', { 70 - file: outFilePosts, 71 - tmpfile: outFilePosts, 72 - } satisfies ts.server.protocol.SavetoRequestArgs); 73 - 74 - await server.waitForResponse( 75 - response => 76 - response.type === 'event' && 77 - response.event === 'semanticDiag' && 78 - response.body.file === outFilePosts 79 - ); 80 - 81 - const res = server.responses 82 - .reverse() 83 - .find( 84 - resp => 85 - resp.type === 'event' && 86 - resp.event === 'semanticDiag' && 87 - resp.body.file === outFilePosts 88 - ); 89 - 90 - expect(res?.body.diagnostics).toEqual([ 91 - { 92 - category: 'message', 93 - code: 52003, 94 - end: { 95 - line: 2, 96 - offset: 31, 97 - }, 98 - start: { 99 - line: 2, 100 - offset: 1, 101 - }, 102 - text: 'Missing Fragment import(s) \'PostFields\' from "./Post".', 103 - }, 104 - ]); 105 - }, 30000); 106 - });