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 5 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 { HashLenSuggest, 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
59 changes: 59 additions & 0 deletions src/plugins/hash-len-suggest.js
@@ -0,0 +1,59 @@
const PLUGIN_NAME = 'Hash length suggest';

export default class HashLenSuggest {
constructor({ instance, selectedHashLen }) {
this.instance = instance;
this.selectedHashLen = selectedHashLen;
this.logger = null;
}

apply(compiler) {
compiler.plugin('done', this.run);

this.logger = compiler.getInfrastructureLogger(PLUGIN_NAME);
}

run() {
let data = this.instance.getStat();
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;
}
}
});

data = null;

const { logger, selectedHashLen } = this;

logger.log('Suggest Minify Plugin');
logger.log('Matched length (len: number):', matchLen);

if (matchLen[selectedHashLen]) {
logger.log(
`🚫 You can't use selected hash length (${selectedHashLen}). Increase the hash length.`
);
process.exit(1);
} else {
logger.log(`Selected hash length (${selectedHashLen}) is OK.`);

if (!matchLen[selectedHashLen - 1]) {
logger.log(
`🎉 You can decrease the hash length (${selectedHashLen} -> ${
selectedHashLen - 1
}).`
);
}
}
}
}
4 changes: 3 additions & 1 deletion src/plugins/index.js
@@ -1,5 +1,7 @@
import importParser from './postcss-import-parser';
import icssParser from './postcss-icss-parser';
import urlParser from './postcss-url-parser';
import HashLenSuggest from './hash-len-suggest';
import OneLetterCss from './one-letter-css';

export { importParser, icssParser, urlParser };
export { HashLenSuggest, OneLetterCss, importParser, icssParser, urlParser };
209 changes: 209 additions & 0 deletions src/plugins/one-letter-css.js
@@ -0,0 +1,209 @@
const { interpolateName } = require('loader-utils');

/**
* Change css-classes to 64-bit prefix (by class position at file) + hash postfix (by file path)
*/

// Parse encoding string, or get default values
const getRule = (externalRule) => {
let iRule = {
type: 'hash',
rule: 'base64',
hashLen: 8,
val: '',
};

iRule.val = `[${iRule.type}:${iRule.rule}:${iRule.hashLen}]`;

const matchHashRule =
externalRule
.replace(/_/g, '')
.match(/^(?:\[local])*\[([a-z\d]+):([a-z\d]+):(\d+)]$/) || [];

if (matchHashRule.length >= 4) {
const [, type, rule, hashLen] = matchHashRule;

iRule = {
type,
rule,
hashLen,
val: `[${type}:${rule}:${hashLen}]`,
};
}

return iRule;
};

export default class OneLetterCssClasses {
constructor() {
// Save separators points from ascii-table
this.a = 'a'.charCodeAt(0);
this.A = 'A'.charCodeAt(0);
this.zero = '0'.charCodeAt(0);
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()

// [a-zA-Z\d_-]
this.encoderSize = 64;
this.symbolsArea = {
// a-z
az: 26,
// A-Z
AZ: 52,
// _
under: 53,
// 0-9 | \d
digit: 63,
// -
// dash: 64
};
// prevent loop hell
this.maxLoop = 5;
this.rootPathLen = process.cwd().length;
}

getSingleSymbol(n) {
const {
a,
A,
zero,
encoderSize,
symbolsArea: { az, AZ, under, digit },
} = this;

if (!n) {
// console.error(`!n, n=${n}`);
return '';
}

if (n > encoderSize) {
// console.error(`n > ${encoderSize}, n=${n}`);
return '';
}

// work with 1 <= n <= 64
if (n <= az) {
return String.fromCharCode(n - 1 + a);
}

if (n <= AZ) {
return String.fromCharCode(n - 1 - az + A);
}

if (n <= under) {
return '_';
}

if (n <= digit) {
return String.fromCharCode(n - 1 - under + zero);
}

return '-';
}

/** Encode classname by position at file, 0 - a, 1 - b, etc */
getNamePrefix(num) {
const { maxLoop, encoderSize } = this;

if (!num) {
return '';
}

let loopCount = 0;
let n = num;
let res = '';

// Divide at loop for 64
// For example, from 1 to 64 - 1 step, from 65 to 4096 (64*64) - 2 steps, etc
while (n && loopCount < maxLoop) {
// Remainder of division, for 1-64 encode
let tail = n % encoderSize;
const origTail = tail;

// Check limits, n === encoderSize. 64 % 64 = 0, but encoding for 64
if (tail === 0) {
tail = encoderSize;
}

// Concat encoding result
res = this.getSingleSymbol(tail) + res;

// Check for new loop
if (Math.floor((n - 1) / encoderSize)) {
// Find the number of bits for next encoding cycle
n = (n - origTail) / encoderSize;

// At limit value (64), go to a new circle,
// -1 to avoid (we have already encoded this)
//
if (origTail === 0) {
n -= 1;
}
} else {
n = 0;
}

loopCount += 1;
}

return res;
}

/**
* Make hash
*/
getLocalIdentWithFileHash(context, localIdentName, localName) {
const { resourcePath } = context;
const { files, rootPathLen } = this;

// To fix difference between stands, work with file path from project root
const resPath = resourcePath.substr(rootPathLen);

// Filename at list, take his name
let fileShort = files[resPath];

// Filename not at list, generate new name, save
if (!fileShort) {
// parse encoding rule
const localIdentRule = getRule(localIdentName);

const fileShortName = interpolateName(context, localIdentRule.val, {
content: resPath,
});

fileShort = { name: fileShortName, lastUsed: 0, ruleNames: {} };
files[resPath] = fileShort;
}

// Take rulename, if exists at current file
let newRuleName = fileShort.ruleNames[localName];

// If rulename not exist - generate new, and save
if (!newRuleName) {
// Increase rules count
fileShort.lastUsed += 1;

// Generate new rulename
newRuleName = this.getNamePrefix(fileShort.lastUsed) + fileShort.name;

// Save rulename
fileShort.ruleNames[localName] = newRuleName;
}

// Check encoding settings for local development (save original rulenames)
const hasLocal = /\[local]/.test(localIdentName);

// If develop mode - add prefix
const res = hasLocal ? `${localName}__${newRuleName}` : newRuleName;

// Add prefix '_' for classes with '-' or digit '\d'
// or '_' (for fix collision)
return /^[\d_-]/.test(res) ? `_${res}` : res;
}

getStat() {
const stat = { ...this.files };

this.files = {};

return stat;
}
}