aboutsummaryrefslogtreecommitdiff
path: root/packages/bun-inspector-protocol/scripts/generate-protocol.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bun-inspector-protocol/scripts/generate-protocol.ts')
-rw-r--r--packages/bun-inspector-protocol/scripts/generate-protocol.ts202
1 files changed, 202 insertions, 0 deletions
diff --git a/packages/bun-inspector-protocol/scripts/generate-protocol.ts b/packages/bun-inspector-protocol/scripts/generate-protocol.ts
new file mode 100644
index 000000000..8da5fe795
--- /dev/null
+++ b/packages/bun-inspector-protocol/scripts/generate-protocol.ts
@@ -0,0 +1,202 @@
+import type { Protocol, Domain, Property } from "../protocol/schema";
+import { readFileSync, writeFileSync } from "node:fs";
+import { spawnSync } from "node:child_process";
+
+run().catch(console.error);
+
+async function run() {
+ const cwd = new URL("../protocol/", import.meta.url);
+ const runner = "Bun" in globalThis ? "bunx" : "npx";
+ const write = (name: string, data: string) => {
+ const path = new URL(name, cwd);
+ writeFileSync(path, data);
+ spawnSync(runner, ["prettier", "--write", path.pathname], { cwd, stdio: "ignore" });
+ };
+ const base = readFileSync(new URL("protocol.d.ts", cwd), "utf-8");
+ const baseNoComments = base.replace(/\/\/.*/g, "");
+ const jsc = await downloadJsc();
+ write("jsc/protocol.json", JSON.stringify(jsc));
+ write("jsc/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(jsc, baseNoComments));
+ const v8 = await downloadV8();
+ write("v8/protocol.json", JSON.stringify(v8));
+ write("v8/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(v8, baseNoComments));
+}
+
+function formatProtocol(protocol: Protocol, extraTs?: string): string {
+ const { name, domains } = protocol;
+ const eventMap = new Map();
+ const commandMap = new Map();
+ let body = `export namespace ${name} {`;
+ for (const { domain, types = [], events = [], commands = [] } of domains) {
+ body += `export namespace ${domain} {`;
+ for (const type of types) {
+ body += formatProperty(type);
+ }
+ for (const { name, description, parameters = [] } of events) {
+ const symbol = `${domain}.${name}`;
+ const title = toTitle(name);
+ eventMap.set(symbol, `${domain}.${title}`);
+ body += formatProperty({
+ id: `${title}Event`,
+ type: "object",
+ description: `${description}\n@event \`${symbol}\``,
+ properties: parameters,
+ });
+ }
+ for (const { name, description, parameters = [], returns = [] } of commands) {
+ const symbol = `${domain}.${name}`;
+ const title = toTitle(name);
+ commandMap.set(symbol, `${domain}.${title}`);
+ body += formatProperty({
+ id: `${title}Request`,
+ type: "object",
+ description: `${description}\n@request \`${symbol}\``,
+ properties: parameters,
+ });
+ body += formatProperty({
+ id: `${title}Response`,
+ type: "object",
+ description: `${description}\n@response \`${symbol}\``,
+ properties: returns,
+ });
+ }
+ body += "};";
+ }
+ for (const type of ["Event", "Request", "Response"]) {
+ const sourceMap = type === "Event" ? eventMap : commandMap;
+ body += formatProperty({
+ id: `${type}Map`,
+ type: "object",
+ properties: [...sourceMap.entries()].map(([name, title]) => ({
+ name: `"${name}"`,
+ type: undefined,
+ $ref: `${title}${type}`,
+ })),
+ });
+ }
+ if (extraTs) {
+ body += extraTs;
+ }
+ return body + "};";
+}
+
+function formatProperty(property: Property): string {
+ const { id, description, type, optional } = property;
+ let body = "";
+ if (id) {
+ if (description) {
+ body += `\n${toComment(description)}\n`;
+ }
+ body += `export type ${id}=`;
+ }
+ if (type === "boolean") {
+ body += "boolean";
+ } else if (type === "number" || type === "integer") {
+ body += "number";
+ } else if (type === "string") {
+ const { enum: choices } = property;
+ if (choices) {
+ body += choices.map(value => `"${value}"`).join("|");
+ } else {
+ body += "string";
+ }
+ } else if (type === "array") {
+ const { items } = property;
+ const itemType = items ? formatProperty(items) : "unknown";
+ body += `${itemType}[]`;
+ } else if (type === "object") {
+ const { properties } = property;
+ if (!properties) {
+ body += "Record<string, unknown>";
+ } else if (properties.length === 0) {
+ body += "{}";
+ } else {
+ body += "{";
+ for (const { name, description, ...property } of properties) {
+ if (description) {
+ body += `\n${toComment(description)}`;
+ }
+ const delimit = property.optional ? "?:" : ":";
+ body += `\n${name}${delimit}${formatProperty({ ...property, id: undefined })};`;
+ }
+ body += "}";
+ }
+ } else if ("$ref" in property) {
+ body += property.$ref;
+ } else {
+ body += "unknown";
+ }
+ if (optional) {
+ body += "|undefined";
+ }
+ if (id) {
+ body += ";";
+ }
+ return body;
+}
+
+/**
+ * @link https://github.com/ChromeDevTools/devtools-protocol/tree/master/json
+ */
+async function downloadV8(): Promise<Protocol> {
+ const baseUrl = "https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json";
+ const domains = ["Runtime", "Console", "Debugger", "Memory", "HeapProfiler", "Profiler", "Network", "Inspector"];
+ return Promise.all([
+ download<Protocol>(`${baseUrl}/js_protocol.json`),
+ download<Protocol>(`${baseUrl}/browser_protocol.json`),
+ ]).then(([js, browser]) => ({
+ name: "V8",
+ version: js.version,
+ domains: [...js.domains, ...browser.domains]
+ .filter(domain => !domains.includes(domain.domain))
+ .sort((a, b) => a.domain.localeCompare(b.domain)),
+ }));
+}
+
+/**
+ * @link https://github.com/WebKit/WebKit/tree/main/Source/JavaScriptCore/inspector/protocol
+ */
+async function downloadJsc(): Promise<Protocol> {
+ const baseUrl = "https://raw.githubusercontent.com/WebKit/WebKit/main/Source/JavaScriptCore/inspector/protocol";
+ const domains = [
+ "Runtime",
+ "Console",
+ "Debugger",
+ "Heap",
+ "ScriptProfiler",
+ "CPUProfiler",
+ "GenericTypes",
+ "Network",
+ "Inspector",
+ ];
+ return {
+ name: "JSC",
+ version: {
+ major: 1,
+ minor: 3,
+ },
+ domains: await Promise.all(domains.map(domain => download<Domain>(`${baseUrl}/${domain}.json`))).then(domains =>
+ domains.sort((a, b) => a.domain.localeCompare(b.domain)),
+ ),
+ };
+}
+
+async function download<V>(url: string): Promise<V> {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`${response.status}: ${url}`);
+ }
+ return response.json();
+}
+
+function toTitle(name: string): string {
+ return name.charAt(0).toUpperCase() + name.slice(1);
+}
+
+function toComment(description?: string): string {
+ if (!description) {
+ return "";
+ }
+ const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
+ return lines.join("\n");
+}