Clean-CSS provides advanced features including source map generation, @import inlining, URL rebasing, and a flexible plugin system for extending functionality.
Source maps enable debugging of minified CSS by mapping the compressed output back to the original source files.
interface SourceMapOptions {
sourceMap?: boolean; // Enable source map generation
sourceMapInlineSources?: boolean; // Embed sources in source map
}const CleanCSS = require('clean-css');
const sourceMapMinifier = new CleanCSS({
sourceMap: true,
sourceMapInlineSources: true // Include original source content
});
const css = `
.header {
background-color: #ffffff;
padding: 20px;
}
`;
const result = sourceMapMinifier.minify(css);
console.log('Minified CSS:', result.styles);
console.log('Source map:', result.sourceMap);
// Write files
const fs = require('fs');
fs.writeFileSync('output.css', result.styles);
fs.writeFileSync('output.css.map', JSON.stringify(result.sourceMap));Process existing source maps from preprocessors like Sass or Less:
const inputSourceMap = JSON.parse(fs.readFileSync('input.css.map', 'utf8'));
const css = fs.readFileSync('input.css', 'utf8');
const result = sourceMapMinifier.minify(css, inputSourceMap);
// The output source map will maintain the chain back to original sourcesconst fileHash = {
'main.scss.css': {
styles: compiledMainCSS,
sourceMap: mainSourceMap
},
'components.scss.css': {
styles: compiledComponentsCSS,
sourceMap: componentsSourceMap
}
};
const result = sourceMapMinifier.minify(fileHash);
// Combined source map maintains mapping to all original .scss filesClean-CSS can process @import rules and inline the referenced stylesheets directly into the output.
interface ImportOptions {
inline?: string | string[]; // Import inlining rules
inlineRequest?: object; // HTTP request options
inlineTimeout?: number; // Timeout for remote fetching
fetch?: object; // Custom fetch function
}// String values:
const localOnlyMinifier = new CleanCSS({ inline: 'local' }); // Local files only (default)
const remoteOnlyMinifier = new CleanCSS({ inline: 'remote' }); // Remote files only
const allMinifier = new CleanCSS({ inline: 'all' }); // All imports
const noneMinifier = new CleanCSS({ inline: 'none' }); // No inlining
const falseMinifier = new CleanCSS({ inline: false }); // Same as 'none'
// Array values for granular control:
const customMinifier = new CleanCSS({
inline: ['local', 'fonts.googleapis.com'] // Local files and specific domain
});const localInlineMinifier = new CleanCSS({
inline: 'local',
rebaseTo: 'dist/css/'
});
const css = `
@import "normalize.css";
@import "components/buttons.css";
@import url("themes/default.css");
body {
font-family: Arial, sans-serif;
}
`;
const result = localInlineMinifier.minify(css);
// All local @import rules are resolved and inlined
// result.inlinedStylesheets contains the file paths that were inlinedconst remoteInlineMinifier = new CleanCSS({
inline: 'remote',
inlineTimeout: 10000, // 10 second timeout
inlineRequest: {
headers: {
'User-Agent': 'CleanCSS/5.3.3'
}
}
});
const css = `
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700");
@import "local-styles.css"; // This will NOT be inlined
body {
font-family: 'Roboto', sans-serif;
}
`;
const result = remoteInlineMinifier.minify(css);/**
* Custom fetch function for handling remote resource requests
* @typedef {function} FetchFunction
* @param {string} uri - Resource URI to fetch
* @param {object} inlineRequest - HTTP request options
* @param {number} inlineTimeout - Request timeout in milliseconds
* @param {function} callback - Callback function (error, body)
*/Using a Custom Fetch Function:
const customFetchMinifier = new CleanCSS({
inline: 'all',
fetch: function(uri, inlineRequest, inlineTimeout, callback) {
// Custom fetch implementation
const request = require('https').get(uri, {
timeout: inlineTimeout,
headers: inlineRequest.headers || {}
}, (response) => {
let body = '';
response.on('data', chunk => body += chunk);
response.on('end', () => callback(null, body));
});
request.on('error', callback);
request.on('timeout', () => callback(new Error('Request timeout')));
}
});Fetch Function with Proxy Support:
const proxyFetchMinifier = new CleanCSS({
inline: 'remote',
fetch: function(uri, inlineRequest, inlineTimeout, callback) {
const proxyOptions = {
hostname: 'proxy.company.com',
port: 8080,
path: uri,
headers: {
'User-Agent': 'CleanCSS/5.3.3',
'Proxy-Authorization': 'Basic ' + Buffer.from('user:pass').toString('base64')
}
};
const request = require('http').get(proxyOptions, (response) => {
let body = '';
response.on('data', chunk => body += chunk);
response.on('end', () => callback(null, body));
});
request.on('error', callback);
request.setTimeout(inlineTimeout, () => {
request.abort();
callback(new Error('Fetch timeout'));
});
}
});URL rebasing adjusts relative paths in CSS url() functions to work correctly from the output location.
interface RebaseOptions {
rebase?: boolean; // Enable URL rebasing
rebaseTo?: string; // Target directory for rebased URLs
}const rebaseMinifier = new CleanCSS({
rebase: true,
rebaseTo: 'dist/css/' // Output directory
});
// Input CSS from src/styles/main.css:
const css = `
.logo {
background-image: url('../images/logo.png');
}
.icon {
background-image: url('../../assets/icons/menu.svg');
}
`;
const result = rebaseMinifier.minify(css);
// URLs are rebased relative to dist/css/
// url('../images/logo.png') becomes url('../../src/images/logo.png')const absoluteRebaseMinifier = new CleanCSS({
rebase: true,
rebaseTo: '/var/www/html/assets/css/'
});
// All relative URLs are rebased to work from the absolute target pathconst rebaseInlineMinifier = new CleanCSS({
inline: 'local',
rebase: true,
rebaseTo: 'build/css/'
});
// When files are inlined, their URLs are also rebased appropriatelyThe plugin system allows you to extend Clean-CSS with custom optimizations at different levels.
/**
* Plugin configuration for extending Clean-CSS functionality
* @typedef {object} PluginObject
* @property {Level1Plugin[]} [plugins] - Array of plugin objects
*/
/**
* Level 1 plugin for property and value transformations
* @typedef {object} Level1Plugin
* @property {object} [level1] - Level 1 plugin methods
* @property {function} [level1.property] - Property transformation function
* @property {function} [level1.value] - Value transformation function
*/
/**
* Level 2 plugin for block-level transformations
* @typedef {object} Level2Plugin
* @property {object} [level2] - Level 2 plugin methods
* @property {function} [level2.block] - Block transformation function
*/
/**
* Complete plugin structure
* @typedef {object} Plugin
* @property {object} [level1] - Level 1 transformations
* @property {function} [level1.property] - Transform properties: (rule, property) => void
* @property {function} [level1.value] - Transform values: (propertyName, propertyValue) => string
* @property {object} [level2] - Level 2 transformations
* @property {function} [level2.block] - Transform token blocks: (tokens) => void
*/Level 1 plugins operate on individual properties and values:
const pluginMinifier = new CleanCSS({
level: {
1: {
// Enable all default level 1 optimizations
}
},
plugins: {
level1: {
// Custom property transformation
property: function(rule, property) {
// Transform property based on rule context
if (property.name === 'color' && property.value === 'black') {
property.value = '#000';
}
},
// Custom value transformation
value: function(propertyName, propertyValue) {
// Transform specific property values
if (propertyName === 'font-weight' && propertyValue === 'normal') {
return '400';
}
return propertyValue;
}
}
}
});Level 2 plugins operate on token blocks and rules:
const blockPluginMinifier = new CleanCSS({
level: {
2: {
// Enable default level 2 optimizations
}
},
plugins: {
level2: {
block: function(tokens) {
// Custom block-level transformations
tokens.forEach(token => {
if (token.kind === 'rule') {
// Custom rule processing
optimizeCustomRules(token);
}
});
}
}
}
});const customOptimizer = new CleanCSS({
level: {
1: {
variableValueOptimizers: ['color', 'fraction']
},
2: {
mergeAdjacentRules: true,
mergeIntoShorthands: true
}
},
plugins: {
level1: {
property: function(rule, property) {
// Remove vendor prefixes for modern browsers
if (property.name.startsWith('-webkit-') ||
property.name.startsWith('-moz-') ||
property.name.startsWith('-ms-')) {
const standardProperty = property.name.replace(/^-\w+-/, '');
if (hasWideSupport(standardProperty)) {
property.unused = true; // Mark for removal
}
}
},
value: function(propertyName, propertyValue) {
// Convert legacy color formats
if (propertyName.includes('color')) {
return optimizeColorValue(propertyValue);
}
return propertyValue;
}
},
level2: {
block: function(tokens) {
// Remove duplicate media queries
removeDuplicateMediaQueries(tokens);
// Merge similar selectors
mergeSimilarSelectors(tokens);
}
}
}
});
function hasWideSupport(property) {
// Check if property has wide browser support
const wellSupportedProperties = [
'transform', 'transition', 'animation', 'border-radius'
];
return wellSupportedProperties.includes(property);
}
function optimizeColorValue(value) {
// Custom color optimization logic
const colorMap = {
'#ffffff': '#fff',
'#000000': '#000',
'rgba(0,0,0,1)': '#000'
};
return colorMap[value] || value;
}interface FormatOptions {
breaks?: BreakOptions;
breakWith?: 'lf' | 'crlf' | 'system';
indentBy?: number;
indentWith?: 'space' | 'tab';
spaces?: SpaceOptions;
wrapAt?: number;
semicolonAfterLastProperty?: boolean;
}
interface BreakOptions {
afterAtRule?: boolean | number;
afterBlockBegins?: boolean | number;
afterBlockEnds?: boolean | number;
afterComment?: boolean | number;
afterProperty?: boolean | number;
afterRuleBegins?: boolean | number;
afterRuleEnds?: boolean | number;
beforeBlockEnds?: boolean | number;
betweenSelectors?: boolean | number;
}const beautifyMinifier = new CleanCSS({
format: 'beautify' // Shortcut for readable formatting
});
// Or custom formatting:
const customFormatMinifier = new CleanCSS({
format: {
breaks: {
afterProperty: true,
afterRuleEnds: 2, // Two line breaks after rules
afterComment: 1
},
spaces: {
beforeBlockBegins: true
},
indentBy: 2,
indentWith: 'space',
breakWith: 'lf'
}
});const keepBreaksMinifier = new CleanCSS({
format: 'keep-breaks' // Preserve existing line breaks
});async function processCSS(input) {
const minifier = new CleanCSS({
returnPromise: true,
level: 2,
sourceMap: true,
inline: 'all',
rebase: true
});
try {
const result = await minifier.minify(input);
// Analyze results
const analysis = {
success: result.errors.length === 0,
warnings: result.warnings,
errors: result.errors,
stats: result.stats,
inlinedFiles: result.inlinedStylesheets,
hasSourceMap: !!result.sourceMap
};
// Log performance metrics
console.log(`Minification completed in ${result.stats.timeSpent}ms`);
console.log(`Size reduction: ${(result.stats.efficiency * 100).toFixed(1)}%`);
if (result.warnings.length > 0) {
console.warn('Warnings:', result.warnings);
}
if (result.errors.length > 0) {
console.error('Errors:', result.errors);
}
return {
css: result.styles,
sourceMap: result.sourceMap,
analysis
};
} catch (errors) {
throw new Error(`Minification failed: ${errors.join(', ')}`);
}
}