UNPKG

@graphql-codegen/typescript

Version:

GraphQL Code Generator plugin for generating TypeScript types

291 lines (285 loc) • 13.1 kB
import { Kind, isEnumType, printSchema, parse, visit, TypeInfo, visitWithTypeInfo, getNamedType, isIntrospectionType, printIntrospectionSchema, isObjectType } from 'graphql'; import { OperationVariablesToObject, BaseTypesVisitor, getConfigValue, normalizeAvoidOptionals, indent, transformComment, wrapWithSingleQuotes, DeclarationBlock } from '@graphql-codegen/visitor-plugin-common'; import autoBind from 'auto-bind'; class TypeScriptOperationVariablesToObject extends OperationVariablesToObject { constructor(_scalars, _convertName, _avoidOptionals, _immutableTypes, _namespacedImportName = null, _enumNames = [], _enumPrefix = true, _enumValues = {}) { super(_scalars, _convertName, _namespacedImportName, _enumNames, _enumPrefix, _enumValues); this._avoidOptionals = _avoidOptionals; this._immutableTypes = _immutableTypes; } clearOptional(str) { const prefix = this._namespacedImportName ? `${this._namespacedImportName}.` : ''; const rgx = new RegExp(`^${prefix}Maybe<(.*?)>$`, 'i'); if (str.startsWith(`${this._namespacedImportName ? `${this._namespacedImportName}.` : ''}Maybe`)) { return str.replace(rgx, '$1'); } return str; } wrapAstTypeWithModifiers(baseType, typeNode) { const prefix = this._namespacedImportName ? `${this._namespacedImportName}.` : ''; if (typeNode.kind === Kind.NON_NULL_TYPE) { const type = this.wrapAstTypeWithModifiers(baseType, typeNode.type); return this.clearOptional(type); } else if (typeNode.kind === Kind.LIST_TYPE) { const innerType = this.wrapAstTypeWithModifiers(baseType, typeNode.type); return `${prefix}Maybe<${this._immutableTypes ? 'ReadonlyArray' : 'Array'}<${innerType}>>`; } else { return `${prefix}Maybe<${baseType}>`; } } formatFieldString(fieldName, isNonNullType, hasDefaultValue) { if (!hasDefaultValue && (this._avoidOptionals || isNonNullType)) { return fieldName; } return `${fieldName}?`; } formatTypeString(fieldType, isNonNullType, hasDefaultValue) { if (!hasDefaultValue && isNonNullType) { return this.clearOptional(fieldType); } return fieldType; } getPunctuation() { return ';'; } } class TsVisitor extends BaseTypesVisitor { constructor(schema, pluginConfig, additionalConfig = {}) { super(schema, pluginConfig, { noExport: getConfigValue(pluginConfig.noExport, false), avoidOptionals: normalizeAvoidOptionals(getConfigValue(pluginConfig.avoidOptionals, false)), maybeValue: getConfigValue(pluginConfig.maybeValue, 'T | null'), constEnums: getConfigValue(pluginConfig.constEnums, false), enumsAsTypes: getConfigValue(pluginConfig.enumsAsTypes, false), futureProofEnums: getConfigValue(pluginConfig.futureProofEnums, false), enumsAsConst: getConfigValue(pluginConfig.enumsAsConst, false), numericEnums: getConfigValue(pluginConfig.numericEnums, false), onlyOperationTypes: getConfigValue(pluginConfig.onlyOperationTypes, false), immutableTypes: getConfigValue(pluginConfig.immutableTypes, false), ...(additionalConfig || {}), }); autoBind(this); const enumNames = Object.values(schema.getTypeMap()) .filter(isEnumType) .map(type => type.name); this.setArgumentsTransformer(new TypeScriptOperationVariablesToObject(this.scalars, this.convertName, this.config.avoidOptionals.object, this.config.immutableTypes, null, enumNames, pluginConfig.enumPrefix, this.config.enumValues)); this.setDeclarationBlockConfig({ enumNameValueSeparator: ' =', ignoreExport: this.config.noExport, }); } getWrapperDefinitions() { const definitions = [this.getMaybeValue()]; if (this.config.wrapFieldDefinitions) { definitions.push(this.getFieldWrapperValue()); } return definitions; } getMaybeValue() { return `${this.getExportPrefix()}type Maybe<T> = ${this.config.maybeValue};`; } clearOptional(str) { if (str.startsWith('Maybe')) { return str.replace(/Maybe<(.*?)>$/, '$1'); } return str; } getExportPrefix() { if (this.config.noExport) { return ''; } return super.getExportPrefix(); } NamedType(node, key, parent, path, ancestors) { return `Maybe<${super.NamedType(node, key, parent, path, ancestors)}>`; } ListType(node) { return `Maybe<${super.ListType(node)}>`; } wrapWithListType(str) { return `${this.config.immutableTypes ? 'ReadonlyArray' : 'Array'}<${str}>`; } NonNullType(node) { const baseValue = super.NonNullType(node); return this.clearOptional(baseValue); } FieldDefinition(node, key, parent) { const typeString = node.type; const originalFieldNode = parent[key]; const addOptionalSign = !this.config.avoidOptionals.field && originalFieldNode.type.kind !== Kind.NON_NULL_TYPE; const comment = this.getFieldComment(node); const { type } = this.config.declarationKind; return (comment + indent(`${this.config.immutableTypes ? 'readonly ' : ''}${node.name}${addOptionalSign ? '?' : ''}: ${typeString}${this.getPunctuation(type)}`)); } InputValueDefinition(node, key, parent) { const originalFieldNode = parent[key]; const addOptionalSign = !this.config.avoidOptionals.inputValue && originalFieldNode.type.kind !== Kind.NON_NULL_TYPE; const comment = transformComment(node.description, 1); const { type } = this.config.declarationKind; return (comment + indent(`${this.config.immutableTypes ? 'readonly ' : ''}${node.name}${addOptionalSign ? '?' : ''}: ${node.type}${this.getPunctuation(type)}`)); } EnumTypeDefinition(node) { const enumName = node.name; // In case of mapped external enum string if (this.config.enumValues[enumName] && this.config.enumValues[enumName].sourceFile) { return `export { ${this.config.enumValues[enumName].typeIdentifier} };\n`; } const getValueFromConfig = (enumValue) => { if (this.config.enumValues[enumName] && this.config.enumValues[enumName].mappedValues && typeof this.config.enumValues[enumName].mappedValues[enumValue] !== 'undefined') { return this.config.enumValues[enumName].mappedValues[enumValue]; } return null; }; const withFutureAddedValue = [ this.config.futureProofEnums ? [indent('| ' + wrapWithSingleQuotes('%future added value'))] : [], ]; const enumTypeName = this.convertName(node, { useTypesPrefix: this.config.enumPrefix }); if (this.config.enumsAsTypes) { return new DeclarationBlock(this._declarationBlockConfig) .export() .asKind('type') .withComment(node.description) .withName(enumTypeName) .withContent('\n' + node.values .map(enumOption => { const name = enumOption.name; const enumValue = getValueFromConfig(name) || name; const comment = transformComment(enumOption.description, 1); return comment + indent('| ' + wrapWithSingleQuotes(enumValue)); }) .concat(...withFutureAddedValue) .join('\n')).string; } if (this.config.numericEnums) { const block = new DeclarationBlock(this._declarationBlockConfig) .export() .withComment(node.description) .withName(enumTypeName) .asKind('enum') .withBlock(node.values .map((enumOption, i) => { const valueFromConfig = getValueFromConfig(enumOption.name); const enumValue = valueFromConfig || i; const comment = transformComment(enumOption.description, 1); return comment + indent(enumOption.name) + ` = ${enumValue}`; }) .concat(...withFutureAddedValue) .join(',\n')).string; return block; } if (this.config.enumsAsConst) { const typeName = `export type ${enumTypeName} = typeof ${enumTypeName}[keyof typeof ${enumTypeName}];`; const enumAsConst = new DeclarationBlock({ ...this._declarationBlockConfig, blockTransformer: block => { return block + ' as const'; }, }) .export() .asKind('const') .withName(enumTypeName) .withComment(node.description) .withBlock(node.values .map(enumOption => { const optionName = this.convertName(enumOption, { useTypesPrefix: false, transformUnderscore: true }); const comment = transformComment(enumOption.description, 1); const name = enumOption.name; const enumValue = getValueFromConfig(name) || name; return comment + indent(`${optionName}: ${wrapWithSingleQuotes(enumValue)}`); }) .join(',\n')).string; return [enumAsConst, typeName].join('\n'); } return new DeclarationBlock(this._declarationBlockConfig) .export() .asKind(this.config.constEnums ? 'const enum' : 'enum') .withName(enumTypeName) .withComment(node.description) .withBlock(this.buildEnumValuesBlock(enumName, node.values)).string; } getPunctuation(declarationKind) { return ';'; } } class TsIntrospectionVisitor extends TsVisitor { constructor(schema, pluginConfig = {}, typesToInclude) { super(schema, pluginConfig); this.typesToInclude = []; this.typesToInclude = typesToInclude; autoBind(this); } DirectiveDefinition() { return null; } ObjectTypeDefinition(node, key, parent) { const name = node.name; if (this.typesToInclude.some(type => type.name === name)) { return super.ObjectTypeDefinition(node, key, parent); } return null; } EnumTypeDefinition(node) { const name = node.name; if (this.typesToInclude.some(type => type.name === name)) { return super.EnumTypeDefinition(node); } return null; } } const plugin = (schema, documents, config) => { const visitor = new TsVisitor(schema, config); const printedSchema = printSchema(schema); const astNode = parse(printedSchema); const visitorResult = visit(astNode, { leave: visitor }); const introspectionDefinitions = includeIntrospectionDefinitions(schema, documents, config); const scalars = visitor.scalarsDefinition; return { prepend: [...visitor.getEnumsImports(), ...visitor.getScalarsImports(), ...visitor.getWrapperDefinitions()], content: [scalars, ...visitorResult.definitions, ...introspectionDefinitions].join('\n'), }; }; function includeIntrospectionDefinitions(schema, documents, config) { const typeInfo = new TypeInfo(schema); const usedTypes = []; const documentsVisitor = visitWithTypeInfo(typeInfo, { Field() { const type = getNamedType(typeInfo.getType()); if (isIntrospectionType(type) && !usedTypes.includes(type)) { usedTypes.push(type); } }, }); documents.forEach(doc => visit(doc.document, documentsVisitor)); const typesToInclude = []; usedTypes.forEach(type => { collectTypes(type); }); const visitor = new TsIntrospectionVisitor(schema, config, typesToInclude); const result = visit(parse(printIntrospectionSchema(schema)), { leave: visitor }); // recursively go through each `usedTypes` and their children and collect all used types // we don't care about Interfaces, Unions and others, but Objects and Enums function collectTypes(type) { if (typesToInclude.includes(type)) { return; } typesToInclude.push(type); if (isObjectType(type)) { const fields = type.getFields(); Object.keys(fields).forEach(key => { const field = fields[key]; const type = getNamedType(field.type); collectTypes(type); }); } } return result.definitions; } export { TsIntrospectionVisitor, TsVisitor, TypeScriptOperationVariablesToObject, includeIntrospectionDefinitions, plugin }; //# sourceMappingURL=index.esm.js.map