Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Rspack Rust Book

Rspack Logo

Rspack is a high performance JavaScript bundler written in Rust. It offers strong compatibility with the webpack ecosystem, allowing for seamless replacement of webpack, and provides lightning fast build speeds.

Sections

Rspack Custom Binding - Getting Started

In this section, we will learn how to create a custom binding for Rspack.

Getting Started

Welcome to Rspack Custom Binding! This guide will help you get started with creating your own native Node.js addon for Rspack.

Prerequisites

Before diving into Rspack Custom Binding, we recommend:

  1. Read the Rationale - Understand why you might want to use custom bindings and how they work
  2. Basic Rust Knowledge - Familiarity with Rust programming language
  3. Node.js Experience - Understanding of Node.js addons and N-API concepts

If you are not familiar with writing Node.js addons and N-API in Rust, don't worry. We will cover the basics in the guide.

Next Steps

Once you understand the rationale and architecture, proceed to the Create From Template guide to set up your development environment.

Rationale of Rspack Custom Binding

The reason why Rspack is so fast is that it's written in Rust and so as the Rspack's internal builtin plugins and builtin loaders.

For most of the time, We assume you've been using Rspack JavaScript API and writing Rspack JavaScript Plugins. And you might probably heard there're some overheads when using JavaScript API. The rumour is true! Rspack is mostly written in Rust and providing the adapting layer with JavaScript API requires a lot of hassle of passing values back and forth between Rust and JavaScript. This creates a lot of overheads and performance issues.

But have you ever wondered if there's a way to extend Rspack's functionality by writing native Rust code and not requiring to sacrifice the performance or if you're able to use the rich Rust APIs? And the answer is yes. This is where Rspack Custom Binding comes in.

To get started with Rspack Custom Binding, you need to know the surface level of how Rspack binding works.

How Rspack Binding Works

If you are using the @rspack/cli or @rspack/core and not knowing what a custom binding is, you are using Rspack binding. It's a simple architecture that allows you to extend Rspack's functionality by leveraging the Rspack JavaScript API. It's just the same as how you use the Webpack JavaScript API to extend Webpack.

Let's take a deep dive into the architecture. It contains 3 parts:

  • npm:@rspack/core: The JavaScript API layer of Rspack. Written in JavaScript.
  • npm:@rspack/binding: The Node.js Addon of Rspack.
  • crate:rspack_binding_api: The N-API glue layer of Rspack. Written in Rust.
flowchart TD
    Core("npm:@rspack/core")
    style Core stroke-width:0px,color:#FFDE59,fill:#545454

    Core --> Binding("npm:@rspack/binding")
    style Binding stroke-width:0px,color:#FFDE59,fill:#545454

    Binding --> APIs("crate:rspack_binding_api")
    style APIs stroke-width:0px,color:#FFDE59,fill:#545454

crate:rspack_binding_api

The N-API glue layer of Rspack.

This layer contains a glue code that bridges the gap between N-API-compatible runtimes, which, most of the time, is Node.js and Rust Core crates.

npm:@rspack/binding

The Node.js Addon of Rspack.

This layer links crate:rspack_binding_api and compiles it into a Node.js Addon (a *.node file) with NAPI-RS. The functionalities that npm:@rspack/core provides are mostly exposed by the Node.js Addon in npm:@rspack/binding.

Note: Maybe you have checked out the code on npm and it does not contain the *.node file. This is because the *.node files are dispatched by the @rspack/binding-* packages (e.g. @rspack/binding-darwin-arm64) for different platforms. Don't worry about this at the moment. We will get into the details in the custom binding section.

npm:@rspack/core

The JavaScript API layer of Rspack.

The internal of npm:@rspack/core is written in JavaScript. It bridges the gap between the Node.js Addon in npm:@rspack/binding and Rspack JavaScript API.

npm:@rspack/cli is a command line tool that uses npm:@rspack/core to build your project.

How Rspack Custom Binding Works

Let's use the diagram below to understand how a custom binding works. It shows a "Before" state, representing the standard Rspack setup, and an "After" state, which illustrates the custom binding approach.

In the Before state, your project uses the default Rspack binding. This is created solely from crate:rspack_binding_api, the core glue layer between Rust and Node.js.

In the After state, you introduce your own native code. As the diagram shows, your User Customizations (like custom Rust plugins) are combined with the original crate:rspack_binding_api.

This combination produces a new, personalized Custom Binding. This becomes your project's new Node.js addon, allowing you to inject high-performance, custom logic directly into Rspack's build process.

Crucially, you can continue to use npm:@rspack/core with your custom binding. This allows you to benefit from native performance and customization without rewriting the JavaScript API layer, reusing all the features it provides. We will cover how to integrate @rspack/core with a custom binding in a later section.

flowchart LR
    subgraph Before ["_Before_"]
        Original("crate:rspack_binding_api")
        style Original stroke-width:0px,color:#FFDE59,fill:#545454
    end

    subgraph After ["_After_"]
        Plugin("User Customizations:<br>- custom plugins")
        style Plugin stroke-width:0px,color:#AB7F45,fill:#FFE2B1

        API("crate:rspack_binding_api")
        style API stroke-width:0px,color:#FFDE59,fill:#545454

        Plugin --> CustomBinding("Custom Binding = <br>crate:rspack_binding_api + User Customizations")
        API --> CustomBinding
        style CustomBinding stroke-width:0px,color:#AB7F45,fill:#FFE2B1
    end

    Before -.-> After

    style Before stroke-dasharray: 5 5
    style After stroke-dasharray: 5 5

Next Steps

Now you have a basic understanding of how Rspack Custom Binding works. Let's move on to the Create From Template guide to set up your development environment.

Create from Template

To create a new repository based on the template, click the button below.

Deploy from Template

Or visit the rspack-binding-template repository and click "Use this template".

After creating your repository, the binding will automatically start building. You can check the compilation progress in the Actions page of your new repository.

The initial commit will trigger a comprehensive CI workflow that includes:

  • Cargo Check - Rust code validation
  • Cargo Clippy - Linting and best practices
  • Build - Cross-platform compilation for multiple targets:
    • macOS (x86_64 and ARM64)
    • Windows (x86_64, i686, and ARM64)
    • Linux (x86_64 GNU/musl, ARM64 GNU/musl, ARMv7)
    • Android (ARM64 and ARMv7)
  • Test - Running tests on Ubuntu, macOS, and Windows

A successful run typically takes around 20 minutes and generates platform-specific binary artifacts. You can see an example of a completed workflow here.

Note: You don't need to check 'include all branches'.

Template Structure

  • crates/binding/ - Rust binding implementation
  • examples/use-plugin/ - Plugin examples
  • lib/ - JavaScript/TypeScript interface code
  • Cargo.toml, package.json - Package configurations
  • .github/, .cargo/ - CI/CD and tooling setup

Tech Stack: Rust, JavaScript/TypeScript, Node.js, Cargo, pnpm, GitHub Actions

Next Steps

In this chapter, we have learned:

  • To create a new repository based on the template.

In the next chapter, we will learn how to setup the repository locally.

First Custom Binding

This chapter will guide you through the process of creating your first custom binding.

Prerequisites

Next Steps

Setup

This section will guide you through setting up your newly created rspack-binding repository for local development.

Prerequisites

Before you begin, make sure you have the following installed:

  • Node.js (>= 18.0.0)
  • Rust (latest stable version)

This repository uses Corepack to manage package managers, so you don't need to install pnpm manually.

Note: According to the official documentation: "Corepack is distributed with Node.js from version 14.19.0 up to (but not including) 25.0.0. Run corepack enable to install the required Yarn and pnpm binaries on your path."

If you're using Node.js 25+ or an older version, you may need to install Corepack manually following the installation guide.

Installation Steps

1. Clone your repository

git clone https://github.com/your-username/your-repo-name.git
cd your-repo-name

2. Enable Corepack

corepack enable

3. Install dependencies

pnpm install

This command reads the pnpm-workspace.yaml configuration and installs dependencies for all workspace projects, including @rspack-template/binding and @rspack-template/core.

Note: The package names @rspack-template/binding and @rspack-template/core are demo names used to make the template runnable. Their functionalities correspond to @rspack/binding and @rspack/core respectively. You can manually replace these package names with your own.

We recommend using npm scope for your package names. As mentioned in the NAPI-RS documentation: "It is recommended to distribute your package under npm scope because @napi-rs/cli will, by default, append different platform suffixes to the npm package name for different platform binary distributions. Using npm scope will help reduce the chance that the package name was already taken."

You should see output similar to this:

❯ pnpm install
Scope: all 3 workspace projects
Lockfile is up to date, resolution step is skipped
Packages: +126
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 126, reused 123, downloaded 3, added 126, done

dependencies:
+ @rspack-template/binding 0.0.2 <- crates/binding
+ @rspack/core 1.4.10

devDependencies:
+ @taplo/cli 0.7.0
+ husky 9.1.7
+ lint-staged 16.1.2

. prepare$ husky
└─ Done in 97ms
Downloading @rspack/binding-darwin-arm64@1.4.10: 17.67 MB/17.67 MB, done
Done in 4.1s using pnpm v10.13.1

For the following tutorials: We will use @rspack-template/test-binding and @rspack-template/test-core as example package names. We'll perform a global replacement of these package names and reinstall dependencies to demonstrate the complete development workflow. See this commit for reference.

4. Build the project

pnpm build

This command will trigger NAPI-RS compilation to build the Rust binding. NAPI-RS is a framework for building pre-compiled Node.js addons in Rust, providing a safe and efficient way to call Rust code from JavaScript.

You should see output similar to this:

❯ pnpm build

> @rspack-template/test-core@0.0.2 build /my-rspack-binding
> pnpm run --filter @rspack-template/test-binding build


> @rspack-template/test-binding@0.0.2 build /my-rspack-binding/crates/binding
> napi build --platform

   Compiling proc-macro2 v1.0.95
   Compiling unicode-ident v1.0.18
   Compiling serde v1.0.219
   Compiling libc v0.2.174
   Compiling version_check v0.9.5
   Compiling crossbeam-utils v0.8.21
   Compiling rayon-core v1.12.1
   Compiling autocfg v1.5.0
   Compiling zerocopy v0.8.26
   Compiling getrandom v0.3.3
   Compiling object v0.36.7
   Compiling parking_lot_core v0.9.11
   Compiling anyhow v1.0.98
   ...
   Compiling rspack_plugin_hmr v0.4.10
   Compiling rspack_plugin_css_chunking v0.4.10
   Compiling rspack_plugin_module_info_header v0.4.10
   Compiling rspack_plugin_sri v0.4.10
   Compiling rspack_binding_builder v0.4.10
   Compiling rspack_binding_builder_macros v0.4.10
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 29s

The build process compiles the Rust code in crates/binding into a native Node.js addon (.node file) that can be called from JavaScript.

Verify Setup

To verify that everything is working correctly, run the example plugin:

node examples/use-plugin/build.js

This will execute the example plugin using your compiled binding, demonstrating that the Rust-JavaScript integration is working properly.

If the example runs successfully, your setup is complete and ready for development:

❯ node examples/use-plugin/build.js
assets by status 1.46 KiB [cached] 1 asset
runtime modules 93 bytes 2 modules
./src/index.js 1 bytes [built] [code generated]
Rspack 1.4.10 compiled successfully in 30 ms

Next Steps

In this chapter, we have learned:

  • To setup the repository locally.
  • To build the project.
  • To verify the setup.

In the next chapter, we will walk you through the MyBannerPlugin as a practical example to demonstrate how to build custom rspack bindings. This plugin will show you the complete workflow from Rust implementation to JavaScript integration.

Create a Plugin

In this chapter, we will explore the MyBannerPlugin that has already been created in the template as a practical example. While the plugin is already implemented, We will walk you through how to create this plugin from scratch and how to use it in JavaScript. This will demonstrate the complete workflow from Rust implementation to JavaScript integration.

What is MyBannerPlugin?

The MyBannerPlugin is a simple plugin that adds a banner comment to the top of generated JavaScript files.

Prerequisites

Before starting this tutorial, make sure you have completed the setup process and can successfully run the example plugin.

Overview

We will guide you through the plugin creation process in the following steps:

  1. Understand the Plugin Structure - Examine the basic Rust plugin structure
  2. Learn the Plugin Logic - Understand how the banner functionality works
  3. NAPI Bindings - See how Rust functionality is exposed to JavaScript using NAPI-RS
  4. JavaScript Integration - Learn how to use the plugin in JavaScript and rspack configuration
  5. Testing the Plugin - Learn how to verify the plugin works correctly

Let's explore the MyBannerPlugin implementation!

1. Understand the Plugin Structure

The MyBannerPlugin is implemented in Rust and follows the standard plugin structure.

  • crates/binding/src/lib.rs - The glue code that exports the plugin to JavaScript
  • crates/binding/src/plugin.rs - The MyBannerPlugin implementation

2. Learn the Plugin Logic

MyBannerPlugin is a simple plugin that adds a banner comment to the top of generated main.js file.

Before we start, be sure to add the following dependencies to your Cargo.toml file:

  • rspack_core - The Rspack core API
  • rspack_error - The Rspack error handling API
  • rspack_hook - The Rspack hook API
  • rspack_sources - The Rspack source API, which is a port of webpack's webpack-sources

2.1. Initialize the Plugin

The MyBannerPlugin is implemented as a struct with a banner field. The banner field is a String that contains the banner comment. The new method is a constructor that takes a String and returns a MyBannerPlugin instance.

The MyBannerPlugin struct is annotated with #[plugin] to indicate that it is a plugin. The #[plugin] macro is provided by the rspack_hook crate.

It also implements the Plugin trait, which is provided by the rspack_core crate. The Plugin trait is a core trait for all plugins. It requires the name method to return the name of the plugin, and the apply method to apply the plugin to the compilation, which is just the same as the apply method in the Rspack JavaScript Plugin API.

In this example, the name method returns "MyBannerPlugin", and the apply method is currently to be implemented.

/// A plugin that adds a banner to the output `main.js`.
#[derive(Debug)]
#[plugin]
pub struct MyBannerPlugin {
  banner: String,
}

impl MyBannerPlugin {
  pub fn new(banner: String) -> Self {
    Self::new_inner(banner)
  }
}

impl Plugin for MyBannerPlugin {
  fn name(&self) -> &'static str {
    "MyBannerPlugin"
  }

  fn apply(
    &self,
    ctx: PluginContext<&mut ApplyContext>,
    _options: &CompilerOptions,
  ) -> rspack_error::Result<()> {
    Ok(())
  }
}

2.2 Implement with Rust hooks

Just like hooks in the Rspack JavaScript Plugin API, hooks in Rust are implemented as a function that takes a reference to the plugin instance and a reference to the certain categories.

The apply method is called with a PluginContext instance and a CompilerOptions instance.

In this example, we will append the banner to the main.js file. So we need to implement the process_assets hook.

To tap the process_assets hook, we need to declare a function and annotate it with #[plugin_hook] which is provided by rspack_hook. And the process_assets is a compilation hook. That means we need to import the hook CompilationProcessAssets from rspack_core. Set stage to Compilation::PROCESS_ASSETS_STAGE_ADDITIONS and tracing to false to avoid recording the tracing information as we don't need it in this example.

#[plugin_hook(CompilationProcessAssets for MyBannerPlugin, stage = Compilation::PROCESS_ASSETS_STAGE_ADDITIONS, tracing = false)]
async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
  let asset = compilation.assets_mut().get_mut("main.js");
  if let Some(asset) = asset {
    let original_source = asset.get_source().cloned();
    asset.set_source(Some(Arc::new(ConcatSource::new([
      RawSource::from(self.banner.as_str()).boxed(),
      original_source.unwrap().boxed(),
    ]))));
  }

  Ok(())
}

2.3 Tap the hook

impl Plugin for MyBannerPlugin {
  fn name(&self) -> &'static str {
    "MyBannerPlugin"
  }

  fn apply(
    &self,
    ctx: PluginContext<&mut ApplyContext>,
    _options: &CompilerOptions,
  ) -> rspack_error::Result<()> {
    ctx
      .context
      .compilation_hooks
      .process_assets
      .tap(process_assets::new(self));
    Ok(())
  }
}

2.4 Conclusion

In this section, we have learned how to create a plugin in Rust and how to tap the process_assets hook. You can find the full code in the rspack-binding-template repository.

In the next section, we will learn how to expose the plugin to JavaScript.

3. NAPI Bindings

In this section, we will learn how to expose the plugin to JavaScript using NAPI bindings. And then we will create a JavaScript wrapper for the plugin. Also reuse the @rspack/core package to create a new core package to replace the original @rspack/core package.

3.1 Expose the plugin to JavaScript

To expose the plugin to JavaScript, we need to create a NAPI binding.

Now it's time to unveil the mystery of the crates/binding/src/lib.rs file.

Add these dependencies to your Cargo.toml:

  • rspack_binding_builder - Rspack binding builder API
  • rspack_binding_builder_macros - Rspack binding builder macros
  • napi - NAPI-RS crate
  • napi_derive - NAPI-RS derive macro

The crates/binding/src/lib.rs file exports the plugin to JavaScript using NAPI bindings.

Note: Split plugin implementation across files: plugin.rs for logic, lib.rs for JavaScript bindings.

Import required crates and use the register_plugin macro to expose the plugin:

  1. Import napi::bindgen_prelude::* (required by register_plugin macro)
  2. Import register_plugin from rspack_binding_builder_macros
  3. Import napi_derive with #[macro_use] attribute
  4. Use register_plugin with a plugin name and resolver function

The register_plugin macro takes a plugin name (used for JavaScript identification) and a resolver function. The resolver receives napi::Env and napi::Unknown options from JavaScript, returning a BoxPlugin instance.

As expected, when JavaScript calls new rspack.MyBannerPlugin("// banner"), the resolver function receives the banner string. It extracts this string using napi::Unknown::coerce_to_string and creates a BoxPlugin by calling MyBannerPlugin::new(banner).

Note: The Unknown type represents any JavaScript value.

In this example, we use the coerce_to_string method to get the banner string. The coerce_to_string method returns a Result - it will succeed for string-convertible values but error if the value cannot be converted to a string. Additional type validation can be added as needed.

mod plugin;

use napi::bindgen_prelude::*;
use rspack_binding_builder_macros::register_plugin;
use rspack_core::BoxPlugin;

#[macro_use]
extern crate napi_derive;
extern crate rspack_binding_builder;

// Export a plugin named `MyBannerPlugin`.
//
// `register_plugin` is a macro that registers a plugin.
//
// The first argument to `register_plugin` is the name of the plugin.
// The second argument to `register_plugin` is a resolver function that is called with `napi::Env` and the options returned from the resolver function from JS side.
//
// The resolver function should return a `BoxPlugin` instance.
register_plugin!("MyBannerPlugin", |_env: Env, options: Unknown<'_>| {
  let banner = options
    .coerce_to_string()?
    .into_utf8()?
    .as_str()?
    .to_string();
  Ok(Box::new(plugin::MyBannerPlugin::new(banner)) as BoxPlugin)
});

After the plugin is exposed to JavaScript, we can rerun pnpm build in crates/binding to build the plugin. Make sure you have lib.crate-type = ["cdylib"] defined in your Cargo.toml file.

Note: The cdylib crate type is required for the plugin to be used in JavaScript.

This makes this crate a dynamic library, on Linux, it will be a *.so file and on Windows, it will be a *.dll file.

The NAPI-RScli we triggered on pnpm build will rename the *.so or *.dll file to *.node file. So that can be loaded by the NAPI runtime, which, in this case, is the Node.js.

3.2 Create a JavaScript Plugin wrapper

Now that we have the Rust plugin implemented and exposed to JavaScript, we need to create a JavaScript wrapper for it. So that we can use the plugin in JavaScript and rspack configuration.

Check out the lib/index.js file in the rspack-binding-template repository.

Here we will create a MyBannerPlugin class that is a wrapper for the Rust plugin:

// Rewrite the `RSPACK_BINDING` environment variable to the directory of the `.node` file.
// So that we can reuse the `@rspack/core` package to load the right binding.
process.env.RSPACK_BINDING = require('node:path').dirname(
  require.resolve('@rspack-template/test-binding')
);

const binding = require('@rspack-template/test-binding');

// Register the plugin `MyBannerPlugin` exported by `crates/binding/src/lib.rs`.
binding.registerMyBannerPlugin();

const core = require('@rspack/core');

/**
 * Creates a wrapper for the plugin `MyBannerPlugin` exported by `crates/binding/src/lib.rs`.
 *
 * Check out `crates/binding/src/lib.rs` for the original plugin definition.
 * This plugin is used in `examples/use-plugin/build.js`.
 *
 * @example
 * ```js
 * const MyBannerPlugin = require('@rspack-template/test-core').MyBannerPlugin;
 * ```
 *
 * `createNativePlugin` is a function that creates a wrapper for the plugin.
 *
 * The first argument to `createNativePlugin` is the name of the plugin.
 * The second argument to `createNativePlugin` is a resolver function.
 *
 * Options used to call `new MyBannerPlugin` will be passed as the arguments to the resolver function.
 * The return value of the resolver function will be used to initialize the plugin in `MyBannerPlugin` on the Rust side.
 *
 * For the following code:
 *
 * ```js
 * new MyBannerPlugin('// Hello World')
 * ```
 *
 * The resolver function will be called with `'// Hello World'`.
 *
 */
const MyBannerPlugin = core.experiments.createNativePlugin(
  'MyBannerPlugin',
  function (options) {
    return options;
  }
);

Object.defineProperty(core, 'MyBannerPlugin', {
  value: MyBannerPlugin,
});

module.exports = core;

Let's break down the code:

1. Rewrite the RSPACK_BINDING environment variable

The RSPACK_BINDING environment variable is used to tell the @rspack/core package where to load the binding from. The expected value is an absolute path of the directory of the binding package.

Note: This line should be placed before the require('@rspack/core') line. Otherwise, the @rspack/core package will not be able to find the binding.

In this example, we use the require.resolve method to get the path of the @rspack-template/test-binding package. This resolves to the index.js file in the @rspack-template/test-binding package. And then use the dirname method to get the directory of the @rspack-template/test-binding package.

process.env.RSPACK_BINDING = require('node:path').dirname(
  require.resolve('@rspack-template/test-binding')
);

2. Register the plugin to the global plugin list

The register_plugin macro used in the crates/binding/src/lib.rs file exposes the plugin to JavaScript.

For plugin name MyBannerPlugin defined in the crates/binding/src/lib.rs file, the register_plugin macro will expose a JS function named registerMyBannerPlugin to the JavaScript side. You have to call this function to register the plugin to the global plugin list.

Note: Calling registerMyBannerPlugin does not mean the plugin is registered to the current Rspack instance. It only means the plugin is registered to the global plugin list. You will need to use the wrapper defined in the later section to register the plugin to the current Rspack instance or use it in the rspack configuration.

const binding = require('@rspack-template/test-binding');

// Register the plugin `MyBannerPlugin` exported by `crates/binding/src/lib.rs`.
binding.registerMyBannerPlugin();

3. Create a wrapper for the plugin

The createNativePlugin function is a function that creates a wrapper for the plugin. It is defined in the @rspack/core package.

The first argument to createNativePlugin is the name of the plugin defined on the Rust side. The second argument is a resolver function.

In this example, The name of the plugin is "MyBannerPlugin", and the resolver function is called with the options passed to the new MyBannerPlugin constructor, which is the banner string. As we don't need to do anything with the options in this example, we just return the options.

const core = require('@rspack/core');

const MyBannerPlugin = core.experiments.createNativePlugin(
  'MyBannerPlugin',
  function (options) {
    return options;
  }
);

4. Export the plugin wrapper and @rspack/core

Finally, we export the MyBannerPlugin wrapper and the @rspack/core package. This allows us to use the plugin in the rspack configuration and reuse all the other APIs in the @rspack/core package.

Object.defineProperty(core, 'MyBannerPlugin', {
  value: MyBannerPlugin,
});

module.exports = core;

3.3 Conclusion

In this section, we have learned how to expose the plugin to JavaScript using NAPI bindings. And then we have created a JavaScript wrapper for the plugin. Also reuse the @rspack/core package to create a new core package to replace the original @rspack/core package.

In the next section, we will learn how to use the plugin in the rspack configuration.

4. JavaScript Integration

In this section, we will learn how to use the MyBannerPlugin in the rspack configuration.

Check out the examples/use-plugin/build.js file in the rspack-binding-template repository. We've already created the MyBannerPlugin wrapper in the previous section. So we can use it in the rspack configuration.

const path = require('node:path');

const rspack = require('@rspack-template/test-core');

const compiler = rspack({
  context: __dirname,
  mode: 'development',
  entry: {
    main: './src/index.js',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new rspack.MyBannerPlugin(
      '/** Generated by MyBannerPlugin in `@rspack-template/binding` */'
    ),
  ],
});

compiler.run((err, stats) => {
  if (err) {
    console.error(err);
  }
  console.info(stats.toString({ colors: true }));
});

5. Testing the Plugin

You can now run node examples/use-plugin/build.js to see the plugin in action. Check out the output in the dist/main.js, and you will see the banner comment added to the top of the file:

/** Generated by MyBannerPlugin in `@rspack-template/binding` */(() => { // webpackBootstrap
var __webpack_modules__ = ({
"./src/index.js":
...

This is also the same command as the Verify Setup. But now you have the knowledge of what is happening behind the scene.

Next Steps

In this chapter, we have learned:

  • To create a plugin in Rust and how to expose it to JavaScript using NAPI bindings.
  • To create a JavaScript wrapper for the plugin.
  • To reuse the @rspack/core package to create a new core package to replace the original @rspack/core package.
  • To use the plugin in the rspack configuration.

In the next chapter, we will learn to release the plugin to npm with Github Actions.

Release

This chapter covers releasing your plugin to npm using GitHub Actions. The template includes a complete release workflow that handles building, testing, and publishing automatically.

Release workflow

Prerequisites

Before releasing, ensure you have completed these requirements:

1. Update Repository Information

You must update the repository URLs in your package.json files to match your actual repository, otherwise you'll encounter a sigstore provenance verification error during publishing:

npm error 422 Unprocessable Entity - PUT https://registry.npmjs.org/@your-scope%2fyour-package-darwin-x64
Error verifying sigstore provenance bundle: Failed to validate repository information:
package.json: "repository.url" is "git+https://github.com/rspack-contrib/rspack-binding-template.git",
expected to match "https://github.com/your-username/your-repository" from provenance

Update the following files:

  • package.json - Update the repository.url, bugs.url, and homepage fields
  • crates/binding/package.json - Update the repository.url, bugs.url, and homepage fields

For example, change:

{
  "repository": {
    "type": "git",
    "url": "git+https://github.com/rspack-contrib/rspack-binding-template.git"
  },
  "bugs": {
    "url": "https://github.com/rspack-contrib/rspack-binding-template/issues"
  },
  "homepage": "https://github.com/rspack-contrib/rspack-binding-template#readme"
}

To:

{
  "repository": {
    "type": "git",
    "url": "git+https://github.com/your-username/your-repository.git"
  },
  "bugs": {
    "url": "https://github.com/your-username/your-repository/issues"
  },
  "homepage": "https://github.com/your-username/your-repository#readme"
}

2. Configure NPM Token

The release workflow requires an Environment secret with NPM_TOKEN to be set in the repository settings:

  1. On GitHub, navigate to the main page of the repository.

  2. Under your repository name, click Settings. If you cannot see the "Settings" tab, select the dropdown menu, then click Settings.

Repository settings button
  1. In the left sidebar, click Environments.

  2. Click "New environment" to add a new environment.

Repository settings environments
  1. Type npm as the name for the environment.

  2. Click "Add environment secret".

  3. Enter the name for your secret as "NPM_TOKEN".

  4. Enter the value for your secret.

    Note: If you don't have a token, you can follow this guide to create a new token.

    If you're using "Granular Access Token", make sure to select the "Read and write" scope and select "Only select packages and scopes" and select the scope for the package you want to publish (e.g. @rspack-template).

  5. Click Add secret.

Repository settings environments secrets

1. Create a releasing branch

To release a new version, you need to create a new branch. You can use any branch name you want, but it's recommended to use a name that indicates the version you're releasing.

For example, if you're releasing version 0.0.1, you can create a branch named release-v0.0.1.

git checkout -b release-v0.0.1

2. Trigger a version bump

Before releasing, you need to bump the versions in both package.json and crates/binding/package.json.

rspack-binding-template does not come with any version bump tool. You can either manually bump the versions in both package.jsons or setup any version bump tool.

For example: PR: chore: release v0.0.1

3. Trigger the release workflow

  1. Navigate to ActionsRelease in your repository
  2. Click Run workflow
  3. Configure options:
    • Use workflow from: Select the branch you want to release from. (In this case, it's release-v0.0.1)
    • Dry-run mode: Test without publishing
    • NPM tag: Choose latest, alpha, beta, or canary
  4. Click Run workflow button in the popover.
Release workflow selection

The workflow will be triggered and you can see the progress in the Actions tab.

For example: Release v0.0.1

Release workflow run

Deep Dive into the Workflow

The workflow consists of three sequential jobs:

1. Build

Compiles the Node.js addon for all supported targets using the rspack-toolchain build workflow. The build uses the release profile for optimal performance:

[profile.release]
codegen-units = 1
debug = false
lto = "fat"
opt-level = 3
panic = "abort"
strip = true

2. Test

Validates the built bindings using the test suite to ensure everything works correctly before publishing.

3. Release

Publishes the packages to npm registry:

  1. Environment Setup: Configures Node.js 22, pnpm, and dependency caching
  2. Artifact Processing: Downloads compiled bindings and organizes them into platform-specific npm packages using pnpm napi create-npm-dirs and pnpm napi artifacts
  3. Package Preparation: Configures npm authentication and runs pnpm napi pre-publish to prepare platform packages
  4. Publishing: Uses pnpm publish -r to publish all packages with the specified tag

Package Provenance

All packages published through this workflow include npm provenance statements, which enhance supply-chain security by:

  • Provenance attestation: Publicly links packages to their source code and build instructions, allowing developers to verify where and how packages were built
  • Publish attestation: Generated by npm registry when packages are published by authorized users

The workflow automatically enables provenance using the --provenance flag. Packages are signed by Sigstore public servers and logged in a public transparency ledger, providing verifiable proof of the package's origin and build process.

Supported Targets

The workflow builds for these targets:

x86_64-apple-darwin
x86_64-pc-windows-msvc
x86_64-unknown-linux-gnu
x86_64-unknown-linux-musl
i686-pc-windows-msvc
aarch64-unknown-linux-gnu
aarch64-apple-darwin
aarch64-unknown-linux-musl
aarch64-pc-windows-msvc
armv7-linux-androideabi
armv7-unknown-linux-gnueabihf
aarch64-linux-android

For the complete list, see rspack-toolchain supported targets.

Manual Release

To trigger a release:

  1. Navigate to ActionsRelease in your repository
  2. Click Run workflow
  3. Configure options:
    • Dry-run mode: Test without publishing (recommended first)
    • NPM tag: Choose latest, alpha, beta, or canary

The workflow will automatically build, test, and publish your plugin packages to npm, making them available for installation.