Move asset hashing into the CLI (#3988)
* Fix deduping assets * hash assets in the linker * fix extracting assets * Fix the hashed asset name * deserialize the asset back from the link section * prevent the read from being inlined * remove some extra logs * Fix liveview routing * Only look for direct dependencies on dioxus features * fix wasm split test * On wasm, create a new object file with the asset information * Fix multiple assets * add a regression test for 3467 * fix image link * fix linker out file * fix linux linker args * add windows linker command * fix linux link args * fix windows target triple * Expose an option to build assets in the linker intercept * Fix the standalone optimizer and document it in the dx help command * include the cli opt version in the hash * fix clippy * fix ssg port * add a safety note to the read_volatile call * move found assets log * fix some merge conflicts * fix build * make most env var for the linker optional * guess the target triple when dx acts as the linker intercept * automatically find the linker for normal builds * use the right linker in wasm mode * fix mac linker * look inside archive files (rlibs) * remove out.txt * fix clippy * fix typo * fix clippy * fix build * fix dx link name in docs * ignore libcompiler_builtins warnings * fix playwright * pass typocheck * fix printing sysroot * Modify final binary * fix wasm assets * remove old link section logic * fix tests * forward manganis export args * fail to evaluate relocatable address * retain all exports in the same pattern as other platforms * Fix adding assets during a wasm hot patch * less nesting and remove logs * fix clippy * log pdb files * read from pdb * print data section contents * print with object * normal windows builds working * fix hot patch windows * clean up logs * fix clippy * clean up assets * use the most recent pdb * fix doc test * fix typo * tiny nits * make bundled a reference instead of a pointer * add helpful comment to impl * match on file format * Fix asset hashing comment --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
f865e3f559
commit
58669f892d
45 files changed
+1174
-762
No files matched your search
Generated
+7
-6
@@ -3811,7 +3811,7 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
"walrus",
|
"walrus",
|
||||||
"wasm-bindgen-externref-xform",
|
"wasm-bindgen-externref-xform",
|
||||||
"wasm-encoder 0.228.0",
|
"wasm-encoder 0.229.0",
|
||||||
"wasm-opt",
|
"wasm-opt",
|
||||||
"wasm-split-cli",
|
"wasm-split-cli",
|
||||||
"wasmparser 0.226.0",
|
"wasmparser 0.226.0",
|
||||||
@@ -3831,6 +3831,7 @@ version = "0.7.0-alpha.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"browserslist-rs 0.16.0",
|
"browserslist-rs 0.16.0",
|
||||||
|
"built",
|
||||||
"codemap",
|
"codemap",
|
||||||
"const-serialize",
|
"const-serialize",
|
||||||
"grass",
|
"grass",
|
||||||
@@ -15940,12 +15941,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-encoder"
|
name = "wasm-encoder"
|
||||||
version = "0.228.0"
|
version = "0.229.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05d30290541f2d4242a162bbda76b8f2d8b1ac59eab3568ed6f2327d52c9b2c4"
|
checksum = "38ba1d491ecacb085a2552025c10a675a6fddcbd03b1fc9b36c536010ce265d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"leb128fmt",
|
"leb128fmt",
|
||||||
"wasmparser 0.228.0",
|
"wasmparser 0.229.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -16101,9 +16102,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasmparser"
|
name = "wasmparser"
|
||||||
version = "0.228.0"
|
version = "0.229.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"
|
checksum = "0cc3b1f053f5d41aa55640a1fa9b6d1b8a9e4418d118ce308d20e24ff3575a8c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"indexmap 2.9.0",
|
"indexmap 2.9.0",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ pub fn serve_asset_from_raw_path(path: &str) -> Result<Response<Vec<u8>>, AssetS
|
|||||||
/// - [ ] Linux (rpm)
|
/// - [ ] Linux (rpm)
|
||||||
/// - [ ] Linux (deb)
|
/// - [ ] Linux (deb)
|
||||||
/// - [ ] Android
|
/// - [ ] Android
|
||||||
#[allow(unused)]
|
#[allow(unreachable_code)]
|
||||||
fn get_asset_root() -> PathBuf {
|
fn get_asset_root() -> PathBuf {
|
||||||
let cur_exe = std::env::current_exe().unwrap();
|
let cur_exe = std::env::current_exe().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -67,3 +67,6 @@ swc_parallel = { version = "=1.0.1", default-features = false }
|
|||||||
swc_timer = { version = "=1.0.0", default-features = false }
|
swc_timer = { version = "=1.0.0", default-features = false }
|
||||||
swc_visit = { version = "=2.0.0", default-features = false }
|
swc_visit = { version = "=2.0.0", default-features = false }
|
||||||
browserslist-rs = { version = "=0.16.0" }
|
browserslist-rs = { version = "=0.16.0" }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
built = { version = "0.7.5", features = ["git2"] }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
built::write_built_file().expect("Failed to acquire build-time information");
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// The file has been placed there by the build script.
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||||
|
|
||||||
|
pub(crate) fn version() -> String {
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
PKG_VERSION,
|
||||||
|
GIT_COMMIT_HASH_SHORT.unwrap_or("was built without git repository")
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::path::Path;
|
use std::{hash::Hasher, path::Path};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use codemap::SpanLoc;
|
use codemap::SpanLoc;
|
||||||
@@ -146,12 +146,11 @@ pub(crate) fn minify_css(css: &str) -> anyhow::Result<String> {
|
|||||||
Ok(res.code)
|
Ok(res.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process an scss/sass file into css.
|
/// Compile scss with grass
|
||||||
pub(crate) fn process_scss(
|
pub(crate) fn compile_scss(
|
||||||
scss_options: &CssAssetOptions,
|
scss_options: &CssAssetOptions,
|
||||||
source: &Path,
|
source: &Path,
|
||||||
output_path: &Path,
|
) -> anyhow::Result<String> {
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let style = match scss_options.minified() {
|
let style = match scss_options.minified() {
|
||||||
true => OutputStyle::Compressed,
|
true => OutputStyle::Compressed,
|
||||||
false => OutputStyle::Expanded,
|
false => OutputStyle::Expanded,
|
||||||
@@ -162,7 +161,18 @@ pub(crate) fn process_scss(
|
|||||||
.quiet(false)
|
.quiet(false)
|
||||||
.logger(&ScssLogger {});
|
.logger(&ScssLogger {});
|
||||||
|
|
||||||
let css = grass::from_path(source, &options)?;
|
let css = grass::from_path(source, &options)
|
||||||
|
.with_context(|| format!("Failed to compile scss file: {}", source.display()))?;
|
||||||
|
Ok(css)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process an scss/sass file into css.
|
||||||
|
pub(crate) fn process_scss(
|
||||||
|
scss_options: &CssAssetOptions,
|
||||||
|
source: &Path,
|
||||||
|
output_path: &Path,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let css = compile_scss(scss_options, source)?;
|
||||||
let minified = minify_css(&css)?;
|
let minified = minify_css(&css)?;
|
||||||
|
|
||||||
std::fs::write(output_path, minified).with_context(|| {
|
std::fs::write(output_path, minified).with_context(|| {
|
||||||
@@ -199,3 +209,19 @@ impl grass::Logger for ScssLogger {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hash the inputs to the scss file
|
||||||
|
pub(crate) fn hash_scss(
|
||||||
|
scss_options: &CssAssetOptions,
|
||||||
|
source: &Path,
|
||||||
|
hasher: &mut impl Hasher,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// Grass doesn't expose the ast for us to traverse the imports in the file. Instead of parsing scss ourselves
|
||||||
|
// we just hash the expanded version of the file for now
|
||||||
|
let css = compile_scss(scss_options, source)?;
|
||||||
|
|
||||||
|
// Hash the compiled css
|
||||||
|
hasher.write(css.as_bytes());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use manganis::{CssModuleAssetOptions, FolderAssetOptions};
|
||||||
use manganis_core::{AssetOptions, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
|
use manganis_core::{AssetOptions, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ pub(crate) fn process_file_to_with_options(
|
|||||||
}
|
}
|
||||||
if let Some(parent) = output_path.parent() {
|
if let Some(parent) = output_path.parent() {
|
||||||
if !parent.exists() {
|
if !parent.exists() {
|
||||||
std::fs::create_dir_all(parent)?;
|
std::fs::create_dir_all(parent).context("Failed to create directory")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,63 +48,94 @@ pub(crate) fn process_file_to_with_options(
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
));
|
));
|
||||||
|
let resolved_options = resolve_asset_options(source, options);
|
||||||
|
|
||||||
match options {
|
match &resolved_options {
|
||||||
AssetOptions::Unknown => match source.extension().map(|e| e.to_string_lossy()).as_deref() {
|
ResolvedAssetType::Css(options) => {
|
||||||
Some("css") => {
|
|
||||||
process_css(&CssAssetOptions::new(), source, &temp_path)?;
|
|
||||||
}
|
|
||||||
Some("scss" | "sass") => {
|
|
||||||
process_scss(&CssAssetOptions::new(), source, &temp_path)?;
|
|
||||||
}
|
|
||||||
Some("js") => {
|
|
||||||
process_js(&JsAssetOptions::new(), source, &temp_path, !in_folder)?;
|
|
||||||
}
|
|
||||||
Some("json") => {
|
|
||||||
process_json(source, &temp_path)?;
|
|
||||||
}
|
|
||||||
Some("jpg" | "jpeg" | "png" | "webp" | "avif") => {
|
|
||||||
process_image(&ImageAssetOptions::new(), source, &temp_path)?;
|
|
||||||
}
|
|
||||||
Some(_) | None => {
|
|
||||||
if source.is_dir() {
|
|
||||||
process_folder(source, &temp_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(&temp_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: {}",
|
|
||||||
temp_path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
AssetOptions::Css(options) => {
|
|
||||||
process_css(options, source, &temp_path)?;
|
process_css(options, source, &temp_path)?;
|
||||||
}
|
}
|
||||||
AssetOptions::CssModule(options) => {
|
ResolvedAssetType::CssModule(options) => {
|
||||||
process_css_module(options, source, output_path, &temp_path)?;
|
process_css_module(options, source, output_path, &temp_path)?;
|
||||||
}
|
}
|
||||||
AssetOptions::Js(options) => {
|
ResolvedAssetType::Scss(options) => {
|
||||||
|
process_scss(options, source, &temp_path)?;
|
||||||
|
}
|
||||||
|
ResolvedAssetType::Js(options) => {
|
||||||
process_js(options, source, &temp_path, !in_folder)?;
|
process_js(options, source, &temp_path, !in_folder)?;
|
||||||
}
|
}
|
||||||
AssetOptions::Image(options) => {
|
ResolvedAssetType::Image(options) => {
|
||||||
process_image(options, source, &temp_path)?;
|
process_image(options, source, &temp_path)?;
|
||||||
}
|
}
|
||||||
AssetOptions::Folder(_) => {
|
ResolvedAssetType::Json => {
|
||||||
|
process_json(source, &temp_path)?;
|
||||||
|
}
|
||||||
|
ResolvedAssetType::Folder(_) => {
|
||||||
process_folder(source, &temp_path)?;
|
process_folder(source, &temp_path)?;
|
||||||
}
|
}
|
||||||
_ => {
|
ResolvedAssetType::File => {
|
||||||
tracing::warn!("Unknown asset options: {:?}", options);
|
let source_file = std::fs::File::open(source)?;
|
||||||
|
let mut reader = std::io::BufReader::new(source_file);
|
||||||
|
let output_file = std::fs::File::create(&temp_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: {}",
|
||||||
|
temp_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If everything was successful, rename the temp file to the final output path
|
// If everything was successful, rename the temp file to the final output path
|
||||||
std::fs::rename(temp_path, output_path)?;
|
std::fs::rename(temp_path, output_path).context("Failed to rename output file")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) enum ResolvedAssetType {
|
||||||
|
/// An image asset
|
||||||
|
Image(ImageAssetOptions),
|
||||||
|
/// A css asset
|
||||||
|
Css(CssAssetOptions),
|
||||||
|
/// A css module asset
|
||||||
|
CssModule(CssModuleAssetOptions),
|
||||||
|
/// A SCSS asset
|
||||||
|
Scss(CssAssetOptions),
|
||||||
|
/// A javascript asset
|
||||||
|
Js(JsAssetOptions),
|
||||||
|
/// A json asset
|
||||||
|
Json,
|
||||||
|
/// A folder asset
|
||||||
|
Folder(FolderAssetOptions),
|
||||||
|
/// A generic file
|
||||||
|
File,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_asset_options(source: &Path, options: &AssetOptions) -> ResolvedAssetType {
|
||||||
|
match options {
|
||||||
|
AssetOptions::Image(image) => ResolvedAssetType::Image(*image),
|
||||||
|
AssetOptions::Css(css) => ResolvedAssetType::Css(*css),
|
||||||
|
AssetOptions::CssModule(css) => ResolvedAssetType::CssModule(*css),
|
||||||
|
AssetOptions::Js(js) => ResolvedAssetType::Js(*js),
|
||||||
|
AssetOptions::Folder(folder) => ResolvedAssetType::Folder(*folder),
|
||||||
|
AssetOptions::Unknown => resolve_unknown_asset_options(source),
|
||||||
|
_ => {
|
||||||
|
tracing::warn!("Unknown asset options... you may need to update the Dioxus CLI. Defaulting to a generic file: {:?}", options);
|
||||||
|
resolve_unknown_asset_options(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_unknown_asset_options(source: &Path) -> ResolvedAssetType {
|
||||||
|
match source.extension().map(|e| e.to_string_lossy()).as_deref() {
|
||||||
|
Some("scss" | "sass") => ResolvedAssetType::Scss(CssAssetOptions::new()),
|
||||||
|
Some("css") => ResolvedAssetType::Css(CssAssetOptions::new()),
|
||||||
|
Some("js") => ResolvedAssetType::Js(JsAssetOptions::new()),
|
||||||
|
Some("json") => ResolvedAssetType::Json,
|
||||||
|
Some("jpg" | "jpeg" | "png" | "webp" | "avif") => {
|
||||||
|
ResolvedAssetType::Image(ImageAssetOptions::new())
|
||||||
|
}
|
||||||
|
_ if source.is_dir() => ResolvedAssetType::Folder(FolderAssetOptions::new()),
|
||||||
|
_ => ResolvedAssetType::File,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
//! 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::{
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
io::Read,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
css::hash_scss,
|
||||||
|
file::{resolve_asset_options, ResolvedAssetType},
|
||||||
|
js::hash_js,
|
||||||
|
};
|
||||||
|
use manganis::{AssetOptions, BundledAsset};
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
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(
|
||||||
|
options: &AssetOptions,
|
||||||
|
file_path: impl AsRef<Path>,
|
||||||
|
) -> anyhow::Result<AssetHash> {
|
||||||
|
hash_file(options, file_path.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a specific file asset with the given options reading from the source and writing to the output path
|
||||||
|
fn hash_file(options: &AssetOptions, source: &Path) -> anyhow::Result<AssetHash> {
|
||||||
|
// Create a hasher
|
||||||
|
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
options.hash(&mut hash);
|
||||||
|
// Hash the version of CLI opt
|
||||||
|
hash.write(crate::build_info::version().as_bytes());
|
||||||
|
hash_file_with_options(options, source, &mut hash, false)?;
|
||||||
|
|
||||||
|
let hash = hash.finish();
|
||||||
|
Ok(AssetHash::new(hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a specific file asset with additional options
|
||||||
|
pub(crate) fn hash_file_with_options(
|
||||||
|
options: &AssetOptions,
|
||||||
|
source: &Path,
|
||||||
|
hasher: &mut impl Hasher,
|
||||||
|
in_folder: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let resolved_options = resolve_asset_options(source, options);
|
||||||
|
|
||||||
|
match &resolved_options {
|
||||||
|
// Scss and JS can import files during the bundling process. We need to hash
|
||||||
|
// both the files themselves and any imports they have
|
||||||
|
ResolvedAssetType::Scss(options) => {
|
||||||
|
hash_scss(options, source, hasher)?;
|
||||||
|
}
|
||||||
|
ResolvedAssetType::Js(options) => {
|
||||||
|
hash_js(options, source, hasher, !in_folder)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we can just hash the file contents
|
||||||
|
ResolvedAssetType::CssModule(_)
|
||||||
|
| ResolvedAssetType::Css(_)
|
||||||
|
| ResolvedAssetType::Image(_)
|
||||||
|
| ResolvedAssetType::Json
|
||||||
|
| ResolvedAssetType::File => {
|
||||||
|
hash_file_contents(source, hasher)?;
|
||||||
|
}
|
||||||
|
// Or the folder contents recursively
|
||||||
|
ResolvedAssetType::Folder(_) => {
|
||||||
|
let files = std::fs::read_dir(source)?;
|
||||||
|
for file in files.flatten() {
|
||||||
|
let path = file.path();
|
||||||
|
hash_file_with_options(options, &path, hasher, true)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hash_file_contents(source: &Path, hasher: &mut impl Hasher) -> anyhow::Result<()> {
|
||||||
|
// Otherwise, open the file to get its contents
|
||||||
|
let mut file = std::fs::File::open(source)?;
|
||||||
|
|
||||||
|
// 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)?;
|
||||||
|
if read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hasher.write(&buffer[..read]);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a hash to the asset, or log an error if it fails
|
||||||
|
pub fn add_hash_to_asset(asset: &mut BundledAsset) {
|
||||||
|
let source = asset.absolute_source_path();
|
||||||
|
match AssetHash::hash_file_contents(asset.options(), source) {
|
||||||
|
Ok(hash) => {
|
||||||
|
let options = *asset.options();
|
||||||
|
|
||||||
|
// Set the bundled path to the source path with the hash appended before the extension
|
||||||
|
let source_path = PathBuf::from(source);
|
||||||
|
let Some(file_name) = source_path.file_name() else {
|
||||||
|
tracing::error!("Failed to get file name from path: {source}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// The output extension path is the extension set by the options
|
||||||
|
// or the extension of the source file if we don't recognize the file
|
||||||
|
let mut ext = asset.options().extension().map(Into::into).or_else(|| {
|
||||||
|
source_path
|
||||||
|
.extension()
|
||||||
|
.map(|ext| ext.to_string_lossy().to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rewrite scss as css
|
||||||
|
if let Some("scss" | "sass") = ext.as_deref() {
|
||||||
|
ext = Some("css".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash = hash.bytes();
|
||||||
|
let hash = hash
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{byte:x}"))
|
||||||
|
.collect::<String>();
|
||||||
|
let file_stem = source_path.file_stem().unwrap_or(file_name);
|
||||||
|
let mut bundled_path = PathBuf::from(format!("{}-{hash}", file_stem.to_string_lossy()));
|
||||||
|
|
||||||
|
if let Some(ext) = ext {
|
||||||
|
bundled_path.set_extension(ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bundled_path = bundled_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
*asset = BundledAsset::new(source, &bundled_path, options);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Failed to hash asset: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ pub(crate) fn process_image(
|
|||||||
output_path: &Path,
|
output_path: &Path,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut image = image::ImageReader::new(std::io::Cursor::new(&*std::fs::read(source)?))
|
let mut image = image::ImageReader::new(std::io::Cursor::new(&*std::fs::read(source)?))
|
||||||
.with_guessed_format()?
|
.with_guessed_format()
|
||||||
|
.context("Failed to guess image format")?
|
||||||
.decode();
|
.decode();
|
||||||
|
|
||||||
if let Ok(image) = &mut image {
|
if let Ok(image) = &mut image {
|
||||||
@@ -25,10 +26,10 @@ pub(crate) fn process_image(
|
|||||||
|
|
||||||
match (image, image_options.format()) {
|
match (image, image_options.format()) {
|
||||||
(image, ImageFormat::Png) => {
|
(image, ImageFormat::Png) => {
|
||||||
compress_png(image?, output_path);
|
compress_png(image.context("Failed to decode image")?, output_path);
|
||||||
}
|
}
|
||||||
(image, ImageFormat::Jpg) => {
|
(image, ImageFormat::Jpg) => {
|
||||||
compress_jpg(image?, output_path)?;
|
compress_jpg(image.context("Failed to decode image")?, output_path)?;
|
||||||
}
|
}
|
||||||
(Ok(image), ImageFormat::Avif) => {
|
(Ok(image), ImageFormat::Avif) => {
|
||||||
if let Err(error) = image.save(output_path) {
|
if let Err(error) = image.save(output_path) {
|
||||||
@@ -41,21 +42,31 @@ pub(crate) fn process_image(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Ok(image), _) => {
|
(Ok(image), _) => {
|
||||||
image.save(output_path)?;
|
image.save(output_path).with_context(|| {
|
||||||
}
|
|
||||||
// If we can't decode the image or it is of an unknown type, we just copy the file
|
|
||||||
_ => {
|
|
||||||
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 mut writer = std::io::BufWriter::new(output_file);
|
|
||||||
std::io::copy(&mut reader, &mut writer).with_context(|| {
|
|
||||||
format!(
|
format!(
|
||||||
"Failed to write image to output location: {}",
|
"Failed to save image (from {}) with path {}",
|
||||||
|
source.display(),
|
||||||
output_path.display()
|
output_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
// If we can't decode the image or it is of an unknown type, we just copy the file
|
||||||
|
_ => {
|
||||||
|
let source_file = std::fs::File::open(source).context("Failed to open source file")?;
|
||||||
|
let mut reader = std::io::BufReader::new(source_file);
|
||||||
|
let output_file = std::fs::File::create(output_path).with_context(|| {
|
||||||
|
format!("Failed to create output file: {}", output_path.display())
|
||||||
|
})?;
|
||||||
|
let mut writer = std::io::BufWriter::new(output_file);
|
||||||
|
std::io::copy(&mut reader, &mut writer)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to write image to output location: {}",
|
||||||
|
output_path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.context("Failed to copy image data")?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+62
-15
@@ -1,3 +1,4 @@
|
|||||||
|
use std::hash::Hasher;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ use swc_ecma_codegen::text_writer::JsWriter;
|
|||||||
use swc_ecma_loader::{resolvers::node::NodeModulesResolver, TargetEnv};
|
use swc_ecma_loader::{resolvers::node::NodeModulesResolver, TargetEnv};
|
||||||
use swc_ecma_parser::{parse_file_as_module, Syntax};
|
use swc_ecma_parser::{parse_file_as_module, Syntax};
|
||||||
|
|
||||||
|
use crate::hash::hash_file_contents;
|
||||||
|
|
||||||
struct TracingEmitter;
|
struct TracingEmitter;
|
||||||
|
|
||||||
impl Emitter for TracingEmitter {
|
impl Emitter for TracingEmitter {
|
||||||
@@ -43,30 +46,32 @@ impl Emitter for TracingEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run a closure with the swc globals and handler set up
|
||||||
|
fn inside_handler<O>(f: impl FnOnce(&Globals, Lrc<SourceMap>) -> O) -> O {
|
||||||
|
let globals = Globals::new();
|
||||||
|
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
|
||||||
|
let handler = Handler::with_emitter_and_flags(Box::new(TracingEmitter), Default::default());
|
||||||
|
GLOBALS.set(&globals, || HANDLER.set(&handler, || f(&globals, cm)))
|
||||||
|
}
|
||||||
|
|
||||||
fn bundle_js_to_writer(
|
fn bundle_js_to_writer(
|
||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
bundle: bool,
|
bundle: bool,
|
||||||
minify: bool,
|
minify: bool,
|
||||||
write_to: &mut impl std::io::Write,
|
write_to: &mut impl std::io::Write,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let globals = Globals::new();
|
inside_handler(|globals, cm| {
|
||||||
let handler = Handler::with_emitter_and_flags(Box::new(TracingEmitter), Default::default());
|
bundle_js_to_writer_inside_handler(globals, cm, file, bundle, minify, write_to)
|
||||||
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(
|
fn resolve_js_inside_handler(
|
||||||
globals: &Globals,
|
globals: &Globals,
|
||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
bundle: bool,
|
bundle: bool,
|
||||||
minify: bool,
|
cm: &Lrc<SourceMap>,
|
||||||
write_to: &mut impl std::io::Write,
|
) -> anyhow::Result<Module> {
|
||||||
) -> anyhow::Result<()> {
|
if bundle {
|
||||||
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
|
|
||||||
let mut module = if bundle {
|
|
||||||
let node_resolver = NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true);
|
let node_resolver = NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true);
|
||||||
let mut bundler = Bundler::new(
|
let mut bundler = Bundler::new(
|
||||||
globals,
|
globals,
|
||||||
@@ -89,7 +94,7 @@ fn bundle_js_to_writer_inside_handler(
|
|||||||
let bundle = bundles
|
let bundle = bundles
|
||||||
.pop()
|
.pop()
|
||||||
.ok_or_else(|| anyhow::anyhow!("swc did not output any bundles"))?;
|
.ok_or_else(|| anyhow::anyhow!("swc did not output any bundles"))?;
|
||||||
bundle.module
|
Ok(bundle.module)
|
||||||
} else {
|
} else {
|
||||||
let fm = cm.load_file(Path::new(&file)).expect("Failed to load file");
|
let fm = cm.load_file(Path::new(&file)).expect("Failed to load file");
|
||||||
|
|
||||||
@@ -108,8 +113,19 @@ fn bundle_js_to_writer_inside_handler(
|
|||||||
error.cancel();
|
error.cancel();
|
||||||
anyhow::anyhow!("{}", error.message())
|
anyhow::anyhow!("{}", error.message())
|
||||||
})
|
})
|
||||||
})?
|
})
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bundle_js_to_writer_inside_handler(
|
||||||
|
globals: &Globals,
|
||||||
|
cm: Lrc<SourceMap>,
|
||||||
|
file: PathBuf,
|
||||||
|
bundle: bool,
|
||||||
|
minify: bool,
|
||||||
|
write_to: &mut impl std::io::Write,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut module = resolve_js_inside_handler(globals, file, bundle, &cm)?;
|
||||||
|
|
||||||
if minify {
|
if minify {
|
||||||
module = swc_ecma_minifier::optimize(
|
module = swc_ecma_minifier::optimize(
|
||||||
@@ -246,3 +262,34 @@ pub(crate) fn process_js(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hash_js_module(file: PathBuf, hasher: &mut impl Hasher, bundle: bool) -> anyhow::Result<()> {
|
||||||
|
inside_handler(|globals, cm| {
|
||||||
|
_ = resolve_js_inside_handler(globals, file, bundle, &cm)?;
|
||||||
|
|
||||||
|
for file in cm.files().iter() {
|
||||||
|
let hash = file.src_hash;
|
||||||
|
hasher.write(&hash.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hash_js(
|
||||||
|
js_options: &JsAssetOptions,
|
||||||
|
source: &Path,
|
||||||
|
hasher: &mut impl Hasher,
|
||||||
|
bundle: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if js_options.minified() {
|
||||||
|
if let Err(err) = hash_js_module(source.to_path_buf(), hasher, bundle) {
|
||||||
|
tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
|
||||||
|
hash_file_contents(source, hasher)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hash_file_contents(source, hasher)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+65
-79
@@ -1,19 +1,23 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use manganis_core::linker::LinkSection;
|
use manganis::AssetOptions;
|
||||||
use manganis_core::BundledAsset;
|
use manganis_core::BundledAsset;
|
||||||
use object::{read::archive::ArchiveFile, File as ObjectFile, Object, ObjectSection};
|
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::Path;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
mod build_info;
|
||||||
mod css;
|
mod css;
|
||||||
mod file;
|
mod file;
|
||||||
mod folder;
|
mod folder;
|
||||||
|
mod hash;
|
||||||
mod image;
|
mod image;
|
||||||
mod js;
|
mod js;
|
||||||
mod json;
|
mod json;
|
||||||
|
|
||||||
pub use file::process_file_to;
|
pub use file::process_file_to;
|
||||||
|
pub use hash::add_hash_to_asset;
|
||||||
|
|
||||||
/// A manifest of all assets collected from dependencies
|
/// A manifest of all assets collected from dependencies
|
||||||
///
|
///
|
||||||
@@ -21,7 +25,7 @@ pub use file::process_file_to;
|
|||||||
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
|
||||||
pub struct AssetManifest {
|
pub struct AssetManifest {
|
||||||
/// Map of bundled asset name to the asset itself
|
/// Map of bundled asset name to the asset itself
|
||||||
pub assets: HashMap<PathBuf, BundledAsset>,
|
assets: HashMap<PathBuf, HashSet<BundledAsset>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssetManifest {
|
impl AssetManifest {
|
||||||
@@ -31,100 +35,82 @@ impl AssetManifest {
|
|||||||
asset_path: &Path,
|
asset_path: &Path,
|
||||||
options: manganis::AssetOptions,
|
options: manganis::AssetOptions,
|
||||||
) -> anyhow::Result<BundledAsset> {
|
) -> anyhow::Result<BundledAsset> {
|
||||||
let hash = manganis_core::hash::AssetHash::hash_file_contents(asset_path)
|
|
||||||
.context("Failed to hash file")?;
|
|
||||||
|
|
||||||
let output_path_str = asset_path.to_str().ok_or(anyhow::anyhow!(
|
let output_path_str = asset_path.to_str().ok_or(anyhow::anyhow!(
|
||||||
"Failed to convert wasm bindgen output path to string"
|
"Failed to convert wasm bindgen output path to string"
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
let bundled_asset =
|
let mut bundled_asset =
|
||||||
manganis::macro_helpers::create_bundled_asset(output_path_str, hash.bytes(), options);
|
manganis::macro_helpers::create_bundled_asset(output_path_str, options);
|
||||||
|
add_hash_to_asset(&mut bundled_asset);
|
||||||
|
|
||||||
self.assets.insert(asset_path.into(), bundled_asset);
|
self.assets
|
||||||
|
.entry(asset_path.to_path_buf())
|
||||||
|
.or_default()
|
||||||
|
.insert(bundled_asset);
|
||||||
|
|
||||||
Ok(bundled_asset)
|
Ok(bundled_asset)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
/// Insert an existing bundled asset to the manifest
|
||||||
|
pub fn insert_asset(&mut self, asset: BundledAsset) {
|
||||||
|
let asset_path = asset.absolute_source_path();
|
||||||
|
self.assets
|
||||||
|
.entry(asset_path.into())
|
||||||
|
.or_default()
|
||||||
|
.insert(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get any assets that are tied to a specific source file
|
||||||
|
pub fn get_assets_for_source(&self, path: &Path) -> Option<&HashSet<BundledAsset>> {
|
||||||
|
self.assets.get(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the manifest contains a specific asset
|
||||||
|
pub fn contains(&self, asset: &BundledAsset) -> bool {
|
||||||
|
self.assets
|
||||||
|
.get(&PathBuf::from(asset.absolute_source_path()))
|
||||||
|
.is_some_and(|assets| assets.contains(asset))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all the assets in the manifest
|
||||||
|
pub fn assets(&self) -> impl Iterator<Item = &BundledAsset> {
|
||||||
|
self.assets.values().flat_map(|assets| assets.iter())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_from_file(path: &Path) -> anyhow::Result<Self> {
|
pub fn load_from_file(path: &Path) -> anyhow::Result<Self> {
|
||||||
let src = std::fs::read_to_string(path)?;
|
let src = std::fs::read_to_string(path)?;
|
||||||
|
|
||||||
serde_json::from_str(&src)
|
serde_json::from_str(&src)
|
||||||
.with_context(|| format!("Failed to parse asset manifest from {path:?}\n{src}"))
|
.with_context(|| format!("Failed to parse asset manifest from {path:?}\n{src}"))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// Fill this manifest with a file object/rlib files, typically extracted from the linker intercepted
|
|
||||||
pub fn add_from_object_path(&mut self, path: &Path) -> anyhow::Result<()> {
|
/// Optimize a list of assets in parallel
|
||||||
let data = std::fs::read(path)?;
|
pub fn optimize_all_assets(
|
||||||
|
assets_to_transfer: Vec<(PathBuf, PathBuf, AssetOptions)>,
|
||||||
match path.extension().and_then(|ext| ext.to_str()) {
|
on_optimization_start: impl FnMut(&Path, &Path, &AssetOptions) + Sync + Send,
|
||||||
// Parse an rlib as a collection of objects
|
on_optimization_end: impl FnMut(&Path, &Path, &AssetOptions) + Sync + Send,
|
||||||
Some("rlib") => {
|
) -> anyhow::Result<()> {
|
||||||
if let Ok(archive) = object::read::archive::ArchiveFile::parse(&*data) {
|
let on_optimization_start = Arc::new(RwLock::new(on_optimization_start));
|
||||||
self.add_from_archive_file(&archive, &data)?;
|
let on_optimization_end = Arc::new(RwLock::new(on_optimization_end));
|
||||||
}
|
assets_to_transfer
|
||||||
}
|
.par_iter()
|
||||||
_ => {
|
.try_for_each(|(from, to, options)| {
|
||||||
if let Ok(object) = object::File::parse(&*data) {
|
{
|
||||||
self.add_from_object_file(&object)?;
|
let mut on_optimization_start = on_optimization_start.write().unwrap();
|
||||||
}
|
on_optimization_start(from, to, options);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fill this manifest from an rlib / ar file that contains many object files and their entries
|
|
||||||
fn add_from_archive_file(&mut self, archive: &ArchiveFile, data: &[u8]) -> object::Result<()> {
|
|
||||||
// Look through each archive member for object files.
|
|
||||||
// Read the archive member's binary data (we know it's an object file)
|
|
||||||
// And parse it with the normal `object::File::parse` to find the manganis string.
|
|
||||||
for member in archive.members() {
|
|
||||||
let member = member?;
|
|
||||||
let name = String::from_utf8_lossy(member.name()).to_string();
|
|
||||||
|
|
||||||
// Check if the archive member is an object file and parse it.
|
|
||||||
if name.ends_with(".o") {
|
|
||||||
let data = member.data(data)?;
|
|
||||||
let object = object::File::parse(data)?;
|
|
||||||
_ = self.add_from_object_file(&object);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fill this manifest with whatever tables might come from the object file
|
let res = process_file_to(options, from, to);
|
||||||
fn add_from_object_file(&mut self, obj: &ObjectFile) -> anyhow::Result<()> {
|
if let Err(err) = res.as_ref() {
|
||||||
for section in obj.sections() {
|
tracing::error!("Failed to copy asset {from:?}: {err}");
|
||||||
let Ok(section_name) = section.name() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if the link section matches the asset section for one of the platforms we support. This may not be the current platform if the user is cross compiling
|
|
||||||
let matches = LinkSection::ALL
|
|
||||||
.iter()
|
|
||||||
.any(|x| x.link_section == section_name);
|
|
||||||
|
|
||||||
if !matches {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytes = section
|
|
||||||
.uncompressed_data()
|
|
||||||
.context("Could not read uncompressed data from object file")?;
|
|
||||||
|
|
||||||
let mut buffer = const_serialize::ConstReadBuffer::new(&bytes);
|
|
||||||
while let Some((remaining_buffer, asset)) =
|
|
||||||
const_serialize::deserialize_const!(BundledAsset, buffer)
|
|
||||||
{
|
{
|
||||||
self.assets
|
let mut on_optimization_end = on_optimization_end.write().unwrap();
|
||||||
.insert(asset.absolute_source_path().into(), asset);
|
on_optimization_end(from, to, options);
|
||||||
buffer = remaining_buffer;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
res.map(|_| ())
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,6 @@ hyper-rustls = { workspace = true }
|
|||||||
rustls = { workspace = true }
|
rustls = { workspace = true }
|
||||||
rayon = { workspace = true }
|
rayon = { workspace = true }
|
||||||
futures-channel = { workspace = true }
|
futures-channel = { workspace = true }
|
||||||
target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
|
|
||||||
krates = { workspace = true }
|
krates = { workspace = true }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
console = "0.15.11"
|
console = "0.15.11"
|
||||||
@@ -61,7 +60,12 @@ axum-server = { workspace = true, features = ["tls-rustls"] }
|
|||||||
axum-extra = { workspace = true, features = ["typed-header"] }
|
axum-extra = { workspace = true, features = ["typed-header"] }
|
||||||
tower-http = { workspace = true, features = ["full"] }
|
tower-http = { workspace = true, features = ["full"] }
|
||||||
proc-macro2 = { workspace = true, features = ["span-locations"] }
|
proc-macro2 = { workspace = true, features = ["span-locations"] }
|
||||||
syn = { workspace = true, features = ["full", "extra-traits", "visit", "visit-mut"] }
|
syn = { workspace = true, features = [
|
||||||
|
"full",
|
||||||
|
"extra-traits",
|
||||||
|
"visit",
|
||||||
|
"visit-mut",
|
||||||
|
] }
|
||||||
|
|
||||||
headers = "0.4.0"
|
headers = "0.4.0"
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
@@ -108,6 +112,8 @@ log = { version = "0.4", features = ["max_level_off", "release_max_level_off"] }
|
|||||||
tempfile = "3.13"
|
tempfile = "3.13"
|
||||||
manganis = { workspace = true }
|
manganis = { workspace = true }
|
||||||
manganis-core = { workspace = true }
|
manganis-core = { workspace = true }
|
||||||
|
target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
|
||||||
|
wasm-encoder = "0.229.0"
|
||||||
|
|
||||||
# Extracting data from an executable
|
# Extracting data from an executable
|
||||||
object = { workspace = true, features = ["all"] }
|
object = { workspace = true, features = ["all"] }
|
||||||
@@ -130,7 +136,6 @@ local-ip-address = "0.6.3"
|
|||||||
dircpy = "0.3.19"
|
dircpy = "0.3.19"
|
||||||
plist = "1.7.0"
|
plist = "1.7.0"
|
||||||
memoize = "0.5.1"
|
memoize = "0.5.1"
|
||||||
wasm-encoder = "0.228.0"
|
|
||||||
backtrace = "0.3.74"
|
backtrace = "0.3.74"
|
||||||
ar = "0.9.0"
|
ar = "0.9.0"
|
||||||
wasm-bindgen-externref-xform = "0.2.100"
|
wasm-bindgen-externref-xform = "0.2.100"
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
//! The dioxus asset system.
|
||||||
|
//!
|
||||||
|
//! This module provides functionality for extracting assets from a binary file and then writing back
|
||||||
|
//! their asset hashes directly into the binary file. Previously, we performed asset hashing in the
|
||||||
|
//! `asset!()` macro. The new system, implemented here, instead performs the hashing at build time,
|
||||||
|
//! which provides more flexibility in the asset processing pipeline.
|
||||||
|
//!
|
||||||
|
//! We chose to implement this approach since assets might reference each other which means we minimally
|
||||||
|
//! need to parse the asset to create a unique hash for each asset before they are used in the application.
|
||||||
|
//! The hashes are used both for cache busting the asset in the browser and to cache the asset optimization
|
||||||
|
//! process in the build system.
|
||||||
|
//!
|
||||||
|
//! We use the same lessons learned from the hot-patching engine which parses the binary file and its
|
||||||
|
//! symbol table to find symbols that match the `__MANGANIS__` prefix. These symbols are ideally data
|
||||||
|
//! symbols and contain the BundledAsset data type which implements ConstSerialize and ConstDeserialize.
|
||||||
|
//!
|
||||||
|
//! When the binary is built, the `dioxus asset!()` macro will emit its metadata into the __MANGANIS__
|
||||||
|
//! symbols, which we process here. After reading the metadata directly from the executable, we then
|
||||||
|
//! hash it and write the hash directly into the binary file.
|
||||||
|
//!
|
||||||
|
//! During development, we can skip this step for most platforms since local paths are sufficient
|
||||||
|
//! for asset loading. However, for WASM and for production builds, we need to ensure that assets
|
||||||
|
//! can be found relative to the current exe. Unfortunately, on android, the `current_exe` path is wrong,
|
||||||
|
//! so the assets are resolved against the "asset root" - which is covered by the asset loader crate.
|
||||||
|
//!
|
||||||
|
//! Finding the __MANGANIS__ symbols is not quite straightforward when hotpatching, especially on WASM
|
||||||
|
//! since we build and link the module as relocatable, which is not a stable WASM proposal. In this
|
||||||
|
//! implementation, we handle both the non-PIE *and* PIC cases which are rather bespoke to our whole
|
||||||
|
//! build system.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io::{Cursor, Read, Seek, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
use anyhow::Context;
|
||||||
|
use const_serialize::{ConstVec, SerializeConst};
|
||||||
|
use dioxus_cli_opt::AssetManifest;
|
||||||
|
use manganis::BundledAsset;
|
||||||
|
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
|
||||||
|
use pdb::FallibleIterator;
|
||||||
|
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||||
|
use wasmparser::BinaryReader;
|
||||||
|
|
||||||
|
/// Extract all manganis symbols and their sections from the given object file.
|
||||||
|
fn manganis_symbols<'a, 'b, R: ReadRef<'a>>(
|
||||||
|
file: &'b File<'a, R>,
|
||||||
|
) -> impl Iterator<Item = (Symbol<'a, 'b, R>, Section<'a, 'b, R>)> + 'b {
|
||||||
|
file.symbols()
|
||||||
|
.filter(|symbol| {
|
||||||
|
if let Ok(name) = symbol.name() {
|
||||||
|
name.contains("__MANGANIS__")
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter_map(move |symbol| {
|
||||||
|
let section_index = symbol.section_index()?;
|
||||||
|
let section = file.section_by_index(section_index).ok()?;
|
||||||
|
Some((symbol, section))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the offsets of any manganis symbols in the given file.
|
||||||
|
fn find_symbol_offsets<'a, R: ReadRef<'a>>(
|
||||||
|
path: &Path,
|
||||||
|
file_contents: &[u8],
|
||||||
|
file: &File<'a, R>,
|
||||||
|
) -> Result<Vec<u64>> {
|
||||||
|
let pdb_file = find_pdb_file(path);
|
||||||
|
|
||||||
|
match file.format() {
|
||||||
|
// We need to handle dynamic offsets in wasm files differently
|
||||||
|
object::BinaryFormat::Wasm => find_wasm_symbol_offsets(file_contents, file),
|
||||||
|
// Windows puts the symbol information in a PDB file alongside the executable.
|
||||||
|
// If this is a windows PE file and we found a PDB file, we will use that to find the symbol offsets.
|
||||||
|
object::BinaryFormat::Pe if pdb_file.is_some() => {
|
||||||
|
find_pdb_symbol_offsets(&pdb_file.unwrap())
|
||||||
|
}
|
||||||
|
// Otherwise, look for manganis symbols in the object file.
|
||||||
|
_ => find_native_symbol_offsets(file),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the pdb file matching the executable file.
|
||||||
|
fn find_pdb_file(path: &Path) -> Option<PathBuf> {
|
||||||
|
let mut pdb_file = path.with_extension("pdb");
|
||||||
|
// Also try to find it in the same directory as the executable with _'s instead of -'s
|
||||||
|
if let Some(file_name) = pdb_file.file_name() {
|
||||||
|
let new_file_name = file_name.to_string_lossy().replace('-', "_");
|
||||||
|
let altrnate_pdb_file = pdb_file.with_file_name(new_file_name);
|
||||||
|
// Keep the most recent pdb file
|
||||||
|
match (pdb_file.metadata(), altrnate_pdb_file.metadata()) {
|
||||||
|
(Ok(pdb_metadata), Ok(alternate_metadata)) => {
|
||||||
|
if let (Ok(pdb_modified), Ok(alternate_modified)) =
|
||||||
|
(pdb_metadata.modified(), alternate_metadata.modified())
|
||||||
|
{
|
||||||
|
if pdb_modified < alternate_modified {
|
||||||
|
pdb_file = altrnate_pdb_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Err(_), Ok(_)) => {
|
||||||
|
pdb_file = altrnate_pdb_file;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pdb_file.exists() {
|
||||||
|
Some(pdb_file)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the offsets of any manganis symbols in a pdb file.
|
||||||
|
fn find_pdb_symbol_offsets(pdb_file: &Path) -> Result<Vec<u64>> {
|
||||||
|
let pdb_file_handle = std::fs::File::open(pdb_file)?;
|
||||||
|
let mut pdb_file = pdb::PDB::open(pdb_file_handle).context("Failed to open PDB file")?;
|
||||||
|
let Ok(Some(sections)) = pdb_file.sections() else {
|
||||||
|
tracing::error!("Failed to read sections from PDB file");
|
||||||
|
return Ok(Vec::new());
|
||||||
|
};
|
||||||
|
let global_symbols = pdb_file
|
||||||
|
.global_symbols()
|
||||||
|
.context("Failed to read global symbols from PDB file")?;
|
||||||
|
let address_map = pdb_file
|
||||||
|
.address_map()
|
||||||
|
.context("Failed to read address map from PDB file")?;
|
||||||
|
let mut symbols = global_symbols.iter();
|
||||||
|
let mut addresses = Vec::new();
|
||||||
|
while let Ok(Some(symbol)) = symbols.next() {
|
||||||
|
let Ok(pdb::SymbolData::Public(data)) = symbol.parse() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(rva) = data.offset.to_section_offset(&address_map) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = data.name.to_string();
|
||||||
|
if name.contains("__MANGANIS__") {
|
||||||
|
let section = sections
|
||||||
|
.get(rva.section as usize - 1)
|
||||||
|
.expect("Section index out of bounds");
|
||||||
|
|
||||||
|
addresses.push((section.pointer_to_raw_data + rva.offset) as u64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(addresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the offsets of any manganis symbols in a native object file.
|
||||||
|
fn find_native_symbol_offsets<'a, R: ReadRef<'a>>(file: &File<'a, R>) -> Result<Vec<u64>> {
|
||||||
|
let mut offsets = Vec::new();
|
||||||
|
for (symbol, section) in manganis_symbols(file) {
|
||||||
|
let virtual_address = symbol.address();
|
||||||
|
|
||||||
|
let Some((section_range_start, _)) = section.file_range() else {
|
||||||
|
tracing::error!(
|
||||||
|
"Found __MANGANIS__ symbol {:?} in section {}, but the section has no file range",
|
||||||
|
symbol.name(),
|
||||||
|
section.index()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// Translate the section_relative_address to the file offset
|
||||||
|
let section_relative_address: u64 = (virtual_address as i128 - section.address() as i128)
|
||||||
|
.try_into()
|
||||||
|
.expect("Virtual address should be greater than or equal to section address");
|
||||||
|
let file_offset = section_range_start + section_relative_address;
|
||||||
|
offsets.push(file_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(offsets)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the offsets of any manganis symbols in the wasm file.
|
||||||
|
fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
|
||||||
|
file_contents: &[u8],
|
||||||
|
file: &File<'a, R>,
|
||||||
|
) -> Result<Vec<u64>> {
|
||||||
|
// Parse the wasm file to find the globals
|
||||||
|
let parser = wasmparser::Parser::new(0);
|
||||||
|
|
||||||
|
// All integer literal global values in the wasm file
|
||||||
|
let mut global_values = Vec::new();
|
||||||
|
for section in parser.parse_all(file_contents) {
|
||||||
|
let Ok(wasmparser::Payload::GlobalSection(global_section)) = section else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
global_values = global_section
|
||||||
|
.into_iter()
|
||||||
|
.map(|global| {
|
||||||
|
let global = global.ok()?;
|
||||||
|
match global.init_expr.get_operators_reader().into_iter().next() {
|
||||||
|
Some(Ok(wasmparser::Operator::I32Const { value })) => Some(value as u64),
|
||||||
|
Some(Ok(wasmparser::Operator::I64Const { value })) => Some(value as u64),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
}
|
||||||
|
let mut offsets = Vec::new();
|
||||||
|
|
||||||
|
for (symbol, section) in manganis_symbols(file) {
|
||||||
|
let virtual_address = symbol.address();
|
||||||
|
|
||||||
|
let Some((_, section_range_end)) = section.file_range() else {
|
||||||
|
tracing::error!(
|
||||||
|
"Found __MANGANIS__ symbol {:?} in section {}, but the section has no file range",
|
||||||
|
symbol.name(),
|
||||||
|
section.index()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let section_size = section.data()?.len() as u64;
|
||||||
|
let section_start = section_range_end - section_size;
|
||||||
|
// Translate the section_relative_address to the file offset
|
||||||
|
// WASM files have a section address of 0 in object, reparse the data section with wasmparser
|
||||||
|
// to get the correct address and section start
|
||||||
|
let reader = wasmparser::DataSectionReader::new(BinaryReader::new(
|
||||||
|
&file_contents[section_start as usize..section_range_end as usize],
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
.context("Failed to create WASM data section reader")?;
|
||||||
|
let main_memory = reader
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.context("Failed find main memory from WASM data section")?
|
||||||
|
.context("Failed to read main memory from WASM data section")?;
|
||||||
|
let main_memory_offset = match main_memory.kind {
|
||||||
|
wasmparser::DataKind::Active { offset_expr, .. } => {
|
||||||
|
match offset_expr.get_operators_reader().into_iter().next() {
|
||||||
|
Some(Ok(wasmparser::Operator::I32Const { value })) => -value as i128,
|
||||||
|
Some(Ok(wasmparser::Operator::I64Const { value })) => -value as i128,
|
||||||
|
Some(Ok(wasmparser::Operator::GlobalGet { global_index })) => {
|
||||||
|
let Some(value) =
|
||||||
|
global_values.get(global_index as usize).copied().flatten()
|
||||||
|
else {
|
||||||
|
tracing::error!(
|
||||||
|
"Found __MANGANIS__ symbol {:?} in WASM file, but the global index {} is not found",
|
||||||
|
symbol.name(),
|
||||||
|
global_index
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
value as i128
|
||||||
|
}
|
||||||
|
offset_expr => {
|
||||||
|
tracing::error!(
|
||||||
|
"Found __MANGANIS__ symbol {:?} in WASM file, but the offset expression is not a constant is is {:?}",
|
||||||
|
symbol.name(),
|
||||||
|
offset_expr
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::error!(
|
||||||
|
"Found __MANGANIS__ symbol {:?} in WASM file, but the data section is not active",
|
||||||
|
symbol.name()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// main_memory.data is a slice somewhere in file_contents. Find out the offset in the file
|
||||||
|
let data_start_offset = (main_memory.data.as_ptr() as u64)
|
||||||
|
.checked_sub(file_contents.as_ptr() as u64)
|
||||||
|
.expect("Data section start offset should be within the file contents");
|
||||||
|
let section_relative_address: u64 = ((virtual_address as i128) + main_memory_offset)
|
||||||
|
.try_into()
|
||||||
|
.expect("Virtual address should be greater than or equal to section address");
|
||||||
|
let file_offset = data_start_offset + section_relative_address;
|
||||||
|
offsets.push(file_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(offsets)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all assets in the given file, hash them, and write them back to the file.
|
||||||
|
/// Then return an `AssetManifest` containing all the assets found in the file.
|
||||||
|
pub(crate) fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<AssetManifest> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let mut file = std::fs::File::options().write(true).read(true).open(path)?;
|
||||||
|
let mut file_contents = Vec::new();
|
||||||
|
file.read_to_end(&mut file_contents)?;
|
||||||
|
let mut reader = Cursor::new(&file_contents);
|
||||||
|
let read_cache = ReadCache::new(&mut reader);
|
||||||
|
let object_file = object::File::parse(&read_cache)?;
|
||||||
|
let offsets = find_symbol_offsets(path, &file_contents, &object_file)?;
|
||||||
|
|
||||||
|
let mut assets = Vec::new();
|
||||||
|
|
||||||
|
// Read each asset from the data section using the offsets
|
||||||
|
for offset in offsets.iter().copied() {
|
||||||
|
file.seek(std::io::SeekFrom::Start(offset))?;
|
||||||
|
let mut data_in_range = vec![0; BundledAsset::MEMORY_LAYOUT.size()];
|
||||||
|
file.read_exact(&mut data_in_range)?;
|
||||||
|
|
||||||
|
let buffer = const_serialize::ConstReadBuffer::new(&data_in_range);
|
||||||
|
|
||||||
|
if let Some((_, bundled_asset)) = const_serialize::deserialize_const!(BundledAsset, buffer)
|
||||||
|
{
|
||||||
|
assets.push(bundled_asset);
|
||||||
|
} else {
|
||||||
|
tracing::warn!("Found an asset at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the hash to each asset in parallel
|
||||||
|
assets
|
||||||
|
.par_iter_mut()
|
||||||
|
.for_each(dioxus_cli_opt::add_hash_to_asset);
|
||||||
|
|
||||||
|
// Write back the assets to the binary file
|
||||||
|
for (offset, asset) in offsets.into_iter().zip(&assets) {
|
||||||
|
let new_data = ConstVec::new();
|
||||||
|
let new_data = const_serialize::serialize_const(asset, new_data);
|
||||||
|
|
||||||
|
file.seek(std::io::SeekFrom::Start(offset))?;
|
||||||
|
// Write the modified binary data back to the file
|
||||||
|
file.write_all(new_data.as_ref())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the file is a macos binary, we need to re-sign the modified binary
|
||||||
|
if object_file.format() == object::BinaryFormat::MachO {
|
||||||
|
// Spawn the codesign command to re-sign the binary
|
||||||
|
let output = std::process::Command::new("codesign")
|
||||||
|
.arg("--force")
|
||||||
|
.arg("--sign")
|
||||||
|
.arg("-") // Sign with an empty identity
|
||||||
|
.arg(path)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to re-sign the binary with codesign after finalizing the assets: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, create the asset manifest
|
||||||
|
let mut manifest = AssetManifest::default();
|
||||||
|
for asset in assets {
|
||||||
|
manifest.insert_asset(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(manifest)
|
||||||
|
}
|
||||||
@@ -567,16 +567,15 @@ impl AppBuilder {
|
|||||||
let original_artifacts = self.artifacts.as_ref().unwrap();
|
let original_artifacts = self.artifacts.as_ref().unwrap();
|
||||||
let asset_dir = self.build.asset_dir();
|
let asset_dir = self.build.asset_dir();
|
||||||
|
|
||||||
for (k, bundled) in res.assets.assets.iter() {
|
for bundled in res.assets.assets() {
|
||||||
let k = dunce::canonicalize(k)?;
|
if original_artifacts.assets.contains(bundled) {
|
||||||
if original_artifacts.assets.assets.contains_key(k.as_path()) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let from = dunce::canonicalize(PathBuf::from(bundled.absolute_source_path()))?;
|
||||||
|
|
||||||
let from = k.clone();
|
|
||||||
let to = asset_dir.join(bundled.bundled_path());
|
let to = asset_dir.join(bundled.bundled_path());
|
||||||
|
|
||||||
tracing::debug!("Copying asset from patch: {}", k.display());
|
tracing::debug!("Copying asset from patch: {}", from.display());
|
||||||
if let Err(e) = dioxus_cli_opt::process_file_to(bundled.options(), &from, &to) {
|
if let Err(e) = dioxus_cli_opt::process_file_to(bundled.options(), &from, &to) {
|
||||||
tracing::error!("Failed to copy asset: {e}");
|
tracing::error!("Failed to copy asset: {e}");
|
||||||
continue;
|
continue;
|
||||||
@@ -584,13 +583,8 @@ impl AppBuilder {
|
|||||||
|
|
||||||
// If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
|
// If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
|
||||||
if self.build.platform == Platform::Android {
|
if self.build.platform == Platform::Android {
|
||||||
let changed_file = dunce::canonicalize(k).inspect_err(|e| {
|
|
||||||
tracing::debug!("Failed to canonicalize hotreloaded asset: {e}")
|
|
||||||
})?;
|
|
||||||
let bundled_name = PathBuf::from(bundled.bundled_path());
|
let bundled_name = PathBuf::from(bundled.bundled_path());
|
||||||
_ = self
|
_ = self.copy_file_to_android_tmp(&from, &bundled_name).await;
|
||||||
.copy_file_to_android_tmp(&changed_file, &bundled_name)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,10 +643,13 @@ impl AppBuilder {
|
|||||||
/// dir that the system simulator might be providing. We know this is the case for ios simulators
|
/// dir that the system simulator might be providing. We know this is the case for ios simulators
|
||||||
/// and haven't yet checked for android.
|
/// and haven't yet checked for android.
|
||||||
///
|
///
|
||||||
/// This will return the bundled name of the asset such that we can send it to the clients letting
|
/// This will return the bundled name of the assets such that we can send it to the clients letting
|
||||||
/// them know what to reload. It's not super important that this is robust since most clients will
|
/// them know what to reload. It's not super important that this is robust since most clients will
|
||||||
/// kick all stylsheets without necessarily checking the name.
|
/// kick all stylsheets without necessarily checking the name.
|
||||||
pub(crate) async fn hotreload_bundled_asset(&self, changed_file: &PathBuf) -> Option<PathBuf> {
|
pub(crate) async fn hotreload_bundled_assets(
|
||||||
|
&self,
|
||||||
|
changed_file: &PathBuf,
|
||||||
|
) -> Option<Vec<PathBuf>> {
|
||||||
let artifacts = self.artifacts.as_ref()?;
|
let artifacts = self.artifacts.as_ref()?;
|
||||||
|
|
||||||
// Use the build dir if there's no runtime asset dir as the override. For the case of ios apps,
|
// Use the build dir if there's no runtime asset dir as the override. For the case of ios apps,
|
||||||
@@ -668,32 +665,36 @@ impl AppBuilder {
|
|||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
// The asset might've been renamed thanks to the manifest, let's attempt to reload that too
|
// The asset might've been renamed thanks to the manifest, let's attempt to reload that too
|
||||||
let resource = artifacts.assets.assets.get(&changed_file)?;
|
let resources = artifacts.assets.get_assets_for_source(&changed_file)?;
|
||||||
let output_path = asset_dir.join(resource.bundled_path());
|
let mut bundled_names = Vec::new();
|
||||||
|
for resource in resources {
|
||||||
|
let output_path = asset_dir.join(resource.bundled_path());
|
||||||
|
|
||||||
tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
|
tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
|
||||||
|
|
||||||
// Remove the old asset if it exists
|
// Remove the old asset if it exists
|
||||||
_ = std::fs::remove_file(&output_path);
|
_ = std::fs::remove_file(&output_path);
|
||||||
|
|
||||||
// And then process the asset with the options into the **old** asset location. If we recompiled,
|
// And then process the asset with the options into the **old** asset location. If we recompiled,
|
||||||
// the asset would be in a new location because the contents and hash have changed. Since we are
|
// the asset would be in a new location because the contents and hash have changed. Since we are
|
||||||
// hotreloading, we need to use the old asset location it was originally written to.
|
// hotreloading, we need to use the old asset location it was originally written to.
|
||||||
let options = *resource.options();
|
let options = *resource.options();
|
||||||
let res = process_file_to(&options, &changed_file, &output_path);
|
let res = process_file_to(&options, &changed_file, &output_path);
|
||||||
let bundled_name = PathBuf::from(resource.bundled_path());
|
let bundled_name = PathBuf::from(resource.bundled_path());
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
tracing::debug!("Failed to hotreload asset {e}");
|
tracing::debug!("Failed to hotreload asset {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
|
||||||
|
if self.build.platform == Platform::Android {
|
||||||
|
_ = self
|
||||||
|
.copy_file_to_android_tmp(&changed_file, &bundled_name)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
bundled_names.push(bundled_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
|
Some(bundled_names)
|
||||||
if self.build.platform == Platform::Android {
|
|
||||||
_ = self
|
|
||||||
.copy_file_to_android_tmp(&changed_file, &bundled_name)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(bundled_name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy this file to the tmp folder on the android device, returning the path to the copied file
|
/// Copy this file to the tmp folder on the android device, returning the path to the copied file
|
||||||
|
|||||||
@@ -8,12 +8,14 @@
|
|||||||
//! hot-patching Rust code through binary analysis and a custom linker. The [`builder`] module contains
|
//! hot-patching Rust code through binary analysis and a custom linker. The [`builder`] module contains
|
||||||
//! the management of the ongoing build and methods to open the build as a running app.
|
//! the management of the ongoing build and methods to open the build as a running app.
|
||||||
|
|
||||||
|
mod assets;
|
||||||
mod builder;
|
mod builder;
|
||||||
mod context;
|
mod context;
|
||||||
mod patch;
|
mod patch;
|
||||||
mod request;
|
mod request;
|
||||||
mod tools;
|
mod tools;
|
||||||
|
|
||||||
|
pub(crate) use assets::*;
|
||||||
pub(crate) use builder::*;
|
pub(crate) use builder::*;
|
||||||
pub(crate) use context::*;
|
pub(crate) use context::*;
|
||||||
pub(crate) use patch::*;
|
pub(crate) use patch::*;
|
||||||
|
|||||||
@@ -961,10 +961,8 @@ impl BuildRequest {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Traverse the target directory and collect all assets from the incremental cache
|
/// Collect the assets from the final executable and modify the binary in place to point to the right
|
||||||
///
|
/// hashed asset location.
|
||||||
/// This uses "known paths" that have stayed relatively stable during cargo's lifetime.
|
|
||||||
/// One day this system might break and we might need to go back to using the linker approach.
|
|
||||||
fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result<AssetManifest> {
|
fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result<AssetManifest> {
|
||||||
// walk every file in the incremental cache dir, reading and inserting items into the manifest.
|
// walk every file in the incremental cache dir, reading and inserting items into the manifest.
|
||||||
let mut manifest = AssetManifest::default();
|
let mut manifest = AssetManifest::default();
|
||||||
@@ -972,7 +970,7 @@ impl BuildRequest {
|
|||||||
// And then add from the exe directly, just in case it's LTO compiled and has no incremental cache
|
// And then add from the exe directly, just in case it's LTO compiled and has no incremental cache
|
||||||
if !self.skip_assets {
|
if !self.skip_assets {
|
||||||
ctx.status_extracting_assets();
|
ctx.status_extracting_assets();
|
||||||
_ = manifest.add_from_object_path(exe);
|
manifest = super::assets::extract_assets_from_file(exe)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(manifest)
|
Ok(manifest)
|
||||||
@@ -1109,8 +1107,7 @@ impl BuildRequest {
|
|||||||
|
|
||||||
// Create a set of all the paths that new files will be bundled to
|
// Create a set of all the paths that new files will be bundled to
|
||||||
let mut keep_bundled_output_paths: HashSet<_> = assets
|
let mut keep_bundled_output_paths: HashSet<_> = assets
|
||||||
.assets
|
.assets()
|
||||||
.values()
|
|
||||||
.map(|a| asset_dir.join(a.bundled_path()))
|
.map(|a| asset_dir.join(a.bundled_path()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -1149,8 +1146,8 @@ impl BuildRequest {
|
|||||||
let mut assets_to_transfer = vec![];
|
let mut assets_to_transfer = vec![];
|
||||||
|
|
||||||
// Queue the bundled assets
|
// Queue the bundled assets
|
||||||
for (asset, bundled) in &assets.assets {
|
for bundled in assets.assets() {
|
||||||
let from = asset.clone();
|
let from = PathBuf::from(bundled.absolute_source_path());
|
||||||
let to = asset_dir.join(bundled.bundled_path());
|
let to = asset_dir.join(bundled.bundled_path());
|
||||||
|
|
||||||
// prefer to log using a shorter path relative to the workspace dir by trimming the workspace dir
|
// prefer to log using a shorter path relative to the workspace dir by trimming the workspace dir
|
||||||
@@ -1381,9 +1378,7 @@ impl BuildRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now extract the assets from the fat binary
|
// Now extract the assets from the fat binary
|
||||||
artifacts
|
self.collect_assets(&self.patch_exe(artifacts.time_start), ctx)?;
|
||||||
.assets
|
|
||||||
.add_from_object_path(&self.patch_exe(artifacts.time_start))?;
|
|
||||||
|
|
||||||
// Clean up the temps manually
|
// Clean up the temps manually
|
||||||
// todo: we might want to keep them around for debugging purposes
|
// todo: we might want to keep them around for debugging purposes
|
||||||
@@ -1436,6 +1431,14 @@ impl BuildRequest {
|
|||||||
"--pie".to_string(),
|
"--pie".to_string(),
|
||||||
"--experimental-pic".to_string(),
|
"--experimental-pic".to_string(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// retain exports so post-processing has hooks to work with
|
||||||
|
for (idx, arg) in original_args.iter().enumerate() {
|
||||||
|
if *arg == "--export" {
|
||||||
|
out_args.push(arg.to_string());
|
||||||
|
out_args.push(original_args[idx + 1].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This uses "cc" and these args need to be ld compatible
|
// This uses "cc" and these args need to be ld compatible
|
||||||
@@ -3924,7 +3927,7 @@ impl BuildRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inject any resources from manganis into the head
|
// Inject any resources from manganis into the head
|
||||||
for asset in assets.assets.values() {
|
for asset in assets.assets() {
|
||||||
let asset_path = asset.bundled_path();
|
let asset_path = asset.bundled_path();
|
||||||
match asset.options() {
|
match asset.options() {
|
||||||
AssetOptions::Css(css_options) => {
|
AssetOptions::Css(css_options) => {
|
||||||
@@ -3954,7 +3957,11 @@ impl BuildRequest {
|
|||||||
|
|
||||||
// Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system
|
// Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system
|
||||||
let wasm_source_path = self.wasm_bindgen_wasm_output_file();
|
let wasm_source_path = self.wasm_bindgen_wasm_output_file();
|
||||||
if let Some(wasm_path) = assets.assets.get(&wasm_source_path) {
|
if let Some(wasm_assets) = assets.get_assets_for_source(&wasm_source_path) {
|
||||||
|
let wasm_path = wasm_assets
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.expect("There should be exactly one optimized wasm asset");
|
||||||
let wasm_path = wasm_path.bundled_path();
|
let wasm_path = wasm_path.bundled_path();
|
||||||
head_resources.push_str(&format!(
|
head_resources.push_str(&format!(
|
||||||
"<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/assets/{wasm_path}\" crossorigin>"
|
"<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/assets/{wasm_path}\" crossorigin>"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::{fs::create_dir_all, path::PathBuf};
|
use std::{fs::create_dir_all, path::PathBuf};
|
||||||
|
|
||||||
use crate::{Result, StructuredOutput};
|
use crate::{extract_assets_from_file, Result, StructuredOutput};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use dioxus_cli_opt::{process_file_to, AssetManifest};
|
use dioxus_cli_opt::process_file_to;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Parser)]
|
#[derive(Clone, Debug, Parser)]
|
||||||
@@ -10,21 +10,17 @@ pub struct BuildAssets {
|
|||||||
/// The source executable to build assets for.
|
/// The source executable to build assets for.
|
||||||
pub(crate) executable: PathBuf,
|
pub(crate) executable: PathBuf,
|
||||||
|
|
||||||
/// The source directory for the assets.
|
|
||||||
pub(crate) source: PathBuf,
|
|
||||||
|
|
||||||
/// The destination directory for the assets.
|
/// The destination directory for the assets.
|
||||||
pub(crate) destination: PathBuf,
|
pub(crate) destination: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuildAssets {
|
impl BuildAssets {
|
||||||
pub async fn run(self) -> Result<StructuredOutput> {
|
pub async fn run(self) -> Result<StructuredOutput> {
|
||||||
let mut manifest = AssetManifest::default();
|
let manifest = extract_assets_from_file(&self.executable)?;
|
||||||
manifest.add_from_object_path(&self.executable)?;
|
|
||||||
|
|
||||||
create_dir_all(&self.destination)?;
|
create_dir_all(&self.destination)?;
|
||||||
for (path, asset) in manifest.assets.iter() {
|
for asset in manifest.assets() {
|
||||||
let source_path = self.source.join(path);
|
let source_path = PathBuf::from(asset.absolute_source_path());
|
||||||
let destination_path = self.destination.join(asset.bundled_path());
|
let destination_path = self.destination.join(asset.bundled_path());
|
||||||
debug!(
|
debug!(
|
||||||
"Processing asset {} --> {} {:#?}",
|
"Processing asset {} --> {} {:#?}",
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ pub(crate) enum Error {
|
|||||||
#[error("Failed to perform hotpatch: {0}")]
|
#[error("Failed to perform hotpatch: {0}")]
|
||||||
PatchingFailed(#[from] crate::build::PatchError),
|
PatchingFailed(#[from] crate::build::PatchError),
|
||||||
|
|
||||||
|
#[error("Failed to read object file: {0}")]
|
||||||
|
ObjectReadFailed(#[from] object::Error),
|
||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
CapturedPanic(String),
|
CapturedPanic(String),
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ impl Output {
|
|||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
|
|
||||||
if !stdout().is_terminal() {
|
if !stdout().is_terminal() {
|
||||||
return io::Result::Err(io::Error::new(io::ErrorKind::Other, "Not a terminal"));
|
return io::Result::Err(io::Error::other("Not a terminal"));
|
||||||
}
|
}
|
||||||
|
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|||||||
@@ -309,8 +309,10 @@ impl AppServer {
|
|||||||
|
|
||||||
// If it's an asset, we want to hotreload it
|
// If it's an asset, we want to hotreload it
|
||||||
// todo(jon): don't hardcode this here
|
// todo(jon): don't hardcode this here
|
||||||
if let Some(bundled_name) = self.client.hotreload_bundled_asset(path).await {
|
if let Some(bundled_names) = self.client.hotreload_bundled_assets(path).await {
|
||||||
assets.push(PathBuf::from("/assets/").join(bundled_name));
|
for bundled_name in bundled_names {
|
||||||
|
assets.push(PathBuf::from("/assets/").join(bundled_name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a rust file, we want to hotreload it using the filemap
|
// If it's a rust file, we want to hotreload it using the filemap
|
||||||
|
|||||||
@@ -30,4 +30,9 @@ impl<'a> ConstReadBuffer<'a> {
|
|||||||
pub const fn as_ref(&self) -> &[u8] {
|
pub const fn as_ref(&self) -> &[u8] {
|
||||||
self.memory
|
self.memory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a slice of the buffer from the current location to the end of the buffer
|
||||||
|
pub const fn remaining(&self) -> &[u8] {
|
||||||
|
self.memory.split_at(self.location).1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -420,3 +420,17 @@ fn test_const_vec_remove() {
|
|||||||
assert_eq!(value, Some(5678));
|
assert_eq!(value, Some(5678));
|
||||||
assert_eq!(vec.as_ref(), &[]);
|
assert_eq!(vec.as_ref(), &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_const_vec_extend() {
|
||||||
|
const VEC: ConstVec<u32> = {
|
||||||
|
let mut vec = ConstVec::new();
|
||||||
|
vec = vec.push(1234);
|
||||||
|
vec = vec.push(5678);
|
||||||
|
vec = vec.extend(&[91011, 1213]);
|
||||||
|
vec
|
||||||
|
};
|
||||||
|
let vec = VEC;
|
||||||
|
println!("{:?}", vec);
|
||||||
|
assert_eq!(vec.as_ref(), &[1234, 5678, 91011, 1213]);
|
||||||
|
}
|
||||||
@@ -137,7 +137,7 @@ pub enum Layout {
|
|||||||
|
|
||||||
impl Layout {
|
impl Layout {
|
||||||
/// The size of the type in bytes.
|
/// The size of the type in bytes.
|
||||||
const fn size(&self) -> usize {
|
pub const fn size(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Layout::Enum(layout) => layout.size,
|
Layout::Enum(layout) => layout.size,
|
||||||
Layout::Struct(layout) => layout.size,
|
Layout::Struct(layout) => layout.size,
|
||||||
@@ -221,7 +221,7 @@ impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8:
|
|||||||
const MAX_STR_SIZE: usize = 256;
|
const MAX_STR_SIZE: usize = 256;
|
||||||
|
|
||||||
/// A string that is stored in a constant sized buffer that can be serialized and deserialized at compile time
|
/// A string that is stored in a constant sized buffer that can be serialized and deserialized at compile time
|
||||||
#[derive(PartialEq, PartialOrd, Clone, Copy, Hash)]
|
#[derive(Eq, PartialEq, PartialOrd, Clone, Copy, Hash)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct ConstStr {
|
pub struct ConstStr {
|
||||||
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
|
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
|
||||||
|
|||||||
@@ -95,6 +95,67 @@ fn test_serialize_enum() {
|
|||||||
assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data);
|
assert_eq!(deserialize_const!(Enum, buf).unwrap().1, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_list_of_lopsided_enums() {
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, SerializeConst)]
|
||||||
|
#[repr(C, u8)]
|
||||||
|
enum Enum {
|
||||||
|
A,
|
||||||
|
B { one: u8, two: u16 } = 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{:#?}", Enum::MEMORY_LAYOUT);
|
||||||
|
|
||||||
|
let data = [Enum::A, Enum::A];
|
||||||
|
let mut buf = ConstVec::new();
|
||||||
|
buf = serialize_const(&data, buf);
|
||||||
|
println!("{:?}", buf.as_ref());
|
||||||
|
let buf = buf.read();
|
||||||
|
assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data);
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
Enum::B {
|
||||||
|
one: 0x11,
|
||||||
|
two: 0x2233,
|
||||||
|
},
|
||||||
|
Enum::B {
|
||||||
|
one: 0x12,
|
||||||
|
two: 0x2244,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut buf = ConstVec::new();
|
||||||
|
buf = serialize_const(&data, buf);
|
||||||
|
println!("{:?}", buf.as_ref());
|
||||||
|
let buf = buf.read();
|
||||||
|
assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data);
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
Enum::A,
|
||||||
|
Enum::B {
|
||||||
|
one: 0x11,
|
||||||
|
two: 0x2233,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut buf = ConstVec::new();
|
||||||
|
buf = serialize_const(&data, buf);
|
||||||
|
println!("{:?}", buf.as_ref());
|
||||||
|
let buf = buf.read();
|
||||||
|
assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data);
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
Enum::B {
|
||||||
|
one: 0x11,
|
||||||
|
two: 0x2233,
|
||||||
|
},
|
||||||
|
Enum::A,
|
||||||
|
];
|
||||||
|
let mut buf = ConstVec::new();
|
||||||
|
buf = serialize_const(&data, buf);
|
||||||
|
println!("{:?}", buf.as_ref());
|
||||||
|
let buf = buf.read();
|
||||||
|
assert_eq!(deserialize_const!([Enum; 2], buf).unwrap().1, data);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serialize_u8_enum() {
|
fn test_serialize_u8_enum() {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, SerializeConst)]
|
#[derive(Clone, Copy, Debug, PartialEq, SerializeConst)]
|
||||||
|
|||||||
@@ -594,7 +594,7 @@ impl ScopeId {
|
|||||||
|
|
||||||
/// Create a subscription that schedules a future render for the reference component. Unlike [`Self::needs_update`], this function will work outside of the dioxus runtime.
|
/// Create a subscription that schedules a future render for the reference component. Unlike [`Self::needs_update`], this function will work outside of the dioxus runtime.
|
||||||
///
|
///
|
||||||
/// ## Notice: you should prefer using [`crate::prelude::schedule_update_any`]
|
/// ## Notice: you should prefer using [`crate::schedule_update_any`]
|
||||||
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
||||||
Runtime::with_scope(*self, |cx| cx.schedule_update()).unwrap()
|
Runtime::with_scope(*self, |cx| cx.schedule_update()).unwrap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
use crate::AssetOptions;
|
use crate::AssetOptions;
|
||||||
use const_serialize::{ConstStr, SerializeConst};
|
use const_serialize::{deserialize_const, ConstStr, ConstVec, SerializeConst};
|
||||||
use std::path::PathBuf;
|
use std::{fmt::Debug, hash::Hash, path::PathBuf};
|
||||||
|
|
||||||
/// An asset that should be copied by the bundler with some options. This type will be
|
/// An asset that should be copied by the bundler with some options. This type will be
|
||||||
/// serialized into the binary and added to the link section [`LinkSection::CURRENT`](crate::linker::LinkSection::CURRENT).
|
/// serialized into the binary.
|
||||||
/// CLIs that support manganis, should pull out the assets from the link section, optimize,
|
/// CLIs that support manganis, should pull out the assets from the link section, optimize,
|
||||||
/// and write them to the filesystem at [`BundledAsset::bundled_path`] for the application
|
/// and write them to the filesystem at [`BundledAsset::bundled_path`] for the application
|
||||||
/// to use.
|
/// to use.
|
||||||
#[derive(
|
#[derive(Debug, Eq, Clone, Copy, SerializeConst, serde::Serialize, serde::Deserialize)]
|
||||||
Debug,
|
|
||||||
PartialEq,
|
|
||||||
PartialOrd,
|
|
||||||
Clone,
|
|
||||||
Copy,
|
|
||||||
Hash,
|
|
||||||
SerializeConst,
|
|
||||||
serde::Serialize,
|
|
||||||
serde::Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct BundledAsset {
|
pub struct BundledAsset {
|
||||||
/// The absolute path of the asset
|
/// The absolute path of the asset
|
||||||
absolute_source_path: ConstStr,
|
absolute_source_path: ConstStr,
|
||||||
@@ -27,13 +17,46 @@ pub struct BundledAsset {
|
|||||||
options: AssetOptions,
|
options: AssetOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for BundledAsset {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.absolute_source_path == other.absolute_source_path
|
||||||
|
&& self.bundled_path == other.bundled_path
|
||||||
|
&& self.options == other.options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for BundledAsset {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
match self
|
||||||
|
.absolute_source_path
|
||||||
|
.partial_cmp(&other.absolute_source_path)
|
||||||
|
{
|
||||||
|
Some(core::cmp::Ordering::Equal) => {}
|
||||||
|
ord => return ord,
|
||||||
|
}
|
||||||
|
match self.bundled_path.partial_cmp(&other.bundled_path) {
|
||||||
|
Some(core::cmp::Ordering::Equal) => {}
|
||||||
|
ord => return ord,
|
||||||
|
}
|
||||||
|
self.options.partial_cmp(&other.options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for BundledAsset {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.absolute_source_path.hash(state);
|
||||||
|
self.bundled_path.hash(state);
|
||||||
|
self.options.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BundledAsset {
|
impl BundledAsset {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// This should only be called from the macro
|
/// This should only be called from the macro
|
||||||
/// Create a new asset
|
/// Create a new asset
|
||||||
pub const fn new(
|
pub const fn new(
|
||||||
absolute_source_path: &'static str,
|
absolute_source_path: &str,
|
||||||
bundled_path: &'static str,
|
bundled_path: &str,
|
||||||
options: AssetOptions,
|
options: AssetOptions,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -83,6 +106,7 @@ impl BundledAsset {
|
|||||||
pub fn absolute_source_path(&self) -> &str {
|
pub fn absolute_source_path(&self) -> &str {
|
||||||
self.absolute_source_path.as_str()
|
self.absolute_source_path.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the options for the asset
|
/// Get the options for the asset
|
||||||
pub const fn options(&self) -> &AssetOptions {
|
pub const fn options(&self) -> &AssetOptions {
|
||||||
&self.options
|
&self.options
|
||||||
@@ -101,28 +125,49 @@ impl BundledAsset {
|
|||||||
/// img { src: ASSET }
|
/// img { src: ASSET }
|
||||||
/// };
|
/// };
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
pub struct Asset {
|
pub struct Asset {
|
||||||
/// The bundled asset
|
/// A pointer to the bundled asset. This will be resolved after the linker has run and
|
||||||
bundled: BundledAsset,
|
/// put into the lazy asset
|
||||||
/// The link section for the asset
|
///
|
||||||
keep_link_section: fn() -> u8,
|
/// WARNING: Don't read this directly. Reads can get optimized away at compile time before
|
||||||
|
/// the data for this is filled in by the CLI after the binary is built. Instead, use
|
||||||
|
/// [`std::ptr::read_volatile`] to read the data.
|
||||||
|
bundled: &'static [u8],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for Asset {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.resolve().fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for Asset {}
|
||||||
|
unsafe impl Sync for Asset {}
|
||||||
|
|
||||||
impl Asset {
|
impl Asset {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// This should only be called from the macro
|
/// This should only be called from the macro
|
||||||
/// Create a new asset from the bundled form of the asset and the link section
|
/// Create a new asset from the bundled form of the asset and the link section
|
||||||
pub const fn new(bundled: BundledAsset, keep_link_section: fn() -> u8) -> Self {
|
pub const fn new(bundled: &'static [u8]) -> Self {
|
||||||
Self {
|
Self { bundled }
|
||||||
bundled,
|
|
||||||
keep_link_section,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the bundled asset
|
/// Get the bundled asset
|
||||||
pub const fn bundled(&self) -> &BundledAsset {
|
pub fn bundled(&self) -> BundledAsset {
|
||||||
&self.bundled
|
let len = self.bundled.len();
|
||||||
|
let ptr = self.bundled as *const [u8] as *const u8;
|
||||||
|
if ptr.is_null() {
|
||||||
|
panic!("Tried to use an asset that was not bundled. Make sure you are compiling dx as the linker");
|
||||||
|
}
|
||||||
|
let mut bytes = ConstVec::new();
|
||||||
|
for byte in 0..len {
|
||||||
|
// SAFETY: We checked that the pointer was not null above. The pointer is valid for reads and
|
||||||
|
// since we are reading a u8 there are no alignment requirements
|
||||||
|
bytes = bytes.push(unsafe { std::ptr::read_volatile(ptr.add(byte)) });
|
||||||
|
}
|
||||||
|
let read = bytes.read();
|
||||||
|
deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a canonicalized path to the asset
|
/// Return a canonicalized path to the asset
|
||||||
@@ -130,13 +175,10 @@ impl Asset {
|
|||||||
/// Attempts to resolve it against an `assets` folder in the current directory.
|
/// Attempts to resolve it against an `assets` folder in the current directory.
|
||||||
/// If that doesn't exist, it will resolve against the cargo manifest dir
|
/// If that doesn't exist, it will resolve against the cargo manifest dir
|
||||||
pub fn resolve(&self) -> PathBuf {
|
pub fn resolve(&self) -> PathBuf {
|
||||||
// Force a volatile read of the asset link section to ensure the symbol makes it into the binary
|
|
||||||
(self.keep_link_section)();
|
|
||||||
|
|
||||||
#[cfg(feature = "dioxus")]
|
#[cfg(feature = "dioxus")]
|
||||||
// If the asset is relative, we resolve the asset at the current directory
|
// If the asset is relative, we resolve the asset at the current directory
|
||||||
if !dioxus_core_types::is_bundled_app() {
|
if !dioxus_core_types::is_bundled_app() {
|
||||||
return PathBuf::from(self.bundled.absolute_source_path.as_str());
|
return PathBuf::from(self.bundled().absolute_source_path.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "dioxus")]
|
#[cfg(feature = "dioxus")]
|
||||||
@@ -156,7 +198,7 @@ impl Asset {
|
|||||||
|
|
||||||
// Otherwise presumably we're bundled and we can use the bundled path
|
// Otherwise presumably we're bundled and we can use the bundled path
|
||||||
bundle_root.join(PathBuf::from(
|
bundle_root.join(PathBuf::from(
|
||||||
self.bundled.bundled_path.as_str().trim_start_matches('/'),
|
self.bundled().bundled_path.as_str().trim_start_matches('/'),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::collections::HashSet;
|
|||||||
/// Options for a css asset
|
/// Options for a css asset
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
|
Eq,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
PartialOrd,
|
PartialOrd,
|
||||||
Clone,
|
Clone,
|
||||||
@@ -79,6 +80,7 @@ impl CssAssetOptions {
|
|||||||
/// Options for a css module asset
|
/// Options for a css module asset
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
|
Eq,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
PartialOrd,
|
PartialOrd,
|
||||||
Clone,
|
Clone,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crate::AssetOptions;
|
|||||||
/// The builder for a folder asset.
|
/// The builder for a folder asset.
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
|
Eq,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
PartialOrd,
|
PartialOrd,
|
||||||
Clone,
|
Clone,
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
//! 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()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ use crate::AssetOptions;
|
|||||||
/// The type of an image. You can read more about the tradeoffs between image formats [here](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types)
|
/// The type of an image. You can read more about the tradeoffs between image formats [here](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types)
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
|
Eq,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
PartialOrd,
|
PartialOrd,
|
||||||
Clone,
|
Clone,
|
||||||
@@ -31,6 +32,7 @@ pub enum ImageFormat {
|
|||||||
/// The size of an image asset
|
/// The size of an image asset
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
|
Eq,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
PartialOrd,
|
PartialOrd,
|
||||||
Clone,
|
Clone,
|
||||||
@@ -56,6 +58,7 @@ pub enum ImageSize {
|
|||||||
/// Options for an image asset
|
/// Options for an image asset
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
|
Eq,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
PartialOrd,
|
PartialOrd,
|
||||||
Clone,
|
Clone,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crate::AssetOptions;
|
|||||||
/// Options for a javascript asset
|
/// Options for a javascript asset
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
|
Eq,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
PartialOrd,
|
PartialOrd,
|
||||||
Clone,
|
Clone,
|
||||||
|
|||||||
@@ -15,7 +15,3 @@ pub use js::*;
|
|||||||
|
|
||||||
mod asset;
|
mod asset;
|
||||||
pub use asset::*;
|
pub use asset::*;
|
||||||
|
|
||||||
pub mod linker;
|
|
||||||
|
|
||||||
pub mod hash;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
//! Utilities for working with Manganis assets in the linker. This module defines [`LinkSection`] which has information about what section manganis assets are stored in on each platform.
|
|
||||||
|
|
||||||
/// Information about the manganis link section for a given platform
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct LinkSection {
|
|
||||||
/// The link section we pass to the static
|
|
||||||
pub link_section: &'static str,
|
|
||||||
/// The name of the section we find in the binary
|
|
||||||
pub name: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinkSection {
|
|
||||||
/// The list of link sections for all supported platforms
|
|
||||||
pub const ALL: &'static [&'static LinkSection] =
|
|
||||||
&[Self::WASM, Self::MACOS, Self::WINDOWS, Self::ILLUMOS];
|
|
||||||
|
|
||||||
/// Returns the link section used in linux, android, fuchsia, psp, freebsd, and wasm32
|
|
||||||
pub const WASM: &'static LinkSection = &LinkSection {
|
|
||||||
link_section: "manganis",
|
|
||||||
name: "manganis",
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Returns the link section used in macOS, iOS, tvOS
|
|
||||||
pub const MACOS: &'static LinkSection = &LinkSection {
|
|
||||||
link_section: "__DATA,manganis,regular,no_dead_strip",
|
|
||||||
name: "manganis",
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Returns the link section used in windows
|
|
||||||
pub const WINDOWS: &'static LinkSection = &LinkSection {
|
|
||||||
link_section: "mg",
|
|
||||||
name: "mg",
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Returns the link section used in illumos
|
|
||||||
pub const ILLUMOS: &'static LinkSection = &LinkSection {
|
|
||||||
link_section: "set_manganis",
|
|
||||||
name: "set_manganis",
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The link section used on the current platform
|
|
||||||
pub const CURRENT: &'static LinkSection = {
|
|
||||||
#[cfg(any(
|
|
||||||
target_os = "none",
|
|
||||||
target_os = "linux",
|
|
||||||
target_os = "android",
|
|
||||||
target_os = "fuchsia",
|
|
||||||
target_os = "psp",
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_arch = "wasm32"
|
|
||||||
))]
|
|
||||||
{
|
|
||||||
Self::WASM
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))]
|
|
||||||
{
|
|
||||||
Self::MACOS
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
Self::WINDOWS
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "illumos")]
|
|
||||||
{
|
|
||||||
Self::ILLUMOS
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ use crate::{
|
|||||||
/// Settings for a generic asset
|
/// Settings for a generic asset
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
|
Eq,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
PartialOrd,
|
PartialOrd,
|
||||||
Clone,
|
Clone,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use crate::{resolve_path, AssetParseError};
|
use crate::{resolve_path, AssetParseError};
|
||||||
use macro_string::MacroString;
|
use macro_string::MacroString;
|
||||||
use manganis_core::hash::AssetHash;
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
use quote::{quote, ToTokens, TokenStreamExt};
|
use quote::{quote, ToTokens, TokenStreamExt};
|
||||||
use std::path::PathBuf;
|
use std::{
|
||||||
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
use syn::{
|
use syn::{
|
||||||
parse::{Parse, ParseStream},
|
parse::{Parse, ParseStream},
|
||||||
spanned::Spanned as _,
|
spanned::Spanned as _,
|
||||||
@@ -72,24 +74,19 @@ impl ToTokens for AssetParser {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let asset_str = asset.to_string_lossy();
|
let asset_string = asset.to_string_lossy();
|
||||||
let mut asset_str = proc_macro2::Literal::string(&asset_str);
|
let mut asset_str = proc_macro2::Literal::string(&asset_string);
|
||||||
asset_str.set_span(self.path_expr.span());
|
asset_str.set_span(self.path_expr.span());
|
||||||
|
|
||||||
let hash = match AssetHash::hash_file_contents(asset) {
|
let mut hash = DefaultHasher::new();
|
||||||
Ok(hash) => hash,
|
format!("{:?}", self.options.span()).hash(&mut hash);
|
||||||
Err(err) => {
|
format!("{:?}", self.options.to_string()).hash(&mut hash);
|
||||||
let err = err.to_string();
|
asset_string.hash(&mut hash);
|
||||||
tokens.append_all(quote! { compile_error!(#err) });
|
let asset_hash = format!("{:016x}", hash.finish());
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let hash = hash.bytes();
|
|
||||||
|
|
||||||
// Generate the link section for the asset
|
// Generate the link section for the asset
|
||||||
// The link section includes the source path and the output path of 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));
|
let link_section = crate::generate_link_section(quote!(__ASSET), &asset_hash);
|
||||||
|
|
||||||
// generate the asset::new method to deprecate the `./assets/blah.css` syntax
|
// generate the asset::new method to deprecate the `./assets/blah.css` syntax
|
||||||
let constructor = if asset.is_relative() {
|
let constructor = if asset.is_relative() {
|
||||||
@@ -106,21 +103,25 @@ impl ToTokens for AssetParser {
|
|||||||
|
|
||||||
tokens.extend(quote! {
|
tokens.extend(quote! {
|
||||||
{
|
{
|
||||||
// We keep a hash of the contents of the asset for cache busting
|
|
||||||
const __ASSET_HASH: &[u8] = &[#(#hash),*];
|
|
||||||
// The source is used by the CLI to copy the asset
|
// The source is used by the CLI to copy the asset
|
||||||
const __ASSET_SOURCE_PATH: &'static str = #asset_str;
|
const __ASSET_SOURCE_PATH: &'static str = #asset_str;
|
||||||
// The options give the CLI info about how to process the asset
|
// 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
|
// 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
|
// in the constructor. Stable rust doesn't have support for constant functions in traits
|
||||||
const __ASSET_OPTIONS: manganis::AssetOptions = #options.into_asset_options();
|
const __ASSET_OPTIONS: manganis::AssetOptions = #options.into_asset_options();
|
||||||
|
// The input token hash is used to uniquely identify the link section for this asset
|
||||||
|
const __ASSET_HASH: &'static str = #asset_hash;
|
||||||
// Create the asset that the crate will use. This is used both in the return value and
|
// 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
|
// added to the linker for the bundler to copy later
|
||||||
const __ASSET: manganis::BundledAsset = manganis::macro_helpers::#constructor(__ASSET_SOURCE_PATH, __ASSET_HASH, __ASSET_OPTIONS);
|
const __ASSET: manganis::BundledAsset = manganis::macro_helpers::#constructor(__ASSET_SOURCE_PATH, __ASSET_OPTIONS);
|
||||||
|
|
||||||
#link_section
|
#link_section
|
||||||
|
|
||||||
manganis::Asset::new(__ASSET, __keep_link_section)
|
static __REFERENCE_TO_LINK_SECTION: &'static [u8] = &__LINK_SECTION;
|
||||||
|
|
||||||
|
manganis::Asset::new(
|
||||||
|
__REFERENCE_TO_LINK_SECTION
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ use quote::ToTokens;
|
|||||||
/// We force rust to store a serialized representation of the asset description
|
/// We force rust to store a serialized representation of the asset description
|
||||||
/// inside a particular region of the binary, with the label "manganis".
|
/// inside a particular region of the binary, with the label "manganis".
|
||||||
/// After linking, the "manganis" sections of the different object files will be merged.
|
/// After linking, the "manganis" sections of the different object files will be merged.
|
||||||
pub fn generate_link_section(asset: impl ToTokens) -> TokenStream2 {
|
pub fn generate_link_section(asset: impl ToTokens, asset_hash: &str) -> TokenStream2 {
|
||||||
let position = proc_macro2::Span::call_site();
|
let position = proc_macro2::Span::call_site();
|
||||||
let section_name = syn::LitStr::new(
|
let export_name = syn::LitStr::new(&format!("__MANGANIS__{}", asset_hash), position);
|
||||||
manganis_core::linker::LinkSection::CURRENT.link_section,
|
|
||||||
position,
|
|
||||||
);
|
|
||||||
|
|
||||||
quote::quote! {
|
quote::quote! {
|
||||||
// First serialize the asset into a constant sized buffer
|
// First serialize the asset into a constant sized buffer
|
||||||
@@ -22,12 +19,7 @@ pub fn generate_link_section(asset: impl ToTokens) -> TokenStream2 {
|
|||||||
const __LEN: usize = __BYTES.len();
|
const __LEN: usize = __BYTES.len();
|
||||||
|
|
||||||
// Now that we have the size of the asset, copy the bytes into a static array
|
// Now that we have the size of the asset, copy the bytes into a static array
|
||||||
#[link_section = #section_name]
|
#[unsafe(export_name = #export_name)]
|
||||||
#[used]
|
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
|
||||||
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
|
|
||||||
|
|
||||||
fn __keep_link_section() -> u8 {
|
|
||||||
unsafe { std::ptr::read_volatile(__LINK_SECTION.as_ptr()) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
use const_serialize::{serialize_const, ConstVec, SerializeConst};
|
|
||||||
|
|
||||||
// From rustchash - https://github.com/rust-lang/rustc-hash/blob/6745258da00b7251bed4a8461871522d0231a9c7/src/lib.rs#L98
|
|
||||||
const K: u64 = 0xf1357aea2e62a9c5;
|
|
||||||
|
|
||||||
pub(crate) struct ConstHasher {
|
|
||||||
hash: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConstHasher {
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self { hash: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn finish(&self) -> u64 {
|
|
||||||
self.hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn write(mut self, bytes: &[u8]) -> Self {
|
|
||||||
let mut i = 0;
|
|
||||||
while i < bytes.len() {
|
|
||||||
self = self.write_byte(bytes[i]);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn write_byte(mut self, byte: u8) -> Self {
|
|
||||||
self.hash = self.hash.wrapping_add(byte as u64).wrapping_mul(K);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn hash_by_bytes<T: SerializeConst>(self, item: &T) -> Self {
|
|
||||||
let mut bytes = ConstVec::new();
|
|
||||||
bytes = serialize_const(item, bytes);
|
|
||||||
let bytes = bytes.as_ref();
|
|
||||||
self.write(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
mod hash;
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod macro_helpers;
|
pub mod macro_helpers;
|
||||||
pub use manganis_macro::{asset, css_module};
|
pub use manganis_macro::{asset, css_module};
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
pub use const_serialize;
|
pub use const_serialize;
|
||||||
use const_serialize::{serialize_const, ConstStr, ConstVec};
|
use const_serialize::{serialize_const, ConstStr, ConstVec, SerializeConst};
|
||||||
use manganis_core::{AssetOptions, BundledAsset};
|
use manganis_core::{AssetOptions, BundledAsset};
|
||||||
|
|
||||||
use crate::hash::ConstHasher;
|
const PLACEHOLDER_HASH: ConstStr =
|
||||||
|
ConstStr::new("this is a placeholder path which will be replaced by the linker");
|
||||||
|
|
||||||
/// Create a bundled asset from the input path, the content hash, and the asset options
|
/// Create a bundled asset from the input path, the content hash, and the asset options
|
||||||
pub const fn create_bundled_asset(
|
pub const fn create_bundled_asset(input_path: &str, asset_config: AssetOptions) -> BundledAsset {
|
||||||
input_path: &str,
|
BundledAsset::new_from_const(ConstStr::new(input_path), PLACEHOLDER_HASH, asset_config)
|
||||||
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
|
/// Create a bundled asset from the input path, the content hash, and the asset options with a relative asset deprecation warning
|
||||||
@@ -22,204 +18,29 @@ pub const fn create_bundled_asset(
|
|||||||
)]
|
)]
|
||||||
pub const fn create_bundled_asset_relative(
|
pub const fn create_bundled_asset_relative(
|
||||||
input_path: &str,
|
input_path: &str,
|
||||||
content_hash: &[u8],
|
|
||||||
asset_config: AssetOptions,
|
asset_config: AssetOptions,
|
||||||
) -> BundledAsset {
|
) -> BundledAsset {
|
||||||
create_bundled_asset(input_path, content_hash, asset_config)
|
create_bundled_asset(input_path, 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
|
|
||||||
let mut input_path = ConstStr::new(input_path);
|
|
||||||
// Then strip the prefix from the input path. The path comes from the build platform, but
|
|
||||||
// in wasm, we don't know what the path separator is from the build platform. We need to
|
|
||||||
// split by both unix and windows paths and take the smallest one
|
|
||||||
let mut extension = None;
|
|
||||||
match (input_path.rsplit_once('/'), input_path.rsplit_once('\\')) {
|
|
||||||
(Some((_, unix_new_input_path)), Some((_, windows_new_input_path))) => {
|
|
||||||
input_path = if unix_new_input_path.len() < windows_new_input_path.len() {
|
|
||||||
unix_new_input_path
|
|
||||||
} else {
|
|
||||||
windows_new_input_path
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(Some((_, unix_new_input_path)), _) => {
|
|
||||||
input_path = unix_new_input_path;
|
|
||||||
}
|
|
||||||
(_, Some((_, windows_new_input_path))) => {
|
|
||||||
input_path = windows_new_input_path;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if let Some((new_input_path, new_extension)) = input_path.rsplit_once('.') {
|
|
||||||
extension = Some(new_extension);
|
|
||||||
input_path = new_input_path;
|
|
||||||
}
|
|
||||||
// Then add a dash
|
|
||||||
let mut macro_output_path = input_path.push_str("-");
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
hasher = hasher.hash_by_bytes(asset_config);
|
|
||||||
let hash = hasher.finish();
|
|
||||||
|
|
||||||
// Then add the hash in hex form
|
|
||||||
let hash_bytes = hash.to_le_bytes();
|
|
||||||
let mut i = 0;
|
|
||||||
while i < hash_bytes.len() {
|
|
||||||
let byte = hash_bytes[i];
|
|
||||||
let first = byte >> 4;
|
|
||||||
let second = byte & 0x0f;
|
|
||||||
const fn byte_to_char(byte: u8) -> char {
|
|
||||||
match char::from_digit(byte as u32, 16) {
|
|
||||||
Some(c) => c,
|
|
||||||
None => panic!("byte must be a valid digit"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
macro_output_path = macro_output_path.push(byte_to_char(first));
|
|
||||||
macro_output_path = macro_output_path.push(byte_to_char(second));
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally add the extension
|
|
||||||
match asset_config.extension() {
|
|
||||||
Some(extension) => {
|
|
||||||
macro_output_path = macro_output_path.push('.');
|
|
||||||
macro_output_path = macro_output_path.push_str(extension)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
if let Some(extension) = extension {
|
|
||||||
macro_output_path = macro_output_path.push('.');
|
|
||||||
|
|
||||||
let ext_bytes = extension.as_str().as_bytes();
|
|
||||||
|
|
||||||
// Rewrite scss as css
|
|
||||||
if bytes_equal(ext_bytes, b"scss") || bytes_equal(ext_bytes, b"sass") {
|
|
||||||
macro_output_path = macro_output_path.push_str("css")
|
|
||||||
} else {
|
|
||||||
macro_output_path = macro_output_path.push_str(extension.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_output_path
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct the hash used by manganis and cli-opt to uniquely identify a asset based on its contents
|
|
||||||
pub const fn hash_asset(asset_config: &AssetOptions, content_hash: u64) -> ConstStr {
|
|
||||||
let mut string = ConstStr::new("");
|
|
||||||
|
|
||||||
// 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.hash_by_bytes(asset_config);
|
|
||||||
let hash = hasher.finish();
|
|
||||||
|
|
||||||
// Then add the hash in hex form
|
|
||||||
let hash_bytes = hash.to_le_bytes();
|
|
||||||
let mut i = 0;
|
|
||||||
while i < hash_bytes.len() {
|
|
||||||
let byte = hash_bytes[i];
|
|
||||||
let first = byte >> 4;
|
|
||||||
let second = byte & 0x0f;
|
|
||||||
const fn byte_to_char(byte: u8) -> char {
|
|
||||||
match char::from_digit(byte as u32, 16) {
|
|
||||||
Some(c) => c,
|
|
||||||
None => panic!("byte must be a valid digit"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
string = string.push(byte_to_char(first));
|
|
||||||
string = string.push(byte_to_char(second));
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
string
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn bytes_equal(left: &[u8], right: &[u8]) -> bool {
|
|
||||||
if left.len() != right.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut i = 0;
|
|
||||||
while i < left.len() {
|
|
||||||
if left[i] != right[i] {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unique_path() {
|
|
||||||
use manganis_core::{ImageAssetOptions, ImageFormat};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
let mut input_path = PathBuf::from("some");
|
|
||||||
input_path.push("prefix");
|
|
||||||
input_path.push("test.png");
|
|
||||||
let content_hash = 123456789;
|
|
||||||
let asset_config = AssetOptions::Image(ImageAssetOptions::new().with_format(ImageFormat::Avif));
|
|
||||||
let output_path =
|
|
||||||
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
|
|
||||||
assert_eq!(output_path.as_str(), "test-603a88fe296462a3.avif");
|
|
||||||
|
|
||||||
// Changing the path without changing the contents shouldn't change the hash
|
|
||||||
let mut input_path = PathBuf::from("some");
|
|
||||||
input_path.push("prefix");
|
|
||||||
input_path.push("prefix");
|
|
||||||
input_path.push("test.png");
|
|
||||||
let content_hash = 123456789;
|
|
||||||
let asset_config = AssetOptions::Image(ImageAssetOptions::new().with_format(ImageFormat::Avif));
|
|
||||||
let output_path =
|
|
||||||
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
|
|
||||||
assert_eq!(output_path.as_str(), "test-603a88fe296462a3.avif");
|
|
||||||
|
|
||||||
let mut input_path = PathBuf::from("test");
|
|
||||||
input_path.push("ing");
|
|
||||||
input_path.push("test");
|
|
||||||
let content_hash = 123456789;
|
|
||||||
let asset_config = AssetOptions::Unknown;
|
|
||||||
let output_path =
|
|
||||||
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
|
|
||||||
assert_eq!(output_path.as_str(), "test-8d6e32dc0b45f853");
|
|
||||||
|
|
||||||
// Just changing the content hash should change the total hash
|
|
||||||
let mut input_path = PathBuf::from("test");
|
|
||||||
input_path.push("ing");
|
|
||||||
input_path.push("test");
|
|
||||||
let content_hash = 123456780;
|
|
||||||
let asset_config = AssetOptions::Unknown;
|
|
||||||
let output_path =
|
|
||||||
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
|
|
||||||
assert_eq!(output_path.as_str(), "test-40783366737abc4d");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize an asset to a const buffer
|
/// Serialize an asset to a const buffer
|
||||||
pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8> {
|
pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8> {
|
||||||
let write = ConstVec::new();
|
let data = ConstVec::new();
|
||||||
serialize_const(asset, write)
|
let mut data = serialize_const(asset, data);
|
||||||
|
// Reserve the maximum size of the asset
|
||||||
|
while data.len() < BundledAsset::MEMORY_LAYOUT.size() {
|
||||||
|
data = data.push(0);
|
||||||
|
}
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize a const buffer into a BundledAsset
|
||||||
|
pub const fn deserialize_asset(bytes: &[u8]) -> BundledAsset {
|
||||||
|
let bytes = ConstVec::new().extend(bytes);
|
||||||
|
match const_serialize::deserialize_const!(BundledAsset, bytes.read()) {
|
||||||
|
Some((_, asset)) => asset,
|
||||||
|
None => panic!("Failed to deserialize asset. This may be caused by a mismatch between your dioxus and dioxus-cli versions"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy a slice into a constant sized buffer at compile time
|
/// Copy a slice into a constant sized buffer at compile time
|
||||||
|
|||||||
@@ -8,4 +8,19 @@ test("optimized scripts run", async ({ page }) => {
|
|||||||
// should add an editor to the page that shows a main function
|
// should add an editor to the page that shows a main function
|
||||||
const main = page.locator("#main");
|
const main = page.locator("#main");
|
||||||
await expect(main).toContainText("hi");
|
await expect(main).toContainText("hi");
|
||||||
|
|
||||||
|
// Expect the page to contain an image with the id "some_image"
|
||||||
|
const image = page.locator("#some_image");
|
||||||
|
await expect(image).toBeVisible();
|
||||||
|
// Get the image src
|
||||||
|
const src = await image.getAttribute("src");
|
||||||
|
|
||||||
|
// Expect the page to contain an image with the id "some_image_with_the_same_url"
|
||||||
|
const image2 = page.locator("#some_image_with_the_same_url");
|
||||||
|
await expect(image2).toBeVisible();
|
||||||
|
// Get the image src
|
||||||
|
const src2 = await image2.getAttribute("src");
|
||||||
|
|
||||||
|
// Expect the urls to be different
|
||||||
|
expect(src).not.toEqual(src2);
|
||||||
});
|
});
|
||||||
@@ -4,6 +4,8 @@ use dioxus::prelude::*;
|
|||||||
|
|
||||||
const MONACO_FOLDER: Asset = asset!("/monaco-editor-0.52.2/package/min/vs");
|
const MONACO_FOLDER: Asset = asset!("/monaco-editor-0.52.2/package/min/vs");
|
||||||
const SOME_IMAGE: Asset = asset!("/images/toasts.png", ImageAssetOptions::new().with_avif());
|
const SOME_IMAGE: Asset = asset!("/images/toasts.png", ImageAssetOptions::new().with_avif());
|
||||||
|
const SOME_IMAGE_WITH_THE_SAME_URL: Asset =
|
||||||
|
asset!("/images/toasts.png", ImageAssetOptions::new().with_jpg());
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::launch(App);
|
dioxus::launch(App);
|
||||||
@@ -32,7 +34,12 @@ fn App() -> Element {
|
|||||||
"onload": script
|
"onload": script
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
|
id: "some_image",
|
||||||
src: "{SOME_IMAGE}"
|
src: "{SOME_IMAGE}"
|
||||||
}
|
}
|
||||||
|
img {
|
||||||
|
id: "some_image_with_the_same_url",
|
||||||
|
src: "{SOME_IMAGE_WITH_THE_SAME_URL}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,50 +1,51 @@
|
|||||||
// @ts-check
|
// TODO: ssg is disabled in the CLI server
|
||||||
const { test, expect } = require("@playwright/test");
|
// // @ts-check
|
||||||
|
// const { test, expect } = require("@playwright/test");
|
||||||
|
|
||||||
test("nested suspense resolves", async ({ page }) => {
|
// test("nested suspense resolves", async ({ page }) => {
|
||||||
// Wait for the dev server to reload
|
// // Wait for the dev server to reload
|
||||||
await page.goto("http://localhost:5050");
|
// await page.goto("http://localhost:6060");
|
||||||
// Then wait for the page to start loading
|
// // Then wait for the page to start loading
|
||||||
await page.goto("http://localhost:5050", { waitUntil: "commit" });
|
// await page.goto("http://localhost:6060", { waitUntil: "commit" });
|
||||||
|
|
||||||
// Expect the page to contain the suspense result from the server
|
// // Expect the page to contain the suspense result from the server
|
||||||
const mainMessageTitle = page.locator("#title-0");
|
// const mainMessageTitle = page.locator("#title-0");
|
||||||
await expect(mainMessageTitle).toContainText("The robot says hello world");
|
// await expect(mainMessageTitle).toContainText("The robot says hello world");
|
||||||
const mainMessageBody = page.locator("#body-0");
|
// const mainMessageBody = page.locator("#body-0");
|
||||||
await expect(mainMessageBody).toContainText(
|
// await expect(mainMessageBody).toContainText(
|
||||||
"The robot becomes sentient and says hello world"
|
// "The robot becomes sentient and says hello world"
|
||||||
);
|
// );
|
||||||
|
|
||||||
// And expect the title to have resolved on the client
|
// // And expect the title to have resolved on the client
|
||||||
await expect(page).toHaveTitle("The robot says hello world");
|
// await expect(page).toHaveTitle("The robot says hello world");
|
||||||
|
|
||||||
// Nested suspense should be resolved
|
// // Nested suspense should be resolved
|
||||||
const nestedMessageTitle1 = page.locator("#title-1");
|
// const nestedMessageTitle1 = page.locator("#title-1");
|
||||||
await expect(nestedMessageTitle1).toContainText("The world says hello back");
|
// await expect(nestedMessageTitle1).toContainText("The world says hello back");
|
||||||
const nestedMessageBody1 = page.locator("#body-1");
|
// const nestedMessageBody1 = page.locator("#body-1");
|
||||||
await expect(nestedMessageBody1).toContainText(
|
// await expect(nestedMessageBody1).toContainText(
|
||||||
"In a stunning turn of events, the world collectively unites and says hello back"
|
// "In a stunning turn of events, the world collectively unites and says hello back"
|
||||||
);
|
// );
|
||||||
|
|
||||||
const nestedMessageDiv2 = page.locator("#children-2");
|
// const nestedMessageDiv2 = page.locator("#children-2");
|
||||||
await expect(nestedMessageDiv2).toBeEmpty();
|
// await expect(nestedMessageDiv2).toBeEmpty();
|
||||||
const nestedMessageTitle2 = page.locator("#title-2");
|
// const nestedMessageTitle2 = page.locator("#title-2");
|
||||||
await expect(nestedMessageTitle2).toContainText("Goodbye Robot");
|
// await expect(nestedMessageTitle2).toContainText("Goodbye Robot");
|
||||||
const nestedMessageBody2 = page.locator("#body-2");
|
// const nestedMessageBody2 = page.locator("#body-2");
|
||||||
await expect(nestedMessageBody2).toContainText("The robot says goodbye");
|
// await expect(nestedMessageBody2).toContainText("The robot says goodbye");
|
||||||
|
|
||||||
const nestedMessageDiv3 = page.locator("#children-3");
|
// const nestedMessageDiv3 = page.locator("#children-3");
|
||||||
await expect(nestedMessageDiv3).toBeEmpty();
|
// await expect(nestedMessageDiv3).toBeEmpty();
|
||||||
const nestedMessageTitle3 = page.locator("#title-3");
|
// const nestedMessageTitle3 = page.locator("#title-3");
|
||||||
await expect(nestedMessageTitle3).toContainText("Goodbye World");
|
// await expect(nestedMessageTitle3).toContainText("Goodbye World");
|
||||||
const nestedMessageBody3 = page.locator("#body-3");
|
// const nestedMessageBody3 = page.locator("#body-3");
|
||||||
await expect(nestedMessageBody3).toContainText("The world says goodbye");
|
// await expect(nestedMessageBody3).toContainText("The world says goodbye");
|
||||||
|
|
||||||
// Deeply nested suspense should be resolved
|
// // Deeply nested suspense should be resolved
|
||||||
const nestedMessageDiv4 = page.locator("#children-4");
|
// const nestedMessageDiv4 = page.locator("#children-4");
|
||||||
await expect(nestedMessageDiv4).toBeEmpty();
|
// await expect(nestedMessageDiv4).toBeEmpty();
|
||||||
const nestedMessageTitle4 = page.locator("#title-4");
|
// const nestedMessageTitle4 = page.locator("#title-4");
|
||||||
await expect(nestedMessageTitle4).toContainText("Hello World");
|
// await expect(nestedMessageTitle4).toContainText("Hello World");
|
||||||
const nestedMessageBody4 = page.locator("#body-4");
|
// const nestedMessageBody4 = page.locator("#body-4");
|
||||||
await expect(nestedMessageBody4).toContainText("The world says hello again");
|
// await expect(nestedMessageBody4).toContainText("The world says hello again");
|
||||||
});
|
// });
|
||||||
@@ -87,8 +87,8 @@ module.exports = defineConfig({
|
|||||||
{
|
{
|
||||||
cwd: path.join(process.cwd(), "web"),
|
cwd: path.join(process.cwd(), "web"),
|
||||||
command:
|
command:
|
||||||
'cargo run --package dioxus-cli --release -- serve --force-sequential --platform web --addr "127.0.0.1" --port 9999',
|
'cargo run --package dioxus-cli --release -- serve --force-sequential --platform web --addr "127.0.0.1" --port 9990',
|
||||||
port: 9999,
|
port: 9990,
|
||||||
timeout: 50 * 60 * 1000,
|
timeout: 50 * 60 * 1000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# It's recommended to set the flag on a per-target basis:
|
||||||
|
[target.wasm32-unknown-unknown]
|
||||||
|
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
const { test, expect, defineConfig } = require("@playwright/test");
|
const { test, expect, defineConfig } = require("@playwright/test");
|
||||||
|
|
||||||
test("button click", async ({ page }) => {
|
test("button click", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
|
|
||||||
// Expect the page to contain the counter text.
|
// Expect the page to contain the counter text.
|
||||||
const main = page.locator("#main");
|
const main = page.locator("#main");
|
||||||
@@ -21,7 +21,7 @@ test("button click", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("svg", async ({ page }) => {
|
test("svg", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
|
|
||||||
// Expect the page to contain the svg.
|
// Expect the page to contain the svg.
|
||||||
const svg = page.locator("svg");
|
const svg = page.locator("svg");
|
||||||
@@ -36,7 +36,7 @@ test("svg", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("raw attribute", async ({ page }) => {
|
test("raw attribute", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
|
|
||||||
// Expect the page to contain the div with the raw attribute.
|
// Expect the page to contain the div with the raw attribute.
|
||||||
const div = page.locator("div.raw-attribute-div");
|
const div = page.locator("div.raw-attribute-div");
|
||||||
@@ -44,7 +44,7 @@ test("raw attribute", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("hidden attribute", async ({ page }) => {
|
test("hidden attribute", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
|
|
||||||
// Expect the page to contain the div with the hidden attribute.
|
// Expect the page to contain the div with the hidden attribute.
|
||||||
const div = page.locator("div.hidden-attribute-div");
|
const div = page.locator("div.hidden-attribute-div");
|
||||||
@@ -52,7 +52,7 @@ test("hidden attribute", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("dangerous inner html", async ({ page }) => {
|
test("dangerous inner html", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
|
|
||||||
// Expect the page to contain the div with the dangerous inner html.
|
// Expect the page to contain the div with the dangerous inner html.
|
||||||
const div = page.locator("div.dangerous-inner-html-div");
|
const div = page.locator("div.dangerous-inner-html-div");
|
||||||
@@ -60,7 +60,7 @@ test("dangerous inner html", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("input value", async ({ page }) => {
|
test("input value", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
|
|
||||||
// Expect the page to contain the input with the value.
|
// Expect the page to contain the input with the value.
|
||||||
const input = page.locator("input");
|
const input = page.locator("input");
|
||||||
@@ -68,7 +68,7 @@ test("input value", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("style", async ({ page }) => {
|
test("style", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
|
|
||||||
// Expect the page to contain the div with the style.
|
// Expect the page to contain the div with the style.
|
||||||
const div = page.locator("div.style-div");
|
const div = page.locator("div.style-div");
|
||||||
@@ -77,7 +77,7 @@ test("style", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("eval", async ({ page }) => {
|
test("eval", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
|
|
||||||
// Expect the page to contain the div with the eval and have no text.
|
// Expect the page to contain the div with the eval and have no text.
|
||||||
const div = page.locator("div.eval-result");
|
const div = page.locator("div.eval-result");
|
||||||
@@ -95,7 +95,7 @@ test("eval", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("prevent default", async ({ page }) => {
|
test("prevent default", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
|
|
||||||
// Expect the page to contain the div with the eval and have no text.
|
// Expect the page to contain the div with the eval and have no text.
|
||||||
const a = page.locator("a.prevent-default");
|
const a = page.locator("a.prevent-default");
|
||||||
@@ -109,7 +109,7 @@ test("prevent default", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("onmounted", async ({ page }) => {
|
test("onmounted", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
|
|
||||||
// Expect the onmounted event to be called exactly once.
|
// Expect the onmounted event to be called exactly once.
|
||||||
const mountedDiv = page.locator("div.onmounted-div");
|
const mountedDiv = page.locator("div.onmounted-div");
|
||||||
@@ -117,7 +117,7 @@ test("onmounted", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("web-sys closure", async ({ page }) => {
|
test("web-sys closure", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
// wait until the div is mounted
|
// wait until the div is mounted
|
||||||
const scrollDiv = page.locator("div#web-sys-closure-div");
|
const scrollDiv = page.locator("div#web-sys-closure-div");
|
||||||
await scrollDiv.waitFor({ state: "attached" });
|
await scrollDiv.waitFor({ state: "attached" });
|
||||||
@@ -126,7 +126,7 @@ test("web-sys closure", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("document elements", async ({ page }) => {
|
test("document elements", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
// wait until the meta element is mounted
|
// wait until the meta element is mounted
|
||||||
const meta = page.locator("meta#meta-head[name='testing']");
|
const meta = page.locator("meta#meta-head[name='testing']");
|
||||||
await meta.waitFor({ state: "attached" });
|
await meta.waitFor({ state: "attached" });
|
||||||
@@ -157,7 +157,7 @@ test("document elements", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("merge styles", async ({ page }) => {
|
test("merge styles", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
// wait until the div is mounted
|
// wait until the div is mounted
|
||||||
const div = page.locator("div#merge-styles-div");
|
const div = page.locator("div#merge-styles-div");
|
||||||
await div.waitFor({ state: "attached" });
|
await div.waitFor({ state: "attached" });
|
||||||
@@ -167,7 +167,7 @@ test("merge styles", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("select multiple", async ({ page }) => {
|
test("select multiple", async ({ page }) => {
|
||||||
await page.goto("http://localhost:9999");
|
await page.goto("http://localhost:9990");
|
||||||
// wait until the select element is mounted
|
// wait until the select element is mounted
|
||||||
const staticSelect = page.locator("select#static-multiple-select");
|
const staticSelect = page.locator("select#static-multiple-select");
|
||||||
await staticSelect.waitFor({ state: "attached" });
|
await staticSelect.waitFor({ state: "attached" });
|
||||||
|
|||||||
Reference in new issue
Block a user