@fullhuman/purgecss-from-jsx 7.0.2 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -4
- package/lib/purgecss-from-jsx.esm.js +36 -8
- package/lib/purgecss-from-jsx.js +36 -8
- package/package.json +12 -10
package/README.md
CHANGED
|
@@ -1,11 +1,113 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fullhuman/purgecss-from-jsx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@fullhuman/purgecss-from-jsx)
|
|
4
|
+
|
|
5
|
+
JSX extractor for [PurgeCSS](https://purgecss.com). This extractor parses JSX files (React, Preact, etc.) and extracts used selectors from component names, `className`, and `id` attributes.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @fullhuman/purgecss-from-jsx --save-dev
|
|
11
|
+
```
|
|
4
12
|
|
|
5
13
|
## Usage
|
|
6
14
|
|
|
15
|
+
### With PurgeCSS
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { PurgeCSS } from "purgecss";
|
|
19
|
+
import purgeFromJsx from "@fullhuman/purgecss-from-jsx";
|
|
20
|
+
|
|
21
|
+
const result = await new PurgeCSS().purge({
|
|
22
|
+
content: ["**/*.jsx"],
|
|
23
|
+
css: ["**/*.css"],
|
|
24
|
+
extractors: [
|
|
25
|
+
{
|
|
26
|
+
extractor: purgeFromJsx(),
|
|
27
|
+
extensions: ["jsx"],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### With Custom Acorn Options
|
|
34
|
+
|
|
35
|
+
You can pass custom [acorn](https://github.com/acornjs/acorn) parser options:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import purgeFromJsx from "@fullhuman/purgecss-from-jsx";
|
|
39
|
+
|
|
40
|
+
const extractor = purgeFromJsx({
|
|
41
|
+
ecmaVersion: 2020,
|
|
42
|
+
sourceType: "module",
|
|
43
|
+
});
|
|
7
44
|
```
|
|
8
|
-
const purgecssFromJsx = require('purgecss-from-jsx');
|
|
9
45
|
|
|
10
|
-
|
|
46
|
+
### Standalone
|
|
47
|
+
|
|
48
|
+
You can use the extractor standalone to extract selectors from JSX content:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import purgeFromJsx from "@fullhuman/purgecss-from-jsx";
|
|
52
|
+
|
|
53
|
+
const jsx = `
|
|
54
|
+
function App() {
|
|
55
|
+
return (
|
|
56
|
+
<div className="container">
|
|
57
|
+
<Header id="main-header" />
|
|
58
|
+
<button className="btn btn-primary">Click me</button>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const selectors = purgeFromJsx()(jsx);
|
|
65
|
+
|
|
66
|
+
console.log(selectors);
|
|
67
|
+
// ["div", "container", "Header", "main-header", "button", "btn", "btn-primary"]
|
|
11
68
|
```
|
|
69
|
+
|
|
70
|
+
## API
|
|
71
|
+
|
|
72
|
+
### `purgeFromJsx(options?: acorn.Options): (content: string) => string[]`
|
|
73
|
+
|
|
74
|
+
Creates an extractor function for JSX content.
|
|
75
|
+
|
|
76
|
+
#### Parameters
|
|
77
|
+
|
|
78
|
+
- `options` (optional) - [Acorn parser options](https://github.com/acornjs/acorn#interface). Defaults to `{ ecmaVersion: "latest" }`
|
|
79
|
+
|
|
80
|
+
#### Returns
|
|
81
|
+
|
|
82
|
+
A function that takes JSX content as a string and returns an array of selectors.
|
|
83
|
+
|
|
84
|
+
## What It Extracts
|
|
85
|
+
|
|
86
|
+
The extractor parses JSX and extracts:
|
|
87
|
+
|
|
88
|
+
1. **Component names** - JSX element names (e.g., `div`, `Header`, `MyComponent`), including elements inside the value of another attribute (e.g., `icon={<Icon />}`)
|
|
89
|
+
2. **className values** - String values from `className` attributes, split by whitespace
|
|
90
|
+
3. **id values** - String values from `id` attributes
|
|
91
|
+
|
|
92
|
+
When `className` or `id` is an expression, every string in it is extracted, as well as the static parts of template literals and the keys of object literals. For example, `className={active ? "on" : "off"}` gives `on` and `off`, ``className={`card ${size}`}`` gives `card`, and `className={clsx({ active: isActive })}` gives `active`.
|
|
93
|
+
|
|
94
|
+
### Example
|
|
95
|
+
|
|
96
|
+
```jsx
|
|
97
|
+
<Card className="card card-primary" id="featured-card">
|
|
98
|
+
<CardHeader />
|
|
99
|
+
<CardBody className="p-4" />
|
|
100
|
+
</Card>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Extracted selectors: `["Card", "card", "card-primary", "featured-card", "CardHeader", "CardBody", "p-4"]`
|
|
104
|
+
|
|
105
|
+
## Limitations
|
|
106
|
+
|
|
107
|
+
- Only extracts static string values from `className` and `id`
|
|
108
|
+
- Values computed at runtime, like `className={styles.container}` or the `size` of ``className={`btn-${size}`}``, are not extracted
|
|
109
|
+
- For dynamic class names, consider using PurgeCSS's [safelist](https://purgecss.com/safelisting.html) option
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -31,7 +31,7 @@ function purgeFromJsx(options) {
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
JSXAttribute(acornNode, state, callback) {
|
|
34
|
-
var _a;
|
|
34
|
+
var _a, _b;
|
|
35
35
|
const node = acornNode;
|
|
36
36
|
if (!node.value) {
|
|
37
37
|
return;
|
|
@@ -43,15 +43,19 @@ function purgeFromJsx(options) {
|
|
|
43
43
|
case "id":
|
|
44
44
|
case "className":
|
|
45
45
|
{
|
|
46
|
-
// Get
|
|
47
|
-
|
|
46
|
+
// Get all the strings in node.value, e.g. both strings of
|
|
47
|
+
// className={active ? "on" : "off"}
|
|
48
|
+
const valueState = { strings: [] };
|
|
48
49
|
callback(node.value, valueState);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
(_a = state.selectors) === null || _a === void 0 ? void 0 : _a.push(...valueState.text.split(" "));
|
|
50
|
+
for (const text of (_a = valueState.strings) !== null && _a !== void 0 ? _a : []) {
|
|
51
|
+
(_b = state.selectors) === null || _b === void 0 ? void 0 : _b.push(...text.split(/\s+/).filter(Boolean));
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
break;
|
|
55
|
+
default:
|
|
56
|
+
// the value can contain JSX elements, e.g. icon={<Icon />}
|
|
57
|
+
callback(node.value, { selectors: state.selectors });
|
|
58
|
+
break;
|
|
55
59
|
}
|
|
56
60
|
},
|
|
57
61
|
JSXIdentifier(acornNode, state) {
|
|
@@ -62,12 +66,36 @@ function purgeFromJsx(options) {
|
|
|
62
66
|
const node = acornNode;
|
|
63
67
|
state.text = node.namespace.name + ":" + node.name.name;
|
|
64
68
|
},
|
|
65
|
-
// Only
|
|
69
|
+
// Only static strings are collected, not values computed at runtime
|
|
66
70
|
Literal(acornNode, state) {
|
|
71
|
+
var _a;
|
|
67
72
|
const node = acornNode;
|
|
68
73
|
if (typeof node.value === "string") {
|
|
69
|
-
state.
|
|
74
|
+
(_a = state.strings) === null || _a === void 0 ? void 0 : _a.push(node.value);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
// static parts of a template literal, e.g. `btn btn-${size}`
|
|
78
|
+
TemplateElement(acornNode, state) {
|
|
79
|
+
var _a;
|
|
80
|
+
const node = acornNode;
|
|
81
|
+
if (node.value.cooked) {
|
|
82
|
+
(_a = state.strings) === null || _a === void 0 ? void 0 : _a.push(node.value.cooked);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
// keys of the objects in a className or id expression, e.g.
|
|
86
|
+
// clsx({ active: isActive, "is-open": isOpen })
|
|
87
|
+
Property(acornNode, state, callback) {
|
|
88
|
+
const node = acornNode;
|
|
89
|
+
if (state.strings &&
|
|
90
|
+
!node.computed &&
|
|
91
|
+
node.key.type === "Identifier") {
|
|
92
|
+
state.strings.push(node.key.name);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// a string key is a Literal, a computed key any expression
|
|
96
|
+
callback(node.key, state);
|
|
70
97
|
}
|
|
98
|
+
callback(node.value, state);
|
|
71
99
|
},
|
|
72
100
|
}, { ...walk.base });
|
|
73
101
|
return state.selectors || [];
|
package/lib/purgecss-from-jsx.js
CHANGED
|
@@ -53,7 +53,7 @@ function purgeFromJsx(options) {
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
JSXAttribute(acornNode, state, callback) {
|
|
56
|
-
var _a;
|
|
56
|
+
var _a, _b;
|
|
57
57
|
const node = acornNode;
|
|
58
58
|
if (!node.value) {
|
|
59
59
|
return;
|
|
@@ -65,15 +65,19 @@ function purgeFromJsx(options) {
|
|
|
65
65
|
case "id":
|
|
66
66
|
case "className":
|
|
67
67
|
{
|
|
68
|
-
// Get
|
|
69
|
-
|
|
68
|
+
// Get all the strings in node.value, e.g. both strings of
|
|
69
|
+
// className={active ? "on" : "off"}
|
|
70
|
+
const valueState = { strings: [] };
|
|
70
71
|
callback(node.value, valueState);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
(_a = state.selectors) === null || _a === void 0 ? void 0 : _a.push(...valueState.text.split(" "));
|
|
72
|
+
for (const text of (_a = valueState.strings) !== null && _a !== void 0 ? _a : []) {
|
|
73
|
+
(_b = state.selectors) === null || _b === void 0 ? void 0 : _b.push(...text.split(/\s+/).filter(Boolean));
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
break;
|
|
77
|
+
default:
|
|
78
|
+
// the value can contain JSX elements, e.g. icon={<Icon />}
|
|
79
|
+
callback(node.value, { selectors: state.selectors });
|
|
80
|
+
break;
|
|
77
81
|
}
|
|
78
82
|
},
|
|
79
83
|
JSXIdentifier(acornNode, state) {
|
|
@@ -84,12 +88,36 @@ function purgeFromJsx(options) {
|
|
|
84
88
|
const node = acornNode;
|
|
85
89
|
state.text = node.namespace.name + ":" + node.name.name;
|
|
86
90
|
},
|
|
87
|
-
// Only
|
|
91
|
+
// Only static strings are collected, not values computed at runtime
|
|
88
92
|
Literal(acornNode, state) {
|
|
93
|
+
var _a;
|
|
89
94
|
const node = acornNode;
|
|
90
95
|
if (typeof node.value === "string") {
|
|
91
|
-
state.
|
|
96
|
+
(_a = state.strings) === null || _a === void 0 ? void 0 : _a.push(node.value);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
// static parts of a template literal, e.g. `btn btn-${size}`
|
|
100
|
+
TemplateElement(acornNode, state) {
|
|
101
|
+
var _a;
|
|
102
|
+
const node = acornNode;
|
|
103
|
+
if (node.value.cooked) {
|
|
104
|
+
(_a = state.strings) === null || _a === void 0 ? void 0 : _a.push(node.value.cooked);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
// keys of the objects in a className or id expression, e.g.
|
|
108
|
+
// clsx({ active: isActive, "is-open": isOpen })
|
|
109
|
+
Property(acornNode, state, callback) {
|
|
110
|
+
const node = acornNode;
|
|
111
|
+
if (state.strings &&
|
|
112
|
+
!node.computed &&
|
|
113
|
+
node.key.type === "Identifier") {
|
|
114
|
+
state.strings.push(node.key.name);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// a string key is a Literal, a computed key any expression
|
|
118
|
+
callback(node.key, state);
|
|
92
119
|
}
|
|
120
|
+
callback(node.value, state);
|
|
93
121
|
},
|
|
94
122
|
}, { ...walk__namespace.base });
|
|
95
123
|
return state.selectors || [];
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullhuman/purgecss-from-jsx",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "JSX extractor for PurgeCSS",
|
|
5
5
|
"author": "Ffloriel",
|
|
6
6
|
"homepage": "https://github.com/FullHuman/purgecss#readme",
|
|
7
7
|
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.12.0"
|
|
10
|
+
},
|
|
8
11
|
"main": "lib/purgecss-from-jsx.js",
|
|
9
12
|
"module": "lib/purgecss-from-jsx.esm.js",
|
|
10
13
|
"types": "lib/purgecss-from-jsx.d.ts",
|
|
@@ -19,22 +22,21 @@
|
|
|
19
22
|
"type": "git",
|
|
20
23
|
"url": "git+https://github.com/FullHuman/purgecss.git"
|
|
21
24
|
},
|
|
22
|
-
"scripts": {
|
|
23
|
-
"build": "ts-node build.ts",
|
|
24
|
-
"test": "jest"
|
|
25
|
-
},
|
|
26
25
|
"bugs": {
|
|
27
26
|
"url": "https://github.com/FullHuman/purgecss/issues"
|
|
28
27
|
},
|
|
29
28
|
"dependencies": {
|
|
30
|
-
"acorn": "^8.
|
|
31
|
-
"acorn-jsx": "^5.3.
|
|
29
|
+
"acorn": "^8.19.0",
|
|
30
|
+
"acorn-jsx": "^5.3.2",
|
|
32
31
|
"acorn-jsx-walk": "^2.0.0",
|
|
33
|
-
"acorn-walk": "^8.
|
|
32
|
+
"acorn-walk": "^8.3.5"
|
|
34
33
|
},
|
|
35
34
|
"publishConfig": {
|
|
36
35
|
"access": "public",
|
|
37
36
|
"registry": "https://registry.npmjs.org/"
|
|
38
37
|
},
|
|
39
|
-
"
|
|
40
|
-
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "ts-node build.ts",
|
|
40
|
+
"test": "jest"
|
|
41
|
+
}
|
|
42
|
+
}
|