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

feat: import `typescript` types from external file (#39)

* try to implement importing from a global file

* make typescript globals work

* Create twelve-spies-learn.md

Changed files
+212 -229
.changeset
packages
test
+6
.changeset/twelve-spies-learn.md
···
+
---
+
"@0no-co/graphqlsp": minor
+
---
+
+
only run the `typescript` plugin once to generate a set of types that we'll reference from our
+
`typescript-operations`, this to reduce lengthy generated files.
+105
packages/example/__generated__/baseGraphQLSP.ts
···
+
export type Maybe<T> = T | null;
+
export type InputMaybe<T> = Maybe<T>;
+
export type Exact<T extends { [key: string]: unknown }> = {
+
[K in keyof T]: T[K];
+
};
+
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
+
[SubKey in K]?: Maybe<T[SubKey]>;
+
};
+
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
+
[SubKey in K]: Maybe<T[SubKey]>;
+
};
+
/** All built-in and custom scalars, mapped to their actual values */
+
export type Scalars = {
+
ID: string;
+
String: string;
+
Boolean: boolean;
+
Int: number;
+
Float: number;
+
};
+
+
/** Elemental property associated with either a Pokémon or one of their moves. */
+
export type PokemonType =
+
| 'Grass'
+
| 'Poison'
+
| 'Fire'
+
| 'Flying'
+
| 'Water'
+
| 'Bug'
+
| 'Normal'
+
| 'Electric'
+
| 'Ground'
+
| 'Fairy'
+
| 'Fighting'
+
| 'Psychic'
+
| 'Rock'
+
| 'Steel'
+
| 'Ice'
+
| 'Ghost'
+
| 'Dragon'
+
| 'Dark';
+
+
/** Move a Pokémon can perform with the associated damage and type. */
+
export type Attack = {
+
__typename?: 'Attack';
+
name?: Maybe<Scalars['String']>;
+
type?: Maybe<PokemonType>;
+
damage?: Maybe<Scalars['Int']>;
+
};
+
+
/** Requirement that prevents an evolution through regular means of levelling up. */
+
export type EvolutionRequirement = {
+
__typename?: 'EvolutionRequirement';
+
amount?: Maybe<Scalars['Int']>;
+
name?: Maybe<Scalars['String']>;
+
};
+
+
export type PokemonDimension = {
+
__typename?: 'PokemonDimension';
+
minimum?: Maybe<Scalars['String']>;
+
maximum?: Maybe<Scalars['String']>;
+
};
+
+
export type AttacksConnection = {
+
__typename?: 'AttacksConnection';
+
fast?: Maybe<Array<Maybe<Attack>>>;
+
special?: Maybe<Array<Maybe<Attack>>>;
+
};
+
+
export type Pokemon = {
+
__typename?: 'Pokemon';
+
id: Scalars['ID'];
+
name: Scalars['String'];
+
classification?: Maybe<Scalars['String']>;
+
types?: Maybe<Array<Maybe<PokemonType>>>;
+
resistant?: Maybe<Array<Maybe<PokemonType>>>;
+
weaknesses?: Maybe<Array<Maybe<PokemonType>>>;
+
evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>;
+
weight?: Maybe<PokemonDimension>;
+
height?: Maybe<PokemonDimension>;
+
attacks?: Maybe<AttacksConnection>;
+
/** Likelihood of an attempt to catch a Pokémon to fail. */
+
fleeRate?: Maybe<Scalars['Float']>;
+
/** Maximum combat power a Pokémon may achieve at max level. */
+
maxCP?: Maybe<Scalars['Int']>;
+
/** Maximum health points a Pokémon may achieve at max level. */
+
maxHP?: Maybe<Scalars['Int']>;
+
evolutions?: Maybe<Array<Maybe<Pokemon>>>;
+
};
+
+
export type Query = {
+
__typename?: 'Query';
+
/** List out all Pokémon, optionally in pages */
+
pokemons?: Maybe<Array<Maybe<Pokemon>>>;
+
/** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */
+
pokemon?: Maybe<Pokemon>;
+
};
+
+
export type QueryPokemonsArgs = {
+
limit?: InputMaybe<Scalars['Int']>;
+
skip?: InputMaybe<Scalars['Int']>;
+
};
+
+
export type QueryPokemonArgs = {
+
id: Scalars['ID'];
+
};
+1 -106
packages/example/src/Pokemon.generated.ts
···
+
import * as Types from '../__generated__/baseGraphQLSP';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
-
export type Maybe<T> = T | null;
-
export type InputMaybe<T> = Maybe<T>;
-
export type Exact<T extends { [key: string]: unknown }> = {
-
[K in keyof T]: T[K];
-
};
-
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
-
[SubKey in K]?: Maybe<T[SubKey]>;
-
};
-
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
-
[SubKey in K]: Maybe<T[SubKey]>;
-
};
-
/** All built-in and custom scalars, mapped to their actual values */
-
export type Scalars = {
-
ID: string;
-
String: string;
-
Boolean: boolean;
-
Int: number;
-
Float: number;
-
};
-
-
/** Elemental property associated with either a Pokémon or one of their moves. */
-
export type PokemonType =
-
| 'Grass'
-
| 'Poison'
-
| 'Fire'
-
| 'Flying'
-
| 'Water'
-
| 'Bug'
-
| 'Normal'
-
| 'Electric'
-
| 'Ground'
-
| 'Fairy'
-
| 'Fighting'
-
| 'Psychic'
-
| 'Rock'
-
| 'Steel'
-
| 'Ice'
-
| 'Ghost'
-
| 'Dragon'
-
| 'Dark';
-
-
/** Move a Pokémon can perform with the associated damage and type. */
-
export type Attack = {
-
__typename?: 'Attack';
-
name?: Maybe<Scalars['String']>;
-
type?: Maybe<PokemonType>;
-
damage?: Maybe<Scalars['Int']>;
-
};
-
-
/** Requirement that prevents an evolution through regular means of levelling up. */
-
export type EvolutionRequirement = {
-
__typename?: 'EvolutionRequirement';
-
amount?: Maybe<Scalars['Int']>;
-
name?: Maybe<Scalars['String']>;
-
};
-
-
export type PokemonDimension = {
-
__typename?: 'PokemonDimension';
-
minimum?: Maybe<Scalars['String']>;
-
maximum?: Maybe<Scalars['String']>;
-
};
-
-
export type AttacksConnection = {
-
__typename?: 'AttacksConnection';
-
fast?: Maybe<Array<Maybe<Attack>>>;
-
special?: Maybe<Array<Maybe<Attack>>>;
-
};
-
-
export type Pokemon = {
-
__typename?: 'Pokemon';
-
id: Scalars['ID'];
-
name: Scalars['String'];
-
classification?: Maybe<Scalars['String']>;
-
types?: Maybe<Array<Maybe<PokemonType>>>;
-
resistant?: Maybe<Array<Maybe<PokemonType>>>;
-
weaknesses?: Maybe<Array<Maybe<PokemonType>>>;
-
evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>;
-
weight?: Maybe<PokemonDimension>;
-
height?: Maybe<PokemonDimension>;
-
attacks?: Maybe<AttacksConnection>;
-
/** Likelihood of an attempt to catch a Pokémon to fail. */
-
fleeRate?: Maybe<Scalars['Float']>;
-
/** Maximum combat power a Pokémon may achieve at max level. */
-
maxCP?: Maybe<Scalars['Int']>;
-
/** Maximum health points a Pokémon may achieve at max level. */
-
maxHP?: Maybe<Scalars['Int']>;
-
evolutions?: Maybe<Array<Maybe<Pokemon>>>;
-
};
-
-
export type Query = {
-
__typename?: 'Query';
-
/** List out all Pokémon, optionally in pages */
-
pokemons?: Maybe<Array<Maybe<Pokemon>>>;
-
/** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */
-
pokemon?: Maybe<Pokemon>;
-
};
-
-
export type QueryPokemonsArgs = {
-
limit?: InputMaybe<Scalars['Int']>;
-
skip?: InputMaybe<Scalars['Int']>;
-
};
-
-
export type QueryPokemonArgs = {
-
id: Scalars['ID'];
-
};
-
export type FieldsFragment = {
__typename?: 'Pokemon';
classification?: string | null;
+4 -109
packages/example/src/index.generated.ts
···
+
import * as Types from '../__generated__/baseGraphQLSP';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
-
export type Maybe<T> = T | null;
-
export type InputMaybe<T> = Maybe<T>;
-
export type Exact<T extends { [key: string]: unknown }> = {
-
[K in keyof T]: T[K];
-
};
-
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
-
[SubKey in K]?: Maybe<T[SubKey]>;
-
};
-
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
-
[SubKey in K]: Maybe<T[SubKey]>;
-
};
-
/** All built-in and custom scalars, mapped to their actual values */
-
export type Scalars = {
-
ID: string;
-
String: string;
-
Boolean: boolean;
-
Int: number;
-
Float: number;
-
};
-
-
/** Elemental property associated with either a Pokémon or one of their moves. */
-
export type PokemonType =
-
| 'Grass'
-
| 'Poison'
-
| 'Fire'
-
| 'Flying'
-
| 'Water'
-
| 'Bug'
-
| 'Normal'
-
| 'Electric'
-
| 'Ground'
-
| 'Fairy'
-
| 'Fighting'
-
| 'Psychic'
-
| 'Rock'
-
| 'Steel'
-
| 'Ice'
-
| 'Ghost'
-
| 'Dragon'
-
| 'Dark';
-
-
/** Move a Pokémon can perform with the associated damage and type. */
-
export type Attack = {
-
__typename?: 'Attack';
-
name?: Maybe<Scalars['String']>;
-
type?: Maybe<PokemonType>;
-
damage?: Maybe<Scalars['Int']>;
-
};
-
-
/** Requirement that prevents an evolution through regular means of levelling up. */
-
export type EvolutionRequirement = {
-
__typename?: 'EvolutionRequirement';
-
amount?: Maybe<Scalars['Int']>;
-
name?: Maybe<Scalars['String']>;
-
};
-
-
export type PokemonDimension = {
-
__typename?: 'PokemonDimension';
-
minimum?: Maybe<Scalars['String']>;
-
maximum?: Maybe<Scalars['String']>;
-
};
-
-
export type AttacksConnection = {
-
__typename?: 'AttacksConnection';
-
fast?: Maybe<Array<Maybe<Attack>>>;
-
special?: Maybe<Array<Maybe<Attack>>>;
-
};
-
-
export type Pokemon = {
-
__typename?: 'Pokemon';
-
id: Scalars['ID'];
-
name: Scalars['String'];
-
classification?: Maybe<Scalars['String']>;
-
types?: Maybe<Array<Maybe<PokemonType>>>;
-
resistant?: Maybe<Array<Maybe<PokemonType>>>;
-
weaknesses?: Maybe<Array<Maybe<PokemonType>>>;
-
evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>;
-
weight?: Maybe<PokemonDimension>;
-
height?: Maybe<PokemonDimension>;
-
attacks?: Maybe<AttacksConnection>;
-
/** Likelihood of an attempt to catch a Pokémon to fail. */
-
fleeRate?: Maybe<Scalars['Float']>;
-
/** Maximum combat power a Pokémon may achieve at max level. */
-
maxCP?: Maybe<Scalars['Int']>;
-
/** Maximum health points a Pokémon may achieve at max level. */
-
maxHP?: Maybe<Scalars['Int']>;
-
evolutions?: Maybe<Array<Maybe<Pokemon>>>;
-
};
-
-
export type Query = {
-
__typename?: 'Query';
-
/** List out all Pokémon, optionally in pages */
-
pokemons?: Maybe<Array<Maybe<Pokemon>>>;
-
/** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */
-
pokemon?: Maybe<Pokemon>;
-
};
-
-
export type QueryPokemonsArgs = {
-
limit?: InputMaybe<Scalars['Int']>;
-
skip?: InputMaybe<Scalars['Int']>;
-
};
-
-
export type QueryPokemonArgs = {
-
id: Scalars['ID'];
-
};
-
-
export type PokemonsQueryVariables = Exact<{ [key: string]: never }>;
+
export type PokemonsQueryVariables = Types.Exact<{ [key: string]: never }>;
export type PokemonsQuery = {
__typename?: 'Query';
···
name: string;
};
-
export type PokemonQueryVariables = Exact<{
-
id: Scalars['ID'];
+
export type PokemonQueryVariables = Types.Exact<{
+
id: Types.Scalars['ID'];
}>;
export type PokemonQuery = {
+1
packages/graphqlsp/package.json
···
"typescript": "^5.0.0"
},
"dependencies": {
+
"@graphql-codegen/add": "^4.0.1",
"@graphql-codegen/core": "^3.1.0",
"@graphql-codegen/typed-document-node": "^3.0.2",
"@graphql-codegen/typescript": "^3.0.3",
+7 -1
packages/graphqlsp/src/getSchema.ts
···
import fs from 'fs';
import { Logger } from './index';
+
import { generateBaseTypes } from './types/generate';
export const loadSchema = (
root: string,
schema: string,
-
logger: Logger
+
logger: Logger,
+
baseTypesPath: string,
+
scalars: Record<string, unknown>
): { current: GraphQLSchema | null } => {
const ref: { current: GraphQLSchema | null } = { current: null };
let url: URL | undefined;
···
(result as { data: IntrospectionQuery }).data
);
logger(`Got schema for ${url!.toString()}`);
+
generateBaseTypes(ref.current, baseTypesPath, scalars);
} catch (e: any) {
logger(`Got schema error for ${e.message}`);
}
···
ref.current = isJson
? buildClientSchema(JSON.parse(contents))
: buildSchema(contents);
+
generateBaseTypes(ref.current, baseTypesPath, scalars);
});
ref.current = isJson
? buildClientSchema(JSON.parse(contents))
: buildSchema(contents);
+
generateBaseTypes(ref.current, baseTypesPath, scalars);
logger(`Got schema and initialized watcher for ${schema}`);
}
+7 -2
packages/graphqlsp/src/index.ts
···
const proxy = createBasicDecorator(info);
+
const baseTypesPath =
+
info.project.getCurrentDirectory() + '/__generated__/baseGraphQLSP.ts';
const schema = loadSchema(
info.project.getProjectName(),
info.config.schema,
-
logger
+
logger,
+
baseTypesPath,
+
scalars
);
proxy.getSemanticDiagnostics = (filename: string): ts.Diagnostic[] => {
···
schema.current,
parts.join('/'),
texts.join('\n'),
-
scalars
+
scalars,
+
baseTypesPath
).then(() => {
if (isFileDirty(filename, source)) {
return;
+55 -8
packages/graphqlsp/src/types/generate.ts
···
import fs from 'fs';
-
import path from 'path';
+
import { posix as path } from 'path';
import { printSchema, parse, GraphQLSchema } from 'graphql';
import { codegen } from '@graphql-codegen/core';
import * as typescriptPlugin from '@graphql-codegen/typescript';
import * as typescriptOperationsPlugin from '@graphql-codegen/typescript-operations';
import * as typedDocumentNodePlugin from '@graphql-codegen/typed-document-node';
+
import * as addPlugin from '@graphql-codegen/add';
+
import { Logger } from '..';
+
+
export const generateBaseTypes = async (
+
schema: GraphQLSchema | null,
+
outputFile: string,
+
scalars: Record<string, unknown>
+
) => {
+
if (!schema) return;
+
+
const config = {
+
documents: [],
+
config: {
+
scalars,
+
// nonOptionalTypename: true,
+
// avoidOptionals, worth looking into
+
enumsAsTypes: true,
+
globalNamespace: true,
+
},
+
filename: outputFile,
+
schema: parse(printSchema(schema)),
+
plugins: [{ typescript: {} }],
+
pluginMap: {
+
typescript: typescriptPlugin,
+
},
+
};
+
+
// @ts-ignore
+
const output = await codegen(config);
+
let folderParts = outputFile.split('/');
+
folderParts.pop();
+
const folder = path.join(folderParts.join('/'));
+
if (!fs.existsSync(folder)) {
+
fs.mkdirSync(folder);
+
}
+
fs.writeFile(path.join(outputFile), output, 'utf8', err => {
+
console.error(err);
+
});
+
};
export const generateTypedDocumentNodes = async (
schema: GraphQLSchema | null,
outputFile: string,
doc: string,
-
scalars: Record<string, unknown>
+
scalars: Record<string, unknown>,
+
baseTypesPath: string
) => {
if (!schema) return;
+
const parts = outputFile.split('/');
+
parts.pop();
+
let basePath = path
+
.relative(parts.join('/'), baseTypesPath)
+
.replace('.ts', '');
+
// case where files are declared globally, we need to prefix with ./
+
if (basePath === '__generated__/baseGraphQLSP') {
+
basePath = './' + basePath;
+
}
+
const config = {
documents: [
{
···
},
],
config: {
+
namespacedImportName: 'Types',
scalars,
// nonOptionalTypename: true,
// avoidOptionals, worth looking into
···
dedupeOperationSuffix: true,
dedupeFragments: true,
},
-
// used by a plugin internally, although the 'typescript' plugin currently
-
// returns the string output, rather than writing to a file
filename: outputFile,
schema: parse(printSchema(schema)),
plugins: [
-
// TODO: there's optimisations to be had here where we move the typescript and typescript-operations
-
// to a global __generated__ folder and import from it.
-
{ typescript: {} },
{ 'typescript-operations': {} },
{ 'typed-document-node': {} },
+
{ add: { content: `import * as Types from "${basePath}"` } },
],
pluginMap: {
-
typescript: typescriptPlugin,
'typescript-operations': typescriptOperationsPlugin,
'typed-document-node': typedDocumentNodePlugin,
+
add: addPlugin,
},
};
+13
pnpm-lock.yaml
···
packages/graphqlsp:
dependencies:
+
'@graphql-codegen/add':
+
specifier: ^4.0.1
+
version: 4.0.1(graphql@16.6.0)
'@graphql-codegen/core':
specifier: ^3.1.0
version: 3.1.0(graphql@16.6.0)
···
requiresBuild: true
dev: true
optional: true
+
+
/@graphql-codegen/add@4.0.1(graphql@16.6.0):
+
resolution: {integrity: sha512-A7k+9eRfrKyyNfhWEN/0eKz09R5cp4XXxUuNLQAVm/aohmVI2xdMV4lM02rTlM6Pyou3cU/v0iZnhgo6IRpqeg==}
+
peerDependencies:
+
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
+
dependencies:
+
'@graphql-codegen/plugin-helpers': 4.2.0(graphql@16.6.0)
+
graphql: 16.6.0
+
tslib: 2.5.0
+
dev: false
/@graphql-codegen/core@3.1.0(graphql@16.6.0):
resolution: {integrity: sha512-DH1/yaR7oJE6/B+c6ZF2Tbdh7LixF1K8L+8BoSubjNyQ8pNwR4a70mvc1sv6H7qgp6y1bPQ9tKE+aazRRshysw==}
+13 -3
test/e2e/graphqlsp.test.ts
···
describe('simple', () => {
const testFile = path.join(projectPath, 'simple.ts');
const generatedFile = path.join(projectPath, 'simple.generated.ts');
+
const baseGeneratedFile = path.join(
+
projectPath,
+
'__generated__/baseGraphQLSP.ts'
+
);
beforeAll(async () => {
server = new TSServer(projectPath, { debugLog: false });
···
try {
fs.unlinkSync(testFile);
fs.unlinkSync(generatedFile);
+
fs.unlinkSync(baseGeneratedFile);
} catch {}
server.close();
});
···
expect(() => {
fs.lstatSync(testFile);
fs.lstatSync(generatedFile);
+
fs.lstatSync(baseGeneratedFile);
}).not.toThrow();
-
expect(fs.readFileSync(testFile, 'utf-8')).toContain(
+
const testFileContents = fs.readFileSync(testFile, 'utf-8');
+
const generatedFileContents = fs.readFileSync(generatedFile, 'utf-8');
+
+
expect(testFileContents).toContain(
`as typeof import('./simple.generated').AllPostsDocument`
);
-
expect(fs.readFileSync(generatedFile, 'utf-8')).toContain(
-
'export const AllPostsDocument = '
+
expect(generatedFileContents).toContain('export const AllPostsDocument = ');
+
expect(generatedFileContents).toContain(
+
'import * as Types from "./__generated__/baseGraphQLSP"'
);
}, 7500);