English
Development on BSD Systems (OpenBSD/FreeBSD)
This guide covers BSD-specific setup requirements for developing with Tailwind CSS v4 and Patchright E2E testing.
Overview
Tailwind CSS v4 migrated from PostCSS to Rust-based native binaries for better performance. Two native modules are required:
- lightningcss - CSS parsing, transformation, and minification
- @tailwindcss/oxide - Tailwind CSS v4 core engine
These packages don't provide pre-built binaries for OpenBSD/FreeBSD, so they must be built from source.
Prerequisites
bash
# Install Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify installation
cargo --version
rustc --versionBuilding lightningcss
1. Clone and Build
bash
# Clone repository
cd /tmp
git clone https://github.com/parcel-bundler/lightningcss.git
cd lightningcss
# Build the Node.js addon
cargo build --release -p lightningcss_node
# Build the CLI tool (optional)
cargo build --release --bin lightningcss --features cli2. Install Binaries
bash
# Create storage directory
mkdir -p ~/.local/lib/node-native-openbsd
# Copy Node.js addon
cp target/release/liblightningcss_node.so \
~/.local/lib/node-native-openbsd/lightningcss.openbsd-x64.node
# Copy CLI tool (optional)
cp target/release/lightningcss ~/.cargo/bin/Note: The .so shared library is renamed to .node with platform suffix for Node.js NAPI-RS loading.
Building @tailwindcss/oxide
1. Clone and Build
bash
# Clone repository
cd /tmp
git clone https://github.com/tailwindlabs/tailwindcss.git
cd tailwindcss
# Build the oxide crate
cargo build --release -p tailwind-oxide2. Install Binary
bash
# Copy to storage directory
cp target/release/libtailwind_oxide.so \
~/.local/lib/node-native-openbsd/tailwindcss-oxide.openbsd-x64.nodeInstalling Dependencies
npm install with --legacy-peer-deps
Due to peer dependency conflicts between Vite 7 and @sveltejs/[email protected] (which expects Vite 6), you must use the --legacy-peer-deps flag:
bash
# In web/admin
cd web/admin
npm install --legacy-peer-deps
# In web/site
cd web/site
npm install --legacy-peer-depsWhy this is needed:
- Vite 7 is required for the latest features
@sveltejs/[email protected]declares a peer dependency on Vite 6- The packages work correctly together despite the version mismatch
--legacy-peer-depsbypasses npm's strict peer dependency checking
SvelteKit Sync
After installing dependencies, run SvelteKit sync to generate TypeScript configs:
bash
# In web/admin
cd web/admin
./node_modules/.bin/svelte-kit sync
# In web/site
cd web/site
./node_modules/.bin/svelte-kit syncAutomated Installation
After building binaries once, use the postinstall script to automatically restore them after npm install:
Run Manually
bash
# From project root
node scripts/postinstall-openbsd-natives.jsAdd to package.json (Optional)
json
{
"scripts": {
"postinstall": "node scripts/postinstall-openbsd-natives.js"
}
}What the Postinstall Script Does
The script (scripts/postinstall-openbsd-natives.js) performs two operations:
1. Copy Native Binaries
Copies pre-built binaries from ~/.local/lib/node-native-openbsd/ to:
web/admin/node_modules/@tailwindcss/node/node_modules/lightningcss/lightningcss.openbsd-x64.nodeweb/admin/node_modules/@tailwindcss/oxide/tailwindcss-oxide.openbsd-x64.nodeweb/site/node_modules/@tailwindcss/node/node_modules/lightningcss/lightningcss.openbsd-x64.nodeweb/site/node_modules/@tailwindcss/oxide/tailwindcss-oxide.openbsd-x64.node
2. Patch JavaScript Loaders
For @tailwindcss/oxide/index.js:
Adds OpenBSD/FreeBSD platform detection before the "Unsupported OS" error:
javascript
} else if (process.platform === 'openbsd' || process.platform === 'freebsd') {
if (process.arch === 'x64') {
try {
return require('./tailwindcss-oxide.openbsd-x64.node')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on BSD: ${process.arch}`))
}
} else {
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
}For lightningcss:
No patching required—lightningcss already supports custom platform binaries via NAPI-RS naming convention.
Why Patching is Needed
NAPI-RS (Rust-to-Node.js bindings) generates platform detection code automatically, but only for "officially supported" platforms (Windows, macOS, Linux). The generated index.js contains an if/else chain checking process.platform:
javascript
if (process.platform === 'darwin') {
// macOS loading
} else if (process.platform === 'linux') {
// Linux loading
} else if (process.platform === 'win32') {
// Windows loading
} else {
// Error: Unsupported OS
}The patch adds OpenBSD/FreeBSD branches before the final error, telling Node.js to load the .openbsd-x64.node binary.
File Naming Convention
Native Node.js addons follow this pattern:
<package-name>.<platform>-<arch>.nodeExamples:
lightningcss.openbsd-x64.nodetailwindcss-oxide.openbsd-x64.nodetailwindcss-oxide.linux-x64.node(comparison)
Verification
After running the postinstall script, verify builds work:
bash
# Test admin build
cd web/admin
npm run build
# Test site build
cd web/site
npm run buildExpected output:
vite v7.3.6 building ssr environment for production...
✓ 305 modules transformed.
✓ built in X.XXsTroubleshooting
npm ERESOLVE unable to resolve dependency tree
Error:
npm error ERESOLVE unable to resolve dependency tree
npm error peer vite@"^6.0.0" from @sveltejs/[email protected]Cause: Peer dependency conflict between Vite 7 and @sveltejs/vite-plugin-svelte.
Solution:
bash
npm install --legacy-peer-deps"Cannot find module '../lightningcss.openbsd-x64.node'"
Cause: Binary not copied to node_modules.
Solution:
bash
node scripts/postinstall-openbsd-natives.js"Cannot find native binding"
Cause: The oxide index.js wasn't patched.
Solution:
bash
# Re-run postinstall (includes patching)
node scripts/postinstall-openbsd-natives.jsBuild fails with "Unsupported OS: openbsd"
Cause: Patch wasn't applied or was overwritten by npm install.
Solution:
bash
# Always run postinstall after npm install
npm install --legacy-peer-deps
node scripts/postinstall-openbsd-natives.jsCargo build fails
Common issues:
- Missing Rust: Install from https://rustup.rs
- Puppeteer error during npm install in lightningcss repo:
- Don't use
npm buildin the cloned repos - Use
cargo builddirectly instead
- Don't use
- Old Rust version: Update with
rustup update
Alternative: Manual Installation
If the postinstall script doesn't work, manually copy and patch:
bash
# Copy binaries
cp ~/.local/lib/node-native-openbsd/lightningcss.openbsd-x64.node \
web/admin/node_modules/@tailwindcss/node/node_modules/lightningcss/
cp ~/.local/lib/node-native-openbsd/tailwindcss-oxide.openbsd-x64.node \
web/admin/node_modules/@tailwindcss/oxide/
# Patch oxide (lightningcss needs no patch)
# Edit web/admin/node_modules/@tailwindcss/oxide/index.js
# Add the OpenBSD/FreeBSD branch shown aboveMaintenance
After any npm install that updates @tailwindcss/oxide or lightningcss:
bash
# Re-apply patches and binaries
npm install --legacy-peer-deps
node scripts/postinstall-openbsd-natives.jsNote: Always use --legacy-peer-deps when running npm install to avoid peer dependency conflicts between Vite 7 and SvelteKit plugins.
Consider adding to package.json scripts to run automatically.
E2E Testing with Patchright
Overview
Patchright (Playwright fork) requires platform-specific patches and ffmpeg setup for video recording on BSD systems.
Patchright Platform Patches
The scripts/patch-patchright-openbsd.js script automatically patches Patchright after npm install to support OpenBSD/FreeBSD:
Patches Applied:
- Cache directory detection (3 locations)
- User agent platform mapping
- Window insets configuration
- Chrome channel executable path
- Platform detection for BSD systems
- ffmpeg platform mapping
These patches run automatically via the postinstall hook in package.json.
ffmpeg Setup for Video Recording
The Problem
Playwright/Patchright expects a bundled ffmpeg binary at:
~/.cache/ms-playwright/ffmpeg-1011/ffmpeg-linuxHowever, Patchright doesn't distribute ffmpeg binaries for Linux (including OpenBSD which maps to linux-x64). The patchright install ffmpeg command fails because they expect Linux systems to have ffmpeg installed system-wide.
The Solution
Your system's ffmpeg is fully functional with all necessary codecs. Simply symlink it to where Patchright expects it:
bash
# Install ffmpeg (if not already installed)
doas pkg_add ffmpeg
# Create Patchright cache directory
mkdir -p ~/.cache/ms-playwright/ffmpeg-1011
# Symlink system ffmpeg to expected location
ln -sf /usr/local/bin/ffmpeg ~/.cache/ms-playwright/ffmpeg-1011/ffmpeg-linuxVerification
bash
# Verify symlink
ls -la ~/.cache/ms-playwright/ffmpeg-1011/
# Should show:
# lrwxr-xr-x 1 user group 21 Jul 24 08:55 ffmpeg-linux -> /usr/local/bin/ffmpegVideo Recording Configuration
In playwright.config.ts, video recording options:
typescript
video: 'off', // Disabled - no video recording
video: 'on', // Always record (requires ffmpeg)
video: 'retain-on-failure', // Only save videos when tests fail (recommended)
video: 'on-first-retry', // Only record on retriesRecommended: retain-on-failure - saves disk space while keeping videos for debugging failures.
System Requirements for E2E Testing
bash
# Required packages
doas pkg_add chromium ffmpeg
# Verify installations
chrome --version # Should show Chromium version
ffmpeg -version # Should show ffmpeg 8.x with codecsPatchright ships no browser for the BSDs, so point the suite at the system one:
bash
E2E_EXECUTABLE_PATH=/usr/local/bin/chrome npm run test:e2eRunning E2E Tests
bash
# Build and run all tests
npm run test:e2e
# Run without rebuilding
npm run test:e2e:nobuild
# Run with UI (interactive mode)
npm run test:e2e:ui
# Run with debugging
npm run test:e2e:debug
# Run on another port, when something already holds 8080
E2E_PORT=8090 npm run test:e2e
# View test report
npm run test:e2e:reportTroubleshooting E2E Tests
"Executable doesn't exist at .../ffmpeg-linux"
Cause: ffmpeg symlink not created.
Solution:
bash
mkdir -p ~/.cache/ms-playwright/ffmpeg-1011
ln -sf /usr/local/bin/ffmpeg ~/.cache/ms-playwright/ffmpeg-1011/ffmpeg-linux"ERROR: Patchright does not support ffmpeg on linux-x64"
Cause: This is expected - Patchright doesn't distribute ffmpeg for Linux.
Solution: Use the symlink approach above instead of patchright install ffmpeg.
Tests fail to start Chrome
Cause: Chromium not installed, or the suite is looking for patchright's own build, which does not exist for the BSDs.
Solution:
bash
doas pkg_add chromium
which chrome # Should show /usr/local/bin/chrome
E2E_EXECUTABLE_PATH=/usr/local/bin/chrome npm run test:e2ePatchright patches not applied
Cause: npm install was run but postinstall hook didn't execute.
Solution:
bash
npm run postinstall
# Should show: "✅ Successfully applied X patches for openbsd"Why System ffmpeg Works
OpenBSD's ffmpeg (version 8.x) includes all codecs Playwright needs:
- Video: H.264 (libx264), H.265 (libx265), VP8/VP9 (libvpx), AV1 (libaom, libdav1d, libsvtav1)
- Audio: AAC, Opus, Vorbis, MP3 (libmp3lame)
- Containers: MP4, WebM, Matroska
This is maintained by the OpenBSD ports team and kept up-to-date, making it more reliable than bundled binaries.
Related Files
scripts/postinstall-openbsd-natives.js- Automated patch and binary installer for Tailwindscripts/patch-patchright-openbsd.js- Patchright platform patches for E2E testingplaywright.config.ts- E2E test configuration~/.local/lib/node-native-openbsd/- Storage for pre-built Tailwind binaries~/.cache/ms-playwright/- Patchright cache directory
Technical Background
Why Tailwind v4 Needs Native Binaries
Tailwind CSS v4 architecture:
- v3: PostCSS plugin (pure JavaScript, slow)
- v4: Rust-based engine (compiled native code, ~100x faster)
The Rust code is compiled to platform-specific shared libraries (.so on BSD/Linux, .dylib on macOS, .dll on Windows) and wrapped as Node.js native addons (.node files) using NAPI-RS.
Why .so → .node Renaming
Node.js expects native addons with .node extension. The Rust build produces:
liblightningcss_node.so(Rust/Cargo naming)libtailwind_oxide.so(Rust/Cargo naming)
NAPI-RS loader expects:
<package>.<platform>-<arch>.node(Node.js naming)
The copy step renames and moves these files to match Node.js conventions.
NAPI-RS Loading Mechanism
- Package's
index.jschecksprocess.platformandprocess.arch - Constructs filename:
<package>.${platform}-${arch}.node - Attempts
require('./constructed-filename') - If not found, tries fallback platforms
- If all fail, throws "Cannot find native binding"
Our patch adds step 2.5: "If BSD, load .openbsd-x64.node"