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
// This file contains functions exclusive to the default engine systems

use super::serve::get_host_and_port;
use super::EngineOperation;
use crate::server::ServerOptions;
use crate::turbine::Turbine;
use crate::{i18n::TranslationsManager, init::PerseusAppBase, stores::MutableStore};
use fmterr::fmt_err;
use futures::Future;
use std::env;
use sycamore::web::SsrNode;

/// A wrapper around `run_dflt_engine` for apps that only use exporting, and so
/// don't need to bring in a server integration. This is designed to avoid extra
/// dependencies. If `perseus serve` is called on an app using this, it will
/// `panic!` after building everything.
pub async fn run_dflt_engine_export_only<M, T, A>(op: EngineOperation, app: A) -> i32
where
    M: MutableStore + 'static,
    T: TranslationsManager + 'static,
    A: Fn() -> PerseusAppBase<SsrNode, M, T> + 'static + Send + Sync + Clone,
{
    let serve_fn = |_, _, _| async {
        panic!("`run_dflt_engine_export_only` cannot run a server; you should use `run_dflt_engine` instead and import a server integration (e.g. `perseus-warp`)")
    };
    run_dflt_engine(op, app, serve_fn).await
}

/// A convenience function that automatically runs the necessary engine
/// operation based on the given directive. This provides almost no options for
/// customization, and is usually elided by a macro. More advanced use-cases
/// should bypass this and call the functions this calls manually, with their
/// own configurations.
///
/// The third argument to this is a function to produce a server. In simple
/// cases, this will be the `dflt_server` export from your server integration of
/// choice (which is assumed to use a Tokio 1.x runtime). This function must be
/// infallible (any errors should be panics, as they *will* be treated as
/// unrecoverable).
///
/// If the action is to export a single error page, the HTTP status code of the
/// error page to export and the output will be read as the first and second
/// arguments to the binary invocation. If this is not the desired behavior, you
/// should handle the `EngineOperation::ExportErrorPage` case manually.
///
/// This returns an exit code, which should be returned from the process. Any
/// handled errors will be printed to the console.
pub async fn run_dflt_engine<M, T, F, A>(
    op: EngineOperation,
    app: A,
    serve_fn: impl Fn(&'static Turbine<M, T>, ServerOptions, (String, u16)) -> F,
) -> i32
where
    M: MutableStore + 'static,
    T: TranslationsManager + 'static,
    F: Future<Output = ()>,
    A: Fn() -> PerseusAppBase<SsrNode, M, T> + 'static + Send + Sync + Clone,
{
    // The turbine is the core of Perseus' state generation system
    let mut turbine = match Turbine::try_from(app()) {
        Ok(turbine) => turbine,
        Err(err) => {
            eprintln!("{}", fmt_err(&err));
            return 1;
        }
    };

    match op {
        EngineOperation::Build => match turbine.build().await {
            Ok(_) => 0,
            Err(err) => {
                eprintln!("{}", fmt_err(&*err));
                1
            }
        },
        EngineOperation::Export => match turbine.export().await {
            Ok(_) => 0,
            Err(err) => {
                eprintln!("{}", fmt_err(&*err));
                1
            }
        },
        EngineOperation::ExportErrorPage => {
            // Assume the app has already been built and prepare the turbine
            match turbine.populate_after_build().await {
                Ok(_) => (),
                Err(err) => {
                    eprintln!("{}", fmt_err(&err));
                    return 1;
                }
            };

            // Get the HTTP status code to build from the arguments to this executable
            // We print errors directly here because we can, and because this behavior is
            // unique to the default engine
            let args = env::args().collect::<Vec<String>>();
            let code = match args.get(1) {
                Some(arg) => {
                    match arg.parse::<u16>() {
                        Ok(err_code) => err_code,
                        Err(_) => {
                            eprintln!("HTTP status code for error page exporting must be a valid integer.");
                            return 1;
                        }
                    }
                }
                None => {
                    eprintln!("Error page exporting requires an HTTP status code for which to export the error page.");
                    return 1;
                }
            };
            // Get the output to write to from the second argument
            let output = match args.get(2) {
                Some(output) => output,
                None => {
                    eprintln!("Error page exporting requires an output location.");
                    return 1;
                }
            };
            match turbine.export_error_page(code, output).await {
                Ok(_) => 0,
                Err(err) => {
                    eprintln!("{}", fmt_err(&*err));
                    1
                }
            }
        }
        EngineOperation::Serve => {
            // In production, automatically set the working directory
            // to be the parent of the actual binary. This means that disabling
            // debug assertions in development will lead to utterly incomprehensible
            // errors! You have been warned!
            //
            // This cannot be run ouutside the `serve` option, because that would
            // corrupt the location of build artifcats built for deployment.
            if !cfg!(debug_assertions) {
                let binary_loc = env::current_exe().unwrap();
                let binary_dir = binary_loc.parent().unwrap(); // It's a file, there's going to be a parent if we're working on anything close
                                                               // to sanity
                env::set_current_dir(binary_dir).unwrap();
            }

            // Assume the app has already been built and prepare the turbine
            match turbine.populate_after_build().await {
                Ok(_) => (),
                Err(err) => {
                    // Because so many people (including me) have made this mistake
                    eprintln!("{} (if you're running `perseus snoop serve`, make sure you've run `perseus snoop build` first!)", fmt_err(&err));
                    return 1;
                }
            };

            // This returns a `(String, u16)` of the host and port for maximum compatibility
            let addr = get_host_and_port();
            // In production, give the user a heads up that something's actually happening
            #[cfg(not(debug_assertions))]
            println!(
                "Your production app is now live on <http://{host}:{port}>! To change this, re-run this command with different settings for the `PERSEUS_HOST` and `PERSEUS_PORT` environment variables.\nNote that the above address will not reflect any domains configured.",
                host = &addr.0,
                port = &addr.1
            );

            // This actively and intentionally leaks the entire turbine to avoid the
            // overhead of an `Arc`, since we're guaranteed to need an immutable
            // reference to it for the server (we do this here so integration authors don't
            // have to). Since this only runs once, there is no accumulation of
            // unused memory, so this shouldn't be a problem.
            let turbine_static = Box::leak(Box::new(turbine));

            // We have access to default server options when `dflt-engine` is enabled
            serve_fn(turbine_static, ServerOptions::default(), addr).await;
            0
        }
        EngineOperation::Tinker => match turbine.tinker() {
            Ok(_) => 0,
            Err(err) => {
                eprintln!("{}", fmt_err(&err));
                1
            }
        },
    }
}