mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Add script to generate DevTools protocol types for JSC and V8
This commit is contained in:
committed by
Jarred Sumner
parent
ca08cf6b0a
commit
4a36470588
2
packages/bun-devtools/index.ts
Normal file
2
packages/bun-devtools/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./protocol/jsc";
|
||||
export * from "./protocol/v8";
|
||||
8
packages/bun-devtools/package.json
Normal file
8
packages/bun-devtools/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "bun-devtools",
|
||||
"private": true,
|
||||
"module": "./index.ts",
|
||||
"scripts": {
|
||||
"generate-protocol": "bun run scripts/generate-protocol.ts"
|
||||
}
|
||||
}
|
||||
1946
packages/bun-devtools/protocol/jsc.d.ts
vendored
Normal file
1946
packages/bun-devtools/protocol/jsc.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3388
packages/bun-devtools/protocol/v8.d.ts
vendored
Normal file
3388
packages/bun-devtools/protocol/v8.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
255
packages/bun-devtools/scripts/generate-protocol.ts
Normal file
255
packages/bun-devtools/scripts/generate-protocol.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { join } from "node:path";
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
type Protocol = {
|
||||
name: string;
|
||||
version: {
|
||||
major: number;
|
||||
minor: number;
|
||||
};
|
||||
domains: Domain[];
|
||||
};
|
||||
|
||||
type Domain = {
|
||||
domain: string;
|
||||
types: Property[];
|
||||
commands?: {
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters?: Property[];
|
||||
returns?: Property[];
|
||||
}[];
|
||||
events?: {
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters: Property[];
|
||||
}[];
|
||||
};
|
||||
|
||||
type Property = {
|
||||
id?: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
optional?: boolean;
|
||||
} & (
|
||||
| {
|
||||
type: "array";
|
||||
items?: Property;
|
||||
}
|
||||
| {
|
||||
type: "object";
|
||||
properties?: Property[];
|
||||
}
|
||||
| {
|
||||
type: "string";
|
||||
enum?: string[];
|
||||
}
|
||||
| {
|
||||
$ref: string;
|
||||
}
|
||||
);
|
||||
|
||||
function format(property: Property): string {
|
||||
if (property.id) {
|
||||
const comment = property.description
|
||||
? `/** ${property.description} */\n`
|
||||
: "";
|
||||
const body = format({ ...property, id: undefined });
|
||||
return `${comment}export type ${property.id} = ${body};\n`;
|
||||
}
|
||||
if (property.type === "array") {
|
||||
const type = "items" in property ? format(property.items!) : "unknown";
|
||||
return `Array<${type}>`;
|
||||
}
|
||||
if (property.type === "object") {
|
||||
if (!("properties" in property)) {
|
||||
return "Record<string, unknown>";
|
||||
}
|
||||
if (property.properties!.length === 0) {
|
||||
return "{}";
|
||||
}
|
||||
const properties = property
|
||||
.properties!.map((property) => {
|
||||
const comment = property.description
|
||||
? `/** ${property.description} */\n`
|
||||
: "";
|
||||
const name = `${property.name}${property.optional ? "?" : ""}`;
|
||||
return `${comment} ${name}: ${format(property)};`;
|
||||
})
|
||||
.join("\n");
|
||||
return `{\n${properties}}`;
|
||||
}
|
||||
if (property.type === "string") {
|
||||
if (!("enum" in property)) {
|
||||
return "string";
|
||||
}
|
||||
return property.enum!.map((v) => `"${v}"`).join(" | ");
|
||||
}
|
||||
if ("$ref" in property) {
|
||||
if (/^Page|DOM|Security|CSS|IO|Emulation\./.test(property.$ref)) {
|
||||
return "unknown";
|
||||
}
|
||||
return property.$ref;
|
||||
}
|
||||
if (property.type === "integer") {
|
||||
return "number";
|
||||
}
|
||||
return property.type;
|
||||
}
|
||||
|
||||
function formatAll(protocol: Protocol): string {
|
||||
let body = "";
|
||||
const append = (property: Property) => {
|
||||
body += format(property);
|
||||
};
|
||||
const titlize = (name: string) =>
|
||||
name.charAt(0).toUpperCase() + name.slice(1);
|
||||
const events = new Map();
|
||||
const commands = new Map();
|
||||
for (const domain of protocol.domains) {
|
||||
body += `export namespace ${domain.domain} {`;
|
||||
for (const type of domain.types ?? []) {
|
||||
append(type);
|
||||
}
|
||||
for (const event of domain.events ?? []) {
|
||||
const symbol = `${domain.domain}.${event.name}`;
|
||||
const title = titlize(event.name);
|
||||
events.set(symbol, `${domain.domain}.${title}`);
|
||||
append({
|
||||
id: `${title}Event`,
|
||||
type: "object",
|
||||
description: `\`${symbol}\``,
|
||||
properties: event.parameters ?? [],
|
||||
});
|
||||
}
|
||||
for (const command of domain.commands ?? []) {
|
||||
const symbol = `${domain.domain}.${command.name}`;
|
||||
const title = titlize(command.name);
|
||||
commands.set(symbol, `${domain.domain}.${title}`);
|
||||
append({
|
||||
id: `${title}Request`,
|
||||
type: "object",
|
||||
description: `\`${symbol}\``,
|
||||
properties: command.parameters ?? [],
|
||||
});
|
||||
append({
|
||||
id: `${title}Response`,
|
||||
type: "object",
|
||||
description: `\`${symbol}\``,
|
||||
properties: command.returns ?? [],
|
||||
});
|
||||
}
|
||||
body += "};";
|
||||
}
|
||||
for (const type of ["Event", "Request", "Response"]) {
|
||||
const source = type === "Event" ? events : commands;
|
||||
append({
|
||||
id: `${type}Map`,
|
||||
type: "object",
|
||||
properties: [...source.entries()].map(([name, title]) => ({
|
||||
name: `"${name}"`,
|
||||
$ref: `${title}${type}`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
body += `export type Event<T extends keyof EventMap> = {
|
||||
method: T;
|
||||
params: EventMap[T];
|
||||
};
|
||||
export type Request<T extends keyof RequestMap> = {
|
||||
id: number;
|
||||
method: T;
|
||||
params: RequestMap[T];
|
||||
};
|
||||
export type Response<T extends keyof ResponseMap> = {
|
||||
id: number;
|
||||
} & ({
|
||||
method?: T;
|
||||
result: ResponseMap[T];
|
||||
} | {
|
||||
error: {
|
||||
code?: string;
|
||||
message: string;
|
||||
};
|
||||
});`;
|
||||
return `export namespace ${protocol.name.toUpperCase()} {${body}};`;
|
||||
}
|
||||
|
||||
async function downloadV8(): Promise<Protocol> {
|
||||
const baseUrl =
|
||||
"https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json";
|
||||
const filter = [
|
||||
"Runtime",
|
||||
"Network",
|
||||
"Console",
|
||||
"Debugger",
|
||||
"Profiler",
|
||||
"HeapProfiler",
|
||||
];
|
||||
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) => filter.includes(domain.domain))
|
||||
.sort((a, b) => a.domain.localeCompare(b.domain)),
|
||||
}));
|
||||
}
|
||||
|
||||
async function downloadJsc(): Promise<Protocol> {
|
||||
const baseUrl =
|
||||
"https://raw.githubusercontent.com/WebKit/WebKit/main/Source/JavaScriptCore/inspector/protocol";
|
||||
return {
|
||||
name: "jsc",
|
||||
version: {
|
||||
major: 1,
|
||||
minor: 3,
|
||||
},
|
||||
domains: await Promise.all([
|
||||
download<Domain>(`${baseUrl}/Debugger.json`),
|
||||
download<Domain>(`${baseUrl}/Heap.json`),
|
||||
download<Domain>(`${baseUrl}/ScriptProfiler.json`),
|
||||
download<Domain>(`${baseUrl}/Runtime.json`),
|
||||
download<Domain>(`${baseUrl}/Network.json`),
|
||||
download<Domain>(`${baseUrl}/Console.json`),
|
||||
download<Domain>(`${baseUrl}/GenericTypes.json`),
|
||||
]).then((domains) =>
|
||||
domains.sort((a, b) => a.domain.localeCompare(b.domain))
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function run(cwd: string) {
|
||||
const [jsc, v8] = await Promise.all([downloadJsc(), downloadV8()]);
|
||||
try {
|
||||
mkdirSync(cwd);
|
||||
} catch (error) {
|
||||
if (error.code !== "EEXIST") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const write = (name: string, data: string) => {
|
||||
writeFileSync(join(cwd, name), data);
|
||||
spawnSync("bunx", ["prettier", "--write", name], { cwd, stdio: "ignore" });
|
||||
};
|
||||
// Note: Can be uncommented to inspect the JSON protocol files.
|
||||
// write("devtools/jsc.json", JSON.stringify(jsc));
|
||||
// write("devtools/v8.json", JSON.stringify(v8));
|
||||
write("jsc.d.ts", "// GENERATED - DO NOT EDIT\n" + formatAll(jsc));
|
||||
write("v8.d.ts", "// GENERATED - DO NOT EDIT\n" + formatAll(v8));
|
||||
}
|
||||
|
||||
run(join(__dirname, "..", "protocol"))
|
||||
.catch(console.error);
|
||||
BIN
src/bun.js/bun.lockb
Executable file
BIN
src/bun.js/bun.lockb
Executable file
Binary file not shown.
6
src/bun.js/package.json
Normal file
6
src/bun.js/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"expect": "^29.5.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user