diff --git a/.gitignore b/.gitignore index 12099300..7cc2253a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ tsconfig.tsbuildinfo !.yarn/releases !.yarn/sdks !.yarn/versions +.playwright-cli \ No newline at end of file diff --git a/packages/replayio/README.md b/packages/replayio/README.md index 22fd6060..52b3211f 100644 --- a/packages/replayio/README.md +++ b/packages/replayio/README.md @@ -26,6 +26,26 @@ This CLI will automatically prompt you to log into your Replay account (or to re The CLI will also prompt you to download the Replay runtime if you have not already done so. +## Browser facade + +`replayio` exposes Playwright CLI under the `browser` command: + +```bash +replayio browser open https://google.com +replayio browser click "text=Sign in" +replayio browser close +``` + +`@playwright/cli` is launched with Replay Browser through `PLAYWRIGHT_MCP_EXECUTABLE_PATH`. +You can override the Playwright CLI binary with `REPLAYIO_PLAYWRIGHT_CLI_PATH`. + +When `replayio browser close` succeeds, replayio deterministically prints: + +- a generated Playwright test assembled from captured `### Ran Playwright code` blocks +- a numbered step list derived from the captured actions + +Recordings for the closed browser session are also auto-uploaded when authenticated (`replayio login` or `REPLAY_API_KEY`). + ## Contributing Contributing guide can be found [here](contributing.md). diff --git a/packages/replayio/package.json b/packages/replayio/package.json index a96b758c..1a3fed29 100644 --- a/packages/replayio/package.json +++ b/packages/replayio/package.json @@ -29,6 +29,7 @@ ], "homepage": "https://github.com/replayio/replay-cli/blob/main/packages/replayio/README.md", "dependencies": { + "@playwright/cli": "latest", "@replayio/protocol": "^0.71.0", "@replayio/sourcemap-upload": "workspace:^", "@types/semver": "^7.5.6", diff --git a/packages/replayio/src/bin.ts b/packages/replayio/src/bin.ts index 70fa15f7..c72578a7 100644 --- a/packages/replayio/src/bin.ts +++ b/packages/replayio/src/bin.ts @@ -7,6 +7,7 @@ import "./commands/info"; import "./commands/list"; import "./commands/login"; import "./commands/logout"; +import "./commands/browser"; import "./commands/open"; import "./commands/record"; import "./commands/remove"; diff --git a/packages/replayio/src/commands/browser.ts b/packages/replayio/src/commands/browser.ts new file mode 100644 index 00000000..243f7996 --- /dev/null +++ b/packages/replayio/src/commands/browser.ts @@ -0,0 +1,844 @@ +import { spawn, spawnSync } from "child_process"; +import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken"; +import { canUpload } from "@replay-cli/shared/recording/canUpload"; +import { getRecordings } from "@replay-cli/shared/recording/getRecordings"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; +import os from "os"; +import path from "path"; +import { program } from "commander"; +import { exitProcess } from "@replay-cli/shared/process/exitProcess"; +import type { LocalRecording } from "@replay-cli/shared/recording/types"; +import { getBrowserPath } from "../utils/browser/getBrowserPath"; + +type ResolvedCommand = { + command: string; + args: string[]; +}; + +type StoredSnippet = { + code: string; + command: string; + timestamp: string; +}; + +type GeneratedTest = { + testCode: string; + steps: string[]; +}; + +type BrowserJsonResult = { + success: boolean; + data: Record; + error?: string; +}; + +const ENV_PATH_KEYS = [ + "REPLAYIO_PLAYWRIGHT_CLI_PATH", + "REPLAY_PLAYWRIGHT_CLI_PATH", +]; +const PLAYWRIGHT_EXECUTABLE_PATH_ENV = "PLAYWRIGHT_MCP_EXECUTABLE_PATH"; +const PLAYWRIGHT_SESSION_FLAG = "-s"; +const SNIPPET_STORE_DIR = path.join(os.tmpdir(), "replayio-browser-snippets"); +const SNIPPET_STORE_VERSION = 1; + +const OPTIONS_WITH_VALUES = new Set([ + "--profile", + "--state", + "--headers", + "--extension", + "--args", + "--user-agent", + "--proxy", + "--proxy-bypass", + "--provider", + "--device", + "--cdp", + "--config", + "--browser", + PLAYWRIGHT_SESSION_FLAG, +]); + +program + .command("browser") + .description("Proxy to the bundled Replay Playwright CLI") + .allowUnknownOption() + .allowExcessArguments(true) + .helpOption(false) + .action(runBrowser); + +async function runBrowser() { + const forwardArgs = getForwardArgs(); + const jsonMode = forwardArgs.includes("--json"); + const normalizedArgs = normalizePlaywrightCliArgs(forwardArgs); + const helpRequested = isHelpRequest(normalizedArgs); + const action = getForwardAction(normalizedArgs); + const session = resolveBrowserSession(normalizedArgs); + + let childEnv: NodeJS.ProcessEnv; + try { + childEnv = buildBrowserEnv(normalizedArgs); + } catch (error) { + if (jsonMode) { + process.stdout.write( + `${JSON.stringify({ + success: false, + data: {}, + error: formatError(error), + } satisfies BrowserJsonResult)}\n` + ); + } else { + console.error(formatError(error)); + } + await exitProcess(1); + return; + } + + const resolved = resolvePlaywrightCliCommand(); + const command = resolved?.command ?? "playwright-cli"; + const args = [...(resolved?.args ?? []), ...normalizedArgs]; + const closeContext = getSessionCloseContext(normalizedArgs); + + if (action === "open") { + clearSessionSnippets(session); + } + + if (helpRequested) { + const result = spawnSync(command, args, { + encoding: "utf8", + env: childEnv, + }); + + if (result.error) { + if (jsonMode) { + process.stdout.write( + `${JSON.stringify({ + success: false, + data: {}, + error: result.error.message, + } satisfies BrowserJsonResult)}\n` + ); + } else { + await handleSpawnError(result.error); + } + await exitProcess(1); + return; + } + + if (result.stdout) { + process.stdout.write(rewriteHelpOutput(result.stdout)); + } + if (result.stderr) { + process.stderr.write(rewriteHelpOutput(result.stderr)); + } + + await exitProcess(result.status ?? 0); + return; + } + + const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], env: childEnv }); + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", chunk => { + const text = String(chunk); + stdout += text; + if (!jsonMode) { + process.stdout.write(text); + } + }); + + child.stderr.on("data", chunk => { + const text = String(chunk); + stderr += text; + if (!jsonMode) { + process.stderr.write(text); + } + }); + + child.on("error", async (error: NodeJS.ErrnoException) => { + if (jsonMode) { + process.stdout.write( + `${JSON.stringify({ + success: false, + data: {}, + error: error.message, + } satisfies BrowserJsonResult)}\n` + ); + await exitProcess(1); + return; + } + + await handleSpawnError(error); + }); + + child.on("exit", async code => { + const exitCode = code ?? 0; + const capturedBlocks = extractPlaywrightCodeBlocks(`${stdout}\n${stderr}`); + if (capturedBlocks.length > 0) { + appendSessionSnippets(session, action ?? "unknown", capturedBlocks); + } + + let generatedTest: GeneratedTest | null = null; + if (exitCode === 0 && isCloseAction(action)) { + generatedTest = generateTestForSession(session); + } + + if (jsonMode) { + const payload = buildJsonResult({ + args: normalizedArgs, + stdout, + stderr, + exitCode, + generatedTest, + }); + process.stdout.write(`${JSON.stringify(payload)}\n`); + } else if (generatedTest) { + printGeneratedTest(generatedTest, session); + } + + if (exitCode === 0 && closeContext) { + await autoUploadClosedSessionRecordings(closeContext, { silent: jsonMode }); + } + + await exitProcess(exitCode); + }); +} + +function isHelpRequest(args: string[]): boolean { + if (args.length === 0) { + return false; + } + return args[0] === "help" || args.includes("--help") || args.includes("-h"); +} + +function isCloseAction(action: string | undefined): boolean { + return action === "close"; +} + +function rewriteHelpOutput(text: string): string { + return text.replace(/\bplaywright-cli\b/g, "replayio browser"); +} + +async function handleSpawnError(error: NodeJS.ErrnoException) { + if (error.code === "ENOENT") { + const help = [ + "Replay Playwright CLI not found.", + "", + "Reinstall replayio or point to a local build with:", + " export REPLAYIO_PLAYWRIGHT_CLI_PATH=/path/to/playwright-cli/bin/playwright-cli.js", + ].join("\n"); + console.error(help); + } else { + console.error(`Failed to launch playwright-cli: ${error.message}`); + } + + await exitProcess(1); +} + +type SessionCloseContext = { + processGroupId: string; + session: string; + scopedRecordingIds: Set; + fallbackRecordingIds: Set; +}; + +function getSessionCloseContext(args: string[]): SessionCloseContext | null { + const action = getForwardAction(args); + if (!isCloseAction(action)) { + return null; + } + + const session = resolveBrowserSession(args); + const processGroupId = getSessionProcessGroupId(session); + const scopedRecordings = getRecordings(processGroupId); + const fallbackRecordings = getRecordings(); + + return { + processGroupId, + session, + scopedRecordingIds: new Set( + scopedRecordings + .filter(recording => recording.recordingStatus === "recording") + .map(recording => recording.id) + ), + fallbackRecordingIds: new Set( + fallbackRecordings + .filter(recording => recording.recordingStatus === "recording") + .map(recording => recording.id) + ), + }; +} + +async function autoUploadClosedSessionRecordings( + context: SessionCloseContext, + options: { silent: boolean } +) { + const recordings = await waitForSessionRecordings(context); + if (recordings.length === 0) { + return; + } + + const { accessToken } = await getAccessToken(); + if (!accessToken) { + if (!options.silent) { + console.log( + `Recording(s) found for browser session "${context.session}". Run replayio login or set REPLAY_API_KEY to auto-upload.` + ); + } + return; + } + + try { + const failedIds = await uploadRecordingsInSubprocess(recordings, options); + if (!options.silent && failedIds.length > 0) { + console.error( + `Automatic upload failed for browser session "${ + context.session + }". Failed recording id(s): ${failedIds.join(", ")}` + ); + } + } catch (error) { + if (!options.silent) { + console.error( + `Automatic upload failed for browser session "${context.session}": ${formatError(error)}` + ); + } + } +} + +async function uploadRecordingsInSubprocess( + recordings: LocalRecording[], + options: { silent: boolean } +) { + const failedIds: string[] = []; + for (const recording of recordings) { + const exitCode = await runReplayioSubprocess(["upload", recording.id], options); + if (exitCode !== 0) { + failedIds.push(recording.id); + } + } + return failedIds; +} + +async function runReplayioSubprocess(args: string[], options: { silent: boolean }) { + const replayio = resolveReplayioCommand(); + return await new Promise((resolve, reject) => { + const child = spawn(replayio.command, [...replayio.args, ...args], { + env: process.env, + stdio: options.silent ? "ignore" : "inherit", + }); + + child.on("error", reject); + child.on("exit", code => { + resolve(code ?? 1); + }); + }); +} + +async function waitForSessionRecordings(context: SessionCloseContext): Promise { + if (context.scopedRecordingIds.size === 0 && context.fallbackRecordingIds.size === 0) { + return []; + } + + const pollIntervalMs = 1000; + const maxWaitMs = 20000; + const maxAttempts = Math.floor(maxWaitMs / pollIntervalMs) + 1; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const scopedRecordings = getRecordings(context.processGroupId); + const scopedUploadable = scopedRecordings.filter( + recording => context.scopedRecordingIds.has(recording.id) && canUpload(recording) + ); + + if (scopedUploadable.length > 0) { + return scopedUploadable; + } + + if (context.scopedRecordingIds.size === 0) { + const fallbackRecordings = getRecordings(); + const fallbackUploadable = fallbackRecordings.filter( + recording => context.fallbackRecordingIds.has(recording.id) && canUpload(recording) + ); + + if (fallbackUploadable.length > 0) { + return fallbackUploadable; + } + } + + if (attempt < maxAttempts - 1) { + await sleep(pollIntervalMs); + } + } + + return []; +} + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function formatError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +function getForwardArgs(): string[] { + const argv = process.argv; + const index = argv.findIndex(arg => arg === "browser"); + if (index === -1) { + return []; + } + return argv.slice(index + 1); +} + +function normalizePlaywrightCliArgs(args: string[]): string[] { + return args.filter(arg => arg !== "--json"); +} + +function getForwardAction(args: string[]): string | undefined { + let consumeNext = false; + for (const arg of args) { + if (consumeNext) { + consumeNext = false; + continue; + } + + if (arg === "--") { + return undefined; + } + + if (arg.startsWith("--")) { + const key = arg.split("=", 1)[0]; + if (OPTIONS_WITH_VALUES.has(key) && !arg.includes("=")) { + consumeNext = true; + } + continue; + } + + if (arg.startsWith("-")) { + if (OPTIONS_WITH_VALUES.has(arg)) { + consumeNext = true; + } + continue; + } + + return arg; + } + + return undefined; +} + +function resolveBrowserSession(args: string[]): string { + let session = process.env.PLAYWRIGHT_CLI_SESSION || "default"; + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === PLAYWRIGHT_SESSION_FLAG) { + session = args[i + 1] ?? session; + i += 1; + continue; + } + if (arg.startsWith(`${PLAYWRIGHT_SESSION_FLAG}=`)) { + session = arg.slice(`${PLAYWRIGHT_SESSION_FLAG}=`.length); + } + } + return session; +} + +function getSessionProcessGroupId(session: string): string { + return `playwright-cli-session:${session}`; +} + +function buildBrowserEnv(args: string[]): NodeJS.ProcessEnv { + const session = resolveBrowserSession(args); + const processGroupId = getSessionProcessGroupId(session); + const action = getForwardAction(args); + + let metadata: Record = {}; + const rawMetadata = process.env.RECORD_REPLAY_METADATA; + if (rawMetadata) { + try { + const parsed = JSON.parse(rawMetadata); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + metadata = parsed as Record; + } + } catch { + // Ignore invalid user-provided metadata and continue with session metadata. + } + } + + const env: NodeJS.ProcessEnv = { + ...process.env, + RECORD_ALL_CONTENT: process.env.RECORD_ALL_CONTENT || "1", + RECORD_REPLAY_METADATA: JSON.stringify({ + ...metadata, + browserSession: session, + processGroupId, + }), + RECORD_REPLAY_VERBOSE: process.env.RECORD_REPLAY_VERBOSE || "1", + }; + + const shouldSetExecutablePath = + !isHelpRequest(args) && + action !== "install" && + action !== "install-browser" && + !env[PLAYWRIGHT_EXECUTABLE_PATH_ENV]; + + if (shouldSetExecutablePath) { + const executablePath = getBrowserPath(); + if (!existsSync(executablePath)) { + throw new Error( + `Replay Browser not found at ${executablePath}. Run 'replayio install' to install it.` + ); + } + + env[PLAYWRIGHT_EXECUTABLE_PATH_ENV] = executablePath; + } + + if (!env.PLAYWRIGHT_CLI_SESSION) { + env.PLAYWRIGHT_CLI_SESSION = session; + } + + return env; +} + +function resolveReplayioCommand(): ResolvedCommand { + const argv1 = process.argv[1]; + if (argv1 && existsSync(argv1)) { + if (argv1.endsWith(".js") || argv1.endsWith(".mjs")) { + return { command: process.execPath, args: [argv1] }; + } + return { command: argv1, args: [] }; + } + return { command: "replayio", args: [] }; +} + +function resolvePlaywrightCliCommand(): ResolvedCommand | null { + const fromEnv = resolveFromEnv(); + if (fromEnv) { + return fromEnv; + } + + const fromPackage = resolveFromPackage(); + if (fromPackage) { + return fromPackage; + } + + return null; +} + +function resolveFromEnv(): ResolvedCommand | null { + for (const key of ENV_PATH_KEYS) { + const envPath = process.env[key]; + if (!envPath) { + continue; + } + + if (envPath.endsWith(".js") || envPath.endsWith(".mjs")) { + return { command: process.execPath, args: [envPath] }; + } + + return { command: envPath, args: [] }; + } + + return null; +} + +function resolveFromPackage(): ResolvedCommand | null { + try { + const pkgPath = require.resolve("@playwright/cli/package.json"); + const pkgJson = JSON.parse(readFileSync(pkgPath, "utf8")) as { + bin?: string | Record; + }; + const binValue = pkgJson.bin; + const binRelative = + typeof binValue === "string" + ? binValue + : binValue?.["playwright-cli"] ?? binValue?.playwright ?? Object.values(binValue ?? {})[0]; + + if (!binRelative) { + return null; + } + + const binPath = path.resolve(path.dirname(pkgPath), binRelative); + if (!existsSync(binPath)) { + return null; + } + + if (binPath.endsWith(".js") || binPath.endsWith(".mjs")) { + return { command: process.execPath, args: [binPath] }; + } + + return { command: binPath, args: [] }; + } catch { + return null; + } +} + +function extractPlaywrightCodeBlocks(text: string): string[] { + const blocks: string[] = []; + const pattern = /### Ran Playwright code\s*```(?:\w+)?\s*([\s\S]*?)```/g; + let match: RegExpExecArray | null; + + while ((match = pattern.exec(text)) !== null) { + const code = match[1]?.trim(); + if (code) { + blocks.push(code); + } + } + + return blocks; +} + +function appendSessionSnippets(session: string, command: string, codeBlocks: string[]) { + const previous = readSessionSnippets(session); + const next = [ + ...previous, + ...codeBlocks.map( + code => + ({ + code, + command, + timestamp: new Date().toISOString(), + }) satisfies StoredSnippet + ), + ]; + writeSessionSnippets(session, next); +} + +function clearSessionSnippets(session: string) { + const filePath = getSessionStorePath(session); + try { + rmSync(filePath, { force: true }); + } catch { + // Best effort cleanup. + } +} + +function generateTestForSession(session: string): GeneratedTest | null { + const snippets = readSessionSnippets(session); + if (snippets.length === 0) { + clearSessionSnippets(session); + return null; + } + + const steps = snippets.map(snippet => summarizeSnippet(snippet.code)); + const lines: string[] = []; + lines.push(`import { test } from "@playwright/test";`); + lines.push(""); + lines.push(`test("replayio browser session ${escapeForDoubleQuotedString(session)}", async ({ page }) => {`); + + snippets.forEach((snippet, index) => { + lines.push(` // Step ${index + 1}: ${snippet.command}`); + snippet.code.split(/\r?\n/).forEach(codeLine => { + lines.push(` ${codeLine}`); + }); + lines.push(""); + }); + + lines.push("});"); + + clearSessionSnippets(session); + + return { + testCode: lines.join("\n"), + steps, + }; +} + +function printGeneratedTest(generated: GeneratedTest, session: string) { + process.stdout.write(`\n### Deterministic Playwright Test (${session})\n`); + process.stdout.write("```ts\n"); + process.stdout.write(`${generated.testCode.trimEnd()}\n`); + process.stdout.write("```\n"); + process.stdout.write(`### Steps (${generated.steps.length})\n`); + generated.steps.forEach((step, index) => { + process.stdout.write(`${index + 1}. ${step}\n`); + }); +} + +function summarizeSnippet(code: string): string { + const firstLine = code + .split(/\r?\n/) + .map(line => line.trim()) + .find(Boolean); + const summary = firstLine || "(empty snippet)"; + return summary.length > 140 ? `${summary.slice(0, 137)}...` : summary; +} + +function escapeForDoubleQuotedString(value: string): string { + return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); +} + +function getSessionStorePath(session: string): string { + return path.join(SNIPPET_STORE_DIR, `${sanitizeSessionName(session)}.json`); +} + +function sanitizeSessionName(session: string): string { + return session.replace(/[^a-zA-Z0-9._-]/g, "_"); +} + +function readSessionSnippets(session: string): StoredSnippet[] { + const filePath = getSessionStorePath(session); + if (!existsSync(filePath)) { + return []; + } + + try { + const parsed = JSON.parse(readFileSync(filePath, "utf8")) as { + version?: number; + snippets?: StoredSnippet[]; + }; + if (parsed?.version !== SNIPPET_STORE_VERSION || !Array.isArray(parsed.snippets)) { + return []; + } + + return parsed.snippets.filter( + snippet => + Boolean(snippet) && + typeof snippet.code === "string" && + typeof snippet.command === "string" && + typeof snippet.timestamp === "string" + ); + } catch { + return []; + } +} + +function writeSessionSnippets(session: string, snippets: StoredSnippet[]) { + mkdirSync(SNIPPET_STORE_DIR, { recursive: true }); + const filePath = getSessionStorePath(session); + writeFileSync( + filePath, + JSON.stringify( + { + version: SNIPPET_STORE_VERSION, + snippets, + }, + null, + 2 + ) + ); +} + +function buildJsonResult(input: { + args: string[]; + stdout: string; + stderr: string; + exitCode: number; + generatedTest: GeneratedTest | null; +}): BrowserJsonResult { + const data: Record = {}; + const action = getForwardAction(input.args); + const screenshotPath = extractMarkdownLinkTarget(input.stdout, "Screenshot"); + const snapshotPath = extractMarkdownLinkTarget(input.stdout, "Snapshot"); + const maybeUrl = extractPageField(input.stdout, "Page URL"); + const maybeTitle = extractPageField(input.stdout, "Page Title"); + const evalResult = extractResultValue(input.stdout); + + if (screenshotPath) { + data.path = screenshotPath; + } + + if (snapshotPath) { + data.snapshot = snapshotPath; + data.refs = {}; + } + + if (maybeUrl) { + data.url = maybeUrl; + } + + if (maybeTitle) { + data.title = maybeTitle; + } + + if (evalResult !== undefined) { + data.result = evalResult; + } + + if ( + action === "eval" && + input.args[1] === "() => location.href" && + typeof evalResult === "string" + ) { + data.url = evalResult; + } + + if ( + action === "eval" && + input.args[1] === "() => document.title" && + typeof evalResult === "string" + ) { + data.title = evalResult; + } + + if (action === "list") { + data.sessions = extractSessionNamesFromList(input.stdout); + } + + if (input.generatedTest) { + data.generatedTest = input.generatedTest.testCode; + data.generatedSteps = input.generatedTest.steps; + } + + if (input.exitCode !== 0) { + return { + success: false, + data, + error: input.stderr.trim() || input.stdout.trim() || `Command exited with code ${input.exitCode}`, + }; + } + + return { + success: true, + data, + }; +} + +function extractMarkdownLinkTarget(text: string, label: string): string | undefined { + const regex = new RegExp(`- \\[${escapeRegex(label)}\\]\\(([^)]+)\\)`); + const match = text.match(regex); + return match?.[1]; +} + +function extractPageField(text: string, key: string): string | undefined { + const regex = new RegExp(`- ${escapeRegex(key)}:\\s*(.+)`); + const match = text.match(regex); + return match?.[1]?.trim(); +} + +function extractResultValue(text: string): unknown { + const match = text.match(/### Result\s*([\s\S]*?)(?:\n### |\s*$)/); + if (!match?.[1]) { + return undefined; + } + + const raw = match[1].trim(); + if (!raw) { + return undefined; + } + + try { + return JSON.parse(raw); + } catch { + return raw; + } +} + +function extractSessionNamesFromList(text: string): string[] { + const sessions: string[] = []; + const lines = text.split(/\r?\n/); + for (const line of lines) { + const match = line.match(/^- ([^:]+):\s*$/); + if (match?.[1]) { + sessions.push(match[1].trim()); + } + } + return sessions; +} + +function escapeRegex(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} diff --git a/yarn.lock b/yarn.lock index da3c20a8..07e79e91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3289,6 +3289,18 @@ __metadata: languageName: node linkType: hard +"@playwright/cli@npm:latest": + version: 0.1.0 + resolution: "@playwright/cli@npm:0.1.0" + dependencies: + minimist: "npm:^1.2.5" + playwright: "npm:1.59.0-alpha-1770426101000" + bin: + playwright-cli: playwright-cli.js + checksum: 10c0/7f6cbb9fafc91c00455de7c92524f8ec6239e0705f02ef959497df12d503bf959afe13e65302063a85c3e428c73ee73e89554ef3c8b65f199577980c40948c11 + languageName: node + linkType: hard + "@playwright/test@npm:^1.52.0": version: 1.52.0 resolution: "@playwright/test@npm:1.52.0" @@ -13069,7 +13081,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.6, minimist@npm:^1.2.8": +"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 @@ -14007,6 +14019,15 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.59.0-alpha-1770426101000": + version: 1.59.0-alpha-1770426101000 + resolution: "playwright-core@npm:1.59.0-alpha-1770426101000" + bin: + playwright-core: cli.js + checksum: 10c0/cbf57be8ca0076a298fcccb5cad6213c23c9786cf3f3678a0dfab614541d1bdd32ac89b1ca998b851322ef2c4f7de55002838483ddbc330e2dc1be1c02248578 + languageName: node + linkType: hard + "playwright@npm:1.52.0": version: 1.52.0 resolution: "playwright@npm:1.52.0" @@ -14022,6 +14043,21 @@ __metadata: languageName: node linkType: hard +"playwright@npm:1.59.0-alpha-1770426101000": + version: 1.59.0-alpha-1770426101000 + resolution: "playwright@npm:1.59.0-alpha-1770426101000" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.59.0-alpha-1770426101000" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10c0/0e7e1b7b8e26c3893d19e6421c567e6459b5cd5f801576e3a3f79ffdfc312973b6ee36bb7d45d3309e5632d402cd921368a3f42a053aac8f2532de45e2ef1c3b + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.0.0 resolution: "possible-typed-array-names@npm:1.0.0" @@ -15716,6 +15752,7 @@ __metadata: version: 0.0.0-use.local resolution: "replayio@workspace:packages/replayio" dependencies: + "@playwright/cli": "npm:latest" "@replay-cli/pkg-build": "workspace:^" "@replay-cli/shared": "workspace:^" "@replay-cli/tsconfig": "workspace:^"