purgecss-webpack-plugin
Version:
PurgeCSS plugin for webpack - Remove unused css
141 lines (138 loc) • 4.95 kB
JavaScript
import * as fs from 'fs';
import * as path from 'path';
import { PurgeCSS, defaultOptions } from 'purgecss';
import { sources } from 'webpack';
const styleExtensions = [".css", ".scss", ".styl", ".sass", ".less"];
const pluginName = "PurgeCSS";
/**
* Get the filename without ?hash
*
* @param fileName - file name
*/
function getFormattedFilename(fileName) {
if (fileName.includes("?")) {
return fileName.split("?").slice(0, -1).join("");
}
return fileName;
}
/**
* Returns true if the filename is of types of one of the specified extensions
*
* @param filename - file name
* @param extensions - extensions
*/
function isFileOfTypes(filename, extensions) {
const extension = path.extname(getFormattedFilename(filename));
return extensions.includes(extension);
}
function getPurgeCSSOptions(pluginOptions, filesToSearch, asset, fileName, sourceMap) {
const options = {
...defaultOptions,
...pluginOptions,
content: filesToSearch,
css: [
{
raw: asset.source().toString(),
},
],
};
if (typeof options.safelist === "function") {
options.safelist = options.safelist();
}
if (typeof options.blocklist === "function") {
options.blocklist = options.blocklist();
}
return {
content: options.content,
css: options.css,
defaultExtractor: options.defaultExtractor,
extractors: options.extractors,
fontFace: options.fontFace,
keyframes: options.keyframes,
output: options.output,
rejected: options.rejected,
variables: options.variables,
safelist: options.safelist,
blocklist: options.blocklist,
sourceMap: sourceMap ? { inline: false, to: fileName } : false,
};
}
/**
* Create the Source instance result of PurgeCSS
*
* @param name - asset name
* @param asset - webpack asset
* @param purgeResult - result of PurgeCSS purge method
* @param sourceMap - wether sourceMap is enabled
* @returns the new Source
*/
function createSource(name, asset, purgeResult, sourceMap) {
if (!sourceMap || !purgeResult.sourceMap) {
return new sources.RawSource(purgeResult.css);
}
const { source, map } = asset.sourceAndMap();
return new sources.SourceMapSource(purgeResult.css, name, purgeResult.sourceMap, source.toString(), map, false);
}
/**
* @public
*/
class PurgeCSSPlugin {
constructor(options) {
this.purgedStats = {};
this.options = options;
}
apply(compiler) {
compiler.hooks.compilation.tap(pluginName, this.initializePlugin.bind(this));
}
initializePlugin(compilation) {
compilation.hooks.additionalAssets.tapPromise(pluginName, async () => {
let configFileOptions;
try {
const t = path.resolve(process.cwd(), "purgecss.config.js");
configFileOptions = await import(t);
}
catch {
// no config file present
}
this.options = {
...(configFileOptions ? configFileOptions : {}),
...this.options,
};
const entryPaths = typeof this.options.paths === "function"
? this.options.paths()
: this.options.paths;
entryPaths.forEach((p) => {
if (!fs.existsSync(p))
throw new Error(`Path ${p} does not exist.`);
});
return this.runPluginHook(compilation, entryPaths);
});
}
async runPluginHook(compilation, entryPaths) {
const assetsFromCompilation = Object.entries(compilation.assets).filter(([name]) => {
return isFileOfTypes(name, [".css"]);
});
for (const chunk of compilation.chunks) {
const assetsToPurge = assetsFromCompilation.filter(([name]) => {
if (this.options.only) {
if (!this.options.only.some((only) => name.includes(only))) {
return false;
}
}
return chunk.files.has(name);
});
for (const [name, asset] of assetsToPurge) {
const filesToSearch = entryPaths.filter((v) => !styleExtensions.some((ext) => v.endsWith(ext)));
const sourceMapEnabled = !!compilation.compiler.options.devtool;
const purgeCSSOptions = getPurgeCSSOptions(this.options, filesToSearch, asset, name, sourceMapEnabled);
const purgecss = await new PurgeCSS().purge(purgeCSSOptions);
const purged = purgecss[0];
if (purged.rejected) {
this.purgedStats[name] = purged.rejected;
}
compilation.updateAsset(name, createSource(name, asset, purged, sourceMapEnabled));
}
}
}
}
export { PurgeCSSPlugin };