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

Issue:
Parsing issues for certain files
If the source-map-loader (0.2.2) encounters the following, minified file, it throws a warning, that the sourceMap cannot be located

`WARNING in ./node_modules/@atlaskit/single-select/node_modules/@atlaskit/icon/lib/Icon.js
(Emitted value instead of an instance of Error) Cannot find SourceMap 'data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(n))))+"': Error: Can't resolve './data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(n))))+"' in 'K:\dev\github\com.yasoon.calendar\node_modules\@atlaskit\single-select\node_modules\@atlaskit\icon\lib'
 @ ./node_modules/@atlaskit/single-select/node_modules/@atlaskit/icon/glyph/expand.js 16:12-34
 @ ./node_modules/@atlaskit/single-select/dist/bundle.js
 @ ./src/components/createDetailedEventModal.tsx
 @ ./src/components/calendarView.tsx
 @ ./src/index.tsx
 @ ./src/cloudBootstrapApp.tsx`

Full file: [Icon.js](https://gist.github.com/tobiasviehweger/1ead56f4e8d8b67a220ba5fa1ee212f4)

If the file is pretty printed, this does not seem to happen, which is a somewhat weird behavior. This is the function in the "Icon.js" which is parsed incorrectly:
```javascript
function f(e, t) {
        var r = t.css,
            n = t.sourceMap;
        n && (r += "\n/*# sourceMappingURL=data:application/json;base64," + btoa(unescape(encodeURIComponent(JSON.stringify(n)))) + " */");
        var o = new Blob([r], {
                type: "text/css"
            }),
            i = e.href;
        e.href = URL.createObjectURL(o), i && URL.revokeObjectURL(i)
    }
```

Thanks
Tobi

Pull request description:
fix: issue 55
<!--
  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**
- [x] **code refactor**
- [x] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [ ] **metadata update**

### Motivation / Use-Case

Fixes #55
Fixes #94 

### Breaking Changes

No

### Additional Info

No



Patch:
diff --git a/src/index.js b/src/index.js
index 0debb6a..635b492 100644
--- a/src/index.js
+++ b/src/index.js
@@ -19,16 +19,9 @@ import {
   readFile,
   getContentFromSourcesContent,
   isUrlRequest,
+  getSourceMappingUrl,
 } from './utils';
 
-// Matches only the last occurrence of sourceMappingURL
-const baseRegex =
-  '\\s*[@#]\\s*sourceMappingURL\\s*=\\s*([^\\s]*)(?![\\S\\s]*sourceMappingURL)';
-// Matches /* ... */ comments
-const regex1 = new RegExp(`/\\*${baseRegex}\\s*\\*/`);
-// Matches // .... comments
-const regex2 = new RegExp(`//${baseRegex}($|\n|\r\n?)`);
-
 export default function loader(input, inputMap) {
   const options = getOptions(this);
 
@@ -37,17 +30,16 @@ export default function loader(input, inputMap) {
     baseDataPath: 'options',
   });
 
-  const match = input.match(regex1) || input.match(regex2);
+  let { url } = getSourceMappingUrl(input);
+  const { replacementString } = getSourceMappingUrl(input);
   const callback = this.async();
 
-  if (!match) {
+  if (!url) {
     callback(null, input, inputMap);
 
     return;
   }
 
-  let [, url] = match;
-
   const dataURL = parseDataURL(url);
 
   const { context, resolve, addDependency, emitWarning } = this;
@@ -60,7 +52,7 @@ export default function loader(input, inputMap) {
         labelToName(dataURL.mimeType.parameters.get('charset')) || 'UTF-8';
 
       map = decode(dataURL.body, dataURL.encodingName);
-      map = JSON.parse(map);
+      map = JSON.parse(map.replace(/^\)\]\}'/, ''));
     } catch (error) {
       emitWarning(
         `Cannot parse inline SourceMap with Charset ${dataURL.encodingName}: ${error}`
@@ -226,6 +218,6 @@ export default function loader(input, inputMap) {
       delete resultMap.sourcesContent;
     }
 
-    callback(null, input.replace(match[0], ''), resultMap);
+    callback(null, input.replace(replacementString, ''), resultMap);
   }
 }
diff --git a/src/utils.js b/src/utils.js
index b3ff87d..b0eb785 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -3,6 +3,28 @@ import path from 'path';
 
 import sourceMap from 'source-map';
 
+// Matches only the last occurrence of sourceMappingURL
+const innerRegex = /\s*[#@]\s*sourceMappingURL\s*=\s*([^\s'"]*)\s*/;
+
+/* eslint-disable prefer-template */
+const sourceMappingURLRegex = RegExp(
+  '(?:' +
+    '/\\*' +
+    '(?:\\s*\r?\n(?://)?)?' +
+    '(?:' +
+    innerRegex.source +
+    ')' +
+    '\\s*' +
+    '\\*/' +
+    '|' +
+    '//(?:' +
+    innerRegex.source +
+    ')' +
+    ')' +
+    '\\s*'
+);
+/* eslint-enable prefer-template */
+
 async function flattenSourceMap(map) {
   const consumer = await new sourceMap.SourceMapConsumer(map);
   let generatedMap;
@@ -80,9 +102,27 @@ function isUrlRequest(url) {
   return true;
 }
 
+function getSourceMappingUrl(code) {
+  const lines = code.split(/^/m);
+  let match;
+
+  for (let i = lines.length - 1; i >= 0; i--) {
+    match = lines[i].match(sourceMappingURLRegex);
+    if (match) {
+      break;
+    }
+  }
+
+  return {
+    url: match ? match[1] || match[2] || '' : null,
+    replacementString: match ? match[0] : null,
+  };
+}
+
 export {
   flattenSourceMap,
   readFile,
   getContentFromSourcesContent,
   isUrlRequest,
+  getSourceMappingUrl,
 };


Test patch:
diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap
index 7b4d020..e17faf2 100644
--- a/test/__snapshots__/loader.test.js.snap
+++ b/test/__snapshots__/loader.test.js.snap
@@ -6,6 +6,17 @@ exports[`source-map-loader should leave normal files untouched: errors 1`] = `Ar
 
 exports[`source-map-loader should leave normal files untouched: warnings 1`] = `Array []`;
 
+exports[`source-map-loader should leave normal files with fake source-map untouched: css 1`] = `
+"// without SourceMap
+anInvalidDirective = \\"\\\\n/*# sourceMappingURL=data:application/json;base64,\\"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+\\" */\\";
+// comment
+"
+`;
+
+exports[`source-map-loader should leave normal files with fake source-map untouched: errors 1`] = `Array []`;
+
+exports[`source-map-loader should leave normal files with fake source-map untouched: warnings 1`] = `Array []`;
+
 exports[`source-map-loader should process external SourceMaps (external sources): css 1`] = `
 "with SourceMap
 // comment"
@@ -136,7 +147,12 @@ exports[`source-map-loader should skip invalid base64 SourceMap: css 1`] = `
 
 exports[`source-map-loader should skip invalid base64 SourceMap: errors 1`] = `Array []`;
 
-exports[`source-map-loader should skip invalid base64 SourceMap: warnings 1`] = `Array []`;
+exports[`source-map-loader should skip invalid base64 SourceMap: warnings 1`] = `
+Array [
+  "ModuleWarning: Module Warning (from \`replaced original path\`):
+(Emitted value instead of an instance of Error) Cannot parse inline SourceMap with Charset UTF-8: SyntaxError: Unexpected end of JSON input",
+]
+`;
 
 exports[`source-map-loader should support absolute sourceRoot paths in sourcemaps: css 1`] = `
 "with SourceMap
diff --git a/test/__snapshots__/sourceMapperRegexp.test.js.snap b/test/__snapshots__/sourceMapperRegexp.test.js.snap
new file mode 100644
index 0000000..8627b50
--- /dev/null
+++ b/test/__snapshots__/sourceMapperRegexp.test.js.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`source-map-loader should be null: result 1`] = `null`;
+
+exports[`source-map-loader should be null: result 2`] = `null`;
+
+exports[`source-map-loader should find last map: result 1`] = `"/sample-source-map-last.map"`;
+
+exports[`source-map-loader should not include quotes: result 1`] = `"data:application/json;base64,"`;
+
+exports[`source-map-loader should work: result 1`] = `"absolute-sourceRoot-source-map.map"`;
+
+exports[`source-map-loader should work: result 2`] = `"absolute-sourceRoot-source-map.map"`;
+
+exports[`source-map-loader should work: result 3`] = `"absolute-sourceRoot-source-map.map"`;
+
+exports[`source-map-loader should work: result 4`] = `"absolute-sourceRoot-source-map.map"`;
+
+exports[`source-map-loader should work: result 5`] = `"absolute-sourceRoot-source-map.map"`;
+
+exports[`source-map-loader should work: result 6`] = `"absolute-sourceRoot-source-map.map"`;
+
+exports[`source-map-loader should work: result 7`] = `"http://sampledomain.com/external-source-map2.map"`;
+
+exports[`source-map-loader should work: result 8`] = `"data:application/source-map;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5saW5lLXNvdXJjZS1tYXAuanMiLCJzb3VyY2VzIjpbImlubGluZS1zb3VyY2UtbWFwLnR4dCJdLCJzb3VyY2VzQ29udGVudCI6WyJ3aXRoIFNvdXJjZU1hcCJdLCJtYXBwaW5ncyI6IkFBQUEifQ=="`;
+
+exports[`source-map-loader should work: result 9`] = `"/sample-source-map.map"`;
diff --git a/test/fixtures/normal-file2.js b/test/fixtures/normal-file2.js
new file mode 100644
index 0000000..07b85d9
--- /dev/null
+++ b/test/fixtures/normal-file2.js
@@ -0,0 +1,3 @@
+// without SourceMap
+anInvalidDirective = "\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";
+// comment
diff --git a/test/loader.test.js b/test/loader.test.js
index 04c57d4..85f55e2 100644
--- a/test/loader.test.js
+++ b/test/loader.test.js
@@ -26,6 +26,18 @@ describe('source-map-loader', () => {
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
 
+  it('should leave normal files with fake source-map untouched', async () => {
+    const testId = 'normal-file2.js';
+    const compiler = getCompiler(testId);
+    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 process inlined SourceMaps', async () => {
     const testId = 'inline-source-map.js';
     const compiler = getCompiler(testId);
diff --git a/test/sourceMapperRegexp.test.js b/test/sourceMapperRegexp.test.js
new file mode 100644
index 0000000..c819f87
--- /dev/null
+++ b/test/sourceMapperRegexp.test.js
@@ -0,0 +1,106 @@
+import { getSourceMappingUrl } from '../src/utils';
+
+describe('source-map-loader', () => {
+  it('should work', async () => {
+    const code = `/*#sourceMappingURL=absolute-sourceRoot-source-map.map*/`;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should work', async () => {
+    const code = `/*  #sourceMappingURL=absolute-sourceRoot-source-map.map  */`;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should work', async () => {
+    const code = `//#sourceMappingURL=absolute-sourceRoot-source-map.map`;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should work', async () => {
+    const code = `//@sourceMappingURL=absolute-sourceRoot-source-map.map`;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should work', async () => {
+    const code = ` //  #sourceMappingURL=absolute-sourceRoot-source-map.map`;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should work', async () => {
+    const code = ` //  #  sourceMappingURL  =   absolute-sourceRoot-source-map.map  `;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should work', async () => {
+    const code = `// #sourceMappingURL = http://sampledomain.com/external-source-map2.map`;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should work', async () => {
+    const code = `// @sourceMappingURL=data:application/source-map;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5saW5lLXNvdXJjZS1tYXAuanMiLCJzb3VyY2VzIjpbImlubGluZS1zb3VyY2UtbWFwLnR4dCJdLCJzb3VyY2VzQ29udGVudCI6WyJ3aXRoIFNvdXJjZU1hcCJdLCJtYXBwaW5ncyI6IkFBQUEifQ==`;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should work', async () => {
+    const code = `
+    with SourceMap
+    
+    // #sourceMappingURL = /sample-source-map.map
+    // comment
+    `;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should find last map', async () => {
+    const code = `
+    with SourceMap
+    // #sourceMappingURL = /sample-source-map-1.map
+    // #sourceMappingURL = /sample-source-map-2.map
+    // #sourceMappingURL = /sample-source-map-last.map
+    // comment
+    `;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should be null', async () => {
+    const code = `"
+    /*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";`;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should be null', async () => {
+    const code = `anInvalidDirective = "\\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";`;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+
+  it('should not include quotes', async () => {
+    const code = `// # sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+"`;
+    const { url } = getSourceMappingUrl(code);
+
+    expect(url).toMatchSnapshot('result');
+  });
+});