Last active
August 7, 2022 21:30
-
-
Save parkerself22/f9e77d0bddb017f59906dd9904d50bef to your computer and use it in GitHub Desktop.
Quick MVP of graphql-codegen plugin for Shopify hydrogen's useShopQuery
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ReactShopQueryVisitor } from './visitor.js'; | |
import { oldVisit, PluginFunction, PluginValidateFn, Types } from '@graphql-codegen/plugin-helpers'; | |
import { LoadedFragment, RawClientSideBasePluginConfig } from '@graphql-codegen/visitor-plugin-common'; | |
import { concatAST, FragmentDefinitionNode, GraphQLSchema, Kind } from 'graphql'; | |
import { extname } from 'path'; | |
export const plugin: PluginFunction<RawClientSideBasePluginConfig, Types.ComplexPluginOutput> = ( | |
schema: GraphQLSchema, | |
documents: Types.DocumentFile[], | |
config: RawClientSideBasePluginConfig | |
) => { | |
const allAst = concatAST(documents.map(v => v.document) as any); | |
const allFragments: LoadedFragment[] = [ | |
...(allAst.definitions.filter(d => d.kind === Kind.FRAGMENT_DEFINITION) as FragmentDefinitionNode[]).map( | |
fragmentDef => ({ | |
node: fragmentDef, | |
name: fragmentDef.name.value, | |
onType: fragmentDef.typeCondition.name.value, | |
isExternal: false | |
}) | |
), | |
...(config.externalFragments || []) | |
]; | |
const visitor = new ReactShopQueryVisitor(schema, allFragments, config, documents); | |
const visitorResult = oldVisit(allAst, { leave: visitor } as any); | |
return { | |
prepend: visitor.getImports(), | |
content: [visitor.fragments, ...visitorResult.definitions.filter((t: any) => typeof t === 'string')].join('\n') | |
}; | |
}; | |
export const validate: PluginValidateFn<any> = async ( | |
schema: GraphQLSchema, | |
documents: Types.DocumentFile[], | |
config: RawClientSideBasePluginConfig, | |
outputFile: string | |
) => { | |
if (extname(outputFile) !== '.ts' && extname(outputFile) !== '.tsx') { | |
throw new Error(`Plugin "shop-query-gql-codegen" requires extension to be ".ts" or ".tsx"!`); | |
} | |
}; | |
export { ReactShopQueryVisitor }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Types } from '@graphql-codegen/plugin-helpers'; | |
import { | |
ClientSideBasePluginConfig, | |
ClientSideBaseVisitor, | |
DocumentMode, | |
LoadedFragment, | |
RawClientSideBasePluginConfig | |
} from '@graphql-codegen/visitor-plugin-common'; | |
import autoBind from 'auto-bind'; | |
import { pascalCase } from 'change-case-all'; | |
import { GraphQLSchema, OperationDefinitionNode } from 'graphql'; | |
const QUERY_HOOK_NAME_IDENTIFIER = 'useShopQuery'; | |
const PARAMS_IDENTIFIER = 'UseShopQueryBaseParams'; | |
const paramsTypeDeclaration = `type ${PARAMS_IDENTIFIER} = Omit<Parameters<typeof useShopQuery>[0], 'variables' | 'query'>`; | |
export interface ReactApolloPluginConfig extends ClientSideBasePluginConfig { | |
hooksSuffix: string; | |
} | |
export class ReactShopQueryVisitor extends ClientSideBaseVisitor< | |
RawClientSideBasePluginConfig, | |
ReactApolloPluginConfig | |
> { | |
private _externalImportPrefix: string; | |
private imports = new Set<string>(); | |
constructor( | |
schema: GraphQLSchema, | |
fragments: LoadedFragment[], | |
protected rawConfig: RawClientSideBasePluginConfig, | |
documents: Types.DocumentFile[] | |
) { | |
super(schema, fragments, rawConfig, {}); | |
this._externalImportPrefix = this.config.importOperationTypesFrom | |
? `${this.config.importOperationTypesFrom}.` | |
: ''; | |
this._documents = documents; | |
autoBind(this); | |
this.imports.add(`import { ${QUERY_HOOK_NAME_IDENTIFIER}, gql } from '@shopify/hydrogen';`); | |
this.imports.add(`\n \n${paramsTypeDeclaration}\n`); | |
} | |
private getDocumentNodeVariable(node: OperationDefinitionNode, documentVariableName: string): string { | |
return this.config.documentMode === DocumentMode.external | |
? `Operations.${node.name?.value ?? ''}` | |
: documentVariableName; | |
} | |
public getImports(): string[] { | |
const hasOperations = this._collectedOperations.length > 0; | |
if (!hasOperations) { | |
return []; | |
} | |
return [...Array.from(this.imports)]; | |
} | |
private _buildHooks( | |
node: OperationDefinitionNode, | |
operationType: string, | |
documentVariableName: string, | |
operationResultType: string, | |
operationVariablesTypes: string, | |
hasRequiredVariables: boolean | |
): string { | |
const nodeName = node.name?.value ?? ''; | |
const operationName = pascalCase(`${nodeName.replace(operationType, '')}Shop${operationType}`); | |
const varParamsType = hasRequiredVariables | |
? ` & { variables: ${operationVariablesTypes} }` | |
: operationVariablesTypes.length > 0 | |
? `& { variables?: ${operationVariablesTypes} }` | |
: ``; | |
const optionsType = `${PARAMS_IDENTIFIER}${varParamsType}`; | |
const docNodeVariable = this.getDocumentNodeVariable(node, documentVariableName); | |
const paramStr = `options${hasRequiredVariables ? '' : '?'}: ${optionsType}`; | |
const hookFns = [ | |
'', | |
`export function use${operationName}(${paramStr}) { | |
return ${QUERY_HOOK_NAME_IDENTIFIER}<${operationResultType}>({ ...options, query: ${docNodeVariable} }); | |
}`, | |
'' | |
]; | |
return hookFns.join('\n'); | |
} | |
protected buildOperation( | |
node: OperationDefinitionNode, | |
documentVariableName: string, | |
operationType: string, | |
operationResultType: string, | |
operationVariablesTypes: string, | |
hasRequiredVariables: boolean | |
): string { | |
return this._buildHooks( | |
node, | |
operationType, | |
documentVariableName, | |
operationResultType, | |
operationVariablesTypes, | |
hasRequiredVariables | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment