diff --git a/crates/spa/src/lib.rs b/crates/spa/src/lib.rs
index af3be8def..7e38f977b 100644
--- a/crates/spa/src/lib.rs
+++ b/crates/spa/src/lib.rs
@@ -10,4 +10,4 @@
mod vite;
-pub use self::vite::Manifest as ViteManifest;
+pub use self::vite::{FileType, Manifest as ViteManifest};
diff --git a/crates/spa/src/vite.rs b/crates/spa/src/vite.rs
index b488bea6b..b2d7c0c2a 100644
--- a/crates/spa/src/vite.rs
+++ b/crates/spa/src/vite.rs
@@ -48,7 +48,7 @@ pub struct Manifest {
}
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
-enum FileType {
+pub enum FileType {
Script,
Stylesheet,
Woff,
@@ -104,83 +104,24 @@ impl<'a> Asset<'a> {
})
}
- fn src(&self, assets_base: &Utf8Path) -> Utf8PathBuf {
+ /// Get the source path of this asset, relative to the assets base path
+ #[must_use]
+ pub fn src(&self, assets_base: &Utf8Path) -> Utf8PathBuf {
assets_base.join(self.name)
}
- /// Generate a `` tag to preload this entry
- pub fn preload_tag(&self, assets_base: &Utf8Path) -> String {
- let href = self.src(assets_base);
- let integrity = self
- .integrity
- .map(|i| format!(r#"integrity="{i}" "#))
- .unwrap_or_default();
- match self.file_type {
- FileType::Stylesheet => {
- format!(r#""#)
- }
- FileType::Script => {
- format!(r#""#)
- }
- FileType::Woff | FileType::Woff2 => {
- format!(r#""#,)
- }
- FileType::Json => {
- format!(r#""#,)
- }
- FileType::Png => {
- format!(r#""#,)
- }
- }
- }
-
- /// Generate a `` or `"#
- )),
- FileType::Woff | FileType::Woff2 | FileType::Json | FileType::Png => None,
- }
- }
-
- /// Returns `true` if the asset type is a script
+ /// Get the file type of this asset
#[must_use]
- pub fn is_script(&self) -> bool {
- self.file_type == FileType::Script
+ pub fn file_type(&self) -> FileType {
+ self.file_type
}
- /// Returns `true` if the asset type is a stylesheet
+ /// Get the integrity HTML tag attribute, with a leading space, if any
#[must_use]
- pub fn is_stylesheet(&self) -> bool {
- self.file_type == FileType::Stylesheet
- }
-
- /// Returns `true` if the asset type is JSON
- #[must_use]
- pub fn is_json(&self) -> bool {
- self.file_type == FileType::Json
- }
-
- /// Returns `true` if the asset type is a font
- #[must_use]
- pub fn is_font(&self) -> bool {
- self.file_type == FileType::Woff || self.file_type == FileType::Woff2
- }
-
- /// Returns `true` if the asset type is image
- #[must_use]
- pub fn is_image(&self) -> bool {
- self.file_type == FileType::Png
+ pub fn integrity_attr(&self) -> String {
+ self.integrity
+ .map(|i| format!(r#" integrity="{i}""#))
+ .unwrap_or_default()
}
}
diff --git a/crates/templates/src/functions.rs b/crates/templates/src/functions.rs
index 3a8e3d43d..2ca7b12f6 100644
--- a/crates/templates/src/functions.rs
+++ b/crates/templates/src/functions.rs
@@ -10,13 +10,13 @@
//! Additional functions, tests and filters used in templates
use std::{
- collections::{BTreeMap, HashMap},
- fmt::Formatter,
+ collections::{BTreeMap, HashMap, HashSet},
+ fmt::{Formatter, Write as _},
str::FromStr,
- sync::{Arc, atomic::AtomicUsize},
+ sync::{Arc, Mutex, atomic::AtomicUsize},
};
-use camino::Utf8Path;
+use camino::{Utf8Path, Utf8PathBuf};
use mas_i18n::{Argument, ArgumentList, DataLocale, Translator, sprintf::FormattedMessagePart};
use mas_router::UrlBuilder;
use mas_spa::ViteManifest;
@@ -419,6 +419,41 @@ impl mas_i18n::icu_datetime::input::IsoTimeInput for TimeAd
}
}
+#[derive(Default, Debug)]
+struct IncludedAssetsTrackerInner {
+ preloaded: HashSet,
+ included: HashSet,
+}
+
+impl IncludedAssetsTrackerInner {
+ /// Mark an asset as preloaded. Returns true if it was not already marked.
+ fn mark_preloaded(&mut self, asset: &Utf8Path) -> bool {
+ self.preloaded.insert(asset.to_owned())
+ }
+
+ /// Mark an asset as included. Returns true if it was not already marked.
+ fn mark_included(&mut self, asset: &Utf8Path) -> bool {
+ self.preloaded.insert(asset.to_owned());
+ self.included.insert(asset.to_owned())
+ }
+}
+
+/// Helper to track included assets during a template render
+#[derive(Default, Debug)]
+struct IncludedAssetsTracker {
+ inner: Mutex,
+}
+
+impl IncludedAssetsTracker {
+ fn lock(&self) -> std::sync::MutexGuard<'_, IncludedAssetsTrackerInner> {
+ // There is no reason for this mutex to ever get poisoned, so it's fine
+ // to unwrap here
+ self.inner.lock().unwrap()
+ }
+}
+
+impl Object for IncludedAssetsTracker {}
+
struct IncludeAsset {
url_builder: UrlBuilder,
vite_manifest: ViteManifest,
@@ -440,11 +475,20 @@ impl std::fmt::Display for IncludeAsset {
}
impl Object for IncludeAsset {
- fn call(self: &Arc, _state: &State, args: &[Value]) -> Result {
+ fn call(self: &Arc, state: &State, args: &[Value]) -> Result {
let (path,): (&str,) = from_args(args)?;
-
let path: &Utf8Path = path.into();
+ let assets_base: &Utf8Path = self.url_builder.assets_base().into();
+
+ // We store the list of assets we've already included and already preloaded in a
+ // 'temp' object. Those live throughout the template render and reset on each
+ // new render.
+ let tracker =
+ state.get_or_set_temp_object("included_assets_tracker", IncludedAssetsTracker::default);
+ let mut tracker = tracker.lock();
+
+ // Grab the main asset and its imports from the manifest
let (main, imported) = self.vite_manifest.find_assets(path).map_err(|e| {
Error::new(
ErrorKind::InvalidOperation,
@@ -452,18 +496,82 @@ impl Object for IncludeAsset {
)
})?;
- let assets = std::iter::once(main)
- .chain(imported.iter().filter(|a| a.is_stylesheet()).copied())
- .filter_map(|asset| asset.include_tag(self.url_builder.assets_base().into()));
+ // We'll accumulate the output in this string
+ let mut output = String::new();
+ match main.file_type() {
+ mas_spa::FileType::Script => {
+ let integrity = main.integrity_attr();
+ let src = main.src(assets_base);
+ if tracker.mark_included(&src) {
+ writeln!(
+ output,
+ r#""#
+ )
+ .unwrap();
+ }
+ }
+ mas_spa::FileType::Stylesheet => {
+ let integrity = main.integrity_attr();
+ let src = main.src(assets_base);
+ if tracker.mark_included(&src) {
+ writeln!(
+ output,
+ r#""#
+ )
+ .unwrap();
+ }
+ }
- let preloads = imported
- .iter()
- .filter(|a| a.is_script())
- .map(|asset| asset.preload_tag(self.url_builder.assets_base().into()));
+ file_type => {
+ return Err(Error::new(
+ ErrorKind::InvalidOperation,
+ format!(
+ "The target asset is a {file_type:?} file, which is not supported by `include_asset`"
+ ),
+ ));
+ }
+ }
- let tags: Vec = preloads.chain(assets).collect();
+ for asset in imported {
+ let integrity = asset.integrity_attr();
+ let src = asset.src(assets_base);
+ match asset.file_type() {
+ mas_spa::FileType::Stylesheet => {
+ // Imported stylesheets are inserted directly, not just preloaded
+ if tracker.mark_included(&src) {
+ writeln!(
+ output,
+ r#""#
+ )
+ .unwrap();
+ }
+ }
+ mas_spa::FileType::Script => {
+ if tracker.mark_preloaded(&src) {
+ writeln!(
+ output,
+ r#""#,
+ )
+ .unwrap();
+ }
+ }
+ mas_spa::FileType::Png => {
+ if tracker.mark_preloaded(&src) {
+ writeln!(
+ output,
+ r#""#,
+ )
+ .unwrap();
+ }
+ }
+ mas_spa::FileType::Woff | mas_spa::FileType::Woff2 | mas_spa::FileType::Json => {
+ // Skip pre-loading fonts and JSON (translations) as it will
+ // lead to many wasted preloads.
+ }
+ }
+ }
- Ok(Value::from_safe_string(tags.join("\n")))
+ Ok(Value::from_safe_string(output.trim_end().to_owned()))
}
}