diff --git a/src/utils.js b/src/utils.js index c647dc82..76149459 100644 --- a/src/utils.js +++ b/src/utils.js @@ -88,9 +88,7 @@ function getLocalIdent(loaderContext, localIdentName, localName, options) { options ); - return hash - .replace(new RegExp('[^a-zA-Z0-9\\-_\u00A0-\uFFFF]', 'g'), '-') - .replace(/^((-?[0-9])|--)/, '_$1'); + return normalizeIdentifier(hash); } function getFilter(filter, resourcePath, defaultFilter = null) { @@ -107,6 +105,52 @@ function getFilter(filter, resourcePath, defaultFilter = null) { }; } +function normalizeIdentifier(value) { + const escapedSymbols = [ + '~', + '!', + '@', + '#', + '$', + '%', + '&', + '^', + '*', + '(', + ')', + '{', + '}', + '[', + ']', + '`', + '/', + '=', + '?', + '+', + '\\', + '|', + '-', + '_', + ':', + ';', + "'", + '"', + ',', + '<', + '.', + '>', + ]; + + const identifiersRegExp = new RegExp( + `[^a-zA-Z0-9${escapedSymbols.join('\\')}\\-_\u00A0-\uFFFF]`, + 'g' + ); + + return value + .replace(identifiersRegExp, '-') + .replace(/^((-?[0-9])|--)/, '_$1'); +} + export { getImportPrefix, getLocalIdent, diff --git a/test/__snapshots__/localIdentName-option.test.js.snap b/test/__snapshots__/localIdentName-option.test.js.snap index 23e0126c..12b98210 100644 --- a/test/__snapshots__/localIdentName-option.test.js.snap +++ b/test/__snapshots__/localIdentName-option.test.js.snap @@ -35,6 +35,16 @@ Array [ :local(.-a0-34a___f) { color: red; } + +:local(.m_x_\\\\@) { + margin-left: auto !important; + margin-right: auto !important; +} + +:local(.B\\\\&W\\\\?) { + margin-left: auto !important; + margin-right: auto !important; +} ", "", ], @@ -78,6 +88,16 @@ Array [ :local(.-a0-34a___f) { color: red; } + +:local(.m_x_\\\\@) { + margin-left: auto !important; + margin-right: auto !important; +} + +:local(.B\\\\&W\\\\?) { + margin-left: auto !important; + margin-right: auto !important; +} ", "", ], @@ -121,6 +141,16 @@ Array [ :local(.-a0-34a___f) { color: red; } + +:local(.m_x_\\\\@) { + margin-left: auto !important; + margin-right: auto !important; +} + +:local(.B\\\\&W\\\\?) { + margin-left: auto !important; + margin-right: auto !important; +} ", "", ], @@ -164,6 +194,16 @@ Array [ :local(.-a0-34a___f) { color: red; } + +:local(.m_x_\\\\@) { + margin-left: auto !important; + margin-right: auto !important; +} + +:local(.B\\\\&W\\\\?) { + margin-left: auto !important; + margin-right: auto !important; +} ", "", ], @@ -207,6 +247,16 @@ Array [ :local(.-a0-34a___f) { color: red; } + +:local(.m_x_\\\\@) { + margin-left: auto !important; + margin-right: auto !important; +} + +:local(.B\\\\&W\\\\?) { + margin-left: auto !important; + margin-right: auto !important; +} ", "", ], @@ -250,6 +300,16 @@ Array [ :local(.-a0-34a___f) { color: red; } + +:local(.m_x_\\\\@) { + margin-left: auto !important; + margin-right: auto !important; +} + +:local(.B\\\\&W\\\\?) { + margin-left: auto !important; + margin-right: auto !important; +} ", "", ], @@ -293,6 +353,16 @@ Array [ :local(.-a0-34a___f) { color: red; } + +:local(.m_x_\\\\@) { + margin-left: auto !important; + margin-right: auto !important; +} + +:local(.B\\\\&W\\\\?) { + margin-left: auto !important; + margin-right: auto !important; +} ", "", ], @@ -300,3 +370,67 @@ Array [ `; exports[`localIdentName option should use hash prefix: warnings 1`] = `Array []`; + +exports[`localIdentName option should сorrectly replace escaped symbols in selector: errors 1`] = `Array []`; + +exports[`localIdentName option should сorrectly replace escaped symbols in selector: locals 1`] = ` +Object { + "-a0-34a___f": "-a0-34a___f--2nJ5", + "B&W?": "B&W?--1s8i", + "_test": "_test--23te", + "className": "className--1E8H", + "m_x_@": "m_x_@--2G3b", + "someId": "someId--3w7J", + "subClass": "subClass--3lo0", + "test": "test--NW9Y", +} +`; + +exports[`localIdentName option should сorrectly replace escaped symbols in selector: module (evaluated) 1`] = ` +Array [ + Array [ + 1, + ".test--NW9Y { + background: red; +} + +._test--23te { + background: blue; +} + +.className--1E8H { + background: red; +} + +#someId--3w7J { + background: green; +} + +.className--1E8H .subClass--3lo0 { + color: green; +} + +#someId--3w7J .subClass--3lo0 { + color: blue; +} + +.-a0-34a___f--2nJ5 { + color: red; +} + +.m_x_\\\\@--2G3b { + margin-left: auto !important; + margin-right: auto !important; +} + +.B\\\\&W\\\\?--1s8i { + margin-left: auto !important; + margin-right: auto !important; +} +", + "", + ], +] +`; + +exports[`localIdentName option should сorrectly replace escaped symbols in selector: warnings 1`] = `Array []`; diff --git a/test/fixtures/modules/localIdentName.css b/test/fixtures/modules/localIdentName.css index e670d069..282de45d 100644 --- a/test/fixtures/modules/localIdentName.css +++ b/test/fixtures/modules/localIdentName.css @@ -25,3 +25,13 @@ :local(.-a0-34a___f) { color: red; } + +:local(.m_x_\@) { + margin-left: auto !important; + margin-right: auto !important; +} + +:local(.B\&W\?) { + margin-left: auto !important; + margin-right: auto !important; +} diff --git a/test/localIdentName-option.test.js b/test/localIdentName-option.test.js index 91aac177..7ead50ae 100644 --- a/test/localIdentName-option.test.js +++ b/test/localIdentName-option.test.js @@ -117,4 +117,26 @@ describe('localIdentName option', () => { expect(stats.compilation.warnings).toMatchSnapshot('warnings'); expect(stats.compilation.errors).toMatchSnapshot('errors'); }); + + it('should сorrectly replace escaped symbols in selector', async () => { + const config = { + loader: { + options: { + importLoaders: 2, + localIdentName: '[local]--[hash:base64:4]', + modules: true, + }, + }, + }; + const testId = './modules/localIdentName.css'; + const stats = await webpack(testId, config); + const { modules } = stats.toJson(); + const module = modules.find((m) => m.id === testId); + const evaluatedModule = evaluated(module.source, modules); + + expect(evaluatedModule).toMatchSnapshot('module (evaluated)'); + expect(evaluatedModule.locals).toMatchSnapshot('locals'); + expect(stats.compilation.warnings).toMatchSnapshot('warnings'); + expect(stats.compilation.errors).toMatchSnapshot('errors'); + }); });