Skip to content

Instantly share code, notes, and snippets.

@parkerself22
Last active August 7, 2022 21:30
Show Gist options
  • Save parkerself22/f9e77d0bddb017f59906dd9904d50bef to your computer and use it in GitHub Desktop.
Save parkerself22/f9e77d0bddb017f59906dd9904d50bef to your computer and use it in GitHub Desktop.
Quick MVP of graphql-codegen plugin for Shopify hydrogen's useShopQuery
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 };
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