diff --git a/web/packages/core/src/globals.d.ts b/web/packages/core/src/globals.d.ts index 7ba293d5a..fa7ed439f 100644 --- a/web/packages/core/src/globals.d.ts +++ b/web/packages/core/src/globals.d.ts @@ -1,4 +1,3 @@ interface Error { - ruffleIndexError?: number; avmStack?: string; } diff --git a/web/packages/core/src/internal/errors.tsx b/web/packages/core/src/internal/errors.tsx index 21921b108..193172ed9 100644 --- a/web/packages/core/src/internal/errors.tsx +++ b/web/packages/core/src/internal/errors.tsx @@ -14,3 +14,101 @@ export enum PanicError { SwfFetchError, SwfCors, } + +export class LoadSwfError extends Error { + readonly #swfUrl: URL | undefined; + + constructor(swfUrl: URL | undefined) { + super(`Failed to fetch ${swfUrl}`); + this.#swfUrl = swfUrl; + } + + get ruffleIndexError() { + if (this.#swfUrl && !this.#swfUrl.protocol.includes("http")) { + return PanicError.FileProtocol; + } else if ( + window.location.origin === this.#swfUrl?.origin || + // The extension's internal player page is not restricted by CORS + window.location.protocol.includes("extension") + ) { + return PanicError.SwfFetchError; + } else { + // This is a selfhosted build of Ruffle that tried to make a cross-origin request + return PanicError.SwfCors; + } + } +} + +export class InvalidSwfError extends Error { + constructor(swfUrl: URL | undefined) { + super(`Not a valid swf: ${swfUrl}`); + } + + get ruffleIndexError() { + return PanicError.InvalidSwf; + } +} + +export class LoadRuffleWasmError extends Error { + constructor(public cause: Error) { + super("Failed to load Ruffle WASM"); + } + + get ruffleIndexError() { + // Serious duck typing. In error conditions, let's not make assumptions. + if (window.location.protocol === "file:") { + return PanicError.FileProtocol; + } else { + const message = String(this.cause.message).toLowerCase(); + if (message.includes("mime")) { + return PanicError.WasmMimeType; + } else if ( + message.includes("networkerror") || + message.includes("failed to fetch") + ) { + return PanicError.WasmCors; + } else if (message.includes("disallowed by embedder")) { + return PanicError.CSPConflict; + } else if (this.cause.name === "CompileError") { + return PanicError.InvalidWasm; + } else if ( + message.includes("could not download wasm module") && + this.cause.name === "TypeError" + ) { + return PanicError.WasmDownload; + } else if (this.cause.name === "TypeError") { + return PanicError.JavascriptConflict; + } else if ( + navigator.userAgent.includes("Edg") && + message.includes("webassembly is not defined") + ) { + // Microsoft Edge detection. + return PanicError.WasmDisabledMicrosoftEdge; + } else { + return PanicError.WasmNotFound; + } + } + } +} + +export class InvalidOptionsError extends Error { + constructor(message: string) { + super(`Invalid options: ${message}`); + } + + get ruffleIndexError() { + return PanicError.JavascriptConfiguration; + } +} + +export function getRuffleIndexError(error: Error | null): PanicError { + if ( + error instanceof InvalidOptionsError || + error instanceof InvalidSwfError || + error instanceof LoadRuffleWasmError || + error instanceof LoadSwfError + ) { + return error.ruffleIndexError; + } + return PanicError.Unknown; +} diff --git a/web/packages/core/src/internal/ui/panic.tsx b/web/packages/core/src/internal/ui/panic.tsx index d9cc3cdf7..4011f50f7 100644 --- a/web/packages/core/src/internal/ui/panic.tsx +++ b/web/packages/core/src/internal/ui/panic.tsx @@ -2,7 +2,7 @@ import { text, textAsParagraphs } from "../../i18n"; import { createRef } from "tsx-dom"; import { buildInfo } from "../../build-info"; import { RUFFLE_ORIGIN } from "../constants"; -import { PanicError } from "../errors"; +import { getRuffleIndexError, PanicError } from "../errors"; interface PanicLink { type: "open_link"; @@ -284,13 +284,13 @@ function createReportAction({ export function showPanicScreen( container: HTMLElement, - errorIndex: number, + error: Error | null, errorArray: ErrorArray, swfUrl: URL | undefined, ) { const errorText = errorArray.join(""); let { body, actions } = createPanicError( - errorIndex, + getRuffleIndexError(error), errorText, errorArray, swfUrl, diff --git a/web/packages/core/src/ruffle-player.tsx b/web/packages/core/src/ruffle-player.tsx index db187176b..01bf00ffd 100644 --- a/web/packages/core/src/ruffle-player.tsx +++ b/web/packages/core/src/ruffle-player.tsx @@ -19,7 +19,12 @@ import { isExtension } from "./current-script"; import { configureBuilder } from "./internal/builder"; import { showPanicScreen } from "./internal/ui/panic"; import { RUFFLE_ORIGIN } from "./internal/constants"; -import { PanicError } from "./internal/errors"; +import { + InvalidOptionsError, + InvalidSwfError, + LoadRuffleWasmError, + LoadSwfError, +} from "./internal/errors"; const DIMENSION_REGEX = /^\s*(\d+(\.\d+)?(%)?)/; @@ -668,41 +673,9 @@ export class RufflePlayer extends HTMLElement { this.onRuffleDownloadProgress.bind(this), ).catch((e) => { console.error(`Serious error loading Ruffle: ${e}`); - - // Serious duck typing. In error conditions, let's not make assumptions. - if (window.location.protocol === "file:") { - e.ruffleIndexError = PanicError.FileProtocol; - } else { - e.ruffleIndexError = PanicError.WasmNotFound; - const message = String(e.message).toLowerCase(); - if (message.includes("mime")) { - e.ruffleIndexError = PanicError.WasmMimeType; - } else if ( - message.includes("networkerror") || - message.includes("failed to fetch") - ) { - e.ruffleIndexError = PanicError.WasmCors; - } else if (message.includes("disallowed by embedder")) { - e.ruffleIndexError = PanicError.CSPConflict; - } else if (e.name === "CompileError") { - e.ruffleIndexError = PanicError.InvalidWasm; - } else if ( - message.includes("could not download wasm module") && - e.name === "TypeError" - ) { - e.ruffleIndexError = PanicError.WasmDownload; - } else if (e.name === "TypeError") { - e.ruffleIndexError = PanicError.JavascriptConflict; - } else if ( - navigator.userAgent.includes("Edg") && - message.includes("webassembly is not defined") - ) { - // Microsoft Edge detection. - e.ruffleIndexError = PanicError.WasmDisabledMicrosoftEdge; - } - } - this.panic(e); - throw e; + const error = new LoadRuffleWasmError(e); + this.panic(error); + throw error; }); this.newZipWriter = zipWriterClass; configureBuilder(builder, this.loadedConfig || {}); @@ -878,8 +851,7 @@ export class RufflePlayer extends HTMLElement { message: string, ) => asserts condition = (condition, message) => { if (!condition) { - const error = new TypeError(message); - error.ruffleIndexError = PanicError.JavascriptConfiguration; + const error = new InvalidOptionsError(message); this.panic(error); throw error; } @@ -1981,8 +1953,6 @@ export class RufflePlayer extends HTMLElement { return; } - const errorIndex = error?.ruffleIndexError ?? PanicError.Unknown; - const errorArray: Array & { stackIndex: number; avmStackIndex: number; @@ -2019,7 +1989,7 @@ export class RufflePlayer extends HTMLElement { errorArray.push(this.getPanicData()); // Clears out any existing content (ie play button or canvas) and replaces it with the error screen - showPanicScreen(this.container, errorIndex, errorArray, this.swfUrl); + showPanicScreen(this.container, error, errorArray, this.swfUrl); // Do this last, just in case it causes any cascading issues. this.destroy(); @@ -2059,21 +2029,9 @@ export class RufflePlayer extends HTMLElement { div.appendChild(innerDiv); this.container.prepend(div); } else { - const error = new Error("Failed to fetch: " + this.swfUrl); - if (this.swfUrl && !this.swfUrl.protocol.includes("http")) { - error.ruffleIndexError = PanicError.FileProtocol; - } else if (invalidSwf) { - error.ruffleIndexError = PanicError.InvalidSwf; - } else if ( - window.location.origin === this.swfUrl?.origin || - // The extension's internal player page is not restricted by CORS - window.location.protocol.includes("extension") - ) { - error.ruffleIndexError = PanicError.SwfFetchError; - } else { - // This is a selfhosted build of Ruffle that tried to make a cross-origin request - error.ruffleIndexError = PanicError.SwfCors; - } + const error = invalidSwf + ? new InvalidSwfError(this.swfUrl) + : new LoadSwfError(this.swfUrl); this.panic(error); } }