Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: omit unused input/enum types when onlyOperationTypes is enabled #10164

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@ export enum Episode {
Newhope = 'NEWHOPE',
}

/** Units of height */
export enum LengthUnit {
/** Primarily used in the United States */
Foot = 'FOOT',
/** The standard unit around the world */
Meter = 'METER',
}

/** The input object sent when someone is creating a new review */
export type ReviewInput = {
/** Comment about the movie, optional */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export interface ParsedTypesConfig extends ParsedConfig {
wrapEntireDefinitions: boolean;
ignoreEnumValuesFromSchema: boolean;
directiveArgumentAndInputFieldMappings: ParsedDirectiveArgumentAndInputFieldMappings;
/** When non-null, contains a subset of input types & enums that should be generated. See `onlyOperationTypes` */
usedTypes?: Set<string>;
}

export interface RawTypesConfig extends RawConfig {
Expand Down Expand Up @@ -331,7 +333,7 @@ export interface RawTypesConfig extends RawConfig {
*/
onlyEnums?: boolean;
/**
* @description This will cause the generator to emit types for operations only (basically only enums and scalars)
* @description This will cause the generator to only emit types used by one or more operations (basically only enums, inputs, and scalars).
* @default false
*
* @exampleMarkdown
Expand Down Expand Up @@ -675,7 +677,15 @@ export class BaseTypesVisitor<
}

InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode): string {
if (this.config.onlyEnums) return '';
if (
(this.config.onlyOperationTypes &&
!this.config.usedTypes?.has(
// types are wrong; string at runtime?
node.name as unknown as string
)) ||
this.config.onlyEnums
)
return '';

// Why the heck is node.name a string and not { value: string } at runtime ?!
if (isOneOfInputObjectType(this._schema.getType(node.name as unknown as string))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,31 @@ export type SnakeQueryQuery = { __typename: 'Query', snake: { __typename: 'Snake
"
`;

exports[`TypeScript Operations Plugin Operation Definition should only emit used enums when onlyOperationTypes=true 1`] = `
"/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
};

export type InfoInput = {
type: InputEnum;
};

export enum InputEnum {
Name = 'NAME',
Address = 'ADDRESS'
}

export enum OutputEnum {
Keep = 'KEEP'
}
"
`;

exports[`TypeScript Operations Plugin Selection Set Should generate the correct __typename when using both inline fragment and spread over type 1`] = `
"export type UserQueryQueryVariables = Exact<{ [key: string]: never; }>;

Expand Down
44 changes: 44 additions & 0 deletions packages/plugins/typescript/operations/tests/ts-documents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3305,6 +3305,50 @@ describe('TypeScript Operations Plugin', () => {
}>;
`);
});

it('should only emit used enums when onlyOperationTypes=true', async () => {
const testSchema = buildSchema(/* GraphQL */ `
type Query {
info(input: InfoInput, unusedEnum: UnusedEnum = null, unusedType: UnusedType = null): InfoOutput
}

input InfoInput {
type: InputEnum!
}

enum InputEnum {
NAME
ADDRESS
}

type InfoOutput {
type: OutputEnum!
}

enum OutputEnum {
KEEP
}

input UnusedType {
type: UnusedEnum!
}

enum UnusedEnum {
UNUSED
}
`);

const document = parse(/* GraphQL */ `
query InfoQuery($input: InfoInput) {
info(input: $input, unusedEnum: UNUSED) {
type
}
}
`);

const { content } = await tsPlugin(testSchema, [{ location: '', document }], { onlyOperationTypes: true }, {});
expect(content).toMatchSnapshot();
});
});

describe('Union & Interfaces', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/typescript/typescript/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export interface TypeScriptPluginConfig extends RawTypesConfig {
*/
onlyEnums?: boolean;
/**
* @description This will cause the generator to emit types for operations only (basically only enums and scalars).
* @description This will cause the generator to only emit types used by one or more operations (basically only enums, inputs, and scalars).
* Interacts well with `preResolveTypes: true`
* @default false
*
Expand Down
76 changes: 73 additions & 3 deletions packages/plugins/typescript/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { oldVisit, PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
import { getBaseType, oldVisit, PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
import { transformSchemaAST } from '@graphql-codegen/schema-ast';
import {
buildASTSchema,
DocumentNode,
getNamedType,
GraphQLNamedType,
Expand All @@ -12,10 +13,14 @@ import {
TypeInfo,
visit,
visitWithTypeInfo,
GraphQLEnumType,
GraphQLInputObjectType,
} from 'graphql';
import { TypeScriptPluginConfig } from './config.js';
import { TsIntrospectionVisitor } from './introspection-visitor.js';
import { TsVisitor } from './visitor.js';
import { getCachedDocumentNodeFromSchema } from '@graphql-codegen/plugin-helpers';
import { getBaseTypeNode } from '@graphql-codegen/visitor-plugin-common';

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

const visitor = new TsVisitor(_schema, config);
let usedTypes = undefined;
if (config.onlyOperationTypes) {
usedTypes = getUsedTypeNames(schema, documents);
}

const visitor = new TsVisitor(_schema, config, { usedTypes });

const visitorResult = oldVisit(ast, { leave: visitor });
const introspectionDefinitions = includeIntrospectionTypesDefinitions(_schema, documents, config);
Expand Down Expand Up @@ -62,7 +72,7 @@ export function includeIntrospectionTypesDefinitions(
const typeInfo = new TypeInfo(schema);
const usedTypes: GraphQLNamedType[] = [];
const documentsVisitor = visitWithTypeInfo(typeInfo, {
Field() {
Field(node) {
const type = getNamedType(typeInfo.getType());

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

return result.definitions as any[];
}

export function getUsedTypeNames(schema: GraphQLSchema, documents: Types.DocumentFile[]): Set<string> {
if (!schema.astNode) {
const ast = getCachedDocumentNodeFromSchema(schema);
schema = buildASTSchema(ast);
}

const typeInfo = new TypeInfo(schema);

const visited = new Set<string>();
const queue: GraphQLNamedType[] = [];

function enqueue(type: GraphQLNamedType) {
if (
type.astNode && // skip scalars
!visited.has(type.name)
) {
visited.add(type.name);
queue.push(type);
}
}

const visitor = visitWithTypeInfo(typeInfo, {
VariableDefinition() {
const field = typeInfo.getInputType();
const type = getBaseType(field);
enqueue(type);
},
Field() {
const field = typeInfo.getFieldDef();
const type = getBaseType(field.type);
if (type instanceof GraphQLEnumType || type instanceof GraphQLInputObjectType) {
enqueue(type);
}
},
InputObjectTypeDefinition(node) {
for (const field of node.fields ?? []) {
const baseType = getBaseTypeNode(field.type);
const expanded = schema.getType(baseType.name.value);
if (expanded.name) {
enqueue(expanded);
}
}
},
});

for (const doc of documents) {
visit(doc.document, visitor);
}

const typeNames = new Set<string>();
while (true) {
const type = queue.pop();
if (!type) break;
typeNames.add(type.name);
visit(type.astNode, visitor);
}

return typeNames;
}
9 changes: 9 additions & 0 deletions packages/plugins/typescript/typescript/src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,15 @@ export class TsVisitor<
}

EnumTypeDefinition(node: EnumTypeDefinitionNode): string {
if (
this.config.onlyOperationTypes &&
!this.config.usedTypes?.has(
// types are wrong; string at runtime?
node.name as unknown as string
) &&
!this.config.onlyEnums
)
return '';
const enumName = node.name as any as string;

// In case of mapped external enum string
Expand Down
Loading