Skip to content

Instantly share code, notes, and snippets.

@ryantxr
Last active May 22, 2025 13:28
Show Gist options
  • Select an option

  • Save ryantxr/a88122815c4fb8d6edd31ef923072227 to your computer and use it in GitHub Desktop.

Select an option

Save ryantxr/a88122815c4fb8d6edd31ef923072227 to your computer and use it in GitHub Desktop.
Converts pre-v4 style safelist from tailwind.config.js to css @source inline(...)
#!/usr/bin/env node
/**
* Script: generate-safelist.js
*
* Reads the `safelist` array from tailwind.config.js and
* generates a `resources/css/safelist.css` file containing
* @source inline(...) directives for each entry.
*
* Usage: `node scripts/generate-safelist.js`
*/
const fs = require('fs');
const path = require('path');
/**
* Convert a regex like "^border-(red|green|blue)(-(50|100|200))?$"
* into the Tailwind safelist string:
* border-{red,green,blue}{,-{50,100,200}}
*/
function regexToInline(regex) {
// Strip anchors
let pattern = regex.replace(/^\^/, '').replace(/\$$/, '');
// Recursive transformer
function transform(str) {
let out = '';
for (let i = 0; i < str.length;) {
if (str[i] === '(') {
// find matching ')'
let depth = 1, j = i + 1, isNonCapt = false;
if (str[j] === '?' && str[j + 1] === ':') {
isNonCapt = true;
j += 2;
}
const start = j;
while (j < str.length && depth > 0) {
if (str[j] === '(') depth++;
else if (str[j] === ')') depth--;
j++;
}
let content = str.slice(start, j - 1);
// check for trailing "?" (optional)
let optional = false;
if (str[j] === '?') {
optional = true;
j++;
}
// recurse into the group
let inner = transform(content)
.split('|')
// compress pure-number lists to a range if contiguous
.reduce((acc, token) => {
acc.items.push(token);
return acc;
}, { items: [] })
.items
.map(x => x); // no automatic numeric compression here
// build brace block
let brace = `{${inner.join(',')}}`;
if (optional) {
brace = `{,${brace}}`;
}
out += brace;
i = j;
} else {
out += str[i++];
}
}
return out;
}
return transform(pattern);
}
// Adjust paths if needed
const projectRoot = path.resolve(__dirname, '..');
const configPath = path.join(projectRoot, 'tailwind.config.js');
const outputPath = path.join(projectRoot, 'resources', 'css', 'safelist-dev.css');
// Load tailwind.config.js
let config;
try {
config = require(configPath);
} catch (err) {
console.error(`Failed to load Tailwind config at ${configPath}:`, err);
process.exit(1);
}
const safelist = config.safelist;
if (!Array.isArray(safelist) || safelist.length === 0) {
console.error('No safelist array found in tailwind.config.js.');
process.exit(1);
}
// Generate @source inline directives
const lines = safelist.map(entry => {
if (typeof entry === 'string') {
// plain class name
return `@source inline("${entry}");`;
}
if (entry.pattern) {
// pattern can be RegExp or string
let ret = '';
if(entry.pattern instanceof RegExp){
const pattern = regexToInline(entry.pattern.source);
ret = ret + '/* ' + entry.pattern.source + " */\n";
ret = ret + `@source inline("${pattern}");`;
} else {
ret = `@source inline("${entry.pattern}");`;
}
return ret;
}
return null;
}).filter(Boolean);
if (lines.length === 0) {
console.error('No valid safelist entries to generate.');
process.exit(1);
}
// Write output file
const header = `/* AUTO-GENERATED safelist.css - do not edit directly. */`;
const header2 = `/* Instead: add safelist to tailwind.config.css */`;
const content = [header, header2, ...lines].join('\n') + '\n';
fs.writeFileSync(outputPath, content, 'utf8');
console.log(`Generated safelist directives in ${outputPath}`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment