Skip to main content

autumn_plugin_react/
plugin.rs

1//! [`ReactPlugin`]: installs the loader and the app bundles.
2
3use std::borrow::Cow;
4
5use autumn_web::app::AppBuilder;
6use autumn_web::assets::PluginAssets;
7use autumn_web::plugin::Plugin;
8use autumn_web::plugin_contract::PluginContract;
9
10use crate::assets::REACT_ASSETS;
11
12/// The crate name. [`ReactPlugin`] with no app bundles uses it as its
13/// plugin name. Each [`ReactPlugin`] declares it in its contract.
14pub const PLUGIN_NAME: &str = env!("CARGO_PKG_NAME");
15
16/// Installs React islands in an Autumn app.
17///
18/// It serves the loader ([`REACT_ASSETS`]) and each app bundle under
19/// `/static/_plugins/<namespace>/`. It reads no configuration.
20///
21/// ```rust,no_run
22/// use autumn_plugin_react::ReactPlugin;
23/// use autumn_web::assets::PluginAssets;
24///
25/// static ISLANDS: PluginAssets =
26///     PluginAssets::from_files("app-islands", &[("islands.js", b"/* build */")]);
27///
28/// # async fn run() {
29/// autumn_web::app()
30///     .plugin(ReactPlugin::new().bundle(&ISLANDS))
31///     .run()
32///     .await;
33/// # }
34/// ```
35///
36/// The plugin name includes each bundle namespace and a fingerprint of its
37/// files, for example `autumn-plugin-react[app-islands@1a2b3c4d]`. Thus a
38/// library crate and the app can each install a `ReactPlugin` with their
39/// own bundles. Autumn skips a second plugin with the same bundles. Two
40/// different bundles with one namespace stop the app at start-up.
41#[derive(Debug, Default)]
42#[must_use]
43pub struct ReactPlugin {
44    bundles: Vec<&'static PluginAssets>,
45}
46
47impl ReactPlugin {
48    /// Makes the plugin with no app bundles.
49    pub const fn new() -> Self {
50        Self {
51            bundles: Vec::new(),
52        }
53    }
54
55    /// Adds an app bundle of compiled React components.
56    ///
57    /// The plugin uses a bundle one time, also when you add it two times.
58    ///
59    /// # Panics
60    ///
61    /// Panics when the bundle namespace is `react`. The loader bundle uses
62    /// it. Autumn also stops at start-up when two different bundles use one
63    /// namespace.
64    pub fn bundle(mut self, bundle: &'static PluginAssets) -> Self {
65        assert!(
66            bundle.namespace() != REACT_ASSETS.namespace(),
67            "the namespace `react` belongs to {PLUGIN_NAME}; give the app bundle another namespace"
68        );
69        if !self.bundles.iter().any(|b| std::ptr::eq(*b, bundle)) {
70            self.bundles.push(bundle);
71        }
72        self
73    }
74}
75
76/// `namespace@fingerprint`. The fingerprint is FNV-1a (32 bit) over the
77/// hashed URLs of the files, so it changes when any file changes.
78fn bundle_id(bundle: &PluginAssets) -> String {
79    let mut hash: u32 = 0x811c_9dc5;
80    for asset in bundle.iter() {
81        for byte in asset.url().bytes().chain([0]) {
82            hash ^= u32::from(byte);
83            hash = hash.wrapping_mul(0x0100_0193);
84        }
85    }
86    format!("{}@{hash:08x}", bundle.namespace())
87}
88
89impl Plugin for ReactPlugin {
90    fn name(&self) -> Cow<'static, str> {
91        if self.bundles.is_empty() {
92            return Cow::Borrowed(PLUGIN_NAME);
93        }
94        let mut ids: Vec<String> = self.bundles.iter().map(|b| bundle_id(b)).collect();
95        ids.sort_unstable();
96        ids.dedup();
97        Cow::Owned(format!("{PLUGIN_NAME}[{}]", ids.join(",")))
98    }
99
100    fn contract(&self) -> Option<PluginContract> {
101        Some(
102            PluginContract::new(PLUGIN_NAME)
103                .plugin_version(env!("CARGO_PKG_VERSION"))
104                .autumn_web("0.10"),
105        )
106    }
107
108    fn build(self, app: AppBuilder) -> AppBuilder {
109        self.bundles
110            .into_iter()
111            .fold(app.plugin_assets(&REACT_ASSETS), AppBuilder::plugin_assets)
112    }
113}