ff-ruyijs is a free, open source data extraction & web scraping project written in C++ and released under AGPL-3.0. It has 505 GitHub stars, 262 forks and 3 open issues, and was last pushed 27 hours ago. On this registry it ranks #147 of 149 tracked projects in Data Extraction & Web Scraping, with 5 head-to-head comparisons available.

What is ff-ruyijs?

ff-ruyijs — published on PyPI as ruyijs — is a headless browser runtime that embeds the genuine Firefox JavaScript engine and networking stack so Python developers can execute a site's JavaScript for scraping, automation and JS reverse engineering without launching a full browser.

What it is

ff-ruyijs is a C++ project, distributed as Python wheels, that packages Mozilla's SpiderMonkey (from Firefox 159) together with the Necko network stack and exposes them to Python. It is deliberately not a browser: there is no window, no layout engine and no renderer, only a JavaScript and Web API environment in which a site's own scripts, challenges, signatures and fingerprints run the way they would inside Firefox. The project describes itself as a minimalist browser built for AI-driven automation, and its source is released under AGPL-3.0 with a commercial licence available for closed-source or proprietary use.

The concrete problem it addresses is the weight and detectability of conventional headless automation. Headless Chrome and Playwright hand over an entire browser process — GPU rendering, window management, substantial memory use — and anti-bot vendors have spent years learning to recognise that process. ff-ruyijs takes the opposite route by keeping only the Firefox internals that matter for automation. The result is a runtime that starts quickly, stays light, and executes JavaScript through the real engine rather than an impersonation of it, which is the point for anyone whose pages only appear after a script has run.

Key capabilities

  • Executes site JavaScript on authentic SpiderMonkey from Firefox 159 and the Necko network stack, rather than a reimplemented or spoofed engine.
  • Runs with no window, no layout and no renderer, keeping startup fast and the process footprint small relative to a full browser.
  • Isolates each Context in its own process with a persistent JavaScript world and its own proxy, so sessions never leak into one another.
  • Supports offline operation: scripts and responses can be served locally to replay a captured flow with no network access at all.
  • Ships prebuilt wheels for Windows x64, macOS arm64 and Linux x86_64 on Python 3.10+, with the Windows wheel bundling a SHA256-pinned SpiderMonkey + Necko runtime of roughly 42 MB.
  • On macOS arm64, loads pages over direct HTTPS through libnecko_core.dylib and libnecko_quic.dylib, linked against the NSS from the same Firefox source; Context(proxy=...) uses an HTTP CONNECT tunnel into libnecko_core when that binary is absent.
  • Builds are produced by GitHub Actions from the official Firefox 159.0 source.

Who uses it and how

  • Web scraping teams working against sites that gate content behind JavaScript challenges, where a plain HTTP client returns nothing usable.
  • JavaScript reverse engineering and signature reproduction work; the README frames the project as research into next-generation AI browsers and explicitly rejects brute-force reverse engineering.
  • Automation pipelines that need many isolated sessions running in parallel, using one Context per process with its own proxy configuration.
  • Data collection workflows that must be reproducible offline, replaying a captured flow without touching the network.
  • Teams standardised on 64-bit Python 3.10+ across Windows x64, macOS arm64 or Linux x86_64.

Getting started

Install from PyPI with python -m pip install ruyijs; the wheels carry the runtime, so local scripts run offline immediately after installation with no browser download.

How it compares

Headless Chrome and Playwright deliver a complete browser — GPU rendering, window management and a memory-hungry process — whereas ff-ruyijs embeds only the Firefox internals relevant to automation and leaves the rest behind. That makes it a narrower tool than either: it targets JavaScript execution and networking rather than full page rendering. Its AGPL-3.0 licence with a paid commercial option also differs from permissively licensed automation stacks, which matters if closed-source distribution is planned.

When to use it — and when not

Choose it when a plain HTTP client cannot get your pages and you want Firefox-accurate JavaScript execution without a full browser. Be aware of the operating constraints: 64-bit Python only, macOS 26 or later on Apple Silicon, and Linux with glibc 2.39 or later, with no xpcshell in the macOS arm64 build. Because there is no renderer, anything requiring layout or visual output is out of scope, and closed-source or proprietary use requires a commercial licence rather than the AGPL grant.

project readme (upstream, from github) — read inline

🦊 ruyiJS

Run any site's JavaScript from Python — with the real Firefox engine.

A new-generation, minimalist browser purpose-built for AI-driven automation.
The authentic Firefox JavaScript engine and networking stack, with no window and no renderer —
made for web scraping, JS reverse engineering, data collection, and getting past bot checks.

PyPI Downloads Python License Windows macOS Linux LinuxDO

Sponsored by

           

Contributors

      

Install  ·  Features  ·  Verification systems  ·  Examples  ·  FAQ  ·  Contact  ·  LinuxDO  ·  English · 简体中文

⚠️ Disclaimer: This project is for researching next-generation AI browsers. Any form of brute-force reverse engineering is strictly rejected. The source code is open source under the AGPL-3.0; closed-source or proprietary use requires a commercial license.



Is ruyiJS for you?

Headless Chrome and Playwright hand you a whole browser: a GPU-rendering, window-managing, memory-hungry process that anti-bot vendors have spent years learning to detect. ruyiJS takes the opposite path — it embeds only the Firefox internals that matter for automation and leaves the rest behind:

  • The real engine, not an impersonation. Genuine SpiderMonkey (Firefox 159) and the Necko network stack execute a site's JavaScript, challenges, signatures, and fingerprints exactly as Firefox would.
  • Truly headless. No window, no layout, no renderer. A pure JavaScript + Web API environment that starts fast and stays light.
  • Isolated and reproducible. Each Context is its own process with a persistent JavaScript world and its own proxy, so sessions never leak into each other.
  • Offline when you need it. Serve scripts and responses yourself and replay a whole captured flow with no network.

If you scrape, reverse-engineer web JavaScript, or collect data behind a challenge, it's for you. If a plain HTTP client already gets your pages, you don't need it.

Wheels for Windows x64, macOS arm64 and Linux x86_64 are on PyPI for Python 3.10+. GitHub Actions builds each of them from the official Firefox 159.0 source (build from source).

📦 Installation

python -m pip install ruyijs

Requirements: 64-bit Python 3.10+ on Windows x64, on macOS 26 or later (Apple Silicon), or on Linux x86_64 with glibc 2.39 or later (Ubuntu 24.04, Debian 13, Fedora 40 and newer).

The Windows wheel ships the SHA256-pinned SpiderMonkey + Necko runtime (~42 MB). Local scripts run offline right after install — no browser download, no external dependencies.

macOS arm64: the macosx_26_0_arm64 wheel loads pages over direct HTTPS through libnecko_core.dylib and libnecko_quic.dylib, linked against the NSS of the same Firefox source. It has no xpcshell. Context(proxy=...) uses an HTTP CONNECT tunnel into libnecko_core when that binary is absent. The Necko helper runs on the Python that installed ruyijs; its packages are dependencies of the macOS wheel. Details and the remaining gaps are in docs/macos.md.

Linux x86_64: the manylinux_2_39_x86_64 wheel is a native ELF build, contributed by @Starktomy in #5. Text uses fontconfig, FreeType and HarfBuzz with the fonts installed on the machine, WebCrypto uses OpenSSL 3, and the Necko helper (NSS 3.131, Neqo for HTTP/3) brings its own Python. See docs/linux.md.

To upgrade:

python -m pip install --upgrade ruyijs

To verify the installation:

import ruyijs
print(ruyijs.__version__)   # 0.3.1

Build from source

git clone --recurse-submodules https://github.com/cutetailbud/ff-ruyijs

The firefox/ submodule is the official Firefox 159.0 release (mozilla-firefox/firefox at FIREFOX_159_0_RELEASE, a shallow clone). The workflows that build the PyPI wheels are the reference for a local build: .github/workflows/wheels.yml runs ci/firefox/build.sh js|browser|toolchain and tools/build.py on Windows and macOS, and the steps of ci/linux/build.sh on Linux.

🚀 Quick start

1 · Open a context and run JavaScript. Variables and functions persist across eval calls within the same context.

from ruyijs import Context

with Context() as context:
    context.eval("let count = 40")
    print(context.eval("count += 2"))        # 42
    print(context.eval("navigator.userAgent"))

3 · Point it at a real target. Configure the environment, load the page, and let the site's own JavaScript mint whatever token it demands.

with Context(proxy="http://127.0.0.1:7890") as context:
    context.configure_fingerprint({
        "navigator": {"language": "zh-CN", "hardwareConcurrency": 8},
        "locale": {"timezone": "Asia/Shanghai"},
    })
    context.load_page("https://target.example/", html)
    token = context.eval("window.__sign(payload)")   # run the page's signer
    print(token)

🏗️ How it fits together

Python  ──►  ruyiJS Context (isolated process)  ──►  Target site
                 ├─ SpiderMonkey — Firefox 159 JS engine
                 ├─ 38 browser contexts — DOM · Canvas · Workers · iframes · …
                 ├─ Skia — Firefox 159 2D graphics behind Canvas
                 └─ Necko — HTTP/1.1 · 2 · 3, per-context proxy

Your Python code drives a Context. Inside it, the genuine SpiderMonkey engine runs the page's scripts against a real DOM and 38 bounded browser subsystems, while in-page fetch/XHR ride the Necko transport. The implementation lives under src/ (domain, application, and every browser subsystem) and python/ruyijs/ (the binding).

✨ Features

Run a site's JavaScript

Execute scripts in a persistent context, load real HTML with iframes and classic scripts, and query the resulting DOM. References to live JS objects come back as JSObject / JSFunction you can call from Python.

Capability How
Execute JS, keep state context.eval(src) — variables and functions persist
Get a callable back context.eval(src, to_py=False) → JSFunction
Load a page context.load_page(url, html) — relative URLs resolve against url
Top-level navigation context.navigate(url) — no Referer, Sec-Fetch-Site: none
Query the DOM context.eval("document.querySelector('#x').textContent")
with Context() as context:
    context.set("config", {"offset": 2})
    add = context.eval("(value) => value + config.offset", to_py=False)
    print(add(40))  # 42

Configure the browser environment

Set Navigator, Screen, viewport, timezone, and locale through one validated profile surface. Commit it before the first page script runs.

with Context() as context:
    context.configure_fingerprint({
        "navigator": {"language": "zh-CN", "hardwareConcurrency": 8},
        "screen": {"width": 1920, "height": 1080},
        "viewport": {"width": 1280, "height": 720},
        "locale": {"timezone": "Asia/Shanghai"},
    })
    print(context.eval("[navigator.language, screen.width]"))  # ['zh-CN', 1920]

Native network over Necko

In-page fetch and XHR ride real HTTP/1.1, 2, and 3, through a proxy scoped to the context. Serve responses yourself with put_resource() to replay an entire captured flow offline. Inspect every request with net_log(). On macOS, pages load over direct HTTPS. A proxy uses the donor xpcshell when it is present, and an HTTP CONNECT tunnel into libnecko_core when it is not (docs/macos.md).

Native TLS fingerprint — switchable across Firefox 100 – 159

The network stack is Firefox's own Necko + NSS (the same TLS library real Firefox uses). The TLS ClientHello — cipher suites, extensions, ALPN, key shares, and their ordering — is produced by the real NSS library, not an emulation, so bot-detection systems that inspect TLS cannot distinguish ruyiJS traffic from a genuine Firefox session.

The Necko transport can freely switch between Firefox 100 – 159 TLS profiles. Each profile reproduces that version's exact ClientHello, giving you a different JA3 / JA4 fingerprint on the wire — useful for rotating browser identities, evading per-version blocks, or testing how a site reacts to different Firefox releases.

Choose the release per context with tls= ("100"–"159"; default 159). On Windows this sets the TLS ClientHello, HTTP/2 settings and HTTP/3 priorities of that context's Necko transport; set a matching user agent with configure_fingerprint when the page should agree. On macOS, direct HTTPS uses arm64 libnecko_core.dylib when that library reports NSS 3.131. The shell prefers dist/necko_tls/libnecko_core.dylib, then the staged dist/necko copy. An existing NECKO_CORE_LIBRARY wins. Context() and Context(tls="100") both opened https://example.com/ at status 200 and protocol h2. The measured handshake was 15 ciphers, 5 groups, and 3 key shares for 159, and 17, 6, and 2 for 100. Library GETs at the cluster boundaries 100, 119, 132, 135, 151, 154, 156, and 159 also returned 200 and h2, and the counts matched the profile table. The other versions were not each opened as a page. Omitted request headers and the HTTP/2 settings follow the 0.2.1 product table for that tls= value. A header the caller sets wins. HTTP/3 fields in that table are unused, and HTTP/3 is unchanged. A proxy, a missing library, or a different NSS build stays on the donor xpcshell. The proxy was not retested in this run. Plain HTTP still returns necko 804b0033 from that donor (docs/tls-profile.md). The PyPI macOS wheel has no xpcshell, so the proxy case is not available there.

with Context(proxy="http://127.0.0.1:7890", timeout=30, tls="120") as context:
    context.load_page("https://example.com/", "")
    context.eval("globalThis.r = null; fetch('/api').then(x => x.json()).then(v => r = v)")
    context.advance(25)               # drive the event loop until it settles
    print(context.eval("r"))

Canvas on Firefox's native Skia

Canvas 2D draws through Firefox's own official Skia (gfx/skia from the Firefox 159 release), not a re-implementation.

Python ↔ JavaScript bridge

Pass data in with set(), get callable JS functions out, and expose Python functions to the running script. Workers, Service Workers, and iframes are included.

with Context() as context:
    context.expose(lambda a, b: a + b, "add")
    print(context.eval("add(20, 22)"))  # 42

Many independent sessions in one process

A Context starts its own process. An Engine runs many Contexts as sessions of one process instead, each on its own thread with its own heap, page, clock, cookies, storage, proxy and TLS profile, in parallel. prewarm and network= trim the Necko helper processes, which are most of a Context's memory: four sessions measured 219 MiB in 5 processes against 385 MiB in 12 for four Contexts, and 151 MiB in 3 when they share one network (docs/engine.md).

from ruyijs import Context, Engine, use_engine

with Engine() as engine:
    a = engine.context(proxy="http://127.0.0.1:7890")
    b = engine.context(tls="120")
    with use_engine(engine):
        c = Context()          # code that creates its own Contexts joins the engine too

Deterministic clock

Drive timers with a logical clock for perfectly reproducible runs, or use the system clock for live work. The host advances time explicitly.

with Context() as context:
    context.configure_clock("logical", epoch_ms=1700000000000)
    context.eval("globalThis.result = 0; setTimeout(() => { result = 42 }, 10)")
    context.advance(10)
    print(context.eval("result"))  # 42

Errors and logs

JavaScript exceptions surface as JSError with name, message, and stack. console_log() and net_log() return the captured console and network records.

from ruyijs import Context, JSError

with Context(timeout=10) as context:
    try:
        context.eval("throw new TypeError('bad input')", filename="demo.js")
    except JSError as error:
        print(error.name, error.message)  # TypeError bad input
        print(error.stack)

🛡️ Verification systems it defeats

Each example drives the site's own JavaScript to produce the parameters it demands, then completes the flow end to end. All ship runnable in the package: python -m ruyijs.examples .

System Category Example
Akamai Bot Manager 3.x sensor_data + sbsd challenge akamai
DataDome Interstitial device-check (plv2 + plv3) datadome
CreepJS Fingerprint audit — passes every section creepjs

🔒 Strictly prohibited for any unlawful use. ruyiJS is provided solely for research into a new generation of AI browsers. Live results depend on current network conditions and each site's present responses.

🧪 Examples

Examples ship inside the package — no need to clone the repo:

python -m ruyijs.examples                      # list every example and its install path
python -m ruyijs.examples akamai               # Akamai Bot Manager 3.x
python -m ruyijs.examples datadome             # DataDome interstitial device-check
python -m ruyijs.examples creepjs              # CreepJS fingerprint audit
python -m ruyijs.examples --shared akamai datadome creepjs   # several at once, one shared engine

Every example supports --direct (direct connection) and --proxy http://127.0.0.1:7890. Add --help after a name for all options, and --output result.json to save results. Each example directory ships its own README.

📚 Documentation & API

Context.eval / set / expose Execute JS, pass data in, expose Python callbacks
load_page / navigate / put_resource Load HTML, top-level navigation, serve resources offline
configure_fingerprint / configure_environment Navigator, Screen, viewport, timezone, locale
configure_clock / advance / tick Logical vs. system clock, drive the event loop
JSError / console_log / net_log Errors as exceptions, console and network records
JSObject / JSFunction / get_runtime_path Live JS handles, bundled-runtime path
Engine / engine.context / use_engine Many independent Contexts as sessions of one process

📏 Scope & limitations

ruyiJS provides JavaScript execution and a broad Web API environment on Windows x64, macOS arm64 and Linux x86_64. It has no browser window and no full page layout or rendering — it is a tool for automation, analysis, and research. Use it in compliance with the terms of service of any site you interact with and with applicable law. The platform differences are in docs/macos.md and docs/linux.md.

Related: ruyiHttp — a Requests-style Python HTTP library built on Firefox's native networking components.

❓ FAQ / Troubleshooting

What Python versions are supported?

64-bit Python 3.10 and above on every platform, tested up to Python 3.14. On macOS the Necko helper runs on the interpreter that installed ruyijs; the Linux wheel brings its own Python 3.14 for it.

Does it work on macOS or Linux?

Yes. PyPI has a macosx_26_0_arm64 wheel (macOS 26 or later) and a manylinux_2_39_x86_64 wheel (glibc 2.39 or later). The macOS wheel has no xpcshell; since 0.3.1 Context(proxy=...) uses an HTTP CONNECT tunnel there. See docs/macos.md and docs/linux.md.

The JS engine is Mozilla's official SpiderMonkey and the networking stack is Mozilla's official Necko — both are built from the official Firefox source, referenced as the firefox/ submodule.

Do I need to download a browser?

No. Every wheel bundles the SpiderMonkey + Necko runtime built from the Firefox 159.0 source, and pip install ruyijs needs no browser. On macOS, pip also installs the Python packages of the Necko helper.

Context hangs or doesn't respond
  • Make sure you use the with statement (with Context() as ctx:) so the process is cleaned up properly.
  • If using a proxy, verify it is reachable: context = Context(proxy="http://127.0.0.1:7890").
  • Increase the timeout: Context(timeout=60).
  • On Wine, upgrade to ≥ 0.1.5 which fixes a known navigate deadlock.
JSError: script error

The site's JavaScript threw an exception. Inspect error.name, error.message, and error.stack to locate the cause. If the page relies on APIs not yet implemented, the stack trace will point to the missing call.

How do I use a proxy?
with Context(proxy="http://127.0.0.1:7890") as ctx:
    ctx.navigate("https://example.com/")

HTTP, HTTPS, and SOCKS5 proxies are supported per-context.

How do I run examples?
python -m ruyijs.examples              # list all examples
python -m ruyijs.examples akamai --proxy http://127.0.0.1:7890
python -m ruyijs.examples creepjs

💬 Contact

Questions, permission requests, commercial licensing, or just to talk shop — add the author on WeChat, or scan to join the discussion group.


WeChat: 如意

Group: AI浏览器ruyijs过检测交流群3

The group QR refreshes periodically; if it has expired, add the author on WeChat (如意) for an invite.

📄 License

ff-ruyijs is dual-licensed:

  • AGPL-3.0 — free for any use, commercial included, as long as you comply: if you distribute ff-ruyijs or a modified version, or offer it to users over a network (a hosted service or API), you must publish the complete source of your version under the AGPL-3.0.
  • Commercial license — required to use ff-ruyijs in closed-source products or services, or to keep your modifications private.

For a commercial license, contact via WeChat (ID: 如意) or open an issue on GitHub titled "Commercial license".

Mozilla-derived code (SpiderMonkey, Necko) stays under the MPL-2.0. Releases up to 0.2.1 keep the license they shipped with.

See NOTICE for third-party attributions.

If ff-ruyijs saved you from reimplementing a browser, a ⭐ helps others find it.

Frequently asked questions

Is ff-ruyijs free to use?

ff-ruyijs is open source under the AGPL-3.0 licence. There is no licence fee and no seat count — you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does ff-ruyijs do?

Run any site JavaScript from Python with the real Firefox engine. Headless SpiderMonkey + Necko for automation and scraping.

What is ff-ruyijs written in?

ff-ruyijs is primarily written in C++. Its source is publicly available at https://github.com/cutetailbud/ff-ruyijs, and it has 505 GitHub stars.