aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main/src')
-rw-r--r--packages/main/src/controllers/__tests__/initConfig.spec.ts2
-rw-r--r--packages/main/src/controllers/initConfig.ts14
-rw-r--r--packages/main/src/devTools.ts3
-rw-r--r--packages/main/src/index.ts72
-rw-r--r--packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts24
-rw-r--r--packages/main/src/stores/Config.ts9
-rw-r--r--packages/main/src/utils/log.ts5
7 files changed, 68 insertions, 61 deletions
diff --git a/packages/main/src/controllers/__tests__/initConfig.spec.ts b/packages/main/src/controllers/__tests__/initConfig.spec.ts
index 9a5f85e..11e7690 100644
--- a/packages/main/src/controllers/__tests__/initConfig.spec.ts
+++ b/packages/main/src/controllers/__tests__/initConfig.spec.ts
@@ -133,7 +133,7 @@ describe('when it has loaded the config', () => {
133 }); 133 });
134 134
135 it('should throttle saving changes to the config file', () => { 135 it('should throttle saving changes to the config file', () => {
136 mocked(persistenceService.writeConfig).mockResolvedValue(undefined); 136 mocked(persistenceService.writeConfig).mockResolvedValue();
137 config.setThemeSource('dark'); 137 config.setThemeSource('dark');
138 jest.advanceTimersByTime(lessThanThrottleMs); 138 jest.advanceTimersByTime(lessThanThrottleMs);
139 config.setThemeSource('light'); 139 config.setThemeSource('light');
diff --git a/packages/main/src/controllers/initConfig.ts b/packages/main/src/controllers/initConfig.ts
index e83b8da..915f451 100644
--- a/packages/main/src/controllers/initConfig.ts
+++ b/packages/main/src/controllers/initConfig.ts
@@ -38,7 +38,7 @@ export default async function initConfig(
38): Promise<Disposer> { 38): Promise<Disposer> {
39 log.trace('Initializing config controller'); 39 log.trace('Initializing config controller');
40 40
41 let lastSnapshotOnDisk: ConfigSnapshotOut | null = null; 41 let lastSnapshotOnDisk: ConfigSnapshotOut | undefined;
42 42
43 async function readConfig(): Promise<boolean> { 43 async function readConfig(): Promise<boolean> {
44 const result = await persistenceService.readConfig(); 44 const result = await persistenceService.readConfig();
@@ -46,8 +46,8 @@ export default async function initConfig(
46 try { 46 try {
47 applySnapshot(config, result.data); 47 applySnapshot(config, result.data);
48 lastSnapshotOnDisk = getSnapshot(config); 48 lastSnapshotOnDisk = getSnapshot(config);
49 } catch (err) { 49 } catch (error) {
50 log.error('Failed to apply config snapshot', result.data, err); 50 log.error('Failed to apply config snapshot', result.data, error);
51 } 51 }
52 } 52 }
53 return result.found; 53 return result.found;
@@ -70,8 +70,8 @@ export default async function initConfig(
70 debounce((snapshot) => { 70 debounce((snapshot) => {
71 // We can compare snapshots by reference, since it is only recreated on store changes. 71 // We can compare snapshots by reference, since it is only recreated on store changes.
72 if (lastSnapshotOnDisk !== snapshot) { 72 if (lastSnapshotOnDisk !== snapshot) {
73 writeConfig().catch((err) => { 73 writeConfig().catch((error) => {
74 log.error('Failed to write config on config change', err); 74 log.error('Failed to write config on config change', error);
75 }); 75 });
76 } 76 }
77 }, debounceTime), 77 }, debounceTime),
@@ -80,8 +80,8 @@ export default async function initConfig(
80 const disposeWatcher = persistenceService.watchConfig(async () => { 80 const disposeWatcher = persistenceService.watchConfig(async () => {
81 try { 81 try {
82 await readConfig(); 82 await readConfig();
83 } catch (err) { 83 } catch (error) {
84 log.error('Failed to read config', err); 84 log.error('Failed to read config', error);
85 } 85 }
86 }, debounceTime); 86 }, debounceTime);
87 87
diff --git a/packages/main/src/devTools.ts b/packages/main/src/devTools.ts
index 69f1514..4ca1bf3 100644
--- a/packages/main/src/devTools.ts
+++ b/packages/main/src/devTools.ts
@@ -49,7 +49,8 @@ export async function installDevToolsExtensions(): Promise<void> {
49 /* eslint-disable-next-line 49 /* eslint-disable-next-line
50 import/no-extraneous-dependencies, 50 import/no-extraneous-dependencies,
51 global-require, 51 global-require,
52 @typescript-eslint/no-var-requires 52 @typescript-eslint/no-var-requires,
53 unicorn/prefer-module
53 */ 54 */
54 } = require('electron-devtools-installer') as typeof import('electron-devtools-installer'); 55 } = require('electron-devtools-installer') as typeof import('electron-devtools-installer');
55 await installExtension([REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS], { 56 await installExtension([REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS], {
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts
index 1f80e44..02e6cda 100644
--- a/packages/main/src/index.ts
+++ b/packages/main/src/index.ts
@@ -19,9 +19,9 @@
19 * SPDX-License-Identifier: AGPL-3.0-only 19 * SPDX-License-Identifier: AGPL-3.0-only
20 */ 20 */
21 21
22import { arch } from 'os'; 22import { arch } from 'node:os';
23import { join } from 'path'; 23import path from 'node:path';
24import { URL } from 'url'; 24import { URL } from 'node:url';
25 25
26import { 26import {
27 ServiceToMainIpcMessage, 27 ServiceToMainIpcMessage,
@@ -101,11 +101,14 @@ app.setAboutPanelOptions({
101 version: '', 101 version: '',
102}); 102});
103 103
104// eslint-disable-next-line unicorn/prefer-module -- Electron apps run in a commonjs environment.
105const thisDir = __dirname;
106
104function getResourcePath(relativePath: string): string { 107function getResourcePath(relativePath: string): string {
105 return join(__dirname, relativePath); 108 return path.join(thisDir, relativePath);
106} 109}
107 110
108const baseUrl = `file://${__dirname}`; 111const baseUrl = `file://${thisDir}`;
109function getResourceUrl(relativePath: string): string { 112function getResourceUrl(relativePath: string): string {
110 return new URL(relativePath, baseUrl).toString(); 113 return new URL(relativePath, baseUrl).toString();
111} 114}
@@ -117,15 +120,15 @@ const serviceInject: WebSource = {
117 url: getResourceUrl(serviceInjectRelativePath), 120 url: getResourceUrl(serviceInjectRelativePath),
118}; 121};
119 122
120let mainWindow: BrowserWindow | null = null; 123let mainWindow: BrowserWindow | undefined;
121 124
122const store = createMainStore(); 125const store = createMainStore();
123init(store) 126init(store)
124 .then((disposeCompositionRoot) => { 127 .then((disposeCompositionRoot) => {
125 app.on('will-quit', disposeCompositionRoot); 128 app.on('will-quit', disposeCompositionRoot);
126 }) 129 })
127 .catch((err) => { 130 .catch((error) => {
128 log.log('Failed to initialize application', err); 131 log.log('Failed to initialize application', error);
129 }); 132 });
130 133
131const rendererBaseUrl = getResourceUrl('../renderer/'); 134const rendererBaseUrl = getResourceUrl('../renderer/');
@@ -136,7 +139,7 @@ function shouldCancelMainWindowRequest(url: string, method: string): boolean {
136 let normalizedUrl: string; 139 let normalizedUrl: string;
137 try { 140 try {
138 normalizedUrl = new URL(url).toString(); 141 normalizedUrl = new URL(url).toString();
139 } catch (_err) { 142 } catch {
140 return true; 143 return true;
141 } 144 }
142 if (isDevelopment) { 145 if (isDevelopment) {
@@ -233,7 +236,7 @@ async function createWindow(): Promise<unknown> {
233 'from webContents', 236 'from webContents',
234 event.sender.id, 237 event.sender.id,
235 ); 238 );
236 return null; 239 throw new Error('Invalid IPC call');
237 } 240 }
238 return getSnapshot(store.shared); 241 return getSnapshot(store.shared);
239 }); 242 });
@@ -262,22 +265,22 @@ async function createWindow(): Promise<unknown> {
262 .then((data) => { 265 .then((data) => {
263 serviceInject.code = data; 266 serviceInject.code = data;
264 }) 267 })
265 .catch((err) => { 268 .catch((error) => {
266 log.error('Error while reloading', serviceInjectPath, err); 269 log.error('Error while reloading', serviceInjectPath, error);
267 }) 270 })
268 .then(() => { 271 .then(() => {
269 browserView.webContents.reload(); 272 browserView.webContents.reload();
270 }) 273 })
271 .catch((err) => { 274 .catch((error) => {
272 log.error('Failed to reload browserView', err); 275 log.error('Failed to reload browserView', error);
273 }); 276 });
274 break; 277 break;
275 default: 278 default:
276 log.error('Unexpected action from UI renderer:', actionToDispatch); 279 log.error('Unexpected action from UI renderer:', actionToDispatch);
277 break; 280 break;
278 } 281 }
279 } catch (err) { 282 } catch (error) {
280 log.error('Error while dispatching renderer action', rawAction, err); 283 log.error('Error while dispatching renderer action', rawAction, error);
281 } 284 }
282 }); 285 });
283 286
@@ -285,9 +288,18 @@ async function createWindow(): Promise<unknown> {
285 webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); 288 webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch);
286 }); 289 });
287 290
288 ipcMain.handle(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) => 291 ipcMain.handle(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) => {
289 event.sender.id === browserView.webContents.id ? serviceInject : null, 292 if (event.sender.id !== browserView.webContents.id) {
290 ); 293 log.warn(
294 'Unexpected',
295 ServiceToMainIpcMessage.ApiExposedInMainWorld,
296 'from webContents',
297 event.sender.id,
298 );
299 throw new Error('Invalid IPC call');
300 }
301 return serviceInject;
302 });
291 303
292 browserView.webContents.on('ipc-message', (_event, channel, ...args) => { 304 browserView.webContents.on('ipc-message', (_event, channel, ...args) => {
293 try { 305 try {
@@ -303,8 +315,8 @@ async function createWindow(): Promise<unknown> {
303 log.error('Unknown IPC message:', channel, args); 315 log.error('Unknown IPC message:', channel, args);
304 break; 316 break;
305 } 317 }
306 } catch (err) { 318 } catch (error) {
307 log.error('Error while processing IPC message:', channel, args, err); 319 log.error('Error while processing IPC message:', channel, args, error);
308 } 320 }
309 }); 321 });
310 322
@@ -316,9 +328,7 @@ async function createWindow(): Promise<unknown> {
316 328
317 browserView.webContents.session.webRequest.onBeforeSendHeaders( 329 browserView.webContents.session.webRequest.onBeforeSendHeaders(
318 ({ url, requestHeaders }, callback) => { 330 ({ url, requestHeaders }, callback) => {
319 const requestUserAgent = url.match( 331 const requestUserAgent = /^[^:]+:\/\/accounts\.google\.[^./]+\//.test(url)
320 /^[^:]+:\/\/accounts\.google\.[^./]+\//,
321 )
322 ? chromelessUserAgent 332 ? chromelessUserAgent
323 : userAgent; 333 : userAgent;
324 callback({ 334 callback({
@@ -332,15 +342,15 @@ async function createWindow(): Promise<unknown> {
332 342
333 browserView.webContents 343 browserView.webContents
334 .loadURL('https://gitlab.com/say-hi-to-sophie/sophie') 344 .loadURL('https://gitlab.com/say-hi-to-sophie/sophie')
335 .catch((err) => { 345 .catch((error) => {
336 log.error('Failed to load browser', err); 346 log.error('Failed to load browser', error);
337 }); 347 });
338 348
339 return mainWindow.loadURL(pageUrl); 349 return mainWindow.loadURL(pageUrl);
340} 350}
341 351
342app.on('second-instance', () => { 352app.on('second-instance', () => {
343 if (mainWindow !== null) { 353 if (mainWindow !== undefined) {
344 if (!mainWindow.isVisible()) { 354 if (!mainWindow.isVisible()) {
345 mainWindow.show(); 355 mainWindow.show();
346 } 356 }
@@ -363,14 +373,14 @@ app
363 if (isDevelopment) { 373 if (isDevelopment) {
364 try { 374 try {
365 await installDevToolsExtensions(); 375 await installDevToolsExtensions();
366 } catch (err) { 376 } catch (error) {
367 log.error('Failed to install devtools extensions', err); 377 log.error('Failed to install devtools extensions', error);
368 } 378 }
369 } 379 }
370 380
371 return createWindow(); 381 return createWindow();
372 }) 382 })
373 .catch((err) => { 383 .catch((error) => {
374 log.error('Failed to create window', err); 384 log.error('Failed to create window', error);
375 process.exit(1); 385 process.exit(1);
376 }); 386 });
diff --git a/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts b/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts
index e92f706..a11a9da 100644
--- a/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts
+++ b/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts
@@ -17,9 +17,9 @@
17 * 17 *
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20import { watch } from 'fs'; 20import { watch } from 'node:fs';
21import { readFile, stat, writeFile } from 'fs/promises'; 21import { readFile, stat, writeFile } from 'node:fs/promises';
22import { join } from 'path'; 22import path from 'node:path';
23 23
24import JSON5 from 'json5'; 24import JSON5 from 'json5';
25import throttle from 'lodash-es/throttle'; 25import throttle from 'lodash-es/throttle';
@@ -39,26 +39,26 @@ export default class ConfigPersistenceServiceImpl
39 39
40 private writingConfig = false; 40 private writingConfig = false;
41 41
42 private timeLastWritten: Date | null = null; 42 private timeLastWritten: Date | undefined;
43 43
44 constructor( 44 constructor(
45 private readonly userDataDir: string, 45 private readonly userDataDir: string,
46 private readonly configFileName: string = 'config.json5', 46 private readonly configFileName: string = 'config.json5',
47 ) { 47 ) {
48 this.configFileName = configFileName; 48 this.configFileName = configFileName;
49 this.configFilePath = join(this.userDataDir, this.configFileName); 49 this.configFilePath = path.join(this.userDataDir, this.configFileName);
50 } 50 }
51 51
52 async readConfig(): Promise<ReadConfigResult> { 52 async readConfig(): Promise<ReadConfigResult> {
53 let configStr; 53 let configStr;
54 try { 54 try {
55 configStr = await readFile(this.configFilePath, 'utf8'); 55 configStr = await readFile(this.configFilePath, 'utf8');
56 } catch (err) { 56 } catch (error) {
57 if ((err as NodeJS.ErrnoException).code === 'ENOENT') { 57 if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
58 log.debug('Config file', this.configFilePath, 'was not found'); 58 log.debug('Config file', this.configFilePath, 'was not found');
59 return { found: false }; 59 return { found: false };
60 } 60 }
61 throw err; 61 throw error;
62 } 62 }
63 log.info('Read config file', this.configFilePath); 63 log.info('Read config file', this.configFilePath);
64 return { 64 return {
@@ -92,8 +92,8 @@ export default class ConfigPersistenceServiceImpl
92 const stats = await stat(this.configFilePath); 92 const stats = await stat(this.configFilePath);
93 mtime = stats.mtime; 93 mtime = stats.mtime;
94 log.trace('Config file last modified at', mtime); 94 log.trace('Config file last modified at', mtime);
95 } catch (err) { 95 } catch (error) {
96 if ((err as NodeJS.ErrnoException).code === 'ENOENT') { 96 if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
97 log.debug( 97 log.debug(
98 'Config file', 98 'Config file',
99 this.configFilePath, 99 this.configFilePath,
@@ -101,11 +101,11 @@ export default class ConfigPersistenceServiceImpl
101 ); 101 );
102 return; 102 return;
103 } 103 }
104 throw err; 104 throw error;
105 } 105 }
106 if ( 106 if (
107 !this.writingConfig && 107 !this.writingConfig &&
108 (this.timeLastWritten === null || mtime > this.timeLastWritten) 108 (this.timeLastWritten === undefined || mtime > this.timeLastWritten)
109 ) { 109 ) {
110 log.debug( 110 log.debug(
111 'Found a config file modified at', 111 'Found a config file modified at',
diff --git a/packages/main/src/stores/Config.ts b/packages/main/src/stores/Config.ts
index 06dbdeb..ca90c0c 100644
--- a/packages/main/src/stores/Config.ts
+++ b/packages/main/src/stores/Config.ts
@@ -18,12 +18,7 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { 21import { config as originalConfig, ThemeSource } from '@sophie/shared';
22 config as originalConfig,
23 ConfigSnapshotIn,
24 ConfigSnapshotOut,
25 ThemeSource,
26} from '@sophie/shared';
27import { Instance } from 'mobx-state-tree'; 22import { Instance } from 'mobx-state-tree';
28 23
29export const config = originalConfig.actions((self) => ({ 24export const config = originalConfig.actions((self) => ({
@@ -34,4 +29,4 @@ export const config = originalConfig.actions((self) => ({
34 29
35export interface Config extends Instance<typeof config> {} 30export interface Config extends Instance<typeof config> {}
36 31
37export type { ConfigSnapshotIn, ConfigSnapshotOut }; 32export type { ConfigSnapshotIn, ConfigSnapshotOut } from '@sophie/shared';
diff --git a/packages/main/src/utils/log.ts b/packages/main/src/utils/log.ts
index 5218721..0a632d8 100644
--- a/packages/main/src/utils/log.ts
+++ b/packages/main/src/utils/log.ts
@@ -18,8 +18,9 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21// eslint-disable-next-line unicorn/import-style -- Import the type `ChalkInstance` separately.
21import chalk, { ChalkInstance } from 'chalk'; 22import chalk, { ChalkInstance } from 'chalk';
22import loglevel, { Logger } from 'loglevel'; 23import loglevel from 'loglevel';
23import prefix from 'loglevel-plugin-prefix'; 24import prefix from 'loglevel-plugin-prefix';
24 25
25if (import.meta.env?.DEV) { 26if (import.meta.env?.DEV) {
@@ -54,7 +55,7 @@ prefix.apply(loglevel, {
54 }, 55 },
55}); 56});
56 57
57export function getLogger(loggerName: string): Logger { 58export function getLogger(loggerName: string): loglevel.Logger {
58 return loglevel.getLogger(loggerName); 59 return loglevel.getLogger(loggerName);
59} 60}
60 61