#!/usr/bin/env tsx /** * Post-processing script to add missing @atproto/api imports to generated TypeScript. * * ## Why This Exists * * The @atproto/lex-cli generator creates TypeScript client code that references * standard AT Protocol namespace types (ComAtprotoRepoListRecords, ComAtprotoRepoGetRecord, etc.) * but doesn't import them. This is expected behavior - the generated clients are meant * to be consumed alongside @atproto/api which provides these types. * * We add the missing imports automatically as a post-generation step to keep the * build clean without modifying the upstream generator. * * ## When This Breaks * * If @atproto/lex-cli changes its output format (whitespace, quotes, import paths), * the regex pattern may need updates. The script will fail with a clear error message * indicating which pattern needs adjustment. * * ## Maintenance * * - Update REQUIRED_IMPORTS if @atproto/api adds/removes namespace types * - Update ANCHOR_IMPORT_REGEX if lex-cli changes import format * - Run tests after updates: pnpm --filter @atbb/lexicon test scripts/__tests__/ */ import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; /** Types we inject from @atproto/api */ const REQUIRED_IMPORTS = [ 'ComAtprotoRepoListRecords', 'ComAtprotoRepoGetRecord', 'ComAtprotoRepoCreateRecord', 'ComAtprotoRepoPutRecord', 'ComAtprotoRepoDeleteRecord', ] as const; /** Pattern to find the anchor import we inject after */ const ANCHOR_IMPORT_REGEX = /import\s+\*\s+as\s+ComAtprotoRepoStrongRef\s+from\s+['"]\.\/types\/com\/atproto\/repo\/strongRef(?:\.js)?['"]/; /** * Check if all required imports are already present (idempotent check). * Only returns true if ALL specific types we need are present. */ function hasAllRequiredImports(content: string): boolean { return REQUIRED_IMPORTS.every(typeName => { // Look for "type TypeName" in imports to avoid false positives // from other @atproto/api imports that might be added in future const pattern = new RegExp(`type\\s+${typeName}\\b`); return pattern.test(content); }); } /** * Generate the import statement to inject. */ function generateImportStatement(): string { const imports = REQUIRED_IMPORTS.map(name => ` type ${name},`).join('\n'); return `import {\n${imports}\n} from '@atproto/api'`; } async function fixGeneratedIndex(customPath?: string): Promise { const indexPath = customPath || join(process.cwd(), "dist/types/index.ts"); // Read file with specific error handling let content: string; try { content = await readFile(indexPath, "utf-8"); } catch (error) { if (error instanceof Error && 'code' in error) { const nodeError = error as NodeJS.ErrnoException; if (nodeError.code === 'ENOENT') { throw new Error( `Generated index file not found at: ${indexPath}\n` + `Run 'pnpm --filter @atbb/lexicon build:types' to generate it first.` ); } if (nodeError.code === 'EACCES') { throw new Error( `Permission denied reading ${indexPath}.\n` + `Check file permissions and ensure you have read access.` ); } } throw new Error( `Failed to read ${indexPath}: ${error instanceof Error ? error.message : String(error)}` ); } // Check if imports are already present (idempotent) if (hasAllRequiredImports(content)) { console.log("Generated types already have all required @atproto/api imports"); return; } // Find the anchor import line to inject after const match = content.match(ANCHOR_IMPORT_REGEX); if (!match) { throw new Error( `Could not find expected ComAtprotoRepoStrongRef import.\n` + `This suggests @atproto/lex-cli changed its output format.\n` + `Searched for pattern: ${ANCHOR_IMPORT_REGEX.source}\n` + `Update ANCHOR_IMPORT_REGEX in fix-generated-types.ts to match the new format.` ); } const anchorLine = match[0]; const importStatement = generateImportStatement(); // Inject imports after anchor line const fixed = content.replace(anchorLine, `${anchorLine}\n${importStatement}`); // Validate replacement worked if (fixed === content) { throw new Error( `String replacement failed.\n` + `Pattern matched but replace() didn't modify content.\n` + `This is a bug in the script logic.` ); } // Validate imports were actually added if (!REQUIRED_IMPORTS.every(imp => fixed.includes(`type ${imp}`))) { throw new Error( `Import injection failed.\n` + `Content was modified but required imports are missing.\n` + `This is a bug in the script logic.` ); } // Write file with specific error handling try { await writeFile(indexPath, fixed, "utf-8"); } catch (error) { if (error instanceof Error && 'code' in error) { const nodeError = error as NodeJS.ErrnoException; if (nodeError.code === 'EACCES') { throw new Error( `Permission denied writing ${indexPath}.\n` + `Check file permissions and ensure you have write access.` ); } if (nodeError.code === 'ENOSPC') { throw new Error( `No space left on device writing ${indexPath}.\n` + `Free up disk space and try again.` ); } } throw new Error( `Failed to write ${indexPath}: ${error instanceof Error ? error.message : String(error)}` ); } console.log("Added missing @atproto/api imports to generated types"); } async function main() { try { // Allow passing custom path via command line for testing const customPath = process.argv[2]; await fixGeneratedIndex(customPath); } catch (error) { console.error("Failed to fix generated types:", error instanceof Error ? error.message : String(error)); process.exit(1); } } main();