aboutsummaryrefslogtreecommitdiffstats
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
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>
-rw-r--r--.electron-builder.config.cjs1
-rw-r--r--config/srcPlugin.js85
-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
-rw-r--r--scripts/build.js9
-rw-r--r--scripts/watch.js90
-rw-r--r--yarn.lock3
18 files changed, 162 insertions, 138 deletions
diff --git a/.electron-builder.config.cjs b/.electron-builder.config.cjs
index 4340623..efd9ef7 100644
--- a/.electron-builder.config.cjs
+++ b/.electron-builder.config.cjs
@@ -15,7 +15,6 @@ const config = {
15 'packages/main/dist/**', 15 'packages/main/dist/**',
16 'packages/preload/dist/**', 16 'packages/preload/dist/**',
17 'packages/renderer/dist/**', 17 'packages/renderer/dist/**',
18 'packages/service-inject/dist/**',
19 'packages/service-preload/dist/**', 18 'packages/service-preload/dist/**',
20 'locales/**', 19 'locales/**',
21 // Do not ship with source maps. 20 // Do not ship with source maps.
diff --git a/config/srcPlugin.js b/config/srcPlugin.js
new file mode 100644
index 0000000..13bee7b
--- /dev/null
+++ b/config/srcPlugin.js
@@ -0,0 +1,85 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import { readFile } from 'node:fs/promises';
22
23/** @type {string} */
24const PLUGIN_NAMESPACE = 'sophie-src';
25
26/** @type {RegExp} */
27// eslint-disable-next-line security/detect-non-literal-regexp -- Argument is const.
28const FILTER_REGEXP = new RegExp(`^${PLUGIN_NAMESPACE}:(.+)$`);
29
30/**
31 * An esbuild plugin that import the source of referenced file or package as a string.
32 *
33 * Prefix the imported file name with `sophie-src:` to import its source instead.
34 *
35 * @type { import('esbuild').Plugin }
36 */
37const srcPlugin = {
38 name: 'sophie-inline-service-inject-plugin',
39 setup(build) {
40 build.onResolve(
41 {
42 filter: FILTER_REGEXP,
43 },
44 async ({ path: pathToResolve, importer, resolveDir }) => {
45 const match = FILTER_REGEXP.exec(pathToResolve);
46 if (match === null) {
47 return {
48 path: '',
49 namespace: PLUGIN_NAMESPACE,
50 errors: [
51 {
52 text: `Could not resolve ${pathToResolve}`,
53 },
54 ],
55 };
56 }
57 const { path, errors, warnings } = await build.resolve(match[1], {
58 importer,
59 resolveDir,
60 kind: 'import-statement',
61 });
62 return {
63 path,
64 namespace: PLUGIN_NAMESPACE,
65 errors,
66 warnings,
67 };
68 },
69 );
70 build.onLoad(
71 {
72 filter: /./,
73 namespace: PLUGIN_NAMESPACE,
74 },
75 async ({ path }) => {
76 const src = await readFile(path, 'utf8');
77 return {
78 contents: `export default ${JSON.stringify(src)};`,
79 };
80 },
81 );
82 },
83};
84
85export default srcPlugin;
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>;
diff --git a/scripts/build.js b/scripts/build.js
index 66a1506..eb0e8a3 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -37,12 +37,9 @@ function buildAll() {
37 Promise.all([buildServiceShared, buildShared]).then(() => 37 Promise.all([buildServiceShared, buildShared]).then(() =>
38 buildPackageEsbuild('main'), 38 buildPackageEsbuild('main'),
39 ), 39 ),
40 buildServiceShared.then(() => 40 buildServiceShared
41 Promise.all([ 41 .then(() => buildPackageEsbuild('service-inject'))
42 buildPackageEsbuild('service-inject'), 42 .then(() => buildPackageEsbuild('service-preload')),
43 buildPackageEsbuild('service-preload'),
44 ]),
45 ),
46 buildShared.then(() => 43 buildShared.then(() =>
47 Promise.all([ 44 Promise.all([
48 buildPackageEsbuild('preload'), 45 buildPackageEsbuild('preload'),
diff --git a/scripts/watch.js b/scripts/watch.js
index 9cd65c8..3c3a8fd 100644
--- a/scripts/watch.js
+++ b/scripts/watch.js
@@ -1,6 +1,5 @@
1import { spawn } from 'node:child_process'; 1import { spawn } from 'node:child_process';
2import path from 'node:path'; 2import path from 'node:path';
3import { exit, kill } from 'node:process';
4 3
5import { watch } from 'chokidar'; 4import { watch } from 'chokidar';
6import electronPath from 'electron'; 5import electronPath from 'electron';
@@ -21,6 +20,12 @@ const serviceSharedModule = path.join(
21 '../packages/service-shared/dist/index.mjs', 20 '../packages/service-shared/dist/index.mjs',
22); 21);
23 22
23/** @type {string} */
24const serviceInjectModule = path.join(
25 thisDir,
26 '../packages/service-inject/dist/index.js',
27);
28
24/** @type {RegExp[]} */ 29/** @type {RegExp[]} */
25const stderrIgnorePatterns = [ 30const stderrIgnorePatterns = [
26 // warning about devtools extension 31 // warning about devtools extension
@@ -111,50 +116,6 @@ async function setupEsbuildWatcher(packageName, extraPaths, callback) {
111} 116}
112 117
113/** 118/**
114 * @param {string} packageName
115 * @returns {Promise<import('vite').ViteDevServer>}
116 */
117async function setupDevServer(packageName) {
118 const viteDevServer = await createServer({
119 build: {
120 watch: {
121 skipWrite: true,
122 clearScreen: false,
123 },
124 },
125 configFile: path.join(thisDir, `../packages/${packageName}/vite.config.js`),
126 });
127 await viteDevServer.listen();
128 return viteDevServer;
129}
130
131/**
132 * @param {(event: import('vite').HMRPayload) => void} sendEvent
133 * @returns {Promise<void>}
134 */
135function setupPreloadPackageWatcher(sendEvent) {
136 return setupEsbuildWatcher('preload', [sharedModule], () => {
137 sendEvent({
138 type: 'full-reload',
139 });
140 });
141}
142
143/**
144 * @param {string} packageName
145 * @param {(event: import('vite').HMRPayload) => void} sendEvent
146 * @returns {Promise<void>}
147 */
148function setupServicePackageWatcher(packageName, sendEvent) {
149 return setupEsbuildWatcher(packageName, [serviceSharedModule], () => {
150 sendEvent({
151 type: 'custom',
152 event: 'sophie:reload-services',
153 });
154 });
155}
156
157/**
158 * @param {import('vite').ViteDevServer} viteDevServer 119 * @param {import('vite').ViteDevServer} viteDevServer
159 * @returns {Promise<void>} 120 * @returns {Promise<void>}
160 */ 121 */
@@ -280,7 +241,16 @@ async function setupDevEnvironment() {
280 * @returns {Promise<void>} 241 * @returns {Promise<void>}
281 */ 242 */
282 async function startDevServer() { 243 async function startDevServer() {
283 viteDevServer = await setupDevServer('renderer'); 244 viteDevServer = await createServer({
245 build: {
246 watch: {
247 skipWrite: true,
248 clearScreen: false,
249 },
250 },
251 configFile: path.join(thisDir, `../packages/renderer/vite.config.js`),
252 });
253 await viteDevServer.listen();
284 } 254 }
285 255
286 /** 256 /**
@@ -289,7 +259,11 @@ async function setupDevEnvironment() {
289 async function watchRendererPackages() { 259 async function watchRendererPackages() {
290 await setupEsbuildWatcher('shared'); 260 await setupEsbuildWatcher('shared');
291 await Promise.all([ 261 await Promise.all([
292 setupPreloadPackageWatcher(sendEvent), 262 setupEsbuildWatcher('preload', [sharedModule], () =>
263 sendEvent({
264 type: 'full-reload',
265 }),
266 ),
293 startDevServer(), 267 startDevServer(),
294 ]); 268 ]);
295 } 269 }
@@ -299,21 +273,29 @@ async function setupDevEnvironment() {
299 */ 273 */
300 async function watchServicePackages() { 274 async function watchServicePackages() {
301 await setupEsbuildWatcher('service-shared'); 275 await setupEsbuildWatcher('service-shared');
302 await Promise.all([ 276 await setupEsbuildWatcher('service-inject', [serviceSharedModule]);
303 setupServicePackageWatcher('service-inject', sendEvent), 277 await setupEsbuildWatcher(
304 setupServicePackageWatcher('service-preload', sendEvent), 278 'service-preload',
305 ]); 279 [serviceSharedModule, serviceInjectModule],
280 () =>
281 sendEvent({
282 type: 'custom',
283 event: 'sophie:reload-services',
284 }),
285 );
306 } 286 }
307 287
308 await Promise.all([watchRendererPackages(), watchServicePackages()]); 288 await Promise.all([
289 watchRendererPackages(),
290 watchServicePackages(),
291 setupTranslationsWatcher(sendEvent),
292 ]);
309 293
310 if (viteDevServer === undefined) { 294 if (viteDevServer === undefined) {
311 console.error('Failed to create vite dev server'); 295 console.error('Failed to create vite dev server');
312 return; 296 return;
313 } 297 }
314 298
315 setupTranslationsWatcher(sendEvent);
316
317 console.log('\uD83C\uDF80 Sophie is starting up'); 299 console.log('\uD83C\uDF80 Sophie is starting up');
318 await setupMainPackageWatcher(viteDevServer); 300 await setupMainPackageWatcher(viteDevServer);
319} 301}
diff --git a/yarn.lock b/yarn.lock
index 5a844c8..79a8a17 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1356,7 +1356,7 @@ __metadata:
1356 languageName: unknown 1356 languageName: unknown
1357 linkType: soft 1357 linkType: soft
1358 1358
1359"@sophie/service-inject@workspace:packages/service-inject": 1359"@sophie/service-inject@workspace:*, @sophie/service-inject@workspace:packages/service-inject":
1360 version: 0.0.0-use.local 1360 version: 0.0.0-use.local
1361 resolution: "@sophie/service-inject@workspace:packages/service-inject" 1361 resolution: "@sophie/service-inject@workspace:packages/service-inject"
1362 dependencies: 1362 dependencies:
@@ -1368,6 +1368,7 @@ __metadata:
1368 version: 0.0.0-use.local 1368 version: 0.0.0-use.local
1369 resolution: "@sophie/service-preload@workspace:packages/service-preload" 1369 resolution: "@sophie/service-preload@workspace:packages/service-preload"
1370 dependencies: 1370 dependencies:
1371 "@sophie/service-inject": "workspace:*"
1371 "@sophie/service-shared": "workspace:*" 1372 "@sophie/service-shared": "workspace:*"
1372 "@types/color-string": ^1.5.2 1373 "@types/color-string": ^1.5.2
1373 color-string: ^1.9.0 1374 color-string: ^1.9.0