@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,240 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { ObjectStackManifest } from '@objectstack/spec/system';
4
-
5
- /**
6
- * Hono Server Plugin Manifest
7
- *
8
- * HTTP server adapter plugin using the Hono framework.
9
- * Provides northbound HTTP/REST API gateway capabilities.
10
- */
11
- const HonoServerPlugin: ObjectStackManifest = {
12
- id: 'com.objectstack.server.hono',
13
- name: 'Hono Server Adapter',
14
- version: '1.0.0',
15
- type: 'adapter',
16
- description: 'HTTP server adapter using Hono framework. Exposes ObjectStack Runtime Protocol via REST API endpoints.',
17
-
18
- configuration: {
19
- title: 'Hono Server Configuration',
20
- properties: {
21
- port: {
22
- type: 'number',
23
- default: 3000,
24
- description: 'HTTP server port',
25
- },
26
- staticRoot: {
27
- type: 'string',
28
- description: 'Path to static files directory (optional)',
29
- },
30
- },
31
- },
32
-
33
- // Plugin Capability Declaration
34
- capabilities: {
35
- // Protocols This Plugin Implements
36
- implements: [
37
- {
38
- protocol: {
39
- id: 'com.objectstack.protocol.http.v1',
40
- label: 'HTTP Server Protocol v1',
41
- version: { major: 1, minor: 0, patch: 0 },
42
- description: 'Standard HTTP server capabilities',
43
- },
44
- conformance: 'full',
45
- certified: false,
46
- },
47
- {
48
- protocol: {
49
- id: 'com.objectstack.protocol.api.rest.v1',
50
- label: 'REST API Protocol v1',
51
- version: { major: 1, minor: 0, patch: 0 },
52
- description: 'RESTful API endpoint implementation',
53
- },
54
- conformance: 'full',
55
- features: [
56
- {
57
- name: 'meta_protocol',
58
- enabled: true,
59
- description: 'Metadata discovery endpoints',
60
- },
61
- {
62
- name: 'data_protocol',
63
- enabled: true,
64
- description: 'CRUD data operations',
65
- },
66
- {
67
- name: 'ui_protocol',
68
- enabled: true,
69
- description: 'UI view metadata endpoints',
70
- },
71
- ],
72
- certified: false,
73
- },
74
- ],
75
-
76
- // Interfaces This Plugin Provides
77
- provides: [
78
- {
79
- id: 'com.objectstack.server.hono.interface.http_server',
80
- name: 'IHttpServer',
81
- description: 'HTTP server service interface',
82
- version: { major: 1, minor: 0, patch: 0 },
83
- stability: 'stable',
84
- methods: [
85
- {
86
- name: 'get',
87
- description: 'Register GET route handler',
88
- parameters: [
89
- {
90
- name: 'path',
91
- type: 'string',
92
- required: true,
93
- description: 'Route path pattern',
94
- },
95
- {
96
- name: 'handler',
97
- type: 'Function',
98
- required: true,
99
- description: 'Route handler function',
100
- },
101
- ],
102
- returnType: 'void',
103
- async: false,
104
- },
105
- {
106
- name: 'post',
107
- description: 'Register POST route handler',
108
- parameters: [
109
- {
110
- name: 'path',
111
- type: 'string',
112
- required: true,
113
- description: 'Route path pattern',
114
- },
115
- {
116
- name: 'handler',
117
- type: 'Function',
118
- required: true,
119
- description: 'Route handler function',
120
- },
121
- ],
122
- returnType: 'void',
123
- async: false,
124
- },
125
- {
126
- name: 'patch',
127
- description: 'Register PATCH route handler',
128
- parameters: [
129
- {
130
- name: 'path',
131
- type: 'string',
132
- required: true,
133
- description: 'Route path pattern',
134
- },
135
- {
136
- name: 'handler',
137
- type: 'Function',
138
- required: true,
139
- description: 'Route handler function',
140
- },
141
- ],
142
- returnType: 'void',
143
- async: false,
144
- },
145
- {
146
- name: 'delete',
147
- description: 'Register DELETE route handler',
148
- parameters: [
149
- {
150
- name: 'path',
151
- type: 'string',
152
- required: true,
153
- description: 'Route path pattern',
154
- },
155
- {
156
- name: 'handler',
157
- type: 'Function',
158
- required: true,
159
- description: 'Route handler function',
160
- },
161
- ],
162
- returnType: 'void',
163
- async: false,
164
- },
165
- {
166
- name: 'listen',
167
- description: 'Start the HTTP server',
168
- parameters: [
169
- {
170
- name: 'port',
171
- type: 'number',
172
- required: true,
173
- description: 'Port number',
174
- },
175
- ],
176
- returnType: 'Promise<void>',
177
- async: true,
178
- },
179
- {
180
- name: 'close',
181
- description: 'Stop the HTTP server',
182
- parameters: [],
183
- returnType: 'void',
184
- async: false,
185
- },
186
- ],
187
- },
188
- ],
189
-
190
- // Dependencies on Other Plugins/Services
191
- requires: [
192
- {
193
- pluginId: 'com.objectstack.engine.objectql',
194
- version: '^0.6.0',
195
- optional: true,
196
- reason: 'ObjectStack Runtime Protocol implementation service',
197
- requiredCapabilities: [
198
- 'com.objectstack.protocol.runtime.v1',
199
- ],
200
- },
201
- ],
202
-
203
- // Extension Points This Plugin Defines
204
- extensionPoints: [
205
- {
206
- id: 'com.objectstack.server.hono.extension.middleware',
207
- name: 'HTTP Middleware',
208
- description: 'Register custom HTTP middleware',
209
- type: 'hook',
210
- cardinality: 'multiple',
211
- contract: {
212
- signature: '(req: Request, res: Response, next: Function) => void | Promise<void>',
213
- },
214
- },
215
- {
216
- id: 'com.objectstack.server.hono.extension.route',
217
- name: 'Custom Routes',
218
- description: 'Register custom API routes',
219
- type: 'action',
220
- cardinality: 'multiple',
221
- contract: {
222
- input: 'RouteDefinition',
223
- signature: '(app: HonoApp) => void',
224
- },
225
- },
226
- ],
227
-
228
- // No extensions contributed to other plugins
229
- extensions: [],
230
- },
231
-
232
- contributes: {
233
- // System Events
234
- events: [
235
- 'kernel:ready',
236
- ],
237
- },
238
- };
239
-
240
- export default HonoServerPlugin;
package/src/adapter.ts DELETED
@@ -1,222 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- // Export IHttpServer from core
4
- export * from '@objectstack/core';
5
-
6
- import {
7
- IHttpServer,
8
- RouteHandler,
9
- Middleware
10
- } from '@objectstack/core';
11
- import { Hono } from 'hono';
12
- import { serve } from '@hono/node-server';
13
- import { serveStatic } from '@hono/node-server/serve-static';
14
-
15
- /**
16
- * Hono Implementation of IHttpServer
17
- */
18
- export class HonoHttpServer implements IHttpServer {
19
- private app: Hono;
20
- private server: any;
21
- private listeningPort: number | undefined;
22
-
23
- constructor(
24
- private port: number = 3000,
25
- private staticRoot?: string
26
- ) {
27
- this.app = new Hono();
28
- }
29
-
30
- // internal helper to convert standard handler to Hono handler
31
- private wrap(handler: RouteHandler) {
32
- return async (c: any) => {
33
- let body: any = {};
34
-
35
- // Try to parse JSON body first if content-type is JSON
36
- if (c.req.header('content-type')?.includes('application/json')) {
37
- try {
38
- body = await c.req.json();
39
- } catch(e) {
40
- // If JSON parsing fails, try parseBody
41
- try {
42
- body = await c.req.parseBody();
43
- } catch(e2) {}
44
- }
45
- } else {
46
- // For non-JSON content types, use parseBody
47
- try {
48
- body = await c.req.parseBody();
49
- } catch(e) {}
50
- }
51
-
52
- const req = {
53
- params: c.req.param(),
54
- query: c.req.query(),
55
- body,
56
- headers: c.req.header(),
57
- method: c.req.method,
58
- path: c.req.path
59
- };
60
-
61
- let capturedResponse: any;
62
- let streamController: ReadableStreamDefaultController | null = null;
63
- let streamEncoder: TextEncoder | null = null;
64
- let streamHeaders: Record<string, string> = {};
65
- let isStreaming = false;
66
-
67
- const res = {
68
- json: (data: any) => { capturedResponse = c.json(data); },
69
- send: (data: string) => { capturedResponse = c.html(data); },
70
- status: (code: number) => { c.status(code); return res; },
71
- header: (name: string, value: string) => {
72
- c.header(name, value);
73
- streamHeaders[name] = value;
74
- return res;
75
- },
76
- write: (chunk: string | Uint8Array) => {
77
- isStreaming = true;
78
- if (streamController && streamEncoder) {
79
- const data = typeof chunk === 'string' ? streamEncoder.encode(chunk) : chunk;
80
- streamController.enqueue(data);
81
- }
82
- },
83
- end: () => {
84
- if (streamController) {
85
- streamController.close();
86
- }
87
- },
88
- };
89
-
90
- // Create a streaming response wrapper — if handler calls res.write(),
91
- // we return a ReadableStream; otherwise fall back to capturedResponse.
92
- const streamPromise = new Promise<Response | null>((resolve) => {
93
- const stream = new ReadableStream({
94
- start(controller) {
95
- streamController = controller;
96
- streamEncoder = new TextEncoder();
97
- },
98
- });
99
-
100
- // Run the handler; once it's done, check if streaming was used
101
- const result = handler(req as any, res as any);
102
- const done = result instanceof Promise ? result : Promise.resolve(result);
103
- done.then(() => {
104
- if (isStreaming) {
105
- resolve(new Response(stream, {
106
- status: 200,
107
- headers: streamHeaders,
108
- }));
109
- } else {
110
- // Not streaming — close the unused stream and return null
111
- streamController?.close();
112
- resolve(null);
113
- }
114
- }).catch(() => {
115
- streamController?.close();
116
- resolve(null);
117
- });
118
- });
119
-
120
- const streamResponse = await streamPromise;
121
- return streamResponse ?? capturedResponse;
122
- };
123
- }
124
-
125
- get(path: string, handler: RouteHandler) {
126
- this.app.get(path, this.wrap(handler));
127
- }
128
- post(path: string, handler: RouteHandler) {
129
- this.app.post(path, this.wrap(handler));
130
- }
131
- put(path: string, handler: RouteHandler) {
132
- this.app.put(path, this.wrap(handler));
133
- }
134
- delete(path: string, handler: RouteHandler) {
135
- this.app.delete(path, this.wrap(handler));
136
- }
137
- patch(path: string, handler: RouteHandler) {
138
- this.app.patch(path, this.wrap(handler));
139
- }
140
-
141
- use(pathOrHandler: string | Middleware, handler?: Middleware) {
142
- if (typeof pathOrHandler === 'string' && handler) {
143
- // Path based middleware
144
- // Hono middleware signature is different (c, next) => ...
145
- this.app.use(pathOrHandler, async (c, next) => {
146
- // Simplistic conversion
147
- await handler({} as any, {} as any, next);
148
- });
149
- } else if (typeof pathOrHandler === 'function') {
150
- // Global middleware
151
- this.app.use('*', async (c, next) => {
152
- await pathOrHandler({} as any, {} as any, next);
153
- });
154
- }
155
- }
156
-
157
- /**
158
- * Mount a sub-application or router
159
- */
160
- mount(path: string, subApp: Hono) {
161
- this.app.route(path, subApp);
162
- }
163
-
164
-
165
- async listen(port: number) {
166
- if (this.staticRoot) {
167
- this.app.get('/*', serveStatic({ root: this.staticRoot }));
168
- }
169
-
170
- const targetPort = port || this.port;
171
- const maxRetries = 20;
172
-
173
- for (let attempt = 0; attempt < maxRetries; attempt++) {
174
- const tryPort = targetPort + attempt;
175
- try {
176
- await this.tryListen(tryPort);
177
- return;
178
- } catch (err: any) {
179
- if (err.code === 'EADDRINUSE' && attempt < maxRetries - 1) {
180
- if (this.server && typeof this.server.close === 'function') {
181
- this.server.close();
182
- }
183
- continue;
184
- }
185
- throw err;
186
- }
187
- }
188
- }
189
-
190
- private tryListen(port: number): Promise<void> {
191
- return new Promise<void>((resolve, reject) => {
192
- const server = serve({
193
- fetch: this.app.fetch,
194
- port
195
- }, (info) => {
196
- this.listeningPort = info.port;
197
- resolve();
198
- });
199
- this.server = server;
200
- server.on('error', (err: any) => {
201
- reject(err);
202
- });
203
- });
204
- }
205
-
206
- getPort() {
207
- return this.listeningPort || this.port;
208
- }
209
-
210
- // Expose raw app for scenarios where standard interface is not enough
211
- getRawApp() {
212
- return this.app;
213
- }
214
-
215
- async close() {
216
- if (this.server && typeof this.server.close === 'function') {
217
- this.server.close();
218
- }
219
- }
220
-
221
-
222
- }
@@ -1,113 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { HonoServerPlugin } from './hono-plugin';
3
- import { PluginContext } from '@objectstack/core';
4
- import { HonoHttpServer } from './adapter';
5
-
6
- vi.mock('fs', async (importOriginal) => {
7
- const actual = await importOriginal<typeof import('fs')>();
8
- return {
9
- ...actual,
10
- existsSync: vi.fn().mockReturnValue(true)
11
- };
12
- });
13
-
14
- vi.mock('@hono/node-server/serve-static', () => ({
15
- serveStatic: vi.fn(() => (c: any, next: any) => next())
16
- }));
17
-
18
- vi.mock('./adapter', () => ({
19
- HonoHttpServer: vi.fn(function() {
20
- return {
21
- mount: vi.fn(),
22
- start: vi.fn(),
23
- stop: vi.fn(),
24
- getApp: vi.fn(),
25
- listen: vi.fn(),
26
- getPort: vi.fn().mockReturnValue(3000),
27
- close: vi.fn(),
28
- getRawApp: vi.fn().mockReturnValue({
29
- get: vi.fn(),
30
- })
31
- };
32
- })
33
- }));
34
-
35
- describe('HonoServerPlugin', () => {
36
- let context: any;
37
- let logger: any;
38
- let kernel: any;
39
-
40
- beforeEach(() => {
41
- vi.clearAllMocks();
42
-
43
- logger = {
44
- info: vi.fn(),
45
- debug: vi.fn(),
46
- warn: vi.fn(),
47
- error: vi.fn()
48
- };
49
-
50
- kernel = {
51
- getService: vi.fn(),
52
- };
53
-
54
- context = {
55
- logger,
56
- getKernel: vi.fn().mockReturnValue(kernel),
57
- registerService: vi.fn(),
58
- hook: vi.fn(),
59
- getService: vi.fn()
60
- };
61
- });
62
-
63
- it('should initialize and register server', async () => {
64
- const plugin = new HonoServerPlugin();
65
- await plugin.init(context as PluginContext);
66
-
67
- expect(context.registerService).toHaveBeenCalledWith('http-server', expect.any(Object));
68
- expect(HonoHttpServer).toHaveBeenCalled();
69
- });
70
-
71
- it('should register IHttpServer service on init', async () => {
72
- const plugin = new HonoServerPlugin();
73
- await plugin.init(context as PluginContext);
74
-
75
- expect(context.registerService).toHaveBeenCalledWith('http.server', expect.any(Object));
76
- expect(context.registerService).toHaveBeenCalledWith('http-server', expect.any(Object));
77
- });
78
-
79
- it('should start without errors', async () => {
80
- const plugin = new HonoServerPlugin();
81
- await plugin.init(context as PluginContext);
82
- await plugin.start(context as PluginContext);
83
-
84
- // Plugin should register kernel:ready hook to start listening
85
- expect(context.hook).toHaveBeenCalledWith('kernel:ready', expect.any(Function));
86
- });
87
-
88
- it('should handle errors gracefully on start', async () => {
89
- // Simulate a start that doesn't crash even without routes
90
- const plugin = new HonoServerPlugin();
91
- await plugin.init(context as PluginContext);
92
- await expect(plugin.start(context as PluginContext)).resolves.not.toThrow();
93
- });
94
-
95
- it('should configure static files and SPA fallback when enabled', async () => {
96
- const plugin = new HonoServerPlugin({
97
- staticRoot: './public',
98
- spaFallback: true
99
- });
100
-
101
- await plugin.init(context as PluginContext);
102
- await plugin.start(context as PluginContext);
103
-
104
- const serverInstance = (HonoHttpServer as any).mock.instances[0];
105
- const rawApp = serverInstance.getRawApp();
106
-
107
- expect(serverInstance.getRawApp).toHaveBeenCalled();
108
- // Should register static files middleware
109
- expect(rawApp.get).toHaveBeenCalledWith('/*', expect.anything());
110
- // Should register SPA fallback middleware
111
- expect(rawApp.get).toHaveBeenCalledWith('/*', expect.anything());
112
- });
113
- });