An experimental TypeSpec syntax for Lexicon

docs

Changed files
+352 -615
packages
example
website
src
pages
+346 -609
DOCS.md
··· 4 4 5 5 ## Introduction 6 6 7 - ### What's TypeSpec? 8 - 9 - [TypeSpec](https://typespec.io/) is a TypeScript-like language for writing schemas for data and API calls. 10 - 11 - TypeSpec offers flexible syntax for describing schemas, as well as the tooling for it (like LSP), but it doesn't specify the semantics or the output format. It can target different output formats via *emitters*. For example, there's a [JSON Schema emitter](https://typespec.io/docs/emitters/json-schema/reference/) and a [Protobuf emitter](https://typespec.io/docs/emitters/protobuf/reference/) for TypeSpec. Emitters determine the output format. Emitters can define or restrict the available built-in types. Emitters can also define their own *decorators* that attach to different pieces of syntax. 12 - 13 7 ### What's Lexicon? 14 8 15 - [Lexicon](https://atproto.com/specs/lexicon) is a schema format used by [AT](https://atproto.com/) applications. 16 - 17 - Here is a small Lexicon schema defining an `app.bsky.bookmark.defs` Lexicon containing a `listItemView` definition, which describes an `object` with a required `uri` property of type `at-uri`: 9 + [Lexicon](https://atproto.com/specs/lexicon) is a schema format used by [AT](https://atproto.com/) applications. Here's a small example: 18 10 19 11 ```json 20 12 { ··· 32 24 } 33 25 ``` 34 26 35 - You would then generate code from this schema that takes care of parsing and validating a piece of data of that shape, as well as the type definitions (e.g. for Go or TypeScript). 27 + This schema is then used to generate code for parsing of these objects, their validation, and their types. 36 28 37 - ### What's typelex? 29 + ### What's TypeSpec? 38 30 39 - Typelex is a TypeSpec emitter that targets AT Lexicon as the output format. As such, it adds built-in Lexicon data types and a few decorators to control the structure of the Lexicon output. 31 + [TypeSpec](https://typespec.io/) is a TypeScript-like language for writing schemas for data and API calls. It offers flexible syntax and tooling (like LSP), but doesn't specify output format—that's what *emitters* do. For example, there's a [JSON Schema emitter](https://typespec.io/docs/emitters/json-schema/reference/) and a [Protobuf emitter](https://typespec.io/docs/emitters/protobuf/reference/). Emitters can define built-in types and *decorators* for different pieces of syntax. 32 + 33 + ### What's typelex? 40 34 41 - Here's the above Lexicon written in TypeSpec (with the typelex emitter): 35 + Typelex is a TypeSpec emitter targeting Lexicon. Here's the same schema in TypeSpec: 42 36 43 37 ```typescript 44 38 import "@typelex/emitter"; ··· 50 44 } 51 45 ``` 52 46 53 - Then you run the compiler, and it generates the Lexicon JSON for you. 47 + Run the compiler, and it generates Lexicon JSON for you. 54 48 55 - It is important to note that the JSON format is in which you'll publish your Lexicons. Typelex only exists for authoring convenience and does not aim to supplant or replace Lexicon JSON itself. Think of it as a "CoffeeScript for Lexicon" (however terrible that may be). 49 + The JSON is what you'll publish—typelex just makes authoring easier. Think of it as "CoffeeScript for Lexicon" (however terrible that may be). 56 50 57 - Typelex tries to stay faithful to both TypeSpec idioms and Lexicon concepts, which makes it a bit confusing and sometimes not elegant. Since we can't add new keywords to TypeSpec, often there's a decorator that fills the need. For example, you'll have to write `@procedure op` whereas ideally there would just be a `procedure` keyword, or you'll have to write `model` for something that Lexicon calls a "def". In essence, you have to learn both Lexicon *and* TypeSpec. It is a good idea to scan through the [TypeSpec language overview](https://typespec.io/docs/language-basics/overview/) to get a feel for it. 51 + Typelex tries to stay faithful to both TypeSpec idioms and Lexicon concepts, which is a tricky balance. Since we can't add keywords to TypeSpec, decorators fill the gaps—you'll write `@procedure op` instead of `procedure`, or `model` for what Lexicon calls a "def". One downside of this approach is you'll need to learn both Lexicon *and* TypeSpec to know what you're doing. Scan the [TypeSpec language overview](https://typespec.io/docs/language-basics/overview/) to get a feel for it, it isn't much. 58 52 59 - Personally, I find JSON Lexicons difficult to read and to author so the tradeoff is worth it for me. 53 + Personally, I find JSON Lexicons hard to read and author, so the tradeoff works for me. 60 54 61 55 ### Playground 62 56 63 57 [Open Playground](https://playground.typelex.org/) to play with a bunch of lexicons and to see how typelex code translates to Lexicon JSON. If you already know Lexicon, you can learn the syntax by just opening the Lexicons you're familiar with. 64 58 65 59 ## Quick Start 66 - 67 - Let's dive into some common patterns and see how to write them. 68 60 69 61 ### Namespaces 70 62 71 - A namespace corresponds to a Lexicon output file. For example: 63 + A namespace corresponds to a Lexicon file: 72 64 73 65 ```typescript 74 66 import "@typelex/emitter"; ··· 80 72 } 81 73 ``` 82 74 83 - will emit `app/bsky/feed/defs.json`: 75 + This emits `app/bsky/feed/defs.json`: 84 76 85 77 ```json 86 78 { 87 79 "lexicon": 1, 88 80 "id": "app.bsky.feed.defs", 89 - // ... 81 + "defs": { ... } 90 82 } 91 83 ``` 92 84 93 - You can [try it in the playground](https://playground.typelex.org/?c=aW1wb3J0ICJAdHlwZWxleC9lbWl0dGVyIjsKCm5hbWVzcGFjZSBhcHAuYnNreS5mZWVkLmRlZnMgewogIG1vZGVsIFBvc3RWaWV3xRMgIHJlcGx5Q291bnQ%2FOiBpbnRlZ2VyO8gab3N01RtsaWtl0xl9Cn0K&e=%40typelex%2Femitter&options=%7B%7D&vs=%7B%7D). 85 + [Try it in the playground](https://playground.typelex.org/?c=aW1wb3J0ICJAdHlwZWxleC9lbWl0dGVyIjsKCm5hbWVzcGFjZSBhcHAuYnNreS5mZWVkLmRlZnMgewogIG1vZGVsIFBvc3RWaWV3xRMgIHJlcGx5Q291bnQ%2FOiBpbnRlZ2VyO8gab3N01RtsaWtl0xl9Cn0K&e=%40typelex%2Femitter&options=%7B%7D&vs=%7B%7D). 94 86 95 - ### Reserved Words 87 + Use backticks for reserved words: 96 88 97 - Use backticks for TypeScript/TypeSpec reserved words: 89 + ```typescript 90 + import "@typelex/emitter"; 98 91 99 - ```typescript 100 92 namespace app.bsky.feed.post.`record` { } 101 - 102 93 namespace `pub`.blocks.blockquote { } 103 94 ``` 104 95 105 - ### Multiple Namespaces in One File 106 - 107 - Multiple namespaces can be defined in one file: 96 + You can define multiple namespaces in one file: 108 97 109 98 ```typescript 110 99 import "@typelex/emitter"; 111 100 112 101 namespace com.example.foo { 113 - @rec("tid") 114 - model Main { 115 - bar?: com.example.bar.Main 116 - } 102 + model Main { /* ... */ } 117 103 } 118 104 119 105 namespace com.example.bar { 120 - model Main { 121 - @maxGraphemes(1) 122 - bla?: string; 123 - } 106 + model Main { /* ... */ } 124 107 } 125 108 ``` 126 109 127 - This single `.tsp` file will emit two Lexicon files: 128 - 129 - ``` 130 - - com/example/foo.json 131 - - com/example/bar.json 132 - ``` 110 + This emits two files: `com/example/foo.json` and `com/example/bar.json`. 133 111 134 - You can `import` other `.tsp` files to avoid defining the same thing multiple times. The output structure is only determined by namespaces, not by how you split your input files. 112 + You can `import` other `.tsp` files to reuse definitions. Output structure is determined solely by namespaces, not by source file organization. 135 113 136 114 ## Models 137 115 138 - A namespace may contain [`model`s](https://typespec.io/docs/language-basics/models/) inside. By default, **every `model` turns into a Lexicon definition** (`defs` in the Lexicon output). For example, if you have a `PostView` model defined like this: 116 + By default, **every `model` becomes a Lexicon definition**: 139 117 140 118 ```typescript 141 119 import "@typelex/emitter"; 142 120 143 121 namespace app.bsky.feed.defs { 144 - model PostView { 145 - // ... 146 - } 122 + model PostView { /* ... */ } 123 + model ViewerState { /* ... */ } 147 124 } 148 125 ``` 149 126 150 - It will then become `postView` definition in the `defs` of this namespace's Lexicon: 127 + Model names convert PascalCase → camelCase: 151 128 152 129 ```json 153 130 { 154 - "lexicon": 1, 155 131 "id": "app.bsky.feed.defs", 156 132 "defs": { 157 - "postView": { 158 - // ... 159 - } 133 + "postView": { /* ... */ }, 134 + "viewerState": { /* ... */ } 160 135 } 136 + // ... 161 137 } 162 138 ``` 163 139 164 - A namespace may have multiple `model`s: 165 - 166 - ```typescript 167 - namespace app.bsky.feed.defs { 168 - model PostView { 169 - // ... 170 - } 140 + Models in the same namespace can be in separate `namespace` blocks or even different files (via [`import`](https://typespec.io/docs/language-basics/imports/)). TypeSpec bundles them all into one Lexicon file per namespace. 171 141 172 - model ViewerState { 173 - // ... 174 - } 175 - } 176 - ``` 142 + ### Namespace Conventions: `.defs` vs `Main` 177 143 178 - Every `model` becomes an entry in `defs`: 179 - 180 - ```json 181 - { 182 - "lexicon": 1, 183 - "id": "app.bsky.feed.defs", 184 - "defs": { 185 - "postView": { 186 - // ... 187 - }, 188 - "viewerState": { 189 - // .. 190 - } 191 - } 192 - } 193 - ``` 144 + By convention, a namespace must either have a `Main` model or end with `.defs`. 194 145 195 - This works even if they're declared in separate `namespace` blocks: 146 + Use `.defs` for a grabbag of reusable definitions: 196 147 197 148 ```typescript 198 - namespace app.bsky.feed.defs { 199 - model PostView { 200 - // ... 201 - } 202 - } 149 + import "@typelex/emitter"; 203 150 204 151 namespace app.bsky.feed.defs { 205 - model ViewerState { 206 - // ... 207 - } 152 + model PostView { /* ... */ } 153 + model ViewerState { /* ... */ } 208 154 } 209 155 ``` 210 156 211 - These blocks could even be in different files, one [`import`ing](https://typespec.io/docs/language-basics/imports/) the other. 212 - 213 - In either case, TypeSpec will find all the models inside each namespace (such as `app.bsky.feed.defs`), and bundle each namespace into a separate Lexicon file (such as `app/bsky/feed/defs.json`) with all its `defs`. 214 - 215 - ### Namespace Conventions: `.defs` vs `Main` 216 - 217 - By Lexicon convention, a namespace must either have a `Main` model or end with `.defs`. 218 - 219 - You should end your namespace in `.defs` if you want a grabbag of reusable definitions: 157 + For a Lexicon about one main concept, add a `Main` model instead: 220 158 221 159 ```typescript 222 - namespace app.bsky.feed.defs { 223 - model PostView { 224 - // ... 225 - } 226 - 227 - model ViewerState { 228 - // ... 229 - } 230 - } 231 - ``` 232 - 233 - On the other hand, if your Lexicon is about one main concept, don't add `.defs` to the namespace, and instead pick some model to be called `Main`: 160 + import "@typelex/emitter"; 234 161 235 - ```typescript 236 162 namespace app.bsky.embed.video { 237 - model Main { 238 - @required 239 - video: Blob<#["video/mp4"], 100000000>; 240 - 241 - @maxItems(20) 242 - captions?: Caption[]; 243 - } 244 - 245 - model Caption { 246 - // ... 247 - } 163 + model Main { /* ... */ } 164 + model Caption { /* ... */ } 248 165 } 249 166 ``` 250 167 251 - Either do one or the other, or you'll get a compile error forcing you to choose. 168 + Pick one or the other—the compiler will error if you don't. 252 169 253 170 ### References 254 171 255 - A `model` can reference another `model` as a data type like so: 172 + Models can reference other models: 256 173 257 174 ```typescript 175 + import "@typelex/emitter"; 176 + 258 177 namespace app.bsky.embed.video { 259 178 model Main { 260 - // A reference to the `Caption` model below: 261 179 captions?: Caption[]; 262 180 } 263 - 264 - model Caption { 265 - // ... 266 - } 181 + model Caption { /* ... */ } 267 182 } 268 183 ``` 269 184 270 - In the resulting Lexicon JSON, this becomes a `ref` to the local `#caption` def: 185 + This becomes a local `ref`: 271 186 272 187 ```json 273 - { 274 - "lexicon": 1, 275 - "id": "app.bsky.embed.video", 276 - "defs": { 277 - "main": { 278 - "type": "object", 279 - "properties": { 280 - "captions": { 281 - "type": "array", 282 - "items": { 283 - // A reference to the `caption` def below: 284 - "type": "ref", 285 - "ref": "#caption" 286 - } 287 - } 288 - } 289 - }, 290 - "caption": { 291 - "type": "object", 292 - "properties": {} 293 - } 294 - } 188 + // ... 189 + "captions": { 190 + "type": "array", 191 + "items": { "type": "ref", "ref": "#caption" } 295 192 } 193 + // ... 296 194 ``` 297 195 298 - You can also reference `model`s from other namespaces: 196 + You can also reference models from other namespaces: 299 197 300 198 ```typescript 301 199 import "@typelex/emitter"; 302 200 303 201 namespace app.bsky.actor.profile { 304 202 model Main { 305 - // A reference to the `SelfLabel` model from another Lexicon: 306 203 labels?: (com.atproto.label.defs.SelfLabels | unknown); 307 204 } 308 205 } 309 206 310 207 namespace com.atproto.label.defs { 311 - model SelfLabels { 312 - // ... 313 - } 208 + model SelfLabels { /* ... */ } 314 209 } 315 210 ``` 316 211 317 - This will become a fully qualified reference: 212 + This becomes a fully qualified reference: 318 213 319 214 ```json 320 215 // ... ··· 325 220 // ... 326 221 ``` 327 222 328 - This will work even if you move the `com.atproto.label.defs` definition to another `.tsp` file, as long as you don't forget to `import` that file. 223 + ([See it in the Playground](https://playground.typelex.org/?c=aW1wb3J0ICJAdHlwZWxleC9lbWl0dGVyIjsKCm5hbWVzcGFjZSBhcHAuYnNreS5hY3Rvci5wcm9maWxlIHsKICBAcmVjKCJsaXRlcmFsOnNlbGYiKQogIG1vZGVsIE1haW7FJiAgZGlzcGxheU5hbWU%2FOiBzdHJpbmc7xRpsYWJlbHM%2FOiAoY29tLmF0cHJvdG8uxRYuZGVmcy5TZWxmTMUlIHwgdW5rbm93binEPH0KfewAptY%2F5QCA5gCPy0nEFSAgLy8gLi4uxko%3D&e=%40typelex%2Femitter&options=%7B%7D&vs=%7B%7D).) 329 224 330 - As you can see [in the Playground](https://playground.typelex.org/?c=aW1wb3J0ICJAdHlwZWxleC9lbWl0dGVyIjsKCm5hbWVzcGFjZSBhcHAuYnNreS5hY3Rvci5wcm9maWxlIHsKICBAcmVjKCJsaXRlcmFsOnNlbGYiKQogIG1vZGVsIE1haW7FJiAgZGlzcGxheU5hbWU%2FOiBzdHJpbmc7xRpsYWJlbHM%2FOiAoY29tLmF0cHJvdG8uxRYuZGVmcy5TZWxmTMUlIHwgdW5rbm93binEPH0KfewAptY%2F5QCA5gCPy0nEFSAgLy8gLi4uxko%3D&e=%40typelex%2Femitter&options=%7B%7D&vs=%7B%7D), this actually emits *two Lexicon files* since there are two separate namespaces being declared. 331 - 332 - This makes sense if both definitions are yours, but what if you just want to *refer to* someone else's Lexicon? 225 + This works across files too—just remember to `import` the file with the definition. 333 226 334 227 ### External Stubs 335 228 336 - The easiest way to use someone else's Lexicon is to import its definition, assuming it's also written in TypeSpec. 337 - 338 - ```typescript 339 - import "../atproto.tsp" 340 - ``` 341 - 342 - In practice, you probably won't have all the definitions of the Lexicons you depend on written in TypeSpec. However, you can **stub out any definition you depend on**: 229 + If you don't have TypeSpec definitions for external Lexicons, you can stub them out: 343 230 344 231 ```typescript 345 232 import "@typelex/emitter"; 346 233 347 234 namespace app.bsky.actor.profile { 348 235 model Main { 349 - // A reference to a stub 350 236 labels?: (com.atproto.label.defs.SelfLabels | unknown); 351 237 } 352 238 } 353 239 354 - // This is a stub! It's fine for it to be empty. 355 - // Think of it similarly to a .d.ts definition in TypeScript. 240 + // Empty stub (like .d.ts in TypeScript) 356 241 namespace com.atproto.label.defs { 357 242 model SelfLabels { } 358 243 } 359 244 ``` 360 245 361 - You could place stubs together and import them from the definitions that need them, and then ignore the (incomplete) Lexicon JSON output for those stubs (since it's going to be empty). 246 + You could collect stubs in one file and import them: 362 247 363 248 ```typescript 364 249 import "@typelex/emitter"; 365 - import "../atproto-stubs.tsp"; // All your stubs here 250 + import "../atproto-stubs.tsp"; 366 251 367 252 namespace app.bsky.actor.profile { 368 253 model Main { 369 - // A reference to a stub 370 254 labels?: (com.atproto.label.defs.SelfLabels | unknown); 371 255 } 372 256 } 373 257 ``` 374 258 375 - I'm not sure which organizational patterns work best in practice. Try different things and let me know. 259 + You'll want to replace the stubbed lexicons in the output folder with their real JSON before running codegen. 376 260 377 261 ### Inline Models 378 262 379 - By default, every `model` becomes a top-level entry in Lexicon `defs`. 380 - 381 - This: 263 + By default, every `model` becomes a top-level def: 382 264 383 265 ```typescript 384 266 import "@typelex/emitter"; ··· 387 269 model Main { 388 270 captions?: Caption[]; 389 271 } 390 - 391 - model Caption { 392 - text?: string 393 - } 272 + model Caption { /* ... */ } 394 273 } 395 274 ``` 396 275 397 - turns into: 398 - 399 - ```json 400 - { 401 - "lexicon": 1, 402 - "id": "app.bsky.embed.video", 403 - "defs": { 404 - "main": { 405 - // ... 406 - }, 407 - "caption": { 408 - // ... 409 - } 410 - } 411 - } 412 - ``` 276 + This creates two defs: `main` and `caption`. 413 277 414 - Sometimes, you might want to avoid exposing a model as its own `def`, and you just want it to be expanded inline. Put the `@inline` decorator on the `model` to force it into being inlined at every usage site: 278 + Use `@inline` to expand a model inline instead: 415 279 416 280 ```typescript 417 281 import "@typelex/emitter"; ··· 428 292 } 429 293 ``` 430 294 431 - Then it will be inlined wherever it's being used: 295 + Now `Caption` is expanded inline: 432 296 433 297 ```json 434 - { 435 - "lexicon": 1, 436 - "id": "app.bsky.embed.video", 437 - "defs": { 438 - "main": { 439 - "type": "object", 440 - "properties": { 441 - "captions": { 442 - "type": "array", 443 - "items": { 444 - // That's our Caption, inlined. 445 - "type": "object", 446 - "properties": { 447 - "text": { "type": "string" } 448 - } 449 - } 450 - } 451 - } 452 - } 298 + // ... 299 + "captions": { 300 + "type": "array", 301 + "items": { 302 + "type": "object", 303 + "properties": { "text": { "type": "string" } } 453 304 } 454 305 } 306 + // ... 455 307 ``` 456 308 457 - Note this means that different usages of `Caption` will have no relation to each other from the Lexicon perspective. The fact that the `Caption` abstraction exists will essentially be erased. 309 + Note that `Caption` won't exist as a separate def—the abstraction is erased in the output. 458 310 459 311 ## Top-Level Lexicon Types 460 312 461 - In TypeSpec, definitions of things are called [Models](https://typespec.io/docs/language-basics/models/). So you'll see `model Foo { }` used for almost everything, but with different decorators that make its purpose more concrete. 313 + TypeSpec uses `model` for almost everything. Decorators specify what kind of Lexicon type it becomes. 462 314 463 315 ### Objects 464 316 465 - If you haven't marked your `model` with any decorator, it will be a Lexicon *object*. 317 + A plain `model` becomes a Lexicon object: 466 318 467 319 ```typescript 320 + import "@typelex/emitter"; 321 + 468 322 namespace com.example.post { 469 - model Main { 470 - // ... 471 - } 323 + model Main { /* ... */ } 472 324 } 473 325 ``` 474 326 475 - becomes 327 + Output: 476 328 477 329 ```json 478 - { 479 - "lexicon": 1, 480 - "id": "com.example.post", 481 - "defs": { 482 - "main": { 483 - "type": "object", 484 - "properties": { 485 - // ... 486 - } 487 - } 488 - } 330 + // ... 331 + "main": { 332 + "type": "object", 333 + "properties": { /* ... */ } 489 334 } 335 + // ... 490 336 ``` 491 337 492 338 ### Records 493 339 494 - Mark a `model` with a `@rec` decorator to make it a Lexicon *record*. 495 - 496 - For example: 340 + Use `@rec` to make a model a Lexicon record: 497 341 498 342 ```typescript 499 - import "@typelex/emitter"; // Don't forget this import! 343 + import "@typelex/emitter"; 500 344 501 345 namespace com.example.post { 502 346 @rec("tid") 503 - model Main { 504 - // ... 505 - } 347 + model Main { /* ... */ } 506 348 } 507 349 ``` 508 350 509 - (Note it's `@rec` and not `@record` because unfortunately "record" is reserved in TypeSpec.) 510 - 511 - This becomes: 351 + Output: 512 352 513 353 ```json 514 - { 515 - "lexicon": 1, 516 - "id": "com.example.post", 517 - "defs": { 518 - "main": { 519 - "type": "record", 520 - "key": "tid", 521 - "record": { 522 - "type": "object", 523 - "properties": { 524 - // ... 525 - } 526 - } 527 - } 528 - } 354 + // ... 355 + "main": { 356 + "type": "record", 357 + "key": "tid", 358 + "record": { "type": "object", "properties": { /* ... */ } } 529 359 } 360 + // ... 530 361 ``` 531 362 532 - You can pass any [Record Key Type](https://atproto.com/specs/record-key) to `@rec`, like `@rec("nsid")`, `@rec("literal:self")`, etc. 363 + You can pass any [Record Key Type](https://atproto.com/specs/record-key): `@rec("tid")`, `@rec("nsid")`, `@rec("literal:self")`, etc. 364 + 365 + (It's `@rec` not `@record` because "record" is reserved in TypeSpec.) 533 366 534 367 ### Queries 535 368 536 - In TypeSpec, functions are defined with [`op`](https://typespec.io/docs/language-basics/operations/) (for "operation"). Since Lexicon distinguishes *queries* and *procedures*, you need to mark an `op` with either `@query` or `@procedure`. 537 - 538 - Here is an example query: 369 + In TypeSpec, use [`op`](https://typespec.io/docs/language-basics/operations/) for functions. Mark with `@query` for queries: 539 370 540 371 ```typescript 541 372 import "@typelex/emitter"; 542 373 543 - // examples/com/atproto/repo/getRecord.tsp 544 374 namespace com.atproto.repo.getRecord { 545 375 @query 546 376 op main( ··· 555 385 } 556 386 ``` 557 387 558 - The sequential arguments to `main` are rendered as `params` in the generated JSON, while the return type becomes its `output`: 388 + Arguments become `parameters`, return type becomes `output`: 559 389 560 390 ```json 561 - { 562 - "lexicon": 1, 563 - "id": "com.atproto.repo.getRecord", 564 - "defs": { 565 - "main": { 566 - "type": "query", 567 - "parameters": { 568 - "type": "params", 569 - "properties": { 570 - "repo": { /* ... */ }, 571 - "collection": { /* ... */ }, 572 - "rkey": { /* ... */ }, 573 - "cid": { /* ... */ } 574 - }, 575 - "required": ["repo", "collection", "rkey"] 391 + // ... 392 + "main": { 393 + "type": "query", 394 + "parameters": { 395 + "type": "params", 396 + "properties": { 397 + "repo": { /* ... */ }, 398 + "collection": { /* ... */ }, 399 + // ... 400 + }, 401 + "required": ["repo", "collection", "rkey"] 402 + }, 403 + "output": { 404 + "encoding": "application/json", 405 + "schema": { 406 + "type": "object", 407 + "properties": { 408 + "uri": { /* ... */ }, 409 + "cid": { /* ... */ } 576 410 }, 577 - "output": { 578 - "encoding": "application/json", 579 - "schema": { 580 - "type": "object", 581 - "properties": { 582 - "uri": { /* ... */ }, 583 - "cid": { /* ... */ } 584 - }, 585 - "required": ["uri"] 586 - } 587 - } 411 + "required": ["uri"] 588 412 } 589 413 } 590 414 } 415 + // ... 591 416 ``` 592 417 593 - Note that `encoding` is assumed to be `"application/json"`. You can override it with the `@encoding` decorator: 418 + `encoding` defaults to `"application/json"`. Override with `@encoding("foo/bar")` on the `op`. 594 419 595 - ```typescript 596 - @query 597 - @encoding("foo/bar") 598 - op main( 599 - // ... 600 - ``` 601 - 602 - You may also declare `errors` with the `@errors` decorator like so: 420 + Declare errors with `@errors`: 603 421 604 422 ```typescript 605 423 import "@typelex/emitter"; ··· 607 425 namespace com.atproto.repo.getRecord { 608 426 @query 609 427 @errors(FooError, BarError) 610 - op main( 611 - // ... 612 - ): { 613 - // ... 614 - }; 428 + op main(/* ... */): { /* ... */ }; 615 429 616 430 model FooError {} 617 431 model BarError {} 618 432 } 619 433 ``` 620 434 621 - If you don't like writing the output definition inline as the `main` return type, you can extract it to a separate `model`. You'll probably want to mark that model as `@inline` so it doesn't clutter the `defs`. 435 + You can extract the `{ /* ... */ }` output type to a separate `@inline` model if you prefer. 622 436 623 437 ### Procedures 624 438 625 - Procedures are declared with `@procedure op`: 439 + Use `@procedure` for procedures. The first argument must be called `input`: 626 440 627 441 ```typescript 628 442 import "@typelex/emitter"; 629 443 630 444 namespace com.example.createRecord { 631 - /** Create a new record */ 632 445 @procedure 633 446 op main(input: { 634 447 @required text: string; ··· 639 452 } 640 453 ``` 641 454 642 - Note that, unlike with queries, you can't change the argument names. The first argument *must* be called `input`. It will correspond to the `input` section inside the procedure's JSON: 455 + Output: 643 456 644 457 ```json 645 - { 646 - "lexicon": 1, 647 - "id": "com.example.createRecord", 648 - "defs": { 649 - "main": { 650 - "type": "procedure", 651 - "description": "Create a new record", 652 - "input": { 653 - "encoding": "application/json", 654 - "schema": { 655 - "type": "object", 656 - "properties": { 657 - "text": { "type": "string" } 658 - }, 659 - "required": ["text"] 660 - } 458 + // ... 459 + "main": { 460 + "type": "procedure", 461 + "input": { 462 + "encoding": "application/json", 463 + "schema": { 464 + "type": "object", 465 + "properties": { "text": { "type": "string" } }, 466 + "required": ["text"] 467 + } 468 + }, 469 + "output": { 470 + "encoding": "application/json", 471 + "schema": { 472 + "type": "object", 473 + "properties": { 474 + "uri": { /* ... */ }, 475 + "cid": { /* ... */ } 661 476 }, 662 - "output": { 663 - "encoding": "application/json", 664 - "schema": { 665 - "type": "object", 666 - "properties": { 667 - "uri": { /* ... */ }, 668 - "cid": { /* ... */ } 669 - }, 670 - "required": ["uri", "cid"] 671 - } 672 - } 477 + "required": ["uri", "cid"] 673 478 } 674 479 } 675 480 } 676 - ``` 677 - 678 - Although this is very rarely done, procedures can also receive parameters (like queries). However, they are declared inside an optional second object argument like `@procedure op(input: {}, parameters: {})`. 679 - 680 - Use `: void` for procedures with no output: 681 - 682 - ```typescript 683 - @procedure 684 - op main(input: { 685 - @required uri: atUri; 686 - }): void; 481 + // ... 687 482 ``` 688 483 689 - Use `: never` with `@encoding()` for output with encoding but no schema: 484 + Procedures can also receive parameters (rarely used): `op main(input: {}, parameters: {})`. 690 485 691 - ```typescript 692 - @query 693 - @encoding("application/json") 694 - op main(id?: string): never; 695 - ``` 486 + Use `: void` for no output, or `: never` with `@encoding()` on the `op` for output with encoding but no schema. 696 487 697 488 ### Subscriptions 698 489 699 - Defining an `op` with `@subscription` gives you a subscription: 490 + Use `@subscription` for subscriptions: 700 491 701 492 ```typescript 702 493 import "@typelex/emitter"; ··· 704 495 namespace com.atproto.sync.subscribeRepos { 705 496 @subscription 706 497 @errors(FutureCursor, ConsumerTooSlow) 707 - op main( 708 - cursor?: integer 709 - ): Commit | Sync | unknown; 710 - 711 - model Commit { 712 - // ... 713 - } 714 - 715 - model Sync { 716 - // ... 717 - } 498 + op main(cursor?: integer): Commit | Sync | unknown; 718 499 500 + model Commit { /* ... */ } 501 + model Sync { /* ... */ } 719 502 model FutureCursor {} 720 503 model ConsumerTooSlow {} 721 504 } 722 505 ``` 723 506 724 - This becomes: 507 + Output: 725 508 726 509 ```json 727 - { 728 - "lexicon": 1, 729 - "id": "com.atproto.sync.subscribeRepos", 730 - "defs": { 731 - "main": { 732 - "type": "subscription", 733 - "parameters": { 734 - "type": "params", 735 - "properties": { 736 - "cursor": { /* ... */ } 737 - } 738 - }, 739 - "message": { 740 - "schema": { 741 - "type": "union", 742 - "refs": ["#commit", "#sync"] 743 - } 744 - }, 745 - "errors": [{ "name": "FutureCursor" }, { "name": "ConsumerTooSlow" }] 746 - }, 747 - "commit": { /* ... */ }, 748 - "sync": { /* ... */ } 749 - } 510 + // ... 511 + "main": { 512 + "type": "subscription", 513 + "parameters": { 514 + "type": "params", 515 + "properties": { "cursor": { /* ... */ } } 516 + }, 517 + "message": { 518 + "schema": { 519 + "type": "union", 520 + "refs": ["#commit", "#sync"] 521 + } 522 + }, 523 + "errors": [{ "name": "FutureCursor" }, { "name": "ConsumerTooSlow" }] 750 524 } 525 + // ... 751 526 ``` 752 527 753 528 ### Tokens 754 529 755 - Empty models marked with `@token` become token definitions: 530 + Use `@token` for empty token models: 756 531 757 532 ```typescript 758 - /** Indicates spam content */ 759 - @token 760 - model ReasonSpam {} 533 + namespace com.example.moderation.defs { 534 + @token 535 + model ReasonSpam {} 761 536 762 - /** Indicates policy violation */ 763 - @token 764 - model ReasonViolation {} 537 + @token 538 + model ReasonViolation {} 765 539 766 - model Report { 767 - @required reason: (ReasonSpam | ReasonViolation | unknown); 540 + model Report { 541 + @required reason: (ReasonSpam | ReasonViolation | unknown); 542 + } 768 543 } 769 544 ``` 770 545 771 - **Maps to:** 546 + Output: 547 + 772 548 ```json 773 - { 774 - "report": { 775 - "properties": { 776 - "reason": { 777 - "type": "union", 778 - "refs": ["#reasonSpam", "#reasonViolation"] 779 - } 549 + // ... 550 + "reasonSpam": { "type": "token" }, 551 + "reasonViolation": { "type": "token" }, 552 + "report": { 553 + "type": "object", 554 + "properties": { 555 + "reason": { 556 + "type": "union", 557 + "refs": ["#reasonSpam", "#reasonViolation"] 780 558 } 781 559 }, 782 - "reasonSpam": { 783 - "type": "token", 784 - "description": "Indicates spam content" 785 - } 560 + "required": ["reason"] 786 561 } 562 + // ... 787 563 ``` 788 564 789 565 ## Data Types ··· 824 600 Use `[]` suffix: 825 601 826 602 ```typescript 827 - model Main { 828 - /** Array of strings */ 829 - stringArray?: string[]; 603 + import "@typelex/emitter"; 830 604 831 - /** Array with size constraints */ 832 - @minItems(1) 833 - @maxItems(10) 834 - limitedArray?: integer[]; 605 + namespace com.example.arrays { 606 + model Main { 607 + stringArray?: string[]; 835 608 836 - /** Array of references */ 837 - items?: Item[]; 609 + @minItems(1) 610 + @maxItems(10) 611 + limitedArray?: integer[]; 838 612 839 - /** Array of union types */ 840 - mixed?: (TypeA | TypeB | unknown)[]; 613 + items?: Item[]; 614 + mixed?: (TypeA | TypeB | unknown)[]; 615 + } 616 + // ... 841 617 } 842 618 ``` 843 619 844 - **Maps to:** `{"type": "array", "items": {...}}` 620 + Output: `{ "type": "array", "items": {...} }`. 845 621 846 - **Note:** `@minItems`/`@maxItems` in TypeSpec map to `minLength`/`maxLength` in JSON. 622 + Note: `@minItems`/`@maxItems` map to `minLength`/`maxLength` in JSON. 847 623 848 624 ### Blobs 849 625 850 626 ```typescript 851 - model Main { 852 - /** Basic blob */ 853 - file?: Blob; 627 + import "@typelex/emitter"; 854 628 855 - /** Image up to 5MB */ 856 - image?: Blob<#["image/*"], 5000000>; 857 - 858 - /** Specific types up to 2MB */ 859 - photo?: Blob<#["image/png", "image/jpeg"], 2000000>; 629 + namespace com.example.blobs { 630 + model Main { 631 + file?: Blob; 632 + image?: Blob<#["image/*"], 5000000>; 633 + photo?: Blob<#["image/png", "image/jpeg"], 2000000>; 634 + } 860 635 } 861 636 ``` 862 637 863 - **Maps to:** 638 + Output: 639 + 864 640 ```json 865 - { 866 - "file": {"type": "blob"}, 867 - "image": { 868 - "type": "blob", 869 - "accept": ["image/*"], 870 - "maxSize": 5000000 871 - } 641 + // ... 642 + "image": { 643 + "type": "blob", 644 + "accept": ["image/*"], 645 + "maxSize": 5000000 872 646 } 647 + // ... 873 648 ``` 874 649 875 650 ## Required and Optional Fields 876 651 877 - In Lexicon, the default is to make fields optional. Use `?:` for that: 652 + In Lexicon, fields are optional by default. Use `?:`: 878 653 879 654 ```typescript 880 655 import "@typelex/emitter"; 881 656 882 657 namespace tools.ozone.moderation.defs { 883 658 model SubjectStatusView { 884 - subjectBlobCids?: cid[]; 885 659 subjectRepoHandle?: string; 886 660 } 887 661 } 888 662 ``` 889 663 890 - This becomes: 664 + **Think thrice before adding required fields**—you can't make them optional later. 891 665 892 - ```json 893 - { 894 - "lexicon": 1, 895 - "id": "tools.ozone.moderation.defs", 896 - "defs": { 897 - "subjectStatusView": { 898 - "type": "object", 899 - "properties": { 900 - "subjectBlobCids": { 901 - "type": "array", 902 - "items": { "type": "string", "format": "cid" } 903 - }, 904 - "subjectRepoHandle": { "type": "string" } 905 - } 906 - } 907 - } 908 - } 909 - ``` 910 - 911 - Think twice before adding a required field because you won't be able to make it optional later without violating the schema contract (and making all existing records invalid). In practice, this often means you'd have to deprecate the field and create another field, which is messy. 912 - 913 - This is why required fields need a special `@required` decorator forcing you to think twice. 666 + This is why `@required` is explicit: 914 667 915 668 ```typescript 916 669 import "@typelex/emitter"; 917 670 918 671 namespace tools.ozone.moderation.defs { 919 672 model SubjectStatusView { 920 - subjectBlobCids?: cid[]; 921 673 subjectRepoHandle?: string; 922 - 923 674 @required createdAt: datetime; 924 675 } 925 676 } 926 677 ``` 927 678 928 - This becomes: 679 + Output: 929 680 930 681 ```json 931 682 // ... 932 683 "required": ["createdAt"] 684 + // ... 933 685 ``` 934 686 935 687 ## Unions 936 688 937 689 ### Open Unions (Recommended) 938 690 939 - In Lexicon, unions like "A or B" default to being *open*, i.e. allowing you to add C in the future. 940 - 941 - To declare an open union, write `| unknown` at the end, like `Images | Video | unknown`: 691 + Unions default to being *open*—allowing you to add more options later. Write `| unknown`: 942 692 943 693 ```typescript 944 694 import "@typelex/emitter"; ··· 948 698 embed?: Images | Video | unknown; 949 699 } 950 700 951 - model Images { 952 - // ... 953 - } 954 - 955 - model Video { 956 - // ... 957 - } 701 + model Images { /* ... */ } 702 + model Video { /* ... */ } 958 703 } 959 704 ``` 960 705 961 - This produces a `union`: 706 + Output: 962 707 963 708 ```json 964 - { 965 - "lexicon": 1, 966 - "id": "app.bsky.feed.post", 967 - "defs": { 968 - "main": { 969 - "type": "object", 970 - "properties": { 971 - "embed": { 972 - "type": "union", 973 - "refs": ["#images", "#video"] 974 - } 975 - } 976 - }, 977 - "images": { /* ... */ }, 978 - "video": { /* ... */ } 979 - } 709 + // ... 710 + "embed": { 711 + "type": "union", 712 + "refs": ["#images", "#video"] 980 713 } 714 + // ... 981 715 ``` 982 716 983 - Then later you can add more types to the union. 984 - 985 - You may also write the same thing with the `union` syntax instead of writing `A | B | C | unknown` directly: 717 + You can also use the `union` syntax to give it a name: 986 718 987 719 ```typescript 720 + import "@typelex/emitter"; 721 + 988 722 namespace app.bsky.feed.post { 989 723 model Main { 990 724 embed?: EmbedType; ··· 992 726 993 727 @inline union EmbedType { Images, Video, unknown } 994 728 995 - model Images { 996 - // ... 997 - } 998 - 999 - model Video { 1000 - // ... 1001 - } 729 + model Images { /* ... */ } 730 + model Video { /* ... */ } 1002 731 } 1003 732 ``` 1004 733 1005 - This is completely equivalent. The `@inline` decorator states that it doesn't become a part of your defs. 734 + The `@inline` prevents it from becoming a separate def in the output. 1006 735 1007 736 ### Known Values (Open Enums) 1008 737 1009 - You can suggest common values but allow others: 738 + Suggest common values but allow others with `| string`: 1010 739 1011 740 ```typescript 1012 741 import "@typelex/emitter"; ··· 1018 747 } 1019 748 ``` 1020 749 1021 - Note the `| string` that says you may add more "known" values later. 1022 - 1023 - The `union` syntax also works for this: 750 + The `union` syntax works here too: 1024 751 1025 752 ```typescript 1026 753 import "@typelex/emitter"; ··· 1034 761 } 1035 762 ``` 1036 763 1037 - Here, `@inline` prevents creation of a `languages` top-level def. If you *do* want to make it reusable from other Lexicons, remove the `@inline` annotation and refer to `com.example.Languages` from somewhere else. 764 + You can remove `@inline` to make it a reusable `def` accessible from other Lexicons. 1038 765 1039 766 ### Closed Unions and Enums (Discouraged) 1040 767 1041 - If you want to create a *closed* union (which are **heavily discouraged** in Lexicon), you can mark your union with a `@closed` decorator. This will let you remove the `unknown` case from it. 768 + **Heavily discouraged** in Lexicon. 1042 769 1043 - There is no shorthand notation for closed unions, so you'll have to write `@closed @inline union { A, B }`. 770 + Marking a `union` as `@closed` lets you remove `unknown` from the list of options: 1044 771 1045 772 ```typescript 1046 773 import "@typelex/emitter"; 1047 774 1048 775 namespace com.atproto.repo.applyWrites { 1049 - @procedure 1050 - op main(input: { 1051 - @required 1052 - writes: WriteAction[]; 1053 - }): { 1054 - // ... 1055 - }; 776 + model Main { 777 + @required writes: WriteAction[]; 778 + } 1056 779 1057 780 @closed // Discouraged! 1058 781 @inline 1059 - union WriteAction { Create, Update, Delete, } 782 + union WriteAction { Create, Update, Delete } 1060 783 1061 - model Create { 1062 - // ... 1063 - } 1064 - 1065 - model Update { 1066 - // ... 1067 - } 1068 - 1069 - model Delete { 1070 - // ... 1071 - } 784 + model Create { /* ... */ } 785 + model Update { /* ... */ } 786 + model Delete { /* ... */ } 1072 787 } 1073 788 ``` 1074 789 1075 - This gives you: 790 + Output: 1076 791 1077 792 ```json 793 + // ... 1078 794 "writes": { 1079 795 "type": "array", 1080 796 "items": { ··· 1083 799 "closed": true 1084 800 } 1085 801 } 802 + // ... 1086 803 ``` 1087 804 1088 - You can also declare this with strings or numbers: 805 + With strings or numbers, this becomes a closed `enum`: 1089 806 1090 807 ```typescript 1091 - @closed // Discouraged! 1092 - @inline 1093 - union WriteAction { "create", "update", "delete" } 808 + import "@typelex/emitter"; 809 + 810 + namespace com.atproto.repo.applyWrites { 811 + model Main { 812 + @required action: WriteAction; 813 + } 814 + 815 + @closed // Discouraged! 816 + @inline 817 + union WriteAction { "create", "update", "delete" } 818 + } 1094 819 ``` 1095 820 1096 - These would become a Lexicon closed `enum`: 821 + Output: 1097 822 1098 823 ```json 1099 - "items": { 1100 - "type": "string", 1101 - "enum": ["create", "update", "delete"] 1102 - } 824 + // ... 825 + "type": "string", 826 + "enum": ["create", "update", "delete"] 827 + // ... 1103 828 ``` 1104 829 1105 - Avoid closed unions and closed enums if you can. 830 + Avoid closed unions/enums when possible. 1106 831 1107 832 ## Constraints 1108 833 1109 - ### String Constraints 834 + ### Strings 1110 835 1111 836 ```typescript 1112 - model Main { 1113 - /** Byte length constraints */ 1114 - @minLength(1) 1115 - @maxLength(100) 1116 - text?: string; 837 + import "@typelex/emitter"; 1117 838 1118 - /** Grapheme cluster length constraints */ 1119 - @minGraphemes(1) 1120 - @maxGraphemes(50) 1121 - displayName?: string; 839 + namespace com.example { 840 + model Main { 841 + @minLength(1) 842 + @maxLength(100) 843 + text?: string; 844 + 845 + @minGraphemes(1) 846 + @maxGraphemes(50) 847 + displayName?: string; 848 + } 1122 849 } 1123 850 ``` 1124 851 1125 - **Maps to:** `minLength`/`maxLength`, `minGraphemes`/`maxGraphemes` 852 + Maps to: `minLength`/`maxLength`, `minGraphemes`/`maxGraphemes` 1126 853 1127 - ### Integer Constraints 854 + ### Integers 1128 855 1129 856 ```typescript 1130 - model Main { 1131 - @minValue(1) 1132 - @maxValue(100) 1133 - score?: integer; 857 + import "@typelex/emitter"; 858 + 859 + namespace com.example { 860 + model Main { 861 + @minValue(1) 862 + @maxValue(100) 863 + score?: integer; 864 + } 1134 865 } 1135 866 ``` 1136 867 1137 - **Maps to:** `minimum`/`maximum` 868 + Maps to: `minimum`/`maximum` 1138 869 1139 - ### Bytes Constraints 870 + ### Bytes 1140 871 1141 872 ```typescript 1142 - model Main { 1143 - @minBytes(1) 1144 - @maxBytes(1024) 1145 - data?: bytes; 873 + import "@typelex/emitter"; 874 + 875 + namespace com.example { 876 + model Main { 877 + @minBytes(1) 878 + @maxBytes(1024) 879 + data?: bytes; 880 + } 1146 881 } 1147 882 ``` 1148 883 1149 - **Maps to:** `minLength`/`maxLength` 884 + Maps to: `minLength`/`maxLength` 1150 885 1151 - **Note:** Use `@minBytes`/`@maxBytes` in TypeSpec, but they map to `minLength`/`maxLength` in JSON. 1152 - 1153 - ### Array Constraints 886 + ### Arrays 1154 887 1155 888 ```typescript 1156 - model Main { 1157 - @minItems(1) 1158 - @maxItems(10) 1159 - items?: string[]; 889 + import "@typelex/emitter"; 890 + 891 + namespace com.example { 892 + model Main { 893 + @minItems(1) 894 + @maxItems(10) 895 + items?: string[]; 896 + } 1160 897 } 1161 898 ``` 1162 899 1163 - **Maps to:** `minLength`/`maxLength` 900 + Maps to: `minLength`/`maxLength` 1164 901 1165 - **Note:** Use `@minItems`/`@maxItems` in TypeSpec, but they map to `minLength`/`maxLength` in JSON. 1166 - 1167 - ## Default and Constant Values 902 + ## Defaults and Constants 1168 903 1169 904 ### Defaults 1170 905 1171 906 ```typescript 1172 - model Main { 1173 - version?: integer = 1; 1174 - lang?: string = "en"; 907 + import "@typelex/emitter"; 908 + 909 + namespace com.example { 910 + model Main { 911 + version?: integer = 1; 912 + lang?: string = "en"; 913 + } 1175 914 } 1176 915 ``` 1177 916 1178 - **Maps to:** `{"default": 1}`, `{"default": "en"}` 917 + Maps to: `{"default": 1}`, `{"default": "en"}` 1179 918 1180 919 ### Constants 1181 920 1182 - Use `@readOnly` with default value: 921 + Use `@readOnly` with a default: 1183 922 1184 923 ```typescript 1185 - model Main { 1186 - @readOnly status?: string = "active"; 924 + import "@typelex/emitter"; 925 + 926 + namespace com.example { 927 + model Main { 928 + @readOnly status?: string = "active"; 929 + } 1187 930 } 1188 931 ``` 1189 932 1190 - **Maps to:** `{"const": "active"}` 933 + Maps to: `{"const": "active"}` 1191 934 1192 935 ## Nullable Fields 1193 936 1194 937 Use `| null` for nullable fields: 1195 938 1196 939 ```typescript 1197 - model Main { 1198 - @required createdAt: datetime; 1199 - updatedAt?: datetime | null; // can be omitted or null 1200 - deletedAt?: datetime; // can only be omitted 1201 - } 1202 - ``` 940 + import "@typelex/emitter"; 1203 941 1204 - **Maps to:** 1205 - ```json 1206 - { 1207 - "required": ["createdAt"], 1208 - "nullable": ["updatedAt"], 1209 - "properties": { ... } 942 + namespace com.example { 943 + model Main { 944 + @required createdAt: datetime; 945 + updatedAt?: datetime | null; // can be omitted or null 946 + deletedAt?: datetime; // can only be omitted 947 + } 1210 948 } 1211 949 ``` 1212 950 1213 - ## Naming Conventions 951 + Output: 1214 952 1215 - Model names convert from PascalCase to camelCase in defs: 1216 - 1217 - ```typescript 1218 - model StatusEnum { ... } // becomes "statusEnum" 1219 - model UserMetadata { ... } // becomes "userMetadata" 1220 - model Main { ... } // becomes "main" 953 + ```json 954 + // ... 955 + "required": ["createdAt"], 956 + "nullable": ["updatedAt"] 957 + // ... 1221 958 ```
+2 -2
packages/example/package.json
··· 5 5 "type": "module", 6 6 "scripts": { 7 7 "build": "pnpm run build:typelex && pnpm run build:codegen", 8 - "build:typelex": "tsp compile typelex/main.tsp", 9 - "build:codegen": "lex gen-server --yes ./src lexicon/app/example/*.json" 8 + "build:lexicons": "tsp compile typelex/main.tsp", 9 + "build:codegen": "lex gen-server --yes ./src lexicons/app/example/*.json" 10 10 }, 11 11 "dependencies": { 12 12 "@atproto/lex-cli": "^0.9.5",
+1 -1
packages/example/tspconfig.yaml
··· 2 2 - "@typelex/emitter" 3 3 options: 4 4 "@typelex/emitter": 5 - output-dir: "./lexicon" 5 + output-dir: "./lexicons"
+3 -3
packages/website/src/pages/index.astro
··· 343 343 - "@typelex/emitter" 344 344 options: 345 345 "@typelex/emitter": 346 - output-dir: "./lexicon"`, 'yaml')} /> 346 + output-dir: "./lexicons"`, 'yaml')} /> 347 347 </div> 348 348 </div> 349 349 ··· 354 354 <figure class="install-box" set:html={await highlightCode(`{ 355 355 "scripts": { 356 356 // ... 357 - "build:lexicon": "tsp compile typelex/main.tsp" 357 + "build:lexicons": "tsp compile typelex/main.tsp" 358 358 } 359 359 }`, 'json')} /> 360 360 </div> ··· 364 364 <div class="step-number">5</div> 365 365 <div class="step-content"> 366 366 <h3>Generate Lexicon files</h3> 367 - <figure class="install-box" set:html={await highlightCode(`npm run build:lexicon`, 'bash')} /> 367 + <figure class="install-box" set:html={await highlightCode(`npm run build:lexicons`, 'bash')} /> 368 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 369 </div> 370 370 </div>