Frontend Development 20 min read

Mako – A Fast, Zero‑Config Web Bundler and Its Architecture

Mako is a lightning‑fast, zero‑configuration web bundler whose Rust core and parallel Node.js pool handle loading, parsing, transforming, dependency analysis, and asset generation across JavaScript/TypeScript, CSS, assets, and more, while offering HMR, code‑splitting, plugin extensibility, and production‑grade reliability.

DeWu Technology
DeWu Technology
DeWu Technology
Mako – A Fast, Zero‑Config Web Bundler and Its Architecture

Mako is a modern web bundling tool designed for web applications, libraries, and frameworks. It aims for speed, reliability, and ease‑of‑use, and is already used in hundreds of production projects.

Key Features

Zero‑configuration: start from a single JS/TS file and Mako automatically handles TypeScript, Less, CSS, CSS Modules, React, images, fonts, WASM, Node polyfills, etc.

Production‑grade reliability: tested across thousands of projects and npm packages.

Lightning‑fast builds: core bundling logic is written in Rust and parallelized with piscina in Node.js.

Hot Module Replacement (HMR): automatic browser updates without manual refresh; React Fast Refresh is integrated.

Code splitting, Module Concatenation and other optimizations comparable to Webpack.

Performance Test

Benchmarking (cold start, HMR, cold build, etc.) shows Mako outperforms other Rust‑based bundlers and Webpack.

Project Architecture

The build process consists of four major stages: entry → Compiler → build → generate . Each stage is further divided:

Entry : discovers the entry file(s).

Compiler : creates a Compiler instance that orchestrates the whole compilation.

Build : loads files, parses them into an AST, transforms the AST, analyzes dependencies, and creates module objects.

Generate : emits the final assets.

Load Stage

Files are loaded according to their extensions. The loader supports virtual modules, raw imports, JS/TS, CSS, MD/MDX, SVG, TOML, WASM, XML, YAML, JSON and generic assets. The core of the loader is shown below:

impl Load {
    pub fn load(file: &File, context: Arc<Context>) -> Result<Content> {
        // plugin first
        let content: Option<Content> = context.plugin_driver.load(&PluginLoadParam { file }, &context)?;
        if let Some(content) = content { return Ok(content); }
        // virtual module handling
        if file.path.to_str().unwrap() == "virtual:inline_css:runtime" {
            return Ok(Content::Js(JsContent { content: r#"export function moduleToDom(css) { ... }"#.to_string(), ..Default::default() }));
        }
        // file existence check
        if !file.pathname.exists() || !file.pathname.is_file() {
            return Err(anyhow!(LoadError::FileNotFound { path: file.path.to_string_lossy().to_string() }));
        }
        // extension based handling (JS, CSS, MD, SVG, TOML, WASM, XML, YAML, JSON, assets)
        // ... (omitted for brevity, each branch returns a Content variant)
        Ok(Content::Js(JsContent { content: format!("module.exports = {}", content), ..Default::default() }))
    }
}

Parse Stage

The source file is parsed into a ModuleAst (either Script or Css ). Plugins can provide custom parsing. Simplified implementation:

impl Parse {
    pub fn parse(file: &File, context: Arc<Context>) -> Result<ModuleAst> {
        // plugin first
        if let Some(ast) = context.plugin_driver.parse(&PluginParseParam { file }, &context)? { return Ok(ast); }
        // JS handling
        if let Some(Content::Js(_)) = &file.content {
            let ast = JsAst::new(file, context.clone())?;
            return Ok(ModuleAst::Script(ast));
        }
        // CSS handling
        if let Some(Content::Css(_)) = &file.content {
            // CSS AST creation (omitted)
        }
        Ok(ModuleAst::None)
    }
}

Transform Stage

SWC visitors are applied to the AST to perform transformations such as resolver, helper injection, symbol conflict fixing, URL asset handling, React refresh, environment variable replacement, etc. A condensed version:

impl Transform {
    pub fn transform(ast: &mut ModuleAst, file: &File, context: Arc<Context>) -> Result<()> {
        match ast {
            ModuleAst::Script(ast) => {
                let mut visitors: Vec<Box<dyn VisitMut>> = vec![
                    Box::new(resolver(...)),
                    Box::new(FixHelperInjectPosition::new()),
                    Box::new(FixSymbolConflict::new(...)),
                    // ... many more visitors based on file type and config
                ];
                ast.transform(&mut visitors, &mut vec![], file, true, context.clone())?;
                Ok(())
            }
            ModuleAst::Css(ast) => {
                // CSS visitors (prefixer, flexbugs, px2rem, etc.)
                Ok(())
            }
            ModuleAst::None => Ok(()),
        }
    }
}

Build Stage (Compiler)

The Compiler creates a channel and spawns parallel tasks for each file using a thread pool. The simplified core loop:

impl Compiler {
    pub fn build(&self, files: Vec<File>) -> Result<HashSet<ModuleId>> {
        let (rs, rr) = channel::<Result<Module>>();
        let build_with_pool = |file: File, parent: Option<ResolverResource>| {
            let rs = rs.clone();
            let ctx = self.context.clone();
            thread_pool::spawn(move || {
                let result = Self::build_module(&file, parent, ctx.clone());
                rs.send(result).unwrap();
            });
        };
        for file in files { build_with_pool(file, None); }
        // collect results, handle errors, return module ids
    }
}

build_module Function

pub fn build_module(file: &File, parent: Option<ResolverResource>, context: Arc<Context>) -> Result<Module> {
    // 1. load
    let mut file = file.clone();
    let content = load::Load::load(&file, context.clone())?;
    file.set_content(content);
    // 2. parse
    let mut ast = parse::Parse::parse(&file, context.clone())?;
    // 3. transform
    transform::Transform::transform(&mut ast, &file, context.clone())?;
    // 4. analyze deps + resolve
    let deps = analyze_deps::AnalyzeDeps::analyze_deps(&ast, &file, context.clone())?;
    // 5. create module
    let module_id = ModuleId::new(file.path.to_string_lossy().to_string());
    let module = Module::new(module_id, file.is_entry, Some(ModuleInfo { file, deps, ast, ..Default::default() }));
    Ok(module)
}

Plugin System

Plugins implement the Plugin trait, providing hooks such as load , parse , transform_js , before_resolve , after_build , etc. Example trait definition:

pub trait Plugin: Any + Send + Sync {
    fn name(&self) -> &str;
    fn modify_config(&self, _config: &mut Config, _root: &Path, _args: &Args) -> Result<()> { Ok(()) }
    fn load(&self, _param: &PluginLoadParam, _context: &Arc<Context>) -> Result<Option<Content>> { Ok(None) }
    fn parse(&self, _param: &PluginParseParam, _context: &Arc<Context>) -> Result<Option<ModuleAst>> { Ok(None) }
    // ... other lifecycle methods ...
}

Overall Flow

The compilation proceeds through Load → Parse → Transform → Analyze Deps → Create Module → Generate . After generation, assets are emitted, and the build finishes.

In summary, Mako combines a Rust core for performance with a flexible plugin architecture, offering a zero‑config experience while remaining extensible for advanced use‑cases.

PerformanceRustbuild toolMakoPlugin ArchitectureWeb Bundler
DeWu Technology
Written by

DeWu Technology

A platform for sharing and discussing tech knowledge, guiding you toward the cloud of technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.