UNPKG

@graphql-codegen/core

Version:

<p align="center"> <img src="https://github.com/dotansimha/graphql-code-generator/blob/master/logo.png?raw=true" /> </p>

300 lines (291 loc) • 12 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const pluginHelpers = require('@graphql-codegen/plugin-helpers'); const graphql = require('graphql'); const utils = require('@graphql-tools/utils'); const schema = require('@graphql-tools/schema'); async function executePlugin(options, plugin) { if (!plugin || !plugin.plugin || typeof plugin.plugin !== 'function') { throw new pluginHelpers.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 || graphql.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 pluginHelpers.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 (graphql.isSchema(schemaOrAST)) { return federationDirectives.some(directive => schemaOrAST.getDirective(directive)); } else if (utils.isDocumentNode(schemaOrAST)) { return schemaOrAST.definitions.some(def => def.kind === graphql.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(pluginHelpers.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 ? schema.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 ? pluginHelpers.getCachedDocumentNodeFromSchema(schemaInstance) : options.schema; if (schemaInstance && documents.length > 0 && shouldValidateDocumentsAgainstSchema(skipDocumentsValidation)) { const ignored = ['NoUnusedFragments', 'NoUnusedVariables', 'KnownDirectives']; if (typeof skipDocumentsValidation === 'object' && skipDocumentsValidation.ignoreRules) { ignored.push(...utils.asArray(skipDocumentsValidation.ignoreRules)); } const extraFragments = pickFlag('externalFragments', options.config) || []; const errors = await utils.validateGraphQlDocuments(schemaInstance, [ ...documents, ...extraFragments.map(f => ({ location: f.importFrom, document: { kind: graphql.Kind.DOCUMENT, definitions: [f.node] }, })), ], graphql.specifiedRules.filter(rule => !ignored.some(ignoredRule => rule.name.startsWith(ignoredRule)))); utils.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 (pluginHelpers.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(graphql.print(node)); if (length === definitionKindMap[node.name.value].contents.size) { return null; } } return deduplicatedDefinitions.add(node); } files.forEach(file => { const deduplicatedDefinitions = new Set(); graphql.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 pluginHelpers.DetailedError(`Not all ${definitionKindName}s have an unique name: ${duplicated.join(', ')}`, ` Not all ${definitionKindName}s have an unique name ${list} `); } }); } exports.codegen = codegen; exports.executePlugin = executePlugin;