Files
stanp-opencode/src/playwright.ts
T
StanPonomarev 4532af33fb
CI / verify (pull_request) Successful in 8s
fix: playwritgt timing out
2026-09-12 19:17:30 +02:00

138 lines
4.8 KiB
TypeScript

import { access, readFile, realpath } from "node:fs/promises";
import { constants } from "node:fs";
import { dirname, join, parse, resolve } from "node:path";
import { z } from "zod";
const dockerCommand = ["docker", "run", "-i", "--rm", "--init", "mcr.microsoft.com/playwright/mcp"];
const directPackageManifest = z.object({
name: z.literal("@playwright/mcp"),
bin: z.string(),
});
const namedPackageManifest = z.object({
name: z.literal("@playwright/mcp"),
bin: z.object({ "playwright-mcp": z.string() }),
});
const playwrightPackageManifest = z.object({
name: z.literal("playwright"),
bin: z.object({ playwright: z.string() }),
});
const playwrightTestPackageManifest = z.object({
name: z.literal("@playwright/test"),
bin: z.object({ playwright: z.string() }),
dependencies: z.object({ playwright: z.string() }),
});
async function packageExecutable(packageRoot: string) {
try {
const input = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
const direct = directPackageManifest.safeParse(input);
const bin = direct.success
? direct.data.bin
: namedPackageManifest.parse(input).bin["playwright-mcp"];
const executable = resolve(packageRoot, bin);
await access(executable, constants.X_OK);
return executable;
} catch {
return;
}
}
async function ordinaryPackageExecutable(packageRoot: string) {
try {
const manifest = playwrightPackageManifest.parse(
JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")),
);
const executable = resolve(packageRoot, manifest.bin.playwright);
await access(executable, constants.X_OK);
return executable;
} catch {
return;
}
}
async function playwrightDependency(packageRoot: string) {
let current = await realpath(packageRoot);
const root = parse(current).root;
while (true) {
const executable = await ordinaryPackageExecutable(
join(current, "node_modules/playwright"),
);
if (executable) return executable;
if (current === root) return;
current = dirname(current);
}
}
async function testPackageExecutable(packageRoot: string) {
try {
const manifest = playwrightTestPackageManifest.parse(
JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")),
);
if (!(await playwrightDependency(packageRoot))) return;
const executable = resolve(packageRoot, manifest.bin.playwright);
await access(executable, constants.X_OK);
return executable;
} catch {
return;
}
}
async function verifiedPathExecutable(candidate: string, kind: "mcp" | "playwright") {
try {
await access(candidate, constants.X_OK);
const target = await realpath(candidate);
let directory = dirname(target);
const root = parse(directory).root;
while (true) {
const executable =
kind === "mcp"
? await packageExecutable(directory)
: ((await ordinaryPackageExecutable(directory)) ??
(await testPackageExecutable(directory)));
if (executable && (await realpath(executable)) === target) return candidate;
if (directory === root) return;
directory = dirname(directory);
}
} catch {
return;
}
}
export async function playwrightMcpCommand(
directory: string,
path = process.env.PATH ?? "",
): Promise<string[]> {
let current = resolve(directory);
const root = parse(current).root;
while (true) {
const executable = await packageExecutable(join(current, "node_modules/@playwright/mcp"));
if (executable) return [executable];
if (current === root) break;
current = dirname(current);
}
for (const entry of path.split(process.platform === "win32" ? ";" : ":")) {
if (!entry) continue;
const executable = await verifiedPathExecutable(join(entry, "playwright-mcp"), "mcp");
if (executable) return [executable];
}
current = resolve(directory);
while (true) {
const playwright = await ordinaryPackageExecutable(
join(current, "node_modules/playwright"),
);
const test = await testPackageExecutable(join(current, "node_modules/@playwright/test"));
if (playwright || test) return ["npx", "-y", "@playwright/mcp@latest"];
if (current === root) break;
current = dirname(current);
}
for (const entry of path.split(process.platform === "win32" ? ";" : ":")) {
if (!entry) continue;
const executable = await verifiedPathExecutable(join(entry, "playwright"), "playwright");
if (executable) return ["npx", "-y", "@playwright/mcp@latest"];
}
return [...dockerCommand];
}