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.

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

authored by

Jovi De Croock and committed by
GitHub
d81a4b35 187d603b

+212 -229
+6
.changeset/twelve-spies-learn.md
··· 1 + --- 2 + "@0no-co/graphqlsp": minor 3 + --- 4 + 5 + only run the `typescript` plugin once to generate a set of types that we'll reference from our 6 + `typescript-operations`, this to reduce lengthy generated files.
+105
packages/example/__generated__/baseGraphQLSP.ts
··· 1 + export type Maybe<T> = T | null; 2 + export type InputMaybe<T> = Maybe<T>; 3 + export type Exact<T extends { [key: string]: unknown }> = { 4 + [K in keyof T]: T[K]; 5 + }; 6 + export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { 7 + [SubKey in K]?: Maybe<T[SubKey]>; 8 + }; 9 + export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { 10 + [SubKey in K]: Maybe<T[SubKey]>; 11 + }; 12 + /** All built-in and custom scalars, mapped to their actual values */ 13 + export type Scalars = { 14 + ID: string; 15 + String: string; 16 + Boolean: boolean; 17 + Int: number; 18 + Float: number; 19 + }; 20 + 21 + /** Elemental property associated with either a Pokémon or one of their moves. */ 22 + export type PokemonType = 23 + | 'Grass' 24 + | 'Poison' 25 + | 'Fire' 26 + | 'Flying' 27 + | 'Water' 28 + | 'Bug' 29 + | 'Normal' 30 + | 'Electric' 31 + | 'Ground' 32 + | 'Fairy' 33 + | 'Fighting' 34 + | 'Psychic' 35 + | 'Rock' 36 + | 'Steel' 37 + | 'Ice' 38 + | 'Ghost' 39 + | 'Dragon' 40 + | 'Dark'; 41 + 42 + /** Move a Pokémon can perform with the associated damage and type. */ 43 + export type Attack = { 44 + __typename?: 'Attack'; 45 + name?: Maybe<Scalars['String']>; 46 + type?: Maybe<PokemonType>; 47 + damage?: Maybe<Scalars['Int']>; 48 + }; 49 + 50 + /** Requirement that prevents an evolution through regular means of levelling up. */ 51 + export type EvolutionRequirement = { 52 + __typename?: 'EvolutionRequirement'; 53 + amount?: Maybe<Scalars['Int']>; 54 + name?: Maybe<Scalars['String']>; 55 + }; 56 + 57 + export type PokemonDimension = { 58 + __typename?: 'PokemonDimension'; 59 + minimum?: Maybe<Scalars['String']>; 60 + maximum?: Maybe<Scalars['String']>; 61 + }; 62 + 63 + export type AttacksConnection = { 64 + __typename?: 'AttacksConnection'; 65 + fast?: Maybe<Array<Maybe<Attack>>>; 66 + special?: Maybe<Array<Maybe<Attack>>>; 67 + }; 68 + 69 + export type Pokemon = { 70 + __typename?: 'Pokemon'; 71 + id: Scalars['ID']; 72 + name: Scalars['String']; 73 + classification?: Maybe<Scalars['String']>; 74 + types?: Maybe<Array<Maybe<PokemonType>>>; 75 + resistant?: Maybe<Array<Maybe<PokemonType>>>; 76 + weaknesses?: Maybe<Array<Maybe<PokemonType>>>; 77 + evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>; 78 + weight?: Maybe<PokemonDimension>; 79 + height?: Maybe<PokemonDimension>; 80 + attacks?: Maybe<AttacksConnection>; 81 + /** Likelihood of an attempt to catch a Pokémon to fail. */ 82 + fleeRate?: Maybe<Scalars['Float']>; 83 + /** Maximum combat power a Pokémon may achieve at max level. */ 84 + maxCP?: Maybe<Scalars['Int']>; 85 + /** Maximum health points a Pokémon may achieve at max level. */ 86 + maxHP?: Maybe<Scalars['Int']>; 87 + evolutions?: Maybe<Array<Maybe<Pokemon>>>; 88 + }; 89 + 90 + export type Query = { 91 + __typename?: 'Query'; 92 + /** List out all Pokémon, optionally in pages */ 93 + pokemons?: Maybe<Array<Maybe<Pokemon>>>; 94 + /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ 95 + pokemon?: Maybe<Pokemon>; 96 + }; 97 + 98 + export type QueryPokemonsArgs = { 99 + limit?: InputMaybe<Scalars['Int']>; 100 + skip?: InputMaybe<Scalars['Int']>; 101 + }; 102 + 103 + export type QueryPokemonArgs = { 104 + id: Scalars['ID']; 105 + };
+1 -106
packages/example/src/Pokemon.generated.ts
··· 1 + import * as Types from '../__generated__/baseGraphQLSP'; 1 2 import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 2 - export type Maybe<T> = T | null; 3 - export type InputMaybe<T> = Maybe<T>; 4 - export type Exact<T extends { [key: string]: unknown }> = { 5 - [K in keyof T]: T[K]; 6 - }; 7 - export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { 8 - [SubKey in K]?: Maybe<T[SubKey]>; 9 - }; 10 - export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { 11 - [SubKey in K]: Maybe<T[SubKey]>; 12 - }; 13 - /** All built-in and custom scalars, mapped to their actual values */ 14 - export type Scalars = { 15 - ID: string; 16 - String: string; 17 - Boolean: boolean; 18 - Int: number; 19 - Float: number; 20 - }; 21 - 22 - /** Elemental property associated with either a Pokémon or one of their moves. */ 23 - export type PokemonType = 24 - | 'Grass' 25 - | 'Poison' 26 - | 'Fire' 27 - | 'Flying' 28 - | 'Water' 29 - | 'Bug' 30 - | 'Normal' 31 - | 'Electric' 32 - | 'Ground' 33 - | 'Fairy' 34 - | 'Fighting' 35 - | 'Psychic' 36 - | 'Rock' 37 - | 'Steel' 38 - | 'Ice' 39 - | 'Ghost' 40 - | 'Dragon' 41 - | 'Dark'; 42 - 43 - /** Move a Pokémon can perform with the associated damage and type. */ 44 - export type Attack = { 45 - __typename?: 'Attack'; 46 - name?: Maybe<Scalars['String']>; 47 - type?: Maybe<PokemonType>; 48 - damage?: Maybe<Scalars['Int']>; 49 - }; 50 - 51 - /** Requirement that prevents an evolution through regular means of levelling up. */ 52 - export type EvolutionRequirement = { 53 - __typename?: 'EvolutionRequirement'; 54 - amount?: Maybe<Scalars['Int']>; 55 - name?: Maybe<Scalars['String']>; 56 - }; 57 - 58 - export type PokemonDimension = { 59 - __typename?: 'PokemonDimension'; 60 - minimum?: Maybe<Scalars['String']>; 61 - maximum?: Maybe<Scalars['String']>; 62 - }; 63 - 64 - export type AttacksConnection = { 65 - __typename?: 'AttacksConnection'; 66 - fast?: Maybe<Array<Maybe<Attack>>>; 67 - special?: Maybe<Array<Maybe<Attack>>>; 68 - }; 69 - 70 - export type Pokemon = { 71 - __typename?: 'Pokemon'; 72 - id: Scalars['ID']; 73 - name: Scalars['String']; 74 - classification?: Maybe<Scalars['String']>; 75 - types?: Maybe<Array<Maybe<PokemonType>>>; 76 - resistant?: Maybe<Array<Maybe<PokemonType>>>; 77 - weaknesses?: Maybe<Array<Maybe<PokemonType>>>; 78 - evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>; 79 - weight?: Maybe<PokemonDimension>; 80 - height?: Maybe<PokemonDimension>; 81 - attacks?: Maybe<AttacksConnection>; 82 - /** Likelihood of an attempt to catch a Pokémon to fail. */ 83 - fleeRate?: Maybe<Scalars['Float']>; 84 - /** Maximum combat power a Pokémon may achieve at max level. */ 85 - maxCP?: Maybe<Scalars['Int']>; 86 - /** Maximum health points a Pokémon may achieve at max level. */ 87 - maxHP?: Maybe<Scalars['Int']>; 88 - evolutions?: Maybe<Array<Maybe<Pokemon>>>; 89 - }; 90 - 91 - export type Query = { 92 - __typename?: 'Query'; 93 - /** List out all Pokémon, optionally in pages */ 94 - pokemons?: Maybe<Array<Maybe<Pokemon>>>; 95 - /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ 96 - pokemon?: Maybe<Pokemon>; 97 - }; 98 - 99 - export type QueryPokemonsArgs = { 100 - limit?: InputMaybe<Scalars['Int']>; 101 - skip?: InputMaybe<Scalars['Int']>; 102 - }; 103 - 104 - export type QueryPokemonArgs = { 105 - id: Scalars['ID']; 106 - }; 107 - 108 3 export type FieldsFragment = { 109 4 __typename?: 'Pokemon'; 110 5 classification?: string | null;
+4 -109
packages/example/src/index.generated.ts
··· 1 + import * as Types from '../__generated__/baseGraphQLSP'; 1 2 import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 2 - export type Maybe<T> = T | null; 3 - export type InputMaybe<T> = Maybe<T>; 4 - export type Exact<T extends { [key: string]: unknown }> = { 5 - [K in keyof T]: T[K]; 6 - }; 7 - export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { 8 - [SubKey in K]?: Maybe<T[SubKey]>; 9 - }; 10 - export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { 11 - [SubKey in K]: Maybe<T[SubKey]>; 12 - }; 13 - /** All built-in and custom scalars, mapped to their actual values */ 14 - export type Scalars = { 15 - ID: string; 16 - String: string; 17 - Boolean: boolean; 18 - Int: number; 19 - Float: number; 20 - }; 21 - 22 - /** Elemental property associated with either a Pokémon or one of their moves. */ 23 - export type PokemonType = 24 - | 'Grass' 25 - | 'Poison' 26 - | 'Fire' 27 - | 'Flying' 28 - | 'Water' 29 - | 'Bug' 30 - | 'Normal' 31 - | 'Electric' 32 - | 'Ground' 33 - | 'Fairy' 34 - | 'Fighting' 35 - | 'Psychic' 36 - | 'Rock' 37 - | 'Steel' 38 - | 'Ice' 39 - | 'Ghost' 40 - | 'Dragon' 41 - | 'Dark'; 42 - 43 - /** Move a Pokémon can perform with the associated damage and type. */ 44 - export type Attack = { 45 - __typename?: 'Attack'; 46 - name?: Maybe<Scalars['String']>; 47 - type?: Maybe<PokemonType>; 48 - damage?: Maybe<Scalars['Int']>; 49 - }; 50 - 51 - /** Requirement that prevents an evolution through regular means of levelling up. */ 52 - export type EvolutionRequirement = { 53 - __typename?: 'EvolutionRequirement'; 54 - amount?: Maybe<Scalars['Int']>; 55 - name?: Maybe<Scalars['String']>; 56 - }; 57 - 58 - export type PokemonDimension = { 59 - __typename?: 'PokemonDimension'; 60 - minimum?: Maybe<Scalars['String']>; 61 - maximum?: Maybe<Scalars['String']>; 62 - }; 63 - 64 - export type AttacksConnection = { 65 - __typename?: 'AttacksConnection'; 66 - fast?: Maybe<Array<Maybe<Attack>>>; 67 - special?: Maybe<Array<Maybe<Attack>>>; 68 - }; 69 - 70 - export type Pokemon = { 71 - __typename?: 'Pokemon'; 72 - id: Scalars['ID']; 73 - name: Scalars['String']; 74 - classification?: Maybe<Scalars['String']>; 75 - types?: Maybe<Array<Maybe<PokemonType>>>; 76 - resistant?: Maybe<Array<Maybe<PokemonType>>>; 77 - weaknesses?: Maybe<Array<Maybe<PokemonType>>>; 78 - evolutionRequirements?: Maybe<Array<Maybe<EvolutionRequirement>>>; 79 - weight?: Maybe<PokemonDimension>; 80 - height?: Maybe<PokemonDimension>; 81 - attacks?: Maybe<AttacksConnection>; 82 - /** Likelihood of an attempt to catch a Pokémon to fail. */ 83 - fleeRate?: Maybe<Scalars['Float']>; 84 - /** Maximum combat power a Pokémon may achieve at max level. */ 85 - maxCP?: Maybe<Scalars['Int']>; 86 - /** Maximum health points a Pokémon may achieve at max level. */ 87 - maxHP?: Maybe<Scalars['Int']>; 88 - evolutions?: Maybe<Array<Maybe<Pokemon>>>; 89 - }; 90 - 91 - export type Query = { 92 - __typename?: 'Query'; 93 - /** List out all Pokémon, optionally in pages */ 94 - pokemons?: Maybe<Array<Maybe<Pokemon>>>; 95 - /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ 96 - pokemon?: Maybe<Pokemon>; 97 - }; 98 - 99 - export type QueryPokemonsArgs = { 100 - limit?: InputMaybe<Scalars['Int']>; 101 - skip?: InputMaybe<Scalars['Int']>; 102 - }; 103 - 104 - export type QueryPokemonArgs = { 105 - id: Scalars['ID']; 106 - }; 107 - 108 - export type PokemonsQueryVariables = Exact<{ [key: string]: never }>; 3 + export type PokemonsQueryVariables = Types.Exact<{ [key: string]: never }>; 109 4 110 5 export type PokemonsQuery = { 111 6 __typename?: 'Query'; ··· 123 18 name: string; 124 19 }; 125 20 126 - export type PokemonQueryVariables = Exact<{ 127 - id: Scalars['ID']; 21 + export type PokemonQueryVariables = Types.Exact<{ 22 + id: Types.Scalars['ID']; 128 23 }>; 129 24 130 25 export type PokemonQuery = {
+1
packages/graphqlsp/package.json
··· 40 40 "typescript": "^5.0.0" 41 41 }, 42 42 "dependencies": { 43 + "@graphql-codegen/add": "^4.0.1", 43 44 "@graphql-codegen/core": "^3.1.0", 44 45 "@graphql-codegen/typed-document-node": "^3.0.2", 45 46 "@graphql-codegen/typescript": "^3.0.3",
+7 -1
packages/graphqlsp/src/getSchema.ts
··· 10 10 import fs from 'fs'; 11 11 12 12 import { Logger } from './index'; 13 + import { generateBaseTypes } from './types/generate'; 13 14 14 15 export const loadSchema = ( 15 16 root: string, 16 17 schema: string, 17 - logger: Logger 18 + logger: Logger, 19 + baseTypesPath: string, 20 + scalars: Record<string, unknown> 18 21 ): { current: GraphQLSchema | null } => { 19 22 const ref: { current: GraphQLSchema | null } = { current: null }; 20 23 let url: URL | undefined; ··· 54 57 (result as { data: IntrospectionQuery }).data 55 58 ); 56 59 logger(`Got schema for ${url!.toString()}`); 60 + generateBaseTypes(ref.current, baseTypesPath, scalars); 57 61 } catch (e: any) { 58 62 logger(`Got schema error for ${e.message}`); 59 63 } ··· 70 74 ref.current = isJson 71 75 ? buildClientSchema(JSON.parse(contents)) 72 76 : buildSchema(contents); 77 + generateBaseTypes(ref.current, baseTypesPath, scalars); 73 78 }); 74 79 75 80 ref.current = isJson 76 81 ? buildClientSchema(JSON.parse(contents)) 77 82 : buildSchema(contents); 83 + generateBaseTypes(ref.current, baseTypesPath, scalars); 78 84 logger(`Got schema and initialized watcher for ${schema}`); 79 85 } 80 86
+7 -2
packages/graphqlsp/src/index.ts
··· 68 68 69 69 const proxy = createBasicDecorator(info); 70 70 71 + const baseTypesPath = 72 + info.project.getCurrentDirectory() + '/__generated__/baseGraphQLSP.ts'; 71 73 const schema = loadSchema( 72 74 info.project.getProjectName(), 73 75 info.config.schema, 74 - logger 76 + logger, 77 + baseTypesPath, 78 + scalars 75 79 ); 76 80 77 81 proxy.getSemanticDiagnostics = (filename: string): ts.Diagnostic[] => { ··· 191 195 schema.current, 192 196 parts.join('/'), 193 197 texts.join('\n'), 194 - scalars 198 + scalars, 199 + baseTypesPath 195 200 ).then(() => { 196 201 if (isFileDirty(filename, source)) { 197 202 return;
+55 -8
packages/graphqlsp/src/types/generate.ts
··· 1 1 import fs from 'fs'; 2 - import path from 'path'; 2 + import { posix as path } from 'path'; 3 3 import { printSchema, parse, GraphQLSchema } from 'graphql'; 4 4 import { codegen } from '@graphql-codegen/core'; 5 5 import * as typescriptPlugin from '@graphql-codegen/typescript'; 6 6 import * as typescriptOperationsPlugin from '@graphql-codegen/typescript-operations'; 7 7 import * as typedDocumentNodePlugin from '@graphql-codegen/typed-document-node'; 8 + import * as addPlugin from '@graphql-codegen/add'; 9 + import { Logger } from '..'; 10 + 11 + export const generateBaseTypes = async ( 12 + schema: GraphQLSchema | null, 13 + outputFile: string, 14 + scalars: Record<string, unknown> 15 + ) => { 16 + if (!schema) return; 17 + 18 + const config = { 19 + documents: [], 20 + config: { 21 + scalars, 22 + // nonOptionalTypename: true, 23 + // avoidOptionals, worth looking into 24 + enumsAsTypes: true, 25 + globalNamespace: true, 26 + }, 27 + filename: outputFile, 28 + schema: parse(printSchema(schema)), 29 + plugins: [{ typescript: {} }], 30 + pluginMap: { 31 + typescript: typescriptPlugin, 32 + }, 33 + }; 34 + 35 + // @ts-ignore 36 + const output = await codegen(config); 37 + let folderParts = outputFile.split('/'); 38 + folderParts.pop(); 39 + const folder = path.join(folderParts.join('/')); 40 + if (!fs.existsSync(folder)) { 41 + fs.mkdirSync(folder); 42 + } 43 + fs.writeFile(path.join(outputFile), output, 'utf8', err => { 44 + console.error(err); 45 + }); 46 + }; 8 47 9 48 export const generateTypedDocumentNodes = async ( 10 49 schema: GraphQLSchema | null, 11 50 outputFile: string, 12 51 doc: string, 13 - scalars: Record<string, unknown> 52 + scalars: Record<string, unknown>, 53 + baseTypesPath: string 14 54 ) => { 15 55 if (!schema) return; 16 56 57 + const parts = outputFile.split('/'); 58 + parts.pop(); 59 + let basePath = path 60 + .relative(parts.join('/'), baseTypesPath) 61 + .replace('.ts', ''); 62 + // case where files are declared globally, we need to prefix with ./ 63 + if (basePath === '__generated__/baseGraphQLSP') { 64 + basePath = './' + basePath; 65 + } 66 + 17 67 const config = { 18 68 documents: [ 19 69 { ··· 22 72 }, 23 73 ], 24 74 config: { 75 + namespacedImportName: 'Types', 25 76 scalars, 26 77 // nonOptionalTypename: true, 27 78 // avoidOptionals, worth looking into ··· 29 80 dedupeOperationSuffix: true, 30 81 dedupeFragments: true, 31 82 }, 32 - // used by a plugin internally, although the 'typescript' plugin currently 33 - // returns the string output, rather than writing to a file 34 83 filename: outputFile, 35 84 schema: parse(printSchema(schema)), 36 85 plugins: [ 37 - // TODO: there's optimisations to be had here where we move the typescript and typescript-operations 38 - // to a global __generated__ folder and import from it. 39 - { typescript: {} }, 40 86 { 'typescript-operations': {} }, 41 87 { 'typed-document-node': {} }, 88 + { add: { content: `import * as Types from "${basePath}"` } }, 42 89 ], 43 90 pluginMap: { 44 - typescript: typescriptPlugin, 45 91 'typescript-operations': typescriptOperationsPlugin, 46 92 'typed-document-node': typedDocumentNodePlugin, 93 + add: addPlugin, 47 94 }, 48 95 }; 49 96
+13
pnpm-lock.yaml
··· 62 62 63 63 packages/graphqlsp: 64 64 dependencies: 65 + '@graphql-codegen/add': 66 + specifier: ^4.0.1 67 + version: 4.0.1(graphql@16.6.0) 65 68 '@graphql-codegen/core': 66 69 specifier: ^3.1.0 67 70 version: 3.1.0(graphql@16.6.0) ··· 1042 1045 requiresBuild: true 1043 1046 dev: true 1044 1047 optional: true 1048 + 1049 + /@graphql-codegen/add@4.0.1(graphql@16.6.0): 1050 + resolution: {integrity: sha512-A7k+9eRfrKyyNfhWEN/0eKz09R5cp4XXxUuNLQAVm/aohmVI2xdMV4lM02rTlM6Pyou3cU/v0iZnhgo6IRpqeg==} 1051 + peerDependencies: 1052 + 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 1053 + dependencies: 1054 + '@graphql-codegen/plugin-helpers': 4.2.0(graphql@16.6.0) 1055 + graphql: 16.6.0 1056 + tslib: 2.5.0 1057 + dev: false 1045 1058 1046 1059 /@graphql-codegen/core@3.1.0(graphql@16.6.0): 1047 1060 resolution: {integrity: sha512-DH1/yaR7oJE6/B+c6ZF2Tbdh7LixF1K8L+8BoSubjNyQ8pNwR4a70mvc1sv6H7qgp6y1bPQ9tKE+aazRRshysw==}
+13 -3
test/e2e/graphqlsp.test.ts
··· 14 14 describe('simple', () => { 15 15 const testFile = path.join(projectPath, 'simple.ts'); 16 16 const generatedFile = path.join(projectPath, 'simple.generated.ts'); 17 + const baseGeneratedFile = path.join( 18 + projectPath, 19 + '__generated__/baseGraphQLSP.ts' 20 + ); 17 21 18 22 beforeAll(async () => { 19 23 server = new TSServer(projectPath, { debugLog: false }); ··· 46 50 try { 47 51 fs.unlinkSync(testFile); 48 52 fs.unlinkSync(generatedFile); 53 + fs.unlinkSync(baseGeneratedFile); 49 54 } catch {} 50 55 server.close(); 51 56 }); ··· 54 59 expect(() => { 55 60 fs.lstatSync(testFile); 56 61 fs.lstatSync(generatedFile); 62 + fs.lstatSync(baseGeneratedFile); 57 63 }).not.toThrow(); 58 64 59 - expect(fs.readFileSync(testFile, 'utf-8')).toContain( 65 + const testFileContents = fs.readFileSync(testFile, 'utf-8'); 66 + const generatedFileContents = fs.readFileSync(generatedFile, 'utf-8'); 67 + 68 + expect(testFileContents).toContain( 60 69 `as typeof import('./simple.generated').AllPostsDocument` 61 70 ); 62 - expect(fs.readFileSync(generatedFile, 'utf-8')).toContain( 63 - 'export const AllPostsDocument = ' 71 + expect(generatedFileContents).toContain('export const AllPostsDocument = '); 72 + expect(generatedFileContents).toContain( 73 + 'import * as Types from "./__generated__/baseGraphQLSP"' 64 74 ); 65 75 }, 7500); 66 76