From bbb691001dea8767449c9f589720cf86df7cc212 Mon Sep 17 00:00:00 2001 From: Xuanru Li <157947275+xuanruli@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:30:21 +0000 Subject: [PATCH] fix(inspector): component reports show the animation code inside large wrappers and only the film's own files (#5989) * fix(inspector): component reports show the animation code inside large wrappers and only the film's own files A caller frame whose statement is a large wrapper (an IIFE around a scene) is reported as its call line, so the merged snippet no longer swallows the creation inside it and outgrows maxBytes. Sources takes the files the inspected document was bundled from (BundleOptions.onRead), so mockups, studies and contact sheets in the project no longer add CSS candidates, script chunks or ambiguous tags. The headless service passes its bundle's reads; prepareLiveInspection takes them as an optional argument. * chore(inspector): satisfy source comment ratchet --- packages/inspector/src/live.ts | 3 +- packages/inspector/src/service.ts | 4 +- packages/inspector/src/sources.ts | 29 +++++++--- packages/inspector/tests/sources.test.ts | 68 ++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 8 deletions(-) diff --git a/packages/inspector/src/live.ts b/packages/inspector/src/live.ts index f36306d8f4..c6b017e11c 100644 --- a/packages/inspector/src/live.ts +++ b/packages/inspector/src/live.ts @@ -37,6 +37,7 @@ export function prepareLiveInspection( html: string, provenance: readonly BundleScriptProvenance[], sourceVersion: string, + reads?: readonly string[], ): LiveInspection { const project = realpathSync(projectDir); if (fingerprint(project) !== sourceVersion) throw new Error("source_changed_during_load"); @@ -55,7 +56,7 @@ export function prepareLiveInspection( pageGeneration: generation, sourceContext() { if (!context) { - const sources = new Sources(project); + const sources = new Sources(project, reads); context = { sources, map: sources.mapper(document, [...provenance]) }; } return context; diff --git a/packages/inspector/src/service.ts b/packages/inspector/src/service.ts index 1c6dab1930..1d270b45f1 100644 --- a/packages/inspector/src/service.ts +++ b/packages/inspector/src/service.ts @@ -111,9 +111,11 @@ export class Inspector { const start = performance.now(); const generation = randomUUID(); const provenance: BundleScriptProvenance[] = []; + const reads = new Set(); const html = await bundleToSingleHtml(this.project, { runtime: "placeholder", scriptProvenance: (p) => provenance.push(p), + onRead: (path) => reads.add(path), }); if (fingerprint(this.project) !== digest) throw new Error("source_changed_during_load: retry against a stable project"); @@ -205,7 +207,7 @@ export class Inspector { fingerprint: digest, sourceContext: () => { if (!sourceContext) { - const sources = new Sources(project); + const sources = new Sources(project, reads); sourceContext = { sources, map: sources.mapper(html, provenance) }; this.stats.sourceLoads++; } diff --git a/packages/inspector/src/sources.ts b/packages/inspector/src/sources.ts index ba728078e0..36d45505d7 100644 --- a/packages/inspector/src/sources.ts +++ b/packages/inspector/src/sources.ts @@ -1,5 +1,5 @@ -import { closeSync, fstatSync, openSync, readFileSync, readdirSync } from "node:fs"; -import { extname, join, relative } from "node:path"; +import { closeSync, fstatSync, openSync, readFileSync, readdirSync, realpathSync } from "node:fs"; +import { extname, join, relative, resolve } from "node:path"; import { createHash } from "node:crypto"; import { parse, type Node } from "acorn"; import postcss from "postcss"; @@ -41,6 +41,14 @@ function occurrences(text: string, needle: string) { } return hits; } +function realPath(path: string) { + try { + return realpathSync(path); + } catch { + return resolve(path); + } +} +const SHOWN_WHOLE = 2000; const excluded = new Set([...PREVIEW_IGNORED_DIRS, ".agents", ".claude", "evidence", "snapshots"]); function sourceFiles(project: string) { const files: string[] = []; @@ -195,10 +203,16 @@ export class Sources { readonly cssRules: { id: string; selector: string; file: string; start: number; end: number }[] = []; readonly htmlTags: HtmlTag[] = []; - constructor(readonly project: string) { + constructor( + readonly project: string, + served?: Iterable, + ) { + const root = realPath(project); + const bundled = served && new Set([...served].map((path) => relative(root, realPath(path)))); for (const path of sourceFiles(project)) { const file = relative(project, path); if (/(?:^|\/)(?:vendor|lib)\//.test(file)) continue; + if (bundled && !bundled.has(file)) continue; const fd = openSync(path, "r"); let text: string; try { @@ -354,18 +368,21 @@ export class Sources { s.type === "FunctionDeclaration" && hit.local >= s.start && hit.local < s.end && - s.end - s.start <= 2000, + s.end - s.start <= SHOWN_WHOLE, ) .sort((a, b) => a.end - a.start - (b.end - b.start))[0]; - const range = helper ?? enclosing; + const caller = result.length > 0; + const callLine = caller && !helper && enclosing.end - enclosing.start > SHOWN_WHOLE; + const range = callLine ? { start: hit.local, end: hit.local + 1 } : (helper ?? enclosing); const snippet = this.slice( hit.chunk.file, hit.chunk.offset + range.start, hit.chunk.offset + range.end, - result.length ? "animation caller" : "animation creation", + caller ? "animation caller" : "animation creation", target, ); if (snippet) result.push(snippet); + if (callLine) continue; for (const dependency of hit.chunk.dependencies(range)) { const definition = this.slice( hit.chunk.file, diff --git a/packages/inspector/tests/sources.test.ts b/packages/inspector/tests/sources.test.ts index 0a5f080cf8..1521cd7993 100644 --- a/packages/inspector/tests/sources.test.ts +++ b/packages/inspector/tests/sources.test.ts @@ -132,4 +132,72 @@ describe("source records", () => { rmSync(dir, { recursive: true, force: true }); } }); + it("gives a caller inside a large wrapper as its call line, keeping the creation it encloses", () => { + const dir = mkdtempSync(join(tmpdir(), "inspect-caller-")); + try { + const filler = Array.from({ length: 80 }, (_, n) => ` const step${n} = ${n} * 2;`).join( + "\n", + ); + const html = ``; + writeFileSync(join(dir, "index.html"), html); + const lines = html.split("\n"); + const at = (text: string) => lines.findIndex((line) => line.includes(text)) + 1; + const stack = [ + `at http://127.0.0.1:4000/index.html:${at("gsap.to")}:3`, + `at http://127.0.0.1:4000/index.html:${at("})();")}:1`, + ].join("\n"); + const [creation, caller, ...rest] = new Sources(dir).mapper(html, [])(stack, 0); + expect(creation.kind).toEqual(["animation creation"]); + expect(creation.code.trim()).toBe('gsap.to("#title", { x: 100 });'); + expect(caller.kind).toEqual(["animation caller"]); + expect(caller.code).toBe("})();\n"); + expect(rest).toEqual([]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("indexes only the files the inspected document was bundled from", async () => { + const dir = mkdtempSync(join(tmpdir(), "inspect-served-")); + try { + mkdirSync(join(dir, "compositions")); + mkdirSync(join(dir, "mockups")); + writeFileSync(join(dir, "theme.css"), ".title { color: red; }\n"); + writeFileSync( + join(dir, "index.html"), + `

Hi

`, + ); + writeFileSync( + join(dir, "compositions/scene.html"), + ``, + ); + writeFileSync( + join(dir, "mockups/a.html"), + `

Mock

`, + ); + const reads: string[] = []; + await bundleToSingleHtml(dir, { onRead: (path) => reads.push(path) }); + const files = (sources: Sources) => + [...new Set(sources.cssRules.map((rule) => rule.file))].sort(); + expect(files(new Sources(dir))).toEqual([ + "compositions/scene.html", + "mockups/a.html", + "theme.css", + ]); + const served = new Sources(dir, reads); + expect(files(served)).toEqual(["compositions/scene.html", "theme.css"]); + expect(served.chunks.some((chunk) => chunk.file === "mockups/a.html")).toBe(false); + const title = { + ref: "r", + tag: "h1", + id: "title", + hfId: null, + classes: ["title"], + composition: "root", + }; + expect(new Sources(dir).markup(title, 0)).toBeNull(); + expect(served.markup(title, 0)?.file).toBe("index.html"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); });