From 852c3b0eaed48265354046d068f0cfa565827e7c Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 27 Jan 2022 16:49:11 +0100 Subject: refactor: Extract resource path management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets us access absolute paths and URLs without directly calling node APIs. Signed-off-by: Kristóf Marussy --- packages/main/package.json | 1 + packages/main/src/devTools.ts | 94 ----------------- packages/main/src/index.ts | 70 +++++-------- .../src/infrastructure/electron/impl/devTools.ts | 94 +++++++++++++++++ .../main/src/infrastructure/resources/Resources.ts | 27 +++++ .../impl/__tests__/getDistResources.spec.ts | 112 +++++++++++++++++++++ .../resources/impl/getDistResources.ts | 59 +++++++++++ packages/main/src/initReactions.ts | 1 + packages/main/types/importMeta.d.ts | 2 +- yarn.lock | 1 + 10 files changed, 321 insertions(+), 140 deletions(-) delete mode 100644 packages/main/src/devTools.ts create mode 100644 packages/main/src/infrastructure/electron/impl/devTools.ts create mode 100644 packages/main/src/infrastructure/resources/Resources.ts create mode 100644 packages/main/src/infrastructure/resources/impl/__tests__/getDistResources.spec.ts create mode 100644 packages/main/src/infrastructure/resources/impl/getDistResources.ts 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 @@ "esbuild": "^0.14.14", "git-repo-info": "^2.1.1", "jest": "^27.4.7", + "jest-each": "^27.4.6", "jest-mock": "^27.4.6", "source-map-support": "^0.5.21" } diff --git a/packages/main/src/devTools.ts b/packages/main/src/devTools.ts deleted file mode 100644 index 10f4545..0000000 --- a/packages/main/src/devTools.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { BrowserWindow } from 'electron'; - -/** - * URL prefixes Sophie is allowed load in dev mode. - * - * In dev mode, in addition to the application itself, - * Sophie must be able do download and load the devtools and related extensions, - * so we have to make exceptions in the UI process request filter. - */ -export const DEVMODE_ALLOWED_URL_PREFIXES = [ - 'chrome-extension:', - 'devtools:', - 'https://clients2.google.com/service/update2/crx', - 'https://clients2.googleusercontent.com/crx', -]; - -/** - * Enables using source maps for node stack traces. - */ -export function enableStacktraceSourceMaps(): void { - const sourceMapSupport = - /* eslint-disable-next-line - import/no-extraneous-dependencies, - global-require, - @typescript-eslint/no-var-requires, - unicorn/prefer-module -- - Hack to lazily require a CJS module from an ES module transpiled into a CJS module. - */ - require('source-map-support') as typeof import('source-map-support'); - sourceMapSupport.install(); -} - -/** - * Installs the react and redux developer tools extensions. - * - * We use the redux devtools and connect the mobx store to it with `mst-middlewares`, - * because the mobx-state-tree devtools are currently unmaintained. - */ -export async function installDevToolsExtensions(): Promise { - const { - default: installExtension, - REACT_DEVELOPER_TOOLS, - REDUX_DEVTOOLS, - /* eslint-disable-next-line - import/no-extraneous-dependencies, - global-require, - @typescript-eslint/no-var-requires, - unicorn/prefer-module -- - Hack to lazily require a CJS module from an ES module transpiled into a CJS module. - */ - } = require('electron-devtools-installer') as typeof import('electron-devtools-installer'); - await installExtension([REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS], { - forceDownload: false, - loadExtensionOptions: { - allowFileAccess: true, - }, - }); -} - -/** - * Opens the developer tools while applying a workaround to enable the redux devtools. - * - * @param browserWindow The browser window to open the devtools in. - * @see https://github.com/MarshallOfSound/electron-devtools-installer/issues/195#issuecomment-998872878 - */ -export function openDevToolsWhenReady(browserWindow: BrowserWindow): void { - const { webContents } = browserWindow; - webContents.once('dom-ready', () => { - webContents.once('devtools-opened', () => { - browserWindow?.focus(); - }); - webContents.openDevTools(); - }); -} 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 @@ import { readFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { arch } from 'node:os'; -import path from 'node:path'; import { URL } from 'node:url'; import { @@ -46,7 +45,8 @@ import { enableStacktraceSourceMaps, installDevToolsExtensions, openDevToolsWhenReady, -} from './devTools'; +} from './infrastructure/electron/impl/devTools'; +import getDistResources from './infrastructure/resources/impl/getDistResources'; import initReactions from './initReactions'; import { createMainStore } from './stores/MainStore'; import { getLogger } from './utils/log'; @@ -107,23 +107,12 @@ app.setAboutPanelOptions({ version: '', }); -// eslint-disable-next-line unicorn/prefer-module -- Electron apps run in a commonjs environment. -const thisDir = __dirname; +const resources = getDistResources(isDevelopment); -function getResourcePath(relativePath: string): string { - return path.join(thisDir, relativePath); -} - -const baseUrl = `file://${thisDir}`; -function getResourceUrl(relativePath: string): string { - return new URL(relativePath, baseUrl).toString(); -} - -const serviceInjectRelativePath = '../../service-inject/dist/index.js'; -const serviceInjectPath = getResourcePath(serviceInjectRelativePath); +const serviceInjectPath = resources.getPath('service-inject', 'index.js'); const serviceInject: WebSource = { code: readFileSync(serviceInjectPath, 'utf8'), - url: getResourceUrl(serviceInjectRelativePath), + url: resources.getFileURL('service-inject', 'index.js'), }; let mainWindow: BrowserWindow | undefined; @@ -139,36 +128,30 @@ initReactions(store) log.log('Failed to initialize application', error); }); -const rendererBaseUrl = getResourceUrl('../renderer/'); +const rendererBaseURL = resources.getRendererURL('/'); function shouldCancelMainWindowRequest(url: string, method: string): boolean { if (method !== 'GET') { return true; } - let normalizedUrl: string; + let normalizedURL: string; try { - normalizedUrl = new URL(url).toString(); + normalizedURL = new URL(url).toString(); } catch { return true; } - if (isDevelopment) { - if ( - DEVMODE_ALLOWED_URL_PREFIXES.some((prefix) => - normalizedUrl.startsWith(prefix), - ) - ) { - return false; - } - if (import.meta.env.VITE_DEV_SERVER_URL !== undefined) { - const isHttp = normalizedUrl.startsWith( - import.meta.env.VITE_DEV_SERVER_URL, - ); - const isWs = normalizedUrl.startsWith( - import.meta.env.VITE_DEV_SERVER_URL.replace(/^http:/, 'ws:'), - ); - return !isHttp && !isWs; - } + if ( + isDevelopment && + DEVMODE_ALLOWED_URL_PREFIXES.some((prefix) => + normalizedURL.startsWith(prefix), + ) + ) { + return false; } - return !normalizedUrl.startsWith(getResourceUrl(rendererBaseUrl)); + const isHttp = normalizedURL.startsWith(rendererBaseURL); + const isWs = normalizedURL.startsWith( + rendererBaseURL.replace(/^http:/, 'ws:'), + ); + return !isHttp && !isWs; } async function createWindow(): Promise { @@ -178,7 +161,7 @@ async function createWindow(): Promise { webPreferences: { sandbox: true, devTools: isDevelopment, - preload: getResourcePath('../../preload/dist/index.cjs'), + preload: resources.getPath('preload', 'index.cjs'), }, }); @@ -200,13 +183,10 @@ async function createWindow(): Promise { }, ); - const pageUrl = - isDevelopment && import.meta.env.VITE_DEV_SERVER_URL !== undefined - ? import.meta.env.VITE_DEV_SERVER_URL - : getResourceUrl('../renderer/dist/index.html'); + const pageURL = resources.getRendererURL('index.html'); webContents.on('will-navigate', (event, url) => { - if (url !== pageUrl) { + if (url !== pageURL) { event.preventDefault(); } }); @@ -225,7 +205,7 @@ async function createWindow(): Promise { webPreferences: { sandbox: true, nodeIntegrationInSubFrames: true, - preload: getResourcePath('../../service-preload/dist/index.cjs'), + preload: resources.getPath('service-preload', 'index.cjs'), partition: 'persist:service', }, }); @@ -370,7 +350,7 @@ async function createWindow(): Promise { log.error('Failed to load browser', error); }); - return mainWindow.loadURL(pageUrl); + return mainWindow.loadURL(pageURL); } app.on('second-instance', () => { diff --git a/packages/main/src/infrastructure/electron/impl/devTools.ts b/packages/main/src/infrastructure/electron/impl/devTools.ts new file mode 100644 index 0000000..10f4545 --- /dev/null +++ b/packages/main/src/infrastructure/electron/impl/devTools.ts @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { BrowserWindow } from 'electron'; + +/** + * URL prefixes Sophie is allowed load in dev mode. + * + * In dev mode, in addition to the application itself, + * Sophie must be able do download and load the devtools and related extensions, + * so we have to make exceptions in the UI process request filter. + */ +export const DEVMODE_ALLOWED_URL_PREFIXES = [ + 'chrome-extension:', + 'devtools:', + 'https://clients2.google.com/service/update2/crx', + 'https://clients2.googleusercontent.com/crx', +]; + +/** + * Enables using source maps for node stack traces. + */ +export function enableStacktraceSourceMaps(): void { + const sourceMapSupport = + /* eslint-disable-next-line + import/no-extraneous-dependencies, + global-require, + @typescript-eslint/no-var-requires, + unicorn/prefer-module -- + Hack to lazily require a CJS module from an ES module transpiled into a CJS module. + */ + require('source-map-support') as typeof import('source-map-support'); + sourceMapSupport.install(); +} + +/** + * Installs the react and redux developer tools extensions. + * + * We use the redux devtools and connect the mobx store to it with `mst-middlewares`, + * because the mobx-state-tree devtools are currently unmaintained. + */ +export async function installDevToolsExtensions(): Promise { + const { + default: installExtension, + REACT_DEVELOPER_TOOLS, + REDUX_DEVTOOLS, + /* eslint-disable-next-line + import/no-extraneous-dependencies, + global-require, + @typescript-eslint/no-var-requires, + unicorn/prefer-module -- + Hack to lazily require a CJS module from an ES module transpiled into a CJS module. + */ + } = require('electron-devtools-installer') as typeof import('electron-devtools-installer'); + await installExtension([REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS], { + forceDownload: false, + loadExtensionOptions: { + allowFileAccess: true, + }, + }); +} + +/** + * Opens the developer tools while applying a workaround to enable the redux devtools. + * + * @param browserWindow The browser window to open the devtools in. + * @see https://github.com/MarshallOfSound/electron-devtools-installer/issues/195#issuecomment-998872878 + */ +export function openDevToolsWhenReady(browserWindow: BrowserWindow): void { + const { webContents } = browserWindow; + webContents.once('dom-ready', () => { + webContents.once('devtools-opened', () => { + browserWindow?.focus(); + }); + webContents.openDevTools(); + }); +} 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 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export default interface Resources { + getPath(packageName: string, relativePathInPackage: string): string; + + getFileURL(packageName: string, relativePathInPackage: string): string; + + getRendererURL(relativePathInRendererPackage: string): string; +} 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 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import os from 'node:os'; + +import eachModule from 'jest-each'; + +import Resources from '../../Resources'; +import getDistResources from '../getDistResources'; + +// Workaround for jest ESM loader incorrectly wrapping the import in another layer of `default`. +const each = + (eachModule as Partial).default ?? eachModule; + +const defaultDevServerURL = 'http://localhost:3000/'; + +const [ + thisDir, + preloadIndexPath, + preloadIndexFileURL, + rendererIndexFileURL, + rendererRootFileURL, +] = + os.platform() === 'win32' + ? [ + 'C:\\Program Files\\sophie\\resources\\app.asar\\main\\dist', + 'C:\\Program Files\\sophie\\resources\\app.asar\\preload\\dist\\index.cjs', + 'file:///C:/Program Files/sophie/resources/app.asar/preload/dist/index.cjs', + 'file:///C:/Program Files/sophie/resources/app.asar/renderer/dist/index.html', + 'file:///C:/Program Files/sophie/resources/app.asar/renderer/dist/', + ] + : [ + '/opt/sophie/resources/app.asar/main/dist', + '/opt/sophie/resources/app.asar/preload/dist/index.cjs', + 'file:///opt/sophie/resources/app.asar/preload/dist/index.cjs', + 'file:///opt/sophie/resources/app.asar/renderer/dist/index.html', + 'file:///opt/sophie/resources/app.asar/renderer/dist/', + ]; + +const fileURLs: [string, string] = [rendererIndexFileURL, rendererRootFileURL]; + +each([ + ['not in dev mode', false, undefined, ...fileURLs], + [ + 'not in dev mode with VITE_DEV_SERVER_URL set', + false, + defaultDevServerURL, + ...fileURLs, + ], + ['in dev mode with no VITE_DEV_SERVER_URL', true, undefined, ...fileURLs], + [ + 'in dev mode with VITE_DEV_SERVER_URL set', + true, + defaultDevServerURL, + `${defaultDevServerURL}index.html`, + defaultDevServerURL, + ], +]).describe( + 'when %s', + ( + _description: string, + devMode: boolean, + devServerURL: string, + rendererIndexURL: string, + rendererRootURL: string, + ) => { + let resources: Resources; + + beforeEach(() => { + resources = getDistResources(devMode, thisDir, devServerURL); + }); + + it('getPath should return the path to the requested resource', () => { + const path = resources.getPath('preload', 'index.cjs'); + expect(path).toBe(preloadIndexPath); + }); + + it('getFileURL should return the file URL to the requested resource', () => { + const url = resources.getFileURL('preload', 'index.cjs'); + expect(url).toBe(preloadIndexFileURL); + }); + + describe('getRendererURL', () => { + it('should return the URL to the requested resource', () => { + const url = resources.getRendererURL('index.html'); + expect(url).toBe(rendererIndexURL); + }); + + it('should return the root URL', () => { + const url = resources.getRendererURL('/'); + expect(url).toBe(rendererRootURL); + }); + }); + }, +); 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 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import path from 'node:path'; +import { pathToFileURL, URL } from 'node:url'; + +import Resources from '../Resources'; + +export default function getDistResources( + devMode: boolean, + /* + eslint-disable-next-line unicorn/prefer-module -- + Electron apps run in a commonjs environment, so there is no `import.meta.url`. + */ + thisDir = __dirname, + devServerURL = import.meta.env?.VITE_DEV_SERVER_URL, +): Resources { + const packagesRoot = path.join(thisDir, '..', '..'); + + function getPath(packageName: string, relativePathInPackage: string): string { + return path.join(packagesRoot, packageName, 'dist', relativePathInPackage); + } + + function getFileURL( + packageName: string, + relativePathInPackage: string, + ): string { + const absolutePath = getPath(packageName, relativePathInPackage); + return pathToFileURL(absolutePath).toString(); + } + + return { + getPath, + getFileURL, + getRendererURL: + devMode && devServerURL !== undefined + ? (relativePathInRendererPackage) => + new URL(relativePathInRendererPackage, devServerURL).toString() + : (relativePathInRendererPackage) => + getFileURL('renderer', relativePathInRendererPackage), + }; +} 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( store.shared, configRepository, ); + await app.whenReady(); const disposeNativeThemeController = synchronizeNativeTheme(store.shared); 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 { DEV: boolean; MODE: string; PROD: boolean; - VITE_DEV_SERVER_URL: string; + VITE_DEV_SERVER_URL?: string | undefined; GIT_SHA: string; GIT_BRANCH: string; 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: fs-extra: ^10.0.0 git-repo-info: ^2.1.1 jest: ^27.4.7 + jest-each: ^27.4.6 jest-mock: ^27.4.6 json5: ^2.2.0 lodash-es: ^4.17.21 -- cgit v1.2.3-54-g00ecf