From 10387c9d0817cd14cbfa66e2e8cab0c82ce8feda Mon Sep 17 00:00:00 2001 From: "Henry Q. Dineen" Date: Thu, 19 Mar 2026 00:25:47 -0400 Subject: [PATCH 1/6] fix: panic in add_entry_batch when loader calls importModule When `compilation.addEntry()` is called from the JS `make` hook, `add_entry_batch` eagerly builds modules via `update_module_graph` to match webpack's callback semantics (the callback receives the built module). However, the `ModuleExecutor` event channel was never initialized for this code path, so any loader that calls `this.importModule()` during the build would panic: panicked at crates/rspack_core/src/compilation/build_module_graph/module_executor/mod.rs should have event sender The normal build path (`do_build_module_graph`) calls `before_build_module_graph` which sets up the channel before `update_module_graph` runs. `add_entry_batch` skipped this step. This fix ensures the module executor is started before the eager build in `add_entry_batch` and torn down afterward so that `build_module_graph_pass` can set it up fresh. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../build_module_graph/module_executor/mod.rs | 4 ++ crates/rspack_core/src/compilation/mod.rs | 19 ++++++++ .../add-entry-import-module/data.js | 1 + .../add-entry-import-module/entry.js | 2 + .../add-entry-import-module/index.js | 3 ++ .../add-entry-import-module/loader.js | 7 +++ .../add-entry-import-module/rspack.config.js | 45 +++++++++++++++++++ 7 files changed, 81 insertions(+) create mode 100644 tests/rspack-test/configCases/compilation/add-entry-import-module/data.js create mode 100644 tests/rspack-test/configCases/compilation/add-entry-import-module/entry.js create mode 100644 tests/rspack-test/configCases/compilation/add-entry-import-module/index.js create mode 100644 tests/rspack-test/configCases/compilation/add-entry-import-module/loader.js create mode 100644 tests/rspack-test/configCases/compilation/add-entry-import-module/rspack.config.js diff --git a/crates/rspack_core/src/compilation/build_module_graph/module_executor/mod.rs b/crates/rspack_core/src/compilation/build_module_graph/module_executor/mod.rs index 3295e29ab7d4..b089def62ae6 100644 --- a/crates/rspack_core/src/compilation/build_module_graph/module_executor/mod.rs +++ b/crates/rspack_core/src/compilation/build_module_graph/module_executor/mod.rs @@ -63,6 +63,10 @@ impl Default for ModuleExecutor { } impl ModuleExecutor { + pub fn is_running(&self) -> bool { + self.event_sender.is_some() + } + pub async fn before_build_module_graph(&mut self, compilation: &Compilation) -> Result<()> { let mut make_artifact = self.make_artifact.steal(); let mut exports_info_artifact = self.exports_info_artifact.steal(); diff --git a/crates/rspack_core/src/compilation/mod.rs b/crates/rspack_core/src/compilation/mod.rs index 930f321ebaf2..9036edfe28f4 100644 --- a/crates/rspack_core/src/compilation/mod.rs +++ b/crates/rspack_core/src/compilation/mod.rs @@ -652,6 +652,17 @@ impl Compilation { self.add_entry(entry, options).await?; } + // ensure module executor is running so loaders can call `importModule` + let started_executor = match &self.module_executor { + Some(me) if !me.is_running() => { + let mut module_executor = self.module_executor.take().expect("checked above"); + module_executor.before_build_module_graph(self).await?; + self.module_executor = Some(module_executor); + true + } + _ => false, + }; + let make_artifact = self.build_module_graph_artifact.steal(); let exports_info_artifact = self.exports_info_artifact.steal(); let (make_artifact, exports_info_artifact) = update_module_graph( @@ -672,6 +683,14 @@ impl Compilation { self.build_module_graph_artifact = make_artifact.into(); self.exports_info_artifact = exports_info_artifact.into(); + // tear down the executor so build_module_graph_pass can set it up fresh + if started_executor { + if let Some(mut module_executor) = self.module_executor.take() { + module_executor.after_build_module_graph(self).await?; + self.module_executor = Some(module_executor); + } + } + Ok(()) } diff --git a/tests/rspack-test/configCases/compilation/add-entry-import-module/data.js b/tests/rspack-test/configCases/compilation/add-entry-import-module/data.js new file mode 100644 index 000000000000..ee83bb053e7c --- /dev/null +++ b/tests/rspack-test/configCases/compilation/add-entry-import-module/data.js @@ -0,0 +1 @@ +export default "hello from imported module"; diff --git a/tests/rspack-test/configCases/compilation/add-entry-import-module/entry.js b/tests/rspack-test/configCases/compilation/add-entry-import-module/entry.js new file mode 100644 index 000000000000..7a917cddbbf3 --- /dev/null +++ b/tests/rspack-test/configCases/compilation/add-entry-import-module/entry.js @@ -0,0 +1,2 @@ +import data from "./data.js"; +export default data; diff --git a/tests/rspack-test/configCases/compilation/add-entry-import-module/index.js b/tests/rspack-test/configCases/compilation/add-entry-import-module/index.js new file mode 100644 index 000000000000..491cdce92f64 --- /dev/null +++ b/tests/rspack-test/configCases/compilation/add-entry-import-module/index.js @@ -0,0 +1,3 @@ +it("should have built the dynamically added entry", () => { + expect(__STATS__.compilation.namedChunks).toHaveProperty("dynamic"); +}); diff --git a/tests/rspack-test/configCases/compilation/add-entry-import-module/loader.js b/tests/rspack-test/configCases/compilation/add-entry-import-module/loader.js new file mode 100644 index 000000000000..4465ab561fc2 --- /dev/null +++ b/tests/rspack-test/configCases/compilation/add-entry-import-module/loader.js @@ -0,0 +1,7 @@ +/** @type {import("@rspack/core").PitchLoaderDefinitionFunction} */ +exports.pitch = async function (remaining) { + const result = await this.importModule( + this.resourcePath + ".webpack[javascript/auto]" + "!=!" + remaining + ); + return result.default || result; +}; diff --git a/tests/rspack-test/configCases/compilation/add-entry-import-module/rspack.config.js b/tests/rspack-test/configCases/compilation/add-entry-import-module/rspack.config.js new file mode 100644 index 000000000000..7a395668c044 --- /dev/null +++ b/tests/rspack-test/configCases/compilation/add-entry-import-module/rspack.config.js @@ -0,0 +1,45 @@ +const path = require("path"); + +const PLUGIN_NAME = "AddEntryWithImportModulePlugin"; + +class AddEntryWithImportModulePlugin { + /** + * @param {import("@rspack/core").Compiler} compiler + */ + apply(compiler) { + const { EntryPlugin } = compiler.rspack; + + compiler.hooks.make.tapPromise(PLUGIN_NAME, compilation => { + return new Promise((resolve, reject) => { + compilation.addEntry( + compiler.context, + EntryPlugin.createDependency( + path.resolve(__dirname, "entry.js") + ), + { name: "dynamic" }, + (err) => { + if (err) reject(err); + else resolve(); + } + ); + }); + }); + } +} + +/**@type {import("@rspack/core").Configuration}*/ +module.exports = { + output: { + filename: "[name].js" + }, + module: { + rules: [ + { + test: /entry\.js$/, + use: [{ loader: "./loader", options: {} }], + type: "asset/source" + } + ] + }, + plugins: [new AddEntryWithImportModulePlugin()] +}; From 164e9f5f33bbbac4b4ff856747be294be3ece4bf Mon Sep 17 00:00:00 2001 From: "Henry Q. Dineen" Date: Thu, 19 Mar 2026 00:37:24 -0400 Subject: [PATCH 2/6] fix: tear down module executor on error path Defer the `?` on `update_module_graph` result until after the executor is torn down, so the background task isn't orphaned if the build fails. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rspack_core/src/compilation/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/rspack_core/src/compilation/mod.rs b/crates/rspack_core/src/compilation/mod.rs index 9036edfe28f4..f029556a5d36 100644 --- a/crates/rspack_core/src/compilation/mod.rs +++ b/crates/rspack_core/src/compilation/mod.rs @@ -665,7 +665,7 @@ impl Compilation { let make_artifact = self.build_module_graph_artifact.steal(); let exports_info_artifact = self.exports_info_artifact.steal(); - let (make_artifact, exports_info_artifact) = update_module_graph( + let build_result = update_module_graph( self, make_artifact, exports_info_artifact, @@ -679,9 +679,7 @@ impl Compilation { .collect(), )], ) - .await?; - self.build_module_graph_artifact = make_artifact.into(); - self.exports_info_artifact = exports_info_artifact.into(); + .await; // tear down the executor so build_module_graph_pass can set it up fresh if started_executor { @@ -691,6 +689,10 @@ impl Compilation { } } + let (make_artifact, exports_info_artifact) = build_result?; + self.build_module_graph_artifact = make_artifact.into(); + self.exports_info_artifact = exports_info_artifact.into(); + Ok(()) } From a66beba0d020811d9e9f3f8b91f2fb103990da1b Mon Sep 17 00:00:00 2001 From: "Henry Q. Dineen" Date: Thu, 19 Mar 2026 00:40:12 -0400 Subject: [PATCH 3/6] fix: restore artifacts before executor teardown after_build_module_graph reads compilation.build_module_graph_artifact, so the artifacts must be restored before teardown. On error, reset to defaults to avoid reading stolen state. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rspack_core/src/compilation/mod.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/rspack_core/src/compilation/mod.rs b/crates/rspack_core/src/compilation/mod.rs index f029556a5d36..16bf2980a8a1 100644 --- a/crates/rspack_core/src/compilation/mod.rs +++ b/crates/rspack_core/src/compilation/mod.rs @@ -681,6 +681,20 @@ impl Compilation { ) .await; + // restore artifacts before teardown — after_build_module_graph reads them + match build_result { + Ok((make_artifact, exports_info_artifact)) => { + self.build_module_graph_artifact = make_artifact.into(); + self.exports_info_artifact = exports_info_artifact.into(); + } + Err(ref _e) => { + // artifacts were consumed by update_module_graph; restore defaults + // so after_build_module_graph doesn't read stolen state + self.build_module_graph_artifact = Default::default(); + self.exports_info_artifact = Default::default(); + } + } + // tear down the executor so build_module_graph_pass can set it up fresh if started_executor { if let Some(mut module_executor) = self.module_executor.take() { @@ -689,11 +703,7 @@ impl Compilation { } } - let (make_artifact, exports_info_artifact) = build_result?; - self.build_module_graph_artifact = make_artifact.into(); - self.exports_info_artifact = exports_info_artifact.into(); - - Ok(()) + build_result.map(|_| ()) } pub async fn add_include(&mut self, args: Vec<(BoxDependency, EntryOptions)>) -> Result<()> { From 9cf8f9ace8d576423ffabf8c86919615eff944ff Mon Sep 17 00:00:00 2001 From: "Henry Q. Dineen" Date: Thu, 19 Mar 2026 00:43:28 -0400 Subject: [PATCH 4/6] style: use build_result? + Ok(()) to match existing convention Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rspack_core/src/compilation/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/rspack_core/src/compilation/mod.rs b/crates/rspack_core/src/compilation/mod.rs index 16bf2980a8a1..bdaebb11bd79 100644 --- a/crates/rspack_core/src/compilation/mod.rs +++ b/crates/rspack_core/src/compilation/mod.rs @@ -703,7 +703,9 @@ impl Compilation { } } - build_result.map(|_| ()) + build_result?; + + Ok(()) } pub async fn add_include(&mut self, args: Vec<(BoxDependency, EntryOptions)>) -> Result<()> { From 9c3351ed01aeab9af89a0a9943a1ef170a8f5a99 Mon Sep 17 00:00:00 2001 From: "Henry Q. Dineen" Date: Thu, 19 Mar 2026 00:49:05 -0400 Subject: [PATCH 5/6] fix: compile error and clean up error path handling Use StealCell::new instead of Default, move artifacts instead of cloning on the success path, and simplify the match. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rspack_core/src/compilation/mod.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/rspack_core/src/compilation/mod.rs b/crates/rspack_core/src/compilation/mod.rs index bdaebb11bd79..ac55cfab11f3 100644 --- a/crates/rspack_core/src/compilation/mod.rs +++ b/crates/rspack_core/src/compilation/mod.rs @@ -682,18 +682,18 @@ impl Compilation { .await; // restore artifacts before teardown — after_build_module_graph reads them - match build_result { + let build_result = match build_result { Ok((make_artifact, exports_info_artifact)) => { self.build_module_graph_artifact = make_artifact.into(); self.exports_info_artifact = exports_info_artifact.into(); + Ok(()) } - Err(ref _e) => { - // artifacts were consumed by update_module_graph; restore defaults - // so after_build_module_graph doesn't read stolen state - self.build_module_graph_artifact = Default::default(); - self.exports_info_artifact = Default::default(); + Err(e) => { + self.build_module_graph_artifact = StealCell::new(BuildModuleGraphArtifact::new()); + self.exports_info_artifact = StealCell::new(ExportsInfoArtifact::default()); + Err(e) } - } + }; // tear down the executor so build_module_graph_pass can set it up fresh if started_executor { @@ -703,9 +703,7 @@ impl Compilation { } } - build_result?; - - Ok(()) + build_result } pub async fn add_include(&mut self, args: Vec<(BoxDependency, EntryOptions)>) -> Result<()> { From 1ecfff2873a48c28db235745738b87e3dae16797 Mon Sep 17 00:00:00 2001 From: "Henry Q. Dineen" Date: Thu, 19 Mar 2026 01:15:38 -0400 Subject: [PATCH 6/6] fix: collapse nested if to satisfy clippy Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rspack_core/src/compilation/mod.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/rspack_core/src/compilation/mod.rs b/crates/rspack_core/src/compilation/mod.rs index ac55cfab11f3..15264b4ea7f2 100644 --- a/crates/rspack_core/src/compilation/mod.rs +++ b/crates/rspack_core/src/compilation/mod.rs @@ -696,11 +696,9 @@ impl Compilation { }; // tear down the executor so build_module_graph_pass can set it up fresh - if started_executor { - if let Some(mut module_executor) = self.module_executor.take() { - module_executor.after_build_module_graph(self).await?; - self.module_executor = Some(module_executor); - } + if started_executor && let Some(mut module_executor) = self.module_executor.take() { + module_executor.after_build_module_graph(self).await?; + self.module_executor = Some(module_executor); } build_result