AT Protocol lexicon schemas for Sifa professional profiles (id.sifa.*)
sifa.id/
1/**
2 * Post-generation fixup for lex-cli output.
3 *
4 * 1. Replaces the generated index.ts with clean type re-exports (only id.sifa.* types)
5 * 2. Fixes missing .js import extensions for NodeNext compatibility
6 * 3. Injects the authProfileAccess lexicon into schemaDict/ids
7 */
8import { readdir, readFile, writeFile } from 'node:fs/promises';
9import { join, relative } from 'node:path';
10
11const GENERATED_DIR = new URL('../src/generated', import.meta.url).pathname;
12const TYPES_DIR = join(GENERATED_DIR, 'types');
13const LEXICONS_DIR = new URL('../lexicons', import.meta.url).pathname;
14
15const EXCLUDED_LEXICONS = [
16 { file: 'id/sifa/authProfileAccess.json', dictKey: 'IdSifaAuthProfileAccess' },
17];
18
19async function getTypeFiles(dir) {
20 const entries = await readdir(dir, { withFileTypes: true, recursive: true });
21 return entries
22 .filter((e) => e.isFile() && e.name.endsWith('.ts'))
23 .map((e) => join(e.parentPath ?? e.path, e.name));
24}
25
26function toExportName(filePath) {
27 const rel = relative(TYPES_DIR, filePath).replace(/\.ts$/, '');
28 return rel
29 .split('/')
30 .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
31 .join('');
32}
33
34async function buildReplacementIndex(typeFiles) {
35 // Only export id.sifa.* types (not com.atproto.* or community.lexicon.*)
36 const sifaFiles = typeFiles.filter((f) => {
37 const rel = relative(TYPES_DIR, f);
38 return rel.startsWith('id/sifa/');
39 });
40
41 const exports = sifaFiles
42 .map((file) => {
43 const name = toExportName(file);
44 const relPath = './' + relative(GENERATED_DIR, file).replace(/\.ts$/, '.js');
45 return `export * as ${name} from "${relPath}";`;
46 })
47 .sort();
48
49 return `/**
50 * GENERATED CODE - Re-exports only.
51 * Only id.sifa.* types are exported. External types (com.atproto.*, community.lexicon.*)
52 * are included in generation for reference resolution but not re-exported.
53 */
54${exports.join('\n')}
55export { schemas, validate } from "./lexicons.js";
56`;
57}
58
59async function fixImportExtensions(filePath) {
60 let content = await readFile(filePath, 'utf-8');
61 const original = content;
62 content = content.replace(/from '(\.\.?\/[^']+?)(?<!\.js)'/g, "from '$1.js'");
63 if (content !== original) {
64 await writeFile(filePath, content);
65 }
66}
67
68async function injectExcludedLexicons(lexiconsFile) {
69 let content = await readFile(lexiconsFile, 'utf-8');
70
71 for (const { file, dictKey } of EXCLUDED_LEXICONS) {
72 if (content.includes(` ${dictKey}:`)) continue;
73
74 const lexiconJson = JSON.parse(await readFile(join(LEXICONS_DIR, file), 'utf-8'));
75
76 const schemaDictEntry = ` ${dictKey}: ${JSON.stringify(lexiconJson, null, 4).replace(/\n/g, '\n ')},\n`;
77 content = content.replace(
78 '} as const satisfies Record<string, LexiconDoc>',
79 `${schemaDictEntry}} as const satisfies Record<string, LexiconDoc>`,
80 );
81
82 const idsEntry = ` ${dictKey}: '${lexiconJson.id}',\n`;
83 const idsMatch = content.match(/export const ids = \{[\s\S]*?\} as const/);
84 if (idsMatch && !idsMatch[0].includes(dictKey)) {
85 content = content.replace(/(\} as const)$/m, `${idsEntry}$1`);
86 }
87
88 console.log(`Injected excluded lexicon: ${dictKey} (${lexiconJson.id})`);
89 }
90
91 await writeFile(lexiconsFile, content);
92}
93
94async function main() {
95 const typeFiles = await getTypeFiles(TYPES_DIR);
96 const indexContent = await buildReplacementIndex(typeFiles);
97 await writeFile(join(GENERATED_DIR, 'index.ts'), indexContent);
98
99 for (const file of typeFiles) {
100 await fixImportExtensions(file);
101 }
102
103 await fixImportExtensions(join(GENERATED_DIR, 'lexicons.ts'));
104 await fixImportExtensions(join(GENERATED_DIR, 'util.ts'));
105
106 await injectExcludedLexicons(join(GENERATED_DIR, 'lexicons.ts'));
107
108 const sifaFiles = typeFiles.filter((f) => relative(TYPES_DIR, f).startsWith('id/sifa/'));
109 console.log(
110 `Fixed ${typeFiles.length + 2} generated files (${sifaFiles.length} sifa types exported, ${typeFiles.length - sifaFiles.length} external types included for resolution)`,
111 );
112}
113
114main();