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 5, 2024
1 parent d3dc84d commit 9de0811
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 16 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,7 @@ export interface ParsedTypesConfig extends ParsedConfig {
wrapEntireDefinitions: boolean;
ignoreEnumValuesFromSchema: boolean;
directiveArgumentAndInputFieldMappings: ParsedDirectiveArgumentAndInputFieldMappings;
usedTypes?: Set<string>;
}

export interface RawTypesConfig extends RawConfig {
Expand Down Expand Up @@ -675,7 +676,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
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 9de0811

Please sign in to comment.