UNPKG

@atproto/lex-cli

Version:

TypeScript codegen tool for atproto Lexicon schemas

369 lines 13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.genServerApi = genServerApi; const ts_morph_1 = require("ts-morph"); const lexicon_1 = require("@atproto/lexicon"); const syntax_1 = require("@atproto/syntax"); const common_1 = require("./common"); const lex_gen_1 = require("./lex-gen"); const util_1 = require("./util"); async function genServerApi(lexiconDocs) { const project = new ts_morph_1.Project({ useInMemoryFileSystem: true, manipulationSettings: { indentationText: ts_morph_1.IndentationText.TwoSpaces }, }); const api = { files: [] }; const lexicons = new lexicon_1.Lexicons(lexiconDocs); const nsidTree = (0, util_1.lexiconsToDefTree)(lexiconDocs); const nsidTokens = (0, util_1.schemasToNsidTokens)(lexiconDocs); for (const lexiconDoc of lexiconDocs) { api.files.push(await lexiconTs(project, lexicons, lexiconDoc)); } api.files.push(await (0, common_1.utilTs)(project)); api.files.push(await (0, common_1.lexiconsTs)(project, lexiconDocs)); api.files.push(await indexTs(project, lexiconDocs, nsidTree, nsidTokens)); return api; } const indexTs = (project, lexiconDocs, nsidTree, nsidTokens) => (0, common_1.gen)(project, '/index.ts', async (file) => { //= import {createServer as createXrpcServer, Server as XrpcServer} from '@atproto/xrpc-server' file.addImportDeclaration({ moduleSpecifier: '@atproto/xrpc-server', namedImports: [ { name: 'Auth', isTypeOnly: true }, { name: 'Options', alias: 'XrpcOptions', isTypeOnly: true }, { name: 'Server', alias: 'XrpcServer' }, { name: 'StreamConfigOrHandler', isTypeOnly: true }, { name: 'MethodConfigOrHandler', isTypeOnly: true }, { name: 'createServer', alias: 'createXrpcServer' }, ], }); //= import {schemas} from './lexicons.js' file .addImportDeclaration({ moduleSpecifier: './lexicons.js', }) .addNamedImport({ name: 'schemas', }); // generate type imports for (const lexiconDoc of lexiconDocs) { if (lexiconDoc.defs.main?.type !== 'query' && lexiconDoc.defs.main?.type !== 'subscription' && lexiconDoc.defs.main?.type !== 'procedure') { continue; } file .addImportDeclaration({ moduleSpecifier: `./types/${lexiconDoc.id.split('.').join('/')}.js`, }) .setNamespaceImport((0, util_1.toTitleCase)(lexiconDoc.id)); } // generate token enums for (const nsidAuthority in nsidTokens) { // export const {THE_AUTHORITY} = { // {Name}: "{authority.the.name}" // } file.addVariableStatement({ isExported: true, declarationKind: ts_morph_1.VariableDeclarationKind.Const, declarations: [ { name: (0, util_1.toScreamingSnakeCase)(nsidAuthority), initializer: [ '{', ...nsidTokens[nsidAuthority].map((nsidName) => `${(0, util_1.toTitleCase)(nsidName)}: "${nsidAuthority}.${nsidName}",`), '}', ].join('\n'), }, ], }); } //= export function createServer(options?: XrpcOptions) { ... } const createServerFn = file.addFunction({ name: 'createServer', returnType: 'Server', parameters: [ { name: 'options', type: 'XrpcOptions', hasQuestionToken: true }, ], isExported: true, }); createServerFn.setBodyText(`return new Server(options)`); //= export class Server {...} const serverCls = file.addClass({ name: 'Server', isExported: true, }); //= xrpc: XrpcServer = createXrpcServer(methodSchemas) serverCls.addProperty({ name: 'xrpc', type: 'XrpcServer', }); // generate classes for the schemas for (const ns of nsidTree) { //= ns: NS serverCls.addProperty({ name: ns.propName, type: ns.className, }); // class... genNamespaceCls(file, ns); } //= constructor (options?: XrpcOptions) { //= this.xrpc = createXrpcServer(schemas, options) //= {namespace declarations} //= } serverCls .addConstructor({ parameters: [ { name: 'options', type: 'XrpcOptions', hasQuestionToken: true }, ], }) .setBodyText([ 'this.xrpc = createXrpcServer(schemas, options)', ...nsidTree.map((ns) => `this.${ns.propName} = new ${ns.className}(this)`), ].join('\n')); }); function genNamespaceCls(file, ns) { //= export class {ns}NS {...} const cls = file.addClass({ name: ns.className, isExported: true, }); //= _server: Server cls.addProperty({ name: '_server', type: 'Server', }); for (const child of ns.children) { //= child: ChildNS cls.addProperty({ name: child.propName, type: child.className, }); // recurse genNamespaceCls(file, child); } //= constructor(server: Server) { //= this._server = server //= {child namespace declarations} //= } const cons = cls.addConstructor(); cons.addParameter({ name: 'server', type: 'Server', }); cons.setBodyText([ `this._server = server`, ...ns.children.map((ns) => `this.${ns.propName} = new ${ns.className}(server)`), ].join('\n')); // methods for (const userType of ns.userTypes) { if (userType.def.type !== 'query' && userType.def.type !== 'subscription' && userType.def.type !== 'procedure') { continue; } const moduleName = (0, util_1.toTitleCase)(userType.nsid); const name = (0, util_1.toCamelCase)(syntax_1.NSID.parse(userType.nsid).name || ''); const isSubscription = userType.def.type === 'subscription'; const method = cls.addMethod({ name, typeParameters: [ { name: 'A', constraint: 'Auth', default: 'void', }, ], }); method.addParameter({ name: 'cfg', type: isSubscription ? `StreamConfigOrHandler< A, ${moduleName}.QueryParams, ${moduleName}.HandlerOutput, >` : `MethodConfigOrHandler< A, ${moduleName}.QueryParams, ${moduleName}.HandlerInput, ${moduleName}.HandlerOutput, >`, }); const methodType = isSubscription ? 'streamMethod' : 'method'; method.setBodyText([ // Placing schema on separate line, since the following one was being formatted // into multiple lines and causing the ts-ignore to ignore the wrong line. `const nsid = '${userType.nsid}' // @ts-ignore`, `return this._server.xrpc.${methodType}(nsid, cfg)`, ].join('\n')); } } const lexiconTs = (project, lexicons, lexiconDoc) => (0, common_1.gen)(project, `/types/${lexiconDoc.id.split('.').join('/')}.ts`, async (file) => { const main = lexiconDoc.defs.main; if (main?.type === 'query' || main?.type === 'procedure') { const streamingInput = main?.type === 'procedure' && main.input?.encoding && !main.input.schema; const streamingOutput = main.output?.encoding && !main.output.schema; if (streamingInput || streamingOutput) { //= import stream from 'node:stream' file.addImportDeclaration({ moduleSpecifier: 'node:stream', defaultImport: 'stream', }); } } (0, lex_gen_1.genCommonImports)(file, lexiconDoc.id); const imports = new Set(); for (const defId in lexiconDoc.defs) { const def = lexiconDoc.defs[defId]; const lexUri = `${lexiconDoc.id}#${defId}`; if (defId === 'main') { if (def.type === 'query' || def.type === 'procedure') { (0, lex_gen_1.genXrpcParams)(file, lexicons, lexUri); (0, lex_gen_1.genXrpcInput)(file, imports, lexicons, lexUri); (0, lex_gen_1.genXrpcOutput)(file, imports, lexicons, lexUri, false); genServerXrpcMethod(file, lexicons, lexUri); } else if (def.type === 'subscription') { (0, lex_gen_1.genXrpcParams)(file, lexicons, lexUri); (0, lex_gen_1.genXrpcOutput)(file, imports, lexicons, lexUri, false); genServerXrpcStreaming(file, lexicons, lexUri); } else if (def.type === 'record') { (0, lex_gen_1.genRecord)(file, imports, lexicons, lexUri); } else { (0, lex_gen_1.genUserType)(file, imports, lexicons, lexUri); } } else { (0, lex_gen_1.genUserType)(file, imports, lexicons, lexUri); } } (0, lex_gen_1.genImports)(file, imports, lexiconDoc.id); }); function genServerXrpcMethod(file, lexicons, lexUri) { const def = lexicons.getDefOrThrow(lexUri, ['query', 'procedure']); //= export interface HandlerInput {...} if (def.type === 'procedure' && def.input?.encoding) { const handlerInput = file.addInterface({ name: 'HandlerInput', isExported: true, }); handlerInput.addProperty({ name: 'encoding', type: def.input.encoding .split(',') .map((v) => `'${v.trim()}'`) .join(' | '), }); handlerInput.addProperty({ name: 'body', type: def.input.schema ? def.input.encoding.includes(',') ? 'InputSchema | stream.Readable' : 'InputSchema' : 'stream.Readable', }); } else { file.addTypeAlias({ isExported: true, name: 'HandlerInput', type: 'void', }); } // export interface HandlerSuccess {...} let hasHandlerSuccess = false; if (def.output?.schema || def.output?.encoding) { hasHandlerSuccess = true; const handlerSuccess = file.addInterface({ name: 'HandlerSuccess', isExported: true, }); if (def.output.encoding) { handlerSuccess.addProperty({ name: 'encoding', type: def.output.encoding .split(',') .map((v) => `'${v.trim()}'`) .join(' | '), }); } if (def.output?.schema) { if (def.output.encoding.includes(',')) { handlerSuccess.addProperty({ name: 'body', type: 'OutputSchema | Uint8Array | stream.Readable', }); } else { handlerSuccess.addProperty({ name: 'body', type: 'OutputSchema' }); } } else if (def.output?.encoding) { handlerSuccess.addProperty({ name: 'body', type: 'Uint8Array | stream.Readable', }); } handlerSuccess.addProperty({ name: 'headers?', type: '{ [key: string]: string }', }); } // export interface HandlerError {...} const handlerError = file.addInterface({ name: 'HandlerError', isExported: true, }); handlerError.addProperties([ { name: 'status', type: 'number' }, { name: 'message?', type: 'string' }, ]); if (def.errors?.length) { handlerError.addProperty({ name: 'error?', type: def.errors.map((err) => `'${err.name}'`).join(' | '), }); } // export type HandlerOutput = ... file.addTypeAlias({ isExported: true, name: 'HandlerOutput', type: `HandlerError | ${hasHandlerSuccess ? 'HandlerSuccess' : 'void'}`, }); } function genServerXrpcStreaming(file, lexicons, lexUri) { const def = lexicons.getDefOrThrow(lexUri, ['subscription']); file.addImportDeclaration({ moduleSpecifier: '@atproto/xrpc-server', namedImports: [{ name: 'ErrorFrame' }], }); file.addImportDeclaration({ moduleSpecifier: 'node:http', namedImports: [{ name: 'IncomingMessage' }], }); // export type HandlerError = ... file.addTypeAlias({ name: 'HandlerError', isExported: true, type: `ErrorFrame<${arrayToUnion(def.errors?.map((e) => e.name))}>`, }); // export type HandlerOutput = ... file.addTypeAlias({ isExported: true, name: 'HandlerOutput', type: `HandlerError | ${def.message?.schema ? 'OutputSchema' : 'void'}`, }); } function arrayToUnion(arr) { if (!arr?.length) { return 'never'; } return arr.map((item) => `'${item}'`).join(' | '); } //# sourceMappingURL=server.js.map