diff --git a/.changeset/light-penguins-explain.md b/.changeset/light-penguins-explain.md new file mode 100644 index 00000000..028c9566 --- /dev/null +++ b/.changeset/light-penguins-explain.md @@ -0,0 +1,26 @@ +--- +'@0no-co/graphqlsp': minor +--- + +Add support for defining multiple indepenent schemas through a new config property called `schemas`, you can +pass a config like the following: + +```json +{ + "name": "@0no-co/graphqlsp", + "schemas": [ + { + "name": "pokemons", + "schema": "./pokemons.graphql", + "tadaOutputLocation": "./pokemons-introspection.d.ts" + }, + { + "name": "weather", + "schema": "./weather.graphql", + "tadaOutputLocation": "./weather-introspection.d.ts" + } + ] +} +``` + +The LSP will depending on what `graphql()` template you use figure out what API you are reaching out to. diff --git a/packages/example-tada/introspection.d.ts b/packages/example-tada/introspection.d.ts index 0e7125df..19817a43 100644 --- a/packages/example-tada/introspection.d.ts +++ b/packages/example-tada/introspection.d.ts @@ -10,29 +10,24 @@ * instead save to a .ts instead of a .d.ts file. */ export type introspection = { + name: 'pokemons'; query: 'Query'; mutation: never; subscription: never; types: { 'Attack': { kind: 'OBJECT'; name: 'Attack'; fields: { 'damage': { name: 'damage'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'type': { name: 'type'; type: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; } }; }; }; - 'Int': unknown; - 'String': unknown; 'AttacksConnection': { kind: 'OBJECT'; name: 'AttacksConnection'; fields: { 'fast': { name: 'fast'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Attack'; ofType: null; }; } }; 'special': { name: 'special'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Attack'; ofType: null; }; } }; }; }; + 'Boolean': unknown; 'EvolutionRequirement': { kind: 'OBJECT'; name: 'EvolutionRequirement'; fields: { 'amount': { name: 'amount'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; - 'Pokemon': { kind: 'OBJECT'; name: 'Pokemon'; fields: { 'attacks': { name: 'attacks'; type: { kind: 'OBJECT'; name: 'AttacksConnection'; ofType: null; } }; 'classification': { name: 'classification'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'evolutionRequirements': { name: 'evolutionRequirements'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'EvolutionRequirement'; ofType: null; }; } }; 'evolutions': { name: 'evolutions'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; }; } }; 'fleeRate': { name: 'fleeRate'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; 'height': { name: 'height'; type: { kind: 'OBJECT'; name: 'PokemonDimension'; ofType: null; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'maxCP': { name: 'maxCP'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'maxHP': { name: 'maxHP'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'resistant': { name: 'resistant'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'types': { name: 'types'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'weaknesses': { name: 'weaknesses'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'weight': { name: 'weight'; type: { kind: 'OBJECT'; name: 'PokemonDimension'; ofType: null; } }; }; }; 'Float': unknown; 'ID': unknown; + 'Int': unknown; + 'Pokemon': { kind: 'OBJECT'; name: 'Pokemon'; fields: { 'attacks': { name: 'attacks'; type: { kind: 'OBJECT'; name: 'AttacksConnection'; ofType: null; } }; 'classification': { name: 'classification'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'evolutionRequirements': { name: 'evolutionRequirements'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'EvolutionRequirement'; ofType: null; }; } }; 'evolutions': { name: 'evolutions'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; }; } }; 'fleeRate': { name: 'fleeRate'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; 'height': { name: 'height'; type: { kind: 'OBJECT'; name: 'PokemonDimension'; ofType: null; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'maxCP': { name: 'maxCP'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'maxHP': { name: 'maxHP'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'resistant': { name: 'resistant'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'types': { name: 'types'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'weaknesses': { name: 'weaknesses'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'weight': { name: 'weight'; type: { kind: 'OBJECT'; name: 'PokemonDimension'; ofType: null; } }; }; }; 'PokemonDimension': { kind: 'OBJECT'; name: 'PokemonDimension'; fields: { 'maximum': { name: 'maximum'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'minimum': { name: 'minimum'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; - 'PokemonType': { kind: 'ENUM'; name: 'PokemonType'; type: 'Bug' | 'Dark' | 'Dragon' | 'Electric' | 'Fairy' | 'Fighting' | 'Fire' | 'Flying' | 'Ghost' | 'Grass' | 'Ground' | 'Ice' | 'Normal' | 'Poison' | 'Psychic' | 'Rock' | 'Steel' | 'Water'; }; + 'PokemonType': { name: 'PokemonType'; enumValues: 'Bug' | 'Dark' | 'Dragon' | 'Electric' | 'Fairy' | 'Fighting' | 'Fire' | 'Flying' | 'Ghost' | 'Grass' | 'Ground' | 'Ice' | 'Normal' | 'Poison' | 'Psychic' | 'Rock' | 'Steel' | 'Water'; }; 'Query': { kind: 'OBJECT'; name: 'Query'; fields: { 'pokemon': { name: 'pokemon'; type: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; } }; 'pokemons': { name: 'pokemons'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; }; } }; }; }; - 'Boolean': unknown; + 'String': unknown; }; }; import * as gqlTada from 'gql.tada'; - -declare module 'gql.tada' { - interface setupSchema { - introspection: introspection; - } -} diff --git a/packages/example-tada/package.json b/packages/example-tada/package.json index d1e46df1..50c09529 100644 --- a/packages/example-tada/package.json +++ b/packages/example-tada/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", - "gql.tada": "^1.4.0", + "gql.tada": "1.5.9-canary-8711af177005f46fa3e06d990b6ba28e353e7f9b", "@urql/core": "^3.0.0", "graphql": "^16.8.1", "urql": "^4.0.6" diff --git a/packages/example-tada/tsconfig.json b/packages/example-tada/tsconfig.json index 4ea74652..4a1546bf 100644 --- a/packages/example-tada/tsconfig.json +++ b/packages/example-tada/tsconfig.json @@ -3,8 +3,13 @@ "plugins": [ { "name": "@0no-co/graphqlsp", - "schema": "./schema.graphql", - "tadaOutputLocation": "./introspection.d.ts" + "schemas": [ + { + "name": "pokemons", + "schema": "./schema.graphql", + "tadaOutputLocation": "./introspection.d.ts" + } + ] } ], "jsx": "react-jsx", diff --git a/packages/graphqlsp/package.json b/packages/graphqlsp/package.json index 591e8c59..8ec0b904 100644 --- a/packages/graphqlsp/package.json +++ b/packages/graphqlsp/package.json @@ -51,7 +51,7 @@ "typescript": "^5.3.3" }, "dependencies": { - "@gql.tada/internal": "^0.1.2", + "@gql.tada/internal": "0.3.0-canary-8711af177005f46fa3e06d990b6ba28e353e7f9b", "graphql": "^16.8.1", "node-fetch": "^2.0.0" }, diff --git a/packages/graphqlsp/src/ast/index.ts b/packages/graphqlsp/src/ast/index.ts index fead37a1..b4adcf3f 100644 --- a/packages/graphqlsp/src/ast/index.ts +++ b/packages/graphqlsp/src/ast/index.ts @@ -124,19 +124,54 @@ export function unrollTadaFragments( return wip; } +export const getSchemaName = ( + node: ts.CallExpression, + typeChecker?: ts.TypeChecker +): string | null => { + if (!typeChecker) return null; + + const expression = ts.isPropertyAccessExpression(node.expression) + ? node.expression.expression + : node.expression; + const type = typeChecker.getTypeAtLocation(expression); + if (type) { + const brandTypeSymbol = type.getProperty('__name'); + if (brandTypeSymbol) { + const brand = typeChecker.getTypeOfSymbol(brandTypeSymbol); + if (brand.isUnionOrIntersection()) { + const found = brand.types.find(x => x.isStringLiteral()); + return found && found.isStringLiteral() ? found.value : null; + } else if (brand.isStringLiteral()) { + return brand.value; + } + } + } + + return null; +}; + export function findAllCallExpressions( sourceFile: ts.SourceFile, info: ts.server.PluginCreateInfo, shouldSearchFragments: boolean = true ): { - nodes: Array; + nodes: Array<{ + node: ts.NoSubstitutionTemplateLiteral; + schema: string | null; + }>; fragments: Array; } { - const result: Array = []; + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); + const result: Array<{ + node: ts.NoSubstitutionTemplateLiteral; + schema: string | null; + }> = []; let fragments: Array = []; let hasTriedToFindFragments = shouldSearchFragments ? false : true; function find(node: ts.Node) { if (ts.isCallExpression(node) && templates.has(node.expression.getText())) { + const name = getSchemaName(node, typeChecker); + const [arg, arg2] = node.arguments; if (!hasTriedToFindFragments && !arg2) { @@ -160,7 +195,7 @@ export function findAllCallExpressions( } if (arg && ts.isNoSubstitutionTemplateLiteral(arg)) { - result.push(arg); + result.push({ node: arg, schema: name }); } return; } else { @@ -172,11 +207,13 @@ export function findAllCallExpressions( } export function findAllPersistedCallExpressions( - sourceFile: ts.SourceFile -): Array { - const result: Array = []; + sourceFile: ts.SourceFile, + info: ts.server.PluginCreateInfo +): Array<{ node: ts.CallExpression; schema: string | null }> { + const result: Array<{ node: ts.CallExpression; schema: string | null }> = []; + const typeChecker = info.languageService.getProgram()?.getTypeChecker(); function find(node: ts.Node) { - if (ts.isCallExpression(node)) { + if (node && ts.isCallExpression(node)) { // This expression ideally for us looks like