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,113 @@
|
||||
import { dirname, extname, join, relative, sep } from 'node:path';
|
||||
import { existsSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.gif': 'image/gif',
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.ico': 'image/x-icon',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.map': 'application/json; charset=utf-8',
|
||||
'.mjs': 'text/javascript; charset=utf-8',
|
||||
'.otf': 'font/otf',
|
||||
'.png': 'image/png',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ttf': 'font/ttf',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.wasm': 'application/wasm',
|
||||
'.webmanifest': 'application/manifest+json; charset=utf-8',
|
||||
'.webp': 'image/webp',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.xml': 'application/xml; charset=utf-8'
|
||||
};
|
||||
|
||||
function toPosixPath(filePath: string): string {
|
||||
return filePath.split(sep).join('/');
|
||||
}
|
||||
|
||||
function listFiles(rootDir: string, currentDir = rootDir): string[] {
|
||||
const entries = readdirSync(currentDir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = join(currentDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...listFiles(rootDir, entryPath));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile()) {
|
||||
files.push(entryPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function resolveMimeType(filePath: string): string {
|
||||
const ext = extname(filePath).toLowerCase();
|
||||
return MIME_TYPES[ext] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const workspaceRoot = join(scriptDir, '..', '..');
|
||||
const webDistDir = join(workspaceRoot, 'web', 'dist');
|
||||
const outputPath = join(workspaceRoot, 'server', 'src', 'web', 'embeddedAssets.generated.ts');
|
||||
const outputDir = dirname(outputPath);
|
||||
|
||||
if (!existsSync(webDistDir)) {
|
||||
throw new Error(`Missing web/dist directory: ${webDistDir}. Run bun run build:web first.`);
|
||||
}
|
||||
|
||||
const indexHtmlPath = join(webDistDir, 'index.html');
|
||||
if (!existsSync(indexHtmlPath)) {
|
||||
throw new Error(`Missing web/dist/index.html. Run bun run build:web first.`);
|
||||
}
|
||||
|
||||
const files = listFiles(webDistDir).sort((a, b) => a.localeCompare(b));
|
||||
if (files.length === 0) {
|
||||
throw new Error(`No files found in web/dist: ${webDistDir}.`);
|
||||
}
|
||||
|
||||
const imports: string[] = [];
|
||||
const manifestLines: string[] = [];
|
||||
|
||||
files.forEach((filePath, index) => {
|
||||
const relativeToDist = toPosixPath(relative(webDistDir, filePath));
|
||||
const requestPath = `/${relativeToDist}`;
|
||||
const importPath = toPosixPath(relative(outputDir, filePath));
|
||||
const importName = `asset${index}`;
|
||||
const mimeType = resolveMimeType(filePath);
|
||||
|
||||
imports.push(`import ${importName} from '${importPath}' assert { type: 'file' };`);
|
||||
manifestLines.push(` { path: '${requestPath}', sourcePath: ${importName}, mimeType: '${mimeType}' },`);
|
||||
});
|
||||
|
||||
const output = [
|
||||
'// This file is generated by server/scripts/generate-embedded-web-assets.ts.',
|
||||
'// Do not edit by hand.',
|
||||
'',
|
||||
...imports,
|
||||
'',
|
||||
'export interface EmbeddedWebAsset {',
|
||||
' path: string;',
|
||||
' sourcePath: string;',
|
||||
' mimeType: string;',
|
||||
'}',
|
||||
'',
|
||||
'export const embeddedAssets: EmbeddedWebAsset[] = [',
|
||||
...manifestLines,
|
||||
'];',
|
||||
''
|
||||
].join('\n');
|
||||
|
||||
writeFileSync(outputPath, output, 'utf-8');
|
||||
console.log(`[embedded-assets] Wrote ${files.length} assets to ${outputPath}`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user