-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Bugfix: enums from external fragments were not generated along with operations #10565
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
6616585
7e204c1
2fc4571
056dbd0
ba85977
6fd5428
790b876
e73b9ba
1f54f42
900aa10
10a2774
10d3b85
9b3dbf8
64b7834
17c9614
ab0ab18
1498d8f
3839b02
ad44930
c2c6ea0
4d86e60
85fd378
d7e75e3
28e61b0
b474a3f
5f4666c
da8f02a
899fd58
4789809
15b4278
24f7aec
ed99011
2740719
65d158a
eec25ef
180a80a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| **/__generated__/** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| #!/usr/bin/env ts-node | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I created the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think so! I find this test is very valuable, as it can be used to catch unexpected bugs, at least in the initial stage after rolling out. I feel we might be able to simplify a few things in the setup. And worst case, if its usefulness is overshadowed by maintenance, we can decide to drop it (or further simplify) later 🙂
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I renamed |
||
|
|
||
| import path from 'path'; | ||
| import { isEmpty, uniq } from 'lodash-es'; | ||
| import { generate } from '@graphql-codegen/cli'; | ||
| import type { Types } from '@graphql-codegen/plugin-helpers'; | ||
| import deleteAsync from 'del'; | ||
|
|
||
| export const GENERATED = '__generated__'; | ||
| export const GLOBAL_TYPES_FILE = 'globalTypes.ts'; | ||
| export const TS_GENERATED_FILE_HEADER = `\ | ||
| /* tslint:disable */ | ||
| /* eslint-disable */ | ||
| // @generated | ||
| // This file was automatically generated and should not be edited. | ||
| `; | ||
|
|
||
| /** | ||
| * The following GraphQL Codegen config matches as closely as possible | ||
| * to the old apollo-tooling codegen | ||
| * @see https://github.com/apollographql/apollo-tooling/issues/2053 | ||
| * */ | ||
| const GRAPHQL_CODEGEN_CONFIG = { | ||
| skipTypename: false, | ||
| useTypeImports: true, | ||
| preResolveTypes: true, // Simplifies the generated types | ||
| namingConvention: 'keep', // Keeps naming as-is | ||
| avoidOptionals: false, // Allow '?' on variables fields | ||
| nonOptionalTypename: true, // Forces `__typename` on all selection sets | ||
| skipTypeNameForRoot: true, // Don't generate __typename for root types | ||
| omitOperationSuffix: true, // Don't add 'Query', 'Mutation' or 'Subscription' suffixes to operation result types | ||
| fragmentSuffix: '', // Don't add 'Fragment' suffix to fragment result types | ||
| extractAllFieldsToTypes: true, // Extracts all fields to separate types (similar to apollo-codegen behavior) | ||
| printFieldsOnNewLines: true, // Prints each field on a new line (similar to apollo-codegen behavior) | ||
| namespacedImportName: '', | ||
| importTypesNamespace: '', | ||
| enumType: 'native', | ||
| generatesOperationTypes: true, | ||
| importSchemaTypesFrom: false, | ||
| }; | ||
|
|
||
| export const main = async () => { | ||
| const cwd = process.cwd(); | ||
|
|
||
| await deleteAsync([`src/**/${GENERATED}`], { cwd }); | ||
|
|
||
| const localSchemaFilePath = `${cwd}/schema.graphql`; | ||
|
|
||
| const { internalIncludes, externalIncludes } = { internalIncludes: [`${cwd}/src/**/*.ts`], externalIncludes: [] }; | ||
|
|
||
| const generateFiles: { [scanPath: string]: Types.ConfiguredOutput } = {}; | ||
|
|
||
| const includesForCodegen = uniq(internalIncludes.map((includes: string) => includes.replace(/\/\*\*\/.+$/, ''))); | ||
|
|
||
| const globalTypesPath = `${cwd}/src/${GENERATED}/${GLOBAL_TYPES_FILE}`; | ||
|
|
||
| const monorepoRoot = path.resolve(cwd, '..'); | ||
|
|
||
| if (!isEmpty(includesForCodegen)) { | ||
| // Prepare the required structure for GraphQL Codegen | ||
| includesForCodegen.forEach((includes: string) => { | ||
| const relativeIncludes = path.relative(monorepoRoot, includes); | ||
|
|
||
| generateFiles[relativeIncludes] = { | ||
| preset: 'near-operation-file', // This preset tells the codegen to generate multiple files instead of one | ||
| presetConfig: { | ||
| extension: '.ts', // Matches the existing Apollo-Codegen file naming | ||
| baseTypesPath: `../${path.relative(cwd, globalTypesPath)}`, // Relative (to repo root) path to the global types file to include | ||
| folder: GENERATED, // Output folder for generated files | ||
| }, | ||
| plugins: [ | ||
| 'typescript-operations', | ||
| { | ||
| add: { | ||
| content: TS_GENERATED_FILE_HEADER, | ||
| }, | ||
| }, | ||
| ], | ||
| }; | ||
| }); | ||
|
|
||
| await generate( | ||
| { | ||
| schema: localSchemaFilePath, | ||
| documents: [ | ||
| // matching js extensins as well - there are cases where js files are not converted to typescript yet | ||
| // (but the package is typescript) | ||
| ...includesForCodegen.map(include => `${include}/**/*.{js,jsx,ts,tsx}`), | ||
| ...externalIncludes, | ||
| `!**/${GENERATED}/**`, | ||
| ], | ||
| config: GRAPHQL_CODEGEN_CONFIG, | ||
| generates: generateFiles, | ||
| silent: true, | ||
| }, | ||
| true // overwrite existing files | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| if (import.meta.url === process.argv[1] || import.meta.url === `file://${process.argv[1]}`) { | ||
| main().catch(e => { | ||
| console.error(e); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "name": "dev-test-alpha", | ||
| "description": "dev-test-alpha", | ||
| "version": "0.0.1", | ||
| "type": "module", | ||
| "dependencies": { | ||
| "@apollo/client": "3.13.8", | ||
| "@graphql-codegen/cli": "workspace:../packages/graphql-codegen-cli", | ||
| "@graphql-codegen/near-operation-file-preset": "4.0.0", | ||
| "@graphql-codegen/plugin-helpers": "workspace:../packages/utils/plugins-helpers", | ||
| "@graphql-codegen/typescript-operations": "workspace:../packages/plugins/typescript/operations", | ||
| "del": "^6.1.1", | ||
| "graphql": "^16.9.0", | ||
| "lodash-es": "^4.17.15" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/lodash-es": "^4.17.6", | ||
| "@types/node": "^25.0.3", | ||
| "typescript": "^5.9.3" | ||
| }, | ||
| "files": [ | ||
| "cli" | ||
| ], | ||
| "scripts": { | ||
| "start": "npx tsx cli/index.ts" | ||
| }, | ||
| "sideEffects": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| schema | ||
| @link(url: "https://specs.apollo.dev/link/v1.0") | ||
| @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) | ||
| @link(url: "https://specs.apollo.dev/tag/v0.3") { | ||
| query: Query | ||
| } | ||
|
|
||
| directive @join__graph(name: String!, url: String!) on ENUM_VALUE | ||
|
|
||
| directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE | ||
|
|
||
| directive @join__field( | ||
| graph: join__Graph | ||
| requires: join__FieldSet | ||
| provides: join__FieldSet | ||
| type: String | ||
| external: Boolean | ||
| override: String | ||
| usedOverridden: Boolean | ||
| overrideLabel: String | ||
| ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ||
|
|
||
| directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE | ||
|
|
||
| directive @join__type( | ||
| graph: join__Graph! | ||
| key: join__FieldSet | ||
| extension: Boolean! = false | ||
| resolvable: Boolean! = true | ||
| isInterfaceObject: Boolean! = false | ||
| ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR | ||
|
|
||
| directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION | ||
|
|
||
| directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA | ||
|
|
||
| directive @tag( | ||
| name: String! | ||
| ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA | ||
|
|
||
| scalar join__FieldSet | ||
|
|
||
| scalar link__Import | ||
|
|
||
| enum link__Purpose { | ||
| EXECUTION | ||
| } | ||
|
|
||
| enum join__Graph { | ||
| SUPER_USER_MANAGER @join__graph(name: "super_user_management", url: "http://i.am.not.used.example.com") | ||
| GRAPHQL_MAIN__SHARD__BASE @join__graph(name: "graphql_main__shard__base", url: "http://i.am.not.used.example.com") | ||
| GRAPHQL_MAIN__SHARD__INTERNAL_TESTING | ||
| @join__graph(name: "graphql_main__shard__internal_testing", url: "http://i.am.not.used.example.com") | ||
| } | ||
|
|
||
| type Query { | ||
| superUser: SuperUser! @join__field(graph: GRAPHQL_MAIN__SHARD__BASE) | ||
| } | ||
|
|
||
| enum UserManagerRoleType @join__type(graph: SUPER_USER_MANAGER) { | ||
| ROLE_TYPE_1 @join__enumValue(graph: SUPER_USER_MANAGER) | ||
|
|
||
| ROLE_TYPE_2 @join__enumValue(graph: SUPER_USER_MANAGER) | ||
|
|
||
| ROLE_TYPE_3 @join__enumValue(graph: SUPER_USER_MANAGER) | ||
| } | ||
|
|
||
| type UserManager @join__type(graph: SUPER_USER_MANAGER) { | ||
| fooUser: User! | ||
|
|
||
| roleType: UserManagerRoleType! | ||
| } | ||
|
|
||
| type User { | ||
| id: ID! | ||
| profilePhoto: UserProfilePhoto @join__field(graph: GRAPHQL_MAIN__SHARD__BASE) | ||
| } | ||
|
|
||
| type UserProfilePhoto @join__type(graph: GRAPHQL_MAIN__SHARD__BASE) { | ||
| id: ID! | ||
| photoUrl: UserPhotoUrl | ||
| } | ||
|
|
||
| type UserPhotoUrl @join__type(graph: GRAPHQL_MAIN__SHARD__BASE) { | ||
| url(size: UserPhotoSize!): String | ||
| } | ||
|
|
||
| enum UserPhotoSize | ||
| @join__type(graph: GRAPHQL_MAIN__SHARD__BASE) | ||
| @join__type(graph: GRAPHQL_MAIN__SHARD__INTERNAL_TESTING) { | ||
| SQUARE_300 | ||
| @join__enumValue(graph: GRAPHQL_MAIN__SHARD__BASE) | ||
| @join__enumValue(graph: GRAPHQL_MAIN__SHARD__INTERNAL_TESTING) | ||
| } | ||
|
|
||
| type SuperUser @join__type(graph: GRAPHQL_MAIN__SHARD__BASE) { | ||
| groupFromAlias(alias: String!): SuperUserGroup! | ||
| } | ||
|
|
||
| type SuperUserGroup | ||
| @join__type(graph: SUPER_USER_MANAGER, key: "id") | ||
| @join__type(graph: GRAPHQL_MAIN__SHARD__BASE, key: "id") { | ||
| id: ID! | ||
|
|
||
| managers(onlyPublic: Boolean! = false): [UserManager!] @join__field(graph: SUPER_USER_MANAGER) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { useQuery, gql } from '@apollo/client'; | ||
| import Helper from './Helper'; | ||
|
|
||
| export const getFooQuery = gql` | ||
| ${Helper.fragments.query} | ||
| query GetFoo($alias: String!, $collectionId: String!) { | ||
| superUser { | ||
| groupFromAlias(alias: $alias) { | ||
| managers(onlyPublic: true) { | ||
| ...HelperFields | ||
| } | ||
| } | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| const Component = () => { | ||
| useQuery(getFooQuery, {}); | ||
| }; | ||
|
|
||
| export default Component; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { gql } from '@apollo/client'; | ||
|
|
||
| const getHelperFieldsFragment = gql` | ||
| fragment HelperFields on UserManager { | ||
| roleType | ||
| fooUser { | ||
| profilePhoto { | ||
| photoUrl { | ||
| url(size: SQUARE_300) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| export const helperPropsFromFragment = (fragment: any) => ({ | ||
| profilePhotoUrl: fragment.fooUser.profilePhoto?.photoUrl.url, | ||
| roleType: fragment.roleType, | ||
| }); | ||
|
|
||
| const Helper = { fragments: { query: {} } }; | ||
|
|
||
| Helper.fragments = { | ||
| query: getHelperFieldsFragment, | ||
| }; | ||
|
|
||
| export default Helper; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2020", | ||
| "module": "NodeNext", | ||
| "moduleResolution": "NodeNext", | ||
| "verbatimModuleSyntax": true, | ||
| "esModuleInterop": true, | ||
| "resolveJsonModule": true, | ||
| "strict": true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -121,7 +121,13 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< | |
| ...(config.externalFragments || []), | ||
| ]; | ||
|
|
||
| this._usedNamedInputTypes = this.collectUsedInputTypes({ schema, documentNode }); | ||
| // Create a combined document that includes external fragments for enum collection | ||
| const documentWithExternalFragments: DocumentNode = { | ||
| ...documentNode, | ||
| definitions: [...documentNode.definitions, ...(config.externalFragments || []).map(f => f.node)], | ||
| }; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if the functionality in the above statement used to create
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call! |
||
|
|
||
| this._usedNamedInputTypes = this.collectUsedInputTypes({ schema, documentNode: documentWithExternalFragments }); | ||
|
|
||
| const processorConfig: SelectionSetProcessorConfig = { | ||
| namespacedImportName: this.config.namespacedImportName, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel we should unignore the generation 🙂
This could be a good test that can be kept as snapshot test, and an advanced use case of codegen
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, generated files added!