DEV Community

Cover image for Implementing Webpack from Scratch, But in Rust - [5] Support Customized JS Plugin
ayou
ayou

Posted on

5 1 1 1 1

Implementing Webpack from Scratch, But in Rust - [5] Support Customized JS Plugin

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/6

The previous article implemented the Plugin system on the Rust side, but left a loose end on how to integrate plugins developed by users using JavaScript into rs-webpack. This article will cover the implementation.

For example, if I have developed a plugin in JavaScript, how can I make it work?

module.exports = class MyPlugin {
    apply(compiler) {
        compiler.hooks.beforeRun.tap('myplugin', (compiler) => {
            console.log("before run", compiler)
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Since we have already referenced rspack earlier, let's continue to follow its lead. After studying it, I found that its approach is roughly as shown in the following diagram:

Image description

Our custom JS plugin myPlugin will be passed from rs-webpack-cli to rs-webpack-core. In rs-webpack-core, it uses a library called @rspack/lite-tapable, developed by the rspack team, to create a beforeRun Hook:

export class Compiler {
    bindingRsWebpack: BindingRsWebpack
    hooks: {
        beforeRun: liteTapable.SyncHook<[string]>;
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Similar to Rust, when the Compiler is initialized, it iterates through all the plugins and executes their apply methods:

constructor(props: RawConfig) {
    const {plugins} = props
    plugins.forEach(plugin => {
        plugin.apply(this)
    })
}
Enter fullscreen mode Exit fullscreen mode

Then, through a series of operations, it wraps a function called register_before_run_taps and passes it to Rust. register_before_run_taps wraps the call to the beforeRun Hook's call function:

this.registers = {
    registerBeforeRunTaps: this.#createHookRegisterTaps(
        RegisterJsTapKind.BeforeRun,
        () => this.hooks.beforeRun,
        queried => (native: string) => {
            // beforeRun.call 
            queried.call(native);
        }
    ),
}
this.bindingRsWebpack = new BindingRsWebpack(props, this.registers)
Enter fullscreen mode Exit fullscreen mode

After this function is executed, it returns an array, and each element in the array can serve as an interceptor for the before_run Hook in Rust (only the call method is implemented):

#[async_trait]
impl Interceptor<BeforeRunHook> for RegisterBeforeRunTaps {
  async fn call(
    &self,
    hook: &BeforeRunHook,
  ) -> rswebpack_error::Result<Vec<<BeforeRunHook as Hook>::Tap>> {
    if let Some(non_skippable_registers) = &self.inner.non_skippable_registers {
      if !non_skippable_registers.is_non_skippable(&RegisterJsTapKind::BeforeRun) {
        return Ok(Vec::new());
      }
    }
    let js_taps = self.inner.call_register(hook).await?;
    let js_taps = js_taps
      .iter()
      .map(|t| Box::new(BeforeRunTap::new(t.clone())) as <BeforeRunHook as Hook>::Tap)
      .collect();
    Ok(js_taps)
  }
}
Enter fullscreen mode Exit fullscreen mode

In rswebpack_binding, these interceptors are applied through the JsHooksAdapterPlugin:

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

  fn apply(&self, _ctx: PluginContext<&mut ApplyContext>) -> rswebpack_error::Result<()> {
    _ctx
      .context
      .compiler_hooks
      .before_run
      .intercept(self.register_before_run_taps.clone());
  }
}
Enter fullscreen mode Exit fullscreen mode

PS: The call function in the interceptor is executed each time the call function of the Hook is invoked. For example, in the following example:

const hook = new SyncHook(['arg1', 'arg2'])

hook.tap('test', (...args) => {
  console.log('test', ...args)
})

hook.intercept({
  // trigger when execute hook.call
  call: (...args) => {
    console.log('Execute interceptor call', ...args)
  },
})

hook.call('a1', 'a2')

// log
Execute interceptor call a1 a2
test a1 a2
Enter fullscreen mode Exit fullscreen mode

When the before_run in Rust calls call, these interceptors' call functions will also be executed, and then the beforeRun.call wrapped in these interceptors on the JS side will be executed, triggering the execution of the corresponding Tap function in myPlugin.

With these steps, the entire Plugin system is completed. The complete changes can be seen here. I won't go through the code one by one, but by following the order in the diagram, you should be able to understand it.

The original intention of this series of articles was to deepen the understanding of webpack by reimplementing it. However, I found that my Rust skills were limited, and I didn't have the ability to implement a Plugin system. Most of the time was spent on integrating Rspack.

During this process, I realized that there are many areas that I don't understand well. I'll mark them down for future study:

  • Napi, such as ThreadsafeFunction. Combining this with Node.js can achieve many things. I'll see if I can come up with some examples later.
  • Asynchronous processing and tokio in Rust.
  • Rust concurrent programming: multithreading, channels, etc.
  • Macro programming in Rust, which is difficult to write and debug.

Please kindly give me a star!

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay