An experimental TypeSpec syntax for Lexicon
56
fork

Configure Feed

Select the types of activity you want to include in your feed.

add cli

authored by

Dan Abramov and committed by
Tangled
3aaa4829 a362d3ed

+1124 -502
+2 -1
package.json
··· 10 10 "example": "pnpm --filter @typelex/example build", 11 11 "playground": "pnpm --filter @typelex/playground dev", 12 12 "validate": "pnpm build && pnpm run validate-lexicons && pnpm test", 13 - "validate-lexicons": "node scripts/validate-lexicons.js" 13 + "validate-lexicons": "node scripts/validate-lexicons.js", 14 + "cli": "pnpm --filter @typelex/cli" 14 15 }, 15 16 "repository": { 16 17 "type": "git",
+3
packages/cli/.gitignore
··· 1 + dist 2 + node_modules 3 + *.log
+66
packages/cli/README.md
··· 1 + # @typelex/cli 2 + 3 + Experimental CLI for typelex 4 + 5 + ## Installation 6 + 7 + ```bash 8 + pnpm add -D @typelex/cli @typelex/emitter 9 + ``` 10 + 11 + ## Usage 12 + 13 + ```bash 14 + typelex compile xyz.statusphere.* 15 + ``` 16 + 17 + This command: 18 + 1. Scans `lexicons/` for all external lexicons (not matching `xyz.statusphere`) 19 + 2. Generates `typelex/externals.tsp` with `@external` stubs 20 + 3. Compiles `typelex/main.tsp` to `lexicons/` (or custom output via `--out`) 21 + 22 + Fixed paths: 23 + - Entry point: `typelex/main.tsp` 24 + - Externals: `typelex/externals.tsp` 25 + 26 + ## Example 27 + 28 + ```typescript 29 + // typelex/main.tsp 30 + import "@typelex/emitter"; 31 + import "./externals.tsp"; 32 + 33 + namespace xyz.statusphere.defs { 34 + model StatusView { 35 + @required uri: atUri; 36 + @required status: string; 37 + @required profile: app.bsky.actor.defs.ProfileView; 38 + } 39 + } 40 + ``` 41 + 42 + ```bash 43 + typelex compile 'xyz.statusphere.*' 44 + ``` 45 + 46 + The CLI scans `lexicons/` for external types and auto-generates `typelex/externals.tsp` with stubs 47 + 48 + ### Integration 49 + 50 + ```json 51 + { 52 + "scripts": { 53 + "build:lexicons": "typelex compile 'xyz.statusphere.*'", 54 + "build:codegen": "lex gen-server --yes ./src lexicons/xyz/statusphere/*.json" 55 + } 56 + } 57 + ``` 58 + 59 + ## Options 60 + 61 + - `--out <directory>` - Output directory for generated Lexicon files (default: `./lexicons`) 62 + - `--watch` - Watch mode for continuous compilation 63 + 64 + ## License 65 + 66 + MIT
+40
packages/cli/package.json
··· 1 + { 2 + "name": "@typelex/cli", 3 + "version": "0.2.0", 4 + "description": "CLI for typelex - TypeSpec-based IDL for ATProto Lexicons", 5 + "main": "dist/index.js", 6 + "type": "module", 7 + "bin": { 8 + "typelex": "dist/cli.js" 9 + }, 10 + "files": [ 11 + "dist", 12 + "src" 13 + ], 14 + "scripts": { 15 + "build": "tsc", 16 + "clean": "rm -rf dist", 17 + "watch": "tsc --watch", 18 + "prepublishOnly": "npm run build" 19 + }, 20 + "keywords": [ 21 + "typespec", 22 + "atproto", 23 + "lexicon", 24 + "cli" 25 + ], 26 + "author": "Dan Abramov <dan.abramov@gmail.com>", 27 + "license": "MIT", 28 + "dependencies": { 29 + "@typespec/compiler": "^1.4.0", 30 + "yargs": "^18.0.0" 31 + }, 32 + "devDependencies": { 33 + "@types/node": "^20.0.0", 34 + "@types/yargs": "^17.0.33", 35 + "typescript": "^5.0.0" 36 + }, 37 + "peerDependencies": { 38 + "@typelex/emitter": "^0.2.0" 39 + } 40 + }
+64
packages/cli/src/cli.ts
··· 1 + #!/usr/bin/env node 2 + import yargs from "yargs"; 3 + import { hideBin } from "yargs/helpers"; 4 + import { compileCommand } from "./commands/compile.js"; 5 + 6 + async function main() { 7 + await yargs(hideBin(process.argv)) 8 + .scriptName("typelex") 9 + .usage("$0 compile <namespace>") 10 + .command( 11 + "compile <namespace>", 12 + "Compile TypeSpec files to Lexicon JSON", 13 + (yargs) => { 14 + return yargs 15 + .positional("namespace", { 16 + describe: "Primary namespace pattern (e.g., app.bsky.*)", 17 + type: "string", 18 + demandOption: true, 19 + }) 20 + .option("out", { 21 + describe: "Output directory for generated Lexicon files (relative to cwd)", 22 + type: "string", 23 + default: "./lexicons", 24 + }); 25 + }, 26 + async (argv) => { 27 + const options: Record<string, unknown> = {}; 28 + if (argv.watch) { 29 + options.watch = true; 30 + } 31 + if (argv.out) { 32 + options.out = argv.out; 33 + } 34 + await compileCommand(argv.namespace, options); 35 + } 36 + ) 37 + .option("watch", { 38 + describe: "Watch mode", 39 + type: "boolean", 40 + default: false, 41 + }) 42 + .demandCommand(1, "You must specify a command") 43 + .help() 44 + .version() 45 + .fail((msg, err) => { 46 + if (err) { 47 + console.error(err); 48 + } else { 49 + console.error(msg); 50 + } 51 + process.exit(1); 52 + }).argv; 53 + } 54 + 55 + process.on("unhandledRejection", (error: unknown) => { 56 + console.error("Unhandled promise rejection!"); 57 + console.error(error); 58 + process.exit(1); 59 + }); 60 + 61 + main().catch((error) => { 62 + console.error(error); 63 + process.exit(1); 64 + });
+68
packages/cli/src/commands/compile.ts
··· 1 + import { resolve } from "path"; 2 + import { spawn } from "child_process"; 3 + import { generateExternalsFile } from "../utils/externals-generator.js"; 4 + import { ensureMainImports } from "../utils/ensure-imports.js"; 5 + 6 + /** 7 + * Compile TypeSpec files to Lexicon JSON 8 + * 9 + * @param namespace - Primary namespace pattern (e.g., "app.bsky.*") 10 + * @param options - Additional compiler options 11 + */ 12 + export async function compileCommand( 13 + namespace: string, 14 + options: Record<string, unknown> = {} 15 + ): Promise<void> { 16 + const cwd = process.cwd(); 17 + const outDir = (options.out as string) || "./lexicons"; 18 + 19 + // Validate that output directory ends with 'lexicons' 20 + const normalizedPath = outDir.replace(/\\/g, '/').replace(/\/+$/, ''); 21 + if (!normalizedPath.endsWith('/lexicons') && normalizedPath !== 'lexicons' && normalizedPath !== './lexicons') { 22 + console.error(`Error: Output directory must end with 'lexicons'`); 23 + console.error(`Got: ${outDir}`); 24 + console.error(`Valid examples: ./lexicons, ../../lexicons, /path/to/lexicons`); 25 + process.exit(1); 26 + } 27 + 28 + // Generate externals first (scans the output directory for external lexicons) 29 + await generateExternalsFile(namespace, cwd, outDir); 30 + 31 + // Ensure required imports are present in main.tsp 32 + await ensureMainImports(cwd); 33 + 34 + // Compile TypeSpec using the TypeSpec CLI 35 + const entrypoint = resolve(cwd, "typelex/main.tsp"); 36 + const args = [ 37 + "compile", 38 + entrypoint, 39 + "--emit", 40 + "@typelex/emitter", 41 + "--option", 42 + `@typelex/emitter.emitter-output-dir={project-root}/${outDir}`, 43 + ]; 44 + 45 + if (options.watch) { 46 + args.push("--watch"); 47 + } 48 + 49 + return new Promise((resolve, reject) => { 50 + const tsp = spawn("tsp", args, { 51 + cwd, 52 + stdio: "inherit", 53 + }); 54 + 55 + tsp.on("close", (code) => { 56 + if (code === 0) { 57 + resolve(); 58 + } else { 59 + process.exit(code ?? 1); 60 + } 61 + }); 62 + 63 + tsp.on("error", (err) => { 64 + console.error("Failed to start TypeSpec compiler:", err); 65 + reject(err); 66 + }); 67 + }); 68 + }
+1
packages/cli/src/index.ts
··· 1 + export { compileCommand } from "./commands/compile.js";
+38
packages/cli/src/utils/ensure-imports.ts
··· 1 + import { readFile } from "fs/promises"; 2 + import { resolve } from "path"; 3 + 4 + const REQUIRED_FIRST_LINE = 'import "@typelex/emitter";'; 5 + const REQUIRED_SECOND_LINE = 'import "./externals.tsp";'; 6 + 7 + /** 8 + * Validates that main.tsp starts with the required imports. 9 + * Fails the build if the first two lines are not exactly as expected. 10 + * 11 + * @param cwd - Current working directory 12 + */ 13 + export async function ensureMainImports(cwd: string): Promise<void> { 14 + const mainPath = resolve(cwd, "typelex/main.tsp"); 15 + 16 + try { 17 + const content = await readFile(mainPath, "utf-8"); 18 + const lines = content.split("\n"); 19 + 20 + if (lines[0]?.trim() !== REQUIRED_FIRST_LINE) { 21 + console.error(`Error: main.tsp must start with: ${REQUIRED_FIRST_LINE}`); 22 + console.error(`Found: ${lines[0] || "(empty line)"}`); 23 + process.exit(1); 24 + } 25 + 26 + if (lines[1]?.trim() !== REQUIRED_SECOND_LINE) { 27 + console.error(`Error: Line 2 of main.tsp must be: ${REQUIRED_SECOND_LINE}`); 28 + console.error(`Found: ${lines[1] || "(empty line)"}`); 29 + process.exit(1); 30 + } 31 + } catch (err) { 32 + if ((err as NodeJS.ErrnoException).code === "ENOENT") { 33 + console.error("Error: typelex/main.tsp not found"); 34 + process.exit(1); 35 + } 36 + throw err; 37 + } 38 + }
+105
packages/cli/src/utils/externals-generator.ts
··· 1 + import { resolve } from "path"; 2 + import { writeFile, mkdir } from "fs/promises"; 3 + import { findExternalLexicons, LexiconDoc, isTokenDef, isModelDef } from "./lexicon.js"; 4 + 5 + /** 6 + * Convert camelCase to PascalCase 7 + */ 8 + function toPascalCase(str: string): string { 9 + return str.charAt(0).toUpperCase() + str.slice(1); 10 + } 11 + 12 + /** 13 + * Extract namespace prefix from pattern (e.g., "app.bsky.*" -> "app.bsky") 14 + */ 15 + function getNamespacePrefix(pattern: string): string { 16 + if (!pattern.endsWith(".*")) { 17 + throw new Error(`Namespace pattern must end with .*: ${pattern}`); 18 + } 19 + return pattern.slice(0, -2); 20 + } 21 + 22 + /** 23 + * Generate TypeSpec external definitions from lexicon documents 24 + */ 25 + function generateExternalsCode(lexicons: Map<string, LexiconDoc>): string { 26 + const lines: string[] = []; 27 + 28 + lines.push('import "@typelex/emitter";'); 29 + lines.push(""); 30 + lines.push("// Generated by typelex"); 31 + lines.push("// This file is auto-generated. Do not edit manually."); 32 + lines.push(""); 33 + 34 + // Sort namespaces for consistent output 35 + const sortedNamespaces = Array.from(lexicons.entries()).sort(([a], [b]) => 36 + a.localeCompare(b) 37 + ); 38 + 39 + for (const [nsid, lexicon] of sortedNamespaces) { 40 + lines.push("@external"); 41 + // Escape reserved keywords in namespace (like 'record') 42 + const escapedNsid = nsid.replace(/\b(record|union|enum|interface|namespace|model|op|import|using|extends|is|scalar|alias|if|else|return|void|never|unknown|any|true|false|null)\b/g, '`$1`'); 43 + lines.push(`namespace ${escapedNsid} {`); 44 + 45 + // Sort definitions for consistent output 46 + const sortedDefs = Object.entries(lexicon.defs).sort(([a], [b]) => 47 + a.localeCompare(b) 48 + ); 49 + 50 + for (const [defName, def] of sortedDefs) { 51 + if (!isModelDef(def)) { 52 + continue; 53 + } 54 + 55 + const modelName = toPascalCase(defName); 56 + const isToken = isTokenDef(def); 57 + 58 + if (isToken) { 59 + lines.push(` @token model ${modelName} { }`); 60 + } else { 61 + lines.push(` model ${modelName} { }`); 62 + } 63 + } 64 + 65 + lines.push("}"); 66 + lines.push(""); 67 + } 68 + 69 + return lines.join("\n"); 70 + } 71 + 72 + /** 73 + * Generate externals.tsp file for the given namespace pattern 74 + */ 75 + export async function generateExternalsFile( 76 + namespacePattern: string, 77 + cwd: string, 78 + outDir: string = "./lexicons" 79 + ): Promise<void> { 80 + try { 81 + const prefix = getNamespacePrefix(namespacePattern); 82 + const lexiconsDir = resolve(cwd, outDir); 83 + const outputFile = resolve(cwd, "typelex/externals.tsp"); 84 + 85 + const externals = await findExternalLexicons(lexiconsDir, prefix); 86 + 87 + if (externals.size === 0) { 88 + // No externals, create empty file 89 + await mkdir(resolve(cwd, "typelex"), { recursive: true }); 90 + await writeFile( 91 + outputFile, 92 + 'import "@typelex/emitter";\n\n// Generated by typelex\n// No external lexicons found\n', 93 + "utf-8" 94 + ); 95 + return; 96 + } 97 + 98 + const code = generateExternalsCode(externals); 99 + await mkdir(resolve(cwd, "typelex"), { recursive: true }); 100 + await writeFile(outputFile, code, "utf-8"); 101 + } catch (error) { 102 + // Re-throw with better context 103 + throw new Error(`Failed to generate externals: ${error instanceof Error ? error.message : String(error)}`); 104 + } 105 + }
+78
packages/cli/src/utils/lexicon.ts
··· 1 + import { readFile } from "fs/promises"; 2 + import { resolve } from "path"; 3 + import { globby } from "globby"; 4 + 5 + export interface LexiconDef { 6 + type: string; 7 + [key: string]: unknown; 8 + } 9 + 10 + export interface LexiconDoc { 11 + lexicon: number; 12 + id: string; 13 + defs: Record<string, LexiconDef>; 14 + } 15 + 16 + /** 17 + * Read and parse a lexicon JSON file 18 + */ 19 + export async function readLexicon(path: string): Promise<LexiconDoc> { 20 + const content = await readFile(path, "utf-8"); 21 + return JSON.parse(content); 22 + } 23 + 24 + /** 25 + * Find all lexicon files in a directory 26 + */ 27 + export async function findLexicons(dir: string): Promise<string[]> { 28 + try { 29 + const pattern = resolve(dir, "**/*.json"); 30 + return await globby(pattern); 31 + } catch { 32 + // If directory doesn't exist, return empty array 33 + return []; 34 + } 35 + } 36 + 37 + /** 38 + * Extract external lexicons that don't match the given namespace 39 + */ 40 + export async function findExternalLexicons( 41 + lexiconsDir: string, 42 + primaryNamespace: string 43 + ): Promise<Map<string, LexiconDoc>> { 44 + const files = await findLexicons(lexiconsDir); 45 + const externals = new Map<string, LexiconDoc>(); 46 + 47 + for (const file of files) { 48 + const lexicon = await readLexicon(file); 49 + if (!lexicon.id.startsWith(primaryNamespace)) { 50 + externals.set(lexicon.id, lexicon); 51 + } 52 + } 53 + 54 + return externals; 55 + } 56 + 57 + /** 58 + * Check if a definition is a token type 59 + */ 60 + export function isTokenDef(def: LexiconDef): boolean { 61 + return def.type === "token"; 62 + } 63 + 64 + /** 65 + * Check if a definition should become a model in TypeSpec 66 + */ 67 + export function isModelDef(def: LexiconDef): boolean { 68 + const type = def.type; 69 + return ( 70 + type === "object" || 71 + type === "token" || 72 + type === "record" || 73 + type === "union" || 74 + type === "string" || 75 + type === "bytes" || 76 + type === "cid-link" 77 + ); 78 + }
+20
packages/cli/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "Node16", 5 + "moduleResolution": "Node16", 6 + "lib": ["ES2022"], 7 + "outDir": "dist", 8 + "rootDir": "src", 9 + "declaration": true, 10 + "declarationMap": true, 11 + "sourceMap": true, 12 + "strict": true, 13 + "esModuleInterop": true, 14 + "skipLibCheck": true, 15 + "forceConsistentCasingInFileNames": true, 16 + "resolveJsonModule": true 17 + }, 18 + "include": ["src/**/*"], 19 + "exclude": ["node_modules", "dist"] 20 + }
+4
packages/cli/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex 4 + // No external lexicons found
+3 -3
packages/example/package.json
··· 5 5 "type": "module", 6 6 "scripts": { 7 7 "build": "pnpm run build:lexicons && pnpm run build:codegen", 8 - "build:lexicons": "tsp compile typelex/main.tsp --emit @typelex/emitter --option '@typelex/emitter.emitter-output-dir={project-root}/lexicons'", 9 - "build:codegen": "lex gen-server --yes ./src lexicons/app/example/*.json" 8 + "build:lexicons": "typelex compile xyz.statusphere.*", 9 + "build:codegen": "lex gen-server --yes ./src lexicons/xyz/statusphere/*.json" 10 10 }, 11 11 "dependencies": { 12 12 "@atproto/lex-cli": "^0.9.5", 13 13 "@atproto/xrpc-server": "^0.9.5", 14 - "@typespec/compiler": "^1.4.0", 14 + "@typelex/cli": "workspace:*", 15 15 "@typelex/emitter": "workspace:*" 16 16 }, 17 17 "devDependencies": {
+45 -6
packages/example/src/index.ts
··· 10 10 createServer as createXrpcServer, 11 11 } from '@atproto/xrpc-server' 12 12 import { schemas } from './lexicons.js' 13 + import * as XyzStatusphereGetStatuses from './types/xyz/statusphere/getStatuses.js' 14 + import * as XyzStatusphereGetUser from './types/xyz/statusphere/getUser.js' 15 + import * as XyzStatusphereSendStatus from './types/xyz/statusphere/sendStatus.js' 13 16 14 17 export function createServer(options?: XrpcOptions): Server { 15 18 return new Server(options) ··· 17 20 18 21 export class Server { 19 22 xrpc: XrpcServer 20 - app: AppNS 23 + xyz: XyzNS 21 24 22 25 constructor(options?: XrpcOptions) { 23 26 this.xrpc = createXrpcServer(schemas, options) 24 - this.app = new AppNS(this) 27 + this.xyz = new XyzNS(this) 25 28 } 26 29 } 27 30 28 - export class AppNS { 31 + export class XyzNS { 29 32 _server: Server 30 - example: AppExampleNS 33 + statusphere: XyzStatusphereNS 31 34 32 35 constructor(server: Server) { 33 36 this._server = server 34 - this.example = new AppExampleNS(server) 37 + this.statusphere = new XyzStatusphereNS(server) 35 38 } 36 39 } 37 40 38 - export class AppExampleNS { 41 + export class XyzStatusphereNS { 39 42 _server: Server 40 43 41 44 constructor(server: Server) { 42 45 this._server = server 46 + } 47 + 48 + getStatuses<A extends Auth = void>( 49 + cfg: MethodConfigOrHandler< 50 + A, 51 + XyzStatusphereGetStatuses.QueryParams, 52 + XyzStatusphereGetStatuses.HandlerInput, 53 + XyzStatusphereGetStatuses.HandlerOutput 54 + >, 55 + ) { 56 + const nsid = 'xyz.statusphere.getStatuses' // @ts-ignore 57 + return this._server.xrpc.method(nsid, cfg) 58 + } 59 + 60 + getUser<A extends Auth = void>( 61 + cfg: MethodConfigOrHandler< 62 + A, 63 + XyzStatusphereGetUser.QueryParams, 64 + XyzStatusphereGetUser.HandlerInput, 65 + XyzStatusphereGetUser.HandlerOutput 66 + >, 67 + ) { 68 + const nsid = 'xyz.statusphere.getUser' // @ts-ignore 69 + return this._server.xrpc.method(nsid, cfg) 70 + } 71 + 72 + sendStatus<A extends Auth = void>( 73 + cfg: MethodConfigOrHandler< 74 + A, 75 + XyzStatusphereSendStatus.QueryParams, 76 + XyzStatusphereSendStatus.HandlerInput, 77 + XyzStatusphereSendStatus.HandlerOutput 78 + >, 79 + ) { 80 + const nsid = 'xyz.statusphere.sendStatus' // @ts-ignore 81 + return this._server.xrpc.method(nsid, cfg) 43 82 } 44 83 }
+100 -153
packages/example/src/lexicons.ts
··· 10 10 import { type $Typed, is$typed, maybe$typed } from './util.js' 11 11 12 12 export const schemaDict = { 13 - AppExampleDefs: { 13 + XyzStatusphereDefs: { 14 14 lexicon: 1, 15 - id: 'app.example.defs', 15 + id: 'xyz.statusphere.defs', 16 16 defs: { 17 - postRef: { 17 + statusView: { 18 18 type: 'object', 19 19 properties: { 20 20 uri: { 21 21 type: 'string', 22 - description: 'AT URI of the post', 22 + format: 'at-uri', 23 23 }, 24 - cid: { 24 + status: { 25 25 type: 'string', 26 - description: 'CID of the post', 26 + maxLength: 32, 27 + minLength: 1, 28 + maxGraphemes: 1, 27 29 }, 28 - }, 29 - description: 'Reference to a post', 30 - required: ['uri', 'cid'], 31 - }, 32 - replyRef: { 33 - type: 'object', 34 - properties: { 35 - root: { 36 - type: 'ref', 37 - ref: 'lex:app.example.defs#postRef', 38 - description: 'Root post in the thread', 30 + createdAt: { 31 + type: 'string', 32 + format: 'datetime', 39 33 }, 40 - parent: { 34 + profile: { 41 35 type: 'ref', 42 - ref: 'lex:app.example.defs#postRef', 43 - description: 'Direct parent post being replied to', 36 + ref: 'lex:xyz.statusphere.defs#profileView', 44 37 }, 45 38 }, 46 - description: 'Reference to a parent post in a reply chain', 47 - required: ['root', 'parent'], 39 + required: ['uri', 'status', 'createdAt', 'profile'], 48 40 }, 49 - entity: { 41 + profileView: { 50 42 type: 'object', 51 43 properties: { 52 - start: { 53 - type: 'integer', 54 - description: 'Start index in text', 55 - }, 56 - end: { 57 - type: 'integer', 58 - description: 'End index in text', 59 - }, 60 - type: { 44 + did: { 61 45 type: 'string', 62 - description: 'Entity type', 46 + format: 'did', 63 47 }, 64 - value: { 48 + handle: { 65 49 type: 'string', 66 - description: 'Entity value (handle, URL, or tag)', 50 + format: 'handle', 67 51 }, 68 52 }, 69 - description: 'Text entity (mention, link, or tag)', 70 - required: ['start', 'end', 'type', 'value'], 71 - }, 72 - notificationType: { 73 - type: 'string', 74 - knownValues: ['like', 'repost', 'follow', 'mention', 'reply'], 75 - description: 'Type of notification', 53 + required: ['did', 'handle'], 76 54 }, 77 55 }, 78 56 }, 79 - AppExampleFollow: { 57 + XyzStatusphereGetStatuses: { 80 58 lexicon: 1, 81 - id: 'app.example.follow', 59 + id: 'xyz.statusphere.getStatuses', 82 60 defs: { 83 61 main: { 84 - type: 'record', 85 - key: 'tid', 86 - record: { 87 - type: 'object', 62 + type: 'query', 63 + description: 'Get a list of the most recent statuses on the network.', 64 + parameters: { 65 + type: 'params', 88 66 properties: { 89 - subject: { 90 - type: 'string', 91 - description: 'DID of the account being followed', 92 - }, 93 - createdAt: { 94 - type: 'string', 95 - format: 'datetime', 96 - description: 'When the follow was created', 67 + limit: { 68 + type: 'integer', 69 + minimum: 1, 70 + maximum: 100, 71 + default: 50, 97 72 }, 98 73 }, 99 - required: ['subject', 'createdAt'], 100 74 }, 101 - description: 'A follow relationship', 102 - }, 103 - }, 104 - }, 105 - AppExampleLike: { 106 - lexicon: 1, 107 - id: 'app.example.like', 108 - defs: { 109 - main: { 110 - type: 'record', 111 - key: 'tid', 112 - record: { 113 - type: 'object', 114 - properties: { 115 - subject: { 116 - type: 'ref', 117 - ref: 'lex:app.example.defs#postRef', 118 - description: 'Post being liked', 75 + output: { 76 + encoding: 'application/json', 77 + schema: { 78 + type: 'object', 79 + properties: { 80 + statuses: { 81 + type: 'array', 82 + items: { 83 + type: 'ref', 84 + ref: 'lex:xyz.statusphere.defs#statusView', 85 + }, 86 + }, 119 87 }, 120 - createdAt: { 121 - type: 'string', 122 - format: 'datetime', 123 - description: 'When the like was created', 124 - }, 88 + required: ['statuses'], 125 89 }, 126 - required: ['subject', 'createdAt'], 127 90 }, 128 - description: 'A like on a post', 129 91 }, 130 92 }, 131 93 }, 132 - AppExamplePost: { 94 + XyzStatusphereGetUser: { 133 95 lexicon: 1, 134 - id: 'app.example.post', 96 + id: 'xyz.statusphere.getUser', 135 97 defs: { 136 98 main: { 137 - type: 'record', 138 - key: 'tid', 139 - record: { 140 - type: 'object', 141 - properties: { 142 - text: { 143 - type: 'string', 144 - description: 'Post text content', 145 - }, 146 - createdAt: { 147 - type: 'string', 148 - format: 'datetime', 149 - description: 'Creation timestamp', 150 - }, 151 - langs: { 152 - type: 'array', 153 - items: { 154 - type: 'string', 99 + type: 'query', 100 + description: "Get the current user's profile and status.", 101 + output: { 102 + encoding: 'application/json', 103 + schema: { 104 + type: 'object', 105 + properties: { 106 + profile: { 107 + type: 'ref', 108 + ref: 'lex:app.bsky.actor.defs#profileView', 155 109 }, 156 - description: 'Languages the post is written in', 157 - }, 158 - entities: { 159 - type: 'array', 160 - items: { 110 + status: { 161 111 type: 'ref', 162 - ref: 'lex:app.example.defs#entity', 112 + ref: 'lex:xyz.statusphere.defs#statusView', 163 113 }, 164 - description: 'Referenced entities in the post', 165 114 }, 166 - reply: { 167 - type: 'ref', 168 - ref: 'lex:app.example.defs#replyRef', 169 - description: 'Post the user is replying to', 170 - }, 115 + required: ['profile'], 171 116 }, 172 - required: ['text', 'createdAt'], 173 117 }, 174 - description: 'A post in the feed', 175 118 }, 176 119 }, 177 120 }, 178 - AppExampleProfile: { 121 + XyzStatusphereSendStatus: { 179 122 lexicon: 1, 180 - id: 'app.example.profile', 123 + id: 'xyz.statusphere.sendStatus', 181 124 defs: { 182 125 main: { 183 - type: 'record', 184 - key: 'self', 185 - record: { 186 - type: 'object', 187 - properties: { 188 - displayName: { 189 - type: 'string', 190 - description: 'Display name', 191 - }, 192 - description: { 193 - type: 'string', 194 - description: 'Profile description', 126 + type: 'procedure', 127 + description: 'Send a status into the ATmosphere.', 128 + input: { 129 + encoding: 'application/json', 130 + schema: { 131 + type: 'object', 132 + properties: { 133 + status: { 134 + type: 'string', 135 + maxLength: 32, 136 + minLength: 1, 137 + maxGraphemes: 1, 138 + }, 195 139 }, 196 - avatar: { 197 - type: 'string', 198 - description: 'Profile avatar image', 140 + required: ['status'], 141 + }, 142 + }, 143 + output: { 144 + encoding: 'application/json', 145 + schema: { 146 + type: 'object', 147 + properties: { 148 + status: { 149 + type: 'ref', 150 + ref: 'lex:xyz.statusphere.defs#statusView', 151 + }, 199 152 }, 200 - banner: { 201 - type: 'string', 202 - description: 'Profile banner image', 203 - }, 153 + required: ['status'], 204 154 }, 205 155 }, 206 - description: 'User profile information', 207 156 }, 208 157 }, 209 158 }, 210 - AppExampleRepost: { 159 + XyzStatusphereStatus: { 211 160 lexicon: 1, 212 - id: 'app.example.repost', 161 + id: 'xyz.statusphere.status', 213 162 defs: { 214 163 main: { 215 164 type: 'record', ··· 217 166 record: { 218 167 type: 'object', 219 168 properties: { 220 - subject: { 221 - type: 'ref', 222 - ref: 'lex:app.example.defs#postRef', 223 - description: 'Post being reposted', 169 + status: { 170 + type: 'string', 171 + maxLength: 32, 172 + minLength: 1, 173 + maxGraphemes: 1, 224 174 }, 225 175 createdAt: { 226 176 type: 'string', 227 177 format: 'datetime', 228 - description: 'When the repost was created', 229 178 }, 230 179 }, 231 - required: ['subject', 'createdAt'], 180 + required: ['status', 'createdAt'], 232 181 }, 233 - description: 'A repost of another post', 234 182 }, 235 183 }, 236 184 }, ··· 267 215 } 268 216 269 217 export const ids = { 270 - AppExampleDefs: 'app.example.defs', 271 - AppExampleFollow: 'app.example.follow', 272 - AppExampleLike: 'app.example.like', 273 - AppExamplePost: 'app.example.post', 274 - AppExampleProfile: 'app.example.profile', 275 - AppExampleRepost: 'app.example.repost', 218 + XyzStatusphereDefs: 'xyz.statusphere.defs', 219 + XyzStatusphereGetStatuses: 'xyz.statusphere.getStatuses', 220 + XyzStatusphereGetUser: 'xyz.statusphere.getUser', 221 + XyzStatusphereSendStatus: 'xyz.statusphere.sendStatus', 222 + XyzStatusphereStatus: 'xyz.statusphere.status', 276 223 } as const
-79
packages/example/src/types/app/example/defs.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - 9 - const is$typed = _is$typed, 10 - validate = _validate 11 - const id = 'app.example.defs' 12 - 13 - /** Reference to a post */ 14 - export interface PostRef { 15 - $type?: 'app.example.defs#postRef' 16 - /** AT URI of the post */ 17 - uri: string 18 - /** CID of the post */ 19 - cid: string 20 - } 21 - 22 - const hashPostRef = 'postRef' 23 - 24 - export function isPostRef<V>(v: V) { 25 - return is$typed(v, id, hashPostRef) 26 - } 27 - 28 - export function validatePostRef<V>(v: V) { 29 - return validate<PostRef & V>(v, id, hashPostRef) 30 - } 31 - 32 - /** Reference to a parent post in a reply chain */ 33 - export interface ReplyRef { 34 - $type?: 'app.example.defs#replyRef' 35 - root: PostRef 36 - parent: PostRef 37 - } 38 - 39 - const hashReplyRef = 'replyRef' 40 - 41 - export function isReplyRef<V>(v: V) { 42 - return is$typed(v, id, hashReplyRef) 43 - } 44 - 45 - export function validateReplyRef<V>(v: V) { 46 - return validate<ReplyRef & V>(v, id, hashReplyRef) 47 - } 48 - 49 - /** Text entity (mention, link, or tag) */ 50 - export interface Entity { 51 - $type?: 'app.example.defs#entity' 52 - /** Start index in text */ 53 - start: number 54 - /** End index in text */ 55 - end: number 56 - /** Entity type */ 57 - type: string 58 - /** Entity value (handle, URL, or tag) */ 59 - value: string 60 - } 61 - 62 - const hashEntity = 'entity' 63 - 64 - export function isEntity<V>(v: V) { 65 - return is$typed(v, id, hashEntity) 66 - } 67 - 68 - export function validateEntity<V>(v: V) { 69 - return validate<Entity & V>(v, id, hashEntity) 70 - } 71 - 72 - /** Type of notification */ 73 - export type NotificationType = 74 - | 'like' 75 - | 'repost' 76 - | 'follow' 77 - | 'mention' 78 - | 'reply' 79 - | (string & {})
+3 -5
packages/example/src/types/app/example/follow.ts packages/example/src/types/xyz/statusphere/status.ts
··· 8 8 9 9 const is$typed = _is$typed, 10 10 validate = _validate 11 - const id = 'app.example.follow' 11 + const id = 'xyz.statusphere.status' 12 12 13 13 export interface Record { 14 - $type: 'app.example.follow' 15 - /** DID of the account being followed */ 16 - subject: string 17 - /** When the follow was created */ 14 + $type: 'xyz.statusphere.status' 15 + status: string 18 16 createdAt: string 19 17 [k: string]: unknown 20 18 }
-30
packages/example/src/types/app/example/like.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - import type * as AppExampleDefs from './defs.js' 9 - 10 - const is$typed = _is$typed, 11 - validate = _validate 12 - const id = 'app.example.like' 13 - 14 - export interface Record { 15 - $type: 'app.example.like' 16 - subject: AppExampleDefs.PostRef 17 - /** When the like was created */ 18 - createdAt: string 19 - [k: string]: unknown 20 - } 21 - 22 - const hashRecord = 'main' 23 - 24 - export function isRecord<V>(v: V) { 25 - return is$typed(v, id, hashRecord) 26 - } 27 - 28 - export function validateRecord<V>(v: V) { 29 - return validate<Record & V>(v, id, hashRecord, true) 30 - }
-36
packages/example/src/types/app/example/post.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - import type * as AppExampleDefs from './defs.js' 9 - 10 - const is$typed = _is$typed, 11 - validate = _validate 12 - const id = 'app.example.post' 13 - 14 - export interface Record { 15 - $type: 'app.example.post' 16 - /** Post text content */ 17 - text: string 18 - /** Creation timestamp */ 19 - createdAt: string 20 - /** Languages the post is written in */ 21 - langs?: string[] 22 - /** Referenced entities in the post */ 23 - entities?: AppExampleDefs.Entity[] 24 - reply?: AppExampleDefs.ReplyRef 25 - [k: string]: unknown 26 - } 27 - 28 - const hashRecord = 'main' 29 - 30 - export function isRecord<V>(v: V) { 31 - return is$typed(v, id, hashRecord) 32 - } 33 - 34 - export function validateRecord<V>(v: V) { 35 - return validate<Record & V>(v, id, hashRecord, true) 36 - }
-34
packages/example/src/types/app/example/profile.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - 9 - const is$typed = _is$typed, 10 - validate = _validate 11 - const id = 'app.example.profile' 12 - 13 - export interface Record { 14 - $type: 'app.example.profile' 15 - /** Display name */ 16 - displayName?: string 17 - /** Profile description */ 18 - description?: string 19 - /** Profile avatar image */ 20 - avatar?: string 21 - /** Profile banner image */ 22 - banner?: string 23 - [k: string]: unknown 24 - } 25 - 26 - const hashRecord = 'main' 27 - 28 - export function isRecord<V>(v: V) { 29 - return is$typed(v, id, hashRecord) 30 - } 31 - 32 - export function validateRecord<V>(v: V) { 33 - return validate<Record & V>(v, id, hashRecord, true) 34 - }
-30
packages/example/src/types/app/example/repost.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - import type * as AppExampleDefs from './defs.js' 9 - 10 - const is$typed = _is$typed, 11 - validate = _validate 12 - const id = 'app.example.repost' 13 - 14 - export interface Record { 15 - $type: 'app.example.repost' 16 - subject: AppExampleDefs.PostRef 17 - /** When the repost was created */ 18 - createdAt: string 19 - [k: string]: unknown 20 - } 21 - 22 - const hashRecord = 'main' 23 - 24 - export function isRecord<V>(v: V) { 25 - return is$typed(v, id, hashRecord) 26 - } 27 - 28 - export function validateRecord<V>(v: V) { 29 - return validate<Record & V>(v, id, hashRecord, true) 30 - }
+45
packages/example/src/types/xyz/statusphere/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate 11 + const id = 'xyz.statusphere.defs' 12 + 13 + export interface StatusView { 14 + $type?: 'xyz.statusphere.defs#statusView' 15 + uri: string 16 + status: string 17 + createdAt: string 18 + profile: ProfileView 19 + } 20 + 21 + const hashStatusView = 'statusView' 22 + 23 + export function isStatusView<V>(v: V) { 24 + return is$typed(v, id, hashStatusView) 25 + } 26 + 27 + export function validateStatusView<V>(v: V) { 28 + return validate<StatusView & V>(v, id, hashStatusView) 29 + } 30 + 31 + export interface ProfileView { 32 + $type?: 'xyz.statusphere.defs#profileView' 33 + did: string 34 + handle: string 35 + } 36 + 37 + const hashProfileView = 'profileView' 38 + 39 + export function isProfileView<V>(v: V) { 40 + return is$typed(v, id, hashProfileView) 41 + } 42 + 43 + export function validateProfileView<V>(v: V) { 44 + return validate<ProfileView & V>(v, id, hashProfileView) 45 + }
+36
packages/example/src/types/xyz/statusphere/getStatuses.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + import type * as XyzStatusphereDefs from './defs.js' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'xyz.statusphere.getStatuses' 13 + 14 + export type QueryParams = { 15 + limit: number 16 + } 17 + export type InputSchema = undefined 18 + 19 + export interface OutputSchema { 20 + statuses: XyzStatusphereDefs.StatusView[] 21 + } 22 + 23 + export type HandlerInput = void 24 + 25 + export interface HandlerSuccess { 26 + encoding: 'application/json' 27 + body: OutputSchema 28 + headers?: { [key: string]: string } 29 + } 30 + 31 + export interface HandlerError { 32 + status: number 33 + message?: string 34 + } 35 + 36 + export type HandlerOutput = HandlerError | HandlerSuccess
+36
packages/example/src/types/xyz/statusphere/getUser.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + import type * as AppBskyActorDefs from '../../app/bsky/actor/defs.js' 9 + import type * as XyzStatusphereDefs from './defs.js' 10 + 11 + const is$typed = _is$typed, 12 + validate = _validate 13 + const id = 'xyz.statusphere.getUser' 14 + 15 + export type QueryParams = {} 16 + export type InputSchema = undefined 17 + 18 + export interface OutputSchema { 19 + profile: AppBskyActorDefs.ProfileView 20 + status?: XyzStatusphereDefs.StatusView 21 + } 22 + 23 + export type HandlerInput = void 24 + 25 + export interface HandlerSuccess { 26 + encoding: 'application/json' 27 + body: OutputSchema 28 + headers?: { [key: string]: string } 29 + } 30 + 31 + export interface HandlerError { 32 + status: number 33 + message?: string 34 + } 35 + 36 + export type HandlerOutput = HandlerError | HandlerSuccess
+40
packages/example/src/types/xyz/statusphere/sendStatus.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + import type * as XyzStatusphereDefs from './defs.js' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'xyz.statusphere.sendStatus' 13 + 14 + export type QueryParams = {} 15 + 16 + export interface InputSchema { 17 + status: string 18 + } 19 + 20 + export interface OutputSchema { 21 + status: XyzStatusphereDefs.StatusView 22 + } 23 + 24 + export interface HandlerInput { 25 + encoding: 'application/json' 26 + body: InputSchema 27 + } 28 + 29 + export interface HandlerSuccess { 30 + encoding: 'application/json' 31 + body: OutputSchema 32 + headers?: { [key: string]: string } 33 + } 34 + 35 + export interface HandlerError { 36 + status: number 37 + message?: string 38 + } 39 + 40 + export type HandlerOutput = HandlerError | HandlerSuccess
+235
packages/example/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex 4 + // This file is auto-generated. Do not edit manually. 5 + 6 + @external 7 + namespace app.bsky.actor.defs { 8 + model AdultContentPref { } 9 + model BskyAppProgressGuide { } 10 + model BskyAppStatePref { } 11 + model ContentLabelPref { } 12 + model FeedViewPref { } 13 + model HiddenPostsPref { } 14 + model InterestsPref { } 15 + model KnownFollowers { } 16 + model LabelerPrefItem { } 17 + model LabelersPref { } 18 + model MutedWord { } 19 + model MutedWordsPref { } 20 + model MutedWordTarget { } 21 + model Nux { } 22 + model PersonalDetailsPref { } 23 + model PostInteractionSettingsPref { } 24 + model ProfileAssociated { } 25 + model ProfileAssociatedChat { } 26 + model ProfileView { } 27 + model ProfileViewBasic { } 28 + model ProfileViewDetailed { } 29 + model SavedFeed { } 30 + model SavedFeedsPref { } 31 + model SavedFeedsPrefV2 { } 32 + model ThreadViewPref { } 33 + model ViewerState { } 34 + } 35 + 36 + @external 37 + namespace app.bsky.actor.profile { 38 + model Main { } 39 + } 40 + 41 + @external 42 + namespace app.bsky.embed.defs { 43 + model AspectRatio { } 44 + } 45 + 46 + @external 47 + namespace app.bsky.embed.external { 48 + model External { } 49 + model Main { } 50 + model View { } 51 + model ViewExternal { } 52 + } 53 + 54 + @external 55 + namespace app.bsky.embed.images { 56 + model Image { } 57 + model Main { } 58 + model View { } 59 + model ViewImage { } 60 + } 61 + 62 + @external 63 + namespace app.bsky.embed.`record` { 64 + model Main { } 65 + model View { } 66 + model ViewBlocked { } 67 + model ViewDetached { } 68 + model ViewNotFound { } 69 + model ViewRecord { } 70 + } 71 + 72 + @external 73 + namespace app.bsky.embed.recordWithMedia { 74 + model Main { } 75 + model View { } 76 + } 77 + 78 + @external 79 + namespace app.bsky.embed.video { 80 + model Caption { } 81 + model Main { } 82 + model View { } 83 + } 84 + 85 + @external 86 + namespace app.bsky.feed.defs { 87 + model BlockedAuthor { } 88 + model BlockedPost { } 89 + @token model ClickthroughAuthor { } 90 + @token model ClickthroughEmbed { } 91 + @token model ClickthroughItem { } 92 + @token model ClickthroughReposter { } 93 + @token model ContentModeUnspecified { } 94 + @token model ContentModeVideo { } 95 + model FeedViewPost { } 96 + model GeneratorView { } 97 + model GeneratorViewerState { } 98 + model Interaction { } 99 + @token model InteractionLike { } 100 + @token model InteractionQuote { } 101 + @token model InteractionReply { } 102 + @token model InteractionRepost { } 103 + @token model InteractionSeen { } 104 + @token model InteractionShare { } 105 + model NotFoundPost { } 106 + model PostView { } 107 + model ReasonPin { } 108 + model ReasonRepost { } 109 + model ReplyRef { } 110 + @token model RequestLess { } 111 + @token model RequestMore { } 112 + model SkeletonFeedPost { } 113 + model SkeletonReasonPin { } 114 + model SkeletonReasonRepost { } 115 + model ThreadContext { } 116 + model ThreadgateView { } 117 + model ThreadViewPost { } 118 + model ViewerState { } 119 + } 120 + 121 + @external 122 + namespace app.bsky.feed.postgate { 123 + model DisableRule { } 124 + model Main { } 125 + } 126 + 127 + @external 128 + namespace app.bsky.feed.threadgate { 129 + model FollowerRule { } 130 + model FollowingRule { } 131 + model ListRule { } 132 + model Main { } 133 + model MentionRule { } 134 + } 135 + 136 + @external 137 + namespace app.bsky.graph.defs { 138 + @token model Curatelist { } 139 + model ListItemView { } 140 + model ListPurpose { } 141 + model ListView { } 142 + model ListViewBasic { } 143 + model ListViewerState { } 144 + @token model Modlist { } 145 + model NotFoundActor { } 146 + @token model Referencelist { } 147 + model Relationship { } 148 + model StarterPackView { } 149 + model StarterPackViewBasic { } 150 + } 151 + 152 + @external 153 + namespace app.bsky.labeler.defs { 154 + model LabelerPolicies { } 155 + model LabelerView { } 156 + model LabelerViewDetailed { } 157 + model LabelerViewerState { } 158 + } 159 + 160 + @external 161 + namespace app.bsky.richtext.facet { 162 + model ByteSlice { } 163 + model Link { } 164 + model Main { } 165 + model Mention { } 166 + model Tag { } 167 + } 168 + 169 + @external 170 + namespace com.atproto.label.defs { 171 + model Label { } 172 + model LabelValue { } 173 + model LabelValueDefinition { } 174 + model LabelValueDefinitionStrings { } 175 + model SelfLabel { } 176 + model SelfLabels { } 177 + } 178 + 179 + @external 180 + namespace com.atproto.repo.applyWrites { 181 + model Create { } 182 + model CreateResult { } 183 + model Delete { } 184 + model DeleteResult { } 185 + model Update { } 186 + model UpdateResult { } 187 + } 188 + 189 + @external 190 + namespace com.atproto.repo.createRecord { 191 + } 192 + 193 + @external 194 + namespace com.atproto.repo.defs { 195 + model CommitMeta { } 196 + } 197 + 198 + @external 199 + namespace com.atproto.repo.deleteRecord { 200 + } 201 + 202 + @external 203 + namespace com.atproto.repo.describeRepo { 204 + } 205 + 206 + @external 207 + namespace com.atproto.repo.getRecord { 208 + } 209 + 210 + @external 211 + namespace com.atproto.repo.importRepo { 212 + } 213 + 214 + @external 215 + namespace com.atproto.repo.listMissingBlobs { 216 + model RecordBlob { } 217 + } 218 + 219 + @external 220 + namespace com.atproto.repo.listRecords { 221 + model Record { } 222 + } 223 + 224 + @external 225 + namespace com.atproto.repo.putRecord { 226 + } 227 + 228 + @external 229 + namespace com.atproto.repo.strongRef { 230 + model Main { } 231 + } 232 + 233 + @external 234 + namespace com.atproto.repo.uploadBlob { 235 + }
+46 -122
packages/example/typelex/main.tsp
··· 1 1 import "@typelex/emitter"; 2 - 3 - // Example showing typelex as source of truth for atproto lexicons 4 - 5 - // ============ Common Types ============ 6 - 7 - namespace app.example.defs { 8 - @doc("Type of notification") 9 - union notificationType { 10 - string, 11 - 12 - Like: "like", 13 - Repost: "repost", 14 - Follow: "follow", 15 - Mention: "mention", 16 - Reply: "reply", 17 - } 18 - 19 - @doc("Reference to a post") 20 - model PostRef { 21 - @doc("AT URI of the post") 22 - @required 23 - uri: string; 24 - 25 - @doc("CID of the post") 26 - @required 27 - cid: string; 28 - } 29 - 30 - @doc("Reference to a parent post in a reply chain") 31 - model ReplyRef { 32 - @doc("Root post in the thread") 33 - @required 34 - root: PostRef; 35 - 36 - @doc("Direct parent post being replied to") 37 - @required 38 - parent: PostRef; 39 - } 40 - 41 - @doc("Text entity (mention, link, or tag)") 42 - model Entity { 43 - @doc("Start index in text") 44 - @required 45 - start: int32; 2 + import "./externals.tsp"; 46 3 47 - @doc("End index in text") 48 - @required 49 - end: int32; 4 + namespace xyz.statusphere.defs { 5 + model StatusView { 6 + @required uri: atUri; 50 7 51 - @doc("Entity type") 52 8 @required 53 - type: string; 9 + @minLength(1) 10 + @maxGraphemes(1) 11 + @maxLength(32) 12 + status: string; 54 13 55 - @doc("Entity value (handle, URL, or tag)") 56 - @required 57 - value: string; 14 + @required createdAt: datetime; 15 + @required profile: ProfileView; 58 16 } 59 - } 60 17 61 - // ============ Records ============ 62 - 63 - namespace app.example.post { 64 - @rec("tid") 65 - @doc("A post in the feed") 66 - model Main { 67 - @doc("Post text content") 68 - @required 69 - text: string; 70 - 71 - @doc("Creation timestamp") 72 - @required 73 - createdAt: datetime; 74 - 75 - @doc("Languages the post is written in") 76 - langs?: string[]; 77 - 78 - @doc("Referenced entities in the post") 79 - entities?: app.example.defs.Entity[]; 80 - 81 - @doc("Post the user is replying to") 82 - reply?: app.example.defs.ReplyRef; 18 + model ProfileView { 19 + @required did: did; 20 + @required handle: handle; 83 21 } 84 22 } 85 23 86 - namespace app.example.follow { 24 + namespace xyz.statusphere.status { 87 25 @rec("tid") 88 - @doc("A follow relationship") 89 26 model Main { 90 - @doc("DID of the account being followed") 91 27 @required 92 - subject: string; 28 + @minLength(1) 29 + @maxGraphemes(1) 30 + @maxLength(32) 31 + status: string; 93 32 94 - @doc("When the follow was created") 95 - @required 96 - createdAt: datetime; 33 + @required createdAt: datetime; 97 34 } 98 35 } 99 36 100 - namespace app.example.like { 101 - @rec("tid") 102 - @doc("A like on a post") 103 - model Main { 104 - @doc("Post being liked") 105 - @required 106 - subject: app.example.defs.PostRef; 107 - 108 - @doc("When the like was created") 109 - @required 110 - createdAt: datetime; 111 - } 37 + namespace xyz.statusphere.sendStatus { 38 + @procedure 39 + @doc("Send a status into the ATmosphere.") 40 + op main( 41 + input: { 42 + @required 43 + @minLength(1) 44 + @maxGraphemes(1) 45 + @maxLength(32) 46 + status: string; 47 + }, 48 + ): { 49 + @required status: xyz.statusphere.defs.StatusView; 50 + }; 112 51 } 113 52 114 - namespace app.example.repost { 115 - @rec("tid") 116 - @doc("A repost of another post") 117 - model Main { 118 - @doc("Post being reposted") 119 - @required 120 - subject: app.example.defs.PostRef; 121 - 122 - @doc("When the repost was created") 123 - @required 124 - createdAt: datetime; 125 - } 53 + namespace xyz.statusphere.getStatuses { 54 + @query 55 + @doc("Get a list of the most recent statuses on the network.") 56 + op main(@minValue(1) @maxValue(100) limit?: integer = 50): { 57 + @required statuses: xyz.statusphere.defs.StatusView[]; 58 + }; 126 59 } 127 60 128 - namespace app.example.profile { 129 - @rec("self") 130 - @doc("User profile information") 131 - model Main { 132 - @doc("Display name") 133 - displayName?: string; 134 - 135 - @doc("Profile description") 136 - description?: string; 137 - 138 - @doc("Profile avatar image") 139 - avatar?: string; 140 - 141 - @doc("Profile banner image") 142 - banner?: string; 143 - } 61 + namespace xyz.statusphere.getUser { 62 + @query 63 + @doc("Get the current user's profile and status.") 64 + op main(): { 65 + @required profile: app.bsky.actor.defs.ProfileView; 66 + status?: xyz.statusphere.defs.StatusView; 67 + }; 144 68 }
+46 -3
pnpm-lock.yaml
··· 12 12 specifier: ^5.0.0 13 13 version: 5.9.3 14 14 15 + packages/cli: 16 + dependencies: 17 + '@typelex/emitter': 18 + specifier: ^0.2.0 19 + version: 0.2.0(@typespec/compiler@1.4.0(@types/node@20.19.19)) 20 + '@typespec/compiler': 21 + specifier: ^1.4.0 22 + version: 1.4.0(@types/node@20.19.19) 23 + yargs: 24 + specifier: ^18.0.0 25 + version: 18.0.0 26 + devDependencies: 27 + '@types/node': 28 + specifier: ^20.0.0 29 + version: 20.19.19 30 + '@types/yargs': 31 + specifier: ^17.0.33 32 + version: 17.0.33 33 + typescript: 34 + specifier: ^5.0.0 35 + version: 5.9.3 36 + 15 37 packages/emitter: 16 38 dependencies: 17 39 '@typespec/compiler': ··· 48 70 '@atproto/xrpc-server': 49 71 specifier: ^0.9.5 50 72 version: 0.9.5 73 + '@typelex/cli': 74 + specifier: workspace:* 75 + version: link:../cli 51 76 '@typelex/emitter': 52 77 specifier: workspace:* 53 78 version: link:../emitter 54 - '@typespec/compiler': 55 - specifier: ^1.4.0 56 - version: 1.4.0(@types/node@20.19.19) 57 79 devDependencies: 58 80 typescript: 59 81 specifier: ^5.0.0 ··· 1645 1667 '@ts-morph/common@0.25.0': 1646 1668 resolution: {integrity: sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==} 1647 1669 1670 + '@typelex/emitter@0.2.0': 1671 + resolution: {integrity: sha512-4Iw6VAnd9nCFGOkJcu9utWdmu9ZyPeAb1QX/B7KerGBmfc2FuIDqgZZ/mZ6c56atcZd62pb2oYF/3RgSFhEsoQ==} 1672 + peerDependencies: 1673 + '@typespec/compiler': ^1.4.0 1674 + 1648 1675 '@types/babel__core@7.20.5': 1649 1676 resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 1650 1677 ··· 1705 1732 1706 1733 '@types/unist@3.0.3': 1707 1734 resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} 1735 + 1736 + '@types/yargs-parser@21.0.3': 1737 + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} 1738 + 1739 + '@types/yargs@17.0.33': 1740 + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} 1708 1741 1709 1742 '@typespec/asset-emitter@0.74.0': 1710 1743 resolution: {integrity: sha512-DWIdlSNhRgBeZ8exfqubfUn0H6mRg4gr0s7zLTdBMUEDHL3Yh0ljnRPkd8AXTZhoW3maTFT69loWTrqx09T5oQ==} ··· 7411 7444 path-browserify: 1.0.1 7412 7445 tinyglobby: 0.2.15 7413 7446 7447 + '@typelex/emitter@0.2.0(@typespec/compiler@1.4.0(@types/node@20.19.19))': 7448 + dependencies: 7449 + '@typespec/compiler': 1.4.0(@types/node@20.19.19) 7450 + 7414 7451 '@types/babel__core@7.20.5': 7415 7452 dependencies: 7416 7453 '@babel/parser': 7.28.4 ··· 7482 7519 csstype: 3.1.3 7483 7520 7484 7521 '@types/unist@3.0.3': {} 7522 + 7523 + '@types/yargs-parser@21.0.3': {} 7524 + 7525 + '@types/yargs@17.0.33': 7526 + dependencies: 7527 + '@types/yargs-parser': 21.0.3 7485 7528 7486 7529 '@typespec/asset-emitter@0.74.0(@typespec/compiler@1.4.0(@types/node@20.19.19))': 7487 7530 dependencies: