@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 CHANGED
@@ -1,11 +1,113 @@
1
- # `purgecss-from-jsx`
1
+ # @fullhuman/purgecss-from-jsx
2
2
 
3
- > TODO: description
3
+ [![npm version](https://img.shields.io/npm/v/@fullhuman/purgecss-from-jsx.svg)](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
- // TODO: DEMONSTRATE API
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 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": "7.0.2",
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": "e95398d961e03a809b5e3da75e3dae1813b46b68"
40
- }
38
+ "scripts": {
39
+ "build": "ts-node build.ts",
40
+ "test": "jest"
41
+ }
42
+ }