@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
JavaScript
// 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