Repository: webpack/sass-loader (JavaScript)

Issue:
Extra `sass-embedded` compilers are not disposed
### Bug report

When using the `sass-embedded` with the `modern-compiler` API, `sass-loader` may create multiple compilers (= make multiple `initAsyncCompiler` calls). Before the created compiler is saved to `sassModernCompilers`, there is a check to handle race conditions:
https://github.com/webpack-contrib/sass-loader/blob/c75c6061266a6df6a7c736de407fcbf05ef5b2be/src/utils.js#L764-L766

But in the case when `sassModernCompilers.has(webpackCompiler) === true`, the newly created `compiler` is not disposed (i.e. `compiler.dispose()` is not called), causing `sass-embedded` to never shut down the `dart sass.snapshot --embedded` process it started, causing the Node.js process to never exit due to alive `child_process`es.

### Actual Behavior

Node.js process does not exit due to alive child processes

### Expected Behavior

If `sassModernCompilers` already has a compiler, should `compiler.dispose()` be called for the newly created compiler? Something like

```diff
 if (!sassModernCompilers.has(webpackCompiler)) {
   sassModernCompilers.set(webpackCompiler, compiler);
   webpackCompiler.hooks.shutdown.tap("sass-loader", () => {
     compiler.dispose();
   });
-}
+} else {
+  compiler.dispose();
+}
```

### How Do We Reproduce?

Unfortunately, even on our closed project with a large number of files, the bug does not reproduce consistently. But it can be easily reproduced by adding a small delay: https://github.com/hikiko4ern/sass-loader-compiler/blob/master/patches/sass-loader.patch

1. clone https://github.com/hikiko4ern/sass-loader-compiler
2. run `pnpm i`
3. run `pnpm webpack`

It should hang after
> webpack 5.95.0 compiled successfully in 233 ms

### Please paste the results of `npx webpack-cli info` here, and mention other relevant information

```
System:
  OS: Linux 5.15 Debian GNU/Linux trixie/sid
  CPU: (32) x64 AMD Ryzen 9 7950X3D 16-Core Processor
  Memory: 53.94 GB / 64.76 GB
Binaries:
  Node: 22.11.0 - /run/user/1000/fnm_multishells/1007232_1730374215459/bin/node
  npm: 10.9.0 - /run/user/1000/fnm_multishells/1007232_1730374215459/bin/npm
Packages:
  sass-loader: ^16.0.2 => 16.0.2
  webpack: ^5.95.0 => 5.95.0
  webpack-cli: ^5.1.4 => 5.1.4
```


Discussion:
@hikiko4ern pr welcome
After upgrading sass-loader to 16.0.3 I still experience the issue where the rspack build process does not exit, something is still holding it up.
Maybe rspack doesn't support https://github.com/webpack-contrib/sass-loader/blob/master/src/utils.js#L768
Yes, I am experiencing the same issue - after successful build the process never exits. If I change to `api: "modern"` all is good



Patch:
diff --git a/src/utils.js b/src/utils.js
index 76d22b36..4ea78ce6 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -768,6 +768,8 @@ function getCompileFn(loaderContext, implementation, apiType) {
               webpackCompiler.hooks.shutdown.tap("sass-loader", () => {
                 compiler.dispose();
               });
+            } else {
+              compiler.dispose();
             }
           }
 


Test patch:
diff --git a/test/__snapshots__/implementation-option.test.js.no-node-sass.snap b/test/__snapshots__/implementation-option.test.js.no-node-sass.snap
index 68f8787b..9ac59734 100644
--- a/test/__snapshots__/implementation-option.test.js.no-node-sass.snap
+++ b/test/__snapshots__/implementation-option.test.js.no-node-sass.snap
@@ -48,6 +48,10 @@ exports[`implementation option not specify: errors 1`] = `[]`;
 
 exports[`implementation option not specify: warnings 1`] = `[]`;
 
+exports[`implementation option should dispose redundant compilers for \`modern-compiler\`: errors 1`] = `[]`;
+
+exports[`implementation option should dispose redundant compilers for \`modern-compiler\`: warnings 1`] = `[]`;
+
 exports[`implementation option should not swallow an error when trying to load a sass implementation: errors 1`] = `
 [
   "ModuleBuildError: Module build failed (from ../src/cjs.js):
diff --git a/test/__snapshots__/implementation-option.test.js.snap b/test/__snapshots__/implementation-option.test.js.snap
index 21d95e88..6f8b6c18 100644
--- a/test/__snapshots__/implementation-option.test.js.snap
+++ b/test/__snapshots__/implementation-option.test.js.snap
@@ -52,6 +52,10 @@ exports[`implementation option not specify: errors 1`] = `[]`;
 
 exports[`implementation option not specify: warnings 1`] = `[]`;
 
+exports[`implementation option should dispose redundant compilers for \`modern-compiler\`: errors 1`] = `[]`;
+
+exports[`implementation option should dispose redundant compilers for \`modern-compiler\`: warnings 1`] = `[]`;
+
 exports[`implementation option should not swallow an error when trying to load a sass implementation: errors 1`] = `
 [
   "ModuleBuildError: Module build failed (from ../src/cjs.js):
diff --git a/test/implementation-option.test.js b/test/implementation-option.test.js
index ba11835c..5e699544 100644
--- a/test/implementation-option.test.js
+++ b/test/implementation-option.test.js
@@ -398,6 +398,70 @@ describe("implementation option", () => {
     await close(compiler);
   });
 
+  it("should dispose redundant compilers for `modern-compiler`", async () => {
+    sassEmbeddedCompilerSpies.mockRestore();
+
+    let isInRace = false;
+
+    let firstDisposeSpy;
+    let secondDisposeSpy;
+
+    const actualFn = sassEmbedded.initAsyncCompiler.bind(sassEmbedded);
+
+    const initSpy = jest
+      .spyOn(sassEmbedded, "initAsyncCompiler")
+      .mockImplementation(async () => {
+        const compiler = await actualFn();
+
+        if (!isInRace) {
+          firstDisposeSpy = jest.spyOn(compiler, "dispose");
+          isInRace = true;
+
+          return new Promise((resolve) => {
+            const interval = setInterval(() => {
+              if (!isInRace) {
+                clearInterval(interval);
+                resolve(compiler);
+              }
+            });
+          });
+        }
+
+        isInRace = false;
+        secondDisposeSpy = jest.spyOn(compiler, "dispose");
+
+        return compiler;
+      });
+
+    const testId1 = getTestId("language", "scss");
+    const testId2 = getTestId("language", "sass");
+    const options = { api: "modern-compiler" };
+
+    // eslint-disable-next-line no-undefined
+    const compiler = getCompiler(undefined, {
+      entry: {
+        one: `./${testId1}`,
+        two: `./${testId2}`,
+      },
+      loader: { options },
+    });
+    const stats = await compile(compiler);
+
+    expect(getWarnings(stats)).toMatchSnapshot("warnings");
+    expect(getErrors(stats)).toMatchSnapshot("errors");
+    expect(initSpy).toHaveBeenCalledTimes(2);
+
+    await close(compiler);
+
+    initSpy.mockRestore();
+
+    expect(firstDisposeSpy).toHaveBeenCalledTimes(1);
+    firstDisposeSpy.mockRestore();
+
+    expect(secondDisposeSpy).toHaveBeenCalledTimes(1);
+    secondDisposeSpy.mockRestore();
+  });
+
   it("should try to load using valid order", async () => {
     jest.doMock("sass", () => {
       const error = new Error("Some error sass");