extract Ghost Browser code to shared ghost-browser.ts
- Constants, types, proxy factory, and handler in one file - Works in both Node.js (executor) and Chrome (extension) environments - executor.ts imports createGhostBrowserChrome factory - background.ts imports handleGhostBrowserCommand handler
This commit is contained in:
@@ -4,6 +4,7 @@ import { createStore } from 'zustand/vanilla'
|
|||||||
import type { ExtensionState, ConnectionState, TabState, TabInfo } from './types'
|
import type { ExtensionState, ConnectionState, TabState, TabInfo } from './types'
|
||||||
import type { CDPEvent, Protocol } from 'playwriter/src/cdp-types'
|
import type { CDPEvent, Protocol } from 'playwriter/src/cdp-types'
|
||||||
import type { ExtensionCommandMessage, ExtensionResponseMessage } from 'playwriter/src/protocol'
|
import type { ExtensionCommandMessage, ExtensionResponseMessage } from 'playwriter/src/protocol'
|
||||||
|
import { handleGhostBrowserCommand, type GhostBrowserCommandParams } from 'playwriter/src/ghost-browser'
|
||||||
import {
|
import {
|
||||||
getActiveRecordings,
|
getActiveRecordings,
|
||||||
handleStartRecording,
|
handleStartRecording,
|
||||||
@@ -278,51 +279,14 @@ class ConnectionManager {
|
|||||||
// This allows calling chrome.ghostPublicAPI, chrome.ghostProxies, chrome.projects
|
// This allows calling chrome.ghostPublicAPI, chrome.ghostProxies, chrome.projects
|
||||||
// from the playwriter executor sandbox when running in Ghost Browser
|
// from the playwriter executor sandbox when running in Ghost Browser
|
||||||
if (message.method === 'ghost-browser') {
|
if (message.method === 'ghost-browser') {
|
||||||
const { namespace, method, args } = message.params as {
|
const result = await handleGhostBrowserCommand(
|
||||||
namespace: 'ghostPublicAPI' | 'ghostProxies' | 'projects'
|
message.params as GhostBrowserCommandParams,
|
||||||
method: string
|
chrome
|
||||||
args: unknown[]
|
)
|
||||||
}
|
if (!result.success) {
|
||||||
try {
|
logger.error('Ghost Browser API error:', result.error)
|
||||||
const api = (chrome as any)[namespace]
|
|
||||||
if (!api) {
|
|
||||||
sendMessage({
|
|
||||||
id: message.id,
|
|
||||||
result: { success: false, error: `chrome.${namespace} not available (not running in Ghost Browser?)` }
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn = api[method]
|
|
||||||
if (typeof fn !== 'function') {
|
|
||||||
// Check if it's a constant/property
|
|
||||||
if (method in api) {
|
|
||||||
sendMessage({ id: message.id, result: { success: true, result: api[method] } })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sendMessage({
|
|
||||||
id: message.id,
|
|
||||||
result: { success: false, error: `chrome.${namespace}.${method} is not a function or property` }
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ghost Browser APIs use callback pattern, wrap in Promise
|
|
||||||
const result = await new Promise((resolve, reject) => {
|
|
||||||
fn.call(api, ...args, (result: unknown) => {
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
reject(new Error(chrome.runtime.lastError.message))
|
|
||||||
} else {
|
|
||||||
resolve(result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sendMessage({ id: message.id, result: { success: true, result } })
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error('Ghost Browser API error:', error)
|
|
||||||
sendMessage({ id: message.id, result: { success: false, error: error.message } })
|
|
||||||
}
|
}
|
||||||
|
sendMessage({ id: message.id, result })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
type ScreenshotResult,
|
type ScreenshotResult,
|
||||||
type SnapshotFormat,
|
type SnapshotFormat,
|
||||||
} from './aria-snapshot.js'
|
} from './aria-snapshot.js'
|
||||||
|
import { createGhostBrowserChrome, type GhostBrowserCommandResult } from './ghost-browser.js'
|
||||||
export type { SnapshotFormat }
|
export type { SnapshotFormat }
|
||||||
import { getCleanHTML, type GetCleanHTMLOptions } from './clean-html.js'
|
import { getCleanHTML, type GetCleanHTMLOptions } from './clean-html.js'
|
||||||
import { getPageMarkdown, type GetPageMarkdownOptions } from './page-markdown.js'
|
import { getPageMarkdown, type GetPageMarkdownOptions } from './page-markdown.js'
|
||||||
@@ -741,55 +742,17 @@ export class PlaywrightExecutor {
|
|||||||
}
|
}
|
||||||
const self = this
|
const self = this
|
||||||
|
|
||||||
// Ghost Browser API integration - creates proxy objects that mirror chrome.ghostPublicAPI,
|
// Ghost Browser API - creates chrome object that mirrors Ghost Browser's APIs
|
||||||
// chrome.ghostProxies, and chrome.projects APIs. Only works when running in Ghost Browser.
|
// See extension/src/ghost-browser-api.d.ts for full API documentation
|
||||||
// See extension/src/ghost-browser-api.d.ts for full API documentation.
|
const chromeGhostBrowser = createGhostBrowserChrome(async (namespace, method, args) => {
|
||||||
const createGhostBrowserProxy = (
|
|
||||||
namespace: 'ghostPublicAPI' | 'ghostProxies' | 'projects',
|
|
||||||
constants: Record<string, unknown> = {}
|
|
||||||
) => {
|
|
||||||
const sendGhostBrowserCommand = async (method: string, args: unknown[]) => {
|
|
||||||
const cdp = await getCDPSession({ page })
|
const cdp = await getCDPSession({ page })
|
||||||
const result = await cdp.send('ghost-browser' as any, { namespace, method, args })
|
const result = await cdp.send('ghost-browser' as any, { namespace, method, args })
|
||||||
const typed = result as { success: boolean; result?: unknown; error?: string }
|
const typed = result as GhostBrowserCommandResult
|
||||||
if (!typed.success) {
|
if (!typed.success) {
|
||||||
throw new Error(typed.error || `Ghost Browser API call failed: ${namespace}.${method}`)
|
throw new Error(typed.error || `Ghost Browser API call failed: ${namespace}.${method}`)
|
||||||
}
|
}
|
||||||
return typed.result
|
return typed.result
|
||||||
}
|
|
||||||
|
|
||||||
return new Proxy(constants, {
|
|
||||||
get(target, prop: string) {
|
|
||||||
// Return constants directly (no await needed)
|
|
||||||
if (prop in target) {
|
|
||||||
return target[prop]
|
|
||||||
}
|
|
||||||
// Return function that sends ghost-browser command
|
|
||||||
return (...args: unknown[]) => sendGhostBrowserCommand(prop, args)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// Chrome object with Ghost Browser API namespaces
|
|
||||||
// These mirror the exact shape of chrome.ghostPublicAPI, chrome.ghostProxies, chrome.projects
|
|
||||||
const chromeGhostBrowser = {
|
|
||||||
ghostPublicAPI: createGhostBrowserProxy('ghostPublicAPI', {
|
|
||||||
NEW_TEMPORARY_IDENTITY: 'OpenInNewSession',
|
|
||||||
DEFAULT_IDENTITY: '',
|
|
||||||
MAX_TEMPORARY_IDENTITIES: 25,
|
|
||||||
}),
|
|
||||||
ghostProxies: createGhostBrowserProxy('ghostProxies', {
|
|
||||||
DIRECT_PROXY: '8f513494-8cf5-41c7-b318-936392222104',
|
|
||||||
SYSTEM_PROXY: '2485b989-7ffb-4442-a45a-d7f9a10c6171',
|
|
||||||
}),
|
|
||||||
projects: createGhostBrowserProxy('projects', {
|
|
||||||
PROJECT_ID_HOME: 'f0673216-13b9-48be-aa41-90763e229e78',
|
|
||||||
PROJECT_ID_UNSAVED: 'fe061488-8a8e-40f0-9e5e-93a1a5e2c273',
|
|
||||||
SESSIONS_MAX: 25,
|
|
||||||
NEW_SESSION: 'OpenInNewSession',
|
|
||||||
GLOBAL_SESSION: '',
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
let vmContextObj: any = {
|
let vmContextObj: any = {
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* Ghost Browser API integration for Playwriter
|
||||||
|
*
|
||||||
|
* Shared code for both executor (Node.js) and extension (Chrome) environments.
|
||||||
|
* See extension/src/ghost-browser-api.d.ts for full API documentation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Constants - same values as in Ghost Browser
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const GHOST_PUBLIC_API_CONSTANTS = {
|
||||||
|
NEW_TEMPORARY_IDENTITY: 'OpenInNewSession',
|
||||||
|
DEFAULT_IDENTITY: '',
|
||||||
|
MAX_TEMPORARY_IDENTITIES: 25,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const GHOST_PROXIES_CONSTANTS = {
|
||||||
|
DIRECT_PROXY: '8f513494-8cf5-41c7-b318-936392222104',
|
||||||
|
SYSTEM_PROXY: '2485b989-7ffb-4442-a45a-d7f9a10c6171',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const GHOST_PROJECTS_CONSTANTS = {
|
||||||
|
PROJECT_ID_HOME: 'f0673216-13b9-48be-aa41-90763e229e78',
|
||||||
|
PROJECT_ID_UNSAVED: 'fe061488-8a8e-40f0-9e5e-93a1a5e2c273',
|
||||||
|
SESSIONS_MAX: 25,
|
||||||
|
NEW_SESSION: 'OpenInNewSession',
|
||||||
|
GLOBAL_SESSION: '',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type GhostBrowserNamespace = 'ghostPublicAPI' | 'ghostProxies' | 'projects'
|
||||||
|
|
||||||
|
export type GhostBrowserCommandParams = {
|
||||||
|
namespace: GhostBrowserNamespace
|
||||||
|
method: string
|
||||||
|
args: unknown[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GhostBrowserCommandResult =
|
||||||
|
| { success: true; result: unknown }
|
||||||
|
| { success: false; error: string }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function signature for sending ghost-browser commands.
|
||||||
|
* In executor: sends via CDP session
|
||||||
|
* In extension: calls chrome.* APIs directly
|
||||||
|
*/
|
||||||
|
export type SendGhostBrowserCommand = (
|
||||||
|
namespace: GhostBrowserNamespace,
|
||||||
|
method: string,
|
||||||
|
args: unknown[]
|
||||||
|
) => Promise<unknown>
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Executor: Chrome object factory
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a proxy object for a Ghost Browser API namespace.
|
||||||
|
* Constants are returned directly, methods are forwarded via sendCommand.
|
||||||
|
*/
|
||||||
|
function createGhostBrowserProxy(
|
||||||
|
namespace: GhostBrowserNamespace,
|
||||||
|
constants: Record<string, unknown>,
|
||||||
|
sendCommand: SendGhostBrowserCommand
|
||||||
|
) {
|
||||||
|
return new Proxy(constants, {
|
||||||
|
get(target, prop: string) {
|
||||||
|
// Return constants directly (no await needed)
|
||||||
|
if (prop in target) {
|
||||||
|
return target[prop]
|
||||||
|
}
|
||||||
|
// Return function that sends ghost-browser command
|
||||||
|
return (...args: unknown[]) => sendCommand(namespace, prop, args)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the chrome object with Ghost Browser API namespaces for the executor sandbox.
|
||||||
|
* Mirrors the exact shape of chrome.ghostPublicAPI, chrome.ghostProxies, chrome.projects.
|
||||||
|
*
|
||||||
|
* @param sendCommand - Function to send commands to the extension
|
||||||
|
*/
|
||||||
|
export function createGhostBrowserChrome(sendCommand: SendGhostBrowserCommand) {
|
||||||
|
return {
|
||||||
|
ghostPublicAPI: createGhostBrowserProxy('ghostPublicAPI', GHOST_PUBLIC_API_CONSTANTS, sendCommand),
|
||||||
|
ghostProxies: createGhostBrowserProxy('ghostProxies', GHOST_PROXIES_CONSTANTS, sendCommand),
|
||||||
|
projects: createGhostBrowserProxy('projects', GHOST_PROJECTS_CONSTANTS, sendCommand),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Extension: Command handler
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles ghost-browser commands in the extension.
|
||||||
|
* Calls the appropriate chrome.* API and returns the result.
|
||||||
|
*
|
||||||
|
* @param params - Command parameters (namespace, method, args)
|
||||||
|
* @param chromeApi - The chrome object (passed to avoid global dependency)
|
||||||
|
* @returns Result object with success/error status
|
||||||
|
*/
|
||||||
|
export async function handleGhostBrowserCommand(
|
||||||
|
params: GhostBrowserCommandParams,
|
||||||
|
chromeApi: typeof chrome
|
||||||
|
): Promise<GhostBrowserCommandResult> {
|
||||||
|
const { namespace, method, args } = params
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = (chromeApi as any)[namespace]
|
||||||
|
if (!api) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `chrome.${namespace} not available (not running in Ghost Browser?)`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn = api[method]
|
||||||
|
if (typeof fn !== 'function') {
|
||||||
|
// Check if it's a constant/property
|
||||||
|
if (method in api) {
|
||||||
|
return { success: true, result: api[method] }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `chrome.${namespace}.${method} is not a function or property`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ghost Browser APIs use callback pattern, wrap in Promise
|
||||||
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
fn.call(api, ...args, (result: unknown) => {
|
||||||
|
if (chromeApi.runtime.lastError) {
|
||||||
|
reject(new Error(chromeApi.runtime.lastError.message))
|
||||||
|
} else {
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true, result }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user