qwik-core 0.1.0

Qwik optimizer compiler
use crate::code_move::fix_path;
use crate::collector::HookCollect;
use crate::entry_strategy::EntryPolicy;
use crate::parse::PathData;

use ast::*;
use std::collections::HashSet;
use std::vec;
use swc_atoms::JsWord;
use swc_common::comments::{Comments, SingleThreadedComments};
use swc_common::{sync::Lrc, SourceMap, DUMMY_SP};
use swc_ecmascript::ast;
use swc_ecmascript::ast::{ExportDecl, Expr, Ident, VarDeclarator};
use swc_ecmascript::visit::{noop_fold_type, Fold, FoldWith};

#[derive(Debug)]
pub struct Hook {
    pub entry: Option<String>,
    pub canonical_filename: String,
    pub name: String,
    pub module_index: usize,
    pub expr: CallExpr,
    pub local_decl: Vec<JsWord>,
    pub local_idents: Vec<JsWord>,
    pub origin: String,
}

pub struct TransformContext {
    pub source_map: Lrc<SourceMap>,
    pub hooks_names: HashSet<String>,
    pub bundling_policy: Box<dyn EntryPolicy>,
}

impl TransformContext {
    pub fn new(bundling_policy: Box<dyn EntryPolicy>) -> Self {
        Self {
            hooks_names: HashSet::with_capacity(10),
            source_map: Lrc::new(SourceMap::default()),
            bundling_policy,
        }
    }
}

pub struct HookTransform<'a> {
    stack_ctxt: Vec<String>,
    module_item: usize,

    root_sym: Option<String>,
    context: &'a mut TransformContext,
    hooks: &'a mut Vec<Hook>,

    path: &'a PathData,

    comments: Option<&'a SingleThreadedComments>,
}

impl<'a> HookTransform<'a> {
    pub fn new(
        context: &'a mut TransformContext,
        path: &'a PathData,
        comments: Option<&'a SingleThreadedComments>,
        hooks: &'a mut Vec<Hook>,
    ) -> Self {
        HookTransform {
            path,
            stack_ctxt: vec![],
            hooks,
            module_item: 0,
            root_sym: None,
            comments,
            context,
        }
    }

    fn get_context_name(&self) -> String {
        let mut ctx = self.stack_ctxt.join("_");
        if self.stack_ctxt.is_empty() {
            ctx += "_h";
        }
        ctx = escape_sym(&ctx);
        if self.context.hooks_names.contains(&ctx) {
            ctx += &self.hooks.len().to_string();
        }
        ctx
    }

    fn handle_var_decl(&mut self, node: VarDecl) -> VarDecl {
        let mut newdecls = vec![];
        for decl in node.decls {
            match decl.name {
                Pat::Ident(ref ident) => {
                    self.root_sym = Some(ident.id.to_string());
                }
                _ => {
                    self.root_sym = None;
                }
            }
            newdecls.push(decl.fold_with(self));
        }
        VarDecl {
            span: DUMMY_SP,
            kind: node.kind,
            decls: newdecls,
            declare: node.declare,
        }
    }
}

impl<'a> Fold for HookTransform<'a> {
    noop_fold_type!();

    fn fold_module(&mut self, node: Module) -> Module {
        let o = node.fold_children_with(self);
        self.hooks
            .sort_by(|a, b| b.module_index.cmp(&a.module_index));
        o
    }

    fn fold_module_item(&mut self, item: ModuleItem) -> ModuleItem {
        let item = match item {
            ModuleItem::Stmt(Stmt::Decl(Decl::Var(node))) => {
                let transformed = self.handle_var_decl(node);
                ModuleItem::Stmt(Stmt::Decl(Decl::Var(transformed)))
            }
            ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(node)) => match node.decl {
                Decl::Var(var) => {
                    let transformed = self.handle_var_decl(var);
                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
                        span: DUMMY_SP,
                        decl: Decl::Var(transformed),
                    }))
                }
                Decl::Class(class) => {
                    self.root_sym = Some(class.ident.sym.to_string());
                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
                        span: DUMMY_SP,
                        decl: Decl::Class(class.fold_with(self)),
                    }))
                }
                Decl::Fn(function) => {
                    self.root_sym = Some(function.ident.sym.to_string());
                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
                        span: DUMMY_SP,
                        decl: Decl::Fn(function.fold_with(self)),
                    }))
                }
                other => ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
                    span: DUMMY_SP,
                    decl: other.fold_with(self),
                })),
            },

            item => item.fold_children_with(self),
        };
        self.module_item += 1;
        item
    }

    fn fold_var_declarator(&mut self, node: VarDeclarator) -> VarDeclarator {
        let mut stacked = false;
        if let Pat::Ident(ref ident) = node.name {
            self.stack_ctxt.push(ident.id.sym.to_string());
            stacked = true;
        };
        let o = node.fold_children_with(self);
        if stacked {
            self.stack_ctxt.pop();
        }
        o
    }

    fn fold_fn_decl(&mut self, node: FnDecl) -> FnDecl {
        self.stack_ctxt.push(node.ident.sym.to_string());
        let o = node.fold_children_with(self);
        self.stack_ctxt.pop();

        o
    }

    fn fold_class_decl(&mut self, node: ClassDecl) -> ClassDecl {
        self.stack_ctxt.push(node.ident.sym.to_string());
        let o = node.fold_children_with(self);
        self.stack_ctxt.pop();

        o
    }

    fn fold_jsx_opening_element(&mut self, node: JSXOpeningElement) -> JSXOpeningElement {
        let mut stacked = false;

        if let JSXElementName::Ident(ref ident) = node.name {
            self.stack_ctxt.push(ident.sym.to_string());
            stacked = true;
        }
        let o = node.fold_children_with(self);
        if stacked {
            self.stack_ctxt.pop();
        }
        o
    }

    fn fold_key_value_prop(&mut self, node: KeyValueProp) -> KeyValueProp {
        let mut stacked = false;
        if let PropName::Ident(ref ident) = node.key {
            self.stack_ctxt.push(ident.sym.to_string());
            stacked = true;
        }
        if let PropName::Str(ref s) = node.key {
            self.stack_ctxt.push(s.value.to_string());
            stacked = true;
        }
        let o = node.fold_children_with(self);
        if stacked {
            self.stack_ctxt.pop();
        }
        o
    }

    fn fold_jsx_attr(&mut self, node: JSXAttr) -> JSXAttr {
        let mut stacked = false;
        if let JSXAttrName::Ident(ref ident) = node.name {
            self.stack_ctxt.push(ident.sym.to_string());
            stacked = true;
        }
        let o = node.fold_children_with(self);
        if stacked {
            self.stack_ctxt.pop();
        }
        o
    }

    fn fold_call_expr(&mut self, node: CallExpr) -> CallExpr {
        if let ExprOrSuper::Expr(expr) = &node.callee {
            if let Expr::Ident(id) = &**expr {
                if id.sym == *"qComponent" {
                    if let Some(comments) = self.comments {
                        comments.add_pure_comment(node.span.lo);
                    }
                } else if id.sym == *"qHook" {
                    let symbol_name = self.get_context_name();
                    let mut canonical_filename =
                        ["h_", &self.path.file_prefix, "_", &symbol_name].concat();
                    canonical_filename.make_ascii_lowercase();

                    let folded = node.fold_children_with(self);
                    let hook_collect = HookCollect::new(&folded);
                    let entry = self.context.bundling_policy.get_entry_for_sym(
                        &symbol_name,
                        self.path,
                        &self.stack_ctxt,
                        &hook_collect,
                        &folded,
                    );

                    let import_path = {
                        let filename = if let Some(ref entry) = entry {
                            entry
                        } else {
                            &canonical_filename
                        };
                        fix_path("a", &self.path.path, &["./", filename].concat())
                    };

                    self.hooks.push(Hook {
                        entry,
                        canonical_filename,

                        name: symbol_name.clone(),
                        module_index: self.module_item,
                        expr: folded,
                        local_decl: hook_collect.get_local_decl(),
                        local_idents: hook_collect.get_local_idents(),

                        origin: self.path.path.clone(),
                    });

                    let node = create_inline_qhook(import_path, &symbol_name);
                    self.context.hooks_names.insert(symbol_name);
                    return node;
                }
            }
        }

        node.fold_children_with(self)
    }
}

fn create_inline_qhook(url: JsWord, symbol: &str) -> CallExpr {
    CallExpr {
        callee: ast::ExprOrSuper::Expr(Box::new(Expr::Ident(Ident::new("qHook".into(), DUMMY_SP)))),
        span: DUMMY_SP,
        type_args: None,
        args: vec![
            ExprOrSpread {
                spread: None,
                expr: Box::new(Expr::Arrow(ArrowExpr {
                    is_async: false,
                    is_generator: false,
                    span: DUMMY_SP,
                    params: vec![],
                    return_type: None,
                    type_params: None,
                    body: BlockStmtOrExpr::Expr(Box::new(Expr::Call(CallExpr {
                        callee: ast::ExprOrSuper::Expr(Box::new(Expr::Ident(Ident::new(
                            "import".into(),
                            DUMMY_SP,
                        )))),
                        span: DUMMY_SP,
                        type_args: None,
                        args: vec![ExprOrSpread {
                            spread: None,
                            expr: Box::new(Expr::Lit(ast::Lit::Str(ast::Str {
                                span: DUMMY_SP,
                                value: url,
                                has_escape: false,
                                kind: ast::StrKind::Synthesized,
                            }))),
                        }],
                    }))),
                })),
            },
            ExprOrSpread {
                spread: None,
                expr: Box::new(Expr::Lit(ast::Lit::Str(ast::Str {
                    span: DUMMY_SP,
                    value: symbol.into(),
                    has_escape: false,
                    kind: ast::StrKind::Synthesized,
                }))),
            },
        ],
    }
}

fn escape_sym(str: &str) -> String {
    str.chars()
        .map(|x| match x {
            'A'..='Z' | 'a'..='z' | '0'..='9' | '_' => x,
            _ => '_',
        })
        .collect()
}