An experimental TypeSpec syntax for Lexicon

Compare changes

Choose any two refs to compare.

Changed files
+7 -184
packages
cli
src
commands
example
+1
packages/cli/package.json
··· 1 1 { 2 2 "name": "@typelex/cli", 3 3 "version": "0.2.0", 4 + "description": "CLI for typelex - TypeSpec-based IDL for ATProto Lexicons", 4 5 "main": "dist/index.js", 5 6 "type": "module", 6 7 "bin": {
+4 -30
packages/cli/src/cli.ts
··· 2 2 import yargs from "yargs"; 3 3 import { hideBin } from "yargs/helpers"; 4 4 import { compileCommand } from "./commands/compile.js"; 5 - import { initCommand } from "./commands/init.js"; 6 5 7 6 async function main() { 8 7 await yargs(hideBin(process.argv)) 9 8 .scriptName("typelex") 10 - .command( 11 - "init", 12 - "Initialize a new typelex project", 13 - () => {}, 14 - async () => { 15 - await initCommand(); 16 - } 17 - ) 9 + .usage("$0 compile <namespace>") 18 10 .command( 19 11 "compile <namespace>", 20 12 "Compile TypeSpec files to Lexicon JSON", 21 13 (yargs) => { 22 14 return yargs 23 15 .positional("namespace", { 24 - describe: "Primary namespace pattern (e.g., com.example.*)", 16 + describe: "Primary namespace pattern (e.g., app.bsky.*)", 25 17 type: "string", 26 18 demandOption: true, 27 19 }) 28 20 .option("out", { 29 - describe: "Output directory for generated Lexicon files (must end with 'lexicons')", 21 + describe: "Output directory for generated Lexicon files (relative to cwd)", 30 22 type: "string", 31 23 default: "./lexicons", 32 24 }); 33 25 }, 34 26 async (argv) => { 35 - if (!argv.namespace) { 36 - console.error("Error: namespace is required"); 37 - console.error("Usage: typelex compile <namespace>"); 38 - console.error("Example: typelex compile com.example.*"); 39 - process.exit(1); 40 - } 41 - 42 - if (!argv.namespace.endsWith(".*")) { 43 - console.error("Error: namespace must end with .*"); 44 - console.error(`Got: ${argv.namespace}`); 45 - console.error("Example: typelex compile com.example.*"); 46 - process.exit(1); 47 - } 48 - 49 27 const options: Record<string, unknown> = {}; 50 28 if (argv.watch) { 51 29 options.watch = true; ··· 61 39 type: "boolean", 62 40 default: false, 63 41 }) 64 - .demandCommand(1) 42 + .demandCommand(1, "You must specify a command") 65 43 .help() 66 44 .version() 67 45 .fail((msg, err) => { 68 46 if (err) { 69 47 console.error(err); 70 - } else if (msg.includes("Not enough non-option arguments")) { 71 - console.error("Error: namespace is required"); 72 - console.error("Usage: typelex compile <namespace>"); 73 - console.error("Example: typelex compile com.example.*"); 74 48 } else { 75 49 console.error(msg); 76 50 }
-152
packages/cli/src/commands/init.ts
··· 1 - import { resolve } from "path"; 2 - import { mkdir, writeFile, readFile, access } from "fs/promises"; 3 - import { spawn } from "child_process"; 4 - import { createInterface } from "readline"; 5 - 6 - function createMainTemplate(namespace: string): string { 7 - return `import "@typelex/emitter"; 8 - import "./externals.tsp"; 9 - 10 - namespace ${namespace}.post { 11 - @rec("tid") 12 - model Main { 13 - @required text: string; 14 - @required createdAt: datetime; 15 - } 16 - } 17 - `; 18 - } 19 - 20 - const EXTERNALS_TSP_TEMPLATE = `import "@typelex/emitter"; 21 - 22 - // Generated by typelex 23 - // This file is auto-generated. Do not edit manually. 24 - `; 25 - 26 - async function promptNamespace(): Promise<string> { 27 - const rl = createInterface({ 28 - input: process.stdin, 29 - output: process.stdout, 30 - }); 31 - 32 - return new Promise((resolve) => { 33 - rl.question("Namespace (e.g., com.example.*): ", (answer) => { 34 - rl.close(); 35 - resolve(answer.trim()); 36 - }); 37 - }); 38 - } 39 - 40 - /** 41 - * Initialize a new typelex project 42 - */ 43 - export async function initCommand(): Promise<void> { 44 - const cwd = process.cwd(); 45 - const typelexDir = resolve(cwd, "typelex"); 46 - const mainTspPath = resolve(typelexDir, "main.tsp"); 47 - const externalsTspPath = resolve(typelexDir, "externals.tsp"); 48 - 49 - console.log("Initializing typelex project...\n"); 50 - 51 - // Prompt for namespace 52 - let namespace = await promptNamespace(); 53 - 54 - // Validate namespace format 55 - while (!namespace.endsWith(".*")) { 56 - console.error(`Error: namespace must end with .*`); 57 - console.error(`Got: ${namespace}\n`); 58 - namespace = await promptNamespace(); 59 - } 60 - 61 - // Remove the .* suffix for use in template 62 - const namespacePrefix = namespace.slice(0, -2); 63 - 64 - // Create typelex directory 65 - await mkdir(typelexDir, { recursive: true }); 66 - 67 - // Check if main.tsp exists and is non-empty 68 - let shouldCreateMain = true; 69 - try { 70 - await access(mainTspPath); 71 - const content = await readFile(mainTspPath, "utf-8"); 72 - if (content.trim().length > 0) { 73 - console.log("✓ typelex/main.tsp already exists, skipping"); 74 - shouldCreateMain = false; 75 - } 76 - } catch { 77 - // File doesn't exist, we'll create it 78 - } 79 - 80 - if (shouldCreateMain) { 81 - await writeFile(mainTspPath, createMainTemplate(namespacePrefix), "utf-8"); 82 - console.log("✓ Created typelex/main.tsp"); 83 - } 84 - 85 - // Always create/overwrite externals.tsp 86 - await writeFile(externalsTspPath, EXTERNALS_TSP_TEMPLATE, "utf-8"); 87 - console.log("✓ Created typelex/externals.tsp"); 88 - 89 - // Add build script to package.json 90 - const packageJsonPath = resolve(cwd, "package.json"); 91 - try { 92 - const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8")); 93 - if (!packageJson.scripts) { 94 - packageJson.scripts = {}; 95 - } 96 - if (!packageJson.scripts["build:typelex"]) { 97 - packageJson.scripts["build:typelex"] = `typelex compile ${namespace}`; 98 - await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8"); 99 - console.log("✓ Added build:typelex script to package.json"); 100 - } else { 101 - console.log("✓ build:typelex script already exists in package.json"); 102 - } 103 - } catch (err) { 104 - console.warn("⚠ Could not update package.json:", (err as Error).message); 105 - } 106 - 107 - // Install dependencies 108 - console.log("\nInstalling dependencies..."); 109 - 110 - // Detect package manager 111 - let packageManager = "npm"; 112 - try { 113 - await access(resolve(cwd, "pnpm-lock.yaml")); 114 - packageManager = "pnpm"; 115 - } catch { 116 - try { 117 - await access(resolve(cwd, "yarn.lock")); 118 - packageManager = "yarn"; 119 - } catch { 120 - // Default to npm 121 - } 122 - } 123 - 124 - return new Promise((resolvePromise, reject) => { 125 - const args = packageManager === "npm" 126 - ? ["install", "--save-dev", "@typelex/cli", "@typelex/emitter"] 127 - : ["add", "-D", "@typelex/cli", "@typelex/emitter"]; 128 - 129 - const install = spawn(packageManager, args, { 130 - cwd, 131 - stdio: "inherit", 132 - }); 133 - 134 - install.on("close", (code) => { 135 - if (code === 0) { 136 - console.log("\n✓ Installed @typelex/cli and @typelex/emitter"); 137 - console.log("\nTypelex initialized successfully!"); 138 - console.log("\nNext steps:"); 139 - console.log(" 1. Edit typelex/main.tsp to define your lexicons"); 140 - console.log(" 2. Run: npm run build:typelex"); 141 - resolvePromise(); 142 - } else { 143 - process.exit(code ?? 1); 144 - } 145 - }); 146 - 147 - install.on("error", (err) => { 148 - console.error("Failed to install dependencies:", err); 149 - reject(err); 150 - }); 151 - }); 152 - }
+2 -2
packages/example/package.json
··· 4 4 "private": true, 5 5 "type": "module", 6 6 "scripts": { 7 - "build": "pnpm run build:typelex && pnpm run build:codegen", 8 - "build:typelex": "typelex compile xyz.statusphere.*", 7 + "build": "pnpm run build:lexicons && pnpm run build:codegen", 8 + "build:lexicons": "typelex compile xyz.statusphere.*", 9 9 "build:codegen": "lex gen-server --yes ./src lexicons/xyz/statusphere/*.json" 10 10 }, 11 11 "dependencies": {