import fs from "node:fs"; import path from "node:path"; import { parse } from "@typescript-eslint/parser"; /** Unwrap `as`/`satisfies` expressions, return a string literal's value. */ function stringFromNode(node) { if (!node) return null; if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") { return stringFromNode(node.expression); } if (node.type === "Literal" && typeof node.value === "string") { return node.value; } return null; } /** * Extract the event `name` from an event-descriptor initializer. Handles the * two shapes the codebase and `pnpm turbo gen event` produce: * - `defineEvent("feature.event", schema)` — the core-events helper * - `{ name: "feature.event" as const, schema }` — the inline descriptor * (used when core-events is not installed) */ function nameFromInit(init) { if (!init) return null; if (init.type === "TSAsExpression" || init.type === "TSSatisfiesExpression") { return nameFromInit(init.expression); } if ( init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "defineEvent" && init.arguments.length > 0 ) { return stringFromNode(init.arguments[0]); } if (init.type === "ObjectExpression") { const nameProp = init.properties.find( (p) => p.type === "Property" && p.key.type === "Identifier" && p.key.name === "name", ); return nameProp ? stringFromNode(nameProp.value) : null; } return null; } /** * Resolve a relative import source (evaluated from `fromFile`) to a `.ts` * file on disk. Returns null for bare/package specifiers or paths that don't * resolve — callers treat null as "can't analyse, skip". */ export function resolveRelativeImport(source, fromFile) { if (typeof source !== "string" || !source.startsWith(".")) return null; const base = path.resolve(path.dirname(fromFile), source); for (const candidate of [`${base}.ts`, path.join(base, "index.ts")]) { if (fs.existsSync(candidate)) return candidate; } return null; } /** * Read an event-contract file and return the `name` of the event descriptor * exported under `exportName`. Returns null when the file can't be read or * parsed, or doesn't export a recognised descriptor under that name. */ export function eventNameFromFile(filePath, exportName) { let source; try { source = fs.readFileSync(filePath, "utf8"); } catch { return null; } let ast; try { ast = parse(source, { loc: false, range: false }); } catch { return null; } for (const stmt of ast.body) { let varDecl = null; if ( stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "VariableDeclaration" ) { varDecl = stmt.declaration; } else if (stmt.type === "VariableDeclaration") { varDecl = stmt; } if (!varDecl) continue; for (const d of varDecl.declarations) { if (d.id.type === "Identifier" && d.id.name === exportName) { return nameFromInit(d.init); } } } return null; }