Skip to content
Merged
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/core/src/compiler/scenePartsManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const hash = (text: string) => createHash("sha1").update(text).digest("hex").sli
* `ignore` lists selectors for parts that change on every build without changing what is shown.
*/
export function addScenePartsManifest(html: string, ignore: readonly string[] = []): string {
if (!html.includes(SCENE_PART_ATTR)) return html;
const doc = parseHTMLContent(html);
const parts = [...doc.querySelectorAll(`[${SCENE_PART_ATTR}]`)];
if (parts.length === 0) return html;
Expand Down
3 changes: 2 additions & 1 deletion packages/studio-server/src/helpers/lazyPreviewImages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ const hasLoading = (img: Element) =>

export function lazyPreviewImages(html: string): string {
if (!/<!doctype|<html[\s>]/i.test(html)) return html;
const images = [...parseHTML(html).document.querySelectorAll("img")];
// The scanner skips comments and raw text, so its `<img` offsets line up with the DOM's images.
const tags = findStartTags(html, "img");
if (tags.length === 0) return html;
const images = [...parseHTML(html).document.querySelectorAll("img")];
if (tags.length !== images.length) return html;
const parts: string[] = [];
let from = 0;
Expand Down
4 changes: 2 additions & 2 deletions packages/studio-server/src/helpers/subComposition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
RUNTIME_BOOTSTRAP_ATTR,
stripEmbeddedRuntimeScripts,
} from "@hyperframes/core/compiler";
import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
import { findStartTags, isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
import { htmlHasGsapCore, insertGsapCoreBeforeHeadPlugin } from "./gsapCoreScript.js";
import { withRootPathsRelative } from "./rootPaths.js";
Expand Down Expand Up @@ -381,5 +381,5 @@ ${rewrittenContent}

/** True for a real `<base>` element; the text "<base" inside a script or comment does not count. */
export function hasBaseElement(html: string): boolean {
return parseHTML(html).document.querySelector("base") !== null;
return findStartTags(html, "base").length > 0;
}
62 changes: 62 additions & 0 deletions packages/studio-server/src/routes/preview.parseCount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { expect, it, vi } from "vitest";
import { Hono } from "hono";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { StudioApiAdapter } from "../types";

const pageParses = vi.hoisted(() => ({ count: 0, minLength: Infinity }));
vi.mock("linkedom", async (actual) => {
const real = await actual<typeof import("linkedom")>();
return {
...real,
parseHTML: (html: string, ...rest: unknown[]) => {
if (html.length >= pageParses.minLength) pageParses.count += 1;
return (real.parseHTML as (...args: unknown[]) => ReturnType<typeof real.parseHTML>)(
html,
...rest,
);
},
};
});

const { registerPreviewRoutes } = await import("./preview");

it("parses a stamped page once per preview build (the read-tracking walk) when it has no images, <base> or scene parts", async () => {
const dir = mkdtempSync(join(tmpdir(), "hf-preview-parses-"));
try {
const shapes = Array.from(
{ length: 200 },
(_, i) => `<path data-hf-id="hf-p${i}" d="M${i} 0h1v1z"/>`,
);
const page =
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>t</title></head><body>` +
`<div data-hf-id="hf-root" data-composition-id="main"><svg data-hf-id="hf-svg">${shapes.join("")}</svg></div>` +
`</body></html>`;
writeFileSync(join(dir, "index.html"), page);
const adapter = {
listProjects: () => [],
resolveProject: async (id: string) => ({ id, dir }),
bundle: async () => page,
lint: async () => ({ findings: [] }),
runtimeUrl: "/api/runtime.js",
rendersDir: () => "/tmp/renders",
startRender: () => ({
id: "job",
status: "rendering",
progress: 0,
outputPath: "/tmp/out.mp4",
}),
} as unknown as StudioApiAdapter;
const app = new Hono();
registerPreviewRoutes(app, adapter);
pageParses.minLength = page.length / 2;

const response = await app.request("http://localhost/projects/demo/preview");

expect(response.status).toBe(200);
expect(pageParses.count).toBe(1);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
Loading