Skip to content

Commit

Permalink
fix: fix glob regression via fast-glob + parallel io
Browse files Browse the repository at this point in the history
This fixes the glob regression issues from v5 + parallelizes the async IO operations for faster purge.

## Proposed changes

Fixes an issue that the previously working glob expressions no longer work in v6 and v7. This changes the glob dependency to fast-glob to fix the issue. I also parallelized the async-io operations for faster purge.
Fixes FullHuman#1266

## Types of changes

What types of changes does your code introduce?
_Put an `x` in the boxes that apply_

- [x] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

## Checklist

_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._

- [x] Lint and unit tests pass locally with my changes
- [x] I have added tests that prove my fix is effective or that my feature works
- [ ] I have added necessary documentation (if appropriate)

## Further comments
  • Loading branch information
aminya committed Dec 15, 2024
1 parent 7d86fb5 commit c43c169
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 294 deletions.
292 changes: 91 additions & 201 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/gulp-purgecss/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"test": "jest"
},
"dependencies": {
"glob": "^11.0.0",
"fast-glob": "^3.3.2",
"plugin-error": "^2.0.0",
"purgecss": "^7.0.2",
"through2": "^4.0.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/gulp-purgecss/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as glob from "glob";
import glob from "fast-glob";
import PluginError from "plugin-error";
import { PurgeCSS } from "purgecss";
import * as internal from "stream";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const path = require("path");
const glob = require("glob");
const glob = require("fast-glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const {PurgeCSSPlugin} = require("../../../src/");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const path = require("path");
const glob = require("glob");
const glob = require("fast-glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { PurgeCSSPlugin } = require("../../../src/");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const path = require("path");
const glob = require("glob");
const glob = require("fast-glob");

const customExtractor = (content) => {
const res = content.match(/[A-z0-9-:/]+/g) || [];
Expand All @@ -19,4 +19,4 @@ module.exports = {
extensions: ["html", "js"],
},
],
}
}
2 changes: 1 addition & 1 deletion packages/purgecss/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
},
"dependencies": {
"commander": "^12.1.0",
"glob": "^11.0.0",
"fast-glob": "^3.3.2",
"postcss": "^8.4.47",
"postcss-selector-parser": "^6.1.2"
},
Expand Down
183 changes: 97 additions & 86 deletions packages/purgecss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import * as fs from "fs";
import * as glob from "glob";
import glob from "fast-glob";
import * as path from "path";
import * as postcss from "postcss";
import selectorParser from "postcss-selector-parser";
Expand Down Expand Up @@ -443,31 +443,36 @@ class PurgeCSS {
): Promise<ExtractorResultSets> {
const selectors = new ExtractorResultSets([]);
const filesNames: string[] = [];
for (const globFile of files) {
try {
await asyncFs.access(globFile, fs.constants.F_OK);
filesNames.push(globFile);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
filesNames.push(
...glob.sync(globFile, {
nodir: true,
ignore: this.options.skippedContentGlobs.map((glob) =>
glob.replace(/^\.\//, ""),
),
}),
);
}
}
await Promise.all(
files.map(async (globFile) => {
try {
await asyncFs.access(globFile, fs.constants.F_OK);
filesNames.push(globFile);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
filesNames.push(
...(await glob(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);
}
await Promise.all(
filesNames.map(async (file) => {
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;
}

Expand All @@ -482,11 +487,13 @@ class PurgeCSS {
extractors: Extractors[],
): Promise<ExtractorResultSets> {
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);
}
await Promise.all(
content.map(async ({ raw, extension }) => {
const extractor = this.getFileExtractor(`.${extension}`, extractors);
const extractedSelectors = await extractSelectors(raw, extractor);
selectors.merge(extractedSelectors);
}),
);
return selectors;
}

Expand Down Expand Up @@ -637,71 +644,75 @@ class PurgeCSS {
cssOptions: Array<string | RawCSS>,
selectors: ExtractorResultSets,
): Promise<ResultPurge[]> {
const sources = [];
const sources: ResultPurge[] = [];

// resolve any globs
const processedOptions: Array<string | RawCSS> = [];
for (const option of cssOptions) {
if (typeof option === "string") {
processedOptions.push(
...glob.sync(option, {
nodir: 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.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
: undefined,
});
const result: ResultPurge = {
css: postCSSResult.toString(),
file: typeof option === "string" ? option : option.name,
};
await Promise.all(
cssOptions.map(async (option) => {
if (typeof option === "string") {
processedOptions.push(
...(await glob(option, {
onlyFiles: true,
ignore: this.options.skippedContentGlobs,
})),
);
} else {
processedOptions.push(option);
}
}),
);

if (this.options.sourceMap) {
result.sourceMap = postCSSResult.map?.toString();
}
await Promise.all(
processedOptions.map(async (option) => {
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.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
: undefined,
});
const result: ResultPurge = {
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();
}
if (this.options.rejected) {
result.rejected = Array.from(this.selectorsRemoved);
this.selectorsRemoved.clear();
}

if (this.options.rejectedCss) {
result.rejectedCss = postcss
.root({ nodes: this.removedNodes })
.toString();
}
if (this.options.rejectedCss) {
result.rejectedCss = postcss
.root({ nodes: this.removedNodes })
.toString();
}

sources.push(result);
}
sources.push(result);
}),
);
return sources;
}

Expand Down

0 comments on commit c43c169

Please sign in to comment.