UNPKG

cssnano

Version:

A modular CSS minifier, built on top of the PostCSS ecosystem.

203 lines (183 loc) 5.74 kB
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' };