Migrate to electron-vite and electron-builder
- Switch from deprecated Vue CLI plugin to `electron-vite` (see nklayman/vue-cli-plugin-electron-builder#1982) - Update main/preload scripts to use `index.cjs` filenames to support `"type": "module"`, resolving crash issue (#233). This crash was related to Electron not supporting ESM (see electron/asar#249, electron/electron#21457). - This commit completes migration to Vite from Vue CLI (#230). Structure changes: - Introduce separate folders for Electron's main and preload processes. - Move TypeHelpers to `src/` to mark tit as accessible by the rest of the code. Config changes: - Make `vite.config.ts` reusable by Electron configuration. - On electron-builder, use `--publish` flag instead of `-p` for clarity. Tests: - Add log for preload script loading verification. - Implement runtime environment sanity checks. - Enhance logging in `check-desktop-runtime-errors`.
This commit is contained in:
11
src/TypeHelpers.ts
Normal file
11
src/TypeHelpers.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type Constructible<T, TArgs extends unknown[] = never> = {
|
||||
prototype: T;
|
||||
apply: (this: unknown, args: TArgs) => void;
|
||||
};
|
||||
|
||||
export type PropertyKeys<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : K;
|
||||
}[keyof T];
|
||||
|
||||
export type ConstructorArguments<T> =
|
||||
T extends new (...args: infer U) => unknown ? U : never;
|
||||
@@ -8,17 +8,18 @@ import LinuxData from '@/application/collections/linux.yaml';
|
||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
||||
import { ViteAppMetadata } from '@/infrastructure/Metadata/Vite/ViteAppMetadata';
|
||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
|
||||
export function parseApplication(
|
||||
parser = parseCategoryCollection,
|
||||
metadata: IAppMetadata = new ViteAppMetadata(),
|
||||
categoryParser = parseCategoryCollection,
|
||||
informationParser = parseProjectInformation,
|
||||
metadata: IAppMetadata = AppMetadataFactory.Current,
|
||||
collectionsData = PreParsedCollections,
|
||||
): IApplication {
|
||||
validateCollectionsData(collectionsData);
|
||||
const information = parseProjectInformation(metadata);
|
||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||
const information = informationParser(metadata);
|
||||
const collections = collectionsData.map((collection) => categoryParser(collection, information));
|
||||
const app = new Application(information, collections);
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,20 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
||||
import { Version } from '@/domain/Version';
|
||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
||||
import { ConstructorArguments } from '@/TypeHelpers';
|
||||
|
||||
export function parseProjectInformation(
|
||||
metadata: IAppMetadata,
|
||||
export function
|
||||
parseProjectInformation(
|
||||
metadata: IAppMetadata = AppMetadataFactory.Current,
|
||||
createProjectInformation: ProjectInformationFactory = (
|
||||
...args
|
||||
) => new ProjectInformation(...args),
|
||||
): IProjectInformation {
|
||||
const version = new Version(
|
||||
metadata.version,
|
||||
);
|
||||
return new ProjectInformation(
|
||||
return createProjectInformation(
|
||||
metadata.name,
|
||||
version,
|
||||
metadata.slogan,
|
||||
@@ -17,3 +23,7 @@ export function parseProjectInformation(
|
||||
metadata.homepageUrl,
|
||||
);
|
||||
}
|
||||
|
||||
export type ProjectInformationFactory = (
|
||||
...args: ConstructorArguments<typeof ProjectInformation>
|
||||
) => IProjectInformation;
|
||||
|
||||
16
src/infrastructure/Metadata/AppMetadataFactory.ts
Normal file
16
src/infrastructure/Metadata/AppMetadataFactory.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IAppMetadata } from './IAppMetadata';
|
||||
import { ViteAppMetadata } from './Vite/ViteAppMetadata';
|
||||
|
||||
export class AppMetadataFactory {
|
||||
public static get Current(): IAppMetadata {
|
||||
if (!this.instance) {
|
||||
this.instance = new ViteAppMetadata();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
private static instance: IAppMetadata;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
}
|
||||
3
src/infrastructure/RuntimeSanity/ISanityCheckOptions.ts
Normal file
3
src/infrastructure/RuntimeSanity/ISanityCheckOptions.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface ISanityCheckOptions {
|
||||
readonly validateMetadata: boolean;
|
||||
}
|
||||
6
src/infrastructure/RuntimeSanity/ISanityValidator.ts
Normal file
6
src/infrastructure/RuntimeSanity/ISanityValidator.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ISanityCheckOptions } from './ISanityCheckOptions';
|
||||
|
||||
export interface ISanityValidator {
|
||||
shouldValidate(options: ISanityCheckOptions): boolean;
|
||||
collectErrors(): Iterable<string>;
|
||||
}
|
||||
28
src/infrastructure/RuntimeSanity/SanityChecks.ts
Normal file
28
src/infrastructure/RuntimeSanity/SanityChecks.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ISanityCheckOptions } from './ISanityCheckOptions';
|
||||
import { ISanityValidator } from './ISanityValidator';
|
||||
import { MetadataValidator } from './Validators/MetadataValidator';
|
||||
|
||||
const SanityValidators: ISanityValidator[] = [
|
||||
new MetadataValidator(),
|
||||
];
|
||||
|
||||
export function validateRuntimeSanity(
|
||||
options: ISanityCheckOptions,
|
||||
validators: readonly ISanityValidator[] = SanityValidators,
|
||||
): void {
|
||||
if (!options) {
|
||||
throw new Error('missing options');
|
||||
}
|
||||
if (!validators?.length) {
|
||||
throw new Error('missing validators');
|
||||
}
|
||||
const errorMessages = validators.reduce((errors, validator) => {
|
||||
if (validator.shouldValidate(options)) {
|
||||
errors.push(...validator.collectErrors());
|
||||
}
|
||||
return errors;
|
||||
}, new Array<string>());
|
||||
if (errorMessages.length > 0) {
|
||||
throw new Error(`Sanity check failed.\n${errorMessages.join('\n---\n')}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
||||
import { ISanityCheckOptions } from '../ISanityCheckOptions';
|
||||
import { ISanityValidator } from '../ISanityValidator';
|
||||
|
||||
export class MetadataValidator implements ISanityValidator {
|
||||
private readonly metadata: IAppMetadata;
|
||||
|
||||
constructor(metadataFactory: () => IAppMetadata = () => AppMetadataFactory.Current) {
|
||||
this.metadata = metadataFactory();
|
||||
}
|
||||
|
||||
public shouldValidate(options: ISanityCheckOptions): boolean {
|
||||
return options.validateMetadata;
|
||||
}
|
||||
|
||||
public* collectErrors(): Iterable<string> {
|
||||
if (!this.metadata) {
|
||||
yield 'missing metadata';
|
||||
return;
|
||||
}
|
||||
const keyValues = capturePropertyValues(this.metadata);
|
||||
if (!Object.keys(keyValues).length) {
|
||||
yield 'Unable to capture metadata key/value pairs';
|
||||
return;
|
||||
}
|
||||
const keysMissingValue = getMissingMetadataKeys(keyValues);
|
||||
if (keysMissingValue.length > 0) {
|
||||
yield `Metadata keys missing: ${keysMissingValue.join(', ')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMissingMetadataKeys(keyValuePairs: Record<string, unknown>): string[] {
|
||||
return Object.entries(keyValuePairs)
|
||||
.reduce((acc, [key, value]) => {
|
||||
if (!value) {
|
||||
acc.push(key);
|
||||
}
|
||||
return acc;
|
||||
}, new Array<string>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures values of properties and getters from the provided instance.
|
||||
* Necessary because code transformations can make class getters non-enumerable during bundling.
|
||||
* This ensures that even if getters are non-enumerable, their values are still captured and used.
|
||||
*/
|
||||
function capturePropertyValues(instance: unknown): Record<string, unknown> {
|
||||
const obj: Record<string, unknown> = {};
|
||||
const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype);
|
||||
|
||||
// Capture regular properties from the instance
|
||||
for (const [key, value] of Object.entries(instance)) {
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
// Capture getter properties from the instance's prototype
|
||||
for (const [key, descriptor] of Object.entries(descriptors)) {
|
||||
if (typeof descriptor.get === 'function') {
|
||||
obj[key] = descriptor.get.call(instance);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeBu
|
||||
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
||||
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
||||
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
||||
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||
import { provideDependencies } from '../bootstrapping/DependencyProvider';
|
||||
|
||||
const singletonAppContext = await buildContext();
|
||||
@@ -32,6 +33,9 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts
|
||||
validateRuntimeSanity({
|
||||
validateMetadata: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
16
src/presentation/electron/main/ElectronConfig.ts
Normal file
16
src/presentation/electron/main/ElectronConfig.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Abstraction for electron-vite specific logic and other Electron CLI helpers/wrappers.
|
||||
* Allows for agnostic application design and centralizes adjustments when switching wrappers.
|
||||
*/
|
||||
|
||||
/// <reference types="electron-vite/node" />
|
||||
import { join } from 'path';
|
||||
import appIcon from '@/presentation/public/icon.png?asset';
|
||||
|
||||
export const APP_ICON_PATH = appIcon;
|
||||
|
||||
export const RENDERER_URL = process.env.ELECTRON_RENDERER_URL;
|
||||
|
||||
export const RENDERER_HTML_PATH = join('file://', __dirname, '../renderer/index.html');
|
||||
|
||||
export const PRELOADER_SCRIPT_PATH = join(__dirname, '../preload/index.cjs');
|
||||
@@ -6,8 +6,8 @@ import log from 'electron-log';
|
||||
import fetch from 'cross-fetch';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Version } from '@/domain/Version';
|
||||
import { ViteAppMetadata } from '@/infrastructure/Metadata/Vite/ViteAppMetadata';
|
||||
import { Version } from '@/domain/Version';
|
||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||
import { UpdateProgressBar } from './UpdateProgressBar';
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
// This is main process of Electron, started as first thing when app starts.
|
||||
// This script is running through entire life of the application.
|
||||
// It doesn't have any windows which you can see on screen, opens the main window from here.
|
||||
// Initializes Electron's main process, always runs in the background, and manages the main window.
|
||||
|
||||
import path from 'path';
|
||||
import {
|
||||
app, protocol, BrowserWindow, shell, screen,
|
||||
} from 'electron';
|
||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
||||
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
|
||||
import log from 'electron-log';
|
||||
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||
import { setupAutoUpdater } from './Update/Updater';
|
||||
import {
|
||||
APP_ICON_PATH, PRELOADER_SCRIPT_PATH, RENDERER_HTML_PATH, RENDERER_URL,
|
||||
} from './ElectronConfig';
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// Path of static assets, magic variable populated by electron
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle
|
||||
declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172
|
||||
const isDevelopment = !app.isPackaged;
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
@@ -27,6 +23,9 @@ protocol.registerSchemesAsPrivileged([
|
||||
]);
|
||||
|
||||
setupLogger();
|
||||
validateRuntimeSanity({
|
||||
validateMetadata: true,
|
||||
});
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
@@ -35,14 +34,11 @@ function createWindow() {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
webPreferences: {
|
||||
contextIsolation: false, // To reach node https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1285
|
||||
// Use pluginOptions.nodeIntegration, leave this alone
|
||||
// See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
|
||||
nodeIntegration: (process.env
|
||||
.ELECTRON_NODE_INTEGRATION as unknown) as boolean,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
preload: PRELOADER_SCRIPT_PATH,
|
||||
},
|
||||
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#set-tray-icon
|
||||
icon: path.join(__static, 'icon.png'),
|
||||
icon: APP_ICON_PATH,
|
||||
});
|
||||
|
||||
win.setMenuBarVisibility(false);
|
||||
@@ -83,17 +79,12 @@ app.on('activate', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', async () => {
|
||||
if (isDevelopment && !process.env.IS_TEST) {
|
||||
// Install Vue Devtools
|
||||
if (isDevelopment) {
|
||||
try {
|
||||
await installExtension(VUEJS_DEVTOOLS);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Vue Devtools failed to install:', e.toString());
|
||||
log.error('Vue Devtools failed to install:', e.toString());
|
||||
}
|
||||
}
|
||||
createWindow();
|
||||
@@ -115,22 +106,19 @@ if (isDevelopment) {
|
||||
}
|
||||
|
||||
function loadApplication(window: BrowserWindow) {
|
||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||
// Load the url of the dev server if in development mode
|
||||
loadUrlWithNodeWorkaround(win, process.env.WEBPACK_DEV_SERVER_URL as string);
|
||||
if (!process.env.IS_TEST) {
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
if (RENDERER_URL) { // Populated in a dev server during development
|
||||
loadUrlWithNodeWorkaround(win, RENDERER_URL);
|
||||
} else {
|
||||
loadUrlWithNodeWorkaround(win, RENDERER_HTML_PATH);
|
||||
}
|
||||
if (isDevelopment) {
|
||||
window.webContents.openDevTools();
|
||||
} else {
|
||||
createProtocol('app');
|
||||
// Load the index.html when not in development
|
||||
loadUrlWithNodeWorkaround(win, 'app://./index.html');
|
||||
const updater = setupAutoUpdater();
|
||||
updater.checkForUpdates();
|
||||
}
|
||||
// Do not remove [APP_INIT_SUCCESS]; it's a marker used in tests to verify
|
||||
// app initialization.
|
||||
log.info('[APP_INIT_SUCCESS] Main window initialized and content loading.');
|
||||
// Do not remove [WINDOW_INIT]; it's a marker used in tests.
|
||||
log.info('[WINDOW_INIT] Main window initialized and content loading.');
|
||||
}
|
||||
|
||||
function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
|
||||
@@ -158,7 +146,7 @@ function getWindowSize(idealWidth: number, idealHeight: number) {
|
||||
|
||||
function setupLogger(): void {
|
||||
log.transports.file.level = 'silly';
|
||||
if (!process.env.IS_TEST) {
|
||||
if (!isDevelopment) {
|
||||
Object.assign(console, log.functions); // override console.log, console.warn etc.
|
||||
}
|
||||
}
|
||||
11
src/presentation/electron/preload/index.ts
Normal file
11
src/presentation/electron/preload/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// This preload script serves as a placeholder to securely expose Electron APIs to the application.
|
||||
// As of now, the application does not utilize any specific Electron APIs through this script.
|
||||
import log from 'electron-log';
|
||||
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||
|
||||
validateRuntimeSanity({
|
||||
validateMetadata: true,
|
||||
});
|
||||
|
||||
// Do not remove [PRELOAD_INIT]; it's a marker used in tests.
|
||||
log.info('[PRELOAD_INIT] Preload script successfully initialized and executed.');
|
||||
Reference in New Issue
Block a user