Skip to main content

sofie_http/
islands.rs

1use http::Method;
2use sofie_core::{Error, Result};
3use sofie_engine::islands::{IslandBundle, IslandLinks, is_island_name, island_screen};
4
5use crate::handler::{Loader, loader_endpoint};
6use crate::routes::{RouteRules, ScreenView, View};
7use crate::{Area, PageRoute, Routes, Screens, screen_key};
8
9pub(crate) struct IslandScreen {
10    pub(crate) key: String,
11    pub(crate) tag: String,
12}
13
14impl<A: Area> Routes<A> {
15    /// A module wraps the island in a page with `screens/<name>.html` holding `<Island … />`.
16    pub fn island<Args: 'static, H: Loader<A, Args>>(
17        &mut self,
18        path: &str,
19        name: &str,
20        loader: H,
21    ) -> PageRoute<'_, A> {
22        let key = screen_key(self.module, A::NAME, name);
23        self.islands.push(IslandScreen {
24            key: key.clone(),
25            tag: name.to_owned(),
26        });
27        let view = View::Screen(ScreenView {
28            key,
29            title: sofie_engine::humanize(&name.replace('-', "_")),
30            #[cfg(feature = "resources")]
31            builtin: None,
32        });
33        PageRoute::new(self.push(
34            Method::GET,
35            path,
36            loader_endpoint(loader),
37            view,
38            RouteRules::default(),
39        ))
40    }
41}
42
43#[derive(Default)]
44pub(crate) struct Islands {
45    pub(crate) bundles: Vec<IslandBundle>,
46}
47
48impl Islands {
49    pub(crate) fn links(&self, area: &str) -> Option<IslandLinks> {
50        #[cfg(all(feature = "development", debug_assertions))]
51        if let Some(links) = crate::development::island_links(area) {
52            return Some(links);
53        }
54        self.bundles
55            .iter()
56            .find(|bundle| bundle.area == area)
57            .map(IslandLinks::of)
58    }
59}
60
61impl crate::assets::Assets {
62    pub(crate) fn with_islands(mut self, screens: &Screens) -> Self {
63        for bundle in &screens.islands.bundles {
64            for asset in [bundle.script, bundle.stylesheet] {
65                self.served.push((asset.path(), asset));
66            }
67        }
68        self
69    }
70}
71
72impl Screens {
73    pub fn islands(&mut self, bundles: Vec<IslandBundle>) -> &mut Self {
74        self.islands.bundles = bundles;
75        self
76    }
77}
78
79pub(crate) fn install(screens: &mut Screens, islands: &[IslandScreen]) -> Result<()> {
80    for island in islands {
81        if !is_island_name(&island.tag) {
82            return Err(Error::internal_message(format!(
83                "screen {} names island `{}`, which is no custom element name: name \
84                 `islands/<name>.svelte` in lowercase with a hyphen, such as `order-chart`",
85                island.key, island.tag
86            )));
87        }
88        if screens.contains(&island.key) {
89            continue;
90        }
91        screens.add_with(&island.key, || {
92            island_screen(&island.key, &island.tag)
93                .map_err(|error| Error::internal(format!("building screen {}", island.key), error))
94        })?;
95    }
96    Ok(())
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn an_island_page_renders_the_island_unless_the_module_wraps_it() {
105        let mut screens = crate::screens_from_sources(&[(
106            "shop/web/wrapped",
107            "<main><Island name=\"wrapped-chart\" props={data} /></main>",
108        )])
109        .unwrap();
110        let islands = [
111            IslandScreen {
112                key: "shop/web/order-chart".to_owned(),
113                tag: "order-chart".to_owned(),
114            },
115            IslandScreen {
116                key: "shop/web/wrapped".to_owned(),
117                tag: "wrapped-chart".to_owned(),
118            },
119        ];
120
121        install(&mut screens, &islands).unwrap();
122
123        let data = serde_json::json!({"count": 1});
124        let page = screens
125            .template("shop/web/order-chart")
126            .unwrap()
127            .render(&data)
128            .unwrap();
129        assert_eq!(
130            page,
131            "<order-chart><script type=\"application/json\">{\"count\":1}</script></order-chart>"
132        );
133        let wrapped = screens
134            .template("shop/web/wrapped")
135            .unwrap()
136            .render(&data)
137            .unwrap();
138        assert!(wrapped.starts_with("<main><wrapped-chart>"), "{wrapped}");
139    }
140
141    #[test]
142    fn an_island_needs_a_custom_element_name() {
143        let error = install(
144            &mut Screens::default(),
145            &[IslandScreen {
146                key: "shop/web/chart".to_owned(),
147                tag: "chart".to_owned(),
148            }],
149        )
150        .unwrap_err();
151
152        assert!(
153            error.to_string().contains("names island `chart`"),
154            "{error}"
155        );
156    }
157
158    #[test]
159    fn a_page_loads_the_bundle_of_its_area_only() {
160        let bundle = IslandBundle::new("member", b"export {};", b"");
161        let islands = Islands {
162            bundles: vec![bundle],
163        };
164
165        assert_eq!(islands.links("member"), Some(IslandLinks::of(&bundle)));
166        assert_eq!(islands.links("console"), None);
167    }
168}