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
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();