From efb20a69c42fd6f7d41d2bcdfedeba9ead4d680c Mon Sep 17 00:00:00 2001 From: George He Date: Sat, 2 Dec 2023 16:46:21 +0800 Subject: [PATCH] feat: enable islated utility process [INS-3378] --- .../insomnia-smoke-test/playwright/test.ts | 28 +- .../tests/smoke/utility-process.test.ts | 283 ++++++++++++++++++ packages/insomnia/.gitignore | 2 + packages/insomnia/esbuild.main.ts | 22 +- packages/insomnia/package.json | 2 +- .../insomnia/src/main/ipc/message-channel.ts | 10 + packages/insomnia/src/main/window-utils.ts | 55 +++- packages/insomnia/src/preload.ts | 12 +- .../src/renderers/utility-process/index.html | 16 + .../src/renderers/utility-process/index.ts | 49 +++ .../renderers/utility-process/inso-object.ts | 190 ++++++++++++ .../src/renderers/utility-process/preload.ts | 7 + .../utility-process/shared-modules.ts | 0 .../utility-process/static-modules.ts | 82 +++++ packages/insomnia/src/ui/index.tsx | 4 + .../src/ui/window-message-handlers.ts | 89 ++++++ 16 files changed, 840 insertions(+), 11 deletions(-) create mode 100644 packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts create mode 100644 packages/insomnia/src/main/ipc/message-channel.ts create mode 100644 packages/insomnia/src/renderers/utility-process/index.html create mode 100644 packages/insomnia/src/renderers/utility-process/index.ts create mode 100644 packages/insomnia/src/renderers/utility-process/inso-object.ts create mode 100644 packages/insomnia/src/renderers/utility-process/preload.ts create mode 100644 packages/insomnia/src/renderers/utility-process/shared-modules.ts create mode 100644 packages/insomnia/src/renderers/utility-process/static-modules.ts create mode 100644 packages/insomnia/src/ui/window-message-handlers.ts diff --git a/packages/insomnia-smoke-test/playwright/test.ts b/packages/insomnia-smoke-test/playwright/test.ts index 7bf1870f5e..c46ddaa7b5 100644 --- a/packages/insomnia-smoke-test/playwright/test.ts +++ b/packages/insomnia-smoke-test/playwright/test.ts @@ -1,5 +1,5 @@ // Read more about creating fixtures https://playwright.dev/docs/test-fixtures -import { ElectronApplication, test as baseTest, TraceMode } from '@playwright/test'; +import { ElectronApplication, expect, Page, test as baseTest, TraceMode } from '@playwright/test'; import path from 'path'; import { @@ -97,11 +97,31 @@ export const test = baseTest.extend<{ await electronApp.close(); }, page: async ({ app }, use) => { - const page = await app.firstWindow(); + // getMainPage waits main renderer loaded + const getMainPage = async () => { + for (let i = 0; i < 100; i++) { + const wins = app.windows(); - await page.waitForLoadState(); + for (const page of wins) { + const title = await page.title(); + if (title === 'Insomnia') { + return page; + } + } - await use(page); + await new Promise(resolve => setTimeout(resolve, 500)); + } + + return undefined; + }; + + const page = await getMainPage(); + if (!page) { + expect(page).toBeDefined(); + } else { + await page.waitForLoadState(); + await use(page); + } }, dataPath: async ({ }, use) => { const insomniaDataPath = randomDataPath(); diff --git a/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts b/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts new file mode 100644 index 0000000000..f62046c2bf --- /dev/null +++ b/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts @@ -0,0 +1,283 @@ +import { expect } from '@playwright/test'; + +import { test } from '../../playwright/test';; + +test.describe('test utility process', async () => { + + const testCases = [ + { + id: 'environment basic operations', + code: ` + const bool2 = pm.environment.has('bool1'); + const num2 = pm.environment.get('num1'); + const str2 = pm.environment.get('str1'); + // add & update + pm.environment.set('bool1', false); + pm.environment.set('num1', 11); + pm.environment.set('str1', 'strr'); + pm.environment.set('bool2', bool2); + pm.environment.set('num2', num2); + pm.environment.set('str2', str2); + // toObject + const newObject = pm.environment.toObject(); + pm.environment.set('newObject.bool', newObject.bool2); + pm.environment.set('newObject.num', newObject.num2); + pm.environment.set('newObject.str', newObject.str2); + // unset + pm.environment.set('willDelete', 11); + pm.environment.unset('willDelete'); + // render + const vals = pm.environment.replaceIn('{{bool1}}-{{num1}}-{{str1}}'); + pm.environment.set('rendered', vals); + `, + context: { + pm: { + environment: { + bool1: true, + num1: 1, + str1: 'str', + }, + }, + }, + expectedResult: { + collectionVariables: {}, + iterationData: {}, + globals: {}, + variables: { + bool1: false, + num1: 11, + str1: 'strr', + bool2: true, + num2: 1, + str2: 'str', + 'newObject.bool': true, + 'newObject.num': 1, + 'newObject.str': 'str', + 'rendered': 'false-11-strr', + }, + environment: { + bool1: false, + num1: 11, + str1: 'strr', + bool2: true, + num2: 1, + str2: 'str', + 'newObject.bool': true, + 'newObject.num': 1, + 'newObject.str': 'str', + 'rendered': 'false-11-strr', + }, + }, + }, + { + id: 'collectionVariables (baseEnvironment) basic operations', + code: ` + const bool2 = pm.collectionVariables.has('bool1'); + const num2 = pm.collectionVariables.get('num1'); + const str2 = pm.collectionVariables.get('str1'); + // add & update + pm.collectionVariables.set('bool1', false); + pm.collectionVariables.set('num1', 11); + pm.collectionVariables.set('str1', 'strr'); + pm.collectionVariables.set('bool2', bool2); + pm.collectionVariables.set('num2', num2); + pm.collectionVariables.set('str2', str2); + // toObject + const newObject = pm.collectionVariables.toObject(); + pm.collectionVariables.set('newObject.bool', newObject.bool2); + pm.collectionVariables.set('newObject.num', newObject.num2); + pm.collectionVariables.set('newObject.str', newObject.str2); + // unset + pm.collectionVariables.set('willDelete', 11); + pm.collectionVariables.unset('willDelete'); + // render + const vals = pm.collectionVariables.replaceIn('{{bool1}}-{{num1}}-{{str1}}'); + pm.collectionVariables.set('rendered', vals); + `, + context: { + pm: { + collectionVariables: { + bool1: true, + num1: 1, + str1: 'str', + }, + }, + }, + expectedResult: { + environment: {}, + variables: { + 'bool1': false, + 'bool2': true, + 'newObject.bool': true, + 'newObject.num': 1, + 'newObject.str': 'str', + 'num1': 11, + 'num2': 1, + 'rendered': 'false-11-strr', + 'str1': 'strr', + 'str2': 'str', + }, + globals: {}, + iterationData: {}, + collectionVariables: { + bool1: false, + num1: 11, + str1: 'strr', + bool2: true, + num2: 1, + str2: 'str', + 'newObject.bool': true, + 'newObject.num': 1, + 'newObject.str': 'str', + 'rendered': 'false-11-strr', + }, + }, + }, + { + id: 'variables basic operations', + code: ` + const unexisting = pm.variables.has('VarNotExist'); + const strFromGlobal = pm.variables.get('glb'); + const strFromCollection = pm.variables.get('col'); + const numFromEnv = pm.variables.get('num'); + const strFromIter = pm.variables.get('str'); + const rendered = pm.variables.replaceIn('{{bool}}-{{num}}-{{str}}'); + // set local + pm.variables.set('strFromGlobal', strFromGlobal); + pm.variables.set('strFromCollection', strFromCollection); + pm.variables.set('numFromEnv', numFromEnv); + pm.variables.set('strFromIter', strFromIter); + pm.variables.set('rendered', rendered); + `, + context: { + pm: { + globals: { + bool: false, + num: 1, + glb: 'glb', + }, + collectionVariables: { + num: 2, + col: 'col', + }, + environment: { + num: 3, + str: 'env', + }, + iterationData: { + str: 'iter', + }, + }, + }, + expectedResult: { + globals: { + bool: false, + num: 1, + glb: 'glb', + }, + collectionVariables: { + num: 2, + col: 'col', + }, + environment: { + num: 3, + str: 'env', + }, + iterationData: { + str: 'iter', + }, + variables: { + strFromGlobal: 'glb', + strFromCollection: 'col', + numFromEnv: 3, + strFromIter: 'iter', + rendered: 'false-3-iter', + bool: false, + col: 'col', + glb: 'glb', + num: 3, + 'str': 'iter', + }, + }, + }, + { + id: 'simple test sendRequest and await/async', + code: ` + let testResp; + try { + await new Promise( + resolve => { + pm.sendRequest( + 'http://127.0.0.1:4010/pets/1', + (err, resp) => { + testResp = resp; + resolve(); + } + ); + } + ); + } catch (e) { + pm.variables.set('error', e); + } + pm.variables.set('resp.code', testResp.code); + `, + context: { + pm: {}, + }, + expectedResult: { + globals: {}, + iterationData: {}, + variables: { + 'resp.code': 200, + }, + environment: {}, + collectionVariables: {}, + }, + }, + ]; + + for (let i = 0; i < testCases.length; i++) { + const tc = testCases[i]; + + // tests begin here + test(tc.id, async ({ page: mainWindow }) => { + test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); + + // action + await mainWindow?.evaluate( + async (tc: any) => { + window.postMessage( + { + action: 'message-event://utility.process/debug', + id: tc.id, + code: tc.code, + context: tc.context, + }, + '*', + ); + }, + tc, + ); + + // assert + let localStorage; + + // TODO: ideally call waitForEvent + for (let i = 0; i < 120; i++) { + localStorage = await mainWindow?.evaluate(() => window.localStorage); + expect(localStorage).toBeDefined(); + + if (localStorage[`test_result:${tc.id}`] || localStorage[`test_error:${tc.id}`]) { + break; + } + await new Promise(resolve => setTimeout(resolve, 500)); + } + + if (localStorage) { // just for suppressing ts complaint + expect(JSON.parse(localStorage[`test_result:${tc.id}`])).toEqual(tc.expectedResult); + } + + }); + + } +}); diff --git a/packages/insomnia/.gitignore b/packages/insomnia/.gitignore index bc1c552dc8..c1f1d865d8 100644 --- a/packages/insomnia/.gitignore +++ b/packages/insomnia/.gitignore @@ -6,3 +6,5 @@ src/main.min.js src/main.min.js.map src/preload.js src/preload.js.map +src/renderers/utility-process/**/*.js +src/renderers/utility-process/**/*.js.map diff --git a/packages/insomnia/esbuild.main.ts b/packages/insomnia/esbuild.main.ts index 8a4595f833..eda082b46b 100644 --- a/packages/insomnia/esbuild.main.ts +++ b/packages/insomnia/esbuild.main.ts @@ -41,6 +41,26 @@ export default async function build(options: Options) { format: 'cjs', external: ['electron'], }); + const preloadUtilityProcess = esbuild.build({ + entryPoints: ['./src/renderers/utility-process/preload.ts'], + outfile: path.join(outdir, 'renderers/utility-process/preload-utility-process.js'), + target: 'esnext', + bundle: true, + platform: 'node', + sourcemap: true, + format: 'cjs', + external: ['electron'], + }); + const utilityProcess = esbuild.build({ + entryPoints: ['./src/renderers/utility-process/index.ts'], + outfile: path.join(outdir, 'renderers/utility-process/utility-process.min.js'), + target: 'esnext', + bundle: true, + platform: 'browser', + sourcemap: true, + format: 'cjs', + external: [], + }); const main = esbuild.build({ entryPoints: ['./src/main.development.ts'], outfile: path.join(outdir, 'main.min.js'), @@ -56,7 +76,7 @@ export default async function build(options: Options) { ...Object.keys(builtinModules), ], }); - return Promise.all([main, preload]); + return Promise.all([main, preload, preloadUtilityProcess, utilityProcess]); } // Build if ran as a cli script diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 19dcf483e8..5b8f282877 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -25,7 +25,7 @@ "package": "npm run build:app && cross-env USE_HARD_LINKS=false electron-builder build --config electron-builder.config.js", "start": "concurrently -n dev,app --kill-others \"npm run start:dev-server\" \"npm run start:electron\"", "start:dev-server": "vite dev", - "start:electron": "cross-env NODE_ENV=development esr esbuild.main.ts && electron .", + "start:electron": "cross-env NODE_ENV=development esr esbuild.main.ts && electron --inspect=5858 .", "test": "jest", "electron:dev-build": "electron ./build/main.min.js", "test:watch": "jest --watch", diff --git a/packages/insomnia/src/main/ipc/message-channel.ts b/packages/insomnia/src/main/ipc/message-channel.ts new file mode 100644 index 0000000000..2deee89bf5 --- /dev/null +++ b/packages/insomnia/src/main/ipc/message-channel.ts @@ -0,0 +1,10 @@ +import { BrowserWindow, ipcMain } from 'electron'; + +// registerUtilityProcessPort broadcasts message ports to observer windows +export function registerUtilityProcessConsumer(consumerWindows: BrowserWindow[]) { + ipcMain.on('ipc://main/publish-port', ev => { + consumerWindows.forEach(win => { + win.webContents.postMessage('ipc://renderers/publish-port', null, ev.ports); + }); + }); +} diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index ab0aa27c4d..11baff789b 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -16,6 +16,7 @@ import { } from '../common/constants'; import { docsBase } from '../common/documentation'; import * as log from '../common/log'; +import { registerUtilityProcessConsumer } from './ipc/message-channel'; import LocalStorage from './local-storage'; const { app, Menu, shell, dialog, clipboard, BrowserWindow } = electron; @@ -26,7 +27,9 @@ const MINIMUM_WIDTH = 500; const MINIMUM_HEIGHT = 400; let newWindow: ElectronBrowserWindow | null = null; +let isolatedUtilityProcess: ElectronBrowserWindow | null = null; const windows = new Set(); +const processes = new Set(); let localStorage: LocalStorage | null = null; interface Bounds { @@ -40,6 +43,45 @@ export function init() { initLocalStorage(); } +export function createIsolatedProcess(parent?: electron.BrowserWindow) { + isolatedUtilityProcess = new BrowserWindow({ + parent, + show: false, + title: 'UtilityProcess', + webPreferences: { + sandbox: true, + contextIsolation: true, + nodeIntegration: false, + webSecurity: true, + preload: path.join(__dirname, 'renderers/utility-process/preload-utility-process.js'), + spellcheck: false, + }, + }); + + const utilityProcessPath = path.resolve(__dirname, './renderers/utility-process/index.html'); + const utilityProcessUrl = pathToFileURL(utilityProcessPath).href; + isolatedUtilityProcess.loadURL(utilityProcessUrl); + + isolatedUtilityProcess.once('ready-to-show', () => { + if (isolatedUtilityProcess) { + isolatedUtilityProcess.show(); + if (parent) { + parent.webContents.openDevTools(); + } + isolatedUtilityProcess.webContents.openDevTools(); + } + }); + + isolatedUtilityProcess?.on('closed', () => { + if (isolatedUtilityProcess) { + processes.delete(isolatedUtilityProcess); + isolatedUtilityProcess = processes.values().next().value || null; + } + }); + + return isolatedUtilityProcess; +} + export function createWindow() { const { bounds, fullscreen, maximize } = getBounds(); const { x, y, width, height } = bounds; @@ -445,10 +487,10 @@ export function createWindow() { helpMenu.submenu?.push({ type: 'separator', }, - { - label: `${MNEMONIC_SYM}About`, - click: aboutMenuClickHandler, - }); + { + label: `${MNEMONIC_SYM}About`, + click: aboutMenuClickHandler, + }); } const developerMenu: MenuItemConstructorOptions = { @@ -547,6 +589,11 @@ export function createWindow() { Menu.setApplicationMenu(Menu.buildFromTemplate(template)); windows.add(newWindow); + + const isolatedProcess = createIsolatedProcess(newWindow); + processes.add(isolatedProcess); + registerUtilityProcessConsumer([newWindow]); + return newWindow; } diff --git a/packages/insomnia/src/preload.ts b/packages/insomnia/src/preload.ts index 6106e39c10..d1788fdb61 100644 --- a/packages/insomnia/src/preload.ts +++ b/packages/insomnia/src/preload.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from 'electron'; +import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; import { gRPCBridgeAPI } from './main/ipc/grpc'; import { CurlBridgeAPI } from './main/network/curl'; @@ -95,3 +95,13 @@ if (process.contextIsolated) { window.shell = shell; window.clipboard = clipboard; } + +// it is different from window.main.on, it requires events to pass ports +ipcRenderer.on('ipc://renderers/publish-port', async (ev: IpcRendererEvent) => { + const windowLoaded = new Promise(resolve => { + window.onload = resolve; + }); + await windowLoaded; + + window.postMessage({ action: 'message-event://renderers/publish-port' }, '*', ev.ports); +}); diff --git a/packages/insomnia/src/renderers/utility-process/index.html b/packages/insomnia/src/renderers/utility-process/index.html new file mode 100644 index 0000000000..5d55a6ad7d --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/index.html @@ -0,0 +1,16 @@ + + + + + + Utility Process + + + +

Utility Process

+ + + diff --git a/packages/insomnia/src/renderers/utility-process/index.ts b/packages/insomnia/src/renderers/utility-process/index.ts new file mode 100644 index 0000000000..21a9a88146 --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/index.ts @@ -0,0 +1,49 @@ +import { initPm } from './inso-object'; + +const executeAction = 'message-port://utility.process/execute'; + +async function init() { + const channel = new MessageChannel(); + + channel.port1.onmessage = async (ev: MessageEvent) => { + const action = ev.data.action; + + if (action === executeAction || action === 'message-port://utility.process/debug') { + try { + const getPm = new Function('pm', 'return pm;'); + const rawPm = getPm(ev.data.options.context.pm); + const pm = initPm(rawPm); + const AsyncFunction = (async () => { }).constructor; + const func = AsyncFunction( + 'pm', + // TODO: support require function + // TODO: support async/await + ` + ${ev.data.options.code}; + return pm.toObject(); + ` + ); + + const result = await func(pm); + channel.port1.postMessage({ + action: action === executeAction ? 'message-port://caller/respond' : 'message-port://caller/debug/respond', + id: action === executeAction ? undefined : ev.data.options.id, + result, + }); + } catch (e) { + console.log(JSON.stringify(e)); + channel.port1.postMessage({ + action: action === executeAction ? 'message-port://caller/respond' : 'message-port://caller/debug/respond', + id: action === executeAction ? undefined : ev.data.options.id, + error: JSON.stringify(e), + }); + } + } else { + console.error(`unknown action ${ev.data}`); + } + }; + + window.postMessage('message-event://preload/publish-port', '*', [channel.port2]); +} + +init(); diff --git a/packages/insomnia/src/renderers/utility-process/inso-object.ts b/packages/insomnia/src/renderers/utility-process/inso-object.ts new file mode 100644 index 0000000000..7e65feb5cf --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/inso-object.ts @@ -0,0 +1,190 @@ +import { getHttpRequestSender, getIntepolator, HttpRequestSender, PmHttpRequest, PmHttpResponse } from './static-modules'; + +class BaseKV { + private kvs = new Map(); + + constructor(jsonObject: object | undefined) { + // TODO: currently it doesn't support getting nested field directly + this.kvs = new Map(Object.entries(jsonObject || {})); + } + + has = (variableName: string) => { + return this.kvs.has(variableName); + }; + + get = (variableName: string) => { + return this.kvs.get(variableName); + }; + + set = (variableName: string, variableValue: boolean | number | string) => { + this.kvs.set(variableName, variableValue); + }; + + unset = (variableName: string) => { + this.kvs.delete(variableName); + }; + + clear = () => { + this.kvs.clear(); + }; + + replaceIn = (template: string) => { + return getIntepolator().render(template, this.toObject()); + }; + + toObject = () => { + return Object.fromEntries(this.kvs.entries()); + }; +} + +class Environment extends BaseKV { + constructor(jsonObject: object | undefined) { + super(jsonObject); + } +} + +class Variables { + // TODO: support vars for all levels + private globals: BaseKV; + private collection: BaseKV; + private environment: BaseKV; + private data: BaseKV; + private local: BaseKV; + + constructor( + args: { + globals: BaseKV; + collection: BaseKV; + environment: BaseKV; + data: BaseKV; + local: BaseKV; + }, + ) { + this.globals = args.globals; + this.collection = args.collection; + this.environment = args.environment; + this.data = args.data; + this.local = args.local; + } + + has = (variableName: string) => { + const globalsHas = this.globals.has(variableName); + const collectionHas = this.collection.has(variableName); + const environmentHas = this.environment.has(variableName); + const dataHas = this.data.has(variableName); + const localHas = this.local.has(variableName); + + return globalsHas || collectionHas || environmentHas || dataHas || localHas; + }; + + get = (variableName: string) => { + let finalVal: boolean | number | string | object | undefined = undefined; + [ + this.local, + this.data, + this.environment, + this.collection, + this.globals, + ].forEach(vars => { + const value = vars.get(variableName); + if (!finalVal && value) { + finalVal = value; + } + }); + + return finalVal; + }; + + set = (variableName: string, variableValue: boolean | number | string) => { + this.local.set(variableName, variableValue); + }; + + replaceIn = (template: string) => { + const context = this.toObject(); + return getIntepolator().render(template, context); + }; + + toObject = () => { + return [ + this.globals, + this.collection, + this.environment, + this.data, + this.local, + ].map( + vars => vars.toObject() + ).reduce( + (ctx, obj) => ({ ...ctx, ...obj }), + {}, + ); + }; +} + +class InsomniaObject { + public globals: Environment; + public collectionVariables: Environment; + public environment: Environment; + public iterationData: Environment; + public variables: Variables; + + private httpRequestSender: HttpRequestSender = getHttpRequestSender(); + + constructor(input: { + globals: Environment; + collectionVariables: Environment; + environment: Environment; + iterationData: Environment; + variables: Variables; + }) { + this.globals = input.globals; + this.collectionVariables = input.collectionVariables; + this.environment = input.environment; + this.iterationData = input.iterationData; + this.variables = input.variables; + } + + toObject = () => { + return { + globals: this.globals.toObject(), + variables: this.variables.toObject(), + environment: this.environment.toObject(), + collectionVariables: this.collectionVariables.toObject(), + iterationData: this.iterationData.toObject(), + }; + }; + + sendRequest = async (req: string | PmHttpRequest, callback: (e?: Error, resp?: PmHttpResponse) => void) => { + return await this.httpRequestSender.sendRequest(req, callback); + }; +} + +interface RawObject { + globals?: object; + environment?: object; + collectionVariables?: object; + iterationData?: object; +} + +export function initPm(rawObj: RawObject) { + const globals = new Environment(rawObj.globals); + const environment = new Environment(rawObj.environment); + const collectionVariables = new Environment(rawObj.collectionVariables); + const iterationData = new Environment(rawObj.iterationData); + const local = new Environment({}); + + const variables = new Variables({ + globals, + environment, + collection: collectionVariables, + data: iterationData, + local, + }); + + return new InsomniaObject({ + globals, + environment, + collectionVariables, + iterationData, + variables, + }); +}; diff --git a/packages/insomnia/src/renderers/utility-process/preload.ts b/packages/insomnia/src/renderers/utility-process/preload.ts new file mode 100644 index 0000000000..369453675e --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/preload.ts @@ -0,0 +1,7 @@ +import { ipcRenderer } from 'electron'; + +window.onmessage = (ev: MessageEvent) => { + if (ev.data === 'message-event://preload/publish-port') { + ipcRenderer.postMessage('ipc://main/publish-port', null, [ev.ports[0]]); + } +}; diff --git a/packages/insomnia/src/renderers/utility-process/shared-modules.ts b/packages/insomnia/src/renderers/utility-process/shared-modules.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia/src/renderers/utility-process/static-modules.ts b/packages/insomnia/src/renderers/utility-process/static-modules.ts new file mode 100644 index 0000000000..2f44e44127 --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/static-modules.ts @@ -0,0 +1,82 @@ +import { configure, type ConfigureOptions, type Environment as NunjuncksEnv } from 'nunjucks'; + +export class PmHttpRequest { + public name: string; + constructor(_: {}, name?: string) { + this.name = name || ''; + } +} + +export interface PmHttpResponse { + code: number; +} + +// common modules +class PmHttpRequestSender { + constructor() { } + + sendRequest = async (req: string | PmHttpRequest, callback: (e?: Error, resp?: PmHttpResponse) => void): Promise => { + if (typeof (req) === 'string') { + // simple request + return fetch(req) + .then(rawResp => { + // TODO: init all response fields + const resp = { + code: rawResp.status, + }; + + callback( + undefined, + resp, + ); + }) + .catch(err => { + callback(err); + }); + } + // TODO: + return; + }; +} + +export interface HttpRequestSender { + sendRequest: (req: string | PmHttpRequest, callback: (e?: Error, resp?: PmHttpResponse) => void) => Promise; +} + +const httpRequestSender = new PmHttpRequestSender(); +export function getHttpRequestSender() { + return httpRequestSender; +} + +class Intepolator { + private engine: NunjuncksEnv; + + constructor(config: ConfigureOptions) { + this.engine = configure(config); + } + + render = (template: string, context: object) => { + // TODO: handle timeout + // TODO: support plugin? + return this.engine.renderString(template, context); + }; +} + +const intepolator = new Intepolator({ + autoescape: false, + // Don't escape HTML + throwOnUndefined: true, + // Strict mode + tags: { + blockStart: '{%', + blockEnd: '%}', + variableStart: '{{', + variableEnd: '}}', + commentStart: '{#', + commentEnd: '#}', + }, +}); + +export function getIntepolator() { + return intepolator; +} diff --git a/packages/insomnia/src/ui/index.tsx b/packages/insomnia/src/ui/index.tsx index d35934a333..2a07b8002c 100644 --- a/packages/insomnia/src/ui/index.tsx +++ b/packages/insomnia/src/ui/index.tsx @@ -35,6 +35,7 @@ import { Migrate } from './routes/onboarding.migrate'; import { shouldOrganizationsRevalidate } from './routes/organization'; import Root from './routes/root'; import { initializeSentry } from './sentry'; +import { getWindowMessageHandler } from './window-message-handlers'; const Organization = lazy(() => import('./routes/organization')); const Project = lazy(() => import('./routes/project')); @@ -49,6 +50,9 @@ initializeLogging(); document.body.setAttribute('data-platform', process.platform); document.title = getProductName(); +const windowMessageHandler = getWindowMessageHandler(); +windowMessageHandler.start(); + try { // In order to run playwight tests that simulate a logged in user // we need to inject state into localStorage diff --git a/packages/insomnia/src/ui/window-message-handlers.ts b/packages/insomnia/src/ui/window-message-handlers.ts new file mode 100644 index 0000000000..424ea78829 --- /dev/null +++ b/packages/insomnia/src/ui/window-message-handlers.ts @@ -0,0 +1,89 @@ + +type MessageHandler = (ev: MessageEvent) => Promise; + +class WindowMessageHandler { + private utilityProcessPort: MessagePort | undefined; + private actionHandlers: Map = new Map(); + + constructor() { } + + publishPortHandler = async (ev: MessageEvent) => { + if (ev.ports.length === 0) { + console.error('no port is found in the publishing port event'); + return; + } + + this.utilityProcessPort = ev.ports[0]; + + this.utilityProcessPort.onmessage = ev => { + if (ev.data.action === 'message-port://caller/respond') { + // TODO: hook to UI and display result + console.log('[main] result from utility process:', ev.data.result); + } else if (ev.data.action === 'message-port://caller/debug/respond') { + if (ev.data.result) { + window.localStorage.setItem(`test_result:${ev.data.id}`, JSON.stringify(ev.data.result)); + } else { + window.localStorage.setItem(`test_error:${ev.data.id}`, JSON.stringify(ev.data.error)); + } + } else { + console.error(`unknown action ${ev}`); + } + }; + }; + + debugEventHandler = async (ev: MessageEvent) => { + if (!this.utilityProcessPort) { + console.error('utility process port is not inited'); + return; + } + + this.utilityProcessPort.postMessage({ + action: 'message-port://utility.process/debug', + options: { + id: ev.data.id, + code: ev.data.code, + context: ev.data.context, + }, + }); + }; + + // startUtilityProcessHandler = async (ev: MessageEvent) => { + + // }; + + // TODO: registerMessagePortEventHandler + + register = (actionName: string, handler: MessageHandler) => { + this.actionHandlers.set(actionName, handler); + }; + + start = () => { + this.register('message-event://renderers/publish-port', this.publishPortHandler); + this.register('message-event://utility.process/debug', this.debugEventHandler); + + window.onmessage = (ev: MessageEvent) => { + const action = ev.data.action; + if (!action) { + // could be react events + return; + } + + const handler = this.actionHandlers.get(action); + if (!handler) { + console.error(`no handler is found for action ${action}`); + return; + } + + handler(ev); + }; + }; + + stop = () => { + this.actionHandlers.clear(); + }; +} + +const windowMessageHandler = new WindowMessageHandler(); +export function getWindowMessageHandler() { + return windowMessageHandler; +}