+346
-609
DOCS.md
+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
+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
+1
-1
packages/example/tspconfig.yaml
+3
-3
packages/website/src/pages/index.astro
+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>