UNPKG

purgecss

Version:

Remove unused css selectors

1,071 lines (1,060 loc) 37.5 kB
'use strict'; var fs = require('fs'); var glob = require('fast-glob'); var path = require('path'); var postcss = require('postcss'); var selectorParser = require('postcss-selector-parser'); var util = require('util'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs); var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob); var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path); var postcss__namespace = /*#__PURE__*/_interopNamespaceDefault(postcss); const IGNORE_ANNOTATION_CURRENT = "purgecss ignore current"; const IGNORE_ANNOTATION_NEXT = "purgecss ignore"; const IGNORE_ANNOTATION_START = "purgecss start ignore"; const IGNORE_ANNOTATION_END = "purgecss end ignore"; const CONFIG_FILENAME = "purgecss.config.js"; // Error Message const ERROR_CONFIG_FILE_LOADING = "Error loading the config file"; function mergeSets(into, from) { if (from) { from.forEach(into.add, into); } } /** * @public */ class ExtractorResultSets { constructor(er) { this.undetermined = new Set(); this.attrNames = new Set(); this.attrValues = new Set(); this.classes = new Set(); this.ids = new Set(); this.tags = new Set(); this.merge(er); } merge(that) { if (Array.isArray(that)) { mergeSets(this.undetermined, that); } else if (that instanceof ExtractorResultSets) { mergeSets(this.undetermined, that.undetermined); mergeSets(this.attrNames, that.attrNames); mergeSets(this.attrValues, that.attrValues); mergeSets(this.classes, that.classes); mergeSets(this.ids, that.ids); mergeSets(this.tags, that.tags); } else { // ExtractorResultDetailed: mergeSets(this.undetermined, that.undetermined); if (that.attributes) { mergeSets(this.attrNames, that.attributes.names); mergeSets(this.attrValues, that.attributes.values); } mergeSets(this.classes, that.classes); mergeSets(this.ids, that.ids); mergeSets(this.tags, that.tags); } return this; } hasAttrName(name) { return this.attrNames.has(name) || this.undetermined.has(name); } someAttrValue(predicate) { for (const val of this.attrValues) { if (predicate(val)) return true; } for (const val of this.undetermined) { if (predicate(val)) return true; } return false; } hasAttrPrefix(prefix) { return this.someAttrValue((value) => value.startsWith(prefix)); } hasAttrSuffix(suffix) { return this.someAttrValue((value) => value.endsWith(suffix)); } hasAttrSubstr(substr) { const wordSubstr = substr.trim().split(" "); return wordSubstr.every((word) => this.someAttrValue((value) => value.includes(word))); } hasAttrValue(value) { return this.attrValues.has(value) || this.undetermined.has(value); } hasClass(name) { return this.classes.has(name) || this.undetermined.has(name); } hasId(id) { return this.ids.has(id) || this.undetermined.has(id); } hasTag(tag) { return this.tags.has(tag) || this.undetermined.has(tag); } } const CSS_SAFELIST = ["*", ":root", ":after", ":before"]; /** * @public */ const defaultOptions = { css: [], content: [], defaultExtractor: (content) => content.match(/[A-Za-z0-9_-]+/g) || [], extractors: [], fontFace: false, keyframes: false, rejected: false, rejectedCss: false, sourceMap: false, stdin: false, stdout: false, variables: false, safelist: { standard: [], deep: [], greedy: [], variables: [], keyframes: [], }, blocklist: [], skippedContentGlobs: [], dynamicAttributes: [], }; /** * @public */ class VariableNode { constructor(declaration) { this.nodes = []; this.isUsed = false; this.value = declaration; } } /** * @public */ class VariablesStructure { constructor() { this.nodes = new Map(); this.usedVariables = new Set(); this.safelist = []; } addVariable(declaration) { const { prop } = declaration; if (!this.nodes.has(prop)) { const node = new VariableNode(declaration); this.nodes.set(prop, [node]); } else { const node = new VariableNode(declaration); const variableNodes = this.nodes.get(prop) || []; this.nodes.set(prop, [...variableNodes, node]); } } addVariableUsage(declaration, matchedVariables) { const { prop } = declaration; const nodes = this.nodes.get(prop); for (const variableMatch of matchedVariables) { // capturing group containing the variable is in index 1 const variableName = variableMatch[1]; if (this.nodes.has(variableName)) { const usedVariableNodes = this.nodes.get(variableName); nodes === null || nodes === void 0 ? void 0 : nodes.forEach((node) => { usedVariableNodes === null || usedVariableNodes === void 0 ? void 0 : usedVariableNodes.forEach((usedVariableNode) => node.nodes.push(usedVariableNode)); }); } } } addVariableUsageInProperties(matchedVariables) { for (const variableMatch of matchedVariables) { // capturing group containing the variable is in index 1 const variableName = variableMatch[1]; this.usedVariables.add(variableName); } } setAsUsed(variableName) { const nodes = this.nodes.get(variableName); if (nodes) { const queue = [...nodes]; while (queue.length !== 0) { const currentNode = queue.pop(); if (currentNode && !currentNode.isUsed) { currentNode.isUsed = true; queue.push(...currentNode.nodes); } } } } removeUnused() { // check unordered usage for (const used of this.usedVariables) { const usedNodes = this.nodes.get(used); if (usedNodes) { for (const usedNode of usedNodes) { const usedVariablesMatchesInDeclaration = usedNode.value.value.matchAll(/var\((.+?)[,)]/g); for (const usage of usedVariablesMatchesInDeclaration) { if (!this.usedVariables.has(usage[1])) { this.usedVariables.add(usage[1]); } } } } } for (const used of this.usedVariables) { this.setAsUsed(used); } for (const [name, declarations] of this.nodes) { for (const declaration of declarations) { if (!declaration.isUsed && !this.isVariablesSafelisted(name)) { declaration.value.remove(); } } } } isVariablesSafelisted(variable) { return this.safelist.some((safelistItem) => { return typeof safelistItem === "string" ? safelistItem === variable : safelistItem.test(variable); }); } } /** * Core package of PurgeCSS * * Contains the core methods to analyze the files, remove unused CSS. * * @packageDocumentation */ const asyncFs = { access: util.promisify(fs__namespace.access), readFile: util.promisify(fs__namespace.readFile), }; /** * Format the user defined safelist into a standardized safelist object * * @param userDefinedSafelist - the user defined safelist * @returns the formatted safelist object that can be used in the PurgeCSS options * * @public */ function standardizeSafelist(userDefinedSafelist = []) { if (Array.isArray(userDefinedSafelist)) { return { ...defaultOptions.safelist, standard: userDefinedSafelist, }; } return { ...defaultOptions.safelist, ...userDefinedSafelist, }; } /** * Load the configuration file from the path * * @param configFile - Path of the config file * @returns The options from the configuration file * * @throws Error * This exception is thrown if the configuration file was not imported * * @public */ async function setOptions(configFile = CONFIG_FILENAME) { let options; try { const t = path__namespace.resolve(process.cwd(), configFile); const importedConfig = await import(t); // Handle both ES modules (direct export) and CommonJS (default export) options = importedConfig.default && typeof importedConfig.default === "object" ? importedConfig.default : importedConfig; } catch (err) { if (err instanceof Error) { throw new Error(`${ERROR_CONFIG_FILE_LOADING} ${err.message}`); } throw new Error(); } return { ...defaultOptions, ...options, safelist: standardizeSafelist(options.safelist), }; } /** * Use the extract function to get the list of selectors * * @param content - content (e.g. html file) * @param extractor - PurgeCSS extractor used to extract the selectors * @returns the sets containing the result of the extractor function */ async function extractSelectors(content, extractor) { return new ExtractorResultSets(await extractor(content)); } /** * Check if the node is a css comment indication to ignore the selector rule * * @param node - node of postcss AST * @param type - type of css comment * @returns true if the node is a PurgeCSS ignore comment */ function isIgnoreAnnotation(node, type) { switch (type) { case "next": return node.text.includes(IGNORE_ANNOTATION_NEXT); case "start": return node.text.includes(IGNORE_ANNOTATION_START); case "end": return node.text.includes(IGNORE_ANNOTATION_END); } } /** * Check if the node correspond to an empty css rule * * @param node - node of postcss AST * @returns true if the rule is empty */ function isRuleEmpty(node) { if ((isPostCSSRule(node) && !node.selector) || ((node === null || node === void 0 ? void 0 : node.nodes) && !node.nodes.length) || (isPostCSSAtRule(node) && ((!node.nodes && !node.params) || (!node.params && node.nodes && !node.nodes.length)))) { return true; } return false; } /** * Check if the node has a css comment indicating to ignore the current selector rule * * @param rule - rule of postcss AST */ function hasIgnoreAnnotation(rule) { let found = false; rule.walkComments((node) => { if (node && node.type === "comment" && node.text.includes(IGNORE_ANNOTATION_CURRENT)) { found = true; node.remove(); } }); return found; } /** * Merge two extractor selectors * * @param extractorSelectorsA - extractor selectors A * @param extractorSelectorsB - extractor selectors B * @returns the merged extractor result sets * * @public */ function mergeExtractorSelectors(...extractors) { const result = new ExtractorResultSets([]); extractors.forEach(result.merge, result); return result; } /** * Strips quotes of a string * * @param str - string to be stripped */ function stripQuotes(str) { return str.replace(/(^["'])|(["']$)/g, ""); } /** * Returns true if the attribute is found in the extractor selectors * * @param attributeNode - node of type `attribute` * @param selectors - extractor selectors */ function isAttributeFound(attributeNode, selectors) { if (!selectors.hasAttrName(attributeNode.attribute)) { return false; } if (typeof attributeNode.value === "undefined") { return true; } switch (attributeNode.operator) { case "$=": return selectors.hasAttrSuffix(attributeNode.value); case "~=": case "*=": return selectors.hasAttrSubstr(attributeNode.value); case "=": return selectors.hasAttrValue(attributeNode.value); case "|=": case "^=": return selectors.hasAttrPrefix(attributeNode.value); default: return true; } } /** * Returns true if the class is found in the extractor selectors * * @param classNode - node of type `class` * @param selectors - extractor selectors */ function isClassFound(classNode, selectors) { return selectors.hasClass(classNode.value); } /** * Returns true if the identifier is found in the extractor selectors * * @param identifierNode - node of type `identifier` * @param selectors - extractor selectors */ function isIdentifierFound(identifierNode, selectors) { return selectors.hasId(identifierNode.value); } /** * Returns true if the tag is found in the extractor selectors * * @param tagNode - node of type `tag` * @param selectors - extractor selectors */ function isTagFound(tagNode, selectors) { return selectors.hasTag(tagNode.value); } /** * Returns true if the selector is inside a pseudo class * (e.g. :nth-child, :nth-of-type, :only-child, :not) * * @param selector - selector */ function isInPseudoClass(selector) { return ((selector.parent && selector.parent.type === "pseudo" && selector.parent.value.startsWith(":")) || false); } /** * Returns true if the selector is inside the pseudo classes :where() or :is() * @param selector - selector */ function isInPseudoClassWhereOrIs(selector) { return ((selector.parent && selector.parent.type === "pseudo" && (selector.parent.value === ":where" || selector.parent.value === ":is")) || false); } /** * Returns true if the selector is a pseudo class at the root level * Pseudo classes checked: :where, :is, :has, :not * @param selector - selector */ function isPseudoClassAtRootLevel(selector) { var _a; let result = false; if (selector.type === "selector" && ((_a = selector.parent) === null || _a === void 0 ? void 0 : _a.type) === "root" && selector.nodes.length === 1) { selector.walk((node) => { if (node.type === "pseudo" && (node.value === ":where" || node.value === ":is" || node.value === ":has" || node.value === ":not")) { result = true; } }); } return result; } function isPostCSSAtRule(node) { return (node === null || node === void 0 ? void 0 : node.type) === "atrule"; } function isPostCSSRule(node) { return (node === null || node === void 0 ? void 0 : node.type) === "rule"; } function isPostCSSComment(node) { return (node === null || node === void 0 ? void 0 : node.type) === "comment"; } /** * Class used to instantiate PurgeCSS and can then be used * to purge CSS files. * * @example * ```ts * await new PurgeCSS().purge({ * content: ['index.html'], * css: ['css/app.css'] * }) * ``` * * @public */ class PurgeCSS { constructor() { this.ignore = false; this.atRules = { fontFace: [], keyframes: [], }; this.usedAnimations = new Set(); this.usedFontFaces = new Set(); this.selectorsRemoved = new Set(); this.removedNodes = []; this.variablesStructure = new VariablesStructure(); this.options = defaultOptions; } collectDeclarationsData(declaration) { const { prop, value } = declaration; // collect css properties data if (this.options.variables) { const usedVariablesMatchesInDeclaration = value.matchAll(/var\((.+?)[,)]/g); if (prop.startsWith("--")) { this.variablesStructure.addVariable(declaration); this.variablesStructure.addVariableUsage(declaration, usedVariablesMatchesInDeclaration); } else { this.variablesStructure.addVariableUsageInProperties(usedVariablesMatchesInDeclaration); } } // collect keyframes data if (this.options.keyframes) { if (prop === "animation" || prop === "animation-name") { for (const word of value.split(/[\s,]+/)) { this.usedAnimations.add(word); } return; } // Also check CSS custom properties for animation names // e.g., --my-animation: fadeIn 0.4s; if (prop.startsWith("--")) { for (const word of value.split(/[\s,]+/)) { this.usedAnimations.add(word); } } } // collect font faces data if (this.options.fontFace) { if (prop === "font-family") { for (const fontName of value.split(",")) { const cleanedFontFace = stripQuotes(fontName.trim()); this.usedFontFaces.add(cleanedFontFace); } } return; } } /** * Get the extractor corresponding to the extension file * @param filename - Name of the file * @param extractors - Array of extractors definition */ getFileExtractor(filename, extractors) { const extractorObj = extractors.find((extractor) => extractor.extensions.find((ext) => filename.endsWith(ext))); return typeof extractorObj === "undefined" ? this.options.defaultExtractor : extractorObj.extractor; } /** * Extract the selectors present in the files using a PurgeCSS extractor * * @param files - Array of files path or glob pattern * @param extractors - Array of extractors */ async extractSelectorsFromFiles(files, extractors) { const selectors = new ExtractorResultSets([]); const filesNames = []; for (const globFile of files) { try { await asyncFs.access(globFile, fs__namespace.constants.F_OK); filesNames.push(globFile); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (err) { filesNames.push(...glob__namespace.sync(globFile, { onlyFiles: true, ignore: this.options.skippedContentGlobs.map((glob) => glob.replace(/^\.\//, "")), })); } } if (files.length > 0 && filesNames.length === 0) { console.warn("No files found from the passed PurgeCSS option 'content'."); } for (const file of filesNames) { const content = await asyncFs.readFile(file, "utf-8"); const extractor = this.getFileExtractor(file, extractors); const extractedSelectors = await extractSelectors(content, extractor); selectors.merge(extractedSelectors); } return selectors; } /** * Extract the selectors present in the passed string using a PurgeCSS extractor * * @param content - Array of content * @param extractors - Array of extractors */ async extractSelectorsFromString(content, extractors) { const selectors = new ExtractorResultSets([]); for (const { raw, extension } of content) { const extractor = this.getFileExtractor(`.${extension}`, extractors); const extractedSelectors = await extractSelectors(raw, extractor); selectors.merge(extractedSelectors); } return selectors; } /** * Evaluate at-rule and register it for future reference * @param node - node of postcss AST */ evaluateAtRule(node) { // keyframes if (this.options.keyframes && node.name.endsWith("keyframes")) { this.atRules.keyframes.push(node); return; } // font-face if (this.options.fontFace && node.name === "font-face" && node.nodes) { for (const childNode of node.nodes) { if (childNode.type === "decl" && childNode.prop === "font-family") { this.atRules.fontFace.push({ name: stripQuotes(childNode.value), node, }); } } } } /** * Evaluate css selector and decide if it should be removed or not * * @param node - node of postcss AST * @param selectors - selectors used in content files */ evaluateRule(node, selectors) { // exit if is in ignoring state activated by an ignore comment if (this.ignore) { return; } // exit if the previous annotation is a ignore next line comment const annotation = node.prev(); if (isPostCSSComment(annotation) && isIgnoreAnnotation(annotation, "next")) { annotation.remove(); return; } // exit if it is inside a keyframes if (node.parent && isPostCSSAtRule(node.parent) && node.parent.name.endsWith("keyframes")) { return; } // exit if it is not a rule if (!isPostCSSRule(node)) { return; } // exit if it has an ignore rule comment inside if (hasIgnoreAnnotation(node)) { return; } const selectorsRemovedFromRule = []; // selector transformer, walk over the list of the parsed selectors twice. // First pass will remove the unused selectors. It goes through // pseudo-classes like :where() and :is() and remove the unused // selectors inside of them, but will not remove the pseudo-classes // themselves. Second pass will remove selectors containing empty // :where and :is. node.selector = selectorParser((selectorsParsed) => { selectorsParsed.walk((selector) => { if (selector.type !== "selector") { return; } const keepSelector = this.shouldKeepSelector(selector, selectors); if (!keepSelector) { if (this.options.rejected) { this.selectorsRemoved.add(selector.toString()); } if (this.options.rejectedCss) { selectorsRemovedFromRule.push(selector.toString()); } selector.remove(); } }); // removes selectors containing empty :where and :is selectorsParsed.walk((selector) => { if (selector.type !== "selector") { return; } if (selector.toString() && /(:where)|(:is)/.test(selector.toString())) { selector.walk((node) => { if (node.type !== "pseudo") return; if (node.value !== ":where" && node.value !== ":is") return; if (node.nodes.length === 0) { selector.remove(); } }); } }); }).processSync(node.selector); // declarations if (node.selector && typeof node.nodes !== "undefined") { for (const childNode of node.nodes) { if (childNode.type !== "decl") continue; this.collectDeclarationsData(childNode); } } // remove empty rules const parent = node.parent; if (!node.selector) { node.remove(); } if (isRuleEmpty(parent)) parent === null || parent === void 0 ? void 0 : parent.remove(); // rebuild the rule with the removed selectors and optionally its parent if (this.options.rejectedCss) { if (selectorsRemovedFromRule.length > 0) { const clone = node.clone(); const parentClone = parent === null || parent === void 0 ? void 0 : parent.clone().removeAll().append(clone); clone.selectors = selectorsRemovedFromRule; const nodeToPreserve = parentClone ? parentClone : clone; this.removedNodes.push(nodeToPreserve); } } } /** * Get the purged version of the css based on the files * * @param cssOptions - css options, files or raw strings * @param selectors - set of extracted css selectors */ async getPurgedCSS(cssOptions, selectors) { var _a; const sources = []; // resolve any globs const processedOptions = []; for (const option of cssOptions) { if (typeof option === "string") { processedOptions.push(...glob__namespace.sync(option, { onlyFiles: true, ignore: this.options.skippedContentGlobs, })); } else { processedOptions.push(option); } } for (const option of processedOptions) { const cssContent = typeof option === "string" ? this.options.stdin ? option : await asyncFs.readFile(option, "utf-8") : option.raw; const isFromFile = typeof option === "string" && !this.options.stdin; const root = postcss__namespace.parse(cssContent, { from: isFromFile ? option : undefined, }); // purge unused selectors this.walkThroughCSS(root, selectors); if (this.options.fontFace) this.removeUnusedFontFaces(); if (this.options.keyframes) this.removeUnusedKeyframes(); if (this.options.variables) this.removeUnusedCSSVariables(); const postCSSResult = root.toResult({ map: this.options.sourceMap, to: typeof this.options.sourceMap === "object" && this.options.sourceMap.to ? this.options.sourceMap.to : isFromFile ? option : undefined, }); const result = { css: postCSSResult.toString(), file: typeof option === "string" ? option : option.name, }; if (this.options.sourceMap) { result.sourceMap = (_a = postCSSResult.map) === null || _a === void 0 ? void 0 : _a.toString(); } if (this.options.rejected) { result.rejected = Array.from(this.selectorsRemoved); this.selectorsRemoved.clear(); } if (this.options.rejectedCss) { result.rejectedCss = postcss__namespace .root({ nodes: this.removedNodes }) .toString(); } sources.push(result); } return sources; } /** * Check if the keyframe is safelisted with the option safelist keyframes * * @param keyframesName - name of the keyframe animation */ isKeyframesSafelisted(keyframesName) { return this.options.safelist.keyframes.some((safelistItem) => { return typeof safelistItem === "string" ? safelistItem === keyframesName : safelistItem.test(keyframesName); }); } /** * Check if the selector is blocklisted with the option blocklist * * @param selector - css selector */ isSelectorBlocklisted(selector) { return this.options.blocklist.some((blocklistItem) => { return typeof blocklistItem === "string" ? blocklistItem === selector : blocklistItem.test(selector); }); } /** * Check if the selector is safelisted with the option safelist standard * * @param selector - css selector */ isSelectorSafelisted(selector) { const isSafelisted = this.options.safelist.standard.some((safelistItem) => { return typeof safelistItem === "string" ? safelistItem === selector : safelistItem.test(selector); }); const isPseudoElement = /^::.*/.test(selector); return CSS_SAFELIST.includes(selector) || isPseudoElement || isSafelisted; } /** * Check if the selector is safelisted with the option safelist deep * * @param selector - selector */ isSelectorSafelistedDeep(selector) { return this.options.safelist.deep.some((safelistItem) => safelistItem.test(selector)); } /** * Check if the selector is safelisted with the option safelist greedy * * @param selector - selector */ isSelectorSafelistedGreedy(selector) { return this.options.safelist.greedy.some((safelistItem) => safelistItem.test(selector)); } /** * Remove unused CSS * * @param userOptions - PurgeCSS options or path to the configuration file * @returns an array of object containing the filename and the associated CSS * * @example Using a configuration file named purgecss.config.js * ```ts * const purgeCSSResults = await new PurgeCSS().purge() * ``` * * @example Using a custom path to the configuration file * ```ts * const purgeCSSResults = await new PurgeCSS().purge('./purgecss.config.js') * ``` * * @example Using the PurgeCSS options * ```ts * const purgeCSSResults = await new PurgeCSS().purge({ * content: ['index.html', '**\/*.js', '**\/*.html', '**\/*.vue'], * css: ['css/app.css'] * }) * ``` */ async purge(userOptions) { this.options = typeof userOptions !== "object" ? await setOptions(userOptions) : { ...defaultOptions, ...userOptions, safelist: standardizeSafelist(userOptions.safelist), }; const { content, css, extractors, safelist } = this.options; if (this.options.variables) { this.variablesStructure.safelist = safelist.variables || []; } const fileFormatContents = content.filter((o) => typeof o === "string"); const rawFormatContents = content.filter((o) => typeof o === "object"); const cssFileSelectors = await this.extractSelectorsFromFiles(fileFormatContents, extractors); const cssRawSelectors = await this.extractSelectorsFromString(rawFormatContents, extractors); return this.getPurgedCSS(css, mergeExtractorSelectors(cssFileSelectors, cssRawSelectors)); } /** * Remove unused CSS variables */ removeUnusedCSSVariables() { this.variablesStructure.removeUnused(); } /** * Remove unused font-faces */ removeUnusedFontFaces() { for (const { name, node } of this.atRules.fontFace) { if (!this.usedFontFaces.has(name)) { node.remove(); } } } /** * Remove unused keyframes */ removeUnusedKeyframes() { for (const node of this.atRules.keyframes) { if (!this.usedAnimations.has(node.params) && !this.isKeyframesSafelisted(node.params)) { node.remove(); } } } /** * Transform a selector node into a string */ getSelectorValue(selector) { return ((selector.type === "attribute" && selector.attribute) || selector.value); } /** * Determine if the selector should be kept, based on the selectors found in the files * * @param selector - set of css selectors found in the content files or string * @param selectorsFromExtractor - selectors in the css rule * * @returns true if the selector should be kept in the processed CSS */ shouldKeepSelector(selector, selectorsFromExtractor) { // selectors in pseudo classes are ignored except :where() and :is(). For those pseudo-classes, we are treating the selectors inside the same way as they would be outside. if (isInPseudoClass(selector) && !isInPseudoClassWhereOrIs(selector)) { return true; } if (isPseudoClassAtRootLevel(selector)) { return true; } // if there is any greedy safelist pattern, run all the selector parts through them // if there is any match, return true if (this.options.safelist.greedy.length > 0) { const selectorParts = selector.nodes.map(this.getSelectorValue); if (selectorParts.some((selectorPart) => selectorPart && this.isSelectorSafelistedGreedy(selectorPart))) { return true; } } let isPresent = false; for (const selectorNode of selector.nodes) { const selectorValue = this.getSelectorValue(selectorNode); // if the selector is safelisted with children // returns true to keep all children selectors if (selectorValue && this.isSelectorSafelistedDeep(selectorValue)) { return true; } // The selector is found in the internal and user-defined safelist if (selectorValue && (CSS_SAFELIST.includes(selectorValue) || this.isSelectorSafelisted(selectorValue))) { isPresent = true; continue; } // The selector is present in the blocklist if (selectorValue && this.isSelectorBlocklisted(selectorValue)) { return false; } switch (selectorNode.type) { case "attribute": // `value` is a dynamic attribute, highly used in input element // the choice is to always leave `value` as it can change based on the user // idem for `checked`, `selected`, `open` isPresent = [ ...this.options.dynamicAttributes, "value", "checked", "selected", "open", ].includes(selectorNode.attribute) ? true : isAttributeFound(selectorNode, selectorsFromExtractor); break; case "class": isPresent = isClassFound(selectorNode, selectorsFromExtractor); break; case "id": isPresent = isIdentifierFound(selectorNode, selectorsFromExtractor); break; case "tag": isPresent = isTagFound(selectorNode, selectorsFromExtractor); break; default: continue; } // selector is not safelisted // and it has not been found as an attribute/class/id/tag if (!isPresent) { return false; } } return isPresent; } /** * Walk through the CSS AST and remove unused CSS * * @param root - root node of the postcss AST * @param selectors - selectors used in content files */ walkThroughCSS(root, selectors) { root.walk((node) => { if (node.type === "rule") { return this.evaluateRule(node, selectors); } if (node.type === "atrule") { return this.evaluateAtRule(node); } if (node.type === "comment") { if (isIgnoreAnnotation(node, "start")) { this.ignore = true; // remove ignore annotation node.remove(); } else if (isIgnoreAnnotation(node, "end")) { this.ignore = false; // remove ignore annotation node.remove(); } } }); } } exports.ExtractorResultSets = ExtractorResultSets; exports.PurgeCSS = PurgeCSS; exports.VariableNode = VariableNode; exports.VariablesStructure = VariablesStructure; exports.defaultOptions = defaultOptions; exports.mergeExtractorSelectors = mergeExtractorSelectors; exports.setOptions = setOptions; exports.standardizeSafelist = standardizeSafelist;