perseus_cli/
deploy.rs

1use crate::errors::*;
2use crate::export;
3use crate::install::Tools;
4use crate::parse::Opts;
5use crate::parse::{DeployOpts, ExportOpts, ServeOpts};
6use crate::serve;
7use fs_extra::copy_items;
8use fs_extra::dir::{copy as copy_dir, CopyOptions};
9use indicatif::MultiProgress;
10use minify_js::{minify, TopLevelMode};
11use std::fs;
12use std::path::Path;
13use std::path::PathBuf;
14
15/// Deploys the user's app to the `pkg/` directory (can be changed with
16/// `-o/--output`). This will build everything for release and then put it all
17/// together in one folder that can be conveniently uploaded to a server, file
18/// host, etc. This can return any kind of error because deploying involves
19/// working with other subcommands.
20///
21/// Note that this will execute a full copy of all static assets, so apps with
22/// large volumes of these may have longer deployment times.
23pub fn deploy(
24    dir: PathBuf,
25    opts: &DeployOpts,
26    tools: &Tools,
27    global_opts: &Opts,
28) -> Result<i32, Error> {
29    // Fork at whether we're using static exporting or not
30    let exit_code = if opts.export_static {
31        deploy_export(dir, opts.output.to_string(), opts, tools, global_opts)?
32    } else {
33        deploy_full(dir, opts.output.to_string(), opts, tools, global_opts)?
34    };
35
36    Ok(exit_code)
37}
38
39/// Deploys the user's app in its entirety, with a bundled server. This can
40/// return any kind of error because deploying involves working with other
41/// subcommands.
42fn deploy_full(
43    dir: PathBuf,
44    output: String,
45    opts: &DeployOpts,
46    tools: &Tools,
47    global_opts: &Opts,
48) -> Result<i32, Error> {
49    // Build everything for production, not running the server
50    let (serve_exit_code, server_path) = serve(
51        dir.clone(),
52        &ServeOpts {
53            no_run: true,
54            no_build: false,
55            release: true,
56            standalone: true,
57            watch: false,
58            custom_watch: Vec::new(),
59            // These have no impact if `no_run` is `true` (which it is), so we can use the defaults
60            // here
61            host: "127.0.0.1".to_string(),
62            port: 8080,
63        },
64        tools,
65        global_opts,
66        &MultiProgress::new(),
67        // Don't emit the "not running" message
68        true,
69    )?;
70    if serve_exit_code != 0 {
71        return Ok(serve_exit_code);
72    }
73    if let Some(server_path) = server_path {
74        // Delete the output directory if it exists and recreate it
75        let output_path = PathBuf::from(&output);
76        if output_path.exists() {
77            if let Err(err) = fs::remove_dir_all(&output_path) {
78                return Err(DeployError::ReplaceOutputDirFailed {
79                    path: output,
80                    source: err,
81                }
82                .into());
83            }
84        }
85        if let Err(err) = fs::create_dir(&output_path) {
86            return Err(DeployError::ReplaceOutputDirFailed {
87                path: output,
88                source: err,
89            }
90            .into());
91        }
92        // Copy in the server executable
93        #[cfg(target_os = "windows")]
94        let to = output_path.join("server.exe");
95        #[cfg(not(target_os = "windows"))]
96        let to = output_path.join("server");
97
98        if let Err(err) = fs::copy(&server_path, &to) {
99            return Err(DeployError::MoveAssetFailed {
100                to: to.to_str().map(|s| s.to_string()).unwrap(),
101                from: server_path,
102                source: err,
103            }
104            .into());
105        }
106        // Copy in the `static/` directory if it exists
107        let from = dir.join("static");
108        if from.exists() {
109            if let Err(err) = copy_dir(&from, &output, &CopyOptions::new()) {
110                return Err(DeployError::MoveDirFailed {
111                    to: output,
112                    from: from.to_str().map(|s| s.to_string()).unwrap(),
113                    source: err,
114                }
115                .into());
116            }
117        }
118        // Copy in the `translations` directory if it exists
119        let from = dir.join("translations");
120        if from.exists() {
121            if let Err(err) = copy_dir(&from, &output, &CopyOptions::new()) {
122                return Err(DeployError::MoveDirFailed {
123                    to: output,
124                    from: from.to_str().map(|s| s.to_string()).unwrap(),
125                    source: err,
126                }
127                .into());
128            }
129        }
130        // Create the `dist/` directory in the output directory
131        if let Err(err) = fs::create_dir(output_path.join("dist")) {
132            return Err(DeployError::CreateDistDirFailed { source: err }.into());
133        }
134        // Copy in the different parts of the `dist/` directory that we need (they all
135        // have to exist)
136        let from = dir.join("dist/static");
137        if let Err(err) = copy_dir(&from, output_path.join("dist"), &CopyOptions::new()) {
138            return Err(DeployError::MoveDirFailed {
139                to: output,
140                from: from.to_str().map(|s| s.to_string()).unwrap(),
141                source: err,
142            }
143            .into());
144        }
145        let from = dir.join("dist/pkg"); // Note: this handles snippets and the like
146        if let Err(err) = copy_dir(&from, output_path.join("dist"), &CopyOptions::new()) {
147            return Err(DeployError::MoveDirFailed {
148                to: output,
149                from: from.to_str().map(|s| s.to_string()).unwrap(),
150                source: err,
151            }
152            .into());
153        }
154        let from = dir.join("dist/mutable");
155        if let Err(err) = copy_dir(&from, output_path.join("dist"), &CopyOptions::new()) {
156            return Err(DeployError::MoveDirFailed {
157                to: output,
158                from: from.to_str().map(|s| s.to_string()).unwrap(),
159                source: err,
160            }
161            .into());
162        }
163        let from = dir.join("dist/render_conf.json");
164        if let Err(err) = fs::copy(&from, output_path.join("dist/render_conf.json")) {
165            return Err(DeployError::MoveAssetFailed {
166                to: output,
167                from: from.to_str().map(|s| s.to_string()).unwrap(),
168                source: err,
169            }
170            .into());
171        }
172
173        if !opts.no_minify_js {
174            minify_js(
175                &dir.join("dist/pkg/perseus_engine.js"),
176                &output_path.join("dist/pkg/perseus_engine.js"),
177            )?
178        }
179
180        println!();
181        println!("Deployment complete 🚀! Your app is now available for serving in the standalone folder '{}'! You can run it by executing the `server` binary in that folder.", &output_path.to_str().map(|s| s.to_string()).unwrap());
182
183        Ok(0)
184    } else {
185        // If we don't have the executable, throw an error
186        Err(ExecutionError::GetServerExecutableFailedSimple.into())
187    }
188}
189
190/// Uses static exporting to deploy the user's app. This can return any kind of
191/// error because deploying involves working with other subcommands.
192fn deploy_export(
193    dir: PathBuf,
194    output: String,
195    opts: &DeployOpts,
196    tools: &Tools,
197    global_opts: &Opts,
198) -> Result<i32, Error> {
199    // Export the app to `.perseus/exported`, using release mode
200    let export_exit_code = export(
201        dir.clone(),
202        &ExportOpts {
203            release: true,
204            serve: false,
205            host: String::new(),
206            port: 0,
207            watch: false,
208            custom_watch: Vec::new(),
209        },
210        tools,
211        global_opts,
212    )?;
213    if export_exit_code != 0 {
214        return Ok(export_exit_code);
215    }
216    // That subcommand produces a self-contained static site at `dist/exported/`
217    // Just copy that out to the output directory
218    let from = dir.join("dist/exported");
219    let output_path = PathBuf::from(&output);
220    // Delete the output directory if it exists and recreate it
221    if output_path.exists() {
222        if let Err(err) = fs::remove_dir_all(&output_path) {
223            return Err(DeployError::ReplaceOutputDirFailed {
224                path: output,
225                source: err,
226            }
227            .into());
228        }
229    }
230    if let Err(err) = fs::create_dir(&output_path) {
231        return Err(DeployError::ReplaceOutputDirFailed {
232            path: output,
233            source: err,
234        }
235        .into());
236    }
237    // Now read the contents of the export directory so that we can copy each asset
238    // in individually That avoids a `pkg/exported/` situation
239    let items = fs::read_dir(&from);
240    let items: Vec<PathBuf> = match items {
241        Ok(items) => {
242            let mut ok_items = Vec::new();
243            for item in items {
244                match item {
245                    Ok(item) => ok_items.push(item.path()),
246                    Err(err) => {
247                        return Err(DeployError::ReadExportDirFailed {
248                            path: from.to_str().map(|s| s.to_string()).unwrap(),
249                            source: err,
250                        }
251                        .into())
252                    }
253                }
254            }
255
256            ok_items
257        }
258        Err(err) => {
259            return Err(DeployError::ReadExportDirFailed {
260                path: from.to_str().map(|s| s.to_string()).unwrap(),
261                source: err,
262            }
263            .into())
264        }
265    };
266    // Now run the copy of each item
267    if let Err(err) = copy_items(&items, &output, &CopyOptions::new()) {
268        return Err(DeployError::MoveExportDirFailed {
269            to: output,
270            from: from.to_str().map(|s| s.to_string()).unwrap(),
271            source: err,
272        }
273        .into());
274    }
275
276    if !opts.no_minify_js {
277        minify_js(
278            &dir.join("dist/exported/.perseus/bundle.js"),
279            &output_path.join(".perseus/bundle.js"),
280        )?
281    }
282
283    println!();
284    println!("Deployment complete 🚀! Your app is now available for serving in the standalone folder '{}'! You can run it by serving the contents of that folder however you'd like.", &output_path.to_str().map(|s| s.to_string()).unwrap());
285
286    Ok(0)
287}
288
289/// Minifies the given JS code.
290fn minify_js(from: &Path, to: &Path) -> Result<(), DeployError> {
291    let js_bundle = fs::read_to_string(from)
292        .map_err(|err| DeployError::ReadUnminifiedJsFailed { source: err })?;
293
294    // TODO Remove this pending wilsonzlin/minify-js#7
295    // `minify-js` has a hard time with non-default exports right now, which is
296    // actually fine, because we don't need `initSync` whatsoever
297    let js_bundle = js_bundle.replace("export { initSync }", "// export { initSync }");
298
299    let mut minified = Vec::new();
300    minify(
301        TopLevelMode::Global,
302        js_bundle.as_bytes().to_vec(),
303        // Guaranteed to be UTF-8 output
304        &mut minified,
305    )
306    .map_err(|err| DeployError::MinifyError { source: err })?;
307    let minified =
308        String::from_utf8(minified).map_err(|err| DeployError::MinifyNotUtf8 { source: err })?;
309    fs::write(to, minified).map_err(|err| DeployError::WriteMinifiedJsFailed { source: err })?;
310
311    Ok(())
312}