Scripting overview

ferridriver embeds a QuickJS engine (ferridriver-script) and exposes it to four first-class surfaces. All four share one bundler (rolldown), one bytecode cache, one set of globals, and one sandbox.

SurfaceEntry pointWhat you write
MCP run_scripttool call from an LLM clientsandboxed JavaScript against the live browser session
BDD JS / TS stepsferridriver bdd --steps 'steps/**/*.{js,ts}'Given / When / Then step bodies for .feature files
Extensionsextensions = ["@scope/pkg", "./extensions"] in ferridriver.tomlESM packages or source files that register MCP tools (tool) and / or BDD steps
Standalone scriptsferridriver run script.tsPlaywright-style scripts with chromium() / firefox() / webkit() globals

All four reach the same Rust core (Page, BrowserContext, Locator, APIRequestContext). The JS layer is a thin binding — the hot path (actionability checks, polling, network) stays in Rust.

Pipeline (compile once, run many)

source files (.js / .ts / .mjs / .tsx / ...)
        │
        ▼  rolldown (TypeScript + node_modules + tree-shake, Platform::Neutral, OutputFormat::Esm)
        │
        ▼  QuickJS bytecode (in-memory, content-hash cached)
        │
        ▼  Module::load per session VM (no re-parse, no resolver — imports already inlined)
        │
        ▼  top-level Given() / tool() side effects populate the Rust ExtensionRegistry
  • Imports work. import './helpers.ts', import pkg from 'some-dep', and import { tool, bdd } from 'ferridriver' are bundled and tree-shaken. Cucumber-compatible code can import from @cucumber/cucumber.

  • No Node, no Bun in the run path. Rolldown runs Platform::Neutral; QuickJS has no Node builtins. The runtime serves a small set of them natively instead, importable with or without the node: prefix and also reachable through require():

    ModuleSurface
    fsThe scoped file I/O of the fs global
    pathPOSIX join / resolve / dirname / basename / extname / normalize / relative / isAbsolute / sep / delimiter
    bufferBuffer — a real Uint8Array subclass, so index access and every typed-array method work. from / alloc / allocUnsafe / concat / isBuffer / isEncoding / byteLength, instance toString / write / copy / subarray / equals / toJSON and the readUInt32BE-style numeric accessors, plus atob / btoa / constants. The string-aware includes / indexOf / fill overrides and swap* / compare are not implemented
    osplatform / arch / type / release / version / machine / endianness / EOL / devNull / homedir / tmpdir / hostname / availableParallelism / cpus / totalmem / freemem / uptime / loadavg / userInfo / getPriority / setPriority / networkInterfaces. No os.constants
    utilformat / formatWithOptions / inspect (the same renderer console uses) / promisify / callbackify / deprecate / inherits / types.* / isDeepStrictEqual, plus the TextEncoder / TextDecoder globals
    eventsEventEmitter — the same class require('events') returns, so a subclass built either way passes instanceof
    assertThe callable assert, ok / equal / strictEqual / deepEqual / deepStrictEqual (and the not… twins) / match / doesNotMatch / throws / doesNotThrow / rejects / doesNotReject / fail / ifError, plus assert.strict and assert/strict. Failures throw an AssertionError carrying actual / expected / operator / code
    urlThe URL / URLSearchParams globals, plus fileURLToPath / pathToFileURL / format. No legacy url.parse
    processThe process global itself — import process from 'node:process' and globalThis.process are one object
    timersThe runtime's setTimeout / setInterval / setImmediate and their clear… twins; timers/promises adds the promise-returning setTimeout / setImmediate

    Anything else Node ships (child_process, net, http, worker_threads, …) is absent by design.

  • Bytecode is cached in-memory and on disk. An in-process cache serves repeat compiles within one process; a cross-process disk cache (under the user cache dir, or FERRIDRIVER_CACHE_DIR) lets an unchanged source tree skip both rolldown and the QuickJS compile on a fresh start. Disk entries live under an ABI-tag directory (QuickJS version, arch, endianness, pointer width) so Module::load only ever reads bytecode from a matching toolchain; a mismatch misses and recompiles. Set FERRIDRIVER_NO_BYTECODE_CACHE to disable the disk cache.

  • One bad file does not abort the batch. Bundle / compile failures are reported per file and skipped; the server still starts.

  • Errors are source-mapped. A thrown error in a bundled step is reported at the original .ts:line:col, stack included.

Picking a surface

  • MCP run_script — agent-driven flows. One tool call runs many browser operations in one LLM turn. See /mcp/tools and the run_script reference.
  • BDD JS / TS steps — human-readable .feature files with step bodies your TS team already knows how to write. See /scripting/bdd-js-api and /bdd/overview.
  • Extensions — when one .ts file should contribute both a reusable MCP tool and matching BDD step. The same file can ship both. See /scripting/extensions.
  • Standalone scripts — one-off automation runs from the CLI (ferridriver run).
  • Named sessions — the same scripts against a browser that stays open between commands, so state accumulates across runs (ferridriver run --session <id>). See /scripting/named-sessions.

Pages in this section

  • Extensions — tool, exposeAsMcpTool, ferridriver.host, handler context, lifecycle.
  • BDD JS / TS API — full Cucumber-shaped reference: Given / When / Then / defineStep, hooks, World, DataTable, parameter types.
  • run_script reference — the MCP action path: parameters, return shape, globals.
  • Named sessions — ferridriver run --session: a browser that outlives the command, streamed console, where bundling and compiling each happen.
  • Sandbox — process, fetch, fs, AbortController, ReadableStream, Blob, FormData, what is absent.
  • Capabilities — allow.commands (declared shell / argv commands) and allow.net (HTTP host allow-list).
  • State and sessions — globalThis vs vars, session lifetime, isolation, BDD per-scenario World.

Design rationale (why the system is shaped this way — VS Code / Deno / WASM / Rollup comparison) is in the maintainer note docs/plugin-architecture.md.