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
+27
-54
@@ -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" }
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in new issue
Block a user