diff --git a/README.md b/README.md index 8020201e..3c554f13 100644 --- a/README.md +++ b/README.md @@ -963,6 +963,38 @@ module.exports = { }; ``` +### Plugin: One Letter CSS + +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(); + +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: 'css-loader', + options: { + modules: { + mode: 'local', + localIdentName: '[hash:base64:8]', + // for develop + // localIdentName: '[local]__[hash:base64:8]', + getLocalIdent: MyOneLetterCss.getLocalIdent, + }, + }, + }, + ], + }, +}; +``` + ## Contributing Please take a moment to read our contributing guidelines if you haven't yet done so. diff --git a/src/plugins/index.js b/src/plugins/index.js index 21e47631..fc21fb30 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 { importParser, icssParser, OneLetterCss, urlParser }; diff --git a/src/plugins/one-letter-css.js b/src/plugins/one-letter-css.js new file mode 100644 index 00000000..7017618c --- /dev/null +++ b/src/plugins/one-letter-css.js @@ -0,0 +1,88 @@ +/** + * @author denisx + */ + +import { interpolateName } from '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 = 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..b064109e --- /dev/null +++ b/test/one-letter-css.test.js @@ -0,0 +1,98 @@ +import { OneLetterCss } from '../src/plugins'; + +/* webpack set */ +const workSets = [ + { + in: [ + { + resourcePath: './file1.css', + }, + '[hash:base64:8]', + 'theme-white', + ], + out: ['a2zADNwsK'], + }, + { + in: [ + { + resourcePath: './file1.css', + }, + '[hash:base64:8]', + 'theme-blue', + ], + out: ['b2zADNwsK'], + }, + { + in: [ + { + resourcePath: './file2.css', + }, + '[hash:base64:8]', + 'text-white', + ], + out: ['a2jlx459O'], + }, + { + in: [ + { + resourcePath: './file2.css', + }, + '[hash:base64:8]', + 'text-blue', + ], + out: ['b2jlx459O'], + }, + // for develop case + { + in: [ + { + resourcePath: './file2.css', + }, + '[local]__[hash:base64:8]', + 'text-blue', + ], + out: ['text-blue__b2jlx459O'], + }, +]; + +/* 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); + }); + }); +});