Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new purgecss-from-jsx plugin #692

Merged
merged 3 commits into from Jul 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;