@richaadgigi/stylexui
Version:
Build responsive, beautiful interfaces faster than ever with utility-first classes and smart defaults. No bloat. No fuss. Just results.
206 lines (182 loc) • 7.88 kB
JavaScript
/**
* @richaadgigi/stylexui — Build-Time Dynamic CSS Extractor
*
* Scans source files for dynamic xui utility classes (e.g. xui-pt-[24px])
* and generates the corresponding CSS rules into a static file so they
* are available before JavaScript runs — eliminating FOUC in Next.js.
*/
;
const fs = require('fs');
const path = require('path');
// ─── Property Map ─────────────────────────────────────────────────────────────
// Mirrors the propertyMap inside xuiDynamicCSS() in index.js.
// Keep these two in sync when adding new properties.
const PROPERTY_MAP = {
"xui-bg": "background-color",
"xui-bg-img": "background-image",
"xui-text": "color",
"xui-img": "max-width",
"xui-column-count": "column-count",
"xui-column-count-gap": "column-gap",
"xui-m": "margin",
"xui-mt": "margin-top",
"xui-mr": "margin-right",
"xui-mb": "margin-bottom",
"xui-ml": "margin-left",
"xui-mx": ["margin-left", "margin-right"],
"xui-my": ["margin-top", "margin-bottom"],
"xui-p": "padding",
"xui-pt": "padding-top",
"xui-pr": "padding-right",
"xui-pb": "padding-bottom",
"xui-pl": "padding-left",
"xui-px": ["padding-left", "padding-right"],
"xui-py": ["padding-top", "padding-bottom"],
"xui-space": "letter-spacing",
"xui-bdr-rad": "border-radius",
"xui-bdr-w": "border-width",
"xui-bdr": "border-color",
"xui-z-index": "z-index",
"xui-min-w": "min-width",
"xui-min-h": "min-height",
"xui-max-w": "max-width",
"xui-max-h": "max-height",
"xui-font-w": "font-weight",
"xui-font-sz": "font-size",
"xui-opacity": "opacity",
"xui-w": "width",
"xui-h": "height",
"xui-line-height": "line-height",
"xui-letter-spacing": "letter-spacing",
"xui-grid-gap": "grid-gap",
"xui-grid-row-gap": "grid-row-gap",
"xui-grid-column-gap": "grid-column-gap",
"xui-gap": "gap",
"xui-row-gap": "row-gap",
"xui-column-gap": "column-gap",
"xui-flex-grow": "flex-grow",
"xui-flex-shrink": "flex-shrink",
"xui-flex-basis": "flex-basis",
"xui-basis": "flex-basis",
"xui-ls": "list-style",
"xui-lsi": "list-style-image",
};
// ─── Responsive Map ───────────────────────────────────────────────────────────
const RESPONSIVE_MAP = {
"xui-sm": "(min-width: 640px)",
"xui-md": "(min-width: 768px)",
"xui-lg": "(min-width: 1024px)",
"xui-xl": "(min-width: 1280px)",
};
// ─── Regex ────────────────────────────────────────────────────────────────────
// Matches: xui-pt-[24px], xui-sm-pt-[24px], xui-bg-[#fff], etc.
const CLASS_REGEX = /xui-(?:(sm|md|lg|xl)-)?([a-z][a-z-]*)-\[([^\]\s"'`]+)\]/g;
// ─── Helpers ──────────────────────────────────────────────────────────────────
function escapeCSSSelector(cls) {
return '.' + cls.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
}
function buildRuleBody(selector, prop, value, hasImportant) {
const suffix = hasImportant ? ' !important' : '';
const declarations = Array.isArray(prop)
? prop.map(p => `${p}: ${value}${suffix}`).join('; ')
: `${prop}: ${value}${suffix}`;
return `${selector} { ${declarations}; }`;
}
// ─── CSS Generator ────────────────────────────────────────────────────────────
function generateCSS(classes) {
const buckets = { base: [], sm: [], md: [], lg: [], xl: [] };
const seen = new Set();
for (const cls of classes) {
if (seen.has(cls)) continue;
seen.add(cls);
// Re-match to extract parts cleanly
const match = cls.match(/^xui-(?:(sm|md|lg|xl)-)?([a-z][a-z-]*)-\[([^\]]+)\]$/);
if (!match) continue;
const [, breakpoint, propKey, rawValue] = match;
const prop = PROPERTY_MAP[`xui-${propKey}`];
if (!prop) continue;
const hasImportant = rawValue.trim().endsWith('!');
const value = rawValue.trim().replace(/!$/, '');
const selector = escapeCSSSelector(cls);
const ruleBody = buildRuleBody(selector, prop, value, hasImportant);
if (breakpoint && RESPONSIVE_MAP[`xui-${breakpoint}`]) {
const mq = RESPONSIVE_MAP[`xui-${breakpoint}`];
buckets[breakpoint].push(`@media ${mq} { ${ruleBody} }`);
} else {
buckets.base.push(ruleBody);
}
}
return [
...buckets.base,
...buckets.sm,
...buckets.md,
...buckets.lg,
...buckets.xl,
].join('\n');
}
// ─── Main Extractor ───────────────────────────────────────────────────────────
/**
* Scans source files for dynamic xui class patterns and generates a CSS file.
*
* @param {object} options
* @param {string[]} [options.srcPatterns] - Glob patterns relative to cwd
* @param {string} [options.outputPath] - Absolute path to write the CSS file
* @param {string} [options.cwd] - Working directory (defaults to process.cwd())
* @returns {Promise<{ css: string, classes: string[], count: number }>}
*/
async function extractDynamicCSS(options = {}) {
// Lazy-load fast-glob to avoid issues if not installed at top level
let fg;
try {
fg = require('fast-glob');
// Handle both default and named exports
if (typeof fg !== 'function' && typeof fg.default === 'function') {
fg = fg.default;
}
} catch (e) {
throw new Error('[stylexui] fast-glob is required. Run: npm install fast-glob');
}
const {
srcPatterns = [
'src/**/*.{ts,tsx,js,jsx,html,mdx}',
'app/**/*.{ts,tsx,js,jsx,html,mdx}',
'pages/**/*.{ts,tsx,js,jsx,html,mdx}',
'components/**/*.{ts,tsx,js,jsx,html,mdx}',
],
outputPath,
cwd = process.cwd(),
} = options;
const files = await fg(srcPatterns, {
cwd,
absolute: true,
ignore: ['**/node_modules/**', '**/.next/**', '**/dist/**', '**/build/**'],
});
const classes = new Set();
for (const file of files) {
let content;
try {
content = fs.readFileSync(file, 'utf-8');
} catch {
continue; // skip unreadable files
}
CLASS_REGEX.lastIndex = 0;
let match;
while ((match = CLASS_REGEX.exec(content)) !== null) {
classes.add(match[0]);
}
}
const css = generateCSS(classes);
const header = `/* Generated by @richaadgigi/stylexui — do not edit manually. Re-generated on each build. */\n`;
const output = header + (css || '/* No dynamic classes found */');
if (outputPath) {
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(outputPath, output, 'utf-8');
const rel = path.relative(cwd, outputPath);
console.log(`[stylexui] ✓ Extracted ${classes.size} dynamic class(es) → ${rel}`);
}
return { css: output, classes: Array.from(classes), count: classes.size };
}
module.exports = { extractDynamicCSS, generateCSS, PROPERTY_MAP, RESPONSIVE_MAP };