From f2a9c5ac575af9a1e3f85be63b758fb9c37077e1 Mon Sep 17 00:00:00 2001 From: Floriel Date: Tue, 22 Feb 2022 14:58:45 +0000 Subject: [PATCH] feat: add source map support #526 --- packages/purgecss/__tests__/sourcemap.test.ts | 26 ++++ packages/purgecss/src/index.ts | 15 +- packages/purgecss/src/options.ts | 4 + packages/purgecss/src/types/index.ts | 143 ++++++++++++++++++ 4 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 packages/purgecss/__tests__/sourcemap.test.ts diff --git a/packages/purgecss/__tests__/sourcemap.test.ts b/packages/purgecss/__tests__/sourcemap.test.ts new file mode 100644 index 00000000..d8703811 --- /dev/null +++ b/packages/purgecss/__tests__/sourcemap.test.ts @@ -0,0 +1,26 @@ +import { PurgeCSS } from '../src/' +import { ROOT_TEST_EXAMPLES } from './utils'; + +describe("source map option", () => { + it('contains the source map inlined in the CSS file', async () => { + const resultsPurge = await new PurgeCSS().purge({ + content: [`${ROOT_TEST_EXAMPLES}others/remove_unused.js`], + css: [`${ROOT_TEST_EXAMPLES}others/remove_unused.css`], + sourceMap: true + }); + + expect(resultsPurge[0].css).toContain('sourceMappingURL=data:application/json;base64'); + }); + + it('contains the source map separately when setting inline to false', async () => { + const resultsPurge = await new PurgeCSS().purge({ + content: [`${ROOT_TEST_EXAMPLES}others/remove_unused.js`], + css: [`${ROOT_TEST_EXAMPLES}others/remove_unused.css`], + sourceMap: { + inline: false + } + }); + + expect(resultsPurge[0].sourceMap).toContain('sources":["__tests__/test_examples/others/remove_unused.css"]'); + }) +}); diff --git a/packages/purgecss/src/index.ts b/packages/purgecss/src/index.ts index 5fabccc6..7d037f37 100644 --- a/packages/purgecss/src/index.ts +++ b/packages/purgecss/src/index.ts @@ -18,7 +18,7 @@ import { IGNORE_ANNOTATION_CURRENT, IGNORE_ANNOTATION_END, IGNORE_ANNOTATION_NEXT, - IGNORE_ANNOTATION_START, + IGNORE_ANNOTATION_START } from "./constants"; import ExtractorResultSets from "./ExtractorResultSets"; import { CSS_SAFELIST } from "./internal-safelist"; @@ -36,7 +36,7 @@ import { RawCSS, ResultPurge, UserDefinedOptions, - UserDefinedSafelist, + UserDefinedSafelist } from "./types"; import { matchAll } from "./utils"; import VariablesStructure from "./VariablesStructure"; @@ -596,7 +596,8 @@ class PurgeCSS { ? option : await asyncFs.readFile(option, "utf-8") : option.raw; - const root = postcss.parse(cssContent); + const isFromFile = typeof option === "string" && !this.options.stdin + const root = postcss.parse(cssContent, { from: isFromFile ? option : undefined }); // purge unused selectors this.walkThroughCSS(root, selectors); @@ -605,11 +606,16 @@ class PurgeCSS { if (this.options.keyframes) this.removeUnusedKeyframes(); if (this.options.variables) this.removeUnusedCSSVariables(); + const postCSSResult = root.toResult({ map: this.options.sourceMap }); const result: ResultPurge = { - css: root.toString(), + css: postCSSResult.toString(), file: typeof option === "string" ? option : option.name, }; + if (this.options.sourceMap) { + result.sourceMap = postCSSResult.map?.toString(); + } + if (this.options.rejected) { result.rejected = Array.from(this.selectorsRemoved); this.selectorsRemoved.clear(); @@ -621,6 +627,7 @@ class PurgeCSS { .toString(); } + sources.push(result); } return sources; diff --git a/packages/purgecss/src/options.ts b/packages/purgecss/src/options.ts index 9ca31bf3..17d460e7 100644 --- a/packages/purgecss/src/options.ts +++ b/packages/purgecss/src/options.ts @@ -1,5 +1,8 @@ import { ExtractorResult, Options } from "./types/"; +/** + * @public + */ export const defaultOptions: Options = { css: [], content: [], @@ -10,6 +13,7 @@ export const defaultOptions: Options = { keyframes: false, rejected: false, rejectedCss: false, + sourceMap: false, stdin: false, stdout: false, variables: false, diff --git a/packages/purgecss/src/types/index.ts b/packages/purgecss/src/types/index.ts index fe8b917c..375fbce7 100644 --- a/packages/purgecss/src/types/index.ts +++ b/packages/purgecss/src/types/index.ts @@ -1,6 +1,13 @@ import * as postcss from "postcss"; +/** + * @public + */ export type PostCSSRoot = postcss.Root; + +/** + * @internal + */ export interface AtRules { fontFace: Array<{ name: string; @@ -10,16 +17,25 @@ export interface AtRules { keyframes: postcss.AtRule[]; } +/** + * @public + */ export interface RawContent { extension: string; raw: T; } +/** + * @public + */ export interface RawCSS { raw: string; name?: string; } +/** + * @public + */ export interface ExtractorResultDetailed { attributes: { names: string[]; @@ -31,45 +47,109 @@ export interface ExtractorResultDetailed { undetermined: string[]; } +/** + * @public + */ export type ExtractorResult = ExtractorResultDetailed | string[]; +/** + * @public + */ export type ExtractorFunction = (content: T) => ExtractorResult; +/** + * @public + */ export interface Extractors { extensions: string[]; extractor: ExtractorFunction; } +/** + * @internal + */ export type IgnoreType = "end" | "start" | "next"; +/** + * @public + */ export type StringRegExpArray = Array; +/** + * @public + */ export type ComplexSafelist = { standard?: StringRegExpArray; + /** + * You can safelist selectors and their children based on a regular + * expression with `safelist.deep` + * + * @example + * + * ```ts + * const purgecss = await new PurgeCSS().purge({ + * content: [], + * css: [], + * safelist: { + * deep: [/red$/] + * } + * }) + * ``` + * + * In this example, selectors such as `.bg-red .child-of-bg` will be left + * in the final CSS, even if `child-of-bg` is not found. + * + */ deep?: RegExp[]; greedy?: RegExp[]; variables?: StringRegExpArray; keyframes?: StringRegExpArray; }; +/** + * @public + */ export type UserDefinedSafelist = StringRegExpArray | ComplexSafelist; +/** + * Options used by PurgeCSS to remove unused CSS + * + * @public + */ export interface UserDefinedOptions { + /** {@inheritDoc Options.content} */ content: Array; + /** {@inheritDoc Options.css} */ css: Array; + /** {@inheritDoc Options.defaultExtractor} */ defaultExtractor?: ExtractorFunction; + /** {@inheritDoc Options.extractors} */ extractors?: Array; + /** {@inheritDoc Options.fontFace} */ fontFace?: boolean; + /** {@inheritDoc Options.keyframes} */ keyframes?: boolean; + /** {@inheritDoc Options.output} */ output?: string; + /** {@inheritDoc Options.rejected} */ rejected?: boolean; + /** {@inheritDoc Options.rejectedCss} */ rejectedCss?: boolean; + /** {@inheritDoc Options.sourceMap } */ + sourceMap?: boolean | postcss.SourceMapOptions + /** {@inheritDoc Options.stdin} */ stdin?: boolean; + /** {@inheritDoc Options.stdout} */ stdout?: boolean; + /** {@inheritDoc Options.variables} */ variables?: boolean; + /** {@inheritDoc Options.safelist} */ safelist?: UserDefinedSafelist; + /** {@inheritDoc Options.blocklist} */ blocklist?: StringRegExpArray; + /** {@inheritDoc Options.skippedContentGlobs} */ skippedContentGlobs?: Array; + /** {@inheritDoc Options.dynamicAttributes} */ dynamicAttributes?: string[]; } @@ -77,11 +157,48 @@ export interface UserDefinedOptions { * Options used by PurgeCSS to remove unused CSS * Those options are used internally * @see {@link UserDefinedOptions} for the options defined by the user + * + * @public */ export interface Options { /** * You can specify content that should be analyzed by PurgeCSS with an * array of filenames or globs. The files can be HTML, Pug, Blade, etc. + * + * @example + * + * ```ts + * await new PurgeCSS().purge({ + * content: ['index.html', '*.js', '*.html', '*.vue'], + * css: ['css/app.css'] + * }) + * ``` + * + * @example + * PurgeCSS also works with raw content. To do this, you need to pass an + * object with the `raw` property instead of a filename. To work properly + * with custom extractors you need to pass the `extension` property along + * with the raw content. + * + * ```ts + * await new PurgeCSS().purge({ + * content: [ + * { + * raw: '
', + * extension: 'html' + * }, + * '*.js', + * '*.html', + * '*.vue' + * ], + * css: [ + * { + * raw: 'body { margin: 0 }' + * }, + * 'css/app.css' + * ] + * }) + * ``` */ content: Array; /** @@ -91,11 +208,28 @@ export interface Options { css: Array; defaultExtractor: ExtractorFunction; extractors: Array; + /** + * If there are any unused \@font-face rules in your css, you can remove + * them by setting the `fontFace` option to `true`. + * + * @defaultValue `false` + * + * @example + * ```ts + * await new PurgeCSS().purge({ + * content: ['index.html', '*.js', '*.html', '*.vue'], + * css: ['css/app.css'], + * fontFace: true + * }) + * ``` + */ fontFace: boolean; keyframes: boolean; output?: string; rejected: boolean; rejectedCss: boolean; + /** {@inheritDoc postcss#SourceMapOptions} */ + sourceMap: boolean | postcss.SourceMapOptions stdin: boolean; stdout: boolean; variables: boolean; @@ -124,8 +258,17 @@ export interface Options { dynamicAttributes: string[]; } +/** + * @public + */ export interface ResultPurge { css: string; + /** + * sourceMap property will be empty if + * {@link UserDefinedOptions.sourceMap} inline is not set to false, as the + * source map will be contained within the text of ResultPurge.css + */ + sourceMap?: string; rejectedCss?: string; file?: string; rejected?: string[];