Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/node/npm/wasm32-wasi/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# `@tailwindcss/oxide-wasm32-wasi`

This is the **wasm32-wasip1-threads** binary for `@tailwindcss/oxide`
This is the **wasm32-wasip1-threads** build of `@tailwindcss/oxide`
3 changes: 0 additions & 3 deletions crates/node/npm/wasm32-wasi/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
"name": "@tailwindcss/oxide-wasm32-wasi",
"version": "4.3.3",
"cpu": [
"wasm32"
],
"main": "tailwindcss-oxide.wasi.cjs",
"files": [
"tailwindcss-oxide.wasm32-wasi.wasm",
Expand Down
186 changes: 185 additions & 1 deletion integrations/oxide/wasm.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { css, js, json, test } from '../utils'
import { css, js, json, test, yaml } from '../utils'

// This test runs the wasm build using the `node:wasi` runtime.
//
Expand Down Expand Up @@ -57,3 +57,187 @@ testFn(
`)
},
)

testFn(
'`@tailwindcss/oxide` falls back to the wasm build when no native binding is available',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/oxide": "workspace:^"
}
}
`,
'pnpm-workspace.yaml': yaml`
# Trick pnpm in only supporting an architecture that @tailwindcss/oxide
# doesn't support, and therefore should fallback to the wasm version.
supportedArchitectures:
os:
- openbsd
cpu:
- x64
`,
'src/index.js': js`
const className = "content-['src/index.js']"
module.exports = { className }
`,
'index.mjs': js`
import { createRequire } from 'node:module'
import { join } from 'node:path'

let require = createRequire(import.meta.url)
let { Scanner } = require('@tailwindcss/oxide')

let loaded = Object.keys(require.cache)

let scanner = new Scanner({
sources: [
{
base: join(process.cwd(), 'src'),
pattern: '**/*',
negated: false,
},
],
})

console.log(
JSON.stringify({
native: loaded.filter((file) => file.endsWith('.node')),
wasi: loaded.some((file) => file.endsWith('tailwindcss-oxide.wasi.cjs')),
candidates: scanner.scan(),
}),
)
process.exit()
`,
},
},
async ({ expect, exec }) => {
// Since vitest runs under `pnpm run`, pnpm's bin shims export a NODE_PATH
// that includes the repository's hidden hoist directory
// (`node_modules/.pnpm/node_modules`), which links every workspace package,
// including all native `@tailwindcss/oxide-*` bindings.
//
// Node uses `NODE_PATH` exactly when the local `node_modules` lookup fails,
// which would defeat the simulated unsupported platform, so clear it.
let output = await exec(`node index.mjs`, { env: { NODE_PATH: '' } })
let { native, wasi, candidates } = JSON.parse(output)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// No native binding was installed or loaded, ...
expect(native).toEqual([])

// ... the wasm32-wasi binding is what actually loaded, ...
expect(wasi).toBe(true)

// ... and scanning real files on disk works through it.
expect(candidates).toMatchInlineSnapshot(`
[
"className",
"const",
"content-['src/index.js']",
"exports",
]
`)
},
)

testFn(
'the wasm build loads even when preopening the filesystem root is denied',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/oxide": "workspace:^"
}
}
`,
'pnpm-workspace.yaml': yaml`
# Trick pnpm in only supporting an architecture that @tailwindcss/oxide
# doesn't support, and therefore should fallback to the wasm version.
supportedArchitectures:
os:
- openbsd
cpu:
- x64
`,
// The wasm bindings generated by `@napi-rs/cli` preopen the filesystem
// root, which sandboxed platforms (e.g. OpenHarmony, Android) deny with
// `UVWASI_EACCES`, making the wasm fallback fail to load on exactly the
// platforms that need it.
//
// We patch `@napi-rs/cli`'s templates to retry with narrower preopens
// (see `patches/@napi-rs__cli@3.7.4.patch`). Simulate such a sandbox by
// denying `/` preopens.
'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)
}
}
`,
Comment on lines +172 to +187

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.

'src/index.js': js`
const className = "content-['src/index.js']"
module.exports = { className }
`,
'index.mjs': js`
import { createRequire } from 'node:module'
import { join } from 'node:path'

let require = createRequire(import.meta.url)
let { Scanner } = require('@tailwindcss/oxide')

let loaded = Object.keys(require.cache)

let scanner = new Scanner({
sources: [
{
base: join(process.cwd(), 'src'),
pattern: '**/*',
negated: false,
},
],
})

console.log(
JSON.stringify({
wasi: loaded.some((file) => file.endsWith('tailwindcss-oxide.wasi.cjs')),
candidates: scanner.scan(),
}),
)
process.exit()
`,
},
},
async ({ expect, exec }) => {
// See the note about NODE_PATH in the test above.
let output = await exec(`node --require ./preload.cjs index.mjs`, { env: { NODE_PATH: '' } })

// Only parse the first line, because Node prints an `ExperimentalWarning`
// about WASI to stderr, which `exec` appends to the output.
let { wasi, candidates } = JSON.parse(output.trim().split('\n')[0])

// The wasm32-wasi binding loaded despite `/` being denied, ...
expect(wasi).toBe(true)

// ... and scanning files under the current working directory still works
// through the narrower preopen.
expect(candidates).toMatchInlineSnapshot(`
[
"className",
"const",
"content-['src/index.js']",
"exports",
]
`)
},
)
80 changes: 80 additions & 0 deletions patches/@napi-rs__cli@3.7.4.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
diff --git a/dist/cli.js b/dist/cli.js
index c4c2d568bc070c07dbf3a1f9f4a0c946466668cf..353bbca86e3261a2ba9a8fc56dc612c7c84dab6d 100755
--- a/dist/cli.js
+++ b/dist/cli.js
@@ -1,4 +1,13 @@
#!/usr/bin/env node
+// PATCHED (see patches/@napi-rs__cli@3.7.4.patch): the embedded wasi loader
+// and worker templates below retry `new WASI(...)` with narrower preopens,
+// because preopening `/` throws `UVWASI_EACCES` on sandboxed platforms (e.g.
+// OpenHarmony), which would make the generated wasm binding fail to load.
+//
+// The same templates also exist in `dist/index.js` and `dist/index.cjs` (the
+// programmatic API). Those are intentionally NOT patched because we only
+// build through the `napi` bin, which runs this file. If we ever start using
+// the programmatic API, update the patch to cover those bundles too.
import { createRequire } from "node:module";
import { Cli, Command, Option } from "clipanion";
import path, { basename, dirname, isAbsolute, join, parse, resolve } from "node:path";
@@ -1021,13 +1030,25 @@ const {

const __rootDir = __nodePath.parse(process.cwd()).root

-const __wasi = new __nodeWASI({
- version: 'preview1',
- env: process.env,
- preopens: {
- [__rootDir]: __rootDir,
+const __wasi = (() => {
+ // Preopening '/' fails with UVWASI_EACCES in sandboxed environments (e.g.
+ // OpenHarmony, Android), which would prevent the wasm binding from loading
+ // at all. Retry with narrower preopens instead. Without any preopens the
+ // binding still loads; only file system access is unavailable.
+ let lastError = null
+ for (const dir of [__rootDir, process.cwd(), null]) {
+ try {
+ return new __nodeWASI({
+ version: 'preview1',
+ env: process.env,
+ preopens: dir === null ? {} : { [dir]: dir },
+ })
+ } catch (error) {
+ lastError = error
+ }
}
-})
+ throw lastError
+})()

const __emnapiContext = __emnapiGetDefaultContext()

@@ -1151,13 +1172,22 @@ const __rootDir = parse(process.cwd()).root;

const handler = new MessageHandler({
onLoad({ wasmModule, wasmMemory }) {
- const wasi = new WASI({
- version: 'preview1',
- env: process.env,
- preopens: {
- [__rootDir]: __rootDir,
- },
- });
+ // Keep in sync with the preopen fallback in the main-thread loader.
+ const wasi = (() => {
+ let lastError = null;
+ for (const dir of [__rootDir, process.cwd(), null]) {
+ try {
+ return new WASI({
+ version: 'preview1',
+ env: process.env,
+ preopens: dir === null ? {} : { [dir]: dir },
+ });
+ } catch (error) {
+ lastError = error;
+ }
+ }
+ throw lastError;
+ })();

return instantiateNapiModuleSync(wasmModule, {
childThread: true,
5 changes: 3 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ packages:
- 'integrations'

patchedDependencies:
'@napi-rs/cli@3.7.4': patches/@napi-rs__cli@3.7.4.patch
'@parcel/watcher@2.6.0': patches/@parcel__watcher@2.6.0.patch
lightningcss@1.33.0: patches/lightningcss@1.33.0.patch

Expand Down