/** * Post-generation fixup script for lex-cli output. * * lex-cli generates TypeScript files with: * 1. Missing .js extensions on relative imports (incompatible with NodeNext) * 2. An XRPC server/client wrapper index.ts we don't need * * Additionally, some lexicons use types lex-cli can't process (e.g. permission-set). * These are excluded from codegen but injected into lexicons.ts schemaDict and ids * so runtime validation and ID lookup still work. * * This script: * - Builds a clean index.ts from discovered type files (no hardcoded list) * - Fixes missing .js import extensions * - Injects excluded lexicons into schemaDict and ids */ import { readdir, readFile, writeFile } from 'node:fs/promises' import { join, relative } from 'node:path' const GENERATED_DIR = new URL('../src/generated', import.meta.url).pathname const TYPES_DIR = join(GENERATED_DIR, 'types') const LEXICONS_DIR = new URL('../lexicons', import.meta.url).pathname // Lexicons excluded from lex-cli codegen but needing schemaDict/ids entries. // Each entry: { file: path relative to lexicons/, dictKey: schemaDict key } const EXCLUDED_LEXICONS = [ { file: 'forum/barazo/authForumAccess.json', dictKey: 'ForumBarazoAuthForumAccess' }, ] async function getTypeFiles(dir) { const entries = await readdir(dir, { withFileTypes: true, recursive: true }) return entries .filter((e) => e.isFile() && e.name.endsWith('.ts')) .map((e) => join(e.parentPath ?? e.path, e.name)) } function toExportName(filePath) { const rel = relative(TYPES_DIR, filePath).replace(/\.ts$/, '') return rel .split('/') .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) .join('') } async function buildReplacementIndex(typeFiles) { const exports = typeFiles .map((file) => { const name = toExportName(file) const relPath = './' + relative(GENERATED_DIR, file).replace(/\.ts$/, '.js') return `export * as ${name} from "${relPath}";` }) .sort() return `/** * GENERATED CODE - Re-exports only. * The XRPC server/client wrappers generated by lex-cli are replaced with * direct type re-exports since we use @atproto/api for PDS interactions. */ ${exports.join('\n')} export { schemas, validate } from "./lexicons.js"; ` } async function fixImportExtensions(filePath) { let content = await readFile(filePath, 'utf-8') const original = content content = content.replace(/from '(\.\.?\/[^']+?)(?', `${schemaDictEntry}} as const satisfies Record` ) // Insert into ids (before the closing `} as const`) // Find the ids block and insert alphabetically const idsEntry = ` ${dictKey}: '${lexiconJson.id}',\n` const idsMatch = content.match(/export const ids = \{[\s\S]*?\} as const/) if (idsMatch && !idsMatch[0].includes(dictKey)) { content = content.replace(/(\} as const)$/m, `${idsEntry}$1`) } console.log(`Injected excluded lexicon: ${dictKey} (${lexiconJson.id})`) } await writeFile(lexiconsFile, content) } async function main() { const typeFiles = await getTypeFiles(TYPES_DIR) const indexContent = await buildReplacementIndex(typeFiles) await writeFile(join(GENERATED_DIR, 'index.ts'), indexContent) for (const file of typeFiles) { await fixImportExtensions(file) } await fixImportExtensions(join(GENERATED_DIR, 'lexicons.ts')) await fixImportExtensions(join(GENERATED_DIR, 'util.ts')) // Inject lexicons that lex-cli can't process await injectExcludedLexicons(join(GENERATED_DIR, 'lexicons.ts')) console.log( `Fixed ${typeFiles.length + 2} generated files (${typeFiles.length} types + lexicons.ts + util.ts)` ) } main()