From 02c7b558bdfcbcf2bc8e17ddcc5881d4377bb7d2 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 12 Mar 2026 00:33:04 +1100 Subject: [PATCH 01/13] Drive-by fix doc --- packages/presets/near-operation-file/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/presets/near-operation-file/src/index.ts b/packages/presets/near-operation-file/src/index.ts index ba826666b1..ca7865d049 100644 --- a/packages/presets/near-operation-file/src/index.ts +++ b/packages/presets/near-operation-file/src/index.ts @@ -177,7 +177,7 @@ export type NearOperationFileConfig = { folder?: string; /** * @description Optional, override the name of the import namespace used to import from the `baseTypesPath` file. - * @default Types + * @default Types (if `baseTypesPath` is set) * * @exampleMarkdown * ```ts filename="codegen.ts" {11} From 777014dec3e06976595c06db6007458883e1d742 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 12 Mar 2026 00:33:18 +1100 Subject: [PATCH 02/13] Wire in filePerOperation --- .../presets/near-operation-file/src/index.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/presets/near-operation-file/src/index.ts b/packages/presets/near-operation-file/src/index.ts index ca7865d049..d421724a6c 100644 --- a/packages/presets/near-operation-file/src/index.ts +++ b/packages/presets/near-operation-file/src/index.ts @@ -200,6 +200,30 @@ export type NearOperationFileConfig = { * ``` */ importTypesNamespace?: string; + + /** + * @description Optional, generates a file per operation + * @default false + * + * @exampleMarkdown + * ```ts filename="codegen.ts" {11} + * import type { CodegenConfig } from '@graphql-codegen/cli'; + * const config: CodegenConfig = { + * // ... + * generates: { + * 'path/to/file.ts': { + * preset: 'near-operation-file', + * presetConfig: { + * filePerOperation: true + * }, + * plugins: ['typescript-operations'], + * }, + * }, + * }; + * export default config; + * ``` + */ + filePerOperation?: boolean; }; export type FragmentNameToFile = { @@ -226,6 +250,7 @@ export const preset: Types.OutputPreset = { (options.presetConfig.baseTypesPath ? 'Types' : undefined); // When there is `baseTypesPath`, we assume there'd be a type import, so we default `importTypesNamespace` value to `Types` for convenience. const importAllFragmentsFrom: FragmentImportFromFn | string | null = options.presetConfig.importAllFragmentsFrom || null; + const filePerOperation = options.presetConfig.filePerOperation || false; const shouldAbsolute = !baseTypesPath.startsWith('~'); From e4165263141ead1bdb2a9c52e44946715afc9eea Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 12 Mar 2026 00:34:01 +1100 Subject: [PATCH 03/13] Implement filePerOperation logic --- packages/presets/near-operation-file/src/index.ts | 8 ++++++-- .../near-operation-file/src/resolve-document-imports.ts | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/presets/near-operation-file/src/index.ts b/packages/presets/near-operation-file/src/index.ts index d421724a6c..8f3923abc6 100644 --- a/packages/presets/near-operation-file/src/index.ts +++ b/packages/presets/near-operation-file/src/index.ts @@ -264,10 +264,14 @@ export const preset: Types.OutputPreset = { schemaObject, { baseDir, - generateFilePath(location: string) { + generateFilePath(location, operationName) { const newFilePath = defineFilepathSubfolder(location, folder); - return appendFileNameToFilePath(newFilePath, fileName, extension); + return appendFileNameToFilePath( + newFilePath, + filePerOperation ? operationName : fileName, + extension, + ); }, schemaTypesSource: { path: shouldAbsolute ? join(options.baseOutputDir, baseTypesPath) : baseTypesPath, diff --git a/packages/presets/near-operation-file/src/resolve-document-imports.ts b/packages/presets/near-operation-file/src/resolve-document-imports.ts index af942ab782..279136fd42 100644 --- a/packages/presets/near-operation-file/src/resolve-document-imports.ts +++ b/packages/presets/near-operation-file/src/resolve-document-imports.ts @@ -25,7 +25,7 @@ export type DocumentImportResolverOptions = { /** * Generates a target file path from the source `document.location` */ - generateFilePath: (location: string) => string; + generateFilePath: (location: string, operationName: string) => string; /** * Schema base types source */ @@ -69,7 +69,12 @@ export function resolveDocumentImports( return documents.map(documentFile => { try { - const generatedFilePath = generateFilePath(documentFile.location); + // FIXME + const operationName = + documentFile.document.definitions.find(d => d.kind === 'OperationDefinition')?.name + ?.value ?? documentFile.document.definitions[0].name.value; + + const generatedFilePath = generateFilePath(documentFile.location, operationName); const importStatements: string[] = []; const { externalFragments, fragmentImports } = resolveFragments( generatedFilePath, From f44f3096679c6372d6f78dc257cae0480e6b442c Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 12 Mar 2026 21:14:03 +1100 Subject: [PATCH 04/13] Add tests --- .../fixtures/file-per-operation.1.graphql | 12 ++++ .../fixtures/file-per-operation.2.graphql | 5 ++ .../fixtures/file-per-operation.3.graphql | 3 + .../tests/near-operation-file.spec.ts | 56 +++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql create mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql create mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql new file mode 100644 index 0000000000..89c50b6a4e --- /dev/null +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql @@ -0,0 +1,12 @@ +query User1a { + user1a: user { + id + } +} + +query User1b { + user1b: user { + id + name + } +} diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql new file mode 100644 index 0000000000..f58b972e83 --- /dev/null +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql @@ -0,0 +1,5 @@ +query User2 { + user2: user { + ...UserFragment3 + } +} diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql new file mode 100644 index 0000000000..9eea8a5020 --- /dev/null +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql @@ -0,0 +1,3 @@ +fragment UserFragment3 on User { + id +} diff --git a/packages/presets/near-operation-file/tests/near-operation-file.spec.ts b/packages/presets/near-operation-file/tests/near-operation-file.spec.ts index 2d03e2843f..825d551a04 100644 --- a/packages/presets/near-operation-file/tests/near-operation-file.spec.ts +++ b/packages/presets/near-operation-file/tests/near-operation-file.spec.ts @@ -1413,6 +1413,62 @@ describe('near-operation-file preset', () => { " `); }); + + it('generates a file per operation (instead of file) when using filePerOperation:true', async () => { + const { result } = await executeCodegen({ + schema: /* GraphQL */ ` + type Query { + user: User + } + + type User { + id: ID! + name: String! + } + `, + documents: path.join(__dirname, 'fixtures/file-per-operation.*.graphql'), + generates: { + [__dirname]: { + preset, + presetConfig: { + filePerOperation: true, + }, + plugins: ['typescript-operations'], + }, + }, + }); + + expect(result.length).toBe(3); + + const file1 = result.find(file => file.filename.endsWith('User1a.generated.ts')); + expect(file1.content).toMatchInlineSnapshot(` + "export type User1aQueryVariables = Exact<{ [key: string]: never; }>; + + + export type User1aQuery = { __typename?: 'Query', user1a?: { __typename?: 'User', id: string } | null }; + + export type User1bQueryVariables = Exact<{ [key: string]: never; }>; + + + export type User1bQuery = { __typename?: 'Query', user1b?: { __typename?: 'User', id: string, name: string } | null }; + " + `); + + const file2 = result.find(file => file.filename.endsWith('User2.generated.ts')); + expect(file2.content).toMatchInlineSnapshot(` + "export type User2QueryVariables = Exact<{ [key: string]: never; }>; + + + export type User2Query = { __typename?: 'Query', user2?: { __typename?: 'User', id: string } | null }; + " + `); + + const file3 = result.find(file => file.filename.endsWith('UserFragment3.generated.ts')); + expect(file3.content).toMatchInlineSnapshot(` + "export type UserFragment3Fragment = { __typename?: 'User', id: string }; + " + `); + }); }); const getFragmentImportsFromResult = (result: Types.GenerateOptions[], index = 0) => From de21c4cf922fe53262392fd290a67264f9642e70 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Fri, 13 Mar 2026 22:31:05 +1100 Subject: [PATCH 05/13] Fix tests --- .../fixtures/file-per-operation.1.graphql | 12 --------- .../fixtures/file-per-operation.1.graphql.ts | 15 +++++++++++ .../fixtures/file-per-operation.2.graphql | 5 ---- .../fixtures/file-per-operation.2.graphql.ts | 7 ++++++ .../fixtures/file-per-operation.3.graphql | 3 --- .../fixtures/file-per-operation.3.graphql.ts | 5 ++++ .../fixtures/file-per-operation.4.graphql | 5 ++++ .../tests/near-operation-file.spec.ts | 25 +++++++++++++++---- 8 files changed, 52 insertions(+), 25 deletions(-) delete mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql create mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts delete mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql create mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql.ts delete mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql create mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql.ts create mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.4.graphql diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql deleted file mode 100644 index 89c50b6a4e..0000000000 --- a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql +++ /dev/null @@ -1,12 +0,0 @@ -query User1a { - user1a: user { - id - } -} - -query User1b { - user1b: user { - id - name - } -} diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts new file mode 100644 index 0000000000..1c4388e691 --- /dev/null +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts @@ -0,0 +1,15 @@ +/* GraphQL */ ` + query User1a { + user1a: user { + id + } + } +`; +/* GraphQL */ ` + query User1b { + user1b: user { + id + name + } + } +`; diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql deleted file mode 100644 index f58b972e83..0000000000 --- a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql +++ /dev/null @@ -1,5 +0,0 @@ -query User2 { - user2: user { - ...UserFragment3 - } -} diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql.ts new file mode 100644 index 0000000000..20d3728d23 --- /dev/null +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql.ts @@ -0,0 +1,7 @@ +/* GraphQL */ ` + query User2 { + user2: user { + ...UserFragment3 + } + } +`; diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql deleted file mode 100644 index 9eea8a5020..0000000000 --- a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql +++ /dev/null @@ -1,3 +0,0 @@ -fragment UserFragment3 on User { - id -} diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql.ts new file mode 100644 index 0000000000..7df103c543 --- /dev/null +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql.ts @@ -0,0 +1,5 @@ +/* GraphQL */ ` + fragment UserFragment3 on User { + id + } +`; diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.4.graphql b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.4.graphql new file mode 100644 index 0000000000..c46d087117 --- /dev/null +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.4.graphql @@ -0,0 +1,5 @@ +query User4($id: ID!) { + user4: user { + name + } +} diff --git a/packages/presets/near-operation-file/tests/near-operation-file.spec.ts b/packages/presets/near-operation-file/tests/near-operation-file.spec.ts index 825d551a04..875633029b 100644 --- a/packages/presets/near-operation-file/tests/near-operation-file.spec.ts +++ b/packages/presets/near-operation-file/tests/near-operation-file.spec.ts @@ -1426,7 +1426,7 @@ describe('near-operation-file preset', () => { name: String! } `, - documents: path.join(__dirname, 'fixtures/file-per-operation.*.graphql'), + documents: path.join(__dirname, 'fixtures/file-per-operation.*.graphql*'), generates: { [__dirname]: { preset, @@ -1438,16 +1438,20 @@ describe('near-operation-file preset', () => { }, }); - expect(result.length).toBe(3); + expect(result.length).toBe(5); - const file1 = result.find(file => file.filename.endsWith('User1a.generated.ts')); - expect(file1.content).toMatchInlineSnapshot(` + const file1a = result.find(file => file.filename.endsWith('User1a.generated.ts')); + expect(file1a.content).toMatchInlineSnapshot(` "export type User1aQueryVariables = Exact<{ [key: string]: never; }>; export type User1aQuery = { __typename?: 'Query', user1a?: { __typename?: 'User', id: string } | null }; + " + `); - export type User1bQueryVariables = Exact<{ [key: string]: never; }>; + const file1b = result.find(file => file.filename.endsWith('User1b.generated.ts')); + expect(file1b.content).toMatchInlineSnapshot(` + "export type User1bQueryVariables = Exact<{ [key: string]: never; }>; export type User1bQuery = { __typename?: 'Query', user1b?: { __typename?: 'User', id: string, name: string } | null }; @@ -1468,6 +1472,17 @@ describe('near-operation-file preset', () => { "export type UserFragment3Fragment = { __typename?: 'User', id: string }; " `); + + const file4 = result.find(file => file.filename.endsWith('User4.generated.ts')); + expect(file4.content).toMatchInlineSnapshot(` + "export type User4QueryVariables = Exact<{ + id: Scalars['ID']['input']; + }>; + + + export type User4Query = { __typename?: 'Query', user4?: { __typename?: 'User', name: string } | null }; + " + `); }); }); From a8c579548b0066df036228b52e64bcb51e3bf319 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Fri, 13 Mar 2026 23:12:17 +1100 Subject: [PATCH 06/13] Handle unnamed operations --- .../near-operation-file/src/fragment-resolver.ts | 2 +- packages/presets/near-operation-file/src/index.ts | 8 +++++--- .../src/resolve-document-imports.ts | 13 ++++++------- .../tests/fixtures/file-per-operation.1.graphql.ts | 8 ++++++++ .../tests/near-operation-file.spec.ts | 14 +++++++++++++- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/presets/near-operation-file/src/fragment-resolver.ts b/packages/presets/near-operation-file/src/fragment-resolver.ts index be73984c92..8ffdfc804b 100644 --- a/packages/presets/near-operation-file/src/fragment-resolver.ts +++ b/packages/presets/near-operation-file/src/fragment-resolver.ts @@ -101,7 +101,7 @@ function buildFragmentRegistry( } const fragmentName = fragment.name.value; - const filePath = generateFilePath(documentRecord.location); + const filePath = generateFilePath(documentRecord.location, fragmentName); const possibleTypes = getPossibleTypes(schemaObject, schemaType); const possibleTypeNames = possibleTypes.map(t => t.name); const imports = createFragmentImports(baseVisitor, fragment.name.value, possibleTypeNames); diff --git a/packages/presets/near-operation-file/src/index.ts b/packages/presets/near-operation-file/src/index.ts index 8f3923abc6..5b5382a42b 100644 --- a/packages/presets/near-operation-file/src/index.ts +++ b/packages/presets/near-operation-file/src/index.ts @@ -202,7 +202,7 @@ export type NearOperationFileConfig = { importTypesNamespace?: string; /** - * @description Optional, generates a file per operation + * @description Optional, generates one file per operation, using the operation name as the filename. Note: if your documents are in `.graphql` files and there are multiple operations or fragments in a single file, the generated filename will be based on the first operation or fragment found. * @default false * * @exampleMarkdown @@ -264,12 +264,14 @@ export const preset: Types.OutputPreset = { schemaObject, { baseDir, - generateFilePath(location, operationName) { + generateFilePath(location, customFilename) { const newFilePath = defineFilepathSubfolder(location, folder); return appendFileNameToFilePath( newFilePath, - filePerOperation ? operationName : fileName, + filePerOperation + ? customFilename // Note: Unnamed operations will cause `operationName` to be undefined. In such case, the generated filename will be based on the source document file. + : fileName, extension, ); }, diff --git a/packages/presets/near-operation-file/src/resolve-document-imports.ts b/packages/presets/near-operation-file/src/resolve-document-imports.ts index 279136fd42..d6685dadbc 100644 --- a/packages/presets/near-operation-file/src/resolve-document-imports.ts +++ b/packages/presets/near-operation-file/src/resolve-document-imports.ts @@ -1,4 +1,4 @@ -import { FragmentDefinitionNode, GraphQLSchema } from 'graphql'; +import { FragmentDefinitionNode, GraphQLSchema, Kind, OperationDefinitionNode } from 'graphql'; import { isUsingTypes, Types } from '@graphql-codegen/plugin-helpers'; import { FragmentImport, @@ -25,7 +25,7 @@ export type DocumentImportResolverOptions = { /** * Generates a target file path from the source `document.location` */ - generateFilePath: (location: string, operationName: string) => string; + generateFilePath: (location: string, customFilename?: string) => string; /** * Schema base types source */ @@ -69,12 +69,11 @@ export function resolveDocumentImports( return documents.map(documentFile => { try { - // FIXME - const operationName = - documentFile.document.definitions.find(d => d.kind === 'OperationDefinition')?.name - ?.value ?? documentFile.document.definitions[0].name.value; + const operationOrFragmentName = documentFile.document.definitions.find( + d => d.kind === Kind.OPERATION_DEFINITION || d.kind === Kind.FRAGMENT_DEFINITION, + )?.name?.value; - const generatedFilePath = generateFilePath(documentFile.location, operationName); + const generatedFilePath = generateFilePath(documentFile.location, operationOrFragmentName); const importStatements: string[] = []; const { externalFragments, fragmentImports } = resolveFragments( generatedFilePath, diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts index 1c4388e691..18f0ff11c0 100644 --- a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts @@ -13,3 +13,11 @@ } } `; + +/* GraphQL */ ` + query { + anon: user { + __typename + } + } +`; diff --git a/packages/presets/near-operation-file/tests/near-operation-file.spec.ts b/packages/presets/near-operation-file/tests/near-operation-file.spec.ts index 875633029b..6b6b9c3fd8 100644 --- a/packages/presets/near-operation-file/tests/near-operation-file.spec.ts +++ b/packages/presets/near-operation-file/tests/near-operation-file.spec.ts @@ -1438,7 +1438,7 @@ describe('near-operation-file preset', () => { }, }); - expect(result.length).toBe(5); + expect(result.length).toBe(6); const file1a = result.find(file => file.filename.endsWith('User1a.generated.ts')); expect(file1a.content).toMatchInlineSnapshot(` @@ -1458,6 +1458,18 @@ describe('near-operation-file preset', () => { " `); + // Unnamed operations falls back to source doc filename + const file1c = result.find(file => + file.filename.endsWith('file-per-operation.1.graphql.generated.ts'), + ); + expect(file1c.content).toMatchInlineSnapshot(` + "export type Unnamed_1_QueryVariables = Exact<{ [key: string]: never; }>; + + + export type Unnamed_1_Query = { __typename?: 'Query', anon?: { __typename: 'User' } | null }; + " + `); + const file2 = result.find(file => file.filename.endsWith('User2.generated.ts')); expect(file2.content).toMatchInlineSnapshot(` "export type User2QueryVariables = Exact<{ [key: string]: never; }>; From 1b2f2d043c1f10e84ee30d4f19d44e6afe9b0a42 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Fri, 13 Mar 2026 23:42:46 +1100 Subject: [PATCH 07/13] Format --- .../presets/near-operation-file/src/resolve-document-imports.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/presets/near-operation-file/src/resolve-document-imports.ts b/packages/presets/near-operation-file/src/resolve-document-imports.ts index d6685dadbc..9b45b81499 100644 --- a/packages/presets/near-operation-file/src/resolve-document-imports.ts +++ b/packages/presets/near-operation-file/src/resolve-document-imports.ts @@ -1,4 +1,4 @@ -import { FragmentDefinitionNode, GraphQLSchema, Kind, OperationDefinitionNode } from 'graphql'; +import { FragmentDefinitionNode, GraphQLSchema, Kind } from 'graphql'; import { isUsingTypes, Types } from '@graphql-codegen/plugin-helpers'; import { FragmentImport, From 6764029371f117cd869c188fb3b690d257675620 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Fri, 13 Mar 2026 23:45:31 +1100 Subject: [PATCH 08/13] Add changeset --- .changeset/afraid-buckets-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/afraid-buckets-fly.md diff --git a/.changeset/afraid-buckets-fly.md b/.changeset/afraid-buckets-fly.md new file mode 100644 index 0000000000..03d9d680f8 --- /dev/null +++ b/.changeset/afraid-buckets-fly.md @@ -0,0 +1,5 @@ +--- +'@graphql-codegen/near-operation-file-preset': minor +--- + +Add `filePerOperation` config to generate filename based on named operation or fragment, instead of source filename From b67ae571e3de84abafcd41eda4e7dea0923de888 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 16 Mar 2026 22:37:39 +1100 Subject: [PATCH 09/13] Add test case for fragment before document use case --- .../fixtures/file-per-operation.5.graphql.ts | 13 +++++++++++++ .../tests/near-operation-file.spec.ts | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.5.graphql.ts diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.5.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.5.graphql.ts new file mode 100644 index 0000000000..8f5dce517e --- /dev/null +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.5.graphql.ts @@ -0,0 +1,13 @@ +/* GraphQL */ ` + fragment UserFragment5a on User { + id + } +`; + +/* GraphQL */ ` + query User5b { + user1a: user { + id + } + } +`; diff --git a/packages/presets/near-operation-file/tests/near-operation-file.spec.ts b/packages/presets/near-operation-file/tests/near-operation-file.spec.ts index 6b6b9c3fd8..c6e1b1bf1d 100644 --- a/packages/presets/near-operation-file/tests/near-operation-file.spec.ts +++ b/packages/presets/near-operation-file/tests/near-operation-file.spec.ts @@ -1438,7 +1438,7 @@ describe('near-operation-file preset', () => { }, }); - expect(result.length).toBe(6); + expect(result.length).toBe(8); const file1a = result.find(file => file.filename.endsWith('User1a.generated.ts')); expect(file1a.content).toMatchInlineSnapshot(` @@ -1495,6 +1495,20 @@ describe('near-operation-file preset', () => { export type User4Query = { __typename?: 'Query', user4?: { __typename?: 'User', name: string } | null }; " `); + + const file5a = result.find(file => file.filename.endsWith('UserFragment5a.generated.ts')); + expect(file5a.content).toMatchInlineSnapshot(` + "export type UserFragment5aFragment = { __typename?: 'User', id: string }; + " + `); + const file5b = result.find(file => file.filename.endsWith('User5b.generated.ts')); + expect(file5b.content).toMatchInlineSnapshot(` + "export type User5bQueryVariables = Exact<{ [key: string]: never; }>; + + + export type User5bQuery = { __typename?: 'Query', user1a?: { __typename?: 'User', id: string } | null }; + " + `); }); }); From cd742c3da93a774a1591ff3ba0765028c25ded32 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 16 Mar 2026 22:42:11 +1100 Subject: [PATCH 10/13] Fix filename fallback logic --- packages/presets/near-operation-file/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/presets/near-operation-file/src/index.ts b/packages/presets/near-operation-file/src/index.ts index 5b5382a42b..6f6d937ae5 100644 --- a/packages/presets/near-operation-file/src/index.ts +++ b/packages/presets/near-operation-file/src/index.ts @@ -269,7 +269,7 @@ export const preset: Types.OutputPreset = { return appendFileNameToFilePath( newFilePath, - filePerOperation + filePerOperation && customFilename ? customFilename // Note: Unnamed operations will cause `operationName` to be undefined. In such case, the generated filename will be based on the source document file. : fileName, extension, From 72cf3f00faf1e1fc114586dcee3ecf792db85f25 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 19 Apr 2026 19:54:42 +1000 Subject: [PATCH 11/13] Implement fragment before operation case for filePerOperation --- .../src/fragment-resolver.ts | 8 +++-- .../presets/near-operation-file/src/index.ts | 20 ++++++----- .../src/resolve-document-imports.ts | 34 +++++++++++++++---- .../fixtures/file-per-operation.6.graphql.ts | 10 ++++++ .../tests/near-operation-file.spec.ts | 13 ++++++- 5 files changed, 68 insertions(+), 17 deletions(-) create mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql.ts diff --git a/packages/presets/near-operation-file/src/fragment-resolver.ts b/packages/presets/near-operation-file/src/fragment-resolver.ts index 8ffdfc804b..8516778aa0 100644 --- a/packages/presets/near-operation-file/src/fragment-resolver.ts +++ b/packages/presets/near-operation-file/src/fragment-resolver.ts @@ -89,7 +89,7 @@ function buildFragmentRegistry( const registry = documents.reduce((prev: FragmentRegistry, documentRecord) => { const fragments: FragmentDefinitionNode[] = documentRecord.document.definitions.filter( d => d.kind === Kind.FRAGMENT_DEFINITION, - ) as FragmentDefinitionNode[]; + ); for (const fragment of fragments) { const schemaType = schemaObject.getType(fragment.typeCondition.name.value); @@ -100,8 +100,12 @@ function buildFragmentRegistry( ); } + const filePath = generateFilePath({ + location: documentRecord.location, + meta: { operations: [], fragments: [fragment] }, + }); + const fragmentName = fragment.name.value; - const filePath = generateFilePath(documentRecord.location, fragmentName); const possibleTypes = getPossibleTypes(schemaObject, schemaType); const possibleTypeNames = possibleTypes.map(t => t.name); const imports = createFragmentImports(baseVisitor, fragment.name.value, possibleTypeNames); diff --git a/packages/presets/near-operation-file/src/index.ts b/packages/presets/near-operation-file/src/index.ts index 6f6d937ae5..8d669ded90 100644 --- a/packages/presets/near-operation-file/src/index.ts +++ b/packages/presets/near-operation-file/src/index.ts @@ -264,16 +264,20 @@ export const preset: Types.OutputPreset = { schemaObject, { baseDir, - generateFilePath(location, customFilename) { + generateFilePath({ location, meta }) { const newFilePath = defineFilepathSubfolder(location, folder); - return appendFileNameToFilePath( - newFilePath, - filePerOperation && customFilename - ? customFilename // Note: Unnamed operations will cause `operationName` to be undefined. In such case, the generated filename will be based on the source document file. - : fileName, - extension, - ); + let customFilename = fileName; + if (filePerOperation) { + // Note: If a file only has unnamed operations (i.e. undefined operation name) or fragment (i.e. undefined fragment name). + // In such case, the generated filename will be based on the source document file. + customFilename = + meta['operations'][0]?.name?.value || + meta['fragments'][0]?.name?.value || + customFilename; + } + + return appendFileNameToFilePath(newFilePath, customFilename, extension); }, schemaTypesSource: { path: shouldAbsolute ? join(options.baseOutputDir, baseTypesPath) : baseTypesPath, diff --git a/packages/presets/near-operation-file/src/resolve-document-imports.ts b/packages/presets/near-operation-file/src/resolve-document-imports.ts index 9b45b81499..4a27b647c4 100644 --- a/packages/presets/near-operation-file/src/resolve-document-imports.ts +++ b/packages/presets/near-operation-file/src/resolve-document-imports.ts @@ -1,4 +1,9 @@ -import { FragmentDefinitionNode, GraphQLSchema, Kind } from 'graphql'; +import { + GraphQLSchema, + Kind, + type FragmentDefinitionNode, + type OperationDefinitionNode, +} from 'graphql'; import { isUsingTypes, Types } from '@graphql-codegen/plugin-helpers'; import { FragmentImport, @@ -25,7 +30,13 @@ export type DocumentImportResolverOptions = { /** * Generates a target file path from the source `document.location` */ - generateFilePath: (location: string, customFilename?: string) => string; + generateFilePath: (params: { + location: string; + meta: { + operations: OperationDefinitionNode[]; + fragments: FragmentDefinitionNode[]; + }; + }) => string; /** * Schema base types source */ @@ -69,11 +80,22 @@ export function resolveDocumentImports( return documents.map(documentFile => { try { - const operationOrFragmentName = documentFile.document.definitions.find( - d => d.kind === Kind.OPERATION_DEFINITION || d.kind === Kind.FRAGMENT_DEFINITION, - )?.name?.value; + const meta: { + operations: OperationDefinitionNode[]; + fragments: FragmentDefinitionNode[]; + } = { + operations: [], + fragments: [], + }; + for (const definition of documentFile.document.definitions) { + if (definition.kind === Kind.OPERATION_DEFINITION) { + meta.operations.push(definition); + } else if (definition.kind === Kind.FRAGMENT_DEFINITION) { + meta.fragments.push(definition); + } + } + const generatedFilePath = generateFilePath({ location: documentFile.location, meta }); - const generatedFilePath = generateFilePath(documentFile.location, operationOrFragmentName); const importStatements: string[] = []; const { externalFragments, fragmentImports } = resolveFragments( generatedFilePath, diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql.ts new file mode 100644 index 0000000000..a2b3b000fd --- /dev/null +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql.ts @@ -0,0 +1,10 @@ +/* GraphQL */ ` + fragment UserFragment6 on User { + id + } + query User6 { + user6: user { + id + } + } +`; diff --git a/packages/presets/near-operation-file/tests/near-operation-file.spec.ts b/packages/presets/near-operation-file/tests/near-operation-file.spec.ts index c6e1b1bf1d..577e4c988b 100644 --- a/packages/presets/near-operation-file/tests/near-operation-file.spec.ts +++ b/packages/presets/near-operation-file/tests/near-operation-file.spec.ts @@ -1438,7 +1438,7 @@ describe('near-operation-file preset', () => { }, }); - expect(result.length).toBe(8); + expect(result.length).toBe(9); const file1a = result.find(file => file.filename.endsWith('User1a.generated.ts')); expect(file1a.content).toMatchInlineSnapshot(` @@ -1509,6 +1509,17 @@ describe('near-operation-file preset', () => { export type User5bQuery = { __typename?: 'Query', user1a?: { __typename?: 'User', id: string } | null }; " `); + + const file6 = result.find(file => file.filename.endsWith('User6.generated.ts')); + expect(file6.content).toMatchInlineSnapshot(` + "export type UserFragment6Fragment = { __typename?: 'User', id: string }; + + export type User6QueryVariables = Exact<{ [key: string]: never; }>; + + + export type User6Query = { __typename?: 'Query', user6?: { __typename?: 'User', id: string } | null }; + " + `); }); }); From 53a59111854bdab69631198779b8bd7202ef75ba Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 19 Apr 2026 19:54:57 +1000 Subject: [PATCH 12/13] Drive-by fix vscode config --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index da75431998..a115c697a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,7 +17,7 @@ "coverage": true, "npm": true }, - "typescript.tsdk": "node_modules/typescript/lib", + "js/ts.tsdk.path": "node_modules/typescript/lib", "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "[typescript]": { From 01c341f24bbb065c5a48d1128cf461b79b2aa374 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 19 Apr 2026 20:05:59 +1000 Subject: [PATCH 13/13] Fix lint issues --- .../tests/fixtures/file-per-operation.1.graphql.ts | 6 +++--- .../tests/fixtures/file-per-operation.2.graphql.ts | 2 +- .../tests/fixtures/file-per-operation.3.graphql.ts | 2 +- .../tests/fixtures/file-per-operation.5.graphql.ts | 4 ++-- .../tests/fixtures/file-per-operation.6.graphql | 8 ++++++++ .../tests/fixtures/file-per-operation.6.graphql.ts | 10 ---------- 6 files changed, 15 insertions(+), 17 deletions(-) create mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql delete mode 100644 packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql.ts diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts index 18f0ff11c0..9b27303e90 100644 --- a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.1.graphql.ts @@ -1,11 +1,11 @@ -/* GraphQL */ ` +export const user1a = /* GraphQL */ ` query User1a { user1a: user { id } } `; -/* GraphQL */ ` +export const user1b = /* GraphQL */ ` query User1b { user1b: user { id @@ -14,7 +14,7 @@ } `; -/* GraphQL */ ` +export const anon = /* GraphQL */ ` query { anon: user { __typename diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql.ts index 20d3728d23..07888a1b35 100644 --- a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql.ts +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.2.graphql.ts @@ -1,4 +1,4 @@ -/* GraphQL */ ` +export const user2 = /* GraphQL */ ` query User2 { user2: user { ...UserFragment3 diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql.ts index 7df103c543..42f9935afd 100644 --- a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql.ts +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.3.graphql.ts @@ -1,4 +1,4 @@ -/* GraphQL */ ` +export const userFragment3 = /* GraphQL */ ` fragment UserFragment3 on User { id } diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.5.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.5.graphql.ts index 8f5dce517e..14e0308b4d 100644 --- a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.5.graphql.ts +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.5.graphql.ts @@ -1,10 +1,10 @@ -/* GraphQL */ ` +export const userFragment5a = /* GraphQL */ ` fragment UserFragment5a on User { id } `; -/* GraphQL */ ` +export const user5b = /* GraphQL */ ` query User5b { user1a: user { id diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql new file mode 100644 index 0000000000..b63a147f74 --- /dev/null +++ b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql @@ -0,0 +1,8 @@ +fragment UserFragment6 on User { + id +} +query User6 { + user6: user { + id + } +} diff --git a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql.ts b/packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql.ts deleted file mode 100644 index a2b3b000fd..0000000000 --- a/packages/presets/near-operation-file/tests/fixtures/file-per-operation.6.graphql.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* GraphQL */ ` - fragment UserFragment6 on User { - id - } - query User6 { - user6: user { - id - } - } -`;