Skip to content

feat(runtime): add a JavaScript evaluation API with named arguments, keeping the old one with warnings - #659

Closed
route wants to merge 1 commit into
mainfrom
feat/js-api-compat
Closed

route wants to merge 1 commit into
mainfrom
feat/js-api-compat

Conversation

@route

@route route commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Redesigns the JavaScript evaluation API so it can land at once without breaking anyone's scripts. FERRUM_JS_API picks how old-style scripts run:

Release Default Switch
0.20 (this PR) legacy: old-style scripts run as before and warn FERRUM_JS_API=new to try the new way
0.21 new FERRUM_JS_API=legacy to go back while migrating
0.22 new removed, along with evaluate_async, evaluate_func, evaluate_on

The old code lives untouched (apart from warnings) in Frame::Runtime::Legacy, and the new pipeline is separate, so 0.21 is a one-line default flip and 0.22 deletes legacy.rb and utils/deprecate.rb. The switch is an environment variable rather than a browser option, so it's clearly temporary and removing it breaks no code.

New API

page.evaluate("a + b", a: 1, b: 2)                              # named arguments become parameters
page.evaluate("function(a, b) { return a - b }", b: 1, a: 3)    # a declaration is used as-is, bound by name
page.evaluate("await fetch(url).then(r => r.status)", url: "/") # await works, timeout: bounds it
list = page.evaluate_handle("document.querySelectorAll('li')") # => Ferrum::RemoteObject
node.execute("this.value = text", text: "hi")                   # Node#execute, Node#evaluate_handle
  • Scripts are read by a small scanner (Frame::Script) that skips strings, template literals, regular expressions and comments.
  • The timeout is enforced in Ruby rather than with a Promise.race in the page, so pages that replace Promise or mock timers don't break it. ScriptTimeoutError is now a TimeoutError, so rescue Ferrum::TimeoutError keeps catching it.
  • Positional arguments keep working in both modes. In the new mode they can't be mixed with named ones.

Legacy mode

A call takes the new path, even in legacy mode, if it uses something only the new API has: named arguments, a function declaration, await, timeout:, or a new method. So Ferrum's own calls and libraries that use these forms behave the same in both modes. Anything else runs the code from main unchanged.

For those old-style calls, Ferrum detects what the new way does differently and warns with a suggested rewrite (the full table is in docs/13-javascript.md, "Upgrading to 0.21"):

Detected How
Keywords read through arguments, or mixed with positional arguments arguments given
Several statements, "a(); b" Runtime.compileScript (parses, doesn't run), cached per script
A script after a leading line break never runs today scanner
A promise or thenable will be awaited result subtype, or Runtime.getProperties for a then function
A thrown non-Error will raise exceptionDetails with a non-error result
Node#evaluate will resolve nodes and dates result subtype, or Runtime.getProperties for arrays

The script runs once, and detection never runs code on its result, so getters and proxy traps don't fire. Warnings point at the first caller outside installed gems, which for Capybara users is the line calling execute_script, and print once per call site. FERRUM_DEPRECATION_WARNINGS=raise turns them into errors, and 0 silences them.

Verified

A script comparing about 40 old-style calls against main returns identical results in legacy mode. The one exception is evaluate("new Error('x')"), which used to crash with NoMethodError and now raises JavaScriptError. spec/frame/runtime/legacy_spec.rb pins those results and warnings, and spec/frame/runtime_spec.rb covers FERRUM_JS_API=new. The suite passes in both modes.

Replaces #660 and #661.

@route
route force-pushed the feat/js-api-compat branch 5 times, most recently from 73bae46 to b69b229 Compare October 22, 2026 16:58
@route route changed the title feat(runtime): add a JavaScript evaluation API with named arguments, keeping the old one with warnings feat(runtime): add a JavaScript evaluation API with named arguments and a js_api switch for old scripts Oct 22, 2026
…keeping the old one with warnings

`#evaluate`, `#evaluate_async`, `#execute`, `#evaluate_func` and `#evaluate_on` had four argument conventions and three
return semantics between them, and scripts reached their arguments through `arguments[0]`. This adds one convention for
all of them, without changing what any existing call returns by default.

New: keyword arguments become the script's parameters, `page.evaluate("a + b", a: 1, b: 2)`, bound by name when the
script is a function declaration. A script may be a function or arrow declaration, used as-is, and may `await`; promises
it returns are awaited, bounded by `timeout:` and raising `ScriptTimeoutError`, now a `TimeoutError`. Added
`#evaluate_handle`, returning a `Ferrum::RemoteObject`, and `Node#execute`/`Node#evaluate_handle`. Scripts are read by a
small scanner, `Frame::Script`, that skips strings, template literals, regular expressions and comments.

Compatible: by default, a call that doesn't use any of that runs exactly as before, through
`Runtime::Legacy`, which also keeps `#evaluate_async`, `#evaluate_func` and `#evaluate_on`. Positional arguments keep
working. What the new way does differently is detected and warned about with a rewrite, pointing at the first caller
outside installed gems: keywords read through `arguments`, mixed positional and keyword arguments, scripts of several
statements or that never run, promises and thenables that will be awaited, thrown non-Errors that will raise, and nodes
`Node#evaluate` will resolve. Detection reads the result and its properties without running code on it.
`FERRUM_DEPRECATION_WARNINGS=raise` turns the warnings into errors, `0` silences them.

`FERRUM_JS_API=new` runs every call the new way. 0.21 makes it the default, keeping `FERRUM_JS_API=legacy` to switch
back, and 0.22 removes `Runtime::Legacy` and the deprecated methods.
@route
route force-pushed the feat/js-api-compat branch from b69b229 to a57fff3 Compare October 22, 2026 18:37
@route route changed the title feat(runtime): add a JavaScript evaluation API with named arguments and a js_api switch for old scripts feat(runtime): add a JavaScript evaluation API with named arguments, keeping the old one with warnings Oct 22, 2026
@route

route commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

Replaced by #673, a fresh PR with the same code and a migration guide in the description.

@route route closed this Oct 22, 2026
@route
route deleted the feat/js-api-compat branch October 22, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant