Files
hyperframes/packages/inspector/src/engine.ts
Xuanru Li 45825faa5d fix(inspector): grep timeline windows, continuation, candidates and timeline text from the Canary dogfood (#5595)
* 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.
2026-10-14 09:16:20 +00:00

92 lines
3.0 KiB
TypeScript

import type { TimelineRequest } from "../../core/src/runtime/timelineInspection";
import { withAuthoredState } from "./authoredState";
import { collectElementDom, formatElementMap, type ElementMap } from "./elementMap.js";
import { mapInspection } from "./mapInspection";
import { queryPage, timelinePage, type PageRunner } from "./pageQueries";
import type { AuthoredHtml, Sources } from "./sources";
import { formatTimeline, type TimelineReport } from "./timelineReport";
import type { InspectRequest, Snippet } from "./types";
export interface SourceContext {
sources: Sources;
map: ReturnType<Sources["mapper"]>;
}
export interface InspectionView {
readonly generation: string;
readonly sourceVersion: string;
readonly pageGeneration?: string;
sourceContext(): SourceContext;
authoredHtml?(): AuthoredHtml;
assertCurrent(): Promise<void>;
}
export type ComponentReport = ReturnType<typeof mapInspection>;
export type ElementMapReport = ElementMap & { sourceVersion: string; generation: string };
function assertPageGeneration(view: InspectionView, actual: string | null) {
if (view.pageGeneration !== undefined && actual !== view.pageGeneration)
throw new Error("stale_preview: the preview reloaded; query again");
}
export async function inspectComponents(
runner: PageRunner,
view: InspectionView,
request: InspectRequest,
context: {
catalog: Map<string, Snippet>;
reused: boolean;
start: number;
visualState?: { time: number | undefined; duration: number | undefined };
},
): Promise<ComponentReport> {
const { sources, map } = view.sourceContext();
const raw = await runner.run(queryPage, {
targets: request.targets,
descendants: request.descendants ?? true,
cssRules: sources.cssRules.map(({ id, selector }) => ({ id, selector })),
});
assertPageGeneration(view, raw.generation);
await view.assertCurrent();
return mapInspection(raw, request, {
...context,
sources,
map,
generation: view.generation,
sourceVersion: view.sourceVersion,
});
}
export async function inspectTimeline(
runner: PageRunner,
view: InspectionView,
request: TimelineRequest,
reused: boolean,
): Promise<TimelineReport> {
const raw = await runner.run(timelinePage, request);
assertPageGeneration(view, raw.generation);
await view.assertCurrent();
return formatTimeline(raw, request, {
generation: view.generation,
map: view.sourceContext().map,
reused,
});
}
export async function inspectElementMap(
runner: PageRunner,
view: InspectionView,
file = "index.html",
): Promise<ElementMapReport> {
const snapshot = await runner.run(collectElementDom, undefined);
assertPageGeneration(view, snapshot.generation);
const authored = view.authoredHtml?.();
const map = formatElementMap(authored ? withAuthoredState(snapshot, authored) : snapshot, {
file,
});
await view.assertCurrent();
return { ...map, sourceVersion: view.sourceVersion, generation: view.generation };
}
export type { PageRunner, TimelineReport };