Repository: webpack-contrib/css-loader
Pull request #1067 (2020-04-03 16:20:59)

feat: auto resolving css-modules import
<!--
  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: #994 

### Breaking Changes

no

### Additional Info

no


Patch:
diff --git a/README.md b/README.md
index a6bee2f..e62ee4a 100644
--- a/README.md
+++ b/README.md
@@ -540,6 +540,66 @@ module.exports = {
 };
 ```
 
+##### `auto`
+
+Type: `Boolean|RegExp`
+Default: `'undefined'`
+
+Allows auto enable css modules based on filename.
+
+###### `Boolean`
+
+Possible values:
+
+- `true` - enable css modules for all files for which `/\.module\.\w+$/i.test(filename)` return true
+- `false` - disable css modules
+
+**webpack.config.js**
+
+```js
+module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.css$/i,
+        loader: 'css-loader',
+        options: {
+          modules: {
+            // All files for which /\.module\.\w+$/i.test(filename) return true
+            auto: true,
+          },
+        },
+      },
+    ],
+  },
+};
+```
+
+###### `RegExp`
+
+Enable css modules for files based on filename and satisfying `/youRegExp/.test(filename)` regex.
+
+**webpack.config.js**
+
+```js
+module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.css$/i,
+        loader: 'css-loader',
+        options: {
+          modules: {
+            // All files for which /youRegExp/i.test(filename) return true
+            auto: /youRegExp/i,
+          },
+        },
+      },
+    ],
+  },
+};
+```
+
 ##### `mode`
 
 Type: `String|Function`
diff --git a/src/index.js b/src/index.js
index c4e08cf..2ee1b78 100644
--- a/src/index.js
+++ b/src/index.js
@@ -20,6 +20,7 @@ import {
   getModuleCode,
   getModulesPlugins,
   normalizeSourceMap,
+  shouldUseModulesPlugins,
 } from './utils';
 
 export default function loader(content, map, meta) {
@@ -34,7 +35,7 @@ export default function loader(content, map, meta) {
   const sourceMap = options.sourceMap || false;
   const plugins = [];
 
-  if (options.modules) {
+  if (shouldUseModulesPlugins(options.modules, this.resourcePath)) {
     plugins.push(...getModulesPlugins(options, this));
   }
 
diff --git a/src/options.json b/src/options.json
index 706e4e9..8d4e11f 100644
--- a/src/options.json
+++ b/src/options.json
@@ -36,6 +36,16 @@
           "type": "object",
           "additionalProperties": false,
           "properties": {
+            "auto": {
+              "anyOf": [
+                {
+                  "type": "object"
+                },
+                {
+                  "type": "boolean"
+                }
+              ]
+            },
             "mode": {
               "anyOf": [
                 {
diff --git a/src/utils.js b/src/utils.js
index a4c6511..d39ffcb 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -96,6 +96,30 @@ function getFilter(filter, resourcePath, defaultFilter = null) {
   };
 }
 
+function shouldUseModulesPlugins(modules, resourcePath) {
+  if (typeof modules === 'undefined') {
+    return false;
+  }
+
+  if (typeof modules === 'boolean') {
+    return modules;
+  }
+
+  if (typeof modules === 'string') {
+    return true;
+  }
+
+  if (typeof modules.auto === 'boolean') {
+    return modules.auto ? /\.module\.\w+$/i.test(resourcePath) : false;
+  }
+
+  if (modules.auto instanceof RegExp) {
+    return modules.auto.test(resourcePath);
+  }
+
+  return true;
+}
+
 function getModulesPlugins(options, loaderContext) {
   let modulesOptions = {
     mode: 'local',
@@ -405,4 +429,5 @@ export {
   getImportCode,
   getModuleCode,
   getExportCode,
+  shouldUseModulesPlugins,
 };


Test patch:
diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap
index ceab550..216b4ea 100644
--- a/test/__snapshots__/modules-option.test.js.snap
+++ b/test/__snapshots__/modules-option.test.js.snap
@@ -3148,6 +3148,124 @@ Array [
 
 exports[`"modules" option should work when the "getLocalIdent" option returns "false": warnings 1`] = `Array []`;
 
+exports[`"modules" option should work with a modules.auto equal "false": errors 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto equal "false": module 1`] = `
+"// Imports
+var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
+exports = ___CSS_LOADER_API_IMPORT___(false);
+// Module
+exports.push([module.id, \\".relative {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
+// Exports
+module.exports = exports;
+"
+`;
+
+exports[`"modules" option should work with a modules.auto equal "false": result 1`] = `
+Array [
+  Array [
+    "./modules/mode/relative.module.css",
+    ".relative {
+  color: red;
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"modules" option should work with a modules.auto equal "false": warnings 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto equal "true": errors 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto equal "true": module 1`] = `
+"// Imports
+var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
+exports = ___CSS_LOADER_API_IMPORT___(false);
+// Module
+exports.push([module.id, \\".y35Nud52-ZFXmqL6AWueX {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
+// Exports
+exports.locals = {
+	\\"relative\\": \\"y35Nud52-ZFXmqL6AWueX\\"
+};
+module.exports = exports;
+"
+`;
+
+exports[`"modules" option should work with a modules.auto equal "true": result 1`] = `
+Array [
+  Array [
+    "./modules/mode/relative.module.css",
+    ".y35Nud52-ZFXmqL6AWueX {
+  color: red;
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"modules" option should work with a modules.auto equal "true": warnings 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto returns "false": errors 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto returns "false": module 1`] = `
+"// Imports
+var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
+exports = ___CSS_LOADER_API_IMPORT___(false);
+// Module
+exports.push([module.id, \\".relative {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
+// Exports
+module.exports = exports;
+"
+`;
+
+exports[`"modules" option should work with a modules.auto returns "false": result 1`] = `
+Array [
+  Array [
+    "./modules/mode/relative.module.css",
+    ".relative {
+  color: red;
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"modules" option should work with a modules.auto returns "false": warnings 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto returns "true": errors 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto returns "true": module 1`] = `
+"// Imports
+var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
+exports = ___CSS_LOADER_API_IMPORT___(false);
+// Module
+exports.push([module.id, \\".y35Nud52-ZFXmqL6AWueX {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
+// Exports
+exports.locals = {
+	\\"relative\\": \\"y35Nud52-ZFXmqL6AWueX\\"
+};
+module.exports = exports;
+"
+`;
+
+exports[`"modules" option should work with a modules.auto returns "true": result 1`] = `
+Array [
+  Array [
+    "./modules/mode/relative.module.css",
+    ".y35Nud52-ZFXmqL6AWueX {
+  color: red;
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"modules" option should work with a modules.auto returns "true": warnings 1`] = `Array []`;
+
 exports[`"modules" option should work with case \`animation\` (\`modules\` value is \`false)\`: errors 1`] = `Array []`;
 
 exports[`"modules" option should work with case \`animation\` (\`modules\` value is \`false)\`: module 1`] = `
diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap
index bc1b236..0802427 100644
--- a/test/__snapshots__/validate-options.test.js.snap
+++ b/test/__snapshots__/validate-options.test.js.snap
@@ -56,7 +56,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"getLocalIdent":[]}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.getLocalIdent should be one of these:
@@ -79,7 +79,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"localIdentRegExp":true}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.localIdentRegExp should be one of these:
@@ -92,7 +92,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"mode":"globals"}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.mode should be one of these:
@@ -106,7 +106,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"mode":"locals"}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.mode should be one of these:
@@ -120,7 +120,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"mode":"pures"}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.mode should be one of these:
@@ -134,7 +134,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"mode":true}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.mode should be one of these:
@@ -148,53 +148,53 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "globals" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules should be a boolean.
     * options.modules should be one of these:
       \\"local\\" | \\"global\\" | \\"pure\\"
     * options.modules should be an object:
-      object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }"
+      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }"
 `;
 
 exports[`validate options should throw an error on the "modules" option with "locals" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules should be a boolean.
     * options.modules should be one of these:
       \\"local\\" | \\"global\\" | \\"pure\\"
     * options.modules should be an object:
-      object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }"
+      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }"
 `;
 
 exports[`validate options should throw an error on the "modules" option with "pures" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules should be a boolean.
     * options.modules should be one of these:
       \\"local\\" | \\"global\\" | \\"pure\\"
     * options.modules should be an object:
-      object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }"
+      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }"
 `;
 
 exports[`validate options should throw an error on the "modules" option with "true" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules should be a boolean.
     * options.modules should be one of these:
       \\"local\\" | \\"global\\" | \\"pure\\"
     * options.modules should be an object:
-      object { mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }"
+      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? }"
 `;
 
 exports[`validate options should throw an error on the "onlyLocals" option with "true" value 1`] = `
diff --git a/test/fixtures/modules/mode/modules.js b/test/fixtures/modules/mode/modules.js
new file mode 100644
index 0000000..27eb52a
--- /dev/null
+++ b/test/fixtures/modules/mode/modules.js
@@ -0,0 +1,5 @@
+import relative from './relative.module.css';
+
+__export__ = relative;
+
+export default relative;
diff --git a/test/fixtures/modules/mode/relative.module.css b/test/fixtures/modules/mode/relative.module.css
new file mode 100644
index 0000000..7359762
--- /dev/null
+++ b/test/fixtures/modules/mode/relative.module.css
@@ -0,0 +1,3 @@
+.relative {
+  color: red;
+}
diff --git a/test/modules-option.test.js b/test/modules-option.test.js
index 31762f6..23617a3 100644
--- a/test/modules-option.test.js
+++ b/test/modules-option.test.js
@@ -688,4 +688,76 @@ describe('"modules" option', () => {
     expect(getWarnings(stats)).toMatchSnapshot('warnings');
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
+
+  it('should work with a modules.auto equal "false"', async () => {
+    const compiler = getCompiler('./modules/mode/modules.js', {
+      modules: {
+        auto: false,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./modules/mode/relative.module.css', stats)
+    ).toMatchSnapshot('module');
+    expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
+      'result'
+    );
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('should work with a modules.auto equal "true"', async () => {
+    const compiler = getCompiler('./modules/mode/modules.js', {
+      modules: {
+        auto: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./modules/mode/relative.module.css', stats)
+    ).toMatchSnapshot('module');
+    expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
+      'result'
+    );
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('should work with a modules.auto returns "true"', async () => {
+    const compiler = getCompiler('./modules/mode/modules.js', {
+      modules: {
+        auto: /relative.module.css$/,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./modules/mode/relative.module.css', stats)
+    ).toMatchSnapshot('module');
+    expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
+      'result'
+    );
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('should work with a modules.auto returns "false"', async () => {
+    const compiler = getCompiler('./modules/mode/modules.js', {
+      modules: {
+        auto: /will no pass/,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./modules/mode/relative.module.css', stats)
+    ).toMatchSnapshot('module');
+    expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
+      'result'
+    );
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
 });