Optimize wasm bindgen asset with manganis (#3531)
This commit is contained in:
1 parent
d8850c7666
commit
13dcd80fbf
29 files changed
+933
-1089
No files matched your search
@@ -5,7 +5,7 @@ use crate::{BuildRequest, Platform};
|
||||
use crate::{Result, TraceSrc};
|
||||
use anyhow::Context;
|
||||
use dioxus_cli_opt::{process_file_to, AssetManifest};
|
||||
use manganis_core::AssetOptions;
|
||||
use manganis::{AssetOptions, JsAssetOptions};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
@@ -265,7 +265,7 @@ impl AppBundle {
|
||||
app: BuildArtifacts,
|
||||
server: Option<BuildArtifacts>,
|
||||
) -> Result<Self> {
|
||||
let bundle = Self { app, server, build };
|
||||
let mut bundle = Self { app, server, build };
|
||||
|
||||
tracing::debug!("Assembling app bundle");
|
||||
|
||||
@@ -301,7 +301,7 @@ impl AppBundle {
|
||||
/// For wasm, we'll want to run `wasm-bindgen` to make it a wasm binary along with some other optimizations
|
||||
/// Other platforms we might do some stripping or other optimizations
|
||||
/// Move the executable to the workdir
|
||||
async fn write_main_executable(&self) -> Result<()> {
|
||||
async fn write_main_executable(&mut self) -> Result<()> {
|
||||
match self.build.build.platform() {
|
||||
// Run wasm-bindgen on the wasm binary and set its output to be in the bundle folder
|
||||
// Also run wasm-opt on the wasm binary, and sets the index.html since that's also the "executable".
|
||||
@@ -328,7 +328,7 @@ impl AppBundle {
|
||||
Platform::Web => {
|
||||
// Run wasm-bindgen and drop its output into the assets folder under "dioxus"
|
||||
self.build.status_wasm_bindgen_start();
|
||||
self.run_wasm_bindgen(&self.app.exe.with_extension("wasm"), &self.build.exe_dir())
|
||||
self.run_wasm_bindgen(&self.app.exe.with_extension("wasm"))
|
||||
.await?;
|
||||
|
||||
// Only run wasm-opt if the feature is enabled
|
||||
@@ -339,7 +339,7 @@ impl AppBundle {
|
||||
// Write the index.html file with the pre-configured contents we got from pre-rendering
|
||||
std::fs::write(
|
||||
self.build.root_dir().join("index.html"),
|
||||
self.build.prepare_html()?,
|
||||
self.prepare_html()?,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -383,29 +383,41 @@ impl AppBundle {
|
||||
// First, clear the asset dir of any files that don't exist in the new manifest
|
||||
_ = tokio::fs::create_dir_all(&asset_dir).await;
|
||||
// Create a set of all the paths that new files will be bundled to
|
||||
let bundled_output_paths: HashSet<_> = self
|
||||
let mut keep_bundled_output_paths: HashSet<_> = self
|
||||
.app
|
||||
.assets
|
||||
.assets
|
||||
.values()
|
||||
.map(|a| asset_dir.join(a.bundled_path()))
|
||||
.collect();
|
||||
// The CLI creates a .version file in the asset dir to keep track of what version of the optimizer
|
||||
// the asset was processed. If that version doesn't match the CLI version, we need to re-optimize
|
||||
// all assets.
|
||||
let version_file = self.build.asset_optimizer_version_file();
|
||||
let clear_cache = std::fs::read_to_string(&version_file)
|
||||
.ok()
|
||||
.filter(|s| s == crate::VERSION.as_str())
|
||||
.is_none();
|
||||
if clear_cache {
|
||||
keep_bundled_output_paths.clear();
|
||||
}
|
||||
|
||||
// one possible implementation of walking a directory only visiting files
|
||||
fn remove_old_assets<'a>(
|
||||
path: &'a Path,
|
||||
bundled_output_paths: &'a HashSet<PathBuf>,
|
||||
keep_bundled_output_paths: &'a HashSet<PathBuf>,
|
||||
) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
// If this asset is in the manifest, we don't need to remove it
|
||||
let canon_path = dunce::canonicalize(path)?;
|
||||
if bundled_output_paths.contains(canon_path.as_path()) {
|
||||
if keep_bundled_output_paths.contains(canon_path.as_path()) {
|
||||
return Ok(());
|
||||
}
|
||||
// Otherwise, if it is a directory, we need to walk it and remove child files
|
||||
if path.is_dir() {
|
||||
for entry in std::fs::read_dir(path)?.flatten() {
|
||||
let path = entry.path();
|
||||
remove_old_assets(&path, bundled_output_paths).await?;
|
||||
remove_old_assets(&path, keep_bundled_output_paths).await?;
|
||||
}
|
||||
if path.read_dir()?.next().is_none() {
|
||||
// If the directory is empty, remove it
|
||||
@@ -420,7 +432,7 @@ impl AppBundle {
|
||||
}
|
||||
|
||||
tracing::debug!("Removing old assets");
|
||||
remove_old_assets(&asset_dir, &bundled_output_paths).await?;
|
||||
remove_old_assets(&asset_dir, &keep_bundled_output_paths).await?;
|
||||
|
||||
// todo(jon): we also want to eventually include options for each asset's optimization and compression, which we currently aren't
|
||||
let mut assets_to_transfer = vec![];
|
||||
@@ -474,6 +486,15 @@ impl AppBundle {
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??;
|
||||
|
||||
// Remove the wasm bindgen output directory if it exists
|
||||
_ = std::fs::remove_dir_all(self.build.wasm_bindgen_out_dir());
|
||||
|
||||
// Write the version file so we know what version of the optimizer we used
|
||||
std::fs::write(
|
||||
self.build.asset_optimizer_version_file(),
|
||||
crate::VERSION.as_str(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -554,9 +575,9 @@ impl AppBundle {
|
||||
.krate
|
||||
.should_pre_compress_web_assets(self.build.build.release);
|
||||
|
||||
let bindgen_dir = self.build.exe_dir();
|
||||
let asset_dir = self.build.asset_dir();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
crate::fastfs::pre_compress_folder(&bindgen_dir, pre_compress)
|
||||
crate::fastfs::pre_compress_folder(&asset_dir, pre_compress)
|
||||
})
|
||||
.await
|
||||
.unwrap()?;
|
||||
@@ -592,15 +613,14 @@ impl AppBundle {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn run_wasm_bindgen(
|
||||
&self,
|
||||
input_path: &Path,
|
||||
bindgen_outdir: &Path,
|
||||
) -> anyhow::Result<()> {
|
||||
pub(crate) async fn run_wasm_bindgen(&mut self, input_path: &Path) -> anyhow::Result<()> {
|
||||
tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen");
|
||||
|
||||
let input_path = input_path.to_path_buf();
|
||||
let bindgen_outdir = bindgen_outdir.to_path_buf();
|
||||
// Make sure the bindgen output directory exists
|
||||
let bindgen_outdir = self.build.wasm_bindgen_out_dir();
|
||||
std::fs::create_dir_all(&bindgen_outdir)?;
|
||||
|
||||
let name = self.build.krate.executable_name().to_string();
|
||||
let keep_debug =
|
||||
// if we're in debug mode, or we're generating debug symbols, keep debug info
|
||||
@@ -630,6 +650,29 @@ impl AppBundle {
|
||||
.await
|
||||
.context("Failed to generate wasm-bindgen bindings")?;
|
||||
|
||||
// After running wasm-bindgen, add the js and wasm asset to the manifest
|
||||
let js_output_path = self.build.wasm_bindgen_js_output_file();
|
||||
let wasm_output_path = self.build.wasm_bindgen_wasm_output_file();
|
||||
let new_assets = [
|
||||
(
|
||||
js_output_path,
|
||||
AssetOptions::Js(JsAssetOptions::new().with_minify(true).with_preload(true)),
|
||||
),
|
||||
(wasm_output_path, AssetOptions::Unknown),
|
||||
];
|
||||
for (asset_path, options) in new_assets {
|
||||
let hash = manganis_core::hash::AssetHash::hash_file_contents(&asset_path)?;
|
||||
let output_path_str = asset_path.to_str().ok_or(anyhow::anyhow!(
|
||||
"Failed to convert wasm bindgen output path to string"
|
||||
))?;
|
||||
let bundled_asset = manganis::macro_helpers::create_bundled_asset(
|
||||
output_path_str,
|
||||
hash.bytes(),
|
||||
options,
|
||||
);
|
||||
self.app.assets.assets.insert(asset_path, bundled_asset);
|
||||
}
|
||||
|
||||
tracing::debug!(dx_src = ?TraceSrc::Bundle, "wasm-bindgen complete in {:?}", start.elapsed());
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -656,6 +656,25 @@ impl BuildRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to the wasm bindgen temporary output folder
|
||||
pub fn wasm_bindgen_out_dir(&self) -> PathBuf {
|
||||
self.root_dir().join("wasm-bindgen")
|
||||
}
|
||||
|
||||
/// Get the path to the wasm bindgen javascript output file
|
||||
pub fn wasm_bindgen_js_output_file(&self) -> PathBuf {
|
||||
self.wasm_bindgen_out_dir()
|
||||
.join(self.krate.executable_name())
|
||||
.with_extension("js")
|
||||
}
|
||||
|
||||
/// Get the path to the wasm bindgen wasm output file
|
||||
pub fn wasm_bindgen_wasm_output_file(&self) -> PathBuf {
|
||||
self.wasm_bindgen_out_dir()
|
||||
.join(format!("{}_bg", self.krate.executable_name()))
|
||||
.with_extension("wasm")
|
||||
}
|
||||
|
||||
/// returns the path to root build folder. This will be our working directory for the build.
|
||||
///
|
||||
/// we only add an extension to the folders where it sorta matters that it's named with the extension.
|
||||
@@ -718,6 +737,11 @@ impl BuildRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to the asset optimizer version file
|
||||
pub fn asset_optimizer_version_file(&self) -> PathBuf {
|
||||
self.platform_dir().join(".cli-version")
|
||||
}
|
||||
|
||||
pub fn platform_exe_name(&self) -> String {
|
||||
match self.build.platform() {
|
||||
Platform::MacOS => self.krate.executable_name().to_string(),
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use dioxus_cli_config::format_base_path_meta_element;
|
||||
use manganis::AssetOptions;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::BuildRequest;
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::AppBundle;
|
||||
|
||||
const DEFAULT_HTML: &str = include_str!("../../assets/web/index.html");
|
||||
const TOAST_HTML: &str = include_str!("../../assets/web/toast.html");
|
||||
|
||||
impl BuildRequest {
|
||||
impl AppBundle {
|
||||
pub(crate) fn prepare_html(&self) -> Result<String> {
|
||||
let mut html = {
|
||||
let crate_root: &Path = &self.krate.crate_dir();
|
||||
let crate_root: &Path = &self.build.krate.crate_dir();
|
||||
let custom_html_file = crate_root.join("index.html");
|
||||
std::fs::read_to_string(custom_html_file).unwrap_or_else(|_| String::from(DEFAULT_HTML))
|
||||
};
|
||||
@@ -25,7 +27,7 @@ impl BuildRequest {
|
||||
// Replace any special placeholders in the HTML with resolved values
|
||||
self.replace_template_placeholders(&mut html);
|
||||
|
||||
let title = self.krate.config.web.app.title.clone();
|
||||
let title = self.build.krate.config.web.app.title.clone();
|
||||
|
||||
replace_or_insert_before("{app_title}", "</title", &title, &mut html);
|
||||
|
||||
@@ -33,13 +35,13 @@ impl BuildRequest {
|
||||
}
|
||||
|
||||
fn is_dev_build(&self) -> bool {
|
||||
!self.build.release
|
||||
!self.build.build.release
|
||||
}
|
||||
|
||||
// Inject any resources from the config into the html
|
||||
fn inject_resources(&self, html: &mut String) -> Result<()> {
|
||||
// Collect all resources into a list of styles and scripts
|
||||
let resources = &self.krate.config.web.resource;
|
||||
let resources = &self.build.krate.config.web.resource;
|
||||
let mut style_list = resources.style.clone().unwrap_or_default();
|
||||
let mut script_list = resources.script.clone().unwrap_or_default();
|
||||
|
||||
@@ -70,7 +72,7 @@ impl BuildRequest {
|
||||
|
||||
// Add the base path to the head if this is a debug build
|
||||
if self.is_dev_build() {
|
||||
if let Some(base_path) = &self.krate.config.web.app.base_path {
|
||||
if let Some(base_path) = &self.build.krate.config.web.app.base_path {
|
||||
head_resources.push_str(&format_base_path_meta_element(base_path));
|
||||
}
|
||||
}
|
||||
@@ -83,9 +85,45 @@ impl BuildRequest {
|
||||
}
|
||||
|
||||
// Inject any resources from manganis into the head
|
||||
// if let Some(assets) = assets {
|
||||
// head_resources.push_str(&assets.head());
|
||||
// }
|
||||
for asset in self.app.assets.assets.values() {
|
||||
let asset_path = asset.bundled_path();
|
||||
match asset.options() {
|
||||
AssetOptions::Css(css_options) => {
|
||||
if css_options.preloaded() {
|
||||
head_resources.push_str(&format!(
|
||||
"<link rel=\"preload\" as=\"style\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
|
||||
))
|
||||
}
|
||||
}
|
||||
AssetOptions::Image(image_options) => {
|
||||
if image_options.preloaded() {
|
||||
head_resources.push_str(&format!(
|
||||
"<link rel=\"preload\" as=\"image\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
|
||||
))
|
||||
}
|
||||
}
|
||||
AssetOptions::Js(js_options) => {
|
||||
if js_options.preloaded() {
|
||||
head_resources.push_str(&format!(
|
||||
"<link rel=\"preload\" as=\"script\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system
|
||||
let wasm_source_path = self.build.wasm_bindgen_wasm_output_file();
|
||||
let wasm_path = self
|
||||
.app
|
||||
.assets
|
||||
.assets
|
||||
.get(&wasm_source_path)
|
||||
.expect("WASM asset should exist in web bundles")
|
||||
.bundled_path();
|
||||
head_resources.push_str(&format!(
|
||||
"<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/assets/{wasm_path}\" crossorigin>"
|
||||
));
|
||||
|
||||
replace_or_insert_before("{style_include}", "</head", &head_resources, html);
|
||||
|
||||
@@ -95,7 +133,7 @@ impl BuildRequest {
|
||||
/// Inject loading scripts if they are not already present
|
||||
fn inject_loading_scripts(&self, html: &mut String) {
|
||||
// If it looks like we are already loading wasm or the current build opted out of injecting loading scripts, don't inject anything
|
||||
if !self.build.inject_loading_scripts || html.contains("__wbindgen_start") {
|
||||
if !self.build.build.inject_loading_scripts || html.contains("__wbindgen_start") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,9 +142,9 @@ impl BuildRequest {
|
||||
"</body",
|
||||
r#"<script>
|
||||
// We can't use a module script here because we need to start the script immediately when streaming
|
||||
import("/{base_path}/wasm/{app_name}.js").then(
|
||||
import("/{base_path}/{js_path}").then(
|
||||
({ default: init }) => {
|
||||
init("/{base_path}/wasm/{app_name}_bg.wasm").then((wasm) => {
|
||||
init("/{base_path}/{wasm_path}").then((wasm) => {
|
||||
if (wasm.__wbindgen_start == undefined) {
|
||||
wasm.main();
|
||||
}
|
||||
@@ -123,22 +161,41 @@ impl BuildRequest {
|
||||
true => html.replace("{DX_TOAST_UTILITIES}", TOAST_HTML),
|
||||
false => html.replace("{DX_TOAST_UTILITIES}", ""),
|
||||
};
|
||||
|
||||
// And try to insert preload links for the wasm and js files
|
||||
*html = html.replace(
|
||||
"</head",
|
||||
r#"<link rel="preload" href="/{base_path}/wasm/{app_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<link rel="preload" href="/{base_path}/wasm/{app_name}.js" as="script">
|
||||
</head"#
|
||||
);
|
||||
}
|
||||
|
||||
/// Replace any special placeholders in the HTML with resolved values
|
||||
fn replace_template_placeholders(&self, html: &mut String) {
|
||||
let base_path = self.krate.config.web.app.base_path();
|
||||
let base_path = self.build.krate.config.web.app.base_path();
|
||||
*html = html.replace("{base_path}", base_path);
|
||||
|
||||
let app_name = &self.krate.executable_name();
|
||||
let app_name = &self.build.krate.executable_name();
|
||||
let wasm_source_path = self.build.wasm_bindgen_wasm_output_file();
|
||||
let wasm_path = self
|
||||
.app
|
||||
.assets
|
||||
.assets
|
||||
.get(&wasm_source_path)
|
||||
.expect("WASM asset should exist in web bundles")
|
||||
.bundled_path();
|
||||
let wasm_path = format!("assets/{wasm_path}");
|
||||
let js_source_path = self.build.wasm_bindgen_js_output_file();
|
||||
let js_path = self
|
||||
.app
|
||||
.assets
|
||||
.assets
|
||||
.get(&js_source_path)
|
||||
.expect("JS asset should exist in web bundles")
|
||||
.bundled_path();
|
||||
let js_path = format!("assets/{js_path}");
|
||||
|
||||
// If the html contains the old `{app_name}` placeholder, replace {app_name}_bg.wasm and {app_name}.js
|
||||
// with the new paths
|
||||
*html = html.replace("wasm/{app_name}_bg.wasm", &wasm_path);
|
||||
*html = html.replace("wasm/{app_name}.js", &js_path);
|
||||
// Otherwise replace the new placeholders
|
||||
*html = html.replace("{wasm_path}", &wasm_path);
|
||||
*html = html.replace("{js_path}", &js_path);
|
||||
// Replace the app_name if we find it anywhere standalone
|
||||
*html = html.replace("{app_name}", app_name);
|
||||
}
|
||||
|
||||
@@ -155,12 +212,14 @@ impl BuildRequest {
|
||||
// The path is actually a web path which is relative to the root of the website
|
||||
let path = path.strip_prefix("/").unwrap_or(path);
|
||||
let asset_dir_path = self
|
||||
.build
|
||||
.krate
|
||||
.legacy_asset_dir()
|
||||
.map(|dir| dir.join(path).canonicalize());
|
||||
|
||||
if let Some(Ok(absolute_path)) = asset_dir_path {
|
||||
let absolute_crate_root = self.krate.crate_dir().canonicalize().unwrap();
|
||||
let absolute_crate_root =
|
||||
self.build.krate.crate_dir().canonicalize().unwrap();
|
||||
PathBuf::from("./")
|
||||
.join(absolute_path.strip_prefix(absolute_crate_root).unwrap())
|
||||
} else {
|
||||
|
||||
@@ -110,7 +110,7 @@ impl Display for Commands {
|
||||
}
|
||||
}
|
||||
|
||||
static VERSION: Lazy<String> = Lazy::new(|| {
|
||||
pub(crate) static VERSION: Lazy<String> = Lazy::new(|| {
|
||||
format!(
|
||||
"{} ({})",
|
||||
crate::dx_build_info::PKG_VERSION,
|
||||
|
||||
@@ -153,7 +153,6 @@ pub(crate) fn proxy_to(
|
||||
if uri.path().starts_with("/assets")
|
||||
|| uri.path().starts_with("/_dioxus")
|
||||
|| uri.path().starts_with("/public")
|
||||
|| uri.path().starts_with("/wasm")
|
||||
{
|
||||
tracing::trace!(dx_src = ?TraceSrc::Dev, "[{}] {}", res.status().as_u16(), uri);
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user