@atproto/lex-cli 0.10.2 → 0.10.3

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.
@@ -1,720 +0,0 @@
1
- import { relative as getRelativePath } from 'node:path/posix'
2
- import { JSDoc, SourceFile, VariableDeclarationKind } from 'ts-morph'
3
- import {
4
- type LexArray,
5
- type LexBlob,
6
- type LexBytes,
7
- type LexCidLink,
8
- type LexIpldType,
9
- type LexObject,
10
- type LexPrimitive,
11
- type LexToken,
12
- Lexicons,
13
- } from '@atproto/lexicon'
14
- import { toCamelCase, toScreamingSnakeCase, toTitleCase } from './util.js'
15
-
16
- interface Commentable {
17
- addJsDoc: ({ description }: { description: string }) => JSDoc
18
- }
19
- export function genComment<T extends Commentable>(
20
- commentable: T,
21
- def: { description?: string },
22
- ): T {
23
- if (def.description) {
24
- commentable.addJsDoc({ description: def.description })
25
- }
26
- return commentable
27
- }
28
-
29
- export function genCommonImports(file: SourceFile, baseNsid: string) {
30
- //= import {ValidationResult, BlobRef} from '@atproto/lexicon'
31
- file
32
- .addImportDeclaration({
33
- moduleSpecifier: '@atproto/lexicon',
34
- })
35
- .addNamedImports([
36
- { name: 'ValidationResult', isTypeOnly: true },
37
- { name: 'BlobRef' },
38
- ])
39
-
40
- //= import {CID} from 'multiformats/cid'
41
- file
42
- .addImportDeclaration({
43
- moduleSpecifier: 'multiformats/cid',
44
- })
45
- .addNamedImports([{ name: 'CID' }])
46
-
47
- //= import { validate as _validate } from '../../lexicons.ts'
48
- file
49
- .addImportDeclaration({
50
- moduleSpecifier: `${baseNsid
51
- .split('.')
52
- .map((_str) => '..')
53
- .join('/')}/lexicons.js`,
54
- })
55
- .addNamedImports([{ name: 'validate', alias: '_validate' }])
56
-
57
- //= import { type $Typed, is$typed as _is$typed, type OmitKey } from '../[...]/util.ts'
58
- file
59
- .addImportDeclaration({
60
- moduleSpecifier: `${baseNsid
61
- .split('.')
62
- .map((_str) => '..')
63
- .join('/')}/util.js`,
64
- })
65
- .addNamedImports([
66
- { name: '$Typed', isTypeOnly: true },
67
- { name: 'is$typed', alias: '_is$typed' },
68
- { name: 'OmitKey', isTypeOnly: true },
69
- ])
70
-
71
- // TypeScript adds protection against circular imports, which hurts bundle
72
- // size. Since we know that lexicon.ts and util.ts do not depend on the file
73
- // being generated, we can safely bypass this protection. Note that we are not
74
- // using `import * as util from '../../util.js'` because typescript will emit
75
- // is own helpers for the import, which we want to avoid.
76
- file.addVariableStatement({
77
- isExported: false,
78
- declarationKind: VariableDeclarationKind.Const,
79
- declarations: [
80
- { name: 'is$typed', initializer: '_is$typed' },
81
- { name: 'validate', initializer: '_validate' },
82
- ],
83
- })
84
-
85
- //= const id = "{baseNsid}"
86
- file.addVariableStatement({
87
- isExported: false, // Do not export to allow tree-shaking
88
- declarationKind: VariableDeclarationKind.Const,
89
- declarations: [{ name: 'id', initializer: JSON.stringify(baseNsid) }],
90
- })
91
- }
92
-
93
- export function genImports(
94
- file: SourceFile,
95
- imports: Set<string>,
96
- baseNsid: string,
97
- ) {
98
- const startPath = '/' + baseNsid.split('.').slice(0, -1).join('/')
99
-
100
- for (const nsid of imports) {
101
- const targetPath = '/' + nsid.split('.').join('/') + '.js'
102
- let resolvedPath = getRelativePath(startPath, targetPath)
103
- if (!resolvedPath.startsWith('.')) {
104
- resolvedPath = `./${resolvedPath}`
105
- }
106
- file.addImportDeclaration({
107
- isTypeOnly: true,
108
- moduleSpecifier: resolvedPath,
109
- namespaceImport: toTitleCase(nsid),
110
- })
111
- }
112
- }
113
-
114
- export function genUserType(
115
- file: SourceFile,
116
- imports: Set<string>,
117
- lexicons: Lexicons,
118
- lexUri: string,
119
- ) {
120
- const def = lexicons.getDefOrThrow(lexUri)
121
- switch (def.type) {
122
- case 'array':
123
- genArray(file, imports, lexUri, def)
124
- break
125
- case 'token':
126
- genToken(file, lexUri, def)
127
- break
128
- case 'object': {
129
- const ifaceName: string = toTitleCase(getHash(lexUri))
130
- genObject(file, imports, lexUri, def, ifaceName, {
131
- typeProperty: true,
132
- })
133
- genObjHelpers(file, lexUri, ifaceName, {
134
- requireTypeProperty: false,
135
- })
136
- break
137
- }
138
-
139
- case 'blob':
140
- case 'bytes':
141
- case 'cid-link':
142
- case 'boolean':
143
- case 'integer':
144
- case 'string':
145
- case 'unknown':
146
- genPrimitiveOrBlob(file, lexUri, def)
147
- break
148
-
149
- default:
150
- throw new Error(
151
- `genLexUserType() called with wrong definition type (${def.type}) in ${lexUri}`,
152
- )
153
- }
154
- }
155
-
156
- function genObject(
157
- file: SourceFile,
158
- imports: Set<string>,
159
- lexUri: string,
160
- def: LexObject,
161
- ifaceName: string,
162
- {
163
- defaultsArePresent = true,
164
- allowUnknownProperties = false,
165
- typeProperty = false,
166
- }: {
167
- defaultsArePresent?: boolean
168
- allowUnknownProperties?: boolean
169
- typeProperty?: boolean | 'required'
170
- } = {},
171
- ) {
172
- const iface = file.addInterface({
173
- name: ifaceName,
174
- isExported: true,
175
- })
176
- genComment(iface, def)
177
-
178
- if (typeProperty) {
179
- const hash = getHash(lexUri)
180
- const baseNsid = stripScheme(stripHash(lexUri))
181
-
182
- //= $type?: <uri>
183
- iface.addProperty({
184
- name: typeProperty === 'required' ? `$type` : `$type?`,
185
- type:
186
- // Not using $Type here because it is less readable than a plain string
187
- // `$Type<${JSON.stringify(baseNsid)}, ${JSON.stringify(hash)}>`
188
- hash === 'main'
189
- ? JSON.stringify(`${baseNsid}`)
190
- : JSON.stringify(`${baseNsid}#${hash}`),
191
- })
192
- }
193
-
194
- const nullableProps = new Set(def.nullable)
195
- if (def.properties) {
196
- for (const propKey in def.properties) {
197
- const propDef = def.properties[propKey]
198
- const propNullable = nullableProps.has(propKey)
199
- const req =
200
- def.required?.includes(propKey) ||
201
- (defaultsArePresent &&
202
- 'default' in propDef &&
203
- propDef.default !== undefined)
204
- if (propDef.type === 'ref' || propDef.type === 'union') {
205
- //= propName: External|External
206
- const types =
207
- propDef.type === 'union'
208
- ? propDef.refs.map((ref) => refToUnionType(ref, lexUri, imports))
209
- : [refToType(propDef.ref, stripScheme(stripHash(lexUri)), imports)]
210
- if (propDef.type === 'union' && !propDef.closed) {
211
- types.push('{ $type: string }')
212
- }
213
- iface.addProperty({
214
- name: `${propKey}${req ? '' : '?'}`,
215
- type: makeType(types, { nullable: propNullable }),
216
- })
217
- continue
218
- } else {
219
- if (propDef.type === 'array') {
220
- //= propName: type[]
221
- let propAst
222
- if (propDef.items.type === 'ref') {
223
- propAst = iface.addProperty({
224
- name: `${propKey}${req ? '' : '?'}`,
225
- type: makeType(
226
- refToType(
227
- propDef.items.ref,
228
- stripScheme(stripHash(lexUri)),
229
- imports,
230
- ),
231
- {
232
- nullable: propNullable,
233
- array: true,
234
- },
235
- ),
236
- })
237
- } else if (propDef.items.type === 'union') {
238
- const types = propDef.items.refs.map((ref) =>
239
- refToUnionType(ref, lexUri, imports),
240
- )
241
- if (!propDef.items.closed) {
242
- types.push('{ $type: string }')
243
- }
244
- propAst = iface.addProperty({
245
- name: `${propKey}${req ? '' : '?'}`,
246
- type: makeType(types, {
247
- nullable: propNullable,
248
- array: true,
249
- }),
250
- })
251
- } else {
252
- propAst = iface.addProperty({
253
- name: `${propKey}${req ? '' : '?'}`,
254
- type: makeType(primitiveOrBlobToType(propDef.items), {
255
- nullable: propNullable,
256
- array: true,
257
- }),
258
- })
259
- }
260
- genComment(propAst, propDef)
261
- } else {
262
- //= propName: type
263
- genComment(
264
- iface.addProperty({
265
- name: `${propKey}${req ? '' : '?'}`,
266
- type: makeType(primitiveOrBlobToType(propDef), {
267
- nullable: propNullable,
268
- }),
269
- }),
270
- propDef,
271
- )
272
- }
273
- }
274
- }
275
-
276
- if (allowUnknownProperties) {
277
- //= [k: string]: unknown
278
- iface.addIndexSignature({
279
- keyName: 'k',
280
- keyType: 'string',
281
- returnType: 'unknown',
282
- })
283
- }
284
- }
285
- }
286
-
287
- export function genToken(file: SourceFile, lexUri: string, def: LexToken) {
288
- //= /** <comment> */
289
- //= export const <TOKEN> = `${id}#<token>`
290
- genComment(
291
- file.addVariableStatement({
292
- isExported: true,
293
- declarationKind: VariableDeclarationKind.Const,
294
- declarations: [
295
- {
296
- name: toScreamingSnakeCase(getHash(lexUri)),
297
- initializer: `\`\${id}#${getHash(lexUri)}\``,
298
- },
299
- ],
300
- }),
301
- def,
302
- )
303
- }
304
-
305
- export function genArray(
306
- file: SourceFile,
307
- imports: Set<string>,
308
- lexUri: string,
309
- def: LexArray,
310
- ) {
311
- if (def.items.type === 'ref') {
312
- file.addTypeAlias({
313
- name: toTitleCase(getHash(lexUri)),
314
- type: `${refToType(
315
- def.items.ref,
316
- stripScheme(stripHash(lexUri)),
317
- imports,
318
- )}[]`,
319
- isExported: true,
320
- })
321
- } else if (def.items.type === 'union') {
322
- const types = def.items.refs.map((ref) =>
323
- refToUnionType(ref, lexUri, imports),
324
- )
325
- if (!def.items.closed) {
326
- types.push('{ $type: string }')
327
- }
328
- file.addTypeAlias({
329
- name: toTitleCase(getHash(lexUri)),
330
- type: `(${types.join('|')})[]`,
331
- isExported: true,
332
- })
333
- } else {
334
- genComment(
335
- file.addTypeAlias({
336
- name: toTitleCase(getHash(lexUri)),
337
- type: `${primitiveOrBlobToType(def.items)}[]`,
338
- isExported: true,
339
- }),
340
- def,
341
- )
342
- }
343
- }
344
-
345
- export function genPrimitiveOrBlob(
346
- file: SourceFile,
347
- lexUri: string,
348
- def: LexPrimitive | LexBlob | LexIpldType,
349
- ) {
350
- genComment(
351
- file.addTypeAlias({
352
- name: toTitleCase(getHash(lexUri)),
353
- type: primitiveOrBlobToType(def),
354
- isExported: true,
355
- }),
356
- def,
357
- )
358
- }
359
-
360
- export function genXrpcParams(
361
- file: SourceFile,
362
- lexicons: Lexicons,
363
- lexUri: string,
364
- defaultsArePresent = true,
365
- ) {
366
- const def = lexicons.getDefOrThrow(lexUri, [
367
- 'query',
368
- 'subscription',
369
- 'procedure',
370
- ])
371
-
372
- // @NOTE We need to use a `type` here instead of an `interface` because we
373
- // need the generated type to be used as generic type parameter like this:
374
- //
375
- // type QueryParams = {} // Generated by this function
376
- //
377
- // type MyUtil<P extends xrpcServer.QueryParam> = (...)
378
- // type NsType = MyUtil<NS.QueryParams> // ERROR if `NS.QueryParams` is an `interface`
379
- //
380
- // Second line will fail if `NS.QueryParams` is an `interface` that does
381
- // not explicitly extend `xrpcServer.QueryParam`, or have a string index
382
- // signature that encompasses `xrpcServer.QueryParam`.
383
-
384
- //= export type QueryParams = {...}
385
- if (def.parameters) {
386
- genComment(
387
- file.addTypeAlias({
388
- name: 'QueryParams',
389
- isExported: true,
390
- type: `{
391
- ${Object.entries(def.parameters.properties)
392
- .map(([paramKey, paramDef]) => {
393
- const req =
394
- def.parameters!.required?.includes(paramKey) ||
395
- (defaultsArePresent &&
396
- 'default' in paramDef &&
397
- paramDef.default !== undefined)
398
- const jsDoc = paramDef.description
399
- ? `/** ${paramDef.description} */\n`
400
- : ''
401
- return `${jsDoc}${paramKey}${req ? '' : '?'}: ${
402
- paramDef.type === 'array'
403
- ? primitiveToType(paramDef.items) + '[]'
404
- : primitiveToType(paramDef)
405
- }`
406
- })
407
- .join('\n')}
408
- }`,
409
- }),
410
- def.parameters,
411
- )
412
- } else {
413
- file.addTypeAlias({
414
- name: 'QueryParams',
415
- isExported: true,
416
- type: '{}',
417
- })
418
- }
419
- }
420
-
421
- export function genXrpcInput(
422
- file: SourceFile,
423
- imports: Set<string>,
424
- lexicons: Lexicons,
425
- lexUri: string,
426
- defaultsArePresent = true,
427
- ) {
428
- const def = lexicons.getDefOrThrow(lexUri, ['query', 'procedure'])
429
-
430
- if (def.type === 'procedure' && def.input?.schema) {
431
- if (def.input.schema.type === 'ref' || def.input.schema.type === 'union') {
432
- //= export type InputSchema = ...
433
-
434
- const types =
435
- def.input.schema.type === 'union'
436
- ? def.input.schema.refs.map((ref) =>
437
- refToUnionType(ref, lexUri, imports),
438
- )
439
- : [
440
- refToType(
441
- def.input.schema.ref,
442
- stripScheme(stripHash(lexUri)),
443
- imports,
444
- ),
445
- ]
446
-
447
- if (def.input.schema.type === 'union' && !def.input.schema.closed) {
448
- types.push('{ $type: string }')
449
- }
450
- file.addTypeAlias({
451
- name: 'InputSchema',
452
- type: types.join('|'),
453
- isExported: true,
454
- })
455
- } else {
456
- //= export interface InputSchema {...}
457
- genObject(file, imports, lexUri, def.input.schema, `InputSchema`, {
458
- defaultsArePresent,
459
- })
460
- }
461
- } else if (def.type === 'procedure' && def.input?.encoding) {
462
- //= export type InputSchema = string | Uint8Array | Blob
463
- file.addTypeAlias({
464
- isExported: true,
465
- name: 'InputSchema',
466
- type: 'string | Uint8Array | Blob',
467
- })
468
- } else {
469
- //= export type InputSchema = undefined
470
- file.addTypeAlias({
471
- isExported: true,
472
- name: 'InputSchema',
473
- type: 'undefined',
474
- })
475
- }
476
- }
477
-
478
- export function genXrpcOutput(
479
- file: SourceFile,
480
- imports: Set<string>,
481
- lexicons: Lexicons,
482
- lexUri: string,
483
- defaultsArePresent = true,
484
- ) {
485
- const def = lexicons.getDefOrThrow(lexUri, [
486
- 'query',
487
- 'subscription',
488
- 'procedure',
489
- ])
490
-
491
- const schema =
492
- def.type === 'subscription' ? def.message?.schema : def.output?.schema
493
- if (schema) {
494
- if (schema.type === 'ref' || schema.type === 'union') {
495
- //= export type OutputSchema = ...
496
- const types =
497
- schema.type === 'union'
498
- ? schema.refs.map((ref) => refToUnionType(ref, lexUri, imports))
499
- : [refToType(schema.ref, stripScheme(stripHash(lexUri)), imports)]
500
- if (schema.type === 'union' && !schema.closed) {
501
- types.push('{ $type: string }')
502
- }
503
- file.addTypeAlias({
504
- name: 'OutputSchema',
505
- type: types.join('|'),
506
- isExported: true,
507
- })
508
- } else {
509
- //= export interface OutputSchema {...}
510
- genObject(file, imports, lexUri, schema, `OutputSchema`, {
511
- defaultsArePresent,
512
- })
513
- }
514
- }
515
- }
516
-
517
- export function genRecord(
518
- file: SourceFile,
519
- imports: Set<string>,
520
- lexicons: Lexicons,
521
- lexUri: string,
522
- ) {
523
- const hash = getHash(lexUri)
524
- const ifaceName: string = toTitleCase(hash)
525
- const def = lexicons.getDefOrThrow(lexUri, ['record'])
526
-
527
- //= export interface {X} {...}
528
- genObject(file, imports, lexUri, def.record, ifaceName, {
529
- defaultsArePresent: true,
530
- allowUnknownProperties: true,
531
- typeProperty: 'required',
532
- })
533
-
534
- //= export function is{X}(v: unknown): v is {X} {...}
535
- genObjHelpers(file, lexUri, ifaceName, {
536
- requireTypeProperty: true,
537
- })
538
-
539
- // For convenience, we re-export the type and the type guard under the generic
540
- // names "Record", "isRecord" and "validateRecord".
541
- // @NOTE This does not account for potential name clashes with a potential
542
- // "#record" def.
543
-
544
- //= export { {X} as Record, is{X} as isRecord }
545
- file.addExportDeclaration({
546
- namedExports: [
547
- {
548
- isTypeOnly: true,
549
- name: ifaceName,
550
- alias: 'Record',
551
- },
552
- {
553
- name: `is${ifaceName}`,
554
- alias: 'isRecord',
555
- },
556
- {
557
- name: `validate${ifaceName}`,
558
- alias: 'validateRecord',
559
- },
560
- ],
561
- })
562
- }
563
-
564
- function genObjHelpers(
565
- file: SourceFile,
566
- lexUri: string,
567
- ifaceName: string,
568
- {
569
- requireTypeProperty,
570
- }: {
571
- requireTypeProperty: boolean
572
- },
573
- ) {
574
- const hash = getHash(lexUri)
575
-
576
- const hashVar = `hash${ifaceName}`
577
-
578
- file.addVariableStatement({
579
- isExported: false,
580
- declarationKind: VariableDeclarationKind.Const,
581
- declarations: [{ name: hashVar, initializer: JSON.stringify(hash) }],
582
- })
583
-
584
- const isX = toCamelCase(`is-${ifaceName}`)
585
-
586
- //= export function is{X}<V>(v: V) {...}
587
- file
588
- .addFunction({
589
- name: isX,
590
- typeParameters: [{ name: `V` }],
591
- parameters: [{ name: `v`, type: `V` }],
592
- isExported: true,
593
- })
594
- .setBodyText(`return is$typed(v, id, ${hashVar})`)
595
-
596
- const validateX = toCamelCase(`validate-${ifaceName}`)
597
-
598
- //= export function validate{X}(v: unknown) {...}
599
- file
600
- .addFunction({
601
- name: validateX,
602
- typeParameters: [{ name: `V` }],
603
- parameters: [{ name: `v`, type: `V` }],
604
- isExported: true,
605
- })
606
- .setBodyText(
607
- `return validate<${ifaceName} & V>(v, id, ${hashVar}${requireTypeProperty ? ', true' : ''})`,
608
- )
609
- }
610
-
611
- export function stripScheme(uri: string): string {
612
- if (uri.startsWith('lex:')) return uri.slice(4)
613
- return uri
614
- }
615
-
616
- export function stripHash(uri: string): string {
617
- return uri.split('#')[0] || ''
618
- }
619
-
620
- export function getHash(uri: string): string {
621
- return uri.split('#').pop() || ''
622
- }
623
-
624
- export function ipldToType(def: LexCidLink | LexBytes) {
625
- if (def.type === 'bytes') {
626
- return 'Uint8Array'
627
- }
628
- return 'CID'
629
- }
630
-
631
- function refToUnionType(
632
- ref: string,
633
- lexUri: string,
634
- imports: Set<string>,
635
- ): string {
636
- const baseNsid = stripScheme(stripHash(lexUri))
637
- return `$Typed<${refToType(ref, baseNsid, imports)}>`
638
- }
639
-
640
- function refToType(
641
- ref: string,
642
- baseNsid: string,
643
- imports: Set<string>,
644
- ): string {
645
- // TODO: import external types!
646
- let [refBase, refHash] = ref.split('#')
647
- refBase = stripScheme(refBase)
648
- if (!refHash) refHash = 'main'
649
-
650
- // internal
651
- if (!refBase || baseNsid === refBase) {
652
- return toTitleCase(refHash)
653
- }
654
-
655
- // external
656
- imports.add(refBase)
657
- return `${toTitleCase(refBase)}.${toTitleCase(refHash)}`
658
- }
659
-
660
- export function primitiveOrBlobToType(
661
- def: LexBlob | LexPrimitive | LexIpldType,
662
- ): string {
663
- switch (def.type) {
664
- case 'blob':
665
- return 'BlobRef'
666
- case 'bytes':
667
- return 'Uint8Array'
668
- case 'cid-link':
669
- return 'CID'
670
- default:
671
- return primitiveToType(def)
672
- }
673
- }
674
-
675
- export function primitiveToType(def: LexPrimitive): string {
676
- switch (def.type) {
677
- case 'string':
678
- if (def.knownValues?.length) {
679
- return `${def.knownValues
680
- .map((v) => JSON.stringify(v))
681
- .join(' | ')} | (string & {})`
682
- } else if (def.enum) {
683
- return def.enum.map((v) => JSON.stringify(v)).join(' | ')
684
- } else if (def.const) {
685
- return JSON.stringify(def.const)
686
- }
687
- return 'string'
688
- case 'integer':
689
- if (def.enum) {
690
- return def.enum.map((v) => JSON.stringify(v)).join(' | ')
691
- } else if (def.const) {
692
- return JSON.stringify(def.const)
693
- }
694
- return 'number'
695
- case 'boolean':
696
- if (def.const) {
697
- return JSON.stringify(def.const)
698
- }
699
- return 'boolean'
700
- case 'unknown':
701
- // @TODO Should we use "object" here ?
702
- // the "Record" identifier from typescript get overwritten by the Record
703
- // interface created by lex-cli.
704
- return '{ [_ in string]: unknown }' // Record<string, unknown>
705
- default:
706
- throw new Error(`Unexpected primitive type: ${JSON.stringify(def)}`)
707
- }
708
- }
709
-
710
- function makeType(
711
- _types: string | string[],
712
- opts?: { array?: boolean; nullable?: boolean },
713
- ) {
714
- const types = ([] as string[]).concat(_types)
715
- if (opts?.nullable) types.push('null')
716
- const arr = opts?.array ? '[]' : ''
717
- if (types.length === 1) return `(${types[0]})${arr}`
718
- if (arr) return `(${types.join(' | ')})${arr}`
719
- return types.join(' | ')
720
- }