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 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}