AFFiNE/packages/frontend/graphql/export-gql-plugin.cjs

237 lines
6.9 KiB
JavaScript

const fs = require('fs');
const path = require('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) 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');
}
},
};