diff --git a/examples/css/README.md b/examples/css/README.md index f68278a7c..cbbe7435d 100644 --- a/examples/css/README.md +++ b/examples/css/README.md @@ -183,7 +183,7 @@ module.exports = __webpack_require__.p + "89a353e9c515885abd8e.png"; /******/ /******/ var uniqueName = "app"; /******/ var loadCssChunkData = (chunkId, link) => { -/******/ var data, token = "", token2, exports = {}, exportsWithId = [], i = 0, cc = 1; +/******/ var data, token = "", token2, exports = {}, exportsWithId = [], exportsWithDashes = [], i = 0, cc = 1; /******/ try { if(!link) link = loadStylesheet(chunkId); data = link.sheet.cssRules; data = data[data.length - 1].style; } catch(e) { data = getComputedStyle(document.head); } /******/ data = data.getPropertyValue("--webpack-" + uniqueName + "-" + chunkId); /******/ if(!data) return; @@ -191,8 +191,8 @@ module.exports = __webpack_require__.p + "89a353e9c515885abd8e.png"; /******/ cc = data.charCodeAt(i); /******/ if(cc == 40) { token2 = token; token = ""; } /******/ else if(cc == 41) { exports[token2.replace(/^_/, "")] = token.replace(/^_/, ""); token = ""; } -/******/ else if(cc == 47) { token = token.replace(/^_/, ""); exports[token] = token; exportsWithId.push(token); token = ""; } -/******/ else if(!cc || cc == 44) { token = token.replace(/^_/, ""); exportsWithId.forEach((x) => (exports[x] = uniqueName + "-" + token + "-" + exports[x])); __webpack_require__.r(exports); __webpack_require__.m[token] = ((exports, module) => { +/******/ else if(cc == 47 || cc == 37) { token = token.replace(/^_/, ""); exports[token] = token; exportsWithId.push(token); if(cc == 37) exportsWithDashes.push(token); token = ""; } +/******/ else if(!cc || cc == 44) { token = token.replace(/^_/, ""); exportsWithId.forEach((x) => (exports[x] = uniqueName + "-" + token + "-" + exports[x])); exportsWithDashes.forEach((x) => (exports[x] = "--" + exports[x])); __webpack_require__.r(exports); __webpack_require__.m[token] = ((exports, module) => { /******/ module.exports = exports; /******/ }).bind(null, exports); token = ""; exports = {}; exportsWithId.length = 0; } /******/ else if(cc == 92) { token += data[++i] } @@ -427,12 +427,48 @@ body { background: red; } +:root { + --app-6-large: 72px; +} + .app-6-main { - font-size: large; + font-size: var(--app-6-large); color: darkblue; } -head{--webpack-app-0:_4,_2,_1,_5,main/_6;} +head{--webpack-app-0:_4,_2,_1,_5,large%main/_6;} +``` + +## production + +```javascript +@import url("https://fonts.googleapis.com/css?family=Open+Sans"); +.img { + width: 150px; + height: 150px; + background: url(89a353e9c515885abd8e.png); +} + + +body { + background: green; + font-family: "Open Sans"; +} + +body { + background: red; +} + +:root { + --app-491-b: 72px; +} + +.app-491-D { + font-size: var(--app-491-b); + color: darkblue; +} + +head{--webpack-app-179:_548,_431,_258,_268,b%D/_491;} ``` # dist/1.output.css @@ -450,16 +486,16 @@ head{--webpack-app-1:_7;} ## Unoptimized ``` -assets by chunk 16.6 KiB (name: main) - asset output.js 16.2 KiB [emitted] (name: main) - asset 0.output.css 333 bytes [emitted] (name: main) +assets by chunk 16.8 KiB (name: main) + asset output.js 16.4 KiB [emitted] (name: main) + asset 0.output.css 385 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.6 KiB (14.6 KiB) = output.js 16.2 KiB 0.output.css 333 bytes 1 auxiliary asset -chunk (runtime: main) output.js, 0.output.css (main) 218 bytes (javascript) 301 bytes (css) 14.6 KiB (asset) 42 bytes (css-import) 9.8 KiB (runtime) [entry] [rendered] +Entrypoint main 16.8 KiB (14.6 KiB) = output.js 16.4 KiB 0.output.css 385 bytes 1 auxiliary asset +chunk (runtime: main) output.js, 0.output.css (main) 218 bytes (javascript) 335 bytes (css) 14.6 KiB (asset) 42 bytes (css-import) 9.94 KiB (runtime) [entry] [rendered] > ./example.js main - runtime modules 9.8 KiB 9 modules - dependent modules 42 bytes (javascript) 14.6 KiB (asset) 301 bytes (css) 42 bytes (css-import) [dependent] 6 modules + runtime modules 9.94 KiB 9 modules + dependent modules 42 bytes (javascript) 14.6 KiB (asset) 335 bytes (css) 42 bytes (css-import) [dependent] 6 modules ./example.js 176 bytes [built] [code generated] [no exports] [used exports unknown] @@ -476,21 +512,21 @@ webpack 5.64.4 compiled successfully ## Production mode ``` -assets by chunk 4.1 KiB (name: main) - asset output.js 3.77 KiB [emitted] [minimized] (name: main) - asset 179.output.css 341 bytes [emitted] (name: main) +assets by chunk 4.2 KiB (name: main) + asset output.js 3.82 KiB [emitted] [minimized] (name: main) + asset 179.output.css 385 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.1 KiB (14.6 KiB) = output.js 3.77 KiB 179.output.css 341 bytes 1 auxiliary asset +Entrypoint main 4.2 KiB (14.6 KiB) = output.js 3.82 KiB 179.output.css 385 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, 179.output.css (main) 218 bytes (javascript) 301 bytes (css) 14.6 KiB (asset) 42 bytes (css-import) 9.8 KiB (runtime) [entry] [rendered] +chunk (runtime: main) output.js, 179.output.css (main) 218 bytes (javascript) 335 bytes (css) 14.6 KiB (asset) 42 bytes (css-import) 9.95 KiB (runtime) [entry] [rendered] > ./example.js main - runtime modules 9.8 KiB 9 modules - dependent modules 42 bytes (javascript) 14.6 KiB (asset) 301 bytes (css) 42 bytes (css-import) [dependent] 6 modules + runtime modules 9.95 KiB 9 modules + dependent modules 42 bytes (javascript) 14.6 KiB (asset) 335 bytes (css) 42 bytes (css-import) [dependent] 6 modules ./example.js 176 bytes [built] [code generated] [no exports] [no exports used] diff --git a/examples/css/style.module.css b/examples/css/style.module.css index 4fac4031e..3fbef791c 100644 --- a/examples/css/style.module.css +++ b/examples/css/style.module.css @@ -1,4 +1,8 @@ +:root { + --large: 72px; +} + .main { - font-size: large; + font-size: var(--large); color: darkblue; } diff --git a/examples/css/template.md b/examples/css/template.md index 841d031dc..9a7dfa888 100644 --- a/examples/css/template.md +++ b/examples/css/template.md @@ -22,6 +22,12 @@ _{{dist/output.js}}_ _{{dist/0.output.css}}_ ``` +## production + +```javascript +_{{production:dist/179.output.css}}_ +``` + # dist/1.output.css ```javascript diff --git a/lib/css/CssLoadingRuntimeModule.js b/lib/css/CssLoadingRuntimeModule.js index c0cbfe49d..5797abae6 100644 --- a/lib/css/CssLoadingRuntimeModule.js +++ b/lib/css/CssLoadingRuntimeModule.js @@ -139,7 +139,7 @@ class CssLoadingRuntimeModule extends RuntimeModule { )};` : "// data-webpack is not used as build has no uniqueName", `var loadCssChunkData = ${runtimeTemplate.basicFunction("chunkId, link", [ - 'var data, token = "", token2, exports = {}, exportsWithId = [], i = 0, cc = 1;', + 'var data, token = "", token2, exports = {}, exportsWithId = [], exportsWithDashes = [], i = 0, cc = 1;', "try { if(!link) link = loadStylesheet(chunkId); data = link.sheet.cssRules; data = data[data.length - 1].style; } catch(e) { data = getComputedStyle(document.head); }", `data = data.getPropertyValue(${ uniqueName @@ -159,9 +159,11 @@ class CssLoadingRuntimeModule extends RuntimeModule { `else if(cc == ${cc( ")" )}) { exports[token2.replace(/^_/, "")] = token.replace(/^_/, ""); token = ""; }`, - `else if(cc == ${cc( - "/" - )}) { token = token.replace(/^_/, ""); exports[token] = token; exportsWithId.push(token); token = ""; }`, + `else if(cc == ${cc("/")} || cc == ${cc( + "%" + )}) { token = token.replace(/^_/, ""); exports[token] = token; exportsWithId.push(token); if(cc == ${cc( + "%" + )}) exportsWithDashes.push(token); token = ""; }`, `else if(!cc || cc == ${cc( "," )}) { token = token.replace(/^_/, ""); exportsWithId.forEach(${runtimeTemplate.expressionFunction( @@ -179,6 +181,9 @@ class CssLoadingRuntimeModule extends RuntimeModule { }) }`, "x" + )}); exportsWithDashes.forEach(${runtimeTemplate.expressionFunction( + `exports[x] = "--" + exports[x]`, + "x" )}); ${RuntimeGlobals.makeNamespaceObject}(exports); ${ RuntimeGlobals.moduleFactories }[token] = (${runtimeTemplate.basicFunction( diff --git a/lib/css/CssModulesPlugin.js b/lib/css/CssModulesPlugin.js index e44580916..69d3c128f 100644 --- a/lib/css/CssModulesPlugin.js +++ b/lib/css/CssModulesPlugin.js @@ -8,9 +8,11 @@ const { ConcatSource } = require("webpack-sources"); const HotUpdateChunk = require("../HotUpdateChunk"); const RuntimeGlobals = require("../RuntimeGlobals"); +const SelfModuleFactory = require("../SelfModuleFactory"); const CssExportDependency = require("../dependencies/CssExportDependency"); const CssImportDependency = require("../dependencies/CssImportDependency"); const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency"); +const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency"); const CssUrlDependency = require("../dependencies/CssUrlDependency"); const StaticExportsDependency = require("../dependencies/StaticExportsDependency"); const { @@ -62,7 +64,7 @@ const escapeCss = (str, omitOptionalUnderscore) => { /[^a-zA-Z0-9_\u0081-\uffff-]/g, s => `\\${s}` ); - return !omitOptionalUnderscore && /^[0-9_-]/.test(escaped) + return !omitOptionalUnderscore && /^(?!--)[0-9_-]/.test(escaped) ? `_${escaped}` : escaped; }; @@ -79,6 +81,7 @@ class CssModulesPlugin { compiler.hooks.compilation.tap( plugin, (compilation, { normalModuleFactory }) => { + const selfFactory = new SelfModuleFactory(compilation.moduleGraph); compilation.dependencyFactories.set( CssUrlDependency, normalModuleFactory @@ -91,6 +94,14 @@ class CssModulesPlugin { CssLocalIdentifierDependency, new CssLocalIdentifierDependency.Template() ); + compilation.dependencyFactories.set( + CssSelfLocalIdentifierDependency, + selfFactory + ); + compilation.dependencyTemplates.set( + CssSelfLocalIdentifierDependency, + new CssSelfLocalIdentifierDependency.Template() + ); compilation.dependencyTemplates.set( CssExportDependency, new CssExportDependency.Template() @@ -262,11 +273,16 @@ class CssModulesPlugin { metaData.push( `${ exports - ? Array.from(exports, ([n, v]) => - v === `${uniqueName ? uniqueName + "-" : ""}${moduleId}-${n}` + ? Array.from(exports, ([n, v]) => { + const shortcutValue = `${ + uniqueName ? uniqueName + "-" : "" + }${moduleId}-${n}`; + return v === shortcutValue ? `${escapeCss(n)}/` - : `${escapeCss(n)}(${escapeCss(v)})` - ).join("") + : v === "--" + shortcutValue + ? `${escapeCss(n)}%` + : `${escapeCss(n)}(${escapeCss(v)})`; + }).join("") : "" }${escapeCss(moduleId)}` ); diff --git a/lib/css/CssParser.js b/lib/css/CssParser.js index edcc75cd7..f67e7eca7 100644 --- a/lib/css/CssParser.js +++ b/lib/css/CssParser.js @@ -10,6 +10,7 @@ const ConstDependency = require("../dependencies/ConstDependency"); const CssExportDependency = require("../dependencies/CssExportDependency"); const CssImportDependency = require("../dependencies/CssImportDependency"); const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency"); +const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency"); const CssUrlDependency = require("../dependencies/CssUrlDependency"); const StaticExportsDependency = require("../dependencies/StaticExportsDependency"); const walkCssTokens = require("./walkCssTokens"); @@ -70,18 +71,21 @@ class LocConverter { const CSS_MODE_TOP_LEVEL = 0; const CSS_MODE_IN_RULE = 1; -const CSS_MODE_AT_IMPORT_EXPECT_URL = 2; +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 = 3; -const CSS_MODE_AT_IMPORT_EXPECT_MEDIA = 4; -const CSS_MODE_AT_OTHER = 5; +const CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS = 4; +const CSS_MODE_AT_IMPORT_EXPECT_MEDIA = 5; +const CSS_MODE_AT_OTHER = 6; const explainMode = mode => { switch (mode) { case CSS_MODE_TOP_LEVEL: return "parsing top level css"; case CSS_MODE_IN_RULE: - return "parsing css rule content"; + return "parsing css rule content (global)"; + case CSS_MODE_IN_LOCAL_RULE: + return "parsing css rule content (local)"; case CSS_MODE_AT_IMPORT_EXPECT_URL: return "parsing @import (expecting url)"; case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS: @@ -124,12 +128,19 @@ class CssParser extends Parser { const module = state.module; + const declaredCssVariables = new Set(); + const locConverter = new LocConverter(source); let mode = CSS_MODE_TOP_LEVEL; let modePos = 0; let modeNestingLevel = 0; let modeData = undefined; + let singleClassSelector = undefined; + let lastIdentifier = undefined; const modeStack = []; + const isTopLevelLocal = () => + modeData === "local" || + (this.defaultMode === "local" && modeData === undefined); const eatWhiteLine = (input, pos) => { for (;;) { const cc = input.charCodeAt(pos); @@ -240,7 +251,57 @@ class CssParser extends Parser { pos = eatWhiteLine(input, pos); return pos; }; + const eatPropertyName = eatUntil(":{};"); + const processLocalDeclaration = (input, pos) => { + modeData = undefined; + const start = pos; + pos = walkCssTokens.eatWhitespaceAndComments(input, pos); + const propertyNameStart = pos; + const [propertyNameEnd, propertyName] = eatText( + input, + pos, + eatPropertyName + ); + if (input.charCodeAt(propertyNameEnd) !== CC_COLON) return start; + pos = propertyNameEnd + 1; + if (propertyName.startsWith("--")) { + // CSS Variable + const { line: sl, column: sc } = locConverter.get(propertyNameStart); + const { line: el, column: ec } = locConverter.get(propertyNameEnd); + const name = propertyName.slice(2); + const dep = new CssLocalIdentifierDependency( + name, + [propertyNameStart, propertyNameEnd], + "--" + ); + dep.setLoc(sl, sc, el, ec); + module.addDependency(dep); + declaredCssVariables.add(name); + } else if ( + propertyName === "animation-name" || + propertyName === "animation" + ) { + modeData = "animation"; + lastIdentifier = undefined; + } + return pos; + }; + const processDeclarationValueDone = (input, pos) => { + if (modeData === "animation" && lastIdentifier) { + const { line: sl, column: sc } = locConverter.get(lastIdentifier[0]); + const { line: el, column: ec } = locConverter.get(lastIdentifier[1]); + const name = input.slice(lastIdentifier[0], lastIdentifier[1]); + const dep = new CssSelfLocalIdentifierDependency(name, lastIdentifier); + dep.setLoc(sl, sc, el, ec); + module.addDependency(dep); + } + }; + const eatKeyframes = eatUntil("{};/"); + const eatNameInVar = eatUntil(",)};/"); walkCssTokens(source, { + isSelector: () => { + return mode !== CSS_MODE_IN_RULE && mode !== CSS_MODE_IN_LOCAL_RULE; + }, url: (input, start, end, contentStart, contentEnd) => { const value = cssUnescape(input.slice(contentStart, contentEnd)); switch (mode) { @@ -298,6 +359,27 @@ class CssParser extends Parser { supports: undefined }; } + if (name === "@keyframes") { + let pos = end; + pos = walkCssTokens.eatWhitespaceAndComments(input, pos); + if (pos === input.length) return pos; + const [newPos, name] = eatText(input, pos, eatKeyframes); + const { line: sl, column: sc } = locConverter.get(pos); + const { line: el, column: ec } = locConverter.get(newPos); + const dep = new CssLocalIdentifierDependency(name, [pos, newPos]); + dep.setLoc(sl, sc, el, ec); + module.addDependency(dep); + pos = newPos; + if (pos === input.length) return pos; + if (input.charCodeAt(pos) !== CC_LEFT_CURLY) { + throw new Error( + `Unexpected ${input[pos]} at ${pos} during parsing of @keyframes (expected '{')` + ); + } + mode = CSS_MODE_IN_LOCAL_RULE; + modeNestingLevel = 1; + return pos + 1; + } return end; }, semicolon: (input, start, end) => { @@ -320,18 +402,31 @@ class CssParser extends Parser { module.addDependency(dep); break; } + case CSS_MODE_IN_LOCAL_RULE: { + processDeclarationValueDone(input, start); + return processLocalDeclaration(input, end); + } + case CSS_MODE_IN_RULE: { + return end; + } } mode = CSS_MODE_TOP_LEVEL; modeData = undefined; + singleClassSelector = undefined; return end; }, leftCurlyBracket: (input, start, end) => { switch (mode) { case CSS_MODE_TOP_LEVEL: - mode = CSS_MODE_IN_RULE; + mode = isTopLevelLocal() + ? CSS_MODE_IN_LOCAL_RULE + : CSS_MODE_IN_RULE; modeNestingLevel = 1; + if (mode === CSS_MODE_IN_LOCAL_RULE) + return processLocalDeclaration(input, end); break; case CSS_MODE_IN_RULE: + case CSS_MODE_IN_LOCAL_RULE: modeNestingLevel++; break; } @@ -339,9 +434,44 @@ class CssParser extends Parser { }, rightCurlyBracket: (input, start, end) => { switch (mode) { + case CSS_MODE_IN_LOCAL_RULE: + processDeclarationValueDone(input, start); + /* falls through */ case CSS_MODE_IN_RULE: if (--modeNestingLevel === 0) { mode = CSS_MODE_TOP_LEVEL; + modeData = undefined; + singleClassSelector = undefined; + } + break; + } + return end; + }, + id: (input, start, end) => { + singleClassSelector = false; + switch (mode) { + case CSS_MODE_TOP_LEVEL: + if (isTopLevelLocal()) { + const name = input.slice(start + 1, end); + const dep = new CssLocalIdentifierDependency(name, [ + start + 1, + end + ]); + const { line: sl, column: sc } = locConverter.get(start); + const { line: el, column: ec } = locConverter.get(end); + dep.setLoc(sl, sc, el, ec); + module.addDependency(dep); + } + break; + } + return end; + }, + identifier: (input, start, end) => { + singleClassSelector = false; + switch (mode) { + case CSS_MODE_IN_LOCAL_RULE: + if (modeData === "animation") { + lastIdentifier = [start, end]; } break; } @@ -350,18 +480,19 @@ class CssParser extends Parser { class: (input, start, end) => { switch (mode) { case CSS_MODE_TOP_LEVEL: { - if ( - modeData === "local" || - (this.defaultMode === "local" && modeData === undefined) - ) { - const dep = new CssLocalIdentifierDependency( - input.slice(start + 1, end), - [start + 1, end] - ); + if (isTopLevelLocal()) { + const name = input.slice(start + 1, end); + const dep = new CssLocalIdentifierDependency(name, [ + start + 1, + end + ]); const { line: sl, column: sc } = locConverter.get(start); const { line: el, column: ec } = locConverter.get(end); dep.setLoc(sl, sc, el, ec); module.addDependency(dep); + if (singleClassSelector === undefined) singleClassSelector = name; + } else { + singleClassSelector = false; } break; } @@ -392,6 +523,7 @@ class CssParser extends Parser { return end; }, pseudoClass: (input, start, end) => { + singleClassSelector = false; switch (mode) { case CSS_MODE_TOP_LEVEL: { const name = input.slice(start, end); @@ -436,12 +568,41 @@ class CssParser extends Parser { } return end; }, + function: (input, start, end) => { + switch (mode) { + case CSS_MODE_IN_LOCAL_RULE: { + const name = input.slice(start, end - 1); + if (name === "var") { + let pos = walkCssTokens.eatWhitespaceAndComments(input, end); + if (pos === input.length) return pos; + const [newPos, name] = eatText(input, pos, eatNameInVar); + if (!name.startsWith("--")) return end; + const { line: sl, column: sc } = locConverter.get(pos); + const { line: el, column: ec } = locConverter.get(newPos); + const dep = new CssSelfLocalIdentifierDependency( + name.slice(2), + [pos, newPos], + "--", + declaredCssVariables + ); + dep.setLoc(sl, sc, el, ec); + module.addDependency(dep); + return newPos; + } + break; + } + } + return end; + }, comma: (input, start, end) => { switch (mode) { case CSS_MODE_TOP_LEVEL: modeData = undefined; modeStack.length = 0; break; + case CSS_MODE_IN_LOCAL_RULE: + processDeclarationValueDone(input, start); + break; } return end; } diff --git a/lib/css/walkCssTokens.js b/lib/css/walkCssTokens.js index 44249d54c..6ba1dcaab 100644 --- a/lib/css/walkCssTokens.js +++ b/lib/css/walkCssTokens.js @@ -7,11 +7,13 @@ /** * @typedef {Object} CssTokenCallbacks + * @property {function(string, number): boolean} isSelector * @property {function(string, number, number, number, number): number=} url * @property {function(string, number, number): number=} string * @property {function(string, number, number): number=} leftParenthesis * @property {function(string, number, number): number=} rightParenthesis * @property {function(string, number, number): number=} pseudoFunction + * @property {function(string, number, number): number=} function * @property {function(string, number, number): number=} pseudoClass * @property {function(string, number, number): number=} atKeyword * @property {function(string, number, number): number=} class @@ -190,7 +192,7 @@ const consumeNumberSign = (input, pos, callbacks) => { const start = pos; pos++; if (pos === input.length) return pos; - if (_startsIdentifier(input, pos)) { + if (callbacks.isSelector(input, pos) && _startsIdentifier(input, pos)) { pos = _consumeIdentifier(input, pos); if (callbacks.id !== undefined) { return callbacks.id(input, start, pos); @@ -244,7 +246,8 @@ const consumeDot = (input, pos, callbacks) => { if (pos === input.length) return pos; const cc = input.charCodeAt(pos); if (_isDigit(cc)) return consumeNumericToken(input, pos - 2, callbacks); - if (!_startsIdentifier(input, pos)) return pos; + if (!callbacks.isSelector(input, pos) || !_startsIdentifier(input, pos)) + return pos; pos = _consumeIdentifier(input, pos); if (callbacks.class !== undefined) return callbacks.class(input, start, pos); return pos; @@ -264,10 +267,19 @@ const consumeNumericToken = (input, pos, callbacks) => { const consumeOtherIdentifier = (input, pos, callbacks) => { const start = pos; pos = _consumeIdentifier(input, pos); - // we could check for CC_LEFT_PARENTHESIS here, - // but we don't need that info - if (callbacks.identifier !== undefined) { - return callbacks.identifier(input, start, pos); + if ( + pos !== input.length && + !callbacks.isSelector(input, pos) && + input.charCodeAt(pos) === CC_LEFT_PARENTHESIS + ) { + pos++; + if (callbacks.function !== undefined) { + return callbacks.function(input, start, pos); + } + } else { + if (callbacks.identifier !== undefined) { + return callbacks.identifier(input, start, pos); + } } return pos; }; @@ -349,7 +361,8 @@ const consumePotentialUrl = (input, pos, callbacks) => { const consumePotentialPseudo = (input, pos, callbacks) => { const start = pos; pos++; - if (!_startsIdentifier(input, pos)) return pos; + if (!callbacks.isSelector(input, pos) || !_startsIdentifier(input, pos)) + return pos; pos = _consumeIdentifier(input, pos); let cc = input.charCodeAt(pos); if (cc === CC_LEFT_PARENTHESIS) { diff --git a/lib/dependencies/CssLocalIdentifierDependency.js b/lib/dependencies/CssLocalIdentifierDependency.js index 07e86657b..02ced9283 100644 --- a/lib/dependencies/CssLocalIdentifierDependency.js +++ b/lib/dependencies/CssLocalIdentifierDependency.js @@ -18,11 +18,13 @@ class CssLocalIdentifierDependency extends NullDependency { /** * @param {string} name name * @param {[number, number]} range range + * @param {string=} prefix prefix */ - constructor(name, range) { + constructor(name, range, prefix = "") { super(); this.name = name; this.range = range; + this.prefix = prefix; } get type() { @@ -51,6 +53,7 @@ class CssLocalIdentifierDependency extends NullDependency { const { write } = context; write(this.name); write(this.range); + write(this.prefix); super.serialize(context); } @@ -58,17 +61,20 @@ class CssLocalIdentifierDependency extends NullDependency { const { read } = context; this.name = read(); this.range = read(); + this.prefix = read(); super.deserialize(context); } } -const escapeCssIdentifier = str => { +const escapeCssIdentifier = (str, omitUnderscore) => { const escaped = `${str}`.replace( // cspell:word uffff /[^a-zA-Z0-9_\u0081-\uffff-]/g, s => `\\${s}` ); - return /^[0-9-]/.test(escaped) ? `_${escaped}` : escaped; + return !omitUnderscore && /^(?!--)[0-9-]/.test(escaped) + ? `_${escaped}` + : escaped; }; CssLocalIdentifierDependency.Template = class CssLocalIdentifierDependencyTemplate extends ( @@ -91,16 +97,15 @@ CssLocalIdentifierDependency.Template = class CssLocalIdentifierDependencyTempla .getUsedName(dep.name, runtime); const moduleId = chunkGraph.getModuleId(module); const identifier = + dep.prefix + (runtimeTemplate.outputOptions.uniqueName ? runtimeTemplate.outputOptions.uniqueName + "-" : "") + - moduleId + - "-" + - (used || ""); + (used ? moduleId + "-" + used : "-"); source.replace( dep.range[0], dep.range[1] - 1, - escapeCssIdentifier(identifier) + escapeCssIdentifier(identifier, dep.prefix) ); if (used) cssExports.set(used, identifier); } diff --git a/lib/dependencies/CssSelfLocalIdentifierDependency.js b/lib/dependencies/CssSelfLocalIdentifierDependency.js new file mode 100644 index 000000000..dcb8be249 --- /dev/null +++ b/lib/dependencies/CssSelfLocalIdentifierDependency.js @@ -0,0 +1,101 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Ivan Kopeykin @vankop +*/ + +"use strict"; + +const Dependency = require("../Dependency"); +const makeSerializable = require("../util/makeSerializable"); +const CssLocalIdentifierDependency = require("./CssLocalIdentifierDependency"); + +/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */ +/** @typedef {import("../Dependency").ExportsSpec} ExportsSpec */ +/** @typedef {import("../Dependency").ReferencedExport} ReferencedExport */ +/** @typedef {import("../DependencyTemplate").CssDependencyTemplateContext} DependencyTemplateContext */ +/** @typedef {import("../ModuleGraph")} ModuleGraph */ +/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ + +class CssSelfLocalIdentifierDependency extends CssLocalIdentifierDependency { + /** + * @param {string} name name + * @param {[number, number]} range range + * @param {string=} prefix prefix + * @param {Set=} declaredSet set of declared names (will only be active when in declared set) + */ + constructor(name, range, prefix = "", declaredSet = undefined) { + super(name, range, prefix); + this.declaredSet = declaredSet; + } + + get type() { + return "css self local identifier"; + } + + get category() { + return "self"; + } + + /** + * @returns {string | null} an identifier to merge equal requests + */ + getResourceIdentifier() { + return `self`; + } + /** + * Returns the exported names + * @param {ModuleGraph} moduleGraph module graph + * @returns {ExportsSpec | undefined} export names + */ + getExports(moduleGraph) { + if (this.declaredSet && !this.declaredSet.has(this.name)) return; + return super.getExports(moduleGraph); + } + + /** + * Returns list of exports referenced by this dependency + * @param {ModuleGraph} moduleGraph module graph + * @param {RuntimeSpec} runtime the runtime for which the module is analysed + * @returns {(string[] | ReferencedExport)[]} referenced exports + */ + getReferencedExports(moduleGraph, runtime) { + if (this.declaredSet && !this.declaredSet.has(this.name)) + return Dependency.NO_EXPORTS_REFERENCED; + return [[this.name]]; + } + + serialize(context) { + const { write } = context; + write(this.declaredSet); + super.serialize(context); + } + + deserialize(context) { + const { read } = context; + this.declaredSet = read(); + super.deserialize(context); + } +} + +CssSelfLocalIdentifierDependency.Template = class CssSelfLocalIdentifierDependencyTemplate extends ( + CssLocalIdentifierDependency.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 {CssSelfLocalIdentifierDependency} */ (dependency); + if (dep.declaredSet && !dep.declaredSet.has(dep.name)) return; + super.apply(dependency, source, templateContext); + } +}; + +makeSerializable( + CssSelfLocalIdentifierDependency, + "webpack/lib/dependencies/CssSelfLocalIdentifierDependency" +); + +module.exports = CssSelfLocalIdentifierDependency; diff --git a/lib/util/internalSerializables.js b/lib/util/internalSerializables.js index 228c1da54..9264c1c00 100644 --- a/lib/util/internalSerializables.js +++ b/lib/util/internalSerializables.js @@ -71,6 +71,8 @@ module.exports = { require("../dependencies/CssImportDependency"), "dependencies/CssLocalIdentifierDependency": () => require("../dependencies/CssLocalIdentifierDependency"), + "dependencies/CssSelfLocalIdentifierDependency": () => + require("../dependencies/CssSelfLocalIdentifierDependency"), "dependencies/CssExportDependency": () => require("../dependencies/CssExportDependency"), "dependencies/CssUrlDependency": () => diff --git a/test/configCases/css/css-modules/index.js b/test/configCases/css/css-modules/index.js index 551b506be..7ec402925 100644 --- a/test/configCases/css/css-modules/index.js +++ b/test/configCases/css/css-modules/index.js @@ -17,7 +17,13 @@ it("should allow to create css modules", done => { : "./style.module.css-local5 ./style.module.css-local6", nested: prod ? "my-app-491-RX undefined my-app-491-X2" - : "./style.module.css-nested1 undefined ./style.module.css-nested3" + : "./style.module.css-nested1 undefined ./style.module.css-nested3", + ident: prod ? "my-app-491-yR" : "./style.module.css-ident", + keyframes: prod ? "my-app-491-y3" : "./style.module.css-localkeyframes", + 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" }); } 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 40efb159a..70a1cd2fa 100644 --- a/test/configCases/css/css-modules/style.module.css +++ b/test/configCases/css/css-modules/style.module.css @@ -20,24 +20,47 @@ color: pink; } -/* @keyframes localkeyframes { +#ident { + color: purple; +} + +@keyframes localkeyframes { 0% { + left: var(--pos1x); + top: var(--pos1y); + color: var(--theme-color1); } 100% { + left: var(--pos2x); + top: var(--pos2y); + color: var(--theme-color2); + } +} + +@keyframes localkeyframes2 { + 0% { + left: 0; + } + 100% { + left: 100px; } } .animation { animation-name: localkeyframes; - animation: 3s ease-in 1s 2 reverse both paused localkeyframes, localkeyframes; -} */ + animation: 3s ease-in 1s 2 reverse both paused localkeyframes, localkeyframes2; + --pos1x: 0px; + --pos1y: 0px; + --pos2x: 10px; + --pos2y: 20px; +} /* .composed { composes: local1; composes: local2; } */ -/* .vars { +.vars { color: var(--local-color); --local-color: red; } @@ -45,4 +68,4 @@ .globalVars :global { color: var(--global-color); --global-color: red; -} */ +} diff --git a/test/configCases/css/css-modules/use-style.js b/test/configCases/css/css-modules/use-style.js index a0494d06f..41f606240 100644 --- a/test/configCases/css/css-modules/use-style.js +++ b/test/configCases/css/css-modules/use-style.js @@ -1,10 +1,14 @@ import * as style from "./style.module.css"; -import { local1, local2, local3, local4 } from "./style.module.css"; +import { local1, local2, local3, local4, ident } from "./style.module.css"; export default { global: style.global, class: style.class, local: `${local1} ${local2} ${local3} ${local4}`, local2: `${style.local5} ${style.local6}`, - nested: `${style.nested1} ${style.nested2} ${style.nested3}` + nested: `${style.nested1} ${style.nested2} ${style.nested3}`, + ident, + keyframes: style.localkeyframes, + animation: style.animation, + vars: `${style["local-color"]} ${style.vars} ${style["global-color"]} ${style.globalVars}` }; diff --git a/test/configCases/css/css-modules/warnings.js b/test/configCases/css/css-modules/warnings.js index e7c5f7785..36ade9aed 100644 --- a/test/configCases/css/css-modules/warnings.js +++ b/test/configCases/css/css-modules/warnings.js @@ -1,6 +1,8 @@ module.exports = [ [/export 'global' \(imported as 'style'\) was not found/], [/export 'nested2' \(imported as 'style'\) was not found/], + [/export 'global-color' \(imported as 'style'\) was not found/], [/export 'global' \(imported as 'style'\) was not found/], - [/export 'nested2' \(imported as 'style'\) was not found/] + [/export 'nested2' \(imported as 'style'\) was not found/], + [/export 'global-color' \(imported as 'style'\) was not found/] ]; diff --git a/test/helpers/FakeDocument.js b/test/helpers/FakeDocument.js index 3804bd31e..aa837df29 100644 --- a/test/helpers/FakeDocument.js +++ b/test/helpers/FakeDocument.js @@ -174,6 +174,9 @@ class FakeSheet { ); }); walkCssTokens(css, { + isSelector() { + return selector === undefined; + }, leftCurlyBracket(source, start, end) { if (selector === undefined) { selector = source.slice(last, start).trim(); diff --git a/test/walkCssTokens.unittest.js b/test/walkCssTokens.unittest.js index 3da12169c..75f0b04ac 100644 --- a/test/walkCssTokens.unittest.js +++ b/test/walkCssTokens.unittest.js @@ -5,6 +5,7 @@ describe("walkCssTokens", () => { it(`should ${name}`, () => { const results = []; walkCssTokens(content, { + isSelector: () => true, url: (input, s, e, cs, ce) => { results.push(["url", input.slice(s, e), input.slice(cs, ce)]); return e;