bevy_react_core 0.7.0

The core bridge of bevy-react (drive bevy_ui from React over an embedded V8 runtime). Apps depend on the `bevy-react` crate.
//! `BevyStyle`'s TypeScript, generated from the style registry: the core
//! properties ([`CORE_STYLES`](super::props::CORE_STYLES)) into the
//! `bevy-react` package's committed `js/src/generated/style.ts` (by the
//! `core_style_ts_is_current` test), an app's own properties into its
//! `bevy.ts` as a `declare module "bevy-react"` augmentation
//! ([`render_app_style_augmentation`]).

use std::collections::BTreeMap;
use std::fmt::Write as _;

use super::AnyStyleProperty;
use crate::ts_codegen::json_key;

/// The `bevy.ts` block augmenting `BevyStyle` with an app's own properties
/// (sorted by name), their value types' declarations added to `decls`.
/// Empty when the app registered none.
pub(crate) fn render_app_style_augmentation<'a>(
    properties: impl IntoIterator<Item = &'a dyn AnyStyleProperty>,
    decls: &mut BTreeMap<String, String>,
) -> String {
    let mut rows: Vec<(&str, String)> = properties
        .into_iter()
        .map(|p| {
            p.ts_decls(decls);
            (p.name(), p.ts_type())
        })
        .collect();
    if rows.is_empty() {
        return String::new();
    }
    rows.sort();
    let mut out = String::from(
        "/** The app's own style properties (`app.add_react_style`). Augments the\n\
         \x20*  `BevyStyle` interface in the `bevy-react` package, so `style` (and the\n\
         \x20*  hover/press/focus variants) type them. */\n\
         declare module \"bevy-react\" {\n\
         \x20 interface BevyStyle {\n",
    );
    for (name, ts) in &rows {
        writeln!(out, "    {}?: {ts};", json_key(name)).unwrap();
    }
    out.push_str("  }\n}\n\n");
    out
}

/// The core generator: it runs from its test (`npm run style:generate`
/// rewrites the committed file), never at runtime.
#[cfg(test)]
mod tests {
    use std::collections::BTreeSet;

    use super::*;
    use crate::ts_codegen::type_idents;

    /// The package module (relative to `js/src/generated/`) each type the core
    /// codecs name is imported from; every other named type lives in `jsx.d.ts`.
    const TYPE_MODULES: &[(&str, &str)] = &[
        ("Animatable", "../animated"),
        ("FilterChainValue", "../filters"),
        ("MorphFilterValue", "../filters"),
    ];
    const DEFAULT_TYPE_MODULE: &str = "../jsx";

    /// The committed core `BevyStyle`, relative to the workspace root.
    pub(crate) const CORE_STYLE_TS_PATH: &str = "js/src/generated/style.ts";

    /// The `bevy-react` package's `BevyStyle` interface: one optional field per
    /// core property, in declaration order, with the imports its types need.
    pub(crate) fn render_core_style_ts(properties: &[&dyn AnyStyleProperty]) -> String {
        let mut decls = BTreeMap::new();
        let rows: Vec<(&str, String)> = properties
            .iter()
            .map(|p| {
                p.ts_decls(&mut decls);
                (p.name(), p.ts_type())
            })
            .collect();

        let mut imports: BTreeMap<&str, BTreeSet<String>> = BTreeMap::new();
        for (_, ts) in &rows {
            for ident in type_idents(ts) {
                if decls.contains_key(&ident) {
                    continue;
                }
                let module = TYPE_MODULES
                    .iter()
                    .find(|(name, _)| *name == ident)
                    .map_or(DEFAULT_TYPE_MODULE, |(_, module)| module);
                imports.entry(module).or_default().insert(ident);
            }
        }

        let mut out = String::from(
            "// @generated by bevy-react from the core style registry (`CORE_STYLES`) —\n\
             // do not edit by hand. Regenerate with `npm run style:generate`.\n\n",
        );
        for (module, names) in &imports {
            let names: Vec<&str> = names.iter().map(String::as_str).collect();
            writeln!(
                out,
                "import type {{ {} }} from {module:?};",
                names.join(", ")
            )
            .unwrap();
        }
        for decl in decls.values() {
            write!(out, "\nexport {decl}\n").unwrap();
        }
        out.push_str(
            "\n/** A CSS-like style object mapped onto `bevy_ui::Node` and its sibling\n\
             \x20*  visual components. Every field is optional; unset fields keep Bevy's\n\
             \x20*  defaults. An app's own style properties (`app.add_react_style`) augment\n\
             \x20*  this interface from its generated `bevy.ts`. */\n\
             export interface BevyStyle {\n",
        );
        for (name, ts) in &rows {
            writeln!(out, "  {}?: {ts};", json_key(name)).unwrap();
        }
        out.push_str("}\n");
        out
    }

    /// The committed core `BevyStyle` matches the registry. Regenerate with
    /// `UPDATE_STYLE_TS=1` (`npm run style:generate`).
    #[test]
    fn core_style_ts_is_current() {
        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../..")
            .join(CORE_STYLE_TS_PATH);
        let rendered = render_core_style_ts(super::super::props::CORE_STYLES);
        if std::env::var_os("UPDATE_STYLE_TS").is_some() {
            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
            std::fs::write(&path, &rendered).unwrap();
            return;
        }
        let committed = std::fs::read_to_string(&path).unwrap_or_default();
        assert!(
            committed == rendered,
            "{CORE_STYLE_TS_PATH} is stale — regenerate with `npm run style:generate`"
        );
    }

    #[test]
    fn type_idents_skip_literals_and_keys() {
        assert_eq!(
            type_idents(r#""flexStart" | Animatable<Length> | { color?: Color; x: number }"#),
            ["Animatable", "Length", "Color"]
        );
    }
}