An experimental TypeSpec syntax for Lexicon

add typelex init

Changed files
+163 -3
packages
cli
src
commands
example
+9 -1
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"; 5 6 6 7 async function main() { 7 8 await yargs(hideBin(process.argv)) 8 9 .scriptName("typelex") 9 - .usage("$0 compile <namespace>") 10 + .command( 11 + "init", 12 + "Initialize a new typelex project", 13 + () => {}, 14 + async () => { 15 + await initCommand(); 16 + } 17 + ) 10 18 .command( 11 19 "compile <namespace>", 12 20 "Compile TypeSpec files to Lexicon JSON",
+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:lexicons && pnpm run build:codegen", 8 - "build:lexicons": "typelex compile xyz.statusphere.*", 7 + "build": "pnpm run build:typelex && pnpm run build:codegen", 8 + "build:typelex": "typelex compile xyz.statusphere.*", 9 9 "build:codegen": "lex gen-server --yes ./src lexicons/xyz/statusphere/*.json" 10 10 }, 11 11 "dependencies": {