mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(cli): add single executable with embedded web assets support
Implement support for bundling web assets into CLI single executable binaries. When built with --with-web-assets, the executable includes the compiled web application and serves it directly without file system access. A stub generator creates empty manifests for normal builds to maintain compatibility. Key changes: - Add --with-web-assets flag to build-executable.ts with manifest validation - Generate embeddedAssets.ts manifest from web/dist during build - Serve embedded assets in web server with fallback to file system - Add hapi server subcommand to start API + web server - Include server sources in CLI tsconfig for compilation scope - Add workspace-level build:single-exe scripts for production builds
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
// Stub types for embeddedAssets.generated.ts when the manifest is not generated.
|
||||
declare module './embeddedAssets.generated' {
|
||||
export interface EmbeddedWebAsset {
|
||||
path: string;
|
||||
sourcePath: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export const embeddedAssets: EmbeddedWebAsset[];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { EmbeddedWebAsset } from './embeddedAssets.generated';
|
||||
|
||||
let embeddedAssetMap: Map<string, EmbeddedWebAsset> | null = null;
|
||||
|
||||
export type { EmbeddedWebAsset };
|
||||
|
||||
export async function loadEmbeddedAssetMap(): Promise<Map<string, EmbeddedWebAsset>> {
|
||||
if (embeddedAssetMap) {
|
||||
return embeddedAssetMap;
|
||||
}
|
||||
|
||||
const { embeddedAssets } = await import('./embeddedAssets.generated');
|
||||
embeddedAssetMap = new Map(embeddedAssets.map((asset) => [asset.path, asset]));
|
||||
return embeddedAssetMap;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import type { SSEManager } from '../sse/sseManager'
|
||||
import type { Server as BunServer } from 'bun'
|
||||
import type { Server as SocketEngine } from '@socket.io/bun-engine'
|
||||
import type { WebSocketData } from '@socket.io/bun-engine'
|
||||
import { loadEmbeddedAssetMap, type EmbeddedWebAsset } from './embeddedAssets'
|
||||
|
||||
function findWebappDistDir(): { distDir: string; indexHtmlPath: string } {
|
||||
const candidates = [
|
||||
@@ -38,10 +39,19 @@ function findWebappDistDir(): { distDir: string; indexHtmlPath: string } {
|
||||
return { distDir, indexHtmlPath: join(distDir, 'index.html') }
|
||||
}
|
||||
|
||||
function serveEmbeddedAsset(asset: EmbeddedWebAsset): Response {
|
||||
return new Response(Bun.file(asset.sourcePath), {
|
||||
headers: {
|
||||
'Content-Type': asset.mimeType
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createWebApp(options: {
|
||||
getSyncEngine: () => SyncEngine | null
|
||||
getSseManager: () => SSEManager | null
|
||||
jwtSecret: Uint8Array
|
||||
embeddedAssetMap: Map<string, EmbeddedWebAsset> | null
|
||||
}): Hono<WebAppEnv> {
|
||||
const app = new Hono<WebAppEnv>()
|
||||
|
||||
@@ -69,6 +79,51 @@ function createWebApp(options: {
|
||||
app.route('/api', createMachinesRoutes(options.getSyncEngine))
|
||||
app.route('/api', createGitRoutes(options.getSyncEngine))
|
||||
|
||||
if (options.embeddedAssetMap) {
|
||||
const embeddedAssetMap = options.embeddedAssetMap
|
||||
const indexHtmlAsset = embeddedAssetMap.get('/index.html')
|
||||
|
||||
if (!indexHtmlAsset) {
|
||||
app.get('*', (c) => {
|
||||
return c.text(
|
||||
'Embedded Mini App is missing index.html. Rebuild the executable after running bun run build:web.',
|
||||
503
|
||||
)
|
||||
})
|
||||
return app
|
||||
}
|
||||
|
||||
app.use('*', async (c, next) => {
|
||||
if (c.req.path.startsWith('/api')) {
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
||||
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
||||
const asset = embeddedAssetMap.get(c.req.path)
|
||||
if (asset) {
|
||||
return serveEmbeddedAsset(asset)
|
||||
}
|
||||
|
||||
await next()
|
||||
})
|
||||
|
||||
app.get('*', async (c, next) => {
|
||||
if (c.req.path.startsWith('/api')) {
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
||||
return serveEmbeddedAsset(indexHtmlAsset)
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
const { distDir, indexHtmlPath } = findWebappDistDir()
|
||||
|
||||
if (!existsSync(indexHtmlPath)) {
|
||||
@@ -110,10 +165,12 @@ export async function startWebServer(options: {
|
||||
jwtSecret: Uint8Array
|
||||
socketEngine: SocketEngine
|
||||
}): Promise<BunServer<WebSocketData>> {
|
||||
const embeddedAssetMap = Bun.isCompiled ? await loadEmbeddedAssetMap() : null
|
||||
const app = createWebApp({
|
||||
getSyncEngine: options.getSyncEngine,
|
||||
getSseManager: options.getSseManager,
|
||||
jwtSecret: options.jwtSecret
|
||||
jwtSecret: options.jwtSecret,
|
||||
embeddedAssetMap
|
||||
})
|
||||
|
||||
const socketHandler = options.socketEngine.handler()
|
||||
|
||||
Reference in New Issue
Block a user