diff --git a/framework/core/js/src/common/Application.tsx b/framework/core/js/src/common/Application.tsx index fe74c37b4d..4db332ce34 100644 --- a/framework/core/js/src/common/Application.tsx +++ b/framework/core/js/src/common/Application.tsx @@ -279,6 +279,13 @@ export default class Application { initialRoute!: string; + /** + * @internal + */ + public currentInitializerExtension: string | null = null; + + private handledErrors: { extension: null | string; errorId: string; error: any }[] = []; + public load(payload: Application['data']) { this.data = payload; this.translator.setLocale(payload.locale); @@ -288,17 +295,19 @@ export default class Application { const caughtInitializationErrors: CallableFunction[] = []; this.initializers.toArray().forEach((initializer) => { + this.currentInitializerExtension = initializer.itemName.includes('/') + ? initializer.itemName.replace(/(\/flarum-ext-)|(\/flarum-)/g, '-') + : initializer.itemName; + try { initializer(this); } catch (e) { - const extension = initializer.itemName.includes('/') - ? initializer.itemName.replace(/(\/flarum-ext-)|(\/flarum-)/g, '-') - : initializer.itemName; - caughtInitializationErrors.push(() => fireApplicationError( - extractText(app.translator.trans('core.lib.error.extension_initialiation_failed_message', { extension })), - `${extension} failed to initialize`, + extractText( + app.translator.trans('core.lib.error.extension_initialiation_failed_message', { extension: this.currentInitializerExtension }) + ), + `${this.currentInitializerExtension} failed to initialize`, e ) ); @@ -727,4 +736,12 @@ export default class Application { return prefix + url + (queryString ? '?' + queryString : ''); } + + public handleErrorOnce(extension: null | string, errorId: string, userTitle: string, consoleTitle: string, error: any) { + if (this.handledErrors.some((e) => e.errorId === errorId)) return; + + this.handledErrors.push({ extension, errorId, error }); + + fireApplicationError(userTitle, consoleTitle, error); + } } diff --git a/framework/core/js/src/common/extend.ts b/framework/core/js/src/common/extend.ts index 4bb3853a69..6ee151d91a 100644 --- a/framework/core/js/src/common/extend.ts +++ b/framework/core/js/src/common/extend.ts @@ -1,3 +1,6 @@ +import extractText from './utils/extractText'; +import app from './app'; + /** * Extend an object's method by running its output through a mutating callback * every time it is called. @@ -28,6 +31,8 @@ export function extend, K extends KeyOfType, ...args: Parameters) => void ) { + const extension = app.currentInitializerExtension; + // A lazy loaded module, only apply the function after the module is loaded. if (typeof object === 'string') { let [namespace, id] = flarum.reg.namespaceAndIdFromPath(object); @@ -45,7 +50,17 @@ export function extend, K extends KeyOfType) { const value = original ? original.apply(this, args) : undefined; - callback.apply(this, [value, ...args]); + try { + callback.apply(this, [value, ...args]); + } catch (e) { + app.handleErrorOnce( + extension, + `${extension}::extend::${object.constructor.name}::${method.toString()}`, + extractText(app.translator.trans('core.lib.error.extension_runtime_failed_message', { extension })), + `${extension} failed to extend ${object.constructor.name}::${method.toString()}`, + e + ); + } return value; } as T[K]; @@ -86,6 +101,8 @@ export function override, K extends KeyOfType) => void ) { + const extension = app.currentInitializerExtension; + // A lazy loaded module, only apply the function after the module is loaded. if (typeof object === 'string') { let [namespace, id] = flarum.reg.namespaceAndIdFromPath(object); @@ -101,7 +118,17 @@ export function override, K extends KeyOfType) { - return newMethod.apply(this, [original?.bind(this), ...args]); + try { + return newMethod.apply(this, [original?.bind(this), ...args]); + } catch (e) { + app.handleErrorOnce( + extension, + `${extension}::extend::${object.constructor.name}::${method.toString()}`, + extractText(app.translator.trans('core.lib.error.extension_runtime_failed_message', { extension })), + `${extension} failed to override ${object.constructor.name}::${method.toString()}`, + e + ); + } } as T[K]; Object.assign(object[method], original); diff --git a/framework/core/js/src/common/helpers/fireApplicationError.ts b/framework/core/js/src/common/helpers/fireApplicationError.ts index b4e8c9e85e..7b6651bcd1 100644 --- a/framework/core/js/src/common/helpers/fireApplicationError.ts +++ b/framework/core/js/src/common/helpers/fireApplicationError.ts @@ -3,9 +3,9 @@ import app from '../app'; /** * Fire a Flarum error which is shown in the JS console for everyone and in an alert for the admin. * - * @param userTitle: a user friendly title of the error, should be localized. - * @param consoleTitle: an error title that goes in the console, doesn't have to be localized. - * @param error: the error. + * @param userTitle a user friendly title of the error, should be localized. + * @param consoleTitle an error title that goes in the console, doesn't have to be localized. + * @param error the error. */ export default function fireApplicationError(userTitle: string, consoleTitle: string, error: any) { console.group(`%c${consoleTitle}`, 'background-color: #d83e3e; color: #ffffff; font-weight: bold;'); diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index a401e9bff9..3286204230 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -722,6 +722,7 @@ core: db_error_message: "Database query failed. This may be caused by an incompatibility between an extension and your database driver." dependent_extensions_message: "Cannot disable {extension} until the following dependent extensions are disabled: {extensions}" extension_initialiation_failed_message: "{extension} failed to initialize, check the browser console for further information." + extension_runtime_failed_message: "{extension} encountered an error while running. Check the browser console for further information." generic_message: "Oops! Something went wrong. Please reload the page and try again." generic_cross_origin_message: "Oops! Something went wrong during a cross-origin request. Please reload the page and try again." missing_dependencies_message: "Cannot enable {extension} until the following dependencies are enabled: {extensions}"