chore: add preload module for Webviews

part of https://github.com/containers/podman-desktop/issues/5140
Signed-off-by: Florent Benoit <fbenoit@redhat.com>
This commit is contained in:
Florent Benoit 2024-01-18 18:49:34 +01:00 committed by Florent BENOIT
parent 9c1aea6817
commit ac6daec4a3
10 changed files with 595 additions and 4 deletions

View file

@ -57,6 +57,7 @@
"ignorePatterns": [
"packages/preload/exposedInMainWorld.d.ts",
"packages/preload-docker-extension/exposedInDockerExtension.d.ts",
"packages/preload-webview/exposedInWebview.d.ts",
"node_modules/**",
"**/dist/**"
],

View file

@ -21,7 +21,7 @@
]
},
"scripts": {
"build": "npm run build:main && npm run build:preload && npm run build:preload-docker-extension && npm run build:renderer && npm run build:extensions",
"build": "npm run build:main && npm run build:preload && npm run build:preload-docker-extension && npm run build:preload-webview && npm run build:renderer && npm run build:extensions",
"build:main": "cd ./packages/main && vite build",
"build:extensions": "npm run build:extensions:compose && npm run build:extensions:docker && npm run build:extensions:lima && npm run build:extensions:podman && npm run build:extensions:kubecontext && npm run build:extensions:kind && npm run build:extensions:registries && npm run build:extensions:kubectl-cli",
"build:extensions:compose": "cd ./extensions/compose && npm run build",
@ -35,14 +35,15 @@
"build:extension-api": "cd ./packages/extension-api && vite build",
"build:preload": "cd ./packages/preload && vite build",
"build:preload-docker-extension": "cd ./packages/preload-docker-extension && vite build",
"build:preload:types": "dts-cb -i \"packages/preload/tsconfig.json\" -o \"packages/preload/exposedInMainWorld.d.ts\" && dts-cb -i \"packages/preload-docker-extension/tsconfig.json\" -o \"packages/preload-docker-extension/exposedInDockerExtension.d.ts\"",
"build:preload-webview": "cd ./packages/preload-webview && vite build",
"build:preload:types": "dts-cb -i \"packages/preload/tsconfig.json\" -o \"packages/preload/exposedInMainWorld.d.ts\" && dts-cb -i \"packages/preload-docker-extension/tsconfig.json\" -o \"packages/preload-docker-extension/exposedInDockerExtension.d.ts\" && dts-cb -i \"packages/preload-webview/tsconfig.json\" -o \"packages/preload-webview/exposedInWebview.d.ts\"",
"build:renderer": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite -c packages/renderer/vite.config.js build",
"compile": "cross-env MODE=production npm run build && electron-builder build --config .electron-builder.config.cjs --dir --config.asar=false",
"compile:next": "cross-env MODE=production npm run build && electron-builder build --publish always --config .electron-builder.config.cjs",
"compile:pull-request": "cross-env MODE=production npm run build && electron-builder build --publish never --config .electron-builder.config.cjs",
"compile:current": "cross-env MODE=production npm run build && electron-builder build --config .electron-builder.config.cjs",
"test": "npm run test:unit && npm run test:e2e",
"test:unit": "npm run test:main && npm run test:preload && npm run test:preload-docker-extension && npm run test:renderer && npm run test:tools && npm run test:extensions",
"test:unit": "npm run test:main && npm run test:preload && npm run test:preload-docker-extension && npm run test:preload-webview && npm run test:renderer && npm run test:tools && npm run test:extensions",
"test:e2e": "npm run test:e2e:build && npm run test:e2e:run",
"test:e2e:build": "cross-env NODE_ENV=development MODE=development DEBUG=pw:browser npm run build",
"test:e2e:run": "xvfb-maybe --auto-servernum --server-args='-screen 0 1280x960x24' -- vitest run tests/src/ --pool=threads --poolOptions.threads.singleThread --no-file-parallelism",
@ -53,6 +54,7 @@
"test:main": "vitest run -r packages/main --passWithNoTests --coverage",
"test:preload": "vitest run -r packages/preload --passWithNoTests --coverage",
"test:preload-docker-extension": "vitest run -r packages/preload-docker-extension --passWithNoTests --coverage",
"test:preload-webview": "vitest run -r packages/preload-webview --coverage",
"test:extensions": "vitest run -r extensions --passWithNoTests --coverage && npm run test:extensions:compose && npm run test:extensions:kind && npm run test:extensions:docker && npm run test:extensions:lima && npm run test:extensions:kube && npm run test:extensions:podman && npm run test:extensions:registries && npm run test:extensions:kubectl-cli",
"test:extensions:kind": "vitest run -r extensions/kind --passWithNoTests --coverage ",
"test:extensions:compose": "vitest run -r extensions/compose --passWithNoTests --coverage",
@ -77,6 +79,7 @@
"typecheck:main": "tsc --noEmit -p packages/main/tsconfig.json",
"typecheck:preload": "tsc --noEmit -p packages/preload/tsconfig.json",
"typecheck:preload-dd-extension": "tsc --noEmit -p packages/preload-docker-extension/tsconfig.json",
"typecheck:preload-webview": "tsc --noEmit -p packages/preload-webview/tsconfig.json",
"typecheck:renderer": "npm run build:preload:types && tsc --noEmit -p packages/renderer/tsconfig.json",
"typecheck:extensions": "npm run typecheck:extensions:compose && npm run typecheck:extensions:kind && npm run typecheck:extensions:docker && npm run typecheck:extensions:lima && npm run typecheck:extensions:kube-context && npm run typecheck:extensions:podman && npm run typecheck:extensions:registries && npm run typecheck:extensions:kubectl-cli",
"typecheck:extensions:kind": "tsc --noEmit --project extensions/kind",
@ -87,7 +90,7 @@
"typecheck:extensions:podman": "tsc --noEmit --project extensions/podman",
"typecheck:extensions:registries": "tsc --noEmit --project extensions/registries",
"typecheck:extensions:kubectl-cli": "tsc --noEmit --project extensions/kubectl-cli",
"typecheck": "npm run typecheck:main && npm run typecheck:preload && npm run typecheck:renderer && npm run typecheck:preload-dd-extension && npm run typecheck:extensions",
"typecheck": "npm run typecheck:main && npm run typecheck:preload && npm run typecheck:renderer && npm run typecheck:preload-dd-extension && npm run typecheck:preload-webview && npm run typecheck:extensions",
"website:build": "cd website && yarn run docusaurus build",
"website:prod": "cd website && yarn run docusaurus build && yarn serve",
"website:dev": "cd website && yarn run docusaurus start",

1
packages/preload-webview/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
exposedInWebview.d.ts

View file

@ -0,0 +1,57 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { beforeEach, expect, test, vi } from 'vitest';
import { init } from '.';
import * as webviewPreload from './webview-preload';
vi.mock('./webview-preload', async () => {
return {
WebviewPreload: vi.fn().mockImplementation(() => {
return {
init: vi.fn().mockResolvedValue(undefined),
};
}),
};
});
beforeEach(() => {
vi.clearAllMocks();
});
test('check call constructor with correct web id by parsing window.location.search', () => {
(window as any).location = {
search: '?webviewId=123',
};
init();
// expect constructor has been called with the correct webviewID
expect(webviewPreload.WebviewPreload).toHaveBeenCalledWith('123');
});
test('check error if invalid window.location.search', () => {
(window as any).location = {
search: '',
};
// expect failure as webviewId is not defined
expect(() => init()).toThrow('The webviewId is not defined');
});

View file

@ -0,0 +1,41 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import { WebviewPreload } from './webview-preload';
/**
* @module preload
*/
export const init = () => {
// parse the query string and grab the webviewId parameter
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const webviewId = urlParams.get('webviewId') ?? undefined;
if (!webviewId) {
throw new Error('The webviewId is not defined');
}
// create the webviewPreload object and call the init method
const webviewPreload = new WebviewPreload(webviewId);
webviewPreload.init().catch((error: unknown) => console.error('Error while initializing the exposure', error));
};
// do not call init methd in case of testing
if (!process.env.VITEST) {
init();
}

View file

@ -0,0 +1,217 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { MockInstance } from 'vitest';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { WebviewPreload } from './webview-preload';
import type { WebviewInfo } from '../../main/src/plugin/api/webview-info';
import type { IpcRendererEvent } from 'electron';
import { ipcRenderer, contextBridge } from 'electron';
let webviewPreload: TestWebwiewPreload;
class TestWebwiewPreload extends WebviewPreload {
async getWebviews(): Promise<WebviewInfo[]> {
return super.getWebviews();
}
buildApi(): unknown {
return super.buildApi();
}
ipcRendererOn(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) {
super.ipcRendererOn(channel, listener);
}
async ipcInvoke(channel: string, ...args: unknown[]): Promise<unknown> {
return super.ipcInvoke(channel, ...args);
}
changeContent() {
super.changeContent();
}
postWebviewMessage(message: unknown) {
super.postWebviewMessage(message);
}
}
const webviewInfo: WebviewInfo = {
id: '123',
viewType: 'test',
sourcePath: 'testPath',
icon: 'testIcon',
name: 'test',
html: '<html>hello world</html>',
uuid: '12-12-12-12',
state: { foo: 'bar' },
};
vi.mock('electron', async () => {
return {
contextBridge: {
exposeInMainWorld: vi.fn(),
},
ipcRenderer: {
on: vi.fn(),
emit: vi.fn(),
handle: vi.fn(),
invoke: vi.fn(),
},
ipcMain: {
on: vi.fn(),
emit: vi.fn(),
handle: vi.fn(),
},
};
});
let spyIpcRendererOn: MockInstance<
[channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void],
void
>;
let spyBuildApi: MockInstance<[], unknown>;
beforeEach(() => {
vi.resetAllMocks();
webviewPreload = new TestWebwiewPreload('123');
// mock the window object
(window as any).addEventListener = vi.fn();
// override the getWebviews method
const spyGetWebviews = vi.spyOn(webviewPreload, 'getWebviews');
spyGetWebviews.mockResolvedValue([webviewInfo]);
// override buildApi method
spyBuildApi = vi.spyOn(webviewPreload, 'buildApi');
spyBuildApi.mockReturnValue(() => {});
// override ipcRendererOn
spyIpcRendererOn = vi.spyOn(webviewPreload, 'ipcRendererOn');
spyIpcRendererOn.mockImplementation(() => {});
});
test('check init method', async () => {
await webviewPreload.init();
// check it adds addEventListener to the window object
expect(window.addEventListener).toHaveBeenCalledWith('DOMContentLoaded', expect.any(Function));
// check exposure of the function to javascript
expect(vi.mocked(contextBridge.exposeInMainWorld)).toHaveBeenCalledWith(
'acquirePodmanDesktopApi',
expect.any(Function),
);
// check we register 2 event listener on ipcRenderer
expect(spyIpcRendererOn).toHaveBeenCalledWith('webview-post-message', expect.any(Function));
expect(spyIpcRendererOn).toHaveBeenCalledWith('webview-update-html', expect.any(Function));
});
describe('ipcInvoke', () => {
test('check custom ipcInvoke method', async () => {
// override the ipcRenderer.invoke method
const spyIpcRendererInvoke = vi.spyOn(ipcRenderer, 'invoke');
const fakeResult = 'foo';
// fake remote implementation sending no error and foo as result
spyIpcRendererInvoke.mockImplementation(() => Promise.resolve({ result: fakeResult, error: undefined }));
const result = await webviewPreload.ipcInvoke('test', 'arg1');
expect(result).toStrictEqual(fakeResult);
expect(spyIpcRendererInvoke).toHaveBeenCalledWith('test', 'arg1');
});
test('check custom ipcInvoke method with error', async () => {
// override the ipcRenderer.invoke method
const spyIpcRendererInvoke = vi.spyOn(ipcRenderer, 'invoke');
const fakeError = new Error('dummy error');
// fake remote implementation sending no error and foo as result
spyIpcRendererInvoke.mockImplementation(() => Promise.resolve({ result: undefined, error: fakeError }));
await expect(webviewPreload.ipcInvoke('test', 'arg1')).rejects.toThrow('dummy error');
expect(spyIpcRendererInvoke).toHaveBeenCalledWith('test', 'arg1');
});
});
test('check changeContent', async () => {
// spy document.write method
const spyDocumentWrite = vi.spyOn(document, 'write');
// override window.addEventListener to keep the callback
const spyAddEventListener = vi.spyOn(window, 'addEventListener');
// call changeContent it should not do anything as we're missing all conditions
webviewPreload.changeContent();
// check document.write method has not been called
expect(spyDocumentWrite).not.toHaveBeenCalled();
// call init to set the webviewInfo
await webviewPreload.init();
const callback: any = spyAddEventListener.mock.calls[0][1];
// call the callback that should call changeContent as we'll have two mandatory fields
callback();
// wait timeout execute
await new Promise(resolve => setTimeout(resolve, 100));
// check the document.write method has been called
expect(spyDocumentWrite).toHaveBeenCalledWith(`<!DOCTYPE html>
<html style="font-family: Montserrat;"><head></head><body>hello world</body></html>`);
});
test('check buildApi', async () => {
// spy postWebviewMessage
const spyPostWebviewMessage = vi.spyOn(webviewPreload, 'postWebviewMessage');
// spy ipcInvoke
const spyIpcInvoke = vi.spyOn(webviewPreload, 'ipcInvoke');
spyIpcInvoke.mockImplementation(() => Promise.resolve({ result: undefined, error: undefined }));
// remove spy on buildApi
spyBuildApi.mockRestore();
// init to set the webviewInfo
await webviewPreload.init();
const buildFunction: any = webviewPreload.buildApi();
// call the build function
const podmanDesktopApi = buildFunction();
// check the podmanDesktopApi object is returned
//get state that should be the one from the webviewInfo
expect(podmanDesktopApi.getState()).toStrictEqual(webviewInfo.state);
//post message
podmanDesktopApi.postMessage('test');
expect(spyPostWebviewMessage).toHaveBeenCalledWith({ command: 'onmessage', data: 'test' });
// clear calls on spyIpcInvoke
spyIpcInvoke.mockClear();
//set state
const newFakeState = { updated: 'state' };
podmanDesktopApi.setState(newFakeState);
// check ipcInvoke has been called
expect(spyIpcInvoke).toHaveBeenCalledWith('webviewRegistry:update-state', webviewInfo.id, newFakeState);
});

View file

@ -0,0 +1,147 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import type { WebviewInfo } from '../../main/src/plugin/api/webview-info';
import type { IpcRendererEvent } from 'electron';
import { contextBridge, ipcRenderer } from 'electron';
interface ErrorMessage {
name: string;
message: string;
extra: unknown;
}
export class WebviewPreload {
#webviewId: string;
#webviewInfo: WebviewInfo | undefined;
#domLoaded: boolean = false;
#acquiredApi: boolean = false;
constructor(webviewId: string) {
this.#webviewId = webviewId;
}
protected decodeError(error: ErrorMessage) {
const e = new Error(error.message);
e.name = error.name;
Object.assign(e, error.extra);
return e;
}
protected async ipcInvoke(channel: string, ...args: unknown[]): Promise<unknown> {
const { error, result } = await ipcRenderer.invoke(channel, ...args);
if (error) {
throw this.decodeError(error);
}
return result;
}
protected postWebviewMessage(message: unknown) {
this.ipcInvoke('webviewRegistry:post-message', this.#webviewInfo?.id, message).catch((error: unknown) =>
console.error('Error while posting message', error),
);
}
protected changeContent() {
if (!this.#webviewInfo) {
return;
}
if (!this.#domLoaded) {
return;
}
let webviewHtmlContent = '';
if (this.#webviewInfo) {
webviewHtmlContent = this.#webviewInfo.html;
}
// use a timeout to perform the update
setTimeout(() => {
const webviewContentHtml = new DOMParser().parseFromString(webviewHtmlContent, 'text/html');
webviewContentHtml.documentElement.style.setProperty('font-family', 'Montserrat');
const htmlContent = '<!DOCTYPE html>\n' + webviewContentHtml.documentElement.outerHTML;
document.open();
document.write(htmlContent);
document.close();
}, 0);
}
// build the function that will be exposed to the webview for getState/postMessage/setState
protected buildApi(): unknown {
return () => {
// initialize the state from the webview
let state: unknown = this.#webviewInfo?.state ?? {};
if (this.#acquiredApi) {
throw new Error('An instance of the Podman Desktop API has already been acquired');
}
// can only be called once;
this.#acquiredApi = true;
return Object.freeze({
getState: () => {
return state;
},
postMessage: (msg: unknown) => {
return this.postWebviewMessage({ command: 'onmessage', data: msg });
},
setState: async (newState: unknown) => {
state = newState;
// need to send back the state to the main process
this.ipcInvoke('webviewRegistry:update-state', this.#webviewInfo?.id, newState).catch((error: unknown) => {
console.error('Error while updating the state', error);
});
},
});
};
}
protected getWebviews(): Promise<WebviewInfo[]> {
return this.ipcInvoke('webviewRegistry:listWebviews') as Promise<WebviewInfo[]>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected ipcRendererOn(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.on(channel, listener);
}
async init(): Promise<void> {
window.addEventListener('DOMContentLoaded', () => {
this.#domLoaded = true;
this.changeContent();
});
contextBridge.exposeInMainWorld('acquirePodmanDesktopApi', this.buildApi());
const webviews: WebviewInfo[] = await this.getWebviews();
this.#webviewInfo = webviews.find(webview => webview.id === this.#webviewId);
this.changeContent();
// broadcast messages from the main process to the webview
this.ipcRendererOn('webview-post-message', (_, target: { message: unknown }) => {
window.dispatchEvent(new MessageEvent('message', { data: target.message }));
});
this.ipcRendererOn('webview-update-html', (_, html) => {
if (this.#webviewInfo) {
this.#webviewInfo.html = html;
this.changeContent();
}
});
}
}

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"sourceMap": false,
"moduleResolution": "Node",
"skipLibCheck": true,
"strict": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"types": ["node"],
"baseUrl": ".",
"paths": {
"/@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "exposedInWebview.d.ts", "../../types/**/*.d.ts"]
}

View file

@ -0,0 +1,65 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import { chrome } from '../../.electron-vendors.cache.json';
import { join } from 'path';
import { builtinModules } from 'module';
import { coverageConfig } from '../../vitest-shared-extensions.config';
const PACKAGE_ROOT = __dirname;
const PACKAGE_NAME = 'preload-webview';
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
const config = {
mode: process.env.MODE,
root: PACKAGE_ROOT,
envDir: process.cwd(),
resolve: {
alias: {
'/@/': join(PACKAGE_ROOT, 'src') + '/',
},
},
build: {
sourcemap: 'inline',
target: `chrome${chrome}`,
outDir: 'dist',
assetsDir: '.',
minify: process.env.MODE !== 'development',
lib: {
entry: 'src/index.ts',
formats: ['cjs'],
},
rollupOptions: {
external: ['electron', ...builtinModules.flatMap(p => [p, `node:${p}`])],
output: {
entryFileNames: '[name].cjs',
},
},
emptyOutDir: true,
reportCompressedSize: false,
},
test: {
environment: 'jsdom',
...coverageConfig(PACKAGE_ROOT, PACKAGE_NAME),
},
};
export default config;

View file

@ -1,5 +1,23 @@
#!/usr/bin/env node
/**********************************************************************
* Copyright (C) 2022-2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import { createServer, build, createLogger } from 'vite';
import electronPath from 'electron';
import { spawn } from 'child_process';
@ -141,6 +159,26 @@ const setupPreloadDockerExtensionPackageWatcher = ({ ws }) =>
},
});
const setupPreloadWebviewPackageWatcher = ({ ws }) =>
getWatcher({
name: 'reload-page-on-preload-webview-package-change',
configFile: 'packages/preload-webview/vite.config.js',
writeBundle() {
// Generating exposedInWebview.d.ts when preload package is changed.
generateAsync({
input: 'packages/preload-webview/tsconfig.json',
output: 'packages/preload-webview/exposedInWebview.d.ts',
});
if (ws) {
ws.send({
type: 'full-reload',
});
}
},
});
/**
* Start or restart App when source files are changed
* @param {{ws: import('vite').WebSocketServer}} WebSocketServer
@ -192,6 +230,7 @@ const setupExtensionApiWatcher = name => {
}
await setupPreloadPackageWatcher(viteDevServer);
await setupPreloadDockerExtensionPackageWatcher(viteDevServer);
await setupPreloadWebviewPackageWatcher(viteDevServer);
await setupMainPackageWatcher(viteDevServer);
} catch (e) {
console.error(e);