@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
JavaScript
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;
}