Skip to content

Commit 318b83d

Browse files
committed
Fully cut over to tokio
Cut over to [email protected]. Much more simple interface, and works quite nicely with the async ecosystem. Cut over to axum for web server. Much better! The updated server stack is now more intelligently handling 404s. This is a common issue where users may end up making a request for some non-html static asset, but they get their path slightly wrong and are served the index.html instead of a 404. This typically results in users struggling to find the problem, and often times users have opened issues in GH. This updated algorithm will now evaluate 404s more closely and only respond with the index.html when the `accept` header allows for text/html or */*. Add to changelog: - all download utils have been make fully async. - added a subcommand to clean cached tools (wasm-bindgen & wasm-opt). This may be useful if there are ever issues with old tools which need to be removed. - resolves #198 (more intelligent 404s & index.html). - cut over to tokio 1.x. - cut over to axum for web server. Todo: - [ ] ensure the shutdown broadcaster is closed as soon as a signal is received, else ^C too early will cause the signal to be missed and other parts of the app which have not initialized will not see the drop.
1 parent f82ded8 commit 318b83d

27 files changed

+1272
-1664
lines changed

Cargo.lock

Lines changed: 642 additions & 1215 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "trunk"
3-
version = "0.12.1"
3+
version = "0.13.0"
44
edition = "2018"
55
description = "Build, bundle & ship your Rust WASM application to the web."
66
license = "MIT/Apache-2.0"
@@ -18,34 +18,38 @@ panic = "abort"
1818
[dependencies]
1919
ansi_term = "0.12"
2020
anyhow = "1"
21-
async-process = "1"
22-
async-std = { version = "1.9", default-features = false, features = ["attributes", "std", "unstable"] }
23-
async-tungstenite = { version = "0.10", default-features = false, features = ["async-std-runtime"] }
21+
async-compression = { version = "0.3", default-features = false, features = ["deflate", "futures-io", "gzip", "tokio"] }
22+
axum = { version = "0.1", features = ["ws"] }
23+
bytes = "1"
2424
cargo-lock = "6"
2525
cargo_metadata = "0.12"
2626
console = "0.14"
2727
directories-next = "2"
2828
dunce = "1"
2929
envy = "0.4"
30-
flate2 = "1"
3130
fs_extra = "1"
3231
futures = "0.3"
33-
http-types = "2"
32+
http = "0.2"
33+
hyper = "0.14"
34+
hyper-staticfile = "0.6"
3435
lazy_static = "1"
35-
nipper = "0.1.9"
36-
notify = "4"
36+
nipper = "0.1"
37+
notify = "5.0.0-pre.11"
3738
open = "1"
3839
remove_dir_all = "0.6"
40+
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "stream", "trust-dns"] }
3941
sass-rs = "0.2"
4042
seahash = "4"
4143
serde = { version = "1", features = ["derive"] }
4244
structopt = "0.3"
4345
structopt-derive = "0.4"
44-
surf = "2"
45-
tar = "0.4"
46-
tide = { version = "0.16.0", default-features = false, features = ["h1-server", "sessions", "unstable"] }
47-
tide-websockets = "0.3.0"
46+
# See https://docs.rs/tokio/latest/tokio/#feature-flags - we basically use all of the features.
47+
tokio = { version = "1", default-features = false, features = ["full"] }
48+
tokio-stream = { version = "0.1", default-features = false, features = ["fs", "time", "net", "signal", "sync"] }
49+
tokio-tar = "0.3"
50+
tokio-tungstenite = "0.14"
4851
toml = "0.5"
52+
tower-http = { version = "0.1", features = ["trace"] }
4953
tracing = "0.1"
5054
tracing-subscriber = "0.2"
5155
which = "4"

Trunk.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ dist = "dist"
1111
public_url = "/"
1212

1313
[watch]
14-
# Paths to watch, defaults to build target parent folder.
15-
path = ["src"]
14+
# Paths to watch. The `build.target`'s parent folder is watched by default.
15+
watch = []
1616
# Paths to ignore.
1717
ignore = []
1818

examples/yew/Trunk.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
11
[build]
22
target = "index.html"
33
dist = "dist"
4+
public_url = "/static/"
5+
6+
[[proxy]]
7+
# This WebSocket proxy example has a backend and ws field. This example will listen for
8+
# WebSocket connections at `/api/ws` and proxy them to `ws://localhost:9000/api/ws`.
9+
backend = "ws://localhost:9090/api/ws"
10+
ws = true
11+
12+
[[proxy]]
13+
# This proxy example has a backend and a rewrite field. Requests received on `rewrite` will be
14+
# proxied to the backend after rewriting the `rewrite` prefix to the `backend`'s URI prefix.
15+
# E.G., `/api/v1/resource/x/y/z` -> `/resource/x/y/z`
16+
rewrite = "/api/v1/"
17+
backend = "http://localhost:9090/"
18+
19+
[[proxy]]
20+
# This proxy specifies only the backend, which is the only required field. In this example,
21+
# request URIs are not modified when proxied.
22+
backend = "http://localhost:9090/api/v2/"

src/build.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ use std::path::PathBuf;
44
use std::sync::Arc;
55

66
use anyhow::{Context, Result};
7-
use async_std::fs;
8-
use futures::channel::mpsc::Sender;
9-
use futures::stream::StreamExt;
7+
use futures::prelude::*;
8+
use tokio::fs;
9+
use tokio::sync::mpsc;
10+
use tokio_stream::wrappers::ReadDirStream;
1011

1112
use crate::common::{remove_dir_all, BUILDING, ERROR, SUCCESS};
1213
use crate::config::{RtcBuild, STAGE_DIR};
@@ -30,7 +31,7 @@ impl BuildSystem {
3031
///
3132
/// Reducing the number of assumptions here should help us to stay flexible when adding new
3233
/// commands, refactoring and the like.
33-
pub async fn new(cfg: Arc<RtcBuild>, ignore_chan: Option<Sender<PathBuf>>) -> Result<Self> {
34+
pub async fn new(cfg: Arc<RtcBuild>, ignore_chan: Option<mpsc::Sender<PathBuf>>) -> Result<Self> {
3435
let html_pipeline = Arc::new(HtmlPipeline::new(cfg.clone(), ignore_chan)?);
3536
Ok(Self { cfg, html_pipeline })
3637
}
@@ -69,6 +70,7 @@ impl BuildSystem {
6970
.clone()
7071
.spawn()
7172
.await
73+
.context("error joining HTML pipeline")?
7274
.context("error from HTML pipeline")?;
7375

7476
// Move distribution from staging dist to final dist
@@ -117,6 +119,7 @@ impl BuildSystem {
117119

118120
let mut entries = fs::read_dir(&staging_dist)
119121
.await
122+
.map(ReadDirStream::new)
120123
.context("error reading staging dist dir")?;
121124
while let Some(entry) = entries.next().await {
122125
let entry = entry.context("error reading contents of staging dist dir")?;
@@ -133,7 +136,10 @@ impl BuildSystem {
133136
async fn clean_final(&self) -> Result<()> {
134137
let final_dist = self.cfg.final_dist.clone();
135138

136-
let mut entries = fs::read_dir(&final_dist).await.context("error reading final dist dir")?;
139+
let mut entries = fs::read_dir(&final_dist)
140+
.await
141+
.map(ReadDirStream::new)
142+
.context("error reading final dist dir")?;
137143
while let Some(entry) = entries.next().await {
138144
let entry = entry.context("error reading contents of final dist dir")?;
139145
if entry.file_name() == STAGE_DIR {
@@ -145,9 +151,7 @@ impl BuildSystem {
145151
.await
146152
.context("error reading metadata of file in final dist dir")?;
147153
if file_type.is_dir() {
148-
remove_dir_all(entry.path().into())
149-
.await
150-
.context("error cleaning final dist")?;
154+
remove_dir_all(entry.path()).await.context("error cleaning final dist")?;
151155
} else if file_type.is_symlink() || file_type.is_file() {
152156
fs::remove_file(entry.path()).await.context("error cleaning final dist")?;
153157
}

src/cmd/clean.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
use std::path::PathBuf;
2+
use std::process::Stdio;
23

3-
use anyhow::{ensure, Result};
4-
use async_process::{Command, Stdio};
4+
use anyhow::{ensure, Context, Result};
55
use structopt::StructOpt;
6+
use tokio::process::Command;
67

78
use crate::common::remove_dir_all;
89
use crate::config::{ConfigOpts, ConfigOptsClean};
10+
use crate::tools::cache_dir;
911

1012
/// Clean output artifacts.
1113
#[derive(StructOpt)]
1214
#[structopt(name = "clean")]
1315
pub struct Clean {
1416
#[structopt(flatten)]
1517
pub clean: ConfigOptsClean,
18+
/// Optionally clean any cached tools used by Trunk
19+
///
20+
/// These tools are cached in a platform dependent "projects" dir. Removing them will cause
21+
/// them to be downloaded by Trunk next time they are needed.
22+
#[structopt(short, long)]
23+
pub tools: bool,
1624
}
1725

1826
impl Clean {
@@ -21,6 +29,7 @@ impl Clean {
2129
let cfg = ConfigOpts::rtc_clean(self.clean, config)?;
2230
let _ = remove_dir_all(cfg.dist.clone()).await;
2331
if cfg.cargo {
32+
tracing::debug!("cleaning cargo dir");
2433
let output = Command::new("cargo")
2534
.arg("clean")
2635
.stdout(Stdio::piped())
@@ -29,6 +38,11 @@ impl Clean {
2938
.await?;
3039
ensure!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
3140
}
41+
if self.tools {
42+
tracing::debug!("cleaning trunk tools cache dir");
43+
let path = cache_dir().await.context("error getting cache dir path")?;
44+
remove_dir_all(path).await?;
45+
}
3246
Ok(())
3347
}
3448
}

src/cmd/serve.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::path::PathBuf;
22

3-
use anyhow::Result;
3+
use anyhow::{Context, Result};
44
use structopt::StructOpt;
5+
use tokio::sync::broadcast;
56

67
use crate::config::{ConfigOpts, ConfigOptsBuild, ConfigOptsServe, ConfigOptsWatch};
78
use crate::serve::ServeSystem;
@@ -21,9 +22,16 @@ pub struct Serve {
2122
impl Serve {
2223
#[tracing::instrument(level = "trace", skip(self, config))]
2324
pub async fn run(self, config: Option<PathBuf>) -> Result<()> {
25+
let (shutdown_tx, _shutdown_rx) = broadcast::channel(1);
2426
let cfg = ConfigOpts::rtc_serve(self.build, self.watch, self.serve, config)?;
25-
let system = ServeSystem::new(cfg).await?;
26-
system.run().await?;
27+
let system = ServeSystem::new(cfg, shutdown_tx.clone()).await?;
28+
29+
let system_handle = tokio::spawn(system.run());
30+
let _res = tokio::signal::ctrl_c().await.context("error awaiting shutdown signal")?;
31+
tracing::debug!("received shutdown signal");
32+
let _res = shutdown_tx.send(());
33+
system_handle.await.context("error awaiting system shutdown")??;
34+
2735
Ok(())
2836
}
2937
}

src/cmd/watch.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::path::PathBuf;
22

3-
use anyhow::Result;
3+
use anyhow::{Context, Result};
44
use structopt::StructOpt;
5+
use tokio::sync::broadcast;
56

67
use crate::config::{ConfigOpts, ConfigOptsBuild, ConfigOptsWatch};
78
use crate::watch::WatchSystem;
@@ -19,10 +20,17 @@ pub struct Watch {
1920
impl Watch {
2021
#[tracing::instrument(level = "trace", skip(self, config))]
2122
pub async fn run(self, config: Option<PathBuf>) -> Result<()> {
23+
let (shutdown_tx, _shutdown_rx) = broadcast::channel(1);
2224
let cfg = ConfigOpts::rtc_watch(self.build, self.watch, config)?;
23-
let mut system = WatchSystem::new(cfg).await?;
24-
system.build().await;
25-
system.run().await;
25+
let mut system = WatchSystem::new(cfg, shutdown_tx.clone()).await?;
26+
27+
let _res = system.build().await;
28+
let system_handle = tokio::spawn(system.run());
29+
let _res = tokio::signal::ctrl_c().await.context("error awaiting shutdown signal")?;
30+
tracing::debug!("received shutdown signal");
31+
let _res = shutdown_tx.send(());
32+
system_handle.await.context("error awaiting system shutdown")?;
33+
2634
Ok(())
2735
}
2836
}

src/common.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
//! Common functionality and types.
22
3+
use std::fs::Metadata;
34
use std::path::{Path, PathBuf};
5+
use std::process::Stdio;
46
use std::{ffi::OsStr, io::ErrorKind};
57

68
use anyhow::{anyhow, bail, Context, Result};
7-
use async_process::{Command, Stdio};
8-
use async_std::fs;
9-
use async_std::task::spawn_blocking;
9+
use tokio::fs;
10+
use tokio::process::Command;
1011

1112
use console::Emoji;
1213

@@ -32,7 +33,7 @@ pub async fn copy_dir_recursive(from_dir: PathBuf, to_dir: PathBuf) -> Result<()
3233
return Err(anyhow!("directory can not be copied as it does not exist {:?}", &from_dir));
3334
}
3435

35-
spawn_blocking(move || -> Result<()> {
36+
tokio::task::spawn_blocking(move || -> Result<()> {
3637
let opts = fs_extra::dir::CopyOptions {
3738
overwrite: true,
3839
content_only: true,
@@ -42,6 +43,7 @@ pub async fn copy_dir_recursive(from_dir: PathBuf, to_dir: PathBuf) -> Result<()
4243
Ok(())
4344
})
4445
.await
46+
.context("error awaiting spawned copy dir call")?
4547
.context("error copying directory")
4648
}
4749

@@ -53,21 +55,38 @@ pub async fn remove_dir_all(from_dir: PathBuf) -> Result<()> {
5355
if !path_exists(&from_dir).await? {
5456
return Ok(());
5557
}
56-
spawn_blocking(move || {
58+
tokio::task::spawn_blocking(move || {
5759
::remove_dir_all::remove_dir_all(from_dir.as_path()).context("error removing directory")?;
5860
Ok(())
5961
})
6062
.await
63+
.context("error awaiting spawned remove dir call")?
6164
}
6265

6366
/// Checks if path exists.
6467
pub async fn path_exists(path: impl AsRef<Path>) -> Result<bool> {
65-
let exists = fs::metadata(path.as_ref())
68+
fs::metadata(path.as_ref())
6669
.await
6770
.map(|_| true)
6871
.or_else(|error| if error.kind() == ErrorKind::NotFound { Ok(false) } else { Err(error) })
69-
.with_context(|| format!("error checking for existance of path at {:?}", path.as_ref()))?;
70-
Ok(exists)
72+
.with_context(|| format!("error checking for existance of path at {:?}", path.as_ref()))
73+
}
74+
75+
/// Check whether a given path exists, is a file and marked as executable.
76+
pub async fn is_executable(path: impl AsRef<Path>) -> Result<bool> {
77+
#[cfg(unix)]
78+
let has_executable_flag = |meta: Metadata| {
79+
use std::os::unix::fs::PermissionsExt;
80+
meta.permissions().mode() & 0o100 != 0
81+
};
82+
#[cfg(not(unix))]
83+
let has_executable_flag = |meta: Metadata| true;
84+
85+
fs::metadata(path.as_ref())
86+
.await
87+
.map(|meta| meta.is_file() && has_executable_flag(meta))
88+
.or_else(|error| if error.kind() == ErrorKind::NotFound { Ok(false) } else { Err(error) })
89+
.with_context(|| format!("error checking file mode for file {:?}", path.as_ref()))
7190
}
7291

7392
/// Strip the CWD prefix from the given path.
@@ -90,7 +109,7 @@ pub async fn run_command(name: &str, path: &Path, args: &[impl AsRef<OsStr>]) ->
90109
.stderr(Stdio::inherit())
91110
.spawn()
92111
.with_context(|| format!("error spawning {} call", name))?
93-
.status()
112+
.wait()
94113
.await
95114
.with_context(|| format!("error during {} call", name))?;
96115
if !status.success() {

src/config/manifest.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::path::Path;
22

33
use anyhow::{Context, Result};
4-
use async_std::task::spawn_blocking;
54
use cargo_metadata::{Metadata, MetadataCommand, Package};
5+
use tokio::task::spawn_blocking;
66

77
/// A wrapper around the cargo project's metadata.
88
#[derive(Clone, Debug)]
@@ -22,6 +22,7 @@ impl CargoMetadata {
2222
cmd.manifest_path(dunce::simplified(manifest));
2323
let metadata = spawn_blocking(move || cmd.exec())
2424
.await
25+
.context("error awaiting spawned cargo metadata task")?
2526
.context("error getting cargo metadata")?;
2627

2728
let package = metadata

0 commit comments

Comments
 (0)