@fullhuman/purgecss-from-jsx 8.0.0 → 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 CHANGED
@@ -15,7 +15,7 @@ npm install @fullhuman/purgecss-from-jsx --save-dev
15
15
  ### With PurgeCSS
16
16
 
17
17
  ```js
18
- import PurgeCSS from "purgecss";
18
+ import { PurgeCSS } from "purgecss";
19
19
  import purgeFromJsx from "@fullhuman/purgecss-from-jsx";
20
20
 
21
21
  const result = await new PurgeCSS().purge({
@@ -85,10 +85,12 @@ A function that takes JSX content as a string and returns an array of selectors.
85
85
 
86
86
  The extractor parses JSX and extracts:
87
87
 
88
- 1. **Component names** - JSX element names (e.g., `div`, `Header`, `MyComponent`)
89
- 2. **className values** - String values from `className` attributes, split by spaces
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
90
  3. **id values** - String values from `id` attributes
91
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
+
92
94
  ### Example
93
95
 
94
96
  ```jsx
@@ -103,7 +105,7 @@ Extracted selectors: `["Card", "card", "card-primary", "featured-card", "CardHea
103
105
  ## Limitations
104
106
 
105
107
  - Only extracts static string values from `className` and `id`
106
- - Dynamic expressions like `className={styles.container}` or template literals are not fully supported
108
+ - Values computed at runtime, like `className={styles.container}` or the `size` of ``className={`btn-${size}`}``, are not extracted
107
109
  - For dynamic class names, consider using PurgeCSS's [safelist](https://purgecss.com/safelisting.html) option
108
110
 
109
111
  ## License
@@ -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 text in node.value
47
- const valueState = {};
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
- // node.value is not empty
50
- if (valueState.text) {
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 handle Literal for now, not JSXExpressionContainer | JSXElement
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.text = node.value;
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 || [];
@@ -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 text in node.value
69
- const valueState = {};
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
- // node.value is not empty
72
- if (valueState.text) {
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 handle Literal for now, not JSXExpressionContainer | JSXElement
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.text = node.value;
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": "8.0.0",
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.7.0",
31
- "acorn-jsx": "^5.3.1",
29
+ "acorn": "^8.19.0",
30
+ "acorn-jsx": "^5.3.2",
32
31
  "acorn-jsx-walk": "^2.0.0",
33
- "acorn-walk": "^8.2.0"
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
- "gitHead": "859266ccb4ea80e0911fa32f1e215397978b3c89"
40
- }
38
+ "scripts": {
39
+ "build": "ts-node build.ts",
40
+ "test": "jest"
41
+ }
42
+ }