镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Add a single-threaded browser build of @tailwindcss/oxide - #20684

Open
suasgn wants to merge 3 commits into
tailwindlabs:mainfrom
suasgn:oxide-browser-build
Open

suasgn wants to merge 3 commits into
tailwindlabs:mainfrom
suasgn:oxide-browser-build

Conversation

@suasgn

@suasgn suasgn commented Nov 9, 2026 •

Copy link
Copy Markdown

Summary

@tailwindcss/oxide-wasm32-wasi only has a threaded wasm32-wasip1-threads build. In browsers it needs SharedArrayBuffer, which is only available on cross-origin isolated pages (COOP/COEP headers), so the scanner can't run on most pages.

This adds a separate single-threaded wasm32-wasip1 build for browsers:

  • browser.mjs loads it with non-shared memory and scans an in-memory filesystem (memfs), exported as __fs and __volume. Scanning runs on the calling thread.
  • The pinned NAPI CLI only builds wasm32-wasip1-threads, so scripts/build-browser.mjs builds the module with Cargo directly, and build.rs links emnapi's single-threaded archive. The script reads the artifact path from Cargo's JSON output, and fails the build when the module doesn't link against the memory that browser.mjs creates.
  • The browser field of @tailwindcss/oxide-wasm32-wasi now points to browser.mjs, and the package ships types (index.d.ts, browser.d.mts). The threaded browser loader (tailwindcss-oxide.wasi-browser.js, wasi-worker-browser.mjs) is still published, so code that imports it by path, for example on cross-origin isolated pages, keeps working.
  • @tailwindcss/oxide gets a browser entry that re-exports the browser build.
  • build now runs build:browser, so CI, the release workflows and CONTRIBUTING.md install the wasm32-wasip1 target.

The Node.js and native builds are unchanged.

Related

  • Addresses the browser part of Oxide as WASM Module? #11610: using Oxide on play.tailwindcss.com, and the SharedArrayBuffer errors when loading @tailwindcss/oxide-wasm32-wasi in a browser. The edge runtimes from the original question (e.g. Deno Deploy) are not tested.
  • An alternative to Feat: Adds rust config options to detect threading support in oxide #18292, which builds crates/oxide for wasm32-unknown-unknown with wasm-bindgen. This PR does not change the scanner code, because Rayon runs on the calling thread on wasm32-wasip1. It exposes the same Scanner API as Node.js instead of separate Wasm* types, and supports scan() over memfs. A wasm32-unknown-unknown build without the WASI runtime may be smaller.

Test plan

  • pnpm --filter @tailwindcss/oxide test:ui: new Playwright tests (Chromium, Firefox, WebKit) serve the bundled entry without COOP/COEP headers. They check candidate extraction, UTF-16 positions, memfs discovery with incremental rescans, and use from an application-owned worker. One test makes Worker throw, to check that the build never creates workers.
  • pnpm test:integrations ./integrations/oxide/wasm.test.ts: a new test loads the browser entry of the packed package in Node.js with SharedArrayBuffer removed.
  • pnpm --filter @tailwindcss/oxide build:browser, with the default target directory and with CARGO_BUILD_TARGET_DIR set to another directory. Both copy the fresh module.
  • Linked the module against memory that is too small, and against shared memory, to check that the build rejects both.
  • cargo check --target wasm32-wasip1 -p tailwind-oxide works without EMNAPI_LINK_DIR.

🤖 Generated with Claude Code

The threaded WASI build needs `SharedArrayBuffer`, which browsers only
expose on cross-origin isolated pages (COOP/COEP headers). Add a
separate `wasm32-wasip1` build for browsers that runs on the calling
thread and scans an in-memory filesystem (memfs).

- `scripts/build-browser.mjs` builds it with Cargo directly, since the
  pinned NAPI CLI only builds `wasm32-wasip1-threads`. It reads the
  artifact path from Cargo and checks that the module links against the
  memory `browser.mjs` creates.
- `build.rs` links emnapi's single-threaded archive for this target.
- `@tailwindcss/oxide-wasm32-wasi` points `browser` at the new
  `browser.mjs`, ships its types, and no longer ships the unused
  threaded browser loader.
- `@tailwindcss/oxide` gets a `browser` entry that re-exports it.
- CI, release workflows and CONTRIBUTING.md install `wasm32-wasip1`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@suasgn
suasgn requested a review from a team as a code owner November 9, 2026 17:22
@greptile-apps

greptile-apps Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The reviewed changes appear safe to merge.

Reviews (5) · Last reviewed commit: "Load the browser build asynchronously an..." · Reviewed by Greptile

@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Walkthrough

The change adds a single-threaded wasm32-wasip1 browser build for oxide. It adds a browser loader that initializes WASI, fetches and instantiates the WASM module, and exports the scanner and in-memory filesystem. Package metadata, declarations, build workflows, and artifact handling are updated for the new entry point. Playwright and integration tests cover browser scanning without cross-origin isolation or SharedArrayBuffer.

Merge Risk: Low · up to 07611

TypeScript browser consumers need a workaround to import the virtual filesystem from the package root.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly summarizes the main change: adding a single-threaded browser build of Oxide.
Description check Passed The description explains the browser build, its implementation, package changes, and test plan. It is directly related to the changeset.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@suasgn
suasgn force-pushed the oxide-browser-build branch 2 times, most recently from fd648f9 to 6a31393 Compare November 9, 2026 17:55
@suasgn
suasgn force-pushed the oxide-browser-build branch from 6a31393 to fd648f9 Compare November 9, 2026 17:58
Code that imports `tailwindcss-oxide.wasi-browser.js` by path, for
example on cross-origin isolated pages, keeps working. Document it as
an opt-in for parallel scanning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@suasgn
suasgn force-pushed the oxide-browser-build branch from fd648f9 to db0c97f Compare November 9, 2026 17:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5c3eafad-0d3b-47dd-9b8f-bba24cb2940f
Commits

Reviewing files that changed from the base of the PR and between 8093f21 and db0c97f.

Files selected for processing: 3

  • crates/node/npm/wasm32-wasi/README.md
  • crates/node/npm/wasm32-wasi/package.json
  • crates/node/scripts/move-artifacts.mjs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Comment thread crates/node/npm/wasm32-wasi/README.md Resolved
- Instantiate with `instantiateNapiModule`, so compiling doesn't block
  the page, and Chrome's 8 MB limit on synchronous main-thread compiles
  doesn't apply.
- Use an 8 MB stack instead of napi's 64 MB, and start with 16 MB of
  memory instead of 128 MB. Memory grows on demand.
- Stop recommending the threaded browser loader in the README: its
  `scan()` hangs when it reads more than one file from memfs, because
  the workers' file reads wait on the blocked calling thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

Minor · Route browser root imports to browser types. · package.json:10

crates/node/package.json:10
Minor · Functional Correctness · Quick win

Route browser root imports to browser types.

TypeScript consumers importing __fs or __volume from @tailwindcss/oxide get missing-export errors, even when their bundler selects the browser entry. Provide browser-conditional type routing for consumers that enable the browser condition, while keeping the Node root on index.d.ts.

Review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f147c693-eb41-400b-9ddf-2440c33a2cbc
Commits

Reviewing files that changed from the base of the PR and between db0c97f and 076119e.

Files selected for processing: 3

  • crates/node/build.rs
  • crates/node/npm/wasm32-wasi/README.md
  • crates/node/npm/wasm32-wasi/browser.mjs
Files with no reviewable changes: 1

  • crates/node/npm/wasm32-wasi/README.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

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.

1 participant