Skip to content

Instantly share code, notes, and snippets.

@brantwedel
Created May 19, 2024 23:32
Show Gist options
  • Save brantwedel/fefd89a1ac4ac7144126787967cf6209 to your computer and use it in GitHub Desktop.
Save brantwedel/fefd89a1ac4ac7144126787967cf6209 to your computer and use it in GitHub Desktop.
CSS scoped modules (Express, React)
import express from 'express';
import path from 'path';
import fs from 'fs';
const app = express();
const transformCssModule = (cssFilePath, cssUrlPath, scopeId, isolated = true) => {
const cssContent = fs.readFileSync(cssFilePath, 'utf8');
const classNames = new Set(cssContent.match(/\.[\w-_]+/g) || []);
let exportedClasses = `const isolated = ${isolated ? 'true' : 'false'};\nconst _styles = { $scoped: '__scoped', scoped: '__scoped', ':scoped': '__scoped', root: '__root', $root: '__root', ':root': '__root', host: '__root', $host: '__root', ':host': '__root' };\n`;
// Step 1: Mark classes following `>>>` with `__NOSCOPE__`
let transformedCss = cssContent.replace(/>>>( *\.?[\w-_]+[\s>+{,])+/g, (match) => {
return match.replace(/\.?\w+/g, (className) => `${className}__NOSCOPE__`);
});
// Step 1: Mark classes in :global() with `__NOSCOPE__`
transformedCss = transformedCss.replace(/\:global\((.*)\)/g, (match, p1) => {
return p1.replace(/\w+/g, (className) => `${className}__NOSCOPE__`);
});
// Step 2: Add scope to all other classes, making sure not to scope `__NOSCOPE__` marked classes
transformedCss = transformedCss.replace(/(\.|^)([\w-_]+)(?![^{}]*[;}])/g, (match, p1, p2) => {
if (match.includes('__NOSCOPE__')) {
return match; // Skip adding scope to `__NOSCOPE__` classes
}
return `${p1}${p2}${p1 === '.' ? '--__SCOPE__' : ''}`;
});
// Step 3: Scope raw tag selectors by adding a class parent, unless the selector already contains __SCOPE__
transformedCss = transformedCss.replace(/(^|\s)([^{]+?)(?=\s*{)/g, (match, p1, selector) => {
let rootSelector = selector;
if (!(selector.includes('__SCOPE__') )) {
rootSelector = selector.replace(/(^|\s)([a-z]+)(?=\s|,|;|\{|$)(?![^{}]*\})/gi, (m, sp, tag) => {
if (/^[a-z]+$/.test(tag)) {
return `${sp}.__root--__SCOPEID__ ${tag}`;
}
return m;
});
}
const scopedSelector = selector.replace(/(^|\s)([a-z]+)(?=\s|,|;|\{|$)(?![^{}]*\})/gi, (m, sp, tag) => {
if (!sp.includes('__NOSCOPE__') && /^[a-z]+$/.test(tag)) {
return `${sp}${tag}.__scoped--__SCOPEID__`;
}
return m;
});
return `${p1}${rootSelector}, ${scopedSelector}`;
});
// Remove the __NOSCOPE__ suffix added previously
transformedCss = transformedCss.replace(/__NOSCOPE__/g, '');
transformedCss = transformedCss.replace(/ >>> /g, ' ').replace(/>>>/g, '');
transformedCss = transformedCss.replace(/\:root/g, '.__root--__SCOPEID__');
// Remove redundant nested scopes
transformedCss = transformedCss.replace(/\.__root--__SCOPEID__ (\w+)\s+\.__root--__SCOPEID__/g, '.__root--__SCOPEID__ $1');
// Separate selectors from properties
const scopedCss = transformedCss.replace(/([^{]+)\{([^}]+)\}/g, (match, selectors, properties) => {
const scopedSelectors = selectors.replace(/(\.|^)([\w-_]+)/g, (selMatch, selP1, selP2) => {
if (selMatch.includes('--__SCOPE__') || true) {
return selMatch; // Skip already scoped selectors
}
return `${selP1}${selP2}${selP1 === '.' ? '--__SCOPE__' : ''}`;
});
return `${scopedSelectors} {${properties}}`;
});
classNames.forEach((className) => {
const cleanClassName = className.replace('.', '').trim();
const camelCaseName = cleanClassName.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
// Export global styles
exportedClasses += `_styles['${cleanClassName}'] = '${cleanClassName}';\n`;
exportedClasses += `_styles['${camelCaseName}'] = '${cleanClassName}';\n`;
});
let scopedFunction = `
let scopedCalled = false;
const scoped = (id) => {
scopedCalled = true;
let scopedCss = \`${scopedCss.replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\`;
if (isolated) {
scopedCss = scopedCss.replace(/--__SCOPE__/g, '.__scoped--' + id).replace(/--__SCOPEID__/g, '--' + id);
} else {
scopedCss = scopedCss.replace(/--__SCOPE__/g, '.__scoped--' + id).replace(/--__SCOPEID__/g, '--' + id);
}
const style = document.createElement('style');
style.textContent = scopedCss;
document.head.appendChild(style);
const scopedStyles = {};
Object.keys(_styles).forEach(key => {
if (isolated || _styles[key].startsWith('__')) {
scopedStyles[key] = _styles[key] + '--' + id;
} else {
scopedStyles[key] = '__scoped--' + id + ' ' + _styles[key];
}
});
return scopedStyles;
};`;
if (!scopeId) {
scopedFunction += `
console.debug('%cLoaded unscoped styles: ' + '${cssUrlPath}', 'color: dodgerblue;');
const style = document.createElement('style');
style.textContent = \`${cssContent.replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\`;
document.head.appendChild(style);
const styles = _styles;
`;
} else {
scopedFunction += `
const styles = scoped('${scopeId}');
`;
}
const wrappedCss = `
${exportedClasses}
${scopedFunction}
let helper = function(...args) {
let flatArgs = new Set((!isolated ? ['__scoped--${scopeId}'] : []));
let vals = Object.values(styles);
args.forEach(arg => {
if (!arg) {
flatArgs.add('--missing');
} else {
if (vals.includes(arg)) {
arg.split(' ').forEach(v => {
flatArgs.add(v);
});
} else {
arg.split(' ').forEach(k => {
(styles[k] ?? k ?? '').split(' ').forEach(v => {
flatArgs.add(v);
});
});
}
}
});
return [...flatArgs].join(' ');
}
Object.keys(styles).forEach(k => {
helper[k] = styles[k];
});
export { styles, scoped, helper };
export default helper;
`;
return wrappedCss;
};
// handle requests for CSS modules
app.get(/\/.*.css([@?].*)?\.js$/, (req, res) => {
const relativePath = req.url.replace('.js', '').replace(/css[@?][^\.]*/, 'css');
const cssFilePath = path.join(__dirname, '..', 'client', relativePath);
const cssUrlPath = '' + relativePath; // Adjust this based on your public directory structure
fs.readFile(cssFilePath, 'utf8', (err, data) => {
if (err) {
// CSS file does not exist, send a script to log a warning in the browser
const warningScript = `
console.warn('CSS file "${cssFilePath}" not found.');
`;
res.type('application/javascript');
res.send(warningScript);
return;
}
// Optionally handle scope parsing here if needed
const scopeMatch = req.url.match(/[@?]([^&\.]*)/);
const scopeId = scopeMatch ? scopeMatch[1].replace('!', '') : req.url.replace('.css.js', '').replace(/[\/\.]/g,'-');
const wrappedCss = transformCssModule(cssFilePath, cssUrlPath, scopeId, (req.url.includes('@') || req.url.includes('?')) && !req.url.includes('!') ? false : true); // Assuming we want to use a link tag
res.type('application/javascript');
res.send(wrappedCss);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment