diff --git a/package.json b/package.json
index 30cc8e9b..7984dcf5 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"postcss-purgecss",
"purgecss",
"purgecss-from-html",
+ "purgecss-from-jsx",
"purgecss-from-pug",
"purgecss-from-twig",
"purgecss-webpack-plugin",
diff --git a/packages/purgecss-from-jsx/README.md b/packages/purgecss-from-jsx/README.md
new file mode 100644
index 00000000..b7657470
--- /dev/null
+++ b/packages/purgecss-from-jsx/README.md
@@ -0,0 +1,11 @@
+# `purgecss-from-jsx`
+
+> TODO: description
+
+## Usage
+
+```
+const purgecssFromJsx = require('purgecss-from-jsx');
+
+// TODO: DEMONSTRATE API
+```
diff --git a/packages/purgecss-from-jsx/__tests__/data.ts b/packages/purgecss-from-jsx/__tests__/data.ts
new file mode 100644
index 00000000..55655490
--- /dev/null
+++ b/packages/purgecss-from-jsx/__tests__/data.ts
@@ -0,0 +1,24 @@
+export const TEST_1_CONTENT = `
+import React from "react";
+
+class MyComponent extends React.Component {
+ render() {
+ return (
+
+ Well
+
+
+
+
+ );
+ }
+}
+
+export default MyComponent;
+`;
+
+export const TEST_1_TAG = ["div", "a", "input"];
+
+export const TEST_1_CLASS = ["test-container", "test-footer", "a-link"];
+
+export const TEST_1_ID = ["a-link", "blo"];
diff --git a/packages/purgecss-from-jsx/__tests__/index.test.ts b/packages/purgecss-from-jsx/__tests__/index.test.ts
new file mode 100644
index 00000000..e8bbd06e
--- /dev/null
+++ b/packages/purgecss-from-jsx/__tests__/index.test.ts
@@ -0,0 +1,38 @@
+import purgeJsx from "../src/index";
+
+import { TEST_1_CONTENT, TEST_1_TAG, TEST_1_CLASS, TEST_1_ID } from "./data";
+
+const plugin = purgeJsx({sourceType: "module"});
+
+describe("purgePug", () => {
+ describe("from a normal html document", () => {
+ it("finds tag selectors", () => {
+ const received = plugin(TEST_1_CONTENT);
+ for (const item of TEST_1_TAG) {
+ expect(received.includes(item)).toBe(true);
+ }
+ });
+
+ it("finds classes selectors", () => {
+ const received = plugin(TEST_1_CONTENT);
+ for (const item of TEST_1_CLASS) {
+ expect(received.includes(item)).toBe(true);
+ }
+ });
+
+ it("finds id selectors", () => {
+ const received = plugin(TEST_1_CONTENT);
+ for (const item of TEST_1_ID) {
+ expect(received.includes(item)).toBe(true);
+ }
+ });
+
+ it("finds all selectors", () => {
+ const received = plugin(TEST_1_CONTENT);
+ const selectors = [...TEST_1_TAG, ...TEST_1_CLASS, ...TEST_1_ID];
+ for (const item of selectors) {
+ expect(received.includes(item)).toBe(true);
+ }
+ });
+ });
+});
diff --git a/packages/purgecss-from-jsx/package-lock.json b/packages/purgecss-from-jsx/package-lock.json
new file mode 100644
index 00000000..69b19820
--- /dev/null
+++ b/packages/purgecss-from-jsx/package-lock.json
@@ -0,0 +1,28 @@
+{
+ "name": "purgecss-from-jsx",
+ "version": "4.0.3",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
+ },
+ "acorn-jsx": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
+ "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng=="
+ },
+ "acorn-jsx-walk": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz",
+ "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA=="
+ },
+ "acorn-walk": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.1.tgz",
+ "integrity": "sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w=="
+ }
+ }
+}
diff --git a/packages/purgecss-from-jsx/package.json b/packages/purgecss-from-jsx/package.json
new file mode 100644
index 00000000..a95e620a
--- /dev/null
+++ b/packages/purgecss-from-jsx/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "purgecss-from-jsx",
+ "version": "4.0.3",
+ "description": "JSX extractor for PurgeCSS",
+ "author": "Ffloriel",
+ "homepage": "https://github.com/FullHuman/purgecss#readme",
+ "license": "ISC",
+ "main": "lib/purgecss-from-jsx.js",
+ "directories": {
+ "lib": "lib",
+ "test": "__tests__"
+ },
+ "files": [
+ "lib"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/FullHuman/purgecss.git"
+ },
+ "scripts": {
+ "test": "echo \"Error: run tests from root\" && exit 1"
+ },
+ "bugs": {
+ "url": "https://github.com/FullHuman/purgecss/issues"
+ },
+ "dependencies": {
+ "acorn": "^7.4.0",
+ "acorn-jsx": "^5.3.1",
+ "acorn-jsx-walk": "^2.0.0",
+ "acorn-walk": "^8.1.1"
+ }
+}
diff --git a/packages/purgecss-from-jsx/src/index.ts b/packages/purgecss-from-jsx/src/index.ts
new file mode 100644
index 00000000..c89f680a
--- /dev/null
+++ b/packages/purgecss-from-jsx/src/index.ts
@@ -0,0 +1,79 @@
+import * as acorn from "acorn";
+import * as walk from "acorn-walk";
+import jsx from "acorn-jsx";
+import {extend} from "acorn-jsx-walk";
+
+extend(walk.base);
+
+function purgeFromJsx(options: acorn.Options) {
+ return (content: string): string[] => {
+ // Will be filled during walk
+ const state = {selectors: []};
+
+ // Parse and walk any JSXElement
+ walk.recursive(
+ acorn.Parser.extend(jsx()).parse(content, options),
+ state,
+ {
+ JSXOpeningElement(node: any, state: any, callback) {
+ // JSXIdentifier | JSXMemberExpression | JSXNamespacedName
+ const nameState: any = {};
+ callback(node.name, nameState);
+ if (nameState.text) {
+ state.selectors.push(nameState.text);
+ }
+
+ for (let i = 0; i < node.attributes.length; ++i) {
+ callback(node.attributes[i], state);
+ }
+ },
+ JSXAttribute(node: any, state: any, callback) {
+ // Literal | JSXExpressionContainer | JSXElement | nil
+ if (!node.value) {
+ return;
+ }
+
+ // JSXIdentifier | JSXNamespacedName
+ const nameState: any = {};
+ callback(node.name, nameState);
+
+ // node.name is id or className
+ switch (nameState.text) {
+ case "id":
+ case "className":
+ {
+ // Get text in node.value
+ const valueState: any = {};
+ callback(node.value, valueState);
+
+ // node.value is not empty
+ if (valueState.text) {
+ state.selectors.push(...valueState.text.split(" "));
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ },
+ JSXIdentifier(node: any, state: any) {
+ state.text = node.name;
+ },
+ JSXNamespacedName(node: any, state: any) {
+ state.text = node.namespace.name + ":" + node.name.name;
+ },
+ // Only handle Literal for now, not JSXExpressionContainer | JSXElement
+ Literal(node: any, state: any) {
+ if (typeof node.value === "string") {
+ state.text = node.value;
+ }
+ }
+ },
+ {...walk.base}
+ );
+
+ return state.selectors;
+ };
+}
+
+export default purgeFromJsx;
diff --git a/scripts/build.ts b/scripts/build.ts
index e6ec5f86..4c181ef2 100644
--- a/scripts/build.ts
+++ b/scripts/build.ts
@@ -41,6 +41,10 @@ const packages = [
name: "purgecss-from-pug",
external: ["pug-lexer"],
},
+ {
+ name: "purgecss-from-jsx",
+ external: ["acorn", "acorn-walk", "acorn-jsx", "acorn-jsx-walk"],
+ }
];
async function build(): Promise {
diff --git a/tsconfig.json b/tsconfig.json
index be69e7cd..785d2fa2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,7 +21,8 @@
"*" : ["types/*"],
"purgecss": ["packages/purgecss/src"],
"@fullhuman/purgecss-from-html": ["packages/purgecss-from-html/src"],
- "@fullhuman/purgecss-from-pug": ["packages/purgecss-from-pug/src"]
+ "@fullhuman/purgecss-from-pug": ["packages/purgecss-from-pug/src"],
+ "@fullhuman/purgecss-from-jsx": ["packages/purgecss-from-jsx/src"]
}
},
"include": [
diff --git a/types/acorn-jsx-walk.d.ts b/types/acorn-jsx-walk.d.ts
new file mode 100644
index 00000000..f52e00ea
--- /dev/null
+++ b/types/acorn-jsx-walk.d.ts
@@ -0,0 +1 @@
+export function extend(base: any): void;
\ No newline at end of file
diff --git a/types/acorn-jsx.d.ts b/types/acorn-jsx.d.ts
new file mode 100644
index 00000000..fe9e9cde
--- /dev/null
+++ b/types/acorn-jsx.d.ts
@@ -0,0 +1,9 @@
+import acorn from "acorn";
+declare function jsx(options?: jsx.Options): (BaseParser: typeof acorn.Parser) => typeof acorn.Parser;
+export declare namespace jsx {
+ interface Options {
+ allowNamespaces?: boolean;
+ allowNamespacedObjects?: boolean;
+ }
+}
+export default jsx;