@sanity/codegen
Version:
Codegen toolkit for Sanity.io
328 lines (327 loc) • 12.9 kB
JavaScript
import * as t from '@babel/types';
import { typeEvaluate } from 'groq-js';
import { safeParseQuery } from '../safeParseQuery.js';
import { ARRAY_OF, INTERNAL_REFERENCE_SYMBOL } from './constants.js';
import { getFilterArrayUnionType, getUniqueIdentifierForName, isIdentifierName, weakMapMemo } from './helpers.js';
export class SchemaTypeGenerator {
schema;
evaluateQuery = weakMapMemo(// eslint-disable-next-line unicorn/consistent-function-scoping
({ query })=>{
const ast = safeParseQuery(query);
const typeNode = typeEvaluate(ast, this.schema);
const tsType = this.generateTsType(typeNode);
const stats = walkAndCountQueryTypeNodeStats(typeNode);
return {
stats,
tsType
};
});
arrayOfUsed = false;
identifiers = new Map();
tsTypes = new Map();
constructor(schema){
this.schema = schema;
const uniqueTypeNames = new Set();
for (const type of schema){
if (uniqueTypeNames.has(type.name)) {
throw new Error(`Duplicate type name "${type.name}" in schema. Type names must be unique within the same schema.`);
}
uniqueTypeNames.add(type.name);
}
for (const type of schema){
const currentIdentifierNames = new Set([
...this.identifiers.values()
].map((id)=>id.name));
const uniqueIdentifier = getUniqueIdentifierForName(type.name, currentIdentifierNames);
this.identifiers.set(type.name, uniqueIdentifier);
}
for (const type of schema){
this.tsTypes.set(type.name, this.generateTsType(type));
}
}
getType(typeName) {
const tsType = this.tsTypes.get(typeName);
const id = this.identifiers.get(typeName);
if (tsType && id) return {
id,
tsType
};
return undefined;
}
hasType(typeName) {
return this.tsTypes.has(typeName);
}
isArrayOfUsed() {
return this.arrayOfUsed;
}
*[Symbol.iterator]() {
for (const { name } of this.schema){
yield {
name,
...this.getType(name)
};
}
}
typeNames() {
return this.schema.map((schemaType)=>schemaType.name);
}
/**
* Helper function used to generate TS types for arrays of inline types, or arrays of inline types
* wrapped in the ArrayOf wrapper that adds _key prop
*/ generateArrayOfTsType(typeNode) {
this.arrayOfUsed = true;
const typeNodes = this.generateTsType(typeNode.of);
return t.tsTypeReference(ARRAY_OF, t.tsTypeParameterInstantiation([
typeNodes
]));
}
// Helper function used to generate TS types for array type nodes.
generateArrayTsType(typeNode) {
// if it's an array of a single inline type, wrap it in ArrayOf
if (typeNode.of.type === 'inline') {
return this.generateArrayOfTsType(typeNode);
}
// if it's not an inline object and not a union, wrap in Array
if (typeNode.of.type !== 'union') {
const typeNodes = this.generateTsType(typeNode.of);
return t.tsTypeReference(t.identifier('Array'), t.tsTypeParameterInstantiation([
typeNodes
]));
}
// if it's not a union type or all of the union type members are non-inlines, wrap type in Array
if (typeNode.of.of.every((unionTypeNode)=>unionTypeNode.type !== 'inline')) {
const typeNodes = this.generateTsType(typeNode.of);
return t.tsTypeReference(t.identifier('Array'), t.tsTypeParameterInstantiation([
typeNodes
]));
}
// all the union types nodes are inline
if (typeNode.of.of.every((unionMember)=>unionMember.type === 'inline')) {
return this.generateArrayOfTsType(typeNode);
}
// some of the union types are inlines, while some are not - split and recurse
const arrayOfNonInline = getFilterArrayUnionType(typeNode, (member)=>member.type !== 'inline');
const arrayOfInline = getFilterArrayUnionType(typeNode, (member)=>member.type === 'inline');
return t.tsUnionType([
this.generateArrayTsType(arrayOfNonInline),
this.generateArrayTsType(arrayOfInline)
]);
}
// Helper function used to generate TS types for document type nodes.
generateDocumentTsType(document) {
const props = Object.entries(document.attributes).map(([key, node])=>this.generateTsObjectProperty(key, node));
return t.tsTypeLiteral(props);
}
generateInlineTsType(typeNode) {
const id = this.identifiers.get(typeNode.name);
if (!id) {
// Not found in schema, return unknown type
return t.addComment(t.tsUnknownKeyword(), 'trailing', ` Unable to locate the referenced type "${typeNode.name}" in schema`, true);
}
return t.tsTypeReference(id);
}
// Helper function used to generate TS types for object type nodes.
generateObjectTsType(typeNode) {
const props = [];
for (const [key, attribute] of Object.entries(typeNode.attributes)){
props.push(this.generateTsObjectProperty(key, attribute));
}
const rest = typeNode.rest;
if (rest) {
switch(rest.type){
case 'inline':
{
const resolved = this.generateInlineTsType(rest);
// if object rest is unknown, we can't generate a type literal for it
if (t.isTSUnknownKeyword(resolved)) return resolved;
return t.tsIntersectionType([
t.tsTypeLiteral(props),
resolved
]);
}
case 'object':
{
for (const [key, attribute] of Object.entries(rest.attributes)){
props.push(this.generateTsObjectProperty(key, attribute));
}
break;
}
case 'unknown':
{
return t.tsUnknownKeyword();
}
default:
{
// @ts-expect-error This should never happen
throw new Error(`Type "${rest.type}" not found in schema`);
}
}
}
if (typeNode.dereferencesTo) {
const derefType = Object.assign(t.tsPropertySignature(INTERNAL_REFERENCE_SYMBOL, t.tsTypeAnnotation(t.tsLiteralType(t.stringLiteral(typeNode.dereferencesTo)))), {
computed: true,
optional: true
});
props.push(derefType);
}
return t.tsTypeLiteral(props);
}
// Helper function used to generate TS types for object properties.
generateTsObjectProperty(key, attribute) {
const type = this.generateTsType(attribute.value);
const keyNode = isIdentifierName(key) ? t.identifier(key) : t.stringLiteral(key);
const propertySignature = t.tsPropertySignature(keyNode, t.tsTypeAnnotation(type));
propertySignature.optional = attribute.optional;
return propertySignature;
}
generateTsType(typeNode) {
switch(typeNode.type){
case 'array':
{
return this.generateArrayTsType(typeNode);
}
case 'boolean':
{
if (typeNode.value !== undefined) {
return t.tsLiteralType(t.booleanLiteral(typeNode.value));
}
return t.tsBooleanKeyword();
}
case 'document':
{
return this.generateDocumentTsType(typeNode);
}
case 'inline':
{
return this.generateInlineTsType(typeNode);
}
case 'null':
{
return t.tsNullKeyword();
}
case 'number':
{
if (typeNode.value !== undefined) {
return t.tsLiteralType(t.numericLiteral(typeNode.value));
}
return t.tsNumberKeyword();
}
case 'object':
{
return this.generateObjectTsType(typeNode);
}
case 'string':
{
if (typeNode.value !== undefined) {
return t.tsLiteralType(t.stringLiteral(typeNode.value));
}
return t.tsStringKeyword();
}
case 'type':
{
return this.generateTsType(typeNode.value);
}
case 'union':
{
return this.generateUnionTsType(typeNode);
}
case 'unknown':
{
return t.tsUnknownKeyword();
}
default:
{
throw new Error(`Encountered unsupported node type "${// @ts-expect-error This should never happen
typeNode.type}" while generating schema types`);
}
}
}
// Helper function used to generate TS types for union type nodes.
generateUnionTsType(typeNode) {
if (typeNode.of.length === 0) return t.tsNeverKeyword();
if (typeNode.of.length === 1) return this.generateTsType(typeNode.of[0]);
return t.tsUnionType(typeNode.of.map((node)=>this.generateTsType(node)));
}
}
export function walkAndCountQueryTypeNodeStats(typeNode) {
switch(typeNode.type){
case 'array':
{
const acc = walkAndCountQueryTypeNodeStats(typeNode.of);
acc.allTypes += 1; // count the array type itself
return acc;
}
case 'object':
{
// if the rest is unknown, we count it as one unknown type
if (typeNode.rest && typeNode.rest.type === 'unknown') {
return {
allTypes: 2,
emptyUnions: 0,
unknownTypes: 1
} // count the object type itself as well
;
}
const restStats = typeNode.rest ? walkAndCountQueryTypeNodeStats(typeNode.rest) : {
allTypes: 0,
emptyUnions: 0,
unknownTypes: 0
};
// count the object type itself
restStats.allTypes += 1;
const attrs = Object.values(typeNode.attributes);
let acc = restStats;
for (const attribute of attrs){
const { allTypes, emptyUnions, unknownTypes } = walkAndCountQueryTypeNodeStats(attribute.value);
acc = {
allTypes: acc.allTypes + allTypes,
emptyUnions: acc.emptyUnions + emptyUnions,
unknownTypes: acc.unknownTypes + unknownTypes
};
}
return acc;
}
case 'union':
{
if (typeNode.of.length === 0) {
return {
allTypes: 1,
emptyUnions: 1,
unknownTypes: 0
};
}
let acc = {
allTypes: 1,
emptyUnions: 0,
unknownTypes: 0
} // count the union type itself
;
for (const type of typeNode.of){
const { allTypes, emptyUnions, unknownTypes } = walkAndCountQueryTypeNodeStats(type);
acc = {
allTypes: acc.allTypes + allTypes,
emptyUnions: acc.emptyUnions + emptyUnions,
unknownTypes: acc.unknownTypes + unknownTypes
};
}
return acc;
}
case 'unknown':
{
return {
allTypes: 1,
emptyUnions: 0,
unknownTypes: 1
};
}
default:
{
return {
allTypes: 1,
emptyUnions: 0,
unknownTypes: 0
};
}
}
}
//# sourceMappingURL=schemaTypeGenerator.js.map