Skip to content

Commit 1132fe1

Browse files
committed
feat: omit unused input/enum types when onlyOperationTypes is enabled
1 parent 3fd4486 commit 1132fe1

File tree

8 files changed

+324
-16
lines changed

8 files changed

+324
-16
lines changed

dev-test/star-wars/types.preResolveTypes.onlyOperationTypes.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,6 @@ export enum Episode {
3131
Newhope = 'NEWHOPE',
3232
}
3333

34-
/** Units of height */
35-
export enum LengthUnit {
36-
/** Primarily used in the United States */
37-
Foot = 'FOOT',
38-
/** The standard unit around the world */
39-
Meter = 'METER',
40-
}
41-
4234
/** The input object sent when someone is creating a new review */
4335
export type ReviewInput = {
4436
/** Comment about the movie, optional */

packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export interface ParsedTypesConfig extends ParsedConfig {
6060
wrapEntireDefinitions: boolean;
6161
ignoreEnumValuesFromSchema: boolean;
6262
directiveArgumentAndInputFieldMappings: ParsedDirectiveArgumentAndInputFieldMappings;
63+
/** When non-null, contains a subset of input types & enums that should be generated. See `onlyOperationTypes` */
64+
usedTypes?: Set<string>;
6365
}
6466

6567
export interface RawTypesConfig extends RawConfig {
@@ -675,7 +677,15 @@ export class BaseTypesVisitor<
675677
}
676678

677679
InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode): string {
678-
if (this.config.onlyEnums) return '';
680+
if (
681+
(this.config.onlyOperationTypes &&
682+
!this.config.usedTypes?.has(
683+
// types are wrong; string at runtime?
684+
node.name as unknown as string
685+
)) ||
686+
this.config.onlyEnums
687+
)
688+
return '';
679689

680690
// Why the heck is node.name a string and not { value: string } at runtime ?!
681691
if (isOneOfInputObjectType(this._schema.getType(node.name as unknown as string))) {

packages/plugins/typescript/operations/tests/__snapshots__/ts-documents.spec.ts.snap

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,31 @@ export type SnakeQueryQuery = { __typename: 'Query', snake: { __typename: 'Snake
157157
"
158158
`;
159159
160+
exports[`TypeScript Operations Plugin Operation Definition should only emit used enums when onlyOperationTypes=true 1`] = `
161+
"/** All built-in and custom scalars, mapped to their actual values */
162+
export type Scalars = {
163+
ID: { input: string; output: string; }
164+
String: { input: string; output: string; }
165+
Boolean: { input: boolean; output: boolean; }
166+
Int: { input: number; output: number; }
167+
Float: { input: number; output: number; }
168+
};
169+
170+
export type InfoInput = {
171+
type: InputEnum;
172+
};
173+
174+
export enum InputEnum {
175+
Name = 'NAME',
176+
Address = 'ADDRESS'
177+
}
178+
179+
export enum OutputEnum {
180+
Keep = 'KEEP'
181+
}
182+
"
183+
`;
184+
160185
exports[`TypeScript Operations Plugin Selection Set Should generate the correct __typename when using both inline fragment and spread over type 1`] = `
161186
"export type UserQueryQueryVariables = Exact<{ [key: string]: never; }>;
162187

packages/plugins/typescript/operations/tests/ts-documents.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3305,6 +3305,50 @@ describe('TypeScript Operations Plugin', () => {
33053305
}>;
33063306
`);
33073307
});
3308+
3309+
it('should only emit used enums when onlyOperationTypes=true', async () => {
3310+
const testSchema = buildSchema(/* GraphQL */ `
3311+
type Query {
3312+
info(input: InfoInput, unusedEnum: UnusedEnum = null, unusedType: UnusedType = null): InfoOutput
3313+
}
3314+
3315+
input InfoInput {
3316+
type: InputEnum!
3317+
}
3318+
3319+
enum InputEnum {
3320+
NAME
3321+
ADDRESS
3322+
}
3323+
3324+
type InfoOutput {
3325+
type: OutputEnum!
3326+
}
3327+
3328+
enum OutputEnum {
3329+
KEEP
3330+
}
3331+
3332+
input UnusedType {
3333+
type: UnusedEnum!
3334+
}
3335+
3336+
enum UnusedEnum {
3337+
UNUSED
3338+
}
3339+
`);
3340+
3341+
const document = parse(/* GraphQL */ `
3342+
query InfoQuery($input: InfoInput) {
3343+
info(input: $input, unusedEnum: UNUSED) {
3344+
type
3345+
}
3346+
}
3347+
`);
3348+
3349+
const { content } = await tsPlugin(testSchema, [{ location: '', document }], { onlyOperationTypes: true }, {});
3350+
expect(content).toMatchSnapshot();
3351+
});
33083352
});
33093353

33103354
describe('Union & Interfaces', () => {

packages/plugins/typescript/typescript/src/index.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { oldVisit, PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
1+
import { getBaseType, oldVisit, PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
22
import { transformSchemaAST } from '@graphql-codegen/schema-ast';
33
import {
4+
buildASTSchema,
45
DocumentNode,
56
getNamedType,
67
GraphQLNamedType,
@@ -12,10 +13,14 @@ import {
1213
TypeInfo,
1314
visit,
1415
visitWithTypeInfo,
16+
GraphQLEnumType,
17+
GraphQLInputObjectType,
1518
} from 'graphql';
1619
import { TypeScriptPluginConfig } from './config.js';
1720
import { TsIntrospectionVisitor } from './introspection-visitor.js';
1821
import { TsVisitor } from './visitor.js';
22+
import { getCachedDocumentNodeFromSchema } from '@graphql-codegen/plugin-helpers';
23+
import { getBaseTypeNode } from '@graphql-codegen/visitor-plugin-common';
1924

2025
export * from './config.js';
2126
export * from './introspection-visitor.js';
@@ -29,7 +34,12 @@ export const plugin: PluginFunction<TypeScriptPluginConfig, Types.ComplexPluginO
2934
) => {
3035
const { schema: _schema, ast } = transformSchemaAST(schema, config);
3136

32-
const visitor = new TsVisitor(_schema, config);
37+
let usedTypes = undefined;
38+
if (config.onlyOperationTypes) {
39+
usedTypes = getUsedTypeNames(schema, documents);
40+
}
41+
42+
const visitor = new TsVisitor(_schema, config, { usedTypes });
3343

3444
const visitorResult = oldVisit(ast, { leave: visitor });
3545
const introspectionDefinitions = includeIntrospectionTypesDefinitions(_schema, documents, config);
@@ -62,7 +72,7 @@ export function includeIntrospectionTypesDefinitions(
6272
const typeInfo = new TypeInfo(schema);
6373
const usedTypes: GraphQLNamedType[] = [];
6474
const documentsVisitor = visitWithTypeInfo(typeInfo, {
65-
Field() {
75+
Field(node) {
6676
const type = getNamedType(typeInfo.getType());
6777

6878
if (type && isIntrospectionType(type) && !usedTypes.includes(type)) {
@@ -106,3 +116,63 @@ export function includeIntrospectionTypesDefinitions(
106116

107117
return result.definitions as any[];
108118
}
119+
120+
export function getUsedTypeNames(schema: GraphQLSchema, documents: Types.DocumentFile[]): Set<string> {
121+
if (!schema.astNode) {
122+
const ast = getCachedDocumentNodeFromSchema(schema);
123+
schema = buildASTSchema(ast);
124+
}
125+
126+
const typeInfo = new TypeInfo(schema);
127+
128+
const visited = new Set<string>();
129+
const queue: GraphQLNamedType[] = [];
130+
131+
function enqueue(type: GraphQLNamedType) {
132+
if (
133+
type.astNode && // skip scalars
134+
!visited.has(type.name)
135+
) {
136+
visited.add(type.name);
137+
queue.push(type);
138+
}
139+
}
140+
141+
const visitor = visitWithTypeInfo(typeInfo, {
142+
VariableDefinition() {
143+
const field = typeInfo.getInputType();
144+
const type = getBaseType(field);
145+
enqueue(type);
146+
},
147+
Field() {
148+
const field = typeInfo.getFieldDef();
149+
const type = getBaseType(field.type);
150+
if (type instanceof GraphQLEnumType || type instanceof GraphQLInputObjectType) {
151+
enqueue(type);
152+
}
153+
},
154+
InputObjectTypeDefinition(node) {
155+
for (const field of node.fields ?? []) {
156+
const baseType = getBaseTypeNode(field.type);
157+
const expanded = schema.getType(baseType.name.value);
158+
if (expanded.name) {
159+
enqueue(expanded);
160+
}
161+
}
162+
},
163+
});
164+
165+
for (const doc of documents) {
166+
visit(doc.document, visitor);
167+
}
168+
169+
const typeNames = new Set<string>();
170+
while (true) {
171+
const type = queue.pop();
172+
if (!type) break;
173+
typeNames.add(type.name);
174+
visit(type.astNode, visitor);
175+
}
176+
177+
return typeNames;
178+
}

packages/plugins/typescript/typescript/src/visitor.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,15 @@ export class TsVisitor<
351351
}
352352

353353
EnumTypeDefinition(node: EnumTypeDefinitionNode): string {
354+
if (
355+
this.config.onlyOperationTypes &&
356+
!this.config.usedTypes?.has(
357+
// types are wrong; string at runtime?
358+
node.name as unknown as string
359+
) &&
360+
!this.config.onlyEnums
361+
)
362+
return '';
354363
const enumName = node.name as any as string;
355364

356365
// In case of mapped external enum string

0 commit comments

Comments
 (0)