mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -111,9 +111,11 @@ export class Inspector {
|
||||
const start = performance.now();
|
||||
const generation = randomUUID();
|
||||
const provenance: BundleScriptProvenance[] = [];
|
||||
const reads = new Set<string>();
|
||||
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++;
|
||||
}
|
||||
|
||||
@@ -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<string>,
|
||||
) {
|
||||
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,
|
||||
|
||||
@@ -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 = `<html><body><script>\n(function () {\n${filler}\n gsap.to("#title", { x: 100 });\n})();\n</script></body></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"),
|
||||
`<!doctype html><html><head><link rel="stylesheet" href="theme.css"></head><body><div id="stage" data-composition-id="root" data-start="0" data-duration="4" data-width="1920" data-height="1080"><h1 id="title" class="title">Hi</h1><div data-composition-id="scene" data-composition-src="compositions/scene.html" data-start="0" data-duration="2"></div></div><script>window.__timelines={};</script></body></html>`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, "compositions/scene.html"),
|
||||
`<template id="scene-template"><div data-composition-id="scene" data-width="1920" data-height="1080"><p class="line">Yo</p><style>.line { font-size: 40px; }</style></div></template>`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, "mockups/a.html"),
|
||||
`<style>.title { color: green; }</style><h1 id="title" class="title">Mock</h1><script>const mock = 1;</script>`,
|
||||
);
|
||||
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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user