feat: cache shader transition preview frames (#634)

* feat: cache shader transition preview frames

* fix: move shader transition loading to player
This commit is contained in:
Vance Ingalls
2026-05-06 01:25:09 -07:00
committed by GitHub
parent 64457d9fe7
commit a7b308b667
23 changed files with 3475 additions and 243 deletions
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { shouldWatchProjectFile } from "./fileWatcher.js";
describe("shouldWatchProjectFile", () => {
it("watches files that can affect the project signature", () => {
expect(shouldWatchProjectFile("index.html")).toBe(true);
expect(shouldWatchProjectFile("src/scene.tsx")).toBe(true);
expect(shouldWatchProjectFile("assets/hero.png")).toBe(true);
expect(shouldWatchProjectFile("Dockerfile")).toBe(true);
});
it("skips generated and dependency directories excluded from signatures", () => {
expect(shouldWatchProjectFile("node_modules/pkg/index.js")).toBe(false);
expect(shouldWatchProjectFile("renders/output.mp4")).toBe(false);
expect(shouldWatchProjectFile("dist/index.html")).toBe(false);
expect(shouldWatchProjectFile(".hyperframes/cache.json")).toBe(false);
});
});
+22 -4
View File
@@ -8,9 +8,27 @@ export interface ProjectWatcher {
close(): void;
}
const WATCHED_EXTENSIONS = new Set([".html", ".css", ".js", ".json"]);
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;
@@ -19,13 +37,13 @@ export function createProjectWatcher(projectDir: string): ProjectWatcher {
try {
watcher = watch(projectDir, { recursive: true }, (_event, filename) => {
if (!filename) return;
const ext = "." + filename.split(".").pop()?.toLowerCase();
if (!WATCHED_EXTENSIONS.has(ext)) return;
const relativePath = filename.toString();
if (!shouldWatchProjectFile(relativePath)) return;
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
for (const fn of listeners) {
fn(filename);
fn(relativePath);
}
}, DEBOUNCE_MS);
});
+11
View File
@@ -14,6 +14,7 @@ import { loadRuntimeSource } from "./runtimeSource.js";
import { VERSION as version } from "../version.js";
import {
createStudioApi,
createProjectSignature,
getMimeType,
type StudioApiAdapter,
type ResolvedProject,
@@ -144,6 +145,10 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
// ── CLI adapter for the shared studio API ──────────────────────────────
const project: ResolvedProject = { id: projectId, dir: projectDir, title: projectId };
let cachedProjectSignature: string | null = null;
watcher.addListener(() => {
cachedProjectSignature = null;
});
const adapter: StudioApiAdapter = {
listProjects: () => [project],
@@ -169,6 +174,12 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
}
},
getProjectSignature(dir: string): string {
if (resolve(dir) !== resolve(projectDir)) return createProjectSignature(dir);
cachedProjectSignature ??= createProjectSignature(projectDir);
return cachedProjectSignature;
},
async lint(html: string, opts?: { filePath?: string }) {
const { lintHyperframeHtml } = await import("@hyperframes/core/lint");
return lintHyperframeHtml(html, opts);