autumn_plugin_gsap/
assets.rs1use autumn_web::assets::PluginAssets;
12
13pub const ASSETS_NAMESPACE: &str = "gsap";
15
16pub(crate) const GSAP_JS: &str = "gsap.min.js";
18pub(crate) const SCROLL_TRIGGER_JS: &str = "ScrollTrigger.min.js";
20pub(crate) const SPLIT_TEXT_JS: &str = "SplitText.min.js";
22pub(crate) const INIT_JS: &str = "init.js";
24pub(crate) const GSAP_CSS: &str = "gsap.css";
26
27pub static GSAP_ASSETS: PluginAssets = PluginAssets::from_files(
37 ASSETS_NAMESPACE,
38 &[
39 (GSAP_JS, include_bytes!("../assets/gsap.min.js")),
40 (
41 SCROLL_TRIGGER_JS,
42 include_bytes!("../assets/ScrollTrigger.min.js"),
43 ),
44 (SPLIT_TEXT_JS, include_bytes!("../assets/SplitText.min.js")),
45 (INIT_JS, include_bytes!("../assets/init.js")),
46 (GSAP_CSS, include_bytes!("../assets/gsap.css")),
47 ],
48);
49
50pub const GSAP_VERSION: &str = "3.15.0";
52
53pub const GSAP_LICENSE: &str = "https://gsap.com/standard-license";
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58#[non_exhaustive]
59pub struct UpstreamFile {
60 pub path: &'static str,
62 pub source: &'static str,
64 pub integrity: &'static str,
66}
67
68pub const GSAP_UPSTREAM: &[UpstreamFile] = &[
73 UpstreamFile {
74 path: GSAP_JS,
75 source: "https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js",
76 integrity: "sha384-XmJ9SoHtVOHoQUcKvFAzVXwdkKo1Ie3bhmSoIAkcdsHGaIrVJIkmozyq0FJeb/Ly",
77 },
78 UpstreamFile {
79 path: SCROLL_TRIGGER_JS,
80 source: "https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js",
81 integrity: "sha384-wl5TeDVvOWt30Pbf8aSo2ZrzsOjddu3avOBvHe+p+OhJt9gP6w9YXmDkN5DK2/dF",
82 },
83 UpstreamFile {
84 path: SPLIT_TEXT_JS,
85 source: "https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/SplitText.min.js",
86 integrity: "sha384-SWJ0lLVRoipvHh59xj0pL7uC7Ih51F+5smaFtrG+2nr+TlDZU5SYJHmxfolbeNTr",
87 },
88];
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use base64::Engine as _;
94 use sha2::{Digest as _, Sha384};
95
96 fn sri(bytes: &[u8]) -> String {
97 let digest = Sha384::digest(bytes);
98 format!(
99 "sha384-{}",
100 base64::engine::general_purpose::STANDARD.encode(digest)
101 )
102 }
103
104 #[test]
105 fn bundle_holds_exactly_the_served_files() {
106 let files: Vec<&str> = GSAP_ASSETS
107 .iter()
108 .map(autumn_web::assets::PluginAsset::logical_path)
109 .collect();
110 assert_eq!(
111 files,
112 [SCROLL_TRIGGER_JS, SPLIT_TEXT_JS, GSAP_CSS, GSAP_JS, INIT_JS],
113 "sorted by path; manifest.json is absent"
114 );
115 assert_eq!(GSAP_ASSETS.namespace(), ASSETS_NAMESPACE);
116 assert_eq!(GSAP_ASSETS.mount_path(), "/static/_plugins/gsap");
117 }
118
119 #[test]
120 fn plugin_files_are_not_empty() {
121 for path in [INIT_JS, GSAP_CSS] {
122 let asset = GSAP_ASSETS.get(path).expect("bundled");
123 assert!(asset.bytes().len() > 100, "{path} has content");
124 }
125 }
126
127 #[test]
128 fn bundle_integrity_matches_embedded_bytes() {
129 for asset in GSAP_ASSETS.iter() {
130 assert_eq!(
131 asset.integrity(),
132 sri(asset.bytes()),
133 "{}",
134 asset.logical_path()
135 );
136 }
137 }
138
139 #[test]
140 fn vendored_files_match_the_pinned_upstream_hashes() {
141 for f in GSAP_UPSTREAM {
142 let (path, source) = (f.path, f.source);
143 let asset = GSAP_ASSETS.get(path).expect("bundled");
144 assert_eq!(sri(asset.bytes()), f.integrity, "{path}");
145 assert!(
146 source.contains(&format!("gsap@{GSAP_VERSION}/dist/{path}")),
147 "{source}"
148 );
149 }
150 }
151
152 #[test]
153 fn vendored_files_keep_the_upstream_license_header() {
154 for path in GSAP_UPSTREAM.iter().map(|f| f.path) {
155 let bytes = GSAP_ASSETS.get(path).expect("bundled").bytes();
156 let head = String::from_utf8_lossy(&bytes[..300]);
157 assert!(head.contains(GSAP_VERSION), "{path}: {head}");
158 assert!(head.contains(GSAP_LICENSE), "{path}: {head}");
159 }
160 }
161
162 #[test]
163 fn urls_are_fingerprinted_under_the_plugin_mount() {
164 for asset in GSAP_ASSETS.iter() {
165 let path = asset.logical_path();
166 assert_eq!(asset.plain_url(), format!("/static/_plugins/gsap/{path}"));
167 let (stem, ext) = path.rsplit_once('.').expect("extension");
168 let url = asset.url();
169 let hash = url
170 .strip_prefix(&format!("/static/_plugins/gsap/{stem}."))
171 .and_then(|rest| rest.strip_suffix(&format!(".{ext}")))
172 .unwrap_or_else(|| panic!("{url} is the hashed form of {path}"));
173 assert_eq!(hash.len(), 8, "{url}");
174 assert!(hash.bytes().all(|b| b.is_ascii_hexdigit()), "{url}");
175 }
176 }
177
178 #[test]
179 fn content_types_match_the_files() {
180 for asset in GSAP_ASSETS.iter() {
181 let want = if std::path::Path::new(asset.logical_path())
182 .extension()
183 .is_some_and(|e| e == "css")
184 {
185 "text/css; charset=utf-8"
186 } else {
187 "text/javascript; charset=utf-8"
188 };
189 assert_eq!(asset.content_type(), want, "{}", asset.logical_path());
190 }
191 }
192
193 #[test]
194 fn manifest_agrees_with_constants() {
195 let manifest: serde_json::Value =
196 serde_json::from_str(include_str!("../assets/manifest.json")).expect("json");
197 assert_eq!(manifest["version"], GSAP_VERSION);
198 assert_eq!(manifest["license"], GSAP_LICENSE);
199 for f in GSAP_UPSTREAM {
200 let path = f.path;
201 let entry = &manifest["files"][path];
202 assert_eq!(entry["source"], f.source, "{path}");
203 assert_eq!(entry["integrity"], f.integrity, "{path}");
204 let bytes = GSAP_ASSETS.get(path).expect("bundled").bytes().len();
205 assert_eq!(entry["bytes"], bytes, "{path}");
206 }
207 }
208}