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}