@sanity/codegen
Version:
Codegen toolkit for Sanity.io
244 lines (243 loc) • 10.1 kB
JavaScript
/* eslint-disable unicorn/consistent-function-scoping */ import process from 'node:process';
import * as t from '@babel/types';
import { createSelector } from 'reselect';
import { resultSuffix } from '../casing.js';
import { ALL_SANITY_SCHEMA_TYPES, ARRAY_OF, INTERNAL_REFERENCE_SYMBOL, SANITY_QUERIES } from './constants.js';
import { computeOnce, generateCode, getUniqueIdentifierForName, normalizePrintablePath } from './helpers.js';
import { SchemaTypeGenerator } from './schemaTypeGenerator.js';
import { QueryEvaluationError } from './types.js';
/**
* A class used to generate TypeScript types from a given schema
* @beta
*/ export class TypeGenerator {
getSchemaTypeGenerator = createSelector([
(options)=>options.schema
], (schema)=>new SchemaTypeGenerator(schema));
getSchemaTypeDeclarations = createSelector([
(options)=>options.root,
(options)=>options.schemaPath,
this.getSchemaTypeGenerator
], (root = process.cwd(), schemaPath, schema)=>[
...schema
].map(({ id, name, tsType }, index)=>{
const typeAlias = t.tsTypeAliasDeclaration(id, null, tsType);
let ast = t.exportNamedDeclaration(typeAlias);
if (index === 0 && schemaPath) {
ast = t.addComments(ast, 'leading', [
{
type: 'CommentLine',
value: ` Source: ${normalizePrintablePath(root, schemaPath)}`
}
]);
}
const code = generateCode(ast);
return {
ast,
code,
id,
name,
tsType
};
}));
getAllSanitySchemaTypesDeclaration = createSelector([
this.getSchemaTypeDeclarations
], (schemaTypes)=>{
const ast = t.exportNamedDeclaration(t.tsTypeAliasDeclaration(ALL_SANITY_SCHEMA_TYPES, null, schemaTypes.length > 0 ? t.tsUnionType(schemaTypes.map(({ id })=>t.tsTypeReference(id))) : t.tsNeverKeyword()));
const code = generateCode(ast);
return {
ast,
code,
id: ALL_SANITY_SCHEMA_TYPES
};
});
getArrayOfDeclaration = computeOnce(()=>{
// Creates: type ArrayOf<T> = Array<T & { _key: string }>;
const typeParam = t.tsTypeParameter(null, null, 'T');
const intersectionType = t.tsIntersectionType([
t.tsTypeReference(t.identifier('T')),
t.tsTypeLiteral([
t.tsPropertySignature(t.identifier('_key'), t.tsTypeAnnotation(t.tsStringKeyword()))
])
]);
const arrayType = t.tsTypeReference(t.identifier('Array'), t.tsTypeParameterInstantiation([
intersectionType
]));
const ast = t.tsTypeAliasDeclaration(ARRAY_OF, t.tsTypeParameterDeclaration([
typeParam
]), arrayType);
const code = generateCode(ast);
return {
ast,
code,
id: ARRAY_OF
};
});
getInternalReferenceSymbolDeclaration = computeOnce(()=>{
const typeOperator = t.tsTypeOperator(t.tsSymbolKeyword(), 'unique');
const id = INTERNAL_REFERENCE_SYMBOL;
id.typeAnnotation = t.tsTypeAnnotation(typeOperator);
const declaration = t.variableDeclaration('const', [
t.variableDeclarator(id)
]);
declaration.declare = true;
const ast = t.exportNamedDeclaration(declaration);
const code = generateCode(ast);
return {
ast,
code,
id
};
});
static async getEvaluatedModules({ queries: extractedModules, reporter: report, root = process.cwd(), schemaTypeDeclarations, schemaTypeGenerator }) {
if (!extractedModules) {
report?.stream.evaluatedModules.end();
return [];
}
const currentIdentifiers = new Set(schemaTypeDeclarations.map(({ id })=>id.name));
const evaluatedModuleResults = [];
for await (const { filename, ...extractedModule } of extractedModules){
const queries = [];
const errors = [
...extractedModule.errors
];
for (const extractedQuery of extractedModule.queries){
const { variable } = extractedQuery;
try {
const { stats, tsType } = schemaTypeGenerator.evaluateQuery(extractedQuery);
const id = getUniqueIdentifierForName(resultSuffix(variable.id.name), currentIdentifiers);
const typeAlias = t.tsTypeAliasDeclaration(id, null, tsType);
const trimmedQuery = extractedQuery.query.replaceAll(/(\r\n|\n|\r)/gm, '').trim();
const ast = t.addComments(t.exportNamedDeclaration(typeAlias), 'leading', [
{
type: 'CommentLine',
value: ` Source: ${normalizePrintablePath(root, filename)}`
},
{
type: 'CommentLine',
value: ` Variable: ${variable.id.name}`
},
{
type: 'CommentLine',
value: ` Query: ${trimmedQuery}`
}
]);
const evaluatedQueryResult = {
ast,
code: generateCode(ast),
id,
stats,
tsType,
...extractedQuery
};
currentIdentifiers.add(id.name);
queries.push(evaluatedQueryResult);
} catch (cause) {
errors.push(new QueryEvaluationError({
cause,
filename,
variable
}));
}
}
const evaluatedModule = {
errors,
filename,
queries
};
report?.stream.evaluatedModules.emit(evaluatedModule);
evaluatedModuleResults.push(evaluatedModule);
}
report?.stream.evaluatedModules.end();
return evaluatedModuleResults;
}
static async getQueryMapDeclaration({ evaluatedModules, overloadClientMethods = true }) {
if (!overloadClientMethods) return {
ast: t.program([]),
code: ''
};
const queries = evaluatedModules.flatMap((module)=>module.queries);
if (queries.length === 0) return {
ast: t.program([]),
code: ''
};
const typesByQuerystring = {};
for (const { id, query } of queries){
typesByQuerystring[query] ??= [];
typesByQuerystring[query].push(id.name);
}
const queryReturnInterface = t.tsInterfaceDeclaration(SANITY_QUERIES, null, [], t.tsInterfaceBody(Object.entries(typesByQuerystring).map(([query, types])=>{
return t.tsPropertySignature(t.stringLiteral(query), t.tsTypeAnnotation(types.length > 0 ? t.tsUnionType(types.map((type)=>t.tsTypeReference(t.identifier(type)))) : t.tsNeverKeyword()));
})));
const declareModule = t.declareModule(t.stringLiteral('@sanity/client'), t.blockStatement([
queryReturnInterface
]));
const clientImport = t.addComments(t.importDeclaration([], t.stringLiteral('@sanity/client')), 'leading', [
{
type: 'CommentLine',
value: ' Query TypeMap'
}
]);
const ast = t.program([
clientImport,
declareModule
]);
const code = generateCode(ast);
return {
ast,
code
};
}
async generateTypes(options) {
const { reporter: report } = options;
const internalReferenceSymbol = this.getInternalReferenceSymbolDeclaration();
const schemaTypeGenerator = this.getSchemaTypeGenerator(options);
const schemaTypeDeclarations = this.getSchemaTypeDeclarations(options);
const allSanitySchemaTypesDeclaration = this.getAllSanitySchemaTypesDeclaration(options);
report?.event.generatedSchemaTypes({
allSanitySchemaTypesDeclaration,
internalReferenceSymbol,
schemaTypeDeclarations
});
const program = t.program([]);
let code = '';
for (const declaration of schemaTypeDeclarations){
program.body.push(declaration.ast);
code += declaration.code;
}
program.body.push(allSanitySchemaTypesDeclaration.ast);
code += allSanitySchemaTypesDeclaration.code;
program.body.push(internalReferenceSymbol.ast);
code += internalReferenceSymbol.code;
const evaluatedModules = await TypeGenerator.getEvaluatedModules({
...options,
schemaTypeDeclarations,
schemaTypeGenerator
});
// Only generate ArrayOf if it's actually used
if (schemaTypeGenerator.isArrayOfUsed()) {
const arrayOfDeclaration = this.getArrayOfDeclaration();
program.body.push(arrayOfDeclaration.ast);
code += arrayOfDeclaration.code;
}
for (const { queries } of evaluatedModules){
for (const query of queries){
program.body.push(query.ast);
code += query.code;
}
}
const queryMapDeclaration = await TypeGenerator.getQueryMapDeclaration({
...options,
evaluatedModules
});
program.body.push(...queryMapDeclaration.ast.body);
code += queryMapDeclaration.code;
report?.event.generatedQueryTypes({
queryMapDeclaration
});
return {
ast: program,
code
};
}
}
//# sourceMappingURL=typeGenerator.js.map