From 1e30f9c9a92eac08fb4214c90ffffd47c0ea4009 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 28 Aug 2024 18:09:00 +0200 Subject: [PATCH 01/27] start adding devtools --- package.json | 5 ++ src/devtools/DevToolsViewProvider.ts | 112 +++++++++++++++++++++++++++ src/extension.ts | 9 +++ 3 files changed, 126 insertions(+) create mode 100644 src/devtools/DevToolsViewProvider.ts diff --git a/package.json b/package.json index 82ccd307d..462c0e950 100644 --- a/package.json +++ b/package.json @@ -246,6 +246,11 @@ "command": "apollographql/showStats", "title": "Show Status", "category": "Apollo" + }, + { + "command": "apollographql/showDevTools", + "title": "Show Apollo Client DevTools", + "category": "Apollo" } ] }, diff --git a/src/devtools/DevToolsViewProvider.ts b/src/devtools/DevToolsViewProvider.ts new file mode 100644 index 000000000..e81fb2dbc --- /dev/null +++ b/src/devtools/DevToolsViewProvider.ts @@ -0,0 +1,112 @@ +import { Debug } from "src/debug"; +import * as vscode from "vscode"; + +export class DevToolsViewProvider { + public static readonly viewType = "apollo.client.devTools"; + + private _view?: vscode.WebviewView; + + constructor(private readonly _extensionUri: vscode.Uri) {} + + public static show(extensionUri: vscode.Uri) { + const column = vscode.window.activeTextEditor + ? vscode.window.activeTextEditor.viewColumn + : undefined; + + const panel = vscode.window.createWebviewPanel( + DevToolsViewProvider.viewType, + "Apollo Client DevTools", + column || vscode.ViewColumn.One, + { + enableScripts: true, + localResourceRoots: [extensionUri], + }, + ); + + panel.webview.html = this._getHtmlForWebview(panel.webview, extensionUri); + + panel.webview.onDidReceiveMessage((data) => { + console.log(data); + }); + + panel.webview + .postMessage({ + id: 123, + source: "apollo-client-devtools", + type: "actor", + message: { type: "initializePanel" }, + }) + .then((x) => Debug.info("delivered: " + x)); + } + + // public addColor() { + // if (this._view) { + // this._view.show?.(true); // `show` is not implemented in 1.49 but is for 1.50 insiders + // this._view.webview.postMessage({ type: "addColor" }); + // } + // } + + private static _getHtmlForWebview( + webview: vscode.Webview, + extensionUri: vscode.Uri, + ) { + // Get the local path to main script run in the webview, then convert it to a uri we can use in the webview. + const scriptUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, "lib", "devtool-build", "panel.js"), + ); + Debug.info( + vscode.Uri.joinPath( + extensionUri, + "lib", + "devtool-build", + "panel.js", + ).toString(), + ); + Debug.info(scriptUri.toString()); + + // Use a nonce to only allow a specific script to be run. + const nonce = getNonce(); + + return ` + + + + + + + + + + +
+ + + +`; + } +} + +function getNonce() { + let text = ""; + const possible = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} diff --git a/src/extension.ts b/src/extension.ts index 725241375..50f87b344 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,6 +27,7 @@ import { printStatsToClientOutputChannel, } from "./utils"; import { Debug } from "./debug"; +import { DevToolsViewProvider } from "./devtools/DevToolsViewProvider"; const { version } = require("../package.json"); @@ -329,6 +330,14 @@ export async function activate( }, }); + const provider = new DevToolsViewProvider(context.extensionUri); + + context.subscriptions.push( + commands.registerCommand("apollographql/showDevTools", () => { + DevToolsViewProvider.show(context.extensionUri); + }), + ); + await client.start(); return { outputChannel, From 30e5b0270c11c0801b72adbde3ee47d41dc63551 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 29 Aug 2024 18:39:29 +0200 Subject: [PATCH 02/27] first working version --- devtool-build | 1 + package-lock.json | 3 ++- package.json | 11 +++++++++ src/devtools/DevToolsViewProvider.ts | 19 +++++++++++++-- src/devtools/server.ts | 36 ++++++++++++++++++++++++++++ src/extension.ts | 18 ++++++++++++-- start-ac.mjs | 27 +++++++++++++++++++++ 7 files changed, 110 insertions(+), 5 deletions(-) create mode 120000 devtool-build create mode 100644 src/devtools/server.ts create mode 100644 start-ac.mjs diff --git a/devtool-build b/devtool-build new file mode 120000 index 000000000..89409ec21 --- /dev/null +++ b/devtool-build @@ -0,0 +1 @@ +/Users/tronic/tmp/apollo-client-devtools/build \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2683d764d..d7b0442f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "vscode-languageserver-textdocument": "1.0.12", "vscode-uri": "3.0.8", "which": "4.0.0", + "ws": "^8.18.0", "zod": "3.23.8", "zod-validation-error": "3.3.1" }, @@ -14562,7 +14563,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 462c0e950..bbf21ab81 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "vscode-languageserver-textdocument": "1.0.12", "vscode-uri": "3.0.8", "which": "4.0.0", + "ws": "8.18.0", "zod": "3.23.8", "zod-validation-error": "3.3.1" }, @@ -251,6 +252,16 @@ "command": "apollographql/showDevTools", "title": "Show Apollo Client DevTools", "category": "Apollo" + }, + { + "command": "apollographql/startDevToolsServer", + "title": "Start Apollo Client DevTools Server", + "category": "Apollo" + }, + { + "command": "apollographql/stopDevToolsServer", + "title": "Stop Apollo Client DevTools Server", + "category": "Apollo" } ] }, diff --git a/src/devtools/DevToolsViewProvider.ts b/src/devtools/DevToolsViewProvider.ts index e81fb2dbc..138ce7040 100644 --- a/src/devtools/DevToolsViewProvider.ts +++ b/src/devtools/DevToolsViewProvider.ts @@ -1,5 +1,6 @@ import { Debug } from "src/debug"; import * as vscode from "vscode"; +import { devtoolsEvents } from "./server"; export class DevToolsViewProvider { public static readonly viewType = "apollo.client.devTools"; @@ -26,7 +27,10 @@ export class DevToolsViewProvider { panel.webview.html = this._getHtmlForWebview(panel.webview, extensionUri); panel.webview.onDidReceiveMessage((data) => { - console.log(data); + devtoolsEvents.emit("fromDevTools", data); + }); + devtoolsEvents.addListener("toDevTools", (data) => { + panel.webview.postMessage(data); }); panel.webview @@ -52,7 +56,7 @@ export class DevToolsViewProvider { ) { // Get the local path to main script run in the webview, then convert it to a uri we can use in the webview. const scriptUri = webview.asWebviewUri( - vscode.Uri.joinPath(extensionUri, "lib", "devtool-build", "panel.js"), + vscode.Uri.joinPath(extensionUri, "devtool-build", "panel.js"), ); Debug.info( vscode.Uri.joinPath( @@ -94,6 +98,17 @@ export class DevToolsViewProvider {
+ diff --git a/src/devtools/server.ts b/src/devtools/server.ts new file mode 100644 index 000000000..ae0b98caf --- /dev/null +++ b/src/devtools/server.ts @@ -0,0 +1,36 @@ +import { WebSocketServer } from "ws"; +import { Disposable } from "vscode"; +import { runServer } from "../../devtool-build/vscode-server"; +import { Debug } from "../debug"; +import { EventEmitter } from "node:events"; + +export const devtoolsEvents = new EventEmitter<{ + toDevTools: [unknown]; + fromDevTools: [unknown]; +}>(); +devtoolsEvents.addListener("toDevTools", (msg) => { + Debug.info("WS > DevTools: " + JSON.stringify(msg)); +}); +devtoolsEvents.addListener("fromDevTools", (msg) => { + Debug.info("DevTools > WS: " + JSON.stringify(msg)); +}); + +export function startServer(port: number): Disposable { + let wss: WebSocketServer | null = new WebSocketServer({ port }); + runServer(wss, { + addListener: (listener) => { + devtoolsEvents.addListener("fromDevTools", listener); + return () => { + devtoolsEvents.removeListener("fromDevTools", listener); + }; + }, + postMessage: (message) => { + devtoolsEvents.emit("toDevTools", message); + }, + }); + + return new Disposable(() => { + wss?.close(); + wss = null; + }); +} diff --git a/src/extension.ts b/src/extension.ts index 50f87b344..8d0129382 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,6 +28,7 @@ import { } from "./utils"; import { Debug } from "./debug"; import { DevToolsViewProvider } from "./devtools/DevToolsViewProvider"; +import { startServer } from "./devtools/server"; const { version } = require("../package.json"); @@ -330,13 +331,26 @@ export async function activate( }, }); - const provider = new DevToolsViewProvider(context.extensionUri); - context.subscriptions.push( commands.registerCommand("apollographql/showDevTools", () => { + commands.executeCommand("apollographql/startDevToolsServer"); DevToolsViewProvider.show(context.extensionUri); }), ); + let devtoolServer: Disposable | null = null; + context.subscriptions.push( + commands.registerCommand("apollographql/startDevToolsServer", () => { + if (!devtoolServer) { + context.subscriptions.push((devtoolServer = startServer(8090))); + } + }), + ); + context.subscriptions.push( + commands.registerCommand("apollographql/stopDevToolsServer", () => { + devtoolServer?.dispose(); + devtoolServer = null; + }), + ); await client.start(); return { diff --git a/start-ac.mjs b/start-ac.mjs new file mode 100644 index 000000000..4272d3b69 --- /dev/null +++ b/start-ac.mjs @@ -0,0 +1,27 @@ +import { registerClient } from "./devtool-build/vscode-client.js"; +import WebSocket from "ws"; +import { ApolloClient, InMemoryCache } from "@apollo/client/core/index.js"; +import { MockLink } from "@apollo/client/testing/core/index.js"; +import gql from "graphql-tag"; + +globalThis.WebSocket ||= WebSocket; + +const helloWorld = gql` + query { + hello + } +`; + +const link = new MockLink([ + { + request: { query: helloWorld }, + result: { data: { hello: "world" } }, + }, +]); +const client = new ApolloClient({ + link, + cache: new InMemoryCache(), +}); +client.watchQuery({ query: helloWorld }).subscribe({ next() {} }); + +registerClient(client, "ws://localhost:8090"); From 41541e146872a7741c6e86dd41310a62101eddb6 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 30 Aug 2024 12:26:46 +0200 Subject: [PATCH 03/27] set `retainContextWhenHidden` --- src/devtools/DevToolsViewProvider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/devtools/DevToolsViewProvider.ts b/src/devtools/DevToolsViewProvider.ts index 138ce7040..3e8a59800 100644 --- a/src/devtools/DevToolsViewProvider.ts +++ b/src/devtools/DevToolsViewProvider.ts @@ -21,6 +21,7 @@ export class DevToolsViewProvider { { enableScripts: true, localResourceRoots: [extensionUri], + retainContextWhenHidden: true, }, ); From f32b841af29bf3c86ba80c8832eb48f8c578a044 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 30 Aug 2024 12:54:46 +0200 Subject: [PATCH 04/27] testing harness --- start-ac.mjs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) mode change 100644 => 100755 start-ac.mjs diff --git a/start-ac.mjs b/start-ac.mjs old mode 100644 new mode 100755 index 4272d3b69..51a64eb8f --- a/start-ac.mjs +++ b/start-ac.mjs @@ -1,3 +1,8 @@ +#!/usr/bin/env node + +// for testing, start a few of these, e.g. with +// while true; do echo "foo\nbar\nbaz" | parallel ./start-ac.mjs; sleep 1; done + import { registerClient } from "./devtool-build/vscode-client.js"; import WebSocket from "ws"; import { ApolloClient, InMemoryCache } from "@apollo/client/core/index.js"; @@ -16,12 +21,24 @@ const link = new MockLink([ { request: { query: helloWorld }, result: { data: { hello: "world" } }, + maxUsageCount: 1000, + }, + { + request: { + query: gql` + query { + hi + } + `, + }, + result: { data: { hi: "universe" } }, + maxUsageCount: 1000, }, ]); const client = new ApolloClient({ link, cache: new InMemoryCache(), + devtools: { name: process.argv[2] }, }); client.watchQuery({ query: helloWorld }).subscribe({ next() {} }); - registerClient(client, "ws://localhost:8090"); From a76879b2d5ea904ddc75580de6daba18da49145a Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 2 Sep 2024 13:42:15 +0200 Subject: [PATCH 05/27] show in panel instead we currently have no good way of handling multiple webviews targeting different clients at once --- package.json | 26 +++++++++--- resources/apollo.svg | 16 +++++++ src/devtools/DevToolsViewProvider.ts | 62 ++++++++++++---------------- src/extension.ts | 12 ++++-- 4 files changed, 70 insertions(+), 46 deletions(-) create mode 100644 resources/apollo.svg diff --git a/package.json b/package.json index bbf21ab81..11fa08084 100644 --- a/package.json +++ b/package.json @@ -248,11 +248,6 @@ "title": "Show Status", "category": "Apollo" }, - { - "command": "apollographql/showDevTools", - "title": "Show Apollo Client DevTools", - "category": "Apollo" - }, { "command": "apollographql/startDevToolsServer", "title": "Start Apollo Client DevTools Server", @@ -263,7 +258,26 @@ "title": "Stop Apollo Client DevTools Server", "category": "Apollo" } - ] + ], + "viewsContainers": { + "panel": [ + { + "id": "client-devtools", + "title": "Apollo Client DevTools", + "icon": "resources/apollo.svg" + } + ] + }, + "views": { + "client-devtools": [ + { + "type": "webview", + "id": "vscode-apollo-client-devtools", + "name": "Apollo Client DevTools", + "icon": "resources/apollo.svg" + } + ] + } }, "galleryBanner": { "color": "#1d127d", diff --git a/resources/apollo.svg b/resources/apollo.svg new file mode 100644 index 000000000..fa39c2b84 --- /dev/null +++ b/resources/apollo.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/src/devtools/DevToolsViewProvider.ts b/src/devtools/DevToolsViewProvider.ts index 3e8a59800..95433796a 100644 --- a/src/devtools/DevToolsViewProvider.ts +++ b/src/devtools/DevToolsViewProvider.ts @@ -2,31 +2,25 @@ import { Debug } from "src/debug"; import * as vscode from "vscode"; import { devtoolsEvents } from "./server"; -export class DevToolsViewProvider { - public static readonly viewType = "apollo.client.devTools"; - - private _view?: vscode.WebviewView; +export class DevToolsViewProvider implements vscode.WebviewViewProvider { + public static readonly viewType = "vscode-apollo-client-devtools"; constructor(private readonly _extensionUri: vscode.Uri) {} - - public static show(extensionUri: vscode.Uri) { - const column = vscode.window.activeTextEditor - ? vscode.window.activeTextEditor.viewColumn - : undefined; - - const panel = vscode.window.createWebviewPanel( - DevToolsViewProvider.viewType, - "Apollo Client DevTools", - column || vscode.ViewColumn.One, - { - enableScripts: true, - localResourceRoots: [extensionUri], - retainContextWhenHidden: true, - }, + async resolveWebviewView( + panel: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + token: vscode.CancellationToken, + ): Promise { + vscode.commands.executeCommand("apollographql/startDevToolsServer"); + panel.webview.options = { + enableScripts: true, + localResourceRoots: [this._extensionUri], + }; + panel.webview.html = DevToolsViewProvider._getHtmlForWebview( + panel.webview, + this._extensionUri, ); - panel.webview.html = this._getHtmlForWebview(panel.webview, extensionUri); - panel.webview.onDidReceiveMessage((data) => { devtoolsEvents.emit("fromDevTools", data); }); @@ -34,23 +28,19 @@ export class DevToolsViewProvider { panel.webview.postMessage(data); }); - panel.webview - .postMessage({ - id: 123, - source: "apollo-client-devtools", - type: "actor", - message: { type: "initializePanel" }, - }) - .then((x) => Debug.info("delivered: " + x)); + const delivered = await panel.webview.postMessage({ + id: 123, + source: "apollo-client-devtools", + type: "actor", + message: { type: "initializePanel" }, + }); + if (!delivered) { + Debug.error( + "Failed to deliver initialization message to Apollo Client DevTools", + ); + } } - // public addColor() { - // if (this._view) { - // this._view.show?.(true); // `show` is not implemented in 1.49 but is for 1.50 insiders - // this._view.webview.postMessage({ type: "addColor" }); - // } - // } - private static _getHtmlForWebview( webview: vscode.Webview, extensionUri: vscode.Uri, diff --git a/src/extension.ts b/src/extension.ts index 8d0129382..0b6e9ccdd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -331,11 +331,15 @@ export async function activate( }, }); + const provider = new DevToolsViewProvider(context.extensionUri); context.subscriptions.push( - commands.registerCommand("apollographql/showDevTools", () => { - commands.executeCommand("apollographql/startDevToolsServer"); - DevToolsViewProvider.show(context.extensionUri); - }), + window.registerWebviewViewProvider( + DevToolsViewProvider.viewType, + provider, + { + webviewOptions: { retainContextWhenHidden: true }, + }, + ), ); let devtoolServer: Disposable | null = null; context.subscriptions.push( From 1f14d26f0ca02863306d328c139974b2af83e333 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 2 Sep 2024 13:52:52 +0200 Subject: [PATCH 06/27] add CSP --- src/devtools/DevToolsViewProvider.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/devtools/DevToolsViewProvider.ts b/src/devtools/DevToolsViewProvider.ts index 95433796a..bf75a17c4 100644 --- a/src/devtools/DevToolsViewProvider.ts +++ b/src/devtools/DevToolsViewProvider.ts @@ -67,7 +67,22 @@ export class DevToolsViewProvider implements vscode.WebviewViewProvider { - + + + + + + Apollo Client DevTools