diff --git a/package.json b/package.json
index ad48a80e..ecc01636 100644
--- a/package.json
+++ b/package.json
@@ -6,12 +6,33 @@
"@types/node": "^12.7.11",
"jest": "^24.9.0",
"lerna": "^3.14.1",
+ "lint-staged": "^9.4.2",
"prettier": "^1.18.2",
+ "rollup": "^1.23.1",
+ "rollup-plugin-node-resolve": "^5.2.0",
+ "rollup-plugin-terser": "^5.1.2",
+ "rollup-plugin-typescript2": "^0.24.3",
"ts-jest": "^24.1.0",
+ "ts-node": "^8.4.1",
"typescript": "^3.6.3"
},
"scripts": {
+ "build": "ts-node scripts/build.ts",
"prettier": "prettier --write --parser typescript 'packages/**/*.ts'",
"test": "jest"
+ },
+ "gitHooks": {
+ "pre-commit": "lint-staged",
+ "commit-msg": "node scripts/verify-commit.js"
+ },
+ "lint-staged": {
+ "*.js": [
+ "prettier --write",
+ "git add"
+ ],
+ "*.ts": [
+ "npm run prettier",
+ "git add"
+ ]
}
}
diff --git a/packages/postcss-purgecss/README.md b/packages/postcss-purgecss/README.md
new file mode 100644
index 00000000..2022d09c
--- /dev/null
+++ b/packages/postcss-purgecss/README.md
@@ -0,0 +1,11 @@
+# `postcss-purgecss`
+
+> TODO: description
+
+## Usage
+
+```
+const postcssPurgecss = require('postcss-purgecss');
+
+// TODO: DEMONSTRATE API
+```
diff --git a/packages/postcss-purgecss/__tests__/fixtures/expected/font-keyframes.css b/packages/postcss-purgecss/__tests__/fixtures/expected/font-keyframes.css
new file mode 100644
index 00000000..21f4b319
--- /dev/null
+++ b/packages/postcss-purgecss/__tests__/fixtures/expected/font-keyframes.css
@@ -0,0 +1,62 @@
+@font-face {
+ font-family: 'Cerebri Sans';
+ font-weight: 400;
+ font-style: normal;
+ src: url('../fonts/CerebriSans-Regular.eot?') format('eot'), url('../fonts/CerebriSans-Regular.otf') format('opentype'), url('../fonts/CerebriSans-Regular.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Regular.ttf') format('truetype'), url('../fonts/CerebriSans-Regular.woff') format('woff');
+}
+
+@font-face {
+ font-family: 'Cerebri Bold';
+ font-weight: 400;
+ font-style: normal;
+ src: url('../fonts/CerebriSans-Bold.eot?') format('eot'), url('../fonts/CerebriSans-Bold.otf') format('opentype'), url('../fonts/CerebriSans-Bold.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Bold.ttf') format('truetype'), url('../fonts/CerebriSans-Bold.woff') format('woff');
+}
+
+.used {
+ color: red;
+ font-family: 'Cerebri Sans';
+}
+
+.used2 {
+color: blue;
+font-family: Cerebri Bold, serif;
+}
+
+
+@keyframes bounce {
+ from, 20%, 53%, 80%, to {
+ animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 1.000);
+ transform: translate3d(1, 1, 0);
+ }
+}
+
+.bounce {
+ -webkit-animation-name: bounce;
+ animation-name: bounce;
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+}
+
+@keyframes scale {
+ from {
+ transform: scale(1);
+ }
+
+ to {
+ transform: scale(2);
+ }
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.scale-spin {
+ animation: spin 300ms linear infinite forwards,scale 300ms linear infinite alternate;
+}
diff --git a/packages/postcss-purgecss/__tests__/fixtures/expected/simple.css b/packages/postcss-purgecss/__tests__/fixtures/expected/simple.css
new file mode 100644
index 00000000..d0212ee2
--- /dev/null
+++ b/packages/postcss-purgecss/__tests__/fixtures/expected/simple.css
@@ -0,0 +1,3 @@
+.used-class {
+ color: black;
+}
diff --git a/packages/postcss-purgecss/__tests__/fixtures/src/font-keyframes/font-keyframes.css b/packages/postcss-purgecss/__tests__/fixtures/src/font-keyframes/font-keyframes.css
new file mode 100644
index 00000000..fa28b67b
--- /dev/null
+++ b/packages/postcss-purgecss/__tests__/fixtures/src/font-keyframes/font-keyframes.css
@@ -0,0 +1,87 @@
+@font-face {
+ font-family: 'Cerebri Sans';
+ font-weight: 400;
+ font-style: normal;
+ src: url('../fonts/CerebriSans-Regular.eot?') format('eot'), url('../fonts/CerebriSans-Regular.otf') format('opentype'), url('../fonts/CerebriSans-Regular.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Regular.ttf') format('truetype'), url('../fonts/CerebriSans-Regular.woff') format('woff');
+}
+
+@font-face {
+ font-family: 'Cerebri Bold';
+ font-weight: 400;
+ font-style: normal;
+ src: url('../fonts/CerebriSans-Bold.eot?') format('eot'), url('../fonts/CerebriSans-Bold.otf') format('opentype'), url('../fonts/CerebriSans-Bold.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Bold.ttf') format('truetype'), url('../fonts/CerebriSans-Bold.woff') format('woff');
+}
+
+@font-face {
+ font-family: 'OtherFont';
+ font-weight: 400;
+ font-style: normal;
+ src: url('xxx')
+}
+
+.unused {
+ color: black;
+}
+
+.used {
+ color: red;
+ font-family: 'Cerebri Sans';
+}
+
+.used2 {
+color: blue;
+font-family: Cerebri Bold, serif;
+}
+
+
+@keyframes bounce {
+ from, 20%, 53%, 80%, to {
+ animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 1.000);
+ transform: translate3d(1, 1, 0);
+ }
+}
+
+.bounce {
+ -webkit-animation-name: bounce;
+ animation-name: bounce;
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+}
+
+@keyframes flash {
+ from, 50%, to {
+ opacity: 1;
+ }
+
+ 25%, 75% {
+ opacity: 0.5;
+ }
+}
+
+.flash {
+ animation: flash
+}
+
+@keyframes scale {
+ from {
+ transform: scale(1);
+ }
+
+ to {
+ transform: scale(2);
+ }
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.scale-spin {
+ animation: spin 300ms linear infinite forwards,scale 300ms linear infinite alternate;
+}
diff --git a/packages/postcss-purgecss/__tests__/fixtures/src/font-keyframes/font-keyframes.html b/packages/postcss-purgecss/__tests__/fixtures/src/font-keyframes/font-keyframes.html
new file mode 100644
index 00000000..87ab68b0
--- /dev/null
+++ b/packages/postcss-purgecss/__tests__/fixtures/src/font-keyframes/font-keyframes.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/postcss-purgecss/__tests__/fixtures/src/simple/simple.css b/packages/postcss-purgecss/__tests__/fixtures/src/simple/simple.css
new file mode 100644
index 00000000..a213a749
--- /dev/null
+++ b/packages/postcss-purgecss/__tests__/fixtures/src/simple/simple.css
@@ -0,0 +1,11 @@
+.used-class {
+ color: black;
+}
+
+.unused-class {
+ color: black;
+}
+
+.another-one-not-found {
+ color: black;
+}
diff --git a/packages/postcss-purgecss/__tests__/fixtures/src/simple/simple.html b/packages/postcss-purgecss/__tests__/fixtures/src/simple/simple.html
new file mode 100644
index 00000000..56954e7a
--- /dev/null
+++ b/packages/postcss-purgecss/__tests__/fixtures/src/simple/simple.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/postcss-purgecss/__tests__/index.test.ts b/packages/postcss-purgecss/__tests__/index.test.ts
new file mode 100644
index 00000000..7232a1b7
--- /dev/null
+++ b/packages/postcss-purgecss/__tests__/index.test.ts
@@ -0,0 +1,58 @@
+const fs = require("fs");
+const postcss = require("postcss");
+
+import purgeCSSPlugin from "./../src/";
+
+describe("Purgecss postcss plugin", () => {
+ const files = ["simple", "font-keyframes"];
+
+ for (const file of files) {
+ it(`remove unused css successfully: ${file}`, done => {
+ const input = fs
+ .readFileSync(`${__dirname}/fixtures/src/${file}/${file}.css`)
+ .toString();
+ const expected = fs
+ .readFileSync(`${__dirname}/fixtures/expected/${file}.css`)
+ .toString();
+ postcss([
+ purgeCSSPlugin({
+ content: [`${__dirname}/fixtures/src/${file}/${file}.html`],
+ fontFace: true,
+ keyframes: true
+ })
+ ])
+ .process(input, { from: undefined })
+ .then((result: any) => {
+ expect(result.css).toBe(expected);
+ expect(result.warnings().length).toBe(0);
+ done();
+ });
+ });
+ }
+
+ for (const file of ["simple"]) {
+ it(`queues messages when using reject flag: ${file}`, done => {
+ const input = fs
+ .readFileSync(`${__dirname}/fixtures/src/${file}/${file}.css`)
+ .toString();
+ const expected = fs
+ .readFileSync(`${__dirname}/fixtures/expected/${file}.css`)
+ .toString();
+ postcss([
+ purgeCSSPlugin({
+ content: [`${__dirname}/fixtures/src/${file}/${file}.html`],
+ rejected: true
+ })
+ ])
+ .process(input, { from: undefined })
+ .then((result: any) => {
+ expect(result.css).toBe(expected);
+ expect(result.warnings().length).toBe(0);
+ expect(result.messages.length).toBeGreaterThan(0);
+ expect(result.messages[0].text).toMatch(/unused-class/);
+ expect(result.messages[0].text).toMatch(/another-one-not-found/);
+ done();
+ });
+ });
+ }
+});
diff --git a/packages/postcss-purgecss/package.json b/packages/postcss-purgecss/package.json
new file mode 100644
index 00000000..1b7353dd
--- /dev/null
+++ b/packages/postcss-purgecss/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "postcss-purgecss",
+ "version": "2.0.0",
+ "description": "> TODO: description",
+ "author": "Ffloriel ",
+ "homepage": "https://github.com/FullHuman/purgecss#readme",
+ "license": "ISC",
+ "main": "lib/postcss-purgecss.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": {
+ "postcss": "^7.0.18",
+ "purgecss": "^2.0.0"
+ }
+}
diff --git a/packages/postcss-purgecss/src/index.ts b/packages/postcss-purgecss/src/index.ts
new file mode 100644
index 00000000..92ca6aed
--- /dev/null
+++ b/packages/postcss-purgecss/src/index.ts
@@ -0,0 +1,67 @@
+import postcss from "postcss";
+import {
+ walkThroughCSS,
+ extractSelectorsFromFiles,
+ extractSelectorsFromString,
+ setPurgeCSSOptions,
+ removeUnusedFontFaces,
+ removeUnusedKeyframes,
+ selectorsRemoved
+} from "purgecss/src/index";
+import { RawContent, UserDefinedOptions } from "purgecss/src/types";
+import { defaultOptions } from "purgecss/src/options";
+
+type PurgeCSSPostCSSOptions = Omit;
+
+const purgeCSSPlugin = postcss.plugin("postcss-plugin-purgecss", function(
+ opts: PurgeCSSPostCSSOptions
+) {
+ return async function(root, result) {
+ const options = {
+ ...defaultOptions,
+ ...opts
+ };
+
+ setPurgeCSSOptions(options);
+
+ const { content, extractors } = options;
+
+ const fileFormatContents = content.filter(
+ o => typeof o === "string"
+ ) as string[];
+ const rawFormatContents = content.filter(
+ o => typeof o === "object"
+ ) as RawContent[];
+
+ const cssFileSelectors = await extractSelectorsFromFiles(
+ fileFormatContents,
+ extractors
+ );
+ const cssRawSelectors = extractSelectorsFromString(
+ rawFormatContents,
+ extractors
+ );
+
+ const selectors = new Set([...cssFileSelectors, ...cssRawSelectors]);
+
+ //purge unused selectors
+ walkThroughCSS(root, selectors);
+
+ if (options.fontFace) removeUnusedFontFaces();
+ if (options.keyframes) removeUnusedKeyframes();
+
+ if (options.rejected && selectorsRemoved.size > 0) {
+ result.messages.push({
+ type: "purgecss",
+ plugin: "postcss-purgecss",
+ text: `purging ${selectorsRemoved.size} selectors:
+ ${Array.from(selectorsRemoved)
+ .map(selector => selector.trim())
+ .join("\n ")}`
+ });
+ selectorsRemoved.clear();
+ }
+ };
+});
+
+export default purgeCSSPlugin;
diff --git a/packages/purgecss/__tests__/index.test.ts b/packages/purgecss/__tests__/index.test.ts
index 7d62286d..b7dd1092 100644
--- a/packages/purgecss/__tests__/index.test.ts
+++ b/packages/purgecss/__tests__/index.test.ts
@@ -2,19 +2,20 @@ import purgeCSS from "./../src/index";
const root = "./packages/purgecss/__tests__/test_examples/";
-describe('purgecss with config file', () => {
-
- it('initialize without error with a config file specified', () => {
+describe("purgecss with config file", () => {
+ it("initialize without error with a config file specified", () => {
expect(async () => {
- await purgeCSS('./packages/purgecss/__tests__/purgecss.config.js')
- }).not.toThrow()
- })
+ await purgeCSS("./packages/purgecss/__tests__/purgecss.config.js");
+ }).not.toThrow();
+ });
- it('throws an error if config file is not found', async() => {
- expect.assertions(1)
- await expect(purgeCSS('./packages/purgecss/__tests__/purgecss_wrong_path.config.js')).rejects.toThrow()
- })
-})
+ it("throws an error if config file is not found", async () => {
+ expect.assertions(1);
+ await expect(
+ purgeCSS("./packages/purgecss/__tests__/purgecss_wrong_path.config.js")
+ ).rejects.toThrow();
+ });
+});
describe("filters out unused selectors", () => {
let purgedCSS: string;
diff --git a/packages/purgecss/lib/purgecss.es.js b/packages/purgecss/lib/purgecss.es.js
new file mode 100644
index 00000000..7867efa7
--- /dev/null
+++ b/packages/purgecss/lib/purgecss.es.js
@@ -0,0 +1 @@
+import{parse as e}from"postcss";import t from"postcss-selector-parser";import{promises as n,constants as r}from"fs";import o from"glob";import s from"path";const c={css:[],content:[],defaultExtractor:e=>e.match(/[A-Za-z0-9_-]+/g)||[],extractors:[],fontFace:!1,keyframes:!1,rejected:!1,stdin:!1,stdout:!1,variables:!1,whitelist:[],whitelistPatterns:[],whitelistPatternsChildren:[]},i="purgecss ignore current",a="purgecss ignore",f="purgecss start ignore",u="purgecss end ignore",l="purgecss.config.js",d="Error loading the config file",p=["*","::-webkit-scrollbar","::selection",":root","::before","::after"],m=["class","id","universal","pseudo"];let y,w=!1;const h={fontFace:[],keyframes:[]},g=new Set,v=new Set,x=new Set;async function k(e=l){let t;try{const n=s.join(process.cwd(),e);t=await import(n)}catch(e){throw new Error(`${d} ${e.message}`)}return{...c,...t}}function S(e){y=e}function F(e,t){const n=new Set(t(e));return n.delete(""),n}async function j(e,t){let s=new Set;for(const c of e){let e=[];try{await n.access(c,r.F_OK),e.push(c)}catch(t){e=o.sync(c)}for(const r of e){const e=F(await n.readFile(r,"utf-8"),P(r,t));s=new Set([...s,...e])}}return s}function b(e,t){let n=new Set;for(const{raw:r,extension:o}of e){const e=F(r,P(`.${o}`,t));n=new Set([...n,...e])}return n}function P(e,t){const n=t.find(t=>t.extensions.find(t=>e.endsWith(t)));return void 0===n?y.defaultExtractor:n.extractor}async function $(e,n){const r=e.prev();if(w)return;if(r&&"comment"===r.type&&C(r,"next"))return void r.remove();if(e.parent&&"atrule"===e.parent.type&&"keyframes"===e.parent.name)return;if("rule"===e.type&&function(e){let t=!1;return e.walkComments(e=>{e&&"comment"===e.type&&e.text.includes(i)&&(t=!0,e.remove())}),t}(e))return;if("rule"!==e.type)return;let o=!0;if(e.selector=t(e=>{e.walk(e=>{if("selector"!==e.type)return;const t=function(e){const t=new Set;if(e.parent&&":not"===e.parent.value&&"pseudo"===e.parent.type)return t;for(const{type:n,value:r}of e.nodes)m.includes(n)&&void 0!==r?t.add(r):"tag"!==n||void 0===r||/[+]|n|-|(even)|(odd)|^from$|^to$|^\d/.test(r)||t.add(r);return t}(e);(o=function(e,t){for(const n of t){const t=n.replace(/\\/g,"");if(!t.startsWith(":")){if(W(t))return!0;if(!(e.has(t)||p.includes(t)||E(t)))return!1}}return!0}(n,t))||(y.rejected&&x.add(e.toString()),e.remove())})}).processSync(e.selector),o&&void 0!==e.nodes)for(const t of e.nodes){if("decl"!==t.type)continue;const{prop:e,value:n}=t;if(y.keyframes&&("animation"===e||"animation-name"===e))for(const e of n.split(/[\s,]+/))g.add(e);if(y.fontFace&&"font-family"===e)for(const e of n.split(",")){const t=z(e.trim());v.add(t)}}const s=e.parent;e.selector||e.remove(),function(e){if("rule"===e.type&&!e.selector||e.nodes&&!e.nodes.length||"atrule"===e.type&&(!e.nodes&&!e.params||!e.params&&e.nodes&&!e.nodes.length))return!0;return!1}(s)&&s.remove()}function C(e,t){switch(t){case"next":return e.text.includes(a);case"start":return e.text.includes(f);case"end":return e.text.includes(u)}}function E(e){return p.includes(e)||y.whitelist&&y.whitelist.some(t=>t===e)||y.whitelistPatterns&&y.whitelistPatterns.some(t=>t.test(e))}function W(e){return y.whitelistPatternsChildren&&y.whitelistPatternsChildren.some(t=>t.test(e))}function A(){for(const{name:e,node:t}of h.fontFace)v.has(e)||t.remove()}function _(){for(const e of h.keyframes)g.has(e.params)||e.remove()}function z(e){return e.replace(/(^["'])|(["']$)/g,"")}function K(e,t){e.walk(e=>"rule"===e.type?$(e,t):"atrule"===e.type?function(e){if(y.keyframes&&e.name.endsWith("keyframes"))h.keyframes.push(e);else if(y.fontFace&&"font-face"===e.name&&e.nodes)for(const t of e.nodes)"decl"===t.type&&"font-family"===t.prop&&h.fontFace.push({name:z(t.value),node:e})}(e):void("comment"===e.type&&(C(e,"start")?(w=!0,e.remove()):C(e,"end")&&(w=!1,e.remove()))))}export default async function(t){y="object"!=typeof t?await k(t):{...c,...t};const{content:r,css:s,extractors:i}=y,a=r.filter(e=>"string"==typeof e),f=r.filter(e=>"object"==typeof e),u=await j(a,i),l=b(f,i);return async function(t,r){const s=[],c=[];for(const e of t)"string"==typeof e?c.push(...o.sync(e)):c.push(e);for(const t of c){const o="string"==typeof t?y.stdin?t:await n.readFile(t,"utf-8"):t.raw,c=e(o);K(c,r),y.fontFace&&A(),y.keyframes&&_();const i={css:c.toString(),file:"string"==typeof t?t:void 0};"string"==typeof t&&(i.file=t),y.rejected&&(i.rejected=Array.from(x),x.clear()),s.push(i)}return s}(s,new Set([...u,...l]))}export{j as extractSelectorsFromFiles,b as extractSelectorsFromString,y as options,A as removeUnusedFontFaces,_ as removeUnusedKeyframes,x as selectorsRemoved,k as setOptions,S as setPurgeCSSOptions,K as walkThroughCSS};
diff --git a/packages/purgecss/lib/purgecss.esm.js b/packages/purgecss/lib/purgecss.esm.js
new file mode 100644
index 00000000..7867efa7
--- /dev/null
+++ b/packages/purgecss/lib/purgecss.esm.js
@@ -0,0 +1 @@
+import{parse as e}from"postcss";import t from"postcss-selector-parser";import{promises as n,constants as r}from"fs";import o from"glob";import s from"path";const c={css:[],content:[],defaultExtractor:e=>e.match(/[A-Za-z0-9_-]+/g)||[],extractors:[],fontFace:!1,keyframes:!1,rejected:!1,stdin:!1,stdout:!1,variables:!1,whitelist:[],whitelistPatterns:[],whitelistPatternsChildren:[]},i="purgecss ignore current",a="purgecss ignore",f="purgecss start ignore",u="purgecss end ignore",l="purgecss.config.js",d="Error loading the config file",p=["*","::-webkit-scrollbar","::selection",":root","::before","::after"],m=["class","id","universal","pseudo"];let y,w=!1;const h={fontFace:[],keyframes:[]},g=new Set,v=new Set,x=new Set;async function k(e=l){let t;try{const n=s.join(process.cwd(),e);t=await import(n)}catch(e){throw new Error(`${d} ${e.message}`)}return{...c,...t}}function S(e){y=e}function F(e,t){const n=new Set(t(e));return n.delete(""),n}async function j(e,t){let s=new Set;for(const c of e){let e=[];try{await n.access(c,r.F_OK),e.push(c)}catch(t){e=o.sync(c)}for(const r of e){const e=F(await n.readFile(r,"utf-8"),P(r,t));s=new Set([...s,...e])}}return s}function b(e,t){let n=new Set;for(const{raw:r,extension:o}of e){const e=F(r,P(`.${o}`,t));n=new Set([...n,...e])}return n}function P(e,t){const n=t.find(t=>t.extensions.find(t=>e.endsWith(t)));return void 0===n?y.defaultExtractor:n.extractor}async function $(e,n){const r=e.prev();if(w)return;if(r&&"comment"===r.type&&C(r,"next"))return void r.remove();if(e.parent&&"atrule"===e.parent.type&&"keyframes"===e.parent.name)return;if("rule"===e.type&&function(e){let t=!1;return e.walkComments(e=>{e&&"comment"===e.type&&e.text.includes(i)&&(t=!0,e.remove())}),t}(e))return;if("rule"!==e.type)return;let o=!0;if(e.selector=t(e=>{e.walk(e=>{if("selector"!==e.type)return;const t=function(e){const t=new Set;if(e.parent&&":not"===e.parent.value&&"pseudo"===e.parent.type)return t;for(const{type:n,value:r}of e.nodes)m.includes(n)&&void 0!==r?t.add(r):"tag"!==n||void 0===r||/[+]|n|-|(even)|(odd)|^from$|^to$|^\d/.test(r)||t.add(r);return t}(e);(o=function(e,t){for(const n of t){const t=n.replace(/\\/g,"");if(!t.startsWith(":")){if(W(t))return!0;if(!(e.has(t)||p.includes(t)||E(t)))return!1}}return!0}(n,t))||(y.rejected&&x.add(e.toString()),e.remove())})}).processSync(e.selector),o&&void 0!==e.nodes)for(const t of e.nodes){if("decl"!==t.type)continue;const{prop:e,value:n}=t;if(y.keyframes&&("animation"===e||"animation-name"===e))for(const e of n.split(/[\s,]+/))g.add(e);if(y.fontFace&&"font-family"===e)for(const e of n.split(",")){const t=z(e.trim());v.add(t)}}const s=e.parent;e.selector||e.remove(),function(e){if("rule"===e.type&&!e.selector||e.nodes&&!e.nodes.length||"atrule"===e.type&&(!e.nodes&&!e.params||!e.params&&e.nodes&&!e.nodes.length))return!0;return!1}(s)&&s.remove()}function C(e,t){switch(t){case"next":return e.text.includes(a);case"start":return e.text.includes(f);case"end":return e.text.includes(u)}}function E(e){return p.includes(e)||y.whitelist&&y.whitelist.some(t=>t===e)||y.whitelistPatterns&&y.whitelistPatterns.some(t=>t.test(e))}function W(e){return y.whitelistPatternsChildren&&y.whitelistPatternsChildren.some(t=>t.test(e))}function A(){for(const{name:e,node:t}of h.fontFace)v.has(e)||t.remove()}function _(){for(const e of h.keyframes)g.has(e.params)||e.remove()}function z(e){return e.replace(/(^["'])|(["']$)/g,"")}function K(e,t){e.walk(e=>"rule"===e.type?$(e,t):"atrule"===e.type?function(e){if(y.keyframes&&e.name.endsWith("keyframes"))h.keyframes.push(e);else if(y.fontFace&&"font-face"===e.name&&e.nodes)for(const t of e.nodes)"decl"===t.type&&"font-family"===t.prop&&h.fontFace.push({name:z(t.value),node:e})}(e):void("comment"===e.type&&(C(e,"start")?(w=!0,e.remove()):C(e,"end")&&(w=!1,e.remove()))))}export default async function(t){y="object"!=typeof t?await k(t):{...c,...t};const{content:r,css:s,extractors:i}=y,a=r.filter(e=>"string"==typeof e),f=r.filter(e=>"object"==typeof e),u=await j(a,i),l=b(f,i);return async function(t,r){const s=[],c=[];for(const e of t)"string"==typeof e?c.push(...o.sync(e)):c.push(e);for(const t of c){const o="string"==typeof t?y.stdin?t:await n.readFile(t,"utf-8"):t.raw,c=e(o);K(c,r),y.fontFace&&A(),y.keyframes&&_();const i={css:c.toString(),file:"string"==typeof t?t:void 0};"string"==typeof t&&(i.file=t),y.rejected&&(i.rejected=Array.from(x),x.clear()),s.push(i)}return s}(s,new Set([...u,...l]))}export{j as extractSelectorsFromFiles,b as extractSelectorsFromString,y as options,A as removeUnusedFontFaces,_ as removeUnusedKeyframes,x as selectorsRemoved,k as setOptions,S as setPurgeCSSOptions,K as walkThroughCSS};
diff --git a/packages/purgecss/lib/purgecss.js b/packages/purgecss/lib/purgecss.js
new file mode 100644
index 00000000..b3f639c2
--- /dev/null
+++ b/packages/purgecss/lib/purgecss.js
@@ -0,0 +1 @@
+"use strict";function _interopDefault(e){return e&&"object"==typeof e&&"default"in e?e.default:e}function _interopNamespace(e){if(e&&e.__esModule)return e;var t={};return e&&Object.keys(e).forEach((function(o){var r=Object.getOwnPropertyDescriptor(e,o);Object.defineProperty(t,o,r.get?r:{enumerable:!0,get:function(){return e[o]}})})),t.default=e,t}Object.defineProperty(exports,"__esModule",{value:!0});var postcss=require("postcss"),selectorParser=_interopDefault(require("postcss-selector-parser")),fs=require("fs"),glob=_interopDefault(require("glob")),path=_interopDefault(require("path"));const defaultOptions={css:[],content:[],defaultExtractor:e=>e.match(/[A-Za-z0-9_-]+/g)||[],extractors:[],fontFace:!1,keyframes:!1,rejected:!1,stdin:!1,stdout:!1,variables:!1,whitelist:[],whitelistPatterns:[],whitelistPatternsChildren:[]},IGNORE_ANNOTATION_CURRENT="purgecss ignore current",IGNORE_ANNOTATION_NEXT="purgecss ignore",IGNORE_ANNOTATION_START="purgecss start ignore",IGNORE_ANNOTATION_END="purgecss end ignore",CONFIG_FILENAME="purgecss.config.js",ERROR_CONFIG_FILE_LOADING="Error loading the config file",CSS_WHITELIST=["*","::-webkit-scrollbar","::selection",":root","::before","::after"],SELECTOR_STANDARD_TYPES=["class","id","universal","pseudo"];let ignore=!1;const atRules={fontFace:[],keyframes:[]},usedAnimations=new Set,usedFontFaces=new Set,selectorsRemoved=new Set;async function setOptions(e=CONFIG_FILENAME){let t;try{const o=path.join(process.cwd(),e);t=await new Promise((function(e){e(_interopNamespace(require(o)))}))}catch(e){throw new Error(`${ERROR_CONFIG_FILE_LOADING} ${e.message}`)}return{...defaultOptions,...t}}function setPurgeCSSOptions(e){exports.options=e}async function purge(e){exports.options="object"!=typeof e?await setOptions(e):{...defaultOptions,...e};const{content:t,css:o,extractors:r}=exports.options,s=t.filter(e=>"string"==typeof e),n=t.filter(e=>"object"==typeof e),i=await extractSelectorsFromFiles(s,r),a=extractSelectorsFromString(n,r);return getPurgedCSS(o,new Set([...i,...a]))}function extractSelectors(e,t){const o=new Set(t(e));return o.delete(""),o}async function extractSelectorsFromFiles(e,t){let o=new Set;for(const r of e){let e=[];try{await fs.promises.access(r,fs.constants.F_OK),e.push(r)}catch(t){e=glob.sync(r)}for(const r of e){const e=extractSelectors(await fs.promises.readFile(r,"utf-8"),getFileExtractor(r,t));o=new Set([...o,...e])}}return o}function extractSelectorsFromString(e,t){let o=new Set;for(const{raw:r,extension:s}of e){const e=extractSelectors(r,getFileExtractor(`.${s}`,t));o=new Set([...o,...e])}return o}function getFileExtractor(e,t){const o=t.find(t=>t.extensions.find(t=>e.endsWith(t)));return void 0===o?exports.options.defaultExtractor:o.extractor}async function getPurgedCSS(e,t){const o=[],r=[];for(const t of e)"string"==typeof t?r.push(...glob.sync(t)):r.push(t);for(const e of r){const r="string"==typeof e?exports.options.stdin?e:await fs.promises.readFile(e,"utf-8"):e.raw,s=postcss.parse(r);walkThroughCSS(s,t),exports.options.fontFace&&removeUnusedFontFaces(),exports.options.keyframes&&removeUnusedKeyframes();const n={css:s.toString(),file:"string"==typeof e?e:void 0};"string"==typeof e&&(n.file=e),exports.options.rejected&&(n.rejected=Array.from(selectorsRemoved),selectorsRemoved.clear()),o.push(n)}return o}function getSelectorsInRule(e){const t=new Set;if(e.parent&&":not"===e.parent.value&&"pseudo"===e.parent.type)return t;for(const{type:o,value:r}of e.nodes)SELECTOR_STANDARD_TYPES.includes(o)&&void 0!==r?t.add(r):"tag"!==o||void 0===r||/[+]|n|-|(even)|(odd)|^from$|^to$|^\d/.test(r)||t.add(r);return t}function evaluateAtRule(e){if(exports.options.keyframes&&e.name.endsWith("keyframes"))atRules.keyframes.push(e);else if(exports.options.fontFace&&"font-face"===e.name&&e.nodes)for(const t of e.nodes)"decl"===t.type&&"font-family"===t.prop&&atRules.fontFace.push({name:stripQuotes(t.value),node:e})}async function evaluateRule(e,t){const o=e.prev();if(ignore)return;if(o&&"comment"===o.type&&isIgnoreAnnotation(o,"next"))return void o.remove();if(e.parent&&"atrule"===e.parent.type&&"keyframes"===e.parent.name)return;if("rule"===e.type&&hasIgnoreAnnotation(e))return;if("rule"!==e.type)return;let r=!0;if(e.selector=selectorParser(e=>{e.walk(e=>{if("selector"!==e.type)return;const o=getSelectorsInRule(e);(r=shouldKeepSelector(t,o))||(exports.options.rejected&&selectorsRemoved.add(e.toString()),e.remove())})}).processSync(e.selector),r&&void 0!==e.nodes)for(const t of e.nodes){if("decl"!==t.type)continue;const{prop:e,value:o}=t;if(exports.options.keyframes&&("animation"===e||"animation-name"===e))for(const e of o.split(/[\s,]+/))usedAnimations.add(e);if(exports.options.fontFace&&"font-family"===e)for(const e of o.split(",")){const t=stripQuotes(e.trim());usedFontFaces.add(t)}}const s=e.parent;e.selector||e.remove(),isRuleEmpty(s)&&s.remove()}function isIgnoreAnnotation(e,t){switch(t){case"next":return e.text.includes(IGNORE_ANNOTATION_NEXT);case"start":return e.text.includes(IGNORE_ANNOTATION_START);case"end":return e.text.includes(IGNORE_ANNOTATION_END)}}function isRuleEmpty(e){return!!("rule"===e.type&&!e.selector||e.nodes&&!e.nodes.length||"atrule"===e.type&&(!e.nodes&&!e.params||!e.params&&e.nodes&&!e.nodes.length))}function isSelectorWhitelisted(e){return CSS_WHITELIST.includes(e)||exports.options.whitelist&&exports.options.whitelist.some(t=>t===e)||exports.options.whitelistPatterns&&exports.options.whitelistPatterns.some(t=>t.test(e))}function isSelectorWhitelistedChildren(e){return exports.options.whitelistPatternsChildren&&exports.options.whitelistPatternsChildren.some(t=>t.test(e))}function hasIgnoreAnnotation(e){let t=!1;return e.walkComments(e=>{e&&"comment"===e.type&&e.text.includes(IGNORE_ANNOTATION_CURRENT)&&(t=!0,e.remove())}),t}function removeUnusedFontFaces(){for(const{name:e,node:t}of atRules.fontFace)usedFontFaces.has(e)||t.remove()}function removeUnusedKeyframes(){for(const e of atRules.keyframes)usedAnimations.has(e.params)||e.remove()}function shouldKeepSelector(e,t){for(const o of t){const t=o.replace(/\\/g,"");if(!t.startsWith(":")){if(isSelectorWhitelistedChildren(t))return!0;if(!(e.has(t)||CSS_WHITELIST.includes(t)||isSelectorWhitelisted(t)))return!1}}return!0}function stripQuotes(e){return e.replace(/(^["'])|(["']$)/g,"")}function walkThroughCSS(e,t){e.walk(e=>"rule"===e.type?evaluateRule(e,t):"atrule"===e.type?evaluateAtRule(e):void("comment"===e.type&&(isIgnoreAnnotation(e,"start")?(ignore=!0,e.remove()):isIgnoreAnnotation(e,"end")&&(ignore=!1,e.remove()))))}exports.default=purge,exports.extractSelectorsFromFiles=extractSelectorsFromFiles,exports.extractSelectorsFromString=extractSelectorsFromString,exports.removeUnusedFontFaces=removeUnusedFontFaces,exports.removeUnusedKeyframes=removeUnusedKeyframes,exports.selectorsRemoved=selectorsRemoved,exports.setOptions=setOptions,exports.setPurgeCSSOptions=setPurgeCSSOptions,exports.walkThroughCSS=walkThroughCSS;
diff --git a/packages/purgecss/src/index.ts b/packages/purgecss/src/index.ts
index bb938d79..4cbac562 100644
--- a/packages/purgecss/src/index.ts
+++ b/packages/purgecss/src/index.ts
@@ -31,7 +31,7 @@ import { CSS_WHITELIST } from "./internal-whitelist";
import { SELECTOR_STANDARD_TYPES } from "./selector-types";
let ignore = false;
-let options: Options;
+export let options: Options;
const atRules: AtRules = {
fontFace: [],
keyframes: []
@@ -39,13 +39,13 @@ const atRules: AtRules = {
const usedAnimations: Set = new Set();
const usedFontFaces: Set = new Set();
-const selectorsRemoved: Set = new Set();
+export const selectorsRemoved: Set = new Set();
/**
* Load the configuration file from the path
* @param configFile Path of the config file
*/
-async function setOptions(
+export async function setOptions(
configFile: string = CONFIG_FILENAME
): Promise {
let options: Options;
@@ -61,6 +61,10 @@ async function setOptions(
};
}
+export function setPurgeCSSOptions(purgeCSSOptions: Options): void {
+ options = purgeCSSOptions;
+}
+
/**
* Remove unused css
* @param options PurgeCSS options
@@ -116,7 +120,7 @@ function extractSelectors(
* @param files Array of files path or glob pattern
* @param extractors Array of extractors
*/
-async function extractSelectorsFromFiles(
+export async function extractSelectorsFromFiles(
files: string[],
extractors: Extractors[]
): Promise> {
@@ -144,7 +148,7 @@ async function extractSelectorsFromFiles(
* @param content Array of content
* @param extractors Array of extractors
*/
-function extractSelectorsFromString(
+export function extractSelectorsFromString(
content: RawContent[],
extractors: Extractors[]
): Set {
@@ -436,7 +440,7 @@ function hasIgnoreAnnotation(rule: postcss.Rule): boolean {
return found;
}
-function removeUnusedFontFaces(): void {
+export function removeUnusedFontFaces(): void {
for (const { name, node } of atRules.fontFace) {
if (!usedFontFaces.has(name)) {
node.remove();
@@ -444,7 +448,7 @@ function removeUnusedFontFaces(): void {
}
}
-function removeUnusedKeyframes(): void {
+export function removeUnusedKeyframes(): void {
for (const node of atRules.keyframes) {
if (!usedAnimations.has(node.params)) {
node.remove();
@@ -498,7 +502,7 @@ function stripQuotes(str: string) {
* @param root root node of the postcss AST
* @param selectors selectors used in content files
*/
-function walkThroughCSS(root: postcss.Root, selectors: Set) {
+export function walkThroughCSS(root: postcss.Root, selectors: Set) {
root.walk(node => {
if (node.type === "rule") {
return evaluateRule(node, selectors);
diff --git a/scripts/build.ts b/scripts/build.ts
new file mode 100644
index 00000000..d45fa9c7
--- /dev/null
+++ b/scripts/build.ts
@@ -0,0 +1,48 @@
+const path = require('path')
+const rollup = require('rollup')
+const { terser } = require('rollup-plugin-terser')
+const typescript = require('rollup-plugin-typescript2')
+
+const packagesDirectory = path.resolve(__dirname, './../packages')
+
+const packages = [
+ 'postcss-purgecss',
+ 'purgecss',
+ 'purgecss-from-html',
+ 'purgecss-from-pug'
+]
+
+async function build() {
+ const bundle = await rollup.rollup({
+ input: path.resolve(packagesDirectory, `./${packages[1]}/src/index.ts`),
+ plugins: [
+ typescript({
+ tsconfigOverride: {
+ compilerOptions: {
+ declaration: true,
+ declarationMap: true
+ },
+ exclude: ['**/__tests__'],
+ // declarationDir: path.resolve(packagesDirectory, packages[1], `./lib/`),
+ useTsconfigDeclarationDir: true
+ }
+ }),
+ terser()
+ ]
+ })
+
+ await bundle.write({
+ file: path.resolve(packagesDirectory, packages[1], `./lib/${packages[1]}.es.js`),
+ format: 'es'
+ })
+ await bundle.write({
+ file: path.resolve(packagesDirectory, packages[1], `./lib/${packages[1]}.esm.js`),
+ format: 'esm'
+ })
+ await bundle.write({
+ file: path.resolve(packagesDirectory, packages[1], `./lib/${packages[1]}.js`),
+ format: 'cjs'
+ })
+}
+
+build()
\ No newline at end of file
diff --git a/scripts/verify-commit.js b/scripts/verify-commit.js
new file mode 100644
index 00000000..1ba27895
--- /dev/null
+++ b/scripts/verify-commit.js
@@ -0,0 +1,14 @@
+// Invoked on the commit-msg git hook by yorkie.
+
+const msgPath = process.env.GIT_PARAMS
+const msg = require('fs')
+ .readFileSync(msgPath, 'utf-8')
+ .trim()
+
+const commitRE = /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip)(\(.+\))?: .{1,50}/
+
+if (!commitRE.test(msg)) {
+ console.log()
+ console.error(`invalid commit message format.`)
+ process.exit(1)
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 05c7f809..d0ecf1f9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,7 @@
{
"compilerOptions": {
+ "declaration": true,
+ "declarationDir": ".",
"baseUrl": ".",
"outDir": "dist",
"sourceMap": false,