diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-01-27 16:49:11 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-02-08 21:43:17 +0100 |
commit | 852c3b0eaed48265354046d068f0cfa565827e7c (patch) | |
tree | 858a60359dda635da2647900564e4963f68d57df /packages | |
parent | chore: Annotate shared packages for purity (diff) | |
download | sophie-852c3b0eaed48265354046d068f0cfa565827e7c.tar.gz sophie-852c3b0eaed48265354046d068f0cfa565827e7c.tar.zst sophie-852c3b0eaed48265354046d068f0cfa565827e7c.zip |
refactor: Extract resource path management
Lets us access absolute paths and URLs without directly calling node
APIs.
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'packages')
-rw-r--r-- | packages/main/package.json | 1 | ||||
-rw-r--r-- | packages/main/src/index.ts | 70 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/impl/devTools.ts (renamed from packages/main/src/devTools.ts) | 0 | ||||
-rw-r--r-- | packages/main/src/infrastructure/resources/Resources.ts | 27 | ||||
-rw-r--r-- | packages/main/src/infrastructure/resources/impl/__tests__/getDistResources.spec.ts | 112 | ||||
-rw-r--r-- | packages/main/src/infrastructure/resources/impl/getDistResources.ts | 59 | ||||
-rw-r--r-- | packages/main/src/initReactions.ts | 1 | ||||
-rw-r--r-- | packages/main/types/importMeta.d.ts | 2 |
8 files changed, 226 insertions, 46 deletions
diff --git a/packages/main/package.json b/packages/main/package.json index 9b87835..ea10b84 100644 --- a/packages/main/package.json +++ b/packages/main/package.json | |||
@@ -38,6 +38,7 @@ | |||
38 | "esbuild": "^0.14.14", | 38 | "esbuild": "^0.14.14", |
39 | "git-repo-info": "^2.1.1", | 39 | "git-repo-info": "^2.1.1", |
40 | "jest": "^27.4.7", | 40 | "jest": "^27.4.7", |
41 | "jest-each": "^27.4.6", | ||
41 | "jest-mock": "^27.4.6", | 42 | "jest-mock": "^27.4.6", |
42 | "source-map-support": "^0.5.21" | 43 | "source-map-support": "^0.5.21" |
43 | } | 44 | } |
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index a886a16..128ae35 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts | |||
@@ -22,7 +22,6 @@ | |||
22 | import { readFileSync } from 'node:fs'; | 22 | import { readFileSync } from 'node:fs'; |
23 | import { readFile } from 'node:fs/promises'; | 23 | import { readFile } from 'node:fs/promises'; |
24 | import { arch } from 'node:os'; | 24 | import { arch } from 'node:os'; |
25 | import path from 'node:path'; | ||
26 | import { URL } from 'node:url'; | 25 | import { URL } from 'node:url'; |
27 | 26 | ||
28 | import { | 27 | import { |
@@ -46,7 +45,8 @@ import { | |||
46 | enableStacktraceSourceMaps, | 45 | enableStacktraceSourceMaps, |
47 | installDevToolsExtensions, | 46 | installDevToolsExtensions, |
48 | openDevToolsWhenReady, | 47 | openDevToolsWhenReady, |
49 | } from './devTools'; | 48 | } from './infrastructure/electron/impl/devTools'; |
49 | import getDistResources from './infrastructure/resources/impl/getDistResources'; | ||
50 | import initReactions from './initReactions'; | 50 | import initReactions from './initReactions'; |
51 | import { createMainStore } from './stores/MainStore'; | 51 | import { createMainStore } from './stores/MainStore'; |
52 | import { getLogger } from './utils/log'; | 52 | import { getLogger } from './utils/log'; |
@@ -107,23 +107,12 @@ app.setAboutPanelOptions({ | |||
107 | version: '', | 107 | version: '', |
108 | }); | 108 | }); |
109 | 109 | ||
110 | // eslint-disable-next-line unicorn/prefer-module -- Electron apps run in a commonjs environment. | 110 | const resources = getDistResources(isDevelopment); |
111 | const thisDir = __dirname; | ||
112 | 111 | ||
113 | function getResourcePath(relativePath: string): string { | 112 | const serviceInjectPath = resources.getPath('service-inject', 'index.js'); |
114 | return path.join(thisDir, relativePath); | ||
115 | } | ||
116 | |||
117 | const baseUrl = `file://${thisDir}`; | ||
118 | function getResourceUrl(relativePath: string): string { | ||
119 | return new URL(relativePath, baseUrl).toString(); | ||
120 | } | ||
121 | |||
122 | const serviceInjectRelativePath = '../../service-inject/dist/index.js'; | ||
123 | const serviceInjectPath = getResourcePath(serviceInjectRelativePath); | ||
124 | const serviceInject: WebSource = { | 113 | const serviceInject: WebSource = { |
125 | code: readFileSync(serviceInjectPath, 'utf8'), | 114 | code: readFileSync(serviceInjectPath, 'utf8'), |
126 | url: getResourceUrl(serviceInjectRelativePath), | 115 | url: resources.getFileURL('service-inject', 'index.js'), |
127 | }; | 116 | }; |
128 | 117 | ||
129 | let mainWindow: BrowserWindow | undefined; | 118 | let mainWindow: BrowserWindow | undefined; |
@@ -139,36 +128,30 @@ initReactions(store) | |||
139 | log.log('Failed to initialize application', error); | 128 | log.log('Failed to initialize application', error); |
140 | }); | 129 | }); |
141 | 130 | ||
142 | const rendererBaseUrl = getResourceUrl('../renderer/'); | 131 | const rendererBaseURL = resources.getRendererURL('/'); |
143 | function shouldCancelMainWindowRequest(url: string, method: string): boolean { | 132 | function shouldCancelMainWindowRequest(url: string, method: string): boolean { |
144 | if (method !== 'GET') { | 133 | if (method !== 'GET') { |
145 | return true; | 134 | return true; |
146 | } | 135 | } |
147 | let normalizedUrl: string; | 136 | let normalizedURL: string; |
148 | try { | 137 | try { |
149 | normalizedUrl = new URL(url).toString(); | 138 | normalizedURL = new URL(url).toString(); |
150 | } catch { | 139 | } catch { |
151 | return true; | 140 | return true; |
152 | } | 141 | } |
153 | if (isDevelopment) { | 142 | if ( |
154 | if ( | 143 | isDevelopment && |
155 | DEVMODE_ALLOWED_URL_PREFIXES.some((prefix) => | 144 | DEVMODE_ALLOWED_URL_PREFIXES.some((prefix) => |
156 | normalizedUrl.startsWith(prefix), | 145 | normalizedURL.startsWith(prefix), |
157 | ) | 146 | ) |
158 | ) { | 147 | ) { |
159 | return false; | 148 | return false; |
160 | } | ||
161 | if (import.meta.env.VITE_DEV_SERVER_URL !== undefined) { | ||
162 | const isHttp = normalizedUrl.startsWith( | ||
163 | import.meta.env.VITE_DEV_SERVER_URL, | ||
164 | ); | ||
165 | const isWs = normalizedUrl.startsWith( | ||
166 | import.meta.env.VITE_DEV_SERVER_URL.replace(/^http:/, 'ws:'), | ||
167 | ); | ||
168 | return !isHttp && !isWs; | ||
169 | } | ||
170 | } | 149 | } |
171 | return !normalizedUrl.startsWith(getResourceUrl(rendererBaseUrl)); | 150 | const isHttp = normalizedURL.startsWith(rendererBaseURL); |
151 | const isWs = normalizedURL.startsWith( | ||
152 | rendererBaseURL.replace(/^http:/, 'ws:'), | ||
153 | ); | ||
154 | return !isHttp && !isWs; | ||
172 | } | 155 | } |
173 | 156 | ||
174 | async function createWindow(): Promise<unknown> { | 157 | async function createWindow(): Promise<unknown> { |
@@ -178,7 +161,7 @@ async function createWindow(): Promise<unknown> { | |||
178 | webPreferences: { | 161 | webPreferences: { |
179 | sandbox: true, | 162 | sandbox: true, |
180 | devTools: isDevelopment, | 163 | devTools: isDevelopment, |
181 | preload: getResourcePath('../../preload/dist/index.cjs'), | 164 | preload: resources.getPath('preload', 'index.cjs'), |
182 | }, | 165 | }, |
183 | }); | 166 | }); |
184 | 167 | ||
@@ -200,13 +183,10 @@ async function createWindow(): Promise<unknown> { | |||
200 | }, | 183 | }, |
201 | ); | 184 | ); |
202 | 185 | ||
203 | const pageUrl = | 186 | const pageURL = resources.getRendererURL('index.html'); |
204 | isDevelopment && import.meta.env.VITE_DEV_SERVER_URL !== undefined | ||
205 | ? import.meta.env.VITE_DEV_SERVER_URL | ||
206 | : getResourceUrl('../renderer/dist/index.html'); | ||
207 | 187 | ||
208 | webContents.on('will-navigate', (event, url) => { | 188 | webContents.on('will-navigate', (event, url) => { |
209 | if (url !== pageUrl) { | 189 | if (url !== pageURL) { |
210 | event.preventDefault(); | 190 | event.preventDefault(); |
211 | } | 191 | } |
212 | }); | 192 | }); |
@@ -225,7 +205,7 @@ async function createWindow(): Promise<unknown> { | |||
225 | webPreferences: { | 205 | webPreferences: { |
226 | sandbox: true, | 206 | sandbox: true, |
227 | nodeIntegrationInSubFrames: true, | 207 | nodeIntegrationInSubFrames: true, |
228 | preload: getResourcePath('../../service-preload/dist/index.cjs'), | 208 | preload: resources.getPath('service-preload', 'index.cjs'), |
229 | partition: 'persist:service', | 209 | partition: 'persist:service', |
230 | }, | 210 | }, |
231 | }); | 211 | }); |
@@ -370,7 +350,7 @@ async function createWindow(): Promise<unknown> { | |||
370 | log.error('Failed to load browser', error); | 350 | log.error('Failed to load browser', error); |
371 | }); | 351 | }); |
372 | 352 | ||
373 | return mainWindow.loadURL(pageUrl); | 353 | return mainWindow.loadURL(pageURL); |
374 | } | 354 | } |
375 | 355 | ||
376 | app.on('second-instance', () => { | 356 | app.on('second-instance', () => { |
diff --git a/packages/main/src/devTools.ts b/packages/main/src/infrastructure/electron/impl/devTools.ts index 10f4545..10f4545 100644 --- a/packages/main/src/devTools.ts +++ b/packages/main/src/infrastructure/electron/impl/devTools.ts | |||
diff --git a/packages/main/src/infrastructure/resources/Resources.ts b/packages/main/src/infrastructure/resources/Resources.ts new file mode 100644 index 0000000..269c838 --- /dev/null +++ b/packages/main/src/infrastructure/resources/Resources.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | export default interface Resources { | ||
22 | getPath(packageName: string, relativePathInPackage: string): string; | ||
23 | |||
24 | getFileURL(packageName: string, relativePathInPackage: string): string; | ||
25 | |||
26 | getRendererURL(relativePathInRendererPackage: string): string; | ||
27 | } | ||
diff --git a/packages/main/src/infrastructure/resources/impl/__tests__/getDistResources.spec.ts b/packages/main/src/infrastructure/resources/impl/__tests__/getDistResources.spec.ts new file mode 100644 index 0000000..d045e54 --- /dev/null +++ b/packages/main/src/infrastructure/resources/impl/__tests__/getDistResources.spec.ts | |||
@@ -0,0 +1,112 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import os from 'node:os'; | ||
22 | |||
23 | import eachModule from 'jest-each'; | ||
24 | |||
25 | import Resources from '../../Resources'; | ||
26 | import getDistResources from '../getDistResources'; | ||
27 | |||
28 | // Workaround for jest ESM loader incorrectly wrapping the import in another layer of `default`. | ||
29 | const each = | ||
30 | (eachModule as Partial<typeof import('jest-each')>).default ?? eachModule; | ||
31 | |||
32 | const defaultDevServerURL = 'http://localhost:3000/'; | ||
33 | |||
34 | const [ | ||
35 | thisDir, | ||
36 | preloadIndexPath, | ||
37 | preloadIndexFileURL, | ||
38 | rendererIndexFileURL, | ||
39 | rendererRootFileURL, | ||
40 | ] = | ||
41 | os.platform() === 'win32' | ||
42 | ? [ | ||
43 | 'C:\\Program Files\\sophie\\resources\\app.asar\\main\\dist', | ||
44 | 'C:\\Program Files\\sophie\\resources\\app.asar\\preload\\dist\\index.cjs', | ||
45 | 'file:///C:/Program Files/sophie/resources/app.asar/preload/dist/index.cjs', | ||
46 | 'file:///C:/Program Files/sophie/resources/app.asar/renderer/dist/index.html', | ||
47 | 'file:///C:/Program Files/sophie/resources/app.asar/renderer/dist/', | ||
48 | ] | ||
49 | : [ | ||
50 | '/opt/sophie/resources/app.asar/main/dist', | ||
51 | '/opt/sophie/resources/app.asar/preload/dist/index.cjs', | ||
52 | 'file:///opt/sophie/resources/app.asar/preload/dist/index.cjs', | ||
53 | 'file:///opt/sophie/resources/app.asar/renderer/dist/index.html', | ||
54 | 'file:///opt/sophie/resources/app.asar/renderer/dist/', | ||
55 | ]; | ||
56 | |||
57 | const fileURLs: [string, string] = [rendererIndexFileURL, rendererRootFileURL]; | ||
58 | |||
59 | each([ | ||
60 | ['not in dev mode', false, undefined, ...fileURLs], | ||
61 | [ | ||
62 | 'not in dev mode with VITE_DEV_SERVER_URL set', | ||
63 | false, | ||
64 | defaultDevServerURL, | ||
65 | ...fileURLs, | ||
66 | ], | ||
67 | ['in dev mode with no VITE_DEV_SERVER_URL', true, undefined, ...fileURLs], | ||
68 | [ | ||
69 | 'in dev mode with VITE_DEV_SERVER_URL set', | ||
70 | true, | ||
71 | defaultDevServerURL, | ||
72 | `${defaultDevServerURL}index.html`, | ||
73 | defaultDevServerURL, | ||
74 | ], | ||
75 | ]).describe( | ||
76 | 'when %s', | ||
77 | ( | ||
78 | _description: string, | ||
79 | devMode: boolean, | ||
80 | devServerURL: string, | ||
81 | rendererIndexURL: string, | ||
82 | rendererRootURL: string, | ||
83 | ) => { | ||
84 | let resources: Resources; | ||
85 | |||
86 | beforeEach(() => { | ||
87 | resources = getDistResources(devMode, thisDir, devServerURL); | ||
88 | }); | ||
89 | |||
90 | it('getPath should return the path to the requested resource', () => { | ||
91 | const path = resources.getPath('preload', 'index.cjs'); | ||
92 | expect(path).toBe(preloadIndexPath); | ||
93 | }); | ||
94 | |||
95 | it('getFileURL should return the file URL to the requested resource', () => { | ||
96 | const url = resources.getFileURL('preload', 'index.cjs'); | ||
97 | expect(url).toBe(preloadIndexFileURL); | ||
98 | }); | ||
99 | |||
100 | describe('getRendererURL', () => { | ||
101 | it('should return the URL to the requested resource', () => { | ||
102 | const url = resources.getRendererURL('index.html'); | ||
103 | expect(url).toBe(rendererIndexURL); | ||
104 | }); | ||
105 | |||
106 | it('should return the root URL', () => { | ||
107 | const url = resources.getRendererURL('/'); | ||
108 | expect(url).toBe(rendererRootURL); | ||
109 | }); | ||
110 | }); | ||
111 | }, | ||
112 | ); | ||
diff --git a/packages/main/src/infrastructure/resources/impl/getDistResources.ts b/packages/main/src/infrastructure/resources/impl/getDistResources.ts new file mode 100644 index 0000000..f3c3f7b --- /dev/null +++ b/packages/main/src/infrastructure/resources/impl/getDistResources.ts | |||
@@ -0,0 +1,59 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import path from 'node:path'; | ||
22 | import { pathToFileURL, URL } from 'node:url'; | ||
23 | |||
24 | import Resources from '../Resources'; | ||
25 | |||
26 | export default function getDistResources( | ||
27 | devMode: boolean, | ||
28 | /* | ||
29 | eslint-disable-next-line unicorn/prefer-module -- | ||
30 | Electron apps run in a commonjs environment, so there is no `import.meta.url`. | ||
31 | */ | ||
32 | thisDir = __dirname, | ||
33 | devServerURL = import.meta.env?.VITE_DEV_SERVER_URL, | ||
34 | ): Resources { | ||
35 | const packagesRoot = path.join(thisDir, '..', '..'); | ||
36 | |||
37 | function getPath(packageName: string, relativePathInPackage: string): string { | ||
38 | return path.join(packagesRoot, packageName, 'dist', relativePathInPackage); | ||
39 | } | ||
40 | |||
41 | function getFileURL( | ||
42 | packageName: string, | ||
43 | relativePathInPackage: string, | ||
44 | ): string { | ||
45 | const absolutePath = getPath(packageName, relativePathInPackage); | ||
46 | return pathToFileURL(absolutePath).toString(); | ||
47 | } | ||
48 | |||
49 | return { | ||
50 | getPath, | ||
51 | getFileURL, | ||
52 | getRendererURL: | ||
53 | devMode && devServerURL !== undefined | ||
54 | ? (relativePathInRendererPackage) => | ||
55 | new URL(relativePathInRendererPackage, devServerURL).toString() | ||
56 | : (relativePathInRendererPackage) => | ||
57 | getFileURL('renderer', relativePathInRendererPackage), | ||
58 | }; | ||
59 | } | ||
diff --git a/packages/main/src/initReactions.ts b/packages/main/src/initReactions.ts index 87ad425..a87b323 100644 --- a/packages/main/src/initReactions.ts +++ b/packages/main/src/initReactions.ts | |||
@@ -34,6 +34,7 @@ export default async function initReactions( | |||
34 | store.shared, | 34 | store.shared, |
35 | configRepository, | 35 | configRepository, |
36 | ); | 36 | ); |
37 | await app.whenReady(); | ||
37 | const disposeNativeThemeController = synchronizeNativeTheme(store.shared); | 38 | const disposeNativeThemeController = synchronizeNativeTheme(store.shared); |
38 | 39 | ||
39 | return () => { | 40 | return () => { |
diff --git a/packages/main/types/importMeta.d.ts b/packages/main/types/importMeta.d.ts index efcf48a..7426961 100644 --- a/packages/main/types/importMeta.d.ts +++ b/packages/main/types/importMeta.d.ts | |||
@@ -3,7 +3,7 @@ interface ImportMeta { | |||
3 | DEV: boolean; | 3 | DEV: boolean; |
4 | MODE: string; | 4 | MODE: string; |
5 | PROD: boolean; | 5 | PROD: boolean; |
6 | VITE_DEV_SERVER_URL: string; | 6 | VITE_DEV_SERVER_URL?: string | undefined; |
7 | GIT_SHA: string; | 7 | GIT_SHA: string; |
8 | GIT_BRANCH: string; | 8 | GIT_BRANCH: string; |
9 | BUILD_DATE: number; | 9 | BUILD_DATE: number; |