diff --git a/examples/css/README.md b/examples/css/README.md index cc1b19e6daa..d87ad8e5198 100644 --- a/examples/css/README.md +++ b/examples/css/README.md @@ -13,12 +13,31 @@ document.getElementsByTagName("main")[0].className = main; ```javascript @import "style-imported.css"; -@import "https://fonts.googleapis.com/css?family=Open+Sans"; +@import "https://fonts.googleapis.com/css?family=Open+Sans" screen; +@import "https://unpkg.com/jquery-ui@1.13.1/themes/base/draggable.css" supports(touch-action: none); +@import url( "style3.css" ) layer( base ) supports( font-weight: bold ) screen and (min-width: 1024px); + +@layer base, special; body { background: green; font-family: "Open Sans"; } + +@layer special { + .item { + color: rebeccapurple; + } +} + +@layer base { + .item { + color: black; + border: 5px solid black; + font-size: 1.3em; + padding: .5em; + } +} ``` # dist/output.js @@ -39,6 +58,19 @@ body { module.exports = __webpack_require__.p + "89a353e9c515885abd8e.png"; +/***/ }), + +/***/ 5: +/*!****************************************************************************************************************!*\ + !*** https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc.ttf ***! + \****************************************************************************************************************/ +/*! default exports */ +/*! exports [not provided] [no usage info] */ +/*! runtime requirements: __webpack_require__.p, module, __webpack_require__.* */ +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +module.exports = __webpack_require__.p + "8b49cef9eef7a6b1c4cb.ttf"; + /***/ }) /******/ }); @@ -150,7 +182,6 @@ module.exports = __webpack_require__.p + "89a353e9c515885abd8e.png"; /******/ doneFns && doneFns.forEach((fn) => (fn(event))); /******/ if(prev) return prev(event); /******/ } -/******/ ; /******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000); /******/ script.onerror = onScriptComplete.bind(null, script.onerror); /******/ script.onload = onScriptComplete.bind(null, script.onload); @@ -393,12 +424,12 @@ var __webpack_exports__ = {}; /*! runtime requirements: __webpack_require__, __webpack_require__.r, __webpack_exports__, __webpack_require__.e, __webpack_require__.* */ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _style_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./style.css */ 1); -/* harmony import */ var _style2_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./style2.css */ 5); -/* harmony import */ var _style_module_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./style.module.css */ 6); +/* harmony import */ var _style2_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./style2.css */ 8); +/* harmony import */ var _style_module_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./style.module.css */ 9); -__webpack_require__.e(/*! import() */ 1).then(__webpack_require__.bind(__webpack_require__, /*! ./lazy-style.css */ 7)); +__webpack_require__.e(/*! import() */ 1).then(__webpack_require__.bind(__webpack_require__, /*! ./lazy-style.css */ 10)); document.getElementsByTagName("main")[0].className = _style_module_css__WEBPACK_IMPORTED_MODULE_2__.main; @@ -411,7 +442,6 @@ document.getElementsByTagName("main")[0].className = _style_module_css__WEBPACK_ # dist/output.css ```javascript -@import url("https://fonts.googleapis.com/css?family=Open+Sans"); .img { width: 150px; height: 150px; @@ -419,43 +449,95 @@ document.getElementsByTagName("main")[0].className = _style_module_css__WEBPACK_ } +@media screen { + @font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: normal; + src: url(8b49cef9eef7a6b1c4cb.ttf) format('truetype'); + } +} + +@supports(touch-action: none) { + /*! + * jQuery UI Draggable 1.13.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + .ui-draggable-handle { + -ms-touch-action: none; + touch-action: none; + } +} + +@layer base { + @supports(font-weight: bold) { + @media screen and (min-width: 1024px) { + body { + font-weight: bold; + text-decoration: underline; + } + } + } +} + +@layer base, special; + body { background: green; font-family: "Open Sans"; } +@layer special { + .item { + color: rebeccapurple; + } +} + +@layer base { + .item { + color: black; + border: 5px solid black; + font-size: 1.3em; + padding: .5em; + } +} + body { background: red; } :root { - --app-6-large: 72px; + --app-9-large: 72px; } -.app-6-main { - font-size: var(--app-6-large); +.app-9-main { + font-size: var(--app-9-large); color: darkblue; } @media (min-width: 1024px) { - .app-6-main { + .app-9-main { color: green; } } @supports (display: grid) { - .app-6-main { - display: grid - } + .app-9-main { + display: grid + } } -head{--webpack-app-0:_4,_2,_1,_5,large%main/_6;} +head{--webpack-app-0:_2,_4,_6,_7,_1,_8,large%main/_9;} ``` ## production ```javascript -@import url("https://fonts.googleapis.com/css?family=Open+Sans"); .img { width: 150px; height: 150px; @@ -463,11 +545,64 @@ head{--webpack-app-0:_4,_2,_1,_5,large%main/_6;} } +@media screen { + @font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: normal; + src: url(8b49cef9eef7a6b1c4cb.ttf) format('truetype'); + } +} + +@supports(touch-action: none) { + /*! + * jQuery UI Draggable 1.13.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + .ui-draggable-handle { + -ms-touch-action: none; + touch-action: none; + } +} + +@layer base { + @supports(font-weight: bold) { + @media screen and (min-width: 1024px) { + body { + font-weight: bold; + text-decoration: underline; + } + } + } +} + +@layer base, special; + body { background: green; font-family: "Open Sans"; } +@layer special { + .item { + color: rebeccapurple; + } +} + +@layer base { + .item { + color: black; + border: 5px solid black; + font-size: 1.3em; + padding: .5em; + } +} + body { background: red; } @@ -488,12 +623,12 @@ body { } @supports (display: grid) { - .app-491-D { - display: grid - } + .app-491-D { + display: grid + } } -head{--webpack-app-179:_548,_431,_258,_268,b%D/_491;} +head{--webpack-app-179:_431,_572,_863,_252,_258,_268,b%D/_491;} ``` # dist/1.output.css @@ -503,7 +638,7 @@ body { color: blue; } -head{--webpack-app-1:_7;} +head{--webpack-app-1:_10;} ``` # Info @@ -511,16 +646,18 @@ head{--webpack-app-1:_7;} ## Unoptimized ``` -assets by chunk 17 KiB (name: main) - asset output.js 16.5 KiB [emitted] (name: main) - asset output.css 520 bytes [emitted] (name: main) -asset 89a353e9c515885abd8e.png 14.6 KiB [emitted] [immutable] [from: images/file.png] (auxiliary name: main) -asset 1.output.css 49 bytes [emitted] -Entrypoint main 17 KiB (14.6 KiB) = output.js 16.5 KiB output.css 520 bytes 1 auxiliary asset -chunk (runtime: main) output.js, output.css (main) 218 bytes (javascript) 458 bytes (css) 14.6 KiB (asset) 42 bytes (css-import) 10 KiB (runtime) [entry] [rendered] +assets by info 45.2 KiB [immutable] + asset 8b49cef9eef7a6b1c4cb.ttf 30.6 KiB [emitted] [immutable] [from: https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc.ttf] (auxiliary name: main) + asset 89a353e9c515885abd8e.png 14.6 KiB [emitted] [immutable] [from: images/file.png] (auxiliary name: main) +assets by chunk 18.4 KiB (name: main) + asset output.js 17.1 KiB [emitted] (name: main) + asset output.css 1.29 KiB [emitted] (name: main) +asset 1.output.css 50 bytes [emitted] +Entrypoint main 18.4 KiB (45.2 KiB) = output.js 17.1 KiB output.css 1.29 KiB 2 auxiliary assets +chunk (runtime: main) output.js, output.css (main) 260 bytes (javascript) 1.39 KiB (css) 45.2 KiB (asset) 10 KiB (runtime) [entry] [rendered] > ./example.js main + dependent modules 84 bytes (javascript) 45.2 KiB (asset) 1.39 KiB (css) [dependent] 9 modules runtime modules 10 KiB 9 modules - dependent modules 42 bytes (javascript) 14.6 KiB (asset) 458 bytes (css) 42 bytes (css-import) [dependent] 6 modules ./example.js 176 bytes [built] [code generated] [no exports] [used exports unknown] @@ -531,30 +668,32 @@ chunk (runtime: main) 1.output.css 23 bytes [no exports] [used exports unknown] import() ./lazy-style.css ./example.js 4:0-26 -webpack 5.72.0 compiled successfully +webpack 5.77.0 compiled successfully ``` ## Production mode ``` -assets by chunk 4.38 KiB (name: main) - asset output.js 3.87 KiB [emitted] [minimized] (name: main) - asset output.css 518 bytes [emitted] (name: main) -asset 89a353e9c515885abd8e.png 14.6 KiB [emitted] [immutable] [from: images/file.png] (auxiliary name: main) +assets by info 45.2 KiB [immutable] + asset 8b49cef9eef7a6b1c4cb.ttf 30.6 KiB [emitted] [immutable] [from: https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc.ttf] (auxiliary name: main) + asset 89a353e9c515885abd8e.png 14.6 KiB [emitted] [immutable] [from: images/file.png] (auxiliary name: main) +assets by chunk 5.22 KiB (name: main) + asset output.js 3.93 KiB [emitted] [minimized] (name: main) + asset output.css 1.29 KiB [emitted] (name: main) asset 159.output.css 53 bytes [emitted] -Entrypoint main 4.38 KiB (14.6 KiB) = output.js 3.87 KiB output.css 518 bytes 1 auxiliary asset +Entrypoint main 5.22 KiB (45.2 KiB) = output.js 3.93 KiB output.css 1.29 KiB 2 auxiliary assets chunk (runtime: main) 159.output.css 23 bytes > ./lazy-style.css ./example.js 4:0-26 ./lazy-style.css 23 bytes [built] [code generated] [no exports] import() ./lazy-style.css ./example.js 4:0-26 -chunk (runtime: main) output.js, output.css (main) 218 bytes (javascript) 458 bytes (css) 14.6 KiB (asset) 42 bytes (css-import) 10 KiB (runtime) [entry] [rendered] +chunk (runtime: main) output.js, output.css (main) 260 bytes (javascript) 1.39 KiB (css) 45.2 KiB (asset) 10 KiB (runtime) [entry] [rendered] > ./example.js main + dependent modules 84 bytes (javascript) 45.2 KiB (asset) 1.39 KiB (css) [dependent] 9 modules runtime modules 10 KiB 9 modules - dependent modules 42 bytes (javascript) 14.6 KiB (asset) 458 bytes (css) 42 bytes (css-import) [dependent] 6 modules ./example.js 176 bytes [built] [code generated] [no exports] [no exports used] entry ./example.js main -webpack 5.72.0 compiled successfully +webpack 5.77.0 compiled successfully ``` diff --git a/examples/css/index.html b/examples/css/index.html index 9b3f06397ab..2212111ec25 100644 --- a/examples/css/index.html +++ b/examples/css/index.html @@ -5,6 +5,12 @@
Hello World

+
+ I am displayed in color: rebeccapurple because the + special layer comes after the base layer. My + black border, font-size, and padding come from the + base layer. +
diff --git a/examples/css/style.css b/examples/css/style.css index 8b855420284..b0e9e3d26e8 100644 --- a/examples/css/style.css +++ b/examples/css/style.css @@ -1,7 +1,26 @@ @import "style-imported.css"; -@import "https://fonts.googleapis.com/css?family=Open+Sans"; +@import "https://fonts.googleapis.com/css?family=Open+Sans" screen; +@import "https://unpkg.com/jquery-ui@1.13.1/themes/base/draggable.css" supports(touch-action: none); +@import url( "style3.css" ) layer( base ) supports( font-weight: bold ) screen and (min-width: 1024px); + +@layer base, special; body { background: green; font-family: "Open Sans"; } + +@layer special { + .item { + color: rebeccapurple; + } +} + +@layer base { + .item { + color: black; + border: 5px solid black; + font-size: 1.3em; + padding: .5em; + } +} diff --git a/examples/css/style3.css b/examples/css/style3.css new file mode 100644 index 00000000000..cebb7af0436 --- /dev/null +++ b/examples/css/style3.css @@ -0,0 +1,4 @@ +body { + font-weight: bold; + text-decoration: underline; +} diff --git a/examples/css/webpack.config.js b/examples/css/webpack.config.js index 93ef7f910e6..e1ad7704678 100644 --- a/examples/css/webpack.config.js +++ b/examples/css/webpack.config.js @@ -3,6 +3,14 @@ module.exports = { uniqueName: "app" }, experiments: { + buildHttp: { + allowedUris: [ + "https://fonts.googleapis.com", + "https://fonts.gstatic.com", + "https://unpkg.com" + ], + frozen: false + }, css: true } }; diff --git a/examples/css/webpack.lock b/examples/css/webpack.lock new file mode 100644 index 00000000000..579dc3820a5 --- /dev/null +++ b/examples/css/webpack.lock @@ -0,0 +1,6 @@ +{ + "https://fonts.googleapis.com/css?family=Open+Sans": { "integrity": "sha512-N58gP8WenQgxVpqofshprDTqvAh3tZ/OkhNgKd2uCcRAk6LXLVB8echZKWx78ETx6P9+C1KJfDYMuy9zpBYAfg==", "contentType": "text/css; charset=utf-8" }, + "https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc.ttf": { "integrity": "sha512-PUea/AZwAh372fk4PYolSDpxSWNtgsJ0bzdL6LBVIxn+sRMONrSs3Po7qVk8veWuuJwZq0TZePsXUblXAZCz9w==", "contentType": "font/ttf" }, + "https://unpkg.com/jquery-ui@1.13.1/themes/base/draggable.css": { "integrity": "sha512-ID6NYyrTEGxfWaVutJAR/C+668CmUVWh8htwDW3NuBfdFpezO0ANTzD4Hl2G7HO7BrK1B+OwzauU0vG2f6gSCw==", "contentType": "text/css; charset=utf-8" }, + "version": 1 +} diff --git a/examples/css/webpack.lock.data/https_fonts.googleapis.com/css_family_Open_Sans_6f42d275465c16986db8 b/examples/css/webpack.lock.data/https_fonts.googleapis.com/css_family_Open_Sans_6f42d275465c16986db8 new file mode 100644 index 00000000000..ff64d990c5c --- /dev/null +++ b/examples/css/webpack.lock.data/https_fonts.googleapis.com/css_family_Open_Sans_6f42d275465c16986db8 @@ -0,0 +1,7 @@ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: normal; + src: url(https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc.ttf) format('truetype'); +} diff --git a/examples/css/webpack.lock.data/https_fonts.gstatic.com/s_opensans_v29_memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc_f3d8ca9f27ed4eaad40e.ttf b/examples/css/webpack.lock.data/https_fonts.gstatic.com/s_opensans_v29_memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc_f3d8ca9f27ed4eaad40e.ttf new file mode 100644 index 00000000000..913cf11e754 Binary files /dev/null and b/examples/css/webpack.lock.data/https_fonts.gstatic.com/s_opensans_v29_memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc_f3d8ca9f27ed4eaad40e.ttf differ diff --git a/examples/css/webpack.lock.data/https_unpkg.com/jquery-ui_1.13.1_themes_base_draggable_acb29bf51808e282c28e.css b/examples/css/webpack.lock.data/https_unpkg.com/jquery-ui_1.13.1_themes_base_draggable_acb29bf51808e282c28e.css new file mode 100644 index 00000000000..dddbcb6d17e --- /dev/null +++ b/examples/css/webpack.lock.data/https_unpkg.com/jquery-ui_1.13.1_themes_base_draggable_acb29bf51808e282c28e.css @@ -0,0 +1,12 @@ +/*! + * jQuery UI Draggable 1.13.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ +.ui-draggable-handle { + -ms-touch-action: none; + touch-action: none; +} diff --git a/lib/css/CssModulesPlugin.js b/lib/css/CssModulesPlugin.js index 23c3d5d3517..bb8432bc9f4 100644 --- a/lib/css/CssModulesPlugin.js +++ b/lib/css/CssModulesPlugin.js @@ -10,6 +10,7 @@ const HotUpdateChunk = require("../HotUpdateChunk"); const RuntimeGlobals = require("../RuntimeGlobals"); const SelfModuleFactory = require("../SelfModuleFactory"); const CssExportDependency = require("../dependencies/CssExportDependency"); +const CssImportDecoratorDependency = require("../dependencies/CssImportDecoratorDependency"); const CssImportDependency = require("../dependencies/CssImportDependency"); const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency"); const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency"); @@ -121,6 +122,14 @@ class CssModulesPlugin { CssImportDependency, new CssImportDependency.Template() ); + compilation.dependencyFactories.set( + CssImportDecoratorDependency, + normalModuleFactory + ); + compilation.dependencyTemplates.set( + CssImportDecoratorDependency, + new CssImportDecoratorDependency.Template() + ); compilation.dependencyTemplates.set( StaticExportsDependency, new StaticExportsDependency.Template() diff --git a/lib/css/CssParser.js b/lib/css/CssParser.js index 33383eaa699..174dd675cc6 100644 --- a/lib/css/CssParser.js +++ b/lib/css/CssParser.js @@ -73,10 +73,10 @@ const CSS_MODE_TOP_LEVEL = 0; const CSS_MODE_IN_RULE = 1; const CSS_MODE_IN_LOCAL_RULE = 2; const CSS_MODE_AT_IMPORT_EXPECT_URL = 3; -// TODO implement layer and supports for @import -const CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS = 4; -const CSS_MODE_AT_IMPORT_EXPECT_MEDIA = 5; -const CSS_MODE_AT_OTHER = 6; +const CSS_MODE_AT_IMPORT_EXPECT_LAYER = 4; +const CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS = 5; +const CSS_MODE_AT_IMPORT_EXPECT_MEDIA = 6; +const CSS_MODE_AT_OTHER = 7; const explainMode = mode => { switch (mode) { @@ -88,8 +88,10 @@ const explainMode = mode => { return "parsing css rule content (local)"; case CSS_MODE_AT_IMPORT_EXPECT_URL: return "parsing @import (expecting url)"; + case CSS_MODE_AT_IMPORT_EXPECT_LAYER: + return "parsing @import (expecting optionally layer)"; case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS: - return "parsing @import (expecting optionally supports or media query)"; + return "parsing @import (expecting optionally supports)"; case CSS_MODE_AT_IMPORT_EXPECT_MEDIA: return "parsing @import (expecting optionally media query)"; case CSS_MODE_AT_OTHER: @@ -309,11 +311,13 @@ class CssParser extends Parser { switch (mode) { case CSS_MODE_AT_IMPORT_EXPECT_URL: { modeData.url = value; - mode = CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS; + modePos = end; + mode = CSS_MODE_AT_IMPORT_EXPECT_LAYER; break; } - case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS: + case CSS_MODE_AT_IMPORT_EXPECT_LAYER: case CSS_MODE_AT_IMPORT_EXPECT_MEDIA: + case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS: throw new Error( `Unexpected ${input.slice( start, @@ -332,11 +336,57 @@ class CssParser extends Parser { } return end; }, + layer: (input, start, end, contentStart, contentEnd) => { + const value = cssUnescape(input.slice(contentStart, contentEnd)); + switch (mode) { + case CSS_MODE_AT_IMPORT_EXPECT_LAYER: { + modeData.layer = value.trim(); + modePos = end; + mode = CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS; + break; + } + case CSS_MODE_AT_IMPORT_EXPECT_URL: + case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS: + case CSS_MODE_AT_IMPORT_EXPECT_MEDIA: + throw new Error( + `Unexpected ${input.slice( + start, + end + )} at ${start} during ${explainMode(mode)}` + ); + } + return end; + }, + supports: (input, start, end, contentStart, contentEnd) => { + const value = cssUnescape(input.slice(contentStart, contentEnd)); + switch (mode) { + case CSS_MODE_AT_IMPORT_EXPECT_LAYER: + case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS: { + modeData.supports = value.trim(); + modePos = end; + mode = CSS_MODE_AT_IMPORT_EXPECT_MEDIA; + break; + } + case CSS_MODE_AT_IMPORT_EXPECT_MEDIA: + case CSS_MODE_AT_IMPORT_EXPECT_URL: + throw new Error( + `Unexpected ${input.slice( + start, + end + )} at ${start} during ${explainMode(mode)}` + ); + } + return end; + }, string: (input, start, end) => { switch (mode) { case CSS_MODE_AT_IMPORT_EXPECT_URL: { modeData.url = cssUnescape(input.slice(start + 1, end - 1)); - mode = CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS; + modePos = end; + mode = CSS_MODE_AT_IMPORT_EXPECT_LAYER; + break; + } + default: { break; } } @@ -358,7 +408,9 @@ class CssParser extends Parser { modeData = { start: start, url: undefined, - supports: undefined + layer: undefined, + supports: undefined, + media: undefined }; } if (name === "@keyframes") { @@ -394,12 +446,28 @@ class CssParser extends Parser { } return pos + 1; } + if (name === "@layer") { + let pos = end; + const [newPos] = eatText(input, pos, eatAtRuleNested); + pos = newPos; + if (pos === input.length) return pos; + if ( + input.charCodeAt(pos) !== CC_LEFT_CURLY && + input.charCodeAt(pos) !== CC_SEMICOLON + ) { + throw new Error( + `Unexpected ${input[pos]} at ${pos} during parsing of @layer (expected '{' or ';')` + ); + } + return pos + 1; + } return end; }, semicolon: (input, start, end) => { switch (mode) { case CSS_MODE_AT_IMPORT_EXPECT_URL: throw new Error(`Expected URL for @import at ${start}`); + case CSS_MODE_AT_IMPORT_EXPECT_LAYER: case CSS_MODE_AT_IMPORT_EXPECT_MEDIA: case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS: { const { line: sl, column: sc } = locConverter.get(modeData.start); @@ -409,6 +477,7 @@ class CssParser extends Parser { const dep = new CssImportDependency( modeData.url, [modeData.start, end], + modeData.layer, modeData.supports, media ); @@ -488,6 +557,9 @@ class CssParser extends Parser { lastIdentifier = [start, end]; } break; + case CSS_MODE_AT_IMPORT_EXPECT_MEDIA: + modeData.mediaStart = start - 1; + break; } return end; }, diff --git a/lib/css/walkCssTokens.js b/lib/css/walkCssTokens.js index 6ba1dcaabb3..d083b3d7875 100644 --- a/lib/css/walkCssTokens.js +++ b/lib/css/walkCssTokens.js @@ -9,6 +9,8 @@ * @typedef {Object} CssTokenCallbacks * @property {function(string, number): boolean} isSelector * @property {function(string, number, number, number, number): number=} url + * @property {function(string, number, number, number, number): number=} supports + * @property {function(string, number, number, number, number): number=} layer * @property {function(string, number, number): number=} string * @property {function(string, number, number): number=} leftParenthesis * @property {function(string, number, number): number=} rightParenthesis @@ -57,6 +59,8 @@ const CC_AT_SIGN = "@".charCodeAt(0); const CC_LOW_LINE = "_".charCodeAt(0); const CC_LOWER_A = "a".charCodeAt(0); +const CC_LOWER_L = "l".charCodeAt(0); +const CC_LOWER_S = "s".charCodeAt(0); const CC_LOWER_U = "u".charCodeAt(0); const CC_LOWER_E = "e".charCodeAt(0); const CC_LOWER_Z = "z".charCodeAt(0); @@ -357,6 +361,119 @@ const consumePotentialUrl = (input, pos, callbacks) => { } }; +/** @type {CharHandler} */ +const consumePotentialLayer = (input, pos, callbacks) => { + const start = pos; + pos = _consumeIdentifier(input, pos); + if (pos === start + 5 && input.slice(start, pos + 1) === "layer(") { + pos++; + let cc = input.charCodeAt(pos); + while (_isWhiteSpace(cc)) { + pos++; + if (pos === input.length) return pos; + cc = input.charCodeAt(pos); + } + const contentStart = pos; + let contentEnd; + for (;;) { + while (_isWhiteSpace(cc)) { + pos++; + if (pos === input.length) return pos; + cc = input.charCodeAt(pos); + } + if (cc === CC_RIGHT_PARENTHESIS) { + contentEnd = pos; + let previousPos = pos - 1; + let previousCc = input.charCodeAt(previousPos); + while (_isWhiteSpace(previousCc)) { + contentEnd -= 1; + previousPos -= 1; + previousCc = input.charCodeAt(previousPos); + } + pos++; + if (callbacks.layer !== undefined) { + return callbacks.layer(input, start, pos, contentStart, contentEnd); + } + return pos; + } else if (cc === CC_LEFT_PARENTHESIS) { + return pos; + } else { + pos++; + } + if (pos === input.length) return pos; + cc = input.charCodeAt(pos); + } + } else { + if (callbacks.identifier !== undefined) { + return callbacks.identifier(input, start, pos); + } + return pos; + } +}; + +/** @type {CharHandler} */ +const consumePotentialSupports = (input, pos, callbacks) => { + const start = pos; + let level = 0; + pos = _consumeIdentifier(input, pos); + if (pos === start + 8 && input.slice(start, pos + 1) === "supports(") { + pos++; + let cc = input.charCodeAt(pos); + while (_isWhiteSpace(cc)) { + pos++; + if (pos === input.length) return pos; + cc = input.charCodeAt(pos); + } + const contentStart = pos; + let contentEnd; + for (;;) { + while (_isWhiteSpace(cc)) { + pos++; + if (pos === input.length) return pos; + cc = input.charCodeAt(pos); + } + if (cc === CC_RIGHT_PARENTHESIS) { + if (level > 0) { + level -= 1; + pos++; + } else { + contentEnd = pos; + let previousPos = pos - 1; + let previousCc = input.charCodeAt(previousPos); + while (_isWhiteSpace(previousCc)) { + contentEnd -= 1; + previousPos -= 1; + previousCc = input.charCodeAt(previousPos); + } + pos++; + if (callbacks.supports !== undefined) { + return callbacks.supports( + input, + start, + pos, + contentStart, + contentEnd + ); + } + return pos; + } + } else if (cc === CC_LEFT_PARENTHESIS) { + level += 1; + pos++; + } else { + pos++; + } + if (pos === input.length) return pos; + cc = input.charCodeAt(pos); + } + } else { + if (callbacks.identifier !== undefined) { + return callbacks.identifier(input, start, pos); + } + return pos; + } +}; + /** @type {CharHandler} */ const consumePotentialPseudo = (input, pos, callbacks) => { const start = pos; @@ -568,6 +685,10 @@ const CHAR_MAP = Array.from({ length: 0x80 }, (_, cc) => { return consumeLessThan; case CC_AT_SIGN: return consumeAt; + case CC_LOWER_L: + return consumePotentialLayer; + case CC_LOWER_S: + return consumePotentialSupports; case CC_LOWER_U: return consumePotentialUrl; case CC_LOW_LINE: diff --git a/lib/dependencies/CssImportDecoratorDependency.js b/lib/dependencies/CssImportDecoratorDependency.js new file mode 100644 index 00000000000..2ace0e50dea --- /dev/null +++ b/lib/dependencies/CssImportDecoratorDependency.js @@ -0,0 +1,112 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Ivan Kopeykin @vankop +*/ + +"use strict"; + +const Template = require("../Template"); +const makeSerializable = require("../util/makeSerializable"); +const NullDependency = require("./NullDependency"); + +/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */ +/** @typedef {import("webpack-sources").Source} Source */ +/** @typedef {import("../ChunkGraph")} ChunkGraph */ +/** @typedef {import("../Dependency")} Dependency */ +/** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */ +/** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */ +/** @typedef {import("../Module")} Module */ +/** @typedef {import("../ModuleGraph")} ModuleGraph */ +/** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */ +/** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */ +/** @typedef {import("../util/Hash")} Hash */ +/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ + +class CssImportDecoratorDependency extends NullDependency { + /** + * @param {string[][]} decorations The decorations to wrap the source + */ + constructor(decorations) { + super(); + this.decorations = decorations; + } + + get type() { + return "css @import decorator"; + } +} + +/** + * @param {string | undefined} layer The layer query to parse + * @param {string | undefined} supports The supports query to parse + * @param {string | undefined} media The media query to parse + * @returns {string[][]} The decorations to wrap the source + */ +const getDecorator = (layer, supports, media) => { + const decorations = []; + + if (layer) { + decorations.push([`@layer ${layer} {`, `}`]); + } + + if (supports) { + decorations.push([`@supports(${supports}) {`, `}`]); + } + + if (media) { + decorations.push([`@media ${media} {`, `}`]); + } + + return decorations; +}; + +CssImportDecoratorDependency.Template = class CssImportDecoratorDependencyTemplate extends ( + NullDependency.Template +) { + /** + * @param {Dependency} dependency the dependency for which the template should be applied + * @param {ReplaceSource} source the current replace source which can be modified + * @param {DependencyTemplateContext} templateContext the context object + * @returns {void} + */ + apply(dependency, source, templateContext) { + const dep = /** @type {CssImportDecoratorDependency} */ (dependency); + const { decorations } = dep; + const replacedSource = `${source.source()}`; + + // We use the replaced source and reset the replacements as a patch for to replace all the code + // @ts-expect-error + source._replacements = []; + + source.replace( + 0, + source.size(), + ` +${decorations + .map(([before, after], idx) => { + return Array(idx) + .fill() + .reduce(tpl => Template.indent(tpl), before); + }) + .join("\n")} +${Array(decorations.length) + .fill() + .reduce(tpl => Template.indent(tpl), replacedSource)} +${decorations + .map(([before, after], idx) => { + return Array(decorations.length - 1 - idx) + .fill() + .reduce(tpl => Template.indent(tpl), after); + }) + .join("\n")}` + ); + } +}; + +makeSerializable( + CssImportDecoratorDependency, + "webpack/lib/dependencies/CssImportDecoratorDependency" +); + +module.exports = CssImportDecoratorDependency; +module.exports.getDecorator = getDecorator; diff --git a/lib/dependencies/CssImportDependency.js b/lib/dependencies/CssImportDependency.js index 8f02d6e1fc3..97eb002413a 100644 --- a/lib/dependencies/CssImportDependency.js +++ b/lib/dependencies/CssImportDependency.js @@ -6,6 +6,7 @@ "use strict"; const makeSerializable = require("../util/makeSerializable"); +const CssImportDecoratorDependency = require("./CssImportDecoratorDependency"); const ModuleDependency = require("./ModuleDependency"); /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */ @@ -17,6 +18,7 @@ const ModuleDependency = require("./ModuleDependency"); /** @typedef {import("../ModuleGraph")} ModuleGraph */ /** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */ /** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */ +/** @typedef {import("../NormalModule")} NormalModule */ /** @typedef {import("../util/Hash")} Hash */ /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ @@ -24,12 +26,14 @@ class CssImportDependency extends ModuleDependency { /** * @param {string} request request * @param {[number, number]} range range of the argument + * @param {string | undefined} layer layer rule * @param {string | undefined} supports list of supports conditions * @param {string | undefined} media list of media conditions */ - constructor(request, range, supports, media) { + constructor(request, range, layer, supports, media) { super(request); this.range = range; + this.layer = layer; this.supports = supports; this.media = media; } @@ -64,6 +68,23 @@ CssImportDependency.Template = class CssImportDependencyTemplate extends ( const dep = /** @type {CssImportDependency} */ (dependency); source.replace(dep.range[0], dep.range[1] - 1, ""); + + if (!dep.layer && !dep.media && !dep.supports) { + return; + } + + const dependencyModule = /** @type {NormalModule} */ ( + templateContext.moduleGraph.getModule(dep) + ); + + const decorations = CssImportDecoratorDependency.getDecorator( + dep.layer, + dep.supports, + dep.media + ); + const decoratorDep = new CssImportDecoratorDependency(decorations); + + dependencyModule.addDependency(decoratorDep); } }; diff --git a/lib/util/internalSerializables.js b/lib/util/internalSerializables.js index 4fe124cdb3a..f1d58101287 100644 --- a/lib/util/internalSerializables.js +++ b/lib/util/internalSerializables.js @@ -67,6 +67,8 @@ module.exports = { require("../dependencies/ContextElementDependency"), "dependencies/CriticalDependencyWarning": () => require("../dependencies/CriticalDependencyWarning"), + "dependencies/CssImportDecoratorDependency": () => + require("../dependencies/CssImportDecoratorDependency"), "dependencies/CssImportDependency": () => require("../dependencies/CssImportDependency"), "dependencies/CssLocalIdentifierDependency": () => diff --git a/test/configCases/css/_imports/index.js b/test/configCases/css/_imports/index.js new file mode 100644 index 00000000000..609b5d39199 --- /dev/null +++ b/test/configCases/css/_imports/index.js @@ -0,0 +1,19 @@ +import * as style from "./style.css"; + +/** + * This test is not working due to missing support of @media, @supports and + * @layer in JSDOM (which relies on CSSOM). + **/ +it("should compile at import rules", done => { + expect(style).toEqual(nsObj({})); + import("./style2.css").then(x => { + expect(x).toEqual(nsObj({})); + const style = getComputedStyle(document.body); + expect(style.getPropertyValue("background")).toBe(" orange"); + expect(style.getPropertyValue("display")).toBe(" flex"); + expect(style.getPropertyValue("font-size")).toBe(" 32px"); + expect(style.getPropertyValue("color")).toBe(" orange"); + expect(style.getPropertyValue("padding")).toBe(" 20px 10px"); + done(); + }, done); +}); diff --git a/test/configCases/css/_imports/style-imported1.css b/test/configCases/css/_imports/style-imported1.css new file mode 100644 index 00000000000..054919226e5 --- /dev/null +++ b/test/configCases/css/_imports/style-imported1.css @@ -0,0 +1,3 @@ +body { + background: orange; +} diff --git a/test/configCases/css/_imports/style-imported2.css b/test/configCases/css/_imports/style-imported2.css new file mode 100644 index 00000000000..3ad32ef667b --- /dev/null +++ b/test/configCases/css/_imports/style-imported2.css @@ -0,0 +1,3 @@ +body { + display: flex; +} diff --git a/test/configCases/css/_imports/style-imported3.css b/test/configCases/css/_imports/style-imported3.css new file mode 100644 index 00000000000..72503cac070 --- /dev/null +++ b/test/configCases/css/_imports/style-imported3.css @@ -0,0 +1,3 @@ +body { + font-size: 32px; +} diff --git a/test/configCases/css/_imports/style.css b/test/configCases/css/_imports/style.css new file mode 100644 index 00000000000..f2866193a8d --- /dev/null +++ b/test/configCases/css/_imports/style.css @@ -0,0 +1,7 @@ +@import "style-imported1.css" screen; +@import "style-imported2.css" supports(display: flex); +@import "style-imported3.css" supports(display: block) screen; + +body { + border: 1px solid red; +} diff --git a/test/configCases/css/_imports/style2-imported.css b/test/configCases/css/_imports/style2-imported.css new file mode 100644 index 00000000000..d7d70f7ffae --- /dev/null +++ b/test/configCases/css/_imports/style2-imported.css @@ -0,0 +1,4 @@ +body { + color: orange; + padding: 20px 10px; +} diff --git a/test/configCases/css/_imports/style2.css b/test/configCases/css/_imports/style2.css new file mode 100644 index 00000000000..3deb47485c4 --- /dev/null +++ b/test/configCases/css/_imports/style2.css @@ -0,0 +1,19 @@ +@import "./style2-imported.css" layer(special); + +body { + color: green; +} + +@layer base, special; + +@layer special { + body { + text-decoration: underline; + } +} + +@layer base { + body { + color: green; + } +} diff --git a/test/configCases/css/_imports/test.config.js b/test/configCases/css/_imports/test.config.js new file mode 100644 index 00000000000..0590757288f --- /dev/null +++ b/test/configCases/css/_imports/test.config.js @@ -0,0 +1,8 @@ +module.exports = { + moduleScope(scope) { + const link = scope.window.document.createElement("link"); + link.rel = "stylesheet"; + link.href = "bundle0.css"; + scope.window.document.head.appendChild(link); + } +}; diff --git a/test/configCases/css/_imports/webpack.config.js b/test/configCases/css/_imports/webpack.config.js new file mode 100644 index 00000000000..cfb8e5c0346 --- /dev/null +++ b/test/configCases/css/_imports/webpack.config.js @@ -0,0 +1,8 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + target: "web", + mode: "development", + experiments: { + css: true + } +}; diff --git a/test/configCases/css/css-modules-in-node/index.js b/test/configCases/css/css-modules-in-node/index.js index e7b6603297b..9389515a6f0 100644 --- a/test/configCases/css/css-modules-in-node/index.js +++ b/test/configCases/css/css-modules-in-node/index.js @@ -53,6 +53,7 @@ it("should allow to create css modules", done => { supportsInMedia: prod ? "my-app-491-SQ" : "./style.module.css-displayFlexInSupportsInMedia", + layer: prod ? "my-app-491-EY" : "./style.module.css-layer" }); } catch (e) { return done(e); diff --git a/test/configCases/css/css-modules/index.js b/test/configCases/css/css-modules/index.js index 8fb2375c18e..a1f505e0d33 100644 --- a/test/configCases/css/css-modules/index.js +++ b/test/configCases/css/css-modules/index.js @@ -56,6 +56,7 @@ it("should allow to create css modules", done => { supportsInMedia: prod ? "my-app-491-SQ" : "./style.module.css-displayFlexInSupportsInMedia", + layer: prod ? "my-app-491-EY" : "./style.module.css-layer" }); } catch (e) { return done(e); diff --git a/test/configCases/css/css-modules/style.module.css b/test/configCases/css/css-modules/style.module.css index 8e98a3db9a2..d0e74763d1c 100644 --- a/test/configCases/css/css-modules/style.module.css +++ b/test/configCases/css/css-modules/style.module.css @@ -166,3 +166,8 @@ } } } +@layer default { + .layer { + color: green; + } +} diff --git a/test/configCases/css/css-modules/use-style.js b/test/configCases/css/css-modules/use-style.js index e3f624a9991..8871f2b999f 100644 --- a/test/configCases/css/css-modules/use-style.js +++ b/test/configCases/css/css-modules/use-style.js @@ -27,4 +27,5 @@ export default { supportsWithOperator: style.floatRightInNegativeSupports, mediaInSupports: style.displayFlexInMediaInSupports, supportsInMedia: style.displayFlexInSupportsInMedia, + layer: style.layer }; diff --git a/test/walkCssTokens.unittest.js b/test/walkCssTokens.unittest.js index 75f0b04acd7..29eff995215 100644 --- a/test/walkCssTokens.unittest.js +++ b/test/walkCssTokens.unittest.js @@ -10,6 +10,14 @@ describe("walkCssTokens", () => { results.push(["url", input.slice(s, e), input.slice(cs, ce)]); return e; }, + layer: (input, s, e, cs, ce) => { + results.push(["layer", input.slice(s, e), input.slice(cs, ce)]); + return e; + }, + supports: (input, s, e, cs, ce) => { + results.push(["supports", input.slice(s, e), input.slice(cs, ce)]); + return e; + }, leftCurlyBracket: (input, s, e) => { results.push(["leftCurlyBracket", input.slice(s, e)]); return e; @@ -300,4 +308,114 @@ describe("walkCssTokens", () => { ] `) ); + + test( + "parse at import rules", + `@import url( "style.css" ) layer( my-layer ) supports( display: flex ) screen and (min-width: 1024px); +@import url("style2.css") supports(not (display: flex) and selector(A > B)); +@import url("style3.css") supports((display: grid) and (not (display: inline-grid))); +@import url("style4.css") supports((display: grid) or (display: inline-grid));`, + e => + e.toMatchInlineSnapshot(` + Array [ + Array [ + "atKeyword", + "@import", + ], + Array [ + "url", + "url( \\"style.css\\" )", + "style.css", + ], + Array [ + "layer", + "layer( my-layer )", + "my-layer", + ], + Array [ + "supports", + "supports( display: flex )", + "display: flex", + ], + Array [ + "identifier", + "screen", + ], + Array [ + "identifier", + "and", + ], + Array [ + "leftParenthesis", + "(", + ], + Array [ + "identifier", + "min-width", + ], + Array [ + "rightParenthesis", + ")", + ], + Array [ + "semicolon", + ";", + ], + Array [ + "atKeyword", + "@import", + ], + Array [ + "url", + "url(\\"style2.css\\")", + "style2.css", + ], + Array [ + "supports", + "supports(not (display: flex) and selector(A > B))", + "not (display: flex) and selector(A > B)", + ], + Array [ + "semicolon", + ";", + ], + Array [ + "atKeyword", + "@import", + ], + Array [ + "url", + "url(\\"style3.css\\")", + "style3.css", + ], + Array [ + "supports", + "supports((display: grid) and (not (display: inline-grid)))", + "(display: grid) and (not (display: inline-grid))", + ], + Array [ + "semicolon", + ";", + ], + Array [ + "atKeyword", + "@import", + ], + Array [ + "url", + "url(\\"style4.css\\")", + "style4.css", + ], + Array [ + "supports", + "supports((display: grid) or (display: inline-grid))", + "(display: grid) or (display: inline-grid)", + ], + Array [ + "semicolon", + ";", + ], + ] + `) + ); });