cssnano
Version:
A modular CSS minifier, built on top of the PostCSS ecosystem.
203 lines (183 loc) • 5.74 kB
JavaScript
import { createRequire } from 'node:module';
import fs from 'node:fs';
import path from 'node:path';
import postcss from 'postcss';
import defaultPreset from 'cssnano-preset-default';
const require = createRequire(import.meta.url);
const configFileNames = ['.cssnanorc.json', 'cssnano.config.js'];
/** @typedef {boolean | { exclude?: boolean } | void | undefined} PluginOptions */
/** @typedef {import('postcss').PluginCreator<any>} PluginCreator */
/** @typedef {[PluginCreator, PluginOptions]} PresetPlugin */
/** @typedef {(options?: any) => { plugins: PresetPlugin[] }} PresetFactory */
/** @typedef {string | PresetFactory | [string | PresetFactory, object] | { plugins: PresetPlugin[] }} PresetSpec */
/** @typedef {string | PluginCreator | [string | PluginCreator, object?]} PluginSpec */
/** @typedef {{ preset?: PresetSpec, plugins?: PluginSpec[], configFile?: string }} Options */
/**
* @param {string} moduleId
* @returns {boolean}
*/
function isResolvable(moduleId) {
try {
require.resolve(moduleId);
return true;
} catch {
return false;
}
}
/**
* preset can be one of four possibilities:
* preset = 'default'
* preset = ['default', {}]
* preset = function <- to be invoked
* preset = {plugins: []} <- already invoked function
*
* @param {unknown} preset
* @return {PresetPlugin[]}
*/
function resolvePreset(preset) {
let fn, options;
if (Array.isArray(preset)) {
fn = preset[0];
options = preset[1];
} else {
fn = preset;
options = {};
}
// For JS setups where we invoked the preset already
if (typeof fn === 'object' && fn !== null && 'plugins' in fn) {
return fn.plugins;
}
// Provide an alias for the default preset, as it is built-in.
if (fn === 'default') {
return defaultPreset(options).plugins;
}
// For non-JS setups; we'll need to invoke the preset ourselves.
if (typeof fn === 'function') {
return fn(options).plugins;
}
// Try loading a preset from node_modules
if (isResolvable(fn)) {
return require(fn)(options).plugins;
}
// Only the built-in preset names support shorthand resolution. Other
// strings must be passed to the module resolver unchanged.
if (fn === 'lite' || fn === 'advanced') {
const sugar = `cssnano-preset-${fn}`;
if (isResolvable(sugar)) {
return require(sugar)(options).plugins;
}
}
// If all else fails, we probably have a typo in the config somewhere
throw new Error(
`Cannot load preset "${fn}". Please check your configuration for errors and try again.`
);
}
/**
* @param {string} filePath
* @returns {unknown}
*/
function loadConfigFile(filePath) {
if (filePath.endsWith('.json')) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
const config = createRequire(filePath)(filePath);
if (
config !== null &&
typeof config === 'object' &&
Object.prototype.toString.call(config) === '[object Module]' &&
'default' in config
) {
return config.default;
}
return config;
}
/**
* @param {string} [configFile]
* @returns {unknown | null}
*/
function loadDiscoveredConfig(configFile) {
if (configFile) {
const configPath = path.resolve(process.cwd(), configFile);
if (!fs.existsSync(configPath)) {
throw new Error(
`Cannot find cssnano configuration file "${configFile}".`
);
}
return loadConfigFile(configPath);
}
for (const fileName of configFileNames) {
const configPath = path.join(process.cwd(), fileName);
if (!fs.existsSync(configPath)) continue;
return loadConfigFile(configPath);
}
return null;
}
/**
* @param {Options} options
* @returns {PresetPlugin[]}
*/
function resolveConfig(options) {
if (options.preset) {
if (
Array.isArray(options.preset) &&
/** @type {unknown[]} */ (options.preset).length === 0
) {
return [];
}
return resolvePreset(options.preset);
}
if (Array.isArray(options.plugins)) return [];
const config = loadDiscoveredConfig(options.configFile);
const preset =
config !== null && typeof config === 'object' && 'preset' in config
? config.preset
: config;
return resolvePreset(config === null ? 'default' : preset);
}
/**
* @param {Options=} options
* @return {import('postcss').Processor}
*/
function cssnanoPlugin(options = {}) {
let nanoPlugins = resolveConfig(options);
if (Array.isArray(options.plugins)) {
nanoPlugins = nanoPlugins.slice();
const inputPlugins = options.plugins;
for (const plugin of inputPlugins) {
if (Array.isArray(plugin)) {
const [pluginDef, opts = {}] = plugin;
if (typeof pluginDef === 'string' && isResolvable(pluginDef)) {
nanoPlugins.push([
/** @type {PluginCreator} */ (require(pluginDef)),
opts,
]);
} else {
nanoPlugins.push([/** @type {PluginCreator} */ (pluginDef), opts]);
}
} else if (typeof plugin === 'string' && isResolvable(plugin)) {
nanoPlugins.push([/** @type {PluginCreator} */ (require(plugin)), {}]);
} else {
nanoPlugins.push([/** @type {PluginCreator} */ (plugin), {}]);
}
}
}
const plugins = [];
for (const nanoPlugin of nanoPlugins) {
if (Array.isArray(nanoPlugin)) {
const [processor, opts] = nanoPlugin;
if (
typeof opts === 'undefined' ||
(typeof opts === 'object' && !opts.exclude) ||
(typeof opts === 'boolean' && opts === true)
) {
plugins.push(processor(opts));
}
} else {
plugins.push(nanoPlugin);
}
}
return postcss(plugins);
}
/** @type {true} */
cssnanoPlugin.postcss = true;
export { cssnanoPlugin as default, cssnanoPlugin as 'module.exports' };