@merkur/plugin-css-scrambler
Version:
Merkur plugin for scrambling CSS classes.
198 lines (166 loc) • 5.53 kB
JavaScript
;
const fs = require('fs');
const path = require('path');
const selectorParser = require('postcss-selector-parser');
const { numberToCssClass } = require('../lib/index.cjs');
function postCssScrambler(options) {
return {
postcssPlugin: 'css-scrambler',
Once(root) {
let uniqueHash, prefixesTable, mainPartsTable;
if (options.generateHashTable) {
const tableData = generateHashTable(root, options.uniqueIdentifier);
[uniqueHash, prefixesTable, mainPartsTable] = tableData;
const directory = path.dirname(options.hashTable);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
fs.writeFileSync(options.hashTable, JSON.stringify(tableData));
} else {
[prefixesTable, mainPartsTable] = JSON.parse(
fs.readFileSync(options.hashTable),
);
}
const scramblingParser = selectorParser((selector) => {
selector.walkClasses((classNameNode) => {
const className = classNameNode.value;
if (/^\d+%/.test(className)) {
// the selector parser does not handle decimal numbers in
// selectors (used in keyframes), so we need to handle
// those ourselves.
return;
}
const parts = className.split('-');
const prefix = parts[0];
const mainPart = parts.slice(1).join('-');
const prefixIndex = prefixesTable.indexOf(prefix);
const mainPartIndex = mainPartsTable.indexOf(mainPart);
if (prefixIndex === -1 || mainPartIndex === -1) {
throw new Error(
`The ${className} CSS class in not in the hash table`,
);
}
const scrambledPrefix = numberToCssClass(prefixIndex);
const scrambledMainPart = numberToCssClass(mainPartIndex);
classNameNode.value = `${scrambledPrefix}_${scrambledMainPart}_${uniqueHash}`;
});
});
root.walkRules((rule) => {
let result = scramblingParser.process(rule.selector).result;
// postcss-selector-parser +3.0.0 compatibility
if (result === undefined) {
result = scramblingParser.processSync(rule.selector);
}
rule.selector = result;
});
},
};
}
postCssScrambler.postcss = true;
function generateHashTable(css, uniqueIdentifier = '') {
const prefixes = new Set();
const mainParts = new Set();
const populatingParser = selectorParser((selector) => {
selector.walkClasses((classNameNode) => {
const className = classNameNode.value;
if (/^\d+%/.test(className)) {
// the selector parser does not handle decimal numbers in
// selectors (used in keyframes), so we need to handle
// those ourselves.
return;
}
const parts = className.split('-');
const prefix = parts[0];
const mainPart = parts.slice(1).join('-');
prefixes.add(prefix);
mainParts.add(mainPart);
});
});
css.walkRules((rule) => {
populatingParser.process(rule.selector);
});
return [
generateIdentifierHash(uniqueIdentifier),
[...prefixes],
[...mainParts],
];
}
function generateIdentifierHash(identifier) {
let hash = 5381,
index = identifier.length;
while (index) {
hash = (hash * 33) ^ identifier.charCodeAt(--index);
}
return (hash >>> 0).toString(32);
}
function applyPostCssScramblePlugin(options) {
return (config) => {
if (process.env.NODE_ENV === 'development') {
return config;
}
const postCssScramblePlugin = postCssScrambler({
generateHashTable: true,
hashTable: path.resolve(
process.env.WIDGET_DIRNAME,
'./build/static/hashtable.json',
),
...options,
});
// try to find existing postcss loader
for (const rule of config.module.rules) {
if (!rule.use) {
continue;
}
const postCssUseEntryIndex = rule.use.findIndex(
(useEntry) =>
useEntry === 'postcss-loader' || useEntry.loader === 'postcss-loader',
);
if (~postCssUseEntryIndex) {
const postCssLoader = rule.use[postCssUseEntryIndex];
if (typeof postCssLoader === 'string') {
// convert string loader to object definition
rule.use[postCssUseEntryIndex] = {
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [postCssScramblePlugin],
},
},
};
} else {
// extend options of object defined loader
rule.use[postCssUseEntryIndex].options = {
...postCssLoader.options,
postcssOptions: {
...postCssLoader.options.postcssOptions,
plugins: [
...postCssLoader.options.postcssOptions.plugins,
postCssScramblePlugin,
],
},
};
}
return config;
}
}
// add postcss loader to rule matching css files
const cssRuleIndex = config.module.rules.findIndex((rule) =>
rule.test.test('.css'),
);
if (~cssRuleIndex) {
config.module.rules[cssRuleIndex].use.push({
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [postCssScramblePlugin],
},
},
});
}
return config;
};
}
module.exports = {
applyPostCssScramblePlugin,
postCssScrambler,
};