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)
})
}
}
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:
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]>;
}
...
}
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)
})
}
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)
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)
}
}
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());
}
}
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
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!
Top comments (0)