Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --bundle option to web commands #195

Merged
merged 16 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ actix-web = "4.9.0"

# Opening the app in the browser
webbrowser = "1.0.2"

# Copying directories
fs_extra = "1.3.0"
13 changes: 10 additions & 3 deletions src/build/args.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::{Args, Subcommand};
use clap::{ArgAction, Args, Subcommand};

use crate::external_cli::{arg_builder::ArgBuilder, cargo::build::CargoBuildArgs};

Expand All @@ -16,7 +16,7 @@ pub struct BuildArgs {
impl BuildArgs {
/// Determine if the app is being built for the web.
pub(crate) fn is_web(&self) -> bool {
matches!(self.subcommand, Some(BuildSubcommands::Web))
matches!(self.subcommand, Some(BuildSubcommands::Web(_)))
}

/// The profile used to compile the app.
Expand All @@ -38,5 +38,12 @@ impl BuildArgs {
#[derive(Debug, Subcommand)]
pub enum BuildSubcommands {
/// Build your app for the browser.
Web,
Web(BuildWebArgs),
}

#[derive(Debug, Args)]
pub struct BuildWebArgs {
// Bundle all web artifacts into a single folder.
#[arg(short = 'b', long = "bundle", action = ArgAction::SetTrue, default_value_t = false)]
pub create_packed_bundle: bool,
}
13 changes: 12 additions & 1 deletion src/build/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use args::BuildSubcommands;

use crate::{
external_cli::{cargo, rustup, wasm_bindgen, CommandHelpers},
run::select_run_binary,
web::bundle::{create_web_bundle, PackedBundle, WebBundle},
};

pub use self::args::BuildArgs;
Expand All @@ -10,7 +13,7 @@ mod args;
pub fn build(args: &BuildArgs) -> anyhow::Result<()> {
let cargo_args = args.cargo_args_builder();

if args.is_web() {
if let Some(BuildSubcommands::Web(web_args)) = &args.subcommand {
ensure_web_setup()?;

let metadata = cargo::metadata::metadata_with_args(["--no-deps"])?;
Expand All @@ -28,6 +31,14 @@ pub fn build(args: &BuildArgs) -> anyhow::Result<()> {
args.profile(),
)?;
wasm_bindgen::bundle(&bin_target)?;

if web_args.create_packed_bundle {
let web_bundle = create_web_bundle(&metadata, args.profile(), bin_target, true)?;

if let WebBundle::Packed(PackedBundle { path }) = &web_bundle {
println!("Created bundle at file://{}", path.display());
}
}
} else {
cargo::build::command().args(cargo_args).ensure_status()?;
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pub mod external_cli;
pub mod lint;
pub mod run;
pub mod template;
pub(crate) mod web;
4 changes: 4 additions & 0 deletions src/run/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ pub struct RunWebArgs {
/// Open the app in the browser.
#[arg(short = 'o', long = "open", action = ArgAction::SetTrue, default_value_t = false)]
pub open: bool,

// Bundle all web artifacts into a single folder.
#[arg(short = 'b', long = "bundle", action = ArgAction::SetTrue, default_value_t = false)]
pub create_packed_bundle: bool,
}
16 changes: 15 additions & 1 deletion src/run/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::path::PathBuf;

use anyhow::Context;
use args::RunSubcommands;

use crate::{
Expand All @@ -8,6 +9,7 @@ use crate::{
cargo::{self, metadata::Metadata},
wasm_bindgen, CommandHelpers,
},
web::bundle::{create_web_bundle, PackedBundle, WebBundle},
};

pub use self::args::RunArgs;
Expand Down Expand Up @@ -38,6 +40,18 @@ pub fn run(args: &RunArgs) -> anyhow::Result<()> {
)?;
wasm_bindgen::bundle(&bin_target)?;

let web_bundle = create_web_bundle(
&metadata,
args.profile(),
bin_target,
web_args.create_packed_bundle,
)
.context("Failed to create web bundle")?;

if let WebBundle::Packed(PackedBundle { path }) = &web_bundle {
println!("Created bundle at file://{}", path.display());
}

let port = web_args.port;
let url = format!("http://localhost:{port}");

Expand All @@ -53,7 +67,7 @@ pub fn run(args: &RunArgs) -> anyhow::Result<()> {
println!("Open your app at <{url}>!");
}

serve::serve(bin_target, port)?;
serve::serve(web_bundle, port)?;
} else {
// For native builds, wrap `cargo run`
cargo::run::command().args(cargo_args).ensure_status()?;
Expand Down
91 changes: 40 additions & 51 deletions src/run/serve.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Serving the app locally for the browser.
use actix_web::{rt, web, App, HttpResponse, HttpServer, Responder};
use std::path::Path;

use super::BinTarget;
use crate::web::bundle::{Index, LinkedBundle, PackedBundle, WebBundle};

/// Serve a static HTML file with the given content.
async fn serve_static_html(content: &'static str) -> impl Responder {
Expand All @@ -15,62 +14,52 @@ async fn serve_static_html(content: &'static str) -> impl Responder {
.body(content)
}

/// Create the default `index.html` if the user didn't provide one.
fn default_index(bin_target: &BinTarget) -> &'static str {
let template = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/web/index.html"
));

// Insert correct path to JS bindings
let index = template.replace(
"./build/bevy_app.js",
format!("./build/{}.js", bin_target.bin_name).as_str(),
);

// Only static strings can be served in the web app,
// so we leak the string memory to convert it to a static reference.
// PERF: This is assumed to be used only once and is needed for the rest of the app running
// time, making the memory leak acceptable.
Box::leak(index.into_boxed_str())
}

/// Launch a web server running the Bevy app.
pub(crate) fn serve(bin_target: BinTarget, port: u16) -> anyhow::Result<()> {
let index_html = default_index(&bin_target);

pub(crate) fn serve(web_bundle: WebBundle, port: u16) -> anyhow::Result<()> {
rt::System::new().block_on(
HttpServer::new(move || {
let mut app = App::new();
let bin_target = bin_target.clone();

// Serve the build artifacts at the `/build/*` route
// A custom `index.html` will have to call `/build/{bin_name}.js`
app = app.service(
actix_files::Files::new("/build", bin_target.artifact_directory.clone())
// This potentially includes artifacts which we will not need,
// but we can't add the bin name to the check due to lifetime requirements
.path_filter(move |path, _| {
path.file_stem().is_some_and(|stem| {
// Using `.starts_with` instead of equality, because of the `_bg` suffix
// of the WASM bindings
stem.to_string_lossy().starts_with(&bin_target.bin_name)
}) && (path.extension().is_some_and(|ext| ext == "js")
|| path.extension().is_some_and(|ext| ext == "wasm"))
}),
);
match web_bundle.clone() {
WebBundle::Packed(PackedBundle { path }) => {
app = app.service(actix_files::Files::new("/", path).index_file("index.html"));
}
WebBundle::Linked(LinkedBundle {
build_artifact_path,
wasm_file_name,
js_file_name,
index,
assets_path,
}) => {
// Serve the build artifacts at the `/build/*` route
// A custom `index.html` will have to call `/build/{bin_name}.js`
app = app.service(
actix_files::Files::new("/build", build_artifact_path)
// This potentially includes artifacts which we will not need,
// but we can't add the bin name to the check due to lifetime
// requirements
.path_filter(move |path, _| {
path.file_name() == Some(&js_file_name)
|| path.file_name() == Some(&wasm_file_name)
}),
);

// If the app has an assets folder, serve it under `/assets`
if Path::new("assets").exists() {
app = app.service(actix_files::Files::new("/assets", "./assets"))
}
// If the app has an assets folder, serve it under `/assets`
if let Some(assets_path) = assets_path {
app = app.service(actix_files::Files::new("/assets", assets_path))
}

if Path::new("web").exists() {
// Serve the contents of the `web` folder under `/`, if it exists
app = app.service(actix_files::Files::new("/", "./web").index_file("index.html"));
} else {
// If the user doesn't provide a custom web setup, serve a default `index.html`
app = app.route("/", web::get().to(|| serve_static_html(index_html)))
match index {
Index::Folder(path) => {
app = app.service(
actix_files::Files::new("/", path).index_file("index.html"),
);
}
Index::Static(contents) => {
app = app.route("/", web::get().to(move || serve_static_html(contents)))
}
}
}
}

app
Expand Down
Loading
Loading