Implementing Webpack from Scratch, But in Rust - [3] Using NAPI-RS to Create Node.js Addons

ayou - Oct 31 - - Dev Community

Referencing mini-webpack, I implemented a simple webpack from scratch using Rust. This allowed me to gain a deeper understanding of webpack and also improve my Rust skills. It's a win-win situation!

Code repository: https://github.com/ParadeTo/rs-webpack

This article corresponds to the Pull Request: https://github.com/ParadeTo/rs-webpack/pull/4

The title seems unrelated to this series, as implementing webpack with Rust involves Node.js plugin development. But don't worry, let me explain. When we use webpack for bundling, don't we often run the following command?

webpack --config webpack.config.js
Enter fullscreen mode Exit fullscreen mode

Similarly, we want our RS Webpack to support such a command. But how can we get the content exported from webpack.config.js in Rust? One solution is to have a JS runtime to execute webpack.config.js. However, this approach seems a bit heavy, and it requires the JS runtime to be able to recreate the content of webpack.config.js in Rust. After searching for tools, I couldn't find anything suitable, so I had to take a different approach.

By examining the source code of Rspack, I found that it uses NAPI-RS to develop Node.js plugins. The specific approach is to write the core code of the packer using Rust, compile it into a plugin using NAPI-RS for use in Node.js, and let Node.js handle the import and parsing of the configuration file, passing it as a parameter to the interface provided by Rust.

To learn how to use NAPI-RS, you can refer to the official website. This article mainly explains how to transform our project into the desired result.

First, let's change our project structure as follows:

.
├── Cargo.lock
├── Cargo.toml
├── crates // Rust crates
│   ├── rswebpack_binding // Generated by NAPI
│   └── rswebpack_core
├── packages // JS packages
│   └── rswebpack-cli
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── readme.md
Enter fullscreen mode Exit fullscreen mode
  • Under crates, we have Rust projects. rswebpack_binding is generated by NAPI and is mainly used for exporting interfaces. rswebpack_core is the core library, where we moved the relevant code from the previous article.
  • Under packages, we have JS projects. rswebpack-cli will eventually be published as a command-line tool.

In the rswebpack_binding crate, the code is relatively simple. It just wraps the original Compiler:

// lib.rs
#![deny(clippy::all)]

use napi::Result;
use raw_config::RawConfig;
use rs_webpack_core::compiler::Compiler;
#[macro_use]
extern crate napi_derive;

mod raw_config;

#[napi]
pub struct RsWebpack {
  compiler: Box<Compiler>,
}

#[napi]
impl RsWebpack {
  #[napi(constructor)]
  pub fn new(raw_config: RawConfig) -> Result<Self> {
    let config = raw_config.try_into().expect("Config transform error");
    Ok(Self {
      compiler: Box::new(Compiler::new(config)),
    })
  }

  #[napi]
  pub fn run(&mut self) {
    self.compiler.as_mut().run();
  }
}

// raw_config.rs
use rswebpack_core::config::{Config, Output};

#[napi(object)]
pub struct RawOutput {
  pub path: String,
  pub filename: String,
}

impl TryFrom<RawOutput> for Output {
  type Error = ();

  fn try_from(value: RawOutput) -> Result<Self, Self::Error> {
    Ok(Output {
      path: value.path.into(),
      filename: value.filename.into(),
    })
  }
}

#[napi(object)]
pub struct RawConfig {
  pub root: String,
  pub entry: String,
  pub output: RawOutput,
}

impl TryFrom<RawConfig> for Config {
  type Error = ();

  fn try_from(value: RawConfig) -> Result<Self, Self::Error> {
    Ok(Config {
      root: value.root.into(),
      entry: value.entry.into(),
      output: value.output.try_into()?,
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, we define RawConfig to receive the configuration passed from JS, and we also specify how to convert RawConfig into Config. However, the conversion rules are currently very simple.

As for rswebpack-cli, it's even simpler. We just need to parse the command-line arguments, read the configuration, and call the interface exported by the plugin:

#!/usr/bin/env node
const path = require('path')
const {RsWebpack} = require('@rswebpack/binding')

const argv = require('yargs-parser')(process.argv.slice(2))

const config = require(path.resolve(
  process.cwd(),
  argv.config || 'rswebpack.config.js'
))

const rsWebpack = new RsWebpack(config)
rsWebpack.run()
Enter fullscreen mode Exit fullscreen mode

Don't forget to configure the command name in package.json:

{
  "name": "@rswebpack/cli",
  "dependencies": {
    "@rswebpack/binding": "workspace:*",
    "yargs-parser": "^21.1.1"
  },
  "bin": {
    "rswebpack": "./index.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

Then run npm link. After that, create a new directory:

.
├── const.js
├── index.js
└── rswebpack.config.js
Enter fullscreen mode Exit fullscreen mode

The rswebpack.config.js file should contain the following:

const path = require('path')

module.exports = {
  root: path.resolve(__dirname),
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, 'out'),
    filename: 'bundle.js',
  },
}
Enter fullscreen mode Exit fullscreen mode

Finally, run rswebpack --config rswebpack.config.js. If it outputs bundle.js correctly, it means the refactor was successful.

Please kindly give me a star!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player