@canlooks/roost-electron-renderer 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C.CanLiang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,275 @@
1
+ # @canlooks/roost-electron-renderer
2
+
3
+ Electron renderer process plugin for the [Roost](https://github.com/canlooks/roost) micro-service framework. Provides seamless IPC-based remote procedure call (RPC) proxying — call main-process controller actions from the renderer as if they were local methods.
4
+
5
+ ## Overview
6
+
7
+ In an Electron application, business logic typically runs in the main process via Roost controllers decorated with `@Controller` and `@Action`. This package creates transparent **proxy instances** of those controllers in the renderer process. Every `@Action`-decorated method on a proxy is replaced with an `ipcRenderer.invoke()` call, routing arguments through Electron's IPC channel to the main process and returning the result as a promise.
8
+
9
+ The renderer code never touches IPC directly — it simply calls methods on controller instances.
10
+
11
+ ```
12
+ ┌─ Renderer Process ─────────────────────┐
13
+ │ │
14
+ │ const { myCtrl } = │
15
+ │ await createRoostRenderer( │
16
+ │ { MyController }, │
17
+ │ { ipcRenderer } │
18
+ │ ) │
19
+ │ │
20
+ │ // Looks like a local call... │
21
+ │ const result = await myCtrl.doWork(x) │
22
+ │ │ │
23
+ └─────────────────────┼───────────────────┘
24
+ │ ipcRenderer.invoke(channel, path, ...args)
25
+
26
+ ┌─ Main Process ─────────────────────────┐
27
+ │ │
28
+ │ @Controller('api') │
29
+ │ class MyController { │
30
+ │ @Action('doWork') │
31
+ │ doWork(x) { ... } │
32
+ │ } │
33
+ │ │
34
+ └─────────────────────────────────────────┘
35
+ ```
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install @canlooks/roost-electron-renderer
41
+ ```
42
+
43
+ **Peer dependencies:**
44
+
45
+ - [`@canlooks/roost`](https://www.npmjs.com/package/@canlooks/roost) — the core Roost framework
46
+ - [`electron`](https://www.npmjs.com/package/electron) — provides `ipcRenderer`
47
+
48
+ ## API Reference
49
+
50
+ ### `createRoostRenderer(controllers, options)`
51
+
52
+ Creates proxy controller instances whose `@Action` methods are wired to `ipcRenderer.invoke`.
53
+
54
+ ```typescript
55
+ function createRoostRenderer<T extends Record<string, ComponentType>>(
56
+ controllers: T,
57
+ options: CreateRoostRendererOptions
58
+ ): Promise<{ [K in keyof T]: InstanceType<T[K]> }>
59
+ ```
60
+
61
+ #### Parameters
62
+
63
+ | Parameter | Type | Description |
64
+ |-----------|------|-------------|
65
+ | `controllers` | `Record<string, ComponentType>` | Map of controller classes keyed by name. Each class must be decorated with `@Controller` from `@canlooks/roost`. |
66
+ | `options` | `CreateRoostRendererOptions` | Configuration for the renderer proxy. |
67
+
68
+ #### Returns
69
+
70
+ An object with the same keys as the input `controllers` map, where each value is an **instance** of the corresponding controller class with its `@Action` methods rewritten to invoke IPC.
71
+
72
+ #### `CreateRoostRendererOptions`
73
+
74
+ | Property | Type | Required | Default | Description |
75
+ |----------|------|----------|---------|-------------|
76
+ | `ipcRenderer` | `IpcRenderer` | Yes | — | The Electron `ipcRenderer` instance from the renderer process. |
77
+ | `channel` | `string` | No | `'@canlooks/roost-electron'` | Custom IPC channel name. |
78
+
79
+ ### `rewriteActions(instances, options)`
80
+
81
+ > **Internal.** Exported for advanced use cases. Rewrites `@Action` methods on controller instances to proxy through `ipcRenderer.invoke`.
82
+
83
+ ```typescript
84
+ function rewriteActions(
85
+ instances: any[],
86
+ options: CreateRoostRendererOptions
87
+ ): void
88
+ ```
89
+
90
+ ## How It Works
91
+
92
+ ### Action Path Resolution
93
+
94
+ Each `@Action`-decorated method is mapped to an IPC key derived from the controller and action paths:
95
+
96
+ | Controller Decorator | Action Decorator | Resolved IPC Key |
97
+ |----------------------|------------------|------------------|
98
+ | `@Controller('api')` | `@Action('hello')` | `'/api/hello'` |
99
+ | `@Controller('users')` | `@Action('list')` | `'/users/list'` |
100
+ | `@Controller()` | `@Action('status')` | `'/status'` |
101
+
102
+ The resolved key is passed as the second argument to `ipcRenderer.invoke(channel, key, ...args)`.
103
+
104
+ ### Method Rewriting
105
+
106
+ - Only methods decorated with `@Action` are rewritten. Regular methods and properties are left untouched.
107
+ - Rewriting happens **per-instance**, not on the prototype. The original class prototype remains intact.
108
+ - Each call to a rewritten method results in a fresh `ipcRenderer.invoke()` call — no caching or batching.
109
+ - All arguments passed to the method are forwarded as variadic arguments to `ipcRenderer.invoke`.
110
+ - The return value of `ipcRenderer.invoke` (a `Promise`) is returned directly, preserving the original reference.
111
+
112
+ ### Error Propagation
113
+
114
+ Errors thrown by the main process handler (or IPC errors) are propagated as rejected promises:
115
+
116
+ ```typescript
117
+ const { ctrl } = await createRoostRenderer({ TestController }, { ipcRenderer })
118
+
119
+ try {
120
+ await ctrl.hello('World')
121
+ } catch (err) {
122
+ // err is the exact rejection from ipcRenderer.invoke
123
+ }
124
+ ```
125
+
126
+ ## Usage
127
+
128
+ ### Basic Example
129
+
130
+ ```typescript
131
+ // ═══════════════════════════════════════════════════════════════════
132
+ // Shared controller definition (e.g., in a shared package)
133
+ // ═══════════════════════════════════════════════════════════════════
134
+
135
+ import { Controller, Action } from '@canlooks/roost'
136
+
137
+ @Controller('api')
138
+ export class ApiController {
139
+ @Action('greet')
140
+ greet(name: string): string {
141
+ return `Hello, ${name}!`
142
+ }
143
+
144
+ @Action('add')
145
+ add(a: number, b: number): number {
146
+ return a + b
147
+ }
148
+
149
+ // Regular methods are NOT proxied
150
+ getVersion(): string {
151
+ return '1.0.0'
152
+ }
153
+ }
154
+ ```
155
+
156
+ ```typescript
157
+ // ═══════════════════════════════════════════════════════════════════
158
+ // Main process — sets up IPC handler with Roost
159
+ // ═══════════════════════════════════════════════════════════════════
160
+
161
+ import { app, BrowserWindow, ipcMain } from 'electron'
162
+ import { Roost } from '@canlooks/roost'
163
+
164
+ app.whenReady().then(async () => {
165
+ const roost = await Roost.create({
166
+ named: { ApiController }
167
+ })
168
+
169
+ // Handle incoming IPC calls
170
+ ipcMain.handle('@canlooks/roost-electron', async (_event, path, ...args) => {
171
+ const results = await roost.invoke(path, ...args)
172
+ return results[0] // Return the first result for single-action calls
173
+ })
174
+
175
+ // ... create BrowserWindow, load renderer
176
+ })
177
+ ```
178
+
179
+ ```typescript
180
+ // ═══════════════════════════════════════════════════════════════════
181
+ // Renderer process — creates proxy and calls methods transparently
182
+ // ═══════════════════════════════════════════════════════════════════
183
+
184
+ import { createRoostRenderer } from '@canlooks/roost-electron-renderer'
185
+ import { ipcRenderer } from 'electron'
186
+
187
+ async function main() {
188
+ const { ApiController: api } = await createRoostRenderer(
189
+ { ApiController },
190
+ { ipcRenderer }
191
+ )
192
+
193
+ // These look like local calls but go through IPC to the main process:
194
+ const greeting = await api.greet('World') // → "Hello, World!"
195
+ const sum = await api.add(3, 4) // → 7
196
+
197
+ // Non-action methods are NOT proxied — they run locally:
198
+ const version = api.getVersion() // → "1.0.0" (local call)
199
+ }
200
+ ```
201
+
202
+ ### Multiple Controllers
203
+
204
+ ```typescript
205
+ const { UserController, OrderController } = await createRoostRenderer(
206
+ { UserController, OrderController },
207
+ { ipcRenderer }
208
+ )
209
+
210
+ // Each controller's @Action methods are independently proxied
211
+ const users = await UserController.list()
212
+ const order = await OrderController.findById(42)
213
+ ```
214
+
215
+ ### Custom IPC Channel
216
+
217
+ ```typescript
218
+ const { ApiController: api } = await createRoostRenderer(
219
+ { ApiController },
220
+ {
221
+ ipcRenderer,
222
+ channel: 'my-custom-channel' // Must match main process ipcMain.handle()
223
+ }
224
+ )
225
+ ```
226
+
227
+ ### Controllers Without Actions
228
+
229
+ Controllers with no `@Action` methods are handled gracefully — the instance is returned as-is with no method rewriting:
230
+
231
+ ```typescript
232
+ @Controller('config')
233
+ class ConfigController {
234
+ theme = 'dark'
235
+ setTheme(t: string) { this.theme = t }
236
+ }
237
+
238
+ const { ConfigController: config } = await createRoostRenderer(
239
+ { ConfigController },
240
+ { ipcRenderer }
241
+ )
242
+
243
+ console.log(config.theme) // 'dark' — normal property access
244
+ ```
245
+
246
+ ## TypeScript Support
247
+
248
+ The package is written in TypeScript and ships with full type declarations. The return type of `createRoostRenderer` is **fully inferred** from the input controller map — each property is correctly typed as an instance of the corresponding class.
249
+
250
+ ```typescript
251
+ const renderers = await createRoostRenderer(
252
+ { ApiController, UserController },
253
+ { ipcRenderer }
254
+ )
255
+
256
+ // TypeScript knows these types:
257
+ renderers.ApiController.greet(name: string): Promise<string>
258
+ renderers.UserController.list(): Promise<string[]>
259
+ ```
260
+
261
+ The `ComponentType` constraint ensures only class constructors (not plain objects or primitives) can be passed as controllers.
262
+
263
+ ## Main Process Integration
264
+
265
+ This package handles the **renderer side** of the IPC bridge. The main process must:
266
+
267
+ 1. Create a Roost app with the same controllers.
268
+ 2. Register an `ipcMain.handle()` listener on the same channel.
269
+ 3. Call `roost.invoke(path, ...args)` and return the result.
270
+
271
+ See the [Roost framework documentation](https://github.com/canlooks/roost) for details on main-process setup, including the `@canlooks/roost-electron` package which provides main-process IPC handling out of the box.
272
+
273
+ ## License
274
+
275
+ MIT © [C.CanLiang](https://github.com/canlooks)
@@ -0,0 +1,7 @@
1
+ import type { IpcRenderer } from 'electron';
2
+ import { ComponentType } from '@canlooks/roost';
3
+ export type CreateRoostRendererOptions = {
4
+ channel?: string;
5
+ ipcRenderer: IpcRenderer;
6
+ };
7
+ export declare function createRoostRenderer<T extends Record<string, ComponentType>>(controllers: T, options: CreateRoostRendererOptions): Promise<{ [K in keyof T]: InstanceType<T[K]>; }>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRoostRenderer = createRoostRenderer;
4
+ const roost_1 = require("@canlooks/roost");
5
+ const rewriteAction_1 = require("./rewriteAction");
6
+ async function createRoostRenderer(controllers, options) {
7
+ const renderers = {};
8
+ const app = await roost_1.Roost.create({
9
+ named: controllers,
10
+ readOnly: true
11
+ });
12
+ const instances = await Promise.all(Object.keys(controllers).map(async (name) => {
13
+ return renderers[name] = await app.container.get(name);
14
+ }));
15
+ (0, rewriteAction_1.rewriteActions)(instances, options);
16
+ return renderers;
17
+ }
@@ -0,0 +1 @@
1
+ export * from './createRenderer';
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./createRenderer"), exports);
@@ -0,0 +1,2 @@
1
+ import { CreateRoostRendererOptions } from './createRenderer';
2
+ export declare function rewriteActions(instances: any[], options: CreateRoostRendererOptions): void;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rewriteActions = rewriteActions;
4
+ const roost_1 = require("@canlooks/roost");
5
+ function rewriteActions(instances, options) {
6
+ instances.forEach(instance => {
7
+ const completeKeyMap = (0, roost_1.getActionsKey)(instance);
8
+ if (completeKeyMap) {
9
+ for (const [property, { path, pattern }] of completeKeyMap) {
10
+ const key = path || pattern;
11
+ if (key) {
12
+ instance[property] = (...args) => {
13
+ return options.ipcRenderer.invoke(options.channel || '@canlooks/roost-electron', key, ...args);
14
+ };
15
+ }
16
+ }
17
+ }
18
+ });
19
+ }
@@ -0,0 +1,7 @@
1
+ import type { IpcRenderer } from 'electron';
2
+ import { ComponentType } from '@canlooks/roost';
3
+ export type CreateRoostRendererOptions = {
4
+ channel?: string;
5
+ ipcRenderer: IpcRenderer;
6
+ };
7
+ export declare function createRoostRenderer<T extends Record<string, ComponentType>>(controllers: T, options: CreateRoostRendererOptions): Promise<{ [K in keyof T]: InstanceType<T[K]>; }>;
@@ -0,0 +1,14 @@
1
+ import { Roost } from '@canlooks/roost';
2
+ import { rewriteActions } from './rewriteAction.js';
3
+ export async function createRoostRenderer(controllers, options) {
4
+ const renderers = {};
5
+ const app = await Roost.create({
6
+ named: controllers,
7
+ readOnly: true
8
+ });
9
+ const instances = await Promise.all(Object.keys(controllers).map(async (name) => {
10
+ return renderers[name] = await app.container.get(name);
11
+ }));
12
+ rewriteActions(instances, options);
13
+ return renderers;
14
+ }
@@ -0,0 +1 @@
1
+ export * from './createRenderer.js';
@@ -0,0 +1 @@
1
+ export * from './createRenderer.js';
@@ -0,0 +1,2 @@
1
+ import { CreateRoostRendererOptions } from './createRenderer.js';
2
+ export declare function rewriteActions(instances: any[], options: CreateRoostRendererOptions): void;
@@ -0,0 +1,16 @@
1
+ import { getActionsKey } from '@canlooks/roost';
2
+ export function rewriteActions(instances, options) {
3
+ instances.forEach(instance => {
4
+ const completeKeyMap = getActionsKey(instance);
5
+ if (completeKeyMap) {
6
+ for (const [property, { path, pattern }] of completeKeyMap) {
7
+ const key = path || pattern;
8
+ if (key) {
9
+ instance[property] = (...args) => {
10
+ return options.ipcRenderer.invoke(options.channel || '@canlooks/roost-electron', key, ...args);
11
+ };
12
+ }
13
+ }
14
+ }
15
+ });
16
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@canlooks/roost-electron-renderer",
3
+ "version": "0.0.1",
4
+ "author": "C.CanLiang <canlooks@gmail.com>",
5
+ "description": "A backend micro service framework",
6
+ "keywords": [
7
+ "micro service"
8
+ ],
9
+ "main": "dist/cjs/index.js",
10
+ "module": "dist/esm/index.js",
11
+ "types": "dist/esm/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/esm/index.d.ts",
15
+ "import": "./dist/esm/index.js",
16
+ "require": "./dist/cjs/index.js"
17
+ }
18
+ },
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "registry": "https://registry.npmjs.org/"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/canlooks/roost"
26
+ },
27
+ "homepage": "https://github.com/canlooks/roost",
28
+ "bugs": {
29
+ "url": "https://github.com/canlooks/roost/issues",
30
+ "email": "canlooks@gmail.com"
31
+ },
32
+ "license": "MIT",
33
+ "scripts": {
34
+ "clean": "npx shx rm -rf dist",
35
+ "build": "tsc -m esnext --outDir dist/esm & tsc -m commonjs --outDir dist/cjs",
36
+ "build:alias": "tsc-alias --outDir dist/esm",
37
+ "rebuild": "npm run clean && npm run build && npm run build:alias",
38
+ "test": "vitest run"
39
+ },
40
+ "dependencies": {
41
+ "@canlooks/roost": "^0.0.1",
42
+ "tslib": "^2.8.1"
43
+ },
44
+ "devDependencies": {
45
+ "@types/express": "^5.0.6",
46
+ "@types/node": "^25.9.1",
47
+ "@types/react": "^19.2.17",
48
+ "@types/react-dom": "^19.2.3",
49
+ "electron": "^42.3.0",
50
+ "react": "^19.2.7",
51
+ "react-dom": "^19.2.7",
52
+ "tsc-alias": "^1.8.17",
53
+ "typescript": "^6.0.3",
54
+ "vite": "^8.0.16",
55
+ "vitest": "^4.1.7"
56
+ }
57
+ }