Skip to content

Commit

Permalink
feat: new purgecss-from-jsx plugin (#692)
Browse files Browse the repository at this point in the history
* added acorn acorn-walk acorn-jsx acorn-jsx-walk dependencies, add a new purgecss plugin for extracting id, class and tag from JSX content

* replace object by any in .d.ts because it doesn't work with build machine

* update acorn-walk, remove acorn-walk.d.ts (not needed anymore)
  • Loading branch information
Nauja committed Jul 6, 2021
1 parent 45e0533 commit 3570c7d
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -6,6 +6,7 @@
"postcss-purgecss",
"purgecss",
"purgecss-from-html",
"purgecss-from-jsx",
"purgecss-from-pug",
"purgecss-from-twig",
"purgecss-webpack-plugin",
Expand Down
11 changes: 11 additions & 0 deletions 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
```
24 changes: 24 additions & 0 deletions 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 (
<React.Fragment>
<div className="test-container">Well</div>
<div className="test-footer" id="an-id"></div>
<a href="#" id="a-link" className="a-link"></a>
<input id="blo" type="text" disabled/>
</React.Fragment>
);
}
}
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"];
38 changes: 38 additions & 0 deletions 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);
}
});
});
});
28 changes: 28 additions & 0 deletions packages/purgecss-from-jsx/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions 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"
}
}
79 changes: 79 additions & 0 deletions 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;
4 changes: 4 additions & 0 deletions scripts/build.ts
Expand Up @@ -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<void> {
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Expand Up @@ -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": [
Expand Down
1 change: 1 addition & 0 deletions types/acorn-jsx-walk.d.ts
@@ -0,0 +1 @@
export function extend(base: any): void;
9 changes: 9 additions & 0 deletions 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;

0 comments on commit 3570c7d

Please sign in to comment.