Repository: webpack-contrib/css-loader
Pull request #1086 (2020-05-28 18:16:33)

feat: allow modules.auto to be a filter function
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

The modules.auto feature as added in #1067 is pretty sweet. but sometimes you need more than a simple regex to check against your file path.

This PR allows `modules.auto` to be a function that that accepts the resourcePath and returns a boolean.

Along the way I tweaked some test case names to be a little clearer about what gets passed into modules.auto, which causes a bit of snapshot diff noise.

### Breaking Changes

n/a

### Additional Info

n/a

jsf-clabot: [![CLA assistant check](https://cla.js.foundation/pull/badge/not_signed)](https://cla.js.foundation/webpack-contrib/css-loader?pullRequest=1086) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla.js.foundation/webpack-contrib/css-loader?pullRequest=1086) before we can accept your contribution.<br/>
codecov[bot]: # [Codecov](https://codecov.io/gh/webpack-contrib/css-loader/pull/1086?src=pr&el=h1) Report
> Merging [#1086](https://codecov.io/gh/webpack-contrib/css-loader/pull/1086?src=pr&el=desc) into [master](https://codecov.io/gh/webpack-contrib/css-loader/commit/3a1f1f2f19dd61d2be77afce3def2f616320eede&el=desc) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1086      +/-   ##
==========================================
+ Coverage   97.28%   97.29%   +0.01%     
==========================================
  Files          10       10              
  Lines         479      481       +2     
  Branches      161      162       +1     
==========================================
+ Hits          466      468       +2     
  Misses         12       12              
  Partials        1        1              
```


| [Impacted Files](https://codecov.io/gh/webpack-contrib/css-loader/pull/1086?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/utils.js](https://codecov.io/gh/webpack-contrib/css-loader/pull/1086/diff?src=pr&el=tree#diff-c3JjL3V0aWxzLmpz) | `98.89% <100.00%> (+0.01%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/webpack-contrib/css-loader/pull/1086?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/css-loader/pull/1086?src=pr&el=footer). Last update [3a1f1f2...136ac02](https://codecov.io/gh/webpack-contrib/css-loader/pull/1086?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

BPScott: @evilebottnawi: Thanks! I've updated it to use typeof and have added some validate-options tests
BPScott: CI looks unrelated. It's failing running webpack 5 tests on node 8 thanks to BigInt not existing. That seems reasonable as they bumped their minimum version a while back: https://github.com/webpack/webpack/pull/9868

Sounds like we should remove wepback 5 in node 8 from the test matrix?
evilebottnawi: @BPScott Yes, let's test webpack@5 only for 10/12/14
BPScott: #1089 Removes that CI check. I'll rebase this PR once that is merged.
evilebottnawi: @BPScott merged :+1: 

Patch:
diff --git a/README.md b/README.md
index d1b58a3..3b43ea6 100644
--- a/README.md
+++ b/README.md
@@ -542,7 +542,7 @@ module.exports = {
 
 ##### `auto`
 
-Type: `Boolean|RegExp`
+Type: `Boolean|RegExp|Function`
 Default: `'undefined'`
 
 Allows auto enable css modules based on filename.
@@ -576,7 +576,7 @@ module.exports = {
 
 ###### `RegExp`
 
-Enable css modules for files based on a filename and satisfying your regex.
+Enable css modules for files based on the filename satisfying your regex check.
 
 **webpack.config.js**
 
@@ -598,6 +598,30 @@ module.exports = {
 };
 ```
 
+###### `Function`
+
+Enable css modules for files based on the filename satisfying your filter function check.
+
+**webpack.config.js**
+
+```js
+module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.css$/i,
+        loader: 'css-loader',
+        options: {
+          modules: {
+            auto: (resourcePath) => resourcePath.endsWith('.custom-module.css'),
+          },
+        },
+      },
+    ],
+  },
+};
+```
+
 ##### `mode`
 
 Type: `String|Function`
diff --git a/src/options.json b/src/options.json
index 6c8409b..43ac0f9 100644
--- a/src/options.json
+++ b/src/options.json
@@ -41,6 +41,9 @@
                 {
                   "instanceof": "RegExp"
                 },
+                {
+                  "instanceof": "Function"
+                },
                 {
                   "type": "boolean"
                 }
diff --git a/src/utils.js b/src/utils.js
index 45e8f7a..8bc0a62 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -117,6 +117,10 @@ function shouldUseModulesPlugins(modules, resourcePath) {
     return modules.auto.test(resourcePath);
   }
 
+  if (typeof modules.auto === 'function') {
+    return modules.auto(resourcePath);
+  }
+
   return true;
 }
 


Test patch:
diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap
index 216b4ea..03f8436 100644
--- a/test/__snapshots__/modules-option.test.js.snap
+++ b/test/__snapshots__/modules-option.test.js.snap
@@ -3148,9 +3148,9 @@ 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 Boolean that is "false": errors 1`] = `Array []`;
 
-exports[`"modules" option should work with a modules.auto equal "false": module 1`] = `
+exports[`"modules" option should work with a modules.auto Boolean that is "false": module 1`] = `
 "// Imports
 var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
 exports = ___CSS_LOADER_API_IMPORT___(false);
@@ -3161,7 +3161,7 @@ module.exports = exports;
 "
 `;
 
-exports[`"modules" option should work with a modules.auto equal "false": result 1`] = `
+exports[`"modules" option should work with a modules.auto Boolean that is "false": result 1`] = `
 Array [
   Array [
     "./modules/mode/relative.module.css",
@@ -3174,11 +3174,11 @@ Array [
 ]
 `;
 
-exports[`"modules" option should work with a modules.auto equal "false": warnings 1`] = `Array []`;
+exports[`"modules" option should work with a modules.auto Boolean that is "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 Boolean that is "true": errors 1`] = `Array []`;
 
-exports[`"modules" option should work with a modules.auto equal "true": module 1`] = `
+exports[`"modules" option should work with a modules.auto Boolean that is "true": module 1`] = `
 "// Imports
 var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
 exports = ___CSS_LOADER_API_IMPORT___(false);
@@ -3192,7 +3192,7 @@ module.exports = exports;
 "
 `;
 
-exports[`"modules" option should work with a modules.auto equal "true": result 1`] = `
+exports[`"modules" option should work with a modules.auto Boolean that is "true": result 1`] = `
 Array [
   Array [
     "./modules/mode/relative.module.css",
@@ -3205,11 +3205,11 @@ Array [
 ]
 `;
 
-exports[`"modules" option should work with a modules.auto equal "true": warnings 1`] = `Array []`;
+exports[`"modules" option should work with a modules.auto Boolean that is "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 Function that returns "false": errors 1`] = `Array []`;
 
-exports[`"modules" option should work with a modules.auto returns "false": module 1`] = `
+exports[`"modules" option should work with a modules.auto Function that returns "false": module 1`] = `
 "// Imports
 var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
 exports = ___CSS_LOADER_API_IMPORT___(false);
@@ -3220,7 +3220,7 @@ module.exports = exports;
 "
 `;
 
-exports[`"modules" option should work with a modules.auto returns "false": result 1`] = `
+exports[`"modules" option should work with a modules.auto Function that returns "false": result 1`] = `
 Array [
   Array [
     "./modules/mode/relative.module.css",
@@ -3233,11 +3233,11 @@ Array [
 ]
 `;
 
-exports[`"modules" option should work with a modules.auto returns "false": warnings 1`] = `Array []`;
+exports[`"modules" option should work with a modules.auto Function that 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 Function that returns "true": errors 1`] = `Array []`;
 
-exports[`"modules" option should work with a modules.auto returns "true": module 1`] = `
+exports[`"modules" option should work with a modules.auto Function that returns "true": module 1`] = `
 "// Imports
 var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\");
 exports = ___CSS_LOADER_API_IMPORT___(false);
@@ -3251,7 +3251,7 @@ module.exports = exports;
 "
 `;
 
-exports[`"modules" option should work with a modules.auto returns "true": result 1`] = `
+exports[`"modules" option should work with a modules.auto Function that returns "true": result 1`] = `
 Array [
   Array [
     "./modules/mode/relative.module.css",
@@ -3264,7 +3264,66 @@ Array [
 ]
 `;
 
-exports[`"modules" option should work with a modules.auto returns "true": warnings 1`] = `Array []`;
+exports[`"modules" option should work with a modules.auto Function that returns "true": warnings 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto RegExp that returns "false": errors 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto RegExp that 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 RegExp that returns "false": result 1`] = `
+Array [
+  Array [
+    "./modules/mode/relative.module.css",
+    ".relative {
+  color: red;
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"modules" option should work with a modules.auto RegExp that returns "false": warnings 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto RegExp that returns "true": errors 1`] = `Array []`;
+
+exports[`"modules" option should work with a modules.auto RegExp that 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 RegExp that returns "true": result 1`] = `
+Array [
+  Array [
+    "./modules/mode/relative.module.css",
+    ".y35Nud52-ZFXmqL6AWueX {
+  color: red;
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"modules" option should work with a modules.auto RegExp that returns "true": warnings 1`] = `Array []`;
 
 exports[`"modules" option should work with case \`animation\` (\`modules\` value is \`false)\`: errors 1`] = `Array []`;
 
diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap
index 0802427..b5e12dc 100644
--- a/test/__snapshots__/validate-options.test.js.snap
+++ b/test/__snapshots__/validate-options.test.js.snap
@@ -43,6 +43,20 @@ exports[`validate options should throw an error on the "localsConvention" option
    -> Style of exported classnames (https://github.com/webpack-contrib/css-loader#localsconvention)."
 `;
 
+exports[`validate options should throw an error on the "modules" option with "{"auto":"invalid"}" 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 { 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.auto should be one of these:
+      RegExp | function | boolean
+      Details:
+       * options.modules.auto should be an instance of RegExp.
+       * options.modules.auto should be an instance of function.
+       * options.modules.auto should be a boolean."
+`;
+
 exports[`validate options should throw an error on the "modules" option with "{"context":true}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules.context should be a string."
diff --git a/test/modules-option.test.js b/test/modules-option.test.js
index 23617a3..b26026b 100644
--- a/test/modules-option.test.js
+++ b/test/modules-option.test.js
@@ -689,7 +689,7 @@ describe('"modules" option', () => {
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
 
-  it('should work with a modules.auto equal "false"', async () => {
+  it('should work with a modules.auto Boolean that is "false"', async () => {
     const compiler = getCompiler('./modules/mode/modules.js', {
       modules: {
         auto: false,
@@ -707,7 +707,7 @@ describe('"modules" option', () => {
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
 
-  it('should work with a modules.auto equal "true"', async () => {
+  it('should work with a modules.auto Boolean that is "true"', async () => {
     const compiler = getCompiler('./modules/mode/modules.js', {
       modules: {
         auto: true,
@@ -725,7 +725,7 @@ describe('"modules" option', () => {
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
 
-  it('should work with a modules.auto returns "true"', async () => {
+  it('should work with a modules.auto RegExp that returns "true"', async () => {
     const compiler = getCompiler('./modules/mode/modules.js', {
       modules: {
         auto: /relative.module.css$/,
@@ -743,7 +743,7 @@ describe('"modules" option', () => {
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
 
-  it('should work with a modules.auto returns "false"', async () => {
+  it('should work with a modules.auto RegExp that returns "false"', async () => {
     const compiler = getCompiler('./modules/mode/modules.js', {
       modules: {
         auto: /will no pass/,
@@ -760,4 +760,40 @@ describe('"modules" option', () => {
     expect(getWarnings(stats)).toMatchSnapshot('warnings');
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
+
+  it('should work with a modules.auto Function that returns "true"', async () => {
+    const compiler = getCompiler('./modules/mode/modules.js', {
+      modules: {
+        auto: (relativePath) => relativePath.endsWith('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 Function that returns "false"', async () => {
+    const compiler = getCompiler('./modules/mode/modules.js', {
+      modules: {
+        auto: (relativePath) => relativePath.endsWith('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');
+  });
 });
diff --git a/test/validate-options.test.js b/test/validate-options.test.js
index 96a595c..40d5501 100644
--- a/test/validate-options.test.js
+++ b/test/validate-options.test.js
@@ -28,6 +28,10 @@ describe('validate options', () => {
         { localIdentRegExp: 'page-(.*)\\.js' },
         { localIdentRegExp: /page-(.*)\.js/ },
         { exportGlobals: true },
+        { auto: true },
+        { auto: false },
+        { auto: /custom-regex/ },
+        { auto: () => true },
       ],
       failure: [
         'true',
@@ -44,6 +48,7 @@ describe('validate options', () => {
         { getLocalIdent: [] },
         { localIdentRegExp: true },
         { exportGlobals: 'invalid' },
+        { auto: 'invalid' },
       ],
     },
     sourceMap: {