@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,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
- }
@@ -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
- }
@@ -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
- }