1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
use crate::plugins::PluginAction;
use crate::server::{get_render_cfg, HtmlShell};
use crate::utils::get_path_prefix_server;
use crate::{
    i18n::{FsTranslationsManager, Locales, TranslationsManager},
    state::GlobalStateCreator,
    stores::{FsMutableStore, ImmutableStore, MutableStore},
    templates::TemplateMap,
    ErrorPages, Html, Plugins, Template,
};
use futures::Future;
use std::pin::Pin;
use std::{collections::HashMap, rc::Rc};
use sycamore::{
    prelude::{component, view},
    view::View,
    SsrNode,
};

/// The default index view, because some simple apps won't need anything fancy here. The user should be able to provide the smallest possible amount of information for their app to work.
static DFLT_INDEX_VIEW: &str = r#"<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    </head>
    <body>
        <div id="root"></div>
    </body>
</html>"#;

// This is broken out for debug implementation ease
struct TemplateGetters<G: Html>(Vec<Box<dyn Fn() -> Template<G>>>);
impl<G: Html> std::fmt::Debug for TemplateGetters<G> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TemplateGetters").finish()
    }
}
// This is broken out for debug implementation ease
struct ErrorPagesGetter<G: Html>(Box<dyn Fn() -> ErrorPages<G>>);
impl<G: Html> std::fmt::Debug for ErrorPagesGetter<G> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ErrorPagesGetters").finish()
    }
}

/// The different types of translations managers that can be stored. This allows us to store dummy translations managers directly, without holding futures. If this stores a full
/// translations manager though, it will store it as a `Future`, which is later evaluated.
enum Tm<T: TranslationsManager> {
    Dummy(T),
    Full(Pin<Box<dyn Future<Output = T>>>),
}
impl<T: TranslationsManager> std::fmt::Debug for Tm<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Tm").finish()
    }
}

/// An automatically implemented trait for asynchronous functions that return instances of `TranslationsManager`. This is needed so we can store the 'promise' of getting a translations
/// manager in future by executing a stored asynchronous function (because we don't want to take in the actual value, which would require asynchronous initialization functions, which we
/// can't have in environments like the browser).
#[doc(hidden)]
pub trait TranslationsManagerGetter {
    type Output: TranslationsManager;
    fn call(&self) -> Box<dyn Future<Output = Self::Output>>;
}
impl<T, F, Fut> TranslationsManagerGetter for F
where
    T: TranslationsManager,
    F: Fn() -> Fut,
    Fut: Future<Output = T> + 'static,
{
    type Output = T;
    fn call(&self) -> Box<dyn Future<Output = Self::Output>> {
        Box::new(self())
    }
}

/// The options for constructing a Perseus app. These encompass all the information Perseus needs to know to create your app. Every Perseus app using the engine must export one of these.
///
/// Note that this is an interim storage point, it's not depended on by any part of the core logic, and therefore custom engines can entirely ignore this.
#[derive(Debug)]
pub struct PerseusAppBase<G: Html, M: MutableStore, T: TranslationsManager> {
    /// The HTML ID of the root `<div>` element into which Perseus will be injected.
    root: String,
    /// A list of function that produce templates for the app to use. These are stored as functions so that they can be called an arbitrary number of times.
    // From this, we can construct the necessary kind of template map (we can call the user-given functions an arbitrary number of times)
    template_getters: TemplateGetters<G>,
    /// The app's error pages.
    error_pages: ErrorPagesGetter<G>,
    /// The global state creator for the app.
    global_state_creator: GlobalStateCreator,
    /// The internationalization information for the app.
    locales: Locales,
    /// The static aliases the app serves.
    static_aliases: HashMap<String, String>,
    /// The plugins the app uses.
    plugins: Rc<Plugins<G>>,
    /// The app's immutable store.
    immutable_store: ImmutableStore,
    /// The HTML template that'll be used to render the app into. This must be static, but can be generated or sourced in any way. Note that this MUST
    /// contain a `<div>` with the `id` set to whatever the value of `self.root` is.
    index_view: String,
    /// The app's mutable store.
    mutable_store: M,
    /// The app's translations manager, expressed as a function yielding a `Future`. This is only ever needed on the server-side, and can't be set up properly on the client-side because
    /// we can't use futures in the app initialization in Wasm.
    translations_manager: Tm<T>,
}

// The usual implementation in which the default mutable store is used
// We don't need to have a similar one for the default translations manager because things are completely generic there
impl<G: Html, T: TranslationsManager> PerseusAppBase<G, FsMutableStore, T> {
    /// Creates a new instance of a Perseus app using the default filesystem-based mutable store. For most apps, this will be sufficient. Note that this initializes the translations manager
    /// as a dummy, and adds no templates or error pages.
    ///
    /// In development, you can get away with defining no error pages, but production apps (e.g. those created with `perseus deploy`) MUST set their own custom error pages.
    ///
    /// This is asynchronous because it creates a translations manager in the background.
    // It makes no sense to implement `Default` on this, so we silence Clippy deliberately
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        Self::new_with_mutable_store(FsMutableStore::new("./dist/mutable".to_string()))
    }
}
// If one's using the default translations manager, caching should be handled automatically for them
impl<G: Html, M: MutableStore> PerseusAppBase<G, M, FsTranslationsManager> {
    /// The same as `.locales_and_translations_manager()`, but this accepts a literal `Locales` `struct`, which means this can be used when you're using `FsTranslationsManager` but when you don't
    /// know if your app is using i18n or not (almost always middleware).
    pub fn locales_lit_and_translations_manager(mut self, locales: Locales) -> Self {
        let using_i18n = locales.using_i18n;
        self.locales = locales;
        // If we're using i18n, do caching stuff
        // If not, use a dummy translations manager
        if using_i18n {
            // By default, all translations are cached
            let all_locales: Vec<String> = self
                .locales
                .get_all()
                .iter()
                // We have a `&&String` at this point, hence the double clone
                .cloned()
                .cloned()
                .collect();
            let tm_fut = FsTranslationsManager::new(
                crate::internal::i18n::DFLT_TRANSLATIONS_DIR.to_string(),
                all_locales,
                crate::internal::i18n::TRANSLATOR_FILE_EXT.to_string(),
            );
            self.translations_manager = Tm::Full(Box::pin(tm_fut));
        } else {
            self.translations_manager = Tm::Dummy(FsTranslationsManager::new_dummy());
        }

        self
    }
    /// Sets the internationalization information for an app using the default translations manager (`FsTranslationsManager`). This handles locale caching and the like automatically for you,
    /// though you could alternatively use `.locales()` and `.translations_manager()` independently to customize various behaviors. This takes the same arguments as `.locales()`, so
    /// the first argument is the default locale (used as a fallback for users with no locale preferences set in their browsers), and the second is a list of other locales supported.
    ///
    /// If you're not using i18n, you don't need to call this function. If you for some reason do have to though (e.g. overriding some other preferences in middleware), use `.disable_i18n()`,
    /// not this, as you're very likely to shoot yourself in the foot! (If i18n is disabled, the default locale MUST be set to `xx-XX`, for example.)
    pub fn locales_and_translations_manager(self, default: &str, other: &[&str]) -> Self {
        let locales = Locales {
            default: default.to_string(),
            other: other.iter().map(|s| s.to_string()).collect(),
            using_i18n: true,
        };

        self.locales_lit_and_translations_manager(locales)
    }
}
// The base implementation, generic over the mutable store and translations manager
impl<G: Html, M: MutableStore, T: TranslationsManager> PerseusAppBase<G, M, T> {
    /// Creates a new instance of a Perseus app, with the default options and a custom mutable store.
    pub fn new_with_mutable_store(mutable_store: M) -> Self {
        Self {
            root: "root".to_string(),
            // We do initialize with no templates, because an app without templates is in theory possible (and it's more convenient to call `.template()` for each one)
            template_getters: TemplateGetters(Vec::new()),
            // We do offer default error pages, but they'll panic if they're called for production building
            error_pages: ErrorPagesGetter(Box::new(ErrorPages::default)),
            global_state_creator: GlobalStateCreator::default(),
            // By default, we'll disable i18n (as much as I may want more websites to support more languages...)
            locales: Locales {
                default: "xx-XX".to_string(),
                other: Vec::new(),
                using_i18n: false,
            },
            // By default, we won't serve any static content outside the `static/` directory
            static_aliases: HashMap::new(),
            // By default, we won't use any plugins
            plugins: Rc::new(Plugins::new()),
            // This is relative to `.perseus/`
            immutable_store: ImmutableStore::new("./dist".to_string()),
            mutable_store,
            translations_manager: Tm::Dummy(T::new_dummy()),
            // Many users won't need anything fancy in the index view, so we provide a default
            index_view: DFLT_INDEX_VIEW.to_string(),
        }
    }

    // Setters (these all consume `self`)
    /// Sets the HTML ID of the `<div>` element at which to insert Perseus.
    pub fn root(mut self, val: &str) -> Self {
        self.root = val.to_string();
        self
    }
    /// Sets all the app's templates. This takes a vector of boxed functions that return templates.
    pub fn templates(mut self, val: Vec<Box<dyn Fn() -> Template<G>>>) -> Self {
        self.template_getters.0 = val;
        self
    }
    /// Adds a single new template to the app (convenience function). This takes a function that returns a template.
    pub fn template(mut self, val: impl Fn() -> Template<G> + 'static) -> Self {
        self.template_getters.0.push(Box::new(val));
        self
    }
    /// Sets the app's error pages.
    pub fn error_pages(mut self, val: impl Fn() -> ErrorPages<G> + 'static) -> Self {
        self.error_pages = ErrorPagesGetter(Box::new(val));
        self
    }
    /// Sets the app's global state creator.
    pub fn global_state_creator(mut self, val: GlobalStateCreator) -> Self {
        self.global_state_creator = val;
        self
    }
    /// Sets the locales information for the app. The first argument is the default locale (used as a fallback for users with no locale preferences set in their browsers), and
    /// the second is a list of other locales supported.
    ///
    /// Note that this does not update the translations manager, which must be done separately (if you're using `FsTranslationsManager`, the default, you can use
    /// `.locales_and_translations_manager()` to set both at once).
    ///
    /// If you're not using i18n, you don't need to call this function. If you for some reason do have to though (e.g. overriding some other preferences in middleware), use `.disable_i18n()`,
    /// not this, as you're very likely to shoot yourself in the foot! (If i18n is disabled, the default locale MUST be set to `xx-XX`, for example.)
    pub fn locales(mut self, default: &str, other: &[&str]) -> Self {
        self.locales = Locales {
            default: default.to_string(),
            other: other.iter().map(|s| s.to_string()).collect(),
            using_i18n: true,
        };
        self
    }
    /// Sets the locales information directly based on an instance of `Locales`. Usually, end users will use `.locales()` instead for a friendlier interface.
    pub fn locales_lit(mut self, val: Locales) -> Self {
        self.locales = val;
        self
    }
    /// Sets the translations manager. If you're using the default translations manager (`FsTranslationsManager`), you can use `.locales_and_translations_manager()` to set this automatically
    /// based on the locales information. This takes a `Future<Output = T>`, where `T` is your translations manager's type.
    ///
    /// The reason that this takes a `Future` is to avoid the use of `.await` in your app definition code, which must be synchronous due to constraints of Perseus' client-side systems.
    /// When your code is run on the server, the `Future` will be `.await`ed on, but on Wasm, it will be discarded and ignored, since the translations manager isn't needed in Wasm.
    ///
    /// This is generally intended for use with custom translations manager or specific use-cases with the default (mostly to do with custom caching behavior).
    pub fn translations_manager(mut self, val: impl Future<Output = T> + 'static) -> Self {
        self.translations_manager = Tm::Full(Box::pin(val));
        self
    }
    /// Explicitly disables internationalization. You shouldn't ever need to call this, as it's the default, but you may want to if you're writing middleware that doesn't support i18n.
    pub fn disable_i18n(mut self) -> Self {
        self.locales = Locales {
            default: "xx-XX".to_string(),
            other: Vec::new(),
            using_i18n: false,
        };
        // All translations manager must implement this function, which is designed for this exact purpose
        self.translations_manager = Tm::Dummy(T::new_dummy());
        self
    }
    /// Sets all the app's static aliases. This takes a map of URLs (e.g. `/file`) to resource paths, relative to the project directory (e.g. `style.css`).
    pub fn static_aliases(mut self, val: HashMap<String, String>) -> Self {
        self.static_aliases = val;
        self
    }
    /// Adds a single static alias (convenience function). This takes a URL path (e.g. `/file`) followed by a path to a resource (which must be within the project directory, e.g. `style.css`).
    pub fn static_alias(mut self, url: &str, resource: &str) -> Self {
        // We don't elaborate the alias to an actual filesystem path until the getter
        self.static_aliases
            .insert(url.to_string(), resource.to_string());
        self
    }
    /// Sets the plugins that the app will use.
    pub fn plugins(mut self, val: Plugins<G>) -> Self {
        self.plugins = Rc::new(val);
        self
    }
    /// Sets the mutable store for the app to use, which you would change for some production server environments if you wanted to store build artifacts that can change at runtime in a
    /// place other than on the filesystem (created for serverless functions specifically).
    pub fn mutable_store(mut self, val: M) -> Self {
        self.mutable_store = val;
        self
    }
    /// Sets the immutable store for the app to use. You should almost never need to change this unless you're not working with the CLI.
    pub fn immutable_store(mut self, val: ImmutableStore) -> Self {
        self.immutable_store = val;
        self
    }
    /// Sets the index view as a string. This should be used if you're using an `index.html` file or the like.
    ///
    /// Note: if possible, you should switch to using `.index_view()`, which uses a Sycamore view rather than an HTML string.
    pub fn index_view_str(mut self, val: &str) -> Self {
        self.index_view = val.to_string();
        self
    }
    /// Sets the index view using a Sycamore view, which avoids the need to write any HTML by hand whatsoever. Note that this must contain a `<head>` and `<body>` at a minimum.
    ///
    /// Warning: this view can't be reactive (yet). It will be rendered to a static string, which won't be hydrated.
    // The lifetime of the provided function doesn't need to be static, because we render using it and then we're done with it
    pub fn index_view<'a>(mut self, f: impl Fn() -> View<SsrNode> + 'a) -> Self {
        // This, very problematically, could add hydration IDs to the `<head>` and `<body>`, which we MUST NOT have (or the HTML shell's interpolation breaks in unexpected ways)
        let html_str = sycamore::render_to_string(f);
        // So, we get rid of the hydration IDs completely
        // We have to get rid of leftover spaces as well to make sure we're completely good for the naive string replacement
        #[cfg(not(target_arch = "wasm32"))]
        let html_str = regex::Regex::new(r#"data-hk=".*?""#)
            .unwrap()
            .replace_all(&html_str, "")
            .to_string()
            .replace(" >", ">");
        self.index_view = html_str;
        self
    }
    // Setters
    /// Gets the HTML ID of the `<div>` at which to insert Perseus.
    pub fn get_root(&self) -> String {
        self.plugins
            .control_actions
            .settings_actions
            .set_app_root
            .run((), self.plugins.get_plugin_data())
            .unwrap_or_else(|| self.root.to_string())
    }
    /// Gets the index view as a string, without generating an HTML shell (pass this into `::get_html_shell()` to do that).
    ///
    /// Note that this automatically adds `<!DOCTYPE html>` to the start of the HTMl shell produced, which can only be overriden with a control plugin (though you should really never do this
    /// in Perseus, which targets HTML on the web).
    #[cfg(feature = "server-side")]
    pub fn get_index_view_str(&self) -> String {
        // We have to add an HTML document type declaration, otherwise the browser could think it's literally anything! (This shouldn't be a problem, but it could be in 100 years...)
        format!("<!DOCTYPE html>\n{}", self.index_view)
    }
    /// Gets an HTML shell from an index view string. This is broken out so that it can be executed after the app has been built (which requries getting the translations manager, consuming
    /// `self`). As inconvenient as this is, it's necessitated, otherwise exporting would try to access the built app before it had actually been built.
    #[cfg(feature = "server-side")]
    pub async fn get_html_shell(
        index_view_str: String,
        root: &str,
        immutable_store: &ImmutableStore,
        plugins: &Plugins<G>,
    ) -> HtmlShell {
        // Construct an HTML shell
        let mut html_shell = HtmlShell::new(
            index_view_str,
            root,
            // TODO Handle this properly (good enough for now because that's what we weere already doing)
            &get_render_cfg(immutable_store)
                .await
                .expect("Couldn't get render configuration!"),
            &get_path_prefix_server(),
        );

        // Apply the myriad plugin actions to the HTML shell (replacing the whole thing first if need be)
        let shell_str = plugins
            .control_actions
            .settings_actions
            .html_shell_actions
            .set_shell
            .run((), plugins.get_plugin_data())
            .unwrap_or(html_shell.shell);
        html_shell.shell = shell_str;
        // For convenience, we alias the HTML shell functional actions
        let hsf_actions = &plugins
            .functional_actions
            .settings_actions
            .html_shell_actions;

        // These all return `Vec<String>`, so the code is almost identical for all the places for flexible interpolation
        html_shell.head_before_boundary.push(
            hsf_actions
                .add_to_head_before_boundary
                .run((), plugins.get_plugin_data())
                .values()
                .flatten()
                .cloned()
                .collect(),
        );
        html_shell.scripts_before_boundary.push(
            hsf_actions
                .add_to_scripts_before_boundary
                .run((), plugins.get_plugin_data())
                .values()
                .flatten()
                .cloned()
                .collect(),
        );
        html_shell.head_after_boundary.push(
            hsf_actions
                .add_to_head_after_boundary
                .run((), plugins.get_plugin_data())
                .values()
                .flatten()
                .cloned()
                .collect(),
        );
        html_shell.scripts_after_boundary.push(
            hsf_actions
                .add_to_scripts_after_boundary
                .run((), plugins.get_plugin_data())
                .values()
                .flatten()
                .cloned()
                .collect(),
        );
        html_shell.before_content.push(
            hsf_actions
                .add_to_before_content
                .run((), plugins.get_plugin_data())
                .values()
                .flatten()
                .cloned()
                .collect(),
        );
        html_shell.after_content.push(
            hsf_actions
                .add_to_after_content
                .run((), plugins.get_plugin_data())
                .values()
                .flatten()
                .cloned()
                .collect(),
        );

        html_shell
    }
    /// Gets the templates in an `Rc`-based `HashMap` for non-concurrent access.
    pub fn get_templates_map(&self) -> TemplateMap<G> {
        let mut map = HashMap::new();

        // Now add the templates the user provided
        for template_getter in self.template_getters.0.iter() {
            let template = template_getter();
            map.insert(template.get_path(), Rc::new(template));
        }

        // This will return a map of plugin name to a vector of templates to add
        let extra_templates = self
            .plugins
            .functional_actions
            .settings_actions
            .add_templates
            .run((), self.plugins.get_plugin_data());
        for (_plugin_name, plugin_templates) in extra_templates {
            // Turn that vector into a template map by extracting the template root paths as keys
            for template in plugin_templates {
                map.insert(template.get_path(), Rc::new(template));
            }
        }

        map
    }
    /// Gets the templates in an `Arc`-based `HashMap` for concurrent access. This should only be relevant on the server-side.
    #[cfg(feature = "server-side")]
    pub fn get_atomic_templates_map(&self) -> crate::templates::ArcTemplateMap<G> {
        let mut map = HashMap::new();

        // Now add the templates the user provided
        for template_getter in self.template_getters.0.iter() {
            let template = template_getter();
            map.insert(template.get_path(), std::sync::Arc::new(template));
        }

        // This will return a map of plugin name to a vector of templates to add
        let extra_templates = self
            .plugins
            .functional_actions
            .settings_actions
            .add_templates
            .run((), self.plugins.get_plugin_data());
        for (_plugin_name, plugin_templates) in extra_templates {
            // Turn that vector into a template map by extracting the template root paths as keys
            for template in plugin_templates {
                map.insert(template.get_path(), std::sync::Arc::new(template));
            }
        }

        map
    }
    /// Gets the error pages used in the app. This returns an `Rc`.
    pub fn get_error_pages(&self) -> ErrorPages<G> {
        let mut error_pages = (self.error_pages.0)();
        let extra_error_pages = self
            .plugins
            .functional_actions
            .settings_actions
            .add_error_pages
            .run((), self.plugins.get_plugin_data());
        for (_plugin_name, plugin_error_pages) in extra_error_pages {
            for (status, error_page) in plugin_error_pages {
                error_pages.add_page_rc(status, error_page);
            }
        }

        error_pages
    }
    /// Gets the global state creator. This can't be directly modified by plugins because of reactive type complexities.
    pub fn get_global_state_creator(&self) -> GlobalStateCreator {
        self.global_state_creator.clone()
    }
    /// Gets the locales information.
    pub fn get_locales(&self) -> Locales {
        let locales = self.locales.clone();
        self.plugins
            .control_actions
            .settings_actions
            .set_locales
            .run(locales.clone(), self.plugins.get_plugin_data())
            .unwrap_or(locales)
    }
    /// Gets the server-side translations manager. Like the mutable store, this can't be modified by plugins due to trait complexities.
    ///
    /// This involves evaluating the future stored for the translations manager, and so this consumes `self`.
    #[cfg(feature = "server-side")]
    pub async fn get_translations_manager(self) -> T {
        match self.translations_manager {
            Tm::Dummy(tm) => tm,
            Tm::Full(tm) => tm.await,
        }
    }
    /// Gets the immutable store.
    #[cfg(feature = "server-side")]
    pub fn get_immutable_store(&self) -> ImmutableStore {
        let immutable_store = self.immutable_store.clone();
        self.plugins
            .control_actions
            .settings_actions
            .set_immutable_store
            .run(immutable_store.clone(), self.plugins.get_plugin_data())
            .unwrap_or(immutable_store)
    }
    /// Gets the mutable store. This can't be modified by plugins due to trait complexities, so plugins should instead expose a function that the user can use to manually set it.
    #[cfg(feature = "server-side")]
    pub fn get_mutable_store(&self) -> M {
        self.mutable_store.clone()
    }
    /// Gets the plugins registered for the app. These are passed around and used in a way that doesn't require them to be concurrently accessible, and so are provided in an `Rc`.
    pub fn get_plugins(&self) -> Rc<Plugins<G>> {
        self.plugins.clone()
    }
    /// Gets the static aliases. This will check all provided resource paths to ensure they don't reference files outside the project directory, due to potential security risks in production
    /// (we don't want to accidentally serve an arbitrary in a production environment where a path may point to somewhere evil, like an alias to `/etc/passwd`).
    #[cfg(feature = "server-side")]
    pub fn get_static_aliases(&self) -> HashMap<String, String> {
        let mut static_aliases = self.static_aliases.clone();
        // This will return a map of plugin name to another map of static aliases that that plugin produced
        let extra_static_aliases = self
            .plugins
            .functional_actions
            .settings_actions
            .add_static_aliases
            .run((), self.plugins.get_plugin_data());
        for (_plugin_name, aliases) in extra_static_aliases {
            let new_aliases: HashMap<String, String> = aliases
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect();
            static_aliases.extend(new_aliases);
        }

        let mut scoped_static_aliases = HashMap::new();
        for (url, path) in static_aliases {
            // We need to move this from being scoped to the app to being scoped for `.perseus/`
            // TODO Make sure this works properly on Windows (seems to..)
            let new_path = if path.starts_with('/') {
                // Absolute paths are a security risk and are disallowed
                // The reason for this is that they could point somewhere completely different on a production server (like an alias to `/etc/passwd`)
                // Allowing these would also inevitably cause head-scratching in production, it's much easier to disallow these
                panic!(
                    "it's a security risk to include absolute paths in `static_aliases` ('{}'), please make this relative to the project directory",
                    path
                );
            } else if path.starts_with("../") {
                // Anything outside this directory is a security risk as well
                panic!("it's a security risk to include paths outside the current directory in `static_aliases` ('{}')", path);
            } else if path.starts_with("./") {
                // `./` -> `../` (moving to execution from `.perseus/`)
                // But if we're operating standalone, it stays the same
                if cfg!(feature = "standalone") {
                    path.to_string()
                } else {
                    format!(".{}", path)
                }
            } else {
                // Anything else gets a `../` prepended
                // But if we're operating standalone, it stays the same
                if cfg!(feature = "standalone") {
                    path.to_string()
                } else {
                    format!("../{}", path)
                }
            };

            scoped_static_aliases.insert(url, new_path);
        }

        scoped_static_aliases
    }
}

/// The component that represents the entrypoint at which Perseus will inject itself. You can use this with the `.index_view()` method of `PerseusApp` to avoid having to create the entrypoint
/// `<div>` manually.
#[component(PerseusRoot<G>)]
pub fn perseus_root() -> View<G> {
    view! {
        div(dangerously_set_inner_html = "<div id=\"root\"></div>")
    }
}

/// An alias for the usual kind of Perseus app, which uses the filesystem-based mutable store and translations manager.
pub type PerseusApp<G> = PerseusAppBase<G, FsMutableStore, FsTranslationsManager>;
/// An alias for a Perseus app that uses a custom mutable store type.
pub type PerseusAppWithMutableStore<G, M> = PerseusAppBase<G, M, FsTranslationsManager>;
/// An alias for a Perseus app that uses a custom translations manager type.
pub type PerseusAppWithTranslationsManager<G, T> = PerseusAppBase<G, FsMutableStore, T>;
/// An alias for a fully customizable Perseus app that can accept a custom mutable store and a custom translations manager. Alternatively, you could just use `PerseusAppBase` directly.
pub type PerseusAppWithMutableStoreAndTranslationsManager<G, M, T> = PerseusAppBase<G, M, T>;