Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 1 addition & 26 deletions playground/src/index.css
Original file line number Diff line number Diff line change
@@ -1,26 +1 @@
body {
margin: 0;
color: #fff;
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
background-image: linear-gradient(to bottom, #020917, #101725);
}

.content {
display: flex;
min-height: 100vh;
line-height: 1.1;
text-align: center;
flex-direction: column;
justify-content: center;
}

.content h1 {
font-size: 3.6rem;
font-weight: 700;
}

.content p {
font-size: 1.2rem;
font-weight: 400;
opacity: 0.5;
}
@tailwind utilities;
2 changes: 1 addition & 1 deletion playground/src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import './index.css';

document.querySelector('#root').innerHTML = `
<div class="content">
<div class="flex">
<h1>Vanilla Rsbuild</h1>
<p>Start building amazing things with Rsbuild.</p>
</div>
Expand Down
20 changes: 20 additions & 0 deletions src/Set.prototype.isSubsetOf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* See: {@link https://github.com/tc39/proposal-set-methods | Set Methods for JavaScript}
*/
export function isSubsetOf<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
// Node.js v22 will have native implementation.
if (typeof a.isSubsetOf === 'function') {
return a.isSubsetOf(b);
}

if (a.size > b.size) {
return false;
}

for (const item of a) {
if (!b.has(item)) {
return false;
}
}
return true;
}
84 changes: 65 additions & 19 deletions src/TailwindCSSRspackPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { pathToFileURL } from 'node:url';

import { createFilter } from '@rollup/pluginutils';
import type { PostCSSLoaderOptions, Rspack } from '@rsbuild/core';
import type { Processor } from 'postcss';

import { isSubsetOf } from './Set.prototype.isSubsetOf.js';

/**
* The options for {@link TailwindRspackPlugin}.
Expand Down Expand Up @@ -176,6 +179,11 @@ export type { TailwindRspackPluginOptions };
class TailwindRspackPluginImpl {
name = 'TailwindRspackPlugin';

static #postcssProcessorCache = new Map<
/** entryName */ string,
[entryModules: ReadonlySet<string>, Processor]
>();

constructor(
private compiler: Rspack.Compiler,
private options: TailwindRspackPluginOptions,
Expand All @@ -185,7 +193,6 @@ class TailwindRspackPluginImpl {
resolve: compiler.options.context!,
});

const { RawSource } = compiler.webpack.sources;
compiler.hooks.thisCompilation.tap(this.name, (compilation) => {
compilation.hooks.processAssets.tapPromise(this.name, async () => {
await Promise.all(
Expand All @@ -202,6 +209,20 @@ class TailwindRspackPluginImpl {
return;
}

const cache =
TailwindRspackPluginImpl.#postcssProcessorCache.get(entryName);
if (compiler.modifiedFiles && cache) {
const [cachedEntryModules, cachedPostcssProcessor] = cache;
if (isSubsetOf(compiler.modifiedFiles, cachedEntryModules)) {
await this.#transformCSSAssets(
compilation,
cachedPostcssProcessor,
cssFiles,
);
return;
}
}

// collect all the modules corresponding to specific entry
const entryModules = new Set<string>();

Expand All @@ -213,6 +234,18 @@ class TailwindRspackPluginImpl {
}
}

if (compiler.modifiedFiles && cache) {
const [cachedEntryModules, cachedPostcssProcessor] = cache;
if (isSubsetOf(entryModules, cachedEntryModules)) {
await this.#transformCSSAssets(
compilation,
cachedPostcssProcessor,
cssFiles,
);
return;
}
}

const [
{ default: postcss },
{ default: tailwindcss },
Expand All @@ -226,7 +259,7 @@ class TailwindRspackPluginImpl {
),
]);

const postcssTransform = postcss([
const processor = postcss([
...(options.postcssOptions?.plugins ?? []),
// We use a config path to avoid performance issue of TailwindCSS
// See: https://github.com/tailwindlabs/tailwindcss/issues/14229
Expand All @@ -235,30 +268,43 @@ class TailwindRspackPluginImpl {
}),
]);

// iterate all css asset in entry and inject entry modules into tailwind content
await Promise.all(
cssFiles.map(async (asset) => {
const content = asset.source.source();
// transform .css which contains tailwind mixin
// FIXME: add custom postcss config
const transformResult = await postcssTransform.process(
content,
{ from: asset.name, ...options.postcssOptions },
);
// FIXME: add sourcemap support
compilation.updateAsset(
asset.name,
new RawSource(transformResult.css),
);
}),
);
TailwindRspackPluginImpl.#postcssProcessorCache.set(entryName, [
entryModules,
processor,
]);

await this.#transformCSSAssets(compilation, processor, cssFiles);
},
),
);
});
});
}

async #transformCSSAssets(
compilation: Rspack.Compilation,
postcssProcessor: Processor,
cssFiles: Array<Rspack.Asset>,
) {
const { RawSource } = this.compiler.webpack.sources;

// iterate all css asset in entry and inject entry modules into tailwind content
await Promise.all(
cssFiles.map(async (asset) => {
const content = asset.source.source();
// transform .css which contains tailwind mixin
// FIXME: add custom postcss config
const transformResult = await postcssProcessor.process(content, {
from: asset.name,
...this.options.postcssOptions,
});
// FIXME: avoid `updateAsset` when no change is found.
// FIXME: add sourcemap support
compilation.updateAsset(asset.name, new RawSource(transformResult.css));
}),
);
}

async ensureTempDir(entryName: string): Promise<string> {
const prefix = path.join(tmpdir(), entryName);
await mkdir(path.dirname(prefix), { recursive: true });
Expand Down
46 changes: 46 additions & 0 deletions test/isSubsetOf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { expect, test } from '@playwright/test';

import { isSubsetOf } from '../src/Set.prototype.isSubsetOf';

test.describe('Set.prototype.isSubsetOf', () => {
test('subsets', () => {
const set1 = new Set([1, 2, 3]);
const set2 = new Set([4, 5, 6]);
const set3 = new Set([1, 2, 3, 4, 5, 6]);

expect(isSubsetOf(set1, set2)).toBe(false);
expect(isSubsetOf(set2, set1)).toBe(false);
expect(isSubsetOf(set1, set3)).toBe(true);
expect(isSubsetOf(set2, set3)).toBe(true);
});

test('empty sets', () => {
const s1 = new Set([]);
const s2 = new Set([1, 2]);

expect(isSubsetOf(s1, s2)).toBe(true);

const s3 = new Set([1, 2]);
const s4 = new Set([]);

expect(isSubsetOf(s3, s4)).toBe(false);

const s5 = new Set([]);
const s6 = new Set([]);

expect(isSubsetOf(s5, s6)).toBe(true);
});

test('self', () => {
const s1 = new Set([1, 2]);

expect(isSubsetOf(s1, s1)).toBe(true);
});

test('same', () => {
const s1 = new Set([1, 2]);
const s2 = new Set([1, 2]);

expect(isSubsetOf(s1, s2)).toBe(true);
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"outDir": "./dist",
"baseUrl": "./",
"target": "ES2021",
"lib": ["DOM", "ES2021"],
"lib": ["DOM", "ES2021", "ESNext.Collection"],
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
Expand Down
Loading