/** * Helpers for finding an {@link XOTemplate} export in a loaded ES module. */ /** * Returns true when `value` looks like an XOTemplate object (pre-schema check). * Used only to pick the correct export before {@link parseTemplate} validates fully. */ export function isTemplateLike( value: unknown, ): value is Record { if (value === null || typeof value !== "object" || Array.isArray(value)) { return false; } const candidate = value as Record; return ( typeof candidate.$schema === "string" && typeof candidate.name === "string" && typeof candidate.roles === "object" && candidate.roles !== null ); } /** * Picks the single XOTemplate export from a dynamically loaded module. * * Resolution order: * 1. `default` export, when template-like * 2. Exactly one named template-like export * * @throws When no template export exists or multiple template exports are found. */ export function pickTemplateExport( moduleExports: Record, ): Record { const defaultExport = moduleExports.default; if (isTemplateLike(defaultExport)) { return defaultExport; } const namedTemplateExports = Object.entries(moduleExports).filter( ([exportName, exportValue]) => exportName !== "default" && isTemplateLike(exportValue), ); if (namedTemplateExports.length === 1) { const [, exportValue] = namedTemplateExports[0]!; if (!isTemplateLike(exportValue)) { throw new Error("No XOTemplate export found."); } return exportValue; } if (namedTemplateExports.length > 1) { const exportNames = namedTemplateExports.map(([name]) => name).join(", "); throw new Error( `Multiple template exports found (${exportNames}). ` + "Use a single named export or a default export.", ); } throw new Error( "No XOTemplate export found. Export a template object as `default` or a named export.", ); }