Skip to content

Commit 3570c7d

Browse files
authored
feat: new purgecss-from-jsx plugin (#692)
* added acorn acorn-walk acorn-jsx acorn-jsx-walk dependencies, add a new purgecss plugin for extracting id, class and tag from JSX content * replace object by any in .d.ts because it doesn't work with build machine * update acorn-walk, remove acorn-walk.d.ts (not needed anymore)
1 parent 45e0533 commit 3570c7d

File tree

11 files changed

+229
-1
lines changed

11 files changed

+229
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"postcss-purgecss",
77
"purgecss",
88
"purgecss-from-html",
9+
"purgecss-from-jsx",
910
"purgecss-from-pug",
1011
"purgecss-from-twig",
1112
"purgecss-webpack-plugin",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# `purgecss-from-jsx`
2+
3+
> TODO: description
4+
5+
## Usage
6+
7+
```
8+
const purgecssFromJsx = require('purgecss-from-jsx');
9+
10+
// TODO: DEMONSTRATE API
11+
```
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export const TEST_1_CONTENT = `
2+
import React from "react";
3+
4+
class MyComponent extends React.Component {
5+
render() {
6+
return (
7+
<React.Fragment>
8+
<div className="test-container">Well</div>
9+
<div className="test-footer" id="an-id"></div>
10+
<a href="#" id="a-link" className="a-link"></a>
11+
<input id="blo" type="text" disabled/>
12+
</React.Fragment>
13+
);
14+
}
15+
}
16+
17+
export default MyComponent;
18+
`;
19+
20+
export const TEST_1_TAG = ["div", "a", "input"];
21+
22+
export const TEST_1_CLASS = ["test-container", "test-footer", "a-link"];
23+
24+
export const TEST_1_ID = ["a-link", "blo"];
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import purgeJsx from "../src/index";
2+
3+
import { TEST_1_CONTENT, TEST_1_TAG, TEST_1_CLASS, TEST_1_ID } from "./data";
4+
5+
const plugin = purgeJsx({sourceType: "module"});
6+
7+
describe("purgePug", () => {
8+
describe("from a normal html document", () => {
9+
it("finds tag selectors", () => {
10+
const received = plugin(TEST_1_CONTENT);
11+
for (const item of TEST_1_TAG) {
12+
expect(received.includes(item)).toBe(true);
13+
}
14+
});
15+
16+
it("finds classes selectors", () => {
17+
const received = plugin(TEST_1_CONTENT);
18+
for (const item of TEST_1_CLASS) {
19+
expect(received.includes(item)).toBe(true);
20+
}
21+
});
22+
23+
it("finds id selectors", () => {
24+
const received = plugin(TEST_1_CONTENT);
25+
for (const item of TEST_1_ID) {
26+
expect(received.includes(item)).toBe(true);
27+
}
28+
});
29+
30+
it("finds all selectors", () => {
31+
const received = plugin(TEST_1_CONTENT);
32+
const selectors = [...TEST_1_TAG, ...TEST_1_CLASS, ...TEST_1_ID];
33+
for (const item of selectors) {
34+
expect(received.includes(item)).toBe(true);
35+
}
36+
});
37+
});
38+
});

packages/purgecss-from-jsx/package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "purgecss-from-jsx",
3+
"version": "4.0.3",
4+
"description": "JSX extractor for PurgeCSS",
5+
"author": "Ffloriel",
6+
"homepage": "https://github.com/FullHuman/purgecss#readme",
7+
"license": "ISC",
8+
"main": "lib/purgecss-from-jsx.js",
9+
"directories": {
10+
"lib": "lib",
11+
"test": "__tests__"
12+
},
13+
"files": [
14+
"lib"
15+
],
16+
"repository": {
17+
"type": "git",
18+
"url": "git+https://github.com/FullHuman/purgecss.git"
19+
},
20+
"scripts": {
21+
"test": "echo \"Error: run tests from root\" && exit 1"
22+
},
23+
"bugs": {
24+
"url": "https://github.com/FullHuman/purgecss/issues"
25+
},
26+
"dependencies": {
27+
"acorn": "^7.4.0",
28+
"acorn-jsx": "^5.3.1",
29+
"acorn-jsx-walk": "^2.0.0",
30+
"acorn-walk": "^8.1.1"
31+
}
32+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as acorn from "acorn";
2+
import * as walk from "acorn-walk";
3+
import jsx from "acorn-jsx";
4+
import {extend} from "acorn-jsx-walk";
5+
6+
extend(walk.base);
7+
8+
function purgeFromJsx(options: acorn.Options) {
9+
return (content: string): string[] => {
10+
// Will be filled during walk
11+
const state = {selectors: []};
12+
13+
// Parse and walk any JSXElement
14+
walk.recursive(
15+
acorn.Parser.extend(jsx()).parse(content, options),
16+
state,
17+
{
18+
JSXOpeningElement(node: any, state: any, callback) {
19+
// JSXIdentifier | JSXMemberExpression | JSXNamespacedName
20+
const nameState: any = {};
21+
callback(node.name, nameState);
22+
if (nameState.text) {
23+
state.selectors.push(nameState.text);
24+
}
25+
26+
for (let i = 0; i < node.attributes.length; ++i) {
27+
callback(node.attributes[i], state);
28+
}
29+
},
30+
JSXAttribute(node: any, state: any, callback) {
31+
// Literal | JSXExpressionContainer | JSXElement | nil
32+
if (!node.value) {
33+
return;
34+
}
35+
36+
// JSXIdentifier | JSXNamespacedName
37+
const nameState: any = {};
38+
callback(node.name, nameState);
39+
40+
// node.name is id or className
41+
switch (nameState.text) {
42+
case "id":
43+
case "className":
44+
{
45+
// Get text in node.value
46+
const valueState: any = {};
47+
callback(node.value, valueState);
48+
49+
// node.value is not empty
50+
if (valueState.text) {
51+
state.selectors.push(...valueState.text.split(" "));
52+
}
53+
}
54+
break;
55+
default:
56+
break;
57+
}
58+
},
59+
JSXIdentifier(node: any, state: any) {
60+
state.text = node.name;
61+
},
62+
JSXNamespacedName(node: any, state: any) {
63+
state.text = node.namespace.name + ":" + node.name.name;
64+
},
65+
// Only handle Literal for now, not JSXExpressionContainer | JSXElement
66+
Literal(node: any, state: any) {
67+
if (typeof node.value === "string") {
68+
state.text = node.value;
69+
}
70+
}
71+
},
72+
{...walk.base}
73+
);
74+
75+
return state.selectors;
76+
};
77+
}
78+
79+
export default purgeFromJsx;

scripts/build.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ const packages = [
4141
name: "purgecss-from-pug",
4242
external: ["pug-lexer"],
4343
},
44+
{
45+
name: "purgecss-from-jsx",
46+
external: ["acorn", "acorn-walk", "acorn-jsx", "acorn-jsx-walk"],
47+
}
4448
];
4549

4650
async function build(): Promise<void> {

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"*" : ["types/*"],
2222
"purgecss": ["packages/purgecss/src"],
2323
"@fullhuman/purgecss-from-html": ["packages/purgecss-from-html/src"],
24-
"@fullhuman/purgecss-from-pug": ["packages/purgecss-from-pug/src"]
24+
"@fullhuman/purgecss-from-pug": ["packages/purgecss-from-pug/src"],
25+
"@fullhuman/purgecss-from-jsx": ["packages/purgecss-from-jsx/src"]
2526
}
2627
},
2728
"include": [

types/acorn-jsx-walk.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export function extend(base: any): void;

0 commit comments

Comments
 (0)