Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: resolve issue #1028: optimising naming for best compression #1061

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 };
92 changes: 92 additions & 0 deletions src/plugins/one-letter-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* @author denisx <github.com@denisx.com>
*/

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;
}
}
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/one-letter-css';

/* 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);
});
});
});