@objectstack/plugin-hono-server 4.0.3 → 4.0.5

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.
@@ -1,328 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { Plugin, PluginContext, IHttpServer } from '@objectstack/core';
4
- import {
5
- RestServerConfig,
6
- } from '@objectstack/spec/api';
7
- import { HonoHttpServer } from './adapter';
8
- import { serveStatic } from '@hono/node-server/serve-static';
9
- import * as fs from 'fs';
10
- import * as path from 'path';
11
-
12
- export interface StaticMount {
13
- root: string;
14
- path?: string;
15
- rewrite?: boolean;
16
- spa?: boolean;
17
- }
18
-
19
- export interface HonoPluginOptions {
20
- port?: number;
21
- staticRoot?: string;
22
- /**
23
- * Multiple static resource mounts
24
- */
25
- staticMounts?: StaticMount[];
26
- /**
27
- * REST server configuration
28
- * Controls automatic endpoint generation and API behavior
29
- */
30
- restConfig?: RestServerConfig;
31
- /**
32
- * Whether to register standard ObjectStack CRUD endpoints
33
- * @default true
34
- */
35
- registerStandardEndpoints?: boolean;
36
- /**
37
- * Whether to load endpoints from API Registry
38
- * @default true
39
- */
40
- useApiRegistry?: boolean;
41
-
42
- /**
43
- * Whether to enable SPA fallback
44
- * If true, returns index.html for non-API 404s
45
- * @default false
46
- */
47
- spaFallback?: boolean;
48
- }
49
-
50
- /**
51
- * Hono Server Plugin
52
- *
53
- * Provides HTTP server capabilities using Hono framework.
54
- * Registers the IHttpServer service so other plugins can register routes.
55
- *
56
- * Route registration is handled by plugins:
57
- * - `@objectstack/rest` → CRUD, metadata, discovery, UI, batch
58
- * - `createDispatcherPlugin()` → auth, graphql, analytics, packages, etc.
59
- */
60
- export class HonoServerPlugin implements Plugin {
61
- name = 'com.objectstack.server.hono';
62
- type = 'server';
63
- version = '0.9.0';
64
-
65
- // Constants
66
- private static readonly DEFAULT_ENDPOINT_PRIORITY = 100;
67
- private static readonly CORE_ENDPOINT_PRIORITY = 950;
68
- private static readonly DISCOVERY_ENDPOINT_PRIORITY = 900;
69
-
70
- private options: HonoPluginOptions;
71
- private server: HonoHttpServer;
72
-
73
- constructor(options: HonoPluginOptions = {}) {
74
- this.options = {
75
- port: 3000,
76
- registerStandardEndpoints: true,
77
- useApiRegistry: true,
78
- spaFallback: false,
79
- ...options
80
- };
81
- // We handle static root manually in start() to support SPA fallback
82
- this.server = new HonoHttpServer(this.options.port);
83
- }
84
-
85
- /**
86
- * Init phase - Setup HTTP server and register as service
87
- */
88
- init = async (ctx: PluginContext) => {
89
- ctx.logger.debug('Initializing Hono server plugin', {
90
- port: this.options.port,
91
- staticRoot: this.options.staticRoot
92
- });
93
-
94
- // Register HTTP server service as IHttpServer
95
- // Register as 'http.server' to match core requirements
96
- ctx.registerService('http.server', this.server);
97
- // Alias 'http-server' for backward compatibility
98
- ctx.registerService('http-server', this.server);
99
- ctx.logger.debug('HTTP server service registered', { serviceName: 'http.server' });
100
- }
101
-
102
- /**
103
- * Start phase - Configure static files and start listening
104
- */
105
- start = async (ctx: PluginContext) => {
106
- ctx.logger.debug('Starting Hono server plugin');
107
-
108
- // Configure Static Files & SPA Fallback
109
- const mounts: StaticMount[] = this.options.staticMounts || [];
110
-
111
- // Auto-discover UI Plugins
112
- try {
113
- const rawKernel = ctx.getKernel() as any;
114
- if (rawKernel.plugins) {
115
- const loadedPlugins = rawKernel.plugins instanceof Map
116
- ? Array.from(rawKernel.plugins.values())
117
- : Array.isArray(rawKernel.plugins) ? rawKernel.plugins : Object.values(rawKernel.plugins);
118
-
119
- for (const plugin of (loadedPlugins as any[])) {
120
- // Check for UI Plugin signature
121
- // Support legacy 'ui-plugin' and new 'ui' type
122
- if ((plugin.type === 'ui' || plugin.type === 'ui-plugin') && plugin.staticPath) {
123
- // Derive base route from name: @org/console -> console
124
- const slug = plugin.slug || plugin.name.split('/').pop();
125
- const baseRoute = `/${slug}`;
126
-
127
- ctx.logger.debug(`Auto-mounting UI Plugin: ${plugin.name}`, {
128
- path: baseRoute,
129
- root: plugin.staticPath
130
- });
131
-
132
- mounts.push({
133
- root: plugin.staticPath,
134
- path: baseRoute,
135
- rewrite: true, // Strip prefix: /console/assets/x -> /assets/x
136
- spa: true
137
- });
138
-
139
- // Handle Default Plugin Redirect
140
- if (plugin.default || plugin.isDefault) {
141
- const rawApp = this.server.getRawApp();
142
- rawApp.get('/', (c) => c.redirect(baseRoute));
143
- ctx.logger.debug(`Set default UI redirect: / -> ${baseRoute}`);
144
- }
145
- }
146
- }
147
- }
148
- } catch (err: any) {
149
- ctx.logger.warn('Failed to auto-discover UI plugins', { error: err.message || err });
150
- }
151
-
152
- // Backward compatibility for staticRoot
153
- if (this.options.staticRoot) {
154
- mounts.push({
155
- root: this.options.staticRoot,
156
- path: '/',
157
- rewrite: false,
158
- spa: this.options.spaFallback
159
- });
160
- }
161
-
162
- if (mounts.length > 0) {
163
- const rawApp = this.server.getRawApp();
164
-
165
- for (const mount of mounts) {
166
- const mountRoot = path.resolve(process.cwd(), mount.root);
167
-
168
- if (!fs.existsSync(mountRoot)) {
169
- ctx.logger.warn(`Static mount root not found: ${mountRoot}. Skipping.`);
170
- continue;
171
- }
172
-
173
- const mountPath = mount.path || '/';
174
- const normalizedPath = mountPath.startsWith('/') ? mountPath : `/${mountPath}`;
175
- const routePattern = normalizedPath === '/' ? '/*' : `${normalizedPath.replace(/\/$/, '')}/*`;
176
-
177
- // Routes to register: both /mount and /mount/*
178
- const routes = normalizedPath === '/' ? [routePattern] : [normalizedPath, routePattern];
179
-
180
- ctx.logger.debug('Mounting static files', {
181
- to: routes,
182
- from: mountRoot,
183
- rewrite: mount.rewrite,
184
- spa: mount.spa
185
- });
186
-
187
- routes.forEach(route => {
188
- // 1. Serve Static Files
189
- rawApp.get(
190
- route,
191
- serveStatic({
192
- root: mount.root,
193
- rewriteRequestPath: (reqPath) => {
194
- if (mount.rewrite && normalizedPath !== '/') {
195
- // /console/assets/style.css -> /assets/style.css
196
- if (reqPath.startsWith(normalizedPath)) {
197
- return reqPath.substring(normalizedPath.length) || '/';
198
- }
199
- }
200
- return reqPath;
201
- }
202
- })
203
- );
204
-
205
- // 2. SPA Fallback (Scoped)
206
- if (mount.spa) {
207
- rawApp.get(route, async (c, next) => {
208
- // Skip if API path check
209
- const config = this.options.restConfig || {};
210
- const basePath = config.api?.basePath || '/api';
211
-
212
- if (c.req.path.startsWith(basePath)) {
213
- return next();
214
- }
215
-
216
- return serveStatic({
217
- root: mount.root,
218
- rewriteRequestPath: () => 'index.html'
219
- })(c, next);
220
- });
221
- }
222
- });
223
- }
224
- }
225
-
226
- // Start server on kernel:ready hook
227
- ctx.hook('kernel:ready', async () => {
228
- // Register standard endpoints before starting to listen
229
- if (this.options.registerStandardEndpoints) {
230
- this.registerDiscoveryAndCrudEndpoints(ctx);
231
- }
232
-
233
- const port = this.options.port ?? 3000;
234
- ctx.logger.debug('Starting HTTP server', { port });
235
-
236
- await this.server.listen(port);
237
-
238
- const actualPort = this.server.getPort();
239
- if (actualPort !== port) {
240
- ctx.logger.warn(`Port ${port} is in use, using port ${actualPort} instead`);
241
- }
242
- ctx.logger.info('HTTP server started successfully', {
243
- port: actualPort,
244
- url: `http://localhost:${actualPort}`
245
- });
246
- });
247
- }
248
-
249
- /**
250
- * Register discovery and basic CRUD endpoints.
251
- * Called when `registerStandardEndpoints` is true, before the server starts listening.
252
- */
253
- private registerDiscoveryAndCrudEndpoints(ctx: PluginContext) {
254
- const rawApp = this.server.getRawApp();
255
- const prefix = '/api/v1';
256
-
257
- // Build the standard discovery response
258
- const discovery = {
259
- version: 'v1',
260
- apiName: 'ObjectStack API',
261
- routes: {
262
- data: `${prefix}/data`,
263
- metadata: `${prefix}/meta`,
264
- auth: `${prefix}/auth`,
265
- packages: `${prefix}/packages`,
266
- analytics: `${prefix}/analytics`,
267
- realtime: `${prefix}/realtime`,
268
- workflow: `${prefix}/workflow`,
269
- automation: `${prefix}/automation`,
270
- ai: `${prefix}/ai`,
271
- notifications: `${prefix}/notifications`,
272
- i18n: `${prefix}/i18n`,
273
- storage: `${prefix}/storage`,
274
- ui: `${prefix}/ui`,
275
- },
276
- };
277
-
278
- // Discovery endpoints
279
- rawApp.get('/.well-known/objectstack', (c: any) => c.redirect(`${prefix}/discovery`));
280
- rawApp.get(`${prefix}/discovery`, (c: any) => c.json({ data: discovery }));
281
-
282
- ctx.logger.info('Registered discovery endpoints', { prefix });
283
-
284
- // Basic CRUD data endpoints — delegate to kernel.broker when available
285
- const getBroker = () => (ctx.getKernel() as any).broker;
286
-
287
- // Create
288
- rawApp.post(`${prefix}/data/:object`, async (c: any) => {
289
- const broker = getBroker();
290
- if (!broker) return c.json({ error: 'Broker not available' }, 500);
291
- const object = c.req.param('object');
292
- const data = await c.req.json().catch(() => ({}));
293
- const result = await broker.call('data.create', { object, data }, {});
294
- return c.json(result);
295
- });
296
-
297
- // Get by ID
298
- rawApp.get(`${prefix}/data/:object/:id`, async (c: any) => {
299
- const broker = getBroker();
300
- if (!broker) return c.json({ error: 'Broker not available' }, 500);
301
- const object = c.req.param('object');
302
- const id = c.req.param('id');
303
- const result = await broker.call('data.get', { object, id }, {});
304
- return result ? c.json(result) : c.json({ error: 'Not found' }, 404);
305
- });
306
-
307
- // Find / List
308
- rawApp.get(`${prefix}/data/:object`, async (c: any) => {
309
- const broker = getBroker();
310
- if (!broker) return c.json({ error: 'Broker not available' }, 500);
311
- const object = c.req.param('object');
312
- const filters = c.req.query();
313
- const result = await broker.call('data.find', { object, filters }, {});
314
- return c.json(result);
315
- });
316
-
317
- ctx.logger.debug('Registered standard CRUD data endpoints', { prefix });
318
- }
319
-
320
- /**
321
- * Destroy phase - Stop server
322
- */
323
- async destroy() {
324
- this.server.close();
325
- // Note: Can't use ctx.logger here since we're in destroy
326
- console.log('[HonoServerPlugin] Server stopped');
327
- }
328
- }
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- export * from './hono-plugin';
4
- export * from './adapter';
5
-
package/tsconfig.json DELETED
@@ -1,24 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "ignoreDeprecations": "6.0",
7
- "strict": true,
8
- "esModuleInterop": true,
9
- "declaration": true,
10
- "outDir": "./dist",
11
- "types": [
12
- "node"
13
- ],
14
- "rootDir": "./src"
15
- },
16
- "include": [
17
- "src/**/*"
18
- ],
19
- "exclude": [
20
- "node_modules",
21
- "dist",
22
- "**/*.test.ts"
23
- ]
24
- }