Repository: webpack-contrib/source-map-loader
Pull request #124 (2020-08-27 15:53:06)

Filter source mapping 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**
- [x] **metadata update**

### Motivation / Use-Case

Fixes #123 

### Breaking Changes

No

### Additional Info

No



codecov[bot]: # [Codecov](https://codecov.io/gh/webpack-contrib/source-map-loader/pull/124?src=pr&el=h1) Report
> Merging [#124](https://codecov.io/gh/webpack-contrib/source-map-loader/pull/124?src=pr&el=desc) into [master](https://codecov.io/gh/webpack-contrib/source-map-loader/commit/fbd1d76c3fd3b57e6a9728c219a10fbfa888216f?el=desc) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/webpack-contrib/source-map-loader/pull/124/graphs/tree.svg?width=650&height=150&src=pr&token=yyUOoO5dbI)](https://codecov.io/gh/webpack-contrib/source-map-loader/pull/124?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master      #124   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         4           
  Lines          145       161   +16     
  Branches        37        45    +8     
=========================================
+ Hits           145       161   +16     
```


| [Impacted Files](https://codecov.io/gh/webpack-contrib/source-map-loader/pull/124?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/index.js](https://codecov.io/gh/webpack-contrib/source-map-loader/pull/124/diff?src=pr&el=tree#diff-c3JjL2luZGV4Lmpz) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/webpack-contrib/source-map-loader/pull/124?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/webpack-contrib/source-map-loader/pull/124?src=pr&el=footer). Last update [fbd1d76...87e7072](https://codecov.io/gh/webpack-contrib/source-map-loader/pull/124?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index 7a75d13..f0bea5c 100644
--- a/README.md
+++ b/README.md
@@ -48,16 +48,70 @@ module.exports = {
 };
 ```
 
-`source-map-loader` extracts existing source maps from all JavaScript entries.
+The `source-map-loader` extracts existing source maps from all JavaScript entries.
 This includes both inline source maps as well as those linked via URL.
 All source map data is passed to webpack for processing as per a chosen [source map style](https://webpack.js.org/configuration/devtool/) specified by the `devtool` option in [webpack.config.js](https://webpack.js.org/configuration/).
 This loader is especially useful when using 3rd-party libraries having their own source maps.
 If not extracted and processed into the source map of the webpack bundle, browsers may misinterpret source map data. `source-map-loader` allows webpack to maintain source map data continuity across libraries so ease of debugging is preserved.
-`source-map-loader` will extract from any JavaScript file, including those in the `node_modules` directory.
+The `source-map-loader` will extract from any JavaScript file, including those in the `node_modules` directory.
 Be mindful in setting [include](https://webpack.js.org/configuration/module/#rule-include) and [exclude](https://webpack.js.org/configuration/module/#rule-exclude) rule conditions to maximize bundling performance.
 
 And run `webpack` via your preferred method.
 
+## Options
+
+|                 Name                 |     Type     |   Default   | Description                                    |
+| :----------------------------------: | :----------: | :---------: | :--------------------------------------------- |
+| **[`filterSourceMappingUrl`](#url)** | `{Function}` | `undefined` | Allows to control `SourceMappingURL` behaviour |
+
+### filterSourceMappingUrl
+
+Type: `Function`
+Default: `undefined`
+
+Allows you to specify the behavior of the loader for `SourceMappingURL` comment.
+
+The function must return one of the values:
+
+- `true` or `'consume'` - consume the source map and remove `SourceMappingURL` comment (default behavior)
+- `false` or `'remove'` - do not consume the source map and remove `SourceMappingURL` comment
+- `skip` - do not consume the source map and do not remove `SourceMappingURL` comment
+
+Example configuration:
+
+**webpack.config.js**
+
+```js
+module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        enforce: 'pre',
+        use: [
+          {
+            loader: 'source-map-loader',
+            options: {
+              filterSourceMappingUrl: (url, resourcePath) => {
+                if (/broker-source-map-url\.js$/i.test(url)) {
+                  return false;
+                }
+
+                if (/keep-source-mapping-url\.js$/i.test(resourcePath)) {
+                  return 'skip';
+                }
+
+                return true;
+              },
+            },
+          },
+        ],
+      },
+    ],
+  },
+};
+```
+
 ## Examples
 
 ### Ignoring Warnings
diff --git a/src/index.js b/src/index.js
index 136228a..77c758c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -19,6 +19,7 @@ export default async function loader(input, inputMap) {
   });
 
   const { sourceMappingURL, replacementString } = getSourceMappingURL(input);
+
   const callback = this.async();
 
   if (!sourceMappingURL) {
@@ -27,6 +28,30 @@ export default async function loader(input, inputMap) {
     return;
   }
 
+  let behaviourSourceMappingUrl;
+
+  try {
+    behaviourSourceMappingUrl =
+      typeof options.filterSourceMappingUrl !== 'undefined'
+        ? options.filterSourceMappingUrl(sourceMappingURL, this.resourcePath)
+        : 'consume';
+  } catch (error) {
+    callback(error);
+
+    return;
+  }
+
+  // eslint-disable-next-line default-case
+  switch (behaviourSourceMappingUrl) {
+    case 'skip':
+      callback(null, input, inputMap);
+      return;
+    case false:
+    case 'remove':
+      callback(null, input.replace(replacementString, ''), inputMap);
+      return;
+  }
+
   let sourceURL;
   let sourceContent;
 
diff --git a/src/options.json b/src/options.json
index 97ea2a4..41fc5b4 100644
--- a/src/options.json
+++ b/src/options.json
@@ -1,4 +1,9 @@
 {
   "type": "object",
-  "additionalProperties": false
+  "additionalProperties": false,
+  "properties": {
+    "filterSourceMappingUrl": {
+      "instanceof": "Function"
+    }
+  }
 }


Test patch:
diff --git a/test/__snapshots__/filterSourceMappingUrl.test.js.snap b/test/__snapshots__/filterSourceMappingUrl.test.js.snap
new file mode 100644
index 0000000..a42ab1e
--- /dev/null
+++ b/test/__snapshots__/filterSourceMappingUrl.test.js.snap
@@ -0,0 +1,87 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`filterSourceMappingUrl option should emit error: errors 1`] = `
+Array [
+  "ModuleBuildError: Module build failed (from \`replaced original path\`):
+Error: error",
+]
+`;
+
+exports[`filterSourceMappingUrl option should emit error: warnings 1`] = `Array []`;
+
+exports[`filterSourceMappingUrl option should work with "remove" value: css 1`] = `
+"with SourceMap
+// comment
+"
+`;
+
+exports[`filterSourceMappingUrl option should work with "remove" value: errors 1`] = `Array []`;
+
+exports[`filterSourceMappingUrl option should work with "remove" value: warnings 1`] = `Array []`;
+
+exports[`filterSourceMappingUrl option should work with "skip" value: css 1`] = `
+"with SourceMap
+//#sourceMappingURL=http://sampledomain.com/external-source-map2.map
+// comment
+"
+`;
+
+exports[`filterSourceMappingUrl option should work with "skip" value: errors 1`] = `Array []`;
+
+exports[`filterSourceMappingUrl option should work with "skip" value: warnings 1`] = `Array []`;
+
+exports[`filterSourceMappingUrl option should work with false value: css 1`] = `
+"with SourceMap
+// comment
+"
+`;
+
+exports[`filterSourceMappingUrl option should work with false value: errors 1`] = `Array []`;
+
+exports[`filterSourceMappingUrl option should work with false value: warnings 1`] = `Array []`;
+
+exports[`filterSourceMappingUrl option should work with true value: css 1`] = `
+"with SourceMap
+// comment"
+`;
+
+exports[`filterSourceMappingUrl option should work with true value: errors 1`] = `Array []`;
+
+exports[`filterSourceMappingUrl option should work with true value: map 1`] = `
+Object {
+  "file": "external-source-map.js",
+  "mappings": "AAAA",
+  "sources": Array [
+    "/test/fixtures/external-source-map.txt - (normalized for test)",
+  ],
+  "sourcesContent": Array [
+    "with SourceMap",
+  ],
+  "version": 3,
+}
+`;
+
+exports[`filterSourceMappingUrl option should work with true value: warnings 1`] = `Array []`;
+
+exports[`filterSourceMappingUrl option should work: css 1`] = `
+"with SourceMap
+// comment"
+`;
+
+exports[`filterSourceMappingUrl option should work: errors 1`] = `Array []`;
+
+exports[`filterSourceMappingUrl option should work: map 1`] = `
+Object {
+  "file": "external-source-map.js",
+  "mappings": "AAAA",
+  "sources": Array [
+    "/test/fixtures/external-source-map.txt - (normalized for test)",
+  ],
+  "sourcesContent": Array [
+    "with SourceMap",
+  ],
+  "version": 3,
+}
+`;
+
+exports[`filterSourceMappingUrl option should work: warnings 1`] = `Array []`;
diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap
index 0577472..6b110b6 100644
--- a/test/__snapshots__/validate-options.test.js.snap
+++ b/test/__snapshots__/validate-options.test.js.snap
@@ -1,49 +1,89 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`validate options should throw an error on the "filterSourceMappingUrl" option with "/test/" value 1`] = `
+"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
+ - options.filterSourceMappingUrl should be an instance of function."
+`;
+
+exports[`validate options should throw an error on the "filterSourceMappingUrl" option with "[]" value 1`] = `
+"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
+ - options.filterSourceMappingUrl should be an instance of function."
+`;
+
+exports[`validate options should throw an error on the "filterSourceMappingUrl" option with "{"foo":"bar"}" value 1`] = `
+"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
+ - options.filterSourceMappingUrl should be an instance of function."
+`;
+
+exports[`validate options should throw an error on the "filterSourceMappingUrl" option with "{}" value 1`] = `
+"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
+ - options.filterSourceMappingUrl should be an instance of function."
+`;
+
+exports[`validate options should throw an error on the "filterSourceMappingUrl" option with "1" value 1`] = `
+"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
+ - options.filterSourceMappingUrl should be an instance of function."
+`;
+
+exports[`validate options should throw an error on the "filterSourceMappingUrl" option with "false" value 1`] = `
+"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
+ - options.filterSourceMappingUrl should be an instance of function."
+`;
+
+exports[`validate options should throw an error on the "filterSourceMappingUrl" option with "test" value 1`] = `
+"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
+ - options.filterSourceMappingUrl should be an instance of function."
+`;
+
+exports[`validate options should throw an error on the "filterSourceMappingUrl" option with "true" value 1`] = `
+"Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
+ - options.filterSourceMappingUrl should be an instance of function."
+`;
+
 exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = `
 "Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object {}"
+   object { filterSourceMappingUrl? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = `
 "Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object {}"
+   object { filterSourceMappingUrl? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = `
 "Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object {}"
+   object { filterSourceMappingUrl? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = `
 "Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object {}"
+   object { filterSourceMappingUrl? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = `
 "Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object {}"
+   object { filterSourceMappingUrl? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = `
 "Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object {}"
+   object { filterSourceMappingUrl? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = `
 "Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object {}"
+   object { filterSourceMappingUrl? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = `
 "Invalid options object. Source Map Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object {}"
+   object { filterSourceMappingUrl? }"
 `;
diff --git a/test/filterSourceMappingUrl.test.js b/test/filterSourceMappingUrl.test.js
new file mode 100644
index 0000000..743652a
--- /dev/null
+++ b/test/filterSourceMappingUrl.test.js
@@ -0,0 +1,100 @@
+import {
+  compile,
+  getCodeFromBundle,
+  getCompiler,
+  getErrors,
+  normalizeMap,
+  getWarnings,
+} from './helpers';
+
+describe('filterSourceMappingUrl option', () => {
+  it('should work', async () => {
+    expect.assertions(6);
+
+    const testId = 'external-source-map.js';
+    const compiler = getCompiler(testId, {
+      filterSourceMappingUrl: (sourceMappingURL, resourcePath) => {
+        expect(sourceMappingURL).toBeDefined();
+        expect(resourcePath).toBeDefined();
+
+        return true;
+      },
+    });
+    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 work with true value', async () => {
+    const testId = 'external-source-map.js';
+    const compiler = getCompiler(testId, {
+      filterSourceMappingUrl: () => true,
+    });
+    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 work with "skip" value', async () => {
+    const testId = 'http-source-map.js';
+    const compiler = getCompiler(testId, {
+      filterSourceMappingUrl: () => 'skip',
+    });
+    const stats = await compile(compiler);
+    const codeFromBundle = getCodeFromBundle(stats, compiler);
+
+    expect(codeFromBundle.map).toBeUndefined();
+    expect(codeFromBundle.css).toMatchSnapshot('css');
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('should work with "remove" value', async () => {
+    const testId = 'http-source-map.js';
+    const compiler = getCompiler(testId, {
+      filterSourceMappingUrl: () => 'remove',
+    });
+    const stats = await compile(compiler);
+    const codeFromBundle = getCodeFromBundle(stats, compiler);
+
+    expect(codeFromBundle.map).toBeUndefined();
+    expect(codeFromBundle.css).toMatchSnapshot('css');
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('should work with false value', async () => {
+    const testId = 'http-source-map.js';
+    const compiler = getCompiler(testId, {
+      filterSourceMappingUrl: () => false,
+    });
+    const stats = await compile(compiler);
+    const codeFromBundle = getCodeFromBundle(stats, compiler);
+
+    expect(codeFromBundle.map).toBeUndefined();
+    expect(codeFromBundle.css).toMatchSnapshot('css');
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('should emit error', async () => {
+    const testId = 'external-source-map.js';
+    const compiler = getCompiler(testId, {
+      filterSourceMappingUrl: () => {
+        throw new Error('error');
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+});
diff --git a/test/validate-options.test.js b/test/validate-options.test.js
index 1a307e6..cc0d213 100644
--- a/test/validate-options.test.js
+++ b/test/validate-options.test.js
@@ -2,6 +2,10 @@ import { getCompiler, compile } from './helpers';
 
 describe('validate options', () => {
   const tests = {
+    filterSourceMappingUrl: {
+      success: [() => false, () => 'false', () => 'remove', () => 'skip'],
+      failure: [1, true, false, 'test', /test/, [], {}, { foo: 'bar' }],
+    },
     unknown: {
       success: [],
       failure: [1, true, false, 'test', /test/, [], {}, { foo: 'bar' }],