Skip to content

structured more like angular#817

Closed
HoseinErfani wants to merge 10 commits intobelnadris:mainfrom
HoseinErfani:main
Closed

structured more like angular#817
HoseinErfani wants to merge 10 commits intobelnadris:mainfrom
HoseinErfani:main

Conversation

@HoseinErfani
Copy link
Copy Markdown

No description provided.

@belnadris
Copy link
Copy Markdown
Owner

belnadris commented Feb 27, 2026

Hi @HoseinErfani, thanks for the effort on this refactor! After reviewing, we won't be merging this PR. Here's why:

  1. No description - it's hard to evaluate the intent and trade-offs without context on what problem this solves.
  2. No prior discussion - a restructuring of this scope (42 files) is best discussed in an issue or RFC before implementation.
  3. Committed build artifacts - compiled .js files and an Nx binary cache file are included, which we don't commit to the repo.
  4. Removes LICENSE.md - this needs explicit maintainer intent.
  5. Indentation change - switching from 2 to 4 spaces would cause project-wide whitespace churn.

If you're interested in contributing an architectural improvement, feel free to open an issue to discuss the direction first - we'd be happy to collaborate that way. Thanks again!

@belnadris belnadris closed this Feb 27, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request restructures an Angular-Electron application to follow an Angular-like architecture with class-based components, decorators, and dependency injection using InversifyJS. The main goal is to modernize the Electron main process code to mirror Angular's programming patterns.

Changes:

  • Introduced decorator-based event handling for Tray, Window, and IPC events using custom decorators (@TrayListener, @windowlistener, @IpcListener)
  • Implemented dependency injection in the Electron main process using InversifyJS with @injectable and @inject decorators
  • Refactored the main.ts entry point to use a class-based architecture with a MainWindow class extending base classes for window management

Reviewed changes

Copilot reviewed 39 out of 43 changed files in this pull request and generated 25 comments.

Show a summary per file
File Description
src/assets/icons/electron.svg Added SVG icon for Electron tray
src/assets/icons/electron.png Added PNG icon for Electron tray
src/app/core/services/electron/electron.service.ts Added updateTrayText method and ElectronWindow type, reformatted imports
src/app/core/services/electron/electron.service.spec.ts Deleted test file
src/app/app.component.ts Removed TranslateService, added interval-based tray text update demo
src/app/app.component.html Commented out router-outlet, added counter display
shared/types/ipc-channel.type.ts Added type definition for IPC channel payloads
shared/enums/ipc-channel.enum.ts Added enum for IPC channel names
package.json Added inversify and reflect-metadata dependencies, removed -o flag from ng:serve
package-lock.json Updated lockfile with new dependencies
app/windows/main.window.ts Created MainWindow class with decorator-based event handling
app/utils/services/file.service.ts Created FileService for path resolution
app/utils/interfaces/on-app-ready.interface.ts Created OnAppReady lifecycle interface
app/utils/hooks/provide.hook.ts Created useProvide hook for DI container setup
app/utils/decorators/*.ts Created decorators for tray, window, and IPC event listening
app/utils/base-classes/*.ts Created base classes for window management with DI
app/utils/enums/*.ts Created enums for event names
app/utils/types/*.ts Created type definitions for event parameters
app/main.ts Refactored to use new architecture with DI and MainWindow class
README.md Completely rewritten to document new decorator-based architecture
LICENSE.md Deleted MIT license
.editorconfig Changed indent_size from 2 to 4 spaces

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +3
<!--<router-outlet></router-outlet>-->

{{s}}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The router-outlet is commented out but not removed, and the component now displays a simple counter. This appears to be temporary debug/demo code that should either be properly implemented or removed before merging to ensure the application's routing functionality is preserved.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +63
updateTrayText(text: IPCChannelType[IPCChannelEnum.UPDATE_TRAY_TEXT]) {
this.ipcRenderer.send(IPCChannelEnum.UPDATE_TRAY_TEXT, text);
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method 'updateTrayText' uses camelCase, which is consistent with TypeScript conventions. However, the parameter 'text' is typed as a complex indexed type. Consider adding JSDoc documentation to clarify the purpose and expected values for this method, especially since it's a public API of the service.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +31
if (!target.__trayEvents) {
target.__trayEvents = [];
}

const eventKey = `${eventName}_${propertyKey}`;

// Prevent duplicate event binding
if (!target.__trayEvents.includes(eventKey)) {
target.__trayEvents.push(eventKey);

const originalOnAppReady = target.onAppReady;

target.onAppReady = function (...args: any[]) {
if (typeof originalOnAppReady === 'function') {
originalOnAppReady.apply(this, args);
}

const tray = this.tray as Tray;

if (tray) {
tray.on(eventName as any, (...eventArgs: any[]) => {
// Ensure the method signature matches the event's arguments
if (typeof this[propertyKey] === 'function') {
const expectedArgs = eventArgs as TrayEventType[T];
this[propertyKey](expectedArgs);
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decorator modifies target.onAppReady by wrapping it, but this happens multiple times when multiple decorators are applied to different methods in the same class. Each decorator call wraps the previous onAppReady, creating a chain of wrapper functions. This could lead to the original onAppReady being called multiple times. Consider a different approach like storing listeners in a metadata array and registering them once in a single onAppReady wrapper.

Suggested change
if (!target.__trayEvents) {
target.__trayEvents = [];
}
const eventKey = `${eventName}_${propertyKey}`;
// Prevent duplicate event binding
if (!target.__trayEvents.includes(eventKey)) {
target.__trayEvents.push(eventKey);
const originalOnAppReady = target.onAppReady;
target.onAppReady = function (...args: any[]) {
if (typeof originalOnAppReady === 'function') {
originalOnAppReady.apply(this, args);
}
const tray = this.tray as Tray;
if (tray) {
tray.on(eventName as any, (...eventArgs: any[]) => {
// Ensure the method signature matches the event's arguments
if (typeof this[propertyKey] === 'function') {
const expectedArgs = eventArgs as TrayEventType[T];
this[propertyKey](expectedArgs);
// Store metadata about tray listeners on the prototype
if (!target.__trayListenersMetadata) {
target.__trayListenersMetadata = [];
}
const listeners: Array<{ eventName: TrayEventEnum; propertyKey: string }> =
target.__trayListenersMetadata;
// Prevent duplicate metadata entries for the same event/method pair
const alreadyRegistered = listeners.some(
(listener) => listener.eventName === eventName && listener.propertyKey === propertyKey,
);
if (!alreadyRegistered) {
listeners.push({ eventName, propertyKey });
}
// Wrap onAppReady only once per class/prototype
if (!target.__trayOnAppReadyWrapped) {
target.__trayOnAppReadyWrapped = true;
const originalOnAppReady = target.onAppReady;
target.onAppReady = function (...args: any[]) {
// Call the original onAppReady implementation once
if (typeof originalOnAppReady === 'function') {
originalOnAppReady.apply(this, args);
}
const tray = this.tray as Tray;
if (!tray) {
return;
}
const meta: Array<{ eventName: TrayEventEnum; propertyKey: string }> =
(this as any).__trayListenersMetadata;
if (!Array.isArray(meta)) {
return;
}
for (const { eventName, propertyKey } of meta) {
tray.on(eventName as any, (...eventArgs: any[]) => {
// Ensure the method signature matches the event's arguments
const handler = (this as any)[propertyKey];
if (typeof handler === 'function') {
const expectedArgs = eventArgs as TrayEventType[typeof eventName];
handler(expectedArgs);

Copilot uses AI. Check for mistakes.
Comment thread src/app/app.component.ts
Comment on lines +13 to +15
constructor(
private electronService: ElectronService,
) {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TranslateService import and usage has been removed from AppComponent, but this appears to break existing functionality. The test file (app.component.spec.ts) still imports TranslateModule, suggesting that translation was a core feature. This is a breaking change that should be addressed.

Copilot uses AI. Check for mistakes.
show: false,
webPreferences: {
nodeIntegration: true,
allowRunningInsecureContent: true,
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting 'allowRunningInsecureContent: true' allows the renderer to load insecure (HTTP) content even when the page is loaded via HTTPS. This is a security vulnerability that could expose users to man-in-the-middle attacks. This setting should be removed or set to false.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +29
target.onAppReady = function (...args: any[]) {
if (typeof originalOnAppReady === 'function') {
originalOnAppReady.apply(this, args);
}

// Listen to the IPC event
ipcMain.on(channel, (event: IpcMainEvent, ...eventArgs: any[]) => {
if (typeof this[propertyKey] === 'function') {
this[propertyKey](event, ...eventArgs);
}
});
};
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IpcListener decorator registers event listeners on ipcMain but never removes them. If the MainWindow instance is destroyed and recreated, duplicate event listeners will accumulate, causing memory leaks and potential double-execution of handlers. The decorator should track listeners and provide cleanup when the instance is destroyed.

Suggested change
target.onAppReady = function (...args: any[]) {
if (typeof originalOnAppReady === 'function') {
originalOnAppReady.apply(this, args);
}
// Listen to the IPC event
ipcMain.on(channel, (event: IpcMainEvent, ...eventArgs: any[]) => {
if (typeof this[propertyKey] === 'function') {
this[propertyKey](event, ...eventArgs);
}
});
};
// Wrap onAppReady to register the IPC listener when the app is ready
target.onAppReady = function (...args: any[]) {
if (typeof originalOnAppReady === 'function') {
originalOnAppReady.apply(this, args);
}
// Ensure a per-instance storage for IPC listeners
if (!this.__ipcListeners) {
this.__ipcListeners = [];
}
// Listener function that will be registered with ipcMain
const listener = (event: IpcMainEvent, ...eventArgs: any[]) => {
if (typeof this[propertyKey] === 'function') {
this[propertyKey](event, ...eventArgs);
}
};
// Listen to the IPC event and keep track of the listener for cleanup
ipcMain.on(channel, listener);
this.__ipcListeners.push({channel, listener});
};
// Register a one-time cleanup hook to remove all IPC listeners on destroy
if (!target.__ipcCleanupRegistered) {
const originalOnDestroy = target.onDestroy;
target.onDestroy = function (...destroyArgs: any[]) {
if (Array.isArray(this.__ipcListeners)) {
this.__ipcListeners.forEach((entry: any) => {
if (entry && entry.channel && entry.listener) {
ipcMain.removeListener(entry.channel, entry.listener);
}
});
this.__ipcListeners = [];
}
if (typeof originalOnDestroy === 'function') {
originalOnDestroy.apply(this, destroyArgs);
}
};
target.__ipcCleanupRegistered = true;
}

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +32
window.on(eventName as any, (...eventArgs: any[]) => {
// Ensure the method signature matches the event's arguments
if (typeof this[propertyKey] === 'function') {
const expectedArgs = eventArgs as WindowEventType[T];
this[propertyKey](expectedArgs);
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WindowListener decorator registers event listeners without cleanup, which can cause memory leaks. When a window is created and destroyed multiple times, old event listeners will persist in memory. Consider implementing proper cleanup when the window is destroyed.

Suggested change
window.on(eventName as any, (...eventArgs: any[]) => {
// Ensure the method signature matches the event's arguments
if (typeof this[propertyKey] === 'function') {
const expectedArgs = eventArgs as WindowEventType[T];
this[propertyKey](expectedArgs);
}
const listener = (...eventArgs: any[]) => {
// Ensure the method signature matches the event's arguments
if (typeof this[propertyKey] === 'function') {
const expectedArgs = eventArgs as WindowEventType[T];
this[propertyKey](expectedArgs);
}
};
window.on(eventName as any, listener);
// Clean up the listener when the window is closed to avoid memory leaks
window.on('closed', () => {
window.removeListener(eventName as any, listener);

Copilot uses AI. Check for mistakes.
private _window$: ReplaySubject<BrowserWindow> = new ReplaySubject<BrowserWindow>();

constructor() {
app.whenReady().then(() => this.onAppReady());
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor calls app.whenReady() which sets up the ready handler, but if the app is already ready when this constructor is called, the onAppReady() method will still be called. However, there's a race condition: decorators modify onAppReady on the prototype, but if the instance is created after the app is ready, the decorated version might be called before all decorators have been applied. Consider using a different initialization pattern.

Suggested change
app.whenReady().then(() => this.onAppReady());
if (this.app.isReady()) {
this.onAppReady();
} else {
this.app.once('ready', () => this.onAppReady());
}

Copilot uses AI. Check for mistakes.
Comment thread src/app/app.component.ts
Comment on lines +19 to +27
interval(1000).pipe(
map((_: number) => {
this.s++;
return this.s.toString();
}),
tap(time => {
this.electronService.updateTrayText(time);
})
).subscribe()
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subscription created by the interval observable is never unsubscribed. This will cause a memory leak as the interval will continue to run even after the component is destroyed. The subscription should be stored and unsubscribed in the ngOnDestroy lifecycle hook.

Copilot uses AI. Check for mistakes.
Comment thread app/main.ts
Comment on lines 7 to 9
} catch (e) {
// Catch Error
// throw e;

}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty catch block silently swallows all errors, making debugging impossible. At minimum, the error should be logged to the console, or better yet, implement proper error handling and reporting.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants