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.
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.
DeWu Technology
A platform for sharing and discussing tech knowledge, guiding you toward the cloud of technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.