Use wasm as a fallback for @tailwindcss/oxide - #20383
Conversation
Confidence Score: 5/5The 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 |
WalkthroughThe 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 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
crates/node/npm/wasm32-wasi/README.mdcrates/node/npm/wasm32-wasi/package.jsonintegrations/oxide/wasm.test.tspatches/@napi-rs__cli@3.7.4.patchpnpm-workspace.yaml
💤 Files with no reviewable changes (1)
- crates/node/npm/wasm32-wasi/package.json
| '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) | ||
| } | ||
| } | ||
| `, |
There was a problem hiding this comment.
📐 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.patchRepository: 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.patchRepository: 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.patchRepository: 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:
- 1: https://napi.rs/docs/concepts/webassembly
- 2: https://napi.rs/blog/announce-v3
- 3: https://app.unpkg.com/@napi-rs/cli@3.8.2/files/docs/wasi.md
- 4: https://napi.rs/docs/cli/napi-config
- 5: https://app.unpkg.com/@napi-rs/cli@3.7.4/files/src/api/templates/wasi-worker-template.ts
- 6: https://napi.rs/
🌐 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:
- 1: https://registry.npmjs.org/@napi-rs/cli
- 2: https://app.unpkg.com/@napi-rs/cli@3.7.4/files/src/api/templates/wasi-worker-template.ts
- 3: https://github.com/napi-rs/napi-rs/compare/@napi-rs/wasm-runtime@0.1.0...@napi-rs/wasm-runtime@0.1.1
- 4: https://app.unpkg.com/@napi-rs/wasm-runtime@1.2.2/files/runtime.cjs
- 5: https://napi.rs/docs/concepts/webassembly
🏁 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}")
PYRepository: 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:
- 1: https://www.npmjs.com/package/@napi-rs/wasm-runtime
- 2: https://napi.rs/docs/concepts/webassembly
- 3: https://app.unpkg.com/@napi-rs/wasm-runtime@1.2.2/files/runtime.cjs
- 4: https://unpkg.com/@napi-rs/cli@3.7.4/src/api/__tests__/__snapshots__/templates.spec.ts.md
- 5: https://app.unpkg.com/@napi-rs/cli@3.7.4/files/src/api/templates/load-wasi-template.ts
- 6: https://github.com/napi-rs/package-template/blob/refs/heads/main/wasi-worker.mjs
- 7: https://github.com/napi-rs/wasm-tools/blob/main/wasi-worker.mjs
- 8: https://github.com/napi-rs/package-template/blob/refs/heads/main/wasi-worker-browser.mjs
🌐 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:
- 1: https://app.unpkg.com/@napi-rs/cli@3.7.4/files/src/api/templates/wasi-worker-template.ts
- 2: https://registry.npmjs.org/@napi-rs/cli
- 3: napi-rs/napi-rs@44dc39f
- 4: https://app.unpkg.com/@napi-rs/cli@3.7.4/files/src/api/artifacts.ts
🏁 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}")
PYRepository: 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.
Right now, we use Rust for
@tailwindcss/oxidewhich has 2 responsibilities:@sourcedirectives.Since this is using native code, we use napi-rs to get native
.nodefiles 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:
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:
But then it starts to get a bit out of hand once we start looking at Linux based versions:
... 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-wasibuild 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_EACCEScrash 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 viapnpm patchto 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:
supportedArchitectures) into installing for a platform we explicitly don't support, and assert@tailwindcss/oxideloads the wasm binding and scans files from disk./, and assert the wasm binding still loads and scans.[ci-all]