An experimental TypeSpec syntax for Lexicon
56
fork

Configure Feed

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

bla

+134 -8
+14 -8
typelex-emitter/src/emitter.ts
··· 96 96 97 97 // For now, assume the first model in a lexicon is the main one 98 98 if (Object.keys(lexicon.defs).length === 0) { 99 - lexicon.defs.main = { 99 + // Check if this is the lexicon schema special case 100 + const key = lexiconId === "com.atproto.lexicon.schema" ? "nsid" : "tid"; 101 + 102 + const recordDef: any = { 100 103 type: "record", 101 - key: "tid", 104 + key: key, 102 105 record: modelDef, 103 106 }; 107 + 108 + // Move description from record object to record def 109 + const description = getDoc(this.program, model); 110 + if (description) { 111 + recordDef.description = description; 112 + delete modelDef.description; 113 + } 114 + 115 + lexicon.defs.main = recordDef; 104 116 } else { 105 117 lexicon.defs[defName] = modelDef; 106 - } 107 - 108 - // Set description if available 109 - const description = getDoc(this.program, model); 110 - if (description && !lexicon.description) { 111 - lexicon.description = description; 112 118 } 113 119 } 114 120
+102
typelex-emitter/test/fixtures.test.ts
··· 1 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 2 + import { exec } from "child_process"; 3 + import { promisify } from "util"; 4 + import { readFile, rm, mkdir, writeFile, readdir, stat } from "fs/promises"; 5 + import { join, relative, dirname } from "path"; 6 + import { fileURLToPath } from "url"; 7 + import { randomBytes } from "crypto"; 8 + import { tmpdir } from "os"; 9 + 10 + const execAsync = promisify(exec); 11 + const __filename = fileURLToPath(import.meta.url); 12 + const __dirname = dirname(__filename); 13 + 14 + describe("Fixtures Tests", { timeout: 30000 }, () => { 15 + const fixturesDir = join(__dirname, "fixtures"); 16 + const inputDir = join(fixturesDir, "input"); 17 + const outputDir = join(fixturesDir, "output"); 18 + 19 + let tempDir: string; 20 + 21 + beforeEach(async () => { 22 + // Create a unique temp directory for each test 23 + tempDir = join(tmpdir(), `typelex-test-${randomBytes(8).toString('hex')}`); 24 + await mkdir(tempDir, { recursive: true }); 25 + }); 26 + 27 + afterEach(async () => { 28 + // Always clean up temp directory 29 + if (tempDir) { 30 + await rm(tempDir, { recursive: true, force: true }).catch(() => { 31 + // Ignore errors during cleanup 32 + }); 33 + } 34 + }); 35 + 36 + async function findAllTspFiles(dir: string): Promise<string[]> { 37 + const files: string[] = []; 38 + 39 + async function walk(currentDir: string) { 40 + const entries = await readdir(currentDir); 41 + 42 + for (const entry of entries) { 43 + const fullPath = join(currentDir, entry); 44 + const stats = await stat(fullPath); 45 + 46 + if (stats.isDirectory()) { 47 + await walk(fullPath); 48 + } else if (entry.endsWith('.tsp')) { 49 + files.push(fullPath); 50 + } 51 + } 52 + } 53 + 54 + await walk(dir); 55 + return files; 56 + } 57 + 58 + it("should compile com.atproto.lexicon.schema correctly", async () => { 59 + const tspFile = join(inputDir, "com/atproto/lexicon/schema.tsp"); 60 + const expectedJsonFile = join(outputDir, "com/atproto/lexicon/schema.json"); 61 + 62 + // Write config to temp directory 63 + const configPath = join(tempDir, "tspconfig.yaml"); 64 + const config = ` 65 + emit: 66 + - "@typelex/emitter" 67 + options: 68 + "@typelex/emitter": 69 + output-dir: "./output" 70 + `; 71 + await writeFile(configPath, config); 72 + 73 + // Compile the TypeSpec file 74 + try { 75 + const { stdout, stderr } = await execAsync( 76 + `npx -p @typespec/compiler tsp compile ${tspFile} --config ${configPath}`, 77 + { cwd: tempDir, timeout: 20000 } 78 + ); 79 + 80 + console.log("Compilation stdout:", stdout); 81 + if (stderr) console.log("Compilation stderr:", stderr); 82 + 83 + // Only fail on actual errors, not warnings 84 + if (stdout.includes("Found") && stdout.includes("error")) { 85 + throw new Error(`Compilation failed with errors: ${stdout}\n${stderr}`); 86 + } 87 + } catch (error: any) { 88 + console.error("Compilation error:", error); 89 + throw error; 90 + } 91 + 92 + // Read the generated JSON 93 + const generatedPath = join(tempDir, "output/com/atproto/lexicon/schema.json"); 94 + const generatedJson = JSON.parse(await readFile(generatedPath, "utf-8")); 95 + 96 + // Read the expected JSON 97 + const expectedJson = JSON.parse(await readFile(expectedJsonFile, "utf-8")); 98 + 99 + // Compare 100 + expect(generatedJson).toEqual(expectedJson); 101 + }); 102 + });
+11
typelex-emitter/test/fixtures/input/com/atproto/identity/defs.tsp
··· 1 + namespace com.atproto.identity; 2 + 3 + model IdentityInfo { 4 + did: string; 5 + 6 + @doc("The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document.") 7 + handle: string; 8 + 9 + @doc("The complete DID document for the identity.") 10 + didDoc: unknown; 11 + }
+7
typelex-emitter/test/fixtures/input/com/atproto/lexicon/schema.tsp
··· 1 + namespace com.atproto.lexicon; 2 + 3 + @doc("Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc).") 4 + model Schema { 5 + @doc("Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system.") 6 + lexicon: int32; 7 + }