Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/lint/src/rules/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ function findRuntimeMediaSrcMutationFindings(ctx: LintContext): HyperframeLintFi
const findings: HyperframeLintFinding[] = [];
for (const script of ctx.scripts) {
for (const mutation of extractMediaSrcMutations(script.content)) {
if (!mutation.selector) continue;
let targets: Element[];
try {
const id = /^#[A-Za-z_][\w-]*$/.test(mutation.selector) ? mutation.selector.slice(1) : null;
Expand Down
13 changes: 5 additions & 8 deletions packages/parsers/src/mediaSrcMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import * as acorn from "acorn";
import * as acornWalk from "acorn-walk";

export interface MediaSrcMutation {
selector: string;
/** Null when the write's target is not a statically resolvable DOM lookup. */
selector: string | null;
operation: "src_assignment" | "set_attribute";
raw: string;
}
Expand Down Expand Up @@ -206,7 +207,7 @@ function selectorFromDomLookup(
return method === "getElementById" ? `#${value}` : value;
}

/** Find literal source writes whose target is a statically resolvable DOM lookup. */
/** Find source writes, with the target's selector when it is a statically resolvable DOM lookup. */
export function extractMediaSrcMutations(script: string): MediaSrcMutation[] {
try {
const ast = parseProgram(script);
Expand All @@ -220,10 +221,8 @@ export function extractMediaSrcMutations(script: string): MediaSrcMutation[] {
memberName(node.left) !== "src"
)
return;
const selector = lookupSelector(node.left.object, scopeAt(ancestors));
if (!selector) return;
mutations.push({
selector,
selector: lookupSelector(node.left.object, scopeAt(ancestors)) ?? null,
operation: "src_assignment",
raw: script.slice(node.start, node.end),
});
Expand All @@ -232,10 +231,8 @@ export function extractMediaSrcMutations(script: string): MediaSrcMutation[] {
if (node.callee.type !== "MemberExpression" || memberName(node.callee) !== "setAttribute")
return;
if (literalString(node.arguments[0])?.toLowerCase() !== "src") return;
const selector = lookupSelector(node.callee.object, scopeAt(ancestors));
if (!selector) return;
mutations.push({
selector,
selector: lookupSelector(node.callee.object, scopeAt(ancestors)) ?? null,
operation: "set_attribute",
raw: script.slice(node.start, node.end),
});
Expand Down
37 changes: 37 additions & 0 deletions packages/producer/src/services/render/stages/probeStage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,43 @@ describe("runProbeStage — forceScreenshot threading", () => {
expect(capturedCfgs).toHaveLength(0);
});

it("probes a src write whose target is resolved at runtime when the document has media", async () => {
resetRetryMocks();
capturedCfgs.length = 0;
const { runProbeStage } = await import("./probeStage.js");
const input = makeProbeInput({});
input.composition.duration = 5;
input.compiled.html = `<video id="clip" data-start="0" data-key="clipUrl" src="video-a.mp4"></video>
<script>
const S = { clipUrl: "video-b.mp4" };
document.querySelectorAll("[data-key]").forEach((el) => {
el.src = S[el.getAttribute("data-key")];
});
</script>`;

await runProbeStage(input);

expect(capturedCfgs.length).toBeGreaterThan(0);
});

it("does not probe a src write resolved at runtime when the document has no media", async () => {
resetRetryMocks();
capturedCfgs.length = 0;
const { runProbeStage } = await import("./probeStage.js");
const input = makeProbeInput({});
input.composition.duration = 5;
input.compiled.html = `<img class="logo" src="a.png">
<script>
document.querySelectorAll(".logo").forEach((el) => {
el.src = "b.png";
});
</script>`;

await runProbeStage(input);

expect(capturedCfgs).toHaveLength(0);
});

it("probes when a source child of existing media is mutated", async () => {
resetRetryMocks();
capturedCfgs.length = 0;
Expand Down
7 changes: 7 additions & 0 deletions packages/producer/src/services/render/stages/probeStage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,15 @@ function hasRuntimeMediaChanges(html: string): boolean {
if (name === "video" || name === "audio") return true;
return name === "source" && element.closest("video, audio") !== null;
};
const hasManagedMedia = document.querySelector("video, audio") !== null;
for (const script of scriptBodies) {
for (const mutation of extractMediaSrcMutations(script)) {
// A write whose target can't be resolved statically, such as one in a loop
// over querySelectorAll, may land on any media in the document.
if (!mutation.selector) {
if (hasManagedMedia) return true;
continue;
}
try {
const id = /^#[A-Za-z_][\w-]*$/.test(mutation.selector) ? mutation.selector.slice(1) : null;
const idTarget = id ? document.getElementById(id) : null;
Expand Down
Loading