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: add one-letter-css plugin #1181

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
41 changes: 41 additions & 0 deletions README.md
Expand Up @@ -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.
Expand Down
87 changes: 87 additions & 0 deletions 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;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use weak cache here, otherwise is was memory leak in watch mode

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need help with it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexander-akait help plz


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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have logger for webpack, avoid using console.log https://webpack.js.org/api/logging/

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix


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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can avoid using plugin, we have the identifier of module, so we can generate names based on this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use file path to make long cash. and make shortest hash as we can (but need check for collisions)

3 changes: 2 additions & 1 deletion 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 };
92 changes: 92 additions & 0 deletions src/plugins/one-letter-css.js
@@ -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 = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here potential memory leak in watch mode, if will have a lot of renamed files, the object will grow

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed at last call - getStat()

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