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
base: master
Are you sure you want to change the base?
Changes from 2 commits
bbe543a
c742164
d5c9c1d
c2938f8
1f513e9
8185951
b06b840
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
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 { OneLetterCss, importParser, icssParser, urlParser }; |
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 = {}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
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: ['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); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need help with it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexander-akait help plz