Dev loop: debug-only page scripts from disk + tab rebuild on change (waybetter-host devloop, page_script!), generic devmode watcher, waybetter dev, buildRuntime watch, template scripts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CRTkafa 2026-09-30 22:20:22 +07:00
parent 9d9f0ab10a
commit e84470a95d
11 changed files with 377 additions and 26 deletions

View file

@ -24,6 +24,8 @@
//! shows, its page color and delivery script) over a site's built-in themes.
//! - [`workspaces`]: saved tab/pane/deck layouts, bounded and well-formed (never evaluated).
//! - [`devmode`]: developer mode's debounced watcher of plugins, custom CSS and a theme file.
//! - `devloop` (debug builds with `app` only): page scripts read from disk and every tab rebuilt when
//! one changes; an app names its files with [`page_script!`].
//! - [`settings`]: the shell's settings (home page, where links open, session restore) and the page's
//! feature flags, as validated data and the scripts that hand them to site tabs.
//! - [`modes`]: the onboarding mode the owner started from (the presets are the app's feature table).
@ -74,7 +76,55 @@ pub mod zoom;
#[cfg(feature = "app")]
pub mod app;
#[cfg(all(feature = "app", debug_assertions))]
pub mod devloop;
#[cfg(feature = "tauri")]
pub mod host;
#[cfg(all(feature = "tauri", feature = "net"))]
pub mod updater;
/// An app's page script for `SiteHooks::init_scripts`: `$embedded` (an `include_str!`) in every
/// build, except that a debug build reads `$file` (relative to the app crate's manifest directory)
/// from disk each time and falls back to the embedded copy when it is missing. The files named
/// this way are watched: a change rebuilds every tab (`devloop`). In a release build the disk arm
/// is not compiled at all: the expansion is `$embedded.to_owned()`, `$file` appears nowhere.
#[macro_export]
macro_rules! page_script {
($embedded:expr, $file:literal) => {{
#[cfg(debug_assertions)]
{
$crate::devloop::from_disk(concat!(env!("CARGO_MANIFEST_DIR"), "/", $file), $embedded)
}
#[cfg(not(debug_assertions))]
{
($embedded).to_owned()
}
}};
}
#[cfg(test)]
mod release_guard {
/// Release builds must not contain the dev loop: every mention of it in this crate is behind
/// `cfg(debug_assertions)` (checked on the source, so it also holds when only debug is built).
#[test]
fn every_devloop_mention_is_debug_only() {
for (file, source) in [("lib.rs", include_str!("lib.rs")), ("app.rs", include_str!("app.rs"))] {
let lines: Vec<&str> = source.lines().collect();
for (index, line) in lines.iter().enumerate() {
let code = line.trim_start();
if code.starts_with("//") || !(code.contains("devloop::") || code.starts_with("pub mod devloop")) || code.contains("$crate::devloop") {
continue;
}
let gated = (index.saturating_sub(2)..index).any(|before| lines[before].contains("debug_assertions"));
assert!(gated, "{file}:{}: devloop used outside cfg(debug_assertions)", index + 1);
}
}
}
/// `cargo test --release`: the macro is just the embedded text.
#[cfg(not(debug_assertions))]
#[test]
fn release_page_script_is_the_embedded_copy() {
assert_eq!(crate::page_script!("embedded", "no/such/file.js"), "embedded");
}
}