@devisfuture/electron-modular 1.1.16 → 1.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,34 @@
12
12
  - Code of Conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
13
13
  - Security policy: [SECURITY.md](SECURITY.md)
14
14
 
15
+ ## Navigation
16
+
17
+ - [Overview](#overview) — High-level summary and goals.
18
+ - [What It Solves](#what-it-solves) — Practical pain points addressed.
19
+ - [What You Get](#what-you-get) — Core capabilities provided.
20
+ - [Key Features](#key-features) — Quick feature checklist.
21
+ - [Example App](#example-app) — Starter/example repositories.
22
+ - [Installation](#installation) — Install and peer dependency notes.
23
+ - [Folders (build outputs)](#folders-build-outputs) — Build output locations.
24
+ - [Quick Start](#quick-start) — Minimal bootstrap example for `main.ts`.
25
+ - [Module Structure](#module-structure) — Recommended file layout for a feature module.
26
+ - [Two Approaches to Using Modules](#two-approaches-to-using-modules) — Design choices for module APIs.
27
+ - [Approach 1: Direct Service Injection](#approach-1-direct-service-injection-simple) — Simple DI usage.
28
+ - [Approach 2: Provider Pattern](#approach-2-provider-pattern-advanced) — Provider/factory-based contracts.
29
+ - [IPC Handlers](#ipc-handlers) — Organizing main ↔ renderer communication.
30
+ - [Window Managers](#window-managers) — Window lifecycle and helpers.
31
+ - [Preload script — default behavior](#preload-script--default-behavior-) — Default preload handling.
32
+ - [Lifecycle Hooks (Window & WebContents events)](#lifecycle-hooks-window--webcontents-events-) — Event hook mapping.
33
+ - [Opening windows with URL params](#opening-windows-with-url-params-dynamic-routes-) — Route/hash usage.
34
+ - [TypeScript types — TWindows["myWindow"]](#typescript-types--twindowsmywindow) — Typing conventions for windows.
35
+ - [API Reference](#api-reference) — Reference for decorators and core functions.
36
+ - [Core Decorators](#core-decorators) — `@RgModule`, `@Injectable`, `@Inject`, `@IpcHandler`, `@WindowManager`.
37
+ - [Core Functions](#core-functions) — `initSettings`, `bootstrapModules`, `getWindow`, `destroyWindows`.
38
+ - [Lazy Loading modules](#lazy-loading-modules) — Deferred init behavior and constraints.
39
+ - [Lifecycle Interfaces](#lifecycle-interfaces) — IPC and window interface contracts.
40
+ - [Best Practices](#best-practices) — Recommended development patterns.
41
+ - [Type Everything](#5-type-everything) — Type-safety guidance and tips.
42
+
15
43
  ## Overview
16
44
 
17
45
  A lightweight dependency injection container for Electron's main process that brings modular architecture and clean code organization to your desktop applications.
@@ -544,6 +572,7 @@ Defines a module with its dependencies and providers.
544
572
  - `ipc?: Class[]` - IPC handler classes
545
573
  - `windows?: Class[]` - Window manager classes
546
574
  - `exports?: Class[]` - Providers to export
575
+ - `lazy?: { enabled: true; trigger: string }` - Defers module initialization until renderer invokes the trigger channel
547
576
 
548
577
  #### `@Injectable()`
549
578
 
@@ -643,6 +672,110 @@ Bootstraps all modules and initializes the DI container.
643
672
  await bootstrapModules([AppModule, AuthModule, ResourcesModule]);
644
673
  ```
645
674
 
675
+ Lazy modules are registered but not initialized during bootstrap. Initialization happens on first `ipcRenderer.invoke(trigger)` from renderer process.
676
+
677
+ ```typescript
678
+ @RgModule({
679
+ providers: [AnalyticsService],
680
+ ipc: [AnalyticsIpc],
681
+ lazy: {
682
+ enabled: true,
683
+ trigger: "analytics",
684
+ },
685
+ })
686
+ export class AnalyticsModule {}
687
+
688
+ await bootstrapModules([UserModule, AnalyticsModule]);
689
+ // UserModule: initialized immediately
690
+ // AnalyticsModule: initialized on first ipcRenderer.invoke("analytics")
691
+ ```
692
+
693
+ Notes:
694
+
695
+ - Lazy loading defers runtime initialization work (provider resolution, module instantiation, IPC `onInit`).
696
+ - It does not perform JavaScript code-splitting by itself; module code is still loaded by your app bundle strategy.
697
+ - Each lazy trigger must be unique across modules in the same bootstrap call.
698
+
699
+ ### Lazy Loading modules
700
+
701
+ Lazy Loading lets you defer module initialization until the renderer explicitly requests it via `ipcRenderer.invoke(trigger)`.
702
+
703
+ Why it exists:
704
+
705
+ - Reduces startup work in the main process for modules that are not needed immediately.
706
+ - Improves perceived startup time when some features are rarely used.
707
+
708
+ When to use it:
709
+
710
+ - Heavy modules (database connections, expensive service wiring, feature-specific IPC setup).
711
+ - Feature modules opened only after a user action (analytics, advanced settings, reports).
712
+
713
+ When it usually does not help:
714
+
715
+ - Modules required at application start (auth bootstrap, app shell wiring, core windows).
716
+ - Very small modules where deferred initialization adds complexity without measurable gain.
717
+
718
+ Important constraints:
719
+
720
+ - A lazy module **cannot** declare `exports`.
721
+ - A lazy module can import only eager modules.
722
+ - An eager module cannot import a lazy module.
723
+
724
+ These constraints guarantee clear module boundaries: lazy modules are activated explicitly, while shared cross-module dependencies stay eager and deterministic.
725
+
726
+ #### Example: valid lazy module
727
+
728
+ ```typescript
729
+ @RgModule({
730
+ imports: [DatabaseCoreModule], // eager module
731
+ providers: [AnalyticsService],
732
+ ipc: [AnalyticsIpc],
733
+ lazy: {
734
+ enabled: true,
735
+ trigger: "analytics:init",
736
+ },
737
+ })
738
+ export class AnalyticsModule {}
739
+
740
+ await bootstrapModules([AppModule, AnalyticsModule]);
741
+
742
+ // Renderer side:
743
+ await ipcRenderer.invoke("analytics:init");
744
+ ```
745
+
746
+ #### Example: invalid (lazy + exports)
747
+
748
+ ```typescript
749
+ @RgModule({
750
+ providers: [AnalyticsService],
751
+ exports: [AnalyticsService], // ❌ not allowed for lazy modules
752
+ lazy: {
753
+ enabled: true,
754
+ trigger: "analytics:init",
755
+ },
756
+ })
757
+ export class AnalyticsModule {}
758
+ ```
759
+
760
+ #### Example: invalid (eager imports lazy)
761
+
762
+ ```typescript
763
+ @RgModule({
764
+ providers: [],
765
+ lazy: {
766
+ enabled: true,
767
+ trigger: "database:init",
768
+ },
769
+ })
770
+ export class DatabaseModule {}
771
+
772
+ @RgModule({
773
+ imports: [DatabaseModule], // ❌ eager module cannot import lazy module
774
+ providers: [ReportsService],
775
+ })
776
+ export class ReportsModule {}
777
+ ```
778
+
646
779
  #### `getWindow<T>(hash)`
647
780
 
648
781
  Retrieves a window instance by its hash identifier.
@@ -784,6 +917,7 @@ Defines a module.
784
917
  - `ipc?: Class[]` - IPC handler classes
785
918
  - `windows?: Class[]` - Window manager classes
786
919
  - `exports?: Class[]` - Providers to export
920
+ - `lazy?: { enabled: true; trigger: string }` - Defers module initialization until renderer invokes the trigger channel
787
921
 
788
922
  ### `@Injectable()`
789
923
 
@@ -1,14 +1,35 @@
1
- import { ModuleDecoratorMissingError } from "../errors/index.js";
1
+ import { ModuleDecoratorMissingError, DuplicateLazyTriggerError, InvalidLazyTriggerError, } from "../errors/index.js";
2
2
  import { instantiateModule } from "./instantiate-module.js";
3
3
  import { initializeModule } from "./initialize-module.js";
4
4
  import { container } from "../container.js";
5
5
  import { initializeIpcHandlers } from "./initialize-ipc/handlers.js";
6
+ import { registerLazyModule } from "./register-lazy-module.js";
7
+ import { validateLazyConstraints } from "./validate-lazy-constraints.js";
8
+ const getValidLazyTrigger = (moduleClass, metadata) => {
9
+ const trigger = metadata.lazy?.trigger;
10
+ if (typeof trigger !== "string" || trigger.trim().length === 0) {
11
+ throw new InvalidLazyTriggerError(moduleClass.name);
12
+ }
13
+ return trigger.trim();
14
+ };
6
15
  export const bootstrapModules = async (modulesClass) => {
16
+ const lazyTriggerRegistry = new Map();
7
17
  for (const moduleClass of modulesClass) {
8
18
  const metadata = Reflect.getMetadata("RgModule", moduleClass);
9
19
  if (!metadata) {
10
20
  throw new ModuleDecoratorMissingError(moduleClass.name);
11
21
  }
22
+ validateLazyConstraints(moduleClass, metadata);
23
+ if (metadata.lazy?.enabled) {
24
+ const trigger = getValidLazyTrigger(moduleClass, metadata);
25
+ const existingModule = lazyTriggerRegistry.get(trigger);
26
+ if (existingModule) {
27
+ throw new DuplicateLazyTriggerError(trigger, existingModule, moduleClass.name);
28
+ }
29
+ lazyTriggerRegistry.set(trigger, moduleClass.name);
30
+ registerLazyModule(moduleClass, metadata);
31
+ continue;
32
+ }
12
33
  await initializeModule(moduleClass, metadata);
13
34
  await instantiateModule(moduleClass);
14
35
  await container.resolve(moduleClass, moduleClass);
@@ -41,7 +41,7 @@ export const initializeIpcHandlers = async (moduleClass, metadata) => {
41
41
  for (const ipcClass of metadata.ipc) {
42
42
  const ipcInstance = await container.resolve(moduleClass, ipcClass);
43
43
  if (ipcInstance?.onInit) {
44
- ipcInstance.onInit({ getWindow });
44
+ await ipcInstance.onInit({ getWindow });
45
45
  }
46
46
  }
47
47
  };
@@ -3,7 +3,9 @@ import { registerProviders } from "./register-providers.js";
3
3
  import { registerImports } from "./register-imports.js";
4
4
  import { registerWindows } from "./register-windows.js";
5
5
  import { registerIpcHandlers } from "./register-ipc-handlers.js";
6
+ import { validateLazyConstraints } from "./validate-lazy-constraints.js";
6
7
  export const initializeModule = async (moduleClass, metadata) => {
8
+ validateLazyConstraints(moduleClass, metadata);
7
9
  const isNewModule = container.addModule(moduleClass, metadata);
8
10
  container.setModuleMetadata(moduleClass, metadata);
9
11
  if (!isNewModule) {
@@ -11,7 +13,7 @@ export const initializeModule = async (moduleClass, metadata) => {
11
13
  }
12
14
  await Promise.all([
13
15
  registerProviders(moduleClass, metadata),
14
- registerImports(metadata),
16
+ registerImports(moduleClass, metadata),
15
17
  registerWindows(moduleClass, metadata),
16
18
  registerIpcHandlers(moduleClass, metadata),
17
19
  ]);
@@ -1,2 +1,3 @@
1
1
  import type { RgModuleMetadata } from "../types/module-metadata.js";
2
- export declare const registerImports: (metadata: RgModuleMetadata) => Promise<void>;
2
+ import type { Constructor } from "../types/constructor.js";
3
+ export declare const registerImports: (moduleClass: Constructor, metadata: RgModuleMetadata) => Promise<void>;
@@ -1,10 +1,14 @@
1
+ import { EagerModuleCannotImportLazyModuleError } from "../errors/index.js";
1
2
  import { initializeModule } from "./initialize-module.js";
2
- export const registerImports = async (metadata) => {
3
+ export const registerImports = async (moduleClass, metadata) => {
3
4
  if (!metadata.imports) {
4
5
  return;
5
6
  }
6
7
  for (const importedModuleClass of metadata.imports) {
7
8
  const importedModuleMetadata = Reflect.getMetadata("RgModule", importedModuleClass);
9
+ if (importedModuleMetadata?.lazy?.enabled && !metadata.lazy?.enabled) {
10
+ throw new EagerModuleCannotImportLazyModuleError(moduleClass.name, importedModuleClass.name);
11
+ }
8
12
  if (importedModuleMetadata) {
9
13
  await initializeModule(importedModuleClass, importedModuleMetadata);
10
14
  }
@@ -0,0 +1,3 @@
1
+ import type { RgModuleMetadata } from "../types/module-metadata.js";
2
+ import type { Constructor } from "../types/constructor.js";
3
+ export declare const registerLazyModule: (moduleClass: Constructor, metadata: RgModuleMetadata) => void;
@@ -0,0 +1,48 @@
1
+ import { ipcMain } from "electron";
2
+ import { initializeModule } from "./initialize-module.js";
3
+ import { instantiateModule } from "./instantiate-module.js";
4
+ import { container } from "../container.js";
5
+ import { initializeIpcHandlers } from "./initialize-ipc/handlers.js";
6
+ import { InvalidLazyTriggerError } from "../errors/index.js";
7
+ const getValidLazyTrigger = (moduleClass, metadata) => {
8
+ const trigger = metadata.lazy?.trigger;
9
+ if (typeof trigger !== "string" || trigger.trim().length === 0) {
10
+ throw new InvalidLazyTriggerError(moduleClass.name);
11
+ }
12
+ return trigger.trim();
13
+ };
14
+ export const registerLazyModule = (moduleClass, metadata) => {
15
+ const trigger = getValidLazyTrigger(moduleClass, metadata);
16
+ let initPromise = null;
17
+ ipcMain.handle(trigger, async () => {
18
+ if (initPromise) {
19
+ return initPromise;
20
+ }
21
+ initPromise = (async () => {
22
+ try {
23
+ await initializeModule(moduleClass, metadata);
24
+ await instantiateModule(moduleClass);
25
+ await container.resolve(moduleClass, moduleClass);
26
+ if (metadata.windows?.length && !metadata.ipc?.length) {
27
+ console.warn(`Warning: Window(s) declared in module "${moduleClass.name}" but no IPC handlers found to manage them.`);
28
+ }
29
+ await initializeIpcHandlers(moduleClass, metadata);
30
+ return {
31
+ initialized: true,
32
+ name: trigger,
33
+ };
34
+ }
35
+ catch (error) {
36
+ initPromise = null;
37
+ return {
38
+ initialized: false,
39
+ name: trigger,
40
+ error: {
41
+ message: error instanceof Error ? error.message : String(error),
42
+ },
43
+ };
44
+ }
45
+ })();
46
+ return initPromise;
47
+ });
48
+ };
@@ -0,0 +1,3 @@
1
+ import type { Constructor } from "../types/constructor.js";
2
+ import type { RgModuleMetadata } from "../types/module-metadata.js";
3
+ export declare const validateLazyConstraints: (moduleClass: Constructor, metadata: RgModuleMetadata) => void;
@@ -0,0 +1,18 @@
1
+ import { LazyModuleCannotImportLazyModuleError, LazyModuleExportsNotAllowedError, } from "../errors/index.js";
2
+ export const validateLazyConstraints = (moduleClass, metadata) => {
3
+ if (!metadata.lazy?.enabled) {
4
+ return;
5
+ }
6
+ if ((metadata.exports?.length ?? 0) > 0) {
7
+ throw new LazyModuleExportsNotAllowedError(moduleClass.name);
8
+ }
9
+ if (!metadata.imports?.length) {
10
+ return;
11
+ }
12
+ for (const importedModuleClass of metadata.imports) {
13
+ const importedModuleMetadata = Reflect.getMetadata("RgModule", importedModuleClass);
14
+ if (importedModuleMetadata?.lazy?.enabled) {
15
+ throw new LazyModuleCannotImportLazyModuleError(moduleClass.name, importedModuleClass.name);
16
+ }
17
+ }
18
+ };
@@ -16,4 +16,19 @@ export declare class InvalidProviderError extends BaseError {
16
16
  export declare class SettingsNotInitializedError extends BaseError {
17
17
  constructor();
18
18
  }
19
+ export declare class InvalidLazyTriggerError extends BaseError {
20
+ constructor(moduleName: string);
21
+ }
22
+ export declare class DuplicateLazyTriggerError extends BaseError {
23
+ constructor(trigger: string, firstModule: string, secondModule: string);
24
+ }
25
+ export declare class LazyModuleExportsNotAllowedError extends BaseError {
26
+ constructor(moduleName: string);
27
+ }
28
+ export declare class LazyModuleCannotImportLazyModuleError extends BaseError {
29
+ constructor(moduleName: string, importedModuleName: string);
30
+ }
31
+ export declare class EagerModuleCannotImportLazyModuleError extends BaseError {
32
+ constructor(moduleName: string, importedModuleName: string);
33
+ }
19
34
  export {};
@@ -29,3 +29,28 @@ export class SettingsNotInitializedError extends BaseError {
29
29
  super("App settings cache has not been initialized.", "SettingsNotInitializedError");
30
30
  }
31
31
  }
32
+ export class InvalidLazyTriggerError extends BaseError {
33
+ constructor(moduleName) {
34
+ super(`Invalid lazy trigger in module "${moduleName}". "lazy.trigger" must be a non-empty string.`, "InvalidLazyTriggerError");
35
+ }
36
+ }
37
+ export class DuplicateLazyTriggerError extends BaseError {
38
+ constructor(trigger, firstModule, secondModule) {
39
+ super(`Duplicate lazy trigger "${trigger}" detected in modules "${firstModule}" and "${secondModule}". Each lazy module must use a unique trigger.`, "DuplicateLazyTriggerError");
40
+ }
41
+ }
42
+ export class LazyModuleExportsNotAllowedError extends BaseError {
43
+ constructor(moduleName) {
44
+ super(`Invalid lazy module "${moduleName}". Lazy modules cannot declare exports.`, "LazyModuleExportsNotAllowedError");
45
+ }
46
+ }
47
+ export class LazyModuleCannotImportLazyModuleError extends BaseError {
48
+ constructor(moduleName, importedModuleName) {
49
+ super(`Invalid lazy module "${moduleName}". It cannot import lazy module "${importedModuleName}". Lazy modules can only import eager modules.`, "LazyModuleCannotImportLazyModuleError");
50
+ }
51
+ }
52
+ export class EagerModuleCannotImportLazyModuleError extends BaseError {
53
+ constructor(moduleName, importedModuleName) {
54
+ super(`Invalid eager module "${moduleName}". It cannot import lazy module "${importedModuleName}". Eager modules must import only eager modules.`, "EagerModuleCannotImportLazyModuleError");
55
+ }
56
+ }
@@ -5,3 +5,4 @@ export type { TProviderToken, TClassProvider, TFactoryProvider, TValueProvider,
5
5
  export type { TWindowFactory, TWindowCreate } from "./window-factory.js";
6
6
  export type { WindowManagerOptions, TWindowManagerWithHandlers, } from "./window-manager.js";
7
7
  export type { TMetadataWindow } from "./window-metadata.js";
8
+ export type { TLazyConfig, TLazyModuleResponse } from "./lazy.js";
@@ -3,5 +3,5 @@ export type TParamOnInit<N = string> = {
3
3
  getWindow: (name?: N) => TWindowFactory;
4
4
  };
5
5
  export type TIpcHandlerInterface = {
6
- onInit: (data: TParamOnInit) => void;
6
+ onInit?: (data: TParamOnInit) => void | Promise<void>;
7
7
  };
@@ -0,0 +1,11 @@
1
+ export type TLazyConfig = {
2
+ enabled: true;
3
+ trigger: string;
4
+ };
5
+ export type TLazyModuleResponse = {
6
+ initialized: boolean;
7
+ name: string;
8
+ error?: {
9
+ message: string;
10
+ };
11
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,10 +1,12 @@
1
1
  import type { TIpcHandlerInterface } from "./ipc-handler.js";
2
2
  import type { Constructor } from "./constructor.js";
3
3
  import type { TProvider, TProviderToken } from "./provider.js";
4
+ import type { TLazyConfig } from "./lazy.js";
4
5
  export type RgModuleMetadata = {
5
6
  imports?: Constructor[];
6
7
  ipc?: (new (...args: any[]) => TIpcHandlerInterface)[];
7
8
  windows?: Constructor[];
8
9
  providers?: TProvider[];
9
10
  exports?: TProviderToken[];
11
+ lazy?: TLazyConfig;
10
12
  };
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export * from "./@core/bootstrap/register-imports.js";
6
6
  export * from "./@core/bootstrap/register-ipc-handlers.js";
7
7
  export * from "./@core/bootstrap/register-providers.js";
8
8
  export * from "./@core/bootstrap/register-windows.js";
9
+ export * from "./@core/bootstrap/register-lazy-module.js";
9
10
  export * from "./@core/bootstrap/settings.js";
10
11
  export * from "./@core/bootstrap/initialize-ipc/handlers.js";
11
12
  export * from "./@core/bootstrap/initialize-ipc/window-creator.js";
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export * from "./@core/bootstrap/register-imports.js";
6
6
  export * from "./@core/bootstrap/register-ipc-handlers.js";
7
7
  export * from "./@core/bootstrap/register-providers.js";
8
8
  export * from "./@core/bootstrap/register-windows.js";
9
+ export * from "./@core/bootstrap/register-lazy-module.js";
9
10
  export * from "./@core/bootstrap/settings.js";
10
11
  export * from "./@core/bootstrap/initialize-ipc/handlers.js";
11
12
  export * from "./@core/bootstrap/initialize-ipc/window-creator.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devisfuture/electron-modular",
3
- "version": "1.1.16",
3
+ "version": "1.2.16",
4
4
  "description": "Core module system, DI container, IPC handlers, and window utilities for Electron main process.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,6 +41,7 @@
41
41
  "devDependencies": {
42
42
  "@types/node": "^20.11.0",
43
43
  "@vitest/coverage-v8": "^2.1.9",
44
+ "@vitest/ui": "^2.1.9",
44
45
  "electron": "^36.4.0",
45
46
  "typescript": "^5.7.2",
46
47
  "vitest": "^2.1.8"