@assistant-ui/vscode 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +159 -2
- package/dist/external-url.d.ts +3 -0
- package/dist/external-url.d.ts.map +1 -0
- package/dist/external-url.js +19 -0
- package/dist/external-url.js.map +1 -0
- package/dist/host/html.d.ts +65 -0
- package/dist/host/html.d.ts.map +1 -0
- package/dist/host/html.js +126 -0
- package/dist/host/html.js.map +1 -0
- package/dist/host/router.d.ts +24 -0
- package/dist/host/router.d.ts.map +1 -0
- package/dist/host/router.js +166 -0
- package/dist/host/router.js.map +1 -0
- package/dist/host/serve.d.ts +34 -0
- package/dist/host/serve.d.ts.map +1 -0
- package/dist/host/serve.js +93 -0
- package/dist/host/serve.js.map +1 -0
- package/dist/host.d.ts +5 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +4 -0
- package/dist/protocol.d.ts +46 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +30 -0
- package/dist/protocol.js.map +1 -0
- package/dist/route-url.d.ts +3 -0
- package/dist/route-url.d.ts.map +1 -0
- package/dist/route-url.js +7 -0
- package/dist/route-url.js.map +1 -0
- package/dist/webview/csp-nonce.d.ts +6 -0
- package/dist/webview/csp-nonce.d.ts.map +1 -0
- package/dist/webview/csp-nonce.js +12 -0
- package/dist/webview/csp-nonce.js.map +1 -0
- package/dist/webview/fetch.d.ts +14 -0
- package/dist/webview/fetch.d.ts.map +1 -0
- package/dist/webview/fetch.js +164 -0
- package/dist/webview/fetch.js.map +1 -0
- package/dist/webview/id.d.ts +2 -0
- package/dist/webview/id.d.ts.map +1 -0
- package/dist/webview/id.js +5 -0
- package/dist/webview/id.js.map +1 -0
- package/dist/webview/links.d.ts +14 -0
- package/dist/webview/links.d.ts.map +1 -0
- package/dist/webview/links.js +44 -0
- package/dist/webview/links.js.map +1 -0
- package/dist/webview/rpc.d.ts +3 -0
- package/dist/webview/rpc.d.ts.map +1 -0
- package/dist/webview/rpc.js +32 -0
- package/dist/webview/rpc.js.map +1 -0
- package/dist/webview/storage.d.ts +8 -0
- package/dist/webview/storage.d.ts.map +1 -0
- package/dist/webview/storage.js +24 -0
- package/dist/webview/storage.js.map +1 -0
- package/dist/webview/vscode-api.d.ts +11 -0
- package/dist/webview/vscode-api.d.ts.map +1 -0
- package/dist/webview/vscode-api.js +22 -0
- package/dist/webview/vscode-api.js.map +1 -0
- package/dist/webview.d.ts +7 -0
- package/dist/webview.d.ts.map +1 -0
- package/dist/webview.js +7 -0
- package/package.json +87 -3
- package/src/external-url.ts +19 -0
- package/src/host/html.ts +224 -0
- package/src/host/router.ts +253 -0
- package/src/host/serve.ts +150 -0
- package/src/host.ts +27 -0
- package/src/protocol.ts +131 -0
- package/src/route-url.ts +6 -0
- package/src/theme.css +327 -0
- package/src/webview/csp-nonce.ts +10 -0
- package/src/webview/fetch.ts +210 -0
- package/src/webview/id.ts +4 -0
- package/src/webview/links.ts +56 -0
- package/src/webview/rpc.ts +35 -0
- package/src/webview/storage.ts +24 -0
- package/src/webview/vscode-api.ts +31 -0
- package/src/webview.ts +14 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isWebviewToHostMessage,
|
|
3
|
+
VSCODE_BRIDGE_CHANNEL,
|
|
4
|
+
VSCODE_VIRTUAL_ORIGIN,
|
|
5
|
+
type FetchRequestMessage,
|
|
6
|
+
type HostToWebviewMessage,
|
|
7
|
+
} from "../protocol";
|
|
8
|
+
import { isForeignHttpUrl } from "../route-url";
|
|
9
|
+
|
|
10
|
+
export type RouteHandler = (request: Request) => Response | Promise<Response>;
|
|
11
|
+
|
|
12
|
+
export type HttpMethod =
|
|
13
|
+
| "GET"
|
|
14
|
+
| "HEAD"
|
|
15
|
+
| "POST"
|
|
16
|
+
| "PUT"
|
|
17
|
+
| "PATCH"
|
|
18
|
+
| "DELETE"
|
|
19
|
+
| "OPTIONS";
|
|
20
|
+
|
|
21
|
+
export type RouteHandlers = { [M in HttpMethod]?: RouteHandler };
|
|
22
|
+
|
|
23
|
+
export type WebviewRoutes = Record<string, RouteHandlers>;
|
|
24
|
+
|
|
25
|
+
export type WebviewLike = {
|
|
26
|
+
postMessage(message: any): PromiseLike<boolean>;
|
|
27
|
+
onDidReceiveMessage(listener: (message: any) => unknown): {
|
|
28
|
+
dispose(): unknown;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ServeWebviewRoutesOptions = {
|
|
33
|
+
/** Milliseconds to coalesce response chunks before posting them. */
|
|
34
|
+
flushInterval?: number;
|
|
35
|
+
/** Buffered byte count that posts a chunk without waiting. */
|
|
36
|
+
flushSize?: number;
|
|
37
|
+
onError?: (error: unknown) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type Disposable = { dispose(): void };
|
|
41
|
+
|
|
42
|
+
const BODYLESS_METHODS = new Set(["GET", "HEAD"]);
|
|
43
|
+
|
|
44
|
+
const concat = (parts: Uint8Array[], size: number) => {
|
|
45
|
+
const out = new Uint8Array(size);
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
out.set(part, offset);
|
|
49
|
+
offset += part.byteLength;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const errorMessage = (error: unknown) =>
|
|
55
|
+
error instanceof Error ? error.message : String(error);
|
|
56
|
+
|
|
57
|
+
export function serveRoutes(
|
|
58
|
+
webview: WebviewLike,
|
|
59
|
+
routes: WebviewRoutes,
|
|
60
|
+
{
|
|
61
|
+
flushInterval = 16,
|
|
62
|
+
flushSize = 64 * 1024,
|
|
63
|
+
onError = (error) => console.error(error),
|
|
64
|
+
}: ServeWebviewRoutesOptions = {},
|
|
65
|
+
): Disposable {
|
|
66
|
+
const inflight = new Map<string, AbortController>();
|
|
67
|
+
|
|
68
|
+
const post = (message: HostToWebviewMessage, controller: AbortController) => {
|
|
69
|
+
Promise.resolve(webview.postMessage(message)).then(
|
|
70
|
+
(delivered) => {
|
|
71
|
+
if (!delivered) controller.abort();
|
|
72
|
+
},
|
|
73
|
+
() => controller.abort(),
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const dispatch = async (
|
|
78
|
+
request: Request,
|
|
79
|
+
pathname: string,
|
|
80
|
+
): Promise<Response> => {
|
|
81
|
+
const route = Object.hasOwn(routes, pathname) ? routes[pathname] : null;
|
|
82
|
+
if (!route) return new Response("Not Found", { status: 404 });
|
|
83
|
+
const handler = Object.hasOwn(route, request.method)
|
|
84
|
+
? route[request.method as HttpMethod]
|
|
85
|
+
: null;
|
|
86
|
+
if (!handler) {
|
|
87
|
+
return new Response("Method Not Allowed", {
|
|
88
|
+
status: 405,
|
|
89
|
+
headers: { Allow: Object.keys(route).join(", ") },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
return await handler(request);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (request.signal.aborted) throw error;
|
|
96
|
+
onError(error);
|
|
97
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const pump = async (
|
|
102
|
+
id: string,
|
|
103
|
+
body: ReadableStream<Uint8Array>,
|
|
104
|
+
controller: AbortController,
|
|
105
|
+
) => {
|
|
106
|
+
const { signal } = controller;
|
|
107
|
+
const reader = body.getReader();
|
|
108
|
+
const onAbort = () => {
|
|
109
|
+
reader.cancel(signal.reason).catch(() => undefined);
|
|
110
|
+
};
|
|
111
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
112
|
+
|
|
113
|
+
let buffered: Uint8Array[] = [];
|
|
114
|
+
let size = 0;
|
|
115
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
116
|
+
const flush = () => {
|
|
117
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
118
|
+
timer = undefined;
|
|
119
|
+
if (size === 0 || signal.aborted) return;
|
|
120
|
+
const chunk = concat(buffered, size);
|
|
121
|
+
buffered = [];
|
|
122
|
+
size = 0;
|
|
123
|
+
post(
|
|
124
|
+
{ channel: VSCODE_BRIDGE_CHANNEL, kind: "fetch:chunk", id, chunk },
|
|
125
|
+
controller,
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
while (true) {
|
|
131
|
+
const { done, value } = await reader.read();
|
|
132
|
+
if (done || signal.aborted) break;
|
|
133
|
+
if (value.byteLength === 0) continue;
|
|
134
|
+
buffered.push(value);
|
|
135
|
+
size += value.byteLength;
|
|
136
|
+
if (size >= flushSize || flushInterval <= 0) flush();
|
|
137
|
+
else timer ??= setTimeout(flush, flushInterval);
|
|
138
|
+
}
|
|
139
|
+
flush();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
flush();
|
|
142
|
+
throw error;
|
|
143
|
+
} finally {
|
|
144
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
145
|
+
signal.removeEventListener("abort", onAbort);
|
|
146
|
+
reader.releaseLock();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const handle = async (message: FetchRequestMessage) => {
|
|
151
|
+
const { id } = message;
|
|
152
|
+
if (inflight.has(id)) {
|
|
153
|
+
onError(new Error(`Ignored a request reusing the in-flight id ${id}`));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const controller = new AbortController();
|
|
157
|
+
inflight.set(id, controller);
|
|
158
|
+
try {
|
|
159
|
+
const url = new URL(message.url);
|
|
160
|
+
if (isForeignHttpUrl(url)) {
|
|
161
|
+
throw new TypeError(`Refused a request for ${url.origin}`);
|
|
162
|
+
}
|
|
163
|
+
// The webview chooses this URL, so a route sees only its path and query.
|
|
164
|
+
const requestUrl = new URL(VSCODE_VIRTUAL_ORIGIN);
|
|
165
|
+
requestUrl.pathname = url.pathname;
|
|
166
|
+
requestUrl.search = url.search;
|
|
167
|
+
const request = new Request(requestUrl, {
|
|
168
|
+
method: message.method,
|
|
169
|
+
headers: message.headers,
|
|
170
|
+
body: BODYLESS_METHODS.has(message.method.toUpperCase())
|
|
171
|
+
? null
|
|
172
|
+
: message.body,
|
|
173
|
+
signal: controller.signal,
|
|
174
|
+
});
|
|
175
|
+
const response = await dispatch(request, url.pathname);
|
|
176
|
+
if (controller.signal.aborted) {
|
|
177
|
+
void response.body?.cancel().catch(() => undefined);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
post(
|
|
181
|
+
{
|
|
182
|
+
channel: VSCODE_BRIDGE_CHANNEL,
|
|
183
|
+
kind: "fetch:head",
|
|
184
|
+
id,
|
|
185
|
+
status: response.status,
|
|
186
|
+
statusText: response.statusText,
|
|
187
|
+
headers: [...response.headers],
|
|
188
|
+
},
|
|
189
|
+
controller,
|
|
190
|
+
);
|
|
191
|
+
if (request.method === "HEAD") {
|
|
192
|
+
void response.body?.cancel().catch(() => undefined);
|
|
193
|
+
} else if (response.body) {
|
|
194
|
+
await pump(id, response.body, controller);
|
|
195
|
+
}
|
|
196
|
+
if (!controller.signal.aborted) {
|
|
197
|
+
post(
|
|
198
|
+
{ channel: VSCODE_BRIDGE_CHANNEL, kind: "fetch:end", id },
|
|
199
|
+
controller,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (controller.signal.aborted) return;
|
|
204
|
+
onError(error);
|
|
205
|
+
post(
|
|
206
|
+
{
|
|
207
|
+
channel: VSCODE_BRIDGE_CHANNEL,
|
|
208
|
+
kind: "fetch:error",
|
|
209
|
+
id,
|
|
210
|
+
message: errorMessage(error),
|
|
211
|
+
},
|
|
212
|
+
controller,
|
|
213
|
+
);
|
|
214
|
+
} finally {
|
|
215
|
+
if (inflight.get(id) === controller) inflight.delete(id);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const subscription = webview.onDidReceiveMessage((message: unknown) => {
|
|
220
|
+
if (!isWebviewToHostMessage(message)) return;
|
|
221
|
+
if (message.kind === "fetch:request") void handle(message);
|
|
222
|
+
else if (message.kind === "fetch:reset") {
|
|
223
|
+
const controllers = [...inflight.values()];
|
|
224
|
+
inflight.clear();
|
|
225
|
+
for (const controller of controllers) controller.abort();
|
|
226
|
+
} else if (message.kind === "fetch:abort") {
|
|
227
|
+
const controller = inflight.get(message.id);
|
|
228
|
+
if (controller) {
|
|
229
|
+
inflight.delete(message.id);
|
|
230
|
+
controller.abort();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
dispose: () => {
|
|
237
|
+
subscription.dispose();
|
|
238
|
+
for (const [id, controller] of inflight) {
|
|
239
|
+
post(
|
|
240
|
+
{
|
|
241
|
+
channel: VSCODE_BRIDGE_CHANNEL,
|
|
242
|
+
kind: "fetch:error",
|
|
243
|
+
id,
|
|
244
|
+
message: "Webview routes disposed",
|
|
245
|
+
},
|
|
246
|
+
controller,
|
|
247
|
+
);
|
|
248
|
+
controller.abort();
|
|
249
|
+
}
|
|
250
|
+
inflight.clear();
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { DEFAULT_EXTERNAL_SCHEMES, parseExternalUrl } from "../external-url";
|
|
2
|
+
import {
|
|
3
|
+
isWebviewToHostMessage,
|
|
4
|
+
VSCODE_BRIDGE_CHANNEL,
|
|
5
|
+
type RpcRequestMessage,
|
|
6
|
+
type RpcResponseMessage,
|
|
7
|
+
} from "../protocol";
|
|
8
|
+
import {
|
|
9
|
+
serveRoutes,
|
|
10
|
+
type Disposable,
|
|
11
|
+
type ServeWebviewRoutesOptions,
|
|
12
|
+
type WebviewLike,
|
|
13
|
+
type WebviewRoutes,
|
|
14
|
+
} from "./router";
|
|
15
|
+
|
|
16
|
+
/** Matches `vscode.Memento`, whose `update(key, undefined)` removes the key. */
|
|
17
|
+
export type MementoLike = {
|
|
18
|
+
get(key: string): unknown;
|
|
19
|
+
update(key: string, value: unknown): PromiseLike<void>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ServeWebviewHostOptions = ServeWebviewRoutesOptions & {
|
|
23
|
+
routes?: WebviewRoutes;
|
|
24
|
+
/**
|
|
25
|
+
* Opens a URL forwarded by `installLinkInterceptor`, after the host has
|
|
26
|
+
* checked its scheme against `externalSchemes`.
|
|
27
|
+
*/
|
|
28
|
+
openExternal?: (url: string) => PromiseLike<boolean> | boolean | void;
|
|
29
|
+
/** URL schemes `openExternal` may receive, such as `"https:"`. */
|
|
30
|
+
externalSchemes?: readonly string[];
|
|
31
|
+
/** Backs `createVSCodeStorage`, such as `context.globalState`. */
|
|
32
|
+
storage?: MementoLike;
|
|
33
|
+
/** Namespaces the webview's keys inside `storage`. */
|
|
34
|
+
storagePrefix?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type RpcHandler = (...params: unknown[]) => unknown;
|
|
38
|
+
|
|
39
|
+
const errorMessage = (error: unknown) =>
|
|
40
|
+
error instanceof Error ? error.message : String(error);
|
|
41
|
+
|
|
42
|
+
const stringParam = (value: unknown, name: string): string => {
|
|
43
|
+
if (typeof value !== "string")
|
|
44
|
+
throw new TypeError(`${name} must be a string`);
|
|
45
|
+
return value;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Serves `vscodeFetch` routes and the webview's host calls, such as
|
|
50
|
+
* `installLinkInterceptor` opening links and `createVSCodeStorage`, for one
|
|
51
|
+
* webview. Host calls without a handler reject in the webview.
|
|
52
|
+
*/
|
|
53
|
+
export function serveWebviewHost(
|
|
54
|
+
webview: WebviewLike,
|
|
55
|
+
{
|
|
56
|
+
routes = {},
|
|
57
|
+
openExternal,
|
|
58
|
+
externalSchemes = DEFAULT_EXTERNAL_SCHEMES,
|
|
59
|
+
storage,
|
|
60
|
+
storagePrefix = "@assistant-ui/vscode:",
|
|
61
|
+
...routeOptions
|
|
62
|
+
}: ServeWebviewHostOptions = {},
|
|
63
|
+
): Disposable {
|
|
64
|
+
const handlers: Record<string, RpcHandler> = {};
|
|
65
|
+
|
|
66
|
+
if (openExternal) {
|
|
67
|
+
handlers.openExternal = async (value) => {
|
|
68
|
+
const url = parseExternalUrl(value, externalSchemes);
|
|
69
|
+
if (!url) throw new Error(`Refused to open ${String(value)}`);
|
|
70
|
+
return (await openExternal(url)) !== false;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (storage) {
|
|
75
|
+
const storageKey = (key: unknown) =>
|
|
76
|
+
storagePrefix + stringParam(key, "key");
|
|
77
|
+
handlers["storage.getItem"] = (key) => {
|
|
78
|
+
const value = storage.get(storageKey(key));
|
|
79
|
+
return typeof value === "string" ? value : null;
|
|
80
|
+
};
|
|
81
|
+
handlers["storage.setItem"] = async (key, value) => {
|
|
82
|
+
await storage.update(storageKey(key), stringParam(value, "value"));
|
|
83
|
+
};
|
|
84
|
+
handlers["storage.removeItem"] = async (key) => {
|
|
85
|
+
await storage.update(storageKey(key), undefined);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const respond = (
|
|
90
|
+
response:
|
|
91
|
+
| { id: string; ok: true; result: unknown }
|
|
92
|
+
| { id: string; ok: false; message: string },
|
|
93
|
+
) => {
|
|
94
|
+
const message: RpcResponseMessage = {
|
|
95
|
+
channel: VSCODE_BRIDGE_CHANNEL,
|
|
96
|
+
kind: "rpc:response",
|
|
97
|
+
...response,
|
|
98
|
+
};
|
|
99
|
+
Promise.resolve(webview.postMessage(message)).catch(() => undefined);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const pending = new Set<string>();
|
|
103
|
+
|
|
104
|
+
const handle = async ({ id, method, params }: RpcRequestMessage) => {
|
|
105
|
+
if (pending.has(id)) return;
|
|
106
|
+
pending.add(id);
|
|
107
|
+
try {
|
|
108
|
+
const handler = Object.hasOwn(handlers, method) ? handlers[method] : null;
|
|
109
|
+
if (!handler) throw new Error(`${method} is not served by this host`);
|
|
110
|
+
const result = await handler(...params);
|
|
111
|
+
if (pending.delete(id)) respond({ id, ok: true, result });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (pending.delete(id)) {
|
|
114
|
+
respond({ id, ok: false, message: errorMessage(error) });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const server = serveRoutes(webview, routes, routeOptions);
|
|
120
|
+
const subscription = webview.onDidReceiveMessage((message: unknown) => {
|
|
121
|
+
if (isWebviewToHostMessage(message) && message.kind === "rpc:request") {
|
|
122
|
+
void handle(message);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
dispose: () => {
|
|
128
|
+
subscription.dispose();
|
|
129
|
+
for (const id of pending) {
|
|
130
|
+
respond({ id, ok: false, message: "Webview host disposed" });
|
|
131
|
+
}
|
|
132
|
+
pending.clear();
|
|
133
|
+
server.dispose();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Serves `routes` to `createVSCodeFetch` / `vscodeFetch` calls made inside
|
|
140
|
+
* `webview`, streaming each response back as it is produced. Host calls such
|
|
141
|
+
* as `installLinkInterceptor` or `createVSCodeStorage` reject in the webview;
|
|
142
|
+
* use `serveWebviewHost` to serve them.
|
|
143
|
+
*/
|
|
144
|
+
export function serveWebviewRoutes(
|
|
145
|
+
webview: WebviewLike,
|
|
146
|
+
routes: WebviewRoutes,
|
|
147
|
+
options?: ServeWebviewRoutesOptions,
|
|
148
|
+
): Disposable {
|
|
149
|
+
return serveWebviewHost(webview, { ...options, routes });
|
|
150
|
+
}
|
package/src/host.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createCspNonce,
|
|
3
|
+
createWebviewCsp,
|
|
4
|
+
renderWebviewHtml,
|
|
5
|
+
type RenderWebviewHtmlOptions,
|
|
6
|
+
type WebviewCspMode,
|
|
7
|
+
type WebviewCspOptions,
|
|
8
|
+
type WebviewCspSource,
|
|
9
|
+
type WebviewResourceLike,
|
|
10
|
+
type WebviewSurface,
|
|
11
|
+
} from "./host/html";
|
|
12
|
+
export {
|
|
13
|
+
type Disposable,
|
|
14
|
+
type HttpMethod,
|
|
15
|
+
type RouteHandler,
|
|
16
|
+
type RouteHandlers,
|
|
17
|
+
type ServeWebviewRoutesOptions,
|
|
18
|
+
type WebviewLike,
|
|
19
|
+
type WebviewRoutes,
|
|
20
|
+
} from "./host/router";
|
|
21
|
+
export {
|
|
22
|
+
serveWebviewHost,
|
|
23
|
+
serveWebviewRoutes,
|
|
24
|
+
type MementoLike,
|
|
25
|
+
type ServeWebviewHostOptions,
|
|
26
|
+
} from "./host/serve";
|
|
27
|
+
export * from "./protocol";
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export const VSCODE_BRIDGE_CHANNEL = "aui-vscode";
|
|
2
|
+
|
|
3
|
+
export const VSCODE_VIRTUAL_ORIGIN = "https://webview.vscode.invalid";
|
|
4
|
+
|
|
5
|
+
export type BridgeHeaders = [name: string, value: string][];
|
|
6
|
+
|
|
7
|
+
type BridgeEnvelope<TKind extends string> = {
|
|
8
|
+
channel: typeof VSCODE_BRIDGE_CHANNEL;
|
|
9
|
+
kind: TKind;
|
|
10
|
+
id: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type FetchRequestMessage = BridgeEnvelope<"fetch:request"> & {
|
|
14
|
+
url: string;
|
|
15
|
+
method: string;
|
|
16
|
+
headers: BridgeHeaders;
|
|
17
|
+
body: Uint8Array<ArrayBuffer> | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type FetchAbortMessage = BridgeEnvelope<"fetch:abort">;
|
|
21
|
+
|
|
22
|
+
export type FetchResetMessage = BridgeEnvelope<"fetch:reset">;
|
|
23
|
+
|
|
24
|
+
export type FetchHeadMessage = BridgeEnvelope<"fetch:head"> & {
|
|
25
|
+
status: number;
|
|
26
|
+
statusText: string;
|
|
27
|
+
headers: BridgeHeaders;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type FetchChunkMessage = BridgeEnvelope<"fetch:chunk"> & {
|
|
31
|
+
chunk: Uint8Array<ArrayBuffer>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type FetchEndMessage = BridgeEnvelope<"fetch:end">;
|
|
35
|
+
|
|
36
|
+
export type FetchErrorMessage = BridgeEnvelope<"fetch:error"> & {
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type RpcRequestMessage = BridgeEnvelope<"rpc:request"> & {
|
|
41
|
+
method: string;
|
|
42
|
+
params: unknown[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type RpcResponseMessage = BridgeEnvelope<"rpc:response"> &
|
|
46
|
+
({ ok: true; result: unknown } | { ok: false; message: string });
|
|
47
|
+
|
|
48
|
+
export type WebviewToHostMessage =
|
|
49
|
+
| FetchRequestMessage
|
|
50
|
+
| FetchAbortMessage
|
|
51
|
+
| FetchResetMessage
|
|
52
|
+
| RpcRequestMessage;
|
|
53
|
+
|
|
54
|
+
export type HostToWebviewMessage =
|
|
55
|
+
| FetchHeadMessage
|
|
56
|
+
| FetchChunkMessage
|
|
57
|
+
| FetchEndMessage
|
|
58
|
+
| FetchErrorMessage
|
|
59
|
+
| RpcResponseMessage;
|
|
60
|
+
|
|
61
|
+
export type BridgeMessage = WebviewToHostMessage | HostToWebviewMessage;
|
|
62
|
+
|
|
63
|
+
type Fields = Record<string, unknown>;
|
|
64
|
+
|
|
65
|
+
const isString = (value: unknown): value is string => typeof value === "string";
|
|
66
|
+
|
|
67
|
+
const isBytes = (value: unknown) => value instanceof Uint8Array;
|
|
68
|
+
|
|
69
|
+
const isHeaders = (value: unknown) =>
|
|
70
|
+
Array.isArray(value) &&
|
|
71
|
+
value.every(
|
|
72
|
+
(entry) =>
|
|
73
|
+
Array.isArray(entry) &&
|
|
74
|
+
entry.length === 2 &&
|
|
75
|
+
isString(entry[0]) &&
|
|
76
|
+
isString(entry[1]),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const hasNoPayload = () => true;
|
|
80
|
+
|
|
81
|
+
const WEBVIEW_TO_HOST: Record<
|
|
82
|
+
WebviewToHostMessage["kind"],
|
|
83
|
+
(message: Fields) => boolean
|
|
84
|
+
> = {
|
|
85
|
+
"fetch:request": (m) =>
|
|
86
|
+
isString(m.url) &&
|
|
87
|
+
isString(m.method) &&
|
|
88
|
+
isHeaders(m.headers) &&
|
|
89
|
+
(m.body === null || isBytes(m.body)),
|
|
90
|
+
"fetch:abort": hasNoPayload,
|
|
91
|
+
"fetch:reset": hasNoPayload,
|
|
92
|
+
"rpc:request": (m) => isString(m.method) && Array.isArray(m.params),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const HOST_TO_WEBVIEW: Record<
|
|
96
|
+
HostToWebviewMessage["kind"],
|
|
97
|
+
(message: Fields) => boolean
|
|
98
|
+
> = {
|
|
99
|
+
"fetch:head": (m) =>
|
|
100
|
+
typeof m.status === "number" &&
|
|
101
|
+
isString(m.statusText) &&
|
|
102
|
+
isHeaders(m.headers),
|
|
103
|
+
"fetch:chunk": (m) => isBytes(m.chunk),
|
|
104
|
+
"fetch:end": hasNoPayload,
|
|
105
|
+
"fetch:error": (m) => isString(m.message),
|
|
106
|
+
"rpc:response": (m) =>
|
|
107
|
+
m.ok === true || (m.ok === false && isString(m.message)),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const matches = (
|
|
111
|
+
validators: Record<string, (message: Fields) => boolean>,
|
|
112
|
+
value: unknown,
|
|
113
|
+
) => {
|
|
114
|
+
if (typeof value !== "object" || value === null) return false;
|
|
115
|
+
const message = value as Fields;
|
|
116
|
+
return (
|
|
117
|
+
message.channel === VSCODE_BRIDGE_CHANNEL &&
|
|
118
|
+
isString(message.id) &&
|
|
119
|
+
isString(message.kind) &&
|
|
120
|
+
Object.hasOwn(validators, message.kind) &&
|
|
121
|
+
validators[message.kind]!(message)
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const isWebviewToHostMessage = (
|
|
126
|
+
value: unknown,
|
|
127
|
+
): value is WebviewToHostMessage => matches(WEBVIEW_TO_HOST, value);
|
|
128
|
+
|
|
129
|
+
export const isHostToWebviewMessage = (
|
|
130
|
+
value: unknown,
|
|
131
|
+
): value is HostToWebviewMessage => matches(HOST_TO_WEBVIEW, value);
|
package/src/route-url.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { VSCODE_VIRTUAL_ORIGIN } from "./protocol";
|
|
2
|
+
|
|
3
|
+
/** Routes are served on the virtual origin, so an `http:` or `https:` URL on any other origin reaches none of them. */
|
|
4
|
+
export const isForeignHttpUrl = (url: URL) =>
|
|
5
|
+
(url.protocol === "http:" || url.protocol === "https:") &&
|
|
6
|
+
url.origin !== VSCODE_VIRTUAL_ORIGIN;
|