use autumn_web::assets::PluginAssets;
pub const ASSETS_NAMESPACE: &str = "gsap";
pub(crate) const GSAP_JS: &str = "gsap.min.js";
pub(crate) const SCROLL_TRIGGER_JS: &str = "ScrollTrigger.min.js";
pub(crate) const SPLIT_TEXT_JS: &str = "SplitText.min.js";
pub(crate) const INIT_JS: &str = "init.js";
pub(crate) const GSAP_CSS: &str = "gsap.css";
pub static GSAP_ASSETS: PluginAssets = PluginAssets::from_files(
ASSETS_NAMESPACE,
&[
(GSAP_JS, include_bytes!("../assets/gsap.min.js")),
(
SCROLL_TRIGGER_JS,
include_bytes!("../assets/ScrollTrigger.min.js"),
),
(SPLIT_TEXT_JS, include_bytes!("../assets/SplitText.min.js")),
(INIT_JS, include_bytes!("../assets/init.js")),
(GSAP_CSS, include_bytes!("../assets/gsap.css")),
],
);
pub const GSAP_VERSION: &str = "3.15.0";
pub const GSAP_LICENSE: &str = "https://gsap.com/standard-license";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct UpstreamFile {
pub path: &'static str,
pub source: &'static str,
pub integrity: &'static str,
}
pub const GSAP_UPSTREAM: &[UpstreamFile] = &[
UpstreamFile {
path: GSAP_JS,
source: "https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js",
integrity: "sha384-XmJ9SoHtVOHoQUcKvFAzVXwdkKo1Ie3bhmSoIAkcdsHGaIrVJIkmozyq0FJeb/Ly",
},
UpstreamFile {
path: SCROLL_TRIGGER_JS,
source: "https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js",
integrity: "sha384-wl5TeDVvOWt30Pbf8aSo2ZrzsOjddu3avOBvHe+p+OhJt9gP6w9YXmDkN5DK2/dF",
},
UpstreamFile {
path: SPLIT_TEXT_JS,
source: "https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/SplitText.min.js",
integrity: "sha384-SWJ0lLVRoipvHh59xj0pL7uC7Ih51F+5smaFtrG+2nr+TlDZU5SYJHmxfolbeNTr",
},
];
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine as _;
use sha2::{Digest as _, Sha384};
fn sri(bytes: &[u8]) -> String {
let digest = Sha384::digest(bytes);
format!(
"sha384-{}",
base64::engine::general_purpose::STANDARD.encode(digest)
)
}
#[test]
fn bundle_holds_exactly_the_served_files() {
let files: Vec<&str> = GSAP_ASSETS
.iter()
.map(autumn_web::assets::PluginAsset::logical_path)
.collect();
assert_eq!(
files,
[SCROLL_TRIGGER_JS, SPLIT_TEXT_JS, GSAP_CSS, GSAP_JS, INIT_JS],
"sorted by path; manifest.json is absent"
);
assert_eq!(GSAP_ASSETS.namespace(), ASSETS_NAMESPACE);
assert_eq!(GSAP_ASSETS.mount_path(), "/static/_plugins/gsap");
}
#[test]
fn plugin_files_are_not_empty() {
for path in [INIT_JS, GSAP_CSS] {
let asset = GSAP_ASSETS.get(path).expect("bundled");
assert!(asset.bytes().len() > 100, "{path} has content");
}
}
#[test]
fn bundle_integrity_matches_embedded_bytes() {
for asset in GSAP_ASSETS.iter() {
assert_eq!(
asset.integrity(),
sri(asset.bytes()),
"{}",
asset.logical_path()
);
}
}
#[test]
fn vendored_files_match_the_pinned_upstream_hashes() {
for f in GSAP_UPSTREAM {
let (path, source) = (f.path, f.source);
let asset = GSAP_ASSETS.get(path).expect("bundled");
assert_eq!(sri(asset.bytes()), f.integrity, "{path}");
assert!(
source.contains(&format!("gsap@{GSAP_VERSION}/dist/{path}")),
"{source}"
);
}
}
#[test]
fn vendored_files_keep_the_upstream_license_header() {
for path in GSAP_UPSTREAM.iter().map(|f| f.path) {
let bytes = GSAP_ASSETS.get(path).expect("bundled").bytes();
let head = String::from_utf8_lossy(&bytes[..300]);
assert!(head.contains(GSAP_VERSION), "{path}: {head}");
assert!(head.contains(GSAP_LICENSE), "{path}: {head}");
}
}
#[test]
fn urls_are_fingerprinted_under_the_plugin_mount() {
for asset in GSAP_ASSETS.iter() {
let path = asset.logical_path();
assert_eq!(asset.plain_url(), format!("/static/_plugins/gsap/{path}"));
let (stem, ext) = path.rsplit_once('.').expect("extension");
let url = asset.url();
let hash = url
.strip_prefix(&format!("/static/_plugins/gsap/{stem}."))
.and_then(|rest| rest.strip_suffix(&format!(".{ext}")))
.unwrap_or_else(|| panic!("{url} is the hashed form of {path}"));
assert_eq!(hash.len(), 8, "{url}");
assert!(hash.bytes().all(|b| b.is_ascii_hexdigit()), "{url}");
}
}
#[test]
fn content_types_match_the_files() {
for asset in GSAP_ASSETS.iter() {
let want = if std::path::Path::new(asset.logical_path())
.extension()
.is_some_and(|e| e == "css")
{
"text/css; charset=utf-8"
} else {
"text/javascript; charset=utf-8"
};
assert_eq!(asset.content_type(), want, "{}", asset.logical_path());
}
}
#[test]
fn manifest_agrees_with_constants() {
let manifest: serde_json::Value =
serde_json::from_str(include_str!("../assets/manifest.json")).expect("json");
assert_eq!(manifest["version"], GSAP_VERSION);
assert_eq!(manifest["license"], GSAP_LICENSE);
for f in GSAP_UPSTREAM {
let path = f.path;
let entry = &manifest["files"][path];
assert_eq!(entry["source"], f.source, "{path}");
assert_eq!(entry["integrity"], f.integrity, "{path}");
let bytes = GSAP_ASSETS.get(path).expect("bundled").bytes().len();
assert_eq!(entry["bytes"], bytes, "{path}");
}
}
}