@gaearon/lex-builder 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/dist/filter.d.ts +7 -0
  3. package/dist/filter.d.ts.map +1 -0
  4. package/dist/filter.js +30 -0
  5. package/dist/filter.js.map +1 -0
  6. package/dist/filtered-indexer.d.ts +2100 -0
  7. package/dist/filtered-indexer.d.ts.map +1 -0
  8. package/dist/filtered-indexer.js +56 -0
  9. package/dist/filtered-indexer.js.map +1 -0
  10. package/dist/formatter.d.ts +13 -0
  11. package/dist/formatter.d.ts.map +1 -0
  12. package/dist/formatter.js +34 -0
  13. package/dist/formatter.js.map +1 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +16 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lex-builder.d.ts +36 -0
  19. package/dist/lex-builder.d.ts.map +1 -0
  20. package/dist/lex-builder.js +144 -0
  21. package/dist/lex-builder.js.map +1 -0
  22. package/dist/lex-def-builder.d.ts +69 -0
  23. package/dist/lex-def-builder.d.ts.map +1 -0
  24. package/dist/lex-def-builder.js +734 -0
  25. package/dist/lex-def-builder.js.map +1 -0
  26. package/dist/lexicon-directory-indexer.d.ts +11 -0
  27. package/dist/lexicon-directory-indexer.d.ts.map +1 -0
  28. package/dist/lexicon-directory-indexer.js +46 -0
  29. package/dist/lexicon-directory-indexer.js.map +1 -0
  30. package/dist/polyfill.d.ts +1 -0
  31. package/dist/polyfill.d.ts.map +1 -0
  32. package/dist/polyfill.js +7 -0
  33. package/dist/polyfill.js.map +1 -0
  34. package/dist/ref-resolver.d.ts +53 -0
  35. package/dist/ref-resolver.d.ts.map +1 -0
  36. package/dist/ref-resolver.js +277 -0
  37. package/dist/ref-resolver.js.map +1 -0
  38. package/dist/ts-lang.d.ts +6 -0
  39. package/dist/ts-lang.d.ts.map +1 -0
  40. package/dist/ts-lang.js +150 -0
  41. package/dist/ts-lang.js.map +1 -0
  42. package/dist/util.d.ts +12 -0
  43. package/dist/util.d.ts.map +1 -0
  44. package/dist/util.js +72 -0
  45. package/dist/util.js.map +1 -0
  46. package/package.json +53 -0
  47. package/src/filter.ts +41 -0
  48. package/src/filtered-indexer.test.ts +84 -0
  49. package/src/filtered-indexer.ts +60 -0
  50. package/src/formatter.ts +42 -0
  51. package/src/index.ts +23 -0
  52. package/src/lex-builder.ts +186 -0
  53. package/src/lex-def-builder.ts +980 -0
  54. package/src/lexicon-directory-indexer.ts +52 -0
  55. package/src/polyfill.ts +7 -0
  56. package/src/ref-resolver.test.ts +75 -0
  57. package/src/ref-resolver.ts +368 -0
  58. package/src/ts-lang.ts +150 -0
  59. package/src/util.ts +72 -0
  60. package/tsconfig.build.json +13 -0
  61. package/tsconfig.json +7 -0
  62. package/tsconfig.tests.json +9 -0
@@ -0,0 +1,980 @@
1
+ import {
2
+ JSDocStructure,
3
+ OptionalKind,
4
+ SourceFile,
5
+ VariableDeclarationKind,
6
+ } from 'ts-morph'
7
+ import {
8
+ LexiconArray,
9
+ LexiconArrayItems,
10
+ LexiconBlob,
11
+ LexiconBoolean,
12
+ LexiconBytes,
13
+ LexiconCid,
14
+ LexiconDocument,
15
+ LexiconError,
16
+ LexiconIndexer,
17
+ LexiconInteger,
18
+ LexiconObject,
19
+ LexiconParameters,
20
+ LexiconPayload,
21
+ LexiconPermissionSet,
22
+ LexiconProcedure,
23
+ LexiconQuery,
24
+ LexiconRecord,
25
+ LexiconRef,
26
+ LexiconRefUnion,
27
+ LexiconString,
28
+ LexiconSubscription,
29
+ LexiconToken,
30
+ LexiconUnknown,
31
+ } from '@atproto/lex-document'
32
+ import { l } from '@atproto/lex-schema'
33
+ import {
34
+ RefResolver,
35
+ RefResolverOptions,
36
+ ResolvedRef,
37
+ getPublicIdentifiers,
38
+ } from './ref-resolver.js'
39
+ import { asNamespaceExport } from './ts-lang.js'
40
+
41
+ export type LexDefBuilderOptions = RefResolverOptions & {
42
+ lib?: string
43
+ allowLegacyBlobs?: boolean
44
+ pureAnnotations?: boolean
45
+ }
46
+
47
+ /**
48
+ * Utility class to build a TypeScript source file from a lexicon document.
49
+ */
50
+ export class LexDefBuilder {
51
+ private readonly refResolver: RefResolver
52
+
53
+ constructor(
54
+ private readonly options: LexDefBuilderOptions,
55
+ private readonly file: SourceFile,
56
+ private readonly doc: LexiconDocument,
57
+ indexer: LexiconIndexer,
58
+ ) {
59
+ this.refResolver = new RefResolver(doc, file, indexer, options)
60
+ }
61
+
62
+ private pure(code: string) {
63
+ return this.options.pureAnnotations ? markPure(code) : code
64
+ }
65
+
66
+ async build() {
67
+ this.file.addVariableStatement({
68
+ declarationKind: VariableDeclarationKind.Const,
69
+ declarations: [
70
+ { name: '$nsid', initializer: JSON.stringify(this.doc.id) },
71
+ ],
72
+ })
73
+
74
+ this.file.addExportDeclaration({
75
+ namedExports: [{ name: '$nsid' }],
76
+ })
77
+
78
+ const defs = Object.keys(this.doc.defs)
79
+ if (defs.length) {
80
+ const moduleSpecifier = this.options?.lib ?? '@atproto/lex-schema'
81
+ this.file
82
+ .addImportDeclaration({ moduleSpecifier })
83
+ .addNamedImports([{ name: 'l' }])
84
+
85
+ for (const hash of defs) {
86
+ await this.addDef(hash)
87
+ }
88
+ }
89
+ }
90
+
91
+ private addUtils(definitions: Record<string, undefined | string>) {
92
+ const entries = Object.entries(definitions).filter(
93
+ (e): e is [(typeof e)[0], NonNullable<(typeof e)[1]>] => e[1] != null,
94
+ )
95
+ if (entries.length) {
96
+ this.file.addVariableStatement({
97
+ isExported: true,
98
+ declarationKind: VariableDeclarationKind.Const,
99
+ declarations: entries.map(([name, initializer]) => ({
100
+ name,
101
+ initializer,
102
+ })),
103
+ })
104
+ }
105
+ }
106
+
107
+ private async addDef(hash: string) {
108
+ const def = Object.hasOwn(this.doc.defs, hash) ? this.doc.defs[hash] : null
109
+ if (def == null) return
110
+
111
+ switch (def.type) {
112
+ case 'permission-set':
113
+ return this.addPermissionSet(hash, def)
114
+ case 'procedure':
115
+ return this.addProcedure(hash, def)
116
+ case 'query':
117
+ return this.addQuery(hash, def)
118
+ case 'subscription':
119
+ return this.addSubscription(hash, def)
120
+ case 'record':
121
+ return this.addRecord(hash, def)
122
+ case 'token':
123
+ return this.addToken(hash, def)
124
+ case 'object':
125
+ return this.addObject(hash, def)
126
+ case 'array':
127
+ return this.addArray(hash, def)
128
+ default:
129
+ await this.addSchema(hash, def, {
130
+ type: await this.compileContainedType(def),
131
+ schema: await this.compileContainedSchema(def),
132
+ validationUtils: true,
133
+ })
134
+ }
135
+ }
136
+
137
+ private async addPermissionSet(hash: string, def: LexiconPermissionSet) {
138
+ const permission = def.permissions.map((def) => {
139
+ const options = stringifyOptions(def, undefined, ['resource', 'type'])
140
+ return this.pure(
141
+ `l.permission(${JSON.stringify(def.resource)}, ${options})`,
142
+ )
143
+ })
144
+
145
+ const options = stringifyOptions(def, [
146
+ 'title',
147
+ 'title:lang',
148
+ 'detail',
149
+ 'detail:lang',
150
+ ] satisfies (keyof l.PermissionSetOptions)[])
151
+
152
+ await this.addSchema(hash, def, {
153
+ schema: this.pure(
154
+ `l.permissionSet($nsid, [${permission.join(',')}], ${options})`,
155
+ ),
156
+ })
157
+ }
158
+
159
+ private async addProcedure(hash: string, def: LexiconProcedure) {
160
+ if (hash !== 'main') {
161
+ throw new Error(`Definition ${hash} cannot be of type ${def.type}`)
162
+ }
163
+
164
+ // @TODO Build the types instead of using an inferred type.
165
+
166
+ const ref = await this.addSchema(hash, def, {
167
+ schema: this.pure(`
168
+ l.procedure(
169
+ $nsid,
170
+ ${await this.compileParamsSchema(def.parameters)},
171
+ ${await this.compilePayload(def.input)},
172
+ ${await this.compilePayload(def.output)},
173
+ ${await this.compileErrors(def.errors)}
174
+ )
175
+ `),
176
+ })
177
+
178
+ this.addMethodTypeUtils(ref, def)
179
+ this.addUtils({
180
+ $lxm: this.pure(`${ref.varName}.nsid`),
181
+ $params: this.pure(`${ref.varName}.parameters`),
182
+ $input: this.pure(`${ref.varName}.input`),
183
+ $output: this.pure(`${ref.varName}.output`),
184
+ })
185
+ }
186
+
187
+ private async addQuery(hash: string, def: LexiconQuery) {
188
+ if (hash !== 'main') {
189
+ throw new Error(`Definition ${hash} cannot be of type ${def.type}`)
190
+ }
191
+
192
+ // @TODO Build the types instead of using an inferred type.
193
+
194
+ const ref = await this.addSchema(hash, def, {
195
+ schema: this.pure(`
196
+ l.query(
197
+ $nsid,
198
+ ${await this.compileParamsSchema(def.parameters)},
199
+ ${await this.compilePayload(def.output)},
200
+ ${await this.compileErrors(def.errors)}
201
+ )
202
+ `),
203
+ })
204
+
205
+ this.addMethodTypeUtils(ref, def)
206
+ this.addUtils({
207
+ $lxm: this.pure(`${ref.varName}.nsid`),
208
+ $params: `${ref.varName}.parameters`,
209
+ $output: `${ref.varName}.output`,
210
+ })
211
+ }
212
+
213
+ private async addSubscription(hash: string, def: LexiconSubscription) {
214
+ if (hash !== 'main') {
215
+ throw new Error(`Definition ${hash} cannot be of type ${def.type}`)
216
+ }
217
+
218
+ // @TODO Build the types instead of using an inferred type.
219
+
220
+ const ref = await this.addSchema(hash, def, {
221
+ schema: this.pure(`
222
+ l.subscription(
223
+ $nsid,
224
+ ${await this.compileParamsSchema(def.parameters)},
225
+ ${await this.compileBodySchema(def.message?.schema)},
226
+ ${await this.compileErrors(def.errors)}
227
+ )
228
+ `),
229
+ })
230
+
231
+ this.addMethodTypeUtils(ref, def)
232
+ this.addUtils({
233
+ $lxm: this.pure(`${ref.varName}.nsid`),
234
+ $params: `${ref.varName}.parameters`,
235
+ $message: `${ref.varName}.message`,
236
+ })
237
+ }
238
+
239
+ addMethodTypeUtils(
240
+ ref: ResolvedRef,
241
+ def: LexiconProcedure | LexiconQuery | LexiconSubscription,
242
+ ) {
243
+ this.file.addTypeAlias({
244
+ isExported: true,
245
+ name: 'Params',
246
+ type: `l.InferMethodParams<typeof ${ref.varName}>`,
247
+ docs: compileDocs(def.parameters?.description),
248
+ })
249
+
250
+ if (def.type === 'procedure') {
251
+ this.file.addTypeAlias({
252
+ isExported: true,
253
+ name: 'Input',
254
+ type: `l.InferMethodInput<typeof ${ref.varName}>`,
255
+ docs: compileDocs(def.input?.description),
256
+ })
257
+
258
+ this.file.addTypeAlias({
259
+ isExported: true,
260
+ name: 'InputBody',
261
+ type: `l.InferMethodInputBody<typeof ${ref.varName}>`,
262
+ docs: compileDocs(def.input?.description),
263
+ })
264
+ }
265
+
266
+ if (def.type === 'procedure' || def.type === 'query') {
267
+ this.file.addTypeAlias({
268
+ isExported: true,
269
+ name: 'Output',
270
+ type: `l.InferMethodOutput<typeof ${ref.varName}>`,
271
+ docs: compileDocs(def.output?.description),
272
+ })
273
+
274
+ this.file.addTypeAlias({
275
+ isExported: true,
276
+ name: 'OutputBody',
277
+ type: `l.InferMethodOutputBody<typeof ${ref.varName}>`,
278
+ docs: compileDocs(def.output?.description),
279
+ })
280
+ }
281
+
282
+ if (def.type === 'subscription') {
283
+ this.file.addTypeAlias({
284
+ isExported: true,
285
+ name: 'Message',
286
+ type: `l.InferSubscriptionMessage<typeof ${ref.varName}>`,
287
+ docs: compileDocs(def.message?.description),
288
+ })
289
+ }
290
+ }
291
+
292
+ private async addRecord(hash: string, def: LexiconRecord) {
293
+ if (hash !== 'main') {
294
+ throw new Error(`Definition ${hash} cannot be of type ${def.type}`)
295
+ }
296
+
297
+ const key = JSON.stringify(def.key ?? 'any')
298
+ const objectSchema = await this.compileObjectSchema(def.record)
299
+
300
+ const properties = await this.compilePropertiesTypes(def.record)
301
+ properties.unshift(`$type: ${JSON.stringify(l.$type(this.doc.id, hash))}`)
302
+
303
+ await this.addSchema(hash, def, {
304
+ type: `{ ${properties.join(';')} }`,
305
+ schema: (ref) =>
306
+ this.pure(
307
+ `l.record<${key}, ${ref.typeName}>(${key}, $nsid, ${objectSchema})`,
308
+ ),
309
+ objectUtils: true,
310
+ validationUtils: true,
311
+ })
312
+ }
313
+
314
+ private async addObject(hash: string, def: LexiconObject) {
315
+ const objectSchema = await this.compileObjectSchema(def)
316
+
317
+ const properties = await this.compilePropertiesTypes(def)
318
+ properties.unshift(`$type?: ${JSON.stringify(l.$type(this.doc.id, hash))}`)
319
+
320
+ await this.addSchema(hash, def, {
321
+ type: `{ ${properties.join(';')} }`,
322
+ schema: (ref) =>
323
+ this.pure(
324
+ `l.typedObject<${ref.typeName}>($nsid, ${JSON.stringify(hash)}, ${objectSchema})`,
325
+ ),
326
+ objectUtils: true,
327
+ validationUtils: true,
328
+ })
329
+ }
330
+
331
+ private async addToken(hash: string, def: LexiconToken) {
332
+ await this.addSchema(hash, def, {
333
+ schema: this.pure(`l.token($nsid, ${JSON.stringify(hash)})`),
334
+ type: JSON.stringify(l.$type(this.doc.id, hash)),
335
+ validationUtils: true,
336
+ })
337
+ }
338
+
339
+ private async addArray(hash: string, def: LexiconArray) {
340
+ // @TODO It could be nice to expose the array item type as a separate type.
341
+ // This was not done (yet) as there is no easy way to name it to avoid
342
+ // collisions.
343
+
344
+ const itemSchema = await this.compileContainedSchema(def.items)
345
+ const options = stringifyOptions(def, [
346
+ 'minLength',
347
+ 'maxLength',
348
+ ] satisfies (keyof l.ArraySchemaOptions)[])
349
+
350
+ await this.addSchema(hash, def, {
351
+ type: `(${await this.compileContainedType(def.items)})[]`,
352
+ // @NOTE Not using compileArraySchema to allow specifying the generic
353
+ // parameter to l.array<>.
354
+ schema: (ref) =>
355
+ this.pure(
356
+ `l.array<${ref.typeName}[number]>(${itemSchema}, ${options})`,
357
+ ),
358
+ validationUtils: true,
359
+ })
360
+ }
361
+
362
+ private async addSchema(
363
+ hash: string,
364
+ def: { description?: string },
365
+ {
366
+ type,
367
+ schema,
368
+ objectUtils,
369
+ validationUtils,
370
+ }: {
371
+ type?: string | ((ref: ResolvedRef) => string)
372
+ schema?: string | ((ref: ResolvedRef) => string)
373
+ objectUtils?: boolean
374
+ validationUtils?: boolean
375
+ },
376
+ ): Promise<ResolvedRef> {
377
+ const ref = await this.refResolver.resolveLocal(hash)
378
+ const pub = getPublicIdentifiers(hash)
379
+
380
+ if (type) {
381
+ this.file.addTypeAlias({
382
+ name: ref.typeName,
383
+ type: typeof type === 'function' ? type(ref) : type,
384
+ docs: compileDocs(def.description),
385
+ })
386
+
387
+ this.file.addExportDeclaration({
388
+ isTypeOnly: true,
389
+ namedExports: [
390
+ {
391
+ name: ref.typeName,
392
+ alias:
393
+ ref.typeName === pub.typeName
394
+ ? undefined
395
+ : asNamespaceExport(pub.typeName),
396
+ },
397
+ ],
398
+ })
399
+ }
400
+
401
+ if (schema) {
402
+ this.file.addVariableStatement({
403
+ declarationKind: VariableDeclarationKind.Const,
404
+ declarations: [
405
+ {
406
+ name: ref.varName,
407
+ initializer: typeof schema === 'function' ? schema(ref) : schema,
408
+ },
409
+ ],
410
+ docs: compileDocs(def.description),
411
+ })
412
+
413
+ this.file.addExportDeclaration({
414
+ namedExports: [
415
+ {
416
+ name: ref.varName,
417
+ alias:
418
+ ref.varName === pub.varName
419
+ ? undefined
420
+ : asNamespaceExport(pub.varName),
421
+ },
422
+ ],
423
+ })
424
+ }
425
+
426
+ if (hash === 'main' && objectUtils) {
427
+ this.addUtils({
428
+ $isTypeOf: markPure(`${ref.varName}.isTypeOf.bind(${ref.varName})`),
429
+ $build: markPure(`${ref.varName}.build.bind(${ref.varName})`),
430
+ $type: markPure(`${ref.varName}.$type`),
431
+ })
432
+ }
433
+
434
+ if (hash === 'main' && validationUtils) {
435
+ this.addUtils({
436
+ $assert: markPure(`${ref.varName}.assert.bind(${ref.varName})`),
437
+ $check: markPure(`${ref.varName}.check.bind(${ref.varName})`),
438
+ $cast: markPure(`${ref.varName}.cast.bind(${ref.varName})`),
439
+ $ifMatches: markPure(`${ref.varName}.ifMatches.bind(${ref.varName})`),
440
+ $matches: markPure(`${ref.varName}.matches.bind(${ref.varName})`),
441
+ $parse: markPure(`${ref.varName}.parse.bind(${ref.varName})`),
442
+ $safeParse: markPure(`${ref.varName}.safeParse.bind(${ref.varName})`),
443
+ $validate: markPure(`${ref.varName}.validate.bind(${ref.varName})`),
444
+ $safeValidate: markPure(
445
+ `${ref.varName}.safeValidate.bind(${ref.varName})`,
446
+ ),
447
+ })
448
+ }
449
+
450
+ return ref
451
+ }
452
+
453
+ private async compilePayload(def: LexiconPayload | undefined) {
454
+ if (!def) return this.pure(`l.payload()`)
455
+
456
+ // Special case for JSON object payloads
457
+ if (def.encoding === 'application/json' && def.schema?.type === 'object') {
458
+ const properties = await this.compilePropertiesSchemas(def.schema)
459
+ return this.pure(`l.jsonPayload({${properties.join(',')}})`)
460
+ }
461
+
462
+ const encodedEncoding = JSON.stringify(def.encoding)
463
+ if (def.schema) {
464
+ const bodySchema = await this.compileBodySchema(def.schema)
465
+ return this.pure(`l.payload(${encodedEncoding}, ${bodySchema})`)
466
+ } else {
467
+ return this.pure(`l.payload(${encodedEncoding})`)
468
+ }
469
+ }
470
+
471
+ private async compileBodySchema(
472
+ def?: LexiconRef | LexiconRefUnion | LexiconObject,
473
+ ): Promise<string> {
474
+ if (!def) return 'undefined'
475
+ if (def.type === 'object') return this.compileObjectSchema(def)
476
+ return this.compileContainedSchema(def)
477
+ }
478
+
479
+ private async compileParamsSchema(def: undefined | LexiconParameters) {
480
+ if (!def) return this.pure(`l.params()`)
481
+
482
+ const properties = await this.compilePropertiesSchemas(def)
483
+ return this.pure(
484
+ properties.length === 0
485
+ ? `l.params()`
486
+ : `l.params({${properties.join(',')}})`,
487
+ )
488
+ }
489
+
490
+ private async compileErrors(defs?: readonly LexiconError[]) {
491
+ if (!defs?.length) return ''
492
+ return JSON.stringify(defs.map((d) => d.name))
493
+ }
494
+
495
+ private async compileObjectSchema(def: LexiconObject): Promise<string> {
496
+ const properties = await this.compilePropertiesSchemas(def)
497
+ return this.pure(`l.object({${properties.join(',')}})`)
498
+ }
499
+
500
+ private async compilePropertiesSchemas(options: {
501
+ properties: Record<string, LexiconArray | LexiconArrayItems>
502
+ required?: readonly string[]
503
+ nullable?: readonly string[]
504
+ }): Promise<string[]> {
505
+ for (const opt of ['required', 'nullable'] as const) {
506
+ if (options[opt]) {
507
+ for (const prop of options[opt]) {
508
+ if (!Object.hasOwn(options.properties, prop)) {
509
+ throw new Error(`No schema found for ${opt} property "${prop}"`)
510
+ }
511
+ }
512
+ }
513
+ }
514
+
515
+ return Promise.all(
516
+ Object.entries(options.properties).map((entry) => {
517
+ return this.compilePropertyEntrySchema(entry, options)
518
+ }),
519
+ )
520
+ }
521
+
522
+ private async compilePropertiesTypes(options: {
523
+ properties: Record<string, LexiconArray | LexiconArrayItems>
524
+ required?: readonly string[]
525
+ nullable?: readonly string[]
526
+ }) {
527
+ return Promise.all(
528
+ Object.entries(options.properties).map((entry) => {
529
+ return this.compilePropertyEntryType(entry, options)
530
+ }),
531
+ )
532
+ }
533
+
534
+ private async compilePropertyEntrySchema(
535
+ [key, def]: [string, LexiconArray | LexiconArrayItems],
536
+ options: {
537
+ required?: readonly string[]
538
+ nullable?: readonly string[]
539
+ },
540
+ ) {
541
+ const isNullable = options.nullable?.includes(key)
542
+ const isRequired = options.required?.includes(key)
543
+
544
+ let schema = await this.compileContainedSchema(def)
545
+
546
+ if (isNullable) {
547
+ schema = this.pure(`l.nullable(${schema})`)
548
+ }
549
+
550
+ if (!isRequired) {
551
+ schema = this.pure(`l.optional(${schema})`)
552
+ }
553
+
554
+ return `${JSON.stringify(key)}:${schema}`
555
+ }
556
+
557
+ private async compilePropertyEntryType(
558
+ [key, def]: [string, LexiconArray | LexiconArrayItems],
559
+ options: {
560
+ required?: readonly string[]
561
+ nullable?: readonly string[]
562
+ },
563
+ ) {
564
+ const isNullable = options.nullable?.includes(key)
565
+ const isRequired = options.required?.includes(key)
566
+
567
+ const optional = isRequired ? '' : '?'
568
+ const append = isNullable ? ' | null' : ''
569
+
570
+ const jsDoc = compileLeadingTrivia(def.description) || ''
571
+ const name = JSON.stringify(key)
572
+ const type = await this.compileContainedType(def)
573
+
574
+ return `${jsDoc}${name}${optional}:${type}${append}`
575
+ }
576
+
577
+ private async compileContainedSchema(
578
+ def: LexiconArray | LexiconArrayItems,
579
+ ): Promise<string> {
580
+ switch (def.type) {
581
+ case 'unknown':
582
+ return this.compileUnknownSchema(def)
583
+ case 'boolean':
584
+ return this.compileBooleanSchema(def)
585
+ case 'integer':
586
+ return this.compileIntegerSchema(def)
587
+ case 'string':
588
+ return this.compileStringSchema(def)
589
+ case 'bytes':
590
+ return this.compileBytesSchema(def)
591
+ case 'blob':
592
+ return this.compileBlobSchema(def)
593
+ case 'cid-link':
594
+ return this.compileCidLinkSchema(def)
595
+ case 'ref':
596
+ return this.compileRefSchema(def)
597
+ case 'union':
598
+ return this.compileRefUnionSchema(def)
599
+ case 'array':
600
+ return this.compileArraySchema(def)
601
+ default:
602
+ // @ts-expect-error
603
+ throw new Error(`Unsupported def type: ${def.type}`)
604
+ }
605
+ }
606
+
607
+ private async compileContainedType(
608
+ def: LexiconArray | LexiconArrayItems,
609
+ ): Promise<string> {
610
+ switch (def.type) {
611
+ case 'unknown':
612
+ return this.compileUnknownType(def)
613
+ case 'boolean':
614
+ return this.compileBooleanType(def)
615
+ case 'integer':
616
+ return this.compileIntegerType(def)
617
+ case 'string':
618
+ return this.compileStringType(def)
619
+ case 'bytes':
620
+ return this.compileBytesType(def)
621
+ case 'blob':
622
+ return this.compileBlobType(def)
623
+ case 'cid-link':
624
+ return this.compileCidLinkType(def)
625
+ case 'ref':
626
+ return this.compileRefType(def)
627
+ case 'union':
628
+ return this.compileRefUnionType(def)
629
+ case 'array':
630
+ return this.compileArrayType(def)
631
+ default:
632
+ // @ts-expect-error
633
+ throw new Error(`Unsupported def type: ${def.type}`)
634
+ }
635
+ }
636
+
637
+ private async compileArraySchema(def: LexiconArray): Promise<string> {
638
+ const itemSchema = await this.compileContainedSchema(def.items)
639
+ const options = stringifyOptions(def, [
640
+ 'minLength',
641
+ 'maxLength',
642
+ ] satisfies (keyof l.ArraySchemaOptions)[])
643
+ return this.pure(`l.array(${itemSchema}, ${options})`)
644
+ }
645
+
646
+ private async compileArrayType(def: LexiconArray): Promise<string> {
647
+ return `(${await this.compileContainedType(def.items)})[]`
648
+ }
649
+
650
+ private async compileUnknownSchema(_def: LexiconUnknown): Promise<string> {
651
+ return this.pure(`l.unknownObject()`)
652
+ }
653
+
654
+ private async compileUnknownType(_def: LexiconUnknown): Promise<string> {
655
+ return `l.UnknownObject`
656
+ }
657
+
658
+ private withDefault(schema: string, defaultValue: unknown) {
659
+ if (defaultValue === undefined) return schema
660
+
661
+ return this.pure(
662
+ `l.withDefault(${schema}, ${JSON.stringify(defaultValue)})`,
663
+ )
664
+ }
665
+
666
+ private async compileBooleanSchema(def: LexiconBoolean): Promise<string> {
667
+ const schema = l.boolean()
668
+
669
+ if (def.default !== undefined) {
670
+ schema.check(def.default)
671
+ }
672
+
673
+ if (hasConst(def)) return this.compileConstSchema(def)
674
+
675
+ return this.withDefault(this.pure(`l.boolean()`), def.default)
676
+ }
677
+
678
+ private async compileBooleanType(def: LexiconBoolean): Promise<string> {
679
+ if (hasConst(def)) return this.compileConstType(def)
680
+ return 'boolean'
681
+ }
682
+
683
+ private async compileIntegerSchema(def: LexiconInteger): Promise<string> {
684
+ const schema = l.integer(def)
685
+
686
+ if (hasConst(def)) {
687
+ schema.check(def.const)
688
+ }
689
+
690
+ if (hasEnum(def)) {
691
+ for (const val of def.enum) schema.check(val)
692
+ }
693
+
694
+ if (def.default !== undefined) {
695
+ schema.check(def.default)
696
+ }
697
+
698
+ if (hasConst(def)) return this.compileConstSchema(def)
699
+ if (hasEnum(def)) return this.compileEnumSchema(def)
700
+
701
+ const options = stringifyOptions(def, [
702
+ 'maximum',
703
+ 'minimum',
704
+ ] satisfies (keyof l.IntegerSchemaOptions)[])
705
+
706
+ return this.withDefault(this.pure(`l.integer(${options})`), def.default)
707
+ }
708
+
709
+ private async compileIntegerType(def: LexiconInteger): Promise<string> {
710
+ if (hasConst(def)) return this.compileConstType(def)
711
+ if (hasEnum(def)) return this.compileEnumType(def)
712
+
713
+ return 'number'
714
+ }
715
+
716
+ private async compileStringSchema(def: LexiconString): Promise<string> {
717
+ const schema = l.string(def)
718
+
719
+ if (hasConst(def)) {
720
+ schema.check(def.const)
721
+ }
722
+
723
+ if (hasEnum(def)) {
724
+ for (const val of def.enum) schema.check(val)
725
+ }
726
+
727
+ if (def.default !== undefined) {
728
+ schema.check(def.default)
729
+ }
730
+
731
+ if (hasConst(def)) return this.compileConstSchema(def)
732
+ if (hasEnum(def)) return this.compileEnumSchema(def)
733
+
734
+ const options = stringifyOptions(def, [
735
+ 'format',
736
+ 'maxGraphemes',
737
+ 'minGraphemes',
738
+ 'maxLength',
739
+ 'minLength',
740
+ ] satisfies (keyof l.StringSchemaOptions)[])
741
+
742
+ return this.withDefault(this.pure(`l.string(${options})`), def.default)
743
+ }
744
+
745
+ private async compileStringType(def: LexiconString): Promise<string> {
746
+ if (hasConst(def)) return this.compileConstType(def)
747
+ if (hasEnum(def)) return this.compileEnumType(def)
748
+
749
+ switch (def.format) {
750
+ case undefined:
751
+ break
752
+ case 'datetime':
753
+ return 'l.DatetimeString'
754
+ case 'uri':
755
+ return 'l.UriString'
756
+ case 'at-uri':
757
+ return 'l.AtUriString'
758
+ case 'did':
759
+ return 'l.DidString'
760
+ case 'handle':
761
+ return 'l.HandleString'
762
+ case 'at-identifier':
763
+ return 'l.AtIdentifierString'
764
+ case 'nsid':
765
+ return 'l.NsidString'
766
+ case 'tid':
767
+ return 'l.TidString'
768
+ case 'cid':
769
+ return 'l.CidString'
770
+ case 'language':
771
+ return 'l.LanguageString'
772
+ case 'record-key':
773
+ return 'l.RecordKeyString'
774
+ default:
775
+ throw new Error(`Unknown string format: ${def.format}`)
776
+ }
777
+
778
+ if (def.knownValues?.length) {
779
+ return (
780
+ def.knownValues.map((v) => JSON.stringify(v)).join(' | ') +
781
+ ' | l.UnknownString'
782
+ )
783
+ }
784
+
785
+ return 'string'
786
+ }
787
+
788
+ private async compileBytesSchema(def: LexiconBytes): Promise<string> {
789
+ const options = stringifyOptions(def, [
790
+ 'minLength',
791
+ 'maxLength',
792
+ ] satisfies (keyof l.BytesSchemaOptions)[])
793
+ return this.pure(`l.bytes(${options})`)
794
+ }
795
+
796
+ private async compileBytesType(_def: LexiconBytes): Promise<string> {
797
+ return 'Uint8Array'
798
+ }
799
+
800
+ private async compileBlobSchema(def: LexiconBlob): Promise<string> {
801
+ const opts = { ...def, allowLegacy: this.options.allowLegacyBlobs === true }
802
+ const options = stringifyOptions(opts, [
803
+ 'maxSize',
804
+ 'accept',
805
+ 'allowLegacy',
806
+ ] satisfies (keyof l.BlobSchemaOptions)[])
807
+ return this.pure(`l.blob(${options})`)
808
+ }
809
+
810
+ private async compileBlobType(_def: LexiconBlob): Promise<string> {
811
+ return this.options.allowLegacyBlobs
812
+ ? 'l.BlobRef | l.LegacyBlobRef'
813
+ : 'l.BlobRef'
814
+ }
815
+
816
+ private async compileCidLinkSchema(_def: LexiconCid): Promise<string> {
817
+ return this.pure(`l.cid()`)
818
+ }
819
+
820
+ private async compileCidLinkType(_def: LexiconCid): Promise<string> {
821
+ return 'l.Cid'
822
+ }
823
+
824
+ private async compileRefSchema(def: LexiconRef): Promise<string> {
825
+ const { varName, typeName } = await this.refResolver.resolve(def.ref)
826
+ // @NOTE "as any" is needed in schemas with circular refs as TypeScript
827
+ // cannot infer the type of a value that depends on its initializer type
828
+ return this.pure(`l.ref<${typeName}>((() => ${varName}) as any)`)
829
+ }
830
+
831
+ private async compileRefType(def: LexiconRef): Promise<string> {
832
+ const ref = await this.refResolver.resolve(def.ref)
833
+ return ref.typeName
834
+ }
835
+
836
+ private async compileRefUnionSchema(def: LexiconRefUnion): Promise<string> {
837
+ if (def.refs.length === 0 && def.closed) {
838
+ return this.pure(`l.never()`)
839
+ }
840
+
841
+ const refs = await Promise.all(
842
+ def.refs.map(async (ref: string) => {
843
+ const { varName, typeName } = await this.refResolver.resolve(ref)
844
+ // @NOTE "as any" is needed in schemas with circular refs as TypeScript
845
+ // cannot infer the type of a value that depends on its initializer type
846
+ return this.pure(`l.typedRef<${typeName}>((() => ${varName}) as any)`)
847
+ }),
848
+ )
849
+
850
+ return this.pure(
851
+ `l.typedUnion([${refs.join(',')}], ${def.closed ?? false})`,
852
+ )
853
+ }
854
+
855
+ private async compileRefUnionType(def: LexiconRefUnion): Promise<string> {
856
+ const types = await Promise.all(
857
+ def.refs.map(async (ref) => {
858
+ const { typeName } = await this.refResolver.resolve(ref)
859
+ return `l.$Typed<${typeName}>`
860
+ }),
861
+ )
862
+ if (!def.closed) types.push('l.Unknown$TypedObject')
863
+ return types.join(' | ') || 'never'
864
+ }
865
+
866
+ private async compileConstSchema<
867
+ T extends null | number | string | boolean,
868
+ >(def: { const: T; enum?: readonly T[]; default?: T }): Promise<string> {
869
+ if (hasEnum(def) && !def.enum.includes(def.const)) {
870
+ return this.pure(`l.never()`)
871
+ }
872
+
873
+ const result = this.pure(`l.literal(${JSON.stringify(def.const)})`)
874
+
875
+ return this.withDefault(result, def.default)
876
+ }
877
+
878
+ private async compileConstType<
879
+ T extends null | number | string | boolean,
880
+ >(def: { const: T; enum?: readonly T[] }): Promise<string> {
881
+ if (hasEnum(def) && !def.enum.includes(def.const)) {
882
+ return 'never'
883
+ }
884
+ return JSON.stringify(def.const)
885
+ }
886
+
887
+ private async compileEnumSchema<T extends null | number | string>(def: {
888
+ enum: readonly T[]
889
+ default?: T
890
+ }): Promise<string> {
891
+ if (def.enum.length === 0) {
892
+ return this.pure(`l.never()`)
893
+ }
894
+
895
+ const result =
896
+ def.enum.length === 1
897
+ ? this.pure(`l.literal(${JSON.stringify(def.enum[0])})`)
898
+ : this.pure(`l.enum(${JSON.stringify(def.enum)})`)
899
+
900
+ return this.withDefault(result, def.default)
901
+ }
902
+
903
+ private async compileEnumType<T extends null | number | string>(def: {
904
+ enum: readonly T[]
905
+ }): Promise<string> {
906
+ return def.enum.map((v) => JSON.stringify(v)).join(' | ') || 'never'
907
+ }
908
+ }
909
+
910
+ type ParsedDescription = OptionalKind<JSDocStructure> & {
911
+ description?: string
912
+ tags?: { tagName: string; text?: string }[]
913
+ }
914
+
915
+ function parseDescription(description: string): ParsedDescription {
916
+ if (/deprecated/i.test(description)) {
917
+ const deprecationMatch = description.match(
918
+ /(\s*deprecated\s*(?:--?|:)?\s*([^-]*)(?:-+)?)/i,
919
+ )
920
+ if (deprecationMatch) {
921
+ const { 1: match, 2: deprecationNotice } = deprecationMatch
922
+ return {
923
+ description: description.replace(match, '').trim() || undefined,
924
+ tags: [{ tagName: 'deprecated', text: deprecationNotice?.trim() }],
925
+ }
926
+ } else {
927
+ return {
928
+ description: description.trim() || undefined,
929
+ tags: [{ tagName: 'deprecated' }],
930
+ }
931
+ }
932
+ }
933
+
934
+ return {
935
+ description: description.trim() || undefined,
936
+ }
937
+ }
938
+
939
+ function compileLeadingTrivia(description?: string) {
940
+ if (!description) return undefined
941
+ const parsed = parseDescription(description)
942
+ if (!parsed.description && !parsed.tags?.length) return undefined
943
+ const tags = parsed.tags
944
+ ?.map(({ tagName, text }) => (text ? `@${tagName} ${text}` : `@${tagName}`))
945
+ ?.join('\n')
946
+ const text = `\n${[parsed.description, tags].filter(Boolean).join('\n\n')}`
947
+ return `\n\n/**${text.replaceAll('\n', '\n * ')}\n */\n`
948
+ }
949
+
950
+ function compileDocs(description?: string) {
951
+ if (!description) return undefined
952
+ return [parseDescription(description)]
953
+ }
954
+
955
+ function stringifyOptions<O extends Record<string, unknown>>(
956
+ obj: O,
957
+ include?: (keyof O)[],
958
+ exclude?: (keyof O)[],
959
+ ) {
960
+ const filtered = Object.entries(obj).filter(
961
+ ([k]) => (!include || include.includes(k)) && !exclude?.includes(k),
962
+ )
963
+ return filtered.length ? JSON.stringify(Object.fromEntries(filtered)) : ''
964
+ }
965
+
966
+ function hasConst<T extends { const?: unknown }>(
967
+ def: T,
968
+ ): def is T & { const: NonNullable<T['const']> } {
969
+ return def.const != null
970
+ }
971
+
972
+ function hasEnum<T extends { enum?: readonly unknown[] }>(
973
+ def: T,
974
+ ): def is T & { enum: unknown[] } {
975
+ return def.enum != null
976
+ }
977
+
978
+ function markPure<T extends string>(v: T): `/*#__PURE__*/ ${T}` {
979
+ return `/*#__PURE__*/ ${v}`
980
+ }