Skip to content

Instantly share code, notes, and snippets.

@joshuacerbito
Created August 16, 2021 12:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshuacerbito/2eca2b704d19400745aaaab4dc730620 to your computer and use it in GitHub Desktop.
Save joshuacerbito/2eca2b704d19400745aaaab4dc730620 to your computer and use it in GitHub Desktop.
Custom CSS Loader for BEM
const { getOptions } = require("loader-utils");
const camelCased = s => s && s.replace(/-([a-z0-9])/g, g => g[1].toUpperCase());
const localsMatcher = /exports\.locals\s=\s\{([\s\S]+)\};/g;
module.exports = function(source) {
// You can set the prefix used for the modifiers in the webpack config
const { modifierPrefix = "$" } = getOptions(this);
// We find the exports.locals piece of code
return source.replace(localsMatcher, (_, object) => {
// Convert the entries to an object
const locals = JSON.parse(`{${object}}`);
// Traverse the identifiers.
// We sort lexicographically to ensure identifiers are processed in this
// order: blocks, block modifiers, elements, element modifiers.
// This way we can use object spreadding to easily build the identifiers
// structure without too much sanity checks ;)
const bemLocals = Object.keys(locals)
.sort()
.reduce((acc, identifier) => {
// Extract the parts of each identifier
const [rawBlockElement, rawModifier] = identifier.split("--");
const [rawBlock, rawElement] = rawBlockElement.split("__");
// Camelcase each part.
const block = camelCased(rawBlock);
const element = camelCased(rawElement);
const modifier = camelCased(rawModifier);
// Get the exported className for the identifier
const className = locals[identifier];
// Element modifier. Add the modifier toString placeholder
// to the block element
if (element && modifier) {
acc[block] = {
...acc[block],
[element]: {
...acc[block][element],
[`${modifierPrefix}${modifier}`]: {
_bem_: true,
toString: className,
},
},
};
// Element or block modifier. Add the element/modifier toString
// placeholder to the the block
} else if (modifier || element) {
acc[block] = {
...acc[block],
[element ? element : `${modifierPrefix}${modifier}`]: {
_bem_: true,
toString: className,
},
};
// New block. Add the toString placeholder
} else {
acc[block] = {
_bem_: true,
toString: className,
};
}
return acc;
}, {});
// Stringify the identifiers structure. Swap toString placeholders with
// real arrow functions
const newLocals = JSON.stringify(bemLocals, null, "\t").replace(
/"toString":(.+)/g,
(_, g) => `"toString": () => ${g}`,
);
// Swap the original locals for the new ones
return source.replace(localsMatcher, (_, g) => `exports.locals = ${newLocals};`);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment