An experimental TypeSpec syntax for Lexicon

refactor: rename to typlex and restructure as monorepo

BREAKING CHANGE: Project renamed from typelex to typlex

- Rename @typelex → @typlex (no 'e')
- Restructure to monorepo with packages/ directory
- packages/emitter - @typlex/emitter (TypeSpec emitter)
- packages/cli - @typlex/cli (new CLI tool)
- packages/example - @typlex/example (demo integration)

The CLI can be used before @atproto/lexicon tooling:
1. typlex main.tsp → generates JSON lexicons
2. atproto-lexicon build → validates and generates schemas

All tests passing (22/22)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

+101 -22
+7 -8
package.json
··· 1 1 { 2 - "name": "typelex-monorepo", 2 + "name": "typlex-monorepo", 3 3 "version": "0.1.0", 4 4 "private": true, 5 5 "description": "TypeSpec-based IDL for ATProto Lexicons", 6 6 "workspaces": [ 7 - "typelex-emitter", 8 - "typelex-example" 7 + "packages/*" 9 8 ], 10 9 "scripts": { 11 - "build": "npm run build -w @typelex/emitter", 12 - "test": "npm run test:ci -w @typelex/emitter", 13 - "test:watch": "npm test -w @typelex/emitter", 14 - "example": "npm run build -w typelex-example", 10 + "build": "npm run build -w @typlex/emitter && npm run build -w @typlex/cli", 11 + "test": "npm run test:ci -w @typlex/emitter", 12 + "test:watch": "npm test -w @typlex/emitter", 13 + "example": "npm run build -w @typlex/example", 15 14 "validate": "npm run build && npm run test && npm run example" 16 15 }, 17 16 "repository": { 18 17 "type": "git", 19 - "url": "https://github.com/yourusername/typelex.git" 18 + "url": "https://github.com/yourusername/typlex.git" 20 19 }, 21 20 "keywords": [ 22 21 "typespec",
+26
packages/cli/package.json
··· 1 + { 2 + "name": "@typlex/cli", 3 + "version": "0.1.0", 4 + "description": "CLI to compile TypeSpec lexicons to JSON (can be used before @atproto/lexicon CLI)", 5 + "type": "module", 6 + "bin": { 7 + "typlex": "./dist/cli.js" 8 + }, 9 + "main": "dist/index.js", 10 + "scripts": { 11 + "build": "tsc", 12 + "clean": "rm -rf dist", 13 + "watch": "tsc --watch" 14 + }, 15 + "keywords": ["typespec", "atproto", "lexicon", "bluesky", "cli"], 16 + "author": "", 17 + "license": "MIT", 18 + "dependencies": { 19 + "@typespec/compiler": "^0.64.0", 20 + "@typlex/emitter": "workspace:*" 21 + }, 22 + "devDependencies": { 23 + "@types/node": "^20.0.0", 24 + "typescript": "^5.0.0" 25 + } 26 + }
+43
packages/cli/src/cli.ts
··· 1 + #!/usr/bin/env node 2 + import { compile, CompilerHost, logDiagnostics, NodeHost } from "@typespec/compiler"; 3 + import { fileURLToPath } from "url"; 4 + import { dirname, resolve } from "path"; 5 + 6 + async function main() { 7 + const args = process.argv.slice(2); 8 + 9 + if (args.length === 0) { 10 + console.error("Usage: typlex <path-to-tsp-file>"); 11 + console.error("Compiles TypeSpec lexicon definitions to JSON"); 12 + process.exit(1); 13 + } 14 + 15 + const tspPath = resolve(process.cwd(), args[0]); 16 + const host = NodeHost; 17 + 18 + try { 19 + const program = await compile(host, tspPath, { 20 + emit: ["@typlex/emitter"], 21 + options: { 22 + "@typlex/emitter": { 23 + "output-dir": resolve(process.cwd(), "lexicons"), 24 + }, 25 + }, 26 + }); 27 + 28 + if (program.diagnostics.length > 0) { 29 + logDiagnostics(program.diagnostics, host.logSink); 30 + } 31 + 32 + if (program.diagnostics.some((d) => d.severity === "error")) { 33 + process.exit(1); 34 + } 35 + 36 + console.log("✓ Lexicons compiled successfully to ./lexicons"); 37 + } catch (error) { 38 + console.error("Error compiling lexicons:", error); 39 + process.exit(1); 40 + } 41 + } 42 + 43 + main();
+1
packages/cli/src/index.ts
··· 1 + export { compile } from "@typespec/compiler";
+8
packages/cli/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "outDir": "dist", 5 + "rootDir": "src" 6 + }, 7 + "include": ["src/**/*"] 8 + }
+2
packages/example/.atprotorc.yaml
··· 1 + lexicons: 2 + - lexicons/**/*.json
typelex-emitter/.gitignore packages/emitter/.gitignore
typelex-emitter/README.md packages/emitter/README.md
typelex-emitter/lib/decorators.tsp packages/emitter/lib/decorators.tsp
typelex-emitter/lib/main.tsp packages/emitter/lib/main.tsp
typelex-emitter/package-lock.json packages/emitter/package-lock.json
typelex-emitter/package.json packages/emitter/package.json
typelex-emitter/src/decorators.ts packages/emitter/src/decorators.ts
typelex-emitter/src/emitter.ts packages/emitter/src/emitter.ts
typelex-emitter/src/index.ts packages/emitter/src/index.ts
typelex-emitter/src/types.ts packages/emitter/src/types.ts
typelex-emitter/test/scenarios.test.ts packages/emitter/test/scenarios.test.ts
+1 -1
typelex-emitter/test/scenarios/app-bsky-embed-defs/input/main.tsp packages/emitter/test/scenarios/app-bsky-embed-defs/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace app.bsky.embed { 4 4 // Description goes on the model for defs, unlike standalone lexicons where it goes at lexicon level
typelex-emitter/test/scenarios/app-bsky-embed-defs/output/app/bsky/embed/defs.json packages/emitter/test/scenarios/app-bsky-embed-defs/output/app/bsky/embed/defs.json
+1 -1
typelex-emitter/test/scenarios/app-bsky-embed-external/input/main.tsp packages/emitter/test/scenarios/app-bsky-embed-external/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace app.bsky.embed.external { 4 4 @doc("A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).")
typelex-emitter/test/scenarios/app-bsky-embed-external/output/app/bsky/embed/external.json packages/emitter/test/scenarios/app-bsky-embed-external/output/app/bsky/embed/external.json
+1 -1
typelex-emitter/test/scenarios/app-bsky-embed-images/input/main.tsp packages/emitter/test/scenarios/app-bsky-embed-images/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace app.bsky.embed { 4 4 @doc("width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.")
typelex-emitter/test/scenarios/app-bsky-embed-images/output/app/bsky/embed/defs.json packages/emitter/test/scenarios/app-bsky-embed-images/output/app/bsky/embed/defs.json
typelex-emitter/test/scenarios/app-bsky-embed-images/output/app/bsky/embed/images.json packages/emitter/test/scenarios/app-bsky-embed-images/output/app/bsky/embed/images.json
+1 -1
typelex-emitter/test/scenarios/atproto-repo-strongref/input/main.tsp packages/emitter/test/scenarios/atproto-repo-strongref/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace com.atproto.repo { 4 4 @lexiconMain
typelex-emitter/test/scenarios/atproto-repo-strongref/output/com/atproto/repo/strongRef.json packages/emitter/test/scenarios/atproto-repo-strongref/output/com/atproto/repo/strongRef.json
+1 -1
typelex-emitter/test/scenarios/com-atproto-identity-defs/input/main.tsp packages/emitter/test/scenarios/com-atproto-identity-defs/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace com.atproto.identity { 4 4 model IdentityInfo {
typelex-emitter/test/scenarios/com-atproto-identity-defs/output/com/atproto/identity/defs.json packages/emitter/test/scenarios/com-atproto-identity-defs/output/com/atproto/identity/defs.json
+1 -1
typelex-emitter/test/scenarios/com-atproto-repo-defs/input/main.tsp packages/emitter/test/scenarios/com-atproto-repo-defs/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace com.atproto.repo { 4 4 model CommitMeta {
typelex-emitter/test/scenarios/com-atproto-repo-defs/output/com/atproto/repo/defs.json packages/emitter/test/scenarios/com-atproto-repo-defs/output/com/atproto/repo/defs.json
+1 -1
typelex-emitter/test/scenarios/com-atproto-repo-strongRef/input/main.tsp packages/emitter/test/scenarios/com-atproto-repo-strongRef/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace com.atproto.repo { 4 4 @lexiconMain
typelex-emitter/test/scenarios/com-atproto-repo-strongRef/output/com/atproto/repo/strongRef.json packages/emitter/test/scenarios/com-atproto-repo-strongRef/output/com/atproto/repo/strongRef.json
+1 -1
typelex-emitter/test/scenarios/com-atproto-server-defs/input/main.tsp packages/emitter/test/scenarios/com-atproto-server-defs/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace com.atproto.server { 4 4 model InviteCode {
typelex-emitter/test/scenarios/com-atproto-server-defs/output/com/atproto/server/defs.json packages/emitter/test/scenarios/com-atproto-server-defs/output/com/atproto/server/defs.json
+1 -1
typelex-emitter/test/scenarios/constraints/input/main.tsp packages/emitter/test/scenarios/constraints/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace test.ns { 4 4 model Post {
typelex-emitter/test/scenarios/constraints/output/test/ns/defs.json packages/emitter/test/scenarios/constraints/output/test/ns/defs.json
+1 -1
typelex-emitter/test/scenarios/identity-defs/input/main.tsp packages/emitter/test/scenarios/identity-defs/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace com.atproto.identity; 4 4
typelex-emitter/test/scenarios/identity-defs/output/com/atproto/identity/defs.json packages/emitter/test/scenarios/identity-defs/output/com/atproto/identity/defs.json
+1 -1
typelex-emitter/test/scenarios/repo-defs/input/main.tsp packages/emitter/test/scenarios/repo-defs/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 namespace com.atproto.repo; 4 4
typelex-emitter/test/scenarios/repo-defs/output/com/atproto/repo/defs.json packages/emitter/test/scenarios/repo-defs/output/com/atproto/repo/defs.json
+1 -1
typelex-emitter/test/scenarios/same-namespace-ref/input/main.tsp packages/emitter/test/scenarios/same-namespace-ref/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 // Multiple models in same namespace - refs should use # format 4 4 namespace app.bsky.feed {
typelex-emitter/test/scenarios/same-namespace-ref/output/app/bsky/feed/defs.json packages/emitter/test/scenarios/same-namespace-ref/output/app/bsky/feed/defs.json
+1 -1
typelex-emitter/test/scenarios/simple-ref/input/main.tsp packages/emitter/test/scenarios/simple-ref/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 // Define a simple model that will be referenced 4 4 namespace com.atproto.repo {
typelex-emitter/test/scenarios/simple-ref/output/app/bsky/feed/defs.json packages/emitter/test/scenarios/simple-ref/output/app/bsky/feed/defs.json
typelex-emitter/test/scenarios/simple-ref/output/com/atproto/repo/defs.json packages/emitter/test/scenarios/simple-ref/output/com/atproto/repo/defs.json
+1 -1
typelex-emitter/test/scenarios/union-same-ns/input/main.tsp packages/emitter/test/scenarios/union-same-ns/input/main.tsp
··· 1 - import "@typelex/emitter"; 1 + import "@typlex/emitter"; 2 2 3 3 // Union with types in same namespace - should use # refs 4 4 namespace app.bsky.feed {
typelex-emitter/test/scenarios/union-same-ns/output/app/bsky/feed/defs.json packages/emitter/test/scenarios/union-same-ns/output/app/bsky/feed/defs.json
typelex-emitter/test/smoke.test.ts packages/emitter/test/smoke.test.ts
typelex-emitter/test/transform.test.ts packages/emitter/test/transform.test.ts
typelex-emitter/test/unit.test.ts packages/emitter/test/unit.test.ts
typelex-emitter/tsconfig.json packages/emitter/tsconfig.json
typelex-emitter/vitest.config.ts packages/emitter/vitest.config.ts
typelex-example/.gitignore packages/example/.gitignore
typelex-example/README.md packages/example/README.md
typelex-example/main.tsp packages/example/main.tsp
typelex-example/package-lock.json packages/example/package-lock.json
typelex-example/package.json packages/example/package.json
typelex-example/tspconfig.yaml packages/example/tspconfig.yaml