UNPKG

@t1mmen/srtd

Version:

Supabase Repeatable Template Definitions (srtd): 🪄 Live-reloading SQL templates for Supabase DX. Make your database changes reviewable and migrations maintainable! 🚀

126 lines • 5.24 kB
// src/hooks/useTemplateManager.ts import { useEffect, useMemo, useRef, useState } from 'react'; import { TemplateManager } from '../lib/templateManager.js'; import { getConfig } from '../utils/config.js'; import { findProjectRoot } from '../utils/findProjectRoot.js'; export function useTemplateManager(baseDir) { const [templates, setTemplates] = useState([]); const [updates, setUpdates] = useState([]); const [errors, setErrors] = useState(new Map()); const [isLoading, setIsLoading] = useState(true); const [templateDir, setTemplateDir] = useState(); const [latestPath, setLatestPath] = useState(); const managerRef = useRef(); const sortedTemplates = useMemo(() => [...templates].sort((a, b) => { const dateA = a.buildState.lastAppliedDate || ''; const dateB = b.buildState.lastAppliedDate || ''; return dateA.localeCompare(dateB); }), [templates]); const stats = useMemo(() => ({ total: templates.length, needsBuild: templates.filter(t => !t.buildState.lastBuildDate || t.currentHash !== t.buildState.lastBuildHash).length, recentlyChanged: updates.filter(u => { const timestamp = new Date(u.timestamp); const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); return timestamp > fiveMinutesAgo; }).length, errors: errors.size, }), [templates, updates, errors]); useEffect(() => { let mounted = true; async function init() { try { const cwd = baseDir || (await findProjectRoot()); const config = await getConfig(cwd); setTemplateDir(config.templateDir); managerRef.current = await TemplateManager.create(cwd, { silent: true }); // Setup event handlers managerRef.current.on('templateChanged', template => { if (!mounted) return; setLatestPath(template.path); setUpdates(prev => [ { type: 'changed', template, timestamp: new Date().toISOString(), }, ...prev, ].slice(0, 50)); }); managerRef.current.on('templateApplied', template => { if (!mounted) return; setLatestPath(template.path); setTemplates(prev => { const rest = prev.filter(t => t.path !== template.path); return [...rest, template]; }); setUpdates(prev => [ { type: 'applied', template, timestamp: new Date().toISOString(), }, ...prev, ].slice(0, 50)); setErrors(prev => { const next = new Map(prev); next.delete(template.path); return next; }); }); managerRef.current.on('templateError', ({ template, error: err }) => { if (!mounted) return; // Ensure error is properly stringified const errorMsg = typeof err === 'object' ? err instanceof Error ? err.message : JSON.stringify(err) : String(err); setErrors(prev => new Map(prev).set(template.path, errorMsg)); setUpdates(prev => [ { type: 'error', template, timestamp: new Date().toISOString(), error: errorMsg, }, ...prev, ].slice(0, 50)); }); // Initial load const initialTemplates = await managerRef.current.findTemplates(); const statuses = await Promise.all(initialTemplates.map(t => managerRef.current?.getTemplateStatus(t))); if (mounted) { setTemplates(statuses.filter((s) => s !== null)); setIsLoading(false); } // Start watching await managerRef.current.watch(); } catch (err) { if (mounted) { setErrors(new Map().set('global', String(err))); setIsLoading(false); } } } void init(); return () => { mounted = false; managerRef.current?.[Symbol.dispose](); }; }, [baseDir]); return { templates: sortedTemplates, updates, stats, isLoading, errors, latestPath, templateDir, }; } //# sourceMappingURL=useTemplateManager.js.map