@hyperframes/engine 0.9.7 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  * the page to expose window.__hf, then captures frames deterministically
7
7
  * via Chrome's BeginFrame API or Page.captureScreenshot fallback.
8
8
  */
9
- import { type Browser, type CDPSession, type Page, type Protocol } from "puppeteer-core";
9
+ import { type Browser, type CDPSession, type HTTPRequest, type Page, type Protocol } from "puppeteer-core";
10
10
  import { type RawAuthoredTiming } from "@hyperframes/core";
11
11
  import { type MotionBlurPlan } from "./motionBlur.js";
12
12
  import { type BrowserLease, type CaptureMode } from "./browserManager.js";
@@ -89,6 +89,8 @@ export interface CaptureSession {
89
89
  * were hitting that wall — a 705-render spike at the 45s setup bucket).
90
90
  */
91
91
  scriptLoadFailures: string[];
92
+ /** Closed origin and error of each of `scriptLoadFailures`, same order. */
93
+ scriptLoadCauses?: ScriptLoadCause[];
92
94
  /** Uncaught page errors; a timed-out timeline wait with any of these becomes a script failure. */
93
95
  pageErrors: string[];
94
96
  /** The first VFX chain error; its frames would lack the effect, so it stops the render. */
@@ -464,6 +466,15 @@ export declare class VfxFailureError extends Error {
464
466
  readonly detail: string;
465
467
  constructor(detail: string);
466
468
  }
469
+ type ScriptLoadOrigin = "local" | "cdn" | "remote" | "unknown";
470
+ export interface ScriptLoadCause {
471
+ origin: ScriptLoadOrigin;
472
+ /** `net::ERR_*`, `http-<status>` or `unknown`. */
473
+ code: string;
474
+ }
475
+ /** Where a failed script came from and why, never its URL, host or path. */
476
+ export declare function classifyScriptLoadCause(url: string, serverUrl: string, failure: string | number): ScriptLoadCause;
477
+ export declare function recordFailedRequest(session: CaptureSession, request: Pick<HTTPRequest, "resourceType" | "url" | "method" | "failure">): void;
467
478
  /** `runtime-error:<first line>` for the error a framework wrapper logs when a composition script throws. */
468
479
  export declare function classifyConsoleScriptError(type: string, text: string): string | null;
469
480
  export declare function classifyConsoleScriptFailure(type: string, text: string): string | null;
@@ -1686,6 +1686,7 @@ export function recordSubTimelineWarning(session, timeoutMs) {
1686
1686
  const threwThenTimedOut = session.pageErrors.length > 0 && session.scriptLoadFailures.length === 0;
1687
1687
  const scriptFailure = isScriptFailure(session);
1688
1688
  const sources = [...session.scriptLoadFailures, ...session.pageErrors];
1689
+ const loadCauses = session.scriptLoadCauses ?? [];
1689
1690
  const pending = session.pendingTimelineIds ?? [];
1690
1691
  const pendingSuffix = pending.length > 0 ? ` (still unregistered: ${pending.join(", ")})` : "";
1691
1692
  const isolated = session.isolatedCompositions ?? [];
@@ -1713,7 +1714,8 @@ export function recordSubTimelineWarning(session, timeoutMs) {
1713
1714
  `${pendingSuffix} (${sources.join(", ")}). Fix the error; a composition animated by ` +
1714
1715
  `CSS or rAF rather than a GSAP timeline must mark its host with data-no-timeline.`
1715
1716
  : scriptFailure
1716
- ? `A sub-composition timeline script failed to load (${sources.join(", ")})`
1717
+ ? `A sub-composition timeline script failed to load (${sources.join(", ")})` +
1718
+ scriptLoadCauseSuffix(loadCauses)
1717
1719
  : `Sub-composition timelines did not become ready within ${timeoutMs}ms${pendingSuffix}. ` +
1718
1720
  `This can be intentional: a composition driven by CSS animations or rAF never registers ` +
1719
1721
  `window.__timelines[id], and marking its host with data-no-timeline skips the wait entirely. ` +
@@ -1891,11 +1893,44 @@ function recordPageError(session, error) {
1891
1893
  // (e.g. a `requestfailed` following the 4xx), and repeated <script> tags for
1892
1894
  // the same URL duplicate it further — dedupe so the fail-fast warning names
1893
1895
  // each failed URL once.
1894
- function recordScriptLoadFailure(session, url) {
1896
+ function recordScriptLoadFailure(session, url, cause) {
1895
1897
  if (!session.scriptLoadFailures.includes(url)) {
1896
1898
  session.scriptLoadFailures.push(url);
1899
+ (session.scriptLoadCauses ??= []).push(cause);
1897
1900
  }
1898
1901
  }
1902
+ const CDN_HOSTS = /^(?:cdn\.jsdelivr\.net|unpkg\.com|cdnjs\.cloudflare\.com)$/;
1903
+ /** Where a failed script came from and why, never its URL, host or path. */
1904
+ export function classifyScriptLoadCause(url, serverUrl, failure) {
1905
+ let origin = "unknown";
1906
+ if (url.startsWith(`${serverUrl}/`))
1907
+ origin = "local";
1908
+ else if (/^https?:\/\//.test(url)) {
1909
+ origin = CDN_HOSTS.test(new URL(url).hostname) ? "cdn" : "remote";
1910
+ }
1911
+ const code = typeof failure === "number"
1912
+ ? `http-${failure}`
1913
+ : /^net::ERR_[A-Z0-9_]{1,40}$/.test(failure)
1914
+ ? failure
1915
+ : "unknown";
1916
+ return { origin, code };
1917
+ }
1918
+ function scriptLoadCauseSuffix(causes) {
1919
+ const shown = causes.slice(0, 4).map((c) => `${c.origin}/${c.code}`);
1920
+ return shown.length === 0 ? "" : ` [script_load: ${shown.join(", ")}]`;
1921
+ }
1922
+ const CANCELLED_BY_THE_PAGE_NOT_REFUSED = "net::ERR_ABORTED";
1923
+ export function recordFailedRequest(session, request) {
1924
+ const resourceType = request.resourceType();
1925
+ const url = request.url();
1926
+ const failureText = request.failure()?.errorText ?? "unknown";
1927
+ if (resourceType === "script" && failureText !== CANCELLED_BY_THE_PAGE_NOT_REFUSED) {
1928
+ recordScriptLoadFailure(session, url, classifyScriptLoadCause(url, session.serverUrl, failureText));
1929
+ }
1930
+ if (shouldIgnoreRequestFailureDiagnostic({ resourceType, url, failureText }))
1931
+ return;
1932
+ appendBrowserDiagnostic(session, formatRequestFailureDiagnostic({ method: request.method(), resourceType, url, failureText }));
1933
+ }
1899
1934
  const SCRIPT_ERROR_LABEL = "[HyperFrames] composition script error:";
1900
1935
  /** `runtime-error:<first line>` for the error a framework wrapper logs when a composition script throws. */
1901
1936
  export function classifyConsoleScriptError(type, text) {
@@ -1980,7 +2015,7 @@ export async function initializeSession(session) {
1980
2015
  // A blocked script can never register its timeline (#3352); a thrown one may still, so it only counts on timeout.
1981
2016
  const scriptFailure = classifyConsoleScriptFailure(type, text);
1982
2017
  if (scriptFailure)
1983
- recordScriptLoadFailure(session, scriptFailure);
2018
+ recordScriptLoadFailure(session, scriptFailure, { origin: "unknown", code: "unknown" });
1984
2019
  recordConsoleScriptError(session, classifyConsoleScriptError(type, text));
1985
2020
  });
1986
2021
  page.on("pageerror", (err) => {
@@ -1991,22 +2026,7 @@ export async function initializeSession(session) {
1991
2026
  console.error(text);
1992
2027
  appendBrowserDiagnostic(session, text);
1993
2028
  });
1994
- page.on("requestfailed", (request) => {
1995
- const resourceType = request.resourceType();
1996
- const url = request.url();
1997
- const failureText = request.failure()?.errorText ?? "unknown";
1998
- if (resourceType === "script") {
1999
- recordScriptLoadFailure(session, request.url());
2000
- }
2001
- if (shouldIgnoreRequestFailureDiagnostic({ resourceType, url, failureText }))
2002
- return;
2003
- appendBrowserDiagnostic(session, formatRequestFailureDiagnostic({
2004
- method: request.method(),
2005
- resourceType,
2006
- url,
2007
- failureText,
2008
- }));
2009
- });
2029
+ page.on("requestfailed", (request) => recordFailedRequest(session, request));
2010
2030
  const projectScripts = new Set();
2011
2031
  page.on("response", (response) => {
2012
2032
  const status = response.status();
@@ -2018,8 +2038,9 @@ export async function initializeSession(session) {
2018
2038
  projectScripts.add(responseUrl);
2019
2039
  return;
2020
2040
  }
2021
- if (isScript)
2022
- recordScriptLoadFailure(session, responseUrl);
2041
+ if (isScript) {
2042
+ recordScriptLoadFailure(session, responseUrl, classifyScriptLoadCause(responseUrl, serverUrl, status));
2043
+ }
2023
2044
  appendBrowserDiagnostic(session, formatHttpErrorDiagnostic({
2024
2045
  method: request.method(),
2025
2046
  resourceType: request.resourceType(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -59,8 +59,8 @@
59
59
  "puppeteer": "~25.11.0",
60
60
  "puppeteer-core": "^25.11.0",
61
61
  "undici": "7.25.0",
62
- "@hyperframes/core": "^0.9.7",
63
- "@hyperframes/parsers": "^0.9.7"
62
+ "@hyperframes/core": "^0.9.9",
63
+ "@hyperframes/parsers": "^0.9.9"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "^25.0.10",