diff --git a/.circleci/config.yml b/.circleci/config.yml index 2de3de1..665aa67 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,9 +15,9 @@ orbs: executors: base: docker: - - image: docker.io/playerui/bazel-docker:9 + - image: docker.io/playerui/bazel-docker working_directory: ~/devtools - resource_class: large + resource_class: xlarge environment: TZ: "/usr/share/zoneinfo/America/Los_Angeles" minimal: diff --git a/devtools/client/README.md b/devtools/client/README.md index 7202336..d2a7070 100644 --- a/devtools/client/README.md +++ b/devtools/client/README.md @@ -1,19 +1,22 @@ -# @player-devtools/client +# Player UI Devtools client -The `@player-devtools/client` exposes the Panel with the ReactPlayer, which is responsible for running content sent by Player devtool plugins on the inspected Player UI instance. +The devtools client is split into two packages so headless consumers never load React: -The `Panel` is the shared devtools UI surface, hosted by each client: the [browser extension](https://github.com/player-ui/browser-devtools) for web and the [Flipper plugin](../flipper-plugin) for mobile. The agent-facing [MCP server](../mcp) consumes the same Player devtools instrumentation without rendering the `Panel`. +- [`core/`](./core) — `@player-devtools/client`: the headless client (`createExtensionClient`, state reducer). No React. Consumed by the agent-facing [MCP server](../mcp). +- [`react/`](./react) — `@player-devtools/client-react`: the React `Panel` with the ReactPlayer, responsible for rendering content sent by Player devtool plugins on the inspected Player UI instance. + +The `Panel` is the shared devtools UI surface, hosted by each client: the [browser extension](https://github.com/player-ui/browser-devtools) for web and the [Flipper plugin](../flipper-plugin) for mobile. The agent-facing [MCP server](../mcp) consumes the same Player devtools instrumentation via the headless `@player-devtools/client` core, without rendering the `Panel`. ## Installation The Devtools client is available as an npm package. You can install it using npm or yarn: ```bash -npm install @player-devtools/client +npm install @player-devtools/client-react ``` ```bash -yarn add @player-devtools/client +yarn add @player-devtools/client-react ``` ## Overview @@ -29,7 +32,7 @@ For a more comprehensive understanding of the architecture of the Devtools clien The Devtools client is a React component that receives content from devtools plugins running in the Player UI used by the inspected page. It can be used in your React application like any other React component. ```jsx -import { Panel } from "@player-devtools/client"; +import { Panel } from "@player-devtools/client-react"; import type { MessengerOptions } from "@player-devtools/messenger"; import browser from "webextension-polyfill"; diff --git a/devtools/client/core/BUILD b/devtools/client/core/BUILD new file mode 100644 index 0000000..2e785c7 --- /dev/null +++ b/devtools/client/core/BUILD @@ -0,0 +1,23 @@ +load("@npm//:defs.bzl", "npm_link_all_packages") +load("@rules_player//javascript:defs.bzl", "js_pipeline") +load("//helpers:defs.bzl", "tsup_config", "vitest_config") + +npm_link_all_packages(name = "node_modules") + +tsup_config(name = "tsup_config") + +vitest_config(name = "vitest_config") + +js_pipeline( + package_name = "@player-devtools/client", + deps = [ + ":node_modules/@player-devtools/messenger", + ":node_modules/@player-devtools/types", + ":node_modules/@player-devtools/utils", + "//:node_modules/immer", + "//:node_modules/dequal", + ], + test_deps = [ + "//:node_modules/@player-ui/player", + ], +) diff --git a/devtools/client/package.json b/devtools/client/core/package.json similarity index 100% rename from devtools/client/package.json rename to devtools/client/core/package.json diff --git a/devtools/client/core/src/constants/index.ts b/devtools/client/core/src/constants/index.ts new file mode 100644 index 0000000..7e1a5f6 --- /dev/null +++ b/devtools/client/core/src/constants/index.ts @@ -0,0 +1,9 @@ +import type { ExtensionState } from "@player-devtools/types"; + +export const INITIAL_EXTENSION_STATE: ExtensionState = { + current: { + player: null, + plugin: null, + }, + players: {}, +}; diff --git a/devtools/client/src/index.ts b/devtools/client/core/src/index.ts similarity index 70% rename from devtools/client/src/index.ts rename to devtools/client/core/src/index.ts index 531c6a1..175aa31 100644 --- a/devtools/client/src/index.ts +++ b/devtools/client/core/src/index.ts @@ -1,2 +1 @@ -export { Panel } from "./panel"; export { createExtensionClient, type ExtensionClient } from "./state/client"; diff --git a/devtools/client/src/state/__tests__/reducer.test.ts b/devtools/client/core/src/state/__tests__/reducer.test.ts similarity index 94% rename from devtools/client/src/state/__tests__/reducer.test.ts rename to devtools/client/core/src/state/__tests__/reducer.test.ts index 9fafd1c..fc62891 100644 --- a/devtools/client/src/state/__tests__/reducer.test.ts +++ b/devtools/client/core/src/state/__tests__/reducer.test.ts @@ -9,8 +9,32 @@ import type { ExtensionSelectedPlayerEvent, ExtensionSelectedPluginEvent, } from "@player-devtools/types"; +import type { Flow } from "@player-ui/player"; import { reducer } from "../reducer"; -import { INITIAL_EXTENSION_STATE, INITIAL_FLOW } from "../../constants"; +import { INITIAL_EXTENSION_STATE } from "../../constants"; + +/** Minimal flow fixture used to exercise the reducer's flow handling. */ +const INITIAL_FLOW: Flow = { + id: "initial-flow", + views: [ + { + id: "view-1", + type: "text", + value: "connecting...", + }, + ], + navigation: { + BEGIN: "FLOW_1", + FLOW_1: { + startState: "VIEW_1", + VIEW_1: { + state_type: "VIEW", + ref: "view-1", + transitions: {}, + }, + }, + }, +}; const mockPlayerInitTransaction: Transaction = { id: 1, diff --git a/devtools/client/src/state/client.ts b/devtools/client/core/src/state/client.ts similarity index 100% rename from devtools/client/src/state/client.ts rename to devtools/client/core/src/state/client.ts diff --git a/devtools/client/src/state/reducer.ts b/devtools/client/core/src/state/reducer.ts similarity index 100% rename from devtools/client/src/state/reducer.ts rename to devtools/client/core/src/state/reducer.ts diff --git a/devtools/client/flipper/BUILD b/devtools/client/flipper/BUILD new file mode 100644 index 0000000..c71f7eb --- /dev/null +++ b/devtools/client/flipper/BUILD @@ -0,0 +1,20 @@ +load("@npm//:defs.bzl", "npm_link_all_packages") +load("@rules_player//javascript:defs.bzl", "js_pipeline") +load("//helpers:defs.bzl", "tsup_config", "vitest_config") + +npm_link_all_packages(name = "node_modules") + +tsup_config(name = "tsup_config") + +vitest_config(name = "vitest_config") + +js_pipeline( + package_name = "@player-devtools/client-flipper", + deps = [ + ":node_modules/@player-devtools/types", + "//:node_modules/@types/ws", + "//:node_modules/flipper-server", + "//:node_modules/flipper-server-client", + "//:node_modules/ws", + ], +) diff --git a/devtools/client/flipper/package.json b/devtools/client/flipper/package.json new file mode 100644 index 0000000..3d08f33 --- /dev/null +++ b/devtools/client/flipper/package.json @@ -0,0 +1,8 @@ +{ + "name": "@player-devtools/client-flipper", + "version": "0.0.0-PLACEHOLDER", + "main": "src/index.ts", + "dependencies": { + "@player-devtools/types": "workspace:*" + } +} diff --git a/devtools/client/flipper/src/index.ts b/devtools/client/flipper/src/index.ts new file mode 100644 index 0000000..e829866 --- /dev/null +++ b/devtools/client/flipper/src/index.ts @@ -0,0 +1 @@ +export { FlipperServerTransport } from "./transport"; diff --git a/devtools/mcp/src/transport.ts b/devtools/client/flipper/src/transport.ts similarity index 97% rename from devtools/mcp/src/transport.ts rename to devtools/client/flipper/src/transport.ts index ed43f2d..59e3e4f 100644 --- a/devtools/mcp/src/transport.ts +++ b/devtools/client/flipper/src/transport.ts @@ -8,13 +8,21 @@ import * as net from "net"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; +import { WebSocket as WsWebSocket } from "ws"; import type { CommunicationLayerMethods, ExtensionSupportedEvents, MessengerEvent, TransactionMetadata, + Transport, } from "@player-devtools/types"; +// polyfill WebSocket for Node < 22 +if (typeof (globalThis as { WebSocket?: unknown }).WebSocket === "undefined") { + (globalThis as { WebSocket?: unknown }).WebSocket = + WsWebSocket as unknown as typeof WebSocket; +} + type MessageCallback = ( message: TransactionMetadata & MessengerEvent, ) => void; @@ -31,14 +39,6 @@ type FlipperExecuteMessage = { }; }; -/** Transport interface — implemented by each connection adapter */ -export interface Transport extends CommunicationLayerMethods { - /** Connect to the underlying transport */ - connect(): Promise; - /** Tear down the underlying transport */ - close(): Promise; -} - /** * Flipper headless transport * diff --git a/devtools/client/BUILD b/devtools/client/react/BUILD similarity index 87% rename from devtools/client/BUILD rename to devtools/client/react/BUILD index 9bc0e08..4923a3b 100644 --- a/devtools/client/BUILD +++ b/devtools/client/react/BUILD @@ -9,8 +9,9 @@ tsup_config(name = "tsup_config") vitest_config(name = "vitest_config") js_pipeline( - package_name = "@player-devtools/client", + package_name = "@player-devtools/client-react", deps = [ + ":node_modules/@player-devtools/client", ":node_modules/@player-devtools/messenger", ":node_modules/@player-devtools/types", ":node_modules/@player-devtools/utils", @@ -27,6 +28,6 @@ js_pipeline( "//:node_modules/react", "//:node_modules/react-error-boundary", "//:node_modules/dequal", - "//:node_modules/@devtools-ds/themes" + "//:node_modules/@devtools-ds/themes", ], ) diff --git a/devtools/client/react/package.json b/devtools/client/react/package.json new file mode 100644 index 0000000..99b49e5 --- /dev/null +++ b/devtools/client/react/package.json @@ -0,0 +1,11 @@ +{ + "name": "@player-devtools/client-react", + "version": "0.0.0-PLACEHOLDER", + "main": "src/index.ts", + "dependencies": { + "@player-devtools/client": "workspace:*", + "@player-devtools/messenger": "workspace:*", + "@player-devtools/types": "workspace:*", + "@player-devtools/utils": "workspace:*" + } +} diff --git a/devtools/client/src/helpers/__tests__/flowDiff.test.ts b/devtools/client/react/src/helpers/__tests__/flowDiff.test.ts similarity index 100% rename from devtools/client/src/helpers/__tests__/flowDiff.test.ts rename to devtools/client/react/src/helpers/__tests__/flowDiff.test.ts diff --git a/devtools/client/src/helpers/flowDiff.ts b/devtools/client/react/src/helpers/flowDiff.ts similarity index 100% rename from devtools/client/src/helpers/flowDiff.ts rename to devtools/client/react/src/helpers/flowDiff.ts diff --git a/devtools/client/react/src/index.ts b/devtools/client/react/src/index.ts new file mode 100644 index 0000000..8628fc5 --- /dev/null +++ b/devtools/client/react/src/index.ts @@ -0,0 +1 @@ +export { Panel } from "./panel"; diff --git a/devtools/client/src/panel/index.tsx b/devtools/client/react/src/panel/index.tsx similarity index 94% rename from devtools/client/src/panel/index.tsx rename to devtools/client/react/src/panel/index.tsx index 1e62ed8..5a46bdc 100644 --- a/devtools/client/src/panel/index.tsx +++ b/devtools/client/react/src/panel/index.tsx @@ -24,11 +24,33 @@ import { import { ThemeProvider, useDarkMode } from "@devtools-ds/themes"; -import { INITIAL_FLOW } from "../constants"; import { PLAYER_PLUGINS, PUBSUB_PLUGIN } from "../plugins"; import { useExtensionState } from "../state"; import { flowDiff } from "../helpers/flowDiff"; +/** Placeholder flow rendered until a real Player flow arrives. */ +const INITIAL_FLOW: Flow = { + id: "initial-flow", + views: [ + { + id: "view-1", + type: "text", + value: "connecting...", + }, + ], + navigation: { + BEGIN: "FLOW_1", + FLOW_1: { + startState: "VIEW_1", + VIEW_1: { + state_type: "VIEW", + ref: "view-1", + transitions: {}, + }, + }, + }, +}; + const fallbackRender: ErrorBoundary["props"]["fallbackRender"] = ({ error, }) => { diff --git a/devtools/client/src/plugins/index.ts b/devtools/client/react/src/plugins/index.ts similarity index 100% rename from devtools/client/src/plugins/index.ts rename to devtools/client/react/src/plugins/index.ts diff --git a/devtools/client/src/state/index.ts b/devtools/client/react/src/state/index.ts similarity index 93% rename from devtools/client/src/state/index.ts rename to devtools/client/react/src/state/index.ts index b2bcb00..ad40fb2 100644 --- a/devtools/client/src/state/index.ts +++ b/devtools/client/react/src/state/index.ts @@ -1,7 +1,7 @@ import type { CommunicationLayerMethods } from "@player-devtools/types"; import { useEffect, useMemo, useSyncExternalStore } from "react"; -import { createExtensionClient } from "./client"; +import { createExtensionClient } from "@player-devtools/client"; /** * Thin React adapter over `createExtensionClient`. diff --git a/devtools/client/src/constants/index.ts b/devtools/client/src/constants/index.ts deleted file mode 100644 index 02c6a7b..0000000 --- a/devtools/client/src/constants/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ExtensionState } from "@player-devtools/types"; -import type { Flow } from "@player-ui/player"; - -export const INITIAL_FLOW: Flow = { - id: "initial-flow", - views: [ - { - id: "view-1", - type: "text", - value: "connecting...", - }, - ], - navigation: { - BEGIN: "FLOW_1", - FLOW_1: { - startState: "VIEW_1", - VIEW_1: { - state_type: "VIEW", - ref: "view-1", - transitions: {}, - }, - }, - }, -}; - -export const INITIAL_EXTENSION_STATE: ExtensionState = { - current: { - player: null, - plugin: null, - }, - players: {}, -}; diff --git a/devtools/flipper-plugin/BUILD b/devtools/flipper-plugin/BUILD index a6d1957..82dc5c1 100644 --- a/devtools/flipper-plugin/BUILD +++ b/devtools/flipper-plugin/BUILD @@ -11,7 +11,7 @@ npm_link_all_packages(name = "node_modules") vitest_config(name = "vitest_config") deps = [ - ":node_modules/@player-devtools/client", + ":node_modules/@player-devtools/client-react", ":node_modules/@player-devtools/types", "//:node_modules/dlv", diff --git a/devtools/flipper-plugin/package.json b/devtools/flipper-plugin/package.json index f86f1fc..40205d9 100644 --- a/devtools/flipper-plugin/package.json +++ b/devtools/flipper-plugin/package.json @@ -6,7 +6,7 @@ "version": "0.0.0-PLACEHOLDER", "main": "dist/index.js", "dependencies": { - "@player-devtools/client": "workspace:*", + "@player-devtools/client-react": "workspace:*", "@player-devtools/types": "workspace:*" }, "flipperBundlerEntry": "src/index.tsx", diff --git a/devtools/flipper-plugin/src/index.tsx b/devtools/flipper-plugin/src/index.tsx index f8ea705..1ea454d 100644 --- a/devtools/flipper-plugin/src/index.tsx +++ b/devtools/flipper-plugin/src/index.tsx @@ -11,7 +11,7 @@ import type { MessengerEvent, TransactionMetadata, } from "@player-devtools/types"; -import { Panel } from "@player-devtools/client"; +import { Panel } from "@player-devtools/client-react"; import { Button, ChakraProvider, diff --git a/devtools/mcp/BUILD b/devtools/mcp/BUILD index 98e2cb5..ff22e07 100644 --- a/devtools/mcp/BUILD +++ b/devtools/mcp/BUILD @@ -12,11 +12,10 @@ vitest_config(name = "vitest_config") deps = [ ":node_modules/@player-devtools/client", + ":node_modules/@player-devtools/client-flipper", ":node_modules/@player-devtools/messenger", ":node_modules/@player-devtools/types", "//:node_modules/@modelcontextprotocol/sdk", - "//:node_modules/flipper-server", - "//:node_modules/flipper-server-client", "//:node_modules/zod", ] diff --git a/devtools/mcp/bin/run b/devtools/mcp/bin/run index cf74dd5..c36e51a 100644 --- a/devtools/mcp/bin/run +++ b/devtools/mcp/bin/run @@ -1,6 +1,9 @@ #!/usr/bin/env node -const { FlipperServerTransport, MCPServer } = require("@player-devtools/mcp"); +const { MCPServer } = require("@player-devtools/mcp"); +const { + FlipperServerTransport, +} = require("@player-devtools/client-flipper"); const transport = new FlipperServerTransport(); const server = new MCPServer(transport); diff --git a/devtools/mcp/package.json b/devtools/mcp/package.json index 8961d11..5fdf729 100644 --- a/devtools/mcp/package.json +++ b/devtools/mcp/package.json @@ -11,6 +11,7 @@ ], "dependencies": { "@player-devtools/client": "workspace:*", + "@player-devtools/client-flipper": "workspace:*", "@player-devtools/messenger": "workspace:*", "@player-devtools/types": "workspace:*" }, diff --git a/devtools/mcp/src/index.ts b/devtools/mcp/src/index.ts index 66684f8..34693d3 100644 --- a/devtools/mcp/src/index.ts +++ b/devtools/mcp/src/index.ts @@ -1,2 +1 @@ export { MCPServer } from "./server"; -export { type Transport, FlipperServerTransport } from "./transport"; diff --git a/devtools/mcp/src/server.ts b/devtools/mcp/src/server.ts index 05308f1..1782183 100644 --- a/devtools/mcp/src/server.ts +++ b/devtools/mcp/src/server.ts @@ -2,11 +2,11 @@ import { createExtensionClient, type ExtensionClient, } from "@player-devtools/client"; +import type { Transport } from "@player-devtools/types"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import type { Transport } from "./transport"; import { TOOL_DEFS, type ToolDef } from "./tools"; export class MCPServer { diff --git a/devtools/mcp/src/tools/__tests__/invokeAction.integration.test.ts b/devtools/mcp/src/tools/__tests__/invokeAction.integration.test.ts index 97e242e..76ead21 100644 --- a/devtools/mcp/src/tools/__tests__/invokeAction.integration.test.ts +++ b/devtools/mcp/src/tools/__tests__/invokeAction.integration.test.ts @@ -10,9 +10,7 @@ import type { } from "@player-devtools/types"; // NOTE: this test drives the devtools/MCP side with a real Messenger rather -// than `createExtensionClient`, because that factory currently lives in the -// `@player-devtools/client` barrel alongside the React `Panel`, which can't be -// imported in this node test env (it pulls in UI-only CJS deps). The MCP +// than `createExtensionClient` because it exercises the bus directly — the MCP // handler's playerId→target resolution is unit-tested in select.test.ts; what // THIS test proves is the other half: real DevtoolsPlugins on a shared bus only // handle actions addressed to their own playerID. The frame built below is diff --git a/devtools/types/core/src/index.ts b/devtools/types/core/src/index.ts index 7c28777..1f1fed5 100644 --- a/devtools/types/core/src/index.ts +++ b/devtools/types/core/src/index.ts @@ -249,6 +249,18 @@ export type CommunicationLayerMethods = Pick< "sendMessage" | "addListener" | "removeListener" >; +/** + * A communication layer with a managed lifecycle — implemented by each + * connection adapter (e.g. the Flipper transport) and consumed by clients + * that need to connect/tear down the underlying transport. + */ +export interface Transport extends CommunicationLayerMethods { + /** Connect to the underlying transport */ + connect(): Promise; + /** Tear down the underlying transport */ + close(): Promise; +} + /** Interface representing the Devtools Plugins Store. */ export interface DevtoolsPluginsStore { /** Plugins data. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2665546..66875b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -392,23 +392,44 @@ importers: specifier: ^3.0.0 version: 3.25.76 - devtools/client: + devtools/client/core: dependencies: '@player-devtools/messenger': specifier: workspace:* - version: link:../messenger/core + version: link:../../messenger/core '@player-devtools/types': specifier: workspace:* - version: link:../types/core + version: link:../../types/core '@player-devtools/utils': specifier: workspace:* - version: link:../utils/core + version: link:../../utils/core - devtools/flipper-plugin: + devtools/client/flipper: + dependencies: + '@player-devtools/types': + specifier: workspace:* + version: link:../../types/core + + devtools/client/react: dependencies: '@player-devtools/client': specifier: workspace:* - version: link:../client + version: link:../core + '@player-devtools/messenger': + specifier: workspace:* + version: link:../../messenger/core + '@player-devtools/types': + specifier: workspace:* + version: link:../../types/core + '@player-devtools/utils': + specifier: workspace:* + version: link:../../utils/core + + devtools/flipper-plugin: + dependencies: + '@player-devtools/client-react': + specifier: workspace:* + version: link:../client/react '@player-devtools/types': specifier: workspace:* version: link:../types/core @@ -417,7 +438,10 @@ importers: dependencies: '@player-devtools/client': specifier: workspace:* - version: link:../client + version: link:../client/core + '@player-devtools/client-flipper': + specifier: workspace:* + version: link:../client/flipper '@player-devtools/messenger': specifier: workspace:* version: link:../messenger/core diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2f082e9..afeeead 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,9 @@ packages: - cli - docs/* - - devtools/client + - devtools/client/core + - devtools/client/flipper + - devtools/client/react - devtools/flipper-plugin - devtools/mcp - devtools/plugin/core