Skip to content

Commit 7693ebf

Browse files
committed
feat(cli): ✨ parallelized cli stages and removed rollup
Perseus now has no JS dependencies and is >2x faster in builds! Closes #7. Closes #9.
1 parent f85a947 commit 7693ebf

File tree

10 files changed

+274
-144
lines changed

10 files changed

+274
-144
lines changed

examples/cli/.perseus/main.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import init, { run } from "./dist/pkg/perseus_cli_builder.js";
1+
import init, { run } from "/.perseus/bundle.js";
22
async function main() {
33
await init("/.perseus/bundle.wasm");
44
run();

examples/cli/.perseus/server/src/main.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ async fn main() -> std::io::Result<()> {
1919
App::new().configure(block_on(configurer(
2020
Options {
2121
index: "../index.html".to_string(), // The user must define their own `index.html` file
22-
js_bundle: "dist/pkg/bundle.js".to_string(),
22+
js_bundle: "dist/pkg/perseus_cli_builder.js".to_string(),
23+
js_init: "main.js".to_string(),
2324
// Our crate has the same name, so this will be predictable
2425
wasm_bundle: "dist/pkg/perseus_cli_builder_bg.wasm".to_string(),
2526
templates_map: get_templates_map(),

examples/i18n/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Perseus Starter App</title>
88
<!-- Importing this runs Perseus -->
9-
<script src="/.perseus/bundle.js" defer></script>
9+
<script type="module" src="/.perseus/main.js" defer></script>
1010
</head>
1111
<body>
1212
<div id="root"></div>

packages/perseus-actix-web/src/configurer.rs

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use perseus::{get_render_cfg, ConfigManager, Locales, SsrNode, TemplateMap, Tran
99
pub struct Options {
1010
/// The location on the filesystem of your JavaScript bundle.
1111
pub js_bundle: String,
12+
/// The locales on the filesystem of the file that will invoke your JavaScript bundle. This should have something like `init()` in
13+
/// it.
14+
pub js_init: String,
1215
/// The location on the filesystem of your WASM bundle.
1316
pub wasm_bundle: String,
1417
/// The location on the filesystem of your `index.html` file that includes the JS bundle.
@@ -22,6 +25,9 @@ pub struct Options {
2225
async fn js_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
2326
NamedFile::open(&opts.js_bundle)
2427
}
28+
async fn js_init(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
29+
NamedFile::open(&opts.js_init)
30+
}
2531
async fn wasm_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
2632
NamedFile::open(&opts.wasm_bundle)
2733
}
@@ -48,6 +54,7 @@ pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'st
4854
// TODO chunk JS and WASM bundles
4955
// These allow getting the basic app code (not including the static data)
5056
// This contains everything in the spirit of a pseudo-SPA
57+
.route("/.perseus/main.js", web::get().to(js_init))
5158
.route("/.perseus/bundle.js", web::get().to(js_bundle))
5259
.route("/.perseus/bundle.wasm", web::get().to(wasm_bundle))
5360
// This allows getting the static HTML/JSON of a page

packages/perseus-cli/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ include = [
2222
include_dir = "0.6"
2323
error-chain = "0.12"
2424
cargo_toml = "0.9"
25-
indicatif = "0.16"
25+
indicatif = "0.17.0-beta.1" # Not stable, but otherwise error handling is just about impossible
2626
console = "0.14"
2727
serde = "1"
2828
serde_json = "1"

packages/perseus-cli/src/build.rs

+94-59
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,28 @@
1-
use crate::cmd::run_stage;
1+
use crate::cmd::{cfg_spinner, run_stage};
22
use crate::errors::*;
33
use console::{style, Emoji};
4+
use indicatif::{MultiProgress, ProgressBar};
45
use std::env;
56
use std::fs;
6-
use std::path::PathBuf;
7+
use std::path::{Path, PathBuf};
8+
use std::thread::{self, JoinHandle};
79

810
// Emojis for stages
911
static GENERATING: Emoji<'_, '_> = Emoji("🔨", "");
1012
static BUILDING: Emoji<'_, '_> = Emoji("🏗️ ", ""); // Yes, there's a space here, for some reason it's needed...
11-
static FINALIZING: Emoji<'_, '_> = Emoji("📦", "");
1213

1314
/// Returns the exit code if it's non-zero.
1415
macro_rules! handle_exit_code {
1516
($code:expr) => {
1617
let (_, _, code) = $code;
1718
if code != 0 {
18-
return Ok(code);
19+
return $crate::errors::Result::Ok(code);
1920
}
2021
};
2122
}
2223

23-
/// Actually builds the user's code, program arguments having been interpreted. This needs to know how many steps there are in total
24-
/// because the serving logic also uses it.
25-
pub fn build_internal(dir: PathBuf, num_steps: u8) -> Result<i32> {
26-
let mut target = dir;
27-
target.extend([".perseus"]);
28-
29-
// Static generation
30-
handle_exit_code!(run_stage(
31-
vec![&format!(
32-
"{} run",
33-
env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string())
34-
)],
35-
&target,
36-
format!(
37-
"{} {} Generating your app",
38-
style(format!("[1/{}]", num_steps)).bold().dim(),
39-
GENERATING
40-
)
41-
)?);
42-
// WASM building
43-
handle_exit_code!(run_stage(
44-
vec![&format!(
45-
"{} build --target web",
46-
env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string())
47-
)],
48-
&target,
49-
format!(
50-
"{} {} Building your app to WASM",
51-
style(format!("[2/{}]", num_steps)).bold().dim(),
52-
BUILDING
53-
)
54-
)?);
24+
/// Finalizes the build by renaming some directories.
25+
pub fn finalize(target: &Path) -> Result<()> {
5526
// Move the `pkg/` directory into `dist/pkg/`
5627
let pkg_dir = target.join("dist/pkg");
5728
if pkg_dir.exists() {
@@ -63,34 +34,98 @@ pub fn build_internal(dir: PathBuf, num_steps: u8) -> Result<i32> {
6334
if let Err(err) = fs::rename(target.join("pkg"), target.join("dist/pkg")) {
6435
bail!(ErrorKind::MovePkgDirFailed(err.to_string()));
6536
}
66-
// JS bundle generation
67-
handle_exit_code!(run_stage(
68-
vec![&format!(
69-
"{} main.js --format iife --file dist/pkg/bundle.js",
70-
env::var("PERSEUS_ROLLUP_PATH").unwrap_or_else(|_| "rollup".to_string())
71-
)],
72-
&target,
73-
format!(
74-
"{} {} Finalizing bundle",
75-
style(format!("[3/{}]", num_steps)).bold().dim(),
76-
FINALIZING
77-
)
78-
)?);
7937

80-
Ok(0)
38+
Ok(())
39+
}
40+
41+
// This literally only exists to avoid type complexity warnings in the `build_internal`'s return type
42+
type ThreadHandle = JoinHandle<Result<i32>>;
43+
44+
/// Actually builds the user's code, program arguments having been interpreted. This needs to know how many steps there are in total
45+
/// because the serving logic also uses it. This also takes a `MultiProgress` to interact with so it can be used truly atomically.
46+
/// This returns handles for waiting on the component threads so we can use it composably.
47+
pub fn build_internal(
48+
dir: PathBuf,
49+
spinners: &MultiProgress,
50+
num_steps: u8,
51+
) -> Result<(ThreadHandle, ThreadHandle)> {
52+
let target = dir.join(".perseus");
53+
54+
// Static generation message
55+
let sg_msg = format!(
56+
"{} {} Generating your app",
57+
style(format!("[1/{}]", num_steps)).bold().dim(),
58+
GENERATING
59+
);
60+
// Wasm building message
61+
let wb_msg = format!(
62+
"{} {} Building your app to Wasm",
63+
style(format!("[2/{}]", num_steps)).bold().dim(),
64+
BUILDING
65+
);
66+
67+
// We parallelize the first two spinners (static generation and Wasm building)
68+
// We make sure to add them at the top (the server spinner may have already been instantiated)
69+
let sg_spinner = spinners.insert(0, ProgressBar::new_spinner());
70+
let sg_spinner = cfg_spinner(sg_spinner, &sg_msg);
71+
let sg_target = target.clone();
72+
let wb_spinner = spinners.insert(1, ProgressBar::new_spinner());
73+
let wb_spinner = cfg_spinner(wb_spinner, &wb_msg);
74+
let wb_target = target.clone();
75+
let sg_thread = thread::spawn(move || {
76+
handle_exit_code!(run_stage(
77+
vec![&format!(
78+
"{} run",
79+
env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string())
80+
)],
81+
&sg_target,
82+
&sg_spinner,
83+
&sg_msg
84+
)?);
85+
86+
Ok(0)
87+
});
88+
let wb_thread = thread::spawn(move || {
89+
handle_exit_code!(run_stage(
90+
vec![&format!(
91+
"{} build --target web",
92+
env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string())
93+
)],
94+
&wb_target,
95+
&wb_spinner,
96+
&wb_msg
97+
)?);
98+
99+
Ok(0)
100+
});
101+
102+
Ok((sg_thread, wb_thread))
81103
}
82104

83105
/// Builds the subcrates to get a directory that we can serve. Returns an exit code.
84-
pub fn build(dir: PathBuf, prog_args: &[String]) -> Result<i32> {
106+
pub fn build(dir: PathBuf, _prog_args: &[String]) -> Result<i32> {
107+
let spinners = MultiProgress::new();
85108
// TODO support watching files
86-
// If we should watch for file changes, do so
87-
let should_watch = prog_args.get(1);
88-
let dflt_watch_path = ".".to_string();
89-
let _watch_path = prog_args.get(2).unwrap_or(&dflt_watch_path);
90-
if should_watch == Some(&"-w".to_string()) || should_watch == Some(&"--watch".to_string()) {
91-
todo!("watching not yet supported, try a tool like 'entr'");
109+
110+
let (sg_thread, wb_thread) = build_internal(dir.clone(), &spinners, 2)?;
111+
let sg_res = sg_thread
112+
.join()
113+
.map_err(|_| ErrorKind::ThreadWaitFailed)??;
114+
if sg_res != 0 {
115+
return Ok(sg_res);
116+
}
117+
let wb_res = wb_thread
118+
.join()
119+
.map_err(|_| ErrorKind::ThreadWaitFailed)??;
120+
if wb_res != 0 {
121+
return Ok(wb_res);
92122
}
93-
let exit_code = build_internal(dir.clone(), 3)?;
94123

95-
Ok(exit_code)
124+
// This waits for all the threads and lets the spinners draw to the terminal
125+
// spinners.join().map_err(|_| ErrorKind::ThreadWaitFailed)?;
126+
// And now we can run the finalization stage
127+
finalize(&dir.join(".perseus"))?;
128+
129+
// We've handled errors in the component threads, so the exit code is now zero
130+
Ok(0)
96131
}

packages/perseus-cli/src/cmd.rs

+26-13
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ pub static FAILURE: Emoji<'_, '_> = Emoji("❌", "failed!");
1212
/// Runs the given command conveniently, returning the exit code. Notably, this parses the given command by separating it on spaces.
1313
/// Returns the command's output and the exit code.
1414
pub fn run_cmd(cmd: String, dir: &Path, pre_dump: impl Fn()) -> Result<(String, String, i32)> {
15-
// let mut cmd_args: Vec<&str> = raw_cmd.split(' ').collect();
16-
// let cmd = cmd_args.remove(0);
17-
1815
// We run the command in a shell so that NPM/Yarn binaries can be recognized (see #5)
1916
#[cfg(unix)]
2017
let shell_exec = "sh";
@@ -51,23 +48,39 @@ pub fn run_cmd(cmd: String, dir: &Path, pre_dump: impl Fn()) -> Result<(String,
5148
))
5249
}
5350

54-
/// Runs a series of commands and provides a nice spinner with a custom message. Returns the last command's output and an appropriate exit
55-
/// code (0 if everything worked, otherwise the exit code of the one that failed).
56-
pub fn run_stage(cmds: Vec<&str>, target: &Path, message: String) -> Result<(String, String, i32)> {
57-
// Tell the user about the stage with a nice progress bar
58-
let spinner = ProgressBar::new_spinner();
51+
/// Creates a new spinner.
52+
pub fn cfg_spinner(spinner: ProgressBar, message: &str) -> ProgressBar {
5953
spinner.set_style(ProgressStyle::default_spinner().tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "));
60-
spinner.set_message(format!("{}...", message));
54+
spinner.set_message(format!("{}...", &message));
6155
// Tick the spinner every 50 milliseconds
6256
spinner.enable_steady_tick(50);
6357

58+
spinner
59+
}
60+
/// Instructs the given spinner to show success.
61+
pub fn succeed_spinner(spinner: &ProgressBar, message: &str) {
62+
spinner.finish_with_message(format!("{}...{}", message, SUCCESS));
63+
}
64+
/// Instructs the given spinner to show failure.
65+
pub fn fail_spinner(spinner: &ProgressBar, message: &str) {
66+
spinner.finish_with_message(format!("{}...{}", message, FAILURE));
67+
}
68+
69+
/// Runs a series of commands. Returns the last command's output and an appropriate exit code (0 if everything worked, otherwise th
70+
/// exit code of the first one that failed). This also takes a `Spinner` to use and control.
71+
pub fn run_stage(
72+
cmds: Vec<&str>,
73+
target: &Path,
74+
spinner: &ProgressBar,
75+
message: &str,
76+
) -> Result<(String, String, i32)> {
6477
let mut last_output = (String::new(), String::new());
6578
// Run the commands
6679
for cmd in cmds {
6780
// We make sure all commands run in the target directory ('.perseus/' itself)
6881
let (stdout, stderr, exit_code) = run_cmd(cmd.to_string(), target, || {
69-
// We're done, we'll write a more permanent version of the message
70-
spinner.finish_with_message(format!("{}...{}", message, FAILURE))
82+
// This stage has failed
83+
fail_spinner(spinner, message);
7184
})?;
7285
last_output = (stdout, stderr);
7386
// If we have a non-zero exit code, we should NOT continue (stderr has been written to the console already)
@@ -76,8 +89,8 @@ pub fn run_stage(cmds: Vec<&str>, target: &Path, message: String) -> Result<(Str
7689
}
7790
}
7891

79-
// We're done, we'll write a more permanent version of the message
80-
spinner.finish_with_message(format!("{}...{}", message, SUCCESS));
92+
// Everything has worked for this stage
93+
succeed_spinner(spinner, message);
8194

8295
Ok((last_output.0, last_output.1, 0))
8396
}

packages/perseus-cli/src/errors.rs

+5
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ error_chain! {
7878
description("couldn't move `pkg/` to `dist/pkg/`")
7979
display("Couldn't move `.perseus/pkg/` to `.perseus/dist/pkg`. Error was: '{}'.", err)
8080
}
81+
/// For when an error occurs while trying to wait for a thread.
82+
ThreadWaitFailed {
83+
description("error occurred while trying to wait for thread")
84+
display("Waiting on thread failed.")
85+
}
8186
}
8287
}
8388

packages/perseus-cli/src/prepare.rs

+5-8
Original file line numberDiff line numberDiff line change
@@ -132,35 +132,32 @@ pub fn prepare(dir: PathBuf) -> Result<()> {
132132
}
133133
}
134134

135-
/// Checks if the user has the necessary prerequisites on their system (i.e. `cargo`, `wasm-pack`, and `rollup`). These can all be checked
135+
/// Checks if the user has the necessary prerequisites on their system (i.e. `cargo` and `wasm-pack`). These can all be checked
136136
/// by just trying to run their binaries and looking for errors. If the user has other paths for these, they can define them under the
137-
/// environment variables `PERSEUS_CARGO_PATH`, `PERSEUS_WASM_PACK_PATH`, and `PERSEUS_ROLLUP_PATH`.
137+
/// environment variables `PERSEUS_CARGO_PATH` and `PERSEUS_WASM_PACK_PATH`.
138138
pub fn check_env() -> Result<()> {
139139
// We'll loop through each prerequisite executable to check their existence
140140
// If the spawn returns an error, it's considered not present, success means presence
141141
let prereq_execs = vec![
142142
(
143143
env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()),
144+
"cargo",
144145
"PERSEUS_CARGO_PATH",
145146
),
146147
(
147148
env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string()),
149+
"wasm-pack",
148150
"PERSEUS_WASM_PACK_PATH",
149151
),
150-
// We dangerously assume that the user isn't using `npx`...
151-
(
152-
env::var("PERSEUS_ROLLUP_PATH").unwrap_or_else(|_| "rollup".to_string()),
153-
"PERSEUS_ROLLUP_PATH",
154-
),
155152
];
156153

157154
for exec in prereq_execs {
158155
let res = Command::new(&exec.0).output();
159156
// Any errors are interpreted as meaning that the user doesn't have the prerequisite installed properly.
160157
if let Err(err) = res {
161158
bail!(ErrorKind::PrereqFailed(
162-
exec.0,
163159
exec.1.to_string(),
160+
exec.2.to_string(),
164161
err.to_string()
165162
))
166163
}

0 commit comments

Comments
 (0)