UNPKG

@graphql-codegen/near-operation-file-preset

Version:

GraphQL Code Generator preset for generating operation code near the operation file

142 lines (141 loc) 6.63 kB
import { Kind, print } from 'graphql'; import { BaseVisitor, buildScalarsFromConfig, getConfigValue, getPossibleTypes, } from '@graphql-codegen/visitor-plugin-common'; import { analyzeFragmentUsage } from './utils.js'; /** * Creates fragment imports based on possible types and usage */ function createFragmentImports(baseVisitor, fragmentName, possibleTypes, usedTypes) { const fragmentImports = []; // Always include the document import fragmentImports.push({ name: baseVisitor.getFragmentVariableName(fragmentName), kind: 'document', }); const fragmentSuffix = baseVisitor.getFragmentSuffix(fragmentName); if (possibleTypes.length === 1) { fragmentImports.push({ name: baseVisitor.convertName(fragmentName, { useTypesPrefix: true, suffix: fragmentSuffix, }), kind: 'type', }); } else if (possibleTypes.length > 0) { const typesToImport = usedTypes && usedTypes.length > 0 ? usedTypes : possibleTypes; typesToImport.forEach(typeName => { fragmentImports.push({ name: baseVisitor.convertName(fragmentName, { useTypesPrefix: true, suffix: `_${typeName}` + (fragmentSuffix.length > 0 ? `_${fragmentSuffix}` : ''), }), kind: 'type', }); }); } return fragmentImports; } /** * Used by `buildFragmentResolver` to build a mapping of fragmentNames to paths, importNames, and other useful info */ function buildFragmentRegistry(baseVisitor, { generateFilePath }, { documents }, schemaObject) { const duplicateFragmentNames = []; const registry = documents.reduce((prev, documentRecord) => { const fragments = documentRecord.document.definitions.filter(d => d.kind === Kind.FRAGMENT_DEFINITION); for (const fragment of fragments) { const schemaType = schemaObject.getType(fragment.typeCondition.name.value); if (!schemaType) { throw new Error(`Fragment "${fragment.name.value}" is set on non-existing type "${fragment.typeCondition.name.value}"!`); } const fragmentName = fragment.name.value; const filePath = generateFilePath(documentRecord.location); const possibleTypes = getPossibleTypes(schemaObject, schemaType); const possibleTypeNames = possibleTypes.map(t => t.name); const imports = createFragmentImports(baseVisitor, fragment.name.value, possibleTypeNames); if (prev[fragmentName] && print(fragment) !== print(prev[fragmentName].node)) { duplicateFragmentNames.push(fragmentName); } prev[fragmentName] = { filePath, imports, onType: fragment.typeCondition.name.value, node: fragment, possibleTypes: possibleTypeNames, }; } return prev; }, {}); if (duplicateFragmentNames.length) { throw new Error(`Multiple fragments with the name(s) "${duplicateFragmentNames.join(', ')}" were found.`); } return registry; } /** * Creates a BaseVisitor with standard configuration */ function createBaseVisitor(config, schemaObject) { return new BaseVisitor(config, { scalars: buildScalarsFromConfig(schemaObject, config), dedupeOperationSuffix: getConfigValue(config.dedupeOperationSuffix, false), omitOperationSuffix: getConfigValue(config.omitOperationSuffix, false), fragmentVariablePrefix: getConfigValue(config.fragmentVariablePrefix, ''), fragmentVariableSuffix: getConfigValue(config.fragmentVariableSuffix, 'FragmentDoc'), }); } /** * Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements` */ export default function buildFragmentResolver(collectorOptions, presetOptions, schemaObject, dedupeFragments = false) { const { config } = presetOptions; const baseVisitor = createBaseVisitor(config, schemaObject); const fragmentRegistry = buildFragmentRegistry(baseVisitor, collectorOptions, presetOptions, schemaObject); const { baseOutputDir } = presetOptions; const { baseDir, typesImport } = collectorOptions; function resolveFragments(generatedFilePath, documentFileContent) { const { fragmentsInUse, usedFragmentTypes } = analyzeFragmentUsage(documentFileContent, fragmentRegistry, schemaObject); const externalFragments = []; const fragmentFileImports = {}; for (const [fragmentName, level] of Object.entries(fragmentsInUse)) { const fragmentDetails = fragmentRegistry[fragmentName]; if (!fragmentDetails) continue; // add top level references to the import object // we don't check or global namespace because the calling config can do so if (level === 0 || (dedupeFragments && ['OperationDefinition', 'FragmentDefinition'].includes(documentFileContent.definitions[0].kind))) { if (fragmentDetails.filePath !== generatedFilePath) { // don't emit imports to same location const usedTypesForFragment = usedFragmentTypes[fragmentName] || []; const filteredImports = createFragmentImports(baseVisitor, fragmentName, fragmentDetails.possibleTypes, usedTypesForFragment); if (!fragmentFileImports[fragmentDetails.filePath]) { fragmentFileImports[fragmentDetails.filePath] = []; } fragmentFileImports[fragmentDetails.filePath].push(...filteredImports); } } externalFragments.push({ level, isExternal: true, name: fragmentName, onType: fragmentDetails.onType, node: fragmentDetails.node, }); } return { externalFragments, fragmentImports: Object.entries(fragmentFileImports).map(([fragmentsFilePath, identifiers]) => ({ baseDir, baseOutputDir, outputPath: generatedFilePath, importSource: { path: fragmentsFilePath, identifiers, }, emitLegacyCommonJSImports: presetOptions.config.emitLegacyCommonJSImports, typesImport, })), }; } return resolveFragments; }