Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix: prevent CSS entry from generating empty JS files that overwrite ... #674

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
liwenka1 wants to merge 2 commits into rolldown:main
base: main
Choose a base branch
Loading
from liwenka1:fix/css-entry-lightningcss

Conversation

@liwenka1
Copy link

@liwenka1 liwenka1 commented Dec 27, 2025

Problem

Closes #627

When using CSS files as entries alongside JS files with the same base name (e.g., index.js and index.css), Rolldown generates an empty JS file (0 bytes) for the CSS entry, which overwrites the legitimate JS output.

Example:

// tsdown.config.ts
export default {
 entry: ['index.js', 'index.css'],
}

Before this fix:

  • dist/index.js - 0 bytes (empty, generated from CSS entry)
  • dist/index.css - correct CSS output
  • Original index.js output was overwritten

Solution

I attempted to fix this issue by separating CSS entries from JS entries and processing them independently using LightningCSS. I'm not deeply familiar with this codebase, so please let me know if there are any issues with this approach or if there's a better way to handle this.

Key Changes

  1. Separate CSS and JS entries (src/features/entry.ts)

    • Keep .css extension in entry names to avoid name conflicts
    • Add separateCssEntries() function to split entries by type
  2. Direct CSS processing (src/features/css-bundle.ts)

    • New module to build CSS entries using LightningCSS transform() API
    • Preserves @import statements (not inlined)
    • Supports minify and target options
  3. Watch mode support (src/index.ts)

    • Add CSS file watchers using Node.js fs.watch()
    • Debounce rebuilds (100ms) to avoid excessive builds
    • Rebuild CSS chunks on each Rolldown watch cycle to prevent them from being cleared
  4. Enhanced reporting (src/features/report.ts)

    • Modified ReportPlugin to accept extra chunks (CSS chunks)
    • Unified reporting for both JS and CSS files with gzip/brotli sizes
    • Correct file counts and total sizes

Behavior Changes

After this fix:

  • dist/index.js - correct JS output ✅
  • dist/index.css - correct CSS output ✅
  • No empty JS files generated from CSS entries ✅

Build output:

i dist/index.js 0.06 kB │ gzip: 0.08 kB
i dist/index.css 0.02 kB │ gzip: 0.04 kB
i 2 files, total: 0.08 kB
✔ Build complete in 51ms

Watch mode output:

i dist/index.js 0.06 kB │ gzip: 0.08 kB
i dist/index.css 0.02 kB │ gzip: 0.04 kB
i 2 files, total: 0.08 kB
✔ Rebuilt in 20ms.

Testing

  • ✅ All 141 existing tests pass
  • ✅ Updated snapshots for CSS-related tests (removed empty .mjs files)
  • ✅ Manually tested with watch mode
  • ✅ Lint checks pass

Implementation Details

Why use transform() instead of bundleAsync()?

I chose to use LightningCSS's transform() API instead of bundleAsync() to preserve the original behavior where each CSS file is output separately. This means:

  • @import statements are preserved in the output (not inlined)
  • Each CSS entry generates its own output file
  • Matches the behavior of Rolldown's CSS handling for imported CSS

Watch Mode Implementation

In watch mode, there was a challenge where Rolldown's watcher would clear all chunks (including CSS chunks) on each rebuild. The solution:

  1. CSS chunks are rebuilt on each Rolldown watch cycle (START event)
  2. Separate file watchers monitor CSS files for changes
  3. CSS file changes trigger incremental rebuilds with 100ms debouncing

Reporting Integration

The ReportPlugin was enhanced to accept a callback that provides extra chunks (CSS chunks). This allows:

  • Unified reporting format for all files
  • Correct total file counts and sizes
  • Consistent output between normal and watch modes

Notes

  • CSS @import statements are preserved in the output (not inlined)
  • CSS entries require the lightningcss peer dependency
  • Watch mode uses Node.js built-in fs.watch() (no additional dependencies)
  • CSS chunks are re-added on each Rolldown watch cycle to prevent them from being cleared

I'm not deeply familiar with this codebase, so this is my attempt to fix the issue. Please let me know if there are any problems with this approach or if there's a better way to handle it. Thank you for reviewing! 🙏

Copy link

netlify bot commented Dec 27, 2025
edited
Loading

Deploy Preview for tsdown ready!

Name Link
🔨 Latest commit ce84ed1
🔍 Latest deploy log https://app.netlify.com/projects/tsdown/deploys/6951309c44a3a30008d50763
😎 Deploy Preview https://deploy-preview-674--tsdown.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link

pkg-pr-new bot commented Dec 27, 2025
edited
Loading

Open in StackBlitz

npm i https://pkg.pr.new/tsdown@674
npm i https://pkg.pr.new/create-tsdown@674
npm i https://pkg.pr.new/tsdown-migrate@674

commit: ce84ed1

Copy link
Contributor

Thank you for your contribution, but I've found some issues.

Source

  • tsdown.config.ts
import { defineConfig } from 'tsdown'
export default defineConfig({
 entry: ['./src/index.ts', './src/index.css'],
 dts: {
 vue: true,
 },
})
  • src/index.ts
// import { defineAsyncComponent } from 'vue'
// import './index.css'
export { default as Foo } from './foo.vue'
// export const Foo = defineAsyncComponent(() => import('./foo.vue'))
export const foo = 1
  • src/index.css
a {
 color: red;
}
  • src/foo.vue
<template>
 <div class="foo" />
</template>
<style>
 .bar {
 color: blue;
 }
</style>

Output

  • index.mjs ✓
  • index.d.mts ✓
  • index.css(only have foo.vue style, the CSS of the index.css entry is missing) ✗
.bar {
 color: blue;
}

Copy link
Author

Thanks for the feedback! I've fixed the issue where CSS entry content was missing when used with Vue components.

Now the CSS entry (src/index.css) is properly merged with Vue component styles (<style> in .vue files) when they output to the same filename.

Could you please review again? @jinghaihan

Copy link
Contributor

Thanks for the feedback! I've fixed the issue where CSS entry content was missing when used with Vue components.

Now the CSS entry (src/index.css) is properly merged with Vue component styles (<style> in .vue files) when they output to the same filename.

Could you please review again? @jinghaihan

This has indeed been fixed. Thanks for the contribution!

Copy link
Member

sxzz commented Dec 30, 2025

Before submitting a large PR, it is better to first communicate with the team in the issue and explain your proposed solution. This will help us review your work more effectively.

Copy link
Member

@sxzz sxzz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to rework on your PR later. It's really hard to review.


// Dynamic import lightningcss using importWithError for proper error handling
const lightningcss =
await importWithError<typeof import('lightningcss')>('lightningcss')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lightningcss is not a dep, or peer dep, so it's impossible to import on the userland.

I think we can make this feature as optional and experimental.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really sorry for my oversight! I totally didn’t think through the lightningcss dependency handling properly and took it way too lightly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@sxzz sxzz sxzz left review comments

At least 1 approving review is required to merge this pull request.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

.css file with same name results in empty .js output

AltStyle によって変換されたページ (->オリジナル) /