From bbe543ae0b22ee95117785a4f3a6676d55fb5093 Mon Sep 17 00:00:00 2001 From: denisx Date: Wed, 26 Aug 2020 01:06:44 +0300 Subject: [PATCH 1/5] feat: add one-letter-css plugin --- src/plugins/hash-len-suggest.js | 87 +++++++++++++++++++++++++++++ src/plugins/index.js | 3 +- src/plugins/one-letter-css.js | 92 +++++++++++++++++++++++++++++++ test/one-letter-css.test.js | 98 +++++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 src/plugins/hash-len-suggest.js create mode 100644 src/plugins/one-letter-css.js create mode 100644 test/one-letter-css.test.js diff --git a/src/plugins/hash-len-suggest.js b/src/plugins/hash-len-suggest.js new file mode 100644 index 00000000..121182f1 --- /dev/null +++ b/src/plugins/hash-len-suggest.js @@ -0,0 +1,87 @@ +/* eslint-disable no-console */ + +/* +at webpack settings: +const cssHashLen = 8 +... +{ + loader: 'css-loader', + options: { + modules: { + localIdentName: `[hash:base64:${cssHashLen}]`, + getLocalIdent: MyOneLetterCss.getLocalIdent + } + } +} +... +plugins: [ + ...plugins, + new HashLenSuggest({ + instance: MyOneLetterCss, + selectedHashLen: cssHashLen + }) + ] +*/ + +class HashLenSuggest { + constructor({ instance, selectedHashLen }) { + this.instance = instance; + this.selectedHashLen = selectedHashLen; + } + + apply(compiler) { + compiler.plugin('done', this.run); + } + + static collectHashLen(data) { + const matchLen = {}; + const base = {}; + + Object.values(data).forEach(({ name }) => { + for (let len = 1; len <= name.length; len += 1) { + base[len] = base[len] || {}; + const hash = name.substr(0, len); + + if (base[len][hash]) { + matchLen[len] = matchLen[len] || 0; + matchLen[len] += 1; + } else { + base[len][hash] = 1; + } + } + }); + + return matchLen; + } + + run() { + const { instance, selectedHashLen } = this; + const matchLen = HashLenSuggest.collectHashLen(instance.getStat()); + + console.log(); + console.log('Suggest Minify Plugin'); + console.log('Matched length (len: number):', matchLen); + + if (matchLen[selectedHashLen]) { + console.log( + `🚫 You can't use selected hash length (${selectedHashLen}). Increase the hash length.` + ); + console.log(); + process.exit(1); + } else { + console.log(`Selected hash length (${selectedHashLen}) is OK.`); + + if (!matchLen[selectedHashLen - 1]) { + console.log( + `πŸŽ‰ You can decrease the hash length (${selectedHashLen} -> ${ + selectedHashLen - 1 + }).` + ); + } + + console.log(); + } + } +} + +module.exports = HashLenSuggest; diff --git a/src/plugins/index.js b/src/plugins/index.js index 21e47631..034f349c 100644 --- a/src/plugins/index.js +++ b/src/plugins/index.js @@ -1,5 +1,6 @@ import importParser from './postcss-import-parser'; import icssParser from './postcss-icss-parser'; +import OneLetterCss from './one-letter-css'; import urlParser from './postcss-url-parser'; -export { importParser, icssParser, urlParser }; +export { OneLetterCss, importParser, icssParser, urlParser }; diff --git a/src/plugins/one-letter-css.js b/src/plugins/one-letter-css.js new file mode 100644 index 00000000..7b3f9c84 --- /dev/null +++ b/src/plugins/one-letter-css.js @@ -0,0 +1,92 @@ +/** + * @author denisx + */ + +const loaderUtils = require('loader-utils'); + +export default class OneLetterCss { + constructor() { + // Save char symbol start positions + this.a = 'a'.charCodeAt(0); + this.A = 'A'.charCodeAt(0); + // file hashes cache + this.files = {}; + /** encoding [a-zA-Z] */ + this.symbols = 52; + /** a half of encoding */ + this.half = 26; + /** prevent loop-hell */ + this.maxLoop = 10; + } + + /** encoding by rule count at file, 0 - a, 1 - b, 51 - Z, 52 - ba, etc */ + getName(lastUsed) { + const { a, A, symbols, maxLoop, half } = this; + let name = ''; + let loop = 0; + let main = lastUsed; + let tail = 0; + + while ( + ((main > 0 && tail >= 0) || + // first step anyway needed + loop === 0) && + loop < maxLoop + ) { + const newMain = Math.floor(main / symbols); + + tail = main % symbols; + name = String.fromCharCode((tail >= half ? A - half : a) + tail) + name; + main = newMain; + loop += 1; + } + + return name; + } + + getLocalIdent(context, localIdentName, localName) { + const { resourcePath } = context; + const { files } = this; + + // check file data at cache by absolute path + let fileShort = files[resourcePath]; + + // no file data, lets generate and save + if (!fileShort) { + // if we know file position, we must use base52 encoding with '_' + // between rule position and file position + // to avoid collapse hash combination. a_ab vs aa_b + const fileShortName = loaderUtils.interpolateName( + context, + '[hash:base64:8]', + { + content: resourcePath, + } + ); + + fileShort = { name: fileShortName, lastUsed: -1, ruleNames: {} }; + files[resourcePath] = fileShort; + } + + // Get generative rule name from this file + let newRuleName = fileShort.ruleNames[localName]; + + // If no rule - renerate new, and save + if (!newRuleName) { + // Count +1 + fileShort.lastUsed += 1; + + // Generate new rule name + newRuleName = this.getName(fileShort.lastUsed) + fileShort.name; + + // Saved + fileShort.ruleNames[localName] = newRuleName; + } + + // If has "local" at webpack settings + const hasLocal = /\[local]/.test(localIdentName); + + // If has - add prefix + return hasLocal ? `${localName}__${newRuleName}` : newRuleName; + } +} diff --git a/test/one-letter-css.test.js b/test/one-letter-css.test.js new file mode 100644 index 00000000..4b7729c4 --- /dev/null +++ b/test/one-letter-css.test.js @@ -0,0 +1,98 @@ +import OneLetterCss from '../src/plugins/one-letter-css'; + +/* webpack set */ +const workSets = [ + { + in: [ + { + resourcePath: './file1.css', + }, + '[hash:base64:8]', + 'theme-white', + ], + out: ['alWFTMQJI'], + }, + { + in: [ + { + resourcePath: './file1.css', + }, + '[hash:base64:8]', + 'theme-blue', + ], + out: ['blWFTMQJI'], + }, + { + in: [ + { + resourcePath: './file2.css', + }, + '[hash:base64:8]', + 'text-white', + ], + out: ['a1Fsi85PQ'], + }, + { + in: [ + { + resourcePath: './file2.css', + }, + '[hash:base64:8]', + 'text-blue', + ], + out: ['b1Fsi85PQ'], + }, + // for develop case + { + in: [ + { + resourcePath: './file2.css', + }, + '[local]__[hash:base64:8]', + 'text-blue', + ], + out: ['text-blue__b1Fsi85PQ'], + }, +]; + +/* encoding test set */ +const encodingSets = [ + { + in: [0], + out: ['a'], + }, + { + in: [1], + out: ['b'], + }, + { + in: [51], + out: ['Z'], + }, + { + in: [52], + out: ['ba'], + }, + { + in: [53], + out: ['bb'], + }, +]; + +const MyOneLetterCss = new OneLetterCss(); + +describe('testing work case', () => { + workSets.forEach((set) => { + it(`should check name generate`, () => { + expect(MyOneLetterCss.getLocalIdent(...set.in)).toEqual(...set.out); + }); + }); +}); + +describe('testing encoding method', () => { + encodingSets.forEach((set) => { + it(`should check name generate`, () => { + expect(MyOneLetterCss.getName(...set.in)).toEqual(...set.out); + }); + }); +}); From c7421642525ab5b5366886c5a0de69dac888d0c0 Mon Sep 17 00:00:00 2001 From: denisx Date: Wed, 26 Aug 2020 01:20:22 +0300 Subject: [PATCH 2/5] fix: add readme for css hash length --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index ce5984a6..7a1ccf4c 100644 --- a/README.md +++ b/README.md @@ -1348,6 +1348,47 @@ import styles from 'Component.module.scss'; ctx.fillStyle = `${svars.colorBackgroundCanvas}`; ``` +### Plugin: One Letter CSS (with hash length check) + +For efficient gzip/br compression, plugin combine css hash via one symbol name, +as a classname position at file, with filepath `hash:base64:8`, to have strong sequences + +**webpack.config.js** + +```js +const { OneLetterCss } = require('css-loader/plugins'); +const MyOneLetterCss = new OneLetterCss(); + +const cssHashLen = 8; + +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: 'css-loader', + options: { + modules: { + mode: 'local', + localIdentName: `[hash:base64:${cssHashLen}]`, + // for develop + // localIdentName: `[local]__[hash:base64:${cssHashLen}]`, + getLocalIdent: MyOneLetterCss.getLocalIdent, + }, + }, + }, + ], + }, + plugins: [ + ...plugins, + new HashLenSuggest({ + instance: MyOneLetterCss, + selectedHashLen: cssHashLen, + }), + ], +}; +``` + ## Contributing Please take a moment to read our contributing guidelines if you haven't yet done so. From d5c9c1d0fd43aee91dac83bff65e03ed3ac93c1a Mon Sep 17 00:00:00 2001 From: denisx Date: Wed, 26 Aug 2020 13:36:14 +0300 Subject: [PATCH 3/5] feat: update scripts; add: tests --- README.md | 2 +- src/plugins/hash-len-suggest.js | 41 +---- src/plugins/index.js | 5 +- src/plugins/one-letter-css.js | 231 +++++++++++++++++------ test/one-letter-css.test.js | 316 ++++++++++++++++++++++++++++---- 5 files changed, 460 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index 7a1ccf4c..2b1be8cd 100644 --- a/README.md +++ b/README.md @@ -1356,7 +1356,7 @@ as a classname position at file, with filepath `hash:base64:8`, to have strong s **webpack.config.js** ```js -const { OneLetterCss } = require('css-loader/plugins'); +const { HashLenSuggest, OneLetterCss } = require('css-loader/plugins'); const MyOneLetterCss = new OneLetterCss(); const cssHashLen = 8; diff --git a/src/plugins/hash-len-suggest.js b/src/plugins/hash-len-suggest.js index 121182f1..948bc666 100644 --- a/src/plugins/hash-len-suggest.js +++ b/src/plugins/hash-len-suggest.js @@ -1,29 +1,4 @@ -/* eslint-disable no-console */ - -/* -at webpack settings: -const cssHashLen = 8 -... -{ - loader: 'css-loader', - options: { - modules: { - localIdentName: `[hash:base64:${cssHashLen}]`, - getLocalIdent: MyOneLetterCss.getLocalIdent - } - } -} -... -plugins: [ - ...plugins, - new HashLenSuggest({ - instance: MyOneLetterCss, - selectedHashLen: cssHashLen - }) - ] -*/ - -class HashLenSuggest { +export default class HashLenSuggest { constructor({ instance, selectedHashLen }) { this.instance = instance; this.selectedHashLen = selectedHashLen; @@ -33,7 +8,8 @@ class HashLenSuggest { compiler.plugin('done', this.run); } - static collectHashLen(data) { + run() { + const data = this.instance.getStat(); const matchLen = {}; const base = {}; @@ -51,17 +27,12 @@ class HashLenSuggest { } }); - return matchLen; - } - - run() { - const { instance, selectedHashLen } = this; - const matchLen = HashLenSuggest.collectHashLen(instance.getStat()); - console.log(); console.log('Suggest Minify Plugin'); console.log('Matched length (len: number):', matchLen); + const { selectedHashLen } = this; + if (matchLen[selectedHashLen]) { console.log( `🚫 You can't use selected hash length (${selectedHashLen}). Increase the hash length.` @@ -83,5 +54,3 @@ class HashLenSuggest { } } } - -module.exports = HashLenSuggest; diff --git a/src/plugins/index.js b/src/plugins/index.js index 034f349c..942d2645 100644 --- a/src/plugins/index.js +++ b/src/plugins/index.js @@ -1,6 +1,7 @@ import importParser from './postcss-import-parser'; import icssParser from './postcss-icss-parser'; -import OneLetterCss from './one-letter-css'; import urlParser from './postcss-url-parser'; +import HashLenSuggest from './hash-len-suggest'; +import OneLetterCss from './one-letter-css'; -export { OneLetterCss, importParser, icssParser, urlParser }; +export { HashLenSuggest, OneLetterCss, importParser, icssParser, urlParser }; diff --git a/src/plugins/one-letter-css.js b/src/plugins/one-letter-css.js index 7b3f9c84..065729cf 100644 --- a/src/plugins/one-letter-css.js +++ b/src/plugins/one-letter-css.js @@ -1,92 +1,207 @@ +const { interpolateName } = require('loader-utils'); + /** - * @author denisx + * ЗамСняСм css-классы Π½Π° 64-Π±ΠΈΡ‚Π½Ρ‹ΠΉ прСфикс ΠΏΠΎ Π½ΠΎΠΌΠ΅Ρ€Ρƒ ΠΏΠΎΠ·ΠΈΡ†ΠΈΠΈ Π² Ρ„Π°ΠΉΠ»Π΅ + Ρ…Π΅Ρˆ ΠΎΡ‚ ΠΏΡƒΡ‚ΠΈ Ρ„Π°ΠΉΠ»Π° */ -const loaderUtils = require('loader-utils'); +// ΠŸΠ°Ρ€ΡΠΈΠΌ ΠΊΠΎΠ΄ΠΈΡ€ΡƒΡŽΡ‰ΡƒΡŽ строку, ΠΈΠ»ΠΈ Π±Π΅Ρ€Π΅ΠΌ Π΄Π΅Ρ„ΠΎΠ»Ρ‚Π½Ρ‹Π΅ значСния +const getRule = (externalRule) => { + let iRule = { + type: 'hash', + rule: 'base64', + hashLen: 8, + val: '', + }; + + iRule.val = `[${iRule.type}:${iRule.rule}:${iRule.hashLen}]`; + + const matchHashRule = + externalRule + .replace(/_/g, '') + .match(/^(?:\[local])*\[([a-z\d]+):([a-z\d]+):(\d+)]$/) || []; + + if (matchHashRule.length >= 4) { + const [, type, rule, hashLen] = matchHashRule; + + iRule = { + type, + rule, + hashLen, + val: `[${type}:${rule}:${hashLen}]`, + }; + } + + return iRule; +}; -export default class OneLetterCss { +export default class OneLetterCssClasses { constructor() { - // Save char symbol start positions + // БохраняСм Π½Π°Ρ‡Π°Π»ΡŒΠ½Ρ‹Π΅ Ρ‚ΠΎΡ‡ΠΊΠΈ ΠΈΠ· Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ this.a = 'a'.charCodeAt(0); this.A = 'A'.charCodeAt(0); - // file hashes cache + this.zero = '0'.charCodeAt(0); this.files = {}; - /** encoding [a-zA-Z] */ - this.symbols = 52; - /** a half of encoding */ - this.half = 26; - /** prevent loop-hell */ - this.maxLoop = 10; + // [a-zA-Z\d_-] + this.encoderSize = 64; + this.symbolsArea = { + // a-z + az: 26, + // A-Z + AZ: 52, + // _ + under: 53, + // 0-9 | \d + digit: 63, + // - + // dash: 64 + }; + // prevent loop hell + this.maxLoop = 5; + this.rootPathLen = process.cwd().length; } - /** encoding by rule count at file, 0 - a, 1 - b, 51 - Z, 52 - ba, etc */ - getName(lastUsed) { - const { a, A, symbols, maxLoop, half } = this; - let name = ''; - let loop = 0; - let main = lastUsed; - let tail = 0; - - while ( - ((main > 0 && tail >= 0) || - // first step anyway needed - loop === 0) && - loop < maxLoop - ) { - const newMain = Math.floor(main / symbols); - - tail = main % symbols; - name = String.fromCharCode((tail >= half ? A - half : a) + tail) + name; - main = newMain; - loop += 1; + getSingleSymbol(n) { + const { + a, + A, + zero, + encoderSize, + symbolsArea: { az, AZ, under, digit }, + } = this; + + if (!n) { + console.error(`!n, n=${n}`); + + return ''; + } + + if (n > encoderSize) { + console.error(`n > ${encoderSize}, n=${n}`); + + return ''; + } + + // work with 1 <= n <= 64 + if (n <= az) { + return String.fromCharCode(n - 1 + a); + } + + if (n <= AZ) { + return String.fromCharCode(n - 1 - az + A); + } + + if (n <= under) { + return '_'; + } + + if (n <= digit) { + return String.fromCharCode(n - 1 - under + zero); } - return name; + return '-'; } - getLocalIdent(context, localIdentName, localName) { + /** ΠšΠΎΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ класс ΠΏΠΎ ΠΏΠΎΠ·ΠΈΡ†ΠΈΠΈ Π² спискС, 0 - Π°, 1 - b, ΠΈΡ‚ΠΏ */ + getNamePrefix(num) { + const { maxLoop, encoderSize } = this; + + if (!num) { + return ''; + } + + let loopCount = 0; + let n = num; + let res = ''; + + // НСмного ΡƒΡΠΎΠ²Π΅Π½ΡˆΠ΅Π½ΡΡ‚Π²Π΅Π½Π½Ρ‹ΠΉ Π΅Π½ΠΊΠΎΠ΄Π΅Ρ€. Π’ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹Ρ… ΠΏΡ€ΠΎΡΡ‚Π΅ΠΉΡˆΠΈΡ… ΠΏΡ€ΠΎΠΏΡƒΡΠΊΠ°ΡŽΡ‚ΡΡ ΠΊΠΎΠΌΠ±ΠΈΠ½Π°Ρ†ΠΈΠΈ + // Π₯ΠΎΠ΄ΠΈΠΌ Π² Ρ†ΠΈΠΊΠ»Π΅, Π΄Π΅Π»ΠΈΠΌ Π½Π° ΠΊΠΎΠ΄ΠΈΡ€ΡƒΡŽΡ‰ΠΈΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ (64) + // НапримСр, с 1 ΠΏΠΎ 64 - 1 ΠΏΡ€ΠΎΡ…ΠΎΠ΄, с 65 ΠΏΠΎ 4096 (64*64) - 2 ΠΏΡ€ΠΎΡ…ΠΎΠ΄Π° Ρ†ΠΈΠΊΠ»Π°, ΠΈΡ‚Π΄ + while (n && loopCount < maxLoop) { + // ΠžΡΡ‚Π°Ρ‚ΠΎΠΊ ΠΎΡ‚ дСлСния, Π±ΡƒΠ΄Π΅ΠΌ Π΅Π³ΠΎ ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 64. + let tail = n % encoderSize; + const origTail = tail; + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π³Ρ€Π°Π½ΠΈΡ‡Π½Ρ‹Ρ… Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΉ n === encoderSize. 64 % 64 = 0, Π° ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π±ΡƒΠ΄Π΅ΠΌ 64 + if (tail === 0) { + tail = encoderSize; + } + + // Π‘Π΅Ρ€Π΅ΠΌ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ кодирования, добавляСм Π² строку + res = this.getSingleSymbol(tail) + res; + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Π½ΡƒΠΆΠ½ΠΎ Π»ΠΈ ΡƒΡ…ΠΎΠ΄ΠΈΡ‚ΡŒ Π½Π° Π½ΠΎΠ²Ρ‹ΠΉ Ρ†ΠΈΠΊΠ» + if (Math.floor((n - 1) / encoderSize)) { + // Находим ΠΊΠΎΠ»-Π²ΠΎ разрядов для слСд. Ρ†ΠΈΠΊΠ»Π° кодирования. + n = (n - origTail) / encoderSize; + + // На Π³Ρ€Π°Π½ΠΈΡ‡Π½ΠΎΠΌ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΈ (64) ΡƒΠΉΠ΄Π΅ΠΌ Π½Π° Π½ΠΎΠ²Ρ‹ΠΉ ΠΊΡ€ΡƒΠ³, -1 Ρ‡Ρ‚ΠΎΠ±Ρ‹ этого ΠΈΠ·Π±Π΅ΠΆΠ°Ρ‚ΡŒ (это ΠΌΡ‹ ΡƒΠΆΠ΅ Π·Π°ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Π»ΠΈ Π² Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΌ ΠΏΡ€ΠΎΡ…ΠΎΠ΄Π΅) + if (origTail === 0) { + n -= 1; + } + } else { + n = 0; + } + + loopCount += 1; + } + + return res; + } + + /** + * ΠŸΠ΅Ρ€Π΅ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅ΠΌ Ρ„-ию Ρ…Π΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ класса. + * Π’.ΠΊ. ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π½Π° этапС сборки, Ρ‚ΠΎ Ρ„Π°ΠΉΠ»Ρ‹ Ρ€Π°Π·Π½Ρ‹Π΅, ΠΎΡ‚ΡΡŽΠ΄Π° мСньшС Π²Ρ‹Ρ…Π»ΠΎΠΏΠ° + */ + getLocalIdentWithFileHash(context, localIdentName, localName) { const { resourcePath } = context; - const { files } = this; + const { files, rootPathLen } = this; + + // Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ±Ρ€Π°Ρ‚ΡŒ Ρ€Π°Π·Π½ΠΈΡ†Ρƒ стСндов - оставляСм Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π·Π½Π°Ρ‡ΠΈΠΌΡ‹ΠΉ кусок ΠΏΡƒΡ‚ΠΈ Ρ„Π°ΠΉΠ»Π° + const resPath = resourcePath.substr(rootPathLen); - // check file data at cache by absolute path - let fileShort = files[resourcePath]; + // Π€Π°ΠΉΠ» ΡƒΠΆΠ΅ Π² спискС, Π±Π΅Ρ€Π΅ΠΌ Π΅Π³ΠΎ Π½ΠΎΠ²ΠΎΠ΅ имя + let fileShort = files[resPath]; - // no file data, lets generate and save + // Π€Π°ΠΉΠ»Π° Π½Π΅Ρ‚ Π² спискС, Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ Π½ΠΎΠ²ΠΎΠ΅ имя, ΠΈ сохраняСм if (!fileShort) { - // if we know file position, we must use base52 encoding with '_' - // between rule position and file position - // to avoid collapse hash combination. a_ab vs aa_b - const fileShortName = loaderUtils.interpolateName( - context, - '[hash:base64:8]', - { - content: resourcePath, - } - ); + // парсим ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½ΠΎΠ΅ ΠΏΡ€Π°Π²ΠΈΠ»ΠΎ + const localIdentRule = getRule(localIdentName); + + const fileShortName = interpolateName(context, localIdentRule.val, { + content: resPath, + }); - fileShort = { name: fileShortName, lastUsed: -1, ruleNames: {} }; - files[resourcePath] = fileShort; + fileShort = { name: fileShortName, lastUsed: 0, ruleNames: {} }; + files[resPath] = fileShort; } - // Get generative rule name from this file + // Π‘Π΅Ρ€Π΅ΠΌ сгСнСрированноС имя ΠΏΡ€Π°Π²ΠΈΠ»ΠΎ, Ссли Ρ‚Π°ΠΊΠΎΠ΅ ΡƒΠΆΠ΅ Π±Ρ‹Π»ΠΎ Π² Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΌ Ρ„Π°ΠΉΠ»Π΅ let newRuleName = fileShort.ruleNames[localName]; - // If no rule - renerate new, and save + // Если Π΅Π³ΠΎ Π½Π΅ Π±Ρ‹Π»ΠΎ - Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ Π½ΠΎΠ²ΠΎΠ΅, ΠΈ сохраняСм if (!newRuleName) { - // Count +1 + // Π£Π²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ счСтчик ΠΏΡ€Π°Π²ΠΈΠ»Π° для Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ Ρ„Π°ΠΉΠ»Π° fileShort.lastUsed += 1; - // Generate new rule name - newRuleName = this.getName(fileShort.lastUsed) + fileShort.name; + // Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ Π½ΠΎΠ²ΠΎΠ΅ имя ΠΏΡ€Π°Π²ΠΈΠ»Π° + newRuleName = this.getNamePrefix(fileShort.lastUsed) + fileShort.name; - // Saved + // сохраняСм fileShort.ruleNames[localName] = newRuleName; } - // If has "local" at webpack settings + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Π΅ΡΡ‚ΡŒ Π»ΠΈ Π² Π²Π΅Π±-ΠΏΠ°ΠΊΠ΅ настройки, Ρ‡Ρ‚ΠΎ Π½Π°ΠΌ Π½ΡƒΠΆΠ½Ρ‹ ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π»ΡŒΠ½Ρ‹Π΅ ΠΈΠΌΠ΅Π½Π° классов const hasLocal = /\[local]/.test(localIdentName); - // If has - add prefix - return hasLocal ? `${localName}__${newRuleName}` : newRuleName; + // Если develop-настройка Π΅ΡΡ‚ΡŒ - добавляСм прСфикс + const res = hasLocal ? `${localName}__${newRuleName}` : newRuleName; + + // ДобавляСм прСфикс '_' для классов, Π½Π°Ρ‡ΠΈΠ½Π°ΡŽΡ‰ΠΈΡ…ΡΡ с '-', Ρ†ΠΈΡ„Ρ€Ρ‹ '\d' + // ΠΈΠ»ΠΈ '_' (для ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΉ, Ρ‚.ΠΊ. символ участвуСт Π² ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠΈ) + return /^[\d_-]/.test(res) ? `_${res}` : res; + } + + getStat() { + return this.files; } } diff --git a/test/one-letter-css.test.js b/test/one-letter-css.test.js index 4b7729c4..54d13dd6 100644 --- a/test/one-letter-css.test.js +++ b/test/one-letter-css.test.js @@ -1,98 +1,338 @@ -import OneLetterCss from '../src/plugins/one-letter-css'; +import { HashLenSuggest, OneLetterCss } from '../src/plugins'; -/* webpack set */ +const rootPath = process.cwd(); + +/* Набор для webpack использования */ const workSets = [ { in: [ { - resourcePath: './file1.css', + resourcePath: `${rootPath}/file1.css`, }, '[hash:base64:8]', 'theme-white', ], - out: ['alWFTMQJI'], + out: ['a2qCweEE5'], }, { in: [ { - resourcePath: './file1.css', + resourcePath: `${rootPath}/file1.css`, }, '[hash:base64:8]', 'theme-blue', ], - out: ['blWFTMQJI'], + out: ['b2qCweEE5'], }, { in: [ { - resourcePath: './file2.css', + resourcePath: `${rootPath}/file2.css`, }, '[hash:base64:8]', 'text-white', ], - out: ['a1Fsi85PQ'], + out: ['a1IcJDB21'], }, { in: [ { - resourcePath: './file2.css', + resourcePath: `${rootPath}/file2.css`, }, '[hash:base64:8]', 'text-blue', ], - out: ['b1Fsi85PQ'], + out: ['b1IcJDB21'], }, - // for develop case + // для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ, develop mode { in: [ { - resourcePath: './file2.css', + resourcePath: `${rootPath}/file2.css`, }, '[local]__[hash:base64:8]', 'text-blue', ], - out: ['text-blue__b1Fsi85PQ'], + out: ['text-blue__b1IcJDB21'], + }, + // check shot hashLen + { + in: [ + { + resourcePath: `${rootPath}/file3.css`, + }, + '[hash:base64:4]', + 'text-orig', + ], + out: ['a39NC'], + }, + // check wrong hashRule + { + in: [ + { + resourcePath: `${rootPath}/file4.css`, + }, + '[hash:base64]', + 'text-wrong', + ], + out: ['a34TWGba1'], }, ]; -/* encoding test set */ +/* Набор для ΠΏΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π½Ρ‹Ρ… случаСв */ const encodingSets = [ - { - in: [0], - out: ['a'], + // [in, out] + // ΠΏΡ€ΠΈ Π½ΡƒΠ»Π΅ Π½Π΅Ρ‚ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π°, пустой прСфикс. Π΅Π³ΠΎ Π½Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π½Π΅ Π±Ρ‹Π»ΠΎ ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΈ + // ΠΌΠ΅ΠΆΠ΄Ρƒ 1Ρ‹ΠΌ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ΠΌ ('' + '0aabb' = '_0aabb') ΠΈ 53ΠΈΠΌ ('_' + '0aabb' = '_0aabb') + [0, ''], + // это прСфикс для Ρ…Π΅ΡˆΠ°, ΠΏΠ΅Ρ€Π²ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Π±Π΅Ρ€Π΅ΠΌ с 1 + [1, 'a'], + [2, 'b'], + [25, 'y'], + [26, 'z'], + [27, 'A'], + [28, 'B'], + [51, 'Y'], + [52, 'Z'], + [53, '_'], + [54, '0'], + [55, '1'], + [62, '8'], + [63, '9'], + [64, '-'], + [65, 'aa'], + [66, 'ab'], + [116, 'aZ'], + [117, 'a_'], + [118, 'a0'], + [127, 'a9'], + [128, 'a-'], + [129, 'ba'], + [130, 'bb'], + [190, 'b8'], + [191, 'b9'], + [192, 'b-'], + [193, 'ca'], + [194, 'cb'], + [4158, '-8'], + [4159, '-9'], + // last 2-symbols part, '--', 64*65 (64*64 2-symbols + 64 1-letter) + [4160, '--'], + // start 3-symbols part + [4161, 'aaa'], + [4162, 'aab'], +]; + +const MyOneLetterCss = new OneLetterCss(); + +describe('Testing work case', () => { + workSets.forEach((set) => { + it(`should check classname full generate`, () => { + expect(MyOneLetterCss.getLocalIdentWithFileHash(...set.in)).toEqual( + ...set.out + ); + }); + }); +}); + +describe('Testing encoding method', () => { + encodingSets.forEach(([valIn, valOut]) => { + it(`should check classname prefix generate`, () => { + expect(MyOneLetterCss.getNamePrefix(valIn)).toEqual(valOut); + }); + }); +}); + +describe('Testing encoding func', () => { + it('should check empty call', () => { + const result = MyOneLetterCss.getSingleSymbol(); + + expect(result).toEqual(''); + }); + + it('should check over encoding call', () => { + const result = MyOneLetterCss.getSingleSymbol(65); + + expect(result).toEqual(''); + }); +}); + +const statSample = { + '/file1.css': { + lastUsed: 2, + name: '2qCweEE5', + ruleNames: { + 'theme-blue': 'b2qCweEE5', + 'theme-white': 'a2qCweEE5', + }, }, - { - in: [1], - out: ['b'], + '/file2.css': { + lastUsed: 2, + name: '1IcJDB21', + ruleNames: { + 'text-blue': 'b1IcJDB21', + 'text-white': 'a1IcJDB21', + }, }, + '/file3.css': { + lastUsed: 1, + name: '39NC', + ruleNames: { + 'text-orig': 'a39NC', + }, + }, + '/file4.css': { + lastUsed: 1, + name: '34TWGba1', + ruleNames: { + 'text-wrong': 'a34TWGba1', + }, + }, +}; + +it('should check getStat()', () => { + const result = MyOneLetterCss.getStat(); + + expect(result).toEqual(statSample); +}); + +it('should check prefix when [d-] first letter at result', () => { + const hashRule = '[hash:base64:1]'; + const filePath = { + resourcePath: `${rootPath}/myFilePath.css`, + }; + let result = ''; + + // ΠΏΠ΅Ρ€Π΅Π±ΠΈΡ€Π°Π΅ΠΌ, ΠΏΠΎΠΊΠ° Π½Π΅ Π΄ΠΎΠΉΠ΄Ρ‘ΠΌ Π΄ΠΎ символов, Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‰ΠΈΡ… экранирования [\d_-] + for (let i = 1; i <= 53; i += 1) { + const className = `a${i}`; + + result = MyOneLetterCss.getLocalIdentWithFileHash( + filePath, + hashRule, + className + ); + } + + expect(result).toEqual('__2'); + + // ΠΏΡ€ΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ Π°Π»Π³ΠΎΡ€ΠΈΡ‚ΠΌΠ° ΠΏΡ€ΠΎΠ³Π½Π°Ρ‚ΡŒ тСст Π½Π° ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΈ + // ΠΈΡ‰Π΅ΠΌ ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΈ (Π² ΠΏΠ΅Ρ€Π²ΠΎΠΌ ΠΌΠΈΠ»Π»ΠΈΠΎΠ½Π΅ - чисто :) + // const hashes = {}; + // + // for (let i = 1; i <= 1000000; i += 1) { + // const className = `a${i}`; + // + // result = MyOneLetterCss.getLocalIdentWithFileHash(filePath, hashRule, className); + // + // hashes[result] = hashes[result] || { i: [], count: 0 }; + // hashes[result].count += 1; + // hashes[result].i.push(i); + // } + // + // const collisions = []; + // + // Object.entries(hashes).forEach(([hash, { i, count }]) => { + // if (count > 1) { + // collisions.push({ hash, count, i }); + // } + // }); + // + // expect(collisions).toEqual([]); +}); + +/* eslint-disable class-methods-use-this */ +class MockOneLetterCss { + getStat() { + return statSample; + } +} + +const errorFunc = console.error; +const logFunc = console.log; + +afterEach(() => { + console.error = errorFunc; + console.log = logFunc; +}); + +const HashLenSuggestSets = [ { - in: [51], - out: ['Z'], + cssHashLen: 1, + log: [ + [], + ['Suggest Minify Plugin'], + [ + 'Matched length (len: number):', + { + 1: 1, + }, + ], + ["🚫 You can't use selected hash length (1). Increase the hash length."], + [], + ], + error: [], + exit: 1, }, { - in: [52], - out: ['ba'], + cssHashLen: 2, + log: [ + [], + ['Suggest Minify Plugin'], + [ + 'Matched length (len: number):', + { + 1: 1, + }, + ], + ['Selected hash length (2) is OK.'], + [], + ], + error: [], + exit: 0, }, { - in: [53], - out: ['bb'], + cssHashLen: 3, + log: [ + [], + ['Suggest Minify Plugin'], + [ + 'Matched length (len: number):', + { + 1: 1, + }, + ], + ['Selected hash length (3) is OK.'], + ['πŸŽ‰ You can decrease the hash length (3 -> 2).'], + [], + ], + error: [], + exit: 0, }, ]; -const MyOneLetterCss = new OneLetterCss(); +describe('Testing hash len suggest', () => { + HashLenSuggestSets.forEach(({ cssHashLen, log, error, exit }) => { + it('should check empty call', () => { + console.log = jest.fn(); + console.error = jest.fn(); -describe('testing work case', () => { - workSets.forEach((set) => { - it(`should check name generate`, () => { - expect(MyOneLetterCss.getLocalIdent(...set.in)).toEqual(...set.out); - }); - }); -}); + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); + const myMockOneLetterCss = new MockOneLetterCss(); + + /* eslint-disable no-new */ + const myHashLenSuggest = new HashLenSuggest({ + instance: myMockOneLetterCss, + selectedHashLen: cssHashLen, + }); + + myHashLenSuggest.run(); + + expect(console.log.mock.calls).toEqual(log); + expect(console.error.mock.calls).toEqual(error); + expect(mockExit).toHaveBeenCalledTimes(exit); -describe('testing encoding method', () => { - encodingSets.forEach((set) => { - it(`should check name generate`, () => { - expect(MyOneLetterCss.getName(...set.in)).toEqual(...set.out); + mockExit.mockRestore(); }); }); }); From c2938f863480fb26e98f31631df502dcb8874266 Mon Sep 17 00:00:00 2001 From: denisx Date: Sun, 30 Aug 2020 00:55:20 +0300 Subject: [PATCH 4/5] fix: tests; fix: memory leak --- src/plugins/hash-len-suggest.js | 25 +++++---- src/plugins/one-letter-css.js | 68 +++++++++++----------- test/one-letter-css.test.js | 99 ++++++++++++++++++--------------- 3 files changed, 103 insertions(+), 89 deletions(-) diff --git a/src/plugins/hash-len-suggest.js b/src/plugins/hash-len-suggest.js index 948bc666..df87c02b 100644 --- a/src/plugins/hash-len-suggest.js +++ b/src/plugins/hash-len-suggest.js @@ -1,15 +1,20 @@ +const PLUGIN_NAME = 'Hash length suggest'; + export default class HashLenSuggest { constructor({ instance, selectedHashLen }) { this.instance = instance; this.selectedHashLen = selectedHashLen; + this.logger = null; } apply(compiler) { compiler.plugin('done', this.run); + + this.logger = compiler.getInfrastructureLogger(PLUGIN_NAME); } run() { - const data = this.instance.getStat(); + let data = this.instance.getStat(); const matchLen = {}; const base = {}; @@ -27,30 +32,28 @@ export default class HashLenSuggest { } }); - console.log(); - console.log('Suggest Minify Plugin'); - console.log('Matched length (len: number):', matchLen); + data = null; - const { selectedHashLen } = this; + const { logger, selectedHashLen } = this; + + logger.log('Suggest Minify Plugin'); + logger.log('Matched length (len: number):', matchLen); if (matchLen[selectedHashLen]) { - console.log( + logger.log( `🚫 You can't use selected hash length (${selectedHashLen}). Increase the hash length.` ); - console.log(); process.exit(1); } else { - console.log(`Selected hash length (${selectedHashLen}) is OK.`); + logger.log(`Selected hash length (${selectedHashLen}) is OK.`); if (!matchLen[selectedHashLen - 1]) { - console.log( + logger.log( `πŸŽ‰ You can decrease the hash length (${selectedHashLen} -> ${ selectedHashLen - 1 }).` ); } - - console.log(); } } } diff --git a/src/plugins/one-letter-css.js b/src/plugins/one-letter-css.js index 065729cf..a3d57d29 100644 --- a/src/plugins/one-letter-css.js +++ b/src/plugins/one-letter-css.js @@ -1,10 +1,10 @@ const { interpolateName } = require('loader-utils'); /** - * ЗамСняСм css-классы Π½Π° 64-Π±ΠΈΡ‚Π½Ρ‹ΠΉ прСфикс ΠΏΠΎ Π½ΠΎΠΌΠ΅Ρ€Ρƒ ΠΏΠΎΠ·ΠΈΡ†ΠΈΠΈ Π² Ρ„Π°ΠΉΠ»Π΅ + Ρ…Π΅Ρˆ ΠΎΡ‚ ΠΏΡƒΡ‚ΠΈ Ρ„Π°ΠΉΠ»Π° + * Change css-classes to 64-bit prefix (by class position at file) + hash postfix (by file path) */ -// ΠŸΠ°Ρ€ΡΠΈΠΌ ΠΊΠΎΠ΄ΠΈΡ€ΡƒΡŽΡ‰ΡƒΡŽ строку, ΠΈΠ»ΠΈ Π±Π΅Ρ€Π΅ΠΌ Π΄Π΅Ρ„ΠΎΠ»Ρ‚Π½Ρ‹Π΅ значСния +// Parse encoding string, or get default values const getRule = (externalRule) => { let iRule = { type: 'hash', @@ -36,7 +36,7 @@ const getRule = (externalRule) => { export default class OneLetterCssClasses { constructor() { - // БохраняСм Π½Π°Ρ‡Π°Π»ΡŒΠ½Ρ‹Π΅ Ρ‚ΠΎΡ‡ΠΊΠΈ ΠΈΠ· Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ + // Save separators points from ascii-table this.a = 'a'.charCodeAt(0); this.A = 'A'.charCodeAt(0); this.zero = '0'.charCodeAt(0); @@ -70,14 +70,12 @@ export default class OneLetterCssClasses { } = this; if (!n) { - console.error(`!n, n=${n}`); - + // console.error(`!n, n=${n}`); return ''; } if (n > encoderSize) { - console.error(`n > ${encoderSize}, n=${n}`); - + // console.error(`n > ${encoderSize}, n=${n}`); return ''; } @@ -101,7 +99,7 @@ export default class OneLetterCssClasses { return '-'; } - /** ΠšΠΎΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ класс ΠΏΠΎ ΠΏΠΎΠ·ΠΈΡ†ΠΈΠΈ Π² спискС, 0 - Π°, 1 - b, ΠΈΡ‚ΠΏ */ + /** Encode classname by position at file, 0 - Π°, 1 - b, etc */ getNamePrefix(num) { const { maxLoop, encoderSize } = this; @@ -113,28 +111,29 @@ export default class OneLetterCssClasses { let n = num; let res = ''; - // НСмного ΡƒΡΠΎΠ²Π΅Π½ΡˆΠ΅Π½ΡΡ‚Π²Π΅Π½Π½Ρ‹ΠΉ Π΅Π½ΠΊΠΎΠ΄Π΅Ρ€. Π’ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹Ρ… ΠΏΡ€ΠΎΡΡ‚Π΅ΠΉΡˆΠΈΡ… ΠΏΡ€ΠΎΠΏΡƒΡΠΊΠ°ΡŽΡ‚ΡΡ ΠΊΠΎΠΌΠ±ΠΈΠ½Π°Ρ†ΠΈΠΈ - // Π₯ΠΎΠ΄ΠΈΠΌ Π² Ρ†ΠΈΠΊΠ»Π΅, Π΄Π΅Π»ΠΈΠΌ Π½Π° ΠΊΠΎΠ΄ΠΈΡ€ΡƒΡŽΡ‰ΠΈΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ (64) - // НапримСр, с 1 ΠΏΠΎ 64 - 1 ΠΏΡ€ΠΎΡ…ΠΎΠ΄, с 65 ΠΏΠΎ 4096 (64*64) - 2 ΠΏΡ€ΠΎΡ…ΠΎΠ΄Π° Ρ†ΠΈΠΊΠ»Π°, ΠΈΡ‚Π΄ + // Divide at loop for 64 + // For example, from 1 to 64 - 1 step, from 65 to 4096 (64*64) - 2 steps, etc while (n && loopCount < maxLoop) { - // ΠžΡΡ‚Π°Ρ‚ΠΎΠΊ ΠΎΡ‚ дСлСния, Π±ΡƒΠ΄Π΅ΠΌ Π΅Π³ΠΎ ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 64. + // Remainder of division, for 1-64 encode let tail = n % encoderSize; const origTail = tail; - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π³Ρ€Π°Π½ΠΈΡ‡Π½Ρ‹Ρ… Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΉ n === encoderSize. 64 % 64 = 0, Π° ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π±ΡƒΠ΄Π΅ΠΌ 64 + // Check limits, n === encoderSize. 64 % 64 = 0, but encoding for 64 if (tail === 0) { tail = encoderSize; } - // Π‘Π΅Ρ€Π΅ΠΌ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ кодирования, добавляСм Π² строку + // Concat encoding result res = this.getSingleSymbol(tail) + res; - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Π½ΡƒΠΆΠ½ΠΎ Π»ΠΈ ΡƒΡ…ΠΎΠ΄ΠΈΡ‚ΡŒ Π½Π° Π½ΠΎΠ²Ρ‹ΠΉ Ρ†ΠΈΠΊΠ» + // Check for new loop if (Math.floor((n - 1) / encoderSize)) { - // Находим ΠΊΠΎΠ»-Π²ΠΎ разрядов для слСд. Ρ†ΠΈΠΊΠ»Π° кодирования. + // Find the number of bits for next encoding cycle n = (n - origTail) / encoderSize; - // На Π³Ρ€Π°Π½ΠΈΡ‡Π½ΠΎΠΌ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΈ (64) ΡƒΠΉΠ΄Π΅ΠΌ Π½Π° Π½ΠΎΠ²Ρ‹ΠΉ ΠΊΡ€ΡƒΠ³, -1 Ρ‡Ρ‚ΠΎΠ±Ρ‹ этого ΠΈΠ·Π±Π΅ΠΆΠ°Ρ‚ΡŒ (это ΠΌΡ‹ ΡƒΠΆΠ΅ Π·Π°ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Π»ΠΈ Π² Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΌ ΠΏΡ€ΠΎΡ…ΠΎΠ΄Π΅) + // At limit value (64), go to a new circle, + // -1 to avoid (we have already encoded this) + // if (origTail === 0) { n -= 1; } @@ -149,22 +148,21 @@ export default class OneLetterCssClasses { } /** - * ΠŸΠ΅Ρ€Π΅ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅ΠΌ Ρ„-ию Ρ…Π΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ класса. - * Π’.ΠΊ. ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π½Π° этапС сборки, Ρ‚ΠΎ Ρ„Π°ΠΉΠ»Ρ‹ Ρ€Π°Π·Π½Ρ‹Π΅, ΠΎΡ‚ΡΡŽΠ΄Π° мСньшС Π²Ρ‹Ρ…Π»ΠΎΠΏΠ° + * Make hash */ getLocalIdentWithFileHash(context, localIdentName, localName) { const { resourcePath } = context; const { files, rootPathLen } = this; - // Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ±Ρ€Π°Ρ‚ΡŒ Ρ€Π°Π·Π½ΠΈΡ†Ρƒ стСндов - оставляСм Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π·Π½Π°Ρ‡ΠΈΠΌΡ‹ΠΉ кусок ΠΏΡƒΡ‚ΠΈ Ρ„Π°ΠΉΠ»Π° + // To fix difference between stands, work with file path from project root const resPath = resourcePath.substr(rootPathLen); - // Π€Π°ΠΉΠ» ΡƒΠΆΠ΅ Π² спискС, Π±Π΅Ρ€Π΅ΠΌ Π΅Π³ΠΎ Π½ΠΎΠ²ΠΎΠ΅ имя + // Filename at list, take his name let fileShort = files[resPath]; - // Π€Π°ΠΉΠ»Π° Π½Π΅Ρ‚ Π² спискС, Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ Π½ΠΎΠ²ΠΎΠ΅ имя, ΠΈ сохраняСм + // Filename not at list, generate new name, save if (!fileShort) { - // парсим ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½ΠΎΠ΅ ΠΏΡ€Π°Π²ΠΈΠ»ΠΎ + // parse encoding rule const localIdentRule = getRule(localIdentName); const fileShortName = interpolateName(context, localIdentRule.val, { @@ -175,33 +173,37 @@ export default class OneLetterCssClasses { files[resPath] = fileShort; } - // Π‘Π΅Ρ€Π΅ΠΌ сгСнСрированноС имя ΠΏΡ€Π°Π²ΠΈΠ»ΠΎ, Ссли Ρ‚Π°ΠΊΠΎΠ΅ ΡƒΠΆΠ΅ Π±Ρ‹Π»ΠΎ Π² Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΌ Ρ„Π°ΠΉΠ»Π΅ + // Take rulename, if exists at current file let newRuleName = fileShort.ruleNames[localName]; - // Если Π΅Π³ΠΎ Π½Π΅ Π±Ρ‹Π»ΠΎ - Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ Π½ΠΎΠ²ΠΎΠ΅, ΠΈ сохраняСм + // If rulename not exist - generate new, and save if (!newRuleName) { - // Π£Π²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ счСтчик ΠΏΡ€Π°Π²ΠΈΠ»Π° для Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ Ρ„Π°ΠΉΠ»Π° + // Increase rules count fileShort.lastUsed += 1; - // Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ Π½ΠΎΠ²ΠΎΠ΅ имя ΠΏΡ€Π°Π²ΠΈΠ»Π° + // Generate new rulename newRuleName = this.getNamePrefix(fileShort.lastUsed) + fileShort.name; - // сохраняСм + // Save rulename fileShort.ruleNames[localName] = newRuleName; } - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Π΅ΡΡ‚ΡŒ Π»ΠΈ Π² Π²Π΅Π±-ΠΏΠ°ΠΊΠ΅ настройки, Ρ‡Ρ‚ΠΎ Π½Π°ΠΌ Π½ΡƒΠΆΠ½Ρ‹ ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π»ΡŒΠ½Ρ‹Π΅ ΠΈΠΌΠ΅Π½Π° классов + // Check encoding settings for local development (save original rulenames) const hasLocal = /\[local]/.test(localIdentName); - // Если develop-настройка Π΅ΡΡ‚ΡŒ - добавляСм прСфикс + // If develop mode - add prefix const res = hasLocal ? `${localName}__${newRuleName}` : newRuleName; - // ДобавляСм прСфикс '_' для классов, Π½Π°Ρ‡ΠΈΠ½Π°ΡŽΡ‰ΠΈΡ…ΡΡ с '-', Ρ†ΠΈΡ„Ρ€Ρ‹ '\d' - // ΠΈΠ»ΠΈ '_' (для ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΉ, Ρ‚.ΠΊ. символ участвуСт Π² ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠΈ) + // Add prefix '_' for classes with '-' or digit '\d' + // or '_' (for fix collision) return /^[\d_-]/.test(res) ? `_${res}` : res; } getStat() { - return this.files; + const stat = { ...this.files }; + + this.files = {}; + + return stat; } } diff --git a/test/one-letter-css.test.js b/test/one-letter-css.test.js index 54d13dd6..dec1ba18 100644 --- a/test/one-letter-css.test.js +++ b/test/one-letter-css.test.js @@ -259,63 +259,69 @@ afterEach(() => { const HashLenSuggestSets = [ { cssHashLen: 1, - log: [ - [], - ['Suggest Minify Plugin'], - [ - 'Matched length (len: number):', - { - 1: 1, - }, - ], - ["🚫 You can't use selected hash length (1). Increase the hash length."], - [], - ], - error: [], + // log: [ + // [], + // ['Suggest Minify Plugin'], + // [ + // 'Matched length (len: number):', + // { + // 1: 1, + // }, + // ], + // ["🚫 You can't use selected hash length (1). Increase the hash length."], + // [], + // ], + // error: [], exit: 1, }, { cssHashLen: 2, - log: [ - [], - ['Suggest Minify Plugin'], - [ - 'Matched length (len: number):', - { - 1: 1, - }, - ], - ['Selected hash length (2) is OK.'], - [], - ], - error: [], + // log: [ + // [], + // ['Suggest Minify Plugin'], + // [ + // 'Matched length (len: number):', + // { + // 1: 1, + // }, + // ], + // ['Selected hash length (2) is OK.'], + // [], + // ], + // error: [], exit: 0, }, { cssHashLen: 3, - log: [ - [], - ['Suggest Minify Plugin'], - [ - 'Matched length (len: number):', - { - 1: 1, - }, - ], - ['Selected hash length (3) is OK.'], - ['πŸŽ‰ You can decrease the hash length (3 -> 2).'], - [], - ], - error: [], + // log: [ + // [], + // ['Suggest Minify Plugin'], + // [ + // 'Matched length (len: number):', + // { + // 1: 1, + // }, + // ], + // ['Selected hash length (3) is OK.'], + // ['πŸŽ‰ You can decrease the hash length (3 -> 2).'], + // [], + // ], + // error: [], exit: 0, }, ]; +class MockLogger { + info() {} + log() {} + error() {} +} + describe('Testing hash len suggest', () => { - HashLenSuggestSets.forEach(({ cssHashLen, log, error, exit }) => { + HashLenSuggestSets.forEach(({ cssHashLen, exit }) => { it('should check empty call', () => { - console.log = jest.fn(); - console.error = jest.fn(); + // console.log = jest.fn(); + // console.error = jest.fn(); const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); const myMockOneLetterCss = new MockOneLetterCss(); @@ -326,10 +332,13 @@ describe('Testing hash len suggest', () => { selectedHashLen: cssHashLen, }); + myHashLenSuggest.logger = new MockLogger(); + // myHashLenSuggest.this.logger = new MockLogger(); + // myHashLenSuggest.this.logger = new MockLogger(); myHashLenSuggest.run(); - expect(console.log.mock.calls).toEqual(log); - expect(console.error.mock.calls).toEqual(error); + // expect(console.log.mock.calls).toEqual(log); + // expect(console.error.mock.calls).toEqual(error); expect(mockExit).toHaveBeenCalledTimes(exit); mockExit.mockRestore(); From 1f513e922a702f43934111ef04e60703bf812580 Mon Sep 17 00:00:00 2001 From: denisx Date: Sun, 30 Aug 2020 01:03:09 +0300 Subject: [PATCH 5/5] fix: describtion --- src/plugins/one-letter-css.js | 2 +- test/one-letter-css.test.js | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/plugins/one-letter-css.js b/src/plugins/one-letter-css.js index a3d57d29..75834420 100644 --- a/src/plugins/one-letter-css.js +++ b/src/plugins/one-letter-css.js @@ -99,7 +99,7 @@ export default class OneLetterCssClasses { return '-'; } - /** Encode classname by position at file, 0 - Π°, 1 - b, etc */ + /** Encode classname by position at file, 0 - a, 1 - b, etc */ getNamePrefix(num) { const { maxLoop, encoderSize } = this; diff --git a/test/one-letter-css.test.js b/test/one-letter-css.test.js index dec1ba18..38730c38 100644 --- a/test/one-letter-css.test.js +++ b/test/one-letter-css.test.js @@ -2,7 +2,6 @@ import { HashLenSuggest, OneLetterCss } from '../src/plugins'; const rootPath = process.cwd(); -/* Набор для webpack использования */ const workSets = [ { in: [ @@ -44,7 +43,7 @@ const workSets = [ ], out: ['b1IcJDB21'], }, - // для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ, develop mode + // develop mode { in: [ { @@ -79,13 +78,13 @@ const workSets = [ }, ]; -/* Набор для ΠΏΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π½Ρ‹Ρ… случаСв */ +/* Corner cases */ const encodingSets = [ // [in, out] - // ΠΏΡ€ΠΈ Π½ΡƒΠ»Π΅ Π½Π΅Ρ‚ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π°, пустой прСфикс. Π΅Π³ΠΎ Π½Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π½Π΅ Π±Ρ‹Π»ΠΎ ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΈ - // ΠΌΠ΅ΠΆΠ΄Ρƒ 1Ρ‹ΠΌ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ΠΌ ('' + '0aabb' = '_0aabb') ΠΈ 53ΠΈΠΌ ('_' + '0aabb' = '_0aabb') + // at zero value we have no result, empty prefix. cant use them (collisions) + // between 1st value ('' + '0aabb' = '_0aabb') & 53n value ('_' + '0aabb' = '_0aabb') [0, ''], - // это прСфикс для Ρ…Π΅ΡˆΠ°, ΠΏΠ΅Ρ€Π²ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Π±Π΅Ρ€Π΅ΠΌ с 1 + // hash prefix, 1st value [1, 'a'], [2, 'b'], [25, 'y'], @@ -203,7 +202,7 @@ it('should check prefix when [d-] first letter at result', () => { }; let result = ''; - // ΠΏΠ΅Ρ€Π΅Π±ΠΈΡ€Π°Π΅ΠΌ, ΠΏΠΎΠΊΠ° Π½Π΅ Π΄ΠΎΠΉΠ΄Ρ‘ΠΌ Π΄ΠΎ символов, Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‰ΠΈΡ… экранирования [\d_-] + // look for symbols, needed for prefix for (let i = 1; i <= 53; i += 1) { const className = `a${i}`; @@ -216,8 +215,8 @@ it('should check prefix when [d-] first letter at result', () => { expect(result).toEqual('__2'); - // ΠΏΡ€ΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ Π°Π»Π³ΠΎΡ€ΠΈΡ‚ΠΌΠ° ΠΏΡ€ΠΎΠ³Π½Π°Ρ‚ΡŒ тСст Π½Π° ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΈ - // ΠΈΡ‰Π΅ΠΌ ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΈ (Π² ΠΏΠ΅Ρ€Π²ΠΎΠΌ ΠΌΠΈΠ»Π»ΠΈΠΎΠ½Π΅ - чисто :) + // when change algorithm, we need check values for collisions + // looking for collisions (check 1st 1kk values) // const hashes = {}; // // for (let i = 1; i <= 1000000; i += 1) {