Repository: webpack-contrib/source-map-loader (js)

Issue:
Failed to parse source map from [path] warning
<!--
  Issues are so 🔥

  If you remove or skip this template, you'll make the 🐼 sad and the mighty god
  of Github will appear and pile-drive the close button from a great height
  while making animal noises.

  👉🏽 Need support, advice, or help? Don't open an issue!
  Head to StackOverflow or https://gitter.im/webpack/webpack.
-->

- Operating System: macOS Catalina `10.15.6`
- Node Version: `12.18.2`
- NPM Version: `6.14.5`
- Yarn version: `1.22.4`
- webpack Version: `4.43.0`
- source-map-loader version: `1.0.1`

### Actual Behavior

`Failed to parse source map from [path] file: Error: ENOENT: no such file or directory, open [path]` warnings are logged in the terminal (as well in the browser console log).

#### Remarks
- It failed to load all [`standardized-audio-context`](https://www.npmjs.com/package/standardized-audio-context) package source map files
- Seems that `source-map-loader` treats the path `//# sourceMappingURL=/build/es2019/constants.js.map` as an absolute path, what cause the failure `Error: ENOENT: no such file or directory`
- For my test, removing the prefixed slash `/` from the path `/build/es2019/constants.js.map` fixes the issue, so:
```js
// not work 😢
//# sourceMappingURL=/build/es2019/constants.js.map

// work 😍
//# sourceMappingURL=build/es2019/constants.js.map
```

### Expected Behavior

No `Failed to parse source map from...` warnings should logged in the terminal and in the browser console log.

### Code

```js
{
  test: /\.js$/,
  enforce: 'pre',
  use: ['source-map-loader'],
},
```

Example of how the `sourceMappingURL` directive that fails looks like:
```js
//# sourceMappingURL=/build/es2019/constants.js.map
```

### How Do We Reproduce?

### Prerequisite
Web app project that make use of `source-map-loader`

### Steps
1. Install [`standardized-audio-context`](https://www.npmjs.com/package/standardized-audio-context) package
2. Import any module from `standardized-audio-context` into a `*.js/*.ts` file
3. Start/build the project


Pull request description:
Resolve server-relative url
<!--
  HOLY CRAP a Pull Request. We ❤️ those!

  If you remove or skip this template, you'll make the 🐼 sad and the mighty god
  of Github will appear and pile-drive the close button from a great height
  while making animal noises.

  Please place an x (no spaces!) in all [ ] that apply
-->

This PR contains a:

- [ ] **bugfix**
- [x] new **feature**
- [ ] **code refactor**
- [x] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [ ] **metadata update**

### Motivation / Use-Case

Fixes #119 

### Breaking Changes

No

### Additional Info

No


Patch:
diff --git a/.gitignore b/.gitignore
index 05252a1..2960678 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ test/fixtures/file-protocol-path.js.map
 test/fixtures/map-with-sourceroot.js.map
 test/fixtures/map-without-sourceroot.js.map
 test/fixtures/normal-map.js.map
+/test/fixtures/server-relative-url-path.js.map
 logs
 *.log
 npm-debug.log*
diff --git a/src/utils.js b/src/utils.js
index 736460d..ec93695 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -143,6 +143,41 @@ async function fetchFromFilesystem(loaderContext, sourceURL) {
   return buffer.toString();
 }
 
+async function fetchPathsFromFilesystem(
+  loaderContext,
+  possibleRequests,
+  errorsAccumulator = ''
+) {
+  let result;
+
+  try {
+    result = await fetchFromFilesystem(
+      loaderContext,
+      possibleRequests[0],
+      errorsAccumulator
+    );
+  } catch (error) {
+    // eslint-disable-next-line no-param-reassign
+    errorsAccumulator += `${error.message}\n\n`;
+
+    const [, ...tailPossibleRequests] = possibleRequests;
+
+    if (tailPossibleRequests.length === 0) {
+      error.message = errorsAccumulator;
+
+      throw error;
+    }
+
+    return fetchPathsFromFilesystem(
+      loaderContext,
+      tailPossibleRequests,
+      errorsAccumulator
+    );
+  }
+
+  return result;
+}
+
 async function fetchFromURL(
   loaderContext,
   context,
@@ -191,7 +226,18 @@ async function fetchFromURL(
     let sourceContent;
 
     if (!skipReading) {
-      sourceContent = await fetchFromFilesystem(loaderContext, sourceURL);
+      const possibleRequests = [sourceURL];
+
+      if (url.startsWith('/')) {
+        possibleRequests.push(
+          getAbsolutePath(context, sourceURL.slice(1), sourceRoot)
+        );
+      }
+
+      sourceContent = await fetchPathsFromFilesystem(
+        loaderContext,
+        possibleRequests
+      );
     }
 
     return { sourceURL, sourceContent };


Test patch:
diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap
index faa14bd..c14acc5 100644
--- a/test/__snapshots__/loader.test.js.snap
+++ b/test/__snapshots__/loader.test.js.snap
@@ -15,6 +15,21 @@ Failed to parse source map from \\"data\\" URL: data:application/source-map;base
 ]
 `;
 
+exports[`source-map-loader should emit warning when unresolved server-relative-url-path: css 1`] = `
+"with SourceMap
+// #sourceMappingURL=/unresolved-server-relative-url-path.js.map
+// comment
+"
+`;
+
+exports[`source-map-loader should emit warning when unresolved server-relative-url-path: errors 1`] = `Array []`;
+
+exports[`source-map-loader should emit warning when unresolved server-relative-url-path: warnings 1`] = `
+Array [
+  "ModuleWarning: Module Warning (from \`replaced original path\`):",
+]
+`;
+
 exports[`source-map-loader should leave normal files untouched: css 1`] = `"without SourceMap"`;
 
 exports[`source-map-loader should leave normal files untouched: errors 1`] = `Array []`;
@@ -269,6 +284,33 @@ Failed to parse source map: \\"//sampledomain.com/external-source-map2.map\\" UR
 ]
 `;
 
+exports[`source-map-loader should process server-relative-url-path: css 1`] = `
+"with SourceMap
+// comment
+"
+`;
+
+exports[`source-map-loader should process server-relative-url-path: errors 1`] = `Array []`;
+
+exports[`source-map-loader should process server-relative-url-path: map 1`] = `
+Object {
+  "file": "server-relative-url-path.js",
+  "mappings": "AAAA",
+  "sources": Array [
+    "/test/fixtures/server-relative-url-path.js - (normalized for test)",
+  ],
+  "sourcesContent": Array [
+    "with SourceMap
+// #sourceMappingURL=/server-relative-url-path.js.map
+// comment
+",
+  ],
+  "version": 3,
+}
+`;
+
+exports[`source-map-loader should process server-relative-url-path: warnings 1`] = `Array []`;
+
 exports[`source-map-loader should reject http SourceMaps: css 1`] = `
 "with SourceMap
 //#sourceMappingURL=http://sampledomain.com/external-source-map2.map
diff --git a/test/fixtures/server-relative-url-path.js b/test/fixtures/server-relative-url-path.js
new file mode 100644
index 0000000..1902707
--- /dev/null
+++ b/test/fixtures/server-relative-url-path.js
@@ -0,0 +1,3 @@
+with SourceMap
+// #sourceMappingURL=/server-relative-url-path.js.map
+// comment
diff --git a/test/fixtures/unresolved-server-relative-url-path.js b/test/fixtures/unresolved-server-relative-url-path.js
new file mode 100644
index 0000000..9f0b10a
--- /dev/null
+++ b/test/fixtures/unresolved-server-relative-url-path.js
@@ -0,0 +1,3 @@
+with SourceMap
+// #sourceMappingURL=/unresolved-server-relative-url-path.js.map
+// comment
diff --git a/test/helpers/getWarnings.js b/test/helpers/getWarnings.js
index f5e5ab1..7c987d5 100644
--- a/test/helpers/getWarnings.js
+++ b/test/helpers/getWarnings.js
@@ -1,5 +1,5 @@
 import normalizeErrors from './normalizeErrors';
 
-export default (stats) => {
-  return normalizeErrors(stats.compilation.warnings.sort());
+export default (stats, shortError) => {
+  return normalizeErrors(stats.compilation.warnings.sort(), shortError);
 };
diff --git a/test/helpers/normalizeErrors.js b/test/helpers/normalizeErrors.js
index 1b41f3f..da0d6b3 100644
--- a/test/helpers/normalizeErrors.js
+++ b/test/helpers/normalizeErrors.js
@@ -14,8 +14,14 @@ function removeCWD(str) {
     .replace(new RegExp(cwd, 'g'), '');
 }
 
-export default (errors) => {
-  return errors.map((error) =>
-    removeCWD(error.toString().split('\n').slice(0, 2).join('\n'))
-  );
+export default (errors, shortError) => {
+  return errors.map((error) => {
+    let errorMessage = error.toString();
+
+    if (shortError) {
+      errorMessage = errorMessage.split('\n').slice(0, 1).join('\n');
+    }
+
+    return removeCWD(errorMessage.split('\n').slice(0, 2).join('\n'));
+  });
 };
diff --git a/test/loader.test.js b/test/loader.test.js
index fb69023..e071ac5 100644
--- a/test/loader.test.js
+++ b/test/loader.test.js
@@ -617,4 +617,45 @@ describe('source-map-loader', () => {
     expect(getWarnings(stats)).toMatchSnapshot('warnings');
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
+
+  it('should process server-relative-url-path', async () => {
+    const sourceRoot = path.resolve(__dirname, 'fixtures');
+    const javaScriptFilename = 'server-relative-url-path.js';
+    const sourceFilename = 'server-relative-url-path.js';
+    const sourceMapPath = path.join(
+      sourceRoot,
+      'server-relative-url-path.js.map'
+    );
+
+    // Create the sourcemap file
+    const rawSourceMap = {
+      version: 3,
+      file: javaScriptFilename,
+      sourceRoot,
+      sources: [sourceFilename],
+      mappings: 'AAAA',
+    };
+    fs.writeFileSync(sourceMapPath, JSON.stringify(rawSourceMap));
+
+    const testId = 'server-relative-url-path.js';
+    const compiler = getCompiler(testId);
+    const stats = await compile(compiler);
+    const codeFromBundle = getCodeFromBundle(stats, compiler);
+
+    expect(codeFromBundle.css).toMatchSnapshot('css');
+    expect(normalizeMap(codeFromBundle.map)).toMatchSnapshot('map');
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('should emit warning when unresolved server-relative-url-path', async () => {
+    const testId = 'unresolved-server-relative-url-path.js';
+    const compiler = getCompiler(testId);
+    const stats = await compile(compiler);
+    const codeFromBundle = getCodeFromBundle(stats, compiler);
+
+    expect(codeFromBundle.css).toMatchSnapshot('css');
+    expect(getWarnings(stats, true)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
 });