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:
@@ -70,6 +70,8 @@
|
||||
"build": "shx rm -rf dist && tsc --noEmit && pkgroll",
|
||||
"build:exe": "bun run scripts/build-executable.ts",
|
||||
"build:exe:all": "bun run scripts/build-executable.ts --all",
|
||||
"build:exe:allinone": "bun run scripts/build-executable.ts --with-web-assets",
|
||||
"build:exe:allinone:all": "bun run scripts/build-executable.ts --with-web-assets --all",
|
||||
"test": "bun run build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
||||
"test:win": "bun run build && vitest run",
|
||||
"start": "bun run build && ./bin/happy.mjs",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { dirname, isAbsolute, join } from 'node:path';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const DEFAULT_TARGETS = [
|
||||
@@ -129,6 +129,52 @@ function resolveOutdir(projectRoot: string, outdir: string): string {
|
||||
return join(projectRoot, outdir);
|
||||
}
|
||||
|
||||
function writeStubEmbeddedAssets(workspaceRoot: string): void {
|
||||
const outputPath = join(workspaceRoot, 'server', 'src', 'web', 'embeddedAssets.generated.ts');
|
||||
const contents = [
|
||||
'// This file is generated by cli/scripts/build-executable.ts when --with-web-assets is not used.',
|
||||
'// It intentionally contains no embedded assets.',
|
||||
'',
|
||||
'export interface EmbeddedWebAsset {',
|
||||
' path: string;',
|
||||
' sourcePath: string;',
|
||||
' mimeType: string;',
|
||||
'}',
|
||||
'',
|
||||
'export const embeddedAssets: EmbeddedWebAsset[] = [];',
|
||||
''
|
||||
].join('\n');
|
||||
writeFileSync(outputPath, contents, 'utf-8');
|
||||
}
|
||||
|
||||
function isStubEmbeddedAssets(manifestPath: string): boolean {
|
||||
try {
|
||||
const contents = readFileSync(manifestPath, 'utf-8');
|
||||
return contents.includes('intentionally contains no embedded assets');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureEmbeddedAssetsManifest(workspaceRoot: string, includeWebAssets: boolean): void {
|
||||
const manifestPath = join(workspaceRoot, 'server', 'src', 'web', 'embeddedAssets.generated.ts');
|
||||
if (includeWebAssets) {
|
||||
if (!existsSync(manifestPath)) {
|
||||
throw new Error(
|
||||
'Missing embedded web asset manifest. Run `bun run build:web` and `cd server && bun run generate:embedded-web-assets`, or `bun run build:single-exe` from the repo root.'
|
||||
);
|
||||
}
|
||||
if (isStubEmbeddedAssets(manifestPath)) {
|
||||
throw new Error(
|
||||
'Embedded web asset manifest is a stub. Run `bun run build:web` and `cd server && bun run generate:embedded-web-assets`, or `bun run build:single-exe` from the repo root.'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
writeStubEmbeddedAssets(workspaceRoot);
|
||||
}
|
||||
|
||||
async function buildTarget(projectRoot: string, target: string, outdir: string, name: string): Promise<void> {
|
||||
const { platform, arch } = parseTarget(target);
|
||||
assertArchivesExist(projectRoot, platform, arch);
|
||||
@@ -169,10 +215,11 @@ async function main(): Promise<void> {
|
||||
const outdirArg = getArg(args, '--outdir') ?? 'dist-exe';
|
||||
const name = getArg(args, '--name') ?? 'hapi';
|
||||
const buildAll = args.includes('--all');
|
||||
const includeWebAssets = args.includes('--with-web-assets');
|
||||
|
||||
if (args.includes('--target') && !target) {
|
||||
console.error('Usage: bun run scripts/build-executable.ts [--target <bun-platform[-arch]>] [--outdir dist-exe] [--name hapi]');
|
||||
console.error(' or: bun run scripts/build-executable.ts --all [--outdir dist-exe] [--name hapi]');
|
||||
console.error('Usage: bun run scripts/build-executable.ts [--target <bun-platform[-arch]>] [--outdir dist-exe] [--name hapi] [--with-web-assets]');
|
||||
console.error(' or: bun run scripts/build-executable.ts --all [--outdir dist-exe] [--name hapi] [--with-web-assets]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -182,10 +229,13 @@ async function main(): Promise<void> {
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(scriptDir, '..');
|
||||
const workspaceRoot = join(projectRoot, '..');
|
||||
const outdir = resolveOutdir(projectRoot, outdirArg);
|
||||
const resolvedTarget = buildAll ? undefined : resolveTarget(target);
|
||||
const targets = buildAll ? DEFAULT_TARGETS : [resolvedTarget!];
|
||||
|
||||
ensureEmbeddedAssetsManifest(workspaceRoot, includeWebAssets);
|
||||
|
||||
for (const targetName of targets) {
|
||||
await buildTarget(projectRoot, targetName, outdir, name);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,19 @@ import { withBunRuntimeEnv } from './utils/bunRuntime'
|
||||
return
|
||||
}
|
||||
|
||||
if (subcommand === 'server') {
|
||||
try {
|
||||
await import('../../server/src/index')
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
|
||||
if (process.env.DEBUG) {
|
||||
console.error(error)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await ensureRuntimeAssets()
|
||||
|
||||
// If --version is passed - do not log, its likely daemon inquiring about our version
|
||||
@@ -323,6 +336,7 @@ ${chalk.bold('Usage:')}
|
||||
hapi mcp Start MCP stdio bridge
|
||||
hapi connect (not available in direct-connect mode)
|
||||
hapi notify (not available in direct-connect mode)
|
||||
hapi server Start the API + web server
|
||||
hapi daemon Manage background service that allows
|
||||
to spawn new sessions away from your computer
|
||||
hapi doctor System diagnostics & troubleshooting
|
||||
|
||||
+7
-4
@@ -6,12 +6,13 @@
|
||||
"es2022"
|
||||
],
|
||||
"jsx": "react",
|
||||
"rootDir": "src",
|
||||
"rootDir": "..",
|
||||
"experimentalDecorators": true,
|
||||
"outDir": "dist",
|
||||
"noEmit": true,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
"types": [
|
||||
"node",
|
||||
"bun-types"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
@@ -25,6 +26,8 @@
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.d.ts"
|
||||
"src/**/*.d.ts",
|
||||
"../server/src/**/*.ts",
|
||||
"../server/src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user