vscode-graphql
vscode-graphql copied to clipboard
Work with multiple projects and/or support GraphQL Config
In our project, we're working with multiple different graphs. Sometimes these graphs are actually external services we do not control but we write queries to target them. We've been using VS Code GraphQL: Language Feature Support extension for GraphQL LSP
I've been setting up GraphQL Config https://the-guild.dev/graphql/config to tell the VS Code extension which files should use which schema
Here's the config we are currently using
graphql.config.js
const mxSchema = "backend/packages/graphql-schema/schema.gen.graphql";
module.exports = {
projects: {
default: {
schema: mxSchema,
},
"backend-tests": {
schema: mxSchema,
documents: ["backend/api/src/**/__tests__/**/*.spec.ts"],
},
"server-rendering": {
schema: mxSchema,
documents: [
"backend/server-rendering/src/graphql/**/*!(.gen).{ts,tsx}",
"backend/server-rendering/src/modules/**/graphql/**/*!(.gen).{ts,tsx}",
],
},
"work-order-asset-snapshots": {
schema: mxSchema,
documents: ["backend/graphql/src/mutations/helpers/workOrderAssetSnapshot/**/*.{ts,tsx}"],
},
frontend: {
schema: mxSchema,
documents: [
"frontend/src/**/*.gql!(.gen).{ts,tsx}",
"frontend/src/graphql/**/*!(.gen).{ts,tsx}",
"frontend/src/modules/**/graphql/**/*!(.gen).{ts,tsx}",
"native/src/shared-all/graphql/**/*!(.gen).{ts,tsx}",
"native/src/shared-clients/graphql/**/*!(.gen).{ts,tsx}",
],
},
dashboard: {
schema: mxSchema,
documents: [
"dashboard/src/**/*.gql!(.gen).{ts,tsx}",
"dashboard/src/graphql/**/*!(.gen).{ts,tsx}",
"dashboard/src/modules/**/graphql/**/*!(.gen).{ts,tsx}",
"native/src/shared-all/graphql/**/*!(.gen).{ts,tsx}",
"native/src/shared-clients/graphql/**/*!(.gen).{ts,tsx}",
],
},
native: {
schema: mxSchema,
documents: [
"native/src/**/*.gql!(.gen).{ts,tsx}",
"native/src/graphql/**/*!(.gen).{ts,tsx}",
"native/src/modules/**/graphql/**/*!(.gen).{ts,tsx}",
"native/src/shared-all/graphql/**/*!(.gen).{ts,tsx}",
"native/src/shared-clients/graphql/**/*!(.gen).{ts,tsx}",
],
},
gpl: {
schema: "gql-gen/gpl.gen.graphql",
documents: "backend/common/src/core/gpl/*!(.gen).ts",
},
metal: {
schema: "backend/node_modules/@metal/graphql/schema.gen.graphql",
documents: "backend/common/src/core/metal/*!(.gen).ts",
},
},
};
You'll notice that we have multiple "client" applications targeting the same schema which allows to better understand which fragments belongs to which project.
Also at the end you'll notice that have a local copy of the gpl project that is an external api we call and the metal project has an sdk providing the schema that we're using as well.
Worth noting that we have a separate config for GraphQL Codegen, because the VS Code GraphQL LSP extension doesn't support multiple schema files even though the GraphQL Config standard supports it. So we stitch all our schemas into 1 big schema file and pipe this into the extension instead
Since we are starting to look into Federation, we are investigating better extension to empower our devs and was hoping Apollo's extension would support the GraphQL Config for a more standardized way to provide a great GraphQL experience to our developpers.
I tried the following to at least get support for the main project
const graphqlConfig = require("./graphql.config");
const defaultSchema = graphqlConfig.projects.default.schema;
module.exports = {
client: {
service: {
// can be a string pointing to a single file or an array of strings
localSchemaFile: defaultSchema,
},
includes: Array.from(
new Set(
Array.from(
new Set(
// Object.values(graphqlConfig.projects)
// Somehow native and frontend are causing the extension to crash
[
graphqlConfig.projects.dashboard,
graphqlConfig.projects["server-rendering"],
graphqlConfig.projects["work-order-asset-snapshots"],
]
.filter((p) => p.schema === defaultSchema)
.flatMap((p) => p.documents || []),
),
),
),
),
excludes: ["*.gen.*"],
},
};
Not sure why, but if I include files from the frontend or native projects the extension crashes with this error
/Users/micfer/.vscode/extensions/apollographql.vscode-apollo-2.5.1/lib/language-server/server.js:1084
`)}t(Dor,"dedentBlockStringValue");function kor(i){let r=null;for(let s=1;s<i.length;s++){let c=i[s],u=Mqt(c);if(u!==c.length&&(r===null||u<r)&&(r=u,r===0))break}return r===null?0:r}t(kor,"getBlockStringIndentation");function Mqt(i){let r=0;for(;r<i.length&&(i[r]===" "||i[r]===" ");)r++;return r}t(Mqt,"leadingWhitespace");function Rqt(i){return Mqt(i)===i.length}t(Rqt,"isBlank");var Qqt=jo(Fo(),1);function jqt(i){return i&&typeof i=="object"&&"kind"in i&&i.kind===Qqt.Kind.DOCUMENT}t(jqt,"isDocumentNode");function Uqt(i,r,s){let c=xor([...r,...i].filter(yz),s);return s&&s.sort&&c.sort(sM),c}t(Uqt,"mergeArguments");function xor(i,r){return i.reduce((s,c)=>{let u=s.findIndex(_=>_.name.value===c.name.value);return u===-1?s.concat([c]):(r?.reverseArguments||(s[u]=c),s)},[])}t(xor,"deduplicateArguments");function wor(i,r){return!!i.find(s=>s.name.value===r.name.value)}t(wor,"directiveAlreadyExists");function qqt(i,r){return!!r?.[i.name.value]?.repeatable}t(qqt,"isRepeatableDirective");function Vqt(i,r){return r.some(({value:s})=>s===i.value)}t(Vqt,"nameAlreadyExists");function Jqt(i,r){let s=[...r];for(let c of i){let u=s.findIndex(_=>_.name.value===c.name.value);if(u>-1){let _=s[u];if(_.value.kind==="ListValue"){let g=_.value.values,b=c.value.values;_.value.values=Wqt(g,b,(I,w)=>{let L=I.value;return!L||!w.some(V=>V.value===L)})}else _.value=c.value}else s.push(c)}return s}t(Jqt,"mergeArguments");function Nor(i,r){return i.map((s,c,u)=>{let _=u.findIndex(g=>g.name.value===s.name.value);if(_!==c&&!qqt(s,r)){let g=u[_];return s.arguments=Jqt(s.arguments,g.arguments),null}return s}).filter(yz)}t(Nor,"deduplicateDirectives");function iv(i=[],r=[],s,c){let u=s&&s.reverseDirectives,_=u?i:r,g=u?r:i,b=Nor([..._],c);for(let I of g)if(wor(b,I)&&!qqt(I,c)){let w=b.findIndex(V=>V.name.value===I.name.value),L=b[w];b[w].arguments=Jqt(I.arguments||[],L.arguments||[])}else b.push(I);return b}t(iv,"mergeDirectives");function Gqt(i,r){return r?{...i,arguments:Wqt(r.arguments||[],i.arguments||[],(s,c)=>!Vqt(s.name,c.map(u=>u.name))),locations:[...r.locations,...i.locations.filter(s=>!Vqt(s,r.locations))]}:i}t(Gqt,"mergeDirective");function Wqt(i,r,s){return i.concat(r.filter(c=>s(c,i)))}t(Wqt,"deduplicateLists");function Hqt(i,r,s,c){if(s?.consistentEnumMerge){let g=[];i&&g.push(...i),i=r,r=g}let u=new Map;if(i)for(let g of i)u.set(g.name.value,g);if(r)for(let g of r){let b=g.name.value;if(u.has(b)){let I=u.get(b);I.description=g.description||I.description,I.directives=iv(g.directives,I.directives,c)}else u.set(b,g)}let _=[...u.values()];return s&&s.sort&&_.sort(sM),_}t(Hqt,"mergeEnumValues");var zqt=jo(Fo(),1);function $qt(i,r,s,c){return r?{name:i.name,description:i.description||r.description,kind:s?.convertExtensions||i.kind==="EnumTypeDefinition"||r.kind==="EnumTypeDefinition"?"EnumTypeDefinition":"EnumTypeExtension",loc:i.loc,directives:iv(i.directives,r.directives,s,c),values:Hqt(i.values,r.values,s)}:s?.convertExtensions?{...i,kind:zqt.Kind.ENUM_TYPE_DEFINITION}:i}t($qt,"mergeEnum");var d9=jo(Fo(),1);function Yqt(i){return typeof i=="string"}t(Yqt,"isStringTypes");function Kqt(i){return i instanceof d9.Source}t(Kqt,"isSourceTypes");function hXe(i){let r=i;for(;r.kind===d9.Kind.LIST_TYPE||r.kind==="NonNullType";)r=r.type;return r}t(hXe,"extractType");function gXe(i){return i.kind!==d9.Kind.NAMED_TYPE}t(gXe,"isWrappingTypeNode");function FCe(i){return i.kind===d9.Kind.LIST_TYPE}t(FCe,"isListTypeNode");function lM(i){return i.kind===d9.Kind.NON_NULL_TYPE}t(lM,"isNonNullTypeNode");function Lae(i){return FCe(i)?`[${Lae(i.type)}]`:lM(i)?`${Lae(i.type)}!`:i.name.value}t(Lae,"printTypeNode");var cM;(function(i){i[i.A_SMALLER_THAN_B=-1]="A_SMALLER_THAN_B",i[i.A_EQUALS_B=0]="A_EQUALS_B",i[i.A_GREATER_THAN_B=1]="A_GREATER_THAN_B"})(cM||(cM={}));function Xqt(i,r){return i==null&&r==null?cM.A_EQUALS_B:i==null?cM.A_SMALLER_THAN_B:r==null?cM.A_GREATER_THAN_B:i<r?cM.A_SMALLER_THAN_B:i>r?cM.A_GREATER_THAN_B:cM.A_EQUALS_B}t(Xqt,"defaultStringComparator");function Por(i,r){let s=i.findIndex(c=>c.name.value===r.name.value);return[s>-1?i[s]:null,s]}t(Por,"fieldAlreadyExists");function Ez(i,r,s,c,u){let _=[];if(s!=null&&_.push(...s),r!=null)for(let g of r){let[b,I]=Por(_,g);if(b&&!c?.ignoreFieldConflicts){let w=c?.onFieldTypeConflict&&c.onFieldTypeConflict(b,g,i,c?.throwOnConflict)||Oor(i,b,g,c?.throwOnConflict);w.arguments=Uqt(g.arguments||[],b.arguments||[],c),w.directives=iv(g.directives,b.directives,c,u),w.description=g.description||b.description,_[I]=w}else _.push(g)}if(c&&c.sort&&_.sort(sM),c&&c.exclusions){let g=c.exclusions;return _.filter(b=>!g.includes(`${i.name.value}.${b.name.value}`))}return _}t(Ez,"mergeFields");function Oor(i,r,s,c=!1){let u=Lae(r.type),_=Lae(s.type);if(u!==_){let g=hXe(r.type),b=hXe(s.type);if(g.name.value!==b.name.value)throw new Error(`Field "${s.name.value}" already defined with a different type. Declared as "${g.name.value}", but you tried to override with "${b.name.value}"`);if(!Mae(r.type,s.type,!c))throw new Error(`Field '${i.name.value}.${r.name.value}' changed type from '${u}' to '${_}'`)}return lM(s.type)&&!lM(r.type)&&(r.type=s.type),r}t(Oor,"preventConflicts");function Mae(i,r,s=!1){if(!gXe(i)&&!gXe(r))return i.toString()===r.toString();if(lM(r)){let c=lM(i)?i.type:i;return Mae(c,r.type)}return lM(i)?Mae(r,i,s):FCe(i)?FCe(r)&&Mae(i.type,r.type)||lM(r)&&Mae(i,r.type):!1}t(Mae,"safeChangeForFieldType");var Zqt=jo(Fo(),1);function eJt(i,r,s,c){if(r)try{return{name:i.name,description:i.description||r.description,kind:s?.convertExtensions||i.kind==="InputObjectTypeDefinition"||r.kind==="InputObjectTypeDefinition"?"InputObjectTypeDefinition":"InputObjectTypeExtension",loc:i.loc,fields:Ez(i,i.fields,r.fields,s),directives:iv(i.directives,r.directives,s,c)}}catch(u){throw new Error(`Unable to merge GraphQL input type "${i.name.value}": ${u.message}`)}return s?.convertExtensions?{...i,kind:Zqt.Kind.INPUT_OBJECT_TYPE_DEFINITION}:i}t(eJt,"mergeInputType");var tJt=jo(Fo(),1);function For(i,r){return!!i.find(s=>s.name.value===r.name.value)}t(For,"alreadyExists");function Sz(i=[],r=[],s={}){let c=[...r,...i.filter(u=>!For(r,u))];return s&&s.sort&&c.sort(sM),c}t(Sz,"mergeNamedTypeArray");function nJt(i,r,s,c){if(r)try{return{name:i.name,description:i.description||r.description,kind:s?.convertExtensions||i.kind==="InterfaceTypeDefinition"||r.kind==="InterfaceTypeDefinition"?"InterfaceTypeDefinition":"InterfaceTypeExtension",loc:i.loc,fields:Ez(i,i.fields,r.fields,s,c),directives:iv(i.directives,r.directives,s,c),interfaces:i.interfaces?Sz(i.interfaces,r.interfaces,s):void 0}}catch(u){throw new Error(`Unable to merge GraphQL interface "${i.name.value}": ${u.message}`)}return s?.convertExtensions?{...i,kind:tJt.Kind.INTERFACE_TYPE_DEFINITION}:i}t(nJt,"mergeInterface");var cb=jo(Fo(),1),lJt=jo(dx(),1);var rJt=jo(Fo(),1);function iJt(i,r,s,c){return r?{name:i.name,description:i.description||r.description,kind:s?.convertExtensions||i.kind==="ScalarTypeDefinition"||r.kind==="ScalarTypeDefinition"?"ScalarTypeDefinition":"ScalarTypeExtension",loc:i.loc,directives:iv(i.directives,r.directives,s,c)}:s?.convertExtensions?{...i,kind:rJt.Kind.SCALAR_TYPE_DEFINITION}:i}t(iJt,"mergeScalar");var Tz=jo(Fo(),1);var RCe={query:"Query",mutation:"Mutation",subscription:"Subscription"};function Ror(i=[],r=[]){let s=[];for(let c in RCe){let u=i.find(_=>_.operation===c)||r.find(_=>_.operation===c);u&&s.push(u)}return s}t(Ror,"mergeOperationTypes");function sJt(i,r,s,c){return r?{kind:i.kind===Tz.Kind.SCHEMA_DEFINITION||r.kind===Tz.Kind.SCHEMA_DEFINITION?Tz.Kind.SCHEMA_DEFINITION:Tz.Kind.SCHEMA_EXTENSION,description:i.description||r.description,directives:iv(i.directives,r.directives,s,c),operationTypes:Ror(i.operationTypes,r.operationTypes)}:s?.convertExtensions?{...i,kind:Tz.Kind.SCHEMA_DEFINITION}:i}t(sJt,"mergeSchemaDefs");var oJt=jo(Fo(),1);function aJt(i,r,s,c){if(r)try{return{name:i.name,description:i.description||r.description,kind:s?.convertExtensions||i.kind==="ObjectTypeDefinition"||r.kind==="ObjectTypeDefinition"?"ObjectTypeDefinition":"ObjectTypeExtension",l[Error - 12:02:36 PM] Server process exited with code 1.
[Info - 12:02:36 PM] Connection to server got closed. Server will restart.
true
For what it's worth, right now you can have multiple configurations in multiple folders, and it should find all of those.
As for that crash: do you get any more information than that? Could you rename the config file to end in .cjs to ensure it's evaluated as commonJs?
I didn't know you could have apollo.config.js in multiple folders, I'm gonna try that right now.
As for the error, I cloned this repo and launch the extension in debug and I was able to get the actual error message It seems it crashes on conflicting operations which happens because I was grouping too much stuff together It also happens in a single project where some devs copy pasted some operations and happen to be exactly the same (otherwise GraphQL-Codegen would have complained). I'll have to make this rule stricter That being said, it would be nice if the extension could properly report the error in production build and even better to not crash and actually report the error in the IDE
Debugger listening on ws://127.0.0.1:6009/53ca72a0-f97b-4386-99ff-3235f60a6756
For help, see: https://nodejs.org/en/docs/inspector
/Users/micfer/projects/vscode-graphql/lib/language-server/server.js:324152
throw new Error(
^
Error: ️️There are multiple definitions for the `WorkOrderSnapshotQuery` operation. Please fix all naming conflicts before continuing.
Conflicting definitions found at /Users/micfer/projects/maintainx/frontend/src/graphql/queries/WorkOrderDetailsSnapshotQuery.ts and /Users/micfer/projects/maintainx/backend/graphql/src/mutations/helpers/workOrderAssetSnapshot/queries/workOrderSnapshotQuery.ts.
at GraphQLClientProject.<anonymous> (/Users/micfer/projects/vscode-graphql/lib/language-server/server.js:324152:25)
at invokeFunc (/Users/micfer/projects/vscode-graphql/lib/language-server/server.js:307137:23)
at trailingEdge (/Users/micfer/projects/vscode-graphql/lib/language-server/server.js:307168:18)
at Timeout.timerExpired [as _onTimeout] (/Users/micfer/projects/vscode-graphql/lib/language-server/server.js:307160:18)
at listOnTimeout (node:internal/timers:581:17)
at process.processTimers (node:internal/timers:519:7)
Node.js v20.16.0
[Error - 3:42:59 PM] Server process exited with code 1.
[Error - 3:42:59 PM] The Apollo GraphQL server crashed 5 times in the last 3 minutes. The server will not be restarted. See the output for more information.
Alright as for multi repos I got it working with multiple apollo.config.js files
Now I'm facing another issue where the extension doesn't seem to pick up my local schema files in the include. It works when I'm working on operations great! But when I'm editing the schema locally, I don't have basic go to definition features working.
Since my original issue is solved do you want me to open different issues for this and the duplicate definition error ?
Yeah, let's keep this in a bunch of separate issues.
That said, don't expect too many "schema editing" functionality - what you are creating here are client projects, so the focus is on client development - it mostly just reads the schema in and uses that as a starting point. From the back of my mind, I'm not sure how many features we have enabled in schemas.
For schema development, you would create a rover project - those even have federation and connectors support, but the functionality is currently still a preview feature:
https://www.apollographql.com/docs/graphos/schema-design/connectors/vs-code
Worth mentioning that in 1 project I need to call into multiple different graphql services I initially created apollo.config.mjs files into the various folders hosting the various operations. But those were in sources so TypeScript started picking up apollo.config.mjs file (since we use allowJs)
I had to move the various apollo.config.mjs files to the root of the project in a folder structure like so
- backend
- apollo-configs
- metal
- apollo.config.mjs
- gql
- apollo.config.mjs
- snapshots
- apollo.config.mjs
- src
- metal
- gpl
- snapshots
This feels a bit convoluted and I'm not sure why I couldn't put all my graphql projects into 1 apollo.config file considering the extension does support multiple configs through discoverability The graphql.config.js file I put at the top is much simpler to manage in a monorepo
In the end the work around to have multiple apollo.config.js files worked. I'd still prefer some way to define everything in 1 file Also would be nice to document how to work with multiple projects somewhere (unless I missed it?)
I have a use case where I need to work with two different schemas in the same project, and they need to stay separated.
Codegen allows me to generate types this way by using two different codegen-<project_name>.ts files.
Is there a solution for this?
I have a use case where I need to work with two different schemas in the same project, and they need to stay separated.
Codegen allows me to generate types this way by using two different codegen-<project_name>.ts files.
Is there a solution for this?
Here's the solution I found in one of my project that is using different schemas for different use cases
I have the following project structure
backend/
├── apollo-configs/
│ ├── README.md
│ ├── gpl/
│ │ └── apollo.config.mjs (includes: ../../common/src/core/gpl/*.ts)
│ ├── metal/
│ │ └── apollo.config.mjs (includes: ../../common/src/core/metal/*.ts)
│ ├── tests/
│ │ └── apollo.config.mjs (includes: ../../api/src/**/__tests__/**/*.spec.ts)
│ └── work-order-asset-snapshots/
│ └── apollo.config.mjs (includes: ../../graphql/src/mutations/helpers/workOrderAssetSnapshot/**/*.{ts,tsx})
├── common/
│ └── src/
│ └── core/
│ ├── gpl/
│ └── metal/
├── api/
│ └── src/
│ └── __tests__/
├── graphql/
│ └── src/
│ └── mutations/
│ └── helpers/
│ └── workOrderAssetSnapshot/
The apollo configs cannot be in "src" folders otherwise typescript will try to compile them as project files. apollo.config.mjs files must be in different folders for the extension to pick them all up.