@graphql-codegen/core
Version:
<p align="center"> <img src="https://github.com/dotansimha/graphql-code-generator/blob/master/logo.png?raw=true" /> </p>
295 lines (288 loc) • 11.9 kB
JavaScript
import { DetailedError, getCachedDocumentNodeFromSchema, isComplexPluginOutput, federationSpec } from '@graphql-codegen/plugin-helpers';
import { buildASTSchema, isSchema, Kind, specifiedRules, visit, print } from 'graphql';
import { isDocumentNode, asArray, validateGraphQlDocuments, checkValidationErrors } from '@graphql-tools/utils';
import { mergeSchemas } from '@graphql-tools/schema';
async function executePlugin(options, plugin) {
if (!plugin || !plugin.plugin || typeof plugin.plugin !== 'function') {
throw new DetailedError(`Invalid Custom Plugin "${options.name}"`, `
Plugin ${options.name} does not export a valid JS object with "plugin" function.
Make sure your custom plugin is written in the following form:
module.exports = {
plugin: (schema, documents, config) => {
return 'my-custom-plugin-content';
},
};
`);
}
const outputSchema = options.schemaAst || buildASTSchema(options.schema, options.config);
const documents = options.documents || [];
const pluginContext = options.pluginContext || {};
if (plugin.validate && typeof plugin.validate === 'function') {
try {
// FIXME: Sync validate signature with plugin signature
await plugin.validate(outputSchema, documents, options.config, options.outputFilename, options.allPlugins, pluginContext);
}
catch (e) {
throw new DetailedError(`Plugin "${options.name}" validation failed:`, `
${e.message}
`);
}
}
return Promise.resolve(plugin.plugin(outputSchema, documents, typeof options.config === 'object' ? { ...options.config } : options.config, {
outputFile: options.outputFilename,
allPlugins: options.allPlugins,
pluginContext,
}));
}
function isObjectMap(obj) {
return obj && typeof obj === 'object' && !Array.isArray(obj);
}
function prioritize(...values) {
const picked = values.find(val => typeof val === 'boolean');
if (typeof picked !== 'boolean') {
return values[values.length - 1];
}
return picked;
}
function pickFlag(flag, config) {
return isObjectMap(config) ? config[flag] : undefined;
}
function shouldValidateDuplicateDocuments(skipDocumentsValidationOption) {
// If the value is true, skip all
if (skipDocumentsValidationOption === true) {
return false;
}
// If the value is object with the specific flag, only skip this one
if (typeof skipDocumentsValidationOption === 'object' && skipDocumentsValidationOption.skipDuplicateValidation) {
return false;
}
// If the value is falsy or the specific flag is not set, validate
return true;
}
function shouldValidateDocumentsAgainstSchema(skipDocumentsValidationOption) {
// If the value is true, skip all
if (skipDocumentsValidationOption === true) {
return false;
}
// If the value is object with the specific flag, only skip this one
if (typeof skipDocumentsValidationOption === 'object' && skipDocumentsValidationOption.skipValidationAgainstSchema) {
return false;
}
// If the value is falsy or the specific flag is not set, validate
return true;
}
function getSkipDocumentsValidationOption(options) {
// If the value is set on the root level
if (options.skipDocumentsValidation) {
return options.skipDocumentsValidation;
}
// If the value is set under `config` property
const flagFromConfig = pickFlag('skipDocumentsValidation', options.config);
if (flagFromConfig) {
return flagFromConfig;
}
return false;
}
const federationDirectives = ['key', 'requires', 'provides', 'external'];
function hasFederationSpec(schemaOrAST) {
if (isSchema(schemaOrAST)) {
return federationDirectives.some(directive => schemaOrAST.getDirective(directive));
}
else if (isDocumentNode(schemaOrAST)) {
return schemaOrAST.definitions.some(def => def.kind === Kind.DIRECTIVE_DEFINITION && federationDirectives.includes(def.name.value));
}
return false;
}
async function codegen(options) {
const documents = options.documents || [];
const skipDocumentsValidation = getSkipDocumentsValidationOption(options);
if (documents.length > 0 && shouldValidateDuplicateDocuments(skipDocumentsValidation)) {
validateDuplicateDocuments(documents);
}
const pluginPackages = Object.keys(options.pluginMap).map(key => options.pluginMap[key]);
// merged schema with parts added by plugins
const additionalTypeDefs = [];
for (const plugin of pluginPackages) {
const addToSchema = typeof plugin.addToSchema === 'function' ? plugin.addToSchema(options.config) : plugin.addToSchema;
if (addToSchema) {
additionalTypeDefs.push(addToSchema);
}
}
const federationInConfig = pickFlag('federation', options.config);
const isFederation = prioritize(federationInConfig, false);
if (isFederation && !hasFederationSpec(options.schemaAst || options.schema)) {
additionalTypeDefs.push(federationSpec);
}
// Use mergeSchemas, only if there is no GraphQLSchema provided or the schema should be extended
const mergeNeeded = !options.schemaAst || additionalTypeDefs.length > 0;
const schemaInstance = mergeNeeded
? mergeSchemas({
// If GraphQLSchema provided, use it
schemas: options.schemaAst ? [options.schemaAst] : [],
// If GraphQLSchema isn't provided but DocumentNode is, use it to get the final GraphQLSchema
typeDefs: options.schemaAst ? additionalTypeDefs : [options.schema, ...additionalTypeDefs],
convertExtensions: true,
assumeValid: true,
assumeValidSDL: true,
...options.config,
})
: options.schemaAst;
const schemaDocumentNode = mergeNeeded || !options.schema ? getCachedDocumentNodeFromSchema(schemaInstance) : options.schema;
if (schemaInstance && documents.length > 0 && shouldValidateDocumentsAgainstSchema(skipDocumentsValidation)) {
const ignored = ['NoUnusedFragments', 'NoUnusedVariables', 'KnownDirectives'];
if (typeof skipDocumentsValidation === 'object' && skipDocumentsValidation.ignoreRules) {
ignored.push(...asArray(skipDocumentsValidation.ignoreRules));
}
const extraFragments = pickFlag('externalFragments', options.config) || [];
const errors = await validateGraphQlDocuments(schemaInstance, [
...documents,
...extraFragments.map(f => ({
location: f.importFrom,
document: { kind: Kind.DOCUMENT, definitions: [f.node] },
})),
], specifiedRules.filter(rule => !ignored.some(ignoredRule => rule.name.startsWith(ignoredRule))));
checkValidationErrors(errors);
}
const prepend = new Set();
const append = new Set();
const output = await Promise.all(options.plugins.map(async (plugin) => {
const name = Object.keys(plugin)[0];
const pluginPackage = options.pluginMap[name];
const pluginConfig = plugin[name] || {};
const execConfig = typeof pluginConfig !== 'object'
? pluginConfig
: {
...options.config,
...pluginConfig,
};
const result = await executePlugin({
name,
config: execConfig,
parentConfig: options.config,
schema: schemaDocumentNode,
schemaAst: schemaInstance,
documents: options.documents,
outputFilename: options.filename,
allPlugins: options.plugins,
skipDocumentsValidation: options.skipDocumentsValidation,
pluginContext: options.pluginContext,
}, pluginPackage);
if (typeof result === 'string') {
return result || '';
}
else if (isComplexPluginOutput(result)) {
if (result.append && result.append.length > 0) {
for (const item of result.append) {
if (item) {
append.add(item);
}
}
}
if (result.prepend && result.prepend.length > 0) {
for (const item of result.prepend) {
if (item) {
prepend.add(item);
}
}
}
return result.content || '';
}
return '';
}));
return [...sortPrependValues(Array.from(prepend.values())), ...output, ...Array.from(append.values())]
.filter(Boolean)
.join('\n');
}
function resolveCompareValue(a) {
if (a.startsWith('/*') || a.startsWith('//') || a.startsWith(' *') || a.startsWith(' */') || a.startsWith('*/')) {
return 0;
}
else if (a.startsWith('package')) {
return 1;
}
else if (a.startsWith('import')) {
return 2;
}
else {
return 3;
}
}
function sortPrependValues(values) {
return values.sort((a, b) => {
const aV = resolveCompareValue(a);
const bV = resolveCompareValue(b);
if (aV < bV) {
return -1;
}
if (aV > bV) {
return 1;
}
return 0;
});
}
function validateDuplicateDocuments(files) {
// duplicated names
const definitionMap = {};
function addDefinition(file, node, deduplicatedDefinitions) {
if (typeof node.name !== 'undefined') {
if (!definitionMap[node.kind]) {
definitionMap[node.kind] = {};
}
if (!definitionMap[node.kind][node.name.value]) {
definitionMap[node.kind][node.name.value] = {
paths: new Set(),
contents: new Set(),
};
}
const definitionKindMap = definitionMap[node.kind];
const length = definitionKindMap[node.name.value].contents.size;
definitionKindMap[node.name.value].paths.add(file.location);
definitionKindMap[node.name.value].contents.add(print(node));
if (length === definitionKindMap[node.name.value].contents.size) {
return null;
}
}
return deduplicatedDefinitions.add(node);
}
files.forEach(file => {
const deduplicatedDefinitions = new Set();
visit(file.document, {
OperationDefinition(node) {
addDefinition(file, node, deduplicatedDefinitions);
},
FragmentDefinition(node) {
addDefinition(file, node, deduplicatedDefinitions);
},
});
file.document.definitions = Array.from(deduplicatedDefinitions);
});
const kinds = Object.keys(definitionMap);
kinds.forEach(kind => {
const definitionKindMap = definitionMap[kind];
const names = Object.keys(definitionKindMap);
if (names.length) {
const duplicated = names.filter(name => definitionKindMap[name].contents.size > 1);
if (!duplicated.length) {
return;
}
const list = duplicated
.map(name => `
* ${name} found in:
${[...definitionKindMap[name].paths]
.map(filepath => {
return `
- ${filepath}
`.trimRight();
})
.join('')}
`.trimRight())
.join('');
const definitionKindName = kind.replace('Definition', '').toLowerCase();
throw new DetailedError(`Not all ${definitionKindName}s have an unique name: ${duplicated.join(', ')}`, `
Not all ${definitionKindName}s have an unique name
${list}
`);
}
});
}
export { codegen, executePlugin };