Skip to content

Commit dc59d30

Browse files
committed
feat: add safelist keyframes and css variables
- add safelist for keyframes (#418, #439) - add safelist for css variables
1 parent 41f1c48 commit dc59d30

File tree

7 files changed

+169
-4
lines changed

7 files changed

+169
-4
lines changed

packages/purgecss/__tests__/safelist.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,53 @@ describe("safelist option: greedy", () => {
126126
);
127127
});
128128
});
129+
130+
describe("safelist option: keyframes", () => {
131+
let purgedCSS: string;
132+
beforeAll(async () => {
133+
const resultsPurge = await new PurgeCSS().purge({
134+
content: [`${ROOT_TEST_EXAMPLES}safelist/safelist_keyframes.html`],
135+
css: [`${ROOT_TEST_EXAMPLES}safelist/safelist_keyframes.css`],
136+
safelist: {
137+
keyframes: [/^scale/, "spin"],
138+
},
139+
keyframes: true,
140+
});
141+
purgedCSS = resultsPurge[0].css;
142+
});
143+
144+
it("finds safelisted keyframes", () => {
145+
findInCSS(
146+
expect,
147+
["@keyframes scale", "@keyframes scale-down", "@keyframes spin"],
148+
purgedCSS
149+
);
150+
});
151+
152+
it("excludes non-safelisted keyframes", () => {
153+
notFindInCSS(expect, ["flash"], purgedCSS);
154+
});
155+
});
156+
157+
describe("safelist option: variables", () => {
158+
let purgedCSS: string;
159+
beforeAll(async () => {
160+
const resultsPurge = await new PurgeCSS().purge({
161+
content: [`${ROOT_TEST_EXAMPLES}safelist/safelist_css_variables.html`],
162+
css: [`${ROOT_TEST_EXAMPLES}safelist/safelist_css_variables.css`],
163+
safelist: {
164+
variables: [/^--b/, "--unused-color"],
165+
},
166+
variables: true,
167+
});
168+
purgedCSS = resultsPurge[0].css;
169+
});
170+
171+
it("finds safelisted css variables", () => {
172+
findInCSS(expect, ["--unused-color", "--button-color"], purgedCSS);
173+
});
174+
175+
it("excludes non-safelisted css variables", () => {
176+
notFindInCSS(expect, ["--tertiary-color:"], purgedCSS);
177+
});
178+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
:root {
2+
--primary-color: blue;
3+
--secondary-color: indigo;
4+
--tertiary-color: aqua;
5+
--unused-color: violet;
6+
--used-color: rebeccapurple;
7+
--accent-color: orange;
8+
}
9+
10+
.button {
11+
--button-color: var(--tertiary-color);
12+
--border-color: linear-gradient(to top, var(--secondary-color), var(--used-color, white));
13+
14+
background-color: var(--primary-color);
15+
color: var(--accent-color);
16+
border-color: var(--border-color);
17+
}
18+
19+
.button:focus {
20+
background-color: var(--accent-color);
21+
color: var(--primary-color);
22+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<button class="button">Click me!</button>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
@keyframes bounce {
2+
from, 20%, 53%, 80%, to {
3+
animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 1.000);
4+
transform: translate3d(1, 1, 0);
5+
}
6+
}
7+
8+
.bounce {
9+
-webkit-animation-name: bounce;
10+
animation-name: bounce;
11+
-webkit-transform-origin: center bottom;
12+
transform-origin: center bottom;
13+
}
14+
15+
@keyframes flash {
16+
from, 50%, to {
17+
opacity: 1;
18+
}
19+
20+
25%, 75% {
21+
opacity: 0.5;
22+
}
23+
}
24+
25+
.flash {
26+
animation: flash
27+
}
28+
29+
@keyframes scale {
30+
from {
31+
transform: scale(1);
32+
}
33+
34+
to {
35+
transform: scale(2);
36+
}
37+
}
38+
39+
@keyframes scale-down {
40+
from {
41+
transform: scale(2);
42+
}
43+
44+
to {
45+
transform: scale(1);
46+
}
47+
}
48+
49+
@keyframes spin {
50+
from {
51+
transform: rotate(0deg);
52+
}
53+
54+
to {
55+
transform: rotate(360deg);
56+
}
57+
}
58+
59+
.scale-spin {
60+
animation: spin 300ms linear infinite forwards,scale 300ms linear infinite alternate;
61+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<div class="bounce">
2+
</div>

packages/purgecss/src/VariablesStructure.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import postcss from "postcss";
2+
import { StringRegExpArray } from "./types";
23

34
class VariableNode {
45
public nodes: VariableNode[] = [];
@@ -13,6 +14,7 @@ class VariableNode {
1314
class VariablesStructure {
1415
public nodes: Map<string, VariableNode> = new Map();
1516
public usedVariables: Set<string> = new Set();
17+
public safelist: StringRegExpArray = [];
1618

1719
addVariable(declaration: postcss.Declaration): void {
1820
const { prop } = declaration;
@@ -63,12 +65,20 @@ class VariablesStructure {
6365
for (const used of this.usedVariables) {
6466
this.setAsUsed(used);
6567
}
66-
for (const [, declaration] of this.nodes) {
67-
if (!declaration.isUsed) {
68+
for (const [name, declaration] of this.nodes) {
69+
if (!declaration.isUsed && !this.isVariablesSafelisted(name)) {
6870
declaration.value.remove();
6971
}
7072
}
7173
}
74+
75+
isVariablesSafelisted(variable: string): boolean {
76+
return this.safelist.some((safelistItem) => {
77+
return typeof safelistItem === "string"
78+
? safelistItem === variable
79+
: safelistItem.test(variable);
80+
});
81+
}
7282
}
7383

7484
export default VariablesStructure;

packages/purgecss/src/index.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,18 @@ class PurgeCSS {
542542
return sources;
543543
}
544544

545+
/**
546+
* Check if the keyframe is safelisted with the option safelist keyframes
547+
* @param keyframesName name of the keyframe animation
548+
*/
549+
private isKeyframesSafelisted(keyframesName: string): boolean {
550+
return this.options.safelist.keyframes.some((safelistItem) => {
551+
return typeof safelistItem === "string"
552+
? safelistItem === keyframesName
553+
: safelistItem.test(keyframesName);
554+
});
555+
}
556+
545557
/**
546558
* Check if the selector is safelisted with the option safelist standard
547559
* @param selector css selector
@@ -590,7 +602,11 @@ class PurgeCSS {
590602
...userOptions,
591603
safelist: standardizeSafelist(userOptions.safelist),
592604
};
593-
const { content, css, extractors } = this.options;
605+
const { content, css, extractors, safelist } = this.options;
606+
607+
if (this.options.variables) {
608+
this.variablesStructure.safelist = safelist.variables || [];
609+
}
594610

595611
const fileFormatContents = content.filter(
596612
(o) => typeof o === "string"
@@ -637,7 +653,10 @@ class PurgeCSS {
637653
*/
638654
public removeUnusedKeyframes(): void {
639655
for (const node of this.atRules.keyframes) {
640-
if (!this.usedAnimations.has(node.params)) {
656+
if (
657+
!this.usedAnimations.has(node.params) &&
658+
!this.isKeyframesSafelisted(node.params)
659+
) {
641660
node.remove();
642661
}
643662
}

0 commit comments

Comments
 (0)