From 4de3bd8e198985670502f77c57567eb43fcccbd4 Mon Sep 17 00:00:00 2001 From: Floriel Date: Tue, 1 Mar 2022 01:16:53 +0100 Subject: [PATCH] feat(postcss-purgecss): load options from purgecss config --- .../fixtures/src/config-test/expected.css | 62 ++++++++++ .../fixtures/src/config-test/index.css | 87 +++++++++++++ .../fixtures/src/config-test/index.html | 7 ++ .../src/config-test/purgecss.config.js | 4 + .../postcss-purgecss/__tests__/index.test.ts | 117 ++++++++++-------- packages/postcss-purgecss/build.ts | 1 + packages/postcss-purgecss/src/index.ts | 36 +++++- 7 files changed, 258 insertions(+), 56 deletions(-) create mode 100644 packages/postcss-purgecss/__tests__/fixtures/src/config-test/expected.css create mode 100644 packages/postcss-purgecss/__tests__/fixtures/src/config-test/index.css create mode 100644 packages/postcss-purgecss/__tests__/fixtures/src/config-test/index.html create mode 100644 packages/postcss-purgecss/__tests__/fixtures/src/config-test/purgecss.config.js diff --git a/packages/postcss-purgecss/__tests__/fixtures/src/config-test/expected.css b/packages/postcss-purgecss/__tests__/fixtures/src/config-test/expected.css new file mode 100644 index 00000000..21f4b319 --- /dev/null +++ b/packages/postcss-purgecss/__tests__/fixtures/src/config-test/expected.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/src/config-test/index.css b/packages/postcss-purgecss/__tests__/fixtures/src/config-test/index.css new file mode 100644 index 00000000..fa28b67b --- /dev/null +++ b/packages/postcss-purgecss/__tests__/fixtures/src/config-test/index.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/config-test/index.html b/packages/postcss-purgecss/__tests__/fixtures/src/config-test/index.html new file mode 100644 index 00000000..87ab68b0 --- /dev/null +++ b/packages/postcss-purgecss/__tests__/fixtures/src/config-test/index.html @@ -0,0 +1,7 @@ +
+
+ +
+
+ +
\ No newline at end of file diff --git a/packages/postcss-purgecss/__tests__/fixtures/src/config-test/purgecss.config.js b/packages/postcss-purgecss/__tests__/fixtures/src/config-test/purgecss.config.js new file mode 100644 index 00000000..f928c97f --- /dev/null +++ b/packages/postcss-purgecss/__tests__/fixtures/src/config-test/purgecss.config.js @@ -0,0 +1,4 @@ +module.exports = { + content: [`${__dirname}/index.html`], + fontFace: true +} \ No newline at end of file diff --git a/packages/postcss-purgecss/__tests__/index.test.ts b/packages/postcss-purgecss/__tests__/index.test.ts index 8de72574..2c3a3196 100644 --- a/packages/postcss-purgecss/__tests__/index.test.ts +++ b/packages/postcss-purgecss/__tests__/index.test.ts @@ -5,82 +5,71 @@ import purgeCSSPlugin from "../src/"; describe("Purgecss postcss plugin", () => { const files = ["simple", "font-keyframes"]; - - for (const file of files) { - it(`remove unused css with content option successfully: ${file}`, async () => { - const input = fs + const fileContents = files.map((file) => { + return { + name: file, + input: fs .readFileSync(`${__dirname}/fixtures/src/${file}/${file}.css`) - .toString(); - const expected = fs + .toString(), + output: fs .readFileSync(`${__dirname}/fixtures/expected/${file}.css`) - .toString(); + .toString(), + }; + }); + + it.each(fileContents)( + "remove unused css with content option successfully: $name", + async ({ name, input, output }) => { const result = await postcss([ purgeCSSPlugin({ - content: [`${__dirname}/fixtures/src/${file}/${file}.html`], + content: [`${__dirname}/fixtures/src/${name}/${name}.html`], fontFace: true, keyframes: true, }), ]).process(input, { from: undefined }); - expect(result.css).toBe(expected); + expect(result.css).toBe(output); expect(result.warnings().length).toBe(0); - }); - } - - for (const file of files) { - it(`remove unused css with contentFunction option successfully: ${file}`, (done) => { - const input = fs - .readFileSync(`${__dirname}/fixtures/src/${file}/${file}.css`) - .toString(); - const expected = fs - .readFileSync(`${__dirname}/fixtures/expected/${file}.css`) - .toString(); + } + ); - const sourceFileName = `src/${file}/${file}.css`; + it.each(fileContents)( + "remove unused css with contentFunction option successfully: $name", + async ({ name, input, output }) => { + const sourceFileName = `src/${name}/${name}.css`; const contentFunction = jest .fn() - .mockReturnValue([`${__dirname}/fixtures/src/${file}/${file}.html`]); + .mockReturnValue([`${__dirname}/fixtures/src/${name}/${name}.html`]); - postcss([ + const result = await postcss([ purgeCSSPlugin({ contentFunction, fontFace: true, keyframes: true, }), - ]) - .process(input, { from: sourceFileName }) - .then((result) => { - expect(result.css).toBe(expected); - expect(result.warnings().length).toBe(0); - expect(contentFunction).toHaveBeenCalledTimes(1); - expect(contentFunction.mock.calls[0][0]).toContain(sourceFileName); - done(); - }); - }); - } - - it(`queues messages when using reject flag: simple`, (done) => { - const input = fs - .readFileSync(`${__dirname}/fixtures/src/simple/simple.css`) - .toString(); - const expected = fs - .readFileSync(`${__dirname}/fixtures/expected/simple.css`) - .toString(); - postcss([ + ]).process(input, { from: sourceFileName }); + + expect(result.css).toBe(output); + expect(result.warnings().length).toBe(0); + expect(contentFunction).toHaveBeenCalledTimes(1); + expect(contentFunction.mock.calls[0][0]).toContain(sourceFileName); + } + ); + + it(`queues messages when using reject flag: simple`, async () => { + const result = await postcss([ purgeCSSPlugin({ content: [`${__dirname}/fixtures/src/simple/simple.html`], rejected: true, }), ]) - .process(input, { from: undefined }) - .then((result) => { - 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(); - }); + .process(fileContents[0].input, { from: undefined }); + + expect(result.css).toBe(fileContents[0].output); + 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/); }); it(`lets other plugins transform selectors before purging`, async () => { @@ -109,4 +98,28 @@ describe("Purgecss postcss plugin", () => { expect(result.css).toBe(expected); expect(result.warnings().length).toBe(0); }); + + it('should work with a purgecss config file', async () => { + const cwd = process.cwd(); + const configTestDirectory = `${__dirname}/fixtures/src/config-test/`; + process.chdir(configTestDirectory); + + const input = fs + .readFileSync(`${configTestDirectory}index.css`) + .toString(); + const output = fs + .readFileSync(`${configTestDirectory}expected.css`) + .toString(); + + const result = await postcss([ + purgeCSSPlugin({ + keyframes: true, + }), + ]).process(input, { from: undefined }); + + expect(result.css).toBe(output); + expect(result.warnings().length).toBe(0); + + process.chdir(cwd); + }) }); diff --git a/packages/postcss-purgecss/build.ts b/packages/postcss-purgecss/build.ts index 5f42e0ed..7aa97aee 100644 --- a/packages/postcss-purgecss/build.ts +++ b/packages/postcss-purgecss/build.ts @@ -14,6 +14,7 @@ import * as path from "path"; const rollupConfig = createRollupConfig("postcss-purgecss", [ "postcss", "purgecss", + "path" ]); await buildRollup(rollupConfig); await extractAPI(__dirname); diff --git a/packages/postcss-purgecss/src/index.ts b/packages/postcss-purgecss/src/index.ts index 7c901126..74830e8e 100644 --- a/packages/postcss-purgecss/src/index.ts +++ b/packages/postcss-purgecss/src/index.ts @@ -1,3 +1,13 @@ +/** + * PostCSS Plugin for PurgeCSS + * + * Most bundlers and frameworks to build websites are using PostCSS. + * The easiest way to configure PurgeCSS is with its PostCSS plugin. + * + * @packageDocumentation + */ + +import * as path from "path"; import * as postcss from "postcss"; import { PurgeCSS, @@ -12,16 +22,33 @@ export * from "./types"; const PLUGIN_NAME = "postcss-purgecss"; +/** + * Execute PurgeCSS process on the postCSS root node + * + * @param opts - PurgeCSS options + * @param root - root node of postCSS + * @param helpers - postCSS helpers + */ async function purgeCSS( opts: UserDefinedOptions, root: postcss.Root, { result }: postcss.Helpers ): Promise { const purgeCSS = new PurgeCSS(); + + let configFileOptions: UserDefinedOptions | undefined; + try { + const t = path.resolve(process.cwd(), "purgecss.config.js"); + configFileOptions = await import(t); + } catch { + // no config file present + } + const options = { ...defaultOptions, + ...configFileOptions, ...opts, - safelist: standardizeSafelist(opts?.safelist), + safelist: standardizeSafelist(opts?.safelist || configFileOptions?.safelist), }; if (opts && typeof opts.contentFunction === "function") { @@ -77,9 +104,10 @@ async function purgeCSS( } /** - * - * @param opts - opts - * @returns + * PostCSS Plugin for PurgeCSS + * + * @param opts - PurgeCSS Options + * @returns the postCSS plugin * * @public */