Skip to content

Commit bbe543a

Browse files
committed
feat: add one-letter-css plugin
1 parent 8b865fe commit bbe543a

File tree

4 files changed

+279
-1
lines changed

4 files changed

+279
-1
lines changed

src/plugins/hash-len-suggest.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/* eslint-disable no-console */
2+
3+
/*
4+
at webpack settings:
5+
const cssHashLen = 8
6+
...
7+
{
8+
loader: 'css-loader',
9+
options: {
10+
modules: {
11+
localIdentName: `[hash:base64:${cssHashLen}]`,
12+
getLocalIdent: MyOneLetterCss.getLocalIdent
13+
}
14+
}
15+
}
16+
...
17+
plugins: [
18+
...plugins,
19+
new HashLenSuggest({
20+
instance: MyOneLetterCss,
21+
selectedHashLen: cssHashLen
22+
})
23+
]
24+
*/
25+
26+
class HashLenSuggest {
27+
constructor({ instance, selectedHashLen }) {
28+
this.instance = instance;
29+
this.selectedHashLen = selectedHashLen;
30+
}
31+
32+
apply(compiler) {
33+
compiler.plugin('done', this.run);
34+
}
35+
36+
static collectHashLen(data) {
37+
const matchLen = {};
38+
const base = {};
39+
40+
Object.values(data).forEach(({ name }) => {
41+
for (let len = 1; len <= name.length; len += 1) {
42+
base[len] = base[len] || {};
43+
const hash = name.substr(0, len);
44+
45+
if (base[len][hash]) {
46+
matchLen[len] = matchLen[len] || 0;
47+
matchLen[len] += 1;
48+
} else {
49+
base[len][hash] = 1;
50+
}
51+
}
52+
});
53+
54+
return matchLen;
55+
}
56+
57+
run() {
58+
const { instance, selectedHashLen } = this;
59+
const matchLen = HashLenSuggest.collectHashLen(instance.getStat());
60+
61+
console.log();
62+
console.log('Suggest Minify Plugin');
63+
console.log('Matched length (len: number):', matchLen);
64+
65+
if (matchLen[selectedHashLen]) {
66+
console.log(
67+
`🚫 You can't use selected hash length (${selectedHashLen}). Increase the hash length.`
68+
);
69+
console.log();
70+
process.exit(1);
71+
} else {
72+
console.log(`Selected hash length (${selectedHashLen}) is OK.`);
73+
74+
if (!matchLen[selectedHashLen - 1]) {
75+
console.log(
76+
`🎉 You can decrease the hash length (${selectedHashLen} -> ${
77+
selectedHashLen - 1
78+
}).`
79+
);
80+
}
81+
82+
console.log();
83+
}
84+
}
85+
}
86+
87+
module.exports = HashLenSuggest;

src/plugins/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import importParser from './postcss-import-parser';
22
import icssParser from './postcss-icss-parser';
3+
import OneLetterCss from './one-letter-css';
34
import urlParser from './postcss-url-parser';
45

5-
export { importParser, icssParser, urlParser };
6+
export { OneLetterCss, importParser, icssParser, urlParser };

src/plugins/one-letter-css.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* @author denisx <github.com@denisx.com>
3+
*/
4+
5+
const loaderUtils = require('loader-utils');
6+
7+
export default class OneLetterCss {
8+
constructor() {
9+
// Save char symbol start positions
10+
this.a = 'a'.charCodeAt(0);
11+
this.A = 'A'.charCodeAt(0);
12+
// file hashes cache
13+
this.files = {};
14+
/** encoding [a-zA-Z] */
15+
this.symbols = 52;
16+
/** a half of encoding */
17+
this.half = 26;
18+
/** prevent loop-hell */
19+
this.maxLoop = 10;
20+
}
21+
22+
/** encoding by rule count at file, 0 - a, 1 - b, 51 - Z, 52 - ba, etc */
23+
getName(lastUsed) {
24+
const { a, A, symbols, maxLoop, half } = this;
25+
let name = '';
26+
let loop = 0;
27+
let main = lastUsed;
28+
let tail = 0;
29+
30+
while (
31+
((main > 0 && tail >= 0) ||
32+
// first step anyway needed
33+
loop === 0) &&
34+
loop < maxLoop
35+
) {
36+
const newMain = Math.floor(main / symbols);
37+
38+
tail = main % symbols;
39+
name = String.fromCharCode((tail >= half ? A - half : a) + tail) + name;
40+
main = newMain;
41+
loop += 1;
42+
}
43+
44+
return name;
45+
}
46+
47+
getLocalIdent(context, localIdentName, localName) {
48+
const { resourcePath } = context;
49+
const { files } = this;
50+
51+
// check file data at cache by absolute path
52+
let fileShort = files[resourcePath];
53+
54+
// no file data, lets generate and save
55+
if (!fileShort) {
56+
// if we know file position, we must use base52 encoding with '_'
57+
// between rule position and file position
58+
// to avoid collapse hash combination. a_ab vs aa_b
59+
const fileShortName = loaderUtils.interpolateName(
60+
context,
61+
'[hash:base64:8]',
62+
{
63+
content: resourcePath,
64+
}
65+
);
66+
67+
fileShort = { name: fileShortName, lastUsed: -1, ruleNames: {} };
68+
files[resourcePath] = fileShort;
69+
}
70+
71+
// Get generative rule name from this file
72+
let newRuleName = fileShort.ruleNames[localName];
73+
74+
// If no rule - renerate new, and save
75+
if (!newRuleName) {
76+
// Count +1
77+
fileShort.lastUsed += 1;
78+
79+
// Generate new rule name
80+
newRuleName = this.getName(fileShort.lastUsed) + fileShort.name;
81+
82+
// Saved
83+
fileShort.ruleNames[localName] = newRuleName;
84+
}
85+
86+
// If has "local" at webpack settings
87+
const hasLocal = /\[local]/.test(localIdentName);
88+
89+
// If has - add prefix
90+
return hasLocal ? `${localName}__${newRuleName}` : newRuleName;
91+
}
92+
}

test/one-letter-css.test.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import OneLetterCss from '../src/plugins/one-letter-css';
2+
3+
/* webpack set */
4+
const workSets = [
5+
{
6+
in: [
7+
{
8+
resourcePath: './file1.css',
9+
},
10+
'[hash:base64:8]',
11+
'theme-white',
12+
],
13+
out: ['alWFTMQJI'],
14+
},
15+
{
16+
in: [
17+
{
18+
resourcePath: './file1.css',
19+
},
20+
'[hash:base64:8]',
21+
'theme-blue',
22+
],
23+
out: ['blWFTMQJI'],
24+
},
25+
{
26+
in: [
27+
{
28+
resourcePath: './file2.css',
29+
},
30+
'[hash:base64:8]',
31+
'text-white',
32+
],
33+
out: ['a1Fsi85PQ'],
34+
},
35+
{
36+
in: [
37+
{
38+
resourcePath: './file2.css',
39+
},
40+
'[hash:base64:8]',
41+
'text-blue',
42+
],
43+
out: ['b1Fsi85PQ'],
44+
},
45+
// for develop case
46+
{
47+
in: [
48+
{
49+
resourcePath: './file2.css',
50+
},
51+
'[local]__[hash:base64:8]',
52+
'text-blue',
53+
],
54+
out: ['text-blue__b1Fsi85PQ'],
55+
},
56+
];
57+
58+
/* encoding test set */
59+
const encodingSets = [
60+
{
61+
in: [0],
62+
out: ['a'],
63+
},
64+
{
65+
in: [1],
66+
out: ['b'],
67+
},
68+
{
69+
in: [51],
70+
out: ['Z'],
71+
},
72+
{
73+
in: [52],
74+
out: ['ba'],
75+
},
76+
{
77+
in: [53],
78+
out: ['bb'],
79+
},
80+
];
81+
82+
const MyOneLetterCss = new OneLetterCss();
83+
84+
describe('testing work case', () => {
85+
workSets.forEach((set) => {
86+
it(`should check name generate`, () => {
87+
expect(MyOneLetterCss.getLocalIdent(...set.in)).toEqual(...set.out);
88+
});
89+
});
90+
});
91+
92+
describe('testing encoding method', () => {
93+
encodingSets.forEach((set) => {
94+
it(`should check name generate`, () => {
95+
expect(MyOneLetterCss.getName(...set.in)).toEqual(...set.out);
96+
});
97+
});
98+
});

0 commit comments

Comments
 (0)