You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add a single-threaded browser build of @tailwindcss/oxide - #20684
@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.
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>
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: 4
Check name
Status
Explanation
Title check
The title clearly summarizes the main change: adding a single-threaded browser build of Oxide.
Description check
The description explains the browser build, its implementation, package changes, and test plan. It is directly related to the changeset.
Linked Issues check
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
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.
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>
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.
- 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>
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@tailwindcss/oxide-wasm32-wasionly has a threadedwasm32-wasip1-threadsbuild. In browsers it needsSharedArrayBuffer, 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-wasip1build for browsers:browser.mjsloads it with non-shared memory and scans an in-memory filesystem (memfs), exported as__fsand__volume. Scanning runs on the calling thread.wasm32-wasip1-threads, soscripts/build-browser.mjsbuilds the module with Cargo directly, andbuild.rslinks 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 thatbrowser.mjscreates.browserfield of@tailwindcss/oxide-wasm32-wasinow points tobrowser.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/oxidegets abrowserentry that re-exports the browser build.buildnow runsbuild:browser, so CI, the release workflows and CONTRIBUTING.md install thewasm32-wasip1target.The Node.js and native builds are unchanged.
Related
SharedArrayBuffererrors when loading@tailwindcss/oxide-wasm32-wasiin a browser. The edge runtimes from the original question (e.g. Deno Deploy) are not tested.crates/oxideforwasm32-unknown-unknownwith wasm-bindgen. This PR does not change the scanner code, because Rayon runs on the calling thread onwasm32-wasip1. It exposes the sameScannerAPI as Node.js instead of separateWasm*types, and supportsscan()over memfs. Awasm32-unknown-unknownbuild 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 makesWorkerthrow, 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 withSharedArrayBufferremoved.pnpm --filter @tailwindcss/oxide build:browser, with the default target directory and withCARGO_BUILD_TARGET_DIRset to another directory. Both copy the fresh module.cargo check --target wasm32-wasip1 -p tailwind-oxideworks withoutEMNAPI_LINK_DIR.🤖 Generated with Claude Code