Skip to content

Commit 6be0d9f

Browse files
Hein van Vuurenclaude
andcommitted
fix: Add build output and fix Node.js 20 compatibility
- Replace --experimental-strip-types with npx tsx for Node 20 compat - Add built JavaScript files to repo for direct npx usage - Update .gitignore to allow build/ directory - All scripts now work on Node.js 20.x and 22.x+ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2e68478 commit 6be0d9f

295 files changed

Lines changed: 16175 additions & 8 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ dist
141141
.pnp.*
142142

143143
# Build output directory
144-
build/
145144

146145
log.txt
147146

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
import { CDPSessionEvent } from './third_party/index.js';
7+
/**
8+
* This class makes a puppeteer connection look like DevTools CDPConnection.
9+
*
10+
* Since we connect "root" DevTools targets to specific pages, we scope everything to a puppeteer CDP session.
11+
*
12+
* We don't have to recursively listen for 'sessionattached' as the "root" CDP session sees all child session attached
13+
* events, regardless how deeply nested they are.
14+
*/
15+
export class PuppeteerDevToolsConnection {
16+
#connection;
17+
#observers = new Set();
18+
#sessionEventHandlers = new Map();
19+
constructor(session) {
20+
this.#connection = session.connection();
21+
session.on(CDPSessionEvent.SessionAttached, this.#startForwardingCdpEvents.bind(this));
22+
session.on(CDPSessionEvent.SessionDetached, this.#stopForwardingCdpEvents.bind(this));
23+
this.#startForwardingCdpEvents(session);
24+
}
25+
send(method, params, sessionId) {
26+
if (sessionId === undefined) {
27+
throw new Error('Attempting to send on the root session. This must not happen');
28+
}
29+
const session = this.#connection.session(sessionId);
30+
if (!session) {
31+
throw new Error('Unknown session ' + sessionId);
32+
}
33+
// Rolled protocol version between puppeteer and DevTools doesn't necessarily match
34+
/* eslint-disable @typescript-eslint/no-explicit-any */
35+
return session
36+
.send(method, params)
37+
.then(result => ({ result }))
38+
.catch(error => ({ error }));
39+
/* eslint-enable @typescript-eslint/no-explicit-any */
40+
}
41+
observe(observer) {
42+
this.#observers.add(observer);
43+
}
44+
unobserve(observer) {
45+
this.#observers.delete(observer);
46+
}
47+
#startForwardingCdpEvents(session) {
48+
const handler = this.#handleEvent.bind(this, session.id());
49+
this.#sessionEventHandlers.set(session.id(), handler);
50+
session.on('*', handler);
51+
}
52+
#stopForwardingCdpEvents(session) {
53+
const handler = this.#sessionEventHandlers.get(session.id());
54+
if (handler) {
55+
session.off('*', handler);
56+
}
57+
}
58+
#handleEvent(sessionId, type, event) {
59+
if (typeof type === 'string' &&
60+
type !== CDPSessionEvent.SessionAttached &&
61+
type !== CDPSessionEvent.SessionDetached) {
62+
this.#observers.forEach(observer => observer.onEvent({
63+
method: type,
64+
sessionId,
65+
params: event,
66+
}));
67+
}
68+
}
69+
}

build/src/DevtoolsUtils.js

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
import { PuppeteerDevToolsConnection } from './DevToolsConnectionAdapter.js';
7+
import { ISSUE_UTILS } from './issue-descriptions.js';
8+
import { logger } from './logger.js';
9+
import { Mutex } from './Mutex.js';
10+
import { DevTools } from './third_party/index.js';
11+
export function extractUrlLikeFromDevToolsTitle(title) {
12+
const match = title.match(new RegExp(`DevTools - (.*)`));
13+
return match?.[1] ?? undefined;
14+
}
15+
export function urlsEqual(url1, url2) {
16+
const normalizedUrl1 = normalizeUrl(url1);
17+
const normalizedUrl2 = normalizeUrl(url2);
18+
return normalizedUrl1 === normalizedUrl2;
19+
}
20+
/**
21+
* For the sake of the MCP server, when we determine if two URLs are equal we
22+
* remove some parts:
23+
*
24+
* 1. We do not care about the protocol.
25+
* 2. We do not care about trailing slashes.
26+
* 3. We do not care about "www".
27+
* 4. We ignore the hash parts.
28+
*
29+
* For example, if the user types "record a trace on foo.com", we would want to
30+
* match a tab in the connected Chrome instance that is showing "www.foo.com/"
31+
*/
32+
function normalizeUrl(url) {
33+
let result = url.trim();
34+
// Remove protocols
35+
if (result.startsWith('https://')) {
36+
result = result.slice(8);
37+
}
38+
else if (result.startsWith('http://')) {
39+
result = result.slice(7);
40+
}
41+
// Remove 'www.'. This ensures that we find the right URL regardless of if the user adds `www` or not.
42+
if (result.startsWith('www.')) {
43+
result = result.slice(4);
44+
}
45+
// We use target URLs to locate DevTools but those often do
46+
// no include hash.
47+
const hashIdx = result.lastIndexOf('#');
48+
if (hashIdx !== -1) {
49+
result = result.slice(0, hashIdx);
50+
}
51+
// Remove trailing slash
52+
if (result.endsWith('/')) {
53+
result = result.slice(0, -1);
54+
}
55+
return result;
56+
}
57+
/**
58+
* A mock implementation of an issues manager that only implements the methods
59+
* that are actually used by the IssuesAggregator
60+
*/
61+
export class FakeIssuesManager extends DevTools.Common.ObjectWrapper
62+
.ObjectWrapper {
63+
issues() {
64+
return [];
65+
}
66+
}
67+
export function mapIssueToMessageObject(issue) {
68+
const count = issue.getAggregatedIssuesCount();
69+
const markdownDescription = issue.getDescription();
70+
const filename = markdownDescription?.file;
71+
if (!markdownDescription) {
72+
logger(`no description found for issue:` + issue.code);
73+
return null;
74+
}
75+
const rawMarkdown = filename
76+
? ISSUE_UTILS.getIssueDescription(filename)
77+
: null;
78+
if (!rawMarkdown) {
79+
logger(`no markdown ${filename} found for issue:` + issue.code);
80+
return null;
81+
}
82+
let processedMarkdown;
83+
let title;
84+
try {
85+
processedMarkdown =
86+
DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription.substitutions);
87+
const markdownAst = DevTools.Marked.Marked.lexer(processedMarkdown);
88+
title =
89+
DevTools.MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst);
90+
}
91+
catch {
92+
logger('error parsing markdown for issue ' + issue.code());
93+
return null;
94+
}
95+
if (!title) {
96+
logger('cannot read issue title from ' + filename);
97+
return null;
98+
}
99+
return {
100+
type: 'issue',
101+
item: issue,
102+
message: title,
103+
count,
104+
description: processedMarkdown,
105+
};
106+
}
107+
// DevTools CDP errors can get noisy.
108+
DevTools.ProtocolClient.InspectorBackend.test.suppressRequestErrors = true;
109+
DevTools.I18n.DevToolsLocale.DevToolsLocale.instance({
110+
create: true,
111+
data: {
112+
navigatorLanguage: 'en-US',
113+
settingLanguage: 'en-US',
114+
lookupClosestDevToolsLocale: l => l,
115+
},
116+
});
117+
DevTools.I18n.i18n.registerLocaleDataForTest('en-US', {});
118+
export class UniverseManager {
119+
#browser;
120+
#createUniverseFor;
121+
#universes = new WeakMap();
122+
/** Guard access to #universes so we don't create unnecessary universes */
123+
#mutex = new Mutex();
124+
constructor(browser, factory = DEFAULT_FACTORY) {
125+
this.#browser = browser;
126+
this.#createUniverseFor = factory;
127+
}
128+
async init(pages) {
129+
try {
130+
await this.#mutex.acquire();
131+
const promises = [];
132+
for (const page of pages) {
133+
promises.push(this.#createUniverseFor(page).then(targetUniverse => this.#universes.set(page, targetUniverse)));
134+
}
135+
this.#browser.on('targetcreated', this.#onTargetCreated);
136+
this.#browser.on('targetdestroyed', this.#onTargetDestroyed);
137+
await Promise.all(promises);
138+
}
139+
finally {
140+
this.#mutex.release();
141+
}
142+
}
143+
get(page) {
144+
return this.#universes.get(page) ?? null;
145+
}
146+
dispose() {
147+
this.#browser.off('targetcreated', this.#onTargetCreated);
148+
this.#browser.off('targetdestroyed', this.#onTargetDestroyed);
149+
}
150+
#onTargetCreated = async (target) => {
151+
const page = await target.page();
152+
try {
153+
await this.#mutex.acquire();
154+
if (!page || this.#universes.has(page)) {
155+
return;
156+
}
157+
this.#universes.set(page, await this.#createUniverseFor(page));
158+
}
159+
finally {
160+
this.#mutex.release();
161+
}
162+
};
163+
#onTargetDestroyed = async (target) => {
164+
const page = await target.page();
165+
try {
166+
await this.#mutex.acquire();
167+
if (!page || !this.#universes.has(page)) {
168+
return;
169+
}
170+
this.#universes.delete(page);
171+
}
172+
finally {
173+
this.#mutex.release();
174+
}
175+
};
176+
}
177+
const DEFAULT_FACTORY = async (page) => {
178+
const settingStorage = new DevTools.Common.Settings.SettingsStorage({});
179+
const universe = new DevTools.Foundation.Universe.Universe({
180+
settingsCreationOptions: {
181+
syncedStorage: settingStorage,
182+
globalStorage: settingStorage,
183+
localStorage: settingStorage,
184+
settingRegistrations: DevTools.Common.SettingRegistration.getRegisteredSettings(),
185+
},
186+
overrideAutoStartModels: new Set([DevTools.DebuggerModel]),
187+
});
188+
const session = await page.createCDPSession();
189+
const connection = new PuppeteerDevToolsConnection(session);
190+
const targetManager = universe.context.get(DevTools.TargetManager);
191+
targetManager.observeModels(DevTools.DebuggerModel, SKIP_ALL_PAUSES);
192+
const target = targetManager.createTarget('main', '', 'frame', // eslint-disable-line @typescript-eslint/no-explicit-any
193+
/* parentTarget */ null, session.id(), undefined, connection);
194+
return { target, universe };
195+
};
196+
// We don't want to pause any DevTools universe session ever on the MCP side.
197+
//
198+
// Note that calling `setSkipAllPauses` only affects the session on which it was
199+
// sent. This means DevTools can still pause, step and do whatever. We just won't
200+
// see the `Debugger.paused`/`Debugger.resumed` events on the MCP side.
201+
const SKIP_ALL_PAUSES = {
202+
modelAdded(model) {
203+
void model.agent.invoke_setSkipAllPauses({ skip: true });
204+
},
205+
modelRemoved() {
206+
// Do nothing.
207+
},
208+
};

0 commit comments

Comments
 (0)