Skip to content
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

Bun plugin #16295

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Bun plugin #16295

wants to merge 1 commit into from

Conversation

zackradisic
Copy link

@zackradisic zackradisic commented Feb 6, 2025

(Sorry for creating this PR without opening an issue first)

This PR implements a Bun plugin that makes TailwindCSS fast in Bun.

c612d2faa37f2cff03c8620a08bb2def7268851bb183c2fa8e2763246b9243e5

(bundling 2048 html + react + tailwind files)

Overview

This plugins is comprised of two parts:

  • A regular Bun bundler plugin which is the main driver of everything and invokes the native bundler plugin
  • A native bundler plugin which parallelizes the module graph scanning of candidates. Tailwind was part of the motivation for implementing this API.

Native bundler plugin

The native bundler plugin is used to scan the module graph in parallel with the Scanner struct from @tailwindcss/oxide.

The main logic for this code is in the tw_on_before_parse function.

Native bundler plugins run in parallel on Bun's bundler threads and do not need to do UTF-16 <-> UTF-8 string conversions. This speeds up the plugin a lot.

Native bundler plugins are NAPI modules which export additional symbols (since NAPI modules themselves are dynamically loaded libraries which can be dlopen()'d). The bun-native-plugin crate handles the boilerplate for creating one.

I placed the Bun plugin inside the existing crates/node/lib.rs (the @tailwindcss/oxide package). This reduces the need to create more compiled artifacts at the cost of a relatively small binary size change:

# original size
❯ ls -lhS dist/tailwindcss-oxide-darwin-arm64.tgz
-rw-r--r--@ 1 zackradisic  staff   2.1M Feb  4 20:42 dist/tailwindcss-oxide-darwin-arm64.tgz

# new size
❯ ls -lhS dist/tailwindcss-oxide-darwin-arm64.tgz
-rw-r--r--@ 1 zackradisic  staff   2.2M Feb  4 18:42 dist/tailwindcss-oxide-darwin-arm64.tgz

Please let me know if you would like me to split it out into its own separate package if you don't like the binary size change.

Sharing state between the native plugin and JS

The scanned candidates and other state are held inside a NAPI External. The struct in the code that does
this is called TailwindContextExternal.

A NAPI External is a NAPI value which can be given to JS and which holds a void* data pointer. This data is inaccessible to JS, but a NAPI module can dereference the data and convert it to NAPI values.

This looks a bit like this on the Rust side:

/// Create the TailwindContextExternal and return it to JS wrapped in a Napi External.
///
/// Napi has an `External<T>` type which allows us to wrap it in an
/// external easily.
#[no_mangle]
#[napi]
pub fn twctx_create() -> External<TailwindContextExternal> {
  let external = External::new(TailwindContextExternal {
    module_graph_candidates: Default::default(),
    dirty: AtomicBool::new(false),
  });

  external
}

And the JS side:

// import napi functions which let us manipulate the external
import { twctxCreate, twctxIsDirty, twctxToJs } from '@tailwindcss/oxide'

// create the state, the returned value
// is a Napi External
const external = twctxCreate()

/* ... other code ... */

let moduleGraphCandidates = new Map<string, Set<string>>()
function getSharedCandidates() {
  // check if there are changes
  if (twctxIsDirty(external)) {
    // convert the state into js values
    let rawCandidates: Array<{ id: string; candidates: string[] }> = twctxToJs(external)
    for (let { id, candidates } of rawCandidates) {
      moduleGraphCandidates.set(id, new Set(candidates))
    }
  }
  return moduleGraphCandidates
}

napi-rs version bump

The napi-rs crate was updated to version 2.16.15 so we can use the External::inner_from_raw() function to turn an External's *mut c_void pointer back into TailwindContextExternal.

JS plugin

The JS plugin @tailwindcss-bun/src/index.ts uses logic copied over from the vite plugin implementation but modified to work with Bun's plugin API.

It invokes the native bundler plugin using the .onBeforeParse plugin API function:

// Called on every file which matches the filter
// before it is parsed by Bun
build.onBeforeParse(
  // filter which files the native plugin apply to
  { filter: NON_CSS_ROOT_FILE_RE },

  // pass the napi module, the symbol which points to the plugin main function,
  // and the external which holds the tailwind state
  { napiModule: addon, symbol: 'tw_on_before_parse', external },
)

One thing to note is that Bun's bundler currently does not have an API that is analogous to .addWatchedFile(), so there is currently no way to add additional files to the module graph.

Testing

I added a integrations/bun/index.test.ts file, please let me know if you would like more tests

Modify integration tests

Make `ChangedContent` possibly borrow its contents to avoid unnecessary allocations

Revert "Make `ChangedContent` possibly borrow its contents to avoid unnecessary allocations"

This reverts commit 10e2b0c.

Did not result in significant performance gain.

Modify workflow to test

delete that for now

make it work

udpate

update

messed up lock file

nice

lol

lmao

sanity test

blah

asdf

asdflkjasfd

man wut

wtf

???

cmon

LOOOOOOOOOOOOOOOOOL

clean tht bad boi up

stuff

bring it back

bring it back

change tests

bye bye
@zackradisic zackradisic requested a review from a team as a code owner February 6, 2025 04:16
@philipp-spiess
Copy link
Member

Hey! Really excited for an official Bun plugin! We’re happy to collaborate and work on this together but to set the expectations right it will realistically take a few weeks before someone from our team can focus on this.

One reason for that is that we’re currently working on some changes that will break the existing Oxide <> Node APIs that are necessary fix some critical bugs. These changes will then impact the API design for the updated Oxide bindings necessary for the Bun plugin as well.

I’m sorry that we can’t hop onto this right away but we have to prioritize some other important fixes for the v4 release right now, I hope you understand. We’re pretty stoked about this feature though and hope to get to it soon!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants