aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-01-27 16:49:11 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-02-08 21:43:17 +0100
commit852c3b0eaed48265354046d068f0cfa565827e7c (patch)
tree858a60359dda635da2647900564e4963f68d57df
parentchore: Annotate shared packages for purity (diff)
downloadsophie-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>
-rw-r--r--packages/main/package.json1
-rw-r--r--packages/main/src/index.ts70
-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.ts27
-rw-r--r--packages/main/src/infrastructure/resources/impl/__tests__/getDistResources.spec.ts112
-rw-r--r--packages/main/src/infrastructure/resources/impl/getDistResources.ts59
-rw-r--r--packages/main/src/initReactions.ts1
-rw-r--r--packages/main/types/importMeta.d.ts2
-rw-r--r--yarn.lock1
9 files changed, 227 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 @@
22import { readFileSync } from 'node:fs'; 22import { readFileSync } from 'node:fs';
23import { readFile } from 'node:fs/promises'; 23import { readFile } from 'node:fs/promises';
24import { arch } from 'node:os'; 24import { arch } from 'node:os';
25import path from 'node:path';
26import { URL } from 'node:url'; 25import { URL } from 'node:url';
27 26
28import { 27import {
@@ -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';
49import getDistResources from './infrastructure/resources/impl/getDistResources';
50import initReactions from './initReactions'; 50import initReactions from './initReactions';
51import { createMainStore } from './stores/MainStore'; 51import { createMainStore } from './stores/MainStore';
52import { getLogger } from './utils/log'; 52import { 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. 110const resources = getDistResources(isDevelopment);
111const thisDir = __dirname;
112 111
113function getResourcePath(relativePath: string): string { 112const serviceInjectPath = resources.getPath('service-inject', 'index.js');
114 return path.join(thisDir, relativePath);
115}
116
117const baseUrl = `file://${thisDir}`;
118function getResourceUrl(relativePath: string): string {
119 return new URL(relativePath, baseUrl).toString();
120}
121
122const serviceInjectRelativePath = '../../service-inject/dist/index.js';
123const serviceInjectPath = getResourcePath(serviceInjectRelativePath);
124const serviceInject: WebSource = { 113const 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
129let mainWindow: BrowserWindow | undefined; 118let 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
142const rendererBaseUrl = getResourceUrl('../renderer/'); 131const rendererBaseURL = resources.getRendererURL('/');
143function shouldCancelMainWindowRequest(url: string, method: string): boolean { 132function 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
174async function createWindow(): Promise<unknown> { 157async 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
376app.on('second-instance', () => { 356app.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
21export 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
21import os from 'node:os';
22
23import eachModule from 'jest-each';
24
25import Resources from '../../Resources';
26import getDistResources from '../getDistResources';
27
28// Workaround for jest ESM loader incorrectly wrapping the import in another layer of `default`.
29const each =
30 (eachModule as Partial<typeof import('jest-each')>).default ?? eachModule;
31
32const defaultDevServerURL = 'http://localhost:3000/';
33
34const [
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
57const fileURLs: [string, string] = [rendererIndexFileURL, rendererRootFileURL];
58
59each([
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
21import path from 'node:path';
22import { pathToFileURL, URL } from 'node:url';
23
24import Resources from '../Resources';
25
26export 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;
diff --git a/yarn.lock b/yarn.lock
index 8a0bf4e..a22d873 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1252,6 +1252,7 @@ __metadata:
1252 fs-extra: ^10.0.0 1252 fs-extra: ^10.0.0
1253 git-repo-info: ^2.1.1 1253 git-repo-info: ^2.1.1
1254 jest: ^27.4.7 1254 jest: ^27.4.7
1255 jest-each: ^27.4.6
1255 jest-mock: ^27.4.6 1256 jest-mock: ^27.4.6
1256 json5: ^2.2.0 1257 json5: ^2.2.0
1257 lodash-es: ^4.17.21 1258 lodash-es: ^4.17.21