aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-04-08 02:10:22 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:55:00 +0200
commitab1cda612cf6d427bffb66d5674a3673eb958e50 (patch)
treeaeb01a0b431ca850b75d276af745bde807851839 /packages
parentfix(main): Do not show spurious abort error (diff)
downloadsophie-ab1cda612cf6d427bffb66d5674a3673eb958e50.tar.gz
sophie-ab1cda612cf6d427bffb66d5674a3673eb958e50.tar.zst
sophie-ab1cda612cf6d427bffb66d5674a3673eb958e50.zip
feat(service-preload): Embed service-inject
Embed the service-inject script into the service-preload script to avoid having to load it manually and reduce IPC communication when a service loads. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts39
-rw-r--r--packages/main/src/infrastructure/electron/types.ts2
-rw-r--r--packages/main/src/initReactions.ts1
-rw-r--r--packages/service-inject/esbuild.config.js2
-rw-r--r--packages/service-inject/package.json2
-rw-r--r--packages/service-preload/esbuild.config.js4
-rw-r--r--packages/service-preload/package.json1
-rw-r--r--packages/service-preload/src/index.ts35
-rw-r--r--packages/service-preload/tsconfig.json7
-rw-r--r--packages/service-preload/types/serviceInject.d.ts4
-rw-r--r--packages/service-shared/src/index.ts2
-rw-r--r--packages/service-shared/src/ipc.ts1
-rw-r--r--packages/service-shared/src/schemas.ts12
13 files changed, 36 insertions, 76 deletions
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts b/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts
index 884643a..c72860d 100644
--- a/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts
+++ b/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts
@@ -18,15 +18,9 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { readFile } from 'node:fs/promises';
22
23import { ServiceToMainIpcMessage } from '@sophie/service-shared';
24import { ipcMain, WebSource } from 'electron';
25
26import type MainStore from '../../../stores/MainStore'; 21import type MainStore from '../../../stores/MainStore';
27import type Profile from '../../../stores/Profile'; 22import type Profile from '../../../stores/Profile';
28import type Service from '../../../stores/Service'; 23import type Service from '../../../stores/Service';
29import { getLogger } from '../../../utils/log';
30import type Resources from '../../resources/Resources'; 24import type Resources from '../../resources/Resources';
31import type UserAgents from '../UserAgents'; 25import type UserAgents from '../UserAgents';
32import type { MainWindow, Partition, ServiceView, ViewFactory } from '../types'; 26import type { MainWindow, Partition, ServiceView, ViewFactory } from '../types';
@@ -35,37 +29,17 @@ import ElectronMainWindow from './ElectronMainWindow';
35import ElectronPartition from './ElectronPartition'; 29import ElectronPartition from './ElectronPartition';
36import ElectronServiceView from './ElectronServiceView'; 30import ElectronServiceView from './ElectronServiceView';
37 31
38const log = getLogger('ElectronViewFactory');
39
40export default class ElectronViewFactory implements ViewFactory { 32export default class ElectronViewFactory implements ViewFactory {
41 private readonly webContentsIdToServiceView = new Map< 33 private readonly webContentsIdToServiceView = new Map<
42 number, 34 number,
43 ElectronServiceView 35 ElectronServiceView
44 >(); 36 >();
45 37
46 private serviceInjectSource: WebSource | undefined;
47
48 constructor( 38 constructor(
49 readonly userAgents: UserAgents, 39 readonly userAgents: UserAgents,
50 readonly resources: Resources, 40 readonly resources: Resources,
51 readonly devMode: boolean, 41 readonly devMode: boolean,
52 ) { 42 ) {}
53 ipcMain.handle(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) => {
54 if (!this.webContentsIdToServiceView.has(event.sender.id)) {
55 log.error(
56 'Unexpected',
57 ServiceToMainIpcMessage.ApiExposedInMainWorld,
58 'IPC message from webContents',
59 event.sender.id,
60 );
61 throw new Error('Invalid IPC call');
62 }
63 if (this.serviceInjectSource === undefined) {
64 log.error('Service inject source was not loaded');
65 }
66 return this.serviceInjectSource;
67 });
68 }
69 43
70 async createMainWindow(store: MainStore): Promise<MainWindow> { 44 async createMainWindow(store: MainStore): Promise<MainWindow> {
71 const mainWindow = new ElectronMainWindow(store, this); 45 const mainWindow = new ElectronMainWindow(store, this);
@@ -94,23 +68,12 @@ export default class ElectronViewFactory implements ViewFactory {
94 throw new TypeError('Unexpected ProfileSession is not a WrappedSession'); 68 throw new TypeError('Unexpected ProfileSession is not a WrappedSession');
95 } 69 }
96 70
97 async loadServiceInject(): Promise<void> {
98 const injectPackage = 'service-inject';
99 const injectFile = 'index.js';
100 const injectPath = this.resources.getPath(injectPackage, injectFile);
101 this.serviceInjectSource = {
102 code: await readFile(injectPath, 'utf8'),
103 url: this.resources.getFileURL(injectPackage, injectFile),
104 };
105 }
106
107 dispose(): void { 71 dispose(): void {
108 if (this.webContentsIdToServiceView.size > 0) { 72 if (this.webContentsIdToServiceView.size > 0) {
109 throw new Error( 73 throw new Error(
110 'Must dispose all ServiceView instances before disposing ViewFactory', 74 'Must dispose all ServiceView instances before disposing ViewFactory',
111 ); 75 );
112 } 76 }
113 ipcMain.removeHandler(ServiceToMainIpcMessage.ApiExposedInMainWorld);
114 } 77 }
115 78
116 unregisterServiceView(id: number): void { 79 unregisterServiceView(id: number): void {
diff --git a/packages/main/src/infrastructure/electron/types.ts b/packages/main/src/infrastructure/electron/types.ts
index e5b0fd6..1321048 100644
--- a/packages/main/src/infrastructure/electron/types.ts
+++ b/packages/main/src/infrastructure/electron/types.ts
@@ -31,8 +31,6 @@ export interface ViewFactory {
31 31
32 createServiceView(service: Service, partition: Partition): ServiceView; 32 createServiceView(service: Service, partition: Partition): ServiceView;
33 33
34 loadServiceInject(): Promise<void>;
35
36 dispose(): void; 34 dispose(): void;
37} 35}
38 36
diff --git a/packages/main/src/initReactions.ts b/packages/main/src/initReactions.ts
index 94b1f06..05bc205 100644
--- a/packages/main/src/initReactions.ts
+++ b/packages/main/src/initReactions.ts
@@ -71,7 +71,6 @@ export default async function initReactions(
71 await devToolsLoaded; 71 await devToolsLoaded;
72 return viewFactory.createMainWindow(store); 72 return viewFactory.createMainWindow(store);
73 })(); 73 })();
74 await viewFactory.loadServiceInject();
75 loadServices(store, viewFactory); 74 loadServices(store, viewFactory);
76 store.setMainWindow(await mainWindow); 75 store.setMainWindow(await mainWindow);
77 return () => { 76 return () => {
diff --git a/packages/service-inject/esbuild.config.js b/packages/service-inject/esbuild.config.js
index d8698ac..fbc3e55 100644
--- a/packages/service-inject/esbuild.config.js
+++ b/packages/service-inject/esbuild.config.js
@@ -10,4 +10,6 @@ export default getEsbuildConfig({
10 platform: 'browser', 10 platform: 'browser',
11 target: chrome, 11 target: chrome,
12 sourcemap: 'inline', 12 sourcemap: 'inline',
13 // Absolute URL for displaying source map in the "Page" tab of devtools.
14 sourceRoot: 'sophie-internal://sophie/packages/service-inject/dist/',
13}); 15});
diff --git a/packages/service-inject/package.json b/packages/service-inject/package.json
index c045500..a9c6e08 100644
--- a/packages/service-inject/package.json
+++ b/packages/service-inject/package.json
@@ -4,7 +4,7 @@
4 "private": true, 4 "private": true,
5 "sideEffects": false, 5 "sideEffects": false,
6 "type": "module", 6 "type": "module",
7 "types": "dist-types/index.d.ts", 7 "main": "dist/index.js",
8 "scripts": { 8 "scripts": {
9 "typecheck:workspace": "yarn g:typecheck" 9 "typecheck:workspace": "yarn g:typecheck"
10 }, 10 },
diff --git a/packages/service-preload/esbuild.config.js b/packages/service-preload/esbuild.config.js
index 87e91d8..fb7359b 100644
--- a/packages/service-preload/esbuild.config.js
+++ b/packages/service-preload/esbuild.config.js
@@ -1,6 +1,7 @@
1import { chrome } from '../../config/buildConstants.js'; 1import { chrome } from '../../config/buildConstants.js';
2import fileUrlToDirname from '../../config/fileUrlToDirname.js'; 2import fileUrlToDirname from '../../config/fileUrlToDirname.js';
3import getEsbuildConfig from '../../config/getEsbuildConfig.js'; 3import getEsbuildConfig from '../../config/getEsbuildConfig.js';
4import srcPlugin from '../../config/srcPlugin.js';
4 5
5export default getEsbuildConfig({ 6export default getEsbuildConfig({
6 absWorkingDir: fileUrlToDirname(import.meta.url), 7 absWorkingDir: fileUrlToDirname(import.meta.url),
@@ -10,5 +11,8 @@ export default getEsbuildConfig({
10 platform: 'node', 11 platform: 'node',
11 target: chrome, 12 target: chrome,
12 sourcemap: 'inline', 13 sourcemap: 'inline',
14 // Absolute URL for displaying source map in the "Content scripts" tab of devtools.
15 sourceRoot: 'sophie-internal://sophie/packages/service-preload/dist/',
13 external: ['electron'], 16 external: ['electron'],
17 plugins: [srcPlugin],
14}); 18});
diff --git a/packages/service-preload/package.json b/packages/service-preload/package.json
index 20925ae..9307c2f 100644
--- a/packages/service-preload/package.json
+++ b/packages/service-preload/package.json
@@ -8,6 +8,7 @@
8 "typecheck:workspace": "yarn g:typecheck" 8 "typecheck:workspace": "yarn g:typecheck"
9 }, 9 },
10 "dependencies": { 10 "dependencies": {
11 "@sophie/service-inject": "workspace:*",
11 "@sophie/service-shared": "workspace:*", 12 "@sophie/service-shared": "workspace:*",
12 "color-string": "^1.9.0", 13 "color-string": "^1.9.0",
13 "electron": "18.0.1" 14 "electron": "18.0.1"
diff --git a/packages/service-preload/src/index.ts b/packages/service-preload/src/index.ts
index a49a3a4..99d02ec 100644
--- a/packages/service-preload/src/index.ts
+++ b/packages/service-preload/src/index.ts
@@ -18,9 +18,10 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { ServiceToMainIpcMessage, WebSource } from '@sophie/service-shared';
22import colorString from 'color-string'; 21import colorString from 'color-string';
23import { ipcRenderer, webFrame } from 'electron'; 22import { webFrame } from 'electron';
23// eslint-disable-next-line import/no-unresolved -- Synthetic import provided by an eslint plugin.
24import injectSource from 'sophie-src:@sophie/service-inject';
24 25
25const DEFAULT_BG_COLOR = '#fff'; 26const DEFAULT_BG_COLOR = '#fff';
26 27
@@ -79,35 +80,31 @@ if (webFrame.parent === null) {
79} 80}
80 81
81/** 82/**
82 * Fetches and executes the service inject script in the isolated world. 83 * Executes the service inject script in the isolated world.
83 * 84 *
84 * The service inject script relies on exposed APIs, so this function can only 85 * The service inject script relies on exposed APIs, so this function can only
85 * be called after APIs have been exposed via `contextBridge` to the main world. 86 * be called after APIs have been exposed via `contextBridge` to the main world.
86 * 87 *
87 * We have to call `executeJavaScriptInIsolatedWorld` from the service preload script, 88 * We embed the source code of the inject script into the preload script
88 * beause there is no way currently (electron 16) to execute a script on a 89 * with an esbuild plugin, so there is no need to fetch it separately.
89 * `WebFrameMain` in the main process by specifying a `WebSource`. 90 *
90 * Calling `executeJavaScriptInInsolatedWorld` on a `WebContents` in the main process
91 * will always inject the script into the _top-level_ frame, but here we
92 * are injecting into the _current_ frame instead.
93 * As a tradeoff, the promise returned by `executeJavaScriptInIsolatedWorld` 91 * As a tradeoff, the promise returned by `executeJavaScriptInIsolatedWorld`
94 * will resolve to `unknown` (instead of rejecting) even if the injected script fails, 92 * will resolve to `unknown` (instead of rejecting) even if the injected script fails,
95 * because chromium doesn't dispatch main world errors to isolated worlds. 93 * because chromium doesn't dispatch main world errors to isolated worlds.
96 * 94 *
97 * @return A promise that only rejects if we fail to fetch the inject script. 95 * @return A promise that always resolves to `undefined`.
98 * @see https://www.electronjs.org/docs/latest/api/web-frame#webframeexecutejavascriptinisolatedworldworldid-scripts-usergesture-callback
99 * @see https://www.electronjs.org/docs/latest/api/web-frame-main#frameexecutejavascriptcode-usergesture
100 * @see https://www.electronjs.org/docs/latest/api/web-contents#contentsexecutejavascriptinisolatedworldworldid-scripts-usergesture
101 */ 96 */
102async function fetchAndExecuteInjectScript(): Promise<void> { 97async function fetchAndExecuteInjectScript(): Promise<void> {
103 const apiExposedResponse: unknown = await ipcRenderer.invoke(
104 ServiceToMainIpcMessage.ApiExposedInMainWorld,
105 );
106 const injectSource = WebSource.parse(apiExposedResponse);
107 // Isolated world 0 is the main world. 98 // Isolated world 0 is the main world.
108 await webFrame.executeJavaScriptInIsolatedWorld(0, [injectSource]); 99 await webFrame.executeJavaScriptInIsolatedWorld(0, [
100 {
101 code: injectSource,
102 },
103 ]);
109} 104}
110 105
111fetchAndExecuteInjectScript().catch((error) => { 106fetchAndExecuteInjectScript().catch((error) => {
112 console.error('Failed to fetch inject source:', error); 107 // This will never happen because of
108 // https://www.electronjs.org/docs/latest/api/web-frame#webframeexecutejavascriptinisolatedworldworldid-scripts-usergesture-callback
109 console.error('Failed to execute service inject:', error);
113}); 110});
diff --git a/packages/service-preload/tsconfig.json b/packages/service-preload/tsconfig.json
index 33ce1de..d7fb8cb 100644
--- a/packages/service-preload/tsconfig.json
+++ b/packages/service-preload/tsconfig.json
@@ -9,5 +9,10 @@
9 "path": "../service-shared/tsconfig.build.json" 9 "path": "../service-shared/tsconfig.build.json"
10 } 10 }
11 ], 11 ],
12 "include": ["src/**/*.ts", ".eslintrc.cjs", "esbuild.config.js"] 12 "include": [
13 "src/**/*.ts",
14 "types/**/*.d.ts",
15 ".eslintrc.cjs",
16 "esbuild.config.js"
17 ]
13} 18}
diff --git a/packages/service-preload/types/serviceInject.d.ts b/packages/service-preload/types/serviceInject.d.ts
new file mode 100644
index 0000000..236973e
--- /dev/null
+++ b/packages/service-preload/types/serviceInject.d.ts
@@ -0,0 +1,4 @@
1declare module 'sophie-src:@sophie/service-inject' {
2 const src: string;
3 export default src;
4}
diff --git a/packages/service-shared/src/index.ts b/packages/service-shared/src/index.ts
index a2e5ee5..5165fe5 100644
--- a/packages/service-shared/src/index.ts
+++ b/packages/service-shared/src/index.ts
@@ -20,4 +20,4 @@
20 20
21export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc'; 21export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc';
22 22
23export { UnreadCount, WebSource } from './schemas'; 23export { UnreadCount } from './schemas';
diff --git a/packages/service-shared/src/ipc.ts b/packages/service-shared/src/ipc.ts
index e0a8755..4ead5bd 100644
--- a/packages/service-shared/src/ipc.ts
+++ b/packages/service-shared/src/ipc.ts
@@ -21,6 +21,5 @@
21export enum MainToServiceIpcMessage {} 21export enum MainToServiceIpcMessage {}
22 22
23export enum ServiceToMainIpcMessage { 23export enum ServiceToMainIpcMessage {
24 ApiExposedInMainWorld = 'sophie-service-to-main:api-exposed-in-main-world',
25 SetUnreadCount = 'sophie-service-to-main:set-unread-count', 24 SetUnreadCount = 'sophie-service-to-main:set-unread-count',
26} 25}
diff --git a/packages/service-shared/src/schemas.ts b/packages/service-shared/src/schemas.ts
index bb1926f..799faac 100644
--- a/packages/service-shared/src/schemas.ts
+++ b/packages/service-shared/src/schemas.ts
@@ -31,15 +31,3 @@ export const UnreadCount = /* @__PURE__ */ (() =>
31 Intentionally naming the type the same as the schema definition. 31 Intentionally naming the type the same as the schema definition.
32*/ 32*/
33export type UnreadCount = z.infer<typeof UnreadCount>; 33export type UnreadCount = z.infer<typeof UnreadCount>;
34
35export const WebSource = /* @__PURE__ */ (() =>
36 z.object({
37 code: z.string(),
38 url: z.string().nonempty(),
39 }))();
40
41/*
42 eslint-disable-next-line @typescript-eslint/no-redeclare --
43 Intentionally naming the type the same as the schema definition.
44*/
45export type WebSource = z.infer<typeof WebSource>;