Skip to content

Commit

Permalink
feat: resolve issue webpack-contrib#1028: optimising naming for best …
Browse files Browse the repository at this point in the history
…compression
  • Loading branch information
denisx committed Mar 29, 2020
1 parent 431f620 commit 9f8a741
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 1 deletion.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/index.js
Original file line number Diff line number Diff line change
@@ -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 };
88 changes: 88 additions & 0 deletions src/plugins/one-letter-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @author denisx <[email protected]>
*/

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;
}
}
98 changes: 98 additions & 0 deletions test/one-letter-css.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

0 comments on commit 9f8a741

Please sign in to comment.