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

Issue:
Build produces invalid output with localsConvention value of camelCase
<!--
  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: ArchLinux
- Node Version: 13.11.0
- NPM Version: 6.14.2
- webpack Version: 4.42.1
- css-loader Version: 3.5.0

### Expected Behavior

I want to have a working build.

### Actual Behavior

Instead of a working build I get a few errors like the following since the release of 3.5.0:

```
You may need an additional loader to handle the result of these loaders.
| exports.locals = {
| 	"select": "select--2hibluCduK",
> 	"options-list": "options-list--2Jo8LXWh5s"	"optionsList": "options-list--2Jo8LXWh5s"
| };
| module.exports = exports;
 @ ./src/Sulu/Bundle/AdminBundle/Resources/js/components/Select/select.scss 2:26-187
 @ ./src/Sulu/Bundle/AdminBundle/Resources/js/components/Select/Select.js
 @ ./src/Sulu/Bundle/AdminBundle/Resources/js/components/Select/index.js
 @ ./src/Sulu/Bundle/AdminBundle/Resources/js/components/MultiSelect/MultiSelect.js
 @ ./src/Sulu/Bundle/AdminBundle/Resources/js/components/MultiSelect/index.js
 @ ./src/Sulu/Bundle/AdminBundle/Resources/js/components/index.js
 @ ./src/Sulu/Bundle/MediaBundle/Resources/js/components/MimeTypeIndicator/MimeTypeIndicator.js
 @ ./node_modules/react-styleguidist/lib/loaders/styleguide-loader.js!./node_modules/react-styleguidist/lib/client/index.js
 @ ./node_modules/react-styleguidist/lib/client/index.js
 @ multi regenerator-runtime/runtime ./src/Sulu/Bundle/AdminBundle/Resources/js/containers/Application/global.scss ./src/Sulu/Bundle/AdminBundle/Resources/js/containers/Application/styleguidist.scss ./node_modules/react-styleguidist/lib/client/index
n
```

It seems like a comma is not inserted when using `localsConvention: 'camelCase'` as an option.

### Code

```css
.options-list {
    // unrelated properties
}
```

### How Do We Reproduce?

1. Create a class with a dash
2. Use the `localsConvention: 'camelCase'` option of the `css-loader`
3. See an error like the one I've pasted above

Pull request:
fix: don't generate an invalid code for `locals`
<!--
  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 #1071

### Breaking Changes

No

### Additional Info

No

Patch:
diff --git a/src/utils.js b/src/utils.js
index d39ffcb..45e8f7a 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -348,49 +348,47 @@ function getExportCode(
   let code = '';
   let localsCode = '';
 
-  const addExportToLocalsCode = (name, value, index) => {
-    const isLastItem = index === exports.length - 1;
+  const addExportToLocalsCode = (name, value) => {
+    if (localsCode) {
+      localsCode += `,\n`;
+    }
 
-    localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}${
-      isLastItem ? '' : ',\n'
-    }`;
+    localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`;
   };
 
-  for (const [index, item] of exports.entries()) {
-    const { name, value } = item;
-
+  for (const { name, value } of exports) {
     switch (localsConvention) {
       case 'camelCase': {
-        addExportToLocalsCode(name, value, index);
+        addExportToLocalsCode(name, value);
 
         const modifiedName = camelCase(name);
 
         if (modifiedName !== name) {
-          addExportToLocalsCode(modifiedName, value, index);
+          addExportToLocalsCode(modifiedName, value);
         }
         break;
       }
       case 'camelCaseOnly': {
-        addExportToLocalsCode(camelCase(name), value, index);
+        addExportToLocalsCode(camelCase(name), value);
         break;
       }
       case 'dashes': {
         addExportToLocalsCode(name, value);
 
-        const modifiedName = dashesCamelCase(name, index);
+        const modifiedName = dashesCamelCase(name);
 
         if (modifiedName !== name) {
-          addExportToLocalsCode(modifiedName, value, index);
+          addExportToLocalsCode(modifiedName, value);
         }
         break;
       }
       case 'dashesOnly': {
-        addExportToLocalsCode(dashesCamelCase(name), value, index);
+        addExportToLocalsCode(dashesCamelCase(name), value);
         break;
       }
       case 'asIs':
       default:
-        addExportToLocalsCode(name, value, index);
+        addExportToLocalsCode(name, value);
         break;
     }
   }


Test patch:
diff --git a/test/__snapshots__/localsConvention-option.test.js.snap b/test/__snapshots__/localsConvention-option.test.js.snap
index 4dd156e..6bfefac 100644
--- a/test/__snapshots__/localsConvention-option.test.js.snap
+++ b/test/__snapshots__/localsConvention-option.test.js.snap
@@ -7,14 +7,15 @@ exports[`"localsConvention" option should work when not specified: module 1`] =
 var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
 exports = ___CSS_LOADER_API_IMPORT___(false);
 // Module
-exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\", \\"\\"]);
+exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\\\n.jHKqWGMvMrGp5RVPtApZ8 {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
 // Exports
 exports.locals = {
 	\\"foo\\": \\"bar\\",
 	\\"my-btn-info_is-disabled\\": \\"value\\",
 	\\"btn-info_is-disabled\\": \\"erBXHZCN_thRYfCnk-aH8\\",
 	\\"btn--info_is-disabled_1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\",
-	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\"
+	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\",
+	\\"foo_bar\\": \\"jHKqWGMvMrGp5RVPtApZ8\\"
 };
 module.exports = exports;
 "
@@ -39,6 +40,10 @@ Array [
 a {
   color: yellow;
 }
+
+.jHKqWGMvMrGp5RVPtApZ8 {
+  color: red;
+}
 ",
     "",
   ],
@@ -54,14 +59,15 @@ exports[`"localsConvention" option should work with a value equal to "asIs": mod
 var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
 exports = ___CSS_LOADER_API_IMPORT___(false);
 // Module
-exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\", \\"\\"]);
+exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\\\n.jHKqWGMvMrGp5RVPtApZ8 {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
 // Exports
 exports.locals = {
 	\\"foo\\": \\"bar\\",
 	\\"my-btn-info_is-disabled\\": \\"value\\",
 	\\"btn-info_is-disabled\\": \\"erBXHZCN_thRYfCnk-aH8\\",
 	\\"btn--info_is-disabled_1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\",
-	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\"
+	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\",
+	\\"foo_bar\\": \\"jHKqWGMvMrGp5RVPtApZ8\\"
 };
 module.exports = exports;
 "
@@ -86,6 +92,10 @@ Array [
 a {
   color: yellow;
 }
+
+.jHKqWGMvMrGp5RVPtApZ8 {
+  color: red;
+}
 ",
     "",
   ],
@@ -101,7 +111,7 @@ exports[`"localsConvention" option should work with a value equal to "camelCase"
 var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
 exports = ___CSS_LOADER_API_IMPORT___(false);
 // Module
-exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\", \\"\\"]);
+exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\\\n.jHKqWGMvMrGp5RVPtApZ8 {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
 // Exports
 exports.locals = {
 	\\"foo\\": \\"bar\\",
@@ -111,7 +121,9 @@ exports.locals = {
 	\\"btnInfoIsDisabled\\": \\"erBXHZCN_thRYfCnk-aH8\\",
 	\\"btn--info_is-disabled_1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\",
 	\\"btnInfoIsDisabled1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\",
-	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\"
+	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\",
+	\\"foo_bar\\": \\"jHKqWGMvMrGp5RVPtApZ8\\",
+	\\"fooBar\\": \\"jHKqWGMvMrGp5RVPtApZ8\\"
 };
 module.exports = exports;
 "
@@ -136,6 +148,10 @@ Array [
 a {
   color: yellow;
 }
+
+.jHKqWGMvMrGp5RVPtApZ8 {
+  color: red;
+}
 ",
     "",
   ],
@@ -151,14 +167,15 @@ exports[`"localsConvention" option should work with a value equal to "camelCaseO
 var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
 exports = ___CSS_LOADER_API_IMPORT___(false);
 // Module
-exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\", \\"\\"]);
+exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\\\n.jHKqWGMvMrGp5RVPtApZ8 {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
 // Exports
 exports.locals = {
 	\\"foo\\": \\"bar\\",
 	\\"myBtnInfoIsDisabled\\": \\"value\\",
 	\\"btnInfoIsDisabled\\": \\"erBXHZCN_thRYfCnk-aH8\\",
 	\\"btnInfoIsDisabled1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\",
-	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\"
+	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\",
+	\\"fooBar\\": \\"jHKqWGMvMrGp5RVPtApZ8\\"
 };
 module.exports = exports;
 "
@@ -183,6 +200,10 @@ Array [
 a {
   color: yellow;
 }
+
+.jHKqWGMvMrGp5RVPtApZ8 {
+  color: red;
+}
 ",
     "",
   ],
@@ -198,7 +219,7 @@ exports[`"localsConvention" option should work with a value equal to "dashes": m
 var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
 exports = ___CSS_LOADER_API_IMPORT___(false);
 // Module
-exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\", \\"\\"]);
+exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\\\n.jHKqWGMvMrGp5RVPtApZ8 {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
 // Exports
 exports.locals = {
 	\\"foo\\": \\"bar\\",
@@ -209,7 +230,7 @@ exports.locals = {
 	\\"btn--info_is-disabled_1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\",
 	\\"btnInfo_isDisabled_1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\",
 	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\",
-
+	\\"foo_bar\\": \\"jHKqWGMvMrGp5RVPtApZ8\\"
 };
 module.exports = exports;
 "
@@ -234,6 +255,10 @@ Array [
 a {
   color: yellow;
 }
+
+.jHKqWGMvMrGp5RVPtApZ8 {
+  color: red;
+}
 ",
     "",
   ],
@@ -249,14 +274,15 @@ exports[`"localsConvention" option should work with a value equal to "dashesOnly
 var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
 exports = ___CSS_LOADER_API_IMPORT___(false);
 // Module
-exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\", \\"\\"]);
+exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n  color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n  color: red;\\\\n}\\\\n\\\\na {\\\\n  color: yellow;\\\\n}\\\\n\\\\n.jHKqWGMvMrGp5RVPtApZ8 {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
 // Exports
 exports.locals = {
 	\\"foo\\": \\"bar\\",
 	\\"myBtnInfo_isDisabled\\": \\"value\\",
 	\\"btnInfo_isDisabled\\": \\"erBXHZCN_thRYfCnk-aH8\\",
 	\\"btnInfo_isDisabled_1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\",
-	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\"
+	\\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\",
+	\\"foo_bar\\": \\"jHKqWGMvMrGp5RVPtApZ8\\"
 };
 module.exports = exports;
 "
@@ -281,6 +307,10 @@ Array [
 a {
   color: yellow;
 }
+
+.jHKqWGMvMrGp5RVPtApZ8 {
+  color: red;
+}
 ",
     "",
   ],
diff --git a/test/fixtures/modules/localsConvention/localsConvention.css b/test/fixtures/modules/localsConvention/localsConvention.css
index a0c46e2..894f64b 100644
--- a/test/fixtures/modules/localsConvention/localsConvention.css
+++ b/test/fixtures/modules/localsConvention/localsConvention.css
@@ -18,3 +18,7 @@ a {
   foo: bar;
   my-btn-info_is-disabled: value;
 }
+
+.foo_bar {
+  color: red;
+}