diff --git a/examples/core/plugins/src/plugin.rs b/examples/core/plugins/src/plugin.rs index 2e16004da0..1ccc0fddc5 100644 --- a/examples/core/plugins/src/plugin.rs +++ b/examples/core/plugins/src/plugin.rs @@ -16,16 +16,16 @@ pub fn get_test_plugin() -> Plugin { .register_plugin("test-plugin", |_, _| { let mut map = std::collections::HashMap::new(); map.insert("/Cargo.toml".to_string(), "Cargo.toml".to_string()); - map + Ok(map) }); actions.settings_actions.add_templates.register_plugin( "test-plugin", |_, plugin_data| { if let Some(plugin_data) = plugin_data.downcast_ref::() { let about_page_greeting = plugin_data.about_page_greeting.to_string(); - vec![Template::new("about").template(move |cx| { + Ok(vec![Template::new("about").template(move |cx| { sycamore::view! { cx, p { (about_page_greeting) } } - })] + })]) } else { unreachable!() } @@ -38,6 +38,7 @@ pub fn get_test_plugin() -> Plugin { let test = "[package]\name = \"test\""; let parsed: toml::Value = toml::from_str(test).unwrap(); println!("{}", toml::to_string(&parsed).unwrap()); + Ok(()) }); actions }, diff --git a/packages/perseus/src/client.rs b/packages/perseus/src/client.rs index 0d357f337a..6a0db49767 100644 --- a/packages/perseus/src/client.rs +++ b/packages/perseus/src/client.rs @@ -1,3 +1,4 @@ +use crate::errors::PluginError; use crate::{ checkpoint, plugins::PluginAction, @@ -5,6 +6,7 @@ use crate::{ template::TemplateNodeType, }; use crate::{i18n::TranslationsManager, stores::MutableStore, PerseusAppBase}; +use fmterr::fmt_err; use std::collections::HashMap; use wasm_bindgen::JsValue; @@ -22,7 +24,6 @@ pub fn run_client( app: impl Fn() -> PerseusAppBase, ) -> Result<(), JsValue> { let mut app = app(); - let plugins = app.get_plugins(); let panic_handler = app.take_panic_handler(); checkpoint("begin"); @@ -38,11 +39,28 @@ pub fn run_client( } })); + let res = client_core(app); + if let Err(err) = res { + // This will go to the panic handler we defined above + // Unfortunately, at this stage, we really can't do anything else + panic!("plugin error: {}", fmt_err(&err)); + } + + Ok(()) +} + +/// This executes the actual underlying browser-side logic, including +/// instantiating the user's app. This is broken out due to plugin fallibility. +fn client_core( + app: PerseusAppBase, +) -> Result<(), PluginError> { + let plugins = app.get_plugins(); + plugins .functional_actions .client_actions .start - .run((), plugins.get_plugin_data()); + .run((), plugins.get_plugin_data())?; checkpoint("initial_plugins_complete"); // Get the root we'll be injecting the router into @@ -50,15 +68,15 @@ pub fn run_client( .unwrap() .document() .unwrap() - .query_selector(&format!("#{}", app.get_root())) + .query_selector(&format!("#{}", app.get_root()?)) .unwrap() .unwrap(); // Set up the properties we'll pass to the router let router_props = PerseusRouterProps { - locales: app.get_locales(), + locales: app.get_locales()?, error_pages: app.get_error_pages(), - templates: app.get_templates_map(), + templates: app.get_templates_map()?, render_cfg: get_render_cfg().expect("render configuration invalid or not injected"), pss_max_size: app.get_pss_max_size(), }; diff --git a/packages/perseus/src/engine/build.rs b/packages/perseus/src/engine/build.rs index b29c069c91..beb20d3f6a 100644 --- a/packages/perseus/src/engine/build.rs +++ b/packages/perseus/src/engine/build.rs @@ -1,9 +1,7 @@ use crate::build::{build_app, BuildProps}; +use crate::errors::Error; use crate::{ - errors::{EngineError, ServerError}, - i18n::TranslationsManager, - plugins::PluginAction, - stores::MutableStore, + errors::ServerError, i18n::TranslationsManager, plugins::PluginAction, stores::MutableStore, PerseusAppBase, SsrNode, }; use std::rc::Rc; @@ -15,29 +13,33 @@ use std::rc::Rc; /// Note that this expects to be run in the root of the project. pub async fn build( app: PerseusAppBase, -) -> Result<(), Rc> { +) -> Result<(), Rc> { let plugins = app.get_plugins(); plugins .functional_actions .build_actions .before_build - .run((), plugins.get_plugin_data()); + .run((), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; - let immutable_store = app.get_immutable_store(); + let immutable_store = app + .get_immutable_store() + .map_err(|err| Rc::new(err.into()))?; let mutable_store = app.get_mutable_store(); - let locales = app.get_locales(); + let locales = app.get_locales().map_err(|err| Rc::new(err.into()))?; // Generate the global state let gsc = app.get_global_state_creator(); let global_state = match gsc.get_build_state().await { Ok(global_state) => global_state, Err(err) => { - let err: Rc = Rc::new(ServerError::GlobalStateError(err).into()); + let err: Rc = Rc::new(ServerError::GlobalStateError(err).into()); plugins .functional_actions .build_actions .after_failed_global_state_creation - .run(err.clone(), plugins.get_plugin_data()); + .run(err.clone(), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; return Err(err); } }; @@ -45,7 +47,9 @@ pub async fn build( // Build the site for all the common locales (done in parallel) // All these parameters can be modified by `PerseusApp` and plugins, so there's // no point in having a plugin opportunity here - let templates_map = app.get_atomic_templates_map(); + let templates_map = app + .get_atomic_templates_map() + .map_err(|err| Rc::new(err.into()))?; // We have to get the translations manager last, because it consumes everything let translations_manager = app.get_translations_manager().await; @@ -61,12 +65,13 @@ pub async fn build( }) .await; if let Err(err) = res { - let err: Rc = Rc::new(err.into()); + let err: Rc = Rc::new(err.into()); plugins .functional_actions .build_actions .after_failed_build - .run(err.clone(), plugins.get_plugin_data()); + .run(err.clone(), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; Err(err) } else { @@ -74,7 +79,8 @@ pub async fn build( .functional_actions .build_actions .after_successful_build - .run((), plugins.get_plugin_data()); + .run((), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; Ok(()) } diff --git a/packages/perseus/src/engine/dflt_engine.rs b/packages/perseus/src/engine/dflt_engine.rs index 4827bdcd09..271e68792f 100644 --- a/packages/perseus/src/engine/dflt_engine.rs +++ b/packages/perseus/src/engine/dflt_engine.rs @@ -109,7 +109,13 @@ where EngineOperation::Serve => { // To reduce friction for default servers and user-made servers, we // automatically do the boilerplate that all servers would have to do - let props = get_props(app()); + let props = match get_props(app()) { + Ok(props) => props, + Err(err) => { + eprintln!("{}", fmt_err(&err)); + return 1; + } + }; // This returns a `(String, u16)` of the host and port for maximum compatibility let addr = get_host_and_port(); // In production, give the user a heads up that something's actually happening @@ -123,10 +129,12 @@ where serve_fn(props, addr).await; 0 } - EngineOperation::Tinker => { - // This is infallible (though plugins could panic) - super::engine_tinker(app()); - 0 - } + EngineOperation::Tinker => match super::engine_tinker(app()) { + Ok(_) => 0, + Err(err) => { + eprintln!("{}", fmt_err(&err)); + 1 + } + }, } } diff --git a/packages/perseus/src/engine/export.rs b/packages/perseus/src/engine/export.rs index 85bc27de00..7adb780885 100644 --- a/packages/perseus/src/engine/export.rs +++ b/packages/perseus/src/engine/export.rs @@ -23,12 +23,19 @@ use crate::{i18n::TranslationsManager, stores::MutableStore, PerseusAppBase}; /// Note that this expects to be run in the root of the project. pub async fn export( app: PerseusAppBase, -) -> Result<(), Rc> { +) -> Result<(), Rc> { let plugins = app.get_plugins(); - let static_aliases = app.get_static_aliases(); + let static_aliases = app + .get_static_aliases() + .map_err(|err| Rc::new(err.into()))?; // This won't have any trailing slashes (they're stripped by the immutable store // initializer) - let dest = format!("{}/exported", app.get_immutable_store().get_path()); + let dest = format!( + "{}/exported", + app.get_immutable_store() + .map_err(|err| Rc::new(err.into()))? + .get_path() + ); let static_dir = app.get_static_dir(); build_and_export(app).await?; @@ -40,7 +47,8 @@ pub async fn export( .functional_actions .export_actions .after_successful_export - .run((), plugins.get_plugin_data()); + .run((), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; Ok(()) } @@ -50,36 +58,42 @@ pub async fn export( /// `PerseusApp`. async fn build_and_export( app: PerseusAppBase, -) -> Result<(), Rc> { +) -> Result<(), Rc> { let plugins = app.get_plugins(); plugins .functional_actions .build_actions .before_build - .run((), plugins.get_plugin_data()); + .run((), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; - let immutable_store = app.get_immutable_store(); + let immutable_store = app + .get_immutable_store() + .map_err(|err| Rc::new(err.into()))?; // We don't need this in exporting, but the build process does let mutable_store = app.get_mutable_store(); - let locales = app.get_locales(); + let locales = app.get_locales().map_err(|err| Rc::new(err.into()))?; // Generate the global state let gsc = app.get_global_state_creator(); let global_state = match gsc.get_build_state().await { Ok(global_state) => global_state, Err(err) => { - let err: Rc = Rc::new(ServerError::GlobalStateError(err).into()); + let err: Rc = Rc::new(ServerError::GlobalStateError(err).into()); plugins .functional_actions .export_actions .after_failed_global_state_creation - .run(err.clone(), plugins.get_plugin_data()); + .run(err.clone(), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; return Err(err); } }; - let templates_map = app.get_atomic_templates_map(); + let templates_map = app + .get_atomic_templates_map() + .map_err(|err| Rc::new(err.into()))?; let index_view_str = app.get_index_view_str(); - let root_id = app.get_root(); + let root_id = app.get_root().map_err(|err| Rc::new(err.into()))?; // This consumes `self`, so we get it finally let translations_manager = app.get_translations_manager().await; @@ -97,25 +111,29 @@ async fn build_and_export( }) .await; if let Err(err) = build_res { - let err: Rc = Rc::new(err.into()); + let err: Rc = Rc::new(err.into()); plugins .functional_actions .export_actions .after_failed_build - .run(err.clone(), plugins.get_plugin_data()); + .run(err.clone(), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; return Err(err); } plugins .functional_actions .export_actions .after_successful_build - .run((), plugins.get_plugin_data()); + .run((), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; // The app has now been built, so we can safely instantiate the HTML shell // (which needs access to the render config, generated in the above build step) // It doesn't matter if the type parameters here are wrong, this function // doesn't use them let index_view = - PerseusApp::get_html_shell(index_view_str, &root_id, &immutable_store, &plugins).await; + PerseusApp::get_html_shell(index_view_str, &root_id, &immutable_store, &plugins) + .await + .map_err(|err| Rc::new(err.into()))?; // Turn the build artifacts into self-contained static files let export_res = export_app(ExportProps { templates: &templates_map, @@ -128,12 +146,13 @@ async fn build_and_export( }) .await; if let Err(err) = export_res { - let err: Rc = Rc::new(err.into()); + let err: Rc = Rc::new(err.into()); plugins .functional_actions .export_actions .after_failed_export - .run(err.clone(), plugins.get_plugin_data()); + .run(err.clone(), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; return Err(err); } @@ -152,7 +171,7 @@ fn copy_static_aliases( plugins: &Plugins, static_aliases: &HashMap, dest: &str, -) -> Result<(), Rc> { +) -> Result<(), Rc> { // Loop through any static aliases and copy them in too // Unlike with the server, these could override pages! // We'll copy from the alias to the path (it could be a directory or a file) @@ -168,12 +187,13 @@ fn copy_static_aliases( to, from: path.to_string(), }; - let err = Rc::new(err); + let err: Rc = Rc::new(err.into()); plugins .functional_actions .export_actions .after_failed_static_alias_dir_copy - .run(err.clone(), plugins.get_plugin_data()); + .run(err.clone(), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; return Err(err); } } else if let Err(err) = fs::copy(&from, &to) { @@ -182,12 +202,13 @@ fn copy_static_aliases( to, from: path.to_string(), }; - let err = Rc::new(err); + let err: Rc = Rc::new(err.into()); plugins .functional_actions .export_actions .after_failed_static_alias_file_copy - .run(err.clone(), plugins.get_plugin_data()); + .run(err.clone(), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; return Err(err); } } @@ -202,7 +223,7 @@ fn copy_static_dir( plugins: &Plugins, static_dir_raw: &str, dest: &str, -) -> Result<(), Rc> { +) -> Result<(), Rc> { // Copy the `static` directory into the export package if it exists // If the user wants extra, they can use static aliases, plugins are unnecessary // here @@ -218,12 +239,13 @@ fn copy_static_dir( path: static_dir_raw.to_string(), dest: dest.to_string(), }; - let err = Rc::new(err); + let err: Rc = Rc::new(err.into()); plugins .functional_actions .export_actions .after_failed_static_copy - .run(err.clone(), plugins.get_plugin_data()); + .run(err.clone(), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; return Err(err); } } diff --git a/packages/perseus/src/engine/export_error_page.rs b/packages/perseus/src/engine/export_error_page.rs index dd6a61b277..defe63fcd2 100644 --- a/packages/perseus/src/engine/export_error_page.rs +++ b/packages/perseus/src/engine/export_error_page.rs @@ -1,6 +1,10 @@ use crate::{ - errors::EngineError, i18n::TranslationsManager, plugins::PluginAction, - server::build_error_page, stores::MutableStore, PerseusApp, PerseusAppBase, SsrNode, + errors::{EngineError, Error}, + i18n::TranslationsManager, + plugins::PluginAction, + server::build_error_page, + stores::MutableStore, + PerseusApp, PerseusAppBase, SsrNode, }; use std::{fs, rc::Rc}; @@ -16,25 +20,30 @@ pub async fn export_error_page( app: PerseusAppBase, code: u16, output: &str, -) -> Result<(), Rc> { +) -> Result<(), Rc> { let plugins = app.get_plugins(); let error_pages = app.get_atomic_error_pages(); // Prepare the HTML shell let index_view_str = app.get_index_view_str(); - let root_id = app.get_root(); - let immutable_store = app.get_immutable_store(); + let root_id = app.get_root().map_err(|err| Rc::new(err.into()))?; + let immutable_store = app + .get_immutable_store() + .map_err(|err| Rc::new(err.into()))?; // We assume the app has already been built before running this (so the render // config must be available) It doesn't matter if the type parameters here // are wrong, this function doesn't use them let html_shell = - PerseusApp::get_html_shell(index_view_str, &root_id, &immutable_store, &plugins).await; + PerseusApp::get_html_shell(index_view_str, &root_id, &immutable_store, &plugins) + .await + .map_err(|err| Rc::new(err.into()))?; plugins .functional_actions .export_error_page_actions .before_export_error_page - .run((code, output.to_string()), plugins.get_plugin_data()); + .run((code, output.to_string()), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; // Build that error page as the server does let err_page_str = build_error_page("", code, "", None, &error_pages, &html_shell); @@ -47,12 +56,13 @@ pub async fn export_error_page( source: err, dest: output.to_string(), }; - let err = Rc::new(err); + let err: Rc = Rc::new(err.into()); plugins .functional_actions .export_error_page_actions .after_failed_write - .run(err.clone(), plugins.get_plugin_data()); + .run(err.clone(), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; return Err(err); } }; @@ -61,7 +71,8 @@ pub async fn export_error_page( .functional_actions .export_error_page_actions .after_successful_export_error_page - .run((), plugins.get_plugin_data()); + .run((), plugins.get_plugin_data()) + .map_err(|err| Rc::new(err.into()))?; Ok(()) } diff --git a/packages/perseus/src/engine/serve.rs b/packages/perseus/src/engine/serve.rs index 8a61b7b82c..15c1e4ef70 100644 --- a/packages/perseus/src/engine/serve.rs +++ b/packages/perseus/src/engine/serve.rs @@ -1,3 +1,4 @@ +use crate::errors::PluginError; use crate::i18n::TranslationsManager; use crate::plugins::PluginAction; use crate::server::{ServerOptions, ServerProps}; @@ -33,7 +34,7 @@ pub(crate) fn get_host_and_port() -> (String, u16) { /// errors! You have been warned! pub(crate) fn get_props( app: PerseusAppBase, -) -> ServerProps { +) -> Result, PluginError> { if !cfg!(debug_assertions) { let binary_loc = env::current_exe().unwrap(); let binary_dir = binary_loc.parent().unwrap(); // It's a file, there's going to be a parent if we're working on anything close @@ -47,12 +48,12 @@ pub(crate) fn get_props( .functional_actions .server_actions .before_serve - .run((), plugins.get_plugin_data()); + .run((), plugins.get_plugin_data())?; let static_dir_path = app.get_static_dir(); - let app_root = app.get_root(); - let immutable_store = app.get_immutable_store(); + let app_root = app.get_root()?; + let immutable_store = app.get_immutable_store()?; let index_view_str = app.get_index_view_str(); // By the time this binary is being run, the app has already been built be the // CLI (hopefully!), so we can depend on access to the render config @@ -61,7 +62,7 @@ pub(crate) fn get_props( &app_root, &immutable_store, &plugins, - )); + ))?; let opts = ServerOptions { // We don't support setting some attributes from `wasm-pack` through plugins/`PerseusApp` @@ -73,8 +74,8 @@ pub(crate) fn get_props( // This probably won't exist, but on the off chance that the user needs to support older // browsers, we'll provide it anyway wasm_js_bundle: "dist/pkg/perseus_engine_bg.wasm.js".to_string(), - templates_map: app.get_atomic_templates_map(), - locales: app.get_locales(), + templates_map: app.get_atomic_templates_map()?, + locales: app.get_locales()?, root_id: app_root, snippets: "dist/pkg/snippets".to_string(), error_pages: app.get_atomic_error_pages(), @@ -84,14 +85,14 @@ pub(crate) fn get_props( } else { None }, - static_aliases: app.get_static_aliases(), + static_aliases: app.get_static_aliases()?, }; - ServerProps { + Ok(ServerProps { opts, immutable_store, mutable_store: app.get_mutable_store(), global_state_creator: app.get_global_state_creator(), translations_manager: block_on(app.get_translations_manager()), - } + }) } diff --git a/packages/perseus/src/engine/tinker.rs b/packages/perseus/src/engine/tinker.rs index 9914e0246f..6be54904d9 100644 --- a/packages/perseus/src/engine/tinker.rs +++ b/packages/perseus/src/engine/tinker.rs @@ -1,10 +1,13 @@ +use crate::errors::PluginError; use crate::{i18n::TranslationsManager, stores::MutableStore}; use crate::{plugins::PluginAction, PerseusAppBase, SsrNode}; /// Runs tinker plugin actions. /// /// Note that this expects to be run in the root of the project. -pub fn tinker(app: PerseusAppBase) { +pub fn tinker( + app: PerseusAppBase, +) -> Result<(), PluginError> { let plugins = app.get_plugins(); // Run all the tinker actions // Note: this is deliberately synchronous, tinker actions that need a @@ -12,5 +15,7 @@ pub fn tinker(app: PerseusAppBase, } /// Errors that can occur in the server-side engine system (responsible for diff --git a/packages/perseus/src/init.rs b/packages/perseus/src/init.rs index 3a0a96b519..77a1cf697b 100644 --- a/packages/perseus/src/init.rs +++ b/packages/perseus/src/init.rs @@ -1,3 +1,4 @@ +use crate::errors::PluginError; #[cfg(not(target_arch = "wasm32"))] use crate::server::{get_render_cfg, HtmlShell}; use crate::stores::ImmutableStore; @@ -602,13 +603,15 @@ impl PerseusAppBase { // Getters /// Gets the HTML ID of the `
` at which to insert Perseus. - pub fn get_root(&self) -> String { - self.plugins + pub fn get_root(&self) -> Result { + let root = self + .plugins .control_actions .settings_actions .set_app_root - .run((), self.plugins.get_plugin_data()) - .unwrap_or_else(|| self.root.to_string()) + .run((), self.plugins.get_plugin_data())? + .unwrap_or_else(|| self.root.to_string()); + Ok(root) } /// Gets the directory containing static assets to be hosted under the URL /// `/.perseus/static/`. @@ -642,7 +645,7 @@ impl PerseusAppBase { root: &str, immutable_store: &ImmutableStore, plugins: &Plugins, - ) -> HtmlShell { + ) -> Result { // Construct an HTML shell let mut html_shell = HtmlShell::new( index_view_str, @@ -662,7 +665,7 @@ impl PerseusAppBase { .settings_actions .html_shell_actions .set_shell - .run((), plugins.get_plugin_data()) + .run((), plugins.get_plugin_data())? .unwrap_or(html_shell.shell); html_shell.shell = shell_str; // For convenience, we alias the HTML shell functional actions @@ -676,7 +679,7 @@ impl PerseusAppBase { html_shell.head_before_boundary.push( hsf_actions .add_to_head_before_boundary - .run((), plugins.get_plugin_data()) + .run((), plugins.get_plugin_data())? .values() .flatten() .cloned() @@ -685,7 +688,7 @@ impl PerseusAppBase { html_shell.scripts_before_boundary.push( hsf_actions .add_to_scripts_before_boundary - .run((), plugins.get_plugin_data()) + .run((), plugins.get_plugin_data())? .values() .flatten() .cloned() @@ -694,7 +697,7 @@ impl PerseusAppBase { html_shell.head_after_boundary.push( hsf_actions .add_to_head_after_boundary - .run((), plugins.get_plugin_data()) + .run((), plugins.get_plugin_data())? .values() .flatten() .cloned() @@ -703,7 +706,7 @@ impl PerseusAppBase { html_shell.scripts_after_boundary.push( hsf_actions .add_to_scripts_after_boundary - .run((), plugins.get_plugin_data()) + .run((), plugins.get_plugin_data())? .values() .flatten() .cloned() @@ -712,7 +715,7 @@ impl PerseusAppBase { html_shell.before_content.push( hsf_actions .add_to_before_content - .run((), plugins.get_plugin_data()) + .run((), plugins.get_plugin_data())? .values() .flatten() .cloned() @@ -721,18 +724,18 @@ impl PerseusAppBase { html_shell.after_content.push( hsf_actions .add_to_after_content - .run((), plugins.get_plugin_data()) + .run((), plugins.get_plugin_data())? .values() .flatten() .cloned() .collect(), ); - html_shell + Ok(html_shell) } /// Gets the templates in an `Rc`-based `HashMap` for non-concurrent access. #[cfg(target_arch = "wasm32")] - pub fn get_templates_map(&self) -> TemplateMap { + pub fn get_templates_map(&self) -> Result, PluginError> { // One the browser-side, this is already a `TemplateMap` internally let mut map = self.templates.clone(); @@ -742,7 +745,7 @@ impl PerseusAppBase { .functional_actions .settings_actions .add_templates - .run((), self.plugins.get_plugin_data()); + .run((), self.plugins.get_plugin_data())?; for (_plugin_name, plugin_templates) in extra_templates { // Turn that vector into a template map by extracting the template root paths as // keys @@ -751,12 +754,12 @@ impl PerseusAppBase { } } - map + Ok(map) } /// Gets the templates in an `Arc`-based `HashMap` for concurrent access. /// This should only be relevant on the server-side. #[cfg(not(target_arch = "wasm32"))] - pub fn get_atomic_templates_map(&self) -> ArcTemplateMap { + pub fn get_atomic_templates_map(&self) -> Result, PluginError> { // One the engine-side, this is already an `ArcTemplateMap` internally let mut map = self.templates.clone(); @@ -766,7 +769,7 @@ impl PerseusAppBase { .functional_actions .settings_actions .add_templates - .run((), self.plugins.get_plugin_data()); + .run((), self.plugins.get_plugin_data())?; for (_plugin_name, plugin_templates) in extra_templates { // Turn that vector into a template map by extracting the template root paths as // keys @@ -775,7 +778,7 @@ impl PerseusAppBase { } } - map + Ok(map) } /// Gets the [`ErrorPages`] used in the app. This returns an `Rc`. #[cfg(target_arch = "wasm32")] @@ -799,14 +802,16 @@ impl PerseusAppBase { self.global_state_creator.clone() } /// Gets the locales information. - pub fn get_locales(&self) -> Locales { + pub fn get_locales(&self) -> Result { let locales = self.locales.clone(); - self.plugins + let locales = self + .plugins .control_actions .settings_actions .set_locales - .run(locales.clone(), self.plugins.get_plugin_data()) - .unwrap_or(locales) + .run(locales.clone(), self.plugins.get_plugin_data())? + .unwrap_or(locales); + Ok(locales) } /// Gets the server-side [`TranslationsManager`]. Like the mutable store, /// this can't be modified by plugins due to trait complexities. @@ -822,14 +827,16 @@ impl PerseusAppBase { } /// Gets the [`ImmutableStore`]. #[cfg(not(target_arch = "wasm32"))] - pub fn get_immutable_store(&self) -> ImmutableStore { + pub fn get_immutable_store(&self) -> Result { let immutable_store = self.immutable_store.clone(); - self.plugins + let immutable_store = self + .plugins .control_actions .settings_actions .set_immutable_store - .run(immutable_store.clone(), self.plugins.get_plugin_data()) - .unwrap_or(immutable_store) + .run(immutable_store.clone(), self.plugins.get_plugin_data())? + .unwrap_or(immutable_store); + Ok(immutable_store) } /// Gets the [`MutableStore`]. This can't be modified by plugins due to /// trait complexities, so plugins should instead expose a function that @@ -850,7 +857,7 @@ impl PerseusAppBase { /// accidentally serve an arbitrary in a production environment where a path /// may point to somewhere evil, like an alias to `/etc/passwd`). #[cfg(not(target_arch = "wasm32"))] - pub fn get_static_aliases(&self) -> HashMap { + pub fn get_static_aliases(&self) -> Result, PluginError> { let mut static_aliases = self.static_aliases.clone(); // This will return a map of plugin name to another map of static aliases that // that plugin produced @@ -859,7 +866,7 @@ impl PerseusAppBase { .functional_actions .settings_actions .add_static_aliases - .run((), self.plugins.get_plugin_data()); + .run((), self.plugins.get_plugin_data())?; for (_plugin_name, aliases) in extra_static_aliases { let new_aliases: HashMap = aliases .iter() @@ -893,7 +900,7 @@ impl PerseusAppBase { scoped_static_aliases.insert(url, new_path); } - scoped_static_aliases + Ok(scoped_static_aliases) } /// Takes the user-set panic handler out and returns it as an owned value, /// allowing it to be used as an actual panic hook. diff --git a/packages/perseus/src/plugins/action.rs b/packages/perseus/src/plugins/action.rs index 7abe0d5d20..fd5625668e 100644 --- a/packages/perseus/src/plugins/action.rs +++ b/packages/perseus/src/plugins/action.rs @@ -1,15 +1,36 @@ use std::any::Any; use std::collections::HashMap; -/// A runner function, which takes action data and plugin data. -pub type Runner = Box R + Send>; +use crate::errors::PluginError; + +/// A runner function, which takes action data and plugin data, returning a +/// `Result>`. +// A: some stuff the specific action gets +// dyn Any + Send: the plugin options +// R: the return type +pub type Runner = + Box Result> + Send>; /// A trait for the interface for a plugin action, which abstracts whether it's /// a functional or a control action. +/// +/// `R2` here denotes the return type of the entire plugin series. For instance, +/// functional plugins return a `HashMap` of the results of each plugin. pub trait PluginAction: Send { /// Runs the action. This takes data that the action should expect, along /// with a map of plugins to their data. - fn run(&self, action_data: A, plugin_data: &HashMap>) -> R2; + /// + /// If any of the underlying plugins whose runners are executed by this + /// function return an error, the first error will be returned + /// immediately, and further execution will be aborted. Since + /// execution may happen in an arbitrary order, there is no guarantee that + /// the same error will be thrown each time if multiple plugins are + /// being used. + fn run( + &self, + action_data: A, + plugin_data: &HashMap>, + ) -> Result; /// Registers a plugin that takes this action. /// /// # Panics @@ -21,7 +42,7 @@ pub trait PluginAction: Send { fn register_plugin( &mut self, name: &str, - runner: impl Fn(&A, &(dyn Any + Send)) -> R + Send + 'static, + runner: impl Fn(&A, &(dyn Any + Send)) -> Result> + Send + 'static, ); /// Same as `.register_plugin()`, but takes a prepared runner in a `Box`. fn register_plugin_box(&mut self, name: &str, runner: Runner); diff --git a/packages/perseus/src/plugins/control.rs b/packages/perseus/src/plugins/control.rs index 2349623ccd..161bed507c 100644 --- a/packages/perseus/src/plugins/control.rs +++ b/packages/perseus/src/plugins/control.rs @@ -1,3 +1,4 @@ +use crate::errors::PluginError; use crate::plugins::*; use std::any::Any; use std::collections::HashMap; @@ -15,26 +16,38 @@ pub struct ControlPluginAction { } impl PluginAction> for ControlPluginAction { /// Runs the single registered runner for the action. - fn run(&self, action_data: A, plugin_data: &HashMap>) -> Option { + fn run( + &self, + action_data: A, + plugin_data: &HashMap>, + ) -> Result, PluginError> { // If no runner is defined, this won't have any effect (same as functional // actions with no registered runners) - self.runner.as_ref().map(|runner| { - runner( - &action_data, - // We must have data registered for every active plugin (even if it's empty) - &**plugin_data.get(&self.controller_name).unwrap_or_else(|| { - panic!( - "no plugin data for registered plugin {}", - &self.controller_name - ) - }), - ) - }) + self.runner + .as_ref() + .map(|runner| { + runner( + &action_data, + // We must have data registered for every active plugin (even if it's empty) + &**plugin_data.get(&self.controller_name).unwrap_or_else(|| { + panic!( + "no plugin data for registered plugin {}", + &self.controller_name + ) + }), + ) + .map_err(|err| PluginError { + name: self.controller_name.to_string(), + source: err, + }) + }) + // Turn `Option>` -> `Result, E>` + .transpose() } fn register_plugin( &mut self, name: &str, - runner: impl Fn(&A, &(dyn Any + Send)) -> R + Send + 'static, + runner: impl Fn(&A, &(dyn Any + Send)) -> Result> + Send + 'static, ) { self.register_plugin_box(name, Box::new(runner)) } diff --git a/packages/perseus/src/plugins/functional.rs b/packages/perseus/src/plugins/functional.rs index 5b44fb25f1..f0008e970f 100644 --- a/packages/perseus/src/plugins/functional.rs +++ b/packages/perseus/src/plugins/functional.rs @@ -1,6 +1,7 @@ use super::*; #[cfg(not(target_arch = "wasm32"))] -use crate::errors::EngineError; +use crate::errors::Error; +use crate::errors::PluginError; use crate::Html; use std::any::Any; use std::collections::HashMap; @@ -18,7 +19,7 @@ impl PluginAction> for FunctionalPluginAction>, - ) -> HashMap { + ) -> Result, PluginError> { let mut returns = HashMap::new(); for (plugin_name, runner) in &self.runners { let ret = runner( @@ -27,16 +28,20 @@ impl PluginAction> for FunctionalPluginAction R + Send + 'static, + runner: impl Fn(&A, &(dyn Any + Send)) -> Result> + Send + 'static, ) { self.register_plugin_box(name, Box::new(runner)) } @@ -181,9 +186,9 @@ pub struct FunctionalPluginBuildActions { /// Runs after the build process if it completes successfully. pub after_successful_build: FunctionalPluginAction<(), ()>, /// Runs after the build process if it fails. - pub after_failed_build: FunctionalPluginAction, ()>, + pub after_failed_build: FunctionalPluginAction, ()>, /// Runs after the build process if it failed to generate global state. - pub after_failed_global_state_creation: FunctionalPluginAction, ()>, + pub after_failed_global_state_creation: FunctionalPluginAction, ()>, } /// Functional actions that pertain to the export process. #[cfg(not(target_arch = "wasm32"))] @@ -195,25 +200,25 @@ pub struct FunctionalPluginExportActions { /// successfully. pub after_successful_build: FunctionalPluginAction<(), ()>, /// Runs after the build stage in the export process if it fails. - pub after_failed_build: FunctionalPluginAction, ()>, + pub after_failed_build: FunctionalPluginAction, ()>, /// Runs after the export process if it fails. - pub after_failed_export: FunctionalPluginAction, ()>, + pub after_failed_export: FunctionalPluginAction, ()>, /// Runs if copying the static directory failed. - pub after_failed_static_copy: FunctionalPluginAction, ()>, + pub after_failed_static_copy: FunctionalPluginAction, ()>, /// Runs if copying a static alias that was a directory failed. The argument /// to this is a tuple of the from and to locations of the copy, along with /// the error. - pub after_failed_static_alias_dir_copy: FunctionalPluginAction, ()>, + pub after_failed_static_alias_dir_copy: FunctionalPluginAction, ()>, /// Runs if copying a static alias that was a file failed. The argument to /// this is a tuple of the from and to locations of the copy, along with the /// error. - pub after_failed_static_alias_file_copy: FunctionalPluginAction, ()>, + pub after_failed_static_alias_file_copy: FunctionalPluginAction, ()>, /// Runs after the export process if it completes successfully. pub after_successful_export: FunctionalPluginAction<(), ()>, /// Runs after the export process if it failed to generate global state. /// Note that the error here will always be a `GlobalStateError`, but it /// must be processed as a `ServerError` due to ownership constraints. - pub after_failed_global_state_creation: FunctionalPluginAction, ()>, + pub after_failed_global_state_creation: FunctionalPluginAction, ()>, } /// Functional actions that pertain to the process of exporting an error page. #[cfg(not(target_arch = "wasm32"))] @@ -226,7 +231,7 @@ pub struct FunctionalPluginExportErrorPageActions { /// Runs after a error page was exported successfully. pub after_successful_export_error_page: FunctionalPluginAction<(), ()>, /// Runs if writing to the output file failed. Error and filename are given. - pub after_failed_write: FunctionalPluginAction, ()>, + pub after_failed_write: FunctionalPluginAction, ()>, } /// Functional actions that pertain to the server. #[derive(Default, Debug)]