From 9a928068445b3741cf2e5c5385f23524911dba25 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 10 Feb 2023 17:20:27 -0600 Subject: [PATCH 01/11] Added Extension host and some test command handlers on it --- .erb/configs/webpack.config.base.ts | 38 ++++++-- .../webpack.config.extension-host.prod.ts | 34 ++++++++ .erb/configs/webpack.paths.ts | 4 + .erb/scripts/check-build-exists.ts | 12 +++ package.json | 4 +- src/extension-host/extension-host.ts | 51 +++++++++++ .../services/ExtensionHostWebSocket.ts | 6 ++ src/main/main.ts | 67 +++++++++++++- src/renderer/App.tsx | 87 ++++++++++++++++++- src/renderer/index.tsx | 7 ++ .../services/ClientNetworkConnector.ts | 3 +- src/renderer/services/WebSocketFactory.ts | 19 ++++ src/shared/globalThis.ts | 17 ++++ src/shared/services/CommandService.ts | 19 ++-- src/shared/services/ConnectionService.ts | 2 +- src/shared/services/NetworkService.ts | 8 +- src/shared/util/InternalUtil.ts | 29 +++++-- tsconfig.json | 11 +-- 18 files changed, 379 insertions(+), 39 deletions(-) create mode 100644 .erb/configs/webpack.config.extension-host.prod.ts create mode 100644 src/extension-host/extension-host.ts create mode 100644 src/extension-host/services/ExtensionHostWebSocket.ts create mode 100644 src/renderer/services/WebSocketFactory.ts create mode 100644 src/shared/globalThis.ts diff --git a/.erb/configs/webpack.config.base.ts b/.erb/configs/webpack.config.base.ts index 1ee8c1611e..955dc5c985 100644 --- a/.erb/configs/webpack.config.base.ts +++ b/.erb/configs/webpack.config.base.ts @@ -7,9 +7,14 @@ import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin'; import webpackPaths from './webpack.paths'; import { dependencies as externals } from '../../release/app/package.json'; -const isRenderer = - process.env.npm_lifecycle_script?.includes('webpack.config.renderer') ?? - false; +let processType: string; +if (process.env.npm_lifecycle_script?.includes('webpack.config.renderer')) + processType = 'renderer'; +else if ( + process.env.npm_lifecycle_script?.includes('webpack.config.extension-host') +) + processType = 'extension-host'; +else processType = 'main'; const configuration: webpack.Configuration = { externals: [...Object.keys(externals || {})], @@ -61,16 +66,31 @@ const configuration: webpack.Configuration = { new webpack.IgnorePlugin({ checkResource(resource, context) { // Don't include stuff from the main folder or @main... in renderer and renderer folder in main folder - const exclude = isRenderer - ? resource.startsWith('@main') || resource.includes('main/') - : resource.startsWith('@renderer') || /renderer\//.test(resource); + let exclude = false; + switch (processType) { + case 'renderer': + exclude = + resource.startsWith('@main') || + resource.includes('main/') || + resource.startsWith('@extension-host') || + resource.includes('extension-host/'); + break; + case 'extension-host': + // TODO: put stuff that extension-host and renderer share into a different folder and exclude renderer stuff here + break; + default: // main + exclude = + resource.startsWith('@renderer') || + /renderer\//.test(resource) || + resource.startsWith('@extension-host') || + resource.includes('extension-host/'); + break; + } // Log if a file is excluded just fyi if (!context.includes('node_modules') && exclude) console.log( - `${ - isRenderer ? 'Renderer' : 'Main' - }: Resource ${resource}\n\tat context ${context}: ${ + `${processType}: Resource ${resource}\n\tat context ${context}: ${ exclude ? 'excluded' : 'included' }`, ); diff --git a/.erb/configs/webpack.config.extension-host.prod.ts b/.erb/configs/webpack.config.extension-host.prod.ts new file mode 100644 index 0000000000..232d59947d --- /dev/null +++ b/.erb/configs/webpack.config.extension-host.prod.ts @@ -0,0 +1,34 @@ +/** + * Webpack config for production extension-host process + */ + +import path from 'path'; +import webpack from 'webpack'; +import merge, { mergeWithCustomize } from 'webpack-merge'; +import mainConfig from './webpack.config.main.prod'; +import webpackPaths from './webpack.paths'; +import checkNodeEnv from '../scripts/check-node-env'; +import deleteSourceMaps from '../scripts/delete-source-maps'; + +checkNodeEnv('production'); +deleteSourceMaps(); + +const configuration: webpack.Configuration = { + entry: { + 'extension-host': path.join( + webpackPaths.srcExtensionHostPath, + 'extension-host.ts', + ), + }, + + output: { + path: webpackPaths.distExtensionHostPath, + }, +}; + +export default mergeWithCustomize({ + customizeObject(a, b, key) { + if (key === 'entry') return b; + return merge(a, b); + }, +})(mainConfig, configuration); diff --git a/.erb/configs/webpack.paths.ts b/.erb/configs/webpack.paths.ts index 291a794a87..af84e42a82 100644 --- a/.erb/configs/webpack.paths.ts +++ b/.erb/configs/webpack.paths.ts @@ -6,6 +6,7 @@ const dllPath = path.join(__dirname, '../dll'); const srcPath = path.join(rootPath, 'src'); const srcMainPath = path.join(srcPath, 'main'); +const srcExtensionHostPath = path.join(srcPath, 'extension-host'); const srcRendererPath = path.join(srcPath, 'renderer'); const srcSharedPath = path.join(srcPath, 'shared'); @@ -17,6 +18,7 @@ const srcNodeModulesPath = path.join(srcPath, 'node_modules'); const distPath = path.join(appPath, 'dist'); const distMainPath = path.join(distPath, 'main'); +const distExtensionHostPath = path.join(distPath, 'extension-host'); const distRendererPath = path.join(distPath, 'renderer'); const buildPath = path.join(releasePath, 'build'); @@ -26,6 +28,7 @@ export default { dllPath, srcPath, srcMainPath, + srcExtensionHostPath, srcRendererPath, srcSharedPath, releasePath, @@ -35,6 +38,7 @@ export default { srcNodeModulesPath, distPath, distMainPath, + distExtensionHostPath, distRendererPath, buildPath, }; diff --git a/.erb/scripts/check-build-exists.ts b/.erb/scripts/check-build-exists.ts index 649929572b..49362114c3 100644 --- a/.erb/scripts/check-build-exists.ts +++ b/.erb/scripts/check-build-exists.ts @@ -5,6 +5,10 @@ import fs from 'fs'; import webpackPaths from '../configs/webpack.paths'; const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); +const extensionHostPath = path.join( + webpackPaths.distExtensionHostPath, + 'extension-host.js', +); const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); if (!fs.existsSync(mainPath)) { @@ -15,6 +19,14 @@ if (!fs.existsSync(mainPath)) { ); } +if (!fs.existsSync(extensionHostPath)) { + throw new Error( + chalk.whiteBright.bgRed.bold( + 'The extension host process is not built yet. Build it by running "npm run build:extension-host"', + ), + ); +} + if (!fs.existsSync(rendererPath)) { throw new Error( chalk.whiteBright.bgRed.bold( diff --git a/package.json b/package.json index 0b563516ae..c4aefee8e8 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,9 @@ ], "main": "./src/main/main.ts", "scripts": { - "build": "concurrently \"npm run build:main\" \"npm run build:renderer\"", + "build": "concurrently \"npm run build:main\" \"npm run build:extension-host\" \"npm run build:renderer\"", "build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts", + "build:extension-host": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.extension-host.prod.ts", "build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts", "build:native-modules": "cd ./release/app && npm install", "postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts", @@ -502,6 +503,7 @@ "patterns": [ "!**/**", "src/main/**", + "src/extension-host/**", "src/shared/**" ], "logLevel": "quiet" diff --git a/src/extension-host/extension-host.ts b/src/extension-host/extension-host.ts new file mode 100644 index 0000000000..6abae54648 --- /dev/null +++ b/src/extension-host/extension-host.ts @@ -0,0 +1,51 @@ +import { isClient } from '@shared/util/InternalUtil'; +import * as NetworkService from '@shared/services/NetworkService'; +import papi from '@shared/services/papi'; +import { CommandHandler } from '@shared/util/PapiUtil'; +import { ProcessType } from '@shared/globalThis'; + +// #region globalThis setup + +globalThis.processType = ProcessType.ExtensionHost; + +// #endregion + +// #region Test logs + +console.log('Hello from the extension host!'); +console.log(`Extension host is${isClient() ? '' : ' not'} client`); +console.log(`Extension host process.type = ${process.type}`); +console.log(`Extension host process.env.NODE_ENV = ${process.env.NODE_ENV}`); + +// #endregion + +// #region Services setup + +const commandHandlers: { [commandName: string]: CommandHandler } = { + addMany: async (...nums: number[]) => { + /* const start = performance.now(); */ + /* const result = await papi.commands.sendCommand('addThree', 1, 4, 9); */ + /* console.log( + `addThree(...) = ${result} took ${performance.now() - start} ms`, + ); */ + return nums.reduce((acc, current) => acc + current, 0); + }, +}; + +NetworkService.initialize() + .then(() => { + // Set up test handlers + Object.entries(commandHandlers).forEach(([commandName, handler]) => { + papi.commands.registerCommand(commandName, handler); + }); + + // TODO: Probably should return Promise.all of these registrations + return undefined; + }) + .catch((e) => console.error(e)); + +// #endregion + +process.on('message', () => + console.log('This is just here so this file stays running'), +); diff --git a/src/extension-host/services/ExtensionHostWebSocket.ts b/src/extension-host/services/ExtensionHostWebSocket.ts new file mode 100644 index 0000000000..a9ea4558ad --- /dev/null +++ b/src/extension-host/services/ExtensionHostWebSocket.ts @@ -0,0 +1,6 @@ +import ws from 'ws'; +/** + * extension-host client uses ws as its WebSocket client, but the renderer can't use it. So we need to exclude it from the renderer webpack bundle like this. + */ + +export default ws; diff --git a/src/main/main.ts b/src/main/main.ts index 4cf5c4764b..d7929f0a11 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,5 +1,3 @@ -/* eslint global-require: off, no-console: off, promise/always-return: off */ - /** * This module executes inside of electron's main process. You can start * electron renderer process from here and communicate with the other processes @@ -16,9 +14,17 @@ import * as NetworkService from '@shared/services/NetworkService'; import papi from '@shared/services/papi'; import { CommandHandler } from '@shared/util/PapiUtil'; import windowStateKeeper from 'electron-window-state'; +import { fork } from 'child_process'; +import { ProcessType } from '@shared/globalThis'; import MenuBuilder from './menu'; import { resolveHtmlPath } from './util'; +// #region globalThis setup + +globalThis.processType = ProcessType.Main; + +// #endregion + // #region ELECTRON SETUP class AppUpdater { @@ -34,6 +40,7 @@ class AppUpdater { let mainWindow: BrowserWindow | null = null; if (process.env.NODE_ENV === 'production') { + // eslint-disable-next-line global-require const sourceMapSupport = require('source-map-support'); sourceMapSupport.install(); } @@ -42,11 +49,13 @@ const isDebug = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; if (isDebug) { + // eslint-disable-next-line global-require require('electron-debug')(); } /** Install extensions into the Chromium renderer process */ const installExtensions = async () => { + // eslint-disable-next-line global-require const installer = require('electron-devtools-installer'); const forceDownload = !!process.env.UPGRADE_EXTENSIONS; const extensions = ['REACT_DEVELOPER_TOOLS']; @@ -169,6 +178,8 @@ app // dock icon is clicked and there are no other windows open. if (mainWindow === null) createWindow(); }); + + return undefined; }) .catch(console.log); @@ -178,6 +189,9 @@ app const commandHandlers: { [commandName: string]: CommandHandler } = { echo: async (message: string) => { + return message; + }, + echoRenderer: async (message: string) => { /* const start = performance.now(); */ /* const result = */ await papi.commands.sendCommand('addThree', 1, 4, 9); /* console.log( @@ -185,6 +199,10 @@ const commandHandlers: { [commandName: string]: CommandHandler } = { ); */ return message; }, + echoExtensionHost: async (message: string) => { + await papi.commands.sendCommand('addMany', 3, 5, 7, 1, 4); + return message; + }, throwError: async (message: string) => { throw new Error(`Test Error thrown in throwError command: ${message}`); }, @@ -203,7 +221,52 @@ NetworkService.initialize() Object.entries(commandHandlers).forEach(([commandName, handler]) => { papi.commands.registerCommand(commandName, handler); }); + + // TODO: Probably should return Promise.all of these registrations + return undefined; }) .catch((e) => console.error(e)); // #endregion + +// #region Extension Host + +// Fork a new process for the extension host +const extensionHost = fork( + path.join( + __dirname, + `../extension-host/extension-host.${app.isPackaged ? 'js' : 'ts'}`, + ), + { + execArgv: app.isPackaged + ? [] + : // Enable TypeScript on the extension host in development. These args match the npm run start:main args passed to electronmon + [ + '-r', + 'ts-node/register/transpile-only', + '-r', + 'tsconfig-paths/register', + ], + }, +); + +// This does not pass errors to the main process. Not sure what this does +extensionHost.on('error', (err) => + console.error(`extensionHost.error: ${err.toString()}`), +); + +extensionHost.on('exit', () => console.warn('extensionHost just exited!')); + +setTimeout(async () => { + console.log( + `Add Many (from EH): ${await papi.commands.sendCommand( + 'addMany', + 2, + 5, + 9, + 7, + )}`, + ); +}, 3000); + +// #endregion diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index dc9b0cfebd..3ea307d1a9 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -24,6 +24,23 @@ const test = async () => { const echo = async (message: string) => papi.commands.sendCommand<[string], string>('echo', message); +const echoRenderer = async (message: string) => + papi.commands.sendCommand<[string], string>('echoRenderer', message); + +const echoExtensionHost = async (message: string) => + papi.commands.sendCommand<[string], string>('echoExtensionHost', message); + +const addThree = async (a: number, b: number, c: number) => + papi.commands.sendCommand<[number, number, number], number>( + 'addThree', + a, + b, + c, + ); + +const addMany = async (...nums: number[]) => + papi.commands.sendCommand('addMany', ...nums); + const throwError = async (message: string) => papi.commands.sendCommand<[string], string>('throwError', message); @@ -114,11 +131,79 @@ const Hello = () => { > Echo + + + + +