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

deprecate and flip defaults (#166)

* deprecate and flip defaults

* remove test

* update readme

Changed files
+22 -237
.changeset
packages
test
e2e
+5
.changeset/silly-pots-leave.md
···
···
+
---
+
'@0no-co/graphqlsp': major
+
---
+
+
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
···
**Optional**
-
- `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
- `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about
-
unused fields within the same file.
### GraphQL Code Generator client-preset
···
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
"disableTypegen": true,
"shouldCheckForColocatedFragments": true,
-
"trackFieldUsage": true
}
]
}
···
**Optional**
+
- `template` add an additional template to the defaults `gql` and `graphql`
+
- `templateIsCallExpression` this tells our client that you are using `graphql('doc')` (default: true)
+
when using `false` it will look for tagged template literals
- `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find
+
unused fragments and provide a message notifying you about them (only works with call-expressions, default: true)
- `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about
+
unused fields within the same file. (only works with call-expressions, default: true)
### GraphQL Code Generator client-preset
···
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
"disableTypegen": true,
+
"templateIsCallExpression": true,
"shouldCheckForColocatedFragments": true,
+
"trackFieldUsage": true,
+
"template": "graphql"
}
]
}
+5 -4
packages/graphqlsp/README.md
···
**Optional**
-
- `template` the shape of your template, by default `gql`
-
- `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
- `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about
-
unused fields within the same file.
### GraphQL Code Generator client-preset
···
**Optional**
+
- `template` add an additional template to the defaults `gql` and `graphql`
+
- `templateIsCallExpression` this tells our client that you are using `graphql('doc')` (default: true)
+
when using `false` it will look for tagged template literals
- `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find
+
unused fragments and provide a message notifying you about them (only works with call-expressions, default: true)
- `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about
+
unused fields within the same file. (only works with call-expressions, default: true)
### GraphQL Code Generator client-preset
+1 -112
packages/graphqlsp/src/checkImports.ts
···
import ts from 'typescript/lib/tsserverlibrary';
import { FragmentDefinitionNode, Kind, parse } from 'graphql';
-
import {
-
findAllCallExpressions,
-
findAllImports,
-
findAllTaggedTemplateNodes,
-
getSource,
-
} from './ast';
import { resolveTemplate } from './ast/resolve';
export const MISSING_FRAGMENT_CODE = 52003;
-
-
export const checkImportsForFragments = (
-
source: ts.SourceFile,
-
info: ts.server.PluginCreateInfo
-
) => {
-
const imports = findAllImports(source);
-
-
const shouldCheckForColocatedFragments =
-
info.config.shouldCheckForColocatedFragments ?? false;
-
const tsDiagnostics: ts.Diagnostic[] = [];
-
if (imports.length && shouldCheckForColocatedFragments) {
-
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
-
imports.forEach(imp => {
-
if (!imp.importClause) return;
-
-
const importedNames: string[] = [];
-
if (imp.importClause.name) {
-
importedNames.push(imp.importClause?.name.text);
-
}
-
-
if (
-
imp.importClause.namedBindings &&
-
ts.isNamespaceImport(imp.importClause.namedBindings)
-
) {
-
// TODO: we might need to warn here when the fragment is unused as a namespace import
-
return;
-
} else if (
-
imp.importClause.namedBindings &&
-
ts.isNamedImportBindings(imp.importClause.namedBindings)
-
) {
-
imp.importClause.namedBindings.elements.forEach(el => {
-
importedNames.push(el.name.text);
-
});
-
}
-
-
const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier);
-
if (!symbol) return;
-
-
const moduleExports = typeChecker?.getExportsOfModule(symbol);
-
if (!moduleExports) return;
-
-
const missingImports = new Set<string>();
-
moduleExports.forEach(exp => {
-
if (importedNames.includes(exp.name)) {
-
return;
-
}
-
-
const declarations = exp.getDeclarations();
-
const declaration = declarations?.find(x => {
-
// TODO: check whether the sourceFile.fileName resembles the module
-
// specifier
-
return true;
-
});
-
-
if (!declaration) return;
-
-
const [template] = findAllTaggedTemplateNodes(declaration);
-
if (template) {
-
let node = template;
-
if (
-
ts.isNoSubstitutionTemplateLiteral(node) ||
-
ts.isTemplateExpression(node)
-
) {
-
if (ts.isTaggedTemplateExpression(node.parent)) {
-
node = node.parent;
-
} else {
-
return;
-
}
-
}
-
-
const text = resolveTemplate(
-
node,
-
node.getSourceFile().fileName,
-
info
-
).combinedText;
-
try {
-
const parsed = parse(text, { noLocation: true });
-
if (
-
parsed.definitions.every(x => x.kind === Kind.FRAGMENT_DEFINITION)
-
) {
-
missingImports.add(`'${exp.name}'`);
-
}
-
} catch (e) {
-
return;
-
}
-
}
-
});
-
-
const missing = Array.from(missingImports);
-
if (missing.length) {
-
tsDiagnostics.push({
-
file: source,
-
length: imp.getText().length,
-
start: imp.getStart(),
-
category: ts.DiagnosticCategory.Message,
-
code: MISSING_FRAGMENT_CODE,
-
messageText: `Missing Fragment import(s) ${missing.join(
-
', '
-
)} from ${imp.moduleSpecifier.getText()}.`,
-
});
-
}
-
});
-
}
-
-
return tsDiagnostics;
-
};
export const getColocatedFragmentNames = (
source: ts.SourceFile,
···
import ts from 'typescript/lib/tsserverlibrary';
import { FragmentDefinitionNode, Kind, parse } from 'graphql';
+
import { findAllCallExpressions, findAllImports, getSource } from './ast';
import { resolveTemplate } from './ast/resolve';
export const MISSING_FRAGMENT_CODE = 52003;
export const getColocatedFragmentNames = (
source: ts.SourceFile,
+2 -8
packages/graphqlsp/src/diagnostics.ts
···
import { checkFieldUsageInFile } from './fieldUsage';
import {
MISSING_FRAGMENT_CODE,
-
checkImportsForFragments,
getColocatedFragmentNames,
} from './checkImports';
···
);
const shouldCheckForColocatedFragments =
-
info.config.shouldCheckForColocatedFragments ?? false;
let fragmentDiagnostics: ts.Diagnostic[] = [];
-
console.log(
-
'[GraphhQLSP] Checking for colocated fragments ',
-
!!shouldCheckForColocatedFragments
-
);
if (shouldCheckForColocatedFragments) {
const moduleSpecifierToFragments = getColocatedFragmentNames(
source,
···
return [...tsDiagnostics, ...usageDiagnostics, ...fragmentDiagnostics];
} else {
-
const importDiagnostics = checkImportsForFragments(source, info);
-
return [...tsDiagnostics, ...importDiagnostics];
}
};
···
import { checkFieldUsageInFile } from './fieldUsage';
import {
MISSING_FRAGMENT_CODE,
getColocatedFragmentNames,
} from './checkImports';
···
);
const shouldCheckForColocatedFragments =
+
info.config.shouldCheckForColocatedFragments ?? true;
let fragmentDiagnostics: ts.Diagnostic[] = [];
if (shouldCheckForColocatedFragments) {
const moduleSpecifierToFragments = getColocatedFragmentNames(
source,
···
return [...tsDiagnostics, ...usageDiagnostics, ...fragmentDiagnostics];
} else {
+
return tsDiagnostics;
}
};
+1 -1
packages/graphqlsp/src/fieldUsage.ts
···
info: ts.server.PluginCreateInfo
) => {
const diagnostics: ts.Diagnostic[] = [];
-
const shouldTrackFieldUsage = info.config.trackFieldUsage ?? false;
if (!shouldTrackFieldUsage) return diagnostics;
nodes.forEach(node => {
···
info: ts.server.PluginCreateInfo
) => {
const diagnostics: ts.Diagnostic[] = [];
+
const shouldTrackFieldUsage = info.config.trackFieldUsage ?? true;
if (!shouldTrackFieldUsage) return diagnostics;
nodes.forEach(node => {
-1
test/e2e/fixture-project/tsconfig.json
···
{
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
-
"shouldCheckForColocatedFragments": true,
"templateIsCallExpression": false
}
],
···
{
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
"templateIsCallExpression": false
}
],
-106
test/e2e/fragments.test.ts
···
-
import { expect, afterAll, beforeAll, it, describe } from 'vitest';
-
import { TSServer } from './server';
-
import path from 'node:path';
-
import fs from 'node:fs';
-
import url from 'node:url';
-
import ts from 'typescript/lib/tsserverlibrary';
-
-
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
-
-
const projectPath = path.resolve(__dirname, 'fixture-project');
-
describe('Fragments', () => {
-
const outFilePost = path.join(projectPath, 'Post.ts');
-
const outFilePosts = path.join(projectPath, 'Posts.ts');
-
-
let server: TSServer;
-
beforeAll(async () => {
-
server = new TSServer(projectPath, { debugLog: false });
-
});
-
-
afterAll(() => {
-
try {
-
fs.unlinkSync(outFilePost);
-
fs.unlinkSync(outFilePosts);
-
} catch {}
-
});
-
-
it('should send a message for missing fragment import', async () => {
-
server.sendCommand('open', {
-
file: outFilePost,
-
fileContent: '// empty',
-
scriptKindName: 'TS',
-
} satisfies ts.server.protocol.OpenRequestArgs);
-
-
server.sendCommand('open', {
-
file: outFilePosts,
-
fileContent: '// empty',
-
scriptKindName: 'TS',
-
} satisfies ts.server.protocol.OpenRequestArgs);
-
-
server.sendCommand('updateOpen', {
-
openFiles: [
-
{
-
file: outFilePosts,
-
fileContent: fs.readFileSync(
-
path.join(projectPath, 'fixtures/Posts.ts'),
-
'utf-8'
-
),
-
},
-
{
-
file: outFilePost,
-
fileContent: fs.readFileSync(
-
path.join(projectPath, 'fixtures/Post.ts'),
-
'utf-8'
-
),
-
},
-
],
-
} satisfies ts.server.protocol.UpdateOpenRequestArgs);
-
-
server.sendCommand('saveto', {
-
file: outFilePost,
-
tmpfile: outFilePost,
-
} satisfies ts.server.protocol.SavetoRequestArgs);
-
-
server.sendCommand('saveto', {
-
file: outFilePosts,
-
tmpfile: outFilePosts,
-
} satisfies ts.server.protocol.SavetoRequestArgs);
-
-
server.sendCommand('saveto', {
-
file: outFilePosts,
-
tmpfile: outFilePosts,
-
} satisfies ts.server.protocol.SavetoRequestArgs);
-
-
await server.waitForResponse(
-
response =>
-
response.type === 'event' &&
-
response.event === 'semanticDiag' &&
-
response.body.file === outFilePosts
-
);
-
-
const res = server.responses
-
.reverse()
-
.find(
-
resp =>
-
resp.type === 'event' &&
-
resp.event === 'semanticDiag' &&
-
resp.body.file === outFilePosts
-
);
-
-
expect(res?.body.diagnostics).toEqual([
-
{
-
category: 'message',
-
code: 52003,
-
end: {
-
line: 2,
-
offset: 31,
-
},
-
start: {
-
line: 2,
-
offset: 1,
-
},
-
text: 'Missing Fragment import(s) \'PostFields\' from "./Post".',
-
},
-
]);
-
}, 30000);
-
});
···