An experimental TypeSpec syntax for Lexicon

cool

+127 -47
+3 -1
DOCS.md
··· 1 1 # typelex docs 2 2 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. 3 + typelex is a [TypeSpec](https://typespec.io/) emitter targeting [atproto Lexicon](https://atproto.com/specs/lexicon) JSON as the output format. 4 + 5 + This page 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 6 5 7 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 8
+2 -2
README.md
··· 2 2 3 3 An experimental [TypeSpec](https://typespec.io/) syntax for [Lexicon](https://atproto.com/specs/lexicon). 4 4 5 - See https://typelex.pages.dev/ 5 + See https://typelex.org/ 6 6 7 - **This is a hobby project but maybe somebody will find it helpful.** 7 + **This is an early-stage experiment. It’s probably buggy as hell.** 8 8 9 9 Design is not final and might change. Ideas welcome. 10 10
+3 -2
packages/emitter/package.json
··· 1 1 { 2 2 "name": "@typelex/emitter", 3 - "version": "0.1.0", 3 + "version": "0.1.1", 4 4 "description": "TypeSpec emitter for ATProto Lexicon definitions", 5 5 "main": "dist/index.js", 6 6 "type": "module", 7 7 "files": [ 8 8 "lib", 9 - "src" 9 + "src", 10 + "dist" 10 11 ], 11 12 "exports": { 12 13 ".": {
+119 -42
packages/website/src/pages/index.astro
··· 130 130 <head> 131 131 <meta charset="utf-8" /> 132 132 <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 133 - <meta name="viewport" content="width=device-width" /> 133 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 134 134 <meta name="generator" content={Astro.generator} /> 135 135 <title>typelex – An experimental TypeSpec syntax for Lexicon</title> 136 136 <meta name="description" content="An experimental TypeSpec syntax for AT Protocol Lexicons. Write Lexicons in a more readable syntax using TypeSpec." /> ··· 150 150 <meta property="twitter:image" content="https://typelex.org/og.png" /> 151 151 </head> 152 152 <body> 153 - <div class="container"> 153 + <main class="container"> 154 154 <header> 155 155 <h1>typelex</h1> 156 156 <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> 157 157 158 - <div class="hero-comparison"> 158 + <figure class="hero-comparison"> 159 159 <div class="comparison-content"> 160 160 <div class="hero-panel"> 161 - <h3 class="hero-header"> 161 + <p class="hero-header"> 162 162 Typelex 163 163 <a href={createPlaygroundUrl(`import "@typelex/emitter"; 164 164 ··· 179 179 <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"/> 180 180 </svg> 181 181 </a> 182 - </h3> 182 + </p> 183 183 <div class="hero-code" set:html={await highlightCode(`import "@typelex/emitter"; 184 184 185 185 namespace app.bsky.actor.profile { ··· 196 196 }`, 'typespec')} /> 197 197 </div> 198 198 <div class="hero-panel"> 199 - <h3 class="hero-header"> 199 + <p class="hero-header"> 200 200 Lexicon 201 - </h3> 201 + </p> 202 202 <div class="hero-code" set:html={await highlightCode(stringify({ 203 203 "lexicon": 1, 204 204 "id": "app.bsky.actor.profile", ··· 226 226 }, { maxLength: 50 }), 'json')} /> 227 227 </div> 228 228 </div> 229 - </div> 229 + </figure> 230 230 231 231 <p class="hero-description"> 232 232 Typelex lets you write AT <a target="_blank" href="https://atproto.com/specs/lexicon">Lexicons</a> in a more readable syntax. <br /> 233 233 It uses <a target="_blank" href="https://typespec.io/">TypeSpec</a> for syntax, adding conventions for Lexicons. 234 234 </p> 235 235 236 - <div class="hero-actions"> 236 + <nav class="hero-actions"> 237 237 <a href="#install" class="install-cta">Try It</a> 238 238 <a href="https://tangled.org/@danabra.mov/typelex/blob/main/DOCS.md" target="_blank" rel="noopener noreferrer" class="star-btn"> 239 239 Read Documentation 240 240 </a> 241 - </div> 241 + </nav> 242 242 </header> 243 243 244 - <div class="separator"></div> 244 + <hr class="separator" /> 245 245 246 246 {highlighted.map(({ title, typelexHtml, lexiconHtml, playgroundUrl }) => ( 247 247 <section> 248 248 <h2>{title}</h2> 249 - <div class="comparison"> 249 + <figure class="comparison"> 250 250 <div class="comparison-content"> 251 251 <div class="code-panel"> 252 - <h3 class="code-header"> 252 + <p class="code-header"> 253 253 Typelex 254 254 <a href={playgroundUrl} target="_blank" rel="noopener noreferrer" class="code-playground-link" aria-label="Open in playground"> 255 255 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> ··· 257 257 <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"/> 258 258 </svg> 259 259 </a> 260 - </h3> 260 + </p> 261 261 <div class="code-block" set:html={typelexHtml} /> 262 262 </div> 263 263 <div class="code-panel"> 264 - <h3 class="code-header"> 264 + <p class="code-header"> 265 265 Lexicon 266 - </h3> 266 + </p> 267 267 <div class="code-block" set:html={lexiconHtml} /> 268 268 </div> 269 269 </div> 270 - </div> 270 + </figure> 271 271 </section> 272 272 ))} 273 273 274 - <div class="separator"></div> 274 + <hr class="separator" /> 275 275 276 276 <section class="install-section" id="install"> 277 277 <h2>Install</h2> 278 278 <div class="install-grid"> 279 279 <div class="install-notice"> 280 - <p class="notice-text">This is an early-stage experiment. There are bugs. You must verify the output!</p> 280 + <p class="notice-text">This is an early-stage experiment. It's probably buggy as hell.</p> 281 281 </div> 282 282 <div class="install-step playground-step"> 283 283 <div class="step-number">0</div> ··· 294 294 <div class="step-number">1</div> 295 295 <div class="step-content"> 296 296 <h3>Install packages</h3> 297 - <div class="install-box" set:html={await highlightCode('npm install -D @typespec/compiler @typelex/emitter', 'bash')} /> 297 + <figure class="install-box" set:html={await highlightCode('npm install -D @typespec/compiler @typelex/emitter', 'bash')} /> 298 298 </div> 299 299 </div> 300 300 301 301 <div class="install-step"> 302 302 <div class="step-number">2</div> 303 303 <div class="step-content"> 304 - <h3>Add <a href="https://typespec.io/docs/handbook/configuration/configuration/" target="_blank" rel="noopener noreferrer">tspconfig.yaml</a></h3> 305 - <div class="install-box" set:html={await highlightCode(`emit: 304 + <h3>Create <code>typelex/main.tsp</code></h3> 305 + <figure class="install-box install-box-with-link"> 306 + <a href={createPlaygroundUrl(`import "@typelex/emitter"; 307 + 308 + namespace com.example.actor.profile { 309 + /** My profile. */ 310 + @rec("literal:self") 311 + model Main { 312 + /** Free-form profile description.*/ 313 + @maxGraphemes(256) 314 + description?: string; 315 + } 316 + }`)} target="_blank" rel="noopener noreferrer" class="install-playground-link" aria-label="Open in playground"> 317 + <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 318 + <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"/> 319 + <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"/> 320 + </svg> 321 + </a> 322 + <div set:html={await highlightCode(`import "@typelex/emitter"; 323 + 324 + namespace com.example.actor.profile { 325 + /** My profile. */ 326 + @rec("literal:self") 327 + model Main { 328 + /** Free-form profile description.*/ 329 + @maxGraphemes(256) 330 + description?: string; 331 + } 332 + }`, 'typespec')} /> 333 + </figure> 334 + </div> 335 + <p class="step-description">Or grab any example Lexicon <a target=_blank href="https://playground.typelex.org/">from the Playground</a>.</p> 336 + </div> 337 + 338 + <div class="install-step"> 339 + <div class="step-number">3</div> 340 + <div class="step-content"> 341 + <h3>Create <code><a href="https://typespec.io/docs/handbook/configuration/configuration/" target="_blank" rel="noopener noreferrer">tspconfig.yaml</a></code></h3> 342 + <figure class="install-box" set:html={await highlightCode(`emit: 306 343 - "@typelex/emitter" 307 344 options: 308 345 "@typelex/emitter": ··· 311 348 </div> 312 349 313 350 <div class="install-step"> 314 - <div class="step-number">3</div> 351 + <div class="step-number">4</div> 315 352 <div class="step-content"> 316 - <h3>Configure scripts</h3> 317 - <div class="install-box" set:html={await highlightCode(`{ 353 + <h3>Add a build script to <code>package.json</code></h3> 354 + <figure class="install-box" set:html={await highlightCode(`{ 318 355 "scripts": { 319 - "build": "npm run build:lexicon && npm run build:codegen", 320 - "build:lexicon": "tsp compile . && tsp format **/*.tsp", 321 - "build:codegen": "lex gen-api --yes ./src lexicon/app/example/*.json" 356 + // ... 357 + "build:lexicon": "tsp compile typelex/main.tsp" 322 358 } 323 359 }`, 'json')} /> 324 360 </div> 325 361 </div> 326 362 327 363 <div class="install-step"> 328 - <div class="step-number">4</div> 364 + <div class="step-number">5</div> 365 + <div class="step-content"> 366 + <h3>Generate Lexicon files</h3> 367 + <figure class="install-box" set:html={await highlightCode(`npm run build:lexicon`, 'bash')} /> 368 + <p class="step-description">Lexicon files will be generated in the <code>output-dir</code> from your <code>tspconfig.yaml</code> config.</p> 369 + </div> 370 + </div> 371 + 372 + <div class="install-step"> 373 + <div class="step-number">6</div> 329 374 <div class="step-content"> 330 375 <h3>Set up VS Code</h3> 331 376 <p class="step-description">Install the <a href="https://typespec.io/docs/introduction/editor/vscode/" target="_blank" rel="noopener noreferrer">TypeSpec for VS Code extension</a> for syntax highlighting and IntelliSense.</p> ··· 333 378 </div> 334 379 335 380 <div class="install-step"> 336 - <div class="step-number">5</div> 381 + <div class="step-number">7</div> 337 382 <div class="step-content"> 338 383 <h3>Read the docs</h3> 339 384 <p class="step-description">Check out the <a href="https://tangled.org/@danabra.mov/typelex/blob/main/DOCS.md" target="_blank" rel="noopener noreferrer">documentation</a> to learn more.</p> ··· 341 386 </div> 342 387 </div> 343 388 </section> 344 - 345 - <div class="separator"></div> 389 + 390 + <hr class="separator" /> 346 391 347 392 <footer> 348 393 <p>This is my personal hobby project and is not affiliated with AT or endorsed by anyone.</p> 349 394 <p>Who knows if this is a good idea?</p> 350 395 </footer> 351 - </div> 396 + </main> 352 397 353 398 <script> 354 399 document.addEventListener('DOMContentLoaded', () => { ··· 725 770 height: 1px; 726 771 background: linear-gradient(to right, transparent, #e2e8f0 20%, #e2e8f0 80%, transparent); 727 772 margin: 4rem 0; 773 + border: none; 728 774 } 729 775 730 776 @media (min-width: 768px) { ··· 751 797 752 798 .install-notice { 753 799 padding: 1rem 1.5rem; 754 - background: #fef3c7; 755 - border: 1px solid #fbbf24; 800 + background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); 801 + border: 2px solid #ef4444; 756 802 border-radius: 10px; 757 - text-align: center; 803 + text-align: left; 758 804 } 759 805 760 806 .notice-text { 761 807 margin: 0; 762 808 font-size: 1.0625rem; 763 809 line-height: 1.5; 764 - font-weight: 600; 765 - color: #92400e; 810 + font-weight: 700; 811 + color: #991b1b; 766 812 } 767 813 768 814 .install-grid { ··· 857 903 background: #1e1b29; 858 904 border-radius: 8px; 859 905 overflow: hidden; 906 + margin: 0; 907 + } 908 + 909 + .install-box-with-link { 910 + position: relative; 911 + } 912 + 913 + .install-playground-link { 914 + position: absolute; 915 + top: 0.75rem; 916 + right: 0.75rem; 917 + z-index: 10; 918 + display: inline-flex; 919 + align-items: center; 920 + justify-content: center; 921 + color: #94a3b8; 922 + transition: all 0.2s ease; 923 + text-decoration: none; 924 + opacity: 0.4; 925 + padding: 0.125rem; 926 + } 927 + 928 + .install-playground-link:hover { 929 + color: #c7d2fe; 930 + opacity: 1; 931 + } 932 + 933 + .install-playground-link svg { 934 + width: 1rem; 935 + height: 1rem; 860 936 } 861 937 862 938 .install-box + .step-description { ··· 872 948 873 949 .install-box code { 874 950 font-family: 'Monaco', 'Menlo', monospace; 875 - font-size: 0.8125rem !important; 951 + font-size: 0.75rem !important; 876 952 line-height: 1.6; 877 953 } 878 954 ··· 889 965 890 966 .install-notice { 891 967 padding: 1.5rem 2rem; 968 + border-width: 2px; 892 969 } 893 970 894 971 .notice-text { ··· 913 990 } 914 991 915 992 .install-box code { 916 - font-size: 0.875rem !important; 993 + font-size: 0.8125rem !important; 917 994 } 918 995 } 919 996 ··· 1085 1162 .code-block code, 1086 1163 .hero-code code { 1087 1164 font-family: 'Monaco', 'Menlo', monospace; 1088 - font-size: 0.8125rem !important; 1165 + font-size: 0.75rem !important; 1089 1166 line-height: 1.6; 1090 1167 white-space: pre; 1091 1168 text-align: left; ··· 1094 1171 @media (min-width: 768px) { 1095 1172 .code-block code, 1096 1173 .hero-code code { 1097 - font-size: 0.9375rem !important; 1174 + font-size: 0.875rem !important; 1098 1175 } 1099 1176 } 1100 1177