riptc 0.1.7

Rust implementation of the InertiaJS protocol compatible with `riptc` for generating strong TypeScript bindings.
//! Collection of items, which is anything that can be turned into an swc / ts
//! module item with a namespaced path

//! All of the pre-written typescript utils / functions that contain the
//! internal logic of generated code, such as how to facilitate a `reload`.

use itertools::Itertools;
use rustc_span::Symbol;
use swc_atoms::Atom;
use swc_common::DUMMY_SP;
use swc_ecma_ast::Function;
use swc_ecma_ast::{ModuleDecl, ModuleItem, TsType};

use crate::analyzer::{AxumRoute, BridgeMarkedItem};
use crate::analyzer::{BridgeMarkedItemKind, InertiaProp};
use crate::callbacks::{ConfigStrExt, config};
use crate::namespace::NamespacedPath;
use crate::swc_utils;
use swc_ecma_ast::Expr;

pub const INERTIA_FLASH_PROPS_TYPE_NAME: &str = "FlashProps";
pub const INERTIA_SHARE_PROPS_TYPE_NAME: &str = "ShareProps";

pub const ROUTE_DEFINITIONS_TYPE_NAME: &str = "RouteDefinitions";

pub fn insert_bridge_marked_items<'a, 'tcx>(
    cg: &super::RiptCodegen<'tcx, '_>,
    items: impl Iterator<Item = &'a BridgeMarkedItem<'tcx>>,
) where
    'tcx: 'a,
{
    for item in items {
        match item.kind() {
            BridgeMarkedItemKind::Ty(ty) => {
                // we don't need to insert any node type, this will be tracked automatically
                cg.as_swc(*ty, item.span());
            }
        }
    }
}

/// We don't currently support full generation of non-inertia routes, but we still for now want
/// to scan the params + data types of these routes and generate them
pub fn insert_axum_route_types<'a, 'tcx>(
    cg: &super::RiptCodegen<'tcx, '_>,
    routes: impl Iterator<Item = &'a AxumRoute<'tcx>>,
) where
    'tcx: 'a,
{
    for route in routes {
        // let path = cg.analyzer.namespaced_path(route.id());

        // we just need to call `cg.as_swc` to register the type,
        // there's not actually a node to insert if we're not registering the route itself
        route.path_tys().for_each(|ty| {
            let _ = cg.as_swc(ty, route.span());
        });

        route.data_ty(&cg.analyzer).map(|ty| {
            let _ = cg.as_swc(ty, route.span());
        });
    }
}

pub fn insert_flash_props(cg: &super::RiptCodegen<'_, '_>) {
    let flash_props = cg.analyzer.inertia_flash_props();

    let adt = props_into_object_type(cg, flash_props);

    cg.insert_ts_type(
        NamespacedPath::from_mod_syntax(config().ript_preamble_namespace())
            .with_extra_segment(Symbol::intern(INERTIA_FLASH_PROPS_TYPE_NAME)),
        adt,
    );
}

pub fn insert_share_props(cg: &super::RiptCodegen<'_, '_>) {
    let share_props = cg.analyzer.inertia_share_props();

    let adt = props_into_object_type(cg, share_props);

    cg.insert_ts_type(
        NamespacedPath::from_mod_syntax(config().ript_preamble_namespace())
            .with_extra_segment(Symbol::intern(INERTIA_SHARE_PROPS_TYPE_NAME)),
        adt,
    );
}

pub fn insert_inertia_route_href_builders<'a, 'tcx>(
    cg: &super::RiptCodegen<'tcx, '_>,
    routes: impl Iterator<Item = &'a AxumRoute<'tcx>>,
) where
    'tcx: 'a,
{
    routes.for_each(|r| {
        let (tpl, params) = cg.axum_route_path_template_literal(r.id());
        let path = cg.analyzer.namespaced_path(r.id());

        let hfn = build_href_fn(r.route_path_symbol(), tpl.into(), params);

        let item = ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(swc_ecma_ast::ExportDecl {
            span: DUMMY_SP,
            decl: swc_ecma_ast::Decl::Fn(swc_ecma_ast::FnDecl {
                ident: swc_utils::ident(path.item_name().as_str().namespace_name()),
                declare: false,
                function: Box::new(hfn),
            }),
        }));

        cg.insert_node(path, item);
    })
}

pub fn insert_inertia_route_definitions_type<'a, 'tcx>(
    cg: &super::RiptCodegen<'tcx, '_>,
    routes: impl Iterator<Item = &'a AxumRoute<'tcx>>,
) where
    'tcx: 'a,
{
    let share_props: Vec<_> = cg.analyzer.inertia_share_props().map(|p| *p).collect();

    let routes_ts_defs = routes
        .map(|r| {
            let data_ty = r.data_ty(&cg.analyzer);
            let href = r.route_path_symbol();

            let props_by_component = cg
                .analyzer
                .inertia_props_for_axum_route(r.id())
                .chain(share_props.iter())
                .filter_map(|p| {
                    cg.analyzer
                        .component_for_inertia_prop(p.id())
                        .map(|c| (c, p))
                })
                .group_by(|(component, _)| *component);

            (r.http_method(), href, data_ty, props_by_component, r.span())
        })
        .map(|(method, href, data_ty, props_by_component, route_span)| {
            let method = swc_utils::lit_str_type(method.static_str());
            let href = swc_utils::lit_str_type(href.as_str());
            let data_ty = data_ty.map(|t| cg.as_swc(t, route_span));

            let props_by_component = props_by_component.into_iter().map(|(component, props)| {
                let props_obj = props_into_object_type(cg, props.map(|(_component, p)| p));

                swc_utils::object_member_type_element()
                    .key(component.as_str())
                    .type_ann(props_obj)
                    .optional(false)
                    .build()
            });

            let props_by_component = swc_utils::object_type_from_elements(props_by_component);

            (
                method,
                href,
                data_ty.unwrap_or_else(swc_utils::never_type),
                props_by_component,
            )
        })
        .map(|(method, href, data, props)| {
            vec![
                swc_utils::object_member_type_element()
                    .key("method")
                    .type_ann(method)
                    .optional(false)
                    .build(),
                swc_utils::object_member_type_element()
                    .key("href")
                    .type_ann(href)
                    .optional(false)
                    .build(),
                swc_utils::object_member_type_element()
                    .key("props")
                    .type_ann(props)
                    .optional(false)
                    .build(),
                swc_utils::object_member_type_element()
                    .key("data")
                    .type_ann(data)
                    .optional(false)
                    .build(),
            ]
        });

    let routes_ts_defs = swc_utils::union_type(
        routes_ts_defs.map(|els| swc_utils::object_type_from_elements(els.into_iter())),
    );

    let path = NamespacedPath::from_mod_syntax(config().ript_preamble_namespace())
        .with_extra_segment(Symbol::intern(ROUTE_DEFINITIONS_TYPE_NAME));

    cg.insert_ts_type(path, routes_ts_defs);
}

fn props_into_object_type<'cg, 'tcx>(
    cg: &super::RiptCodegen<'tcx, '_>,
    props: impl Iterator<Item = &'cg InertiaProp<'tcx>>,
) -> swc_ecma_ast::TsType
where
    'tcx: 'cg,
{
    swc_utils::object_type_from_elements(
        cg.inertia_prop_tys_flattened_into_unions(props.map(|p| p))
            .into_iter(),
    )
}

fn build_href_fn(
    raw_sym: Symbol,
    path: Expr,
    path_ty: impl Iterator<Item = (Atom, TsType)>,
) -> Function {
    let params = path_ty
        .map(|(k, v)| {
            (
                k.clone(),
                swc_utils::object_member_type_element()
                    .key(k)
                    .type_ann(v)
                    .optional(false)
                    .build(),
            )
        })
        .collect_vec();

    let return_stmt = swc_utils::object_expr_from_props(
        vec![
            swc_utils::object_prop()
                .key("hrefTemplate")
                .inner(swc_utils::lit_str_as_const(raw_sym.as_str()).into())
                .call(),
            swc_utils::object_prop().key("href").inner(path).call(),
        ]
        .into_iter(),
    );

    swc_utils::binding_fn()
        .params(if params.is_empty() {
            vec![]
        } else {
            vec![swc_utils::destructuring_param_from_elements(
                params.into_iter(),
            )]
        })
        .stmts(vec![swc_utils::return_stmt(return_stmt)])
        .build()
}