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

allow specifying headers to get introspection (#87)

* allow specifying headers to get introspection

* general improvements

Changed files
+64 -51
.changeset
packages
+5
.changeset/cuddly-lobsters-return.md
···
+
---
+
'@0no-co/graphqlsp': minor
+
---
+
+
Allow specifying headers for fetching the introspection
+1 -3
packages/example/src/Pokemon.ts
···
fragment pokemonFields on Pokemon {
id
name
-
...someUnknownFragment
attacks {
fast {
damage
···
export const WeakFields = gql`
fragment weaknessFields on Pokemon {
weaknesses
-
someUnknownField
}
-
` as typeof import('./Pokemon.generated').PokemonFieldsFragmentDoc;
+
` as typeof import('./Pokemon.generated').WeaknessFieldsFragmentDoc;
export const Pokemon = (data: any) => {
const pokemon = useFragment(PokemonFields, data);
+11 -30
packages/example/src/index.generated.ts
···
__typename: 'Pokemon';
id: string;
name: string;
-
resistant?: Array<Types.PokemonType | null> | null;
attacks?: {
__typename: 'AttacksConnection';
fast?: Array<{
···
} | null;
};
-
export type MoreFieldsFragment = {
+
export type WeaknessFieldsFragment = {
__typename: 'Pokemon';
-
resistant?: Array<Types.PokemonType | null> | null;
+
weaknesses?: Array<Types.PokemonType | null> | null;
};
export type PoQueryVariables = Types.Exact<{
···
} | null;
};
-
export const MoreFieldsFragmentDoc = {
-
kind: 'Document',
-
definitions: [
-
{
-
kind: 'FragmentDefinition',
-
name: { kind: 'Name', value: 'moreFields' },
-
typeCondition: {
-
kind: 'NamedType',
-
name: { kind: 'Name', value: 'Pokemon' },
-
},
-
selectionSet: {
-
kind: 'SelectionSet',
-
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'resistant' } },
-
],
-
},
-
},
-
],
-
} as unknown as DocumentNode<MoreFieldsFragment, unknown>;
export const PokemonFieldsFragmentDoc = {
kind: 'Document',
definitions: [
···
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
-
{
-
kind: 'FragmentSpread',
-
name: { kind: 'Name', value: 'moreFields' },
-
},
{
kind: 'Field',
name: { kind: 'Name', value: 'attacks' },
···
],
},
},
+
],
+
} as unknown as DocumentNode<PokemonFieldsFragment, unknown>;
+
export const WeaknessFieldsFragmentDoc = {
+
kind: 'Document',
+
definitions: [
{
kind: 'FragmentDefinition',
-
name: { kind: 'Name', value: 'moreFields' },
+
name: { kind: 'Name', value: 'weaknessFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'Pokemon' },
···
selectionSet: {
kind: 'SelectionSet',
selections: [
-
{ kind: 'Field', name: { kind: 'Name', value: 'resistant' } },
+
{ kind: 'Field', name: { kind: 'Name', value: 'weaknesses' } },
],
},
},
],
-
} as unknown as DocumentNode<PokemonFieldsFragment, unknown>;
+
} as unknown as DocumentNode<WeaknessFieldsFragment, unknown>;
export const PokDocument = {
kind: 'Document',
definitions: [
···
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
-
{ kind: 'Field', name: { kind: 'Name', value: '__typename' } },
{ kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } },
+
{ kind: 'Field', name: { kind: 'Name', value: '__typename' } },
],
},
},
+1 -2
packages/example/src/index.ts
···
id
name
fleeRate
-
-
__typenam
+
__typename
}
}
+2 -1
packages/graphqlsp/README.md
···
### Configuration
-
- `schema` allows you to specify a url, `.json` or `.graphql` file as your schema
+
- `schema` allows you to specify a url, `.json` or `.graphql` file as your schema. If you need to specify headers for your introspection
+
you can opt into the object notation i.e. `{ "schema": { "url": "x", "headers": { "Authorization": "y" } }}`
- `disableTypegen` disables type-generation in general
- `scalars` allows you to pass an object of scalars that we'll feed into `graphql-code-generator`
- `extraTypes` allows you to specify imports or declare types to help with `scalar` definitions
+26 -7
packages/graphqlsp/src/graphql/getSchema.ts
···
import { Logger } from '../index';
import { generateBaseTypes } from './generateTypes';
+
export type SchemaOrigin = {
+
url: string;
+
headers: Record<string, unknown>;
+
};
+
export const loadSchema = (
root: string,
-
schema: string,
+
schema: SchemaOrigin | string,
logger: Logger,
baseTypesPath: string,
shouldTypegen: boolean,
···
const ref: { current: GraphQLSchema | null } = { current: null };
let url: URL | undefined;
+
let isJSON = false;
+
let config: undefined | SchemaOrigin;
+
try {
-
url = new URL(schema);
+
if (typeof schema === 'object') {
+
url = new URL(schema.url);
+
} else {
+
url = new URL(schema);
+
}
} catch (e) {}
if (url) {
logger(`Fetching introspection from ${url.toString()}`);
fetch(url.toString(), {
method: 'POST',
-
headers: {
-
'Content-Type': 'application/json',
-
},
+
headers:
+
isJSON && config
+
? {
+
...(config.headers || {}),
+
'Content-Type': 'application/json',
+
}
+
: {
+
'Content-Type': 'application/json',
+
},
body: JSON.stringify({
query: getIntrospectionQuery({
descriptions: true,
-
schemaDescription: true,
+
schemaDescription: false,
inputValueDeprecation: false,
directiveIsRepeatable: false,
specifiedByUrl: false,
···
else return response.text();
})
.then(result => {
+
logger(`Got result ${JSON.stringify(result)}`);
if (typeof result === 'string') {
logger(`Got error while fetching introspection ${result}`);
} else if (result.data) {
···
logger(`Got invalid response ${JSON.stringify(result)}`);
}
});
-
} else {
+
} else if (typeof schema === 'string') {
const isJson = schema.endsWith('json');
const resolvedPath = path.resolve(path.dirname(root), schema);
logger(`Getting schema from ${resolvedPath}`);
+18 -7
packages/graphqlsp/src/index.ts
···
import ts from 'typescript/lib/tsserverlibrary';
-
import { loadSchema } from './graphql/getSchema';
+
import { SchemaOrigin, loadSchema } from './graphql/getSchema';
import { getGraphQLCompletions } from './autoComplete';
import { getGraphQLQuickInfo } from './quickInfo';
import { getGraphQLDiagnostics } from './diagnostics';
···
export type Logger = (msg: string) => void;
+
type Config = {
+
schema: SchemaOrigin | string;
+
template?: string;
+
disableTypegen?: boolean;
+
extraTypes?: string;
+
scalars?: Record<string, unknown>;
+
shouldCheckForColocatedFragments?: boolean;
+
};
+
function create(info: ts.server.PluginCreateInfo) {
const logger: Logger = (msg: string) =>
info.project.projectService.logger.info(`[GraphQLSP] ${msg}`);
-
logger('config: ' + JSON.stringify(info.config));
-
if (!info.config.schema) {
+
const config: Config = info.config;
+
+
logger('config: ' + JSON.stringify(config));
+
if (!config.schema) {
logger('Missing "schema" option in configuration.');
throw new Error('Please provide a GraphQL Schema!');
}
logger('Setting up the GraphQL Plugin');
-
const scalars = info.config.scalars || {};
-
const extraTypes = info.config.extraTypes;
-
const disableTypegen = info.config.disableTypegen;
+
const scalars = config.scalars || {};
+
const extraTypes = config.extraTypes || '';
+
const disableTypegen = config.disableTypegen || false;
const proxy = createBasicDecorator(info);
···
const schema = loadSchema(
info.project.getProjectName(),
-
info.config.schema,
+
config.schema,
logger,
baseTypesPath,
!disableTypegen,
-1
pnpm-lock.yaml
···
resolution: {directory: packages/graphqlsp, type: directory}
id: file:packages/graphqlsp
name: '@0no-co/graphqlsp'
-
version: 0.7.2
dependencies:
'@graphql-codegen/add': 4.0.1(graphql@16.6.0)
'@graphql-codegen/core': 3.1.0(graphql@16.6.0)