fix: deduplicate content script CSS in build output#2268
fix: deduplicate content script CSS in build output#2268oc-wonton wants to merge 2 commits intowxt-dev:mainfrom
Conversation
When content scripts import CSS files, Vite outputs the CSS in two locations:
1. content-scripts/{name}.css (referenced in manifest)
2. assets/{name}.css (duplicate, not needed)
This change adds a post-build deduplication step that removes identical
CSS files from assets/ when they already exist in content-scripts/ with
the same base name and content. This reduces extension package size by
eliminating unnecessary duplicate files.
The deduplication logic:
- Only compares files with matching base names (e.g., both named "content.css")
- Verifies content is identical before removing
- Preserves legitimate CSS files in assets/ (unlisted-style entrypoints)
- Only removes true duplicates created by Vite's build process
Fixes wxt-dev#1987
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
✅ Deploy Preview for creative-fairy-df92c4 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
@oc-wonton When using tailwindcss in content-script and other pages like popup, options etc. The CSS file is created 2 times. Will this pull also fix it or not? See the 2 files. I am also curious why the CSS file is named as my component name, Button-id.css; it should be styles.css
|
|
@aklinker1 Please review this. |
| afterEach(async () => { | ||
| // Cleanup | ||
| await rm(testOutDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
There was a problem hiding this comment.
That isn't necessary, because you're doing it in beforeEach
| const output: Omit<BuildOutput, 'manifest'> = { | ||
| steps: [ | ||
| { | ||
| entrypoints: [] as any, |
There was a problem hiding this comment.
| entrypoints: [] as any, | |
| entrypoints: [], |
This isn't working?
There was a problem hiding this comment.
It isn't possible to avoid creating duplicated css, instead of removing it after create?
There was a problem hiding this comment.
Yes, it's possible. Rollup's generateBundle hook lets a plugin mutate (or delete entries from) the bundle object before Vite writes it to disk. We already use this pattern in cssEntrypoints.ts to drop the unwanted JS output of CSS-only lib-mode builds.
So a cleaner fix would be a small Vite plugin applied in getLibModeConfig (in packages/wxt/src/core/builders/vite/index.ts) for content-script entrypoints, which tracks CSS outputs throughout the build and deduplicates them before writing them to the file. Would be easier to test too (no temp dirs / FS mocking).
@wxt-dev/analytics
@wxt-dev/auto-icons
@wxt-dev/browser
@wxt-dev/i18n
@wxt-dev/is-background
@wxt-dev/module-react
@wxt-dev/module-solid
@wxt-dev/module-svelte
@wxt-dev/module-vue
@wxt-dev/runner
@wxt-dev/storage
@wxt-dev/unocss
@wxt-dev/webextension-polyfill
wxt
commit: |
There was a problem hiding this comment.
Couple of comments.
Also, we should add an experimental for this. We don't have any others right now, but we shouldn't enable this by default for all projects without further testing.
Line 366 in d9c1401
| `); | ||
| }); | ||
|
|
||
| it('should not duplicate content script CSS in assets/ directory', async () => { |
There was a problem hiding this comment.
Ran this test on main, it already passes
| const csPath = resolve(testOutDir, 'content-scripts/content.css'); | ||
| const assetPath = resolve(testOutDir, 'assets/other.css'); |
There was a problem hiding this comment.
I'm really confused about why you keep calling out "content-scripts" vs "assets". They're the same thing, they can both be deduplicated.
There was a problem hiding this comment.
Move this to an E2E test and use the existing TestProject util. Will reduce all the code duplication here, there's a lot of setup because you're recreating the entire FS setup/cleanup.
I would rather have these be E2E tests too, there seems to be too much setup and specific mocking that could be fragile.
There was a problem hiding this comment.
Yes, it's possible. Rollup's generateBundle hook lets a plugin mutate (or delete entries from) the bundle object before Vite writes it to disk. We already use this pattern in cssEntrypoints.ts to drop the unwanted JS output of CSS-only lib-mode builds.
So a cleaner fix would be a small Vite plugin applied in getLibModeConfig (in packages/wxt/src/core/builders/vite/index.ts) for content-script entrypoints, which tracks CSS outputs throughout the build and deduplicates them before writing them to the file. Would be easier to test too (no temp dirs / FS mocking).

Description
When content scripts import CSS files, Vite outputs the CSS in two locations:
content-scripts/{name}.css(referenced in manifest)assets/{name}.css(duplicate, not needed)This adds a post-build deduplication step that removes identical CSS files from
assets/when they already exist incontent-scripts/with the same base name and content.Related Issues
Fixes #1987
Type of Change
Implementation
deduplicateCss()utility inpackages/wxt/src/core/utils/building/deduplicate-css.tsrebuild.ts)assets/(e.g., unlisted-style entrypoints)Testing
content-scripts/and not duplicated inassets/Additional Notes
Based on maintainer guidance in #1987: each content script is built in a separate Vite build, causing CSS to appear in both locations. This fix deduplicates at the output level rather than changing the build pipeline.