An experimental TypeSpec syntax for Lexicon

0.3.0

Changed files
+36 -71
packages
cli
src
commands
emitter
website
src
+5
CHANGELOG.md
··· 1 + ### 0.3.0 2 + 3 + - New package `@typelex/cli` 4 + - See new recommended workflow on https://typelex.org/#install 5 + 1 6 ### 0.2.0 2 7 3 8 - Add `@external` support
+1 -30
DOCS.md
··· 258 258 259 259 The `@external` decorator tells the emitter to skip JSON output for that namespace. This is useful when referencing definitions from other Lexicons that you don't want to re-emit. 260 260 261 - You could collect external stubs in one file and import them: 262 - 263 - ```typescript 264 - import "@typelex/emitter"; 265 - import "../atproto-stubs.tsp"; 266 - 267 - namespace app.bsky.actor.profile { 268 - model Main { 269 - labels?: (com.atproto.label.defs.SelfLabels | unknown); 270 - } 271 - } 272 - ``` 273 - 274 - Then in `atproto-stubs.tsp`: 275 - 276 - ```typescript 277 - import "@typelex/emitter"; 278 - 279 - @external 280 - namespace com.atproto.label.defs { 281 - model SelfLabels { } 282 - } 283 - 284 - @external 285 - namespace com.atproto.repo.defs { 286 - model StrongRef { } 287 - @token model SomeToken { } // Note: Tokens still need @token 288 - } 289 - // ... more stubs 290 - ``` 261 + Starting with 0.3.0, typelex will automatically generate a `typelex/externals.tsp` file based on the JSON files in your `lexicons/` folder, and enforce that it's imported into your `typelex/main.tsp` entry point. However, this will *not* include Lexicons from your app's namespace, but only external ones. 291 262 292 263 You'll want to ensure the real JSON for external Lexicons is available before running codegen. 293 264
+2 -2
packages/cli/package.json
··· 1 1 { 2 2 "name": "@typelex/cli", 3 - "version": "0.2.15", 3 + "version": "0.3.0", 4 4 "main": "dist/index.js", 5 5 "type": "module", 6 6 "bin": { ··· 40 40 "@typelex/emitter": "workspace:*" 41 41 }, 42 42 "peerDependencies": { 43 - "@typelex/emitter": "^0.2.0" 43 + "@typelex/emitter": "^0.3.0" 44 44 } 45 45 }
+6 -5
packages/cli/src/commands/init.ts
··· 58 58 59 59 return new Promise((resolve) => { 60 60 rl.question( 61 - `Enter your app's root namespace (e.g. ${pc.cyan("com.example.*")}): `, 61 + `Which Lexicons do you want to write in typelex (e.g. ${pc.cyan("com.example.*")})? `, 62 62 (answer) => { 63 63 rl.close(); 64 64 resolve(answer.trim()); ··· 90 90 return initSetup(); 91 91 } 92 92 93 - console.log(`Adding ${gradientText("typelex")}...\n`); 93 + console.log(gradientText("Adding typelex...") + "\n"); 94 94 95 95 // Detect package manager 96 96 let packageManager = "npm"; ··· 240 240 241 241 // Inform about external lexicons 242 242 console.log( 243 - `\nLexicons other than ${pc.cyan(namespace)} will be considered external.`, 243 + `\nLexicons for ${pc.cyan(namespace)} will now be managed by typelex.`, 244 244 ); 245 + console.log(`You can begin writing them in ${pc.cyan("typelex/main.tsp")}.`); 245 246 console.log( 246 - `Put them into the ${pc.cyan(displayLexiconsPath)} folder as JSON.\n`, 247 + `Any external lexicons should remain in ${pc.cyan(displayLexiconsPath)}.\n`, 247 248 ); 248 249 249 250 // Create typelex directory ··· 321 322 console.log(`\n${pc.green("✓")} ${pc.bold("All set!")}`); 322 323 console.log(`\n${pc.bold("Next steps:")}`); 323 324 console.log( 324 - ` ${pc.dim("1.")} Edit ${pc.cyan("typelex/main.tsp")} to define your lexicons`, 325 + ` ${pc.dim("1.")} Edit ${pc.cyan("typelex/main.tsp")} to define the ${pc.cyan(namespace)} lexicons`, 325 326 ); 326 327 console.log( 327 328 ` ${pc.dim("2.")} Keep putting external lexicons into ${pc.cyan(displayLexiconsPath)}`,
+1 -1
packages/emitter/package.json
··· 1 1 { 2 2 "name": "@typelex/emitter", 3 - "version": "0.2.0", 3 + "version": "0.3.0", 4 4 "description": "TypeSpec emitter for ATProto Lexicon definitions", 5 5 "main": "dist/index.js", 6 6 "type": "module",
+17
packages/website/src/layouts/BaseLayout.astro
··· 50 50 51 51 <slot /> 52 52 53 + <script> 54 + // Smooth scroll to top when clicking logo 55 + document.addEventListener('DOMContentLoaded', () => { 56 + const logo = document.querySelector('.logo'); 57 + if (logo) { 58 + logo.addEventListener('click', (e) => { 59 + // Allow Ctrl/Cmd+click to open in new tab 60 + if (e.ctrlKey || e.metaKey || e.shiftKey) { 61 + return; 62 + } 63 + e.preventDefault(); 64 + window.scrollTo({ top: 0, behavior: 'smooth' }); 65 + }); 66 + } 67 + }); 68 + </script> 69 + 53 70 {transparentNav && ( 54 71 <script> 55 72 const nav = document.querySelector('.top-nav');
+4 -33
packages/website/src/pages/index.astro
··· 179 179 <div class="step-content"> 180 180 <h3>Add typelex to your app</h3> 181 181 <figure class="install-box" set:html={await highlightCode('npx @typelex/cli init', 'bash')} /> 182 - <p class="step-description">This will add a few things to your <code>package.json</code>, and create a <code>typelex/</code> folder.</p> 182 + <p class="step-description">This will add a few things to your <code>package.json</code> and create a <code>typelex/</code> folder.</p> 183 183 </div> 184 184 </div> 185 185 ··· 187 187 <div class="step-number">2</div> 188 188 <div class="step-content"> 189 189 <h3>Write your lexicons in <code>typelex/main.tsp</code></h3> 190 - <figure class="install-box install-box-with-link"> 191 - <a href={createPlaygroundUrl(`import "@typelex/emitter"; 192 - import "./externals.tsp"; 193 - 194 - namespace com.myapp.example.profile { 195 - /** My profile. */ 196 - @rec("literal:self") 197 - model Main { 198 - /** Free-form profile description.*/ 199 - @maxGraphemes(256) 200 - description?: string; 201 - } 202 - }`)} target="_blank" rel="noopener noreferrer" class="install-playground-link" aria-label="Open in playground"> 203 - <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 204 - <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"/> 205 - <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"/> 206 - </svg> 207 - </a> 208 - <div set:html={await highlightCode(`import "@typelex/emitter"; 209 - import "./externals.tsp"; 210 - 211 - namespace com.myapp.example.profile { 212 - /** My profile. */ 213 - @rec("literal:self") 214 - model Main { 215 - /** Free-form profile description.*/ 216 - @maxGraphemes(256) 217 - description?: string; 218 - } 219 - }`, 'typespec')} /> 220 - </figure> 190 + <figure class="install-box" set:html={await highlightCode(installCode, 'typespec')} /> 191 + <p class="step-description">Your app's lexicons go here. They may reference any external ones from <code>lexicons/</code>.</p> 221 192 </div> 222 - <p class="step-description">Your app's lexicons go here. They may reference any external ones from <code>lexicons/</code>. 223 193 </div> 224 194 225 195 <div class="install-step"> ··· 622 592 .install-section { 623 593 margin: 0; 624 594 padding: 0; 595 + scroll-margin-top: 5rem; 625 596 } 626 597 627 598 .install-section h2 {