Skip to content

Commit

Permalink
replaced estree walker and fixed leaky blocklist
Browse files Browse the repository at this point in the history
  • Loading branch information
AdrianGonz97 committed Apr 25, 2024
1 parent 58b85cb commit 78ecbfc
Showing 1 changed file with 34 additions and 45 deletions.
79 changes: 34 additions & 45 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,31 @@ import path from 'node:path';
import fg from 'fast-glob';
import color from 'chalk';
import * as css from 'css-tree';
import * as estree from 'estree-walker';
import htmlExtractor from 'purgecss-from-html';
import { normalizePath, type ResolvedConfig, type Plugin } from 'vite';
import { PurgeCSS, mergeExtractorSelectors, standardizeSafelist, defaultOptions } from 'purgecss';
import { PurgeCSS, mergeExtractorSelectors, standardizeSafelist } from 'purgecss';
import {
resolveTailwindConfig,
defaultExtractor,
getContentPaths,
getTailwindClasses,
standardizeTWSafelist,
} from './tailwind.js';
import { log, createLogger } from './logger.js';
import { createLogger, type Logger } from './logger.js';
import { getDefaultPurgeOptions } from './purgecss-options.js';
import type { ExtractorResultDetailed } from 'purgecss';
import type { Node } from 'estree';
import type { PurgeOptions } from './types.js';

const EXT_CSS = /\.(css)$/;
// cache
const files = new Set<string>();
const contentFiles = new Set<string>();
const htmlFiles: string[] = [];

export function purgeCss(purgeOptions?: PurgeOptions): Plugin {
const DEBUG = purgeOptions?.debug ?? false;
const LEGACY = purgeOptions?.legacy ?? false;

let log: Logger;
let viteConfig: ResolvedConfig;

const tailwindConfig = resolveTailwindConfig(purgeOptions?.tailwindConfigPath);
Expand All @@ -54,33 +54,36 @@ export function purgeCss(purgeOptions?: PurgeOptions): Plugin {
apply: 'build',
enforce: 'post',

load(id) {
if (!files.has(id)) return;
// module is included in tailwind's `content` field
moduleIds.add(id);
},

configResolved(config) {
viteConfig = config;
createLogger(viteConfig);
log = createLogger(viteConfig);

// if the files haven't been cached
if (files.size === 0) {
// if the content files haven't been cached
if (contentFiles.size === 0) {
const contentGlobs = getContentPaths(tailwindConfig.content).map((p) => normalizePath(p));
for (const file of fg.globSync(contentGlobs, { cwd: viteConfig.root, absolute: true })) {
if (file.endsWith('.html')) htmlFiles.push(file);
files.add(file);
contentFiles.add(file);
}
}
},

load(id) {
if (!contentFiles.has(id)) return;
// module is included in tailwind's `content` field
moduleIds.add(id);
},

async generateBundle(options, bundle) {
type ChunkOrAsset = (typeof bundle)[string];
type Asset = Extract<ChunkOrAsset, { type: 'asset' }>;
const includedModules: Array<{ raw: string; extension: string }> = [];
const includedAssets: Array<{ raw: string; name: string }> = [];
const extensions = new Set<string>();

const savedTWClasses = new Set<string>();
const generatedTWClasses = new Set<string>();

log.clear();
if (DEBUG) log.info(`${color.greenBright('DEBUG mode activated')}.`);
if (LEGACY) {
Expand All @@ -91,14 +94,13 @@ export function purgeCss(purgeOptions?: PurgeOptions): Plugin {

const purgecss = new PurgeCSS();
purgecss.options = {
...defaultOptions,
...purgeOptions,
...getDefaultPurgeOptions(),
defaultExtractor: extractor,
safelist: standardizeSafelist(safelist),
rejected: DEBUG,
rejectedCss: DEBUG,
};

const generatedTWClasses = new Set<string>();
// a list of selectors found in the original stylesheets
const baseSelectors: ExtractorResultDetailed = {
attributes: { names: [], values: [] },
Expand Down Expand Up @@ -145,7 +147,6 @@ export function purgeCss(purgeOptions?: PurgeOptions): Plugin {
baseSelectors.classes.push(escapedCN);
if (tw.isClass(escapedCN)) {
generatedTWClasses.add(escapedCN);
return;
}
}
},
Expand All @@ -159,44 +160,30 @@ export function purgeCss(purgeOptions?: PurgeOptions): Plugin {
if (info?.isIncluded !== true || info.code === null) continue;

// compiled JS code
includedModules.push({ raw: info.code, extension: 'js' });
const source = fs.readFileSync(id, { encoding: 'utf8' });
includedModules.push({ raw: source, extension: 'tw' });

if (LEGACY) {
// plucks out the `.` (e.g. `.html` -> `html`)
const extension = path.parse(id).ext.slice(1);
extensions.add(extension);
// source code
const source = fs.readFileSync(id, { encoding: 'utf8' });
includedModules.push({ raw: source, extension });
}
}

// not TW classes, but are possibly a selector (used for legacy mode)
const possibleSelectors = new Set<string>();
for (const mod of includedModules) {
if (mod.extension !== 'js') continue;
const ast = this.parse(mod.raw) as Node;

estree.walk(ast, {
enter(node) {
if (node.type === 'Literal' && typeof node.value === 'string') {
const value = node.value;
for (const selector of extractor(value)) {
if (!generatedTWClasses.delete(selector)) possibleSelectors.add(selector);
}
}
if (node.type === 'TemplateElement') {
const value = node.value.cooked ?? node.value.raw;
for (const selector of extractor(value)) {
if (!generatedTWClasses.delete(selector)) possibleSelectors.add(selector);
}
}
if (node.type === 'Identifier') {
const selector = node.name;
if (!generatedTWClasses.delete(selector)) possibleSelectors.add(selector);
}
},
});
if (mod.extension !== 'tw') continue;

for (const selector of extractor(mod.raw)) {
if (generatedTWClasses.delete(selector)) {
savedTWClasses.add(selector);
} else {
possibleSelectors.add(selector);
}
}
}

const htmlSelectors = await purgecss.extractSelectorsFromFiles(htmlFiles, [
Expand All @@ -213,7 +200,7 @@ export function purgeCss(purgeOptions?: PurgeOptions): Plugin {
if (LEGACY) purgecss.options.safelist.standard.push(...possibleSelectors);

// excludes `js` files as they are handled separately above
extensions.delete('js');
extensions.delete('tw');
const moduleSelectors = await purgecss.extractSelectorsFromString(includedModules, [
{ extractor, extensions: Array.from(extensions) },
...(purgeOptions?.purgecss?.extractors ?? []),
Expand All @@ -225,13 +212,15 @@ export function purgeCss(purgeOptions?: PurgeOptions): Plugin {
baseSelectors
);

// purge the stylesheets
const purgeResults = await purgecss.getPurgedCSS(includedAssets, mergedSelectors);

if (DEBUG) {
console.dir(
{
possible_selectors: mergedSelectors,
tailwind_classes_to_remove: generatedTWClasses,
tailwind_classes_to_keep: savedTWClasses,
purgecss_results: purgeResults,
},
{ maxArrayLength: Infinity, maxStringLength: Infinity, depth: Infinity }
Expand Down

0 comments on commit 78ecbfc

Please sign in to comment.