Files
chrome-devtools-mcp/tests/cli-imports.test.ts
Alex Rudenko 552e1f8d25 perf(cli): load a small third-party bundle in the chrome-devtools CLI (#3200)
The chrome-devtools CLI client starts a new process for every command.
It imported src/third_party/index.js, the full bundle with Puppeteer,
the MCP SDK and the DevTools frontend. It also imported src/index.ts for
logDisclaimers, which loads the whole server. One CLI call loaded 79
modules and spent about 670 ms of CPU before it did any work.

Add src/third_party/cli.ts with the modules the CLI path needs: yargs,
semver, PipeTransport and the URLPattern polyfill. Rollup bundles it as
a separate file, build/src/third_party/cli.js. The index bundle treats
./cli.js as external and re-exports from it, so the server code keeps
one import path.

Move logDisclaimers to src/utils/disclaimers.ts. src/index.ts re-exports
it, so the public API does not change.

ConfigParser, daemon/client, check-for-updates, url and the CLI entry
import from third_party/cli.js. src/utils/url.ts imports URLPattern
instead of using the global, so it works on Node 22 without the full
bundle.

A new test, tests/cli-imports.test.ts, walks the import graph of the
built CLI entry and fails if it reaches third_party/index.js.

THIRD_PARTY_NOTICES lists the same packages in a different order.

Results (bundled build):

- One CLI call loads 21 modules instead of 79.
- CPU time to start one CLI call: 667 ms -> 158 ms.
- Import cost: third_party/index.js 552 ms, third_party/cli.js 67 ms.
- cli.js is 349 KB.
- The 9 CLI test files (tests/e2e/*, tests/daemon/client.test.ts) on 2
cores, one file at a time: wall 65.9 s -> 49.6 s (-25%), CPU of all
child processes 143.5 s -> 108.0 s (-25%).
2026-10-16 14:26:14 +00:00

62 lines
1.8 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import {readFileSync} from 'node:fs';
import path from 'node:path';
import {describe, it} from 'node:test';
const BUILD_SRC = path.resolve(import.meta.dirname, '..', 'src');
const CLI_ENTRY = path.join(BUILD_SRC, 'bin', 'chrome-devtools.js');
const FULL_BUNDLE = path.join(BUILD_SRC, 'third_party', 'index.js');
const IMPORT_RE = /\b(?:from|import)\s*\(?\s*['"](\.{1,2}\/[^'"]+)['"]/g;
/**
* Walks the static and dynamic relative imports of the built JS files and
* returns the first path from `entry` to `target`, or undefined.
*/
function findImportPath(entry: string, target: string): string[] | undefined {
const parents = new Map<string, string | undefined>([[entry, undefined]]);
const queue = [entry];
while (queue.length > 0) {
const file = queue.shift();
if (file === undefined) {
break;
}
if (file === target) {
const chain = [];
for (let f: string | undefined = file; f; f = parents.get(f)) {
chain.unshift(path.relative(BUILD_SRC, f));
}
return chain;
}
if (!file.startsWith(BUILD_SRC)) {
continue;
}
const source = readFileSync(file, 'utf8');
for (const match of source.matchAll(IMPORT_RE)) {
const next = path.resolve(path.dirname(file), match[1]);
if (!parents.has(next)) {
parents.set(next, file);
queue.push(next);
}
}
}
return undefined;
}
describe('chrome-devtools CLI imports', () => {
it('does not load the full third-party bundle', () => {
const chain = findImportPath(CLI_ENTRY, FULL_BUNDLE);
assert.strictEqual(
chain,
undefined,
`The CLI must import from third_party/cli.js only. Import chain:\n ${chain?.join('\n -> ')}`,
);
});
});