Skip to content

feat: redesign the JavaScript evaluation API around named arguments - #617

Open
route wants to merge 1 commit into
mainfrom
feat-new-js-api
Open

feat: redesign the JavaScript evaluation API around named arguments#617
route wants to merge 1 commit into
mainfrom
feat-new-js-api

Conversation

@route

@route route commented Aug 23, 2026

Copy link
Copy Markdown
Member

#evaluate, #evaluate_async, #execute, #evaluate_func and #evaluate_on had four different argument conventions and three different return semantics between them. Scrip#evaluate_async spliced its resolve callback in at arguments[arguments.length], so the callback's index shifted with the number of arguments passed. Neither Puppeteer nor Playwright ships an asynchronous variant at all — both always await the returned promise — and CDP has awaitPromise, so none of that machinery was still earning its place.

The API is now three entry points that share one script shape and one argument style, plus the same three on Node with this bound to the element:

  • Keyword arguments become the script's function parameters, in order: page.evaluate("a + b", a: 1, b: 2). Each is sent as its own protocol argument, so a Node still arrives in JavaScript as the live element. When the script is a function declaration, values bind to its own parameter names rather than to hash order.
  • A script is either a bare expression, which gets wrapped, or a function/arrow declaration, which is used as-is. This folds #evaluate_func into #evaluate and is how multi-statement scripts are written.
  • Promises are always awaited, so evaluate("await fetch(url)", url: "/x") works without a separate method. timeout: (seconds, defaulting to the page timeout) bounds the script browser-side and raises ScriptTimeoutError; 0 disables it.
  • #evaluate_handle returns a Ferrum::RemoteObject, an opaque reference to a browser-side value that can be passed straight back in as an argument.
  • Added Node#execute and Node#evaluate_handle.ts reached their arguments through arguments[0], and

Nothing breaks. arguments[n] is still populated inside the generated function, #evaluate_on remain as shims. All four paths warn once per message and call site; FERRUM_DEPRECATION_WARNINGS=raise turns the warnings into errors while migrating a suite, =0 silences them.

Two behaviour changes worth noting: Node#evaluate resolves its result the way Page#evaluate does, so node.evaluate("this.parentNode") returns a Node instead of an empty hash — it previously ran with returnByValue, which flattened every DOM result — and #evaluate now awaits a returned promise instead of serializing it to an empty object.

Detection of a function declaration is deliberately conservative: a script beginning with function counts as one only if it ends at the closing brace, so the IIFE function() { ... }() stays an expression, and arrow detection accepts only a plain identifier parameter list, so (() => 1)() does too.

`#evaluate`, `#evaluate_async`, `#execute`, `#evaluate_func` and `#evaluate_on`
had four different argument conventions and three different return semantics
between them. Scrip`#evaluate_async` spliced its resolve callback in at `arguments[arguments.length]`,
so the callback's index shifted with the number of arguments passed. Neither
Puppeteer nor Playwright ships an asynchronous variant at all — both always await
the returned promise — and CDP has `awaitPromise`, so none of that machinery was
still earning its place.

The API is now three entry points that share one script shape and one argument
style, plus the same three on `Node` with `this` bound to the element:

- Keyword arguments become the script's function parameters, in order:
`page.evaluate("a + b", a: 1, b: 2)`. Each is sent as its own protocol
argument, so a `Node` still arrives in JavaScript as the live element. When
the script is a function declaration, values bind to its own parameter names
rather than to hash order.
- A script is either a bare expression, which gets wrapped, or a function/arrow
declaration, which is used as-is. This folds `#evaluate_func` into `#evaluate`
and is how multi-statement scripts are written.
- Promises are always awaited, so `evaluate("await fetch(url)", url: "/x")` works
without a separate method. `timeout:` (seconds, defaulting to the page timeout)
bounds the script browser-side and raises `ScriptTimeoutError`; `0` disables it.
- `#evaluate_handle` returns a `Ferrum::RemoteObject`, an opaque reference to a
browser-side value that can be passed straight back in as an argument.
- Added `Node#execute` and `Node#evaluate_handle`.ts reached their arguments through `arguments[0]`, and

Nothing breaks. `arguments[n]` is still populated inside the generated function,
`#evaluate_on` remain as shims. All four paths warn once per message and call
site; `FERRUM_DEPRECATION_WARNINGS=raise` turns the warnings into errors while
migrating a suite, `=0` silences them.

Two behaviour changes worth noting: `Node#evaluate` resolves its result the way
`Page#evaluate` does, so `node.evaluate("this.parentNode")` returns a `Node`
instead of an empty hash — it previously ran with `returnByValue`, which
flattened every DOM result — and `#evaluate` now awaits a returned promise
instead of serializing it to an empty object.

Detection of a function declaration is deliberately conservative: a script
beginning with `function` counts as one only if it ends at the closing brace,
so the IIFE `function() { ... }()` stays an expression, and arrow detection
accepts only a plain identifier parameter list, so `(() => 1)()` does too.
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