diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-04-08 02:10:22 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-05-16 00:55:00 +0200 |
commit | ab1cda612cf6d427bffb66d5674a3673eb958e50 (patch) | |
tree | aeb01a0b431ca850b75d276af745bde807851839 /packages | |
parent | fix(main): Do not show spurious abort error (diff) | |
download | sophie-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.ts | 39 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/types.ts | 2 | ||||
-rw-r--r-- | packages/main/src/initReactions.ts | 1 | ||||
-rw-r--r-- | packages/service-inject/esbuild.config.js | 2 | ||||
-rw-r--r-- | packages/service-inject/package.json | 2 | ||||
-rw-r--r-- | packages/service-preload/esbuild.config.js | 4 | ||||
-rw-r--r-- | packages/service-preload/package.json | 1 | ||||
-rw-r--r-- | packages/service-preload/src/index.ts | 35 | ||||
-rw-r--r-- | packages/service-preload/tsconfig.json | 7 | ||||
-rw-r--r-- | packages/service-preload/types/serviceInject.d.ts | 4 | ||||
-rw-r--r-- | packages/service-shared/src/index.ts | 2 | ||||
-rw-r--r-- | packages/service-shared/src/ipc.ts | 1 | ||||
-rw-r--r-- | packages/service-shared/src/schemas.ts | 12 |
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 | ||
21 | import { readFile } from 'node:fs/promises'; | ||
22 | |||
23 | import { ServiceToMainIpcMessage } from '@sophie/service-shared'; | ||
24 | import { ipcMain, WebSource } from 'electron'; | ||
25 | |||
26 | import type MainStore from '../../../stores/MainStore'; | 21 | import type MainStore from '../../../stores/MainStore'; |
27 | import type Profile from '../../../stores/Profile'; | 22 | import type Profile from '../../../stores/Profile'; |
28 | import type Service from '../../../stores/Service'; | 23 | import type Service from '../../../stores/Service'; |
29 | import { getLogger } from '../../../utils/log'; | ||
30 | import type Resources from '../../resources/Resources'; | 24 | import type Resources from '../../resources/Resources'; |
31 | import type UserAgents from '../UserAgents'; | 25 | import type UserAgents from '../UserAgents'; |
32 | import type { MainWindow, Partition, ServiceView, ViewFactory } from '../types'; | 26 | import type { MainWindow, Partition, ServiceView, ViewFactory } from '../types'; |
@@ -35,37 +29,17 @@ import ElectronMainWindow from './ElectronMainWindow'; | |||
35 | import ElectronPartition from './ElectronPartition'; | 29 | import ElectronPartition from './ElectronPartition'; |
36 | import ElectronServiceView from './ElectronServiceView'; | 30 | import ElectronServiceView from './ElectronServiceView'; |
37 | 31 | ||
38 | const log = getLogger('ElectronViewFactory'); | ||
39 | |||
40 | export default class ElectronViewFactory implements ViewFactory { | 32 | export 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 @@ | |||
1 | import { chrome } from '../../config/buildConstants.js'; | 1 | import { chrome } from '../../config/buildConstants.js'; |
2 | import fileUrlToDirname from '../../config/fileUrlToDirname.js'; | 2 | import fileUrlToDirname from '../../config/fileUrlToDirname.js'; |
3 | import getEsbuildConfig from '../../config/getEsbuildConfig.js'; | 3 | import getEsbuildConfig from '../../config/getEsbuildConfig.js'; |
4 | import srcPlugin from '../../config/srcPlugin.js'; | ||
4 | 5 | ||
5 | export default getEsbuildConfig({ | 6 | export 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 | ||
21 | import { ServiceToMainIpcMessage, WebSource } from '@sophie/service-shared'; | ||
22 | import colorString from 'color-string'; | 21 | import colorString from 'color-string'; |
23 | import { ipcRenderer, webFrame } from 'electron'; | 22 | import { webFrame } from 'electron'; |
23 | // eslint-disable-next-line import/no-unresolved -- Synthetic import provided by an eslint plugin. | ||
24 | import injectSource from 'sophie-src:@sophie/service-inject'; | ||
24 | 25 | ||
25 | const DEFAULT_BG_COLOR = '#fff'; | 26 | const 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 | */ |
102 | async function fetchAndExecuteInjectScript(): Promise<void> { | 97 | async 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 | ||
111 | fetchAndExecuteInjectScript().catch((error) => { | 106 | fetchAndExecuteInjectScript().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 @@ | |||
1 | declare 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 | ||
21 | export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc'; | 21 | export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc'; |
22 | 22 | ||
23 | export { UnreadCount, WebSource } from './schemas'; | 23 | export { 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 @@ | |||
21 | export enum MainToServiceIpcMessage {} | 21 | export enum MainToServiceIpcMessage {} |
22 | 22 | ||
23 | export enum ServiceToMainIpcMessage { | 23 | export 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 | */ |
33 | export type UnreadCount = z.infer<typeof UnreadCount>; | 33 | export type UnreadCount = z.infer<typeof UnreadCount>; |
34 | |||
35 | export 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 | */ | ||
45 | export type WebSource = z.infer<typeof WebSource>; | ||