Optimize wasm bindgen asset with manganis (#3531)

This commit is contained in:
Evan Almloff authored and GitHub committed 2025-01-21 15:14:45 -08:00
1 parent d8850c7666
commit 13dcd80fbf
29 files changed
+933 -1089

No files matched your search

+27 -54
View File
@@ -39,57 +39,30 @@ grass = "0.13.4"
codemap = "0.1.3"
# Js minification - swc has introduces minor versions with breaking changes in the past so we pin all of their crates
swc = "=0.285.0"
swc_allocator = { version = "=0.1.8", default-features = false }
swc_atoms = { version = "=0.6.7", default-features = false }
swc_cached = { version = "=0.3.20", default-features = false }
swc_common = { version = "=0.37.5", default-features = false }
swc_compiler_base = { version = "=0.19.0", default-features = false }
swc_config = { version = "=0.1.15", default-features = false }
swc_config_macro = { version = "=0.1.4", default-features = false }
swc_ecma_ast = { version = "=0.118.2", default-features = false }
swc_ecma_codegen = { version = "=0.155.1", default-features = false }
swc_ecma_codegen_macros = { version = "=0.7.7", default-features = false }
swc_ecma_compat_bugfixes = { version = "=0.12.0", default-features = false }
swc_ecma_compat_common = { version = "=0.11.0", default-features = false }
swc_ecma_compat_es2015 = { version = "=0.12.0", default-features = false }
swc_ecma_compat_es2016 = { version = "=0.12.0", default-features = false }
swc_ecma_compat_es2017 = { version = "=0.12.0", default-features = false }
swc_ecma_compat_es2018 = { version = "=0.12.0", default-features = false }
swc_ecma_compat_es2019 = { version = "=0.12.0", default-features = false }
swc_ecma_compat_es2020 = { version = "=0.12.0", default-features = false }
swc_ecma_compat_es2021 = { version = "=0.12.0", default-features = false }
swc_ecma_compat_es2022 = { version = "=0.12.0", default-features = false }
swc_ecma_compat_es3 = { version = "=0.12.0", default-features = false }
swc_ecma_ext_transforms = { version = "=0.120.0", default-features = false }
swc_ecma_lints = { version = "=0.100.0", default-features = false }
swc_ecma_loader = { version = "=0.49.1", default-features = false }
swc_ecma_minifier = { version = "=0.204.0", default-features = false }
swc_ecma_parser = { version = "=0.149.1", default-features = false }
swc_ecma_preset_env = { version = "=0.217.0", default-features = false, features = [
"serde",
] }
swc_ecma_transforms = { version = "=0.239.0", default-features = false }
swc_ecma_transforms_base = { version = "=0.145.0", default-features = false }
swc_ecma_transforms_classes = { version = "=0.134.0", default-features = false }
swc_ecma_transforms_compat = { version = "=0.171.0", default-features = false }
swc_ecma_transforms_macros = { version = "=0.5.5", default-features = false }
swc_ecma_transforms_module = { version = "=0.190.0", default-features = false }
swc_ecma_transforms_optimization = { version = "=0.208.0", default-features = false }
swc_ecma_transforms_proposal = { version = "=0.178.0", default-features = false }
swc_ecma_transforms_react = { version = "=0.191.0", default-features = false }
swc_ecma_transforms_typescript = { version = "=0.198.1", default-features = false }
swc_ecma_usage_analyzer = { version = "=0.30.3", default-features = false }
swc_ecma_utils = { version = "=0.134.2", default-features = false }
swc_ecma_visit = { version = "=0.104.8", default-features = false }
swc_eq_ignore_macros = { version = "=0.1.4", default-features = false }
swc_error_reporters = { version = "=0.21.0", default-features = false }
swc_fast_graph = { version = "=0.25.0", default-features = false }
swc_macros_common = { version = "=0.3.13", default-features = false }
swc_node_comments = { version = "=0.24.0", default-features = false }
swc_timer = { version = "=0.25.0", default-features = false }
swc_trace_macro = { version = "=0.1.3", default-features = false }
swc_transform_common = { version = "=0.1.1", default-features = false }
swc_typescript = { version = "=0.5.0", default-features = false }
swc_visit = { version = "=0.6.2", default-features = false }
browserslist-rs = { version = "=0.16.0" }
swc_allocator = { version = "=2.0.0", default-features = false }
swc_atoms = { version = "=3.0.2", default-features = false }
swc_bundler = { version = "=7.0.0", default-features = false }
swc_cached = { version = "=1.0.0", default-features = false }
swc_common = { version = "=5.0.0", features = ["tty-emitter"], default-features = false }
swc_config = { version = "=1.0.0", default-features = false }
swc_config_macro = { version = "=1.0.0", default-features = false }
swc_ecma_ast = { version = "=5.0.1", default-features = false }
swc_ecma_codegen = { version = "=5.0.1", default-features = false }
swc_ecma_codegen_macros = { version = "=1.0.0", default-features = false }
swc_ecma_loader = { version = "=5.0.0", features = ["cache", "node"], default-features = false }
swc_ecma_minifier = { version = "=7.0.1", default-features = false }
swc_ecma_parser = { version = "=6.0.2", default-features = false }
swc_ecma_transforms_base = { version = "=7.0.0", default-features = false }
swc_ecma_transforms_macros = { version = "=1.0.0", default-features = false }
swc_ecma_transforms_optimization = { version = "=7.0.1", default-features = false }
swc_ecma_usage_analyzer = { version = "=7.0.0", default-features = false }
swc_ecma_utils = { version = "=7.0.0", default-features = false }
swc_ecma_visit = { version = "=5.0.0", default-features = false }
swc_eq_ignore_macros = { version = "=1.0.0", default-features = false }
swc_fast_graph = { version = "=6.0.0", default-features = false }
swc_graph_analyzer = { version = "=5.0.0", default-features = false }
swc_macros_common = { version = "=1.0.0", default-features = false }
swc_parallel = { version = "=1.0.1", default-features = false }
swc_timer = { version = "=1.0.0", default-features = false }
swc_visit = { version = "=2.0.0", default-features = false }
browserslist-rs = { version = "=0.16.0" }
+35 -13
View File
@@ -1,6 +1,6 @@
use anyhow::Context;
use manganis_core::{AssetOptions, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
use std::path::Path;
use std::{ffi::OsStr, path::Path};
use crate::css::process_scss;
@@ -14,6 +14,16 @@ pub fn process_file_to(
options: &AssetOptions,
source: &Path,
output_path: &Path,
) -> anyhow::Result<()> {
process_file_to_with_options(options, source, output_path, false)
}
/// Process a specific file asset with additional options
pub(crate) fn process_file_to_with_options(
options: &AssetOptions,
source: &Path,
output_path: &Path,
in_folder: bool,
) -> anyhow::Result<()> {
// If the file already exists, then we must have a file with the same hash
// already. The hash has the file contents and options, so if we find a file
@@ -26,56 +36,68 @@ pub fn process_file_to(
std::fs::create_dir_all(parent)?;
}
}
// Processing can be slow. Write to a temporary file first and then rename it to the final output path. If everything
// goes well. Without this, the user could quit in the middle of processing and the file will look complete to the
// caching system even though it is empty.
let mut partial_ext = output_path.extension().unwrap_or_default().to_os_string();
partial_ext.push(OsStr::new(".partial"));
let temp_output_path = output_path.with_extension(&partial_ext);
let temp_output_path = &temp_output_path;
match options {
AssetOptions::Unknown => match source.extension().map(|e| e.to_string_lossy()).as_deref() {
Some("css") => {
process_css(&CssAssetOptions::new(), source, output_path)?;
process_css(&CssAssetOptions::new(), source, temp_output_path)?;
}
Some("scss" | "sass") => {
process_scss(&CssAssetOptions::new(), source, output_path)?;
process_scss(&CssAssetOptions::new(), source, temp_output_path)?;
}
Some("js") => {
process_js(&JsAssetOptions::new(), source, output_path)?;
process_js(&JsAssetOptions::new(), source, temp_output_path, !in_folder)?;
}
Some("json") => {
process_json(source, output_path)?;
process_json(source, temp_output_path)?;
}
Some("jpg" | "jpeg" | "png" | "webp" | "avif") => {
process_image(&ImageAssetOptions::new(), source, output_path)?;
process_image(&ImageAssetOptions::new(), source, temp_output_path)?;
}
Some(_) | None => {
if source.is_dir() {
process_folder(source, output_path)?;
process_folder(source, temp_output_path)?;
} else {
let source_file = std::fs::File::open(source)?;
let mut reader = std::io::BufReader::new(source_file);
let output_file = std::fs::File::create(output_path)?;
let output_file = std::fs::File::create(temp_output_path)?;
let mut writer = std::io::BufWriter::new(output_file);
std::io::copy(&mut reader, &mut writer).with_context(|| {
format!(
"Failed to write file to output location: {}",
output_path.display()
temp_output_path.display()
)
})?;
}
}
},
AssetOptions::Css(options) => {
process_css(options, source, output_path)?;
process_css(options, source, temp_output_path)?;
}
AssetOptions::Js(options) => {
process_js(options, source, output_path)?;
process_js(options, source, temp_output_path, !in_folder)?;
}
AssetOptions::Image(options) => {
process_image(options, source, output_path)?;
process_image(options, source, temp_output_path)?;
}
AssetOptions::Folder(_) => {
process_folder(source, output_path)?;
process_folder(source, temp_output_path)?;
}
_ => {
tracing::warn!("Unknown asset options: {:?}", options);
}
}
// If everything was successful, rename the temp file to the final output path
std::fs::rename(temp_output_path, output_path)?;
Ok(())
}
+3 -2
View File
@@ -2,7 +2,7 @@ use std::path::Path;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use super::file::process_file_to;
use crate::file::process_file_to_with_options;
/// Process a folder, optimizing and copying all assets into the output folder
pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()> {
@@ -32,10 +32,11 @@ pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()>
/// Optimize a file without changing any of its contents significantly (e.g. by changing the extension)
fn process_file_minimal(input_path: &Path, output_path: &Path) -> anyhow::Result<()> {
process_file_to(
process_file_to_with_options(
&manganis_core::AssetOptions::Unknown,
input_path,
output_path,
true,
)?;
Ok(())
}
+221 -52
View File
@@ -1,74 +1,243 @@
use std::io::Read;
use std::path::Path;
use std::sync::Arc;
use std::path::PathBuf;
use anyhow::Context;
use manganis_core::JsAssetOptions;
use swc::{config::JsMinifyOptions, try_with_handler, BoolOrDataConfig, JsMinifyExtras};
use swc_common::{sync::Lrc, FileName};
use swc_common::{SourceMap, GLOBALS};
use swc_ecma_minifier::option::MangleOptions;
use swc_common::errors::Emitter;
use swc_common::errors::Handler;
use swc_common::input::SourceFileInput;
use swc_ecma_minifier::option::{ExtraOptions, MinifyOptions};
use swc_ecma_parser::lexer::Lexer;
use swc_ecma_parser::Parser;
use swc_ecma_transforms_base::fixer::fixer;
use swc_ecma_visit::VisitMutWith;
pub(crate) fn minify_js(source: &Path) -> anyhow::Result<String> {
let mut source_file = std::fs::File::open(source)?;
let cm = Arc::new(SourceMap::default());
use std::collections::HashMap;
let mut js = String::new();
source_file.read_to_string(&mut js)?;
let c = swc::Compiler::new(cm.clone());
let output = GLOBALS
.set(&Default::default(), || {
try_with_handler(cm.clone(), Default::default(), |handler| {
let filename = Lrc::new(FileName::Real(source.to_path_buf()));
let fm = cm.new_source_file(filename, js.to_string());
use anyhow::Error;
use swc_bundler::{Bundler, Config, Load, ModuleData, ModuleRecord};
use swc_common::{
errors::HANDLER, sync::Lrc, FileName, FilePathMapping, Globals, Mark, SourceMap, Span, GLOBALS,
};
use swc_ecma_ast::*;
use swc_ecma_codegen::text_writer::JsWriter;
use swc_ecma_loader::{resolvers::node::NodeModulesResolver, TargetEnv};
use swc_ecma_parser::{parse_file_as_module, Syntax};
c.minify(
fm,
handler,
&JsMinifyOptions {
compress: BoolOrDataConfig::from_bool(true),
mangle: BoolOrDataConfig::from_obj(MangleOptions {
keep_fn_names: true,
..Default::default()
}),
..Default::default()
},
JsMinifyExtras::default(),
)
.context("failed to minify javascript")
})
})
.map(|output| output.code);
struct TracingEmitter;
match output {
Ok(output) => Ok(output),
Err(err) => {
tracing::error!("Failed to minify javascript: {}", err);
Ok(js)
impl Emitter for TracingEmitter {
fn emit(&mut self, db: &swc_common::errors::DiagnosticBuilder<'_>) {
match db.level {
swc_common::errors::Level::Bug
| swc_common::errors::Level::Fatal
| swc_common::errors::Level::PhaseFatal
| swc_common::errors::Level::Error => tracing::error!("{}", db.message()),
swc_common::errors::Level::Warning
| swc_common::errors::Level::FailureNote
| swc_common::errors::Level::Cancelled => tracing::warn!("{}", db.message()),
swc_common::errors::Level::Note | swc_common::errors::Level::Help => {
tracing::trace!("{}", db.message())
}
}
}
}
fn bundle_js_to_writer(
file: PathBuf,
bundle: bool,
minify: bool,
write_to: &mut impl std::io::Write,
) -> anyhow::Result<()> {
let globals = Globals::new();
let handler = Handler::with_emitter_and_flags(Box::new(TracingEmitter), Default::default());
GLOBALS.set(&globals, || {
HANDLER.set(&handler, || {
bundle_js_to_writer_inside_handler(&globals, file, bundle, minify, write_to)
})
})
}
fn bundle_js_to_writer_inside_handler(
globals: &Globals,
file: PathBuf,
bundle: bool,
minify: bool,
write_to: &mut impl std::io::Write,
) -> anyhow::Result<()> {
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
let mut module = if bundle {
let node_resolver = NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true);
let mut bundler = Bundler::new(
globals,
cm.clone(),
PathLoader { cm: cm.clone() },
node_resolver,
Config {
require: true,
..Default::default()
},
Box::new(Hook),
);
let mut entries = HashMap::default();
entries.insert("main".to_string(), FileName::Real(file));
let mut bundles = bundler
.bundle(entries)
.context("failed to bundle javascript with swc")?;
// Since we only inserted one entry, there should only be one bundle in the output
let bundle = bundles
.pop()
.ok_or_else(|| anyhow::anyhow!("swc did not output any bundles"))?;
bundle.module
} else {
let fm = cm.load_file(Path::new(&file)).expect("Failed to load file");
let lexer = Lexer::new(
Default::default(),
Default::default(),
SourceFileInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
parser.parse_module().map_err(|err| {
HANDLER.with(|handler| {
let message = err.into_diagnostic(handler).message();
anyhow::anyhow!("{}", message)
})
})?
};
if minify {
module = swc_ecma_minifier::optimize(
std::mem::take(&mut module).into(),
cm.clone(),
None,
None,
&MinifyOptions {
rename: true,
compress: None,
mangle: None,
..Default::default()
},
&ExtraOptions {
unresolved_mark: Mark::new(),
top_level_mark: Mark::new(),
mangle_name_cache: None,
},
)
.expect_module();
module.visit_mut_with(&mut fixer(None));
}
let mut emitter = swc_ecma_codegen::Emitter {
cfg: swc_ecma_codegen::Config::default().with_minify(minify),
cm: cm.clone(),
comments: None,
wr: Box::new(JsWriter::new(cm, "\n", write_to, None)),
};
emitter.emit_module(&module)?;
Ok(())
}
struct PathLoader {
cm: Lrc<SourceMap>,
}
impl Load for PathLoader {
fn load(&self, file: &FileName) -> anyhow::Result<ModuleData> {
let file = match file {
FileName::Real(v) => v,
_ => anyhow::bail!("Only real files are supported"),
};
let fm = self.cm.load_file(file)?;
let module = HANDLER.with(|handler| {
parse_file_as_module(
&fm,
Syntax::Es(Default::default()),
Default::default(),
None,
&mut Vec::new(),
)
.map_err(|err| {
let message = err.into_diagnostic(handler).message();
anyhow::anyhow!("{}", message)
})
.context("Failed to parse javascript")
})?;
Ok(ModuleData {
fm,
module,
helpers: Default::default(),
})
}
}
// Adapted from https://github.com/swc-project/swc/blob/624680b7896cef9d8e30bd5ff910538298016974/bindings/binding_core_node/src/bundle.rs#L266-L302
struct Hook;
impl swc_bundler::Hook for Hook {
fn get_import_meta_props(
&self,
span: Span,
module_record: &ModuleRecord,
) -> Result<Vec<KeyValueProp>, Error> {
let file_name = module_record.file_name.to_string();
Ok(vec![
KeyValueProp {
key: PropName::Ident(IdentName::new("url".into(), span)),
value: Box::new(Expr::Lit(Lit::Str(Str {
span,
raw: None,
value: file_name.into(),
}))),
},
KeyValueProp {
key: PropName::Ident(IdentName::new("main".into(), span)),
value: Box::new(if module_record.is_entry {
Expr::Member(MemberExpr {
span,
obj: Box::new(Expr::MetaProp(MetaPropExpr {
span,
kind: MetaPropKind::ImportMeta,
})),
prop: MemberProp::Ident(IdentName::new("main".into(), span)),
})
} else {
Expr::Lit(Lit::Bool(Bool { span, value: false }))
}),
},
])
}
}
pub(crate) fn process_js(
js_options: &JsAssetOptions,
source: &Path,
output_path: &Path,
bundle: bool,
) -> anyhow::Result<()> {
let js = if js_options.minified() {
minify_js(source)?
let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path)?);
if js_options.minified() {
if let Err(err) = bundle_js_to_writer(source.to_path_buf(), bundle, true, &mut writer) {
tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
}
} else {
let mut source_file = std::fs::File::open(source)?;
let mut source = String::new();
source_file.read_to_string(&mut source)?;
source
};
std::fs::write(output_path, js).with_context(|| {
format!(
"Failed to write js to output location: {}",
output_path.display()
)
})?;
std::io::copy(&mut source_file, &mut writer).with_context(|| {
format!(
"Failed to write js to output location: {}",
output_path.display()
)
})?;
}
Ok(())
}
+2 -1
View File
@@ -7,7 +7,7 @@ description = "CLI for building fullstack web, desktop, and mobile apps with a s
repository = "https://github.com/DioxusLabs/dioxus/"
license = "MIT OR Apache-2.0"
keywords = ["mobile", "gui", "cli", "dioxus", "wasm"]
rust-version = "1.79.0"
rust-version = "1.81.0"
[dependencies]
dioxus-autofmt = { workspace = true }
@@ -106,6 +106,7 @@ log = { version = "0.4", features = ["max_level_off", "release_max_level_off"] }
# link intercept
tempfile = "3.3"
manganis = { workspace = true }
manganis-core = { workspace = true }
# Extracting data from an executable
+61 -18
View File
@@ -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(())
+24
View File
@@ -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(),
+83 -24
View File
@@ -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 {
+1 -1
View File
@@ -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,
-1
View File
@@ -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 {
+1
View File
@@ -598,6 +598,7 @@ fn hide_last_window(window: &Window) {
// back to the app. `NSApplication::hide:` has the correct behaviour
use objc::runtime::Object;
use objc::{msg_send, sel, sel_impl};
#[allow(unexpected_cfgs)]
objc::rc::autoreleasepool(|| unsafe {
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
let nil = std::ptr::null_mut::<Object>();
+1
View File
@@ -215,6 +215,7 @@ impl WebviewInstance {
unsafe {
let window: id = window.ns_window() as id;
#[allow(unexpected_cfgs)]
let _: () = msg_send![window, setCollectionBehavior: NSWindowCollectionBehavior::NSWindowCollectionBehaviorManaged];
}
}
+2 -2
View File
@@ -320,7 +320,7 @@ impl History for LiveviewHistory {
visited_indices
.iter()
.position(|&rhs| timeline.current_index == rhs)
.map_or(false, |index| {
.is_some_and(|index| {
index > 0 && visited_indices[index - 1] == timeline.current_index - 1
})
}
@@ -332,7 +332,7 @@ impl History for LiveviewHistory {
visited_indices
.iter()
.rposition(|&rhs| timeline.current_index == rhs)
.map_or(false, |index| {
.is_some_and(|index| {
index < visited_indices.len() - 1
&& visited_indices[index + 1] == timeline.current_index + 1
})
@@ -59,6 +59,21 @@ impl BundledAsset {
Self::new(absolute_source_path, bundled_path, options)
}
#[doc(hidden)]
/// This should only be called from the macro
/// Create a new asset from const paths
pub const fn new_from_const(
absolute_source_path: ConstStr,
bundled_path: ConstStr,
options: AssetOptions,
) -> Self {
Self {
absolute_source_path,
bundled_path,
options,
}
}
/// Get the bundled name of the asset. This identifier cannot be used to read the asset directly
pub fn bundled_path(&self) -> &str {
self.bundled_path.as_str()
@@ -0,0 +1,94 @@
//! Utilities for creating hashed paths to assets in Manganis. This module defines [`AssetHash`] which is used to create a hashed path to an asset in both the CLI and the macro.
use std::{
error::Error,
hash::{Hash, Hasher},
io::Read,
path::{Path, PathBuf},
};
/// An error that can occur when hashing an asset
#[derive(Debug)]
#[non_exhaustive]
pub enum AssetHashError {
/// An io error occurred
IoError { err: std::io::Error, path: PathBuf },
}
impl std::fmt::Display for AssetHashError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetHashError::IoError { path, err } => {
write!(f, "Failed to read file: {}; {}", path.display(), err)
}
}
}
}
impl Error for AssetHashError {}
/// The opaque hash type manganis uses to identify assets. Each time an asset or asset options change, this hash will
/// change. This hash is included in the URL of the bundled asset for cache busting.
pub struct AssetHash {
/// We use a wrapper type here to hide the exact size of the hash so we can switch to a sha hash in a minor version bump
hash: [u8; 8],
}
impl AssetHash {
/// Create a new asset hash
const fn new(hash: u64) -> Self {
Self {
hash: hash.to_le_bytes(),
}
}
/// Get the hash bytes
pub const fn bytes(&self) -> &[u8] {
&self.hash
}
/// Create a new asset hash for a file. The input file to this function should be fully resolved
pub fn hash_file_contents(file_path: &Path) -> Result<AssetHash, AssetHashError> {
// Create a hasher
let mut hash = std::collections::hash_map::DefaultHasher::new();
// If this is a folder, hash the folder contents
if file_path.is_dir() {
let files = std::fs::read_dir(file_path).map_err(|err| AssetHashError::IoError {
err,
path: file_path.to_path_buf(),
})?;
for file in files.flatten() {
let path = file.path();
Self::hash_file_contents(&path)?.bytes().hash(&mut hash);
}
let hash = hash.finish();
return Ok(AssetHash::new(hash));
}
// Otherwise, open the file to get its contents
let mut file = std::fs::File::open(file_path).map_err(|err| AssetHashError::IoError {
err,
path: file_path.to_path_buf(),
})?;
// We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
// The hash includes the file contents, the options, and the version of manganis. From the macro, we just
// know the file contents, so we only include that hash
let mut buffer = [0; 8192];
loop {
let read = file
.read(&mut buffer)
.map_err(|err| AssetHashError::IoError {
err,
path: file_path.to_path_buf(),
})?;
if read == 0 {
break;
}
hash.write(&buffer[..read]);
}
Ok(AssetHash::new(hash.finish()))
}
}
@@ -17,3 +17,5 @@ mod asset;
pub use asset::*;
pub mod linker;
pub mod hash;
+32 -85
View File
@@ -1,15 +1,35 @@
use manganis_core::hash::AssetHash;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use std::{
hash::Hasher,
io::Read,
path::{Path, PathBuf},
};
use std::path::PathBuf;
use syn::{
parse::{Parse, ParseStream},
LitStr, Token,
};
#[derive(Debug)]
pub(crate) enum AssetParseError {
AssetDoesntExist { path: PathBuf },
InvalidPath { path: PathBuf },
}
impl std::fmt::Display for AssetParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetParseError::AssetDoesntExist { path } => {
write!(f, "Asset at {} doesn't exist", path.display())
}
AssetParseError::InvalidPath { path } => {
write!(
f,
"Asset path {} is invalid. Make sure the asset exists within this crate.",
path.display()
)
}
}
}
}
fn resolve_path(raw: &str) -> Result<PathBuf, AssetParseError> {
// Get the location of the root of the crate which is where all assets are relative to
//
@@ -53,75 +73,6 @@ fn resolve_path(raw: &str) -> Result<PathBuf, AssetParseError> {
Ok(path)
}
fn hash_file_contents(file_path: &Path) -> Result<u64, AssetParseError> {
// Create a hasher
let mut hash = std::collections::hash_map::DefaultHasher::new();
// If this is a folder, hash the folder contents
if file_path.is_dir() {
let files = std::fs::read_dir(file_path).map_err(|err| AssetParseError::IoError {
err,
path: file_path.to_path_buf(),
})?;
for file in files.flatten() {
let path = file.path();
hash_file_contents(&path)?;
}
return Ok(hash.finish());
}
// Otherwise, open the file to get its contents
let mut file = std::fs::File::open(file_path).map_err(|err| AssetParseError::IoError {
err,
path: file_path.to_path_buf(),
})?;
// We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
// The hash includes the file contents, the options, and the version of manganis. From the macro, we just
// know the file contents, so we only include that hash
let mut buffer = [0; 8192];
loop {
let read = file
.read(&mut buffer)
.map_err(AssetParseError::FailedToReadAsset)?;
if read == 0 {
break;
}
hash.write(&buffer[..read]);
}
Ok(hash.finish())
}
#[derive(Debug)]
pub(crate) enum AssetParseError {
AssetDoesntExist { path: PathBuf },
IoError { err: std::io::Error, path: PathBuf },
InvalidPath { path: PathBuf },
FailedToReadAsset(std::io::Error),
}
impl std::fmt::Display for AssetParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetParseError::AssetDoesntExist { path } => {
write!(f, "Asset at {} doesn't exist", path.display())
}
AssetParseError::IoError { path, err } => {
write!(f, "Failed to read file: {}; {}", path.display(), err)
}
AssetParseError::InvalidPath { path } => {
write!(
f,
"Asset path {} is invalid. Make sure the asset exists within this crate.",
path.display()
)
}
AssetParseError::FailedToReadAsset(err) => write!(f, "Failed to read asset: {}", err),
}
}
}
pub struct AssetParser {
/// The span of the source string
path_span: proc_macro2::Span,
@@ -189,7 +140,7 @@ impl ToTokens for AssetParser {
let mut asset_str = proc_macro2::Literal::string(&asset_str);
asset_str.set_span(self.path_span);
let hash = match hash_file_contents(asset) {
let hash = match AssetHash::hash_file_contents(asset) {
Ok(hash) => hash,
Err(err) => {
let err = err.to_string();
@@ -198,15 +149,17 @@ impl ToTokens for AssetParser {
}
};
let hash = hash.bytes();
// Generate the link section for the asset
// The link section includes the source path and the output path of the asset
let link_section = crate::generate_link_section(quote!(__ASSET));
// generate the asset::new method to deprecate the `./assets/blah.css` syntax
let constructor = if asset.is_relative() {
quote::quote! { new_relative }
quote::quote! { create_bundled_asset_relative }
} else {
quote::quote! { new }
quote::quote! { create_bundled_asset }
};
let options = if self.options.is_empty() {
@@ -218,22 +171,16 @@ impl ToTokens for AssetParser {
tokens.extend(quote! {
{
// We keep a hash of the contents of the asset for cache busting
const __ASSET_HASH: u64 = #hash;
const __ASSET_HASH: &[u8] = &[#(#hash),*];
// The source is used by the CLI to copy the asset
const __ASSET_SOURCE_PATH: &'static str = #asset_str;
// The options give the CLI info about how to process the asset
// Note: into_asset_options is not a trait, so we cannot accept the options directly
// in the constructor. Stable rust doesn't have support for constant functions in traits
const __ASSET_OPTIONS: manganis::AssetOptions = #options.into_asset_options();
// We calculate the bundled path from the hash and any transformations done by the options
// This is the final path that the asset will be written to
const __ASSET_BUNDLED_PATH: manganis::macro_helpers::const_serialize::ConstStr = manganis::macro_helpers::generate_unique_path(__ASSET_SOURCE_PATH, __ASSET_HASH, &__ASSET_OPTIONS);
// Get the reference to the string that was generated. We cannot return &'static str from
// generate_unique_path because it would return a reference to data generated in the function
const __ASSET_BUNDLED_PATH_STR: &'static str = __ASSET_BUNDLED_PATH.as_str();
// Create the asset that the crate will use. This is used both in the return value and
// added to the linker for the bundler to copy later
const __ASSET: manganis::BundledAsset = manganis::BundledAsset::#constructor(__ASSET_SOURCE_PATH, __ASSET_BUNDLED_PATH_STR, __ASSET_OPTIONS);
const __ASSET: manganis::BundledAsset = manganis::macro_helpers::#constructor(__ASSET_SOURCE_PATH, __ASSET_HASH, __ASSET_OPTIONS);
#link_section
@@ -4,11 +4,45 @@ use manganis_core::{AssetOptions, BundledAsset};
use crate::hash::ConstHasher;
/// Create a bundled asset from the input path, the content hash, and the asset options
pub const fn create_bundled_asset(
input_path: &str,
content_hash: &[u8],
asset_config: AssetOptions,
) -> BundledAsset {
let hashed_path = generate_unique_path_with_byte_hash(input_path, content_hash, &asset_config);
BundledAsset::new_from_const(ConstStr::new(input_path), hashed_path, asset_config)
}
/// Create a bundled asset from the input path, the content hash, and the asset options with a relative asset deprecation warning
///
/// This method is deprecated and will be removed in a future release.
#[deprecated(
note = "Relative asset!() paths are not supported. Use a path like `/assets/myfile.png` instead of `./assets/myfile.png`"
)]
pub const fn create_bundled_asset_relative(
input_path: &str,
content_hash: &[u8],
asset_config: AssetOptions,
) -> BundledAsset {
create_bundled_asset(input_path, content_hash, asset_config)
}
/// Format the input path with a hash to create an unique output path for the macro in the form `{input_path}-{hash}.{extension}`
pub const fn generate_unique_path(
input_path: &str,
content_hash: u64,
asset_config: &AssetOptions,
) -> ConstStr {
let byte_hash = content_hash.to_le_bytes();
generate_unique_path_with_byte_hash(input_path, &byte_hash, asset_config)
}
/// Format the input path with a hash to create an unique output path for the macro in the form `{input_path}-{hash}.{extension}`
const fn generate_unique_path_with_byte_hash(
input_path: &str,
content_hash: &[u8],
asset_config: &AssetOptions,
) -> ConstStr {
// Format the unique path with the format `{input_path}-{hash}.{extension}`
// Start with the input path
@@ -43,7 +77,7 @@ pub const fn generate_unique_path(
// Hash the contents along with the asset config to create a unique hash for the asset
// When this hash changes, the client needs to re-fetch the asset
let mut hasher = ConstHasher::new();
hasher = hasher.write(&content_hash.to_le_bytes());
hasher = hasher.write(content_hash);
hasher = hasher.hash_by_bytes(asset_config);
let hash = hasher.finish();
@@ -0,0 +1,11 @@
// @ts-check
const { test, expect } = require("@playwright/test");
test("optimized scripts run", async ({ page }) => {
await page.goto("http://localhost:8989");
// Expect the page to load the script after optimizations have been applied. The script
// should add an editor to the page that shows a main function
const main = page.locator("#main");
await expect(main).toContainText("hi");
});
@@ -0,0 +1,3 @@
dist
target
monaco-editor-0.52.2
@@ -0,0 +1,15 @@
[package]
name = "dioxus-cli-optimization-test"
version = "0.0.1"
edition = "2021"
description = "Playwright test for Dioxus CLI optimization"
license = "MIT OR Apache-2.0"
publish = false
[dependencies]
dioxus = { workspace = true, features = ["web"]}
[build-dependencies]
reqwest = { workspace = true, features = ["blocking"] }
flate2 = "1.0.35"
tar = "0.4.43"
@@ -0,0 +1,14 @@
use std::path::PathBuf;
fn main() {
// If the monaco editor folder doesn't exist, download it
let monaco_path = PathBuf::from("monaco-editor-0.52.2");
if monaco_path.exists() {
return;
}
let url = "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz";
let bytes = reqwest::blocking::get(url).unwrap().bytes().unwrap();
let mut archive = tar::Archive::new(flate2::read::GzDecoder::new(bytes.as_ref()));
archive.unpack(&monaco_path).unwrap();
}
@@ -0,0 +1,34 @@
// This test checks the CLI optimizes assets correctly without breaking them
use dioxus::prelude::*;
const MONACO_FOLDER: Asset = asset!("/monaco-editor-0.52.2/package/min/vs");
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
let script = format!("(() => {{
require.config({{ paths: {{ vs: '{MONACO_FOLDER}' }} }});
require(['vs/editor/editor.main'], () => {{
var model = monaco.editor.createModel('fn main() {{\\n\\tprintln!(\\\"hi\\\")\\n}}', 'rust');
var editor = monaco.editor.create(document.getElementById('editor'));
editor.setModel(model);
}})
}})()");
rsx! {
div {
id: "editor",
width: "100vw",
height: "100vw",
}
// Monaco script
script {
src: "{MONACO_FOLDER}/loader.js",
"onload": script
}
}
}
@@ -138,5 +138,15 @@ module.exports = defineConfig({
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "cli-optimization"),
// Remove the cache folder for the cli-optimization build to force a full cache reset
command:
'cargo run --package dioxus-cli --release -- serve --addr "127.0.0.1" --port 8989',
port: 8989,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
],
});