Skip to content

Use wasm as a fallback for @tailwindcss/oxide - #20383

Merged
RobinMalfait merged 3 commits into
mainfrom
feat/use-wasm-fallback
Aug 4, 2026
Merged

Use wasm as a fallback for @tailwindcss/oxide#20383
RobinMalfait merged 3 commits into
mainfrom
feat/use-wasm-fallback

Conversation

@RobinMalfait

Copy link
Copy Markdown
Member

Right now, we use Rust for @tailwindcss/oxide which has 2 responsibilities:

  1. Traverse the file system and figure out which files need to be scanned based on auto source detection and @source directives.
  2. Given those files, extract possible Tailwind CSS classes which we call candidates.

Since this is using native code, we use napi-rs to get native .node files on a per platform / arch basis.

So far so good, however, if you are on an OS that doesn't have a prebuilt binary, you will receive an error that might look like this:

Error: Cannot find native binding. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). Please try `npm i` again after removing both package-lock.json and node_modules directory.
    at Object.<anonymous> (/private/var/folders/1k/bdv8blv93xq7qgwjdwc9z88h0000gn/T/tailwind-integrationspYHIVP/node_modules/.pnpm/@tailwindcss+oxide@file+..+..+..+..+..+..+..+Users+robin+github.com+tailwindlabs+tailwi_47ae1688f61c719f66c73e2ff35e430f/node_modules/@tailwindcss/oxide/index.js:573:19)
    at Module._compile (node:internal/modules/cjs/loader:1829:14)
    at Object..js (node:internal/modules/cjs/loader:1969:10)
    at Module.load (node:internal/modules/cjs/loader:1552:32)
    at Module._load (node:internal/modules/cjs/loader:1354:12)
    at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
    at Module.require (node:internal/modules/cjs/loader:1575:12)
    at require (node:internal/modules/helpers:191:16)
    at file:///private/var/folders/1k/bdv8blv93xq7qgwjdwc9z88h0000gn/T/tailwind-integrationspYHIVP/index.mjs:5:19
    at ModuleJob.run (node:internal/modules/esm/module_job:437:25) {
  cause: Error: Cannot find module '@tailwindcss/oxide-darwin-arm64'

This means that we have to add support for these platforms, and there are some open PRs related to this, which could be closed by this PR:

Today we already have support for the big platforms out there:

  • Windows arm64
  • Windows x64
  • macOS arm64
  • macOS x64

But then it starts to get a bit out of hand once we start looking at Linux based versions:

  • Android arm eabi
  • Android arm64
  • Linux arm64 gnu
  • Linux arm64 gnueabihf
  • Linux arm64 musl
  • Linux x64 gnu
  • Linux x64 musl
  • freebsd x64

... and then we have the pending list from the 3 PRs linked above. Adding support for all of these is not the end of the world, but it gets complex if we need to keep supporting more and more. Right now we rely on a bunch of non-default napi-rs setup in CI just to support these other platforms.

This PR solves that by using the wasm32-wasi build as a universal fallback. The napi-rs generated loader already knows how to fall back to @tailwindcss/oxide-wasm32-wasi, but that package declared "cpu": ["wasm32"], so npm/pnpm never installed it on real hardware. Removing that restriction means the package is installed everywhere, and the loader picks it up whenever no native binding exists.

This PR also fixes a UVWASI_EACCES crash on sandboxed platforms (OpenHarmony, Android): the generated wasm loader preopens /, which those sandboxes deny, so the fallback failed to load on exactly the platforms that need it (see this comment). We patch @napi-rs/cli's codegen templates via pnpm patch to retry with narrower preopens (/ → cwd → none). On such platforms, scanning is limited to files under the current working directory.

Test plan

Added two integration tests:

  1. Trick pnpm (via supportedArchitectures) into installing for a platform we explicitly don't support, and assert @tailwindcss/oxide loads the wasm binding and scans files from disk.
  2. Simulate a sandbox that denies preopening /, and assert the wasm binding still loads and scans.

[ci-all]

@RobinMalfait
RobinMalfait requested a review from a team as a code owner August 4, 2026 15:48
@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

The PR appears safe to merge, with the fallback packaging, generated-loader build path, and sandbox behavior covered consistently.

The wasm package is wired as an optional fallback, the repository’s wasm build invokes the patched CLI templates, and the integration tests exercise both unsupported-platform selection and denied root preopening without revealing a concrete regression.

Reviews (1): Last reviewed commit: "patch wasm to bypass `UVWASI_EACCES`" | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change updates the WASI package description and removes its CPU metadata restriction. It patches the NAPI CLI and worker-thread loader to retry WASI initialization with narrower preopens. The workspace applies this patch to @napi-rs/cli@3.7.4. Linux-gated integration tests cover native-binding fallback and file scanning when filesystem-root preopening is denied.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: using the WASI build as a fallback for @tailwindcss/oxide.
Description check ✅ Passed The description accurately explains the fallback behavior, sandbox handling, affected platforms, and integration tests.
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.

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

Comment @coderabbitai help to get the list of available commands.

@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: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e7b3a24-3639-4811-89bb-da6b90305898

📥 Commits

Reviewing files that changed from the base of the PR and between 05c3a87 and 92d6a7f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • crates/node/npm/wasm32-wasi/README.md
  • crates/node/npm/wasm32-wasi/package.json
  • integrations/oxide/wasm.test.ts
  • patches/@napi-rs__cli@3.7.4.patch
  • pnpm-workspace.yaml
💤 Files with no reviewable changes (1)
  • crates/node/npm/wasm32-wasi/package.json

Comment thread integrations/oxide/wasm.test.ts
Comment on lines +172 to +187
'preload.cjs': js`
const wasi = require('node:wasi')
const RealWASI = wasi.WASI

wasi.WASI = class WASI extends RealWASI {
constructor(options) {
if (options?.preopens?.['/'] !== undefined) {
const error = new Error('UVWASI_EACCES, uvwasi_init')
error.code = 'UVWASI_EACCES'
error.syscall = 'uvwasi_init'
throw error
}
super(options)
}
}
`,

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Determine whether the generated wasi loader constructs WASI on the main thread, in a worker, or both.
set -uo pipefail

# Locate the generated wasi loader and worker bootstrap emitted by `@napi-rs/cli`.
fd -H -t f -E node_modules 'tailwindcss-oxide\.wasi.*\.(c|m)?js|wasi-worker.*\.(c|m)?js' .

# Show every WASI construction site and its threading context.
fd -H -t f -E node_modules 'wasi' . --exec rg -n -C 6 'new (WASI|__nodeWASI)\(|Worker\(|worker_threads|childThread' {} \;

# Confirm the patch hunks map onto those files.
rg -n 'MessageHandler|childThread|onLoad' patches/@napi-rs__cli@3.7.4.patch

Repository: tailwindlabs/tailwindcss

Length of output: 275


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "== test outline =="
ast-grep outline integrations/oxide/wasm.test.ts 2>/dev/null || wc -l integrations/oxide/wasm.test.ts

echo
echo "== relevant wasm.test.ts lines =="
sed -n '130,220p' integrations/oxide/wasm.test.ts

echo
echo "== patch context =="
sed -n '20,90p' patches/@napi-rs__cli@3.7.4.patch

Repository: tailwindlabs/tailwindcss

Length of output: 4732


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "== preload shim and required flags =="
sed -n '160,195p' integrations/oxide/wasm.test.ts

echo
echo "== command containing preload.cjs =="
rg -n '\-\-require|preload\.cjs|node --' integrations/oxide/wasm.test.ts

echo
echo "== `@napi-rs` template WASI construction sites and worker context =="
sed -n '1,140p' patches/@napi-rs__cli@3.7.4.patch

Repository: tailwindlabs/tailwindcss

Length of output: 4518


🌐 Web query:

@napi-rs cli generated wasm template __wasi WASI inline code

💡 Result:

NAPI-RS supports WebAssembly and WASI targets natively through the @napi-rs/cli [1][2]. When you generate a new project using napi new, you can enable WASI support by selecting the appropriate target during the interactive setup, or by manually adding wasm32-wasip1-threads or wasm32-wasip1 to the napi.targets array in your package.json [1][3][4]. The @napi-rs/cli automatically generates the necessary loaders for Node.js and browsers when these targets are configured [1]. These loaders include support for WASI environments and handle the instantiation of the Wasm module [1][3]. Regarding inline code or specific WASI configuration, the system is designed to handle this through the following mechanisms: 1. Build Configuration: You trigger a WASI build using: napi build --platform --release --target wasm32-wasip1-threads [1] 2. Loader Customization: While the CLI generates standard loaders, you can influence browser-specific behavior (such as enabling the in-memory filesystem or error event forwarding) via the napi.wasm.browser configuration object in your package.json [4]: { napi: { wasm: { browser: { fs: true, errorEvent: true } } } } 3. Inline Logic and Workers: The CLI templates generate worker files (e.g., wasi-worker.mjs) that interface with emnapi [1][5]. These internal templates handle the low-level WASI instantiation (preopens, environment variables) [5]. If you need to customize the WASI runtime instantiation, you typically interact with the generated loaders rather than injecting inline code into the CLI template itself [1][3]. 4. Testing: You can test WASI behavior or force its usage—even on native-supported hosts—using environment variables: NAPI_RS_FORCE_WASI=true node app.js [1][3] NAPI_RS_WASI_FLAVOR=wasm32-wasip1 node app.js [3] For most use cases, no manual inline code injection is required because NAPI-RS v3 abstracts the complexities of the WASI runtime, threads, and emnapi integration into the build and packaging flow [2][6].

Citations:


🌐 Web query:

@napi-rs/cli@3.7.4 src/api/templates/wasi-worker-template.ts WASI Worker instantiateNapiModuleSync

💡 Result:

In @napi-rs/cli@3.7.4, the file src/api/templates/wasi-worker-template.ts serves as a template for generating a worker thread environment used by emnapi-based WASI modules [1][2]. The function instantiateNapiModuleSync is imported from @napi-rs/wasm-runtime and is central to this worker's operation [3][2]. It is responsible for synchronously instantiating the WebAssembly module within the worker thread [2]. Key aspects of its usage in this template include: - Configuration: When called, it is passed a configuration object that typically includes { childThread: true, wasi, context: emnapiContext } [2]. - Import Overwriting: The template uses the overwriteImports option within instantiateNapiModuleSync to correctly inject environment, NAPI, and emnapi-specific imports, as well as the shared WebAssembly memory, into the WASI instance [2]. - Threading: By setting childThread: true, the function initializes the WASI module specifically for a secondary thread, enabling support for features like asynchronous work pools and thread-safe functions provided by the emnapi runtime [4][2]. This template ensures that worker threads can handle NAPI-RS/emnapi tasks effectively by setting up the necessary WASI environment and linking the required runtime APIs before the module is executed [5][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "== wasm runtime WASI construction and worker bootstrap =="
python3 - <<'PY'
import urllib.request
url = "https://unpkg.com/@napi-rs/wasm-runtime@1.2.2/runtime.cjs"
data = urllib.request.urlopen(url, timeout=20).read().decode("utf-8", "replace")
for needle in ["new WASI", "new __nodeWASI", "worker_threads", "worker_threads.Worker", "onLoad"]:
    idx = data.find(needle)
    print(f"\n--- first {needle!r} at byte {idx} ---")
    if idx == -1:
        print(none)
    else:
        lines = data[:idx].count("\n")
        chunk = data.splitlines()[max(0, lines-15):lines+20]
        for i, line in enumerate(chunk, lines-14):
            print(f"{i+1:4}: {line}")
PY

Repository: tailwindlabs/tailwindcss

Length of output: 2506


🌐 Web query:

@napi-rs/wasm-runtime runtime.cjs instantiateNapiModuleSync wasi Worker

💡 Result:

In the NAPI-RS ecosystem, @napi-rs/wasm-runtime provides the necessary runtime environment and polyfills to execute NAPI-RS addons compiled to WebAssembly (specifically the wasm32-wasip1-threads target) [1][2]. The instantiateNapiModuleSync function is a core utility exported by this package, responsible for synchronously instantiating the WASM module within a Node.js or browser environment [3][4]. When working with threaded WASM modules that require multi-threading support (via workers), the instantiateNapiModuleSync function is typically used in conjunction with a WASI implementation and a worker-based orchestration strategy [2][5]. Key aspects of this implementation include: 1. Worker Orchestration: For threaded builds, the runtime generates a dedicated worker script (often named wasi-worker.mjs) [6][7][5]. This script initializes a Worker instance, sets up the necessary global environment (like globalThis, require, and Worker), and uses MessageHandler to manage communication between the main thread and worker threads [6][8]. 2. instantiateNapiModuleSync Usage: Inside the worker, instantiateNapiModuleSync is called to load the WASM module [6][5]. It requires several critical options: - childThread: Should be set to true for worker instances [6][8][4]. - wasi: An instance of the WASI environment (provided by @napi-rs/wasm-runtime or Node.js built-ins) that handles system calls [6][8][5]. - overwriteImports: A function used to inject the necessary WASM memory and environment imports (such as napi and emnapi symbols) into the module's import object [6][5]. - context: An emnapi context (retrieved via getDefaultContext) which is essential for managing the NAPI-RS/emnapi state [6][5]. 3. Runtime Files: The runtime.cjs file acts as an entry point for CommonJS environments, re-exporting these essential functions and plugins (such as emnapiAsyncWorkPlugin and emnapiTSFNPlugin) required to support asynchronous work and thread-safe functions in WASM [3]. This setup is automated by the NAPI-RS CLI (napi create-npm), which generates the required worker templates and configuration to handle the complexities of threading and WASI integration automatically [2][5].

Citations:


🌐 Web query:

unpkg @napi-rs/cli@3.7.4 wasi-worker-template.ts "new WASI"

💡 Result:

The wasi-worker-template.ts file is a internal source template used by @napi-rs/cli version 3.7.4 to generate worker code for WASI (WebAssembly System Interface) applications [1][2]. It is responsible for setting up a Node.js worker thread environment that runs WebAssembly modules with WASI support [1]. Key characteristics of this template include: 1. WASI Integration: It imports WASI, MessageHandler, and instantiateNapiModuleSync from @napi-rs/wasm-runtime to manage the WASM module lifecycle and system interface [1][3]. 2. Worker Thread Handling: The template uses node:worker_threads to facilitate communication between the main thread and the worker, specifically handling parentPort messages and setting up global environment variables (globalThis) to emulate a browser-like or runtime environment for the WASM module [1]. 3. Execution: When a worker is initialized, the template uses a MessageHandler to instantiate the WASM module with a configured WASI instance, allowing the module to interact with the file system (via preopens) and perform standard input/output operations [1]. In the context of the @napi-rs/cli, this template is used when the project is configured to build and deploy WASI-based binaries, ensuring that the necessary runtime glue code is correctly bundled for environments that support worker threads [4]. You can view the full content of the template for version 3.7.4 directly via the unpkg URL [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "== exact worker template source from CDN, ignoring local TLS =="
python3 - <<'PY'
import urllib.request, ssl
url = "https://unpkg.com/@napi-rs/cli@3.7.4/files/src/api/templates/wasi-worker-template.ts"
ctx = ssl._create_unverified_context()
text = urllib.request.urlopen(url, context=ctx, timeout=20).read().decode()
lines = text.splitlines()
for needle in ["load-wasi", "WASI", "MessageHandler", "instantiateNapiModuleSync", "worker_threads", "ChildThread"]:
    print(f"\n--- occurrences of {needle!r} ---")
    for i, line in enumerate(lines, 1):
        if needle in line:
            start = max(1, i-8); end = min(len(lines), i+25)
            print(f"{needle!r} at lines {start}-{end}")
            for j in range(start, end+1):
                print(f"{j:4}: {lines[j-1]}")
            break
PY

echo
echo "== load-wasi template =="
python3 - <<'PY'
import urllib.request, ssl
url = "https://unpkg.com/@napi-rs/cli@3.7.4/files/src/api/templates/load-wasi-template.ts"
ctx = ssl._create_unverified_context()
text = urllib.request.urlopen(url, context=ctx, timeout=20).read().decode()
for i, line in enumerate(text.splitlines(), 1):
    print(f"{i:4}: {line}")
PY

Repository: tailwindlabs/tailwindcss

Length of output: 1824


Cover both generated WASI construction paths in the worker shim.

This test runs index.mjs with node --require ./preload.cjs, so it only exercises the main-thread __wasi construction. The patch also changes childThread: true handlers; these construct WASI inside the worker bootstrap, and --require does not propagate to worker threads. Add the same preopen-rejection shim to the worker bootstrap so this test covers both generated templates.

@RobinMalfait
RobinMalfait merged commit d190343 into main Aug 4, 2026
41 of 43 checks passed
@RobinMalfait
RobinMalfait deleted the feat/use-wasm-fallback branch August 4, 2026 16:41
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