@atproto/lex-builder 0.1.4 → 0.1.5

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