cssnano
Version:
A modular CSS minifier, built on top of the PostCSS ecosystem.
257 lines (233 loc) • 7.67 kB
JavaScript
import { createRequire } from 'node:module';
import fs from 'node:fs';
import path from 'node:path';
import postcss from 'postcss';
import defaultPreset from '#presets/default';
import litePreset from '#presets/lite';
const require = createRequire(import.meta.url);
/**
* Built-in presets, used when no installed package matches the name. The
* advanced preset loads on first use, so the default path stays small.
*
* @param {unknown} name
* @param {any} options
* @returns {PresetPlugin[] | undefined}
*/
function builtInPresetPlugins(name, options) {
switch (name) {
case 'cssnano-preset-default':
return defaultPreset(options).plugins;
case 'cssnano-preset-lite':
case 'lite':
return litePreset(options).plugins;
case 'cssnano-preset-advanced':
case 'advanced':
return require('#presets/advanced')(options).plugins;
default:
return undefined;
}
}
/** Built-in preset packages whose installed copy has not been reported yet. */
const unreportedPresetPackages = new Set([
'cssnano-preset-default',
'cssnano-preset-lite',
'cssnano-preset-advanced',
]);
/**
* Loads an installed preset package. When it shares a name with a built-in
* preset, warn once: an outdated install would otherwise silently replace the
* preset that ships with this version of cssnano.
*
* @param {string} moduleId
* @param {string} modulePath
* @param {any} options
* @returns {PresetPlugin[]}
*/
function installedPresetPlugins(moduleId, modulePath, options) {
if (unreportedPresetPackages.delete(moduleId)) {
process.emitWarning(
`cssnano is using the installed package "${moduleId}" (${modulePath}) instead of its built-in preset. ` +
`Uninstall "${moduleId}" to use the built-in preset, or pass the imported preset function to keep the installed one.`,
{ code: 'CSSNANO_INSTALLED_PRESET' }
);
}
return require(modulePath)(options).plugins;
}
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 {string | undefined} the resolved path, if the module is installed
*/
function resolveModule(moduleId) {
try {
return require.resolve(moduleId);
} catch {
return undefined;
}
}
/**
* 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;
}
// The shorthands stand for the cssnano-preset-* packages only, never for an
// unrelated package that happens to be called "lite" or "advanced".
const moduleId =
fn === 'lite' || fn === 'advanced' ? `cssnano-preset-${fn}` : fn;
// Try loading a preset from node_modules
const modulePath = resolveModule(moduleId);
if (modulePath) {
return installedPresetPlugins(moduleId, modulePath, options);
}
// An installed preset wins above; the built-in only fills in when none exists.
const builtIn = builtInPresetPlugins(fn, options);
if (builtIn) {
return builtIn;
}
// 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' && resolveModule(pluginDef)) {
nanoPlugins.push([
/** @type {PluginCreator} */ (require(pluginDef)),
opts,
]);
} else {
nanoPlugins.push([/** @type {PluginCreator} */ (pluginDef), opts]);
}
} else if (typeof plugin === 'string' && resolveModule(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' };