const fs = require('node:fs'); const path = require('node:path'); const { Kind, print } = require('graphql'); const { upperFirst, lowerFirst } = require('lodash'); /** * return exported name used in runtime. * * @param {import('graphql').ExecutableDefinitionNode} def * @returns {string} */ function getExportedName(def) { const name = lowerFirst(def.name?.value); const suffix = def.kind === Kind.OPERATION_DEFINITION ? upperFirst(def.operation) : 'Fragment'; return name.endsWith(suffix) ? name : name + suffix; } /** * @type {import('@graphql-codegen/plugin-helpers').CodegenPlugin} */ module.exports = { plugin: (schema, documents, { output }) => { const nameLocationMap = new Map(); const locationSourceMap = new Map( documents .filter(source => !!source.location) .map(source => [source.location, source]) ); /** * @type {string[]} */ const defs = []; const queries = []; const mutations = []; for (const [location, source] of locationSourceMap) { if ( !source || !source.document || !location || source.document.kind !== Kind.DOCUMENT || !source.document.definitions || !source.document.definitions.length ) { return; } const doc = source.document; if (doc.definitions.length > 1) { throw new Error('Only support one definition per file.'); } const definition = doc.definitions[0]; if (!definition) { throw new Error(`Found empty file ${location}.`); } if ( !definition.selectionSet || !definition.selectionSet.selections || definition.selectionSet.selections.length === 0 ) { throw new Error(`Found empty fields selection in file ${location}`); } if ( definition.kind === Kind.OPERATION_DEFINITION || definition.kind === Kind.FRAGMENT_DEFINITION ) { if (!definition.name) { throw new Error(`Anonymous definition found in ${location}`); } const exportedName = getExportedName(definition); // duplication checking if (nameLocationMap.has(exportedName)) { throw new Error( `name ${exportedName} export from ${location} are duplicated.` ); } else { /** * @type {import('graphql').DefinitionNode[]} */ let importedDefinitions = []; if (source.location) { fs.readFileSync(source.location, 'utf8') .split(/\r\n|\r|\n/) .forEach(line => { if (line[0] === '#') { const [importKeyword, importPath] = line .split(' ') .filter(Boolean); if (importKeyword === '#import') { const realImportPath = path.posix.join( location, '..', importPath.replace(/["']/g, '') ); const imports = locationSourceMap.get(realImportPath)?.document .definitions; if (imports) { importedDefinitions = [ ...importedDefinitions, ...imports, ]; } } } }); } const importing = importedDefinitions .map(def => `\${${getExportedName(def)}}`) .join('\n'); // is query or mutation if (definition.kind === Kind.OPERATION_DEFINITION) { // add for runtime usage doc.operationName = definition.name.value; doc.defName = definition.selectionSet.selections .filter(field => field.kind === Kind.FIELD) .map(field => field.name.value) .join(','); nameLocationMap.set(exportedName, location); const containsFile = doc.definitions.some(def => { const { variableDefinitions } = def; if (variableDefinitions) { return variableDefinitions.some(variableDefinition => { const varType = variableDefinition?.type?.type?.name?.value; const checkContainFile = type => { if (schema.getType(type)?.name === 'Upload') return true; const typeDef = schema.getType(type); const fields = typeDef.getFields?.(); if (!fields || !fields.length) return false; for (let field of Object.values(fields)) { let type = field.type; while (type.ofType) { type = type.ofType; } if (type.name === 'Upload') { return true; } } return false; }; return varType ? checkContainFile(varType) : false; }); } else { return false; } }); defs.push(`export const ${exportedName} = { id: '${exportedName}' as const, operationName: '${doc.operationName}', definitionName: '${doc.defName}', containsFile: ${containsFile}, query: \` ${print(doc)}${importing || ''}\`, }; `); if (definition.operation === 'query') { queries.push(exportedName); } else if (definition.operation === 'mutation') { mutations.push(exportedName); } } else { defs.unshift(`export const ${exportedName} = \` ${print(doc)}${importing || ''}\``); } } } } fs.writeFileSync( output, [ '/* do not manipulate this file manually. */', `export interface GraphQLQuery { id: string; operationName: string; definitionName: string; query: string; containsFile?: boolean; } `, ...defs, ].join('\n') ); const queriesUnion = queries .map(query => { const queryName = upperFirst(query); return `{ name: '${query}', variables: ${queryName}Variables, response: ${queryName} } `; }) .join('|'); const mutationsUnion = mutations .map(query => { const queryName = upperFirst(query); return `{ name: '${query}', variables: ${queryName}Variables, response: ${queryName} } `; }) .join('|'); const queryTypes = queriesUnion ? `export type Queries = ${queriesUnion}` : ''; const mutationsTypes = mutationsUnion ? `export type Mutations = ${mutationsUnion}` : ''; return ` ${queryTypes} ${mutationsTypes} `; }, validate: (_schema, _documents, { output }) => { if (!output) { throw new Error('Export plugin must be used with a output file given'); } }, };