350 lines
10 KiB
JavaScript
350 lines
10 KiB
JavaScript
"use strict";
|
|
|
|
const fs = require(`fs-extra`);
|
|
|
|
const {
|
|
EnumTypeComposer,
|
|
InputTypeComposer,
|
|
InterfaceTypeComposer,
|
|
ObjectTypeComposer,
|
|
ScalarTypeComposer,
|
|
UnionTypeComposer
|
|
} = require(`graphql-compose`);
|
|
|
|
const report = require(`gatsby-cli/lib/reporter`);
|
|
|
|
const {
|
|
internalExtensionNames
|
|
} = require(`./extensions`);
|
|
|
|
const printTypeDefinitions = ({
|
|
config,
|
|
schemaComposer
|
|
}) => {
|
|
if (!config) return Promise.resolve();
|
|
const {
|
|
path,
|
|
include = {},
|
|
exclude = {},
|
|
withFieldTypes
|
|
} = config || {};
|
|
|
|
if (!path) {
|
|
report.error(`Printing type definitions aborted. Please provide a file path.`);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (fs.existsSync(path)) {
|
|
report.error(`Printing type definitions aborted. The file \`${path}\` already exists.`);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const internalTypes = [`Boolean`, `Buffer`, `Date`, `Float`, `ID`, `Int`, `Internal`, `InternalInput`, `JSON`, `Json`, `Node`, `NodeInput`, `Query`, `String`];
|
|
const internalPlugins = [`internal-data-bridge`];
|
|
const typesToExclude = exclude.types || [];
|
|
const pluginsToExclude = exclude.plugins || [];
|
|
|
|
const getName = tc => tc.name || tc.getTypeName();
|
|
|
|
const isInternalType = tc => {
|
|
const typeName = getName(tc);
|
|
|
|
if (internalTypes.includes(typeName)) {
|
|
return true;
|
|
}
|
|
|
|
const plugin = tc.getExtension(`plugin`);
|
|
|
|
if (internalPlugins.includes(plugin)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const shouldIncludeType = tc => {
|
|
const typeName = getName(tc);
|
|
|
|
if (typesToExclude.includes(typeName)) {
|
|
return false;
|
|
}
|
|
|
|
if (include.types && !include.types.includes(typeName)) {
|
|
return false;
|
|
}
|
|
|
|
const plugin = tc.getExtension(`plugin`);
|
|
|
|
if (pluginsToExclude.includes(plugin)) {
|
|
return false;
|
|
}
|
|
|
|
if (include.plugins && !include.plugins.includes(plugin)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}; // Save processed type names, not references to the type composers,
|
|
// because of how graphql-compose, at least in v6, processes
|
|
// inline types
|
|
|
|
|
|
const processedTypes = new Set();
|
|
const typeDefs = new Set();
|
|
|
|
const addType = tc => {
|
|
const typeName = getName(tc);
|
|
|
|
if (!processedTypes.has(typeName) && !isInternalType(tc)) {
|
|
processedTypes.add(typeName);
|
|
return typeDefs.add(tc);
|
|
}
|
|
|
|
processedTypes.add(typeName);
|
|
return null;
|
|
};
|
|
|
|
const addWithFieldTypes = tc => {
|
|
if (addType(tc) && (tc instanceof ObjectTypeComposer || tc instanceof InterfaceTypeComposer || tc instanceof InputTypeComposer)) {
|
|
if (tc instanceof ObjectTypeComposer) {
|
|
const interfaces = tc.getInterfaces();
|
|
interfaces.forEach(iface => {
|
|
const ifaceName = getName(iface);
|
|
|
|
if (ifaceName !== `Node`) {
|
|
addWithFieldTypes(schemaComposer.getAnyTC(ifaceName));
|
|
}
|
|
});
|
|
}
|
|
|
|
tc.getFieldNames().forEach(fieldName => {
|
|
const fieldType = tc.getFieldTC(fieldName);
|
|
addWithFieldTypes(fieldType);
|
|
|
|
if (!(tc instanceof InputTypeComposer)) {
|
|
const fieldArgs = tc.getFieldArgs(fieldName);
|
|
Object.keys(fieldArgs).forEach(argName => {
|
|
addWithFieldTypes(tc.getFieldArgTC(fieldName, argName));
|
|
});
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
schemaComposer.forEach(tc => {
|
|
if (!isInternalType(tc) && shouldIncludeType(tc)) {
|
|
if (withFieldTypes) {
|
|
addWithFieldTypes(tc);
|
|
} else {
|
|
addType(tc);
|
|
}
|
|
}
|
|
});
|
|
const printedTypeDefs = [`### Type definitions saved at ${new Date().toISOString()} ###`];
|
|
|
|
try {
|
|
typeDefs.forEach(tc => printedTypeDefs.push(printType(tc)));
|
|
report.info(`Writing GraphQL type definitions to ${path}`);
|
|
return fs.writeFile(path, printedTypeDefs.join(`\n\n`));
|
|
} catch (error) {
|
|
report.error(`Failed writing type definitions to \`${path}\`.`, error);
|
|
return Promise.resolve();
|
|
}
|
|
};
|
|
|
|
const printType = (tc, typeName) => {
|
|
if (tc instanceof ObjectTypeComposer) {
|
|
return printObjectType(tc);
|
|
} else if (tc instanceof InterfaceTypeComposer) {
|
|
return printInterfaceType(tc);
|
|
} else if (tc instanceof UnionTypeComposer) {
|
|
return printUnionType(tc);
|
|
} else if (tc instanceof EnumTypeComposer) {
|
|
return printEnumType(tc);
|
|
} else if (tc instanceof ScalarTypeComposer) {
|
|
return printScalarType(tc);
|
|
} else if (tc instanceof InputTypeComposer) {
|
|
return printInputObjectType(tc);
|
|
} else {
|
|
throw new Error(`Did not recognize type of ${typeName}.`);
|
|
}
|
|
}; // ------------------------- graphql-js schemaPrinter -------------------------
|
|
|
|
|
|
const {
|
|
astFromValue,
|
|
print,
|
|
GraphQLString,
|
|
DEFAULT_DEPRECATION_REASON
|
|
} = require(`graphql`);
|
|
|
|
const {
|
|
printBlockString
|
|
} = require(`graphql/language/blockString`);
|
|
|
|
const {
|
|
flatMap
|
|
} = require(`graphql/polyfills/flatMap`);
|
|
|
|
const printScalarType = tc => {
|
|
const type = tc.getType();
|
|
return printDescription(type) + `scalar ${type.name}`;
|
|
};
|
|
|
|
const printObjectType = tc => {
|
|
const type = tc.getType();
|
|
const interfaces = type.getInterfaces();
|
|
const implementedInterfaces = interfaces.length ? ` implements ` + interfaces.map(i => i.name).join(` & `) : ``;
|
|
const extensions = tc.getExtensions();
|
|
|
|
if (tc.hasInterface(`Node`)) {
|
|
extensions.dontInfer = null;
|
|
}
|
|
|
|
const directives = tc.schemaComposer.getDirectives();
|
|
const printedDirectives = printDirectives(extensions, directives);
|
|
const fields = tc.hasInterface(`Node`) ? Object.values(type.getFields()).filter(field => ![`id`, `parent`, `children`, `internal`].includes(field.name)) : Object.values(type.getFields());
|
|
return printDescription(type) + `type ${type.name}${implementedInterfaces}${printedDirectives}` + printFields(fields, directives);
|
|
};
|
|
|
|
const printInterfaceType = tc => {
|
|
const type = tc.getType();
|
|
const extensions = tc.getExtensions();
|
|
const directives = tc.schemaComposer.getDirectives();
|
|
const printedDirectives = printDirectives(extensions, directives);
|
|
return printDescription(type) + `interface ${type.name}${printedDirectives}` + printFields(Object.values(type.getFields()), directives);
|
|
};
|
|
|
|
const printUnionType = tc => {
|
|
const type = tc.getType();
|
|
const types = type.getTypes();
|
|
const possibleTypes = types.length ? ` = ` + types.join(` | `) : ``;
|
|
return printDescription(type) + `union ` + type.name + possibleTypes;
|
|
};
|
|
|
|
const printEnumType = tc => {
|
|
const type = tc.getType();
|
|
const values = type.getValues().map((value, i) => printDescription(value, ` `, !i) + ` ` + value.name + printDeprecated(value));
|
|
return printDescription(type) + `enum ${type.name}` + printBlock(values);
|
|
};
|
|
|
|
const printInputObjectType = tc => {
|
|
const type = tc.getType();
|
|
const fields = Object.values(type.getFields()).map((f, i) => printDescription(f, ` `, !i) + ` ` + printInputValue(f));
|
|
return printDescription(type) + `input ${type.name}` + printBlock(fields);
|
|
};
|
|
|
|
const printFields = (fields, directives) => {
|
|
const printedFields = fields.map((f, i) => printDescription(f, ` `, !i) + ` ` + f.name + printArgs(f.args, ` `) + `: ` + String(f.type) + printDirectives(f.extensions || {}, directives) + printDeprecated(f));
|
|
return printBlock(printedFields);
|
|
};
|
|
|
|
const printBlock = items => items.length !== 0 ? ` {\n` + items.join(`\n`) + `\n}` : ``;
|
|
|
|
const printArgs = (args, indentation = ``) => {
|
|
if (args.length === 0) {
|
|
return ``;
|
|
} // If all args have no description, print them on one line
|
|
|
|
|
|
if (args.every(arg => !arg.description)) {
|
|
return `(` + args.map(printInputValue).join(`, `) + `)`;
|
|
}
|
|
|
|
return `(\n` + args.map((arg, i) => printDescription(arg, ` ` + indentation, !i) + ` ` + indentation + printInputValue(arg)).join(`\n`) + `\n` + indentation + `)`;
|
|
};
|
|
|
|
const printInputValue = arg => {
|
|
const defaultAST = astFromValue(arg.defaultValue, arg.type);
|
|
let argDecl = arg.name + `: ` + String(arg.type);
|
|
|
|
if (defaultAST) {
|
|
argDecl += ` = ${print(defaultAST)}`;
|
|
}
|
|
|
|
return argDecl;
|
|
};
|
|
|
|
const printDirectives = (extensions, directives) => Object.entries(extensions).map(([name, args]) => {
|
|
if ([...internalExtensionNames, `deprecated`].includes(name)) return ``;
|
|
return ` @${name}` + printDirectiveArgs(args, directives.find(directive => directive.name === name));
|
|
}).join(``);
|
|
|
|
const printDirectiveArgs = (args, directive) => {
|
|
if (!args || !directive) {
|
|
return ``;
|
|
}
|
|
|
|
const directiveArgs = Object.entries(args);
|
|
|
|
if (directiveArgs.length === 0) {
|
|
return ``;
|
|
}
|
|
|
|
return `(` + directiveArgs.map(([name, value]) => {
|
|
const arg = directive.args && directive.args.find(arg => arg.name === name);
|
|
return arg && `${name}: ${print(astFromValue(value, arg.type))}`;
|
|
}).join(`, `) + `)`;
|
|
};
|
|
|
|
const printDeprecated = fieldOrEnumVal => {
|
|
if (!fieldOrEnumVal.isDeprecated) {
|
|
return ``;
|
|
}
|
|
|
|
const reason = fieldOrEnumVal.deprecationReason;
|
|
const reasonAST = astFromValue(reason, GraphQLString);
|
|
|
|
if (reasonAST && reason !== `` && reason !== DEFAULT_DEPRECATION_REASON) {
|
|
return ` @deprecated(reason: ` + print(reasonAST) + `)`;
|
|
}
|
|
|
|
return ` @deprecated`;
|
|
};
|
|
|
|
const printDescription = (def, indentation = ``, firstInBlock = true) => {
|
|
if (!def.description) {
|
|
return ``;
|
|
}
|
|
|
|
const lines = descriptionLines(def.description, 120 - indentation.length);
|
|
const text = lines.join(`\n`);
|
|
const preferMultipleLines = text.length > 70;
|
|
const blockString = printBlockString(text, ``, preferMultipleLines);
|
|
const prefix = indentation && !firstInBlock ? `\n` + indentation : indentation;
|
|
return prefix + blockString.replace(/\n/g, `\n` + indentation) + `\n`;
|
|
};
|
|
|
|
const descriptionLines = (description, maxLen) => {
|
|
const rawLines = description.split(`\n`);
|
|
return flatMap(rawLines, line => {
|
|
if (line.length < maxLen + 5) {
|
|
return line;
|
|
} // For > 120 character long lines, cut at space boundaries into sublines
|
|
// of ~80 chars.
|
|
|
|
|
|
return breakLine(line, maxLen);
|
|
});
|
|
};
|
|
|
|
const breakLine = (line, maxLen) => {
|
|
const parts = line.split(new RegExp(`((?: |^).{15,${maxLen - 40}}(?= |$))`));
|
|
|
|
if (parts.length < 4) {
|
|
return [line];
|
|
}
|
|
|
|
const sublines = [parts[0] + parts[1] + parts[2]];
|
|
|
|
for (let i = 3; i < parts.length; i += 2) {
|
|
sublines.push(parts[i].slice(1) + parts[i + 1]);
|
|
}
|
|
|
|
return sublines;
|
|
};
|
|
|
|
module.exports = {
|
|
printTypeDefinitions
|
|
};
|
|
//# sourceMappingURL=print.js.map
|