From 2b0616fb9adc69255171aae53ef22580d131852b Mon Sep 17 00:00:00 2001 From: Ffloriel Date: Tue, 8 Oct 2019 16:12:12 +0200 Subject: [PATCH] feat(postcss-purgecss): add package --- package.json | 21 +++++ packages/postcss-purgecss/README.md | 11 +++ .../fixtures/expected/font-keyframes.css | 62 +++++++++++++ .../__tests__/fixtures/expected/simple.css | 3 + .../src/font-keyframes/font-keyframes.css | 87 +++++++++++++++++++ .../src/font-keyframes/font-keyframes.html | 7 ++ .../__tests__/fixtures/src/simple/simple.css | 11 +++ .../__tests__/fixtures/src/simple/simple.html | 8 ++ .../postcss-purgecss/__tests__/index.test.ts | 58 +++++++++++++ packages/postcss-purgecss/package.json | 30 +++++++ packages/postcss-purgecss/src/index.ts | 67 ++++++++++++++ packages/purgecss/__tests__/index.test.ts | 23 ++--- packages/purgecss/lib/purgecss.es.js | 1 + packages/purgecss/lib/purgecss.esm.js | 1 + packages/purgecss/lib/purgecss.js | 1 + packages/purgecss/src/index.ts | 20 +++-- scripts/build.ts | 48 ++++++++++ scripts/verify-commit.js | 14 +++ tsconfig.json | 2 + 19 files changed, 456 insertions(+), 19 deletions(-) create mode 100644 packages/postcss-purgecss/README.md create mode 100644 packages/postcss-purgecss/__tests__/fixtures/expected/font-keyframes.css create mode 100644 packages/postcss-purgecss/__tests__/fixtures/expected/simple.css create mode 100644 packages/postcss-purgecss/__tests__/fixtures/src/font-keyframes/font-keyframes.css create mode 100644 packages/postcss-purgecss/__tests__/fixtures/src/font-keyframes/font-keyframes.html create mode 100644 packages/postcss-purgecss/__tests__/fixtures/src/simple/simple.css create mode 100644 packages/postcss-purgecss/__tests__/fixtures/src/simple/simple.html create mode 100644 packages/postcss-purgecss/__tests__/index.test.ts create mode 100644 packages/postcss-purgecss/package.json create mode 100644 packages/postcss-purgecss/src/index.ts create mode 100644 packages/purgecss/lib/purgecss.es.js create mode 100644 packages/purgecss/lib/purgecss.esm.js create mode 100644 packages/purgecss/lib/purgecss.js create mode 100644 scripts/build.ts create mode 100644 scripts/verify-commit.js 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,