Skip to content

Commit

Permalink
fix: SSR builds interfering with client builds (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
AdrianGonz97 committed Apr 25, 2024
1 parent b35ebf8 commit a248a67
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-jokes-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'vite-plugin-tailwind-purgecss': patch
---

fix: SSR builds no longer interfere with client builds
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,13 @@
"dependencies": {
"chalk": "^5.3.0",
"css-tree": "^2.3.1",
"estree-walker": "^3.0.3",
"fast-glob": "^3.3.2",
"purgecss": "^6.0.0",
"purgecss-from-html": "^6.0.0"
},
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@types/css-tree": "^2.3.7",
"@types/estree": "^1.0.5",
"@types/node": "^18.11.18",
"prettier": "^2.8.1",
"tailwindcss": "^3.3.5",
Expand Down
13 changes: 1 addition & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
4 changes: 1 addition & 3 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import color from 'chalk';
import path from 'node:path';
import type { ResolvedConfig } from 'vite';

export let log: ReturnType<typeof createLogger>;
export type Logger = ReturnType<typeof createLogger>;

export function createLogger(viteConfig: ResolvedConfig) {
const PREFIX = color.cyan('[vite-plugin-tailwind-purgecss]: ');
Expand All @@ -18,7 +18,5 @@ export function createLogger(viteConfig: ResolvedConfig) {
},
};

log = logger;

return logger;
}
23 changes: 23 additions & 0 deletions src/purgecss-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const getDefaultPurgeOptions = () => ({
css: [],
content: [],
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: [],
});

0 comments on commit a248a67

Please sign in to comment.