[READ-ONLY] Mirror of https://github.com/danielroe/roe.dev. This is the code and content for my personal website, built in Nuxt. roe.dev
0

Configure Feed

Select the types of activity you want to include in your feed.

roe.dev / scripts / gen-lex.mjs
4.7 kB 131 lines
1#!/usr/bin/env node 2/** 3 * Regenerate TS types from the lexicon JSON files under `lexicons/`. 4 * 5 * The generated `shared/lex/index.ts` (full XRPC client surface) is removed 6 * after generation because we drive the PDS via `AtpAgent` from 7 * `@atproto/api` and only need the per-record types and the validator 8 * registry. A hand-written barrel is restored from this script. 9 */ 10import { spawn } from 'node:child_process' 11import { readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' 12import { join, resolve } from 'node:path' 13import { fileURLToPath } from 'node:url' 14 15const here = fileURLToPath(new URL('.', import.meta.url)) 16const root = resolve(here, '..') 17const lexRoot = join(root, 'lexicons') 18const outDir = join(root, 'shared/lex') 19 20const lexiconFiles = [] 21function walk (dir) { 22 for (const entry of readdirSync(dir, { withFileTypes: true })) { 23 const full = join(dir, entry.name) 24 if (entry.isDirectory()) walk(full) 25 else if (entry.isFile() && entry.name.endsWith('.json')) lexiconFiles.push(full) 26 } 27} 28walk(lexRoot) 29 30if (!lexiconFiles.length) { 31 console.error('No lexicon JSON files found under', lexRoot) 32 process.exit(1) 33} 34 35console.log(`Generating types for ${lexiconFiles.length} lexicons → ${outDir}`) 36 37/** 38 * `@atproto/lex-cli gen-api` prompts to overwrite each generated file. We 39 * pipe a stream of `y\n` into its stdin instead of shelling out to 40 * `yes y | …`, so lexicon paths containing spaces or shell metacharacters 41 * can't break (or be abused to alter) the command line. 42 */ 43await new Promise((resolvePromise, reject) => { 44 const child = spawn( 45 'pnpm', 46 ['dlx', '@atproto/lex-cli', 'gen-api', outDir, ...lexiconFiles], 47 { stdio: ['pipe', 'inherit', 'inherit'], cwd: root }, 48 ) 49 const writeY = () => { 50 while (child.stdin.writable && child.stdin.write('y\n')) { 51 // keep filling the buffer until the kernel says back off 52 } 53 } 54 child.stdin.on('drain', writeY) 55 child.stdin.on('error', () => {}) 56 writeY() 57 child.on('error', reject) 58 child.on('exit', code => { 59 child.stdin.end() 60 if (code === 0) resolvePromise() 61 else reject(new Error(`lex-cli exited with code ${code}`)) 62 }) 63}) 64 65/** 66 * Rewrite the generated relative-imports from `.js` to `.ts` so Node’s native 67 * type-stripping (`node script.ts`) can resolve them at runtime without a 68 * loader. TypeScript is configured with `allowImportingTsExtensions` to 69 * accept the same shape at type-check time. 70 */ 71function rewriteJsToTs (dir) { 72 for (const entry of readdirSync(dir, { withFileTypes: true })) { 73 const full = join(dir, entry.name) 74 if (entry.isDirectory()) { 75 rewriteJsToTs(full) 76 continue 77 } 78 if (!entry.isFile() || !entry.name.endsWith('.ts')) continue 79 const src = readFileSync(full, 'utf8') 80 const next = src.replace(/((?:from|import)\s+['"])((?:\.{1,2}\/)[^'"]+?)\.js(['"])/g, '$1$2.ts$3') 81 if (next !== src) writeFileSync(full, next) 82 } 83} 84rewriteJsToTs(outDir) 85 86const generatedClient = join(outDir, 'index.ts') 87rmSync(generatedClient, { force: true }) 88 89/** 90 * Build the namespace-export lines by scanning the generated `types/` 91 * tree, so adding a new lexicon JSON doesn't need a matching edit to this 92 * script. 93 */ 94function collectNamespaceExports (dir, prefix = '') { 95 const lines = [] 96 for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { 97 const full = join(dir, entry.name) 98 if (entry.isDirectory()) { 99 lines.push(...collectNamespaceExports(full, `${prefix}${capitalise(entry.name)}`)) 100 continue 101 } 102 if (!entry.isFile() || !entry.name.endsWith('.ts')) continue 103 const base = entry.name.replace(/\.ts$/, '') 104 const namespace = `${prefix}${capitalise(base)}` 105 const relPath = `./types/${full.slice(typesRoot.length + 1).replace(/\\/g, '/')}` 106 lines.push(`export * as ${namespace} from '${relPath}'`) 107 } 108 return lines 109} 110function capitalise (s) { 111 return s.charAt(0).toUpperCase() + s.slice(1) 112} 113const typesRoot = join(outDir, 'types') 114const namespaceLines = collectNamespaceExports(typesRoot).join('\n') 115 116const barrel = `/** 117 * Auto-generated barrel for the dev.roe lexicons. Re-run \`pnpm lex:gen\` to 118 * refresh after adding or removing lexicon JSON files. 119 * 120 * The generated \`gen-api\` client surface was deleted because we drive the PDS 121 * via the existing \`AtpAgent\` from \`@atproto/api\`; we only need the typed 122 * record interfaces and the validator registry. 123 */ 124export { schemas, lexicons, validate, schemaDict } from './lexicons.ts' 125export type { $Typed, Un$Typed, OmitKey } from './util.ts' 126 127${namespaceLines} 128` 129writeFileSync(generatedClient, barrel) 130 131console.log('Done. Barrel written to', generatedClient)