Barazo lexicon schemas and TypeScript types
barazo.forum
1/**
2 * Post-generation fixup script for lex-cli output.
3 *
4 * lex-cli generates TypeScript files with:
5 * 1. Missing .js extensions on relative imports (incompatible with NodeNext)
6 * 2. An XRPC server/client wrapper index.ts we don't need
7 *
8 * Additionally, some lexicons use types lex-cli can't process (e.g. permission-set).
9 * These are excluded from codegen but injected into lexicons.ts schemaDict and ids
10 * so runtime validation and ID lookup still work.
11 *
12 * This script:
13 * - Builds a clean index.ts from discovered type files (no hardcoded list)
14 * - Fixes missing .js import extensions
15 * - Injects excluded lexicons into schemaDict and ids
16 */
17import { readdir, readFile, writeFile } from 'node:fs/promises'
18import { join, relative } from 'node:path'
19
20const GENERATED_DIR = new URL('../src/generated', import.meta.url).pathname
21const TYPES_DIR = join(GENERATED_DIR, 'types')
22const LEXICONS_DIR = new URL('../lexicons', import.meta.url).pathname
23
24// Lexicons excluded from lex-cli codegen but needing schemaDict/ids entries.
25// Each entry: { file: path relative to lexicons/, dictKey: schemaDict key }
26const EXCLUDED_LEXICONS = [
27 { file: 'forum/barazo/authForumAccess.json', dictKey: 'ForumBarazoAuthForumAccess' },
28]
29
30async function getTypeFiles(dir) {
31 const entries = await readdir(dir, { withFileTypes: true, recursive: true })
32 return entries
33 .filter((e) => e.isFile() && e.name.endsWith('.ts'))
34 .map((e) => join(e.parentPath ?? e.path, e.name))
35}
36
37function toExportName(filePath) {
38 const rel = relative(TYPES_DIR, filePath).replace(/\.ts$/, '')
39 return rel
40 .split('/')
41 .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
42 .join('')
43}
44
45async function buildReplacementIndex(typeFiles) {
46 const exports = typeFiles
47 .map((file) => {
48 const name = toExportName(file)
49 const relPath = './' + relative(GENERATED_DIR, file).replace(/\.ts$/, '.js')
50 return `export * as ${name} from "${relPath}";`
51 })
52 .sort()
53
54 return `/**
55 * GENERATED CODE - Re-exports only.
56 * The XRPC server/client wrappers generated by lex-cli are replaced with
57 * direct type re-exports since we use @atproto/api for PDS interactions.
58 */
59${exports.join('\n')}
60export { schemas, validate } from "./lexicons.js";
61`
62}
63
64async function fixImportExtensions(filePath) {
65 let content = await readFile(filePath, 'utf-8')
66 const original = content
67 content = content.replace(/from '(\.\.?\/[^']+?)(?<!\.js)'/g, "from '$1.js'")
68 if (content !== original) {
69 await writeFile(filePath, content)
70 }
71}
72
73async function injectExcludedLexicons(lexiconsFile) {
74 let content = await readFile(lexiconsFile, 'utf-8')
75
76 for (const { file, dictKey } of EXCLUDED_LEXICONS) {
77 // Skip if already present (idempotent)
78 if (content.includes(` ${dictKey}:`)) continue
79
80 const lexiconJson = JSON.parse(await readFile(join(LEXICONS_DIR, file), 'utf-8'))
81
82 // Insert into schemaDict (before the closing `} as const satisfies`)
83 const schemaDictEntry = ` ${dictKey}: ${JSON.stringify(lexiconJson, null, 4).replace(/\n/g, '\n ')},\n`
84 content = content.replace(
85 '} as const satisfies Record<string, LexiconDoc>',
86 `${schemaDictEntry}} as const satisfies Record<string, LexiconDoc>`
87 )
88
89 // Insert into ids (before the closing `} as const`)
90 // Find the ids block and insert alphabetically
91 const idsEntry = ` ${dictKey}: '${lexiconJson.id}',\n`
92 const idsMatch = content.match(/export const ids = \{[\s\S]*?\} as const/)
93 if (idsMatch && !idsMatch[0].includes(dictKey)) {
94 content = content.replace(/(\} as const)$/m, `${idsEntry}$1`)
95 }
96
97 console.log(`Injected excluded lexicon: ${dictKey} (${lexiconJson.id})`)
98 }
99
100 await writeFile(lexiconsFile, content)
101}
102
103async function main() {
104 const typeFiles = await getTypeFiles(TYPES_DIR)
105 const indexContent = await buildReplacementIndex(typeFiles)
106 await writeFile(join(GENERATED_DIR, 'index.ts'), indexContent)
107
108 for (const file of typeFiles) {
109 await fixImportExtensions(file)
110 }
111
112 await fixImportExtensions(join(GENERATED_DIR, 'lexicons.ts'))
113 await fixImportExtensions(join(GENERATED_DIR, 'util.ts'))
114
115 // Inject lexicons that lex-cli can't process
116 await injectExcludedLexicons(join(GENERATED_DIR, 'lexicons.ts'))
117
118 console.log(
119 `Fixed ${typeFiles.length + 2} generated files (${typeFiles.length} types + lexicons.ts + util.ts)`
120 )
121}
122
123main()