UNPKG

licss

Version:

Gulp plugin 'licss' designed for style transformation workflows, and supported .css, .scss, .sass and .pcss files. You can use it to process Bootsrtap framework CSS files (sass version 1.78.0 was used here for the stop warning)

245 lines (244 loc) 10.3 kB
import browserslist from 'browserslist'; import colors from 'colors'; import log from 'fancy-log'; import * as glob from 'glob'; import { browserslistToTargets, bundle, transform } from 'lightningcss'; import { Buffer } from 'node:buffer'; import { dirname, join, relative } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import PluginError from 'plugin-error'; import { PurgeCSS } from 'purgecss'; import { compileString } from 'sass'; import through2 from 'through2'; import rename from './rename.js'; export { rename }; /** * Gulp plugin for style transformation - bundles, compiles, minimizes, and cleans up sass, scss, css, and postcss style sheets. * @param options - optons {} * @param options.compiler use SASS/SCSS or LightningCSS compiler for CSS files * @param option.postprocess Post-Processing via LightningCSS * @param option.loadPaths paths for files to imports for SASS/SCSS compiler * @param option.purgeOptions remove unused CSS from file - options PurgeCSS * @param option.silent enable/disable information messages about the progress of the compilation process * @returns object stream. * * @example * * ```js * // import modules * import { dest, src } from 'gulp' * import licss, { rename } from 'licss' * * // sample task for postcss files * function css() { * return src(['src/styles/main.css'], { sourcemaps: true }) * .pipe(licss({ * silent: false, * postprocess: 'autoprefixer', * purgeOptions: { * content: ["src/*.html", "src/scripts/*.ts"], * }, * })) * .pipe(rename({ suffix: '.min', extname: '.css' })) * .pipe(dest('dist/css', { sourcemaps: '.' })) * } * * // export * export { css } * * ``` */ export default function licss(options = {}) { return through2.obj(async function (file, _, cb) { // Skip null files if (file.isNull()) { return cb(null, file); } // Reject streams if (file.isStream()) { cb(new PluginError('licss', 'Streams are not supported')); return; } // Skip partials if (file.stem.startsWith('_')) { cb(); return; } if (file.isBuffer()) { try { const compiler = options.compiler ?? 'sass'; const postpro = options.postprocess ?? 'full'; const loadPaths = options.loadPaths ?? [dirname(file.path), join(file.cwd, 'node_modules')]; const purgeOptions = options.purgeOptions ?? null; const silent = options.silent ?? true; const extname = file.extname.split('.').pop()?.toLowerCase() ?? ''; // Validate file extension if (!/^(css|scss|sass|pcss)$/i.test(extname)) { throw new Error('• "licss": Unsupported file extension. Supported: .css, .scss, .sass, .pcss'); } // Validate compiler if (options.compiler && !/^(sass|lightningcss)$/i.test(compiler)) { throw new Error('• "licss": Unsupported "compiler" option.\nSupported: "sass", "lightningcss" or undefined. Default: "sass"'); } // Validate postprocess if (options.postprocess && !/^(full|minify|autoprefixer|none)$/i.test(postpro)) { throw new Error('• "licss": Unsupported "postprocess" option.\nSupported: "full", "minify", "autoprefixer", "none" or undefined. Default: "full"'); } // get list supported browsers const targetsList = getTargets(file.cwd); const prefix = postpro === 'autoprefixer' || postpro === 'full'; const minify = postpro === 'minify' || postpro === 'full'; const isPurgMin = purgeOptions ? false : minify; // no initial minify if purge enabled const isSassMin = prefix ? false : isPurgMin; // no initial minify if on autoprefixer const sourceMap = file.sourceMap ? true : false; const isCssFile = /^css$/i.test(extname); const isSassFile = /^(sass|scss)$/i.test(extname); if (!silent) { mess(file, `INFO: postprocess: ${postpro}, minify: ${minify}, autoprefixer: ${prefix}, file:`); } // Bundle process based on file type if ((isCssFile && compiler === 'sass') || isSassFile) { // used SASS compiler if (!silent) { mess(file, 'Run SASS compiler for file:'); } await handleSassCompilation(file, isSassMin, loadPaths, sourceMap); if (prefix) { // used postprocess compiler on base LightningCSS if (!silent) { mess(file, 'Run postprocess compiler on base LightningCSS for file:'); } await handleLightningCSSCompilation(file, isPurgMin, prefix, targetsList, sourceMap, 'many'); } } else { // used LightningCSS Bundle compiler if (!silent) { mess(file, 'Run LightningCSS Bundle compiler for file:'); } await handleLightningcssBundle(file, isPurgMin, prefix, targetsList, sourceMap); } if (purgeOptions) { // Purge unused CSS in production if (!silent) { mess(file, 'Purge unused CSS in file:'); } await purgeTransform(file, purgeOptions); if (!purgeOptions.rejected) { await handleLightningCSSCompilation(file, minify, prefix, targetsList, sourceMap, 'one'); } } } catch (err) { cb(new PluginError('licss', err, { fileName: file.path })); } } cb(null, file); }); } // logs results of the conversion process function mess(file, message) { return log(colors.cyan('licss ') + colors.red('★ ') + colors.magenta(message + ' ') + colors.blue(file.relative)); } // Get target browsers from browserslist function getTargets(cwd) { const config = browserslist.loadConfig({ path: cwd }) ?? browserslist('> 0.2%, last 2 major versions, not dead'); return browserslistToTargets(browserslist(config)); } // Transform CSS with LightningCSS async function handleLightningCSSCompilation(file, minify, prefix, targetsList, sourceMap, type) { if (file.isBuffer()) { const result = transform({ targets: type === 'one' ? undefined : prefix ? targetsList : undefined, filename: file.basename, minify, inputSourceMap: type === 'one' ? undefined : sourceMap ? JSON.stringify(file.sourceMap) : undefined, sourceMap, code: Buffer.from(new TextDecoder().decode(file.contents)), projectRoot: type === 'one' ? file.base : undefined, }); file.extname = '.css'; file.contents = Buffer.from(new TextDecoder().decode(result.code)); if (result.map) file.sourceMap = JSON.parse(new TextDecoder().decode(result.map)); } } // Bundle CSS with LightningCSS async function handleLightningcssBundle(file, minify, prefix, targetsList, sourceMap) { if (file.isBuffer()) { const result = bundle({ targets: prefix ? targetsList : undefined, filename: file.path, minify, sourceMap, projectRoot: file.base, }); file.extname = '.css'; file.contents = Buffer.from(new TextDecoder().decode(result.code)); if (result.map) file.sourceMap = JSON.parse(new TextDecoder().decode(result.map)); } } // Compile sass/scss async function handleSassCompilation(file, minify, loadPaths, makeSourceMap) { if (file.isBuffer()) { const result = compileString(parseImport(file), { url: pathToFileURL(file.path), loadPaths, syntax: file.extname === '.sass' ? 'indented' : 'scss', style: minify ? 'compressed' : 'expanded', sourceMap: makeSourceMap, sourceMapIncludeSources: true, }); file.extname = '.css'; file.contents = Buffer.from(result.css); if (makeSourceMap && result.sourceMap) { addSourceMap(file, JSON.parse(JSON.stringify(result.sourceMap))); } } } // Add or update source map function addSourceMap(file, map) { if (file.isBuffer()) { map.file = file.relative.replace(/\\/g, '/'); map.sources = map.sources.map((path) => { if (path.startsWith('file:')) { path = fileURLToPath(path); } const basePath = file.base; path = relative(basePath, path); return path.replace(/\\/g, '/'); }); file.sourceMap = map; } } // Resolve file paths via glob function getFiles(contentArray, ignore) { return contentArray.reduce((acc, content) => { return [...acc, ...glob.sync(content, { ignore })]; }, []); } // Clean import statements function parseImport(file) { return String(file.contents).replace(/@import +([url(]*)["']([./]*)([a-z-_/]+)\.?(.*)['"]\)?/gi, '@import "$2$3"'); } // Purge unused CSS async function purgeTransform(file, options) { if (file.isBuffer()) { const processedContent = getFiles(options.content, options.skippedContentGlobs); const purgedCSSResults = await new PurgeCSS().purge({ ...options, content: processedContent, css: [ { raw: file.contents.toString(), }, ], stdin: true, sourceMap: false, }); const purge = purgedCSSResults[0]; const result = options.rejected && purge.rejected ? purge.rejected.join(' {}\n') + ' {}' : purge.css; file.contents = Buffer.from(result, 'utf8'); } }