cream-js 0.1.0

A lightweight compiler for modern web applications.
use crate::component::Component;
use crate::component::RenderReturn;
use crate::component::cream_component;
use crate::component::cream_dom_name;
use crate::component::special_trim;
use crate::component::std_lib_path;
use crate::config::Config;
use crate::consts::BUILD_PATH;
use crate::consts::DEFAULT_WINDOW_OBJ;
use crate::consts::ENTRY_FILE;
use crate::helpers::build_source::translate_import_src_to_build;
use crate::helpers::javascript::javascript_function::javascript_function;
use crate::helpers::javascript::transpile_to_js::transpile_to_js;
use crate::std_err::ErrType::NotFound;
use crate::std_err::ErrType::OSError;
use crate::std_err::ErrType::SyntaxError;
use oxc_allocator::CloneIn;
use oxc_ast::ast::Function;
use oxc_codegen::Codegen;
use oxc_codegen::CodegenOptions;
use oxc_codegen::CommentOptions;
use oxc_minifier::Minifier;
use oxc_minifier::MinifierOptions;
use oxc_semantic::SemanticBuilder;
use oxc_transformer::TransformOptions;
use oxc_transformer::Transformer;
use rand::rngs::ThreadRng;
use oxc_allocator::Allocator;
use oxc_ast::AstBuilder;
use oxc_ast::ast::Statement;
use oxc_parser::Parser;
use oxc_span::SourceType;
use crate::std_err::ErrType;
use crate::component::format_oxc_diag;
use crate::std_err::StdErr;
use std::fs;
use std::fs::read_to_string;
use std::io::ErrorKind;
use std::path::Path;
use rand::Rng;
use crate::helpers::dependancy_graph::DependencyGraph;
use crate::helpers::javascript::javascript_assign::javascript_assign;

pub fn final_build_string(render: RenderReturn, comp_id: u64, is_main: bool, config: &Config) -> String {
    special_trim(format!("
            let self; let onRender=function(){{}}; {};
            {}; function {}(params) {{{};
                params.childrens = params.childrens ?? [];
                {}; onRender();
                return {}
            }}; {}
            export {{{} as {}, mount}};
        ",  
            if is_main {
                format!("{}={};", Component::cream_window_obj(), DEFAULT_WINDOW_OBJ)
            } else {
                String::new()
            }, if is_main {
                javascript_assign(
                    &Component::cream_env_object(),
                    &serde_json::to_string(&config.env).unwrap()
                )
            } else {
                String::new()
            },
            cream_component(comp_id),
            render.script, render.rendering_script, 
            cream_dom_name(render.root_dom_id),
            javascript_function(String::from("mount"), 
                &format!(
                    "document.body.appendChild({}(params));",
                    cream_component(comp_id)
                ),
                vec!["params={}".to_string()]
            ),
            cream_component(comp_id),
            render.comp_name
        ))
}

fn is_entry_file(name: &str) -> bool {
    name == ENTRY_FILE
}

impl Component{
    pub fn handle_comp_declr<'b>(
        &mut self,
        stmt: &mut Function,
        new_program: &mut oxc_allocator::Vec<'b, Statement<'b>>,
        allocator: &'b Allocator,
        ast: &AstBuilder<'b>,
        conf: &Config,
        dep_graph: &mut DependencyGraph,
    ) {
        if let Some(body) = &mut stmt.body {
            body.statements.retain(|stmt| {
                match stmt {
                    Statement::ImportDeclaration(src) => {
                        let source = src.clone_in(allocator);
                        
                        if source.source.value.starts_with("@") {
                            let resolved = if let Some(name) = source.source.value.strip_prefix("@std:") {
                                dep_graph.add_std_lib(name);
                                std_lib_path(name)
                            } else if let Some(name) = source.source.value.strip_prefix("@pkg:") {
                                match &conf.packages[name] {
                                    toml::Value::String(s) => s.to_string(),
                                    _ =>  {
                                        StdErr::exec(SyntaxError, "package table must contain string values");
                                        return false
                                    }
                                }
                            } else {
                                translate_import_src_to_build(&source.source.value["@".len()..])
                            };  
                                            
                            let new_import = ast.import_declaration(
                                source.span, 
                                source.specifiers.clone_in(allocator), 
                                ast.string_literal(source.span, ast.str(&resolved), Some(ast.str(&resolved))),
                                source.phase, 
                                source.with_clause.clone_in(allocator), 
                                source.import_kind
                            );

                            new_program.insert(0, Statement::ImportDeclaration(oxc_allocator::Box::new_in(new_import, allocator)));
                        }

                        false
                    },
                    Statement::LabeledStatement(label) if label.label.name == "global" => {
                        new_program.push(label.body.clone_in(allocator));
                        false
                    }
                    _ => true
                }
            });
        }

    }

    pub fn cream_env_object() -> String {
        "window.ENV".to_string()
    }

    pub fn transpile(&mut self, config: &Config, dep_graph: &mut DependencyGraph) {
        let mut succ_read = true;
        let path  = Path::new(&self.name);
        let script: String = read_to_string(path)
            .unwrap_or_else(|e| {
                if e.kind() == ErrorKind::InvalidData {
                    let dest  = Path::new(BUILD_PATH)
                            .join(path.file_name().unwrap());

                    if dest.exists() {
                        succ_read = false;
                        String::new()
                    } else {
                        let _ = fs::copy(path, dest);
                        succ_read = false;
                        String::new() 
                    }
                } else {    

                    StdErr::exec(OSError, &format!(
                        "error reading {}: {}", 
                        &self.name,
                        e
                    ));
                    self.out = String::new();
                    succ_read = false;
                    String::new()
                }
            });

        if !succ_read {
            return
        };

        match Path::new(&self.name).extension().unwrap().to_str().unwrap(){
            "js" | "ts" | "mjs" => {
                self.out = transpile_to_js(
                    &script, &self.name,
                    CodegenOptions {
                        minify: !cfg!(debug_assertions),
                        single_quote: true,
                        comments: CommentOptions::disabled(),
                        ..Default::default()
                    }
                );

                return
            },
            "cream" => {}
            _ => {
                self.out = script;
                return
            }
        }

        let comp = Component::new(script, self.name.clone());
        let mut succ_render = true;
        let render = comp.html_rendering_script()
            .unwrap_or_else(|e| {
                StdErr::exec(SyntaxError, &format!(
                    "error while parsing xml of {}: {}",
                    self.name, e
                ));
                succ_render = false;
                RenderReturn::default()
            });

        if !succ_render {
            return
        }

        if render.comp_name.trim().is_empty() {
            StdErr::exec(NotFound, &format!("component name for {}", self.name));
            self.out = String::new();
            return
        }

        let mut rng = ThreadRng::default();
        let comp_id = rng.next_u64();
        let script_trimmed = final_build_string(render, comp_id, is_entry_file(&self.name), config);

        let allocator = Allocator::default();
        let source_type = SourceType::default().with_module(true);
        let parsed = Parser::new(&allocator, &script_trimmed, source_type).parse();
        let program = parsed.program;

        let allocator = Allocator::default();
        let ast = AstBuilder::new(&allocator);
        let mut new_program = oxc_allocator::Vec::new_in(&allocator);

        for mut stmt in program.body {
            match &mut stmt {
                Statement::LabeledStatement(label) => {
                    if label.label.name == "global" {
                        new_program.push(label.body.clone_in(&allocator));
                    }
                },
                Statement::ImportDeclaration(src) => {
                    let source = src.clone_in(&allocator);
                    
                    if source.source.value.starts_with("@") {
                        let resolved = if source.source.value.starts_with("@std:") {
                            let name = &source.source.value["@std:".len()..].to_string();
                            dep_graph.add_std_lib(name);
                            std_lib_path(name)
                        } else {
                            translate_import_src_to_build(&source.source.value["@".len()..])
                        };  
                                        
                        let new_import = ast.import_declaration(
                            source.span, 
                            source.specifiers.clone_in(&allocator), 
                            ast.string_literal(source.span, ast.str(&resolved), Some(ast.str(&resolved))),
                            source.phase, 
                            source.with_clause.clone_in(&allocator), 
                            source.import_kind
                        );

                        new_program.insert(0, Statement::ImportDeclaration(oxc_allocator::Box::new_in(new_import, &allocator)));
                    }
                },
                Statement::FunctionDeclaration(func) => {
                     if let Some(name) = func.name() && name.to_string() == cream_component(comp_id) {
                         let mut bind = func.clone_in(&allocator);
                         self.handle_comp_declr(
                             &mut bind,
                             &mut new_program,
                             &allocator,
                             &ast,
                             config,
                             dep_graph
                         );

                         new_program.push(Statement::FunctionDeclaration(bind));
                    } else {
                        new_program.push(stmt);
                    }
                }
                _ => {
                    new_program.push(stmt.clone_in(&allocator));
                }

            }
        }

        let mut new_prog = ast.program(
            program.span, 
            program.source_type, 
            "", 
            program.comments, 
            None, 
            oxc_allocator::Vec::new_in(&allocator),
            new_program
        );

        let min_opt = MinifierOptions::default();
        let min = Minifier::new(min_opt);

        let allocator = Allocator::default();
        let transform_options = TransformOptions::default();
        let semantic_ret = SemanticBuilder::new()
            .build(&new_prog);

        let scoping = semantic_ret.semantic.into_scoping();
        let transformer = Transformer::new(
            &allocator,
            Path::new(&self.name),
            &transform_options,
        );
        
        for err in parsed.diagnostics {
            StdErr::exec(ErrType::SyntaxError, &format_oxc_diag(&err, self.name.clone()));
        }
    
        let _ = transformer.build_with_scoping(scoping, &mut new_prog);
        let _ = min.minify(&allocator, &mut new_prog);
        let opt_script = Codegen::new()
            .with_options(CodegenOptions {
                minify: !cfg!(debug_assertions),
                single_quote: true,
                comments: CommentOptions::disabled(),
                ..Default::default()
            })
            .with_source_type(SourceType::mjs())
            .build(&new_prog)
            .code;

        self.out = opt_script;
    }
}