diff --git a/java/src/org/openqa/selenium/bidi/BUILD.bazel b/java/src/org/openqa/selenium/bidi/BUILD.bazel
index 144b60ffc43c1..75ec09fa93724 100644
--- a/java/src/org/openqa/selenium/bidi/BUILD.bazel
+++ b/java/src/org/openqa/selenium/bidi/BUILD.bazel
@@ -1,9 +1,13 @@
-load("//java:defs.bzl", "java_library")
+load("//java:defs.bzl", "java_binary", "java_library")
AUGMENTER_SRCS = [
"BiDiProvider.java",
]
+GENERATOR_SRCS = [
+ "BiDiGenerator.java",
+]
+
java_library(
name = "augmenter",
srcs = AUGMENTER_SRCS,
@@ -26,7 +30,7 @@ java_library(
[
"*.java",
],
- exclude = AUGMENTER_SRCS,
+ exclude = AUGMENTER_SRCS + GENERATOR_SRCS,
),
visibility = [
"//java/src/org/openqa/selenium/bidi:__subpackages__",
@@ -42,3 +46,45 @@ java_library(
"@maven//:org_jspecify_jspecify",
],
)
+
+java_binary(
+ name = "bidi-client-generator",
+ srcs = GENERATOR_SRCS,
+ main_class = "org.openqa.selenium.bidi.BiDiGenerator",
+ deps = [
+ "//java/src/org/openqa/selenium/json",
+ ],
+)
+
+genrule(
+ name = "generate-bidi",
+ srcs = ["//javascript/selenium-webdriver:create-bidi-src_schema"],
+ outs = ["bidi-generated.srcjar"],
+ cmd = "\"$(execpath :bidi-client-generator)\" \"$(location //javascript/selenium-webdriver:create-bidi-src_schema)\" \"$@\"",
+ tools = [":bidi-client-generator"],
+)
+
+# Autogen, not checked in: the build compiles :generate-bidi's srcjar directly, nothing is
+# hand-edited, every build ships fresh output. The cross-binding BiDi codegen decisions doc (G1)
+# calls checking-in-while-unproven the general recommendation, but also names this exact shape —
+# build-time generation straight into bazel-out — as the end state, and notes selenium-devtools/CDP
+# in this repo already works this way. Matching that existing, already-de-risked precedent avoids
+# standing up a separate checked-in + verify-test workflow (with its own CI-coverage question to
+# resolve first) just for BiDi. Trade-off accepted knowingly: less reviewable diff surface while the
+# generator is still new.
+java_library(
+ name = "bidi-generated",
+ srcs = [":generate-bidi"],
+ visibility = [
+ "//java/src/org/openqa/selenium/bidi:__subpackages__",
+ "//java/src/org/openqa/selenium/remote:__pkg__",
+ "//java/test/org/openqa/selenium/bidi:__subpackages__",
+ "//java/test/org/openqa/selenium/grid:__subpackages__",
+ ],
+ deps = [
+ ":bidi",
+ "//java/src/org/openqa/selenium:core",
+ "//java/src/org/openqa/selenium/json",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
diff --git a/java/src/org/openqa/selenium/bidi/BiDiGenerator.java b/java/src/org/openqa/selenium/bidi/BiDiGenerator.java
new file mode 100644
index 0000000000000..4f69d846b7895
--- /dev/null
+++ b/java/src/org/openqa/selenium/bidi/BiDiGenerator.java
@@ -0,0 +1,1949 @@
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The SFC licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.openqa.selenium.bidi;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.openqa.selenium.json.Json;
+
+/**
+ * Generates Java BiDi module classes and their supporting POJOs from the flat binding-neutral
+ * {@code bidi_schema.json} produced by {@code project_bidi_schema.mjs}.
+ *
+ * Usage: {@code BiDiGenerator }
+ */
+public class BiDiGenerator {
+
+ private static final String BASE_PKG = "org.openqa.selenium.bidi";
+
+ // Java reserved words that cannot appear as method names; append "_" to escape.
+ private static final Set JAVA_RESERVED =
+ new java.util.HashSet<>(
+ java.util.Arrays.asList(
+ "abstract",
+ "assert",
+ "boolean",
+ "break",
+ "byte",
+ "case",
+ "catch",
+ "char",
+ "class",
+ "const",
+ "continue",
+ "default",
+ "do",
+ "double",
+ "else",
+ "enum",
+ "extends",
+ "final",
+ "finally",
+ "float",
+ "for",
+ "goto",
+ "if",
+ "implements",
+ "import",
+ "instanceof",
+ "int",
+ "interface",
+ "long",
+ "native",
+ "new",
+ "package",
+ "private",
+ "protected",
+ "public",
+ "return",
+ "short",
+ "static",
+ "strictfp",
+ "super",
+ "switch",
+ "synchronized",
+ "this",
+ "throw",
+ "throws",
+ "transient",
+ "try",
+ "void",
+ "volatile",
+ "while"));
+
+ private static final String API_JAVADOC =
+ "/**\n"
+ + " * This is an unsupported API. No compatibility guarantees are provided.\n"
+ + " * It tracks the W3C WebDriver BiDi specification directly. As the specification\n"
+ + " * evolves, this API will change or be removed without prior notice.\n"
+ + " */\n";
+
+ private static final String LICENSE =
+ "// Licensed to the Software Freedom Conservancy (SFC) under one\n"
+ + "// or more contributor license agreements. See the NOTICE file\n"
+ + "// distributed with this work for additional information\n"
+ + "// regarding copyright ownership. The SFC licenses this file\n"
+ + "// to you under the Apache License, Version 2.0 (the\n"
+ + "// \"License\"); you may not use this file except in compliance\n"
+ + "// with the License. You may obtain a copy of the License at\n"
+ + "//\n"
+ + "// http://www.apache.org/licenses/LICENSE-2.0\n"
+ + "//\n"
+ + "// Unless required by applicable law or agreed to in writing,\n"
+ + "// software distributed under the License is distributed on an\n"
+ + "// \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n"
+ + "// KIND, either express or implied. See the License for the\n"
+ + "// specific language governing permissions and limitations\n"
+ + "// under the License.\n\n"
+ + "// This file is generated. Do not edit — regenerate via BiDiGenerator.\n\n";
+
+ public static void main(String[] args) throws IOException {
+ if (args.length != 2) {
+ System.err.println("Usage: BiDiGenerator ");
+ System.exit(1);
+ }
+
+ Path schemaFile = Paths.get(args[0]);
+ Path outputJar = Paths.get(args[1]).toAbsolutePath();
+
+ String schemaText = new String(Files.readAllBytes(schemaFile), UTF_8);
+ @SuppressWarnings("unchecked")
+ Map schema = (Map) new Json().toType(schemaText, Json.MAP_TYPE);
+
+ Path tempDir = Files.createTempDirectory("bidi-generated");
+ try {
+ new Generator(schema).generateAll(tempDir);
+ packToJar(tempDir, outputJar);
+ } finally {
+ deleteRecursive(tempDir);
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════
+ // Generator
+ // ═══════════════════════════════════════════════════════════════
+
+ private static class Generator {
+
+ private final Map schema;
+ private final Map> types = new LinkedHashMap<>();
+
+ /** Types reachable from command/event params and results — the only ones that get generated. */
+ private final Set reachable;
+
+ /** Types reachable from command params — the only ones that need toMap(). */
+ private final Set senderTypes;
+
+ /**
+ * Types reachable from command results or event params — i.e. types a caller can receive. A
+ * type in both this set and {@code senderTypes} is used bidirectionally (e.g.
+ * script.SharedReference: built to send as a script argument, and also received inside a remote
+ * value) and is the only case that needs an immutable value class plus a separate Builder — see
+ * {@link #appendBuilder}.
+ */
+ private final Set receivableTypes;
+
+ /**
+ * Maps a variant record/union name to every parent union it belongs to. A type can genuinely
+ * belong to more than one union at once (e.g. PrimitiveProtocolValue is a member of both
+ * RemoteValue and LocalValue) — unions are generated as interfaces specifically so this is a
+ * real "implements/extends more than one" relationship, not something that has to be dropped.
+ */
+ private final Map> variantParent;
+
+ /**
+ * Synthetic types (anonymous CDDL constructs hoisted by the normalizer) keyed by their owner
+ * type name. They are emitted as nested static classes instead of top-level files.
+ */
+ private final Map> syntheticChildren;
+
+ @SuppressWarnings("unchecked")
+ Generator(Map schema) {
+ this.schema = schema;
+ Map rawTypes = (Map) schema.get("types");
+ if (rawTypes != null) {
+ for (Map.Entry entry : rawTypes.entrySet()) {
+ types.put(entry.getKey(), (Map) entry.getValue());
+ }
+ }
+ List