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;