mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 04:46:59 +00:00
feat(studio-server): the preview records the kind and origin of each early error (#7041)
* feat(studio-server): the preview records the kind and origin of each early error * fix(studio-server): write preview script values with U+2028/2029 and < escaped
This commit is contained in:
@@ -16,6 +16,7 @@ export function defersMediaIn(doc: Document): boolean {
|
||||
export const STUDIO_PREVIEW_UPCOMING_ATTR = "data-hf-upcoming";
|
||||
export const STUDIO_PREVIEW_LAZY_ATTR = "data-hf-preview-lazy";
|
||||
export const STUDIO_PREVIEW_ERRORS = "__hfPreviewErrors";
|
||||
export const STUDIO_PREVIEW_ERROR_KINDS = "__hfPreviewErrorKinds";
|
||||
|
||||
export const refusedPreviewMessage = (status: number) =>
|
||||
`The preview page could not be loaded (HTTP ${status}).`;
|
||||
|
||||
@@ -22,12 +22,18 @@ import { tmpdir } from "node:os";
|
||||
import { join, parse } from "node:path";
|
||||
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
||||
import {
|
||||
STUDIO_PREVIEW_ERROR_KINDS,
|
||||
STUDIO_PREVIEW_ERRORS,
|
||||
STUDIO_PREVIEW_CAPTURE_MODE,
|
||||
STUDIO_PREVIEW_MARK_META,
|
||||
} from "@hyperframes/core/studio-preview-mark";
|
||||
import { AFTER_FONTS_SCRIPT_TYPE } from "@hyperframes/core/compiler";
|
||||
import { PREVIEW_BUNDLE_OPTIONS, PREVIEW_CAPTURE_PARAM, registerPreviewRoutes } from "./preview";
|
||||
import {
|
||||
PREVIEW_BUNDLE_OPTIONS,
|
||||
PREVIEW_CAPTURE_PARAM,
|
||||
registerPreviewRoutes,
|
||||
scriptJson,
|
||||
} from "./preview";
|
||||
import { registerFileRoutes } from "./files";
|
||||
import { createPreviewDocumentStore } from "../helpers/previewDocumentStore";
|
||||
import { MAX_OPEN_MEDIA_RANGE_BYTES } from "../helpers/mediaRange";
|
||||
@@ -374,6 +380,63 @@ describe("registerPreviewRoutes", () => {
|
||||
expect(previewWindow[STUDIO_PREVIEW_ERRORS]).toEqual(["Uncaught Error: GSAP could not load"]);
|
||||
});
|
||||
|
||||
it("writes a value into a script so it cannot close the tag or end the line", () => {
|
||||
const out = scriptJson("</script><!--\u2028\u2029");
|
||||
expect(out).not.toMatch(/<|\u2028|\u2029/);
|
||||
expect(JSON.parse(out)).toBe("</script><!--\u2028\u2029");
|
||||
});
|
||||
|
||||
it("records the kind and origin of each early error, never its text, and leaves the messages as they were", async () => {
|
||||
const projectDir = createProjectDir();
|
||||
writeFileSync(
|
||||
join(projectDir, "index.html"),
|
||||
"<!DOCTYPE html><html><head></head><body></body></html>",
|
||||
);
|
||||
const app = new Hono();
|
||||
registerPreviewRoutes(app, createAdapter(projectDir));
|
||||
const html = await (await app.request("http://localhost/projects/demo/preview")).text();
|
||||
const body =
|
||||
new RegExp(`<script>([^<]*${STUDIO_PREVIEW_ERROR_KINDS}[^<]*)</script>`).exec(html)?.[1] ??
|
||||
"";
|
||||
|
||||
const previewWindow: Record<string, unknown> = {
|
||||
location: { href: "http://localhost/api/projects/demo/preview/", origin: "http://localhost" },
|
||||
};
|
||||
let raise: (event: object) => void = () => undefined;
|
||||
new Function("window", "addEventListener", body)(
|
||||
previewWindow,
|
||||
(_type: string, listener: typeof raise) => (raise = listener),
|
||||
);
|
||||
class Custom extends Error {}
|
||||
raise({
|
||||
message: "a",
|
||||
error: new SyntaxError("secret one"),
|
||||
filename: "http://localhost/api/runtime.js",
|
||||
});
|
||||
raise({
|
||||
message: "b",
|
||||
error: new TypeError("secret two"),
|
||||
filename: "http://localhost/api/projects/demo/preview/app.js",
|
||||
});
|
||||
raise({
|
||||
message: "c",
|
||||
error: new Custom("secret three"),
|
||||
filename: "http://localhost/assets/studio.js",
|
||||
});
|
||||
raise({ message: "d", filename: "https://cdn.example.com/lib.js" });
|
||||
raise({ message: "e", error: new ReferenceError("secret four"), filename: "" });
|
||||
|
||||
expect(previewWindow[STUDIO_PREVIEW_ERRORS]).toEqual(["a", "b", "c", "d", "e"]);
|
||||
expect(previewWindow[STUDIO_PREVIEW_ERROR_KINDS]).toEqual([
|
||||
{ name: "SyntaxError", origin: "runtime" },
|
||||
{ name: "TypeError", origin: "project" },
|
||||
{ name: "other", origin: "studio" },
|
||||
{ name: "other", origin: "unknown" },
|
||||
{ name: "ReferenceError", origin: "unknown" },
|
||||
]);
|
||||
expect(JSON.stringify(previewWindow[STUDIO_PREVIEW_ERROR_KINDS])).not.toContain("secret");
|
||||
});
|
||||
|
||||
it("serves a later scene's image lazy, and captures every image eager under a capture-mode mark", async () => {
|
||||
const projectDir = createProjectDir();
|
||||
const later =
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import {
|
||||
STUDIO_PREVIEW_CAPTURE_MODE,
|
||||
STUDIO_PREVIEW_MARK_META,
|
||||
STUDIO_PREVIEW_ERROR_KINDS,
|
||||
STUDIO_PREVIEW_ERRORS,
|
||||
} from "@hyperframes/core/studio-preview-mark";
|
||||
import {
|
||||
@@ -273,7 +274,7 @@ const GSAP_FALLBACK_ATTR = "data-hf-gsap-fallback";
|
||||
function gsapCdnFallbackScript(baseUrl: string): string {
|
||||
return `<script ${GSAP_FALLBACK_ATTR}>
|
||||
(function(){
|
||||
var cdnBase=${JSON.stringify(gsapCdnDist(undefined, baseUrl)).replace(/</g, "\\u003c")};
|
||||
var cdnBase=${scriptJson(gsapCdnDist(undefined, baseUrl))};
|
||||
var loaded={};
|
||||
function loadFallback(file){
|
||||
if(loaded[file])return loaded[file];
|
||||
@@ -349,9 +350,21 @@ function previewVariablesFromRequest(rawVariables: string | undefined):
|
||||
/** Captures screenshot right after a seek, so they get every image eager; media loads near the playhead. */
|
||||
export const PREVIEW_CAPTURE_PARAM = "hf-capture";
|
||||
|
||||
export function scriptJson(value: unknown): string {
|
||||
return JSON.stringify(value)
|
||||
.replace(/</g, "\\u003c")
|
||||
.replace(/\u2028/g, "\\u2028")
|
||||
.replace(/\u2029/g, "\\u2029");
|
||||
}
|
||||
|
||||
// Studio's console capture attaches at the iframe's load; this keeps what was raised before it.
|
||||
const EARLY_ERRORS_SCRIPT = `<script>(function(){var seen=window.${STUDIO_PREVIEW_ERRORS}=[];
|
||||
addEventListener("error",function(e){seen.push(e.message||String(e))});})();</script>`;
|
||||
const earlyErrorsScript = (
|
||||
runtimeUrl: string,
|
||||
) => `<script>(function(){var seen=window.${STUDIO_PREVIEW_ERRORS}=[],kinds=window.${STUDIO_PREVIEW_ERROR_KINDS}=[],known={SyntaxError:1,TypeError:1,ReferenceError:1,RangeError:1},runtime=${scriptJson(new URL(runtimeUrl, "http://x").pathname)};
|
||||
addEventListener("error",function(e){seen.push(e.message||String(e));
|
||||
var name=e.error&&e.error.constructor&&e.error.constructor.name,path="";
|
||||
try{var url=e.filename&&new URL(e.filename,window.location.href);if(url&&url.origin===window.location.origin)path=url.pathname}catch(_){}
|
||||
kinds.push({name:known[name]===1?name:"other",origin:!path?"unknown":path===runtime?"runtime":path.indexOf("/api/projects/")===0?"project":"studio"})});})();</script>`;
|
||||
|
||||
function injectStudioPreviewAugmentations(
|
||||
html: string,
|
||||
@@ -367,7 +380,7 @@ function injectStudioPreviewAugmentations(
|
||||
)
|
||||
: injectTagsAtHeadStart(
|
||||
lazyPreviewImages(html),
|
||||
`<meta name="${STUDIO_PREVIEW_MARK_META}">${EARLY_ERRORS_SCRIPT}`,
|
||||
`<meta name="${STUDIO_PREVIEW_MARK_META}">${earlyErrorsScript(adapter.runtimeUrl)}`,
|
||||
);
|
||||
const withFallback = injectGsapCdnFallback(
|
||||
injectProjectSignature(
|
||||
|
||||
Reference in New Issue
Block a user