/
index.ts
136 lines (116 loc) · 3.82 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import * as fs from "fs";
import path from "path";
import PurgeCSS, { defaultOptions } from "purgecss";
import { Compilation, Compiler } from "webpack";
import { ConcatSource } from "webpack-sources";
import { PurgedStats, UserDefinedOptions } from "./types";
const styleExtensions = [".css", ".scss", ".styl", ".sass", ".less"];
const pluginName = "PurgeCSS";
/**
* Get the filename without ?hash
* @param fileName file name
*/
function getFormattedFilename(fileName: string): string {
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: string, extensions: string[]): boolean {
const extension = path.extname(getFormattedFilename(filename));
return extensions.includes(extension);
}
export class PurgeCSSPlugin {
options: UserDefinedOptions;
purgedStats: PurgedStats = {};
constructor(options: UserDefinedOptions) {
this.options = options;
}
apply(compiler: Compiler): void {
compiler.hooks.compilation.tap(
pluginName,
this.initializePlugin.bind(this)
);
}
initializePlugin(compilation: Compilation): void {
compilation.hooks.additionalAssets.tapPromise(pluginName, () => {
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: Compilation,
entryPaths: string[]
): Promise<void> {
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) {
return this.options.only.some((only) => name.includes(only));
}
return Array.isArray(chunk.files)
? chunk.files.includes(name)
: chunk.files.has(name);
});
for (const [name, asset] of assetsToPurge) {
const filesToSearch = entryPaths.filter(
(v) => !styleExtensions.some((ext) => v.endsWith(ext))
);
// Compile through Purgecss and attach to output.
// This loses sourcemaps should there be any!
const options = {
...defaultOptions,
...this.options,
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();
}
const purgecss = await new PurgeCSS().purge({
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,
});
const purged = purgecss[0];
if (purged.rejected) {
this.purgedStats[name] = purged.rejected;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
compilation.updateAsset(name, new ConcatSource(purged.css));
}
}
}
}
export default PurgeCSSPlugin;