Skip to content

[js] Expose BiDi CDDL ast and model as shared artifacts#17657

Merged
titusfortner merged 3 commits into
trunkfrom
agnostic_bidi_model
Jun 10, 2026
Merged

[js] Expose BiDi CDDL ast and model as shared artifacts#17657
titusfortner merged 3 commits into
trunkfrom
agnostic_bidi_model

Conversation

@titusfortner

@titusfortner titusfortner commented Jun 8, 2026

Copy link
Copy Markdown
Member

🔗 Related Issues

Addresses #17654. In draft until #17574 is merged, since it is based on that code.

💥 What does this PR do?

Parses the CDDL to expose two reusable artifacts, instead of being built in memory inside create-bidi-src_ts and discarded. Other bindings (Java/Ruby/Python) can then consume one shared description of the BiDi command/event surface rather than each re-parsing the spec. The CDDL is parsed exactly once, and the shipped JS package is unchanged — the generated TypeScript is byte-identical to #17574.

Target Status Consumes Produces
create-bidi-src_merged_cddl unchanged CDDL spec files create-bidi-src_merged.cddl
create-bidi-src_ts (old) before merged CDDL one .ts module per BiDi domain
create-bidi-src_ast new merged CDDL create-bidi-src_ast.json — full parsed AST
create-bidi-src_json new create-bidi-src_ast.json create-bidi-src_model.json — binding-neutral command/event model
create-bidi-src_ts (new) after create-bidi-src_ast.json + create-bidi-src_model.json one .ts module per BiDi domain
create-bidi-src unchanged the .ts modules .js + .d.ts (tsc)

🔧 Implementation Notes

  • Binding-neutral by design: the model references types by their CDDL name (e.g. browser.CreateUserContextParameters), not cddl2ts's PascalCase, so each binding applies its own naming. Shape per domain: commands: [{method, name, params, result}], events: [{method, name, params}].
  • Honest null fields: params/result are null when a command takes no params / returns no value. Void is decided from the AST at model-build time (absent result type, or one aliased to EmptyResult) rather than in the JS emitter, so a consumer can trust the field without re-deriving it. Verified this matches the existing JS void detection across all 77 commands.
  • create-bidi-src_ast and create-bidi-src_json are visible to //java, //py, //rb so each binding depends on only the artifact it needs.

🤖 AI assistance

  • No substantial AI assistance used
  • AI assisted (complete below)
    • Tool(s): Claude Code
    • What was generated: the staged-pipeline refactor of generate_bidi.mjs, the binding-neutral model extractor, and the Bazel target wiring.
    • I reviewed all AI output and can explain the change

💡 Additional Considerations

Deviations from #17654 (intentional refinements):

  • In the model a command's result is null when it returns nothing and params is null when it takes no arguments, so a consumer can spot void/argument-less commands from the model alone (the issue listed those fields without the empty case). The model also adds a name field — the bare operation name (createUserContext) next to the full method (browser.createUserContext).

Follow-ups / things noticed (left as-is here):

  • Java/Ruby generators consuming create-bidi-src_model.json in future PRs.
  • Recommend that Python change implementation to consume this as well to remove the regex work
  • There is no mechanism to validate the model's completeness against the spec. The extractor finds commands/events by traversing the CDDL CommandData/EventData (and extension) unions, so anything the spec defines but never wires into a union is silently absent. This is a limitation of the CDDL structure, not the extractor. Example: bluetooth.characteristicEventGenerated and bluetooth.descriptorEventGenerated are defined but added to no BluetoothEvent union, so they don't appear in the model (not needed for Selenium, so not chasing this one down right now).

🔄 Types of changes

  • New feature (non-breaking change which adds functionality)

@selenium-ci selenium-ci added C-py Python Bindings C-nodejs JavaScript Bindings B-build Includes scripting, bazel and CI integrations B-devtools Includes everything BiDi or Chrome DevTools related labels Jun 8, 2026
@titusfortner titusfortner force-pushed the agnostic_bidi_model branch from 5ad5308 to 1d2c4a5 Compare June 8, 2026 15:00
@titusfortner titusfortner marked this pull request as ready for review June 8, 2026 15:00
@qodo-code-review

qodo-code-review Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (2) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Remediation recommended

1. No tests for buildModel() 📘 Rule violation ☼ Reliability
Description
This PR adds substantial new model-building and mapping logic (AST→model and model→generator inputs)
without adding or updating tests, increasing regression risk for BiDi generation behavior. A small
unit test suite could validate model shape and void/params handling deterministically.
Code

javascript/selenium-webdriver/generate_bidi.mjs[R630-705]

+function buildModel(ast) {
+  const model = {}
+  const resultTypes = buildResultTypeNames(ast)
+  const ensure = (domain) => (model[domain] ??= { commands: [], events: [] })
+
+  for (const c of extractCommands(ast)) {
+    const result = c.cddlName + 'Result'
+    ensure(c.domain).commands.push({
+      method: c.methodStr,
+      name: c.methodName,
+      params: c.hasParams ? c.paramsCddl : null,
+      result: resultTypes.has(result) ? result : null,
+    })
+  }
+
+  for (const e of extractEvents(ast)) {
+    ensure(e.domain).events.push({
+      method: e.methodStr,
+      name: e.eventName,
+      params: e.paramsCddl || null,
+    })
+  }
+
+  return model
+}
+
+/** Result type names the spec defines with a value; an absent or `EmptyResult`-aliased result is void. */
+function buildResultTypeNames(ast) {
+  const emptyAlias = new Set()
+  for (const d of ast) {
+    const pt = d.PropertyType
+    if (d.Name && d.Type === 'variable' && Array.isArray(pt) && pt.length === 1 && pt[0]?.Value === 'EmptyResult') {
+      emptyAlias.add(d.Name)
+    }
+  }
+  const names = new Set()
+  for (const d of ast) {
+    if (d.Name && d.Name.endsWith('Result') && !emptyAlias.has(d.Name)) names.add(d.Name)
+  }
+  return names
+}
+
+/** Map model commands to the generator's command-entry shape. */
+function modelToCommands(model) {
+  const commands = []
+  for (const [domain, entry] of Object.entries(model)) {
+    for (const c of entry.commands) {
+      commands.push({
+        domain,
+        methodStr: c.method,
+        methodName: c.name,
+        paramsTypeName: c.params !== null ? normalizeDottedName(c.params) : null,
+        hasParams: c.params !== null,
+        resultTypeName: c.result !== null ? normalizeDottedName(c.result) : null,
+      })
+    }
+  }
+  return commands
+}
+
+/** Map model events to the generator's event-entry shape. */
+function modelToEvents(model) {
+  const events = []
+  for (const [domain, entry] of Object.entries(model)) {
+    for (const e of entry.events) {
+      events.push({
+        domain,
+        methodStr: e.method,
+        eventName: e.name,
+        paramsTypeName: e.params !== null ? normalizeDottedName(e.params) : null,
+        onMethodName: 'on' + e.name.charAt(0).toUpperCase() + e.name.slice(1),
+      })
+    }
+  }
+  return events
+}
Evidence
PR Compliance ID 3 requires adding/updating tests when behavior changes. The diff adds new
binding-neutral model construction and conversion functions that directly affect generated outputs,
but the PR does not include any corresponding tests in the changed code.

AGENTS.md: Add/Update Tests for Implemented Changes and Prefer Small Unit Tests
javascript/selenium-webdriver/generate_bidi.mjs[630-705]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New AST→model and model→generator mapping logic was introduced without tests, increasing the chance of silently generating incorrect bindings (e.g., wrong `params`/`result` nullability or wrong domain grouping).

## Issue Context
The generator now relies on `bidi-ast.json` and `bidi-model.json` and uses `buildModel()`, `buildResultTypeNames()`, `modelToCommands()`, and `modelToEvents()` as the core transformation pipeline.

## Fix Focus Areas
- javascript/selenium-webdriver/generate_bidi.mjs[630-705]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. readJson() lacks validation 📘 Rule violation ☼ Reliability
Description
The new pipeline reads --ast/--model JSON using a raw JSON.parse without schema validation or
an actionable error message on parse/shape failure, which can cause confusing failures in
CI/automation. This violates the requirement to validate external command/artifact outputs and fail
safely with explicit errors.
Code

javascript/selenium-webdriver/generate_bidi.mjs[R188-195]

+function readJson(fileArg, label) {
+  const path = resolveInputPath(fileArg)
+  if (!existsSync(path)) {
+    console.error(`Error: ${label} file not found: ${path}`)
+    process.exit(1)
+  }
+  return JSON.parse(readFileSync(path, 'utf8'))
+}
Evidence
PR Compliance ID 13 requires automation scripts to validate preconditions and outputs and fail
safely with explicit errors. The new stage-based flow consumes JSON artifacts via readJson() which
performs JSON.parse without error handling or schema checks, so failures are not validated and may
be non-actionable.

javascript/selenium-webdriver/generate_bidi.mjs[158-171]
javascript/selenium-webdriver/generate_bidi.mjs[188-195]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`readJson()` assumes the JSON file is valid and of the expected shape; parse errors or unexpected schema will surface as a generic exception/stack trace rather than an explicit, actionable error.

## Issue Context
The generator is now a multi-stage pipeline driven by JSON artifacts (`bidi-ast.json`, `bidi-model.json`). These artifacts are produced/consumed by automation (Bazel actions), so robust validation and clear failure modes are important.

## Fix Focus Areas
- javascript/selenium-webdriver/generate_bidi.mjs[158-171]
- javascript/selenium-webdriver/generate_bidi.mjs[188-195]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Artifact outputs can collide ✓ Resolved 🐞 Bug ☼ Reliability
Description
generate_bidi_library hardcodes the shared artifact output filenames (bidi-ast.json /
bidi-model.json) independent of the macro instance name. A second invocation of this macro in the
same Bazel package would attempt to declare the same output paths and fail Bazel analysis due to
output collisions.
Code

javascript/selenium-webdriver/private/generate_bidi.bzl[R157-191]

+    # Step 2: parse the merged CDDL once into the reusable AST artifact.
+    # Exposed to the other bindings so they can consume it directly.
+    ast_target = name + "_ast"
+    ast_out = "bidi-ast.json"
+    js_run_binary(
+        name = ast_target,
+        srcs = [":" + merged_name],
+        outs = [ast_out],
+        args = [
+            "--cddl",
+            "$(location :" + merged_name + ")",
+            "--dump-ast",
+            pkg + "/" + ast_out,
+        ],
+        tool = generator,
+        visibility = _ARTIFACT_VISIBILITY,
+    )
+
+    # Step 3: extract the binding-neutral command/event model from the AST.
+    # Exposed to the other bindings so they can consume it directly.
+    json_target = name + "_json"
+    model_out = "bidi-model.json"
+    js_run_binary(
+        name = json_target,
+        srcs = [":" + ast_target],
+        outs = [model_out],
+        args = [
+            "--ast",
+            "$(location :" + ast_target + ")",
+            "--dump-model",
+            pkg + "/" + model_out,
+        ],
+        tool = generator,
+        visibility = _ARTIFACT_VISIBILITY,
+    )
Evidence
The macro declares fixed output filenames for the AST/model generation steps, so multiple
invocations in the same package would generate the same output paths.

javascript/selenium-webdriver/private/generate_bidi.bzl[157-191]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`generate_bidi_library` always emits `bidi-ast.json` and `bidi-model.json` as output filenames. If the macro is ever instantiated more than once in the same Bazel package, Bazel will report output path collisions.

## Issue Context
This PR introduces shared artifacts via `js_run_binary` (`*_ast` and `*_json` targets). These outputs are currently fixed strings instead of being scoped by the macro instance name.

## Fix Focus Areas
- javascript/selenium-webdriver/private/generate_bidi.bzl[157-191]

## Suggested fix
- Scope outputs by the macro instance name, e.g.:
 - `ast_out = name + "-bidi-ast.json"` (or `name + "_ast.json"`)
 - `model_out = name + "-bidi-model.json"`
- Keep the public artifact names stable if needed by adding a thin `filegroup` (or alias target) that re-exports the uniquely named file under a stable label, rather than a stable filename.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the JavaScript WebDriver BiDi generator so it can emit reusable, cross-binding build artifacts (a parsed CDDL AST JSON and a binding-neutral BiDi command/event model JSON), and wires those artifacts into Bazel as shared targets for other bindings to consume.

Changes:

  • Split generate_bidi.mjs into a 3-stage CLI pipeline: CDDL → AST JSON, AST → model JSON, AST+model → generated TypeScript.
  • Add Bazel targets for bidi-ast.json and bidi-model.json and rewire TS generation to consume them.
  • Open artifact visibility for //java, //py, and //rb consumers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
javascript/selenium-webdriver/generate_bidi.mjs Adds staged pipeline and introduces binding-neutral model build/consumption paths.
javascript/selenium-webdriver/private/generate_bidi.bzl Publishes AST/model artifacts as Bazel targets and updates the generator pipeline wiring.

Comment thread javascript/selenium-webdriver/generate_bidi.mjs
Comment thread javascript/selenium-webdriver/private/generate_bidi.bzl
@qodo-code-review

qodo-code-review Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

Grey Divider

Sorry, something went wrong

We weren't able to complete the code review on our side. Please try again

Grey Divider

Qodo Logo

@pujagani

Copy link
Copy Markdown
Contributor

This looks good to me. Thank you, Titus!

@pujagani pujagani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is useful. Java and other languages can use this next for its generator.

@titusfortner titusfortner merged commit 5a9acfc into trunk Jun 10, 2026
26 checks passed
@titusfortner titusfortner deleted the agnostic_bidi_model branch June 10, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

B-build Includes scripting, bazel and CI integrations B-devtools Includes everything BiDi or Chrome DevTools related C-nodejs JavaScript Bindings C-py Python Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants