aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-01-09 20:33:53 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-01-09 20:33:53 +0100
commitd85f09cbed5f3d2501f791e689011ae127df1cbb (patch)
tree0ed5be55dd5d3dec1d51eb60e7ff229274030a57
parentbuild: Disable single-run eslint-typescript (diff)
downloadsophie-d85f09cbed5f3d2501f791e689011ae127df1cbb.tar.gz
sophie-d85f09cbed5f3d2501f791e689011ae127df1cbb.tar.zst
sophie-d85f09cbed5f3d2501f791e689011ae127df1cbb.zip
build: Add prettier
eslint will also enforce prettier rules, so there is no need to call prettier separately in CI. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r--.electron-builder.config.cjs14
-rw-r--r--.eslintignore3
-rw-r--r--.eslintrc.cjs40
-rw-r--r--.prettierignore26
-rw-r--r--.yarnrc.yml10
-rw-r--r--CONTRIBUTING.md29
-rw-r--r--README.md16
-rw-r--r--config/buildConstants.js8
-rw-r--r--config/getEsbuildConfig.js2
-rw-r--r--config/jest.config.base.js20
-rw-r--r--config/tsconfig.base.json4
-rw-r--r--docs/architecture.md14
-rw-r--r--jest.config.js8
-rw-r--r--package.json4
-rw-r--r--packages/main/esbuild.config.js33
-rw-r--r--packages/main/src/controllers/__tests__/initConfig.spec.ts20
-rw-r--r--packages/main/src/controllers/__tests__/initNativeTheme.spec.ts8
-rw-r--r--packages/main/src/controllers/initConfig.ts21
-rw-r--r--packages/main/src/devTools.ts16
-rw-r--r--packages/main/src/index.ts108
-rw-r--r--packages/main/src/init.ts9
-rw-r--r--packages/main/src/services/ConfigPersistenceService.ts4
-rw-r--r--packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts22
-rw-r--r--packages/main/src/stores/MainStore.ts46
-rw-r--r--packages/main/src/stores/SharedStore.ts5
-rw-r--r--packages/main/src/utils/log.ts7
-rw-r--r--packages/main/tsconfig.json5
-rw-r--r--packages/main/types/importMeta.d.ts2
-rw-r--r--packages/preload/esbuild.config.js8
-rw-r--r--packages/preload/src/contextBridge/__tests__/createSophieRenderer.spec.ts56
-rw-r--r--packages/preload/src/contextBridge/createSophieRenderer.ts29
-rw-r--r--packages/preload/tsconfig.json10
-rw-r--r--packages/preload/types/importMeta.d.ts2
-rw-r--r--packages/renderer/.eslinrc.cjs5
-rw-r--r--packages/renderer/index.html9
-rw-r--r--packages/renderer/src/components/BrowserViewPlaceholder.tsx55
-rw-r--r--packages/renderer/src/components/StoreProvider.tsx13
-rw-r--r--packages/renderer/src/components/ThemeProvider.tsx28
-rw-r--r--packages/renderer/src/components/ToggleDarkModeButton.tsx4
-rw-r--r--packages/renderer/src/devTools.ts4
-rw-r--r--packages/renderer/src/stores/RendererStore.ts81
-rw-r--r--packages/renderer/tsconfig.json10
-rw-r--r--packages/renderer/vite.config.js10
-rw-r--r--packages/service-inject/.eslintrc.cjs5
-rw-r--r--packages/service-inject/esbuild.config.js4
-rw-r--r--packages/service-inject/tsconfig.json12
-rw-r--r--packages/service-preload/esbuild.config.js8
-rw-r--r--packages/service-preload/tsconfig.json6
-rw-r--r--packages/service-preload/types/importMeta.d.ts2
-rw-r--r--packages/service-shared/.eslintrc.cjs5
-rw-r--r--packages/service-shared/esbuild.config.js8
-rw-r--r--packages/service-shared/src/index.ts10
-rw-r--r--packages/service-shared/src/ipc.ts3
-rw-r--r--packages/service-shared/tsconfig.build.json4
-rw-r--r--packages/service-shared/tsconfig.json6
-rw-r--r--packages/shared/.eslintrc.cjs5
-rw-r--r--packages/shared/esbuild.config.js10
-rw-r--r--packages/shared/src/index.ts18
-rw-r--r--packages/shared/src/stores/Config.ts7
-rw-r--r--packages/shared/src/stores/SharedStore.ts3
-rw-r--r--packages/shared/tsconfig.build.json4
-rw-r--r--packages/shared/tsconfig.json6
-rw-r--r--prettier.config.cjs4
-rw-r--r--scripts/build.js31
-rw-r--r--scripts/update-electron-vendors.js26
-rw-r--r--scripts/watch.js101
-rw-r--r--tsconfig.json3
-rw-r--r--yarn.lock57
68 files changed, 636 insertions, 540 deletions
diff --git a/.electron-builder.config.cjs b/.electron-builder.config.cjs
index 9e2d11f..f406cc8 100644
--- a/.electron-builder.config.cjs
+++ b/.electron-builder.config.cjs
@@ -43,18 +43,20 @@ const config = {
43 */ 43 */
44async function burnFuses(context) { 44async function burnFuses(context) {
45 /** @type {string} */ 45 /** @type {string} */
46 const ext = { 46 const ext =
47 darwin: '.app', 47 {
48 win32: '.exe', 48 darwin: '.app',
49 }[context.electronPlatformName] || ''; 49 win32: '.exe',
50 }[context.electronPlatformName] || '';
50 const electronBinaryPath = join( 51 const electronBinaryPath = join(
51 context.appOutDir, 52 context.appOutDir,
52 `${context.packager.appInfo.productFilename}${ext}` 53 `${context.packager.appInfo.productFilename}${ext}`,
53 ); 54 );
54 /** @type {import('@electron/fuses').FuseConfig<boolean>} */ 55 /** @type {import('@electron/fuses').FuseConfig<boolean>} */
55 const fuseConfig = { 56 const fuseConfig = {
56 version: FuseVersion.V1, 57 version: FuseVersion.V1,
57 resetAdHocDarwinSignature: context.electronPlatformName === 'darwin' && context.arch === Arch.arm64, 58 resetAdHocDarwinSignature:
59 context.electronPlatformName === 'darwin' && context.arch === Arch.arm64,
58 [FuseV1Options.RunAsNode]: false, 60 [FuseV1Options.RunAsNode]: false,
59 [FuseV1Options.EnableCookieEncryption]: true, 61 [FuseV1Options.EnableCookieEncryption]: true,
60 [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, 62 [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
diff --git a/.eslintignore b/.eslintignore
index 01b8bcb..afc6ff1 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -22,8 +22,5 @@ coverage
22.idea 22.idea
23npm-debug.log.* 23npm-debug.log.*
24.pnpm-debug.log* 24.pnpm-debug.log*
25*.css.d.ts
26*.sass.d.ts
27*.scss.d.ts
28 25
29dist/ 26dist/
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 7b05d5d..1301de4 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -1,19 +1,15 @@
1const project = [ 1const project = ['./tsconfig.json', './packages/*/tsconfig.json'];
2 './tsconfig.json',
3 './packages/*/tsconfig.json',
4];
5 2
6module.exports = { 3module.exports = {
7 root: true, 4 root: true,
8 plugins: [ 5 plugins: ['@typescript-eslint'],
9 '@typescript-eslint',
10 ],
11 extends: [ 6 extends: [
12 'airbnb', 7 'airbnb',
13 'airbnb-typescript', 8 'airbnb-typescript',
14 'airbnb/hooks', 9 'airbnb/hooks',
15 'plugin:@typescript-eslint/recommended', 10 'plugin:@typescript-eslint/recommended',
16 'plugin:@typescript-eslint/recommended-requiring-type-checking', 11 'plugin:@typescript-eslint/recommended-requiring-type-checking',
12 'plugin:prettier/recommended',
17 ], 13 ],
18 env: { 14 env: {
19 es6: true, 15 es6: true,
@@ -56,9 +52,7 @@ module.exports = {
56 }, 52 },
57 overrides: [ 53 overrides: [
58 { 54 {
59 files: [ 55 files: ['**/stores/**/*.ts'],
60 '**/stores/**/*.ts',
61 ],
62 rules: { 56 rules: {
63 // In a mobx-state-tree action, we assign to the properties of `self` to update the store. 57 // In a mobx-state-tree action, we assign to the properties of `self` to update the store.
64 'no-param-reassign': 'off', 58 'no-param-reassign': 'off',
@@ -67,10 +61,7 @@ module.exports = {
67 }, 61 },
68 }, 62 },
69 { 63 {
70 files: [ 64 files: ['**/__tests__/*.{ts,tsx}', '**/*.{spec,test}.{ts,tsx}'],
71 '**/__tests__/*.{ts,tsx}',
72 '**/*.{spec,test}.{ts,tsx}',
73 ],
74 rules: { 65 rules: {
75 // If a non-null assertion fails in a test, the test will also fail anyways. 66 // If a non-null assertion fails in a test, the test will also fail anyways.
76 '@typescript-eslint/no-non-null-assertion': 'off', 67 '@typescript-eslint/no-non-null-assertion': 'off',
@@ -79,21 +70,14 @@ module.exports = {
79 }, 70 },
80 }, 71 },
81 { 72 {
82 files: [ 73 files: ['**/*.js'],
83 '**/*.js',
84 ],
85 rules: { 74 rules: {
86 // ESM requires extensions for imports. 75 // ESM requires extensions for imports.
87 'import/extensions': [ 76 'import/extensions': ['error', 'ignorePackages'],
88 'error',
89 'ignorePackages',
90 ],
91 }, 77 },
92 }, 78 },
93 { 79 {
94 files: [ 80 files: ['**/*.cjs'],
95 '**/*.cjs',
96 ],
97 parserOptions: { 81 parserOptions: {
98 sourceType: 'script', 82 sourceType: 'script',
99 }, 83 },
@@ -102,10 +86,10 @@ module.exports = {
102 files: [ 86 files: [
103 '.electron-builder.config.cjs', 87 '.electron-builder.config.cjs',
104 'config/**/*.{cjs,js}', 88 'config/**/*.{cjs,js}',
105 'jest.config.js', 89 '*.config.{cjs,js}',
106 'scripts/**/*.js', 90 'scripts/**/*.js',
107 'packages/*/.eslintrc.cjs', 91 'packages/*/.eslintrc.cjs',
108 'packages/*/*.config.js', 92 'packages/*/*.config.{cjs,js}',
109 ], 93 ],
110 env: { 94 env: {
111 // Config files are never run in a browser (even in frontend projects). 95 // Config files are never run in a browser (even in frontend projects).
@@ -126,9 +110,7 @@ module.exports = {
126 }, 110 },
127 }, 111 },
128 { 112 {
129 files: [ 113 files: ['packages/*/*.config.{cjs,js}'],
130 'packages/*/*.config.js',
131 ],
132 rules: { 114 rules: {
133 // Allow relative imports of config files from the root package. 115 // Allow relative imports of config files from the root package.
134 'import/no-relative-packages': 'off', 116 'import/no-relative-packages': 'off',
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..afc6ff1
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,26 @@
1# Logs
2.log/
3logs
4*.log
5
6# Runtime data
7pids
8*.pid
9*.seed
10
11# Coverage directory used by tools like istanbul
12coverage
13.eslintcache
14
15# Dependencies directories
16.yarn
17.vite
18
19# OSX
20.DS_Store
21
22.idea
23npm-debug.log.*
24.pnpm-debug.log*
25
26dist/
diff --git a/.yarnrc.yml b/.yarnrc.yml
index e8caff1..7ffac8d 100644
--- a/.yarnrc.yml
+++ b/.yarnrc.yml
@@ -11,15 +11,15 @@ nodeLinker: node-modules
11packageExtensions: 11packageExtensions:
12 eslint-config-airbnb-typescript@*: 12 eslint-config-airbnb-typescript@*:
13 peerDependencies: 13 peerDependencies:
14 eslint: "*" 14 eslint: '*'
15 eslint-plugin-import: "*" 15 eslint-plugin-import: '*'
16 16
17plugins: 17plugins:
18 - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 18 - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
19 spec: "@yarnpkg/plugin-interactive-tools" 19 spec: '@yarnpkg/plugin-interactive-tools'
20 - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 20 - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
21 spec: "@yarnpkg/plugin-workspace-tools" 21 spec: '@yarnpkg/plugin-workspace-tools'
22 - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 22 - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
23 spec: "@yarnpkg/plugin-typescript" 23 spec: '@yarnpkg/plugin-typescript'
24 24
25yarnPath: .yarn/releases/yarn-3.1.1.cjs 25yarnPath: .yarn/releases/yarn-3.1.1.cjs
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 46ba3d6..e1d73d4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,29 +10,28 @@ Contributors are welcome to add their contact information to the license headers
10> Everyone is permitted to copy and distribute verbatim copies of this 10> Everyone is permitted to copy and distribute verbatim copies of this
11> license document, but changing it is not allowed. 11> license document, but changing it is not allowed.
12> 12>
13>
14> Developer's Certificate of Origin 1.1 13> Developer's Certificate of Origin 1.1
15> 14>
16> By making a contribution to this project, I certify that: 15> By making a contribution to this project, I certify that:
17> 16>
18> (a) The contribution was created in whole or in part by me and I 17> (a) The contribution was created in whole or in part by me and I
19> have the right to submit it under the open source license 18> have the right to submit it under the open source license
20> indicated in the file; or 19> indicated in the file; or
21> 20>
22> (b) The contribution is based upon previous work that, to the best 21> (b) The contribution is based upon previous work that, to the best
23> of my knowledge, is covered under an appropriate open source 22> of my knowledge, is covered under an appropriate open source
24> license and I have the right under that license to submit that 23> license and I have the right under that license to submit that
25> work with modifications, whether created in whole or in part 24> work with modifications, whether created in whole or in part
26> by me, under the same open source license (unless I am 25> by me, under the same open source license (unless I am
27> permitted to submit under a different license), as indicated 26> permitted to submit under a different license), as indicated
28> in the file; or 27> in the file; or
29> 28>
30> (c) The contribution was provided directly to me by some other 29> (c) The contribution was provided directly to me by some other
31> person who certified (a), (b) or (c) and I have not modified 30> person who certified (a), (b) or (c) and I have not modified
32> it. 31> it.
33> 32>
34> (d) I understand and agree that this project and the contribution 33> (d) I understand and agree that this project and the contribution
35> are public and that a record of the contribution (including all 34> are public and that a record of the contribution (including all
36> personal information I submit with it, including my sign-off) is 35> personal information I submit with it, including my sign-off) is
37> maintained indefinitely and may be redistributed consistent with 36> maintained indefinitely and may be redistributed consistent with
38> this project or the open source license(s) involved. 37> this project or the open source license(s) involved.
diff --git a/README.md b/README.md
index b40bab3..6c8b45a 100644
--- a/README.md
+++ b/README.md
@@ -15,13 +15,13 @@ The project structure is based on [Vite Electron Builder Boilerplate](https://gi
15 15
16After installing `node`, you can install `yarn` with 16After installing `node`, you can install `yarn` with
17 17
18``` sh 18```sh
19npm i -g yarn 19npm i -g yarn
20``` 20```
21 21
22To start working, install all dependencies with 22To start working, install all dependencies with
23 23
24``` sh 24```sh
25yarn install --immutable 25yarn install --immutable
26``` 26```
27 27
@@ -33,7 +33,7 @@ yarn types
33 33
34To start a development instance of Sophie, which will reload on source changes, run 34To start a development instance of Sophie, which will reload on source changes, run
35 35
36``` sh 36```sh
37yarn watch 37yarn watch
38``` 38```
39 39
@@ -51,7 +51,7 @@ yarn watch:test
51 51
52To build the application in release mode, run 52To build the application in release mode, run
53 53
54``` sh 54```sh
55yarn compile 55yarn compile
56``` 56```
57 57
@@ -69,8 +69,8 @@ yarn run lint
69 69
70## License 70## License
71 71
72> Copyright (C) 2021-2022 Kristóf Marussy &lt;kristof@marussy.com&gt;<br> 72> Copyright (C) 2021-2022 Kristóf Marussy &lt;kristof@marussy.com&gt;<br>
73> Copyright (C) 2022 Vijay A &lt;vraravam@users.noreply.github.com&gt; 73> Copyright (C) 2022 Vijay A &lt;vraravam@users.noreply.github.com&gt;
74> 74>
75> This program is free software: you can redistribute it and/or modify 75> This program is free software: you can redistribute it and/or modify
76> it under the terms of the GNU Affero General Public License as 76> it under the terms of the GNU Affero General Public License as
@@ -78,7 +78,7 @@ yarn run lint
78> 78>
79> This program is distributed in the hope that it will be useful, 79> This program is distributed in the hope that it will be useful,
80> but WITHOUT ANY WARRANTY; without even the implied warranty of 80> but WITHOUT ANY WARRANTY; without even the implied warranty of
81> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 81> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
82> GNU Affero General Public License for more details. 82> GNU Affero General Public License for more details.
83> You should have received a copy of the GNU Affero General Public License 83> You should have received a copy of the GNU Affero General Public License
84> along with this program. If not, see &lt;[https://www.gnu.org/licenses/](https://www.gnu.org/licenses/)&gt;. 84> along with this program. If not, see &lt;[https://www.gnu.org/licenses/](https://www.gnu.org/licenses/)&gt;.
diff --git a/config/buildConstants.js b/config/buildConstants.js
index 9083d78..3c55500 100644
--- a/config/buildConstants.js
+++ b/config/buildConstants.js
@@ -7,11 +7,15 @@ const thisDir = fileURLToDirname(import.meta.url);
7 7
8// We import this from a vite config, where top-level await is not available (es2021), 8// We import this from a vite config, where top-level await is not available (es2021),
9// so we have to use the synchronous filesystem API. 9// so we have to use the synchronous filesystem API.
10const electronVendorsJson = readFileSync(join(thisDir, '../.electron-vendors.cache.json'), 'utf8'); 10const electronVendorsJson = readFileSync(
11 join(thisDir, '../.electron-vendors.cache.json'),
12 'utf8',
13);
11 14
12/** @type {{ chrome: number; node: number; }} */ 15/** @type {{ chrome: number; node: number; }} */
13// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 16// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
14const { chrome: chromeVersion, node: nodeVersion } = JSON.parse(electronVendorsJson); 17const { chrome: chromeVersion, node: nodeVersion } =
18 JSON.parse(electronVendorsJson);
15 19
16/** @type {string} */ 20/** @type {string} */
17export const banner = `/*! 21export const banner = `/*!
diff --git a/config/getEsbuildConfig.js b/config/getEsbuildConfig.js
index b338d68..930f19f 100644
--- a/config/getEsbuildConfig.js
+++ b/config/getEsbuildConfig.js
@@ -25,7 +25,7 @@ export default function getEsbuildConfig(config, extraMetaEnvVars) {
25 js: banner, 25 js: banner,
26 }, 26 },
27 ...config, 27 ...config,
28 sourcemap: isDevelopment ? (config.sourcemap || true) : false, 28 sourcemap: isDevelopment ? config.sourcemap || true : false,
29 define: { 29 define: {
30 'process.env.NODE_ENV': modeString, 30 'process.env.NODE_ENV': modeString,
31 'process.env.MODE': modeString, 31 'process.env.MODE': modeString,
diff --git a/config/jest.config.base.js b/config/jest.config.base.js
index 463e498..21f93be 100644
--- a/config/jest.config.base.js
+++ b/config/jest.config.base.js
@@ -9,22 +9,22 @@ export default {
9 transform: { 9 transform: {
10 '\\.tsx?$': join(thisDir, 'jestEsbuildTransformer.js'), 10 '\\.tsx?$': join(thisDir, 'jestEsbuildTransformer.js'),
11 }, 11 },
12 extensionsToTreatAsEsm: [ 12 extensionsToTreatAsEsm: ['.ts', '.tsx'],
13 '.ts',
14 '.tsx',
15 ],
16 moduleNameMapper: { 13 moduleNameMapper: {
17 '^@sophie/(.+)$': join(thisDir, '../packages/$1/src/index.ts'), 14 '^@sophie/(.+)$': join(thisDir, '../packages/$1/src/index.ts'),
18 '^(\\.{1,2}/.*)\\.jsx?$': '$1', 15 '^(\\.{1,2}/.*)\\.jsx?$': '$1',
19 // Workaround for jest to recognize the vendored dependencies of chalk. 16 // Workaround for jest to recognize the vendored dependencies of chalk.
20 '^#ansi-styles$': join(thisDir, '../node_modules/chalk/source/vendor/ansi-styles/index.js'), 17 '^#ansi-styles$': join(
21 '^#supports-color$': join(thisDir, '../node_modules/chalk/source/vendor/supports-color/index.js'), 18 thisDir,
19 '../node_modules/chalk/source/vendor/ansi-styles/index.js',
20 ),
21 '^#supports-color$': join(
22 thisDir,
23 '../node_modules/chalk/source/vendor/supports-color/index.js',
24 ),
22 }, 25 },
23 resetMocks: true, 26 resetMocks: true,
24 restoreMocks: true, 27 restoreMocks: true,
25 testEnvironment: 'node', 28 testEnvironment: 'node',
26 testPathIgnorePatterns: [ 29 testPathIgnorePatterns: ['/dist/', '/node_modules/'],
27 '/dist/',
28 '/node_modules/',
29 ],
30}; 30};
diff --git a/config/tsconfig.base.json b/config/tsconfig.base.json
index 255f334..ff7594e 100644
--- a/config/tsconfig.base.json
+++ b/config/tsconfig.base.json
@@ -12,8 +12,6 @@
12 "isolatedModules": true, 12 "isolatedModules": true,
13 "skipLibCheck": true, 13 "skipLibCheck": true,
14 "checkJs": true, 14 "checkJs": true,
15 "lib": [ 15 "lib": ["esnext"]
16 "esnext"
17 ]
18 } 16 }
19} 17}
diff --git a/docs/architecture.md b/docs/architecture.md
index 6467b6f..0122809 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -6,9 +6,9 @@ title: Architecture
6 6
7Sophie is designed to 7Sophie is designed to
8 8
9* display _services_, i.e., web pages, most of which are messaging web applications, 9- display _services_, i.e., web pages, most of which are messaging web applications,
10* allow the user to quickly switch between services and save resources by displaying multiple services in the same desktop applications, 10- allow the user to quickly switch between services and save resources by displaying multiple services in the same desktop applications,
11* provide integrations for services, e.g., to manage unread message counts and notification in a single place. 11- provide integrations for services, e.g., to manage unread message counts and notification in a single place.
12 12
13Integrations for services are provided by pluggable _recipes_ to allow adding support for new services easily. 13Integrations for services are provided by pluggable _recipes_ to allow adding support for new services easily.
14A recipe may come directly from Sophie, a third party (under AGPLv3 licensing), or be created by the user themselves. 14A recipe may come directly from Sophie, a third party (under AGPLv3 licensing), or be created by the user themselves.
@@ -21,9 +21,9 @@ Sophie is built on the [Electron](https://www.electronjs.org/) application frame
21 21
22Electron makes two facilities readily available to display foreign web pages: 22Electron makes two facilities readily available to display foreign web pages:
23 23
24* The easier solution that mimics the HTML `<iframe>` tag is [`<webview>`](https://www.electronjs.org/docs/latest/api/webview-tag/). 24- The easier solution that mimics the HTML `<iframe>` tag is [`<webview>`](https://www.electronjs.org/docs/latest/api/webview-tag/).
25 However, it is deprecated and has some [critical bugs](https://github.com/electron/electron/issues/25469) when loading content with modern web security headers. 25 However, it is deprecated and has some [critical bugs](https://github.com/electron/electron/issues/25469) when loading content with modern web security headers.
26* The new solution is the [`BrowserView`](https://www.electronjs.org/docs/latest/api/browser-view/), which is the approach taken by Sophie. 26- The new solution is the [`BrowserView`](https://www.electronjs.org/docs/latest/api/browser-view/), which is the approach taken by Sophie.
27 The `BrowserView` is an overlay over the main browser window, so _Sophie will not be able render UI elements over foreign web pages_. 27 The `BrowserView` is an overlay over the main browser window, so _Sophie will not be able render UI elements over foreign web pages_.
28 While this is somewhat limiting, it allows us to take advantage of modern Electron APIs and coding practices. 28 While this is somewhat limiting, it allows us to take advantage of modern Electron APIs and coding practices.
29 29
@@ -31,10 +31,10 @@ Electron makes two facilities readily available to display foreign web pages:
31 31
32An Electron application is split into a _main process_ and a number of _renderer processes_: 32An Electron application is split into a _main process_ and a number of _renderer processes_:
33 33
34* The main process has access to NodeJS APIs for direct file system access. 34- The main process has access to NodeJS APIs for direct file system access.
35 It is also the only process that can manipulate browser windows and `BrowserView` instances. 35 It is also the only process that can manipulate browser windows and `BrowserView` instances.
36 Therefore, most of Sophie's logic must be implemented in the main process. 36 Therefore, most of Sophie's logic must be implemented in the main process.
37* The renderer processes are sandboxed processes responsible for rendering and executing web pages, both local and foreign. 37- The renderer processes are sandboxed processes responsible for rendering and executing web pages, both local and foreign.
38 In turn, they are split into an _isolated world_ and a _main world_. 38 In turn, they are split into an _isolated world_ and a _main world_.
39 The isolated world (world id 999) can use [IPC](https://www.electronjs.org/docs/latest/api/ipc-renderer/) to communicate with the main process. 39 The isolated world (world id 999) can use [IPC](https://www.electronjs.org/docs/latest/api/ipc-renderer/) to communicate with the main process.
40 The only way to expose APIs to the main world is through the [`contextBridge`](https://www.electronjs.org/docs/latest/api/context-bridge). 40 The only way to expose APIs to the main world is through the [`contextBridge`](https://www.electronjs.org/docs/latest/api/context-bridge).
diff --git a/jest.config.js b/jest.config.js
index c631fdd..174322a 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,11 +1,7 @@
1/** @type {import('@jest/types').Config.InitialOptions} */ 1/** @type {import('@jest/types').Config.InitialOptions} */
2export default { 2export default {
3 projects: [ 3 projects: ['<rootDir>/packages/*'],
4 '<rootDir>/packages/*',
5 ],
6 /** @type {'babel' | 'v8'} */ 4 /** @type {'babel' | 'v8'} */
7 coverageProvider: 'v8', 5 coverageProvider: 'v8',
8 collectCoverageFrom: [ 6 collectCoverageFrom: ['src/**/*.{ts,tsx}'],
9 'src/**/*.{ts,tsx}',
10 ],
11}; 7};
diff --git a/package.json b/package.json
index 1ca72f9..22cc738 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
51 "devDependencies": { 51 "devDependencies": {
52 "@electron/fuses": "^1.5.0", 52 "@electron/fuses": "^1.5.0",
53 "@types/jest": "^27.4.0", 53 "@types/jest": "^27.4.0",
54 "@types/prettier": "^2",
54 "@typescript-eslint/eslint-plugin": "^5.9.0", 55 "@typescript-eslint/eslint-plugin": "^5.9.0",
55 "@typescript-eslint/parser": "^5.9.0", 56 "@typescript-eslint/parser": "^5.9.0",
56 "@vitejs/plugin-react": "^1.1.4", 57 "@vitejs/plugin-react": "^1.1.4",
@@ -63,14 +64,17 @@
63 "eslint-config-airbnb": "^19.0.4", 64 "eslint-config-airbnb": "^19.0.4",
64 "eslint-config-airbnb-base": "^15.0.0", 65 "eslint-config-airbnb-base": "^15.0.0",
65 "eslint-config-airbnb-typescript": "^16.1.0", 66 "eslint-config-airbnb-typescript": "^16.1.0",
67 "eslint-config-prettier": "^8.3.0",
66 "eslint-formatter-gitlab": "^3.0.0", 68 "eslint-formatter-gitlab": "^3.0.0",
67 "eslint-import-resolver-typescript": "^2.5.0", 69 "eslint-import-resolver-typescript": "^2.5.0",
68 "eslint-plugin-import": "^2.25.4", 70 "eslint-plugin-import": "^2.25.4",
69 "eslint-plugin-jsx-a11y": "^6.5.1", 71 "eslint-plugin-jsx-a11y": "^6.5.1",
72 "eslint-plugin-prettier": "^4.0.0",
70 "eslint-plugin-react": "^7.28.0", 73 "eslint-plugin-react": "^7.28.0",
71 "eslint-plugin-react-hooks": "^4.3.0", 74 "eslint-plugin-react-hooks": "^4.3.0",
72 "git-repo-info": "^2.1.1", 75 "git-repo-info": "^2.1.1",
73 "jest": "^27.4.7", 76 "jest": "^27.4.7",
77 "prettier": "^2.5.1",
74 "rimraf": "^3.0.2", 78 "rimraf": "^3.0.2",
75 "typescript": "^4.5.4", 79 "typescript": "^4.5.4",
76 "vite": "^2.7.10" 80 "vite": "^2.7.10"
diff --git a/packages/main/esbuild.config.js b/packages/main/esbuild.config.js
index 49fba6b..d5f6f1e 100644
--- a/packages/main/esbuild.config.js
+++ b/packages/main/esbuild.config.js
@@ -12,19 +12,20 @@ if (process.env.MODE !== 'development') {
12 12
13const gitInfo = getRepoInfo(); 13const gitInfo = getRepoInfo();
14 14
15export default getEsbuildConfig({ 15export default getEsbuildConfig(
16 absWorkingDir: fileURLToDirname(import.meta.url), 16 {
17 entryPoints: [ 17 absWorkingDir: fileURLToDirname(import.meta.url),
18 'src/index.ts', 18 entryPoints: ['src/index.ts'],
19 ], 19 outfile: 'dist/index.cjs',
20 outfile: 'dist/index.cjs', 20 format: 'cjs',
21 format: 'cjs', 21 platform: 'node',
22 platform: 'node', 22 target: node,
23 target: node, 23 external: externalPackages,
24 external: externalPackages, 24 },
25}, { 25 {
26 VITE_DEV_SERVER_URL: process.env.VITE_DEV_SERVER_URL || null, 26 VITE_DEV_SERVER_URL: process.env.VITE_DEV_SERVER_URL || null,
27 GIT_SHA: gitInfo.abbreviatedSha, 27 GIT_SHA: gitInfo.abbreviatedSha,
28 GIT_BRANCH: gitInfo.branch, 28 GIT_BRANCH: gitInfo.branch,
29 BUILD_DATE: new Date().getTime(), 29 BUILD_DATE: new Date().getTime(),
30}); 30 },
31);
diff --git a/packages/main/src/controllers/__tests__/initConfig.spec.ts b/packages/main/src/controllers/__tests__/initConfig.spec.ts
index e386a07..7b6d6ab 100644
--- a/packages/main/src/controllers/__tests__/initConfig.spec.ts
+++ b/packages/main/src/controllers/__tests__/initConfig.spec.ts
@@ -60,8 +60,12 @@ describe('when initializing', () => {
60 }); 60 });
61 61
62 it('should bail if there is an an error creating the config file', async () => { 62 it('should bail if there is an an error creating the config file', async () => {
63 mocked(persistenceService.writeConfig).mockRejectedValue(new Error('boo')); 63 mocked(persistenceService.writeConfig).mockRejectedValue(
64 await expect(() => initConfig(config, persistenceService)).rejects.toBeInstanceOf(Error); 64 new Error('boo'),
65 );
66 await expect(() =>
67 initConfig(config, persistenceService),
68 ).rejects.toBeInstanceOf(Error);
65 }); 69 });
66 }); 70 });
67 71
@@ -85,7 +89,9 @@ describe('when initializing', () => {
85 mocked(persistenceService.watchConfig).mockImplementationOnce(() => { 89 mocked(persistenceService.watchConfig).mockImplementationOnce(() => {
86 throw new Error('boo'); 90 throw new Error('boo');
87 }); 91 });
88 await expect(() => initConfig(config, persistenceService)).rejects.toBeInstanceOf(Error); 92 await expect(() =>
93 initConfig(config, persistenceService),
94 ).rejects.toBeInstanceOf(Error);
89 }); 95 });
90 }); 96 });
91 97
@@ -102,7 +108,9 @@ describe('when initializing', () => {
102 108
103 it('should bail if it cannot determine whether there is a config file', async () => { 109 it('should bail if it cannot determine whether there is a config file', async () => {
104 mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); 110 mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo'));
105 await expect(() => initConfig(config, persistenceService)).rejects.toBeInstanceOf(Error); 111 await expect(() =>
112 initConfig(config, persistenceService),
113 ).rejects.toBeInstanceOf(Error);
106 }); 114 });
107}); 115});
108 116
@@ -118,7 +126,9 @@ describe('when it has loaded the config', () => {
118 }); 126 });
119 mocked(persistenceService.watchConfig).mockReturnValueOnce(watcherDisposer); 127 mocked(persistenceService.watchConfig).mockReturnValueOnce(watcherDisposer);
120 sutDisposer = await initConfig(config, persistenceService, throttleMs); 128 sutDisposer = await initConfig(config, persistenceService, throttleMs);
121 [[configChangedCallback]] = mocked(persistenceService.watchConfig).mock.calls; 129 [[configChangedCallback]] = mocked(
130 persistenceService.watchConfig,
131 ).mock.calls;
122 jest.resetAllMocks(); 132 jest.resetAllMocks();
123 }); 133 });
124 134
diff --git a/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts b/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts
index bd33f48..d4068af 100644
--- a/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts
+++ b/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts
@@ -58,14 +58,18 @@ it('should synchronize themeSource changes to the nativeTheme', () => {
58}); 58});
59 59
60it('should synchronize shouldUseDarkColors changes to the store', () => { 60it('should synchronize shouldUseDarkColors changes to the store', () => {
61 const listener = mocked(nativeTheme.on).mock.calls.find(([event]) => event === 'updated')![1]; 61 const listener = mocked(nativeTheme.on).mock.calls.find(
62 ([event]) => event === 'updated',
63 )![1];
62 shouldUseDarkColors = true; 64 shouldUseDarkColors = true;
63 listener(); 65 listener();
64 expect(store.shared.shouldUseDarkColors).toBe(true); 66 expect(store.shared.shouldUseDarkColors).toBe(true);
65}); 67});
66 68
67it('should remove the listener on dispose', () => { 69it('should remove the listener on dispose', () => {
68 const listener = mocked(nativeTheme.on).mock.calls.find(([event]) => event === 'updated')![1]; 70 const listener = mocked(nativeTheme.on).mock.calls.find(
71 ([event]) => event === 'updated',
72 )![1];
69 disposeSut(); 73 disposeSut();
70 expect(nativeTheme.off).toBeCalledWith('updated', listener); 74 expect(nativeTheme.off).toBeCalledWith('updated', listener);
71}); 75});
diff --git a/packages/main/src/controllers/initConfig.ts b/packages/main/src/controllers/initConfig.ts
index 1d40762..e83b8da 100644
--- a/packages/main/src/controllers/initConfig.ts
+++ b/packages/main/src/controllers/initConfig.ts
@@ -59,20 +59,23 @@ export default async function initConfig(
59 lastSnapshotOnDisk = snapshot; 59 lastSnapshotOnDisk = snapshot;
60 } 60 }
61 61
62 if (!await readConfig()) { 62 if (!(await readConfig())) {
63 log.info('Config file was not found'); 63 log.info('Config file was not found');
64 await writeConfig(); 64 await writeConfig();
65 log.info('Created config file'); 65 log.info('Created config file');
66 } 66 }
67 67
68 const disposeOnSnapshot = onSnapshot(config, debounce((snapshot) => { 68 const disposeOnSnapshot = onSnapshot(
69 // We can compare snapshots by reference, since it is only recreated on store changes. 69 config,
70 if (lastSnapshotOnDisk !== snapshot) { 70 debounce((snapshot) => {
71 writeConfig().catch((err) => { 71 // We can compare snapshots by reference, since it is only recreated on store changes.
72 log.error('Failed to write config on config change', err); 72 if (lastSnapshotOnDisk !== snapshot) {
73 }); 73 writeConfig().catch((err) => {
74 } 74 log.error('Failed to write config on config change', err);
75 }, debounceTime)); 75 });
76 }
77 }, debounceTime),
78 );
76 79
77 const disposeWatcher = persistenceService.watchConfig(async () => { 80 const disposeWatcher = persistenceService.watchConfig(async () => {
78 try { 81 try {
diff --git a/packages/main/src/devTools.ts b/packages/main/src/devTools.ts
index 0486c36..69f1514 100644
--- a/packages/main/src/devTools.ts
+++ b/packages/main/src/devTools.ts
@@ -52,18 +52,12 @@ export async function installDevToolsExtensions(): Promise<void> {
52 @typescript-eslint/no-var-requires 52 @typescript-eslint/no-var-requires
53 */ 53 */
54 } = require('electron-devtools-installer') as typeof import('electron-devtools-installer'); 54 } = require('electron-devtools-installer') as typeof import('electron-devtools-installer');
55 await installExtension( 55 await installExtension([REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS], {
56 [ 56 forceDownload: false,
57 REACT_DEVELOPER_TOOLS, 57 loadExtensionOptions: {
58 REDUX_DEVTOOLS, 58 allowFileAccess: true,
59 ],
60 {
61 forceDownload: false,
62 loadExtensionOptions: {
63 allowFileAccess: true,
64 },
65 }, 59 },
66 ); 60 });
67} 61}
68 62
69/** 63/**
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts
index bc10b4c..1f80e44 100644
--- a/packages/main/src/index.ts
+++ b/packages/main/src/index.ts
@@ -33,12 +33,7 @@ import {
33 MainToRendererIpcMessage, 33 MainToRendererIpcMessage,
34 RendererToMainIpcMessage, 34 RendererToMainIpcMessage,
35} from '@sophie/shared'; 35} from '@sophie/shared';
36import { 36import { app, BrowserView, BrowserWindow, ipcMain } from 'electron';
37 app,
38 BrowserView,
39 BrowserWindow,
40 ipcMain,
41} from 'electron';
42import { ensureDirSync, readFile, readFileSync } from 'fs-extra'; 37import { ensureDirSync, readFile, readFileSync } from 'fs-extra';
43import { autorun } from 'mobx'; 38import { autorun } from 'mobx';
44import { getSnapshot, onPatch } from 'mobx-state-tree'; 39import { getSnapshot, onPatch } from 'mobx-state-tree';
@@ -97,7 +92,9 @@ app.setAboutPanelOptions({
97 `Node.js: ${process.versions.node}`, 92 `Node.js: ${process.versions.node}`,
98 `Platform: ${osName()}`, 93 `Platform: ${osName()}`,
99 `Arch: ${arch()}`, 94 `Arch: ${arch()}`,
100 `Build date: ${new Date(Number(import.meta.env.BUILD_DATE)).toLocaleString()}`, 95 `Build date: ${new Date(
96 Number(import.meta.env.BUILD_DATE),
97 ).toLocaleString()}`,
101 `Git SHA: ${import.meta.env.GIT_SHA}`, 98 `Git SHA: ${import.meta.env.GIT_SHA}`,
102 `Git branch: ${import.meta.env.GIT_BRANCH}`, 99 `Git branch: ${import.meta.env.GIT_BRANCH}`,
103 ].join('\n'), 100 ].join('\n'),
@@ -123,11 +120,13 @@ const serviceInject: WebSource = {
123let mainWindow: BrowserWindow | null = null; 120let mainWindow: BrowserWindow | null = null;
124 121
125const store = createMainStore(); 122const store = createMainStore();
126init(store).then((disposeCompositionRoot) => { 123init(store)
127 app.on('will-quit', disposeCompositionRoot); 124 .then((disposeCompositionRoot) => {
128}).catch((err) => { 125 app.on('will-quit', disposeCompositionRoot);
129 log.log('Failed to initialize application', err); 126 })
130}); 127 .catch((err) => {
128 log.log('Failed to initialize application', err);
129 });
131 130
132const rendererBaseUrl = getResourceUrl('../renderer/'); 131const rendererBaseUrl = getResourceUrl('../renderer/');
133function shouldCancelMainWindowRequest(url: string, method: string): boolean { 132function shouldCancelMainWindowRequest(url: string, method: string): boolean {
@@ -141,12 +140,20 @@ function shouldCancelMainWindowRequest(url: string, method: string): boolean {
141 return true; 140 return true;
142 } 141 }
143 if (isDevelopment) { 142 if (isDevelopment) {
144 if (DEVMODE_ALLOWED_URL_PREFIXES.some((prefix) => normalizedUrl.startsWith(prefix))) { 143 if (
144 DEVMODE_ALLOWED_URL_PREFIXES.some((prefix) =>
145 normalizedUrl.startsWith(prefix),
146 )
147 ) {
145 return false; 148 return false;
146 } 149 }
147 if (import.meta.env.VITE_DEV_SERVER_URL !== undefined) { 150 if (import.meta.env.VITE_DEV_SERVER_URL !== undefined) {
148 const isHttp = normalizedUrl.startsWith(import.meta.env.VITE_DEV_SERVER_URL); 151 const isHttp = normalizedUrl.startsWith(
149 const isWs = normalizedUrl.startsWith(import.meta.env.VITE_DEV_SERVER_URL.replace(/^http:/, 'ws:')); 152 import.meta.env.VITE_DEV_SERVER_URL,
153 );
154 const isWs = normalizedUrl.startsWith(
155 import.meta.env.VITE_DEV_SERVER_URL.replace(/^http:/, 'ws:'),
156 );
150 return !isHttp && !isWs; 157 return !isHttp && !isWs;
151 } 158 }
152 } 159 }
@@ -168,19 +175,24 @@ async function createWindow(): Promise<unknown> {
168 175
169 webContents.userAgent = originalUserAgent; 176 webContents.userAgent = originalUserAgent;
170 177
171 webContents.session.setPermissionRequestHandler((_webContents, _permission, callback) => { 178 webContents.session.setPermissionRequestHandler(
172 callback(false); 179 (_webContents, _permission, callback) => {
173 }); 180 callback(false);
181 },
182 );
174 183
175 webContents.session.webRequest.onBeforeRequest(({ url, method }, callback) => { 184 webContents.session.webRequest.onBeforeRequest(
176 callback({ 185 ({ url, method }, callback) => {
177 cancel: shouldCancelMainWindowRequest(url, method), 186 callback({
178 }); 187 cancel: shouldCancelMainWindowRequest(url, method),
179 }); 188 });
189 },
190 );
180 191
181 const pageUrl = (isDevelopment && import.meta.env.VITE_DEV_SERVER_URL !== undefined) 192 const pageUrl =
182 ? import.meta.env.VITE_DEV_SERVER_URL 193 isDevelopment && import.meta.env.VITE_DEV_SERVER_URL !== undefined
183 : getResourceUrl('../renderer/dist/index.html'); 194 ? import.meta.env.VITE_DEV_SERVER_URL
195 : getResourceUrl('../renderer/dist/index.html');
184 196
185 webContents.on('will-navigate', (event, url) => { 197 webContents.on('will-navigate', (event, url) => {
186 if (url !== pageUrl) { 198 if (url !== pageUrl) {
@@ -273,9 +285,8 @@ async function createWindow(): Promise<unknown> {
273 webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); 285 webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch);
274 }); 286 });
275 287
276 ipcMain.handle( 288 ipcMain.handle(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) =>
277 ServiceToMainIpcMessage.ApiExposedInMainWorld, 289 event.sender.id === browserView.webContents.id ? serviceInject : null,
278 (event) => (event.sender.id === browserView.webContents.id ? serviceInject : null),
279 ); 290 );
280 291
281 browserView.webContents.on('ipc-message', (_event, channel, ...args) => { 292 browserView.webContents.on('ipc-message', (_event, channel, ...args) => {
@@ -305,7 +316,9 @@ async function createWindow(): Promise<unknown> {
305 316
306 browserView.webContents.session.webRequest.onBeforeSendHeaders( 317 browserView.webContents.session.webRequest.onBeforeSendHeaders(
307 ({ url, requestHeaders }, callback) => { 318 ({ url, requestHeaders }, callback) => {
308 const requestUserAgent = url.match(/^[^:]+:\/\/accounts\.google\.[^./]+\//) 319 const requestUserAgent = url.match(
320 /^[^:]+:\/\/accounts\.google\.[^./]+\//,
321 )
309 ? chromelessUserAgent 322 ? chromelessUserAgent
310 : userAgent; 323 : userAgent;
311 callback({ 324 callback({
@@ -317,9 +330,11 @@ async function createWindow(): Promise<unknown> {
317 }, 330 },
318 ); 331 );
319 332
320 browserView.webContents.loadURL('https://gitlab.com/say-hi-to-sophie/sophie').catch((err) => { 333 browserView.webContents
321 log.error('Failed to load browser', err); 334 .loadURL('https://gitlab.com/say-hi-to-sophie/sophie')
322 }); 335 .catch((err) => {
336 log.error('Failed to load browser', err);
337 });
323 338
324 return mainWindow.loadURL(pageUrl); 339 return mainWindow.loadURL(pageUrl);
325} 340}
@@ -342,17 +357,20 @@ app.on('window-all-closed', () => {
342 } 357 }
343}); 358});
344 359
345app.whenReady().then(async () => { 360app
346 if (isDevelopment) { 361 .whenReady()
347 try { 362 .then(async () => {
348 await installDevToolsExtensions(); 363 if (isDevelopment) {
349 } catch (err) { 364 try {
350 log.error('Failed to install devtools extensions', err); 365 await installDevToolsExtensions();
366 } catch (err) {
367 log.error('Failed to install devtools extensions', err);
368 }
351 } 369 }
352 }
353 370
354 return createWindow(); 371 return createWindow();
355}).catch((err) => { 372 })
356 log.error('Failed to create window', err); 373 .catch((err) => {
357 process.exit(1); 374 log.error('Failed to create window', err);
358}); 375 process.exit(1);
376 });
diff --git a/packages/main/src/init.ts b/packages/main/src/init.ts
index 4487cc4..f3794bb 100644
--- a/packages/main/src/init.ts
+++ b/packages/main/src/init.ts
@@ -27,8 +27,13 @@ import { MainStore } from './stores/MainStore';
27import type Disposer from './utils/Disposer'; 27import type Disposer from './utils/Disposer';
28 28
29export default async function init(store: MainStore): Promise<Disposer> { 29export default async function init(store: MainStore): Promise<Disposer> {
30 const configPersistenceService = new ConfigPersistenceServiceImpl(app.getPath('userData')); 30 const configPersistenceService = new ConfigPersistenceServiceImpl(
31 const disposeConfigController = await initConfig(store.config, configPersistenceService); 31 app.getPath('userData'),
32 );
33 const disposeConfigController = await initConfig(
34 store.config,
35 configPersistenceService,
36 );
32 const disposeNativeThemeController = initNativeTheme(store); 37 const disposeNativeThemeController = initNativeTheme(store);
33 38
34 return () => { 39 return () => {
diff --git a/packages/main/src/services/ConfigPersistenceService.ts b/packages/main/src/services/ConfigPersistenceService.ts
index 7d508c5..ee5696d 100644
--- a/packages/main/src/services/ConfigPersistenceService.ts
+++ b/packages/main/src/services/ConfigPersistenceService.ts
@@ -21,7 +21,9 @@
21import type { ConfigSnapshotOut } from '../stores/Config'; 21import type { ConfigSnapshotOut } from '../stores/Config';
22import type Disposer from '../utils/Disposer'; 22import type Disposer from '../utils/Disposer';
23 23
24export type ReadConfigResult = { found: true; data: unknown; } | { found: false; }; 24export type ReadConfigResult =
25 | { found: true; data: unknown }
26 | { found: false };
25 27
26export default interface ConfigPersistenceService { 28export default interface ConfigPersistenceService {
27 readConfig(): Promise<ReadConfigResult>; 29 readConfig(): Promise<ReadConfigResult>;
diff --git a/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts b/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts
index df8c807..e92f706 100644
--- a/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts
+++ b/packages/main/src/services/impl/ConfigPersistenceServiceImpl.ts
@@ -32,7 +32,9 @@ import type { ReadConfigResult } from '../ConfigPersistenceService';
32 32
33const log = getLogger('configPersistence'); 33const log = getLogger('configPersistence');
34 34
35export default class ConfigPersistenceServiceImpl implements ConfigPersistenceService { 35export default class ConfigPersistenceServiceImpl
36 implements ConfigPersistenceService
37{
36 private readonly configFilePath: string; 38 private readonly configFilePath: string;
37 39
38 private writingConfig = false; 40 private writingConfig = false;
@@ -92,13 +94,19 @@ export default class ConfigPersistenceServiceImpl implements ConfigPersistenceSe
92 log.trace('Config file last modified at', mtime); 94 log.trace('Config file last modified at', mtime);
93 } catch (err) { 95 } catch (err) {
94 if ((err as NodeJS.ErrnoException).code === 'ENOENT') { 96 if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
95 log.debug('Config file', this.configFilePath, 'was deleted after being changed'); 97 log.debug(
98 'Config file',
99 this.configFilePath,
100 'was deleted after being changed',
101 );
96 return; 102 return;
97 } 103 }
98 throw err; 104 throw err;
99 } 105 }
100 if (!this.writingConfig 106 if (
101 && (this.timeLastWritten === null || mtime > this.timeLastWritten)) { 107 !this.writingConfig &&
108 (this.timeLastWritten === null || mtime > this.timeLastWritten)
109 ) {
102 log.debug( 110 log.debug(
103 'Found a config file modified at', 111 'Found a config file modified at',
104 mtime, 112 mtime,
@@ -114,8 +122,10 @@ export default class ConfigPersistenceServiceImpl implements ConfigPersistenceSe
114 }); 122 });
115 123
116 watcher.on('change', (eventType, filename) => { 124 watcher.on('change', (eventType, filename) => {
117 if (eventType === 'change' 125 if (
118 && (filename === this.configFileName || filename === null)) { 126 eventType === 'change' &&
127 (filename === this.configFileName || filename === null)
128 ) {
119 configChanged()?.catch((err) => { 129 configChanged()?.catch((err) => {
120 log.error('Unhandled error while listening for config changes', err); 130 log.error('Unhandled error while listening for config changes', err);
121 }); 131 });
diff --git a/packages/main/src/stores/MainStore.ts b/packages/main/src/stores/MainStore.ts
index 7b26c52..eaf5b3c 100644
--- a/packages/main/src/stores/MainStore.ts
+++ b/packages/main/src/stores/MainStore.ts
@@ -24,26 +24,32 @@ import { applySnapshot, Instance, types } from 'mobx-state-tree';
24import type { Config } from './Config.js'; 24import type { Config } from './Config.js';
25import { sharedStore } from './SharedStore'; 25import { sharedStore } from './SharedStore';
26 26
27export const mainStore = types.model('MainStore', { 27export const mainStore = types
28 browserViewBounds: types.optional(types.model('BrowserViewBounds', { 28 .model('MainStore', {
29 x: 0, 29 browserViewBounds: types.optional(
30 y: 0, 30 types.model('BrowserViewBounds', {
31 width: 0, 31 x: 0,
32 height: 0, 32 y: 0,
33 }), {}), 33 width: 0,
34 shared: types.optional(sharedStore, {}), 34 height: 0,
35}).views((self) => ({ 35 }),
36 get config(): Config { 36 {},
37 return self.shared.config; 37 ),
38 }, 38 shared: types.optional(sharedStore, {}),
39})).actions((self) => ({ 39 })
40 setBrowserViewBounds(bounds: BrowserViewBounds): void { 40 .views((self) => ({
41 applySnapshot(self.browserViewBounds, bounds); 41 get config(): Config {
42 }, 42 return self.shared.config;
43 setShouldUseDarkColors(shouldUseDarkColors: boolean): void { 43 },
44 self.shared.shouldUseDarkColors = shouldUseDarkColors; 44 }))
45 }, 45 .actions((self) => ({
46})); 46 setBrowserViewBounds(bounds: BrowserViewBounds): void {
47 applySnapshot(self.browserViewBounds, bounds);
48 },
49 setShouldUseDarkColors(shouldUseDarkColors: boolean): void {
50 self.shared.shouldUseDarkColors = shouldUseDarkColors;
51 },
52 }));
47 53
48export interface MainStore extends Instance<typeof mainStore> {} 54export interface MainStore extends Instance<typeof mainStore> {}
49 55
diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts
index c023fc7..73245cd 100644
--- a/packages/main/src/stores/SharedStore.ts
+++ b/packages/main/src/stores/SharedStore.ts
@@ -23,7 +23,10 @@ import { Instance, types } from 'mobx-state-tree';
23 23
24import { config } from './Config'; 24import { config } from './Config';
25 25
26export type { SharedStoreSnapshotIn, SharedStoreSnapshotOut } from '@sophie/shared'; 26export type {
27 SharedStoreSnapshotIn,
28 SharedStoreSnapshotOut,
29} from '@sophie/shared';
27 30
28export const sharedStore = originalSharedStore.props({ 31export const sharedStore = originalSharedStore.props({
29 config: types.optional(config, {}), 32 config: types.optional(config, {}),
diff --git a/packages/main/src/utils/log.ts b/packages/main/src/utils/log.ts
index c704797..5218721 100644
--- a/packages/main/src/utils/log.ts
+++ b/packages/main/src/utils/log.ts
@@ -46,9 +46,10 @@ prefix.apply(loglevel, {
46 format(level, name, timestamp) { 46 format(level, name, timestamp) {
47 const levelColor = getColor(level); 47 const levelColor = getColor(level);
48 const timeStr = timestamp.toString(); 48 const timeStr = timestamp.toString();
49 const nameStr = typeof name === 'undefined' 49 const nameStr =
50 ? levelColor(':') 50 typeof name === 'undefined'
51 : ` ${chalk.green(`${name}:`)}`; 51 ? levelColor(':')
52 : ` ${chalk.green(`${name}:`)}`;
52 return `${chalk.gray(`[${timeStr}]`)} ${levelColor(level)}${nameStr}`; 53 return `${chalk.gray(`[${timeStr}]`)} ${levelColor(level)}${nameStr}`;
53 }, 54 },
54}); 55});
diff --git a/packages/main/tsconfig.json b/packages/main/tsconfig.json
index bdf68f4..dad597d 100644
--- a/packages/main/tsconfig.json
+++ b/packages/main/tsconfig.json
@@ -2,10 +2,7 @@
2 "extends": "../../config/tsconfig.base.json", 2 "extends": "../../config/tsconfig.base.json",
3 "compilerOptions": { 3 "compilerOptions": {
4 "noEmit": true, 4 "noEmit": true,
5 "types": [ 5 "types": ["@types/jest", "node"]
6 "@types/jest",
7 "node"
8 ]
9 }, 6 },
10 "references": [ 7 "references": [
11 { 8 {
diff --git a/packages/main/types/importMeta.d.ts b/packages/main/types/importMeta.d.ts
index e422c30..efcf48a 100644
--- a/packages/main/types/importMeta.d.ts
+++ b/packages/main/types/importMeta.d.ts
@@ -7,5 +7,5 @@ interface ImportMeta {
7 GIT_SHA: string; 7 GIT_SHA: string;
8 GIT_BRANCH: string; 8 GIT_BRANCH: string;
9 BUILD_DATE: number; 9 BUILD_DATE: number;
10 } 10 };
11} 11}
diff --git a/packages/preload/esbuild.config.js b/packages/preload/esbuild.config.js
index 66f5e84..d888987 100644
--- a/packages/preload/esbuild.config.js
+++ b/packages/preload/esbuild.config.js
@@ -4,15 +4,11 @@ import getEsbuildConfig from '../../config/getEsbuildConfig.js';
4 4
5export default getEsbuildConfig({ 5export default getEsbuildConfig({
6 absWorkingDir: fileURLToDirname(import.meta.url), 6 absWorkingDir: fileURLToDirname(import.meta.url),
7 entryPoints: [ 7 entryPoints: ['src/index.ts'],
8 'src/index.ts',
9 ],
10 outfile: 'dist/index.cjs', 8 outfile: 'dist/index.cjs',
11 format: 'cjs', 9 format: 'cjs',
12 platform: 'node', 10 platform: 'node',
13 target: chrome, 11 target: chrome,
14 sourcemap: 'inline', 12 sourcemap: 'inline',
15 external: [ 13 external: ['electron'],
16 'electron',
17 ],
18}); 14});
diff --git a/packages/preload/src/contextBridge/__tests__/createSophieRenderer.spec.ts b/packages/preload/src/contextBridge/__tests__/createSophieRenderer.spec.ts
index a38dbac..f63c3f6 100644
--- a/packages/preload/src/contextBridge/__tests__/createSophieRenderer.spec.ts
+++ b/packages/preload/src/contextBridge/__tests__/createSophieRenderer.spec.ts
@@ -40,9 +40,12 @@ jest.unstable_mockModule('electron', () => ({
40 40
41const { ipcRenderer } = await import('electron'); 41const { ipcRenderer } = await import('electron');
42 42
43const { default: createSophieRenderer } = await import('../createSophieRenderer'); 43const { default: createSophieRenderer } = await import(
44 '../createSophieRenderer'
45);
44 46
45const event: Electron.IpcRendererEvent = null as unknown as Electron.IpcRendererEvent; 47const event: Electron.IpcRendererEvent =
48 null as unknown as Electron.IpcRendererEvent;
46 49
47const snapshot: SharedStoreSnapshotIn = { 50const snapshot: SharedStoreSnapshotIn = {
48 shouldUseDarkColors: true, 51 shouldUseDarkColors: true,
@@ -83,7 +86,10 @@ describe('createSophieRenderer', () => {
83 86
84describe('SharedStoreConnector', () => { 87describe('SharedStoreConnector', () => {
85 let sut: SophieRenderer; 88 let sut: SophieRenderer;
86 let onSharedStorePatch: (eventArg: Electron.IpcRendererEvent, patchArg: unknown) => void; 89 let onSharedStorePatch: (
90 eventArg: Electron.IpcRendererEvent,
91 patchArg: unknown,
92 ) => void;
87 const listener = { 93 const listener = {
88 // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars 94 // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
89 onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => {}), 95 onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => {}),
@@ -102,22 +108,25 @@ describe('SharedStoreConnector', () => {
102 it('should request a snapshot from the main process', async () => { 108 it('should request a snapshot from the main process', async () => {
103 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); 109 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot);
104 await sut.onSharedStoreChange(listener); 110 await sut.onSharedStoreChange(listener);
105 expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot); 111 expect(ipcRenderer.invoke).toBeCalledWith(
112 RendererToMainIpcMessage.GetSharedStoreSnapshot,
113 );
106 expect(listener.onSnapshot).toBeCalledWith(snapshot); 114 expect(listener.onSnapshot).toBeCalledWith(snapshot);
107 }); 115 });
108 116
109 it('should catch IPC errors without exposing them', async () => { 117 it('should catch IPC errors without exposing them', async () => {
110 mocked(ipcRenderer.invoke).mockRejectedValue(new Error('s3cr3t')); 118 mocked(ipcRenderer.invoke).mockRejectedValue(new Error('s3cr3t'));
111 await expect(sut.onSharedStoreChange(listener)).rejects.not.toHaveProperty( 119 await expect(
112 'message', 120 sut.onSharedStoreChange(listener),
113 expect.stringMatching(/s3cr3t/), 121 ).rejects.not.toHaveProperty('message', expect.stringMatching(/s3cr3t/));
114 );
115 expect(listener.onSnapshot).not.toBeCalled(); 122 expect(listener.onSnapshot).not.toBeCalled();
116 }); 123 });
117 124
118 it('should not pass on invalid snapshots', async () => { 125 it('should not pass on invalid snapshots', async () => {
119 mocked(ipcRenderer.invoke).mockResolvedValueOnce(invalidSnapshot); 126 mocked(ipcRenderer.invoke).mockResolvedValueOnce(invalidSnapshot);
120 await expect(sut.onSharedStoreChange(listener)).rejects.toBeInstanceOf(Error); 127 await expect(sut.onSharedStoreChange(listener)).rejects.toBeInstanceOf(
128 Error,
129 );
121 expect(listener.onSnapshot).not.toBeCalled(); 130 expect(listener.onSnapshot).not.toBeCalled();
122 }); 131 });
123 }); 132 });
@@ -125,7 +134,10 @@ describe('SharedStoreConnector', () => {
125 describe('dispatchAction', () => { 134 describe('dispatchAction', () => {
126 it('should dispatch valid actions', () => { 135 it('should dispatch valid actions', () => {
127 sut.dispatchAction(action); 136 sut.dispatchAction(action);
128 expect(ipcRenderer.send).toBeCalledWith(RendererToMainIpcMessage.DispatchAction, action); 137 expect(ipcRenderer.send).toBeCalledWith(
138 RendererToMainIpcMessage.DispatchAction,
139 action,
140 );
129 }); 141 });
130 142
131 it('should not dispatch invalid actions', () => { 143 it('should not dispatch invalid actions', () => {
@@ -142,7 +154,9 @@ describe('SharedStoreConnector', () => {
142 154
143 function itRefusesToRegisterAnotherListener(): void { 155 function itRefusesToRegisterAnotherListener(): void {
144 it('should refuse to register another listener', async () => { 156 it('should refuse to register another listener', async () => {
145 await expect(sut.onSharedStoreChange(listener)).rejects.toBeInstanceOf(Error); 157 await expect(sut.onSharedStoreChange(listener)).rejects.toBeInstanceOf(
158 Error,
159 );
146 }); 160 });
147 } 161 }
148 162
@@ -167,7 +181,9 @@ describe('SharedStoreConnector', () => {
167 }); 181 });
168 182
169 it('should catch listener errors', () => { 183 it('should catch listener errors', () => {
170 mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); 184 mocked(listener.onPatch).mockImplementation(() => {
185 throw new Error();
186 });
171 onSharedStorePatch(event, patch); 187 onSharedStorePatch(event, patch);
172 }); 188 });
173 189
@@ -175,7 +191,9 @@ describe('SharedStoreConnector', () => {
175 191
176 describe('after the listener threw in onPatch', () => { 192 describe('after the listener threw in onPatch', () => {
177 beforeEach(() => { 193 beforeEach(() => {
178 mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); 194 mocked(listener.onPatch).mockImplementation(() => {
195 throw new Error();
196 });
179 onSharedStorePatch(event, patch); 197 onSharedStorePatch(event, patch);
180 listener.onPatch.mockRestore(); 198 listener.onPatch.mockRestore();
181 }); 199 });
@@ -217,7 +235,9 @@ describe('SharedStoreConnector', () => {
217 describe('when a listener failed to register due to listener error', () => { 235 describe('when a listener failed to register due to listener error', () => {
218 beforeEach(async () => { 236 beforeEach(async () => {
219 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); 237 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot);
220 mocked(listener.onSnapshot).mockImplementation(() => { throw new Error(); }); 238 mocked(listener.onSnapshot).mockImplementation(() => {
239 throw new Error();
240 });
221 try { 241 try {
222 await sut.onSharedStoreChange(listener); 242 await sut.onSharedStoreChange(listener);
223 } catch { 243 } catch {
@@ -236,15 +256,17 @@ describe('SharedStoreConnector', () => {
236 }; 256 };
237 const listener2 = { 257 const listener2 = {
238 // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars 258 // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
239 onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => { }), 259 onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => {}),
240 // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars 260 // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
241 onPatch: jest.fn((_patch: IJsonPatch) => { }), 261 onPatch: jest.fn((_patch: IJsonPatch) => {}),
242 }; 262 };
243 263
244 it('should fetch a second snapshot', async () => { 264 it('should fetch a second snapshot', async () => {
245 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot2); 265 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot2);
246 await sut.onSharedStoreChange(listener2); 266 await sut.onSharedStoreChange(listener2);
247 expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot); 267 expect(ipcRenderer.invoke).toBeCalledWith(
268 RendererToMainIpcMessage.GetSharedStoreSnapshot,
269 );
248 expect(listener2.onSnapshot).toBeCalledWith(snapshot2); 270 expect(listener2.onSnapshot).toBeCalledWith(snapshot2);
249 }); 271 });
250 272
diff --git a/packages/preload/src/contextBridge/createSophieRenderer.ts b/packages/preload/src/contextBridge/createSophieRenderer.ts
index 2055080..b97503d 100644
--- a/packages/preload/src/contextBridge/createSophieRenderer.ts
+++ b/packages/preload/src/contextBridge/createSophieRenderer.ts
@@ -37,15 +37,18 @@ class SharedStoreConnector {
37 private listener: SharedStoreListener | null = null; 37 private listener: SharedStoreListener | null = null;
38 38
39 constructor(private readonly allowReplaceListener: boolean) { 39 constructor(private readonly allowReplaceListener: boolean) {
40 ipcRenderer.on(MainToRendererIpcMessage.SharedStorePatch, (_event, patch) => { 40 ipcRenderer.on(
41 try { 41 MainToRendererIpcMessage.SharedStorePatch,
42 // `mobx-state-tree` will validate the patch, so we can safely cast here. 42 (_event, patch) => {
43 this.listener?.onPatch(patch as IJsonPatch); 43 try {
44 } catch (err) { 44 // `mobx-state-tree` will validate the patch, so we can safely cast here.
45 log.error('Shared store listener onPatch failed', err); 45 this.listener?.onPatch(patch as IJsonPatch);
46 this.listener = null; 46 } catch (err) {
47 } 47 log.error('Shared store listener onPatch failed', err);
48 }); 48 this.listener = null;
49 }
50 },
51 );
49 } 52 }
50 53
51 async onSharedStoreChange(listener: SharedStoreListener): Promise<void> { 54 async onSharedStoreChange(listener: SharedStoreListener): Promise<void> {
@@ -56,7 +59,9 @@ class SharedStoreConnector {
56 let success = false; 59 let success = false;
57 let snapshot: unknown | null = null; 60 let snapshot: unknown | null = null;
58 try { 61 try {
59 snapshot = await ipcRenderer.invoke(RendererToMainIpcMessage.GetSharedStoreSnapshot); 62 snapshot = await ipcRenderer.invoke(
63 RendererToMainIpcMessage.GetSharedStoreSnapshot,
64 );
60 success = true; 65 success = true;
61 } catch (err) { 66 } catch (err) {
62 log.error('Failed to get initial shared store snapshot', err); 67 log.error('Failed to get initial shared store snapshot', err);
@@ -87,7 +92,9 @@ function dispatchAction(actionToDispatch: Action): void {
87 } 92 }
88} 93}
89 94
90export default function createSophieRenderer(allowReplaceListener: boolean): SophieRenderer { 95export default function createSophieRenderer(
96 allowReplaceListener: boolean,
97): SophieRenderer {
91 const connector = new SharedStoreConnector(allowReplaceListener); 98 const connector = new SharedStoreConnector(allowReplaceListener);
92 return { 99 return {
93 onSharedStoreChange: connector.onSharedStoreChange.bind(connector), 100 onSharedStoreChange: connector.onSharedStoreChange.bind(connector),
diff --git a/packages/preload/tsconfig.json b/packages/preload/tsconfig.json
index 0cb1390..18c72b4 100644
--- a/packages/preload/tsconfig.json
+++ b/packages/preload/tsconfig.json
@@ -2,14 +2,8 @@
2 "extends": "../../config/tsconfig.base.json", 2 "extends": "../../config/tsconfig.base.json",
3 "compilerOptions": { 3 "compilerOptions": {
4 "noEmit": true, 4 "noEmit": true,
5 "lib": [ 5 "lib": ["dom", "dom.iterable", "esnext"],
6 "dom", 6 "types": ["@types/jest"]
7 "dom.iterable",
8 "esnext"
9 ],
10 "types": [
11 "@types/jest"
12 ]
13 }, 7 },
14 "references": [ 8 "references": [
15 { 9 {
diff --git a/packages/preload/types/importMeta.d.ts b/packages/preload/types/importMeta.d.ts
index 9b73170..ff3b17c 100644
--- a/packages/preload/types/importMeta.d.ts
+++ b/packages/preload/types/importMeta.d.ts
@@ -3,5 +3,5 @@ interface ImportMeta {
3 DEV: boolean; 3 DEV: boolean;
4 MODE: string; 4 MODE: string;
5 PROD: boolean; 5 PROD: boolean;
6 } 6 };
7} 7}
diff --git a/packages/renderer/.eslinrc.cjs b/packages/renderer/.eslinrc.cjs
index 37d27ad..ee3cca6 100644
--- a/packages/renderer/.eslinrc.cjs
+++ b/packages/renderer/.eslinrc.cjs
@@ -5,10 +5,7 @@ module.exports = {
5 }, 5 },
6 overrides: [ 6 overrides: [
7 { 7 {
8 files: [ 8 files: ['.eslintrc.cjs', 'vite.config.js'],
9 '.eslintrc.cjs',
10 'vite.config.js',
11 ],
12 env: { 9 env: {
13 browser: false, 10 browser: false,
14 node: true, 11 node: true,
diff --git a/packages/renderer/index.html b/packages/renderer/index.html
index 08469c7..039ce61 100644
--- a/packages/renderer/index.html
+++ b/packages/renderer/index.html
@@ -1,9 +1,12 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html lang="en"> 2<html lang="en">
3 <head> 3 <head>
4 <meta charset="UTF-8"> 4 <meta charset="UTF-8" />
5 <meta http-equiv="Content-Security-Policy" content="script-src 'self' blob:"> 5 <meta
6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 http-equiv="Content-Security-Policy"
7 content="script-src 'self' blob:"
8 />
9 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>Sophie</title> 10 <title>Sophie</title>
8 </head> 11 </head>
9 <body> 12 <body>
diff --git a/packages/renderer/src/components/BrowserViewPlaceholder.tsx b/packages/renderer/src/components/BrowserViewPlaceholder.tsx
index 11c09d6..8f055e7 100644
--- a/packages/renderer/src/components/BrowserViewPlaceholder.tsx
+++ b/packages/renderer/src/components/BrowserViewPlaceholder.tsx
@@ -29,36 +29,37 @@ export default observer(() => {
29 const store = useStore(); 29 const store = useStore();
30 30
31 // eslint-disable-next-line react-hooks/exhaustive-deps -- react-hooks doesn't support `throttle`. 31 // eslint-disable-next-line react-hooks/exhaustive-deps -- react-hooks doesn't support `throttle`.
32 const onResize = useCallback(throttle(([entry]: ResizeObserverEntry[]) => { 32 const onResize = useCallback(
33 if (entry) { 33 throttle(([entry]: ResizeObserverEntry[]) => {
34 const { 34 if (entry) {
35 x, 35 const { x, y, width, height } = entry.target.getBoundingClientRect();
36 y, 36 store.setBrowserViewBounds({
37 width, 37 x,
38 height, 38 y,
39 } = entry.target.getBoundingClientRect(); 39 width,
40 store.setBrowserViewBounds({ 40 height,
41 x, 41 });
42 y, 42 }
43 width, 43 }, 100),
44 height, 44 [store],
45 }); 45 );
46 }
47 }, 100), [store]);
48 46
49 const resizeObserverRef = useRef<ResizeObserver | null>(null); 47 const resizeObserverRef = useRef<ResizeObserver | null>(null);
50 48
51 const ref = useCallback((element: HTMLElement | null) => { 49 const ref = useCallback(
52 if (resizeObserverRef.current !== null) { 50 (element: HTMLElement | null) => {
53 resizeObserverRef.current.disconnect(); 51 if (resizeObserverRef.current !== null) {
54 } 52 resizeObserverRef.current.disconnect();
55 if (element === null) { 53 }
56 resizeObserverRef.current = null; 54 if (element === null) {
57 return; 55 resizeObserverRef.current = null;
58 } 56 return;
59 resizeObserverRef.current = new ResizeObserver(onResize); 57 }
60 resizeObserverRef.current.observe(element); 58 resizeObserverRef.current = new ResizeObserver(onResize);
61 }, [onResize, resizeObserverRef]); 59 resizeObserverRef.current.observe(element);
60 },
61 [onResize, resizeObserverRef],
62 );
62 63
63 return ( 64 return (
64 <Box 65 <Box
diff --git a/packages/renderer/src/components/StoreProvider.tsx b/packages/renderer/src/components/StoreProvider.tsx
index cde6a31..bb8495c 100644
--- a/packages/renderer/src/components/StoreProvider.tsx
+++ b/packages/renderer/src/components/StoreProvider.tsx
@@ -32,13 +32,14 @@ export function useStore(): RendererStore {
32 return store; 32 return store;
33} 33}
34 34
35export default function StoreProvider({ children, store }: { 35export default function StoreProvider({
36 children: JSX.Element | JSX.Element[], 36 children,
37 store: RendererStore, 37 store,
38}: {
39 children: JSX.Element | JSX.Element[];
40 store: RendererStore;
38}): JSX.Element { 41}): JSX.Element {
39 return ( 42 return (
40 <StoreContext.Provider value={store}> 43 <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
41 {children}
42 </StoreContext.Provider>
43 ); 44 );
44} 45}
diff --git a/packages/renderer/src/components/ThemeProvider.tsx b/packages/renderer/src/components/ThemeProvider.tsx
index eacaa52..3943371 100644
--- a/packages/renderer/src/components/ThemeProvider.tsx
+++ b/packages/renderer/src/components/ThemeProvider.tsx
@@ -27,20 +27,18 @@ import React from 'react';
27 27
28import { useStore } from './StoreProvider'; 28import { useStore } from './StoreProvider';
29 29
30export default observer(({ children }: { 30export default observer(
31 children: JSX.Element | JSX.Element[]; 31 ({ children }: { children: JSX.Element | JSX.Element[] }) => {
32}) => { 32 const {
33 const { shared: { shouldUseDarkColors } } = useStore(); 33 shared: { shouldUseDarkColors },
34 } = useStore();
34 35
35 const theme = createTheme({ 36 const theme = createTheme({
36 palette: { 37 palette: {
37 mode: shouldUseDarkColors ? 'dark' : 'light', 38 mode: shouldUseDarkColors ? 'dark' : 'light',
38 }, 39 },
39 }); 40 });
40 41
41 return ( 42 return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>;
42 <MuiThemeProvider theme={theme}> 43 },
43 {children} 44);
44 </MuiThemeProvider>
45 );
46});
diff --git a/packages/renderer/src/components/ToggleDarkModeButton.tsx b/packages/renderer/src/components/ToggleDarkModeButton.tsx
index c8ffdf0..695756a 100644
--- a/packages/renderer/src/components/ToggleDarkModeButton.tsx
+++ b/packages/renderer/src/components/ToggleDarkModeButton.tsx
@@ -28,7 +28,9 @@ import { useStore } from './StoreProvider';
28 28
29export default observer(() => { 29export default observer(() => {
30 const store = useStore(); 30 const store = useStore();
31 const { shared: { shouldUseDarkColors } } = store; 31 const {
32 shared: { shouldUseDarkColors },
33 } = store;
32 34
33 return ( 35 return (
34 <IconButton 36 <IconButton
diff --git a/packages/renderer/src/devTools.ts b/packages/renderer/src/devTools.ts
index 3d3ba99..cb695c3 100644
--- a/packages/renderer/src/devTools.ts
+++ b/packages/renderer/src/devTools.ts
@@ -35,7 +35,9 @@ import type { IAnyStateTreeNode } from 'mobx-state-tree';
35 * @return A promise that resolves when the store was exposed to the devtools. 35 * @return A promise that resolves when the store was exposed to the devtools.
36 * @see https://github.com/SocketCluster/socketcluster-client/issues/118#issuecomment-469064682 36 * @see https://github.com/SocketCluster/socketcluster-client/issues/118#issuecomment-469064682
37 */ 37 */
38export async function exposeToReduxDevtools(model: IAnyStateTreeNode): Promise<void> { 38export async function exposeToReduxDevtools(
39 model: IAnyStateTreeNode,
40): Promise<void> {
39 (window as { global?: unknown }).global = window; 41 (window as { global?: unknown }).global = window;
40 42
41 // Hack to load dev dependencies on demand. 43 // Hack to load dev dependencies on demand.
diff --git a/packages/renderer/src/stores/RendererStore.ts b/packages/renderer/src/stores/RendererStore.ts
index e684759..0b78ce1 100644
--- a/packages/renderer/src/stores/RendererStore.ts
+++ b/packages/renderer/src/stores/RendererStore.ts
@@ -24,12 +24,7 @@ import {
24 SophieRenderer, 24 SophieRenderer,
25 ThemeSource, 25 ThemeSource,
26} from '@sophie/shared'; 26} from '@sophie/shared';
27import { 27import { applySnapshot, applyPatch, Instance, types } from 'mobx-state-tree';
28 applySnapshot,
29 applyPatch,
30 Instance,
31 types,
32} from 'mobx-state-tree';
33 28
34import { getLogger } from '../utils/log'; 29import { getLogger } from '../utils/log';
35 30
@@ -38,29 +33,31 @@ import { getEnv } from './RendererEnv';
38 33
39const log = getLogger('RendererStore'); 34const log = getLogger('RendererStore');
40 35
41export const rendererStore = types.model('RendererStore', { 36export const rendererStore = types
42 shared: types.optional(sharedStore, {}), 37 .model('RendererStore', {
43}).actions((self) => ({ 38 shared: types.optional(sharedStore, {}),
44 setBrowserViewBounds(browserViewBounds: BrowserViewBounds): void { 39 })
45 getEnv(self).dispatchMainAction({ 40 .actions((self) => ({
46 action: 'set-browser-view-bounds', 41 setBrowserViewBounds(browserViewBounds: BrowserViewBounds): void {
47 browserViewBounds, 42 getEnv(self).dispatchMainAction({
48 }); 43 action: 'set-browser-view-bounds',
49 }, 44 browserViewBounds,
50 setThemeSource(themeSource: ThemeSource): void { 45 });
51 getEnv(self).dispatchMainAction({ 46 },
52 action: 'set-theme-source', 47 setThemeSource(themeSource: ThemeSource): void {
53 themeSource, 48 getEnv(self).dispatchMainAction({
54 }); 49 action: 'set-theme-source',
55 }, 50 themeSource,
56 toggleDarkMode(): void { 51 });
57 if (self.shared.shouldUseDarkColors) { 52 },
58 this.setThemeSource('light'); 53 toggleDarkMode(): void {
59 } else { 54 if (self.shared.shouldUseDarkColors) {
60 this.setThemeSource('dark'); 55 this.setThemeSource('light');
61 } 56 } else {
62 }, 57 this.setThemeSource('dark');
63})); 58 }
59 },
60 }));
64 61
65export interface RendererStore extends Instance<typeof rendererStore> {} 62export interface RendererStore extends Instance<typeof rendererStore> {}
66 63
@@ -72,22 +69,26 @@ export interface RendererStore extends Instance<typeof rendererStore> {}
72 * 69 *
73 * @param ipc The `sophieRenderer` context bridge. 70 * @param ipc The `sophieRenderer` context bridge.
74 */ 71 */
75export function createAndConnectRendererStore(ipc: SophieRenderer): RendererStore { 72export function createAndConnectRendererStore(
73 ipc: SophieRenderer,
74): RendererStore {
76 const env: RendererEnv = { 75 const env: RendererEnv = {
77 dispatchMainAction: ipc.dispatchAction, 76 dispatchMainAction: ipc.dispatchAction,
78 }; 77 };
79 const store = rendererStore.create({}, env); 78 const store = rendererStore.create({}, env);
80 79
81 ipc.onSharedStoreChange({ 80 ipc
82 onSnapshot(snapshot) { 81 .onSharedStoreChange({
83 applySnapshot(store.shared, snapshot); 82 onSnapshot(snapshot) {
84 }, 83 applySnapshot(store.shared, snapshot);
85 onPatch(patch) { 84 },
86 applyPatch(store.shared, patch); 85 onPatch(patch) {
87 }, 86 applyPatch(store.shared, patch);
88 }).catch((err) => { 87 },
89 log.error('Failed to connect to shared store', err); 88 })
90 }); 89 .catch((err) => {
90 log.error('Failed to connect to shared store', err);
91 });
91 92
92 return store; 93 return store;
93} 94}
diff --git a/packages/renderer/tsconfig.json b/packages/renderer/tsconfig.json
index 14c3e0c..5453330 100644
--- a/packages/renderer/tsconfig.json
+++ b/packages/renderer/tsconfig.json
@@ -3,14 +3,8 @@
3 "compilerOptions": { 3 "compilerOptions": {
4 "noEmit": true, 4 "noEmit": true,
5 "jsx": "react", 5 "jsx": "react",
6 "lib": [ 6 "lib": ["dom", "dom.iterable", "esnext"],
7 "dom", 7 "types": ["vite/client"]
8 "dom.iterable",
9 "esnext"
10 ],
11 "types": [
12 "vite/client"
13 ]
14 }, 8 },
15 "references": [ 9 "references": [
16 { 10 {
diff --git a/packages/renderer/vite.config.js b/packages/renderer/vite.config.js
index 6440ead..e20e0f1 100644
--- a/packages/renderer/vite.config.js
+++ b/packages/renderer/vite.config.js
@@ -46,9 +46,7 @@ export default {
46 preserveSymlinks: true, 46 preserveSymlinks: true,
47 }, 47 },
48 optimizeDeps: { 48 optimizeDeps: {
49 exclude: [ 49 exclude: ['@sophie/shared'],
50 '@sophie/shared',
51 ],
52 }, 50 },
53 build: { 51 build: {
54 target: chrome, 52 target: chrome,
@@ -59,11 +57,7 @@ export default {
59 minify: !isDevelopment, 57 minify: !isDevelopment,
60 brotliSize: false, 58 brotliSize: false,
61 rollupOptions: { 59 rollupOptions: {
62 external: [ 60 external: ['mst-middlewares', 'remotedev', ...builtinModules],
63 'mst-middlewares',
64 'remotedev',
65 ...builtinModules,
66 ],
67 output: { 61 output: {
68 banner, 62 banner,
69 }, 63 },
diff --git a/packages/service-inject/.eslintrc.cjs b/packages/service-inject/.eslintrc.cjs
index 5555f5b..3131abd 100644
--- a/packages/service-inject/.eslintrc.cjs
+++ b/packages/service-inject/.eslintrc.cjs
@@ -5,10 +5,7 @@ module.exports = {
5 }, 5 },
6 overrides: [ 6 overrides: [
7 { 7 {
8 files: [ 8 files: ['.eslintrc.cjs', 'esbuild.config.js'],
9 '.eslintrc.cjs',
10 'esbuild.config.js',
11 ],
12 env: { 9 env: {
13 browser: false, 10 browser: false,
14 node: true, 11 node: true,
diff --git a/packages/service-inject/esbuild.config.js b/packages/service-inject/esbuild.config.js
index d0b04bb..795b0f6 100644
--- a/packages/service-inject/esbuild.config.js
+++ b/packages/service-inject/esbuild.config.js
@@ -4,9 +4,7 @@ import getEsbuildConfig from '../../config/getEsbuildConfig.js';
4 4
5export default getEsbuildConfig({ 5export default getEsbuildConfig({
6 absWorkingDir: fileURLToDirname(import.meta.url), 6 absWorkingDir: fileURLToDirname(import.meta.url),
7 entryPoints: [ 7 entryPoints: ['src/index.ts'],
8 'src/index.ts',
9 ],
10 outfile: 'dist/index.js', 8 outfile: 'dist/index.js',
11 format: 'iife', 9 format: 'iife',
12 platform: 'browser', 10 platform: 'browser',
diff --git a/packages/service-inject/tsconfig.json b/packages/service-inject/tsconfig.json
index 8f84d98..33ce1de 100644
--- a/packages/service-inject/tsconfig.json
+++ b/packages/service-inject/tsconfig.json
@@ -2,20 +2,12 @@
2 "extends": "../../config/tsconfig.base.json", 2 "extends": "../../config/tsconfig.base.json",
3 "compilerOptions": { 3 "compilerOptions": {
4 "noEmit": true, 4 "noEmit": true,
5 "lib": [ 5 "lib": ["dom", "dom.iterable", "esnext"]
6 "dom",
7 "dom.iterable",
8 "esnext"
9 ]
10 }, 6 },
11 "references": [ 7 "references": [
12 { 8 {
13 "path": "../service-shared/tsconfig.build.json" 9 "path": "../service-shared/tsconfig.build.json"
14 } 10 }
15 ], 11 ],
16 "include": [ 12 "include": ["src/**/*.ts", ".eslintrc.cjs", "esbuild.config.js"]
17 "src/**/*.ts",
18 ".eslintrc.cjs",
19 "esbuild.config.js"
20 ]
21} 13}
diff --git a/packages/service-preload/esbuild.config.js b/packages/service-preload/esbuild.config.js
index 66f5e84..d888987 100644
--- a/packages/service-preload/esbuild.config.js
+++ b/packages/service-preload/esbuild.config.js
@@ -4,15 +4,11 @@ import getEsbuildConfig from '../../config/getEsbuildConfig.js';
4 4
5export default getEsbuildConfig({ 5export default getEsbuildConfig({
6 absWorkingDir: fileURLToDirname(import.meta.url), 6 absWorkingDir: fileURLToDirname(import.meta.url),
7 entryPoints: [ 7 entryPoints: ['src/index.ts'],
8 'src/index.ts',
9 ],
10 outfile: 'dist/index.cjs', 8 outfile: 'dist/index.cjs',
11 format: 'cjs', 9 format: 'cjs',
12 platform: 'node', 10 platform: 'node',
13 target: chrome, 11 target: chrome,
14 sourcemap: 'inline', 12 sourcemap: 'inline',
15 external: [ 13 external: ['electron'],
16 'electron',
17 ],
18}); 14});
diff --git a/packages/service-preload/tsconfig.json b/packages/service-preload/tsconfig.json
index 768c464..189859e 100644
--- a/packages/service-preload/tsconfig.json
+++ b/packages/service-preload/tsconfig.json
@@ -2,11 +2,7 @@
2 "extends": "../../config/tsconfig.base.json", 2 "extends": "../../config/tsconfig.base.json",
3 "compilerOptions": { 3 "compilerOptions": {
4 "noEmit": true, 4 "noEmit": true,
5 "lib": [ 5 "lib": ["dom", "dom.iterable", "esnext"]
6 "dom",
7 "dom.iterable",
8 "esnext"
9 ]
10 }, 6 },
11 "references": [ 7 "references": [
12 { 8 {
diff --git a/packages/service-preload/types/importMeta.d.ts b/packages/service-preload/types/importMeta.d.ts
index 9b73170..ff3b17c 100644
--- a/packages/service-preload/types/importMeta.d.ts
+++ b/packages/service-preload/types/importMeta.d.ts
@@ -3,5 +3,5 @@ interface ImportMeta {
3 DEV: boolean; 3 DEV: boolean;
4 MODE: string; 4 MODE: string;
5 PROD: boolean; 5 PROD: boolean;
6 } 6 };
7} 7}
diff --git a/packages/service-shared/.eslintrc.cjs b/packages/service-shared/.eslintrc.cjs
index c19829d..25fd252 100644
--- a/packages/service-shared/.eslintrc.cjs
+++ b/packages/service-shared/.eslintrc.cjs
@@ -6,10 +6,7 @@ module.exports = {
6 }, 6 },
7 overrides: [ 7 overrides: [
8 { 8 {
9 files: [ 9 files: ['.eslintrc.cjs', 'esbuild.config.js'],
10 '.eslintrc.cjs',
11 'esbuild.config.js',
12 ],
13 env: { 10 env: {
14 node: true, 11 node: true,
15 }, 12 },
diff --git a/packages/service-shared/esbuild.config.js b/packages/service-shared/esbuild.config.js
index ccee72c..2b0dec8 100644
--- a/packages/service-shared/esbuild.config.js
+++ b/packages/service-shared/esbuild.config.js
@@ -4,9 +4,7 @@ import getEsbuildConfig from '../../config/getEsbuildConfig.js';
4 4
5export default getEsbuildConfig({ 5export default getEsbuildConfig({
6 absWorkingDir: fileURLToDirname(import.meta.url), 6 absWorkingDir: fileURLToDirname(import.meta.url),
7 entryPoints: [ 7 entryPoints: ['src/index.ts'],
8 'src/index.ts',
9 ],
10 outfile: 'dist/index.mjs', 8 outfile: 'dist/index.mjs',
11 format: 'esm', 9 format: 'esm',
12 // The package that includes this one will have a header comment, 10 // The package that includes this one will have a header comment,
@@ -14,7 +12,5 @@ export default getEsbuildConfig({
14 banner: {}, 12 banner: {},
15 platform: 'node', 13 platform: 'node',
16 target: [chrome, node], 14 target: [chrome, node],
17 external: [ 15 external: ['zod'],
18 'zod',
19 ],
20}); 16});
diff --git a/packages/service-shared/src/index.ts b/packages/service-shared/src/index.ts
index e111347..94be734 100644
--- a/packages/service-shared/src/index.ts
+++ b/packages/service-shared/src/index.ts
@@ -20,11 +20,5 @@
20 20
21export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc'; 21export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc';
22 22
23export type { 23export type { UnreadCount, WebSource } from './schemas';
24 UnreadCount, 24export { unreadCount, webSource } from './schemas';
25 WebSource,
26} from './schemas';
27export {
28 unreadCount,
29 webSource,
30} from './schemas';
diff --git a/packages/service-shared/src/ipc.ts b/packages/service-shared/src/ipc.ts
index c0dab11..e0a8755 100644
--- a/packages/service-shared/src/ipc.ts
+++ b/packages/service-shared/src/ipc.ts
@@ -18,8 +18,7 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21export enum MainToServiceIpcMessage { 21export enum MainToServiceIpcMessage {}
22}
23 22
24export enum ServiceToMainIpcMessage { 23export enum ServiceToMainIpcMessage {
25 ApiExposedInMainWorld = 'sophie-service-to-main:api-exposed-in-main-world', 24 ApiExposedInMainWorld = 'sophie-service-to-main:api-exposed-in-main-world',
diff --git a/packages/service-shared/tsconfig.build.json b/packages/service-shared/tsconfig.build.json
index 9a0c835..d300514 100644
--- a/packages/service-shared/tsconfig.build.json
+++ b/packages/service-shared/tsconfig.build.json
@@ -6,7 +6,5 @@
6 "emitDeclarationOnly": true, 6 "emitDeclarationOnly": true,
7 "rootDir": "src" 7 "rootDir": "src"
8 }, 8 },
9 "include": [ 9 "include": ["src/**/*.ts"]
10 "src/**/*.ts"
11 ]
12} 10}
diff --git a/packages/service-shared/tsconfig.json b/packages/service-shared/tsconfig.json
index 3e6c6ff..daad7c4 100644
--- a/packages/service-shared/tsconfig.json
+++ b/packages/service-shared/tsconfig.json
@@ -7,9 +7,5 @@
7 "noEmit": true, 7 "noEmit": true,
8 "rootDir": null 8 "rootDir": null
9 }, 9 },
10 "include": [ 10 "include": ["src/**/*.ts", ".eslintrc.cjs", "esbuild.config.js"]
11 "src/**/*.ts",
12 ".eslintrc.cjs",
13 "esbuild.config.js"
14 ]
15} 11}
diff --git a/packages/shared/.eslintrc.cjs b/packages/shared/.eslintrc.cjs
index c19829d..25fd252 100644
--- a/packages/shared/.eslintrc.cjs
+++ b/packages/shared/.eslintrc.cjs
@@ -6,10 +6,7 @@ module.exports = {
6 }, 6 },
7 overrides: [ 7 overrides: [
8 { 8 {
9 files: [ 9 files: ['.eslintrc.cjs', 'esbuild.config.js'],
10 '.eslintrc.cjs',
11 'esbuild.config.js',
12 ],
13 env: { 10 env: {
14 node: true, 11 node: true,
15 }, 12 },
diff --git a/packages/shared/esbuild.config.js b/packages/shared/esbuild.config.js
index 78249ab..44501bd 100644
--- a/packages/shared/esbuild.config.js
+++ b/packages/shared/esbuild.config.js
@@ -4,9 +4,7 @@ import getEsbuildConfig from '../../config/getEsbuildConfig.js';
4 4
5export default getEsbuildConfig({ 5export default getEsbuildConfig({
6 absWorkingDir: fileURLToDirname(import.meta.url), 6 absWorkingDir: fileURLToDirname(import.meta.url),
7 entryPoints: [ 7 entryPoints: ['src/index.ts'],
8 'src/index.ts',
9 ],
10 outfile: 'dist/index.mjs', 8 outfile: 'dist/index.mjs',
11 format: 'esm', 9 format: 'esm',
12 // The package that includes this one will have a header comment, 10 // The package that includes this one will have a header comment,
@@ -14,9 +12,5 @@ export default getEsbuildConfig({
14 banner: {}, 12 banner: {},
15 platform: 'node', 13 platform: 'node',
16 target: [chrome, node], 14 target: [chrome, node],
17 external: [ 15 external: ['mobx', 'mobx-state-tree', 'zod'],
18 'mobx',
19 'mobx-state-tree',
20 'zod',
21 ],
22}); 16});
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 9828ec4..6383f63 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -22,18 +22,14 @@ export type { SophieRenderer } from './contextBridge/SophieRenderer';
22 22
23export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc'; 23export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc';
24 24
25export type { 25export type { Action, BrowserViewBounds, ThemeSource } from './schemas';
26 Action, 26export { action, browserViewBounds, themeSource } from './schemas';
27 BrowserViewBounds,
28 ThemeSource,
29} from './schemas';
30export {
31 action,
32 browserViewBounds,
33 themeSource,
34} from './schemas';
35 27
36export type { Config, ConfigSnapshotIn, ConfigSnapshotOut } from './stores/Config'; 28export type {
29 Config,
30 ConfigSnapshotIn,
31 ConfigSnapshotOut,
32} from './stores/Config';
37export { config } from './stores/Config'; 33export { config } from './stores/Config';
38 34
39export type { 35export type {
diff --git a/packages/shared/src/stores/Config.ts b/packages/shared/src/stores/Config.ts
index 432945c..1d98a33 100644
--- a/packages/shared/src/stores/Config.ts
+++ b/packages/shared/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 { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree';
22 Instance,
23 types,
24 SnapshotIn,
25 SnapshotOut,
26} from 'mobx-state-tree';
27 22
28import { themeSource } from '../schemas'; 23import { themeSource } from '../schemas';
29 24
diff --git a/packages/shared/src/stores/SharedStore.ts b/packages/shared/src/stores/SharedStore.ts
index c6c3ddc..cb14394 100644
--- a/packages/shared/src/stores/SharedStore.ts
+++ b/packages/shared/src/stores/SharedStore.ts
@@ -37,7 +37,8 @@ export interface SharedStore extends Instance<typeof sharedStore> {}
37 37
38export interface SharedStoreSnapshotIn extends SnapshotIn<typeof sharedStore> {} 38export interface SharedStoreSnapshotIn extends SnapshotIn<typeof sharedStore> {}
39 39
40export interface SharedStoreSnapshotOut extends SnapshotOut<typeof sharedStore> {} 40export interface SharedStoreSnapshotOut
41 extends SnapshotOut<typeof sharedStore> {}
41 42
42export interface SharedStoreListener { 43export interface SharedStoreListener {
43 onSnapshot(snapshot: SharedStoreSnapshotIn): void; 44 onSnapshot(snapshot: SharedStoreSnapshotIn): void;
diff --git a/packages/shared/tsconfig.build.json b/packages/shared/tsconfig.build.json
index 9a0c835..d300514 100644
--- a/packages/shared/tsconfig.build.json
+++ b/packages/shared/tsconfig.build.json
@@ -6,7 +6,5 @@
6 "emitDeclarationOnly": true, 6 "emitDeclarationOnly": true,
7 "rootDir": "src" 7 "rootDir": "src"
8 }, 8 },
9 "include": [ 9 "include": ["src/**/*.ts"]
10 "src/**/*.ts"
11 ]
12} 10}
diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json
index 3e6c6ff..daad7c4 100644
--- a/packages/shared/tsconfig.json
+++ b/packages/shared/tsconfig.json
@@ -7,9 +7,5 @@
7 "noEmit": true, 7 "noEmit": true,
8 "rootDir": null 8 "rootDir": null
9 }, 9 },
10 "include": [ 10 "include": ["src/**/*.ts", ".eslintrc.cjs", "esbuild.config.js"]
11 "src/**/*.ts",
12 ".eslintrc.cjs",
13 "esbuild.config.js"
14 ]
15} 11}
diff --git a/prettier.config.cjs b/prettier.config.cjs
new file mode 100644
index 0000000..de2f53c
--- /dev/null
+++ b/prettier.config.cjs
@@ -0,0 +1,4 @@
1module.exports = {
2 singleQuote: true,
3 trailingComma: 'all',
4};
diff --git a/scripts/build.js b/scripts/build.js
index 1236a6c..88267e5 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -14,7 +14,9 @@ const thisDir = fileURLToDirname(import.meta.url);
14async function buildPackageEsbuild(packageName) { 14async function buildPackageEsbuild(packageName) {
15 /** @type {{ default: import('esbuild').BuildOptions }} */ 15 /** @type {{ default: import('esbuild').BuildOptions }} */
16 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Read untyped config file. 16 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Read untyped config file.
17 const { default: config } = await import(`../packages/${packageName}/esbuild.config.js`); 17 const { default: config } = await import(
18 `../packages/${packageName}/esbuild.config.js`
19 );
18 return esbuildBuild(config); 20 return esbuildBuild(config);
19} 21}
20 22
@@ -32,18 +34,21 @@ function buildAll() {
32 const buildServiceShared = buildPackageEsbuild('service-shared'); 34 const buildServiceShared = buildPackageEsbuild('service-shared');
33 const buildShared = buildPackageEsbuild('shared'); 35 const buildShared = buildPackageEsbuild('shared');
34 return Promise.all([ 36 return Promise.all([
35 Promise.all([ 37 Promise.all([buildServiceShared, buildShared]).then(() =>
36 buildServiceShared, 38 buildPackageEsbuild('main'),
37 buildShared, 39 ),
38 ]).then(() => buildPackageEsbuild('main')), 40 buildServiceShared.then(() =>
39 buildServiceShared.then(() => Promise.all([ 41 Promise.all([
40 buildPackageEsbuild('service-inject'), 42 buildPackageEsbuild('service-inject'),
41 buildPackageEsbuild('service-preload'), 43 buildPackageEsbuild('service-preload'),
42 ])), 44 ]),
43 buildShared.then(() => Promise.all([ 45 ),
44 buildPackageEsbuild('preload'), 46 buildShared.then(() =>
45 buildPackageVite('renderer'), 47 Promise.all([
46 ])), 48 buildPackageEsbuild('preload'),
49 buildPackageVite('renderer'),
50 ]),
51 ),
47 ]); 52 ]);
48} 53}
49 54
diff --git a/scripts/update-electron-vendors.js b/scripts/update-electron-vendors.js
index 91cdb61..6ff5c06 100644
--- a/scripts/update-electron-vendors.js
+++ b/scripts/update-electron-vendors.js
@@ -16,10 +16,13 @@ const thisDir = fileURLToDirname(import.meta.url);
16 * @returns {NodeJS.ProcessVersions} 16 * @returns {NodeJS.ProcessVersions}
17 */ 17 */
18function getVendors() { 18function getVendors() {
19 const output = execSync(`${electronPath.toString()} -p "JSON.stringify(process.versions)"`, { 19 const output = execSync(
20 env: { ELECTRON_RUN_AS_NODE: '1' }, 20 `${electronPath.toString()} -p "JSON.stringify(process.versions)"`,
21 encoding: 'utf-8', 21 {
22 }); 22 env: { ELECTRON_RUN_AS_NODE: '1' },
23 encoding: 'utf-8',
24 },
25 );
23 26
24 // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Read untyped output. 27 // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Read untyped output.
25 return JSON.parse(output); 28 return JSON.parse(output);
@@ -34,17 +37,22 @@ function updateVendors() {
34 const electronRelease = getVendors(); 37 const electronRelease = getVendors();
35 38
36 const nodeMajorVersion = electronRelease.node.split('.')[0]; 39 const nodeMajorVersion = electronRelease.node.split('.')[0];
37 const chromeMajorVersion = electronRelease.v8.split('.')[0] + electronRelease.v8.split('.')[1]; 40 const chromeMajorVersion =
41 electronRelease.v8.split('.')[0] + electronRelease.v8.split('.')[1];
38 42
39 const browserslistrcPath = join(thisDir, '../.browserslistrc'); 43 const browserslistrcPath = join(thisDir, '../.browserslistrc');
40 44
41 return Promise.all([ 45 return Promise.all([
42 writeFile( 46 writeFile(
43 join(thisDir, '../.electron-vendors.cache.json'), 47 join(thisDir, '../.electron-vendors.cache.json'),
44 `${JSON.stringify({ 48 `${JSON.stringify(
45 chrome: chromeMajorVersion, 49 {
46 node: nodeMajorVersion, 50 chrome: chromeMajorVersion,
47 }, null, 2)}\n`, 51 node: nodeMajorVersion,
52 },
53 null,
54 2,
55 )}\n`,
48 ), 56 ),
49 57
50 writeFile(browserslistrcPath, `Chrome ${chromeMajorVersion}\n`, 'utf8'), 58 writeFile(browserslistrcPath, `Chrome ${chromeMajorVersion}\n`, 'utf8'),
diff --git a/scripts/watch.js b/scripts/watch.js
index 1345a0f..cf5dbfd 100644
--- a/scripts/watch.js
+++ b/scripts/watch.js
@@ -15,7 +15,10 @@ const thisDir = fileURLToDirname(import.meta.url);
15const sharedModule = join(thisDir, '../packages/shared/dist/index.mjs'); 15const sharedModule = join(thisDir, '../packages/shared/dist/index.mjs');
16 16
17/** @type {string} */ 17/** @type {string} */
18const serviceSharedModule = join(thisDir, '../packages/service-shared/dist/index.mjs'); 18const serviceSharedModule = join(
19 thisDir,
20 '../packages/service-shared/dist/index.mjs',
21);
19 22
20/** @type {RegExp[]} */ 23/** @type {RegExp[]} */
21const stderrIgnorePatterns = [ 24const stderrIgnorePatterns = [
@@ -37,7 +40,9 @@ const stderrIgnorePatterns = [
37async function setupEsbuildWatcher(packageName, extraPaths, callback) { 40async function setupEsbuildWatcher(packageName, extraPaths, callback) {
38 /** @type {{ default: import('esbuild').BuildOptions }} */ 41 /** @type {{ default: import('esbuild').BuildOptions }} */
39 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Read untyped config file. 42 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Read untyped config file.
40 const { default: config } = await import(`../packages/${packageName}/esbuild.config.js`); 43 const { default: config } = await import(
44 `../packages/${packageName}/esbuild.config.js`
45 );
41 config.logLevel = 'info'; 46 config.logLevel = 'info';
42 config.incremental = true; 47 config.incremental = true;
43 48
@@ -55,34 +60,37 @@ async function setupEsbuildWatcher(packageName, extraPaths, callback) {
55 callback(); 60 callback();
56 } 61 }
57 watcher.on('change', () => { 62 watcher.on('change', () => {
58 incrementalBuild.rebuild?.().then(() => { 63 incrementalBuild
59 if (callback) { 64 .rebuild?.()
60 console.log(`\u26a1 Reloading package ${packageName}`); 65 .then(() => {
61 callback(); 66 if (callback) {
62 } 67 console.log(`\u26a1 Reloading package ${packageName}`);
63 }).catch((err) => { 68 callback();
64 if (typeof err === 'object' && 'errors' in err) {
65 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- We just checked.
66 const { errors } = err;
67 if (Array.isArray(errors)) {
68 const errCount = errors.length;
69 console.error(
70 '\ud83d\udd25',
71 errCount,
72 errCount > 1 ? 'errors' : 'error',
73 'while rebuilding package',
74 packageName,
75 );
76 return;
77 } 69 }
78 } 70 })
79 console.error( 71 .catch((err) => {
80 '\ud83d\udd25', 72 if (typeof err === 'object' && 'errors' in err) {
81 'error while rebuilding package', 73 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- We just checked.
82 packageName, 74 const { errors } = err;
83 err, 75 if (Array.isArray(errors)) {
84 ); 76 const errCount = errors.length;
85 }); 77 console.error(
78 '\ud83d\udd25',
79 errCount,
80 errCount > 1 ? 'errors' : 'error',
81 'while rebuilding package',
82 packageName,
83 );
84 return;
85 }
86 }
87 console.error(
88 '\ud83d\udd25',
89 'error while rebuilding package',
90 packageName,
91 err,
92 );
93 });
86 }); 94 });
87} 95}
88 96
@@ -136,7 +144,11 @@ function setupServicePackageWatcher(packageName, sendEvent) {
136 */ 144 */
137function setupMainPackageWatcher(viteDevServer) { 145function setupMainPackageWatcher(viteDevServer) {
138 // Write a value to an environment variable to pass it to the main process. 146 // Write a value to an environment variable to pass it to the main process.
139 const { config: { server: { port, https, host } } } = viteDevServer; 147 const {
148 config: {
149 server: { port, https, host },
150 },
151 } = viteDevServer;
140 const protocol = `http${https ? 's' : ''}:`; 152 const protocol = `http${https ? 's' : ''}:`;
141 const hostOrDefault = typeof host === 'string' ? host : 'localhost'; 153 const hostOrDefault = typeof host === 'string' ? host : 'localhost';
142 const portOrDefault = port || 3000; 154 const portOrDefault = port || 3000;
@@ -148,10 +160,7 @@ function setupMainPackageWatcher(viteDevServer) {
148 160
149 return setupEsbuildWatcher( 161 return setupEsbuildWatcher(
150 'main', 162 'main',
151 [ 163 [serviceSharedModule, sharedModule],
152 serviceSharedModule,
153 sharedModule,
154 ],
155 () => { 164 () => {
156 if (spawnProcess !== null) { 165 if (spawnProcess !== null) {
157 spawnProcess.kill('SIGINT'); 166 spawnProcess.kill('SIGINT');
@@ -191,16 +200,20 @@ async function setupDevEnvironment() {
191 const sharedWatcher = setupEsbuildWatcher('shared'); 200 const sharedWatcher = setupEsbuildWatcher('shared');
192 const serviceSharedWatcher = setupEsbuildWatcher('service-shared'); 201 const serviceSharedWatcher = setupEsbuildWatcher('service-shared');
193 await Promise.all([ 202 await Promise.all([
194 sharedWatcher.then(() => Promise.all([ 203 sharedWatcher.then(() =>
195 setupPreloadPackageWatcher(sendEvent), 204 Promise.all([
196 setupDevServer('renderer').then((devServer) => { 205 setupPreloadPackageWatcher(sendEvent),
197 viteDevServer = devServer; 206 setupDevServer('renderer').then((devServer) => {
198 }), 207 viteDevServer = devServer;
199 ])), 208 }),
200 serviceSharedWatcher.then(() => Promise.all([ 209 ]),
201 setupServicePackageWatcher('service-inject', sendEvent), 210 ),
202 setupServicePackageWatcher('service-preload', sendEvent), 211 serviceSharedWatcher.then(() =>
203 ])), 212 Promise.all([
213 setupServicePackageWatcher('service-inject', sendEvent),
214 setupServicePackageWatcher('service-preload', sendEvent),
215 ]),
216 ),
204 ]); 217 ]);
205 218
206 if (viteDevServer === null) { 219 if (viteDevServer === null) {
diff --git a/tsconfig.json b/tsconfig.json
index 627bed2..4ed25b9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,6 +9,7 @@
9 "scripts/**/*.js", 9 "scripts/**/*.js",
10 ".electron-builder.config.cjs", 10 ".electron-builder.config.cjs",
11 ".eslintrc.cjs", 11 ".eslintrc.cjs",
12 "jest.config.js" 12 "jest.config.js",
13 "prettier.config.cjs"
13 ] 14 ]
14} 15}
diff --git a/yarn.lock b/yarn.lock
index 85c7823..4d051ac 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1569,7 +1569,7 @@ __metadata:
1569 languageName: node 1569 languageName: node
1570 linkType: hard 1570 linkType: hard
1571 1571
1572"@types/prettier@npm:^2.1.5": 1572"@types/prettier@npm:^2, @types/prettier@npm:^2.1.5":
1573 version: 2.4.2 1573 version: 2.4.2
1574 resolution: "@types/prettier@npm:2.4.2" 1574 resolution: "@types/prettier@npm:2.4.2"
1575 checksum: 76e230b2d11028af11fe12e09b2d5b10b03738e9abf819ae6ebb0f78cac13d39f860755ce05ac3855b608222518d956628f5d00322dc206cc6d1f2d8d1519f1e 1575 checksum: 76e230b2d11028af11fe12e09b2d5b10b03738e9abf819ae6ebb0f78cac13d39f860755ce05ac3855b608222518d956628f5d00322dc206cc6d1f2d8d1519f1e
@@ -3998,6 +3998,17 @@ __metadata:
3998 languageName: node 3998 languageName: node
3999 linkType: hard 3999 linkType: hard
4000 4000
4001"eslint-config-prettier@npm:^8.3.0":
4002 version: 8.3.0
4003 resolution: "eslint-config-prettier@npm:8.3.0"
4004 peerDependencies:
4005 eslint: ">=7.0.0"
4006 bin:
4007 eslint-config-prettier: bin/cli.js
4008 checksum: df4cea3032671995bb5ab07e016169072f7fa59f44a53251664d9ca60951b66cdc872683b5c6a3729c91497c11490ca44a79654b395dd6756beb0c3903a37196
4009 languageName: node
4010 linkType: hard
4011
4001"eslint-formatter-gitlab@npm:^3.0.0": 4012"eslint-formatter-gitlab@npm:^3.0.0":
4002 version: 3.0.0 4013 version: 3.0.0
4003 resolution: "eslint-formatter-gitlab@npm:3.0.0" 4014 resolution: "eslint-formatter-gitlab@npm:3.0.0"
@@ -4091,6 +4102,21 @@ __metadata:
4091 languageName: node 4102 languageName: node
4092 linkType: hard 4103 linkType: hard
4093 4104
4105"eslint-plugin-prettier@npm:^4.0.0":
4106 version: 4.0.0
4107 resolution: "eslint-plugin-prettier@npm:4.0.0"
4108 dependencies:
4109 prettier-linter-helpers: ^1.0.0
4110 peerDependencies:
4111 eslint: ">=7.28.0"
4112 prettier: ">=2.0.0"
4113 peerDependenciesMeta:
4114 eslint-config-prettier:
4115 optional: true
4116 checksum: 03d69177a3c21fa2229c7e427ce604429f0b20ab7f411e2e824912f572a207c7f5a41fd1f0a95b9b8afe121e291c1b1f1dc1d44c7aad4b0837487f9c19f5210d
4117 languageName: node
4118 linkType: hard
4119
4094"eslint-plugin-react-hooks@npm:^4.3.0": 4120"eslint-plugin-react-hooks@npm:^4.3.0":
4095 version: 4.3.0 4121 version: 4.3.0
4096 resolution: "eslint-plugin-react-hooks@npm:4.3.0" 4122 resolution: "eslint-plugin-react-hooks@npm:4.3.0"
@@ -4348,6 +4374,13 @@ __metadata:
4348 languageName: node 4374 languageName: node
4349 linkType: hard 4375 linkType: hard
4350 4376
4377"fast-diff@npm:^1.1.2":
4378 version: 1.2.0
4379 resolution: "fast-diff@npm:1.2.0"
4380 checksum: 1b5306eaa9e826564d9e5ffcd6ebd881eb5f770b3f977fcbf38f05c824e42172b53c79920e8429c54eb742ce15a0caf268b0fdd5b38f6de52234c4a8368131ae
4381 languageName: node
4382 linkType: hard
4383
4351"fast-glob@npm:^3.1.1": 4384"fast-glob@npm:^3.1.1":
4352 version: 3.2.7 4385 version: 3.2.7
4353 resolution: "fast-glob@npm:3.2.7" 4386 resolution: "fast-glob@npm:3.2.7"
@@ -7214,6 +7247,24 @@ __metadata:
7214 languageName: node 7247 languageName: node
7215 linkType: hard 7248 linkType: hard
7216 7249
7250"prettier-linter-helpers@npm:^1.0.0":
7251 version: 1.0.0
7252 resolution: "prettier-linter-helpers@npm:1.0.0"
7253 dependencies:
7254 fast-diff: ^1.1.2
7255 checksum: 00ce8011cf6430158d27f9c92cfea0a7699405633f7f1d4a45f07e21bf78e99895911cbcdc3853db3a824201a7c745bd49bfea8abd5fb9883e765a90f74f8392
7256 languageName: node
7257 linkType: hard
7258
7259"prettier@npm:^2.5.1":
7260 version: 2.5.1
7261 resolution: "prettier@npm:2.5.1"
7262 bin:
7263 prettier: bin-prettier.js
7264 checksum: 21b9408476ea1c544b0e45d51ceb94a84789ff92095abb710942d780c862d0daebdb29972d47f6b4d0f7ebbfb0ffbf56cc2cfa3e3e9d1cca54864af185b15b66
7265 languageName: node
7266 linkType: hard
7267
7217"pretty-format@npm:^27.0.0, pretty-format@npm:^27.4.6": 7268"pretty-format@npm:^27.0.0, pretty-format@npm:^27.4.6":
7218 version: 27.4.6 7269 version: 27.4.6
7219 resolution: "pretty-format@npm:27.4.6" 7270 resolution: "pretty-format@npm:27.4.6"
@@ -7924,6 +7975,7 @@ __metadata:
7924 dependencies: 7975 dependencies:
7925 "@electron/fuses": ^1.5.0 7976 "@electron/fuses": ^1.5.0
7926 "@types/jest": ^27.4.0 7977 "@types/jest": ^27.4.0
7978 "@types/prettier": ^2
7927 "@typescript-eslint/eslint-plugin": ^5.9.0 7979 "@typescript-eslint/eslint-plugin": ^5.9.0
7928 "@typescript-eslint/parser": ^5.9.0 7980 "@typescript-eslint/parser": ^5.9.0
7929 "@vitejs/plugin-react": ^1.1.4 7981 "@vitejs/plugin-react": ^1.1.4
@@ -7936,15 +7988,18 @@ __metadata:
7936 eslint-config-airbnb: ^19.0.4 7988 eslint-config-airbnb: ^19.0.4
7937 eslint-config-airbnb-base: ^15.0.0 7989 eslint-config-airbnb-base: ^15.0.0
7938 eslint-config-airbnb-typescript: ^16.1.0 7990 eslint-config-airbnb-typescript: ^16.1.0
7991 eslint-config-prettier: ^8.3.0
7939 eslint-formatter-gitlab: ^3.0.0 7992 eslint-formatter-gitlab: ^3.0.0
7940 eslint-import-resolver-typescript: ^2.5.0 7993 eslint-import-resolver-typescript: ^2.5.0
7941 eslint-plugin-import: ^2.25.4 7994 eslint-plugin-import: ^2.25.4
7942 eslint-plugin-jsx-a11y: ^6.5.1 7995 eslint-plugin-jsx-a11y: ^6.5.1
7996 eslint-plugin-prettier: ^4.0.0
7943 eslint-plugin-react: ^7.28.0 7997 eslint-plugin-react: ^7.28.0
7944 eslint-plugin-react-hooks: ^4.3.0 7998 eslint-plugin-react-hooks: ^4.3.0
7945 git-repo-info: ^2.1.1 7999 git-repo-info: ^2.1.1
7946 jest: ^27.4.7 8000 jest: ^27.4.7
7947 preload: ^0.1.0 8001 preload: ^0.1.0
8002 prettier: ^2.5.1
7948 rimraf: ^3.0.2 8003 rimraf: ^3.0.2
7949 typescript: ^4.5.4 8004 typescript: ^4.5.4
7950 vite: ^2.7.10 8005 vite: ^2.7.10