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 root/atb-56-theme-caching-layer 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();