refactor: embed generated frontend asset directory
This commit is contained in:
@@ -2,8 +2,53 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn generate_asset_manifest() {
|
||||||
|
let manifest_dir = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir"));
|
||||||
|
let generated_dir = manifest_dir.join("assets/web/generated");
|
||||||
|
println!("cargo:rerun-if-changed={}", generated_dir.display());
|
||||||
|
|
||||||
|
let mut filenames = fs::read_dir(&generated_dir)
|
||||||
|
.expect("read generated frontend assets")
|
||||||
|
.map(|entry| entry.expect("read generated asset entry"))
|
||||||
|
.filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_file()))
|
||||||
|
.map(|entry| {
|
||||||
|
entry
|
||||||
|
.file_name()
|
||||||
|
.into_string()
|
||||||
|
.expect("UTF-8 asset filename")
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
filenames.sort();
|
||||||
|
|
||||||
|
let mut output = String::from(
|
||||||
|
"// Generated by build.rs from committed frontend output.\n\
|
||||||
|
pub const GENERATED_ASSETS: &[(&str, &[u8])] = &[\n",
|
||||||
|
);
|
||||||
|
for filename in filenames {
|
||||||
|
assert!(
|
||||||
|
filename
|
||||||
|
.bytes()
|
||||||
|
.all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte)),
|
||||||
|
"unsupported generated asset filename: {filename}"
|
||||||
|
);
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
" ({filename:?}, include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/assets/web/generated/\", {filename:?}))),"
|
||||||
|
)
|
||||||
|
.expect("write asset manifest");
|
||||||
|
}
|
||||||
|
output.push_str("];\n");
|
||||||
|
|
||||||
|
let output_path =
|
||||||
|
PathBuf::from(std::env::var_os("OUT_DIR").expect("out dir")).join("generated_assets.rs");
|
||||||
|
fs::write(output_path, output).expect("write generated asset manifest");
|
||||||
|
}
|
||||||
|
|
||||||
fn utc_ymd_from_unix_secs(secs: i64) -> (i32, u32, u32) {
|
fn utc_ymd_from_unix_secs(secs: i64) -> (i32, u32, u32) {
|
||||||
let days = secs.div_euclid(86_400);
|
let days = secs.div_euclid(86_400);
|
||||||
let z = days + 719_468;
|
let z = days + 719_468;
|
||||||
@@ -20,6 +65,7 @@ fn utc_ymd_from_unix_secs(secs: i64) -> (i32, u32, u32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
generate_asset_manifest();
|
||||||
let secs = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
let secs = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||||
Ok(d) => d.as_secs() as i64,
|
Ok(d) => d.as_secs() as i64,
|
||||||
Err(_) => 0,
|
Err(_) => 0,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
use actix_web::http::header;
|
use actix_web::http::header;
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use actix_web::{get, HttpRequest, HttpResponse, Responder};
|
use actix_web::{get, HttpRequest, HttpResponse, Responder};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use super::{gz_cache_entry, static_asset_response, GzCacheEntry, FAVICON_BYTES, LOGO_BYTES};
|
use super::{gz_cache_entry, static_asset_response, GzCacheEntry, FAVICON_BYTES, LOGO_BYTES};
|
||||||
@@ -28,61 +29,18 @@ macro_rules! define_gz_cache {
|
|||||||
define_gz_cache!(gz_index_html, status::index_html(), "index.html");
|
define_gz_cache!(gz_index_html, status::index_html(), "index.html");
|
||||||
define_gz_cache!(gz_style_css, status::STYLE_CSS, "style.css");
|
define_gz_cache!(gz_style_css, status::STYLE_CSS, "style.css");
|
||||||
define_gz_cache!(gz_themes_css, status::THEMES_CSS, "themes.css");
|
define_gz_cache!(gz_themes_css, status::THEMES_CSS, "themes.css");
|
||||||
define_gz_cache!(gz_app_js, status::APP_JS, "app.js");
|
|
||||||
define_gz_cache!(gz_ui_core_js, status::UI_CORE_JS, "ui-core.js");
|
|
||||||
define_gz_cache!(gz_map_core_js, status::MAP_CORE_JS, "map-core.js");
|
|
||||||
define_gz_cache!(
|
|
||||||
gz_plugin_loader_js,
|
|
||||||
status::PLUGIN_LOADER_JS,
|
|
||||||
"plugin-loader.js"
|
|
||||||
);
|
|
||||||
define_gz_cache!(
|
|
||||||
gz_plugin_runtime_js,
|
|
||||||
status::PLUGIN_RUNTIME_JS,
|
|
||||||
"plugin-runtime.js"
|
|
||||||
);
|
|
||||||
define_gz_cache!(gz_screenshot_js, status::SCREENSHOT_JS, "screenshot.js");
|
|
||||||
define_gz_cache!(
|
|
||||||
gz_decode_history_worker_js,
|
|
||||||
status::DECODE_HISTORY_WORKER_JS,
|
|
||||||
"decode-history-worker.js"
|
|
||||||
);
|
|
||||||
define_gz_cache!(
|
|
||||||
gz_webgl_renderer_js,
|
|
||||||
status::WEBGL_RENDERER_JS,
|
|
||||||
"webgl-renderer.js"
|
|
||||||
);
|
|
||||||
define_gz_cache!(
|
|
||||||
gz_leaflet_ais_tracksymbol_js,
|
|
||||||
status::LEAFLET_AIS_TRACKSYMBOL_JS,
|
|
||||||
"leaflet-ais-tracksymbol.js"
|
|
||||||
);
|
|
||||||
define_gz_cache!(gz_ais_js, status::AIS_JS, "ais.js");
|
|
||||||
define_gz_cache!(gz_vdes_js, status::VDES_JS, "vdes.js");
|
|
||||||
define_gz_cache!(gz_aprs_js, status::APRS_JS, "aprs.js");
|
|
||||||
define_gz_cache!(gz_hf_aprs_js, status::HF_APRS_JS, "hf-aprs.js");
|
|
||||||
define_gz_cache!(gz_ft8_js, status::FT8_JS, "ft8.js");
|
|
||||||
define_gz_cache!(gz_ft4_js, status::FT4_JS, "ft4.js");
|
|
||||||
define_gz_cache!(gz_ft2_js, status::FT2_JS, "ft2.js");
|
|
||||||
define_gz_cache!(gz_wspr_js, status::WSPR_JS, "wspr.js");
|
|
||||||
define_gz_cache!(gz_cw_js, status::CW_JS, "cw.js");
|
|
||||||
define_gz_cache!(gz_sat_js, status::SAT_JS, "sat.js");
|
|
||||||
define_gz_cache!(gz_wefax_js, status::WEFAX_JS, "wefax.js");
|
|
||||||
define_gz_cache!(gz_bookmarks_js, status::BOOKMARKS_JS, "bookmarks.js");
|
|
||||||
define_gz_cache!(gz_scheduler_js, status::SCHEDULER_JS, "scheduler.js");
|
|
||||||
define_gz_cache!(
|
|
||||||
gz_sat_scheduler_js,
|
|
||||||
status::SAT_SCHEDULER_JS,
|
|
||||||
"sat-scheduler.js"
|
|
||||||
);
|
|
||||||
define_gz_cache!(
|
|
||||||
gz_background_decode_js,
|
|
||||||
status::BACKGROUND_DECODE_JS,
|
|
||||||
"background-decode.js"
|
|
||||||
);
|
|
||||||
define_gz_cache!(gz_vchan_js, status::VCHAN_JS, "vchan.js");
|
|
||||||
define_gz_cache!(gz_bandplan_json, status::BANDPLAN_JSON, "bandplan.json");
|
define_gz_cache!(gz_bandplan_json, status::BANDPLAN_JSON, "bandplan.json");
|
||||||
|
|
||||||
|
fn generated_asset_cache() -> &'static HashMap<&'static str, GzCacheEntry> {
|
||||||
|
static CACHE: OnceLock<HashMap<&'static str, GzCacheEntry>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(|| {
|
||||||
|
status::GENERATED_ASSETS
|
||||||
|
.iter()
|
||||||
|
.map(|(name, bytes)| (*name, gz_cache_entry(bytes, name)))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Vendored DSEG14 Classic font
|
// Vendored DSEG14 Classic font
|
||||||
// (binary woff2 — served directly, not through gz_cache)
|
// (binary woff2 — served directly, not through gz_cache)
|
||||||
|
|
||||||
@@ -181,128 +139,28 @@ pub(crate) async fn themes_css(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(&req, "text/css; charset=utf-8", c)
|
static_asset_response(&req, "text/css; charset=utf-8", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// Generated filenames are supplied only by build.rs and resolved through this
|
||||||
// JavaScript assets
|
// allowlist. Unknown names and unsupported MIME types never reach the file system.
|
||||||
// ---------------------------------------------------------------------------
|
fn generated_content_type(filename: &str) -> Option<&'static str> {
|
||||||
|
match filename.rsplit_once('.').map(|(_, extension)| extension) {
|
||||||
#[get("/app.js")]
|
Some("js") => Some("application/javascript; charset=utf-8"),
|
||||||
pub(crate) async fn app_js(req: HttpRequest) -> impl Responder {
|
Some("css") => Some("text/css; charset=utf-8"),
|
||||||
let c = gz_app_js();
|
Some("json" | "map") => Some("application/json; charset=utf-8"),
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
Some("wasm") => Some("application/wasm"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ui-core.js")]
|
#[get("/{filename}")]
|
||||||
pub(crate) async fn ui_core_js(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn generated_asset(req: HttpRequest, path: web::Path<String>) -> impl Responder {
|
||||||
let c = gz_ui_core_js();
|
let filename = path.into_inner();
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
let Some(content_type) = generated_content_type(&filename) else {
|
||||||
}
|
return HttpResponse::NotFound().finish();
|
||||||
|
};
|
||||||
#[get("/map-core.js")]
|
let Some(entry) = generated_asset_cache().get(filename.as_str()) else {
|
||||||
pub(crate) async fn map_core_js(req: HttpRequest) -> impl Responder {
|
return HttpResponse::NotFound().finish();
|
||||||
let c = gz_map_core_js();
|
};
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
static_asset_response(&req, content_type, entry)
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/plugin-loader.js")]
|
|
||||||
pub(crate) async fn plugin_loader_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_plugin_loader_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/plugin-runtime.js")]
|
|
||||||
pub(crate) async fn plugin_runtime_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_plugin_runtime_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/screenshot.js")]
|
|
||||||
pub(crate) async fn screenshot_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_screenshot_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/decode-history-worker.js")]
|
|
||||||
pub(crate) async fn decode_history_worker_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_decode_history_worker_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/webgl-renderer.js")]
|
|
||||||
pub(crate) async fn webgl_renderer_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_webgl_renderer_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/leaflet-ais-tracksymbol.js")]
|
|
||||||
pub(crate) async fn leaflet_ais_tracksymbol_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_leaflet_ais_tracksymbol_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/aprs.js")]
|
|
||||||
pub(crate) async fn aprs_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_aprs_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/hf-aprs.js")]
|
|
||||||
pub(crate) async fn hf_aprs_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_hf_aprs_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/ais.js")]
|
|
||||||
pub(crate) async fn ais_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_ais_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/vdes.js")]
|
|
||||||
pub(crate) async fn vdes_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_vdes_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/ft8.js")]
|
|
||||||
pub(crate) async fn ft8_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_ft8_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/ft4.js")]
|
|
||||||
pub(crate) async fn ft4_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_ft4_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/ft2.js")]
|
|
||||||
pub(crate) async fn ft2_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_ft2_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/wspr.js")]
|
|
||||||
pub(crate) async fn wspr_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_wspr_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/cw.js")]
|
|
||||||
pub(crate) async fn cw_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_cw_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/sat.js")]
|
|
||||||
pub(crate) async fn sat_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_sat_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/wefax.js")]
|
|
||||||
pub(crate) async fn wefax_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_wefax_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/images/{filename}")]
|
#[get("/images/{filename}")]
|
||||||
@@ -329,36 +187,6 @@ pub(crate) async fn wefax_image(path: web::Path<String>) -> impl Responder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/bookmarks.js")]
|
|
||||||
pub(crate) async fn bookmarks_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_bookmarks_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/scheduler.js")]
|
|
||||||
pub(crate) async fn scheduler_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_scheduler_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/sat-scheduler.js")]
|
|
||||||
pub(crate) async fn sat_scheduler_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_sat_scheduler_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/background-decode.js")]
|
|
||||||
pub(crate) async fn background_decode_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_background_decode_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/vchan.js")]
|
|
||||||
pub(crate) async fn vchan_js(req: HttpRequest) -> impl Responder {
|
|
||||||
let c = gz_vchan_js();
|
|
||||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/bandplan.json")]
|
#[get("/bandplan.json")]
|
||||||
pub(crate) async fn bandplan_json(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn bandplan_json(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_bandplan_json();
|
let c = gz_bandplan_json();
|
||||||
@@ -438,3 +266,33 @@ pub(crate) async fn leaflet_layers_2x() -> impl Responder {
|
|||||||
.insert_header((header::CACHE_CONTROL, "public, max-age=604800, immutable"))
|
.insert_header((header::CACHE_CONTROL, "public, max-age=604800, immutable"))
|
||||||
.body(status::LEAFLET_LAYERS_2X)
|
.body(status::LEAFLET_LAYERS_2X)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_asset_manifest_is_an_explicit_safe_allowlist() {
|
||||||
|
assert!(!status::GENERATED_ASSETS.is_empty());
|
||||||
|
for (name, bytes) in status::GENERATED_ASSETS {
|
||||||
|
assert!(!bytes.is_empty(), "generated asset {name} is empty");
|
||||||
|
assert!(!name.contains('/') && !name.contains('\\') && !name.contains(".."));
|
||||||
|
assert!(generated_content_type(name).is_some());
|
||||||
|
}
|
||||||
|
assert!(generated_asset_cache().contains_key("app.js"));
|
||||||
|
assert!(!generated_asset_cache().contains_key("../app.js"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_asset_mime_types_are_restricted() {
|
||||||
|
assert_eq!(
|
||||||
|
generated_content_type("chunk.js"),
|
||||||
|
Some("application/javascript; charset=utf-8")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
generated_content_type("chunk.wasm"),
|
||||||
|
Some("application/wasm")
|
||||||
|
);
|
||||||
|
assert_eq!(generated_content_type("secret.txt"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -642,32 +642,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(assets::logo)
|
.service(assets::logo)
|
||||||
.service(assets::style_css)
|
.service(assets::style_css)
|
||||||
.service(assets::themes_css)
|
.service(assets::themes_css)
|
||||||
.service(assets::app_js)
|
|
||||||
.service(assets::ui_core_js)
|
|
||||||
.service(assets::map_core_js)
|
|
||||||
.service(assets::plugin_loader_js)
|
|
||||||
.service(assets::plugin_runtime_js)
|
|
||||||
.service(assets::screenshot_js)
|
|
||||||
.service(assets::decode_history_worker_js)
|
|
||||||
.service(assets::webgl_renderer_js)
|
|
||||||
.service(assets::leaflet_ais_tracksymbol_js)
|
|
||||||
.service(assets::ais_js)
|
|
||||||
.service(assets::vdes_js)
|
|
||||||
.service(assets::aprs_js)
|
|
||||||
.service(assets::hf_aprs_js)
|
|
||||||
.service(assets::ft8_js)
|
|
||||||
.service(assets::ft4_js)
|
|
||||||
.service(assets::ft2_js)
|
|
||||||
.service(assets::wspr_js)
|
|
||||||
.service(assets::cw_js)
|
|
||||||
.service(assets::sat_js)
|
|
||||||
.service(assets::wefax_js)
|
|
||||||
.service(assets::wefax_image)
|
.service(assets::wefax_image)
|
||||||
.service(assets::bookmarks_js)
|
|
||||||
.service(assets::scheduler_js)
|
|
||||||
.service(assets::sat_scheduler_js)
|
|
||||||
.service(assets::background_decode_js)
|
|
||||||
.service(assets::vchan_js)
|
|
||||||
.service(assets::bandplan_json)
|
.service(assets::bandplan_json)
|
||||||
// Vendored DSEG14 Classic font
|
// Vendored DSEG14 Classic font
|
||||||
.service(assets::dseg14_classic_woff2)
|
.service(assets::dseg14_classic_woff2)
|
||||||
@@ -681,6 +656,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(assets::leaflet_marker_shadow)
|
.service(assets::leaflet_marker_shadow)
|
||||||
.service(assets::leaflet_layers)
|
.service(assets::leaflet_layers)
|
||||||
.service(assets::leaflet_layers_2x)
|
.service(assets::leaflet_layers_2x)
|
||||||
|
.service(assets::generated_asset)
|
||||||
// Virtual channels
|
// Virtual channels
|
||||||
.service(vchan::list_channels)
|
.service(vchan::list_channels)
|
||||||
.service(vchan::allocate_channel)
|
.service(vchan::allocate_channel)
|
||||||
|
|||||||
@@ -11,35 +11,16 @@ const CLIENT_BUILD_DATE: &str = env!("TRX_CLIENT_BUILD_DATE");
|
|||||||
const INDEX_HTML: &str = include_str!("../assets/web/index.html");
|
const INDEX_HTML: &str = include_str!("../assets/web/index.html");
|
||||||
pub const STYLE_CSS: &str = include_str!("../assets/web/style.css");
|
pub const STYLE_CSS: &str = include_str!("../assets/web/style.css");
|
||||||
pub const THEMES_CSS: &str = include_str!("../assets/web/themes.css");
|
pub const THEMES_CSS: &str = include_str!("../assets/web/themes.css");
|
||||||
pub const APP_JS: &str = include_str!("../assets/web/generated/app.js");
|
|
||||||
pub const UI_CORE_JS: &str = include_str!("../assets/web/generated/ui-core.js");
|
|
||||||
pub const MAP_CORE_JS: &str = include_str!("../assets/web/generated/map-core.js");
|
|
||||||
pub const PLUGIN_LOADER_JS: &str = include_str!("../assets/web/generated/plugin-loader.js");
|
|
||||||
pub const PLUGIN_RUNTIME_JS: &str = include_str!("../assets/web/generated/plugin-runtime.js");
|
|
||||||
pub const SCREENSHOT_JS: &str = include_str!("../assets/web/generated/screenshot.js");
|
|
||||||
pub const DECODE_HISTORY_WORKER_JS: &str =
|
|
||||||
include_str!("../assets/web/generated/decode-history-worker.js");
|
|
||||||
pub const WEBGL_RENDERER_JS: &str = include_str!("../assets/web/generated/webgl-renderer.js");
|
|
||||||
pub const LEAFLET_AIS_TRACKSYMBOL_JS: &str =
|
|
||||||
include_str!("../assets/web/generated/leaflet-ais-tracksymbol.js");
|
|
||||||
pub const AIS_JS: &str = include_str!("../assets/web/generated/ais.js");
|
|
||||||
pub const VDES_JS: &str = include_str!("../assets/web/generated/vdes.js");
|
|
||||||
pub const APRS_JS: &str = include_str!("../assets/web/generated/aprs.js");
|
|
||||||
pub const HF_APRS_JS: &str = include_str!("../assets/web/generated/hf-aprs.js");
|
|
||||||
pub const FT8_JS: &str = include_str!("../assets/web/generated/ft8.js");
|
|
||||||
pub const FT4_JS: &str = include_str!("../assets/web/generated/ft4.js");
|
|
||||||
pub const FT2_JS: &str = include_str!("../assets/web/generated/ft2.js");
|
|
||||||
pub const WSPR_JS: &str = include_str!("../assets/web/generated/wspr.js");
|
|
||||||
pub const CW_JS: &str = include_str!("../assets/web/generated/cw.js");
|
|
||||||
pub const SAT_JS: &str = include_str!("../assets/web/generated/sat.js");
|
|
||||||
pub const WEFAX_JS: &str = include_str!("../assets/web/generated/wefax.js");
|
|
||||||
pub const BOOKMARKS_JS: &str = include_str!("../assets/web/generated/bookmarks.js");
|
|
||||||
pub const SCHEDULER_JS: &str = include_str!("../assets/web/generated/scheduler.js");
|
|
||||||
pub const SAT_SCHEDULER_JS: &str = include_str!("../assets/web/generated/sat-scheduler.js");
|
|
||||||
pub const BACKGROUND_DECODE_JS: &str = include_str!("../assets/web/generated/background-decode.js");
|
|
||||||
pub const VCHAN_JS: &str = include_str!("../assets/web/generated/vchan.js");
|
|
||||||
pub const BANDPLAN_JSON: &str = include_str!("../assets/web/bandplan.json");
|
pub const BANDPLAN_JSON: &str = include_str!("../assets/web/bandplan.json");
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/generated_assets.rs"));
|
||||||
|
|
||||||
|
pub fn generated_asset(name: &str) -> Option<&'static [u8]> {
|
||||||
|
GENERATED_ASSETS
|
||||||
|
.iter()
|
||||||
|
.find_map(|(candidate, bytes)| (*candidate == name).then_some(*bytes))
|
||||||
|
}
|
||||||
|
|
||||||
// Vendored DSEG14 Classic font
|
// Vendored DSEG14 Classic font
|
||||||
pub const DSEG14_CLASSIC_WOFF2: &[u8] =
|
pub const DSEG14_CLASSIC_WOFF2: &[u8] =
|
||||||
include_bytes!("../assets/web/vendor/dseg14-classic-latin-400-normal.woff2");
|
include_bytes!("../assets/web/vendor/dseg14-classic-latin-400-normal.woff2");
|
||||||
@@ -90,7 +71,9 @@ mod tests {
|
|||||||
ui_core < app,
|
ui_core < app,
|
||||||
"UI primitives must load before application code"
|
"UI primitives must load before application code"
|
||||||
);
|
);
|
||||||
assert!(UI_CORE_JS.contains("trxUi"));
|
assert!(generated_asset("ui-core.js").is_some_and(|asset| {
|
||||||
|
std::str::from_utf8(asset).is_ok_and(|script| script.contains("trxUi"))
|
||||||
|
}));
|
||||||
let plugin_loader = html
|
let plugin_loader = html
|
||||||
.find("/plugin-loader.js")
|
.find("/plugin-loader.js")
|
||||||
.expect("typed plugin loader is loaded");
|
.expect("typed plugin loader is loaded");
|
||||||
@@ -102,8 +85,12 @@ mod tests {
|
|||||||
plugin_runtime < plugin_loader,
|
plugin_runtime < plugin_loader,
|
||||||
"plugin runtime must load before the lazy loader"
|
"plugin runtime must load before the lazy loader"
|
||||||
);
|
);
|
||||||
assert!(PLUGIN_RUNTIME_JS.contains("registerDecoder"));
|
assert!(generated_asset("plugin-runtime.js").is_some_and(|asset| {
|
||||||
assert!(PLUGIN_LOADER_JS.contains("import(path)"));
|
std::str::from_utf8(asset).is_ok_and(|script| script.contains("registerDecoder"))
|
||||||
|
}));
|
||||||
|
assert!(generated_asset("plugin-loader.js").is_some_and(|asset| {
|
||||||
|
std::str::from_utf8(asset).is_ok_and(|script| script.contains("import(path)"))
|
||||||
|
}));
|
||||||
assert!(html.contains("<summary>Scheduler controls</summary>"));
|
assert!(html.contains("<summary>Scheduler controls</summary>"));
|
||||||
assert!(html.contains("<summary>Audio controls</summary>"));
|
assert!(html.contains("<summary>Audio controls</summary>"));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user