use autumn_web::assets::PluginAssets;
pub const ASSETS_NAMESPACE: &str = "three";
pub const THREE_VERSION: &str = "0.185.1";
pub const THREE_MODULE_URL: &str = "/static/_plugins/three/three.module.min.js";
pub(crate) const CORE_JS: &str = "three.core.min.js";
pub(crate) const MODULE_JS: &str = "three.module.min.js";
pub(crate) const ORBIT_CONTROLS_JS: &str = "OrbitControls.js";
pub(crate) const GLTF_LOADER_JS: &str = "GLTFLoader.js";
pub(crate) const BUFFER_GEOMETRY_UTILS_JS: &str = "BufferGeometryUtils.js";
pub(crate) const SKELETON_UTILS_JS: &str = "SkeletonUtils.js";
pub(crate) const ROOM_ENVIRONMENT_JS: &str = "RoomEnvironment.js";
pub(crate) const PARSE_JS: &str = "parse.js";
pub(crate) const INIT_JS: &str = "init.js";
pub(crate) const THREE_CSS: &str = "three.css";
pub static THREE_ASSETS: PluginAssets = PluginAssets::from_files(
ASSETS_NAMESPACE,
&[
(CORE_JS, include_bytes!("../assets/three.core.min.js")),
(MODULE_JS, include_bytes!("../assets/three.module.min.js")),
(
ORBIT_CONTROLS_JS,
include_bytes!("../assets/OrbitControls.js"),
),
(GLTF_LOADER_JS, include_bytes!("../assets/GLTFLoader.js")),
(
BUFFER_GEOMETRY_UTILS_JS,
include_bytes!("../assets/BufferGeometryUtils.js"),
),
(
SKELETON_UTILS_JS,
include_bytes!("../assets/SkeletonUtils.js"),
),
(
ROOM_ENVIRONMENT_JS,
include_bytes!("../assets/RoomEnvironment.js"),
),
(PARSE_JS, include_bytes!("../assets/parse.js")),
(INIT_JS, include_bytes!("../assets/init.js")),
(THREE_CSS, include_bytes!("../assets/three.css")),
],
);
const BARE_THREE: &str = "} from 'three';";
const RELATIVE_THREE: &str = "} from './three.module.min.js';";
const THREE_REWRITE: (&str, &str) = (BARE_THREE, RELATIVE_THREE);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct VendoredFile {
pub path: &'static str,
pub source: &'static str,
pub upstream_integrity: &'static str,
pub rewrites: &'static [(&'static str, &'static str)],
}
pub const VENDORED: &[VendoredFile] = &[
VendoredFile {
path: CORE_JS,
source: "https://cdn.jsdelivr.net/npm/three@0.185.1/build/three.core.min.js",
upstream_integrity: "sha384-rx+KIp/9ptjArhnFAcpVoOc/ynktDsRtRJKIbC7YVKylEvFu8sgmzk9RmQ+CIV48",
rewrites: &[],
},
VendoredFile {
path: MODULE_JS,
source: "https://cdn.jsdelivr.net/npm/three@0.185.1/build/three.module.min.js",
upstream_integrity: "sha384-QHQk1LzjJlJYNdthXjKCmffpDRZL3EqJ7LfqBzyKyvGgjAYM2ZVuYtFGg42NcAJ/",
rewrites: &[],
},
VendoredFile {
path: ORBIT_CONTROLS_JS,
source: "https://cdn.jsdelivr.net/npm/three@0.185.1/examples/jsm/controls/OrbitControls.js",
upstream_integrity: "sha384-4rziNxOBZKQ69i+w+f89KJ55TCYquwchVbByQwmaOeIOXdOU2PLDn3kOfXHwIJC9",
rewrites: &[THREE_REWRITE],
},
VendoredFile {
path: GLTF_LOADER_JS,
source: "https://cdn.jsdelivr.net/npm/three@0.185.1/examples/jsm/loaders/GLTFLoader.js",
upstream_integrity: "sha384-3CnKaFWE2emo2DOUQi/yFm4SMemUgSZ9IAJe/V2pyJTw9KXWYSmR0MiX/7RoPyiJ",
rewrites: &[
THREE_REWRITE,
(
"from '../utils/BufferGeometryUtils.js';",
"from './BufferGeometryUtils.js';",
),
(
"from '../utils/SkeletonUtils.js';",
"from './SkeletonUtils.js';",
),
],
},
VendoredFile {
path: BUFFER_GEOMETRY_UTILS_JS,
source: "https://cdn.jsdelivr.net/npm/three@0.185.1/examples/jsm/utils/BufferGeometryUtils.js",
upstream_integrity: "sha384-05mkYituMJObxUkTK7xbW9SA45DEaxOge7FQUGrhW3dsFz0fckzykM6RMAPN26rD",
rewrites: &[THREE_REWRITE],
},
VendoredFile {
path: SKELETON_UTILS_JS,
source: "https://cdn.jsdelivr.net/npm/three@0.185.1/examples/jsm/utils/SkeletonUtils.js",
upstream_integrity: "sha384-Pozn8j5+YFr3ak8Pm90ayqDrGYn/DV7vVs/YIIqzJhzeJT0LQksoS1fZQ5lfsYlw",
rewrites: &[THREE_REWRITE],
},
VendoredFile {
path: ROOM_ENVIRONMENT_JS,
source: "https://cdn.jsdelivr.net/npm/three@0.185.1/examples/jsm/environments/RoomEnvironment.js",
upstream_integrity: "sha384-/H49oz0ZtMgJNgMZ+OhhuMuKBOsaiC3kY0/PZSvgJJXAOJJwmSBJjzlV5lJul3MB",
rewrites: &[THREE_REWRITE],
},
];
#[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)
)
}
fn has_ext(path: &str, ext: &str) -> bool {
std::path::Path::new(path)
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case(ext))
}
fn text(path: &str) -> &'static str {
let asset = THREE_ASSETS.get(path).expect("file is bundled");
std::str::from_utf8(asset.bytes()).expect("bundled text is UTF-8")
}
fn specifiers(source: &str) -> Vec<String> {
let mut out = Vec::new();
for line in source.lines() {
let trimmed = line.trim_start();
if trimmed.starts_with('*') || trimmed.starts_with("//") || trimmed.starts_with("/*") {
continue;
}
let statement = trimmed.starts_with("import") || trimmed.starts_with("export");
for marker in ["from", "import("] {
let mut start = 0;
while let Some(at) = line[start..].find(marker) {
let before = line[..start + at].trim_end();
start += at + marker.len();
if marker == "from" && !(statement || before.ends_with('}')) {
continue;
}
let after = line[start..].trim_start();
let Some(quote) = after.chars().next().filter(|c| *c == '"' || *c == '\'')
else {
continue;
};
if let Some(end) = after[1..].find(quote) {
out.push(after[1..=end].to_owned());
}
}
}
}
out
}
#[test]
fn specifier_scan_finds_imports_and_skips_strings() {
let source = "import { a } from './a.js';\n} from \"./b.js\";\nx}from\"./c.js\"\n\
const m = await import(\"./d.js\");\nwarn(`from \"srgb\"`);\n * from 'three'";
assert_eq!(specifiers(source), ["./a.js", "./b.js", "./c.js", "./d.js"]);
}
#[test]
fn bundle_holds_exactly_the_served_files() {
let mut files: Vec<&str> = THREE_ASSETS
.iter()
.map(autumn_web::assets::PluginAsset::logical_path)
.collect();
files.sort_unstable();
let mut expected = vec![
BUFFER_GEOMETRY_UTILS_JS,
GLTF_LOADER_JS,
ORBIT_CONTROLS_JS,
ROOM_ENVIRONMENT_JS,
SKELETON_UTILS_JS,
INIT_JS,
PARSE_JS,
THREE_CSS,
CORE_JS,
MODULE_JS,
];
expected.sort_unstable();
assert_eq!(files, expected);
assert_eq!(THREE_ASSETS.namespace(), ASSETS_NAMESPACE);
assert_eq!(THREE_ASSETS.mount_path(), "/static/_plugins/three");
}
#[test]
fn bundle_integrity_matches_embedded_bytes() {
for asset in THREE_ASSETS.iter() {
assert_eq!(
asset.integrity(),
sri(asset.bytes()),
"{}",
asset.logical_path()
);
}
}
#[test]
fn vendored_files_match_upstream_after_reversing_rewrites() {
assert_eq!(VENDORED.len(), 7, "two core files and five addons");
for file in VENDORED {
let mut source = text(file.path).to_owned();
for (upstream, served) in file.rewrites {
assert_eq!(
source.matches(served).count(),
1,
"{}: rewrite target {served:?} is unique",
file.path
);
source = source.replace(served, upstream);
}
assert_eq!(
sri(source.as_bytes()),
file.upstream_integrity,
"{} matches upstream",
file.path
);
assert!(
file.source.starts_with(&format!(
"https://cdn.jsdelivr.net/npm/three@{THREE_VERSION}/"
)),
"{}",
file.source
);
}
}
#[test]
fn core_files_are_unchanged_and_addons_are_rewritten() {
for file in VENDORED {
let is_core = file.path == CORE_JS || file.path == MODULE_JS;
assert_eq!(file.rewrites.is_empty(), is_core, "{}", file.path);
for (upstream, served) in file.rewrites {
let spec = |text: &'static str| {
text.strip_prefix("} ")
.unwrap_or(text)
.strip_prefix("from '")
.and_then(|rest| rest.strip_suffix("';"))
.filter(|spec| !spec.contains(['\'', ';', ' ']))
};
let from = spec(upstream).unwrap_or_else(|| panic!("{upstream:?}"));
let to = spec(served).unwrap_or_else(|| panic!("{served:?}"));
assert_ne!(from, to);
let target = to.strip_prefix("./").expect("relative target");
assert!(THREE_ASSETS.get(target).is_some(), "{target} is bundled");
assert_eq!(
upstream.starts_with("} "),
served.starts_with("} "),
"same statement shape"
);
}
}
}
#[test]
fn every_module_import_resolves_inside_the_bundle() {
let mut checked = 0;
for asset in THREE_ASSETS.iter() {
let path = asset.logical_path();
if !has_ext(path, "js") {
continue;
}
for spec in specifiers(text(path)) {
let target = spec
.strip_prefix("./")
.unwrap_or_else(|| panic!("{path}: bare or parent import {spec:?}"));
assert!(
THREE_ASSETS.get(target).is_some(),
"{path}: import {spec:?} is not bundled"
);
checked += 1;
}
}
assert!(checked >= 13, "the scan finds the imports ({checked})");
}
#[test]
fn urls_are_fingerprinted_under_the_plugin_mount() {
for asset in THREE_ASSETS.iter() {
let path = asset.logical_path();
assert_eq!(asset.plain_url(), format!("/static/_plugins/three/{path}"));
let (stem, ext) = path.rsplit_once('.').expect("extension");
let hash = asset
.url()
.strip_prefix(&format!("/static/_plugins/three/{stem}."))
.and_then(|rest| rest.strip_suffix(&format!(".{ext}")))
.expect("fingerprinted form");
assert_eq!(hash.len(), 8);
assert!(hash.bytes().all(|b| b.is_ascii_hexdigit()));
}
}
#[test]
fn module_url_is_the_plain_url_of_the_three_module() {
assert_eq!(
THREE_MODULE_URL,
THREE_ASSETS.get(MODULE_JS).expect("bundled").plain_url()
);
}
#[test]
fn content_types_match_the_files() {
for asset in THREE_ASSETS.iter() {
let expected = if has_ext(asset.logical_path(), "css") {
"text/css; charset=utf-8"
} else {
"text/javascript; charset=utf-8"
};
assert_eq!(asset.content_type(), expected, "{}", asset.logical_path());
}
}
#[test]
fn manifest_agrees_with_constants() {
let manifest = include_str!("../assets/manifest.json");
assert!(manifest.contains(&format!("\"version\": \"{THREE_VERSION}\"")));
for file in VENDORED {
assert!(manifest.contains(file.source), "{}", file.source);
assert!(
manifest.contains(file.upstream_integrity),
"{}",
file.upstream_integrity
);
}
}
#[test]
fn vendor_script_agrees_with_constants() {
let script = include_str!("../scripts/vendor.sh");
assert!(script.contains(&format!("VERSION=\"{THREE_VERSION}\"")));
for file in VENDORED {
let hash = file.upstream_integrity.trim_start_matches("sha384-");
assert!(script.contains(hash), "{} pin in vendor.sh", file.path);
}
}
}