Skip to content

Commit d5c9c1d

Browse files
committed
feat: update scripts; add: tests
1 parent c742164 commit d5c9c1d

File tree

5 files changed

+460
-135
lines changed

5 files changed

+460
-135
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1356,7 +1356,7 @@ as a classname position at file, with filepath `hash:base64:8`, to have strong s
13561356
**webpack.config.js**
13571357

13581358
```js
1359-
const { OneLetterCss } = require('css-loader/plugins');
1359+
const { HashLenSuggest, OneLetterCss } = require('css-loader/plugins');
13601360
const MyOneLetterCss = new OneLetterCss();
13611361

13621362
const cssHashLen = 8;

src/plugins/hash-len-suggest.js

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,4 @@
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 {
1+
export default class HashLenSuggest {
272
constructor({ instance, selectedHashLen }) {
283
this.instance = instance;
294
this.selectedHashLen = selectedHashLen;
@@ -33,7 +8,8 @@ class HashLenSuggest {
338
compiler.plugin('done', this.run);
349
}
3510

36-
static collectHashLen(data) {
11+
run() {
12+
const data = this.instance.getStat();
3713
const matchLen = {};
3814
const base = {};
3915

@@ -51,17 +27,12 @@ class HashLenSuggest {
5127
}
5228
});
5329

54-
return matchLen;
55-
}
56-
57-
run() {
58-
const { instance, selectedHashLen } = this;
59-
const matchLen = HashLenSuggest.collectHashLen(instance.getStat());
60-
6130
console.log();
6231
console.log('Suggest Minify Plugin');
6332
console.log('Matched length (len: number):', matchLen);
6433

34+
const { selectedHashLen } = this;
35+
6536
if (matchLen[selectedHashLen]) {
6637
console.log(
6738
`🚫 You can't use selected hash length (${selectedHashLen}). Increase the hash length.`
@@ -83,5 +54,3 @@ class HashLenSuggest {
8354
}
8455
}
8556
}
86-
87-
module.exports = HashLenSuggest;

src/plugins/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import importParser from './postcss-import-parser';
22
import icssParser from './postcss-icss-parser';
3-
import OneLetterCss from './one-letter-css';
43
import urlParser from './postcss-url-parser';
4+
import HashLenSuggest from './hash-len-suggest';
5+
import OneLetterCss from './one-letter-css';
56

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

src/plugins/one-letter-css.js

Lines changed: 173 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,207 @@
1+
const { interpolateName } = require('loader-utils');
2+
13
/**
2-
* @author denisx <github.com@denisx.com>
4+
* Заменяем css-классы на 64-битный префикс по номеру позиции в файле + хеш от пути файла
35
*/
46

5-
const loaderUtils = require('loader-utils');
7+
// Парсим кодирующую строку, или берем дефолтные значения
8+
const getRule = (externalRule) => {
9+
let iRule = {
10+
type: 'hash',
11+
rule: 'base64',
12+
hashLen: 8,
13+
val: '',
14+
};
15+
16+
iRule.val = `[${iRule.type}:${iRule.rule}:${iRule.hashLen}]`;
17+
18+
const matchHashRule =
19+
externalRule
20+
.replace(/_/g, '')
21+
.match(/^(?:\[local])*\[([a-z\d]+):([a-z\d]+):(\d+)]$/) || [];
22+
23+
if (matchHashRule.length >= 4) {
24+
const [, type, rule, hashLen] = matchHashRule;
25+
26+
iRule = {
27+
type,
28+
rule,
29+
hashLen,
30+
val: `[${type}:${rule}:${hashLen}]`,
31+
};
32+
}
33+
34+
return iRule;
35+
};
636

7-
export default class OneLetterCss {
37+
export default class OneLetterCssClasses {
838
constructor() {
9-
// Save char symbol start positions
39+
// Сохраняем начальные точки из таблицы
1040
this.a = 'a'.charCodeAt(0);
1141
this.A = 'A'.charCodeAt(0);
12-
// file hashes cache
42+
this.zero = '0'.charCodeAt(0);
1343
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;
44+
// [a-zA-Z\d_-]
45+
this.encoderSize = 64;
46+
this.symbolsArea = {
47+
// a-z
48+
az: 26,
49+
// A-Z
50+
AZ: 52,
51+
// _
52+
under: 53,
53+
// 0-9 | \d
54+
digit: 63,
55+
// -
56+
// dash: 64
57+
};
58+
// prevent loop hell
59+
this.maxLoop = 5;
60+
this.rootPathLen = process.cwd().length;
2061
}
2162

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;
63+
getSingleSymbol(n) {
64+
const {
65+
a,
66+
A,
67+
zero,
68+
encoderSize,
69+
symbolsArea: { az, AZ, under, digit },
70+
} = this;
71+
72+
if (!n) {
73+
console.error(`!n, n=${n}`);
74+
75+
return '';
76+
}
77+
78+
if (n > encoderSize) {
79+
console.error(`n > ${encoderSize}, n=${n}`);
80+
81+
return '';
82+
}
83+
84+
// work with 1 <= n <= 64
85+
if (n <= az) {
86+
return String.fromCharCode(n - 1 + a);
87+
}
88+
89+
if (n <= AZ) {
90+
return String.fromCharCode(n - 1 - az + A);
91+
}
92+
93+
if (n <= under) {
94+
return '_';
95+
}
96+
97+
if (n <= digit) {
98+
return String.fromCharCode(n - 1 - under + zero);
4299
}
43100

44-
return name;
101+
return '-';
45102
}
46103

47-
getLocalIdent(context, localIdentName, localName) {
104+
/** Кодируем класс по позиции в списке, 0 - а, 1 - b, итп */
105+
getNamePrefix(num) {
106+
const { maxLoop, encoderSize } = this;
107+
108+
if (!num) {
109+
return '';
110+
}
111+
112+
let loopCount = 0;
113+
let n = num;
114+
let res = '';
115+
116+
// Немного усовеншенственный енкодер. В найденых простейших пропускаются комбинации
117+
// Ходим в цикле, делим на кодирующий размер (64)
118+
// Например, с 1 по 64 - 1 проход, с 65 по 4096 (64*64) - 2 прохода цикла, итд
119+
while (n && loopCount < maxLoop) {
120+
// Остаток от деления, будем его кодировать от 1 до 64.
121+
let tail = n % encoderSize;
122+
const origTail = tail;
123+
124+
// Проверка граничных значений n === encoderSize. 64 % 64 = 0, а кодировать будем 64
125+
if (tail === 0) {
126+
tail = encoderSize;
127+
}
128+
129+
// Берем результат кодирования, добавляем в строку
130+
res = this.getSingleSymbol(tail) + res;
131+
132+
// Проверяем, нужно ли уходить на новый цикл
133+
if (Math.floor((n - 1) / encoderSize)) {
134+
// Находим кол-во разрядов для след. цикла кодирования.
135+
n = (n - origTail) / encoderSize;
136+
137+
// На граничном значении (64) уйдем на новый круг, -1 чтобы этого избежать (это мы уже закодировали в текущем проходе)
138+
if (origTail === 0) {
139+
n -= 1;
140+
}
141+
} else {
142+
n = 0;
143+
}
144+
145+
loopCount += 1;
146+
}
147+
148+
return res;
149+
}
150+
151+
/**
152+
* Переопределяем ф-ию хеширования класса.
153+
* Т.к. обработка на этапе сборки, то файлы разные, отсюда меньше выхлопа
154+
*/
155+
getLocalIdentWithFileHash(context, localIdentName, localName) {
48156
const { resourcePath } = context;
49-
const { files } = this;
157+
const { files, rootPathLen } = this;
158+
159+
// Чтобы убрать разницу стендов - оставляем только значимый кусок пути файла
160+
const resPath = resourcePath.substr(rootPathLen);
50161

51-
// check file data at cache by absolute path
52-
let fileShort = files[resourcePath];
162+
// Файл уже в списке, берем его новое имя
163+
let fileShort = files[resPath];
53164

54-
// no file data, lets generate and save
165+
// Файла нет в списке, генерируем новое имя, и сохраняем
55166
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-
);
167+
// парсим переданное правило
168+
const localIdentRule = getRule(localIdentName);
169+
170+
const fileShortName = interpolateName(context, localIdentRule.val, {
171+
content: resPath,
172+
});
66173

67-
fileShort = { name: fileShortName, lastUsed: -1, ruleNames: {} };
68-
files[resourcePath] = fileShort;
174+
fileShort = { name: fileShortName, lastUsed: 0, ruleNames: {} };
175+
files[resPath] = fileShort;
69176
}
70177

71-
// Get generative rule name from this file
178+
// Берем сгенерированное имя правило, если такое уже было в текущем файле
72179
let newRuleName = fileShort.ruleNames[localName];
73180

74-
// If no rule - renerate new, and save
181+
// Если его не было - генерируем новое, и сохраняем
75182
if (!newRuleName) {
76-
// Count +1
183+
// Увеличиваем счетчик правила для текущего файла
77184
fileShort.lastUsed += 1;
78185

79-
// Generate new rule name
80-
newRuleName = this.getName(fileShort.lastUsed) + fileShort.name;
186+
// Генерируем новое имя правила
187+
newRuleName = this.getNamePrefix(fileShort.lastUsed) + fileShort.name;
81188

82-
// Saved
189+
// сохраняем
83190
fileShort.ruleNames[localName] = newRuleName;
84191
}
85192

86-
// If has "local" at webpack settings
193+
// Проверяем, есть ли в веб-паке настройки, что нам нужны оригинальные имена классов
87194
const hasLocal = /\[local]/.test(localIdentName);
88195

89-
// If has - add prefix
90-
return hasLocal ? `${localName}__${newRuleName}` : newRuleName;
196+
// Если develop-настройка есть - добавляем префикс
197+
const res = hasLocal ? `${localName}__${newRuleName}` : newRuleName;
198+
199+
// Добавляем префикс '_' для классов, начинающихся с '-', цифры '\d'
200+
// или '_' (для исключения коллизий, т.к. символ участвует в кодировании)
201+
return /^[\d_-]/.test(res) ? `_${res}` : res;
202+
}
203+
204+
getStat() {
205+
return this.files;
91206
}
92207
}

0 commit comments

Comments
 (0)