import { BaseVisitor , ParsedConfig , RawConfig } from './base-visitor' ; import autoBind from 'auto-bind' ; import { FragmentDefinitionNode , print , OperationDefinitionNode , FragmentSpreadNode , GraphQLSchema , Kind , } from 'graphql' ; import { DepGraph } from 'dependency-graph' ; import gqlTag from 'graphql-tag' ; import { oldVisit , Types } from '@graphql-codegen/plugin-helpers' ; import { getConfigValue , buildScalarsFromConfig } from './utils' ; import { LoadedFragment , ParsedImport } from './types' ; import { basename , extname } from 'path' ; import { pascalCase } from 'change-case-all' ; import { generateFragmentImportStatement } from './imports' ; import { optimizeDocumentNode } from '@graphql-tools/optimize' ; gqlTag . enableExperimentalFragmentVariables ( ) ; export enum DocumentMode { graphQLTag = 'graphQLTag' , documentNode = 'documentNode' , documentNodeImportFragments = 'documentNodeImportFragments' , external = 'external' , string = 'string' } const EXTENSIONS_TO_REMOVE = [ '.ts' , '.tsx' , '.js' , '.jsx' ] ; export interface RawClientSideBasePluginConfig extends RawConfig { description default noGraphQLTag ? : boolean ; default description exampleMarkdown gqlImport ? : string ; default description documentNodeImport ? : string ; default description noExport ? : boolean ; default description dedupeOperationSuffix ? : boolean ; default description omitOperationSuffix ? : boolean ; default description operationResultSuffix ? : string ; default description documentVariablePrefix ? : string ; default description documentVariableSuffix ? : string ; default description fragmentVariablePrefix ? : string ; default description fragmentVariableSuffix ? : string ; default description documentMode ? : DocumentMode ; default description optimizeDocumentNode ? : boolean ; default description importOperationTypesFrom ? : string ; default description exampleMarkdown importDocumentNodeExternallyFrom ? : string ; default description pureMagicComment ? : boolean ; default description experimentalFragmentVariables ? : boolean ; } export interface ClientSideBasePluginConfig extends ParsedConfig { gqlImport : string ; documentNodeImport : string ; operationResultSuffix : string ; dedupeOperationSuffix : boolean ; omitOperationSuffix : boolean ; noExport : boolean ; documentVariablePrefix : string ; documentVariableSuffix : string ; fragmentVariablePrefix : string ; fragmentVariableSuffix : string ; documentMode ? : DocumentMode ; importDocumentNodeExternallyFrom ? : 'near-operation-file' | string ; importOperationTypesFrom ? : string ; globalNamespace ? : boolean ; pureMagicComment ? : boolean ; optimizeDocumentNode : boolean ; experimentalFragmentVariables ? : boolean ; } export class ClientSideBaseVisitor < TRawConfig extends RawClientSideBasePluginConfig = RawClientSideBasePluginConfig , TPluginConfig extends ClientSideBasePluginConfig = ClientSideBasePluginConfig > extends BaseVisitor < TRawConfig , TPluginConfig > { protected _collectedOperations : OperationDefinitionNode [ ] = [ ] ; protected _documents : Types . DocumentFile [ ] = [ ] ; protected _additionalImports : string [ ] = [ ] ; protected _imports = new Set < string > ( ) ; constructor ( protected _schema , protected _fragments , rawConfig , additionalConfig , documents ? ) { super ( rawConfig , { scalars : buildScalarsFromConfig ( _schema , rawConfig ) , dedupeOperationSuffix : getConfigValue ( rawConfig . dedupeOperationSuffix , false ) , optimizeDocumentNode : getConfigValue ( rawConfig . optimizeDocumentNode , true ) , omitOperationSuffix : getConfigValue ( rawConfig . omitOperationSuffix , false ) , gqlImport : rawConfig . gqlImport || null , documentNodeImport : rawConfig . documentNodeImport || null , noExport : ! ! rawConfig . noExport , importOperationTypesFrom : getConfigValue ( rawConfig . importOperationTypesFrom , null ) , operationResultSuffix : getConfigValue ( rawConfig . operationResultSuffix , '' ) , documentVariablePrefix : getConfigValue ( rawConfig . documentVariablePrefix , '' ) , documentVariableSuffix : getConfigValue ( rawConfig . documentVariableSuffix , 'Document' ) , fragmentVariablePrefix : getConfigValue ( rawConfig . fragmentVariablePrefix , '' ) , fragmentVariableSuffix : getConfigValue ( rawConfig . fragmentVariableSuffix , 'FragmentDoc' ) , documentMode : ( ( rawConfig ) => { if ( typeof rawConfig . noGraphQLTag === 'boolean' ) { return rawConfig . noGraphQLTag ? DocumentMode . documentNode : DocumentMode . graphQLTag ; } return getConfigValue ( rawConfig . documentMode , DocumentMode . graphQLTag ) ; } ) ( rawConfig ) , importDocumentNodeExternallyFrom : getConfigValue ( rawConfig . importDocumentNodeExternallyFrom , '' ) , pureMagicComment : getConfigValue ( rawConfig . pureMagicComment , false ) , experimentalFragmentVariables : getConfigValue ( rawConfig . experimentalFragmentVariables , false ) , ... additionalConfig , } as any ) ; this . _documents = documents ; autoBind ( this ) ; } protected _extractFragments ( document , withNested = false ) { if ( ! document ) { return [ ] ; } const names = new Set ( ) ; oldVisit ( document , { enter : { FragmentSpread : ( node ) => { names . add ( node . name . value ) ; if ( withNested ) { const foundFragment = this . _fragments . find ( f => f . name === node . name . value ) ; if ( foundFragment ) { const childItems = this . _extractFragments ( foundFragment . node , true ) ; if ( childItems && childItems . length > 0 ) { for ( const item of childItems ) { names . add ( item ) ; } } } } } , } , } ) ; return Array . from ( names ) ; } protected _transformFragments ( document ) { const includeNestedFragments = this . config . documentMode === DocumentMode . documentNode || ( this . config . dedupeFragments && document . kind === 'OperationDefinition' ) ; return this . _extractFragments ( document , includeNestedFragments ) . map ( document => this . getFragmentVariableName ( document ) ) ; } protected _includeFragments ( fragments , nodeKind ) { if ( fragments && fragments . length > 0 ) { if ( this . config . documentMode === DocumentMode . documentNode ) { return this . _fragments . filter ( f => fragments . includes ( this . getFragmentVariableName ( f . name ) ) ) . map ( fragment => print ( fragment . node ) ) . join ( '\n' ) ; } else if ( this . config . documentMode === DocumentMode . documentNodeImportFragments ) { return '' ; } else { if ( this . config . dedupeFragments && nodeKind !== 'OperationDefinition' ) { return '' ; } return `${ fragments . map ( name => '${' + name + '}' ) . join ( '\n' ) }` ; } } return '' ; } protected _prepareDocument ( documentStr ) { return documentStr ; } protected _gql ( node ) { const fragments = this . _transformFragments ( node ) ; const doc = this . _prepareDocument ( `
    ${ print ( node ) . split ( '\\' ) . join ( '\\\\' ) }
    ${ this . _includeFragments ( fragments , node . kind ) }` ) ; if ( this . config . documentMode === DocumentMode . documentNode ) { let gqlObj = gqlTag ( [ doc ] ) ; if ( this . config . optimizeDocumentNode ) { gqlObj = optimizeDocumentNode ( gqlObj ) ; } return JSON . stringify ( gqlObj ) ; } else if ( this . config . documentMode === DocumentMode . documentNodeImportFragments ) { let gqlObj = gqlTag ( [ doc ] ) ; if ( this . config . optimizeDocumentNode ) { gqlObj = optimizeDocumentNode ( gqlObj ) ; } if ( fragments . length > 0 && ( ! this . config . dedupeFragments || node . kind === 'OperationDefinition' ) ) { const definitions = [ ... gqlObj . definitions . map ( t => JSON . stringify ( t ) ) , ... fragments . map ( name => `...${ name }.definitions` ) , ] . join ( ) ; return `{"kind":"${ Kind . DOCUMENT }","definitions":[${ definitions }]}` ; } return JSON . stringify ( gqlObj ) ; } else if ( this . config . documentMode === DocumentMode . string ) { return '`' + doc + '`' ; } const gqlImport = this . _parseImport ( this . config . gqlImport || 'graphql-tag' ) ; return ( gqlImport . propName || 'gql' ) + '`' + doc + '`' ; } protected _generateFragment ( fragmentDocument ) { const name = this . getFragmentVariableName ( fragmentDocument ) ; const fragmentTypeSuffix = this . getFragmentSuffix ( fragmentDocument ) ; return `export const ${ name } =${ this . config . pureMagicComment ? ' /*#__PURE__*/' : '' } ${ this . _gql ( fragmentDocument ) }${ this . getDocumentNodeSignature ( this . convertName ( fragmentDocument . name . value , { useTypesPrefix : true , suffix : fragmentTypeSuffix , } ) , this . config . experimentalFragmentVariables ? this . convertName ( fragmentDocument . name . value , { suffix : fragmentTypeSuffix + 'Variables' , } ) : 'unknown' , fragmentDocument ) };` ; } private get fragmentsGraph ( ) : DepGraph < LoadedFragment > { const graph = new DepGraph < LoadedFragment > ( { circular : true } ) ; for ( const fragment of this . _fragments ) { if ( graph . hasNode ( fragment . name ) ) { const cachedAsString = print ( graph . getNodeData ( fragment . name ) . node ) ; const asString = print ( fragment . node ) ; if ( cachedAsString !== asString ) { throw new Error ( `Duplicated fragment called '${ fragment . name }'!` ) ; } } graph . addNode ( fragment . name , fragment ) ; } this . _fragments . forEach ( fragment => { const depends = this . _extractFragments ( fragment . node ) ; if ( depends && depends . length > 0 ) { depends . forEach ( name => { graph . addDependency ( fragment . name , name ) ; } ) ; } } ) ; return graph ; } public get fragments ( ) : string { if ( this . _fragments . length === 0 || this . config . documentMode === DocumentMode . external ) { return '' ; } const graph = this . fragmentsGraph ; const orderedDeps = graph . overallOrder ( ) ; const localFragments = orderedDeps . filter ( name => ! graph . getNodeData ( name ) . isExternal ) . map ( name => this . _generateFragment ( graph . getNodeData ( name ) . node ) ) ; return localFragments . join ( '\n' ) ; } protected _parseImport ( importStr ) { if ( importStr === 'gql' ) { return { moduleName : null , propName : 'gql' , } ; } if ( importStr . includes ( '.gql' ) ) { return { moduleName : null , propName : importStr , } ; } const [ moduleName , propName ] = importStr . split ( '#' ) ; return { moduleName , propName , } ; } protected _generateImport ( { moduleName , propName } , varName , isTypeImport ) { const typeImport = isTypeImport && this . config . useTypeImports ? 'import type' : 'import' ; const propAlias = propName === varName ? '' : ` as ${ varName }` ; if ( moduleName ) { return `${ typeImport } ${ propName ? `{ ${ propName }${ propAlias } }` : varName } from '${ moduleName }';` ; } return null ; } private clearExtension ( path ) { const extension = extname ( path ) ; if ( EXTENSIONS_TO_REMOVE . includes ( extension ) ) { return path . replace ( /\.[^/.]+$/ , '' ) ; } return path ; } public getImports ( options = { } ) { ( this . _additionalImports || [ ] ) . forEach ( i => this . _imports . add ( i ) ) ; switch ( this . config . documentMode ) { case DocumentMode . documentNode : case DocumentMode . documentNodeImportFragments : { const documentNodeImport = this . _parseImport ( this . config . documentNodeImport || 'graphql#DocumentNode' ) ; const tagImport = this . _generateImport ( documentNodeImport , 'DocumentNode' , true ) ; if ( tagImport ) { this . _imports . add ( tagImport ) ; } break ; } case DocumentMode . graphQLTag : { const gqlImport = this . _parseImport ( this . config . gqlImport || 'graphql-tag' ) ; const tagImport = this . _generateImport ( gqlImport , 'gql' , false ) ; if ( tagImport ) { this . _imports . add ( tagImport ) ; } break ; } case DocumentMode . external : { if ( this . _collectedOperations . length > 0 ) { if ( this . config . importDocumentNodeExternallyFrom === 'near-operation-file' && this . _documents . length === 1 ) { this . _imports . add ( `import * as Operations from './${ this . clearExtension ( basename ( this . _documents [ 0 ] . location ) ) }';` ) ; } else { if ( ! this . config . importDocumentNodeExternallyFrom ) { console . warn ( 'importDocumentNodeExternallyFrom must be provided if documentMode=external' ) ; } this . _imports . add ( `import * as Operations from '${ this . clearExtension ( this . config . importDocumentNodeExternallyFrom ) }';` ) ; } } break ; } default : break ; } if ( ! options . excludeFragments && ! this . config . globalNamespace ) { const { documentMode , fragmentImports } = this . config ; if ( documentMode === DocumentMode . graphQLTag || documentMode === DocumentMode . string || documentMode === DocumentMode . documentNodeImportFragments ) { const alreadyImported = new Map < string , Set < string > > ( ) ; const deduplicatedImports = fragmentImports . map ( fragmentImport => { const { path , identifiers } = fragmentImport . importSource ; if ( ! alreadyImported . has ( path ) ) { alreadyImported . set ( path , new Set < string > ( ) ) ; } const alreadyImportedForPath = alreadyImported . get ( path ) ; const newIdentifiers = identifiers . filter ( identifier => ! alreadyImportedForPath . has ( identifier . name ) ) ; newIdentifiers . forEach ( newIdentifier => alreadyImportedForPath . add ( newIdentifier . name ) ) ; return { ... fragmentImport , importSource : { ... fragmentImport . importSource , identifiers : newIdentifiers , } , } ; } ) . filter ( fragmentImport => fragmentImport . importSource . identifiers . length > 0 ) ; deduplicatedImports . forEach ( fragmentImport => { if ( fragmentImport . outputPath !== fragmentImport . importSource . path ) { this . _imports . add ( generateFragmentImportStatement ( fragmentImport , 'document' ) ) ; } } ) ; } } return Array . from ( this . _imports ) ; } protected buildOperation ( _node , _documentVariableName , _operationType , _operationResultType , _operationVariablesTypes , _hasRequiredVariables ) { return null ; } protected getDocumentNodeSignature ( _resultType , _variablesTypes , _node ) { if ( this . config . documentMode === DocumentMode . documentNode || this . config . documentMode === DocumentMode . documentNodeImportFragments ) { return ` as unknown as DocumentNode` ; } return '' ; } param node protected checkVariablesRequirements ( node ) { const variables = node . variableDefinitions || [ ] ; if ( variables . length === 0 ) { return false ; } return variables . some ( variableDef => variableDef . type . kind === Kind . NON_NULL_TYPE && ! variableDef . defaultValue ) ; } public getOperationVariableName ( node ) { return this . convertName ( node , { suffix : this . config . documentVariableSuffix , prefix : this . config . documentVariablePrefix , useTypesPrefix : false , } ) ; } public OperationDefinition ( node ) { this . _collectedOperations . push ( node ) ; const documentVariableName = this . getOperationVariableName ( node ) ; const operationType = pascalCase ( node . operation ) ; const operationTypeSuffix = this . getOperationSuffix ( node , operationType ) ; const operationResultType = this . convertName ( node , { suffix : operationTypeSuffix + this . _parsedConfig . operationResultSuffix , } ) ; const operationVariablesTypes = this . convertName ( node , { suffix : operationTypeSuffix + 'Variables' , } ) ; let documentString = '' ; if ( this . config . documentMode !== DocumentMode . external ) { if ( documentVariableName !== '' ) { documentString = `${ this . config . noExport ? '' : 'export' } const ${ documentVariableName } =${ this . config . pureMagicComment ? ' /*#__PURE__*/' : '' } ${ this . _gql ( node ) }${ this . getDocumentNodeSignature ( operationResultType , operationVariablesTypes , node ) };` ; } } const hasRequiredVariables = this . checkVariablesRequirements ( node ) ; const additional = this . buildOperation ( node , documentVariableName , operationType , operationResultType , operationVariablesTypes , hasRequiredVariables ) ; return [ documentString , additional ] . filter ( a => a ) . join ( '\n' ) ; } }