@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.
- package/CHANGELOG.md +26 -0
- package/bin.js +8 -4
- package/dist/codegen/client.d.ts +1 -1
- package/dist/codegen/client.d.ts.map +1 -1
- package/dist/codegen/client.js +1 -1
- package/dist/codegen/client.js.map +1 -1
- package/dist/codegen/common.d.ts +4 -4
- package/dist/codegen/common.d.ts.map +1 -1
- package/dist/codegen/common.js +1 -1
- package/dist/codegen/common.js.map +1 -1
- package/dist/codegen/lex-gen.js +2 -1
- package/dist/codegen/lex-gen.js.map +1 -1
- package/dist/codegen/server.d.ts +1 -1
- package/dist/codegen/server.d.ts.map +1 -1
- package/dist/codegen/server.js +1 -1
- package/dist/codegen/server.js.map +1 -1
- package/dist/codegen/util.js +1 -0
- package/dist/codegen/util.js.map +1 -1
- package/dist/mdgen/index.js +3 -2
- package/dist/mdgen/index.js.map +1 -1
- package/dist/util.d.ts +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js.map +1 -1
- package/package.json +17 -13
- package/src/codegen/client.ts +0 -566
- package/src/codegen/common.ts +0 -283
- package/src/codegen/lex-gen.ts +0 -720
- package/src/codegen/server.ts +0 -448
- package/src/codegen/util.ts +0 -84
- package/src/index.ts +0 -105
- package/src/mdgen/index.ts +0 -77
- package/src/types.ts +0 -14
- package/src/util.ts +0 -151
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -4
package/src/codegen/server.ts
DELETED
|
@@ -1,448 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IndentationText,
|
|
3
|
-
Project,
|
|
4
|
-
SourceFile,
|
|
5
|
-
VariableDeclarationKind,
|
|
6
|
-
} from 'ts-morph'
|
|
7
|
-
import { 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
|
-
export async function genServerApi(
|
|
30
|
-
lexiconDocs: LexiconDoc[],
|
|
31
|
-
): Promise<GeneratedAPI> {
|
|
32
|
-
const project = new Project({
|
|
33
|
-
useInMemoryFileSystem: true,
|
|
34
|
-
manipulationSettings: { indentationText: IndentationText.TwoSpaces },
|
|
35
|
-
})
|
|
36
|
-
const api: GeneratedAPI = { files: [] }
|
|
37
|
-
const lexicons = new Lexicons(lexiconDocs)
|
|
38
|
-
const nsidTree = lexiconsToDefTree(lexiconDocs)
|
|
39
|
-
const nsidTokens = schemasToNsidTokens(lexiconDocs)
|
|
40
|
-
for (const lexiconDoc of lexiconDocs) {
|
|
41
|
-
api.files.push(await lexiconTs(project, lexicons, lexiconDoc))
|
|
42
|
-
}
|
|
43
|
-
api.files.push(await utilTs(project))
|
|
44
|
-
api.files.push(await lexiconsTs(project, lexiconDocs))
|
|
45
|
-
api.files.push(await indexTs(project, lexiconDocs, nsidTree, nsidTokens))
|
|
46
|
-
return api
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const indexTs = (
|
|
50
|
-
project: Project,
|
|
51
|
-
lexiconDocs: LexiconDoc[],
|
|
52
|
-
nsidTree: DefTreeNode[],
|
|
53
|
-
nsidTokens: Record<string, string[]>,
|
|
54
|
-
) =>
|
|
55
|
-
gen(project, '/index.ts', async (file) => {
|
|
56
|
-
//= import {createServer as createXrpcServer, Server as XrpcServer} from '@atproto/xrpc-server'
|
|
57
|
-
file.addImportDeclaration({
|
|
58
|
-
moduleSpecifier: '@atproto/xrpc-server',
|
|
59
|
-
namedImports: [
|
|
60
|
-
{ name: 'Auth', isTypeOnly: true },
|
|
61
|
-
{ name: 'Options', alias: 'XrpcOptions', isTypeOnly: true },
|
|
62
|
-
{ name: 'Server', alias: 'XrpcServer' },
|
|
63
|
-
{ name: 'StreamConfigOrHandler', isTypeOnly: true },
|
|
64
|
-
{ name: 'MethodConfigOrHandler', isTypeOnly: true },
|
|
65
|
-
{ name: 'createServer', alias: 'createXrpcServer' },
|
|
66
|
-
],
|
|
67
|
-
})
|
|
68
|
-
//= import {schemas} from './lexicons.js'
|
|
69
|
-
file
|
|
70
|
-
.addImportDeclaration({
|
|
71
|
-
moduleSpecifier: './lexicons.js',
|
|
72
|
-
})
|
|
73
|
-
.addNamedImport({
|
|
74
|
-
name: 'schemas',
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// generate type imports
|
|
78
|
-
for (const lexiconDoc of lexiconDocs) {
|
|
79
|
-
if (
|
|
80
|
-
lexiconDoc.defs.main?.type !== 'query' &&
|
|
81
|
-
lexiconDoc.defs.main?.type !== 'subscription' &&
|
|
82
|
-
lexiconDoc.defs.main?.type !== 'procedure'
|
|
83
|
-
) {
|
|
84
|
-
continue
|
|
85
|
-
}
|
|
86
|
-
file
|
|
87
|
-
.addImportDeclaration({
|
|
88
|
-
moduleSpecifier: `./types/${lexiconDoc.id.split('.').join('/')}.js`,
|
|
89
|
-
})
|
|
90
|
-
.setNamespaceImport(toTitleCase(lexiconDoc.id))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// generate token enums
|
|
94
|
-
for (const nsidAuthority in nsidTokens) {
|
|
95
|
-
// export const {THE_AUTHORITY} = {
|
|
96
|
-
// {Name}: "{authority.the.name}"
|
|
97
|
-
// }
|
|
98
|
-
file.addVariableStatement({
|
|
99
|
-
isExported: true,
|
|
100
|
-
declarationKind: VariableDeclarationKind.Const,
|
|
101
|
-
declarations: [
|
|
102
|
-
{
|
|
103
|
-
name: toScreamingSnakeCase(nsidAuthority),
|
|
104
|
-
initializer: [
|
|
105
|
-
'{',
|
|
106
|
-
...nsidTokens[nsidAuthority].map(
|
|
107
|
-
(nsidName) =>
|
|
108
|
-
`${toTitleCase(nsidName)}: "${nsidAuthority}.${nsidName}",`,
|
|
109
|
-
),
|
|
110
|
-
'}',
|
|
111
|
-
].join('\n'),
|
|
112
|
-
},
|
|
113
|
-
],
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
//= export function createServer(options?: XrpcOptions) { ... }
|
|
118
|
-
const createServerFn = file.addFunction({
|
|
119
|
-
name: 'createServer',
|
|
120
|
-
returnType: 'Server',
|
|
121
|
-
parameters: [
|
|
122
|
-
{ name: 'options', type: 'XrpcOptions', hasQuestionToken: true },
|
|
123
|
-
],
|
|
124
|
-
isExported: true,
|
|
125
|
-
})
|
|
126
|
-
createServerFn.setBodyText(`return new Server(options)`)
|
|
127
|
-
|
|
128
|
-
//= export class Server {...}
|
|
129
|
-
const serverCls = file.addClass({
|
|
130
|
-
name: 'Server',
|
|
131
|
-
isExported: true,
|
|
132
|
-
})
|
|
133
|
-
//= xrpc: XrpcServer = createXrpcServer(methodSchemas)
|
|
134
|
-
serverCls.addProperty({
|
|
135
|
-
name: 'xrpc',
|
|
136
|
-
type: 'XrpcServer',
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// generate classes for the schemas
|
|
140
|
-
for (const ns of nsidTree) {
|
|
141
|
-
//= ns: NS
|
|
142
|
-
serverCls.addProperty({
|
|
143
|
-
name: ns.propName,
|
|
144
|
-
type: ns.className,
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
// class...
|
|
148
|
-
genNamespaceCls(file, ns)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
//= constructor (options?: XrpcOptions) {
|
|
152
|
-
//= this.xrpc = createXrpcServer(schemas, options)
|
|
153
|
-
//= {namespace declarations}
|
|
154
|
-
//= }
|
|
155
|
-
serverCls
|
|
156
|
-
.addConstructor({
|
|
157
|
-
parameters: [
|
|
158
|
-
{ name: 'options', type: 'XrpcOptions', hasQuestionToken: true },
|
|
159
|
-
],
|
|
160
|
-
})
|
|
161
|
-
.setBodyText(
|
|
162
|
-
[
|
|
163
|
-
'this.xrpc = createXrpcServer(schemas, options)',
|
|
164
|
-
...nsidTree.map(
|
|
165
|
-
(ns) => `this.${ns.propName} = new ${ns.className}(this)`,
|
|
166
|
-
),
|
|
167
|
-
].join('\n'),
|
|
168
|
-
)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
function genNamespaceCls(file: SourceFile, ns: DefTreeNode) {
|
|
172
|
-
//= export class {ns}NS {...}
|
|
173
|
-
const cls = file.addClass({
|
|
174
|
-
name: ns.className,
|
|
175
|
-
isExported: true,
|
|
176
|
-
})
|
|
177
|
-
//= _server: Server
|
|
178
|
-
cls.addProperty({
|
|
179
|
-
name: '_server',
|
|
180
|
-
type: 'Server',
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
for (const child of ns.children) {
|
|
184
|
-
//= child: ChildNS
|
|
185
|
-
cls.addProperty({
|
|
186
|
-
name: child.propName,
|
|
187
|
-
type: child.className,
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
// recurse
|
|
191
|
-
genNamespaceCls(file, child)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
//= constructor(server: Server) {
|
|
195
|
-
//= this._server = server
|
|
196
|
-
//= {child namespace declarations}
|
|
197
|
-
//= }
|
|
198
|
-
const cons = cls.addConstructor()
|
|
199
|
-
cons.addParameter({
|
|
200
|
-
name: 'server',
|
|
201
|
-
type: 'Server',
|
|
202
|
-
})
|
|
203
|
-
cons.setBodyText(
|
|
204
|
-
[
|
|
205
|
-
`this._server = server`,
|
|
206
|
-
...ns.children.map(
|
|
207
|
-
(ns) => `this.${ns.propName} = new ${ns.className}(server)`,
|
|
208
|
-
),
|
|
209
|
-
].join('\n'),
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
// methods
|
|
213
|
-
for (const userType of ns.userTypes) {
|
|
214
|
-
if (
|
|
215
|
-
userType.def.type !== 'query' &&
|
|
216
|
-
userType.def.type !== 'subscription' &&
|
|
217
|
-
userType.def.type !== 'procedure'
|
|
218
|
-
) {
|
|
219
|
-
continue
|
|
220
|
-
}
|
|
221
|
-
const moduleName = toTitleCase(userType.nsid)
|
|
222
|
-
const name = toCamelCase(NSID.parse(userType.nsid).name || '')
|
|
223
|
-
const isSubscription = userType.def.type === 'subscription'
|
|
224
|
-
const method = cls.addMethod({
|
|
225
|
-
name,
|
|
226
|
-
typeParameters: [
|
|
227
|
-
{
|
|
228
|
-
name: 'A',
|
|
229
|
-
constraint: 'Auth',
|
|
230
|
-
default: 'void',
|
|
231
|
-
},
|
|
232
|
-
],
|
|
233
|
-
})
|
|
234
|
-
method.addParameter({
|
|
235
|
-
name: 'cfg',
|
|
236
|
-
type: isSubscription
|
|
237
|
-
? `StreamConfigOrHandler<
|
|
238
|
-
A,
|
|
239
|
-
${moduleName}.QueryParams,
|
|
240
|
-
${moduleName}.HandlerOutput,
|
|
241
|
-
>`
|
|
242
|
-
: `MethodConfigOrHandler<
|
|
243
|
-
A,
|
|
244
|
-
${moduleName}.QueryParams,
|
|
245
|
-
${moduleName}.HandlerInput,
|
|
246
|
-
${moduleName}.HandlerOutput,
|
|
247
|
-
>`,
|
|
248
|
-
})
|
|
249
|
-
const methodType = isSubscription ? 'streamMethod' : 'method'
|
|
250
|
-
method.setBodyText(
|
|
251
|
-
[
|
|
252
|
-
// Placing schema on separate line, since the following one was being formatted
|
|
253
|
-
// into multiple lines and causing the ts-ignore to ignore the wrong line.
|
|
254
|
-
`const nsid = '${userType.nsid}' // @ts-ignore`,
|
|
255
|
-
`return this._server.xrpc.${methodType}(nsid, cfg)`,
|
|
256
|
-
].join('\n'),
|
|
257
|
-
)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const lexiconTs = (project, lexicons: Lexicons, lexiconDoc: LexiconDoc) =>
|
|
262
|
-
gen(
|
|
263
|
-
project,
|
|
264
|
-
`/types/${lexiconDoc.id.split('.').join('/')}.ts`,
|
|
265
|
-
async (file) => {
|
|
266
|
-
const main = lexiconDoc.defs.main
|
|
267
|
-
if (main?.type === 'query' || main?.type === 'procedure') {
|
|
268
|
-
const streamingInput =
|
|
269
|
-
main?.type === 'procedure' &&
|
|
270
|
-
main.input?.encoding &&
|
|
271
|
-
!main.input.schema
|
|
272
|
-
const streamingOutput = main.output?.encoding && !main.output.schema
|
|
273
|
-
if (streamingInput || streamingOutput) {
|
|
274
|
-
//= import stream from 'node:stream'
|
|
275
|
-
file.addImportDeclaration({
|
|
276
|
-
moduleSpecifier: 'node:stream',
|
|
277
|
-
defaultImport: 'stream',
|
|
278
|
-
})
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
genCommonImports(file, lexiconDoc.id)
|
|
283
|
-
|
|
284
|
-
const imports: Set<string> = new Set()
|
|
285
|
-
for (const defId in lexiconDoc.defs) {
|
|
286
|
-
const def = lexiconDoc.defs[defId]
|
|
287
|
-
const lexUri = `${lexiconDoc.id}#${defId}`
|
|
288
|
-
if (defId === 'main') {
|
|
289
|
-
if (def.type === 'query' || def.type === 'procedure') {
|
|
290
|
-
genXrpcParams(file, lexicons, lexUri)
|
|
291
|
-
genXrpcInput(file, imports, lexicons, lexUri)
|
|
292
|
-
genXrpcOutput(file, imports, lexicons, lexUri, false)
|
|
293
|
-
genServerXrpcMethod(file, lexicons, lexUri)
|
|
294
|
-
} else if (def.type === 'subscription') {
|
|
295
|
-
genXrpcParams(file, lexicons, lexUri)
|
|
296
|
-
genXrpcOutput(file, imports, lexicons, lexUri, false)
|
|
297
|
-
genServerXrpcStreaming(file, lexicons, lexUri)
|
|
298
|
-
} else if (def.type === 'record') {
|
|
299
|
-
genRecord(file, imports, lexicons, lexUri)
|
|
300
|
-
} else {
|
|
301
|
-
genUserType(file, imports, lexicons, lexUri)
|
|
302
|
-
}
|
|
303
|
-
} else {
|
|
304
|
-
genUserType(file, imports, lexicons, lexUri)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
genImports(file, imports, lexiconDoc.id)
|
|
308
|
-
},
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
function genServerXrpcMethod(
|
|
312
|
-
file: SourceFile,
|
|
313
|
-
lexicons: Lexicons,
|
|
314
|
-
lexUri: string,
|
|
315
|
-
) {
|
|
316
|
-
const def = lexicons.getDefOrThrow(lexUri, ['query', 'procedure'])
|
|
317
|
-
|
|
318
|
-
//= export interface HandlerInput {...}
|
|
319
|
-
if (def.type === 'procedure' && def.input?.encoding) {
|
|
320
|
-
const handlerInput = file.addInterface({
|
|
321
|
-
name: 'HandlerInput',
|
|
322
|
-
isExported: true,
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
handlerInput.addProperty({
|
|
326
|
-
name: 'encoding',
|
|
327
|
-
type: def.input.encoding
|
|
328
|
-
.split(',')
|
|
329
|
-
.map((v) => `'${v.trim()}'`)
|
|
330
|
-
.join(' | '),
|
|
331
|
-
})
|
|
332
|
-
handlerInput.addProperty({
|
|
333
|
-
name: 'body',
|
|
334
|
-
type: def.input.schema
|
|
335
|
-
? def.input.encoding.includes(',')
|
|
336
|
-
? 'InputSchema | stream.Readable'
|
|
337
|
-
: 'InputSchema'
|
|
338
|
-
: 'stream.Readable',
|
|
339
|
-
})
|
|
340
|
-
} else {
|
|
341
|
-
file.addTypeAlias({
|
|
342
|
-
isExported: true,
|
|
343
|
-
name: 'HandlerInput',
|
|
344
|
-
type: 'void',
|
|
345
|
-
})
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// export interface HandlerSuccess {...}
|
|
349
|
-
let hasHandlerSuccess = false
|
|
350
|
-
if (def.output?.schema || def.output?.encoding) {
|
|
351
|
-
hasHandlerSuccess = true
|
|
352
|
-
const handlerSuccess = file.addInterface({
|
|
353
|
-
name: 'HandlerSuccess',
|
|
354
|
-
isExported: true,
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
if (def.output.encoding) {
|
|
358
|
-
handlerSuccess.addProperty({
|
|
359
|
-
name: 'encoding',
|
|
360
|
-
type: def.output.encoding
|
|
361
|
-
.split(',')
|
|
362
|
-
.map((v) => `'${v.trim()}'`)
|
|
363
|
-
.join(' | '),
|
|
364
|
-
})
|
|
365
|
-
}
|
|
366
|
-
if (def.output?.schema) {
|
|
367
|
-
if (def.output.encoding.includes(',')) {
|
|
368
|
-
handlerSuccess.addProperty({
|
|
369
|
-
name: 'body',
|
|
370
|
-
type: 'OutputSchema | Uint8Array | stream.Readable',
|
|
371
|
-
})
|
|
372
|
-
} else {
|
|
373
|
-
handlerSuccess.addProperty({ name: 'body', type: 'OutputSchema' })
|
|
374
|
-
}
|
|
375
|
-
} else if (def.output?.encoding) {
|
|
376
|
-
handlerSuccess.addProperty({
|
|
377
|
-
name: 'body',
|
|
378
|
-
type: 'Uint8Array | stream.Readable',
|
|
379
|
-
})
|
|
380
|
-
}
|
|
381
|
-
handlerSuccess.addProperty({
|
|
382
|
-
name: 'headers?',
|
|
383
|
-
type: '{ [key: string]: string }',
|
|
384
|
-
})
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// export interface HandlerError {...}
|
|
388
|
-
const handlerError = file.addInterface({
|
|
389
|
-
name: 'HandlerError',
|
|
390
|
-
isExported: true,
|
|
391
|
-
})
|
|
392
|
-
handlerError.addProperties([
|
|
393
|
-
{ name: 'status', type: 'number' },
|
|
394
|
-
{ name: 'message?', type: 'string' },
|
|
395
|
-
])
|
|
396
|
-
if (def.errors?.length) {
|
|
397
|
-
handlerError.addProperty({
|
|
398
|
-
name: 'error?',
|
|
399
|
-
type: def.errors.map((err) => `'${err.name}'`).join(' | '),
|
|
400
|
-
})
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// export type HandlerOutput = ...
|
|
404
|
-
file.addTypeAlias({
|
|
405
|
-
isExported: true,
|
|
406
|
-
name: 'HandlerOutput',
|
|
407
|
-
type: `HandlerError | ${hasHandlerSuccess ? 'HandlerSuccess' : 'void'}`,
|
|
408
|
-
})
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function genServerXrpcStreaming(
|
|
412
|
-
file: SourceFile,
|
|
413
|
-
lexicons: Lexicons,
|
|
414
|
-
lexUri: string,
|
|
415
|
-
) {
|
|
416
|
-
const def = lexicons.getDefOrThrow(lexUri, ['subscription'])
|
|
417
|
-
|
|
418
|
-
file.addImportDeclaration({
|
|
419
|
-
moduleSpecifier: '@atproto/xrpc-server',
|
|
420
|
-
namedImports: [{ name: 'ErrorFrame' }],
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
file.addImportDeclaration({
|
|
424
|
-
moduleSpecifier: 'node:http',
|
|
425
|
-
namedImports: [{ name: 'IncomingMessage' }],
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
// export type HandlerError = ...
|
|
429
|
-
file.addTypeAlias({
|
|
430
|
-
name: 'HandlerError',
|
|
431
|
-
isExported: true,
|
|
432
|
-
type: `ErrorFrame<${arrayToUnion(def.errors?.map((e) => e.name))}>`,
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
// export type HandlerOutput = ...
|
|
436
|
-
file.addTypeAlias({
|
|
437
|
-
isExported: true,
|
|
438
|
-
name: 'HandlerOutput',
|
|
439
|
-
type: `HandlerError | ${def.message?.schema ? 'OutputSchema' : 'void'}`,
|
|
440
|
-
})
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function arrayToUnion(arr?: string[]) {
|
|
444
|
-
if (!arr?.length) {
|
|
445
|
-
return 'never'
|
|
446
|
-
}
|
|
447
|
-
return arr.map((item) => `'${item}'`).join(' | ')
|
|
448
|
-
}
|
package/src/codegen/util.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { type LexUserType, type LexiconDoc } from '@atproto/lexicon'
|
|
2
|
-
import { NSID } from '@atproto/syntax'
|
|
3
|
-
|
|
4
|
-
export interface DefTreeNodeUserType {
|
|
5
|
-
nsid: string
|
|
6
|
-
def: LexUserType
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface DefTreeNode {
|
|
10
|
-
name: string
|
|
11
|
-
className: string
|
|
12
|
-
propName: string
|
|
13
|
-
children: DefTreeNode[]
|
|
14
|
-
userTypes: DefTreeNodeUserType[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function lexiconsToDefTree(lexicons: LexiconDoc[]): DefTreeNode[] {
|
|
18
|
-
const tree: DefTreeNode[] = []
|
|
19
|
-
for (const lexicon of lexicons) {
|
|
20
|
-
if (!lexicon.defs.main) {
|
|
21
|
-
continue
|
|
22
|
-
}
|
|
23
|
-
const node = getOrCreateNode(tree, lexicon.id.split('.').slice(0, -1))
|
|
24
|
-
node.userTypes.push({ nsid: lexicon.id, def: lexicon.defs.main })
|
|
25
|
-
}
|
|
26
|
-
return tree
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getOrCreateNode(tree: DefTreeNode[], path: string[]): DefTreeNode {
|
|
30
|
-
let node: DefTreeNode | undefined
|
|
31
|
-
for (let i = 0; i < path.length; i++) {
|
|
32
|
-
const segment = path[i]
|
|
33
|
-
node = tree.find((v) => v.name === segment)
|
|
34
|
-
if (!node) {
|
|
35
|
-
node = {
|
|
36
|
-
name: segment,
|
|
37
|
-
className: `${toTitleCase(path.slice(0, i + 1).join('-'))}NS`,
|
|
38
|
-
propName: toCamelCase(segment),
|
|
39
|
-
children: [],
|
|
40
|
-
userTypes: [],
|
|
41
|
-
} as DefTreeNode
|
|
42
|
-
tree.push(node)
|
|
43
|
-
}
|
|
44
|
-
tree = node.children
|
|
45
|
-
}
|
|
46
|
-
if (!node) throw new Error(`Invalid schema path: ${path.join('.')}`)
|
|
47
|
-
return node
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function schemasToNsidTokens(
|
|
51
|
-
lexiconDocs: LexiconDoc[],
|
|
52
|
-
): Record<string, string[]> {
|
|
53
|
-
const nsidTokens: Record<string, string[]> = {}
|
|
54
|
-
for (const lexiconDoc of lexiconDocs) {
|
|
55
|
-
const nsidp = NSID.parse(lexiconDoc.id)
|
|
56
|
-
if (!nsidp.name) continue
|
|
57
|
-
for (const defId in lexiconDoc.defs) {
|
|
58
|
-
const def = lexiconDoc.defs[defId]
|
|
59
|
-
if (def.type !== 'token') continue
|
|
60
|
-
const authority = nsidp.segments.slice(0, -1).join('.')
|
|
61
|
-
nsidTokens[authority] ??= []
|
|
62
|
-
nsidTokens[authority].push(
|
|
63
|
-
nsidp.name + (defId === 'main' ? '' : `#${defId}`),
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return nsidTokens
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function toTitleCase(v: string): string {
|
|
71
|
-
v = v.replace(/^([a-z])/gi, (_, g) => g.toUpperCase()) // upper-case first letter
|
|
72
|
-
v = v.replace(/[.#-]([a-z])/gi, (_, g) => g.toUpperCase()) // uppercase any dash, dot, or hash segments
|
|
73
|
-
return v.replace(/[.-]/g, '') // remove lefover dashes or dots
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function toCamelCase(v: string): string {
|
|
77
|
-
v = v.replace(/[.#-]([a-z])/gi, (_, g) => g.toUpperCase()) // uppercase any dash, dot, or hash segments
|
|
78
|
-
return v.replace(/[.-]/g, '') // remove lefover dashes or dots
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function toScreamingSnakeCase(v: string): string {
|
|
82
|
-
v = v.replace(/[.#-]+/gi, '_') // convert dashes, dots, and hashes into underscores
|
|
83
|
-
return v.toUpperCase() // and scream!
|
|
84
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { Command } from 'commander'
|
|
5
|
-
import yesno from 'yesno'
|
|
6
|
-
import { genClientApi } from './codegen/client.js'
|
|
7
|
-
import { genServerApi } from './codegen/server.js'
|
|
8
|
-
import * as mdGen from './mdgen/index.js'
|
|
9
|
-
import {
|
|
10
|
-
applyFileDiff,
|
|
11
|
-
genFileDiff,
|
|
12
|
-
genTsObj,
|
|
13
|
-
printFileDiff,
|
|
14
|
-
readAllLexicons,
|
|
15
|
-
} from './util.js'
|
|
16
|
-
|
|
17
|
-
const program = new Command()
|
|
18
|
-
program.name('lex').description('Lexicon CLI').version('0.0.0')
|
|
19
|
-
|
|
20
|
-
program
|
|
21
|
-
.command('gen-md')
|
|
22
|
-
.description('Generate markdown documentation')
|
|
23
|
-
.option('--yes', 'skip confirmation')
|
|
24
|
-
.argument('<outfile>', 'path of the file to write to', toPath)
|
|
25
|
-
.argument('<lexicons...>', 'paths of the lexicon files to include', toPaths)
|
|
26
|
-
.action(
|
|
27
|
-
async (outFile: string, lexiconPaths: string[], o: { yes?: true }) => {
|
|
28
|
-
if (!outFile.endsWith('.md')) {
|
|
29
|
-
console.error(
|
|
30
|
-
'Must supply the path to a .md file as the first parameter',
|
|
31
|
-
)
|
|
32
|
-
process.exit(1)
|
|
33
|
-
}
|
|
34
|
-
if (!o?.yes) await confirmOrExit()
|
|
35
|
-
console.log('Writing', outFile)
|
|
36
|
-
const lexicons = readAllLexicons(lexiconPaths)
|
|
37
|
-
await mdGen.process(outFile, lexicons)
|
|
38
|
-
},
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
program
|
|
42
|
-
.command('gen-ts-obj')
|
|
43
|
-
.description('Generate a TS file that exports an array of lexicons')
|
|
44
|
-
.argument('<lexicons...>', 'paths of the lexicon files to include', toPaths)
|
|
45
|
-
.action((lexiconPaths: string[]) => {
|
|
46
|
-
const lexicons = readAllLexicons(lexiconPaths)
|
|
47
|
-
console.log(genTsObj(lexicons))
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
program
|
|
51
|
-
.command('gen-api')
|
|
52
|
-
.description('Generate a TS client API')
|
|
53
|
-
.option('--yes', 'skip confirmation')
|
|
54
|
-
.argument('<outdir>', 'path of the directory to write to', toPath)
|
|
55
|
-
.argument('<lexicons...>', 'paths of the lexicon files to include', toPaths)
|
|
56
|
-
.action(async (outDir: string, lexiconPaths: string[], o: { yes?: true }) => {
|
|
57
|
-
const lexicons = readAllLexicons(lexiconPaths)
|
|
58
|
-
const api = await genClientApi(lexicons)
|
|
59
|
-
const diff = genFileDiff(outDir, api)
|
|
60
|
-
console.log('This will write the following files:')
|
|
61
|
-
printFileDiff(diff)
|
|
62
|
-
if (!o?.yes) await confirmOrExit()
|
|
63
|
-
applyFileDiff(diff)
|
|
64
|
-
console.log('API generated.')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
program
|
|
68
|
-
.command('gen-server')
|
|
69
|
-
.description('Generate a TS server API')
|
|
70
|
-
.option('--yes', 'skip confirmation')
|
|
71
|
-
.argument('<outdir>', 'path of the directory to write to', toPath)
|
|
72
|
-
.argument('<lexicons...>', 'paths of the lexicon files to include', toPaths)
|
|
73
|
-
.action(async (outDir: string, lexiconPaths: string[], o: { yes?: true }) => {
|
|
74
|
-
const lexicons = readAllLexicons(lexiconPaths)
|
|
75
|
-
const api = await genServerApi(lexicons)
|
|
76
|
-
const diff = genFileDiff(outDir, api)
|
|
77
|
-
console.log('This will write the following files:')
|
|
78
|
-
printFileDiff(diff)
|
|
79
|
-
if (!o?.yes) await confirmOrExit()
|
|
80
|
-
applyFileDiff(diff)
|
|
81
|
-
console.log('API generated.')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
program.parse()
|
|
85
|
-
|
|
86
|
-
function toPath(v: string) {
|
|
87
|
-
return v ? path.resolve(v) : undefined
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function toPaths(v: string, acc: string[]) {
|
|
91
|
-
acc = acc || []
|
|
92
|
-
acc.push(path.resolve(v))
|
|
93
|
-
return acc
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function confirmOrExit() {
|
|
97
|
-
const ok = await yesno({
|
|
98
|
-
question: 'Are you sure you want to continue? [y/N]',
|
|
99
|
-
defaultValue: false,
|
|
100
|
-
})
|
|
101
|
-
if (!ok) {
|
|
102
|
-
console.log('Aborted.')
|
|
103
|
-
process.exit(0)
|
|
104
|
-
}
|
|
105
|
-
}
|
package/src/mdgen/index.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import { type LexiconDoc } from '@atproto/lexicon'
|
|
3
|
-
|
|
4
|
-
const INSERT_START = [
|
|
5
|
-
'<!-- START lex generated content. Please keep comment here to allow auto update -->',
|
|
6
|
-
"<!-- DON'T EDIT THIS SECTION! INSTEAD RE-RUN lex TO UPDATE -->",
|
|
7
|
-
]
|
|
8
|
-
const INSERT_END = [
|
|
9
|
-
'<!-- END lex generated TOC please keep comment here to allow auto update -->',
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
export async function process(outFilePath: string, lexicons: LexiconDoc[]) {
|
|
13
|
-
let existingContent = ''
|
|
14
|
-
try {
|
|
15
|
-
existingContent = fs.readFileSync(outFilePath, 'utf8')
|
|
16
|
-
} catch {
|
|
17
|
-
// ignore - no existing content
|
|
18
|
-
}
|
|
19
|
-
const fileLines: StringTree = existingContent.split('\n')
|
|
20
|
-
|
|
21
|
-
// find previously generated content
|
|
22
|
-
let startIndex = fileLines.findIndex((line) => matchesStart(line))
|
|
23
|
-
let endIndex = fileLines.findIndex((line) => matchesEnd(line))
|
|
24
|
-
if (startIndex === -1) {
|
|
25
|
-
startIndex = fileLines.length
|
|
26
|
-
}
|
|
27
|
-
if (endIndex === -1) {
|
|
28
|
-
endIndex = fileLines.length
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// generate & insert content
|
|
32
|
-
fileLines.splice(startIndex, endIndex - startIndex + 1, [
|
|
33
|
-
INSERT_START,
|
|
34
|
-
await genMdLines(lexicons),
|
|
35
|
-
INSERT_END,
|
|
36
|
-
])
|
|
37
|
-
|
|
38
|
-
fs.writeFileSync(outFilePath, merge(fileLines), 'utf8')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function genMdLines(lexicons: LexiconDoc[]): Promise<StringTree> {
|
|
42
|
-
const doc: StringTree = []
|
|
43
|
-
for (const lexicon of lexicons) {
|
|
44
|
-
console.log(lexicon.id)
|
|
45
|
-
const desc: StringTree = []
|
|
46
|
-
if (lexicon.description) {
|
|
47
|
-
desc.push(lexicon.description, ``)
|
|
48
|
-
}
|
|
49
|
-
doc.push([
|
|
50
|
-
`---`,
|
|
51
|
-
``,
|
|
52
|
-
`## ${lexicon.id}`,
|
|
53
|
-
'',
|
|
54
|
-
desc,
|
|
55
|
-
'```json',
|
|
56
|
-
JSON.stringify(lexicon, null, 2),
|
|
57
|
-
'```',
|
|
58
|
-
])
|
|
59
|
-
}
|
|
60
|
-
return doc
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
type StringTree = (StringTree | string | undefined)[]
|
|
64
|
-
function merge(arr: StringTree): string {
|
|
65
|
-
return arr
|
|
66
|
-
.flat(10)
|
|
67
|
-
.filter((v) => typeof v === 'string')
|
|
68
|
-
.join('\n')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function matchesStart(line) {
|
|
72
|
-
return /<!-- START lex /.test(line)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function matchesEnd(line) {
|
|
76
|
-
return /<!-- END lex /.test(line)
|
|
77
|
-
}
|