import { ASTKindToNode , NamedTypeNode , parse , printSchema , TypeNode , visit , VisitFn } from 'graphql' ; import casual from 'casual' ; import { PluginFunction } from '@graphql-codegen/plugin-helpers' ; import { pascalCase } from 'pascal-case' ; import { upperCase } from 'upper-case' ; import { sentenceCase } from 'sentence-case' ; import a from 'indefinite' ; type NamingConvention = 'upper-case#upperCase' | 'pascal-case#pascalCase' | 'keep' ; type Options < T = TypeNode > = { typeName : string ; fieldName : string ; types : TypeItem [ ] ; typenamesConvention : NamingConvention ; enumValuesConvention : NamingConvention ; terminateCircularRelationships : boolean ; prefix : string | undefined ; typesPrefix : string ; enumsPrefix : string ; currentType : T ; customScalars ? : ScalarMap ; transformUnderscore : boolean ; } ; const convertName = ( value , fn , transformUnderscore ) : string => { if ( transformUnderscore ) { return fn ( value ) ; } return value . split ( '_' ) . map ( ( s ) => fn ( s ) ) . join ( '_' ) ; } ; const createNameConverter = ( convention , transformUnderscore ) => ( value , prefix = '' ) => { switch ( convention ) { case 'upper-case#upperCase' : { return `${ prefix }${ convertName ( value , ( s ) => upperCase ( s || '' ) , transformUnderscore ) }` ; } case 'keep' : return `${ prefix }${ value }` ; case 'pascal-case#pascalCase' : default : return `${ prefix }${ convertName ( value , ( s ) => pascalCase ( s || '' ) , transformUnderscore ) }` ; } } ; const toMockName = ( typedName , casedName , prefix ? ) => { if ( prefix ) { return `${ prefix }${ casedName }` ; } const firstWord = sentenceCase ( typedName ) . split ( ' ' ) [ 0 ] ; return `${ a ( firstWord , { articleOnly : true } ) }${ casedName }` ; } ; const updateTextCase = ( str , enumValuesConvention , transformUnderscore ) => { const convert = createNameConverter ( enumValuesConvention , transformUnderscore ) ; if ( str . charAt ( 0 ) === '_' ) { return str . replace ( /^(_*)(.*)/ , ( _match , underscorePrefix , typeName ) => `${ underscorePrefix }${ convert ( typeName ) }` ) ; } return convert ( str ) ; } ; const hashedString = ( value ) => { let hash = 0 ; if ( value . length === 0 ) { return hash ; } for ( let i = 0 ; i < value . length ; i ++ ) { const char = value . charCodeAt ( i ) ; hash = ( hash << 5 ) - hash + char ; hash = hash & hash ; } return hash ; } ; const getScalarDefinition = ( value ) : ScalarDefinition => { if ( typeof value === 'string' ) { return { generator : value , arguments : [ ] , } ; } return value ; } ; const getNamedType = ( opts ) : string | number | boolean => { if ( ! opts . currentType ) { return '' ; } casual . seed ( hashedString ( opts . typeName + opts . fieldName ) ) ; const name = opts . currentType . name . value ; const casedName = createNameConverter ( opts . typenamesConvention , opts . transformUnderscore ) ( name ) ; switch ( name ) { case 'String' : return `'${ casual . word }'` ; case 'Float' : return Math . round ( casual . double ( 0 , 10 ) * 100 ) / 100 ; case 'ID' : return `'${ casual . uuid }'` ; case 'Boolean' : return casual . boolean ; case 'Int' : return casual . integer ( 0 , 9999 ) ; default : { const foundType = opts . types . find ( ( enumType ) => enumType . name === name ) ; if ( foundType ) { switch ( foundType . type ) { case 'enum' : { const typenameConverter = createNameConverter ( opts . typenamesConvention , opts . transformUnderscore ) ; const value = foundType . values ? foundType . values [ 0 ] : '' ; return `${ typenameConverter ( foundType . name , opts . enumsPrefix ) }.${ updateTextCase ( value , opts . enumValuesConvention , opts . transformUnderscore ) }` ; } case 'union' : return getNamedType ( { ... opts , currentType : foundType . types && foundType . types [ 0 ] , } ) ; case 'scalar' : { const customScalar = opts . customScalars ? getScalarDefinition ( opts . customScalars [ foundType . name ] ) : null ; if ( ! customScalar || ! customScalar . generator ) { if ( foundType . name === 'Date' ) { return `'${ new Date ( casual . unix_time ) . toISOString ( ) }'` ; } return `'${ casual . word }'` ; } const embeddedGenerator = casual [ customScalar . generator ] ; if ( ! embeddedGenerator && customScalar . generator ) { return customScalar . generator ; } const generatorArgs = Array . isArray ( customScalar . arguments ) ? customScalar . arguments : [ customScalar . arguments ] ; const value = typeof embeddedGenerator === 'function' ? embeddedGenerator ( ... generatorArgs ) : embeddedGenerator ; if ( typeof value === 'string' ) { return `'${ value }'` ; } if ( typeof value === 'object' ) { return `${ JSON . stringify ( value ) }` ; } return value ; } default : throw `foundType is unknown: ${ foundType . name }: ${ foundType . type }` ; } } if ( opts . terminateCircularRelationships ) { return `relationshipsToOmit.has('${ name }') ? {} as ${ name } : ${ toMockName ( name , casedName , opts . prefix ) }({}, relationshipsToOmit)` ; } else { return `${ toMockName ( name , casedName , opts . prefix ) }()` ; } } } } ; const generateMockValue = ( opts ) : string | number | boolean => { switch ( opts . currentType . kind ) { case 'NamedType' : return getNamedType ( { ... opts , currentType : opts . currentType as NamedTypeNode , } ) ; case 'NonNullType' : return generateMockValue ( { ... opts , currentType : opts . currentType . type , } ) ; case 'ListType' : { const value = generateMockValue ( { ... opts , currentType : opts . currentType . type , } ) ; return `[${ value }]` ; } } } ; const getMockString = ( typeName , fields , typenamesConvention , terminateCircularRelationships , addTypename = false , prefix , typesPrefix = '' , transformUnderscore ) => { const typenameConverter = createNameConverter ( typenamesConvention , transformUnderscore ) ; const casedName = typenameConverter ( typeName ) ; const casedNameWithPrefix = typenameConverter ( typeName , typesPrefix ) ; const typename = addTypename ? `\n        __typename: '${ typeName }',` : '' ; const typenameReturnType = addTypename ? `{ __typename: '${ typeName }' } & ` : '' ; if ( terminateCircularRelationships ) { return `
export const ${ toMockName ( typeName , casedName , prefix ) } = (overrides?: Partial<${ casedNameWithPrefix }>, relationshipsToOmit: Set<string> = new Set()): ${ typenameReturnType }${ casedNameWithPrefix } => {
    relationshipsToOmit.add('${ casedName }');
    return {${ typename }
${ fields }
    };
};` ; } else { return `
export const ${ toMockName ( typeName , casedName , prefix ) } = (overrides?: Partial<${ casedNameWithPrefix }>): ${ typenameReturnType }${ casedNameWithPrefix } => {
    return {${ typename }
${ fields }
    };
};` ; } } ; const getImportTypes = ( { typenamesConvention , definitions , types , typesFile , typesPrefix , enumsPrefix , transformUnderscore , } ) => { const typenameConverter = createNameConverter ( typenamesConvention , transformUnderscore ) ; const typeImports = typesPrefix ?. endsWith ( '.' ) ? [ typesPrefix . slice ( 0 , - 1 ) ] : definitions . filter ( ( { typeName } ) => ! ! typeName ) . map ( ( { typeName } ) => typenameConverter ( typeName , typesPrefix ) ) ; const enumTypes = enumsPrefix ?. endsWith ( '.' ) ? [ enumsPrefix . slice ( 0 , - 1 ) ] : types . filter ( ( { type } ) => type === 'enum' ) . map ( ( { name } ) => typenameConverter ( name , enumsPrefix ) ) ; typeImports . push ( ... enumTypes ) ; function onlyUnique ( value , index , self ) { return self . indexOf ( value ) === index ; } return typesFile ? `import { ${ typeImports . filter ( onlyUnique ) . join ( ', ' ) } } from '${ typesFile }';\n` : '' ; } ; type ScalarGeneratorName = keyof Casual . Casual | keyof Casual . functions | string ; type ScalarDefinition = { generator : ScalarGeneratorName ; arguments : unknown ; } ; type ScalarMap = { [ name ] : ScalarGeneratorName | ScalarDefinition ; } ; export interface TypescriptMocksPluginConfig { typesFile ? : string ; enumValues ? : NamingConvention ; typenames ? : NamingConvention ; addTypename ? : boolean ; prefix ? : string ; scalars ? : ScalarMap ; terminateCircularRelationships ? : boolean ; typesPrefix ? : string ; enumsPrefix ? : string ; transformUnderscore ? : boolean ; } interface TypeItem { name : string ; type : 'enum' | 'scalar' | 'union' ; values ? : string [ ] ; types ? : readonly NamedTypeNode [ ] ; } type VisitorType = { [ K in keyof ASTKindToNode ] ? : VisitFn < ASTKindToNode [ keyof ASTKindToNode ] , ASTKindToNode [ K ] > ; } ; export const plugin = ( schema , documents , config ) => { const printedSchema = printSchema ( schema ) ; const astNode = parse ( printedSchema ) ; const enumValuesConvention = config . enumValues || 'pascal-case#pascalCase' ; const typenamesConvention = config . typenames || 'pascal-case#pascalCase' ; const transformUnderscore = config . transformUnderscore ?? true ; const types = [ ] ; const visitor = { EnumTypeDefinition : ( node ) => { const name = node . name . value ; if ( ! types . find ( ( enumType ) => enumType . name === name ) ) { types . push ( { name , type : 'enum' , values : node . values ? node . values . map ( ( node ) => node . name . value ) : [ ] , } ) ; } } , UnionTypeDefinition : ( node ) => { const name = node . name . value ; if ( ! types . find ( ( enumType ) => enumType . name === name ) ) { types . push ( { name , type : 'union' , types : node . types , } ) ; } } , FieldDefinition : ( node ) => { const fieldName = node . name . value ; return { name : fieldName , mockFn : ( typeName ) => { const value = generateMockValue ( { typeName , fieldName , types , typenamesConvention , enumValuesConvention , terminateCircularRelationships : ! ! config . terminateCircularRelationships , prefix : config . prefix , typesPrefix : config . typesPrefix , enumsPrefix : config . enumsPrefix , currentType : node . type , customScalars : config . scalars , transformUnderscore , } ) ; return `        ${ fieldName }: overrides && overrides.hasOwnProperty('${ fieldName }') ? overrides.${ fieldName }! : ${ value },` ; } , } ; } , InputObjectTypeDefinition : ( node ) => { const fieldName = node . name . value ; return { typeName : fieldName , mockFn : ( ) => { const mockFields = node . fields ? node . fields . map ( ( field ) => { const value = generateMockValue ( { typeName : fieldName , fieldName : field . name . value , types , typenamesConvention , enumValuesConvention , terminateCircularRelationships : ! ! config . terminateCircularRelationships , prefix : config . prefix , typesPrefix : config . typesPrefix , enumsPrefix : config . enumsPrefix , currentType : field . type , customScalars : config . scalars , transformUnderscore , } ) ; return `        ${ field . name . value }: overrides && overrides.hasOwnProperty('${ field . name . value }') ? overrides.${ field . name . value }! : ${ value },` ; } ) . join ( '\n' ) : '' ; return getMockString ( fieldName , mockFields , typenamesConvention , ! ! config . terminateCircularRelationships , false , config . prefix , config . typesPrefix , transformUnderscore ) ; } , } ; } , ObjectTypeDefinition : ( node ) => { const typeName = node . name . value ; const { fields } = node ; return { typeName , mockFn : ( ) => { const mockFields = fields ? fields . map ( ( { mockFn } ) => mockFn ( typeName ) ) . join ( '\n' ) : '' ; return getMockString ( typeName , mockFields , typenamesConvention , ! ! config . terminateCircularRelationships , ! ! config . addTypename , config . prefix , config . typesPrefix , transformUnderscore ) ; } , } ; } , InterfaceTypeDefinition : ( node ) => { const typeName = node . name . value ; const { fields } = node ; return { typeName , mockFn : ( ) => { const mockFields = fields ? fields . map ( ( { mockFn } ) => mockFn ( typeName ) ) . join ( '\n' ) : '' ; return getMockString ( typeName , mockFields , typenamesConvention , ! ! config . terminateCircularRelationships , ! ! config . addTypename , config . prefix , config . typesPrefix , transformUnderscore ) ; } , } ; } , ScalarTypeDefinition : ( node ) => { const name = node . name . value ; if ( ! types . find ( ( enumType ) => enumType . name === name ) ) { types . push ( { name , type : 'scalar' , } ) ; } } , } ; const result = visit ( astNode , { leave : visitor } ) ; const definitions = result . definitions . filter ( ( definition ) => ! ! definition ) ; const typesFile = config . typesFile ? config . typesFile . replace ( /\.[\w]+$/ , '' ) : null ; const typesFileImport = getImportTypes ( { typenamesConvention , definitions , types , typesFile , typesPrefix : config . typesPrefix , enumsPrefix : config . enumsPrefix , transformUnderscore : transformUnderscore , } ) ; const mockFns = definitions . map ( ( { mockFn } ) => mockFn ) . filter ( ( mockFn ) => ! ! mockFn ) ; return `${ typesFileImport }${ mockFns . map ( ( mockFn ) => mockFn ( ) ) . join ( '\n' ) }
` ; } ;