Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/afraid-buckets-fly.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"coverage": true,
"npm": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"js/ts.tsdk.path": "node_modules/typescript/lib",
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by updating this because of the new VSCode/TypeScript integration

"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": {
Expand Down
8 changes: 6 additions & 2 deletions packages/presets/near-operation-file/src/fragment-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ function buildFragmentRegistry(
const registry = documents.reduce<FragmentRegistry>((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);
Expand All @@ -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);
const possibleTypes = getPossibleTypes(schemaObject, schemaType);
const possibleTypeNames = possibleTypes.map(t => t.name);
const imports = createFragmentImports(baseVisitor, fragment.name.value, possibleTypeNames);
Expand Down
41 changes: 38 additions & 3 deletions packages/presets/near-operation-file/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -200,6 +200,30 @@ export type NearOperationFileConfig = {
* ```
*/
importTypesNamespace?: string;

/**
* @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
* ```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 = {
Expand All @@ -226,6 +250,7 @@ export const preset: Types.OutputPreset<NearOperationFileConfig> = {
(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('~');

Expand All @@ -239,10 +264,20 @@ export const preset: Types.OutputPreset<NearOperationFileConfig> = {
schemaObject,
{
baseDir,
generateFilePath(location: string) {
generateFilePath({ location, meta }) {
const newFilePath = defineFilepathSubfolder(location, folder);

return appendFileNameToFilePath(newFilePath, 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { FragmentDefinitionNode, GraphQLSchema } from 'graphql';
import {
GraphQLSchema,
Kind,
type FragmentDefinitionNode,
type OperationDefinitionNode,
} from 'graphql';
import { isUsingTypes, Types } from '@graphql-codegen/plugin-helpers';
import {
FragmentImport,
Expand All @@ -25,7 +30,13 @@ export type DocumentImportResolverOptions = {
/**
* Generates a target file path from the source `document.location`
*/
generateFilePath: (location: string) => string;
generateFilePath: (params: {
location: string;
meta: {
operations: OperationDefinitionNode[];
fragments: FragmentDefinitionNode[];
};
}) => string;
/**
* Schema base types source
*/
Expand Down Expand Up @@ -69,7 +80,22 @@ export function resolveDocumentImports<T>(

return documents.map(documentFile => {
try {
const generatedFilePath = generateFilePath(documentFile.location);
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 importStatements: string[] = [];
const { externalFragments, fragmentImports } = resolveFragments(
generatedFilePath,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const user1a = /* GraphQL */ `
query User1a {
user1a: user {
id
}
}
`;
export const user1b = /* GraphQL */ `
query User1b {
user1b: user {
id
name
}
}
`;

export const anon = /* GraphQL */ `
query {
anon: user {
__typename
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const user2 = /* GraphQL */ `
query User2 {
user2: user {
...UserFragment3
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const userFragment3 = /* GraphQL */ `
fragment UserFragment3 on User {
id
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query User4($id: ID!) {
user4: user {
name
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const userFragment5a = /* GraphQL */ `
fragment UserFragment5a on User {
id
}
`;

export const user5b = /* GraphQL */ `
query User5b {
user1a: user {
id
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
fragment UserFragment6 on User {
id
}
query User6 {
user6: user {
id
}
}
108 changes: 108 additions & 0 deletions packages/presets/near-operation-file/tests/near-operation-file.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,114 @@ 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(9);

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 };
"
`);

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 };
"
`);

// 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; }>;


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 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 };
"
`);

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 };
"
`);

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 };
"
`);
});
Comment thread
eddeee888 marked this conversation as resolved.
});

const getFragmentImportsFromResult = (result: Types.GenerateOptions[], index = 0) =>
Expand Down
Loading