mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
- fs.watch's async 'error' event had no listener, crashing the preview
server on EMFILE (exhausted OS watch handles)
- moveKeyframeInScript/resizeKeyframedTweenInScript/removeAllKeyframesFromScript
required object-form keyframes: {"0%": {...}}, silently no-opping on
array-form keyframes: [{...}, {...}]
- a keyframe diamond click's auto-synthesized native click event bubbled
to the ancestor clip's onClick, which toggles selection off when the
clip is already selected (the state every diamond click happens in)
- the clip's trim-resize handles (z-index 4) visually and functionally
covered any keyframe diamond within their 14px edge strip
- synthesizeFlatTweenKeyframes didn't recognize a collapsed
duration:0 + immediateRender static hold (what remove-all-keyframes
produces) as non-animated, so it kept showing a phantom diamond after
Delete All Keyframes
- resolveMediaStartSeconds's fast path for elements with their own
data-start discarded the host composition's inherited start offset,
so a video nested inside a sub-composition played from the root
timeline's time instead of holding until its parent scene began
Fixes #1838
77 lines
2.1 KiB
TypeScript
77 lines
2.1 KiB
TypeScript
import { watch, type FSWatcher } from "node:fs";
|
|
|
|
export type FileChangeListener = (relativePath: string) => void;
|
|
|
|
export interface ProjectWatcher {
|
|
addListener(fn: FileChangeListener): void;
|
|
removeListener(fn: FileChangeListener): void;
|
|
close(): void;
|
|
}
|
|
|
|
const WATCHER_EXCLUDED_DIRS = new Set([
|
|
".cache",
|
|
".git",
|
|
".hyperframes",
|
|
".next",
|
|
".vite",
|
|
"build",
|
|
"coverage",
|
|
"dist",
|
|
"node_modules",
|
|
"outputs",
|
|
"renders",
|
|
]);
|
|
const DEBOUNCE_MS = 300;
|
|
|
|
export function shouldWatchProjectFile(filename: string): boolean {
|
|
if (!filename) return false;
|
|
const parts = filename.split(/[\\/]+/);
|
|
return !parts.some((part) => WATCHER_EXCLUDED_DIRS.has(part));
|
|
}
|
|
|
|
export function createProjectWatcher(projectDir: string): ProjectWatcher {
|
|
const listeners = new Set<FileChangeListener>();
|
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let watcher: FSWatcher | null = null;
|
|
|
|
try {
|
|
watcher = watch(projectDir, { recursive: true }, (_event, filename) => {
|
|
if (!filename) return;
|
|
const relativePath = filename.toString();
|
|
if (!shouldWatchProjectFile(relativePath)) return;
|
|
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(() => {
|
|
for (const fn of listeners) {
|
|
fn(relativePath);
|
|
}
|
|
}, DEBOUNCE_MS);
|
|
});
|
|
// fs.watch can fail asynchronously too (e.g. EMFILE from exhausted OS watch
|
|
// handles) — that surfaces as an 'error' event, not a thrown exception. An
|
|
// EventEmitter 'error' with no listener crashes the whole process, so this
|
|
// listener is required for the same "degrade gracefully" the catch below
|
|
// already promises for the synchronous failure mode.
|
|
watcher.on("error", () => {
|
|
watcher?.close();
|
|
watcher = null;
|
|
});
|
|
} catch {
|
|
// fs.watch may fail on some platforms — degrade gracefully (no auto-refresh)
|
|
}
|
|
|
|
return {
|
|
addListener(fn) {
|
|
listeners.add(fn);
|
|
},
|
|
removeListener(fn) {
|
|
listeners.delete(fn);
|
|
},
|
|
close() {
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
watcher?.close();
|
|
listeners.clear();
|
|
},
|
|
};
|
|
}
|