An experimental TypeSpec syntax for Lexicon

bla

+77 -94
+2 -1
.gitignore
··· 40 40 # npm 41 41 .npm 42 42 43 - .wrangler 43 + .wrangler 44 + .claude
+3 -3
DOCS.md
··· 1 1 # Typelex Docs 2 2 3 - This guide maps atproto Lexicon JSON syntax to typelex (a [TypeSpec](https://typespec.io/) emitter). It assumes you're familiar with Lexicon and want to understand how to express it in TypeSpec. Consult [TypeSpec docs](https://typespec.io/) on details of TypeSpec syntax. 3 + This maps [atproto Lexicon](https://atproto.com/specs/lexicon) JSON syntax to typelex (which is a [TypeSpec](https://typespec.io/) emitter). It assumes you're familiar with Lexicon and want to understand how to express it in TypeSpec. Consult [TypeSpec docs](https://typespec.io/) on details of TypeSpec syntax. 4 4 5 - Btw, this page was mostly written by Claude. I hope it's mostly correct and comprehensible. 5 + This page was mostly written by Claude based on the test fixtures from this repo (which are [deployed in the playground](https://playground.typelex.org/)). I hope it's mostly correct and comprehensible. When in doubt, refer to those fixtures. 6 6 7 7 ## Playground 8 8 9 - Go to https://typelex-playground.pages.dev/ to play with a bunch of lexicons. 9 + Go to https://playground.typelex.org/ to play with a bunch of lexicons. 10 10 11 11 ## Quick Start 12 12
+8 -4
README.md
··· 1 1 # typelex 2 2 3 - An experimental TypeSpec emitter for Lexicon. 3 + An experimental [TypeSpec](https://typespec.io/) emitter for [Lexicon](https://atproto.com/specs/lexicon). 4 4 5 5 See https://typelex.pages.dev/ 6 + 7 + **This is a hobby project but maybe somebody will find it helpful.** 8 + 9 + Design is not final and might change. Ideas welcome. 6 10 7 11 ## Playground 8 12 9 - See https://typelex-playground.pages.dev/ 13 + See https://playground.typelex.org/ 10 14 11 - ## Reference 15 + ## Documentation 12 16 13 - See [DOCS.md](./DOCS.md) 17 + No "proper" docs yet. See [DOCS.md](./DOCS.md) for now. 14 18 15 19 ## License 16 20
+2
packages/emitter/README.md
··· 2 2 3 3 TypeSpec emitter for generating ATProto Lexicon definitions. 4 4 5 + See https://typelex.org/ 6 + 5 7 ## Installation 6 8 7 9 ```bash
+1 -1
packages/playground/README.md
··· 1 - # Typelex Playground 1 + # typelex playground 2 2 3 3 An interactive playground for the Typelex TypeSpec emitter. 4 4
+1 -1
packages/playground/index.html
··· 3 3 <head> 4 4 <meta charset="utf-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 - <title>Typelex Playground</title> 6 + <title>typelex playground</title> 7 7 </head> 8 8 <body> 9 9 <div id="root"></div>
+1 -1
packages/playground/vite.config.ts
··· 7 7 libraries: ["@typespec/compiler", "@typelex/emitter"], 8 8 samples, 9 9 links: { 10 - documentationUrl: "https://tangled.org/@danabra.mov/typlex", 10 + documentationUrl: "https://tangled.org/@danabra.mov/typelex", 11 11 }, 12 12 }); 13 13
+1 -46
packages/website/README.md
··· 1 1 # Typelex Website 2 2 3 - Landing page for typelex - TypeSpec for AT Protocol Lexicons. 4 - 5 - ## Features 6 - 7 - - **Live Examples**: TypeSpec examples are compiled during build to show real Lexicon JSON output 8 - - **Syntax Highlighting**: Using Shiki for beautiful code highlighting 9 - - **Interactive Playground**: Try typelex in the browser (coming soon) 3 + Source for https://typelex.org/ 10 4 11 5 ## Development 12 6 ··· 17 11 18 12 ## Building 19 13 20 - The build process automatically: 21 - 1. Compiles TypeSpec examples **in memory** during the Astro build 22 - 2. Reads TypeSpec source files 23 - 3. Syntax-highlights and displays them side-by-side with generated JSON 24 - 25 14 ```sh 26 15 pnpm run build 27 16 ``` 28 17 29 - No intermediate files are generated - everything happens in memory! 30 - 31 - ## Adding Examples 32 - 33 - 1. Create a new `.tsp` file in `src/examples/` 34 - 2. Add it to the examples array in `src/pages/index.astro` 35 - 3. Run `pnpm run build` to see it on the site 36 - 37 - ## Commands 38 - 39 - | Command | Action | 40 - | :------------------------ | :----------------------------------------------- | 41 - | `pnpm install` | Installs dependencies | 42 - | `pnpm dev` | Starts local dev server at `localhost:4321` | 43 - | `pnpm build` | Build your production site to `./dist/` | 44 - | `pnpm preview` | Preview your build locally, before deploying | 45 - 46 - ## Structure 47 - 48 - ``` 49 - / 50 - ├── src/ 51 - │ ├── examples/ # TypeSpec examples 52 - │ │ ├── profile.tsp 53 - │ │ ├── getQuotes.tsp 54 - │ │ └── ... 55 - │ ├── pages/ 56 - │ │ └── index.astro # Landing page 57 - │ ├── components/ 58 - │ │ └── Playground.tsx 59 - │ └── utils/ 60 - │ └── compile.ts # In-memory TypeSpec compiler 61 - └── package.json 62 - ```
+57 -36
packages/website/src/pages/index.astro
··· 132 132 <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 133 133 <meta name="viewport" content="width=device-width" /> 134 134 <meta name="generator" content={Astro.generator} /> 135 - <title>typelex – TypeSpec for AT protocol Lexicons</title> 135 + <title>typelex – An experimental TypeSpec syntax for Lexicon</title> 136 136 </head> 137 137 <body> 138 138 <div class="container"> 139 139 <header> 140 140 <h1>typelex</h1> 141 - <p class="tagline">An experimental <a href="https://typespec.io" target="_blank" rel="noopener noreferrer">TypeSpec</a> emitter for <a href="https://atproto.com/specs/lexicon" target="_blank" rel="noopener noreferrer">Lexicon</a></p> 141 + <p class="tagline">An experimental <a href="https://typespec.io" target="_blank" rel="noopener noreferrer">TypeSpec</a> syntax for <a href="https://atproto.com/specs/lexicon" target="_blank" rel="noopener noreferrer">Lexicon</a></p> 142 142 143 143 <div class="hero-comparison"> 144 144 <div class="comparison-content"> 145 145 <div class="hero-panel"> 146 146 <h3 class="hero-header"> 147 147 Typelex 148 + <a href={createPlaygroundUrl(`import "@typelex/emitter"; 149 + 150 + namespace app.bsky.actor.profile { 151 + @rec("self") 152 + model Main { 153 + @maxLength(64) 154 + @maxGraphemes(64) 155 + displayName?: string; 156 + 157 + @maxLength(256) 158 + @maxGraphemes(256) 159 + description?: string; 160 + } 161 + }`)} target="_blank" rel="noopener noreferrer" class="code-playground-link" aria-label="Open in playground"> 162 + <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 163 + <path d="M6.5 3.5C6.5 3.22386 6.72386 3 7 3H13C13.2761 3 13.5 3.22386 13.5 3.5V9.5C13.5 9.77614 13.2761 10 13 10C12.7239 10 12.5 9.77614 12.5 9.5V4.70711L6.85355 10.3536C6.65829 10.5488 6.34171 10.5488 6.14645 10.3536C5.95118 10.1583 5.95118 9.84171 6.14645 9.64645L11.7929 4H7C6.72386 4 6.5 3.77614 6.5 3.5Z" fill="currentColor"/> 164 + <path d="M3 5.5C3 4.67157 3.67157 4 4.5 4H5C5.27614 4 5.5 4.22386 5.5 4.5C5.5 4.77614 5.27614 5 5 5H4.5C4.22386 5 4 5.22386 4 5.5V11.5C4 11.7761 4.22386 12 4.5 12H10.5C10.7761 12 11 11.7761 11 11.5V11C11 10.7239 11.2239 10.5 11.5 10.5C11.7761 10.5 12 10.7239 12 11V11.5C12 12.3284 11.3284 13 10.5 13H4.5C3.67157 13 3 12.3284 3 11.5V5.5Z" fill="currentColor"/> 165 + </svg> 166 + </a> 148 167 </h3> 149 168 <div class="hero-code" set:html={await highlightCode(`import "@typelex/emitter"; 150 169 ··· 211 230 212 231 {highlighted.map(({ title, typelexHtml, lexiconHtml, playgroundUrl }) => ( 213 232 <section> 214 - <h2> 215 - {title} 216 - <a href={playgroundUrl} target="_blank" rel="noopener noreferrer" class="playground-link" aria-label="Open in playground"> 217 - <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 218 - <path d="M6.5 3.5C6.5 3.22386 6.72386 3 7 3H13C13.2761 3 13.5 3.22386 13.5 3.5V9.5C13.5 9.77614 13.2761 10 13 10C12.7239 10 12.5 9.77614 12.5 9.5V4.70711L6.85355 10.3536C6.65829 10.5488 6.34171 10.5488 6.14645 10.3536C5.95118 10.1583 5.95118 9.84171 6.14645 9.64645L11.7929 4H7C6.72386 4 6.5 3.77614 6.5 3.5Z" fill="currentColor"/> 219 - <path d="M3 5.5C3 4.67157 3.67157 4 4.5 4H5C5.27614 4 5.5 4.22386 5.5 4.5C5.5 4.77614 5.27614 5 5 5H4.5C4.22386 5 4 5.22386 4 5.5V11.5C4 11.7761 4.22386 12 4.5 12H10.5C10.7761 12 11 11.7761 11 11.5V11C11 10.7239 11.2239 10.5 11.5 10.5C11.7761 10.5 12 10.7239 12 11V11.5C12 12.3284 11.3284 13 10.5 13H4.5C3.67157 13 3 12.3284 3 11.5V5.5Z" fill="currentColor"/> 220 - </svg> 221 - </a> 222 - </h2> 233 + <h2>{title}</h2> 223 234 <div class="comparison"> 224 235 <div class="comparison-content"> 225 236 <div class="code-panel"> 226 237 <h3 class="code-header"> 227 238 Typelex 239 + <a href={playgroundUrl} target="_blank" rel="noopener noreferrer" class="code-playground-link" aria-label="Open in playground"> 240 + <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 241 + <path d="M6.5 3.5C6.5 3.22386 6.72386 3 7 3H13C13.2761 3 13.5 3.22386 13.5 3.5V9.5C13.5 9.77614 13.2761 10 13 10C12.7239 10 12.5 9.77614 12.5 9.5V4.70711L6.85355 10.3536C6.65829 10.5488 6.34171 10.5488 6.14645 10.3536C5.95118 10.1583 5.95118 9.84171 6.14645 9.64645L11.7929 4H7C6.72386 4 6.5 3.77614 6.5 3.5Z" fill="currentColor"/> 242 + <path d="M3 5.5C3 4.67157 3.67157 4 4.5 4H5C5.27614 4 5.5 4.22386 5.5 4.5C5.5 4.77614 5.27614 5 5 5H4.5C4.22386 5 4 5.22386 4 5.5V11.5C4 11.7761 4.22386 12 4.5 12H10.5C10.7761 12 11 11.7761 11 11.5V11C11 10.7239 11.2239 10.5 11.5 10.5C11.7761 10.5 12 10.7239 12 11V11.5C12 12.3284 11.3284 13 10.5 13H4.5C3.67157 13 3 12.3284 3 11.5V5.5Z" fill="currentColor"/> 243 + </svg> 244 + </a> 228 245 </h3> 229 246 <div class="code-block" set:html={typelexHtml} /> 230 247 </div> ··· 309 326 </div> 310 327 </div> 311 328 </section> 329 + 330 + <div class="separator"></div> 312 331 313 332 <footer> 333 + <p>This is my personal hobby project and is not affiliated with AT or endorsed by anyone.</p> 334 + <p>Who knows if this is a good idea?</p> 314 335 </footer> 315 336 </div> 316 337 ··· 929 950 letter-spacing: 0.05em; 930 951 margin: 0; 931 952 color: #94a3b8; 953 + display: flex; 954 + align-items: center; 955 + justify-content: space-between; 956 + } 957 + 958 + .code-playground-link { 959 + display: inline-flex; 960 + align-items: center; 961 + justify-content: center; 962 + color: #94a3b8; 963 + transition: all 0.2s ease; 964 + text-decoration: none; 965 + opacity: 0.4; 966 + padding: 0.125rem; 967 + } 968 + 969 + .code-playground-link:hover { 970 + color: #c7d2fe; 971 + opacity: 1; 972 + } 973 + 974 + .code-playground-link svg { 975 + width: 1rem; 976 + height: 1rem; 932 977 } 933 978 934 979 .code-header a, ··· 1058 1103 max-width: 700px; 1059 1104 margin-left: auto; 1060 1105 margin-right: auto; 1061 - display: flex; 1062 - align-items: center; 1063 - gap: 0.5rem; 1064 - } 1065 - 1066 - .playground-link { 1067 - display: inline-flex; 1068 - align-items: center; 1069 - justify-content: center; 1070 - color: #94a3b8; 1071 - transition: color 0.2s ease, transform 0.2s ease; 1072 - text-decoration: none; 1073 - opacity: 0.6; 1074 - } 1075 - 1076 - .playground-link:hover { 1077 - color: #7a8ef7; 1078 - opacity: 1; 1079 - transform: translateY(-1px); 1080 - } 1081 - 1082 - .playground-link svg { 1083 - width: 1rem; 1084 - height: 1rem; 1085 1106 } 1086 1107 1087 1108 @media (min-width: 768px) { ··· 1094 1115 margin-bottom: 2.5rem; 1095 1116 } 1096 1117 1097 - .playground-link svg { 1118 + .code-playground-link svg { 1098 1119 width: 1.125rem; 1099 1120 height: 1.125rem; 1100 1121 }
+1 -1
packages/website/src/utils/playground-url.ts
··· 1 1 import lzutf8 from 'lzutf8'; 2 2 3 - const PLAYGROUND_BASE_URL = 'https://typelex-playground.pages.dev/'; 3 + const PLAYGROUND_BASE_URL = 'https://playground.typelex.org/'; 4 4 5 5 /** 6 6 * Encodes TypeSpec code to a playground URL using the same compression