WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at main 177 lines 5.9 kB view raw
1#!/usr/bin/env tsx 2/** 3 * Post-processing script to add missing @atproto/api imports to generated TypeScript. 4 * 5 * ## Why This Exists 6 * 7 * The @atproto/lex-cli generator creates TypeScript client code that references 8 * standard AT Protocol namespace types (ComAtprotoRepoListRecords, ComAtprotoRepoGetRecord, etc.) 9 * but doesn't import them. This is expected behavior - the generated clients are meant 10 * to be consumed alongside @atproto/api which provides these types. 11 * 12 * We add the missing imports automatically as a post-generation step to keep the 13 * build clean without modifying the upstream generator. 14 * 15 * ## When This Breaks 16 * 17 * If @atproto/lex-cli changes its output format (whitespace, quotes, import paths), 18 * the regex pattern may need updates. The script will fail with a clear error message 19 * indicating which pattern needs adjustment. 20 * 21 * ## Maintenance 22 * 23 * - Update REQUIRED_IMPORTS if @atproto/api adds/removes namespace types 24 * - Update ANCHOR_IMPORT_REGEX if lex-cli changes import format 25 * - Run tests after updates: pnpm --filter @atbb/lexicon test scripts/__tests__/ 26 */ 27import { readFile, writeFile } from "node:fs/promises"; 28import { join } from "node:path"; 29 30/** Types we inject from @atproto/api */ 31const REQUIRED_IMPORTS = [ 32 'ComAtprotoRepoListRecords', 33 'ComAtprotoRepoGetRecord', 34 'ComAtprotoRepoCreateRecord', 35 'ComAtprotoRepoPutRecord', 36 'ComAtprotoRepoDeleteRecord', 37] as const; 38 39/** Pattern to find the anchor import we inject after */ 40const ANCHOR_IMPORT_REGEX = /import\s+\*\s+as\s+ComAtprotoRepoStrongRef\s+from\s+['"]\.\/types\/com\/atproto\/repo\/strongRef(?:\.js)?['"]/; 41 42/** 43 * Check if all required imports are already present (idempotent check). 44 * Only returns true if ALL specific types we need are present. 45 */ 46function hasAllRequiredImports(content: string): boolean { 47 return REQUIRED_IMPORTS.every(typeName => { 48 // Look for "type TypeName" in imports to avoid false positives 49 // from other @atproto/api imports that might be added in future 50 const pattern = new RegExp(`type\\s+${typeName}\\b`); 51 return pattern.test(content); 52 }); 53} 54 55/** 56 * Generate the import statement to inject. 57 */ 58function generateImportStatement(): string { 59 const imports = REQUIRED_IMPORTS.map(name => ` type ${name},`).join('\n'); 60 return `import {\n${imports}\n} from '@atproto/api'`; 61} 62 63async function fixGeneratedIndex(customPath?: string): Promise<void> { 64 const indexPath = customPath || join(process.cwd(), "dist/types/index.ts"); 65 66 // Read file with specific error handling 67 let content: string; 68 try { 69 content = await readFile(indexPath, "utf-8"); 70 } catch (error) { 71 if (error instanceof Error && 'code' in error) { 72 const nodeError = error as NodeJS.ErrnoException; 73 74 if (nodeError.code === 'ENOENT') { 75 throw new Error( 76 `Generated index file not found at: ${indexPath}\n` + 77 `Run 'pnpm --filter @atbb/lexicon build:types' to generate it first.` 78 ); 79 } 80 81 if (nodeError.code === 'EACCES') { 82 throw new Error( 83 `Permission denied reading ${indexPath}.\n` + 84 `Check file permissions and ensure you have read access.` 85 ); 86 } 87 } 88 89 throw new Error( 90 `Failed to read ${indexPath}: ${error instanceof Error ? error.message : String(error)}` 91 ); 92 } 93 94 // Check if imports are already present (idempotent) 95 if (hasAllRequiredImports(content)) { 96 console.log("Generated types already have all required @atproto/api imports"); 97 return; 98 } 99 100 // Find the anchor import line to inject after 101 const match = content.match(ANCHOR_IMPORT_REGEX); 102 103 if (!match) { 104 throw new Error( 105 `Could not find expected ComAtprotoRepoStrongRef import.\n` + 106 `This suggests @atproto/lex-cli changed its output format.\n` + 107 `Searched for pattern: ${ANCHOR_IMPORT_REGEX.source}\n` + 108 `Update ANCHOR_IMPORT_REGEX in fix-generated-types.ts to match the new format.` 109 ); 110 } 111 112 const anchorLine = match[0]; 113 const importStatement = generateImportStatement(); 114 115 // Inject imports after anchor line 116 const fixed = content.replace(anchorLine, `${anchorLine}\n${importStatement}`); 117 118 // Validate replacement worked 119 if (fixed === content) { 120 throw new Error( 121 `String replacement failed.\n` + 122 `Pattern matched but replace() didn't modify content.\n` + 123 `This is a bug in the script logic.` 124 ); 125 } 126 127 // Validate imports were actually added 128 if (!REQUIRED_IMPORTS.every(imp => fixed.includes(`type ${imp}`))) { 129 throw new Error( 130 `Import injection failed.\n` + 131 `Content was modified but required imports are missing.\n` + 132 `This is a bug in the script logic.` 133 ); 134 } 135 136 // Write file with specific error handling 137 try { 138 await writeFile(indexPath, fixed, "utf-8"); 139 } catch (error) { 140 if (error instanceof Error && 'code' in error) { 141 const nodeError = error as NodeJS.ErrnoException; 142 143 if (nodeError.code === 'EACCES') { 144 throw new Error( 145 `Permission denied writing ${indexPath}.\n` + 146 `Check file permissions and ensure you have write access.` 147 ); 148 } 149 150 if (nodeError.code === 'ENOSPC') { 151 throw new Error( 152 `No space left on device writing ${indexPath}.\n` + 153 `Free up disk space and try again.` 154 ); 155 } 156 } 157 158 throw new Error( 159 `Failed to write ${indexPath}: ${error instanceof Error ? error.message : String(error)}` 160 ); 161 } 162 163 console.log("Added missing @atproto/api imports to generated types"); 164} 165 166async function main() { 167 try { 168 // Allow passing custom path via command line for testing 169 const customPath = process.argv[2]; 170 await fixGeneratedIndex(customPath); 171 } catch (error) { 172 console.error("Failed to fix generated types:", error instanceof Error ? error.message : String(error)); 173 process.exit(1); 174 } 175} 176 177main();