Skip to content
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

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

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 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
21 changes: 20 additions & 1 deletion apps/studio/electron/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { APP_NAME, APP_SCHEMA } from '@onlook/models/constants';
import { BrowserWindow, app, shell } from 'electron';
import { BrowserWindow, app, shell, protocol } from 'electron';
import fixPath from 'fix-path';
import { createRequire } from 'node:module';
import os from 'node:os';
Expand All @@ -11,6 +11,7 @@ import { listenForIpcMessages, removeIpcListeners } from './events';
import run from './run';
import terminal from './run/terminal';
import { updater } from './update';
import fs from 'node:fs';

// Help main inherit $PATH defined in dotfiles (.bashrc/.bash_profile/.zshrc/etc).
fixPath();
Expand Down Expand Up @@ -142,6 +143,10 @@ const listenForExitEvents = () => {

const setupAppEventListeners = () => {
app.whenReady().then(() => {
protocol.handle('onlook', (request) => {
const filePath = path.join(__dirname, '../preload/webview.js');
return new Response(fs.readFileSync(filePath));
});
Comment on lines +146 to +149
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Reading file synchronously could block the main process. Consider using async fs.promises.readFile instead

Suggested change
protocol.handle('onlook', (request) => {
const filePath = path.join(__dirname, '../preload/webview.js');
return new Response(fs.readFileSync(filePath));
});
protocol.handle('onlook', async (request) => {
const filePath = path.join(__dirname, '../preload/webview.js');
const content = await fs.promises.readFile(filePath);
return new Response(content);
});

listenForExitEvents();
initMainWindow();
});
Expand Down Expand Up @@ -190,6 +195,20 @@ 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,
},
Comment on lines +202 to +208
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Enabling all these protocol privileges may expose security risks. Consider limiting to only required privileges

},
]);

if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
Expand Down
100 changes: 62 additions & 38 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 @@ -22,42 +21,67 @@ import { editText, startEditingText, stopEditingText } from './elements/text';
import { setWebviewId } from './state';
import { getTheme, setTheme } from './theme';

const onlookApi = {
// Misc
processDom,
getComputedStyleByDomId,
updateElementInstance,
setWebviewId,

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

// Actions
getActionLocation,
getActionElementByDomId,
getInsertLocation,
getRemoveActionFromDomId,

// Theme
getTheme,
setTheme,

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

// 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() {
contextBridge.exposeInMainWorld('api', {
// Misc
processDom,
getComputedStyleByDomId,
updateElementInstance,
setWebviewId,

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

// Actions
getActionLocation,
getActionElementByDomId,
getInsertLocation,
getRemoveActionFromDomId,

// Theme
getTheme,
setTheme,

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

// Edit text
startEditingText,
editText,
stopEditingText,
});
(window as TOnlookWindow).onlook = {
api: onlookApi,
bridge: {
send: (channel: string, data?: any, transfer?: Transferable[]) => {
window.parent.postMessage({ type: channel, data }, '*', transfer);
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Using '*' as targetOrigin in postMessage is a security risk. Should restrict to specific origins.

},
receive: (handler: (event: MessageEvent, data?: any) => void) => {
window.addEventListener('message', (event) => {
handler(event, event.data);
});
Comment on lines +81 to +83
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Missing origin validation in message event listener. Should verify event.origin matches expected domains.

Suggested change
window.addEventListener('message', (event) => {
handler(event, event.data);
});
window.addEventListener('message', (event) => {
if (event.origin === window.location.origin) {
handler(event, event.data);
}
});

},
},
};
Comment on lines +74 to +86
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
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
24 changes: 12 additions & 12 deletions apps/studio/electron/preload/webview/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
ImageContentData,
} from '@onlook/models/actions';
import { WebviewChannels } from '@onlook/models/constants';
import { ipcRenderer } from 'electron';
import { processDom } from '../dom';
import { groupElements, ungroupElements } from '../elements/dom/group';
import { insertImage, removeImage } from '../elements/dom/image';
Expand All @@ -25,6 +24,7 @@ import {
publishStyleUpdate,
publishUngroupElement,
} from './publish';
import type { TOnlookWindow } from '../api';

export function listenForEvents() {
listenForWindowEvents();
Expand All @@ -34,12 +34,12 @@ export function listenForEvents() {

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, string>>;
Expand All @@ -48,7 +48,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 @@ -60,13 +60,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 @@ -77,7 +77,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 @@ -88,7 +88,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 @@ -100,7 +100,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 @@ -112,7 +112,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 @@ -121,15 +121,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 +132 to 134
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();
});

}
35 changes: 27 additions & 8 deletions apps/studio/electron/preload/webview/events/publish.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { ActionLocation } from '@onlook/models/actions';
import { WebviewChannels } from '@onlook/models/constants';
import type { DomElement } from '@onlook/models/element';
import { ipcRenderer } from 'electron';
import { buildLayerTree } from '../dom';
import { getDomElementByDomId } from '../elements';
import { getDomElement } from '../elements/helpers';
import { elementFromDomId } from '/common/helpers';
import type { TOnlookWindow } from '../api';

export function publishStyleUpdate(domId: string) {
const domEl = getDomElementByDomId(domId, true);
if (!domEl) {
console.error('No domEl found for style update event');
return;
}
ipcRenderer.sendToHost(WebviewChannels.STYLE_UPDATED, { domEl });
(window as TOnlookWindow).onlook.bridge.send(WebviewChannels.STYLE_UPDATED, { domEl });
}

export function publishInsertElement(
Expand All @@ -27,7 +27,11 @@ export function publishInsertElement(
console.error('No domEl or layerMap found for insert element event');
return;
}
ipcRenderer.sendToHost(WebviewChannels.ELEMENT_INSERTED, { domEl, layerMap, editText });
(window as TOnlookWindow).onlook.bridge.send(WebviewChannels.ELEMENT_INSERTED, {
domEl,
layerMap,
editText,
});
}

export function publishRemoveElement(location: ActionLocation) {
Expand All @@ -39,7 +43,10 @@ export function publishRemoveElement(location: ActionLocation) {
console.error('No parentDomEl or layerMap found for remove element event');
return;
}
ipcRenderer.sendToHost(WebviewChannels.ELEMENT_REMOVED, { parentDomEl, layerMap });
(window as TOnlookWindow).onlook.bridge.send(WebviewChannels.ELEMENT_REMOVED, {
parentDomEl,
layerMap,
});
}

export function publishMoveElement(domEl: DomElement) {
Expand All @@ -50,7 +57,10 @@ export function publishMoveElement(domEl: DomElement) {
console.error('No domEl or layerMap found for move element event');
return;
}
ipcRenderer.sendToHost(WebviewChannels.ELEMENT_MOVED, { domEl, layerMap });
(window as TOnlookWindow).onlook.bridge.send(WebviewChannels.ELEMENT_MOVED, {
domEl,
layerMap,
});
}

export function publishGroupElement(domEl: DomElement) {
Expand All @@ -61,7 +71,10 @@ export function publishGroupElement(domEl: DomElement) {
console.error('No domEl or layerMap found for group element event');
return;
}
ipcRenderer.sendToHost(WebviewChannels.ELEMENT_GROUPED, { domEl, layerMap });
(window as TOnlookWindow).onlook.bridge.send(WebviewChannels.ELEMENT_GROUPED, {
domEl,
layerMap,
});
}

export function publishUngroupElement(parentEl: DomElement) {
Expand All @@ -72,7 +85,10 @@ export function publishUngroupElement(parentEl: DomElement) {
console.error('No parentEl or layerMap found for ungroup element event');
return;
}
ipcRenderer.sendToHost(WebviewChannels.ELEMENT_UNGROUPED, { parentEl, layerMap });
(window as TOnlookWindow).onlook.bridge.send(WebviewChannels.ELEMENT_UNGROUPED, {
parentEl,
layerMap,
});
}

export function publishEditText(domEl: DomElement) {
Expand All @@ -83,5 +99,8 @@ export function publishEditText(domEl: DomElement) {
console.error('No domEl or layerMap found for edit text event');
return;
}
ipcRenderer.sendToHost(WebviewChannels.ELEMENT_TEXT_EDITED, { domEl, layerMap });
(window as TOnlookWindow).onlook.bridge.send(WebviewChannels.ELEMENT_TEXT_EDITED, {
domEl,
layerMap,
});
}
Loading