Skip to content

Commit

Permalink
feat(plugins): ✨ added client privileged plugins
Browse files Browse the repository at this point in the history
This eliminates tinker-only plugins in favor of a solution that actually works.
  • Loading branch information
arctic-hen7 committed Oct 30, 2021
1 parent d378e0f commit 686f369
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 29 deletions.
1 change: 1 addition & 0 deletions examples/plugins/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ perseus = { path = "../../packages/perseus" }
sycamore = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.5"

[dev-dependencies]
fantoccini = "0.17"
Expand Down
5 changes: 2 additions & 3 deletions examples/plugins/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ mod error_pages;
mod plugin;
mod templates;

use perseus::define_app;
use perseus::Plugins;
use perseus::{define_app, Plugins};

define_app! {
templates: [
Expand All @@ -12,7 +11,7 @@ define_app! {
],
error_pages: crate::error_pages::get_error_pages(),
plugins: Plugins::new()
.plugin(plugin::get_test_plugin(), plugin::TestPluginData {
.plugin_with_client_privilege(plugin::get_test_plugin, plugin::TestPluginData {
about_page_greeting: "Hey from a plugin!".to_string()
})
}
8 changes: 6 additions & 2 deletions examples/plugins/src/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use perseus::plugins::{empty_control_actions_registrar, Plugin, PluginAction};
use perseus::plugins::{empty_control_actions_registrar, Plugin, PluginAction, PluginEnv};
use perseus::Template;

#[derive(Debug)]
Expand Down Expand Up @@ -37,10 +37,14 @@ pub fn get_test_plugin<G: perseus::GenericNode>() -> Plugin<G, TestPluginData> {
);
actions.tinker.register_plugin("test-plugin", |_, _| {
println!("{:?}", std::env::current_dir().unwrap());
// This is completely pointless, but demonstrates how plugin dependencies can blow up binary sizes if they aren't made tinker-only plugins
let test = "[package]\nname = \"test\"";
let parsed: toml::Value = toml::from_str(test).unwrap();
println!("{}", toml::to_string(&parsed).unwrap());
});
actions
},
empty_control_actions_registrar,
false, // We'd set this to `true` if we only wanted the plugin to run at tinker-time
PluginEnv::Both,
)
}
21 changes: 17 additions & 4 deletions packages/perseus/src/plugins/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ type FunctionalActionsRegistrar<G> =
Box<dyn Fn(FunctionalPluginActions<G>) -> FunctionalPluginActions<G>>;
type ControlActionsRegistrar = Box<dyn Fn(ControlPluginActions) -> ControlPluginActions>;

/// The environments a plugin can run in. These will affect Wasm bundle size.
#[derive(PartialEq, Eq)]
pub enum PluginEnv {
/// The plugin should only run on the client-side, and will be included in the final Wasm binary. More specifically, the plugin
/// will only be included if the target architecture is `wasm32`.
Client,
/// The plugin should only run on the server-side (this includes tinker-time and the build process), and will NOT be included in the
/// final Wasm binary. This will decrease binary sizes, and should be preferred in most cases.
Server,
/// The plugin will ruin everywhere, and will be included in the final Wasm binary.
Both,
}

/// A Perseus plugin. This must be exported by all plugin crates so the user can register the plugin easily.
pub struct Plugin<G: GenericNode, D: Any> {
/// The machine name of the plugin, which will be used as a key in a HashMap with many other plugins. This should be the public
Expand All @@ -18,8 +31,8 @@ pub struct Plugin<G: GenericNode, D: Any> {
/// A function that will be provided control actions. It should then register runners from the plugin for every action
/// that it takes.
pub control_actions_registrar: ControlActionsRegistrar,
/// Whether or not the plugin is tinker-only, in which case it will only be loaded at tinker-time, and will not be sent to the client.
pub is_tinker_only: bool,
/// The environment that the plugin should run in.
pub env: PluginEnv,

plugin_data_type: PhantomData<D>,
}
Expand All @@ -30,13 +43,13 @@ impl<G: GenericNode, D: Any> Plugin<G, D> {
functional_actions_registrar: impl Fn(FunctionalPluginActions<G>) -> FunctionalPluginActions<G>
+ 'static,
control_actions_registrar: impl Fn(ControlPluginActions) -> ControlPluginActions + 'static,
is_tinker_only: bool,
env: PluginEnv,
) -> Self {
Self {
name: name.to_string(),
functional_actions_registrar: Box::new(functional_actions_registrar),
control_actions_registrar: Box::new(control_actions_registrar),
is_tinker_only,
env,
plugin_data_type: PhantomData::default(),
}
}
Expand Down
64 changes: 44 additions & 20 deletions packages/perseus/src/plugins/plugins_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,23 @@ impl<G: GenericNode> Plugins<G> {
Self::default()
}
/// Registers a new plugin, consuming `self`. For control actions, this will check if a plugin has already registered on an action,
/// and throw an error if one has, noting the conflict explicitly in the error message.
pub fn plugin<D: Any>(mut self, plugin: Plugin<G, D>, plugin_data: D) -> Self {
// Check if the plugin is tinker-only
if plugin.is_tinker_only {
// If we're not at tinker-time, no tinker plugin should be registered (avoid larger bundles)
#[cfg(feature = "tinker-plugins")]
{
// Insert the plugin data
let plugin_data: Box<dyn Any> = Box::new(plugin_data);
let res = self.plugin_data.insert(plugin.name.clone(), plugin_data);
// If there was an old value, there are two plugins with the same name, which is very bad (arbitrarily inconsistent behavior overriding)
if res.is_some() {
panic!("two plugins have the same name '{}', which could lead to arbitrary and inconsistent behavior modification (please file an issue with the plugin that doesn't have the same name as its crate)", &plugin.name);
}
// Register functional actions using the plugin's provided registrar
// We don't need to do control actions because `tinker` is a functional action
self.functional_actions =
(plugin.functional_actions_registrar)(self.functional_actions);
};
} else {
/// and throw an error if one has, noting the conflict explicitly in the error message. This can only register plugins that run
/// exclusively on the server-side (including tinker-time and the build process).
pub fn plugin<D: Any>(
mut self,
// This is a function so that it never gets called if we're compiling for Wasm, which means Rust eliminates it as dead code!
plugin: impl Fn() -> Plugin<G, D>,
plugin_data: D,
) -> Self {
// If we're compiling for Wasm, plugins that don't run on the client side shouldn't be added (they'll then be eliminated as dead code)
#[cfg(not(target_arch = "wasm32"))]
{
let plugin = plugin();
// If the plugin can run on the client-side, it should use `.client_plugin()` for verbosity
// We don;t have access to the actual plugin data until now, so we can;t do this at the root of the function
if plugin.env != PluginEnv::Server {
panic!("attempted to register plugin that can run on the client with `.plugin()`, this plugin should be registered with `.plugin_with_client_privilege()` (this will increase your final bundle size)")
}
// Insert the plugin data
let plugin_data: Box<dyn Any> = Box::new(plugin_data);
let res = self.plugin_data.insert(plugin.name.clone(), plugin_data);
Expand All @@ -66,6 +63,33 @@ impl<G: GenericNode> Plugins<G> {

self
}
/// The same as `.plugin()`, but registers a plugin that can run on the client-side. This is deliberately separated out to make
/// conditional compilation feasible and to emphasize to users what's incrasing their bundle sizes. Note that this should also
/// be used for plugins that run on both the client and server.
pub fn plugin_with_client_privilege<D: Any>(
mut self,
// This is a function to preserve a similar API interface with `.plugin()`
plugin: impl Fn() -> Plugin<G, D>,
plugin_data: D,
) -> Self {
let plugin = plugin();
// If the plugin doesn't need client privileges, it shouldn't have them (even though this is just a semantic thing)
if plugin.env == PluginEnv::Server {
panic!("attempted to register plugin that doesn't ever run on the client with `.plugin_with_client_privilege()`, you should use `.plugin()` instead")
}
// Insert the plugin data
let plugin_data: Box<dyn Any> = Box::new(plugin_data);
let res = self.plugin_data.insert(plugin.name.clone(), plugin_data);
// If there was an old value, there are two plugins with the same name, which is very bad (arbitrarily inconsistent behavior overriding)
if res.is_some() {
panic!("two plugins have the same name '{}', which could lead to arbitrary and inconsistent behavior modification (please file an issue with the plugin that doesn't have the same name as its crate)", &plugin.name);
}
// Register functional and control actions using the plugin's provided registrar
self.functional_actions = (plugin.functional_actions_registrar)(self.functional_actions);
self.control_actions = (plugin.control_actions_registrar)(self.control_actions);

self
}
/// Gets a reference to the map of plugin data. Note that each element of plugin data is additionally `Box`ed.
pub fn get_plugin_data(&self) -> &PluginDataMap {
&self.plugin_data
Expand Down

0 comments on commit 686f369

Please sign in to comment.