Repository: webpack/webpack (JavaScript)

Issue:
`experiments.css` cannot be disabled when `experiments.futureDefaults` is enabled
# Bug report

**What is the current behavior?**

When `experiements.futureDefaults` is enabled but `experiments.css` is disabled, webpack fails with the following error.

```
Error: Conflict: Multiple chunks emit assets to the same filename main.css
```

I believe this behaviour was introduced in `5.67.0` via (#15200). Specifically...
https://github.com/webpack/webpack/blob/1489b91a98daf7e59207da7d3b29ad12750a9ecf/lib/config/normalization.js#L185

When `experiments.css` is set to `false`, its value is normalised as `undefined`. So the `experiments.futureDefaults` value is used in its place here...
https://github.com/webpack/webpack/blob/1489b91a98daf7e59207da7d3b29ad12750a9ecf/lib/config/defaults.js#L164

**If the current behavior is a bug, please provide the steps to reproduce.**

`webpack.config.js`

```javascript
{
  experiments: {
    css: false,
    futureDefaults: true,
  },
}
```

**What is the expected behavior?**

webpack should build successfully with `experiments.css` disabled.

I would like to be able to enable `experiments.futureDefaults` so that I can ensure compatibility with the next major webpack version. But for some experiments, like `experiments.css`, I would like to be able opt-out for now until I better understand the upgrade path.

**Other relevant information:**
webpack version:  5.72.0
Node.js version: 16.14.2
Operating System: Ubuntu


Hints:
Thanks for a report, feel free to send a fix

Patch:
diff --git a/declarations/WebpackOptions.d.ts b/declarations/WebpackOptions.d.ts
--- a/declarations/WebpackOptions.d.ts
+++ b/declarations/WebpackOptions.d.ts
@@ -3514,11 +3514,11 @@ export interface ExperimentsNormalizedExtra {
 	/**
 	 * Enable css support.
 	 */
-	css?: CssExperimentOptions;
+	css?: false | CssExperimentOptions;
 	/**
 	 * Compile entrypoints and import()s only when they are accessed.
 	 */
-	lazyCompilation?: LazyCompilationOptions;
+	lazyCompilation?: false | LazyCompilationOptions;
 }
 /**
  * If an dependency matches exactly a property of the object, the property value is used as dependency.
diff --git a/lib/config/defaults.js b/lib/config/defaults.js
--- a/lib/config/defaults.js
+++ b/lib/config/defaults.js
@@ -485,7 +485,7 @@ const applyJavascriptParserOptionsDefaults = (
  * @param {boolean} options.cache is caching enabled
  * @param {boolean} options.syncWebAssembly is syncWebAssembly enabled
  * @param {boolean} options.asyncWebAssembly is asyncWebAssembly enabled
- * @param {CssExperimentOptions} options.css is css enabled
+ * @param {CssExperimentOptions|false} options.css is css enabled
  * @param {boolean} options.futureDefaults is future defaults enabled
  * @param {boolean} options.isNode is node target platform
  * @returns {void}
@@ -1122,7 +1122,7 @@ const applyPerformanceDefaults = (performance, { production }) => {
  * @param {Object} options options
  * @param {boolean} options.production is production
  * @param {boolean} options.development is development
- * @param {CssExperimentOptions} options.css is css enabled
+ * @param {CssExperimentOptions|false} options.css is css enabled
  * @param {boolean} options.records using records
  * @returns {void}
  */
diff --git a/lib/config/normalization.js b/lib/config/normalization.js
--- a/lib/config/normalization.js
+++ b/lib/config/normalization.js
@@ -178,11 +178,10 @@ const getNormalizedWebpackOptions = config => {
 			),
 			lazyCompilation: optionalNestedConfig(
 				experiments.lazyCompilation,
-				options =>
-					options === true ? {} : options === false ? undefined : options
+				options => (options === true ? {} : options)
 			),
 			css: optionalNestedConfig(experiments.css, options =>
-				options === true ? {} : options === false ? undefined : options
+				options === true ? {} : options
 			)
 		})),
 		externals: config.externals,
diff --git a/schemas/WebpackOptions.json b/schemas/WebpackOptions.json
--- a/schemas/WebpackOptions.json
+++ b/schemas/WebpackOptions.json
@@ -897,7 +897,10 @@
         },
         "css": {
           "description": "Enable css support.",
-          "oneOf": [
+          "anyOf": [
+            {
+              "enum": [false]
+            },
             {
               "$ref": "#/definitions/CssExperimentOptions"
             }
@@ -913,7 +916,10 @@
         },
         "lazyCompilation": {
           "description": "Compile entrypoints and import()s only when they are accessed.",
-          "oneOf": [
+          "anyOf": [
+            {
+              "enum": [false]
+            },
             {
               "$ref": "#/definitions/LazyCompilationOptions"
             }
diff --git a/types.d.ts b/types.d.ts
--- a/types.d.ts
+++ b/types.d.ts
@@ -3543,12 +3543,12 @@ declare interface ExperimentsNormalizedExtra {
 	/**
 	 * Enable css support.
 	 */
-	css?: CssExperimentOptions;
+	css?: false | CssExperimentOptions;
 
 	/**
 	 * Compile entrypoints and import()s only when they are accessed.
 	 */
-	lazyCompilation?: LazyCompilationOptions;
+	lazyCompilation?: false | LazyCompilationOptions;
 }
 declare abstract class ExportInfo {
 	name: string;


Test patch:
diff --git a/test/Defaults.unittest.js b/test/Defaults.unittest.js
--- a/test/Defaults.unittest.js
+++ b/test/Defaults.unittest.js
@@ -2221,6 +2221,84 @@ describe("snapshots", () => {
 			+       /^(.+?[\\\\/]node_modules[\\\\/])/,
 		`)
 	);
+
+	test(
+		"experiments.futureDefaults w/ experiments.css disabled",
+		{
+			experiments: {
+				css: false,
+				futureDefaults: true
+			}
+		},
+		e =>
+			e.toMatchInlineSnapshot(`
+			- Expected
+			+ Received
+
+			@@ ... @@
+			-     "asyncWebAssembly": false,
+			-     "backCompat": true,
+			+     "asyncWebAssembly": true,
+			+     "backCompat": false,
+			@@ ... @@
+			-     "cacheUnaffected": false,
+			-     "css": undefined,
+			-     "futureDefaults": false,
+			+     "cacheUnaffected": true,
+			+     "css": false,
+			+     "futureDefaults": true,
+			@@ ... @@
+			-     "topLevelAwait": false,
+			+     "topLevelAwait": true,
+			@@ ... @@
+			+       },
+			+       Object {
+			+         "rules": Array [
+			+           Object {
+			+             "descriptionData": Object {
+			+               "type": "module",
+			+             },
+			+             "resolve": Object {
+			+               "fullySpecified": true,
+			+             },
+			+           },
+			+         ],
+			+         "test": /\\.wasm$/i,
+			+         "type": "webassembly/async",
+			@@ ... @@
+			+         "mimetype": "application/wasm",
+			+         "rules": Array [
+			+           Object {
+			+             "descriptionData": Object {
+			+               "type": "module",
+			+             },
+			+             "resolve": Object {
+			+               "fullySpecified": true,
+			+             },
+			+           },
+			+         ],
+			+         "type": "webassembly/async",
+			+       },
+			+       Object {
+			@@ ... @@
+			+         "exportsPresence": "error",
+			@@ ... @@
+			-     "__dirname": "mock",
+			-     "__filename": "mock",
+			-     "global": true,
+			+     "__dirname": "warn-mock",
+			+     "__filename": "warn-mock",
+			+     "global": "warn",
+			@@ ... @@
+			-     "hashDigestLength": 20,
+			-     "hashFunction": "md4",
+			+     "hashDigestLength": 16,
+			+     "hashFunction": "xxhash64",
+			@@ ... @@
+			-       "<cwd>/node_modules/",
+			+       /^(.+?[\\\\/]node_modules[\\\\/])/,
+		`)
+	);
 });
 
 it("should result in the same target options for same target", () => {