@graphql-codegen/typescript
Version:
GraphQL Code Generator plugin for generating TypeScript types
291 lines (285 loc) • 13.1 kB
JavaScript
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