From cce3ea2a4a8c630ef5163f9ab990818ab09b30ff Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 00:44:02 +1100 Subject: [PATCH 01/13] Draft importSchemaTypesFrom --- .../src/base-documents-visitor.ts | 7 + .../src/graphql-type-utils.ts | 13 + .../other/visitor-plugin-common/src/index.ts | 1 + .../typescript/operations/src/index.ts | 1 + .../typescript/operations/src/visitor.ts | 38 ++- ...-documents.standalone.import-types.spec.ts | 230 ++++++++++++++++++ 6 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 packages/plugins/other/visitor-plugin-common/src/graphql-type-utils.ts create mode 100644 packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts diff --git a/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts index 8ef3b117e88..18b6e477071 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts @@ -42,6 +42,7 @@ export interface ParsedDocumentsConfig extends ParsedTypesConfig { mergeFragmentTypes: boolean; customDirectives: CustomDirectivesConfig; generatesOperationTypes: boolean; + importSchemaTypesFrom: string; } export interface RawDocumentsConfig extends RawTypesConfig { @@ -199,6 +200,11 @@ export interface RawDocumentsConfig extends RawTypesConfig { * ``` */ generatesOperationTypes?: boolean; + + /** + * TODO + */ + importSchemaTypesFrom?: string; } export class BaseDocumentsVisitor< @@ -232,6 +238,7 @@ export class BaseDocumentsVisitor< scalars: buildScalarsFromConfig(_schema, rawConfig, defaultScalars), customDirectives: getConfigValue(rawConfig.customDirectives, { apolloUnmask: false }), generatesOperationTypes: getConfigValue(rawConfig.generatesOperationTypes, true), + importSchemaTypesFrom: getConfigValue(rawConfig.importSchemaTypesFrom, ''), ...((additionalConfig || {}) as any), }); diff --git a/packages/plugins/other/visitor-plugin-common/src/graphql-type-utils.ts b/packages/plugins/other/visitor-plugin-common/src/graphql-type-utils.ts new file mode 100644 index 00000000000..82634cd2849 --- /dev/null +++ b/packages/plugins/other/visitor-plugin-common/src/graphql-type-utils.ts @@ -0,0 +1,13 @@ +import { type GraphQLNamedType, isIntrospectionType, isSpecifiedScalarType } from 'graphql'; + +export const isNativeNamedType = (namedType: GraphQLNamedType): boolean => { + // "Native" NamedType in this context means the following: + // 1. introspection types i.e. with `__` prefixes + // 2. base scalars e.g. Boolean, Int, etc. + // 3. Other natives (mostly base scalars) which was not defined in the schema i.e. no `astNode` + if (isSpecifiedScalarType(namedType) || isIntrospectionType(namedType) || !namedType.astNode) { + return true; + } + + return false; +}; diff --git a/packages/plugins/other/visitor-plugin-common/src/index.ts b/packages/plugins/other/visitor-plugin-common/src/index.ts index bdb72829a17..f645aa973c3 100644 --- a/packages/plugins/other/visitor-plugin-common/src/index.ts +++ b/packages/plugins/other/visitor-plugin-common/src/index.ts @@ -18,3 +18,4 @@ export * from './types.js'; export * from './utils.js'; export * from './variables-to-object.js'; export * from './convert-schema-enum-to-declaration-block-string.js'; +export * from './graphql-type-utils.js'; diff --git a/packages/plugins/typescript/operations/src/index.ts b/packages/plugins/typescript/operations/src/index.ts index 107e330309c..d0d6deb4ec8 100644 --- a/packages/plugins/typescript/operations/src/index.ts +++ b/packages/plugins/typescript/operations/src/index.ts @@ -63,6 +63,7 @@ export const plugin: PluginFunction { + if (!this.config.importSchemaTypesFrom) { + return []; + } + + const hasTypesToImport = Object.keys(this._usedNamedInputTypes).length > 0; + + if (!hasTypesToImport) { + return []; + } + + return [ + generateImportStatement({ + baseDir: '', + baseOutputDir: '', + outputPath: '', + importSource: { + path: '', + namespace: this.config.namespacedImportName, + identifiers: [], + }, + typesImport: true, + // FIXME: rebase with master for the new extension + emitLegacyCommonJSImports: true, + }), + ]; + } + protected getPunctuation(_declarationKind: DeclarationKind): string { return ';'; } protected applyVariablesWrapper(variablesBlock: string, operationType: string): string { - const prefix = this.config.namespacedImportName ? `${this.config.namespacedImportName}.` : ''; const extraType = this.config.allowUndefinedQueryVariables && operationType === 'Query' ? ' | undefined' : ''; - return `${prefix}Exact<${variablesBlock === '{}' ? `{ [key: string]: never; }` : variablesBlock}>${extraType}`; + return `Exact<${variablesBlock === '{}' ? `{ [key: string]: never; }` : variablesBlock}>${extraType}`; } private collectUsedInputTypes({ @@ -236,7 +265,8 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< foundInputType && (foundInputType instanceof GraphQLInputObjectType || foundInputType instanceof GraphQLScalarType || - foundInputType instanceof GraphQLEnumType) + foundInputType instanceof GraphQLEnumType) && + !isNativeNamedType(foundInputType) ) { usedInputTypes[namedTypeNode.name.value] = foundInputType; } diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts new file mode 100644 index 00000000000..a0794a8f1ff --- /dev/null +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts @@ -0,0 +1,230 @@ +import { buildSchema, parse } from 'graphql'; +import { mergeOutputs } from '@graphql-codegen/plugin-helpers'; +import { plugin } from '../src/index.js'; + +describe('TypeScript Operations Plugin - Import Types', () => { + it('imports user-defined types externally with importSchemaTypesFrom correctly', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + user(id: ID!): User + users(input: UsersInput!): UsersResponse! + } + + type ResponseError { + error: ResponseErrorType! + } + + enum ResponseErrorType { + NOT_FOUND + INPUT_VALIDATION_ERROR + FORBIDDEN_ERROR + UNEXPECTED_ERROR + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + "UserRole Description" + enum UserRole { + "UserRole ADMIN" + ADMIN + "UserRole CUSTOMER" + CUSTOMER + } + + "UsersInput Description" + input UsersInput { + "UsersInput from" + from: DateTime + "UsersInput to" + to: DateTime + role: UserRole + } + + type UsersResponseOk { + result: [User!]! + } + union UsersResponse = UsersResponseOk | ResponseError + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query User($id: ID!) { + user(id: $id) { + id + name + role + createdAt + } + } + + query Users($input: UsersInput!) { + users(input: $input) { + ... on UsersResponseOk { + result { + id + } + } + ... on ResponseError { + error + } + } + } + + query UsersWithScalarInput($from: DateTime!, $to: DateTime, $role: UserRole) { + users(input: { from: $from, to: $to, role: $role }) { + ... on UsersResponseOk { + result { + __typename + } + } + ... on ResponseError { + __typename + } + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + importSchemaTypesFrom: './path-to-other-file', + namespacedImportName: 'TypeImport', + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "import type * as TypeImport from './graphql-code-generator'; + + type Exact = { [K in keyof T]: T[K] }; + export type UserQueryVariables = Exact<{ + id: string; + }>; + + + export type UserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, name: string, role: TypeImport.UserRole, createdAt: any } | null }; + + export type UsersQueryVariables = Exact<{ + input: TypeImport.UsersInput; + }>; + + + export type UsersQuery = { __typename?: 'Query', users: + | { __typename?: 'UsersResponseOk', result: Array<{ __typename?: 'User', id: string }> } + | { __typename?: 'ResponseError', error: TypeImport.ResponseErrorType } + }; + + export type UsersWithScalarInputQueryVariables = Exact<{ + from: any; + to?: any | null; + role?: TypeImport.UserRole | null; + }>; + + + export type UsersWithScalarInputQuery = { __typename?: 'Query', users: + | { __typename?: 'UsersResponseOk', result: Array<{ __typename: 'User' }> } + | { __typename: 'ResponseError' } + }; + " + `); + + // FIXME: enable this to ensure type correctness + // validateTs(content, undefined, undefined, undefined, undefined, true); + }); + + it('does not import external types if native GraphQL types are used in Variables and Result', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + user(id: ID!): User + users(input: UsersInput!): UsersResponse! + } + + type ResponseError { + error: ResponseErrorType! + } + + enum ResponseErrorType { + NOT_FOUND + INPUT_VALIDATION_ERROR + FORBIDDEN_ERROR + UNEXPECTED_ERROR + } + + type User { + # Native GraphQL types + id: ID! + name: String! + isOld: Boolean! + ageInt: Int! + ageFloat: Float! + + # User-defined types + role: UserRole! + createdAt: DateTime! + } + + "UserRole Description" + enum UserRole { + "UserRole ADMIN" + ADMIN + "UserRole CUSTOMER" + CUSTOMER + } + + "UsersInput Description" + input UsersInput { + "UsersInput from" + from: DateTime + "UsersInput to" + to: DateTime + role: UserRole + } + + type UsersResponseOk { + result: [User!]! + } + union UsersResponse = UsersResponseOk | ResponseError + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query User($id: ID, $name: String, $bool: Boolean, $int: Int, $float: Float) { + user(id: $id) { + id + name + isOld + ageInt + ageFloat + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { + importSchemaTypesFrom: './path-to-other-file', + namespacedImportName: 'TypeImport', + }), + ]); + + expect(result).toMatchInlineSnapshot(` + "type Exact = { [K in keyof T]: T[K] }; + export type UserQueryVariables = Exact<{ + id?: string | null; + name?: string | null; + bool?: boolean | null; + int?: number | null; + float?: number | null; + }>; + + + export type UserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, name: string, isOld: boolean, ageInt: number, ageFloat: number } | null }; + " + `); + + // FIXME: enable this to ensure type correctness + // validateTs(content, undefined, undefined, undefined, undefined, true); + }); +}); From 557d0b0aa97d6622d4073e54c47cf6a2c3fa205d Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 00:53:47 +1100 Subject: [PATCH 02/13] Minor test fixes --- .../operations/tests/__snapshots__/ts-documents.spec.ts.snap | 2 +- .../tests/ts-documents.standalone.import-types.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/plugins/typescript/operations/tests/__snapshots__/ts-documents.spec.ts.snap b/packages/plugins/typescript/operations/tests/__snapshots__/ts-documents.spec.ts.snap index f364438cc65..b9d144384f1 100644 --- a/packages/plugins/typescript/operations/tests/__snapshots__/ts-documents.spec.ts.snap +++ b/packages/plugins/typescript/operations/tests/__snapshots__/ts-documents.spec.ts.snap @@ -57,7 +57,7 @@ export type ElementMetadataFragment = `; exports[`TypeScript Operations Plugin > Issues > #2916 - Missing import prefix with preResolveTypes: true and near-operation-file preset 1`] = ` -"export type UserQueryVariables = Types.Exact<{ [key: string]: never; }>; +"export type UserQueryVariables = Exact<{ [key: string]: never; }>; export type UserQuery = { user: { id: string, username: string, email: string, dep: Types.Department } }; diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts index a0794a8f1ff..ee08834ab4f 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts @@ -1,4 +1,5 @@ import { buildSchema, parse } from 'graphql'; +import { validateTs } from '@graphql-codegen/testing'; import { mergeOutputs } from '@graphql-codegen/plugin-helpers'; import { plugin } from '../src/index.js'; @@ -224,7 +225,6 @@ describe('TypeScript Operations Plugin - Import Types', () => { " `); - // FIXME: enable this to ensure type correctness - // validateTs(content, undefined, undefined, undefined, undefined, true); + validateTs(result, undefined, undefined, undefined, undefined, true); }); }); From df7bcae3c176f22ef677d370398f037df44123c2 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 22:09:43 +1100 Subject: [PATCH 03/13] Fix test name --- .../tests/ts-documents.standalone.import-types.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts index ee08834ab4f..380c59e6d89 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts @@ -136,7 +136,7 @@ describe('TypeScript Operations Plugin - Import Types', () => { // validateTs(content, undefined, undefined, undefined, undefined, true); }); - it('does not import external types if native GraphQL types are used in Variables and Result', async () => { + it('does not import external types if only native GraphQL types are used in Variables and Result', async () => { const schema = buildSchema(/* GraphQL */ ` type Query { user(id: ID!): User From 1cf5cc9a7fa3772af7f9c1621baf0a635901af22 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 22:34:43 +1100 Subject: [PATCH 04/13] Update dev-tests --- dev-test/githunt/typed-document-nodes.ts | 12 ------------ dev-test/githunt/types.avoidOptionals.ts | 12 ------------ dev-test/githunt/types.d.ts | 12 ------------ dev-test/githunt/types.enumsAsTypes.ts | 12 ------------ dev-test/githunt/types.flatten.preResolveTypes.ts | 12 ------------ dev-test/githunt/types.immutableTypes.ts | 12 ------------ .../types.preResolveTypes.onlyOperationTypes.ts | 12 ------------ dev-test/githunt/types.preResolveTypes.ts | 12 ------------ dev-test/githunt/types.ts | 12 ------------ dev-test/star-wars/types.avoidOptionals.ts | 9 --------- dev-test/star-wars/types.excludeQueryAlpha.ts | 9 --------- dev-test/star-wars/types.excludeQueryBeta.ts | 9 --------- dev-test/star-wars/types.globallyAvailable.d.ts | 9 --------- dev-test/star-wars/types.immutableTypes.ts | 9 --------- .../types.preResolveTypes.onlyOperationTypes.ts | 9 --------- dev-test/star-wars/types.preResolveTypes.ts | 9 --------- dev-test/star-wars/types.skipSchema.ts | 9 --------- dev-test/star-wars/types.ts | 9 --------- 18 files changed, 189 deletions(-) diff --git a/dev-test/githunt/typed-document-nodes.ts b/dev-test/githunt/typed-document-nodes.ts index 1de574e175e..4eed5751e4f 100644 --- a/dev-test/githunt/typed-document-nodes.ts +++ b/dev-test/githunt/typed-document-nodes.ts @@ -169,18 +169,6 @@ export enum VoteType { Up = 'UP', } -/** A list of options for the sort order of the feed */ -export type FeedType = - /** Sort by a combination of freshness and score, using Reddit's algorithm */ - | 'HOT' - /** Newest entries first */ - | 'NEW' - /** Highest score entries first */ - | 'TOP'; - -/** The type of vote to record, when submitting a vote */ -export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; - export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.avoidOptionals.ts b/dev-test/githunt/types.avoidOptionals.ts index 0d19fae050d..66c403d7758 100644 --- a/dev-test/githunt/types.avoidOptionals.ts +++ b/dev-test/githunt/types.avoidOptionals.ts @@ -168,18 +168,6 @@ export enum VoteType { Up = 'UP', } -/** A list of options for the sort order of the feed */ -export type FeedType = - /** Sort by a combination of freshness and score, using Reddit's algorithm */ - | 'HOT' - /** Newest entries first */ - | 'NEW' - /** Highest score entries first */ - | 'TOP'; - -/** The type of vote to record, when submitting a vote */ -export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; - export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.d.ts b/dev-test/githunt/types.d.ts index cf2dc2f2014..b84825f3808 100644 --- a/dev-test/githunt/types.d.ts +++ b/dev-test/githunt/types.d.ts @@ -163,18 +163,6 @@ export type Vote = { /** The type of vote to record, when submitting a vote */ export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; -/** A list of options for the sort order of the feed */ -export type FeedType = - /** Sort by a combination of freshness and score, using Reddit's algorithm */ - | 'HOT' - /** Newest entries first */ - | 'NEW' - /** Highest score entries first */ - | 'TOP'; - -/** The type of vote to record, when submitting a vote */ -export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; - export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.enumsAsTypes.ts b/dev-test/githunt/types.enumsAsTypes.ts index cf2dc2f2014..b84825f3808 100644 --- a/dev-test/githunt/types.enumsAsTypes.ts +++ b/dev-test/githunt/types.enumsAsTypes.ts @@ -163,18 +163,6 @@ export type Vote = { /** The type of vote to record, when submitting a vote */ export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; -/** A list of options for the sort order of the feed */ -export type FeedType = - /** Sort by a combination of freshness and score, using Reddit's algorithm */ - | 'HOT' - /** Newest entries first */ - | 'NEW' - /** Highest score entries first */ - | 'TOP'; - -/** The type of vote to record, when submitting a vote */ -export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; - export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.flatten.preResolveTypes.ts b/dev-test/githunt/types.flatten.preResolveTypes.ts index d41d3fc8780..90348f33215 100644 --- a/dev-test/githunt/types.flatten.preResolveTypes.ts +++ b/dev-test/githunt/types.flatten.preResolveTypes.ts @@ -168,18 +168,6 @@ export enum VoteType { Up = 'UP', } -/** A list of options for the sort order of the feed */ -export type FeedType = - /** Sort by a combination of freshness and score, using Reddit's algorithm */ - | 'HOT' - /** Newest entries first */ - | 'NEW' - /** Highest score entries first */ - | 'TOP'; - -/** The type of vote to record, when submitting a vote */ -export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; - export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.immutableTypes.ts b/dev-test/githunt/types.immutableTypes.ts index 42cf26e9d56..f708b3c3dce 100644 --- a/dev-test/githunt/types.immutableTypes.ts +++ b/dev-test/githunt/types.immutableTypes.ts @@ -168,18 +168,6 @@ export enum VoteType { Up = 'UP', } -/** A list of options for the sort order of the feed */ -export type FeedType = - /** Sort by a combination of freshness and score, using Reddit's algorithm */ - | 'HOT' - /** Newest entries first */ - | 'NEW' - /** Highest score entries first */ - | 'TOP'; - -/** The type of vote to record, when submitting a vote */ -export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; - export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts b/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts index e54e8ab576f..29bff30b9dd 100644 --- a/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts +++ b/dev-test/githunt/types.preResolveTypes.onlyOperationTypes.ts @@ -31,18 +31,6 @@ export enum VoteType { Up = 'UP', } -/** A list of options for the sort order of the feed */ -export type FeedType = - /** Sort by a combination of freshness and score, using Reddit's algorithm */ - | 'HOT' - /** Newest entries first */ - | 'NEW' - /** Highest score entries first */ - | 'TOP'; - -/** The type of vote to record, when submitting a vote */ -export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; - export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.preResolveTypes.ts b/dev-test/githunt/types.preResolveTypes.ts index a6114cf2411..c63166be345 100644 --- a/dev-test/githunt/types.preResolveTypes.ts +++ b/dev-test/githunt/types.preResolveTypes.ts @@ -168,18 +168,6 @@ export enum VoteType { Up = 'UP', } -/** A list of options for the sort order of the feed */ -export type FeedType = - /** Sort by a combination of freshness and score, using Reddit's algorithm */ - | 'HOT' - /** Newest entries first */ - | 'NEW' - /** Highest score entries first */ - | 'TOP'; - -/** The type of vote to record, when submitting a vote */ -export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; - export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/githunt/types.ts b/dev-test/githunt/types.ts index a6114cf2411..c63166be345 100644 --- a/dev-test/githunt/types.ts +++ b/dev-test/githunt/types.ts @@ -168,18 +168,6 @@ export enum VoteType { Up = 'UP', } -/** A list of options for the sort order of the feed */ -export type FeedType = - /** Sort by a combination of freshness and score, using Reddit's algorithm */ - | 'HOT' - /** Newest entries first */ - | 'NEW' - /** Highest score entries first */ - | 'TOP'; - -/** The type of vote to record, when submitting a vote */ -export type VoteType = 'CANCEL' | 'DOWN' | 'UP'; - export type OnCommentAddedSubscriptionVariables = Exact<{ repoFullName: string; }>; diff --git a/dev-test/star-wars/types.avoidOptionals.ts b/dev-test/star-wars/types.avoidOptionals.ts index 96a96ea0b4d..24b23e24f0c 100644 --- a/dev-test/star-wars/types.avoidOptionals.ts +++ b/dev-test/star-wars/types.avoidOptionals.ts @@ -240,15 +240,6 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; -/** The episodes in the Star Wars trilogy */ -export type Episode = - /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ - | 'EMPIRE' - /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ - | 'JEDI' - /** Star Wars Episode IV: A New Hope, released in 1977. */ - | 'NEWHOPE'; - export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.excludeQueryAlpha.ts b/dev-test/star-wars/types.excludeQueryAlpha.ts index 7ccbc0a787c..40703b13094 100644 --- a/dev-test/star-wars/types.excludeQueryAlpha.ts +++ b/dev-test/star-wars/types.excludeQueryAlpha.ts @@ -240,15 +240,6 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; -/** The episodes in the Star Wars trilogy */ -export type Episode = - /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ - | 'EMPIRE' - /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ - | 'JEDI' - /** Star Wars Episode IV: A New Hope, released in 1977. */ - | 'NEWHOPE'; - export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.excludeQueryBeta.ts b/dev-test/star-wars/types.excludeQueryBeta.ts index 9f20261977e..5b38889e9ac 100644 --- a/dev-test/star-wars/types.excludeQueryBeta.ts +++ b/dev-test/star-wars/types.excludeQueryBeta.ts @@ -240,15 +240,6 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; -/** The episodes in the Star Wars trilogy */ -export type Episode = - /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ - | 'EMPIRE' - /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ - | 'JEDI' - /** Star Wars Episode IV: A New Hope, released in 1977. */ - | 'NEWHOPE'; - export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.globallyAvailable.d.ts b/dev-test/star-wars/types.globallyAvailable.d.ts index 0e376af86bc..2efbb00482e 100644 --- a/dev-test/star-wars/types.globallyAvailable.d.ts +++ b/dev-test/star-wars/types.globallyAvailable.d.ts @@ -238,15 +238,6 @@ type StarshipLengthArgs = { unit?: InputMaybe; }; -/** The episodes in the Star Wars trilogy */ -type Episode = - /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ - | 'EMPIRE' - /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ - | 'JEDI' - /** Star Wars Episode IV: A New Hope, released in 1977. */ - | 'NEWHOPE'; - type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.immutableTypes.ts b/dev-test/star-wars/types.immutableTypes.ts index bb485fff875..5849e8f6d04 100644 --- a/dev-test/star-wars/types.immutableTypes.ts +++ b/dev-test/star-wars/types.immutableTypes.ts @@ -240,15 +240,6 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; -/** The episodes in the Star Wars trilogy */ -export type Episode = - /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ - | 'EMPIRE' - /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ - | 'JEDI' - /** Star Wars Episode IV: A New Hope, released in 1977. */ - | 'NEWHOPE'; - export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts b/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts index 9597c939d8f..d5430785c67 100644 --- a/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts +++ b/dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts @@ -49,15 +49,6 @@ export type ReviewInput = { stars: Scalars['Int']['input']; }; -/** The episodes in the Star Wars trilogy */ -export type Episode = - /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ - | 'EMPIRE' - /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ - | 'JEDI' - /** Star Wars Episode IV: A New Hope, released in 1977. */ - | 'NEWHOPE'; - export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.preResolveTypes.ts b/dev-test/star-wars/types.preResolveTypes.ts index 99c11f7e757..5e82a72b7e0 100644 --- a/dev-test/star-wars/types.preResolveTypes.ts +++ b/dev-test/star-wars/types.preResolveTypes.ts @@ -240,15 +240,6 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; -/** The episodes in the Star Wars trilogy */ -export type Episode = - /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ - | 'EMPIRE' - /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ - | 'JEDI' - /** Star Wars Episode IV: A New Hope, released in 1977. */ - | 'NEWHOPE'; - export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.skipSchema.ts b/dev-test/star-wars/types.skipSchema.ts index 99c11f7e757..5e82a72b7e0 100644 --- a/dev-test/star-wars/types.skipSchema.ts +++ b/dev-test/star-wars/types.skipSchema.ts @@ -240,15 +240,6 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; -/** The episodes in the Star Wars trilogy */ -export type Episode = - /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ - | 'EMPIRE' - /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ - | 'JEDI' - /** Star Wars Episode IV: A New Hope, released in 1977. */ - | 'NEWHOPE'; - export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; diff --git a/dev-test/star-wars/types.ts b/dev-test/star-wars/types.ts index 99c11f7e757..5e82a72b7e0 100644 --- a/dev-test/star-wars/types.ts +++ b/dev-test/star-wars/types.ts @@ -240,15 +240,6 @@ export type StarshipLengthArgs = { unit?: InputMaybe; }; -/** The episodes in the Star Wars trilogy */ -export type Episode = - /** Star Wars Episode V: The Empire Strikes Back, released in 1980. */ - | 'EMPIRE' - /** Star Wars Episode VI: Return of the Jedi, released in 1983. */ - | 'JEDI' - /** Star Wars Episode IV: A New Hope, released in 1977. */ - | 'NEWHOPE'; - export type CreateReviewForEpisodeMutationVariables = Exact<{ episode: Episode; review: ReviewInput; From 388a2e425221974925f80995998478f17270eff4 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 22:35:52 +1100 Subject: [PATCH 05/13] Baseline dev-test for importSchemaTypesFrom --- dev-test/codegen.ts | 19 +++++++++++++++++++ .../import-schema-types/_base.generated.ts | 6 ++++++ .../import-schema-types/_types.generated.ts | 11 +++++++++++ .../import-schema-types/base.generated.ts | 6 ++++++ .../import-schema-types/query.graphql | 6 ++++++ .../import-schema-types/types.generated.ts | 11 +++++++++++ dev-test/standalone-operations/schema.graphql | 17 +++++++++++++++++ 7 files changed, 76 insertions(+) create mode 100644 dev-test/standalone-operations/import-schema-types/_base.generated.ts create mode 100644 dev-test/standalone-operations/import-schema-types/_types.generated.ts create mode 100644 dev-test/standalone-operations/import-schema-types/base.generated.ts create mode 100644 dev-test/standalone-operations/import-schema-types/query.graphql create mode 100644 dev-test/standalone-operations/import-schema-types/types.generated.ts create mode 100644 dev-test/standalone-operations/schema.graphql diff --git a/dev-test/codegen.ts b/dev-test/codegen.ts index 04584a36395..5315ef67396 100644 --- a/dev-test/codegen.ts +++ b/dev-test/codegen.ts @@ -256,6 +256,25 @@ const config: CodegenConfig = { }, }, }, + + // standalone-operations + './dev-test/standalone-operations/import-schema-types/_base.generated.ts': { + schema: './dev-test/standalone-operations/schema.graphql', + documents: ['./dev-test/standalone-operations/import-schema-types/*.graphql'], + plugins: ['typescript-operations'], + config: { + generatesOperationTypes: false, + }, + }, + './dev-test/standalone-operations/import-schema-types/_types.generated.ts': { + schema: './dev-test/standalone-operations/schema.graphql', + documents: ['./dev-test/standalone-operations/import-schema-types/*.graphql'], + plugins: ['typescript-operations'], + config: { + importSchemaTypesFrom: './_base.generated', + namespacedImportName: 'Types', + }, + }, }, }; diff --git a/dev-test/standalone-operations/import-schema-types/_base.generated.ts b/dev-test/standalone-operations/import-schema-types/_base.generated.ts new file mode 100644 index 00000000000..8df53ac5f67 --- /dev/null +++ b/dev-test/standalone-operations/import-schema-types/_base.generated.ts @@ -0,0 +1,6 @@ +/** UserRole Description */ +export type UserRole = + /** UserRole ADMIN */ + | 'ADMIN' + /** UserRole CUSTOMER */ + | 'CUSTOMER'; diff --git a/dev-test/standalone-operations/import-schema-types/_types.generated.ts b/dev-test/standalone-operations/import-schema-types/_types.generated.ts new file mode 100644 index 00000000000..26fd8eb2e99 --- /dev/null +++ b/dev-test/standalone-operations/import-schema-types/_types.generated.ts @@ -0,0 +1,11 @@ +import type * as Types from './graphql-code-generator'; + +type Exact = { [K in keyof T]: T[K] }; +export type WithVariablesQueryVariables = Exact<{ + role?: Types.UserRole | null; +}>; + +export type WithVariablesQuery = { + __typename?: 'Query'; + user?: { __typename?: 'User'; id: string; name: string } | null; +}; diff --git a/dev-test/standalone-operations/import-schema-types/base.generated.ts b/dev-test/standalone-operations/import-schema-types/base.generated.ts new file mode 100644 index 00000000000..8df53ac5f67 --- /dev/null +++ b/dev-test/standalone-operations/import-schema-types/base.generated.ts @@ -0,0 +1,6 @@ +/** UserRole Description */ +export type UserRole = + /** UserRole ADMIN */ + | 'ADMIN' + /** UserRole CUSTOMER */ + | 'CUSTOMER'; diff --git a/dev-test/standalone-operations/import-schema-types/query.graphql b/dev-test/standalone-operations/import-schema-types/query.graphql new file mode 100644 index 00000000000..e8de0b4506d --- /dev/null +++ b/dev-test/standalone-operations/import-schema-types/query.graphql @@ -0,0 +1,6 @@ +query WithVariables($role: UserRole) { + user(id: "100") { + id + name + } +} diff --git a/dev-test/standalone-operations/import-schema-types/types.generated.ts b/dev-test/standalone-operations/import-schema-types/types.generated.ts new file mode 100644 index 00000000000..f51c326a287 --- /dev/null +++ b/dev-test/standalone-operations/import-schema-types/types.generated.ts @@ -0,0 +1,11 @@ +import type * as Types from './_base.generated'; + +type Exact = { [K in keyof T]: T[K] }; +export type WithVariablesQueryVariables = Exact<{ + role?: Types.UserRole | null; +}>; + +export type WithVariablesQuery = { + __typename?: 'Query'; + user?: { __typename?: 'User'; id: string; name: string } | null; +}; diff --git a/dev-test/standalone-operations/schema.graphql b/dev-test/standalone-operations/schema.graphql new file mode 100644 index 00000000000..2fd2cb04bc6 --- /dev/null +++ b/dev-test/standalone-operations/schema.graphql @@ -0,0 +1,17 @@ +type Query { + user(id: ID!, role: UserRole): User +} + +type User { + id: ID! + name: String! + role: UserRole! +} + +"UserRole Description" +enum UserRole { + "UserRole ADMIN" + ADMIN + "UserRole CUSTOMER" + CUSTOMER +} From 9601124355d024df8e1d8d38b55ed1b943f65cb1 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 23:17:23 +1100 Subject: [PATCH 06/13] Implement relative import paths correctly --- dev-test/codegen.ts | 2 +- .../import-schema-types/_types.generated.ts | 2 +- .../plugins/typescript/operations/src/index.ts | 9 +++++---- .../typescript/operations/src/visitor.ts | 17 +++++++++++++---- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/dev-test/codegen.ts b/dev-test/codegen.ts index 5315ef67396..a1126bf169e 100644 --- a/dev-test/codegen.ts +++ b/dev-test/codegen.ts @@ -271,7 +271,7 @@ const config: CodegenConfig = { documents: ['./dev-test/standalone-operations/import-schema-types/*.graphql'], plugins: ['typescript-operations'], config: { - importSchemaTypesFrom: './_base.generated', + importSchemaTypesFrom: './dev-test/standalone-operations/import-schema-types/_base.generated.ts', namespacedImportName: 'Types', }, }, diff --git a/dev-test/standalone-operations/import-schema-types/_types.generated.ts b/dev-test/standalone-operations/import-schema-types/_types.generated.ts index 26fd8eb2e99..f51c326a287 100644 --- a/dev-test/standalone-operations/import-schema-types/_types.generated.ts +++ b/dev-test/standalone-operations/import-schema-types/_types.generated.ts @@ -1,4 +1,4 @@ -import type * as Types from './graphql-code-generator'; +import type * as Types from './_base.generated'; type Exact = { [K in keyof T]: T[K] }; export type WithVariablesQueryVariables = Exact<{ diff --git a/packages/plugins/typescript/operations/src/index.ts b/packages/plugins/typescript/operations/src/index.ts index d0d6deb4ec8..b7823699af2 100644 --- a/packages/plugins/typescript/operations/src/index.ts +++ b/packages/plugins/typescript/operations/src/index.ts @@ -8,9 +8,10 @@ import { TypeScriptDocumentsVisitor } from './visitor.js'; export { TypeScriptDocumentsPluginConfig } from './config.js'; export const plugin: PluginFunction = async ( - inputSchema: GraphQLSchema, - rawDocuments: Types.DocumentFile[], - config: TypeScriptDocumentsPluginConfig + inputSchema, + rawDocuments, + config, + { outputFile } ) => { const schema = config.nullability?.errorHandlingClient ? await semanticToStrict(inputSchema) : inputSchema; @@ -21,7 +22,7 @@ export const plugin: PluginFunction v.document)); - const visitor = new TypeScriptDocumentsVisitor(schema, config, allAst); + const visitor = new TypeScriptDocumentsVisitor(schema, config, allAst, outputFile); const operationsResult = oldVisit(allAst, { leave: visitor }); diff --git a/packages/plugins/typescript/operations/src/visitor.ts b/packages/plugins/typescript/operations/src/visitor.ts index d0ceb95f5f1..700de6e045a 100644 --- a/packages/plugins/typescript/operations/src/visitor.ts +++ b/packages/plugins/typescript/operations/src/visitor.ts @@ -59,7 +59,15 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< TypeScriptDocumentsParsedConfig > { protected _usedNamedInputTypes: UsedNamedInputTypes = {}; - constructor(schema: GraphQLSchema, config: TypeScriptDocumentsPluginConfig, documentNode: DocumentNode) { + private _outputPath: string; + + // FIXME: make this object-style param + constructor( + schema: GraphQLSchema, + config: TypeScriptDocumentsPluginConfig, + documentNode: DocumentNode, + outputPath: string + ) { super( config, { @@ -82,6 +90,7 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< schema ); + this._outputPath = outputPath; autoBind(this); const preResolveTypes = getConfigValue(config.preResolveTypes, true); @@ -220,11 +229,11 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< return [ generateImportStatement({ - baseDir: '', + baseDir: process.cwd(), baseOutputDir: '', - outputPath: '', + outputPath: this._outputPath, importSource: { - path: '', + path: this.config.importSchemaTypesFrom, namespace: this.config.namespacedImportName, identifiers: [], }, From b762098ef9c431d0954d287de6150a94bc09232c Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 23:28:12 +1100 Subject: [PATCH 07/13] Update tests to handle outputDir correctly --- .../tests/ts-documents.nullability.spec.ts | 26 +- .../ts-documents.standalone.enum.spec.ts | 228 ++++++++++++------ ...-documents.standalone.import-types.spec.ts | 28 ++- .../tests/ts-documents.standalone.spec.ts | 8 +- 4 files changed, 192 insertions(+), 98 deletions(-) diff --git a/packages/plugins/typescript/operations/tests/ts-documents.nullability.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.nullability.spec.ts index 8f7e8a7c6ee..fb93cae9890 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.nullability.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.nullability.spec.ts @@ -62,11 +62,16 @@ const document = parse(/* GraphQL */ ` describe('TypeScript Operations Plugin - nullability', () => { it('converts semanticNonNull to nonNull when nullability.errorHandlingClient=true', async () => { - const result = await plugin(schema, [{ document }], { - nullability: { - errorHandlingClient: true, + const result = await plugin( + schema, + [{ document }], + { + nullability: { + errorHandlingClient: true, + }, }, - }); + { outputFile: '' } + ); const formattedContent = prettier.format(result.content, { parser: 'typescript' }); expect(formattedContent).toMatchInlineSnapshot(` @@ -103,11 +108,16 @@ describe('TypeScript Operations Plugin - nullability', () => { }); it('does not convert nullability to nonNull when nullability.errorHandlingClient=false', async () => { - const result = await plugin(schema, [{ document }], { - nullability: { - errorHandlingClient: false, + const result = await plugin( + schema, + [{ document }], + { + nullability: { + errorHandlingClient: false, + }, }, - }); + { outputFile: '' } + ); const formattedContent = prettier.format(result.content, { parser: 'typescript' }); expect(formattedContent).toMatchInlineSnapshot(` diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.enum.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.enum.spec.ts index b00857d7364..d058165b47e 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.standalone.enum.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.enum.spec.ts @@ -32,7 +32,7 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], {})]); + const result = mergeOutputs([await plugin(schema, [{ document }], {}, { outputFile: '' })]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -74,7 +74,9 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native-numeric' })]); + const result = mergeOutputs([ + await plugin(schema, [{ document }], { enumType: 'native-numeric' }, { outputFile: '' }), + ]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -126,7 +128,7 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'const' })]); + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'const' }, { outputFile: '' })]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -182,7 +184,9 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native-const' })]); + const result = mergeOutputs([ + await plugin(schema, [{ document }], { enumType: 'native-const' }, { outputFile: '' }), + ]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -233,7 +237,7 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' })]); + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' }, { outputFile: '' })]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -285,17 +289,22 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - enumType: 'string-literal', - enumValues: { - UserRole: { - A_B_C: 0, - X_Y_Z: 'Foo', - _TEST: 'Bar', - My_Value: 1, + await plugin( + schema, + [{ document }], + { + enumType: 'string-literal', + enumValues: { + UserRole: { + A_B_C: 0, + X_Y_Z: 'Foo', + _TEST: 'Bar', + My_Value: 1, + }, }, }, - }), + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` @@ -349,17 +358,22 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - enumType: 'const', - enumValues: { - UserRole: { - A_B_C: 0, - X_Y_Z: 'Foo', - _TEST: 'Bar', - My_Value: 1, + await plugin( + schema, + [{ document }], + { + enumType: 'const', + enumValues: { + UserRole: { + A_B_C: 0, + X_Y_Z: 'Foo', + _TEST: 'Bar', + My_Value: 1, + }, }, }, - }), + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` @@ -413,15 +427,20 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - enumType: 'native', - enumValues: { - UserRole: { - ADMIN: 0, - CUSTOMER: 'test', + await plugin( + schema, + [{ document }], + { + enumType: 'native', + enumValues: { + UserRole: { + ADMIN: 0, + CUSTOMER: 'test', + }, }, }, - }), + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` @@ -473,11 +492,16 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - enumValues: { - UserRole: './my-file#MyEnum', + await plugin( + schema, + [{ document }], + { + enumValues: { + UserRole: './my-file#MyEnum', + }, }, - }), + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` @@ -527,11 +551,16 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - enumValues: { - UserRole: './my-file#NS.ETest', + await plugin( + schema, + [{ document }], + { + enumValues: { + UserRole: './my-file#NS.ETest', + }, }, - }), + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` @@ -582,11 +611,16 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - enumValues: { - UserRole: './my-file#NS.UserRole', + await plugin( + schema, + [{ document }], + { + enumValues: { + UserRole: './my-file#NS.UserRole', + }, }, - }), + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` @@ -642,10 +676,15 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - enumType: 'native', - enumValues: './my-file', - }), + await plugin( + schema, + [{ document }], + { + enumType: 'native', + enumValues: './my-file', + }, + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` @@ -704,10 +743,15 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - enumType: 'native', - enumValues: { UserRole: './my-file#UserRole', UserStatus: './my-file#UserStatus2X' }, - }), + await plugin( + schema, + [{ document }], + { + enumType: 'native', + enumValues: { UserRole: './my-file#UserRole', UserStatus: './my-file#UserStatus2X' }, + }, + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` @@ -762,7 +806,7 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' })]); + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' }, { outputFile: '' })]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -815,7 +859,7 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' })]); + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' }, { outputFile: '' })]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -866,7 +910,9 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { typesPrefix: 'I', enumPrefix: true })]); + const result = mergeOutputs([ + await plugin(schema, [{ document }], { typesPrefix: 'I', enumPrefix: true }, { outputFile: '' }), + ]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; export type IUserRole = @@ -914,7 +960,9 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { typesPrefix: 'I', enumPrefix: false })]); + const result = mergeOutputs([ + await plugin(schema, [{ document }], { typesPrefix: 'I', enumPrefix: false }, { outputFile: '' }), + ]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; export type UserRole = @@ -962,7 +1010,9 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { typesSuffix: 'Z', enumSuffix: true })]); + const result = mergeOutputs([ + await plugin(schema, [{ document }], { typesSuffix: 'Z', enumSuffix: true }, { outputFile: '' }), + ]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; export type UserRoleZ = @@ -1010,7 +1060,9 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { typesSuffix: 'Z', enumSuffix: false })]); + const result = mergeOutputs([ + await plugin(schema, [{ document }], { typesSuffix: 'Z', enumSuffix: false }, { outputFile: '' }), + ]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; export type UserRole = @@ -1059,12 +1111,17 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - namingConvention: { - typeNames: 'change-case-all#lowerCase', - enumValues: 'keep', + await plugin( + schema, + [{ document }], + { + namingConvention: { + typeNames: 'change-case-all#lowerCase', + enumValues: 'keep', + }, }, - }), + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -1114,13 +1171,18 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - enumType: 'native', - namingConvention: { - typeNames: 'keep', - enumValues: 'change-case-all#lowerCase', + await plugin( + schema, + [{ document }], + { + enumType: 'native', + namingConvention: { + typeNames: 'keep', + enumValues: 'change-case-all#lowerCase', + }, }, - }), + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -1171,9 +1233,14 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - noExport: true, - }), + await plugin( + schema, + [{ document }], + { + noExport: true, + }, + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -1223,13 +1290,18 @@ describe('TypeScript Operations Plugin - Enum', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - typesPrefix: 'I', - namingConvention: { enumValues: 'change-case-all#constantCase' }, - enumValues: { - UserRole: './files#default as UserRole', + await plugin( + schema, + [{ document }], + { + typesPrefix: 'I', + namingConvention: { enumValues: 'change-case-all#constantCase' }, + enumValues: { + UserRole: './files#default as UserRole', + }, }, - }), + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` @@ -1277,7 +1349,7 @@ describe('TypeScript Operations Plugin - Enum', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' })]); + const result = mergeOutputs([await plugin(schema, [{ document }], { enumType: 'native' }, { outputFile: '' })]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -1327,7 +1399,7 @@ describe('TypeScript Operations Plugin - Enum `%future added value`', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { futureProofEnums: true })]); + const result = mergeOutputs([await plugin(schema, [{ document }], { futureProofEnums: true }, { outputFile: '' })]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts index 380c59e6d89..3590267ea75 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts @@ -91,14 +91,19 @@ describe('TypeScript Operations Plugin - Import Types', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - importSchemaTypesFrom: './path-to-other-file', - namespacedImportName: 'TypeImport', - }), + await plugin( + schema, + [{ document }], + { + importSchemaTypesFrom: './base-dir/path-to-other-file.generated.ts', + namespacedImportName: 'TypeImport', + }, + { outputFile: './base-dir/this-file.ts' } + ), ]); expect(result).toMatchInlineSnapshot(` - "import type * as TypeImport from './graphql-code-generator'; + "import type * as TypeImport from './path-to-other-file.generated'; type Exact = { [K in keyof T]: T[K] }; export type UserQueryVariables = Exact<{ @@ -204,10 +209,15 @@ describe('TypeScript Operations Plugin - Import Types', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { - importSchemaTypesFrom: './path-to-other-file', - namespacedImportName: 'TypeImport', - }), + await plugin( + schema, + [{ document }], + { + importSchemaTypesFrom: './path-to-other-file', + namespacedImportName: 'TypeImport', + }, + { outputFile: '' } + ), ]); expect(result).toMatchInlineSnapshot(` diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts index 2f1cb9a5485..13cecafd40e 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts @@ -90,7 +90,7 @@ describe('TypeScript Operations Plugin - Standalone', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], {})]); + const result = mergeOutputs([await plugin(schema, [{ document }], {}, { outputFile: '' })]); expect(result).toMatchInlineSnapshot(` "type Exact = { [K in keyof T]: T[K] }; @@ -157,7 +157,7 @@ describe('TypeScript Operations Plugin - Standalone', () => { `); const result = mergeOutputs([ - await plugin(schema, [{ document }], { scalars: { ID: 'string | number | boolean' } }), + await plugin(schema, [{ document }], { scalars: { ID: 'string | number | boolean' } }, { outputFile: '' }), ]); expect(result).toMatchInlineSnapshot(` @@ -283,7 +283,9 @@ describe('TypeScript Operations Plugin - Standalone', () => { } `); - const result = mergeOutputs([await plugin(schema, [{ document }], { generatesOperationTypes: false })]); + const result = mergeOutputs([ + await plugin(schema, [{ document }], { generatesOperationTypes: false }, { outputFile: '' }), + ]); expect(result).toMatchInlineSnapshot(` " From d8465a990ab172ba88e6fd34cd7fb65367defd7a Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 23:30:51 +1100 Subject: [PATCH 08/13] Add test for absolute importSchemaTypesFrom --- ...-documents.standalone.import-types.spec.ts | 137 +++++++++++++++++- 1 file changed, 134 insertions(+), 3 deletions(-) diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts index 3590267ea75..af820bf713a 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.import-types.spec.ts @@ -4,7 +4,7 @@ import { mergeOutputs } from '@graphql-codegen/plugin-helpers'; import { plugin } from '../src/index.js'; describe('TypeScript Operations Plugin - Import Types', () => { - it('imports user-defined types externally with importSchemaTypesFrom correctly', async () => { + it('imports user-defined types externally with relative importSchemaTypesFrom correctly', async () => { const schema = buildSchema(/* GraphQL */ ` type Query { user(id: ID!): User @@ -136,9 +136,140 @@ describe('TypeScript Operations Plugin - Import Types', () => { }; " `); + }); + + it('imports user-defined types externally with absolute importSchemaTypesFrom correctly', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + user(id: ID!): User + users(input: UsersInput!): UsersResponse! + } + + type ResponseError { + error: ResponseErrorType! + } + + enum ResponseErrorType { + NOT_FOUND + INPUT_VALIDATION_ERROR + FORBIDDEN_ERROR + UNEXPECTED_ERROR + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + "UserRole Description" + enum UserRole { + "UserRole ADMIN" + ADMIN + "UserRole CUSTOMER" + CUSTOMER + } + + "UsersInput Description" + input UsersInput { + "UsersInput from" + from: DateTime + "UsersInput to" + to: DateTime + role: UserRole + } + + type UsersResponseOk { + result: [User!]! + } + union UsersResponse = UsersResponseOk | ResponseError + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query User($id: ID!) { + user(id: $id) { + id + name + role + createdAt + } + } + + query Users($input: UsersInput!) { + users(input: $input) { + ... on UsersResponseOk { + result { + id + } + } + ... on ResponseError { + error + } + } + } - // FIXME: enable this to ensure type correctness - // validateTs(content, undefined, undefined, undefined, undefined, true); + query UsersWithScalarInput($from: DateTime!, $to: DateTime, $role: UserRole) { + users(input: { from: $from, to: $to, role: $role }) { + ... on UsersResponseOk { + result { + __typename + } + } + ... on ResponseError { + __typename + } + } + } + `); + + const result = mergeOutputs([ + await plugin( + schema, + [{ document }], + { + importSchemaTypesFrom: '~@my-company/package/types', + namespacedImportName: 'TypeImport', + }, + { outputFile: './base-dir/this-file.ts' } + ), + ]); + + expect(result).toMatchInlineSnapshot(` + "import type * as TypeImport from '@my-company/package/types'; + + type Exact = { [K in keyof T]: T[K] }; + export type UserQueryVariables = Exact<{ + id: string; + }>; + + + export type UserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, name: string, role: TypeImport.UserRole, createdAt: any } | null }; + + export type UsersQueryVariables = Exact<{ + input: TypeImport.UsersInput; + }>; + + + export type UsersQuery = { __typename?: 'Query', users: + | { __typename?: 'UsersResponseOk', result: Array<{ __typename?: 'User', id: string }> } + | { __typename?: 'ResponseError', error: TypeImport.ResponseErrorType } + }; + + export type UsersWithScalarInputQueryVariables = Exact<{ + from: any; + to?: any | null; + role?: TypeImport.UserRole | null; + }>; + + + export type UsersWithScalarInputQuery = { __typename?: 'Query', users: + | { __typename?: 'UsersResponseOk', result: Array<{ __typename: 'User' }> } + | { __typename: 'ResponseError' } + }; + " + `); }); it('does not import external types if only native GraphQL types are used in Variables and Result', async () => { From 31ed37a63dfa95fc8166a2250311f7bf9bafdda6 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 23:34:17 +1100 Subject: [PATCH 09/13] Remove unncessary files --- .../import-schema-types/base.generated.ts | 6 ------ .../import-schema-types/types.generated.ts | 11 ----------- 2 files changed, 17 deletions(-) delete mode 100644 dev-test/standalone-operations/import-schema-types/base.generated.ts delete mode 100644 dev-test/standalone-operations/import-schema-types/types.generated.ts diff --git a/dev-test/standalone-operations/import-schema-types/base.generated.ts b/dev-test/standalone-operations/import-schema-types/base.generated.ts deleted file mode 100644 index 8df53ac5f67..00000000000 --- a/dev-test/standalone-operations/import-schema-types/base.generated.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** UserRole Description */ -export type UserRole = - /** UserRole ADMIN */ - | 'ADMIN' - /** UserRole CUSTOMER */ - | 'CUSTOMER'; diff --git a/dev-test/standalone-operations/import-schema-types/types.generated.ts b/dev-test/standalone-operations/import-schema-types/types.generated.ts deleted file mode 100644 index f51c326a287..00000000000 --- a/dev-test/standalone-operations/import-schema-types/types.generated.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type * as Types from './_base.generated'; - -type Exact = { [K in keyof T]: T[K] }; -export type WithVariablesQueryVariables = Exact<{ - role?: Types.UserRole | null; -}>; - -export type WithVariablesQuery = { - __typename?: 'Query'; - user?: { __typename?: 'User'; id: string; name: string } | null; -}; From dabc2d6bd8e5d572edabe33d57fc1e643e4f4add Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 23:38:39 +1100 Subject: [PATCH 10/13] Add test about unused things --- .../tests/ts-documents.standalone.spec.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts index 13cecafd40e..00ea9a5ca42 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts @@ -301,4 +301,81 @@ describe('TypeScript Operations Plugin - Standalone', () => { validateTs(result, undefined, undefined, undefined, undefined, true); }); + + it('does not generate unused schema enum and input types', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + user(id: ID!): User + users(input: UsersInput!): UsersResponse! + } + + type Mutation { + makeUserAdmin(id: ID!): User! + } + + type Subscription { + userChanges(id: ID!): User! + } + + type ResponseError { + error: ResponseErrorType! + } + + enum ResponseErrorType { + NOT_FOUND + INPUT_VALIDATION_ERROR + FORBIDDEN_ERROR + UNEXPECTED_ERROR + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + } + + "UserRole Description" + enum UserRole { + "UserRole ADMIN" + ADMIN + "UserRole CUSTOMER" + CUSTOMER + } + + "UsersInput Description" + input UsersInput { + "UsersInput from" + from: DateTime + "UsersInput to" + to: DateTime + role: UserRole + } + + type UsersResponseOk { + result: [User!]! + } + union UsersResponse = UsersResponseOk | ResponseError + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + query User { + user(id: "100") { + id + } + } + `); + + const result = mergeOutputs([ + await plugin(schema, [{ document }], { generatesOperationTypes: false }, { outputFile: '' }), + ]); + + expect(result).toMatchInlineSnapshot(` + " + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); }); From 6f233c61a60ccb0e7488dff210337fb587af0a8e Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Dec 2025 23:41:21 +1100 Subject: [PATCH 11/13] Remove comment --- packages/plugins/typescript/operations/src/visitor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugins/typescript/operations/src/visitor.ts b/packages/plugins/typescript/operations/src/visitor.ts index 700de6e045a..24784286521 100644 --- a/packages/plugins/typescript/operations/src/visitor.ts +++ b/packages/plugins/typescript/operations/src/visitor.ts @@ -61,7 +61,6 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< protected _usedNamedInputTypes: UsedNamedInputTypes = {}; private _outputPath: string; - // FIXME: make this object-style param constructor( schema: GraphQLSchema, config: TypeScriptDocumentsPluginConfig, From 4b9251ddb987a6f89f2a7d9adddde6cfca47fb37 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 9 Dec 2025 00:44:50 +1100 Subject: [PATCH 12/13] Add comments --- .../src/base-documents-visitor.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts index 18b6e477071..88f1cb6bf82 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-documents-visitor.ts @@ -180,6 +180,7 @@ export interface RawDocumentsConfig extends RawTypesConfig { /** * @description Whether to generate operation types such as Variables, Query/Mutation/Subscription selection set, and Fragment types + * This can be used with `importSchemaTypesFrom` to generate shared used Enums and Input. * @default true * @exampleMarkdown * ```ts filename="codegen.ts" @@ -202,7 +203,26 @@ export interface RawDocumentsConfig extends RawTypesConfig { generatesOperationTypes?: boolean; /** - * TODO + * @description The absolute (prefixed with `~`) or relative path from `cwd` to the shared used Enums and Input (See `generatesOperationTypes`). + * @default true + * @exampleMarkdown + * ```ts filename="codegen.ts" + * import type { CodegenConfig } from '@graphql-codegen/cli'; + * + * const config: CodegenConfig = { + * // ... + * generates: { + * 'path/to/file.ts': { + * plugins: ['typescript-operations'], + * config: { + * importSchemaTypesFrom: './path/to/shared-types.ts', // relative + * importSchemaTypesFrom: '~@my-org/package' // absolute + * }, + * }, + * }, + * }; + * export default config; + * ``` */ importSchemaTypesFrom?: string; } From 55bd13e4e3d30a1a10c2537b4781edea29289b1c Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 9 Dec 2025 00:50:09 +1100 Subject: [PATCH 13/13] Add changeset --- .changeset/clever-loops-crash.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/clever-loops-crash.md diff --git a/.changeset/clever-loops-crash.md b/.changeset/clever-loops-crash.md new file mode 100644 index 00000000000..c28b7d69057 --- /dev/null +++ b/.changeset/clever-loops-crash.md @@ -0,0 +1,6 @@ +--- +'@graphql-codegen/visitor-plugin-common': minor +'@graphql-codegen/typescript-operations': minor +--- + +Add importSchemaTypesFrom support