Skip to content

Commit

Permalink
feat: omit unused input/enum types when onlyOperationTypes is enabled
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanregner committed Oct 7, 2024
1 parent 3fd4486 commit 577a68d
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 21 deletions.
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

0 comments on commit 577a68d

Please sign in to comment.