Repository: webpack/sass-loader (JavaScript)

Issue:
Custom importer in `modern` api mode may cause `ERR_INVALID_URL_SCHEME` error.
### Bug report
When passing the following `sassOptions` (as shown in the [SASS documentation](https://sass-lang.com/documentation/js-api/interfaces/Importer)) to sass-loader:

```
const sassOptions = {
  importers: [
    {
      canonicalize(url) {
        if (!url.startsWith('bgcolor:')) return null;
        return new URL(url);
      },
      load(canonicalUrl) {
        return {
          contents: `body {background-color: ${canonicalUrl.pathname}}`,
          syntax: 'scss',
        };
      },
    },
  ],
};
```

In `sass` file usage looks like this:

```
@import 'vars';
// this is import by custom importer
@import 'bgcolor:cornflowerblue';

:root {
  --color-fg: $color-fg;
}
```

### Actual Behavior

when running `webpack build`, the following error is thrown:

```
TypeError [ERR_INVALID_URL_SCHEME]: The URL must be of scheme file
....
code: 'ERR_INVALID_URL_SCHEME'
```

### Expected Behavior

The above error should not occur, as is the case in legacy mode (as seen in the reproduced repository).

### How Do We Reproduce?

Reproduce repo:

https://stackblitz.com/edit/sass-loader-err-invalid-url-scheme?file=package.json


- Run this command to reproduce.

```bash
npm run webpack:modern
```

- No error in legacy mode.

```bash
npm run webpack:legacy
```

- Compile with sass to show loaded urls.

```bash
npm run sass
````

### Reason and fix

#### reason

In legacy mode, the `sass.render` method would return included file paths as `Array<string>`. In modern mode, the `compile` method returns `Array<URL>` .

![view result in reproduce repo](https://user-images.githubusercontent.com/13414731/225858702-ca2b857b-d8b1-4f5a-a999-625308006224.png).

Thus, the _file_ `@import 'bgcolor:cornflowerblue'`, imported via a custom importer, will be treated as `URL("bgcolor:cornflowerblue")`, with the `protocol` as `bgcolor:`. Then, line 101 uses `url.fileURLToPath` to normalize the file path, but `fileURLToPath` throws an `ERR_INVALID_URL_SCHEME` error if the `protocol` is not `file:` (see [`fileURLToPath` source](https://github.com/nodejs/node/blob/c3537ae8cc220d58d95bcdf2fd52046c314c4803/lib/internal/url.js#L1178-L1186)).



https://github.com/webpack-contrib/sass-loader/blob/de29518c568b37ca63f703b3cb8717f9fe12091d/src/index.js#L98-L108


In legacy mode, line 115 will not throw an error and line 118 will get `false`, so the error is not encountered.

https://github.com/webpack-contrib/sass-loader/blob/de29518c568b37ca63f703b3cb8717f9fe12091d/src/index.js#L110-L122

#### fix 

Adding `result.loadedUrls.filter(url=>url.protocol === 'file')....` to line 100 can be a possible fix to ignore invalid URLs.

### Please paste the results of `npx webpack-cli info` here, and mention other relevant information

Okay, but may be not useful for this issue.
``` 
System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
  Binaries:
    Node: 16.14.2 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 7.17.0 - /usr/local/bin/npm
  Packages:
    css-loader: ^6.7.3 => 6.7.3 
    sass-loader: ^13.2.0 => 13.2.0 
    style-loader: ^3.3.2 => 3.3.2 
    webpack: ^5.38.1 => 5.76.2 
    webpack-cli: ^4.7.2 => 4.10.0 
```


Discussion:
Yeah, modern API is still experiments, do you want to send a PR with fix?
https://github.com/webpack-contrib/sass-loader/releases/tag/v13.2.1
> https://github.com/webpack-contrib/sass-loader/releases/tag/v13.2.1

I apologize for the delayed response. It seems that you have already fixed it. Thank you very much. I also learnt a lot from your PR. Thank you again. 
@alexander-akait 
I am very sorry for the typo in my original post, which misled you (the `protocol` property in `new URL("file://index.scss")` should be `file:` instead of `file`).

 I truly apologize for that. 

I just found out that the fix for this issue, while solving the problem, has caused a new problem - no files are added to the dependencies so the nested imported SCSS files will not be watched. because the array is empty after being filtered.

https://github.com/webpack-contrib/sass-loader/blob/4dd92dbad0d6b61b73fa08fdf0323f122c849c97/src/index.js#L101


I'm very sorry that my carelessness has caused such a big problem.

I have created a PR here: https://github.com/webpack-contrib/sass-loader/pull/1125 

Thank you!



Patch:
diff --git a/.cspell.json b/.cspell.json
index b85d24a8..0ad02b86 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -15,7 +15,8 @@
     "klona",
     "sharename",
     "wekbit",
-    "commitlint"
+    "commitlint",
+    "bgcolor"
   ],
 
   "ignorePaths": [
diff --git a/lint-staged.config.js b/lint-staged.config.js
index 4e51a30d..372e7ffc 100644
--- a/lint-staged.config.js
+++ b/lint-staged.config.js
@@ -1,4 +1,4 @@
 module.exports = {
-  "*": ["prettier --write --ignore-unknown", "cspell"],
+  "*": ["prettier --write --ignore-unknown", "cspell --no-must-find-files"],
   "*.js": ["eslint --cache --fix"],
 };
diff --git a/src/index.js b/src/index.js
index 2b0a0a0e..6dbcedf3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -97,14 +97,16 @@ async function loader(content) {
 
   // Modern API
   if (typeof result.loadedUrls !== "undefined") {
-    result.loadedUrls.forEach((includedFile) => {
-      const normalizedIncludedFile = url.fileURLToPath(includedFile);
-
-      // Custom `importer` can return only `contents` so includedFile will be relative
-      if (path.isAbsolute(normalizedIncludedFile)) {
-        this.addDependency(normalizedIncludedFile);
-      }
-    });
+    result.loadedUrls
+      .filter((url) => url.protocol === "file")
+      .forEach((includedFile) => {
+        const normalizedIncludedFile = url.fileURLToPath(includedFile);
+
+        // Custom `importer` can return only `contents` so includedFile will be relative
+        if (path.isAbsolute(normalizedIncludedFile)) {
+          this.addDependency(normalizedIncludedFile);
+        }
+      });
   }
   // Legacy API
   else if (


Test patch:
diff --git a/test/__snapshots__/sassOptions-option.test.js.snap b/test/__snapshots__/sassOptions-option.test.js.snap
index b270af18..55bee5f2 100644
--- a/test/__snapshots__/sassOptions-option.test.js.snap
+++ b/test/__snapshots__/sassOptions-option.test.js.snap
@@ -4158,6 +4158,22 @@ exports[`sassOptions option should work when the option like "Object" ('sass-emb
 
 exports[`sassOptions option should work when the option like "Object" ('sass-embedded', 'modern' API, 'scss' syntax): warnings 1`] = `[]`;
 
+exports[`sassOptions option should work with custom scheme import ('dart-sass', 'modern' API, 'sass' syntax): errors 1`] = `[]`;
+
+exports[`sassOptions option should work with custom scheme import ('dart-sass', 'modern' API, 'sass' syntax): warnings 1`] = `[]`;
+
+exports[`sassOptions option should work with custom scheme import ('dart-sass', 'modern' API, 'scss' syntax): errors 1`] = `[]`;
+
+exports[`sassOptions option should work with custom scheme import ('dart-sass', 'modern' API, 'scss' syntax): warnings 1`] = `[]`;
+
+exports[`sassOptions option should work with custom scheme import ('sass-embedded', 'modern' API, 'sass' syntax): errors 1`] = `[]`;
+
+exports[`sassOptions option should work with custom scheme import ('sass-embedded', 'modern' API, 'sass' syntax): warnings 1`] = `[]`;
+
+exports[`sassOptions option should work with custom scheme import ('sass-embedded', 'modern' API, 'scss' syntax): errors 1`] = `[]`;
+
+exports[`sassOptions option should work with custom scheme import ('sass-embedded', 'modern' API, 'scss' syntax): warnings 1`] = `[]`;
+
 exports[`sassOptions option should work with the "fiber" option ('dart-sass', 'legacy' API, 'sass' syntax): css 1`] = `
 "@charset "UTF-8";
 @import "./file.css";
diff --git a/test/sass/modern-vars.sass b/test/sass/modern-vars.sass
new file mode 100644
index 00000000..35c06b47
--- /dev/null
+++ b/test/sass/modern-vars.sass
@@ -0,0 +1,1 @@
+$color-fg: #000
diff --git a/test/sass/modern.sass b/test/sass/modern.sass
new file mode 100644
index 00000000..3f38808d
--- /dev/null
+++ b/test/sass/modern.sass
@@ -0,0 +1,6 @@
+@import 'modern-vars'
+// this is import by custom importer
+@import 'bgcolor:cornflowerblue'
+
+a
+  --color-fg: $color-fg
diff --git a/test/sassOptions-option.test.js b/test/sassOptions-option.test.js
index bac298e5..07a65d26 100644
--- a/test/sassOptions-option.test.js
+++ b/test/sassOptions-option.test.js
@@ -149,6 +149,39 @@ describe("sassOptions option", () => {
           expect(getWarnings(stats)).toMatchSnapshot("warnings");
           expect(getErrors(stats)).toMatchSnapshot("errors");
         });
+
+        it(`should work with custom scheme import ('${implementationName}', '${api}' API, '${syntax}' syntax)`, async () => {
+          const testId = getTestId("modern", syntax);
+          const options = {
+            implementation,
+            api,
+            sassOptions: {
+              // https://sass-lang.com/documentation/js-api/interfaces/Importer
+              importers: [
+                {
+                  canonicalize(url) {
+                    if (!url.startsWith("bgcolor:")) {
+                      return null;
+                    }
+
+                    return new URL(url);
+                  },
+                  load(canonicalUrl) {
+                    return {
+                      contents: `body {background-color: ${canonicalUrl.pathname}}`,
+                      syntax: "scss",
+                    };
+                  },
+                },
+              ],
+            },
+          };
+          const compiler = getCompiler(testId, { loader: { options } });
+          const stats = await compile(compiler);
+
+          expect(getWarnings(stats)).toMatchSnapshot("warnings");
+          expect(getErrors(stats)).toMatchSnapshot("errors");
+        });
       } else {
         it(`should ignore the "file" option ('${implementationName}', '${api}' API, '${syntax}' syntax)`, async () => {
           const testId = getTestId("language", syntax);
diff --git a/test/scss/modern-vars.scss b/test/scss/modern-vars.scss
new file mode 100644
index 00000000..a46f606a
--- /dev/null
+++ b/test/scss/modern-vars.scss
@@ -0,0 +1,1 @@
+$color-fg: #000;
diff --git a/test/scss/modern.scss b/test/scss/modern.scss
new file mode 100644
index 00000000..e06a74ce
--- /dev/null
+++ b/test/scss/modern.scss
@@ -0,0 +1,7 @@
+@import 'modern-vars';
+// this is import by custom importer
+@import 'bgcolor:cornflowerblue';
+
+a {
+  --color-fg: $color-fg;
+}