diff --git a/examples/css/README.md b/examples/css/README.md index 08b76f663b4..c7bb4729180 100644 --- a/examples/css/README.md +++ b/examples/css/README.md @@ -14,11 +14,29 @@ document.getElementsByTagName("main")[0].className = main; ```javascript @import "style-imported.css"; @import "https://fonts.googleapis.com/css?family=Open+Sans"; +@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 @@ -34,7 +52,7 @@ body { \*************************/ /*! default exports */ /*! exports [not provided] [no usage info] */ -/*! runtime requirements: module, __webpack_require__.p, __webpack_require__.* */ +/*! runtime requirements: __webpack_require__.p, module, __webpack_require__.* */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { module.exports = __webpack_require__.p + "89a353e9c515885abd8e.png"; @@ -393,12 +411,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 */ 6); +/* harmony import */ var _style_module_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./style.module.css */ 7); -__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 */ 8)); document.getElementsByTagName("main")[0].className = _style_module_css__WEBPACK_IMPORTED_MODULE_2__.main; @@ -418,26 +436,53 @@ document.getElementsByTagName("main")[0].className = _style_module_css__WEBPACK_ background: url(89a353e9c515885abd8e.png); } +@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-7-large: 72px; } -.app-6-main { - font-size: var(--app-6-large); +.app-7-main { + font-size: var(--app-7-large); color: darkblue; } -head{--webpack-app-0:_4,_2,_1,_5,large%main/_6;} +head{--webpack-app-0:_4,_2,_5,_1,_6,large%main/_7;} ``` ## production @@ -450,12 +495,39 @@ head{--webpack-app-0:_4,_2,_1,_5,large%main/_6;} background: url(89a353e9c515885abd8e.png); } +@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; } @@ -469,7 +541,7 @@ body { color: darkblue; } -head{--webpack-app-179:_548,_431,_258,_268,b%D/_491;} +head{--webpack-app-179:_548,_431,_252,_258,_268,b%D/_491;} ``` # dist/1.output.css @@ -479,7 +551,7 @@ body { color: blue; } -head{--webpack-app-1:_7;} +head{--webpack-app-1:_8;} ``` # Info @@ -487,16 +559,16 @@ head{--webpack-app-1:_7;} ## Unoptimized ``` -assets by chunk 16.9 KiB (name: main) +assets by chunk 17.2 KiB (name: main) asset output.js 16.5 KiB [emitted] (name: main) - asset output.css 385 bytes [emitted] (name: main) + asset output.css 757 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 16.9 KiB (14.6 KiB) = output.js 16.5 KiB output.css 385 bytes 1 auxiliary asset -chunk (runtime: main) output.js, output.css (main) 218 bytes (javascript) 335 bytes (css) 14.6 KiB (asset) 42 bytes (css-import) 10 KiB (runtime) [entry] [rendered] +Entrypoint main 17.2 KiB (14.6 KiB) = output.js 16.5 KiB output.css 757 bytes 1 auxiliary asset +chunk (runtime: main) output.js, output.css (main) 218 bytes (javascript) 699 bytes (css) 14.6 KiB (asset) 42 bytes (css-import) 10 KiB (runtime) [entry] [rendered] > ./example.js main runtime modules 10 KiB 9 modules - dependent modules 42 bytes (javascript) 14.6 KiB (asset) 335 bytes (css) 42 bytes (css-import) [dependent] 6 modules + dependent modules 42 bytes (javascript) 14.6 KiB (asset) 699 bytes (css) 42 bytes (css-import) [dependent] 7 modules ./example.js 176 bytes [built] [code generated] [no exports] [used exports unknown] @@ -507,30 +579,30 @@ chunk (runtime: main) 1.output.css 23 bytes [no exports] [used exports unknown] import() ./lazy-style.css ./example.js 4:0-26 -webpack 5.66.0 compiled successfully +webpack 5.72.1 compiled successfully ``` ## Production mode ``` -assets by chunk 4.25 KiB (name: main) +assets by chunk 4.62 KiB (name: main) asset output.js 3.87 KiB [emitted] [minimized] (name: main) - asset output.css 385 bytes [emitted] (name: main) + asset output.css 759 bytes [emitted] (name: main) asset 89a353e9c515885abd8e.png 14.6 KiB [emitted] [immutable] [from: images/file.png] (auxiliary name: main) asset 159.output.css 53 bytes [emitted] -Entrypoint main 4.25 KiB (14.6 KiB) = output.js 3.87 KiB output.css 385 bytes 1 auxiliary asset +Entrypoint main 4.62 KiB (14.6 KiB) = output.js 3.87 KiB output.css 759 bytes 1 auxiliary asset 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) 335 bytes (css) 14.6 KiB (asset) 42 bytes (css-import) 10 KiB (runtime) [entry] [rendered] +chunk (runtime: main) output.js, output.css (main) 218 bytes (javascript) 699 bytes (css) 14.6 KiB (asset) 42 bytes (css-import) 10 KiB (runtime) [entry] [rendered] > ./example.js main runtime modules 10 KiB 9 modules - dependent modules 42 bytes (javascript) 14.6 KiB (asset) 335 bytes (css) 42 bytes (css-import) [dependent] 6 modules + dependent modules 42 bytes (javascript) 14.6 KiB (asset) 699 bytes (css) 42 bytes (css-import) [dependent] 7 modules ./example.js 176 bytes [built] [code generated] [no exports] [no exports used] entry ./example.js main -webpack 5.66.0 compiled successfully +webpack 5.72.1 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..63195ae0125 100644 --- a/examples/css/style.css +++ b/examples/css/style.css @@ -1,7 +1,25 @@ @import "style-imported.css"; @import "https://fonts.googleapis.com/css?family=Open+Sans"; +@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/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 6e96a152372..df466c706e7 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: @@ -198,6 +200,7 @@ class CssParser extends Parser { } return [pos, text.trimRight()]; }; + const eatAtRuleNested = eatUntil("{};/"); const eatExportName = eatUntil(":};/"); const eatExportValue = eatUntil("};/"); const parseExports = (input, pos) => { @@ -307,11 +310,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, @@ -330,11 +335,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; } } @@ -356,7 +407,9 @@ class CssParser extends Parser { modeData = { start: start, url: undefined, - supports: undefined + layer: undefined, + supports: undefined, + media: undefined }; } if (name === "@keyframes") { @@ -380,12 +433,28 @@ class CssParser extends Parser { modeNestingLevel = 1; 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); @@ -395,6 +464,7 @@ class CssParser extends Parser { const dep = new CssImportDependency( modeData.url, [modeData.start, end], + modeData.layer, modeData.supports, media ); @@ -474,6 +544,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..e750f698ca4 --- /dev/null +++ b/lib/dependencies/CssImportDecoratorDependency.js @@ -0,0 +1,109 @@ +/* + 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("../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 originalSource = `${source.source()}`; + const start = 0; + const end = originalSource.length - 1; + + source.replace( + start, + end, + Array(decorations.length) + .fill() + .reduce(tpl => Template.indent(tpl), `\n${originalSource}`) + ); + + decorations.forEach(([before, after], idx) => { + source.insert( + start, + `${idx !== 0 ? "\n" : ""}${Array(idx) + .fill() + .reduce(tpl => Template.indent(tpl), before)}` + ); + source.insert( + end, + `\n${Array(decorations.length - 1 - idx) + .fill() + .reduce(tpl => Template.indent(tpl), after)}` + ); + }); + } +}; + +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..293baf8953a 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,28 @@ CssImportDependency.Template = class CssImportDependencyTemplate extends ( const dep = /** @type {CssImportDependency} */ (dependency); source.replace(dep.range[0], dep.range[1] - 1, ""); + + if ( + // Do nothing more for external modules + /^(\/\/|https?:\/\/|std:)/.test(dep.request) || + // Do nothing more when there is no import query + (!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 5f432073ae2..542d60a2300 100644 --- a/test/configCases/css/css-modules-in-node/index.js +++ b/test/configCases/css/css-modules-in-node/index.js @@ -20,7 +20,8 @@ it("should allow to create css modules", done => { animation: prod ? "my-app-491-oQ" : "./style.module.css-animation", vars: prod ? "--my-app-491-y4 my-app-491-gR undefined my-app-491-xk" - : "--./style.module.css-local-color ./style.module.css-vars undefined ./style.module.css-globalVars" + : "--./style.module.css-local-color ./style.module.css-vars undefined ./style.module.css-globalVars", + 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 7ec402925fb..64b510c4e1a 100644 --- a/test/configCases/css/css-modules/index.js +++ b/test/configCases/css/css-modules/index.js @@ -23,7 +23,8 @@ it("should allow to create css modules", done => { animation: prod ? "my-app-491-oQ" : "./style.module.css-animation", vars: prod ? "--my-app-491-y4 my-app-491-gR undefined my-app-491-xk" - : "--./style.module.css-local-color ./style.module.css-vars undefined ./style.module.css-globalVars" + : "--./style.module.css-local-color ./style.module.css-vars undefined ./style.module.css-globalVars", + 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 70a1cd2facf..c26c1e294c5 100644 --- a/test/configCases/css/css-modules/style.module.css +++ b/test/configCases/css/css-modules/style.module.css @@ -69,3 +69,9 @@ color: var(--global-color); --global-color: red; } + +@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 41f606240b7..7fa6755a710 100644 --- a/test/configCases/css/css-modules/use-style.js +++ b/test/configCases/css/css-modules/use-style.js @@ -10,5 +10,6 @@ export default { ident, keyframes: style.localkeyframes, animation: style.animation, - vars: `${style["local-color"]} ${style.vars} ${style["global-color"]} ${style.globalVars}` + vars: `${style["local-color"]} ${style.vars} ${style["global-color"]} ${style.globalVars}`, + 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", + ";", + ], + ] + `) + ); });