@atproto/lex-cli 0.10.1 → 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,566 +0,0 @@
1
- import {
2
- IndentationText,
3
- Project,
4
- SourceFile,
5
- VariableDeclarationKind,
6
- } from 'ts-morph'
7
- import { type LexRecord, type LexiconDoc, Lexicons } from '@atproto/lexicon'
8
- import { NSID } from '@atproto/syntax'
9
- import { type GeneratedAPI } from '../types.js'
10
- import { gen, lexiconsTs, utilTs } from './common.js'
11
- import {
12
- genCommonImports,
13
- genImports,
14
- genRecord,
15
- genUserType,
16
- genXrpcInput,
17
- genXrpcOutput,
18
- genXrpcParams,
19
- } from './lex-gen.js'
20
- import {
21
- type DefTreeNode,
22
- lexiconsToDefTree,
23
- schemasToNsidTokens,
24
- toCamelCase,
25
- toScreamingSnakeCase,
26
- toTitleCase,
27
- } from './util.js'
28
-
29
- const ATP_METHODS = {
30
- list: 'com.atproto.repo.listRecords',
31
- get: 'com.atproto.repo.getRecord',
32
- create: 'com.atproto.repo.createRecord',
33
- put: 'com.atproto.repo.putRecord',
34
- delete: 'com.atproto.repo.deleteRecord',
35
- }
36
-
37
- export async function genClientApi(
38
- lexiconDocs: LexiconDoc[],
39
- ): Promise<GeneratedAPI> {
40
- const project = new Project({
41
- useInMemoryFileSystem: true,
42
- manipulationSettings: { indentationText: IndentationText.TwoSpaces },
43
- })
44
- const api: GeneratedAPI = { files: [] }
45
- const lexicons = new Lexicons(lexiconDocs)
46
- const nsidTree = lexiconsToDefTree(lexiconDocs)
47
- const nsidTokens = schemasToNsidTokens(lexiconDocs)
48
- for (const lexiconDoc of lexiconDocs) {
49
- api.files.push(await lexiconTs(project, lexicons, lexiconDoc))
50
- }
51
- api.files.push(await utilTs(project))
52
- api.files.push(await lexiconsTs(project, lexiconDocs))
53
- api.files.push(await indexTs(project, lexiconDocs, nsidTree, nsidTokens))
54
- return api
55
- }
56
-
57
- const indexTs = (
58
- project: Project,
59
- lexiconDocs: LexiconDoc[],
60
- nsidTree: DefTreeNode[],
61
- nsidTokens: Record<string, string[]>,
62
- ) =>
63
- gen(project, '/index.ts', async (file) => {
64
- //= import { XrpcClient, type FetchHandler, type FetchHandlerOptions } from '@atproto/xrpc'
65
- const xrpcImport = file.addImportDeclaration({
66
- moduleSpecifier: '@atproto/xrpc',
67
- })
68
- xrpcImport.addNamedImports([
69
- { name: 'XrpcClient' },
70
- { name: 'FetchHandler', isTypeOnly: true },
71
- { name: 'FetchHandlerOptions', isTypeOnly: true },
72
- ])
73
- //= import {schemas} from './lexicons.js'
74
- file
75
- .addImportDeclaration({ moduleSpecifier: './lexicons.js' })
76
- .addNamedImports([{ name: 'schemas' }])
77
- //= import {CID} from 'multiformats/cid'
78
- file
79
- .addImportDeclaration({
80
- moduleSpecifier: 'multiformats/cid',
81
- })
82
- .addNamedImports([{ name: 'CID' }])
83
-
84
- //= import { type OmitKey, type Un$Typed } from './util.js'
85
- file
86
- .addImportDeclaration({ moduleSpecifier: `./util.js` })
87
- .addNamedImports([
88
- { name: 'OmitKey', isTypeOnly: true },
89
- { name: 'Un$Typed', isTypeOnly: true },
90
- ])
91
-
92
- // generate type imports and re-exports
93
- for (const lexicon of lexiconDocs) {
94
- const moduleSpecifier = `./types/${lexicon.id.split('.').join('/')}.js`
95
- file
96
- .addImportDeclaration({ moduleSpecifier })
97
- .setNamespaceImport(toTitleCase(lexicon.id))
98
- file
99
- .addExportDeclaration({ moduleSpecifier })
100
- .setNamespaceExport(toTitleCase(lexicon.id))
101
- }
102
-
103
- // generate token enums
104
- for (const nsidAuthority in nsidTokens) {
105
- // export const {THE_AUTHORITY} = {
106
- // {Name}: "{authority.the.name}"
107
- // }
108
- file.addVariableStatement({
109
- isExported: true,
110
- declarationKind: VariableDeclarationKind.Const,
111
- declarations: [
112
- {
113
- name: toScreamingSnakeCase(nsidAuthority),
114
- initializer: [
115
- '{',
116
- ...nsidTokens[nsidAuthority].map(
117
- (nsidName) =>
118
- `${toTitleCase(nsidName)}: "${nsidAuthority}.${nsidName}",`,
119
- ),
120
- '}',
121
- ].join('\n'),
122
- },
123
- ],
124
- })
125
- }
126
-
127
- //= export class AtpBaseClient {...}
128
- const clientCls = file.addClass({
129
- name: 'AtpBaseClient',
130
- isExported: true,
131
- extends: 'XrpcClient',
132
- })
133
-
134
- for (const ns of nsidTree) {
135
- //= ns: NS
136
- clientCls.addProperty({
137
- name: ns.propName,
138
- type: ns.className,
139
- })
140
- }
141
-
142
- //= constructor (options: FetchHandler | FetchHandlerOptions) {
143
- //= super(options, schemas)
144
- //= {namespace declarations}
145
- //= }
146
- clientCls.addConstructor({
147
- parameters: [
148
- { name: 'options', type: 'FetchHandler | FetchHandlerOptions' },
149
- ],
150
- statements: [
151
- 'super(options, schemas)',
152
- ...nsidTree.map(
153
- (ns) => `this.${ns.propName} = new ${ns.className}(this)`,
154
- ),
155
- ],
156
- })
157
-
158
- //= /** @deprecated use `this` instead */
159
- //= get xrpc(): XrpcClient {
160
- //= return this
161
- //= }
162
- clientCls
163
- .addGetAccessor({
164
- name: 'xrpc',
165
- returnType: 'XrpcClient',
166
- statements: ['return this'],
167
- })
168
- .addJsDoc('@deprecated use `this` instead')
169
-
170
- // generate classes for the schemas
171
- for (const ns of nsidTree) {
172
- genNamespaceCls(file, ns)
173
- }
174
- })
175
-
176
- function genNamespaceCls(file: SourceFile, ns: DefTreeNode) {
177
- //= export class {ns}NS {...}
178
- const cls = file.addClass({
179
- name: ns.className,
180
- isExported: true,
181
- })
182
- //= _client: XrpcClient
183
- cls.addProperty({
184
- name: '_client',
185
- type: 'XrpcClient',
186
- })
187
-
188
- for (const userType of ns.userTypes) {
189
- if (userType.def.type !== 'record') {
190
- continue
191
- }
192
- //= type: TypeRecord
193
- const name = NSID.parse(userType.nsid).name || ''
194
- cls.addProperty({
195
- name: toCamelCase(name),
196
- type: `${toTitleCase(userType.nsid)}Record`,
197
- })
198
- }
199
-
200
- for (const child of ns.children) {
201
- //= child: ChildNS
202
- cls.addProperty({
203
- name: child.propName,
204
- type: child.className,
205
- })
206
-
207
- // recurse
208
- genNamespaceCls(file, child)
209
- }
210
-
211
- //= constructor(public client: XrpcClient) {
212
- //= this._client = client
213
- //= {child namespace prop declarations}
214
- //= {record prop declarations}
215
- //= }
216
- cls.addConstructor({
217
- parameters: [
218
- {
219
- name: 'client',
220
- type: 'XrpcClient',
221
- },
222
- ],
223
- statements: [
224
- `this._client = client`,
225
- ...ns.children.map(
226
- (ns) => `this.${ns.propName} = new ${ns.className}(client)`,
227
- ),
228
- ...ns.userTypes
229
- .filter((ut) => ut.def.type === 'record')
230
- .map((ut) => {
231
- const name = NSID.parse(ut.nsid).name || ''
232
- return `this.${toCamelCase(name)} = new ${toTitleCase(
233
- ut.nsid,
234
- )}Record(client)`
235
- }),
236
- ],
237
- })
238
-
239
- // methods
240
- for (const userType of ns.userTypes) {
241
- if (userType.def.type !== 'query' && userType.def.type !== 'procedure') {
242
- continue
243
- }
244
- const isGetReq = userType.def.type === 'query'
245
- const moduleName = toTitleCase(userType.nsid)
246
- const name = toCamelCase(NSID.parse(userType.nsid).name || '')
247
- const method = cls.addMethod({
248
- name,
249
- returnType: `Promise<${moduleName}.Response>`,
250
- })
251
- if (isGetReq) {
252
- method.addParameter({
253
- name: 'params?',
254
- type: `${moduleName}.QueryParams`,
255
- })
256
- } else if (userType.def.type === 'procedure') {
257
- method.addParameter({
258
- name: 'data?',
259
- type: `${moduleName}.InputSchema`,
260
- })
261
- }
262
- method.addParameter({
263
- name: 'opts?',
264
- type: `${moduleName}.CallOptions`,
265
- })
266
- method.setBodyText(
267
- [
268
- `return this._client`,
269
- isGetReq
270
- ? `.call('${userType.nsid}', params, undefined, opts)`
271
- : `.call('${userType.nsid}', opts?.qp, data, opts)`,
272
- userType.def.errors?.length
273
- ? // Only add a catch block if there are custom errors
274
- ` .catch((e) => { throw ${moduleName}.toKnownErr(e) })`
275
- : '',
276
- ].join('\n'),
277
- )
278
- }
279
-
280
- // record api classes
281
- for (const userType of ns.userTypes) {
282
- if (userType.def.type !== 'record') {
283
- continue
284
- }
285
- genRecordCls(file, userType.nsid, userType.def)
286
- }
287
- }
288
-
289
- function genRecordCls(file: SourceFile, nsid: string, lexRecord: LexRecord) {
290
- //= export class {type}Record {...}
291
- const cls = file.addClass({
292
- name: `${toTitleCase(nsid)}Record`,
293
- isExported: true,
294
- })
295
- //= _client: XrpcClient
296
- cls.addProperty({
297
- name: '_client',
298
- type: 'XrpcClient',
299
- })
300
-
301
- //= constructor(client: XrpcClient) {
302
- //= this._client = client
303
- //= }
304
- const cons = cls.addConstructor()
305
- cons.addParameter({
306
- name: 'client',
307
- type: 'XrpcClient',
308
- })
309
- cons.setBodyText(`this._client = client`)
310
-
311
- // methods
312
- const typeModule = toTitleCase(nsid)
313
- {
314
- //= list()
315
- const method = cls.addMethod({
316
- isAsync: true,
317
- name: 'list',
318
- returnType: `Promise<{cursor?: string, records: ({uri: string, value: ${typeModule}.Record})[]}>`,
319
- })
320
- method.addParameter({
321
- name: 'params',
322
- type: `OmitKey<${toTitleCase(ATP_METHODS.list)}.QueryParams, "collection">`,
323
- })
324
- method.setBodyText(
325
- [
326
- `const res = await this._client.call('${ATP_METHODS.list}', { collection: '${nsid}', ...params })`,
327
- `return res.data`,
328
- ].join('\n'),
329
- )
330
- }
331
- {
332
- //= get()
333
- const method = cls.addMethod({
334
- isAsync: true,
335
- name: 'get',
336
- returnType: `Promise<{uri: string, cid: string, value: ${typeModule}.Record}>`,
337
- })
338
- method.addParameter({
339
- name: 'params',
340
- type: `OmitKey<${toTitleCase(ATP_METHODS.get)}.QueryParams, "collection">`,
341
- })
342
- method.setBodyText(
343
- [
344
- `const res = await this._client.call('${ATP_METHODS.get}', { collection: '${nsid}', ...params })`,
345
- `return res.data`,
346
- ].join('\n'),
347
- )
348
- }
349
- {
350
- //= create()
351
- const method = cls.addMethod({
352
- isAsync: true,
353
- name: 'create',
354
- returnType: 'Promise<{uri: string, cid: string}>',
355
- })
356
- method.addParameter({
357
- name: 'params',
358
- type: `OmitKey<${toTitleCase(
359
- ATP_METHODS.create,
360
- )}.InputSchema, "collection" | "record">`,
361
- })
362
- method.addParameter({
363
- name: 'record',
364
- type: `Un$Typed<${typeModule}.Record>`,
365
- })
366
- method.addParameter({
367
- name: 'headers?',
368
- type: `Record<string, string>`,
369
- })
370
- const maybeRkeyPart = lexRecord.key?.startsWith('literal:')
371
- ? `rkey: '${lexRecord.key.replace('literal:', '')}', `
372
- : ''
373
- method.setBodyText(
374
- [
375
- `const collection = '${nsid}'`,
376
- `const res = await this._client.call('${ATP_METHODS.create}', undefined, { collection, ${maybeRkeyPart}...params, record: { ...record, $type: collection } }, { encoding: 'application/json', headers })`,
377
- `return res.data`,
378
- ].join('\n'),
379
- )
380
- }
381
- {
382
- //= put()
383
- const method = cls.addMethod({
384
- isAsync: true,
385
- name: 'put',
386
- returnType: 'Promise<{uri: string, cid: string}>',
387
- })
388
- method.addParameter({
389
- name: 'params',
390
- type: `OmitKey<${toTitleCase(ATP_METHODS.put)}.InputSchema, "collection" | "record">`,
391
- })
392
- method.addParameter({
393
- name: 'record',
394
- type: `Un$Typed<${typeModule}.Record>`,
395
- })
396
- method.addParameter({
397
- name: 'headers?',
398
- type: `Record<string, string>`,
399
- })
400
- method.setBodyText(
401
- [
402
- `const collection = '${nsid}'`,
403
- `const res = await this._client.call('${ATP_METHODS.put}', undefined, { collection, ...params, record: { ...record, $type: collection } }, { encoding: 'application/json', headers })`,
404
- `return res.data`,
405
- ].join('\n'),
406
- )
407
- }
408
- {
409
- //= delete()
410
- const method = cls.addMethod({
411
- isAsync: true,
412
- name: 'delete',
413
- returnType: 'Promise<void>',
414
- })
415
- method.addParameter({
416
- name: 'params',
417
- type: `OmitKey<${toTitleCase(
418
- ATP_METHODS.delete,
419
- )}.InputSchema, "collection">`,
420
- })
421
- method.addParameter({
422
- name: 'headers?',
423
- type: `Record<string, string>`,
424
- })
425
-
426
- method.setBodyText(
427
- [
428
- `await this._client.call('${ATP_METHODS.delete}', undefined, { collection: '${nsid}', ...params }, { headers })`,
429
- ].join('\n'),
430
- )
431
- }
432
- }
433
-
434
- const lexiconTs = (project, lexicons: Lexicons, lexiconDoc: LexiconDoc) =>
435
- gen(
436
- project,
437
- `/types/${lexiconDoc.id.split('.').join('/')}.ts`,
438
- async (file) => {
439
- const main = lexiconDoc.defs.main
440
- if (
441
- main?.type === 'query' ||
442
- main?.type === 'subscription' ||
443
- main?.type === 'procedure'
444
- ) {
445
- //= import {HeadersMap, XRPCError} from '@atproto/xrpc'
446
- const xrpcImport = file.addImportDeclaration({
447
- moduleSpecifier: '@atproto/xrpc',
448
- })
449
- xrpcImport.addNamedImports([
450
- { name: 'HeadersMap' },
451
- { name: 'XRPCError' },
452
- ])
453
- }
454
-
455
- genCommonImports(file, lexiconDoc.id)
456
-
457
- const imports: Set<string> = new Set()
458
- for (const defId in lexiconDoc.defs) {
459
- const def = lexiconDoc.defs[defId]
460
- const lexUri = `${lexiconDoc.id}#${defId}`
461
- if (defId === 'main') {
462
- if (def.type === 'query' || def.type === 'procedure') {
463
- genXrpcParams(file, lexicons, lexUri, false)
464
- genXrpcInput(file, imports, lexicons, lexUri, false)
465
- genXrpcOutput(file, imports, lexicons, lexUri)
466
- genClientXrpcCommon(file, lexicons, lexUri)
467
- } else if (def.type === 'subscription') {
468
- continue
469
- } else if (def.type === 'record') {
470
- genRecord(file, imports, lexicons, lexUri)
471
- } else {
472
- genUserType(file, imports, lexicons, lexUri)
473
- }
474
- } else {
475
- genUserType(file, imports, lexicons, lexUri)
476
- }
477
- }
478
- genImports(file, imports, lexiconDoc.id)
479
- },
480
- )
481
-
482
- function genClientXrpcCommon(
483
- file: SourceFile,
484
- lexicons: Lexicons,
485
- lexUri: string,
486
- ) {
487
- const def = lexicons.getDefOrThrow(lexUri, ['query', 'procedure'])
488
-
489
- //= export interface CallOptions {...}
490
- const opts = file.addInterface({
491
- name: 'CallOptions',
492
- isExported: true,
493
- })
494
- opts.addProperty({ name: 'signal?', type: 'AbortSignal' })
495
- opts.addProperty({ name: 'headers?', type: 'HeadersMap' })
496
- if (def.type === 'procedure') {
497
- opts.addProperty({ name: 'qp?', type: 'QueryParams' })
498
- }
499
- if (def.type === 'procedure' && def.input) {
500
- let encodingType = 'string'
501
- if (def.input.encoding !== '*/*') {
502
- encodingType = def.input.encoding
503
- .split(',')
504
- .map((v) => `'${v.trim()}'`)
505
- .join(' | ')
506
- }
507
- opts.addProperty({
508
- name: 'encoding?',
509
- type: encodingType,
510
- })
511
- }
512
-
513
- // export interface Response {...}
514
- const res = file.addInterface({
515
- name: 'Response',
516
- isExported: true,
517
- })
518
- res.addProperty({ name: 'success', type: 'boolean' })
519
- res.addProperty({ name: 'headers', type: 'HeadersMap' })
520
- if (def.output?.schema) {
521
- if (def.output.encoding?.includes(',')) {
522
- res.addProperty({ name: 'data', type: 'OutputSchema | Uint8Array' })
523
- } else {
524
- res.addProperty({ name: 'data', type: 'OutputSchema' })
525
- }
526
- } else if (def.output?.encoding) {
527
- res.addProperty({ name: 'data', type: 'Uint8Array' })
528
- }
529
-
530
- // export class {errcode}Error {...}
531
- const customErrors: { name: string; cls: string }[] = []
532
- for (const error of def.errors || []) {
533
- let name = toTitleCase(error.name)
534
- if (!name.endsWith('Error')) name += 'Error'
535
- const errCls = file.addClass({
536
- name,
537
- extends: 'XRPCError',
538
- isExported: true,
539
- })
540
- errCls.addConstructor({
541
- parameters: [{ name: 'src', type: 'XRPCError' }],
542
- statements: [
543
- 'super(src.status, src.error, src.message, src.headers, { cause: src })',
544
- ],
545
- })
546
-
547
- customErrors.push({ name: error.name, cls: name })
548
- }
549
-
550
- // export function toKnownErr(err: any) {...}
551
- file.addFunction({
552
- name: 'toKnownErr',
553
- isExported: true,
554
- parameters: [{ name: 'e', type: 'any' }],
555
- statements: customErrors.length
556
- ? [
557
- 'if (e instanceof XRPCError) {',
558
- ...customErrors.map(
559
- (err) => `if (e.error === '${err.name}') return new ${err.cls}(e)`,
560
- ),
561
- '}',
562
- 'return e',
563
- ]
564
- : ['return e'],
565
- })
566
- }