create-lego-one 2.0.12 → 2.0.14
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/dist/index.cjs +150 -15
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/template/.cursor/rules/rules.mdc +639 -0
- package/template/.dockerignore +58 -0
- package/template/.env.example +18 -0
- package/template/.eslintignore +5 -0
- package/template/.eslintrc.js +28 -0
- package/template/.prettierignore +6 -0
- package/template/.prettierrc +11 -0
- package/template/CLAUDE.md +634 -0
- package/template/Dockerfile +67 -0
- package/template/PROMPT.md +457 -0
- package/template/README.md +325 -0
- package/template/docker-compose.yml +48 -0
- package/template/docker-entrypoint.sh +23 -0
- package/template/docs/checkpoints/.template.md +64 -0
- package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
- package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
- package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
- package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
- package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
- package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
- package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
- package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
- package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
- package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
- package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
- package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
- package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
- package/template/docs/framework/plans/00-index.md +164 -0
- package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
- package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
- package/template/docs/framework/plans/03-host-kernel.md +1518 -0
- package/template/docs/framework/plans/04-auth-system.md +1466 -0
- package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
- package/template/docs/framework/plans/06-ui-components.md +1478 -0
- package/template/docs/framework/plans/07-communication-system.md +1106 -0
- package/template/docs/framework/plans/08-plugin-system.md +1179 -0
- package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
- package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
- package/template/docs/framework/plans/11-testing.md +935 -0
- package/template/docs/framework/plans/12-deployment.md +896 -0
- package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
- package/template/docs/framework/research/00-modernjs-audit.md +488 -0
- package/template/docs/framework/research/01-system-blueprint.md +721 -0
- package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
- package/template/docs/framework/research/03-host-setup.md +714 -0
- package/template/docs/framework/research/04-plugin-architecture.md +645 -0
- package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
- package/template/docs/framework/research/06-cli-strategy.md +615 -0
- package/template/docs/framework/research/07-deployment.md +629 -0
- package/template/docs/framework/research/README.md +282 -0
- package/template/docs/framework/setup/00-index.md +210 -0
- package/template/docs/framework/setup/01-framework-structure.md +308 -0
- package/template/docs/framework/setup/02-development-workflow.md +405 -0
- package/template/docs/framework/setup/03-environment-setup.md +215 -0
- package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
- package/template/docs/framework/setup/05-plugin-system.md +620 -0
- package/template/docs/framework/setup/06-communication-patterns.md +451 -0
- package/template/docs/framework/setup/07-plugin-development.md +582 -0
- package/template/docs/framework/setup/08-component-library.md +658 -0
- package/template/docs/framework/setup/09-data-integration.md +609 -0
- package/template/docs/framework/setup/10-auth-rbac.md +497 -0
- package/template/docs/framework/setup/11-hooks-api.md +393 -0
- package/template/docs/framework/setup/12-components-api.md +665 -0
- package/template/docs/framework/setup/13-deployment-guide.md +566 -0
- package/template/docs/framework/setup/README.md +548 -0
- package/template/host/package.json +1 -1
- package/template/nginx.conf +72 -0
- package/template/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-todo/package.json +1 -1
- package/template/pocketbase/CHANGELOG.md +911 -0
- package/template/pocketbase/LICENSE.md +17 -0
- package/template/scripts/create-plugin.js +221 -0
- package/template/scripts/deploy.sh +56 -0
- package/template/tsconfig.base.json +26 -0
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
# Communication System Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For AI Implementing This Plan:** This is document 07 of 13. Complete documents 01-06 first.
|
|
4
|
+
|
|
5
|
+
**Goal:** Implement Garfish channel bus for inter-plugin communication, custom toast notification system using channels as demonstration, and state synchronization between host and plugins.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Use Garfish's built-in channel system for pub/sub messaging. Plugins can publish events and host can subscribe (and vice versa). Toast system demonstrates plugin→host communication pattern.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Garfish channels, React hooks, Zustand state bridge, TypeScript
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- ✅ Completed `01-infrastructure-setup.md`
|
|
16
|
+
- ✅ Completed `02-pocketbase-setup.md`
|
|
17
|
+
- ✅ Completed `03-host-kernel.md` (shared state bridge)
|
|
18
|
+
- ✅ Completed `04-auth-system.md`
|
|
19
|
+
- ✅ Completed `05-multitenancy-rbac.md`
|
|
20
|
+
- ✅ Completed `06-ui-components.md` (toast components)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Task 1: Create Garfish Channel Types and Interfaces
|
|
25
|
+
|
|
26
|
+
**Files:**
|
|
27
|
+
- Create: `host/src/kernel/channels/types.ts`
|
|
28
|
+
- Create: `host/src/kernel/channels/events.ts`
|
|
29
|
+
|
|
30
|
+
### Step 1: Create channel types
|
|
31
|
+
|
|
32
|
+
**File:** `host/src/kernel/channels/types.ts`
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import type { Garfish } from 'garfish';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Channel names for different communication topics
|
|
39
|
+
*/
|
|
40
|
+
export enum ChannelName {
|
|
41
|
+
TOAST = 'lego:toast',
|
|
42
|
+
NAVIGATION = 'lego:navigation',
|
|
43
|
+
STATE_UPDATE = 'lego:state:update',
|
|
44
|
+
PLUGIN_READY = 'lego:plugin:ready',
|
|
45
|
+
PLUGIN_ERROR = 'lego:plugin:error',
|
|
46
|
+
AUTH_CHANGE = 'lego:auth:change',
|
|
47
|
+
ORGANIZATION_CHANGE = 'lego:organization:change',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Base event interface
|
|
52
|
+
*/
|
|
53
|
+
export interface ChannelEvent {
|
|
54
|
+
id: string;
|
|
55
|
+
timestamp: number;
|
|
56
|
+
source?: string; // Plugin name or 'host'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Toast event data
|
|
61
|
+
*/
|
|
62
|
+
export interface ToastEventData extends ChannelEvent {
|
|
63
|
+
type: 'success' | 'error' | 'info' | 'warning';
|
|
64
|
+
title: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
duration?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Navigation event data
|
|
71
|
+
*/
|
|
72
|
+
export interface NavigationEventData extends ChannelEvent {
|
|
73
|
+
path: string;
|
|
74
|
+
replace?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* State update event data
|
|
79
|
+
*/
|
|
80
|
+
export interface StateUpdateEventData extends ChannelEvent {
|
|
81
|
+
key: string;
|
|
82
|
+
value: unknown;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Plugin ready event data
|
|
87
|
+
*/
|
|
88
|
+
export interface PluginReadyEventData extends ChannelEvent {
|
|
89
|
+
pluginName: string;
|
|
90
|
+
version: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Plugin error event data
|
|
95
|
+
*/
|
|
96
|
+
export interface PluginErrorEventData extends ChannelEvent {
|
|
97
|
+
pluginName: string;
|
|
98
|
+
error: string;
|
|
99
|
+
stack?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Auth change event data
|
|
104
|
+
*/
|
|
105
|
+
export interface AuthChangeEventData extends ChannelEvent {
|
|
106
|
+
isAuthenticated: boolean;
|
|
107
|
+
userId?: string;
|
|
108
|
+
organizationId?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Organization change event data
|
|
113
|
+
*/
|
|
114
|
+
export interface OrganizationChangeEventData extends ChannelEvent {
|
|
115
|
+
organizationId: string;
|
|
116
|
+
organizationName: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Channel message payload
|
|
121
|
+
*/
|
|
122
|
+
export type ChannelMessage =
|
|
123
|
+
| { channel: ChannelName.TOAST; data: ToastEventData }
|
|
124
|
+
| { channel: ChannelName.NAVIGATION; data: NavigationEventData }
|
|
125
|
+
| { channel: ChannelName.STATE_UPDATE; data: StateUpdateEventData }
|
|
126
|
+
| { channel: ChannelName.PLUGIN_READY; data: PluginReadyEventData }
|
|
127
|
+
| { channel: ChannelName.PLUGIN_ERROR; data: PluginErrorEventData }
|
|
128
|
+
| { channel: ChannelName.AUTH_CHANGE; data: AuthChangeEventData }
|
|
129
|
+
| { channel: ChannelName.ORGANIZATION_CHANGE; data: OrganizationChangeEventData };
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Channel subscriber callback
|
|
133
|
+
*/
|
|
134
|
+
export type ChannelSubscriber<T = ChannelMessage> = (data: T) => void | Promise<void>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Channel API
|
|
138
|
+
*/
|
|
139
|
+
export interface ChannelAPI {
|
|
140
|
+
publish: <T extends ChannelMessage>(message: T) => void;
|
|
141
|
+
subscribe: <T extends ChannelMessage>(
|
|
142
|
+
channel: ChannelName,
|
|
143
|
+
callback: ChannelSubscriber<T>
|
|
144
|
+
) => () => void; // Returns unsubscribe function
|
|
145
|
+
unsubscribe: (channel: ChannelName, callback: ChannelSubscriber) => void;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Step 2: Create event constants
|
|
150
|
+
|
|
151
|
+
**File:** `host/src/kernel/channels/events.ts`
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
/**
|
|
155
|
+
* Event constants for type-safe event publishing
|
|
156
|
+
*/
|
|
157
|
+
export const Events = {
|
|
158
|
+
TOAST: {
|
|
159
|
+
SUCCESS: (title: string, description?: string) => ({
|
|
160
|
+
type: 'success' as const,
|
|
161
|
+
title,
|
|
162
|
+
description,
|
|
163
|
+
}),
|
|
164
|
+
ERROR: (title: string, description?: string) => ({
|
|
165
|
+
type: 'error' as const,
|
|
166
|
+
title,
|
|
167
|
+
description,
|
|
168
|
+
}),
|
|
169
|
+
INFO: (title: string, description?: string) => ({
|
|
170
|
+
type: 'info' as const,
|
|
171
|
+
title,
|
|
172
|
+
description,
|
|
173
|
+
}),
|
|
174
|
+
WARNING: (title: string, description?: string) => ({
|
|
175
|
+
type: 'warning' as const,
|
|
176
|
+
title,
|
|
177
|
+
description,
|
|
178
|
+
}),
|
|
179
|
+
},
|
|
180
|
+
} as const;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Task 2: Create Channel Bus Service
|
|
186
|
+
|
|
187
|
+
**Files:**
|
|
188
|
+
- Create: `host/src/kernel/channels/ChannelBus.ts`
|
|
189
|
+
|
|
190
|
+
### Step 1: Create channel bus singleton
|
|
191
|
+
|
|
192
|
+
**File:** `host/src/kernel/channels/ChannelBus.ts`
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import Garfish from 'garfish';
|
|
196
|
+
import type {
|
|
197
|
+
ChannelName,
|
|
198
|
+
ChannelMessage,
|
|
199
|
+
ChannelSubscriber,
|
|
200
|
+
ChannelAPI,
|
|
201
|
+
} from './types';
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* ChannelBus - Singleton service for inter-plugin communication using Garfish channels
|
|
205
|
+
*/
|
|
206
|
+
class ChannelBus implements ChannelAPI {
|
|
207
|
+
private garfishInstance: typeof Garfish | null = null;
|
|
208
|
+
private subscribers: Map<ChannelName, Set<ChannelSubscriber>> = new Map();
|
|
209
|
+
private initialized = false;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Initialize the channel bus with Garfish instance
|
|
213
|
+
*/
|
|
214
|
+
initialize(garfish: typeof Garfish): void {
|
|
215
|
+
if (this.initialized) {
|
|
216
|
+
console.warn('[ChannelBus] Already initialized');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.garfishInstance = garfish;
|
|
221
|
+
this.initialized = true;
|
|
222
|
+
|
|
223
|
+
console.log('[ChannelBus] Initialized');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Publish a message to a channel
|
|
228
|
+
*/
|
|
229
|
+
publish<T extends ChannelMessage>(message: T): void {
|
|
230
|
+
if (!this.initialized || !this.garfishInstance) {
|
|
231
|
+
console.warn('[ChannelBus] Not initialized, cannot publish message');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const { channel, data } = message;
|
|
236
|
+
|
|
237
|
+
// Add metadata
|
|
238
|
+
const enrichedData = {
|
|
239
|
+
...data,
|
|
240
|
+
id: data.id || this.generateId(),
|
|
241
|
+
timestamp: data.timestamp || Date.now(),
|
|
242
|
+
source: data.source || 'host',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Publish via Garfish channel
|
|
246
|
+
try {
|
|
247
|
+
this.garfishInstance.channel.emit(channel, enrichedData);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error('[ChannelBus] Failed to publish message:', error);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Also notify local subscribers (in same app context)
|
|
253
|
+
const channelSubscribers = this.subscribers.get(channel);
|
|
254
|
+
if (channelSubscribers) {
|
|
255
|
+
channelSubscribers.forEach((callback) => {
|
|
256
|
+
try {
|
|
257
|
+
callback({ channel, data: enrichedData } as T);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('[ChannelBus] Subscriber callback error:', error);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Subscribe to a channel
|
|
267
|
+
* Returns unsubscribe function
|
|
268
|
+
*/
|
|
269
|
+
subscribe<T extends ChannelMessage>(
|
|
270
|
+
channel: ChannelName,
|
|
271
|
+
callback: ChannelSubscriber<T>
|
|
272
|
+
): () => void {
|
|
273
|
+
if (!this.initialized || !this.garfishInstance) {
|
|
274
|
+
console.warn('[ChannelBus] Not initialized, subscription may not work');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Add to local subscribers
|
|
278
|
+
if (!this.subscribers.has(channel)) {
|
|
279
|
+
this.subscribers.set(channel, new Set());
|
|
280
|
+
}
|
|
281
|
+
this.subscribers.get(channel)!.add(callback as ChannelSubscriber);
|
|
282
|
+
|
|
283
|
+
// Also subscribe to Garfish channel for cross-app communication
|
|
284
|
+
if (this.garfishInstance) {
|
|
285
|
+
try {
|
|
286
|
+
this.garfishInstance.channel.on(channel, callback as any);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error('[ChannelBus] Failed to subscribe to Garfish channel:', error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log(`[ChannelBus] Subscribed to channel: ${channel}`);
|
|
293
|
+
|
|
294
|
+
// Return unsubscribe function
|
|
295
|
+
return () => this.unsubscribe(channel, callback as ChannelSubscriber);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Unsubscribe from a channel
|
|
300
|
+
*/
|
|
301
|
+
unsubscribe(channel: ChannelName, callback: ChannelSubscriber): void {
|
|
302
|
+
// Remove from local subscribers
|
|
303
|
+
const channelSubscribers = this.subscribers.get(channel);
|
|
304
|
+
if (channelSubscribers) {
|
|
305
|
+
channelSubscribers.delete(callback);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Also unsubscribe from Garfish channel
|
|
309
|
+
if (this.garfishInstance) {
|
|
310
|
+
try {
|
|
311
|
+
this.garfishInstance.channel.off(channel, callback as any);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('[ChannelBus] Failed to unsubscribe from Garfish channel:', error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log(`[ChannelBus] Unsubscribed from channel: ${channel}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Unsubscribe all from a channel
|
|
322
|
+
*/
|
|
323
|
+
unsubscribeAll(channel: ChannelName): void {
|
|
324
|
+
const channelSubscribers = this.subscribers.get(channel);
|
|
325
|
+
if (channelSubscribers) {
|
|
326
|
+
channelSubscribers.forEach((callback) => {
|
|
327
|
+
if (this.garfishInstance) {
|
|
328
|
+
this.garfishInstance.channel.off(channel, callback as any);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
channelSubscribers.clear();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Clear all subscribers
|
|
337
|
+
*/
|
|
338
|
+
clear(): void {
|
|
339
|
+
this.subscribers.forEach((_, channel) => {
|
|
340
|
+
this.unsubscribeAll(channel);
|
|
341
|
+
});
|
|
342
|
+
this.subscribers.clear();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Generate unique ID for events
|
|
347
|
+
*/
|
|
348
|
+
private generateId(): string {
|
|
349
|
+
return `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get list of active channels
|
|
354
|
+
*/
|
|
355
|
+
getActiveChannels(): ChannelName[] {
|
|
356
|
+
return Array.from(this.subscribers.keys());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get subscriber count for a channel
|
|
361
|
+
*/
|
|
362
|
+
getSubscriberCount(channel: ChannelName): number {
|
|
363
|
+
return this.subscribers.get(channel)?.size || 0;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Export singleton instance
|
|
368
|
+
export const channelBus = new ChannelBus();
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Initialize channel bus (call from host bootstrap)
|
|
372
|
+
*/
|
|
373
|
+
export function initializeChannelBus(garfish: typeof Garfish): void {
|
|
374
|
+
channelBus.initialize(garfish);
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## Task 3: Create Toast Channel Integration
|
|
381
|
+
|
|
382
|
+
**Files:**
|
|
383
|
+
- Create: `host/src/kernel/channels/integrations/ToastIntegration.tsx`
|
|
384
|
+
|
|
385
|
+
### Step 1: Create toast integration component
|
|
386
|
+
|
|
387
|
+
**File:** `host/src/kernel/channels/integrations/ToastIntegration.tsx`
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { useEffect } from 'react';
|
|
391
|
+
import { toast } from '../../components/ui/use-toast';
|
|
392
|
+
import { channelBus } from '../ChannelBus';
|
|
393
|
+
import { ChannelName, type ToastEventData } from '../types';
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* ToastIntegration - Listens to toast channel events and displays them
|
|
397
|
+
*
|
|
398
|
+
* This component subscribes to the TOAST channel and shows toasts
|
|
399
|
+
* when plugins publish events to it. This demonstrates plugin → host communication.
|
|
400
|
+
*/
|
|
401
|
+
export function ToastIntegration() {
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
// Subscribe to toast channel
|
|
404
|
+
const unsubscribe = channelBus.subscribe<ToastEventData>(
|
|
405
|
+
ChannelName.TOAST,
|
|
406
|
+
({ data }) => {
|
|
407
|
+
const { type, title, description, duration = 5000 } = data;
|
|
408
|
+
|
|
409
|
+
// Map toast type to variant
|
|
410
|
+
const variant = type === 'error' || type === 'warning' ? 'destructive' : 'default';
|
|
411
|
+
|
|
412
|
+
// Show toast
|
|
413
|
+
toast({
|
|
414
|
+
variant,
|
|
415
|
+
title,
|
|
416
|
+
description,
|
|
417
|
+
// Note: duration would need custom implementation
|
|
418
|
+
// The default toast doesn't support duration prop
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
console.log(`[ToastIntegration] Received toast:`, { type, title, description });
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
return () => {
|
|
426
|
+
unsubscribe();
|
|
427
|
+
};
|
|
428
|
+
}, []);
|
|
429
|
+
|
|
430
|
+
// This component doesn't render anything
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Hook to publish toast events (for plugins to use)
|
|
436
|
+
*/
|
|
437
|
+
export function usePublishToast() {
|
|
438
|
+
return (data: Omit<ToastEventData, 'id' | 'timestamp' | 'source'>) => {
|
|
439
|
+
channelBus.publish({
|
|
440
|
+
channel: ChannelName.TOAST,
|
|
441
|
+
data: {
|
|
442
|
+
...data,
|
|
443
|
+
id: `toast_${Date.now()}`,
|
|
444
|
+
timestamp: Date.now(),
|
|
445
|
+
source: 'plugin',
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Task 4: Create Channel Hooks for Plugins
|
|
455
|
+
|
|
456
|
+
**Files:**
|
|
457
|
+
- Create: `host/src/kernel/channels/hooks.ts`
|
|
458
|
+
|
|
459
|
+
### Step 1: Create channel hooks
|
|
460
|
+
|
|
461
|
+
**File:** `host/src/kernel/channels/hooks.ts`
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
import { useEffect, useCallback, useRef } from 'react';
|
|
465
|
+
import { channelBus } from './ChannelBus';
|
|
466
|
+
import { ChannelName, type ChannelMessage, type ChannelSubscriber } from './types';
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Subscribe to a Garfish channel
|
|
470
|
+
*
|
|
471
|
+
* @param channel - The channel name to subscribe to
|
|
472
|
+
* @param callback - The callback function when messages are received
|
|
473
|
+
* @param deps - Dependencies for re-subscription
|
|
474
|
+
*/
|
|
475
|
+
export function useChannel<T extends ChannelMessage>(
|
|
476
|
+
channel: ChannelName,
|
|
477
|
+
callback: ChannelSubscriber<T>,
|
|
478
|
+
deps: React.DependencyList = []
|
|
479
|
+
) {
|
|
480
|
+
const callbackRef = useRef(callback);
|
|
481
|
+
|
|
482
|
+
// Update callback ref without causing re-subscription
|
|
483
|
+
useEffect(() => {
|
|
484
|
+
callbackRef.current = callback;
|
|
485
|
+
}, [callback]);
|
|
486
|
+
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
const unsubscribe = channelBus.subscribe<T>(channel, (data) => {
|
|
489
|
+
callbackRef.current(data);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return () => {
|
|
493
|
+
unsubscribe();
|
|
494
|
+
};
|
|
495
|
+
}, [channel, ...deps]);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Publish to a Garfish channel
|
|
500
|
+
*
|
|
501
|
+
* @returns A function to publish messages
|
|
502
|
+
*/
|
|
503
|
+
export function usePublish() {
|
|
504
|
+
return useCallback(<T extends ChannelMessage>(message: T) => {
|
|
505
|
+
channelBus.publish(message);
|
|
506
|
+
}, []);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Publish toast notifications
|
|
511
|
+
*
|
|
512
|
+
* @returns A function to publish toast messages
|
|
513
|
+
*/
|
|
514
|
+
export function useToastChannel() {
|
|
515
|
+
const publish = usePublish();
|
|
516
|
+
|
|
517
|
+
return useCallback((data: Omit<ChannelMessage & { channel: ChannelName.TOAST }, 'channel'>['data']) => {
|
|
518
|
+
publish({
|
|
519
|
+
channel: ChannelName.TOAST,
|
|
520
|
+
data: {
|
|
521
|
+
...data,
|
|
522
|
+
id: `toast_${Date.now()}`,
|
|
523
|
+
timestamp: Date.now(),
|
|
524
|
+
source: 'plugin',
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
}, [publish]);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Publish navigation events
|
|
532
|
+
*
|
|
533
|
+
* @returns A function to publish navigation messages
|
|
534
|
+
*/
|
|
535
|
+
export function useNavigationChannel() {
|
|
536
|
+
const publish = usePublish();
|
|
537
|
+
|
|
538
|
+
return useCallback((path: string, replace = false) => {
|
|
539
|
+
publish({
|
|
540
|
+
channel: ChannelName.NAVIGATION,
|
|
541
|
+
data: {
|
|
542
|
+
path,
|
|
543
|
+
replace,
|
|
544
|
+
id: `nav_${Date.now()}`,
|
|
545
|
+
timestamp: Date.now(),
|
|
546
|
+
source: 'plugin',
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
}, [publish]);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Publish state updates
|
|
554
|
+
*
|
|
555
|
+
* @returns A function to publish state update messages
|
|
556
|
+
*/
|
|
557
|
+
export function useStateUpdateChannel() {
|
|
558
|
+
const publish = usePublish();
|
|
559
|
+
|
|
560
|
+
return useCallback((key: string, value: unknown) => {
|
|
561
|
+
publish({
|
|
562
|
+
channel: ChannelName.STATE_UPDATE,
|
|
563
|
+
data: {
|
|
564
|
+
key,
|
|
565
|
+
value,
|
|
566
|
+
id: `state_${Date.now()}`,
|
|
567
|
+
timestamp: Date.now(),
|
|
568
|
+
source: 'plugin',
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
}, [publish]);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Publish plugin ready event
|
|
576
|
+
*
|
|
577
|
+
* Call this when your plugin has finished initializing
|
|
578
|
+
*/
|
|
579
|
+
export function usePluginReady(pluginName: string, version: string) {
|
|
580
|
+
const publish = usePublish();
|
|
581
|
+
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
publish({
|
|
584
|
+
channel: ChannelName.PLUGIN_READY,
|
|
585
|
+
data: {
|
|
586
|
+
pluginName,
|
|
587
|
+
version,
|
|
588
|
+
id: `ready_${Date.now()}`,
|
|
589
|
+
timestamp: Date.now(),
|
|
590
|
+
source: pluginName,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}, [pluginName, version, publish]);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Publish plugin error event
|
|
598
|
+
*
|
|
599
|
+
* Call this when your plugin encounters an error
|
|
600
|
+
*/
|
|
601
|
+
export function usePublishPluginError() {
|
|
602
|
+
const publish = usePublish();
|
|
603
|
+
|
|
604
|
+
return useCallback((pluginName: string, error: string, stack?: string) => {
|
|
605
|
+
publish({
|
|
606
|
+
channel: ChannelName.PLUGIN_ERROR,
|
|
607
|
+
data: {
|
|
608
|
+
pluginName,
|
|
609
|
+
error,
|
|
610
|
+
stack,
|
|
611
|
+
id: `error_${Date.now()}`,
|
|
612
|
+
timestamp: Date.now(),
|
|
613
|
+
source: pluginName,
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
}, [publish]);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Listen for auth changes
|
|
621
|
+
*/
|
|
622
|
+
export function useAuthChannel(callback: (data: ChannelMessage & { channel: ChannelName.AUTH_CHANGE })['data']) {
|
|
623
|
+
return useChannel(ChannelName.AUTH_CHANGE, callback, []);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Listen for organization changes
|
|
628
|
+
*/
|
|
629
|
+
export function useOrganizationChannel(callback: (data: ChannelMessage & { channel: ChannelName.ORGANIZATION_CHANGE })['data']) {
|
|
630
|
+
return useChannel(ChannelName.ORGANIZATION_CHANGE, callback, []);
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## Task 5: Create Channel Provider and Integration
|
|
637
|
+
|
|
638
|
+
**Files:**
|
|
639
|
+
- Create: `host/src/kernel/channels/ChannelProvider.tsx`
|
|
640
|
+
- Create: `host/src/kernel/channels/index.ts`
|
|
641
|
+
|
|
642
|
+
### Step 1: Create channel provider component
|
|
643
|
+
|
|
644
|
+
**File:** `host/src/kernel/channels/ChannelProvider.tsx`
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
import { useEffect } from 'react';
|
|
648
|
+
import { ToastIntegration } from './integrations/ToastIntegration';
|
|
649
|
+
import { useGlobalKernelState } from '../shared-state';
|
|
650
|
+
import { useAuth } from '../auth/hooks';
|
|
651
|
+
import { usePublish } from './hooks';
|
|
652
|
+
import { ChannelName } from './types';
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* ChannelProvider - Sets up all channel integrations
|
|
656
|
+
*
|
|
657
|
+
* This component:
|
|
658
|
+
* 1. Renders the ToastIntegration component
|
|
659
|
+
* 2. Publishes auth changes when authentication state changes
|
|
660
|
+
* 3. Publishes organization changes when organization changes
|
|
661
|
+
*/
|
|
662
|
+
export function ChannelProvider() {
|
|
663
|
+
const { user, isAuthenticated } = useAuth();
|
|
664
|
+
const { organization } = useGlobalKernelState();
|
|
665
|
+
const publish = usePublish();
|
|
666
|
+
|
|
667
|
+
// Publish auth changes
|
|
668
|
+
useEffect(() => {
|
|
669
|
+
publish({
|
|
670
|
+
channel: ChannelName.AUTH_CHANGE,
|
|
671
|
+
data: {
|
|
672
|
+
isAuthenticated,
|
|
673
|
+
userId: user?.id,
|
|
674
|
+
organizationId: organization?.id,
|
|
675
|
+
id: `auth_${Date.now()}`,
|
|
676
|
+
timestamp: Date.now(),
|
|
677
|
+
source: 'host',
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
}, [isAuthenticated, user?.id, organization?.id, publish]);
|
|
681
|
+
|
|
682
|
+
// Publish organization changes
|
|
683
|
+
useEffect(() => {
|
|
684
|
+
if (organization) {
|
|
685
|
+
publish({
|
|
686
|
+
channel: ChannelName.ORGANIZATION_CHANGE,
|
|
687
|
+
data: {
|
|
688
|
+
organizationId: organization.id,
|
|
689
|
+
organizationName: organization.name,
|
|
690
|
+
id: `org_${Date.now()}`,
|
|
691
|
+
timestamp: Date.now(),
|
|
692
|
+
source: 'host',
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}, [organization, publish]);
|
|
697
|
+
|
|
698
|
+
return (
|
|
699
|
+
<>
|
|
700
|
+
<ToastIntegration />
|
|
701
|
+
</>
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### Step 2: Create channels barrel export
|
|
707
|
+
|
|
708
|
+
**File:** `host/src/kernel/channels/index.ts`
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
export * from './types';
|
|
712
|
+
export * from './events';
|
|
713
|
+
export * from './ChannelBus';
|
|
714
|
+
export * from './hooks';
|
|
715
|
+
export * from './ChannelProvider';
|
|
716
|
+
export * from './integrations/ToastIntegration';
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## Task 6: Update Bootstrap to Initialize Channel Bus
|
|
722
|
+
|
|
723
|
+
**Files:**
|
|
724
|
+
- Modify: `host/src/bootstrap.tsx`
|
|
725
|
+
- Modify: `host/src/kernel/index.ts`
|
|
726
|
+
|
|
727
|
+
### Step 1: Update bootstrap to initialize channels
|
|
728
|
+
|
|
729
|
+
**File:** `host/src/bootstrap.tsx`
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
import { StrictMode } from 'react';
|
|
733
|
+
import { createRoot } from 'react-dom/client';
|
|
734
|
+
import { BrowserRouter } from '@modern-js/runtime/router';
|
|
735
|
+
import App from './App';
|
|
736
|
+
import { registerSharedState } from './kernel/shared-state';
|
|
737
|
+
import { PocketBaseProvider } from './kernel/providers';
|
|
738
|
+
import { QueryProvider } from './kernel/providers';
|
|
739
|
+
import { ThemeProvider } from './kernel/providers';
|
|
740
|
+
import { Toaster } from './kernel/components/ui/toaster';
|
|
741
|
+
import { ChannelProvider, initializeChannelBus } from './kernel/channels';
|
|
742
|
+
import Garfish from 'garfish';
|
|
743
|
+
|
|
744
|
+
// Register shared state bridge for plugins
|
|
745
|
+
registerSharedState();
|
|
746
|
+
|
|
747
|
+
// Initialize Garfish channel bus
|
|
748
|
+
initializeChannelBus(Garfish);
|
|
749
|
+
|
|
750
|
+
// Create root
|
|
751
|
+
const container = document.getElementById('root');
|
|
752
|
+
if (container) {
|
|
753
|
+
const root = createRoot(container);
|
|
754
|
+
|
|
755
|
+
root.render(
|
|
756
|
+
<StrictMode>
|
|
757
|
+
<BrowserRouter>
|
|
758
|
+
<ThemeProvider>
|
|
759
|
+
<QueryProvider>
|
|
760
|
+
<PocketBaseProvider>
|
|
761
|
+
<ChannelProvider />
|
|
762
|
+
<App />
|
|
763
|
+
<Toaster />
|
|
764
|
+
</PocketBaseProvider>
|
|
765
|
+
</QueryProvider>
|
|
766
|
+
</ThemeProvider>
|
|
767
|
+
</BrowserRouter>
|
|
768
|
+
</StrictMode>
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Step 2: Update kernel exports
|
|
774
|
+
|
|
775
|
+
**File:** `host/src/kernel/index.ts`
|
|
776
|
+
|
|
777
|
+
```typescript
|
|
778
|
+
export * from './shared-state';
|
|
779
|
+
export * from './providers';
|
|
780
|
+
export * from './lib/utils';
|
|
781
|
+
export * from './components';
|
|
782
|
+
export * from './channels';
|
|
783
|
+
export * from './auth';
|
|
784
|
+
export * from './rbac';
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## Task 7: Create Example Plugin Channel Usage
|
|
790
|
+
|
|
791
|
+
**Files:**
|
|
792
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/channels.example.ts`
|
|
793
|
+
|
|
794
|
+
### Step 1: Create example for plugin developers
|
|
795
|
+
|
|
796
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/channels.example.ts`
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
/**
|
|
800
|
+
* Example: How plugins can use the channel system
|
|
801
|
+
*
|
|
802
|
+
* This file demonstrates how a plugin can:
|
|
803
|
+
* 1. Subscribe to host events (auth, organization changes)
|
|
804
|
+
* 2. Publish events to the host (toast notifications, navigation)
|
|
805
|
+
* 3. Communicate with other plugins
|
|
806
|
+
*/
|
|
807
|
+
|
|
808
|
+
import { useEffect } from 'react';
|
|
809
|
+
import {
|
|
810
|
+
useToastChannel,
|
|
811
|
+
useAuthChannel,
|
|
812
|
+
useOrganizationChannel,
|
|
813
|
+
usePluginReady,
|
|
814
|
+
usePublishPluginError,
|
|
815
|
+
} from '@lego/kernel/channels';
|
|
816
|
+
|
|
817
|
+
// Example 1: Show toast notification from plugin
|
|
818
|
+
export function useNotifySuccess() {
|
|
819
|
+
const publishToast = useToastChannel();
|
|
820
|
+
|
|
821
|
+
return (title: string, description?: string) => {
|
|
822
|
+
publishToast({
|
|
823
|
+
type: 'success',
|
|
824
|
+
title,
|
|
825
|
+
description,
|
|
826
|
+
});
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Example 2: Listen for auth changes
|
|
831
|
+
export function useAuthListener() {
|
|
832
|
+
useAuthChannel((data) => {
|
|
833
|
+
console.log('[Plugin] Auth changed:', data);
|
|
834
|
+
|
|
835
|
+
if (data.isAuthenticated) {
|
|
836
|
+
console.log('[Plugin] User logged in:', data.userId);
|
|
837
|
+
// Refresh plugin data when user logs in
|
|
838
|
+
} else {
|
|
839
|
+
console.log('[Plugin] User logged out');
|
|
840
|
+
// Clear plugin data when user logs out
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Example 3: Listen for organization changes
|
|
846
|
+
export function useOrganizationListener() {
|
|
847
|
+
useOrganizationChannel((data) => {
|
|
848
|
+
console.log('[Plugin] Organization changed:', data);
|
|
849
|
+
// Refresh plugin data for new organization
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Example 4: Notify host when plugin is ready
|
|
854
|
+
export function usePluginReadyNotification() {
|
|
855
|
+
usePluginReady('@lego/plugin-todo', '1.0.0');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Example 5: Report errors to host
|
|
859
|
+
export function useErrorReporting() {
|
|
860
|
+
const publishError = usePublishPluginError();
|
|
861
|
+
|
|
862
|
+
return (error: Error) => {
|
|
863
|
+
publishError(
|
|
864
|
+
'@lego/plugin-todo',
|
|
865
|
+
error.message,
|
|
866
|
+
error.stack
|
|
867
|
+
);
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Example 6: Complete integration hook
|
|
872
|
+
export function useChannelIntegration() {
|
|
873
|
+
const notifySuccess = useNotifySuccess();
|
|
874
|
+
const reportError = useErrorReporting();
|
|
875
|
+
|
|
876
|
+
// Notify when plugin initializes
|
|
877
|
+
usePluginReadyNotification();
|
|
878
|
+
|
|
879
|
+
// Listen to auth changes
|
|
880
|
+
useAuthListener();
|
|
881
|
+
|
|
882
|
+
// Listen to organization changes
|
|
883
|
+
useOrganizationListener();
|
|
884
|
+
|
|
885
|
+
return {
|
|
886
|
+
notifySuccess,
|
|
887
|
+
reportError,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
---
|
|
893
|
+
|
|
894
|
+
## Task 8: Create Plugin Channel Hook Helper
|
|
895
|
+
|
|
896
|
+
**Files:**
|
|
897
|
+
- Create: `host/src/kernel/channels/plugin-hooks.ts`
|
|
898
|
+
|
|
899
|
+
### Step 1: Create plugin-specific hooks
|
|
900
|
+
|
|
901
|
+
**File:** `host/src/kernel/channels/plugin-hooks.ts`
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
import { useCallback, useEffect } from 'react';
|
|
905
|
+
import { useNavigate } from '@modern-js/runtime/router';
|
|
906
|
+
import { channelBus } from './ChannelBus';
|
|
907
|
+
import { ChannelName, type ToastEventData } from './types';
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Helper hook for plugins to access kernel channels
|
|
911
|
+
*
|
|
912
|
+
* This hook can be used from any plugin via the window bridge:
|
|
913
|
+
* window.__LEGO_KERNEL_STATE__?.usePluginChannels?.()
|
|
914
|
+
*/
|
|
915
|
+
export function usePluginChannels() {
|
|
916
|
+
const navigate = useNavigate();
|
|
917
|
+
|
|
918
|
+
// Show toast notification
|
|
919
|
+
const toast = useCallback((data: Omit<ToastEventData, 'id' | 'timestamp' | 'source'>) => {
|
|
920
|
+
channelBus.publish({
|
|
921
|
+
channel: ChannelName.TOAST,
|
|
922
|
+
data: {
|
|
923
|
+
...data,
|
|
924
|
+
id: `toast_${Date.now()}`,
|
|
925
|
+
timestamp: Date.now(),
|
|
926
|
+
source: 'plugin',
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
}, []);
|
|
930
|
+
|
|
931
|
+
// Navigate to a path
|
|
932
|
+
const navigateTo = useCallback((path: string, replace = false) => {
|
|
933
|
+
// Use direct navigation instead of publishing to channel
|
|
934
|
+
// This avoids circular dependencies and works more reliably
|
|
935
|
+
navigate(path, { replace });
|
|
936
|
+
}, [navigate]);
|
|
937
|
+
|
|
938
|
+
// Subscribe to auth changes
|
|
939
|
+
const onAuthChange = useCallback((callback: (data: ToastEventData) => void) => {
|
|
940
|
+
return channelBus.subscribe(ChannelName.AUTH_CHANGE, callback);
|
|
941
|
+
}, []);
|
|
942
|
+
|
|
943
|
+
// Subscribe to organization changes
|
|
944
|
+
const onOrganizationChange = useCallback((callback: (data: any) => void) => {
|
|
945
|
+
return channelBus.subscribe(ChannelName.ORGANIZATION_CHANGE, callback);
|
|
946
|
+
}, []);
|
|
947
|
+
|
|
948
|
+
return {
|
|
949
|
+
toast,
|
|
950
|
+
navigateTo,
|
|
951
|
+
onAuthChange,
|
|
952
|
+
onOrganizationChange,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Register plugin channels helper to window bridge
|
|
958
|
+
*/
|
|
959
|
+
export function registerPluginChannels() {
|
|
960
|
+
if (typeof window !== 'undefined') {
|
|
961
|
+
(window as any).__LEGO_PLUGIN_CHANNELS__ = {
|
|
962
|
+
toast: (data: Omit<ToastEventData, 'id' | 'timestamp' | 'source'>) => {
|
|
963
|
+
channelBus.publish({
|
|
964
|
+
channel: ChannelName.TOAST,
|
|
965
|
+
data: {
|
|
966
|
+
...data,
|
|
967
|
+
id: `toast_${Date.now()}`,
|
|
968
|
+
timestamp: Date.now(),
|
|
969
|
+
source: 'plugin',
|
|
970
|
+
},
|
|
971
|
+
});
|
|
972
|
+
},
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
### Step 2: Update bridge to include channels
|
|
979
|
+
|
|
980
|
+
**File:** `host/src/kernel/shared-state/bridge.ts`
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
import { useGlobalKernelState } from './store';
|
|
984
|
+
import { registerPluginChannels } from '../channels/plugin-hooks';
|
|
985
|
+
|
|
986
|
+
// Declare window type for shared state
|
|
987
|
+
declare global {
|
|
988
|
+
interface Window {
|
|
989
|
+
__LEGO_KERNEL_STATE__?: {
|
|
990
|
+
useGlobalKernelState: typeof useGlobalKernelState;
|
|
991
|
+
};
|
|
992
|
+
__LEGO_PLUGIN_CHANNELS__?: {
|
|
993
|
+
toast: (data: any) => void;
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Register shared state to window for plugin access
|
|
999
|
+
export function registerSharedState() {
|
|
1000
|
+
if (typeof window !== 'undefined') {
|
|
1001
|
+
window.__LEGO_KERNEL_STATE__ = {
|
|
1002
|
+
useGlobalKernelState,
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// Also register plugin channels helper
|
|
1006
|
+
registerPluginChannels();
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Hook for plugins to access kernel state
|
|
1011
|
+
export function useKernelState() {
|
|
1012
|
+
return useGlobalKernelState;
|
|
1013
|
+
}
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
---
|
|
1017
|
+
|
|
1018
|
+
## Verification
|
|
1019
|
+
|
|
1020
|
+
### Step 1: Build the host
|
|
1021
|
+
|
|
1022
|
+
**Run:**
|
|
1023
|
+
|
|
1024
|
+
```bash
|
|
1025
|
+
cd host
|
|
1026
|
+
pnpm run build
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
Expected: Build completes without errors.
|
|
1030
|
+
|
|
1031
|
+
### Step 2: Start development server
|
|
1032
|
+
|
|
1033
|
+
**Run:**
|
|
1034
|
+
|
|
1035
|
+
```bash
|
|
1036
|
+
cd host
|
|
1037
|
+
pnpm run dev
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
Expected: Server starts on http://localhost:8080
|
|
1041
|
+
|
|
1042
|
+
### Step 3: Test channel system
|
|
1043
|
+
|
|
1044
|
+
1. Open browser console on host app
|
|
1045
|
+
2. Access channel bus: `window.__LEGO_PLUGIN_CHANNELS__`
|
|
1046
|
+
3. Test toast from console:
|
|
1047
|
+
|
|
1048
|
+
```javascript
|
|
1049
|
+
window.__LEGO_PLUGIN_CHANNELS__.toast({
|
|
1050
|
+
type: 'success',
|
|
1051
|
+
title: 'Test from console',
|
|
1052
|
+
description: 'This toast was triggered from the browser console!'
|
|
1053
|
+
});
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
4. Should see a toast notification appear
|
|
1057
|
+
|
|
1058
|
+
### Step 4: Test auth change events
|
|
1059
|
+
|
|
1060
|
+
1. Login to the app
|
|
1061
|
+
2. Open browser console
|
|
1062
|
+
3. Subscribe to auth channel and verify events are published
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
## Summary
|
|
1067
|
+
|
|
1068
|
+
After completing this document, you will have:
|
|
1069
|
+
|
|
1070
|
+
1. ✅ Garfish channel bus singleton service
|
|
1071
|
+
2. ✅ Type-safe channel system with enum-defined channel names
|
|
1072
|
+
3. ✅ Toast integration demonstrating plugin→host communication
|
|
1073
|
+
4. ✅ Hooks for subscribing to and publishing channel events
|
|
1074
|
+
5. ✅ Channel provider that automatically publishes auth/org changes
|
|
1075
|
+
6. ✅ Plugin-friendly channel helpers via window bridge
|
|
1076
|
+
7. ✅ Example code showing how plugins use channels
|
|
1077
|
+
|
|
1078
|
+
**Next:** `08-plugin-system.md` - Implement complete plugin architecture with slot injection, plugin config, and dynamic loading.
|
|
1079
|
+
|
|
1080
|
+
---
|
|
1081
|
+
|
|
1082
|
+
## Files Created
|
|
1083
|
+
|
|
1084
|
+
```
|
|
1085
|
+
host/
|
|
1086
|
+
└── src/
|
|
1087
|
+
└── kernel/
|
|
1088
|
+
└── channels/
|
|
1089
|
+
├── types.ts
|
|
1090
|
+
├── events.ts
|
|
1091
|
+
├── ChannelBus.ts
|
|
1092
|
+
├── hooks.ts
|
|
1093
|
+
├── plugin-hooks.ts
|
|
1094
|
+
├── ChannelProvider.tsx
|
|
1095
|
+
├── integrations/
|
|
1096
|
+
│ └── ToastIntegration.tsx
|
|
1097
|
+
└── index.ts
|
|
1098
|
+
|
|
1099
|
+
bootstrap.tsx (modified)
|
|
1100
|
+
kernel/shared-state/bridge.ts (modified)
|
|
1101
|
+
kernel/index.ts (modified)
|
|
1102
|
+
|
|
1103
|
+
packages/plugins/@lego/plugin-todo/
|
|
1104
|
+
└── src/
|
|
1105
|
+
└── channels.example.ts (example file)
|
|
1106
|
+
```
|