UNPKG

@sanity/codegen

Version:

Codegen toolkit for Sanity.io

244 lines (243 loc) • 10.1 kB
/* 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