@objectstack/plugin-hono-server 4.0.4 → 4.1.0
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/README.md +22 -0
- package/dist/index.d.mts +88 -1
- package/dist/index.d.ts +88 -1
- package/dist/index.js +299 -40
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +299 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +33 -8
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -688
- package/objectstack.config.ts +0 -240
- package/src/adapter.ts +0 -228
- package/src/hono-plugin.test.ts +0 -236
- package/src/hono-plugin.ts +0 -456
- package/src/index.ts +0 -5
- package/src/pattern-matcher.test.ts +0 -180
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -22
package/objectstack.config.ts
DELETED
|
@@ -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,228 +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
|
-
export interface HonoCorsOptions {
|
|
16
|
-
enabled?: boolean;
|
|
17
|
-
origins?: string | string[];
|
|
18
|
-
methods?: string[];
|
|
19
|
-
credentials?: boolean;
|
|
20
|
-
maxAge?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Hono Implementation of IHttpServer
|
|
25
|
-
*/
|
|
26
|
-
export class HonoHttpServer implements IHttpServer {
|
|
27
|
-
private app: Hono;
|
|
28
|
-
private server: any;
|
|
29
|
-
private listeningPort: number | undefined;
|
|
30
|
-
|
|
31
|
-
constructor(
|
|
32
|
-
private port: number = 3000,
|
|
33
|
-
private staticRoot?: string
|
|
34
|
-
) {
|
|
35
|
-
this.app = new Hono();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// internal helper to convert standard handler to Hono handler
|
|
39
|
-
private wrap(handler: RouteHandler) {
|
|
40
|
-
return async (c: any) => {
|
|
41
|
-
let body: any = {};
|
|
42
|
-
|
|
43
|
-
// Try to parse JSON body first if content-type is JSON
|
|
44
|
-
if (c.req.header('content-type')?.includes('application/json')) {
|
|
45
|
-
try {
|
|
46
|
-
body = await c.req.json();
|
|
47
|
-
} catch(e) {
|
|
48
|
-
// If JSON parsing fails, try parseBody
|
|
49
|
-
try {
|
|
50
|
-
body = await c.req.parseBody();
|
|
51
|
-
} catch(e2) {}
|
|
52
|
-
}
|
|
53
|
-
} else {
|
|
54
|
-
// For non-JSON content types, use parseBody
|
|
55
|
-
try {
|
|
56
|
-
body = await c.req.parseBody();
|
|
57
|
-
} catch(e) {}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const req = {
|
|
61
|
-
params: c.req.param(),
|
|
62
|
-
query: c.req.query(),
|
|
63
|
-
body,
|
|
64
|
-
headers: c.req.header(),
|
|
65
|
-
method: c.req.method,
|
|
66
|
-
path: c.req.path
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
let capturedResponse: any;
|
|
70
|
-
let streamController: ReadableStreamDefaultController | null = null;
|
|
71
|
-
let streamEncoder: TextEncoder | null = null;
|
|
72
|
-
let streamHeaders: Record<string, string> = {};
|
|
73
|
-
let isStreaming = false;
|
|
74
|
-
|
|
75
|
-
const res = {
|
|
76
|
-
json: (data: any) => { capturedResponse = c.json(data); },
|
|
77
|
-
send: (data: string) => { capturedResponse = c.html(data); },
|
|
78
|
-
status: (code: number) => { c.status(code); return res; },
|
|
79
|
-
header: (name: string, value: string) => {
|
|
80
|
-
c.header(name, value);
|
|
81
|
-
streamHeaders[name] = value;
|
|
82
|
-
return res;
|
|
83
|
-
},
|
|
84
|
-
write: (chunk: string | Uint8Array) => {
|
|
85
|
-
isStreaming = true;
|
|
86
|
-
if (streamController && streamEncoder) {
|
|
87
|
-
const data = typeof chunk === 'string' ? streamEncoder.encode(chunk) : chunk;
|
|
88
|
-
streamController.enqueue(data);
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
end: () => {
|
|
92
|
-
if (streamController) {
|
|
93
|
-
streamController.close();
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// Create a streaming response wrapper — if handler calls res.write(),
|
|
99
|
-
// we return a ReadableStream; otherwise fall back to capturedResponse.
|
|
100
|
-
const streamPromise = new Promise<Response | null>((resolve) => {
|
|
101
|
-
const stream = new ReadableStream({
|
|
102
|
-
start(controller) {
|
|
103
|
-
streamController = controller;
|
|
104
|
-
streamEncoder = new TextEncoder();
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Run the handler; once it's done, check if streaming was used
|
|
109
|
-
const result = handler(req as any, res as any);
|
|
110
|
-
const done = result instanceof Promise ? result : Promise.resolve(result);
|
|
111
|
-
done.then(() => {
|
|
112
|
-
if (isStreaming) {
|
|
113
|
-
resolve(new Response(stream, {
|
|
114
|
-
status: 200,
|
|
115
|
-
headers: streamHeaders,
|
|
116
|
-
}));
|
|
117
|
-
} else {
|
|
118
|
-
// Not streaming — close the unused stream and return null
|
|
119
|
-
streamController?.close();
|
|
120
|
-
resolve(null);
|
|
121
|
-
}
|
|
122
|
-
}).catch(() => {
|
|
123
|
-
streamController?.close();
|
|
124
|
-
resolve(null);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const streamResponse = await streamPromise;
|
|
129
|
-
return streamResponse ?? capturedResponse;
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
get(path: string, handler: RouteHandler) {
|
|
134
|
-
this.app.get(path, this.wrap(handler));
|
|
135
|
-
}
|
|
136
|
-
post(path: string, handler: RouteHandler) {
|
|
137
|
-
this.app.post(path, this.wrap(handler));
|
|
138
|
-
}
|
|
139
|
-
put(path: string, handler: RouteHandler) {
|
|
140
|
-
this.app.put(path, this.wrap(handler));
|
|
141
|
-
}
|
|
142
|
-
delete(path: string, handler: RouteHandler) {
|
|
143
|
-
this.app.delete(path, this.wrap(handler));
|
|
144
|
-
}
|
|
145
|
-
patch(path: string, handler: RouteHandler) {
|
|
146
|
-
this.app.patch(path, this.wrap(handler));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
use(pathOrHandler: string | Middleware, handler?: Middleware) {
|
|
150
|
-
if (typeof pathOrHandler === 'string' && handler) {
|
|
151
|
-
// Path based middleware
|
|
152
|
-
// Hono middleware signature is different (c, next) => ...
|
|
153
|
-
this.app.use(pathOrHandler, async (c, next) => {
|
|
154
|
-
// Simplistic conversion
|
|
155
|
-
await handler({} as any, {} as any, next);
|
|
156
|
-
});
|
|
157
|
-
} else if (typeof pathOrHandler === 'function') {
|
|
158
|
-
// Global middleware
|
|
159
|
-
this.app.use('*', async (c, next) => {
|
|
160
|
-
await pathOrHandler({} as any, {} as any, next);
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Mount a sub-application or router
|
|
167
|
-
*/
|
|
168
|
-
mount(path: string, subApp: Hono) {
|
|
169
|
-
this.app.route(path, subApp);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
async listen(port: number) {
|
|
174
|
-
if (this.staticRoot) {
|
|
175
|
-
this.app.get('/*', serveStatic({ root: this.staticRoot }));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const targetPort = port || this.port;
|
|
179
|
-
const maxRetries = 20;
|
|
180
|
-
|
|
181
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
182
|
-
const tryPort = targetPort + attempt;
|
|
183
|
-
try {
|
|
184
|
-
await this.tryListen(tryPort);
|
|
185
|
-
return;
|
|
186
|
-
} catch (err: any) {
|
|
187
|
-
if (err.code === 'EADDRINUSE' && attempt < maxRetries - 1) {
|
|
188
|
-
if (this.server && typeof this.server.close === 'function') {
|
|
189
|
-
this.server.close();
|
|
190
|
-
}
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
throw err;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private tryListen(port: number): Promise<void> {
|
|
199
|
-
return new Promise<void>((resolve, reject) => {
|
|
200
|
-
const server = serve({
|
|
201
|
-
fetch: this.app.fetch,
|
|
202
|
-
port
|
|
203
|
-
}, (info) => {
|
|
204
|
-
this.listeningPort = info.port;
|
|
205
|
-
resolve();
|
|
206
|
-
});
|
|
207
|
-
this.server = server;
|
|
208
|
-
server.on('error', (err: any) => {
|
|
209
|
-
reject(err);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
getPort() {
|
|
215
|
-
return this.listeningPort || this.port;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Expose raw app for scenarios where standard interface is not enough
|
|
219
|
-
getRawApp() {
|
|
220
|
-
return this.app;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async close() {
|
|
224
|
-
if (this.server && typeof this.server.close === 'function') {
|
|
225
|
-
this.server.close();
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
package/src/hono-plugin.test.ts
DELETED
|
@@ -1,236 +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
|
-
use: vi.fn(),
|
|
31
|
-
})
|
|
32
|
-
};
|
|
33
|
-
})
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
describe('HonoServerPlugin', () => {
|
|
37
|
-
let context: any;
|
|
38
|
-
let logger: any;
|
|
39
|
-
let kernel: any;
|
|
40
|
-
|
|
41
|
-
beforeEach(() => {
|
|
42
|
-
vi.clearAllMocks();
|
|
43
|
-
|
|
44
|
-
logger = {
|
|
45
|
-
info: vi.fn(),
|
|
46
|
-
debug: vi.fn(),
|
|
47
|
-
warn: vi.fn(),
|
|
48
|
-
error: vi.fn()
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
kernel = {
|
|
52
|
-
getService: vi.fn(),
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
context = {
|
|
56
|
-
logger,
|
|
57
|
-
getKernel: vi.fn().mockReturnValue(kernel),
|
|
58
|
-
registerService: vi.fn(),
|
|
59
|
-
hook: vi.fn(),
|
|
60
|
-
getService: vi.fn()
|
|
61
|
-
};
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should initialize and register server', async () => {
|
|
65
|
-
const plugin = new HonoServerPlugin();
|
|
66
|
-
await plugin.init(context as PluginContext);
|
|
67
|
-
|
|
68
|
-
expect(context.registerService).toHaveBeenCalledWith('http-server', expect.any(Object));
|
|
69
|
-
expect(HonoHttpServer).toHaveBeenCalled();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should register IHttpServer service on init', async () => {
|
|
73
|
-
const plugin = new HonoServerPlugin();
|
|
74
|
-
await plugin.init(context as PluginContext);
|
|
75
|
-
|
|
76
|
-
expect(context.registerService).toHaveBeenCalledWith('http.server', expect.any(Object));
|
|
77
|
-
expect(context.registerService).toHaveBeenCalledWith('http-server', expect.any(Object));
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should start without errors', async () => {
|
|
81
|
-
const plugin = new HonoServerPlugin();
|
|
82
|
-
await plugin.init(context as PluginContext);
|
|
83
|
-
await plugin.start(context as PluginContext);
|
|
84
|
-
|
|
85
|
-
// Plugin should register kernel:ready hook to start listening
|
|
86
|
-
expect(context.hook).toHaveBeenCalledWith('kernel:ready', expect.any(Function));
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should handle errors gracefully on start', async () => {
|
|
90
|
-
// Simulate a start that doesn't crash even without routes
|
|
91
|
-
const plugin = new HonoServerPlugin();
|
|
92
|
-
await plugin.init(context as PluginContext);
|
|
93
|
-
await expect(plugin.start(context as PluginContext)).resolves.not.toThrow();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should configure static files and SPA fallback when enabled', async () => {
|
|
97
|
-
const plugin = new HonoServerPlugin({
|
|
98
|
-
staticRoot: './public',
|
|
99
|
-
spaFallback: true
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
await plugin.init(context as PluginContext);
|
|
103
|
-
await plugin.start(context as PluginContext);
|
|
104
|
-
|
|
105
|
-
const serverInstance = (HonoHttpServer as any).mock.instances[0];
|
|
106
|
-
const rawApp = serverInstance.getRawApp();
|
|
107
|
-
|
|
108
|
-
expect(serverInstance.getRawApp).toHaveBeenCalled();
|
|
109
|
-
// Should register static files middleware
|
|
110
|
-
expect(rawApp.get).toHaveBeenCalledWith('/*', expect.anything());
|
|
111
|
-
// Should register SPA fallback middleware
|
|
112
|
-
expect(rawApp.get).toHaveBeenCalledWith('/*', expect.anything());
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe('CORS wildcard pattern matching', () => {
|
|
116
|
-
beforeEach(() => {
|
|
117
|
-
vi.clearAllMocks();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should enable CORS middleware with wildcard subdomain patterns', async () => {
|
|
121
|
-
const plugin = new HonoServerPlugin({
|
|
122
|
-
cors: {
|
|
123
|
-
origins: ['https://*.objectui.org', 'https://*.objectstack.ai'],
|
|
124
|
-
credentials: true
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
await plugin.init(context as PluginContext);
|
|
129
|
-
|
|
130
|
-
const serverInstance = (HonoHttpServer as any).mock.instances[0];
|
|
131
|
-
const rawApp = serverInstance.getRawApp();
|
|
132
|
-
|
|
133
|
-
// CORS middleware should be registered
|
|
134
|
-
expect(rawApp.use).toHaveBeenCalledWith('*', expect.any(Function));
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should enable CORS middleware with port wildcard patterns', async () => {
|
|
138
|
-
const plugin = new HonoServerPlugin({
|
|
139
|
-
cors: {
|
|
140
|
-
origins: 'http://localhost:*',
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
await plugin.init(context as PluginContext);
|
|
145
|
-
|
|
146
|
-
const serverInstance = (HonoHttpServer as any).mock.instances[0];
|
|
147
|
-
const rawApp = serverInstance.getRawApp();
|
|
148
|
-
|
|
149
|
-
expect(rawApp.use).toHaveBeenCalledWith('*', expect.any(Function));
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should support comma-separated wildcard patterns', async () => {
|
|
153
|
-
const plugin = new HonoServerPlugin({
|
|
154
|
-
cors: {
|
|
155
|
-
origins: 'https://*.objectui.org,https://*.objectstack.ai',
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
await plugin.init(context as PluginContext);
|
|
160
|
-
|
|
161
|
-
const serverInstance = (HonoHttpServer as any).mock.instances[0];
|
|
162
|
-
const rawApp = serverInstance.getRawApp();
|
|
163
|
-
|
|
164
|
-
expect(rawApp.use).toHaveBeenCalledWith('*', expect.any(Function));
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('should support exact origins without wildcards', async () => {
|
|
168
|
-
const plugin = new HonoServerPlugin({
|
|
169
|
-
cors: {
|
|
170
|
-
origins: ['https://app.example.com', 'https://api.example.com'],
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
await plugin.init(context as PluginContext);
|
|
175
|
-
|
|
176
|
-
const serverInstance = (HonoHttpServer as any).mock.instances[0];
|
|
177
|
-
const rawApp = serverInstance.getRawApp();
|
|
178
|
-
|
|
179
|
-
expect(rawApp.use).toHaveBeenCalledWith('*', expect.any(Function));
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should support CORS_ORIGIN environment variable with wildcards', async () => {
|
|
183
|
-
const originalEnv = process.env.CORS_ORIGIN;
|
|
184
|
-
process.env.CORS_ORIGIN = 'https://*.objectui.org,https://*.objectstack.ai';
|
|
185
|
-
|
|
186
|
-
const plugin = new HonoServerPlugin();
|
|
187
|
-
await plugin.init(context as PluginContext);
|
|
188
|
-
|
|
189
|
-
const serverInstance = (HonoHttpServer as any).mock.instances[0];
|
|
190
|
-
const rawApp = serverInstance.getRawApp();
|
|
191
|
-
|
|
192
|
-
expect(rawApp.use).toHaveBeenCalledWith('*', expect.any(Function));
|
|
193
|
-
|
|
194
|
-
// Restore environment
|
|
195
|
-
if (originalEnv !== undefined) {
|
|
196
|
-
process.env.CORS_ORIGIN = originalEnv;
|
|
197
|
-
} else {
|
|
198
|
-
delete process.env.CORS_ORIGIN;
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('should disable CORS when cors option is false', async () => {
|
|
203
|
-
const plugin = new HonoServerPlugin({
|
|
204
|
-
cors: false
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
await plugin.init(context as PluginContext);
|
|
208
|
-
|
|
209
|
-
const serverInstance = (HonoHttpServer as any).mock.instances[0];
|
|
210
|
-
const rawApp = serverInstance.getRawApp();
|
|
211
|
-
|
|
212
|
-
// CORS middleware should NOT be registered
|
|
213
|
-
expect(rawApp.use).not.toHaveBeenCalled();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('should disable CORS when CORS_ENABLED env is false', async () => {
|
|
217
|
-
const originalEnv = process.env.CORS_ENABLED;
|
|
218
|
-
process.env.CORS_ENABLED = 'false';
|
|
219
|
-
|
|
220
|
-
const plugin = new HonoServerPlugin();
|
|
221
|
-
await plugin.init(context as PluginContext);
|
|
222
|
-
|
|
223
|
-
const serverInstance = (HonoHttpServer as any).mock.instances[0];
|
|
224
|
-
const rawApp = serverInstance.getRawApp();
|
|
225
|
-
|
|
226
|
-
expect(rawApp.use).not.toHaveBeenCalled();
|
|
227
|
-
|
|
228
|
-
// Restore environment
|
|
229
|
-
if (originalEnv !== undefined) {
|
|
230
|
-
process.env.CORS_ENABLED = originalEnv;
|
|
231
|
-
} else {
|
|
232
|
-
delete process.env.CORS_ENABLED;
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
});
|