Skip to content

Commit 9aabb7a

Browse files
authored
DevTools <-> VSCode integration (#1497)
1 parent 6a4bf24 commit 9aabb7a

15 files changed

+694
-158
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ build
55
*.zip
66
.DS_Store
77
development/server/db.json
8-
webpack-stats.json
8+
webpack-stats.json
9+
.yalc
10+
yalc.lock

.semgrepignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/extension/vscode/client.ts

README.vscode.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TODO

package-lock.json

Lines changed: 50 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
"web-ext-plugin": "2.9.0",
128128
"webextension-polyfill": "0.12.0",
129129
"webpack": "5.94.0",
130-
"webpack-cli": "5.1.4"
130+
"webpack-cli": "5.1.4",
131+
"ws": "^8.18.0"
131132
}
132133
}

package.vscode.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@apollo/client-devtools-vscode",
3+
"version": "4.18.4-pre.0",
4+
"repository": {
5+
"type": "git",
6+
"url": "git+https://github.com/apollographql/apollo-client-devtools.git"
7+
},
8+
"keywords": ["apollo", "apollo-client", "devtools", "vscode"],
9+
"type": "module",
10+
"exports": {
11+
".": "./vscode-client.js",
12+
"./vscode-client": "./vscode-client.js",
13+
"./vscode-server": "./vscode-server.js",
14+
"./panel": "./panel.js",
15+
"./package.json": "./package.json"
16+
},
17+
"main": "./vscode-client.js",
18+
"typings": "./vscode-client.d.ts",
19+
"author": "[email protected]",
20+
"license": "MIT",
21+
"files": [
22+
"vscode-client.js",
23+
"vscode-server.js",
24+
"panel.js",
25+
"*.d.ts",
26+
"package.json",
27+
"LICENSE.md",
28+
"README.md"
29+
],
30+
"peerDependencies": {
31+
"@apollo/client": "^3.4.0"
32+
},
33+
"dependencies": {
34+
"zen-observable": "^0.8.0"
35+
}
36+
}

src/extension/actor.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export interface Actor {
3838
name: TName,
3939
callback: Extract<ActorMessage, { type: TName }> extends infer Message
4040
? (message: Message) => void
41-
: never
41+
: never,
42+
options?: OptionsWithAbortSignal
4243
) => () => void;
4344
send: (message: ActorMessage) => void;
4445
}
@@ -77,7 +78,7 @@ export function createActor(adapter: MessageAdapter): Actor {
7778
}
7879
}
7980

80-
const on: Actor["on"] = (name, callback) => {
81+
const on: Actor["on"] = (name, callback, options = {}) => {
8182
let listeners = messageListeners.get(name) as Set<typeof callback>;
8283

8384
if (!listeners) {
@@ -88,7 +89,7 @@ export function createActor(adapter: MessageAdapter): Actor {
8889
listeners.add(callback);
8990
startListening();
9091

91-
return () => {
92+
const cleanup = () => {
9293
listeners!.delete(callback);
9394

9495
if (listeners.size === 0) {
@@ -99,6 +100,10 @@ export function createActor(adapter: MessageAdapter): Actor {
99100
stopListening();
100101
}
101102
};
103+
if (options.signal) {
104+
options.signal.addEventListener("abort", cleanup, { once: true });
105+
}
106+
return cleanup;
102107
};
103108

104109
return {
@@ -123,3 +128,7 @@ function isActorMessage(
123128
): message is ApolloClientDevtoolsActorMessage {
124129
return isDevtoolsMessage(message) && message.type === MessageType.Actor;
125130
}
131+
132+
export interface OptionsWithAbortSignal {
133+
signal?: AbortSignal;
134+
}

src/extension/rpc.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ export function createRpcHandler(adapter: MessageAdapter) {
127127
...params: Parameters<RPCRequest[TName]>
128128
) =>
129129
| NoInfer<Awaited<ReturnType<RPCRequest[TName]>>>
130-
| Promise<NoInfer<Awaited<ReturnType<RPCRequest[TName]>>>>
130+
| Promise<NoInfer<Awaited<ReturnType<RPCRequest[TName]>>>>,
131+
options: { signal?: AbortSignal } = {}
131132
) {
132133
if (listeners.has(name)) {
133134
throw new Error("Only one rpc handler can be registered per type");
@@ -159,13 +160,17 @@ export function createRpcHandler(adapter: MessageAdapter) {
159160

160161
startListening();
161162

162-
return () => {
163+
const cleanup = () => {
163164
listeners.delete(name);
164165

165166
if (listeners.size === 0) {
166167
stopListening();
167168
}
168169
};
170+
if (options.signal) {
171+
options.signal.addEventListener("abort", cleanup, { once: true });
172+
}
173+
return cleanup;
169174
};
170175
}
171176

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import type { Actor, OptionsWithAbortSignal } from "../actor";
2+
import type { QueryResult, SafeAny } from "../../types";
3+
import type { ApolloClient, ApolloError } from "@apollo/client";
4+
5+
// Note that we are intentionally not using Apollo Client's gql and
6+
// Observable exports, as we don't want Apollo Client and its dependencies
7+
// to be loaded into each browser tab, when this hook triggered.
8+
import Observable from "zen-observable";
9+
import type { DefinitionNode } from "graphql/language";
10+
import { getMainDefinition } from "./helpers";
11+
12+
type Writable<T> = { -readonly [P in keyof T]: T[P] };
13+
14+
export function handleExplorerRequests(
15+
actor: Actor,
16+
getClientById: (
17+
clientId: string
18+
) => Pick<ApolloClient<SafeAny>, "mutate" | "watchQuery"> | undefined,
19+
options: OptionsWithAbortSignal = {}
20+
) {
21+
return actor.on(
22+
"explorerRequest",
23+
(message) => {
24+
const {
25+
clientId,
26+
operation: queryAst,
27+
operationName,
28+
fetchPolicy,
29+
variables,
30+
} = message.payload;
31+
32+
const client = getClientById(clientId);
33+
34+
if (!client) {
35+
throw new Error("Could not find selected client");
36+
}
37+
38+
const clonedQueryAst = structuredClone(queryAst) as Writable<
39+
typeof queryAst
40+
>;
41+
42+
const filteredDefinitions = clonedQueryAst.definitions.reduce(
43+
(acumm: DefinitionNode[], curr) => {
44+
if (
45+
(curr.kind === "OperationDefinition" &&
46+
curr.name?.value === operationName) ||
47+
curr.kind !== "OperationDefinition"
48+
) {
49+
acumm.push(curr);
50+
}
51+
52+
return acumm;
53+
},
54+
[]
55+
);
56+
57+
clonedQueryAst.definitions = filteredDefinitions;
58+
59+
const definition = getMainDefinition(clonedQueryAst);
60+
61+
const operation = (() => {
62+
if (
63+
definition.kind === "OperationDefinition" &&
64+
definition.operation === "mutation"
65+
) {
66+
return new Observable<QueryResult>((observer) => {
67+
client
68+
.mutate({ mutation: clonedQueryAst, variables })
69+
.then((result) => {
70+
observer.next(result as QueryResult);
71+
});
72+
});
73+
} else {
74+
return client.watchQuery({
75+
query: clonedQueryAst,
76+
variables,
77+
fetchPolicy,
78+
});
79+
}
80+
})();
81+
82+
const operationObservable = operation?.subscribe(
83+
(response: QueryResult) => {
84+
actor.send({
85+
type: "explorerResponse",
86+
payload: { operationName, response },
87+
});
88+
},
89+
(error: ApolloError) => {
90+
actor.send({
91+
type: "explorerResponse",
92+
payload: {
93+
operationName,
94+
response: {
95+
errors: error.graphQLErrors.length
96+
? error.graphQLErrors
97+
: error.networkError && "result" in error.networkError
98+
? typeof error.networkError?.result === "string"
99+
? error.networkError?.result
100+
: error.networkError?.result.errors ?? []
101+
: [],
102+
error: error,
103+
data: null,
104+
loading: false,
105+
networkStatus: 8, // NetworkStatus.error - we want to prevent importing the enum here
106+
},
107+
},
108+
});
109+
}
110+
);
111+
112+
if (
113+
definition.kind === "OperationDefinition" &&
114+
definition.operation === "subscription"
115+
) {
116+
actor.on(
117+
"explorerSubscriptionTermination",
118+
() => {
119+
operationObservable?.unsubscribe();
120+
},
121+
options
122+
);
123+
if (options.signal) {
124+
options.signal.addEventListener(
125+
"abort",
126+
() => {
127+
operationObservable?.unsubscribe();
128+
},
129+
{ once: true }
130+
);
131+
}
132+
}
133+
},
134+
options
135+
);
136+
}

0 commit comments

Comments
 (0)