Skip to content

refactor(draft): replace <webview> with <iframe> #1191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions apps/studio/electron/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { APP_NAME, APP_SCHEMA, MainChannels } from '@onlook/models/constants';
import { BrowserWindow, app, shell } from 'electron';
import { BrowserWindow, app, protocol, shell } from 'electron';
import fixPath from 'fix-path';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import os from 'node:os';
import path from 'node:path';
Expand Down Expand Up @@ -93,6 +94,10 @@ const initMainWindow = () => {

const setupAppEventListeners = () => {
app.whenReady().then(() => {
protocol.handle('onlook', (request) => {
const filePath = path.join(__dirname, '../preload/webview.js');
return new Response(fs.readFileSync(filePath));
});
initMainWindow();
});

Expand Down Expand Up @@ -173,7 +178,24 @@ const setupAppEventListeners = () => {
};

// Main function
const main = async () => {
const main = () => {
setupEnvironment();
configurePlatformSpecifics();

// Register onlook protocol handler for browser-compatible preload script urls
protocol.registerSchemesAsPrivileged([
{
scheme: 'onlook',
privileges: {
standard: true,
secure: true,
allowServiceWorkers: true,
supportFetchAPI: true,
stream: true,
},
},
]);

if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
Expand Down
96 changes: 60 additions & 36 deletions apps/studio/electron/preload/webview/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { contextBridge } from 'electron';
import { processDom } from './dom';
import {
getChildrenCount,
Expand All @@ -24,44 +23,69 @@ import { editText, startEditingText, stopEditingText } from './elements/text';
import { setWebviewId } from './state';
import { getTheme, setTheme } from './theme';

export function setApi() {
contextBridge.exposeInMainWorld('api', {
// Misc
processDom,
getComputedStyleByDomId,
updateElementInstance,
setWebviewId,
getFirstOnlookElement,
const onlookApi = {
// Misc
processDom,
getComputedStyleByDomId,
updateElementInstance,
setWebviewId,
getFirstOnlookElement,

// Elements
getElementAtLoc,
getDomElementByDomId,
setElementType,
getElementType,
getParentElement,
getChildrenCount,
getOffsetParent,
// Elements
getElementAtLoc,
getDomElementByDomId,
setElementType,
getElementType,
getParentElement,
getChildrenCount,
getOffsetParent,

// Actions
getActionLocation,
getActionElementByDomId,
getInsertLocation,
getRemoveActionFromDomId,
// Actions
getActionLocation,
getActionElementByDomId,
getInsertLocation,
getRemoveActionFromDomId,

// Theme
getTheme,
setTheme,
// Theme
getTheme,
setTheme,

// Drag
startDrag,
drag,
endDrag,
getElementIndex,
endAllDrag,
// Drag
startDrag,
drag,
endDrag,
getElementIndex,
endAllDrag,

// Edit text
startEditingText,
editText,
stopEditingText,
});
// Edit text
startEditingText,
editText,
stopEditingText,
};

export type TOnlookWindow = typeof window & {
_onlookWebviewId: string;
onlook: {
api: typeof onlookApi;
bridge: {
send: (channel: string, data?: any, transfer?: Transferable[]) => void;
receive: (handler: (event: MessageEvent, data?: any) => void) => void;
};
};
};

export function setApi() {
(window as TOnlookWindow).onlook = {
api: onlookApi,
bridge: {
send: (channel: string, data?: any, transfer?: Transferable[]) => {
window.parent.postMessage({ type: channel, data }, '*', transfer);
},
receive: (handler: (event: MessageEvent, data?: any) => void) => {
window.addEventListener('message', (event) => {
handler(event, event.data);
});
},
},
};
Comment on lines +78 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Direct window object modification without Object.defineProperty may allow overwriting of onlook API. Consider using Object.defineProperty with configurable: false.

}
4 changes: 2 additions & 2 deletions apps/studio/electron/preload/webview/dom.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { EditorAttributes, WebviewChannels } from '@onlook/models/constants';
import type { LayerNode } from '@onlook/models/element';
import { ipcRenderer } from 'electron';
import { debounce } from './bundles/helpers';
import { getOrAssignDomId } from './ids';
import { getWebviewId } from './state';
import { isValidHtmlElement } from '/common/helpers';
import { getInstanceId, getOid } from '/common/helpers/ids';
import type { TOnlookWindow } from './api';

const processDebounced = debounce((root: HTMLElement) => {
const webviewId = getWebviewId();
Expand All @@ -30,7 +30,7 @@ const processDebounced = debounce((root: HTMLElement) => {
return false;
}

ipcRenderer.sendToHost(WebviewChannels.DOM_PROCESSED, {
(window as TOnlookWindow).onlook.bridge.send(WebviewChannels.DOM_PROCESSED, {
layerMap: Object.fromEntries(layerMap),
rootNode,
});
Expand Down
6 changes: 3 additions & 3 deletions apps/studio/electron/preload/webview/elements/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export function getStyles(element: HTMLElement): {
defined: Record<string, string>;
computed: Record<string, string>;
} {
const computed = getComputedStyle(element);
const computed = getElementComputedStyle(element);
const inline = getInlineStyles(element);
const stylesheet = getStylesheetStyles(element);

Expand All @@ -27,10 +27,10 @@ export function getComputedStyleByDomId(domId: string): Record<string, string> {
if (!element) {
return {};
}
return getComputedStyle(element as HTMLElement);
return getElementComputedStyle(element as HTMLElement);
}

function getComputedStyle(element: HTMLElement): Record<string, string> {
function getElementComputedStyle(element: HTMLElement): Record<string, string> {
const computedStyle = jsonClone(window.getComputedStyle(element)) as unknown as Record<
string,
string
Expand Down
4 changes: 2 additions & 2 deletions apps/studio/electron/preload/webview/events/dom.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EditorAttributes, WebviewChannels } from '@onlook/models/constants';
import type { LayerNode } from '@onlook/models/element';
import { ipcRenderer } from 'electron';
import { buildLayerTree } from '../dom';
import type { TOnlookWindow } from '../api';

export function listenForDomMutation() {
const targetNode = document.body;
Expand Down Expand Up @@ -46,7 +46,7 @@ export function listenForDomMutation() {
}

if (added.size > 0 || removed.size > 0) {
ipcRenderer.sendToHost(WebviewChannels.WINDOW_MUTATED, {
(window as TOnlookWindow).onlook.bridge.send(WebviewChannels.WINDOW_MUTATED, {
added: Object.fromEntries(added),
removed: Object.fromEntries(removed),
});
Comment on lines +49 to 52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Type assertion to TOnlookWindow could fail silently if window.onlook or bridge is undefined. Consider adding a null check or error handling.

Expand Down
28 changes: 15 additions & 13 deletions apps/studio/electron/preload/webview/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
} from '@onlook/models/actions';
import { WebviewChannels } from '@onlook/models/constants';
import type { StyleChange } from '@onlook/models/style';
import { ipcRenderer } from 'electron';
import type { TOnlookWindow } from '../api';
import { processDom } from '../dom';
import { groupElements, ungroupElements } from '../elements/dom/group';
import { insertImage, removeImage } from '../elements/dom/image';
Expand All @@ -30,17 +30,19 @@ import {
export function listenForEvents() {
listenForWindowEvents();
listenForDomMutation();
listenForEditEvents();

// TODO: Disabled for debugging. This is listening to all events instead of specific ones.
// listenForEditEvents();
}

function listenForWindowEvents() {
window.addEventListener('resize', () => {
ipcRenderer.sendToHost(WebviewChannels.WINDOW_RESIZED);
(window as TOnlookWindow).onlook.bridge.send(WebviewChannels.WINDOW_RESIZED);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Type casting window on every bridge access is repetitive and error-prone. Consider creating a helper function or storing the bridge reference.

});
}

function listenForEditEvents() {
ipcRenderer.on(WebviewChannels.UPDATE_STYLE, (_, data) => {
(window as TOnlookWindow).onlook.bridge.receive((_, data) => {
const { domId, change } = data as {
domId: string;
change: Change<Record<string, StyleChange>>;
Expand All @@ -50,7 +52,7 @@ function listenForEditEvents() {
publishStyleUpdate(domId);
});

ipcRenderer.on(WebviewChannels.INSERT_ELEMENT, (_, data) => {
(window as TOnlookWindow).onlook.bridge.receive((_, data) => {
const { element, location, editText } = data as {
element: ActionElement;
location: ActionLocation;
Expand All @@ -62,13 +64,13 @@ function listenForEditEvents() {
}
});

ipcRenderer.on(WebviewChannels.REMOVE_ELEMENT, (_, data) => {
(window as TOnlookWindow).onlook.bridge.receive((_, data) => {
const { location } = data as { location: ActionLocation };
removeElement(location);
publishRemoveElement(location);
});

ipcRenderer.on(WebviewChannels.MOVE_ELEMENT, (_, data) => {
(window as TOnlookWindow).onlook.bridge.receive((_, data) => {
const { domId, newIndex } = data as {
domId: string;
newIndex: number;
Expand All @@ -79,7 +81,7 @@ function listenForEditEvents() {
}
});

ipcRenderer.on(WebviewChannels.EDIT_ELEMENT_TEXT, (_, data) => {
(window as TOnlookWindow).onlook.bridge.receive((_, data) => {
const { domId, content } = data as {
domId: string;
content: string;
Expand All @@ -90,7 +92,7 @@ function listenForEditEvents() {
}
});

ipcRenderer.on(WebviewChannels.GROUP_ELEMENTS, (_, data) => {
(window as TOnlookWindow).onlook.bridge.receive((_, data) => {
const { parent, container, children } = data as {
parent: ActionTarget;
container: GroupContainer;
Expand All @@ -102,7 +104,7 @@ function listenForEditEvents() {
}
});

ipcRenderer.on(WebviewChannels.UNGROUP_ELEMENTS, (_, data) => {
(window as TOnlookWindow).onlook.bridge.receive((_, data) => {
const { parent, container, children } = data as {
parent: ActionTarget;
container: GroupContainer;
Expand All @@ -114,7 +116,7 @@ function listenForEditEvents() {
}
});

ipcRenderer.on(WebviewChannels.INSERT_IMAGE, (_, data) => {
(window as TOnlookWindow).onlook.bridge.receive((_, data) => {
const { domId, image } = data as {
domId: string;
image: ImageContentData;
Expand All @@ -123,15 +125,15 @@ function listenForEditEvents() {
publishStyleUpdate(domId);
});

ipcRenderer.on(WebviewChannels.REMOVE_IMAGE, (_, data) => {
(window as TOnlookWindow).onlook.bridge.receive((_, data) => {
const { domId } = data as {
domId: string;
};
removeImage(domId);
publishStyleUpdate(domId);
});

ipcRenderer.on(WebviewChannels.CLEAN_AFTER_WRITE_TO_CODE, () => {
(window as TOnlookWindow).onlook.bridge.receive(() => {
processDom();
});
Comment on lines +136 to 138
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: This receive handler has no channel identifier and no data type checking. Consider adding type safety for the message data.

Suggested change
(window as TOnlookWindow).onlook.bridge.receive(() => {
processDom();
});
(window as TOnlookWindow).onlook.bridge.receive(WebviewChannels.PROCESS_DOM, (_, data) => {
processDom();
});

}
Loading