mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-07 15:27:14 +00:00
* fix(inspector): report nested hosts' authored window, one location per source line The runtime strips data-duration/data-end from nested composition hosts after boot and keeps them as data-hf-authored-*. Timeline inspection built its resolver without authored attrs, so a host with data-duration="4" reported its GSAP timeline length (3.72s) instead. Timeline text and events[].sources listed the same file:line once per mapped snippet (creation, caller, dependency), so a one-line script printed one location up to nine times. * refactor(inspector): carry the dedupe reason in the helper's name * fix(inspector): end continuation on a page that fits no snippet; fill budget with candidates * fix(inspector): timeline groups by component, names attr values, eases, film end * fix(studio-server): stamp hf ids into a file without reformatting it * fix(inspector): find an element's tag before its served hf id reaches the source * fix(inspector): live element map shows authored classes and text, not the playhead's * fix(inspector): collapse script-built text so the element map ignores the playhead Class names stay in source order, a runtime-stripped id is shown as id="…" rather than a selector, and a grouped row names which children it covers. * fix(inspector): keep paging, stamping, and the live map still on the reported edges A snippet larger than the page ended the walk, so later snippets could not be reached. Raw text ended at a longer tag name, which could stamp an id inside a script. An empty or mixed source element showed live text, and an unknown duration read as a film that ends at 0s. * fix(inspector): mint ids for an empty hf id and keep an unquoted class An empty data-hf-id was treated as already assigned, so stamping left it blank while the older stamper minted one. An unquoted class was read as no class, so the live map dropped it. * fix(inspector): narrow markup before reading a child tag skipMarkup returns a boolean, so the build still sees Markup where a tag is required. Narrow on the kind first so the inspector typechecks. * fix: quiet check false positives and name the ancestor of an id-less target Endpoint contact, disjoint fromTo pairs, and a negative z-index under a stacking-context parent were warnings with bad advice. Layout and contrast blamed a dot grid, a circular avatar's panel, and a visibility override. A faint texture veil is no longer the click target. * fix(lint): drop a history sentence and six gsap comments The comments check treats "load-bearing" as history, and gsap.ts added six comment lines. The names already carry what those lines said. * fix(parsers): ignore an empty id written inside another attribute The empty-id scan matched the first data-hf-id="" in the tag, including one inside a quoted title. It now skips quoted text, so the real attribute is minted. * fix(lint): keep real covers and a negative z-index under a card A gradient scale-down treated every fill like a radial dot, so a panel that starts transparent stopped counting as a cover. A stacking context on any ancestor cleared a negative z-index, and a faint wrapper made images inside it unpickable. * fix: keep an emoji from shifting a stamped id The quote mask replaced one emoji with one space, so the id landed before the closing quote. A radial cover that turns opaque after a clear stop stays fully painted. * fix(lint): score a letter-only dot as a dot, not a color name namesAColor saw #fff and rgba as words, so those dot grids counted as a full cover. Hex and rgb colors are stripped before that word check.
131 lines
4.9 KiB
TypeScript
131 lines
4.9 KiB
TypeScript
import { Sources, markupSnippet, mergeSnippets } from "./sources";
|
|
import type { InspectRequest, RuntimeResponse, Snippet } from "./types";
|
|
const coverage = [
|
|
"GSAP associations observed since page initialization; callback-created animations may require an explicit time query.",
|
|
"Animation source dependencies include lexically resolved declarations in the same script; later mutations, imports, and cross-script bindings are not traced.",
|
|
"CSS selector matches are candidates, not a full cascade or dependency graph. HTML is the authored opening tag when uniquely located.",
|
|
"Direct constructors, other animation systems, iframe contents, removed animations and unexecuted paths may be absent; missing origins are reported.",
|
|
];
|
|
export function mapInspection(
|
|
raw: RuntimeResponse,
|
|
request: InspectRequest,
|
|
context: {
|
|
sources: Sources;
|
|
map: ReturnType<Sources["mapper"]>;
|
|
catalog: Map<string, Snippet>;
|
|
generation: string;
|
|
sourceVersion: string;
|
|
reused: boolean;
|
|
start: number;
|
|
visualState?: { time: number | undefined; duration: number | undefined };
|
|
},
|
|
) {
|
|
const { sources, map, catalog, generation, sourceVersion, reused, start, visualState } = context;
|
|
const maxBytes = request.maxBytes ?? 12000;
|
|
const collected: Snippet[] = [];
|
|
const results = raw.results.map((result, index) => {
|
|
let unmappedAnimations = 0;
|
|
for (const animation of result.animations) {
|
|
const mapped = animation.stack ? map(animation.stack, index) : [];
|
|
if (!mapped.length) unmappedAnimations++;
|
|
collected.push(...mapped);
|
|
for (const stack of animation.attachmentStacks)
|
|
collected.push(
|
|
...map(stack, index).map((snippet) => ({ ...snippet, kind: ["timeline attachment"] })),
|
|
);
|
|
}
|
|
for (const el of result.elements) {
|
|
const snippet = markupSnippet(sources, el, index);
|
|
if (snippet) collected.push(snippet);
|
|
}
|
|
for (const id of result.css) {
|
|
const rule = sources.cssRules.find((rule) => rule.id === id);
|
|
if (rule) {
|
|
const snippet = sources.slice(
|
|
rule.file,
|
|
rule.start,
|
|
rule.end,
|
|
"matched CSS candidate",
|
|
index,
|
|
);
|
|
if (snippet) collected.push(snippet);
|
|
}
|
|
}
|
|
return {
|
|
target: index,
|
|
status: result.status,
|
|
candidates: result.candidates,
|
|
candidatesOmitted: 0,
|
|
animations: result.animations.length,
|
|
coTargets: [
|
|
...new Map(
|
|
result.animations
|
|
.flatMap((animation) => animation.targets)
|
|
.filter((element) => !result.elements.some((own) => own.ref === element.ref))
|
|
.map((element) => [element.ref, element]),
|
|
).values(),
|
|
].slice(0, 10),
|
|
unmappedAnimations,
|
|
scopeTruncated: result.truncated,
|
|
};
|
|
});
|
|
const merged = mergeSnippets(collected, sources);
|
|
for (const snippet of merged) catalog.set(snippet.id, snippet);
|
|
let selected = merged;
|
|
if (request.snippets)
|
|
selected = request.snippets.map((id) => {
|
|
const snippet = catalog.get(id);
|
|
if (!snippet) throw new Error(`unknown_snippet: ${id}`);
|
|
return snippet;
|
|
});
|
|
const response = {
|
|
generation: generation,
|
|
visualState,
|
|
sourceVersion: sourceVersion,
|
|
reused,
|
|
results,
|
|
snippets: [] as Snippet[],
|
|
omitted: [] as { id: string; bytes: number }[],
|
|
omittedCount: 0,
|
|
nextOffset: null as number | null,
|
|
coverage,
|
|
recordLimitReached: raw.recordLimitReached,
|
|
hooked: raw.hooked,
|
|
debugAvailable: true,
|
|
elapsedMs: Math.round(performance.now() - start),
|
|
};
|
|
const offset = request.offset ?? 0;
|
|
if (!Number.isInteger(offset) || offset < 0)
|
|
throw new Error("offset must be a non-negative integer");
|
|
const size = () => Buffer.byteLength(JSON.stringify(response));
|
|
const trimCandidates = (limit: number, keep: number) => {
|
|
while (size() > limit) {
|
|
const longest = response.results.reduce((a, b) =>
|
|
b.candidates.length > a.candidates.length ? b : a,
|
|
);
|
|
if (longest.candidates.length <= keep) return;
|
|
longest.candidates.pop();
|
|
longest.candidatesOmitted++;
|
|
}
|
|
};
|
|
trimCandidates(maxBytes / 2, 5);
|
|
for (let i = offset; i < selected.length; i++) {
|
|
response.snippets.push(selected[i]);
|
|
if (size() > maxBytes - 700) {
|
|
response.snippets.pop();
|
|
response.omittedCount = selected.length - i;
|
|
response.omitted = selected
|
|
.slice(i, i + 8)
|
|
.map((item) => ({ id: item.id, bytes: Buffer.byteLength(item.code) }));
|
|
const nothingFit = response.snippets.length === 0;
|
|
response.nextOffset = nothingFit ? (i + 1 < selected.length ? i + 1 : null) : i;
|
|
break;
|
|
}
|
|
}
|
|
trimCandidates(maxBytes, 0);
|
|
if (Buffer.byteLength(JSON.stringify(response)) > maxBytes)
|
|
throw new Error("metadata_exceeds_budget: use fewer targets or increase maxBytes");
|
|
if (request.debug) return { ...response, debug: raw };
|
|
return response;
|
|
}
|