From 6f76b306751fdab465d9c6702228b119edd8ce30 Mon Sep 17 00:00:00 2001 From: Xuanru Li <157947275+xuanruli@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:01:05 +0000 Subject: [PATCH] fix(inspector): stop audio waveform caches from making every live question stale (#5536) * fix(inspector): stop audio waveform caches from making every live question stale * fix(inspector): drop the comment and format the test * refactor(core): share the folders Studio's preview ignores * fix(core): register the project-folders subpath and bundle it into the inspector --- packages/core/package-subpaths.json | 6 ++++++ packages/core/package.json | 10 +++++++++ packages/core/src/projectFolders.ts | 17 +++++++++++++++ packages/inspector/src/sources.ts | 13 ++---------- packages/inspector/tests/sources.test.ts | 20 +++++++++++++++++- .../src/helpers/projectSignature.ts | 21 +++---------------- 6 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 packages/core/src/projectFolders.ts diff --git a/packages/core/package-subpaths.json b/packages/core/package-subpaths.json index bc58a5c24a..84b0e4c070 100644 --- a/packages/core/package-subpaths.json +++ b/packages/core/package-subpaths.json @@ -56,6 +56,12 @@ "types": "./dist/atomicFile.d.ts", "environments": ["bun", "node"] }, + "./project-folders": { + "source": "./src/projectFolders.ts", + "runtime": "./dist/projectFolders.js", + "types": "./dist/projectFolders.d.ts", + "environments": ["bun", "node"] + }, "./asset-paths": { "source": "./src/compiler/rewriteSubCompPaths.ts", "runtime": "./dist/compiler/rewriteSubCompPaths.js", diff --git a/packages/core/package.json b/packages/core/package.json index 4e2e1927a4..63268946f4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -68,6 +68,12 @@ "import": "./src/atomicFile.ts", "types": "./src/atomicFile.ts" }, + "./project-folders": { + "bun": "./src/projectFolders.ts", + "node": "./dist/projectFolders.js", + "import": "./src/projectFolders.ts", + "types": "./src/projectFolders.ts" + }, "./asset-paths": { "bun": "./src/compiler/rewriteSubCompPaths.ts", "node": "./dist/compiler/rewriteSubCompPaths.js", @@ -564,6 +570,10 @@ "import": "./dist/atomicFile.js", "types": "./dist/atomicFile.d.ts" }, + "./project-folders": { + "import": "./dist/projectFolders.js", + "types": "./dist/projectFolders.d.ts" + }, "./asset-paths": { "import": "./dist/compiler/rewriteSubCompPaths.js", "types": "./dist/compiler/rewriteSubCompPaths.d.ts" diff --git a/packages/core/src/projectFolders.ts b/packages/core/src/projectFolders.ts new file mode 100644 index 0000000000..10c5bd15dd --- /dev/null +++ b/packages/core/src/projectFolders.ts @@ -0,0 +1,17 @@ +/** Folders whose writes never reload Studio's preview, so nothing that judges a preview stale may count them. */ +export const PREVIEW_IGNORED_DIRS: ReadonlySet = new Set([ + ".cache", + ".git", + ".hyperframes", + ".next", + ".thumbnails", + ".transcode-cache", + ".vite", + ".waveform-cache", + "build", + "coverage", + "dist", + "node_modules", + "outputs", + "renders", +]); diff --git a/packages/inspector/src/sources.ts b/packages/inspector/src/sources.ts index 23f8ef5ea3..b8971e02b0 100644 --- a/packages/inspector/src/sources.ts +++ b/packages/inspector/src/sources.ts @@ -7,6 +7,7 @@ import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping"; import type { BundleScriptProvenance } from "../../core/src/compiler/htmlBundler"; import type { ElementRef, Snippet } from "./types"; import { sourceDependencies, type SourceRange } from "./sourceDependencies"; +import { PREVIEW_IGNORED_DIRS } from "../../core/src/projectFolders"; interface Chunk { file: string; @@ -40,17 +41,7 @@ function occurrences(text: string, needle: string) { } return hits; } -const excluded = new Set([ - "node_modules", - ".git", - ".hyperframes", - ".agents", - ".claude", - "evidence", - "outputs", - "renders", - "snapshots", -]); +const excluded = new Set([...PREVIEW_IGNORED_DIRS, ".agents", ".claude", "evidence", "snapshots"]); function sourceFiles(project: string) { const files: string[] = []; function visit(dir: string) { diff --git a/packages/inspector/tests/sources.test.ts b/packages/inspector/tests/sources.test.ts index 59a5d0d6ad..92bcac5d77 100644 --- a/packages/inspector/tests/sources.test.ts +++ b/packages/inspector/tests/sources.test.ts @@ -1,14 +1,32 @@ import { describe, it, expect } from "vitest"; -import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { mkdirSync, mkdtempSync, writeFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Sources, mergeSnippets, fingerprint } from "../src/sources"; +import { PREVIEW_IGNORED_DIRS } from "../../core/src/projectFolders"; import { bundleToSingleHtml, type BundleScriptProvenance, } from "../../core/src/compiler/htmlBundler"; describe("source records", () => { + it("ignores the generated folders Studio's preview signature ignores", () => { + const dir = mkdtempSync(join(tmpdir(), "inspect-source-")); + try { + writeFileSync(join(dir, "index.html"), "
"); + const before = fingerprint(dir); + for (const folder of PREVIEW_IGNORED_DIRS) { + mkdirSync(join(dir, folder)); + writeFileSync(join(dir, folder, "peaks.json"), "[1,2,3]"); + } + expect(fingerprint(dir)).toBe(before); + writeFileSync(join(dir, "data.json"), "{}"); + expect(fingerprint(dir)).not.toBe(before); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("keeps original multiline blocks and deduplicates targets without reformatting", () => { const dir = mkdtempSync(join(tmpdir(), "inspect-source-")); try { diff --git a/packages/studio-server/src/helpers/projectSignature.ts b/packages/studio-server/src/helpers/projectSignature.ts index da440d4027..4201592416 100644 --- a/packages/studio-server/src/helpers/projectSignature.ts +++ b/packages/studio-server/src/helpers/projectSignature.ts @@ -3,6 +3,7 @@ import { createHash } from "node:crypto"; import { lstatSync, readFileSync, readdirSync, type Stats } from "node:fs"; import { extname, isAbsolute, relative, resolve, sep } from "node:path"; import { isAtomicTempPath } from "@hyperframes/core/atomic-file"; +import { PREVIEW_IGNORED_DIRS } from "@hyperframes/core/project-folders"; import type { ResolvedProject, StudioApiAdapter } from "../types.js"; const SIGNATURE_TEXT_EXTENSIONS = new Set([ @@ -17,22 +18,6 @@ const SIGNATURE_TEXT_EXTENSIONS = new Set([ ".ts", ".tsx", ]); -const SIGNATURE_EXCLUDED_DIRS = new Set([ - ".cache", - ".git", - ".hyperframes", - ".next", - ".thumbnails", - ".transcode-cache", - ".vite", - ".waveform-cache", - "build", - "coverage", - "dist", - "node_modules", - "outputs", - "renders", -]); const MAX_SIGNATURE_TEXT_BYTES = 2_000_000; export const STUDIO_SIGNATURE_MANIFEST_PATHS = [ ".hyperframes/studio-manual-edits.json", @@ -72,7 +57,7 @@ export function affectsProjectSignature(projectDir: string, changedPath: string) if (isRenderOutput(projectDir, changedPath, readRenderOutputsOrNone(projectDir))) return false; const segments = relativePath.split(sep); if (STUDIO_SIGNATURE_MANIFEST_PATHS.includes(segments.join("/") as never)) return true; - return !segments.some((segment) => SIGNATURE_EXCLUDED_DIRS.has(segment)); + return !segments.some((segment) => PREVIEW_IGNORED_DIRS.has(segment)); } interface ProjectSignatureFile { @@ -106,7 +91,7 @@ function isTextContentEligible(file: string, size: number): boolean { } const isSkippedEntry = (entry: string) => - SIGNATURE_EXCLUDED_DIRS.has(entry) || isAtomicTempPath(entry); + PREVIEW_IGNORED_DIRS.has(entry) || isAtomicTempPath(entry); function signatureEntryStat( projectDir: string,