Repository: webpack-contrib/css-loader (js)

Issue:
sourceMap always prepend a undefined
I found that `sourceRoot` does not necessarily exist, sometimes it equals `undefined`.

I look for some codes, and found that [`extract-loader`](https://github.com/peerigon/extract-loader/blob/master/src/extractLoader.js#L141), it uses `extractExports` function to transform, (`Function toString()`) `sandbox.module.exports`.

And the `css-loader` use [`cssWithMappingToString`](https://github.com/webpack-contrib/css-loader/blob/master/src/runtime/api.js#L61) and the variables `cssMapping.sourceRoot` to concat string, but [css-loader has been remove the `sourceRoot`](https://github.com/webpack-contrib/css-loader/blob/master/src/index.js#L76) before `cssWithMappingToString`, so that `cssWithMappingToString` can not found `sourceRoot` anymore, and `sourceRoot` always to `undefined`

I suggest `css-loader` can compatibles it, when the `sourceRoot` is undefined, or keeps the `sourceRoot` option.

- cssWithMappingToString: https://github.com/webpack-contrib/css-loader/blob/master/src/runtime/api.js#L61
- css-loader: https://github.com/webpack-contrib/css-loader/blob/master/src/index.js#L76

### Environment

- Operating System: osx 10.14.6
- Node Version: 12.6.0
- NPM Version: 6.11.3
- webpack Version: 4.41.4
- css-loader Version: 3.4.0

### Expected Behavior

Would not prepend `undefined`

### Actual Behavior

```css
body{color:red}body a{color:#fff}body.open{background-color:#fef}
/*# sourceURL=undefined/src/index.scss */
...
```

### Code
```json
// package.json
{
    "extract-loader": "3.1.0",
    "css-loader": "3.4.0",
    "sass-loader": "8.0.0",
    "node-sass": "4.13.0",
}
```

```javascript
// webpack config
{
  loader: 'extract-loader'
},
{
  loader: 'css-loader',
  options: {
    sourceMap: true
  }
},
{
  loader: 'sass-loader',
  options: {
    sourceMap: true,
    sassOptions: {
      sourceMapRoot: '/'
    }
  }
}
```


Pull request:
fix: do not output `undefined` when sourceRoot is unavailable
<!--
  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:

- [x] **bugfix**
- [ ] 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 #1034

### Breaking Changes

No

### Additional Info

https://github.com/mozilla/source-map/issues/340

sourceRoot is optional

Patch:
diff --git a/src/runtime/api.js b/src/runtime/api.js
index b2acad8..bf8a1cb 100644
--- a/src/runtime/api.js
+++ b/src/runtime/api.js
@@ -58,7 +58,7 @@ function cssWithMappingToString(item, useSourceMap) {
   if (useSourceMap && typeof btoa === 'function') {
     const sourceMapping = toComment(cssMapping);
     const sourceURLs = cssMapping.sources.map(
-      (source) => `/*# sourceURL=${cssMapping.sourceRoot}${source} */`
+      (source) => `/*# sourceURL=${cssMapping.sourceRoot || ''}${source} */`
     );
 
     return [content]
diff --git a/src/utils.js b/src/utils.js
index c5b00d1..bdbf791 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -480,9 +480,9 @@ function getExportCode(
 
   if (exportType === 'locals') {
     exportItems.push(
-      `${
-        esModule ? 'export default' : 'module.exports ='
-      } {\n${exportLocalsCode}\n};`
+      `${esModule ? 'export default' : 'module.exports ='} ${
+        exportLocalsCode ? `{\n${exportLocalsCode}\n}` : '{}'
+      };`
     );
   } else {
     if (exportLocalsCode) {


Test patch:
diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap
index ef5ed1b..0f29676 100644
--- a/test/__snapshots__/loader.test.js.snap
+++ b/test/__snapshots__/loader.test.js.snap
@@ -1,5 +1,42 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`loader issue #1033 (2): errors 1`] = `Array []`;
+
+exports[`loader issue #1033 (2): module 1`] = `
+"// Imports
+var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
+exports = ___CSS_LOADER_API_IMPORT___(false);
+// Module
+exports.push([module.id, \\"\\", \\"\\"]);
+// Exports
+module.exports = exports;
+"
+`;
+
+exports[`loader issue #1033 (2): result 1`] = `
+Array [
+  Array [
+    "./modules/issue-1033/issue-1033.css",
+    "",
+    "",
+  ],
+]
+`;
+
+exports[`loader issue #1033 (2): warnings 1`] = `Array []`;
+
+exports[`loader issue #1033: errors 1`] = `Array []`;
+
+exports[`loader issue #1033: module 1`] = `
+"// Exports
+module.exports = {};
+"
+`;
+
+exports[`loader issue #1033: result 1`] = `Object {}`;
+
+exports[`loader issue #1033: warnings 1`] = `Array []`;
+
 exports[`loader should reuse \`ast\` from "postcss-loader": errors 1`] = `Array []`;
 
 exports[`loader should reuse \`ast\` from "postcss-loader": module 1`] = `
diff --git a/test/fixtures/modules/issue-1033/issue-1033.css b/test/fixtures/modules/issue-1033/issue-1033.css
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/modules/issue-1033/issue-1033.js b/test/fixtures/modules/issue-1033/issue-1033.js
new file mode 100644
index 0000000..fcb67b1
--- /dev/null
+++ b/test/fixtures/modules/issue-1033/issue-1033.js
@@ -0,0 +1,5 @@
+import css from './issue-1033.css';
+
+__export__ = css;
+
+export default css;
diff --git a/test/loader.test.js b/test/loader.test.js
index 6874141..8763bd3 100644
--- a/test/loader.test.js
+++ b/test/loader.test.js
@@ -308,4 +308,37 @@ describe('loader', () => {
     expect(getWarnings(stats)).toMatchSnapshot('warnings');
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
+
+  it('issue #1033', async () => {
+    const compiler = getCompiler('./modules/issue-1033/issue-1033.js', {
+      modules: { mode: 'local', localIdentName: '_[local]' },
+      onlyLocals: true,
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./modules/issue-1033/issue-1033.css', stats)
+    ).toMatchSnapshot('module');
+    expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
+      'result'
+    );
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('issue #1033 (2)', async () => {
+    const compiler = getCompiler('./modules/issue-1033/issue-1033.js', {
+      modules: { mode: 'local', localIdentName: '_[local]' },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./modules/issue-1033/issue-1033.css', stats)
+    ).toMatchSnapshot('module');
+    expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
+      'result'
+    );
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
 });
diff --git a/test/runtime/__snapshots__/api.test.js.snap b/test/runtime/__snapshots__/api.test.js.snap
index 59695e4..23ba248 100644
--- a/test/runtime/__snapshots__/api.test.js.snap
+++ b/test/runtime/__snapshots__/api.test.js.snap
@@ -8,6 +8,12 @@ exports[`api should toString a single module 1`] = `"body { a: 1; }"`;
 
 exports[`api should toString multiple modules 1`] = `"body { b: 2; }body { a: 1; }"`;
 
+exports[`api should toString with a source map without "sourceRoot" 1`] = `
+"body { a: 1; }
+/*# sourceURL=./path/to/test.scss */
+/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJmaWxlIjoidGVzdC5zY3NzIiwic291cmNlcyI6WyIuL3BhdGgvdG8vdGVzdC5zY3NzIl0sIm1hcHBpbmdzIjoiQUFBQTsifQ== */"
+`;
+
 exports[`api should toString with media query 1`] = `"body { b: 2; }body { c: 3; }body { b: 2; }@media print {body { b: 2; }}@media print {body { d: 4; }}@media screen {body { a: 1; }}"`;
 
 exports[`api should toString with source mapping 1`] = `
@@ -16,4 +22,4 @@ exports[`api should toString with source mapping 1`] = `
 /*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJmaWxlIjoidGVzdC5zY3NzIiwic291cmNlcyI6WyIuL3BhdGgvdG8vdGVzdC5zY3NzIl0sIm1hcHBpbmdzIjoiQUFBQTsiLCJzb3VyY2VSb290Ijoid2VicGFjazovLyJ9 */"
 `;
 
-exports[`api should toString without source mapping if btoa not avalibale 1`] = `"body { a: 1; }"`;
+exports[`api should toString without source mapping if btoa not available 1`] = `"body { a: 1; }"`;
diff --git a/test/runtime/api.test.js b/test/runtime/api.test.js
index 508139a..8ff7a5d 100644
--- a/test/runtime/api.test.js
+++ b/test/runtime/api.test.js
@@ -101,7 +101,7 @@ describe('api', () => {
     expect(m.toString()).toMatchSnapshot();
   });
 
-  it('should toString without source mapping if btoa not avalibale', () => {
+  it('should toString without source mapping if btoa not available', () => {
     global.btoa = null;
 
     const m = api(true);
@@ -120,4 +120,21 @@ describe('api', () => {
 
     expect(m.toString()).toMatchSnapshot();
   });
+
+  it.only('should toString with a source map without "sourceRoot"', () => {
+    const m = api(true);
+
+    m.push([
+      1,
+      'body { a: 1; }',
+      '',
+      {
+        file: 'test.scss',
+        sources: ['./path/to/test.scss'],
+        mappings: 'AAAA;',
+      },
+    ]);
+
+    expect(m.toString()).toMatchSnapshot();
+  });
 });