UNPKG

@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
/** * @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. */ 'use strict'; 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 };