Skip to content

Commit e6f950d

Browse files
Copilothotlong
andcommitted
feat: add AppMetadataPlugin interface, install/uninstall methods, and example plugins
- Define AppMetadataPlugin and PluginContext interfaces in @object-ui/types - Add install() and uninstall() convenience methods to PluginSystem - Create TodoPlugin (examples/todo/plugin.ts) and KitchenSinkPlugin (examples/kitchen-sink/plugin.ts) - Add stop() and getConfig() to CRMPlugin for full lifecycle support - Update package.json exports for todo and kitchen-sink examples - Refactor root objectstack.config.ts to use plugin-based config collection - Add 5 new tests for install/uninstall (18 total, all passing) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 190c60e commit e6f950d

File tree

10 files changed

+366
-46
lines changed

10 files changed

+366
-46
lines changed

examples/crm/plugin.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ export class CRMPlugin {
5353
logger.info('[CRM] Config is available via crmConfig export for manual merging');
5454
}
5555
}
56+
57+
async stop() {
58+
// Teardown: no persistent resources to release
59+
}
60+
61+
/** Raw stack configuration for legacy/manual merging */
62+
getConfig() {
63+
return config;
64+
}
5665
}
5766

5867
export default CRMPlugin;

examples/kitchen-sink/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"types": "src/index.ts",
88
"exports": {
99
".": "./src/index.ts",
10-
"./objectstack.config": "./objectstack.config.ts"
10+
"./objectstack.config": "./objectstack.config.ts",
11+
"./plugin": "./plugin.ts"
1112
},
1213
"scripts": {
1314
"serve": "objectstack serve objectstack.config.ts",

examples/kitchen-sink/plugin.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Kitchen Sink Example Plugin
3+
*
4+
* Exports the Kitchen Sink configuration as an ObjectStack plugin that can be
5+
* loaded by any ObjectStack application. Other projects can import
6+
* the Kitchen Sink metadata (objects, apps, dashboards, manifest) without
7+
* needing to know the internal structure.
8+
*
9+
* Usage in another project:
10+
*
11+
* import { KitchenSinkPlugin } from '@object-ui/example-kitchen-sink/plugin';
12+
*
13+
* const kernel = new ObjectKernel();
14+
* kernel.use(new KitchenSinkPlugin());
15+
*
16+
* Or import the raw config for merging:
17+
*
18+
* import { kitchenSinkConfig } from '@object-ui/example-kitchen-sink/plugin';
19+
*/
20+
21+
import config from './objectstack.config';
22+
23+
/** Raw Kitchen Sink stack configuration for direct merging */
24+
export const kitchenSinkConfig = config;
25+
26+
/**
27+
* Kitchen Sink Plugin — wraps the Kitchen Sink metadata as a kernel-compatible plugin.
28+
*
29+
* When loaded via `kernel.use(new KitchenSinkPlugin())`, ObjectStack's AppPlugin
30+
* will register all Kitchen Sink objects, apps, dashboards, and seed data.
31+
*/
32+
export class KitchenSinkPlugin {
33+
readonly name = '@object-ui/example-kitchen-sink';
34+
readonly version = '1.0.0';
35+
readonly type = 'app-metadata' as const;
36+
readonly description = 'Kitchen Sink showcase metadata (all field types, views, and UI capabilities)';
37+
38+
async init() {
39+
// No initialization needed
40+
}
41+
42+
async start(ctx: any) {
43+
const logger = ctx.logger || console;
44+
45+
try {
46+
// Dynamically import AppPlugin to keep plugin.ts dependency-light
47+
const { AppPlugin } = await import('@objectstack/runtime');
48+
const appPlugin = new AppPlugin(config);
49+
await ctx.kernel?.use?.(appPlugin);
50+
logger.info('[KitchenSink] Metadata loaded: objects, apps, dashboards, seed data');
51+
} catch (e: any) {
52+
logger.warn(`[KitchenSink] Could not auto-register via AppPlugin: ${e.message}`);
53+
logger.info('[KitchenSink] Config is available via kitchenSinkConfig export for manual merging');
54+
}
55+
}
56+
57+
async stop() {
58+
// Teardown: no persistent resources to release
59+
}
60+
61+
/** Raw stack configuration for legacy/manual merging */
62+
getConfig() {
63+
return config;
64+
}
65+
}
66+
67+
export default KitchenSinkPlugin;

examples/todo/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"private": true,
66
"type": "module",
77
"exports": {
8-
"./objectstack.config": "./objectstack.config.ts"
8+
"./objectstack.config": "./objectstack.config.ts",
9+
"./plugin": "./plugin.ts"
910
},
1011
"scripts": {
1112
"serve": "objectstack serve objectstack.config.ts",

examples/todo/plugin.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Todo Example Plugin
3+
*
4+
* Exports the Todo configuration as an ObjectStack plugin that can be
5+
* loaded by any ObjectStack application. Other projects can import
6+
* the Todo metadata (objects, apps, dashboards, manifest) without
7+
* needing to know the internal structure.
8+
*
9+
* Usage in another project:
10+
*
11+
* import { TodoPlugin } from '@object-ui/example-todo/plugin';
12+
*
13+
* const kernel = new ObjectKernel();
14+
* kernel.use(new TodoPlugin());
15+
*
16+
* Or import the raw config for merging:
17+
*
18+
* import { todoConfig } from '@object-ui/example-todo/plugin';
19+
*/
20+
21+
import config from './objectstack.config';
22+
23+
/** Raw Todo stack configuration for direct merging */
24+
export const todoConfig = config;
25+
26+
/**
27+
* Todo Plugin — wraps the Todo metadata as a kernel-compatible plugin.
28+
*
29+
* When loaded via `kernel.use(new TodoPlugin())`, ObjectStack's AppPlugin
30+
* will register all Todo objects, apps, dashboards, and seed data.
31+
*/
32+
export class TodoPlugin {
33+
readonly name = '@object-ui/example-todo';
34+
readonly version = '1.0.0';
35+
readonly type = 'app-metadata' as const;
36+
readonly description = 'Task management metadata (objects, apps, dashboards, seed data)';
37+
38+
async init() {
39+
// No initialization needed
40+
}
41+
42+
async start(ctx: any) {
43+
const logger = ctx.logger || console;
44+
45+
try {
46+
// Dynamically import AppPlugin to keep plugin.ts dependency-light
47+
const { AppPlugin } = await import('@objectstack/runtime');
48+
const appPlugin = new AppPlugin(config);
49+
await ctx.kernel?.use?.(appPlugin);
50+
logger.info('[Todo] Metadata loaded: objects, apps, dashboards, seed data');
51+
} catch (e: any) {
52+
logger.warn(`[Todo] Could not auto-register via AppPlugin: ${e.message}`);
53+
logger.info('[Todo] Config is available via todoConfig export for manual merging');
54+
}
55+
}
56+
57+
async stop() {
58+
// Teardown: no persistent resources to release
59+
}
60+
61+
/** Raw stack configuration for legacy/manual merging */
62+
getConfig() {
63+
return config;
64+
}
65+
}
66+
67+
export default TodoPlugin;

objectstack.config.ts

Lines changed: 24 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,33 @@
1212
* because CRM and Kitchen Sink both define an `account` object, which would
1313
* trigger an ownership conflict in the ObjectQL Schema Registry.
1414
*
15+
* Plugins: Each example app exports a plugin class (CRMPlugin, TodoPlugin,
16+
* KitchenSinkPlugin) that implements the AppMetadataPlugin interface.
17+
* For standalone use, each plugin can be loaded independently via
18+
* `kernel.use(new CRMPlugin())`. In the dev workspace, we collect their
19+
* configs via `getConfig()` and merge them into a single AppPlugin.
1520
*/
1621
import { defineStack } from '@objectstack/spec';
1722
import { AppPlugin, DriverPlugin } from '@objectstack/runtime';
1823
import { ObjectQLPlugin } from '@objectstack/objectql';
1924
import { InMemoryDriver } from '@objectstack/driver-memory';
2025
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
2126
import { ConsolePlugin } from '@object-ui/console';
22-
import CrmConfig from './examples/crm/objectstack.config';
23-
import TodoConfig from './examples/todo/objectstack.config';
24-
import KitchenSinkConfig from './examples/kitchen-sink/objectstack.config';
27+
import { CRMPlugin } from './examples/crm/plugin';
28+
import { TodoPlugin } from './examples/todo/plugin';
29+
import { KitchenSinkPlugin } from './examples/kitchen-sink/plugin';
2530

26-
const crm = (CrmConfig as any).default || CrmConfig;
27-
const todo = (TodoConfig as any).default || TodoConfig;
28-
const kitchenSink = (KitchenSinkConfig as any).default || KitchenSinkConfig;
31+
// Instantiate example plugins
32+
const plugins = [new CRMPlugin(), new TodoPlugin(), new KitchenSinkPlugin()];
2933

30-
// Base objects from built-in examples
31-
const baseObjects = [
32-
...(crm.objects || []),
33-
...(todo.objects || []),
34-
...(kitchenSink.objects || []),
35-
];
34+
// Collect raw configs from each plugin via getConfig()
35+
const allConfigs = plugins.map((p) => {
36+
const raw = p.getConfig();
37+
return (raw as any).default || raw;
38+
});
3639

37-
// Collect all example configs for view merging
38-
const allConfigs = [crm, todo, kitchenSink];
40+
// Base objects from all plugins
41+
const baseObjects = allConfigs.flatMap((cfg: any) => cfg.objects || []);
3942

4043
// ---------------------------------------------------------------------------
4144
// Merge stack-level views into object definitions.
@@ -110,44 +113,22 @@ function mergeActionsIntoObjects(objects: any[], configs: any[]): any[] {
110113
});
111114
}
112115

113-
// Merge all example configs into a single app bundle for AppPlugin
116+
// Merge all plugin configs into a single app bundle for AppPlugin
114117
const mergedApp = defineStack({
115118
manifest: {
116119
id: 'dev-workspace',
117120
name: 'dev_workspace',
118121
version: '0.0.0',
119122
description: 'ObjectUI monorepo development workspace',
120123
type: 'app',
121-
data: [
122-
...(crm.manifest?.data || []),
123-
...(todo.manifest?.data || []),
124-
...(kitchenSink.manifest?.data || []),
125-
],
124+
data: allConfigs.flatMap((cfg: any) => cfg.manifest?.data || []),
126125
},
127126
objects: baseObjects,
128-
views: [
129-
...(crm.views || []),
130-
...(todo.views || []),
131-
...(kitchenSink.views || []),
132-
],
133-
apps: [
134-
...(crm.apps || []),
135-
...(todo.apps || []),
136-
...(kitchenSink.apps || []),
137-
],
138-
dashboards: [
139-
...(crm.dashboards || []),
140-
...(todo.dashboards || []),
141-
...(kitchenSink.dashboards || []),
142-
],
143-
reports: [
144-
...(crm.reports || []),
145-
],
146-
pages: [
147-
...(crm.pages || []),
148-
...(todo.pages || []),
149-
...(kitchenSink.pages || []),
150-
],
127+
views: allConfigs.flatMap((cfg: any) => cfg.views || []),
128+
apps: allConfigs.flatMap((cfg: any) => cfg.apps || []),
129+
dashboards: allConfigs.flatMap((cfg: any) => cfg.dashboards || []),
130+
reports: allConfigs.flatMap((cfg: any) => cfg.reports || []),
131+
pages: allConfigs.flatMap((cfg: any) => cfg.pages || []),
151132
} as any);
152133

153134
// Re-merge listViews and actions that defineStack stripped from objects

packages/core/src/registry/PluginSystem.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import type { Registry } from './Registry.js';
10-
import type { PluginScope, PluginScopeConfig } from '@object-ui/types';
10+
import type { PluginScope, PluginScopeConfig, AppMetadataPlugin } from '@object-ui/types';
1111
import { PluginScopeImpl } from './PluginScopeImpl.js';
1212

1313
export interface PluginDefinition {
@@ -158,4 +158,49 @@ export class PluginSystem {
158158
getAllPlugins(): PluginDefinition[] {
159159
return Array.from(this.plugins.values());
160160
}
161+
162+
/**
163+
* Install an AppMetadataPlugin at runtime.
164+
*
165+
* Wraps the plugin as a PluginDefinition, calls its `init()` and `start()`
166+
* lifecycle hooks, and loads it into the system.
167+
*
168+
* @param plugin - An AppMetadataPlugin instance
169+
* @param registry - The component registry
170+
* @param ctx - Optional context passed to the plugin's start() hook
171+
*/
172+
async install(plugin: AppMetadataPlugin, registry: Registry, ctx?: Record<string, any>): Promise<void> {
173+
if (this.loaded.has(plugin.name)) {
174+
console.warn(`Plugin "${plugin.name}" is already installed. Skipping.`);
175+
return;
176+
}
177+
178+
await plugin.init();
179+
180+
const definition: PluginDefinition = {
181+
name: plugin.name,
182+
version: plugin.version,
183+
register: () => {},
184+
onLoad: async () => {
185+
await plugin.start(ctx ?? { logger: console });
186+
},
187+
onUnload: async () => {
188+
await plugin.stop();
189+
},
190+
};
191+
192+
await this.loadPlugin(definition, registry);
193+
}
194+
195+
/**
196+
* Uninstall an AppMetadataPlugin at runtime.
197+
*
198+
* Calls the plugin's `stop()` lifecycle hook (via onUnload) and
199+
* removes it from the system.
200+
*
201+
* @param pluginName - Name of the plugin to uninstall
202+
*/
203+
async uninstall(pluginName: string): Promise<void> {
204+
await this.unloadPlugin(pluginName);
205+
}
161206
}

0 commit comments

Comments
 (0)