1111 */
1212
1313import { createHonoApp } from '@objectstack/hono' ;
14- import { createOriginMatcher , hasWildcardPattern } from '@objectstack/plugin-hono-server' ;
14+ import { createOriginMatcher , hasWildcardPattern , HonoHttpServer } from '@objectstack/plugin-hono-server' ;
1515import { getRequestListener } from '@hono/node-server' ;
1616import { ObjectKernel , createRestApiPlugin , createDispatcherPlugin , KernelManager } from '@objectstack/runtime' ;
1717import type { EnvironmentDriverRegistry } from '@objectstack/runtime' ;
18- import { Hono } from 'hono' ;
18+ import type { Hono } from 'hono' ;
1919import stackConfig from '../objectstack.config.js' ;
20- import { listTemplates } from './templates/registry.js' ;
2120
2221// ---------------------------------------------------------------------------
2322// Runtime shape returned by ensureBoot()
@@ -42,6 +41,20 @@ let _bootPromise: Promise<BootResult> | null = null;
4241async function bootKernel ( ) : Promise < BootResult > {
4342 const kernel = new ObjectKernel ( ) ;
4443
44+ // 0. Register an `http.server` (IHttpServer) adapter BEFORE plugins so
45+ // that any plugin's start() hook can resolve `ctx.getService('http.server')`
46+ // and register routes on it. This is the official ObjectStack
47+ // protocol for plugin-supplied HTTP routes (see IHttpServer in
48+ // @objectstack /spec/contracts and HonoServerPlugin's reference
49+ // implementation). The Vercel entrypoint cannot use HonoServerPlugin
50+ // itself because we don't want plugin-hono-server to call listen() —
51+ // Vercel hands us a request directly. Reusing the same adapter class
52+ // keeps route-registration semantics identical between local
53+ // (`objectstack dev`) and serverless deployments.
54+ const httpServer = new HonoHttpServer ( ) ;
55+ kernel . registerService ( 'http.server' , httpServer ) ;
56+ kernel . registerService ( 'http-server' , httpServer ) ; // alias for backward compatibility
57+
4558 // 1. Config plugins (control-plane preset + MultiProjectPlugin)
4659 for ( const plugin of stackConfig . plugins ?? [ ] ) {
4760 await kernel . use ( plugin as any ) ;
@@ -107,51 +120,24 @@ async function ensureBoot(): Promise<BootResult> {
107120// Hono app factory
108121// ---------------------------------------------------------------------------
109122
110- function envFlag ( name : string ) : boolean {
111- return [ '1' , 'true' , 'yes' , 'on' ] . includes ( ( process . env [ name ] ?? '' ) . trim ( ) . toLowerCase ( ) ) ;
112- }
113-
114123async function ensureApp ( ) : Promise < Hono > {
115124 if ( _app ) return _app ;
116125
117126 const { kernel } = await ensureBoot ( ) ;
118- // envRegistry / kernelManager are resolved by HttpDispatcher from the
119- // kernel's service registry (MultiProjectPlugin registered them during
120- // bootKernel), so they do NOT need to be passed explicitly here.
121- const inner = createHonoApp ( { kernel, prefix : '/api/v1' } ) ;
122127
123- // Vercel entrypoint does NOT load plugin-hono-server, so the
124- // `http.server` service is never registered. The route plugins in
125- // `multi-project-plugins.ts` early-return when that service is
126- // missing, leaving `/studio/runtime-config` and `/cloud/templates`
127- // unmounted (404 / empty list).
128- //
129- // We can't simply call `inner.get(...)` after createHonoApp() because
130- // it has already registered an `app.all('${prefix}/*')` dispatcher
131- // catch-all that wins on registration order. Instead wrap `inner` in
132- // an outer Hono whose own routes are matched first, then fall
133- // through to `inner.fetch()` for everything else.
134- if ( envFlag ( 'OBJECTSTACK_MULTI_PROJECT' ) ) {
135- const templatesPayload = listTemplates ( ) . map ( ( { id, label, description, category } ) => ( {
136- id,
137- label,
138- description,
139- category,
140- } ) ) ;
141- const outer = new Hono ( ) ;
142- outer . get ( '/api/v1/studio/runtime-config' , ( c ) =>
143- c . json ( { singleProject : false } ) ) ;
144- outer . get ( '/api/v1/cloud/templates' , ( c ) =>
145- c . json ( {
146- success : true ,
147- data : { templates : templatesPayload , total : templatesPayload . length } ,
148- } ) ) ;
149- outer . all ( '*' , ( c ) => inner . fetch ( c . req . raw ) ) ;
150- _app = outer ;
151- } else {
152- _app = inner ;
153- }
128+ // Plugins have already registered their routes onto the IHttpServer
129+ // (HonoHttpServer) we created in bootKernel(). Pull out its underlying
130+ // Hono so those plugin routes are matched FIRST, then mount the
131+ // dispatcher app underneath via `outer.route('/', inner)` — Hono uses
132+ // registration-order priority, so the plugin routes win the match
133+ // against the dispatcher's catch-all `/api/v1/*` handler.
134+ const httpServer = kernel . getService < HonoHttpServer > ( 'http.server' ) ;
135+ const outer = httpServer . getRawApp ( ) ;
136+
137+ const inner = createHonoApp ( { kernel, prefix : '/api/v1' } ) ;
138+ outer . route ( '/' , inner ) ;
154139
140+ _app = outer ;
155141 return _app ;
156142}
157143
@@ -353,15 +339,17 @@ export default getRequestListener(async (request, env) => {
353339 const contentTypeStr = Array . isArray ( contentType ) ? contentType [ 0 ] : contentType ;
354340 const body = extractBody ( incoming , method , contentTypeStr ) ;
355341 if ( body != null ) {
356- return await app . fetch (
342+ const response = await app . fetch (
357343 new Request ( url , { method, headers : request . headers , body } ) ,
358344 ) ;
345+ return withCorsHeaders ( response , request ) ;
359346 }
360347 }
361348
362- return await app . fetch (
349+ const response = await app . fetch (
363350 new Request ( url , { method, headers : request . headers } ) ,
364351 ) ;
352+ return withCorsHeaders ( response , request ) ;
365353} ) ;
366354
367355export const config = {
0 commit comments