diff --git a/.gitignore b/.gitignore index 1170717c..8887a90a 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,7 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + + +chrome_data +core/server/live/auto-generated-components.ts diff --git a/core/build/bundler.ts b/core/build/bundler.ts index a8395ef8..4d989114 100644 --- a/core/build/bundler.ts +++ b/core/build/bundler.ts @@ -79,34 +79,18 @@ export class Bundler { let liveComponentsGenerator: any = null try { - // ๐Ÿš€ PRE-BUILD: Auto-generate Live Components registration - const generatorModule = await import('./live-components-generator') - liveComponentsGenerator = generatorModule.liveComponentsGenerator - const discoveredComponents = await liveComponentsGenerator.preBuild() - - // ๐Ÿ”Œ PRE-BUILD: Auto-generate FluxStack Plugins registration - const pluginsGeneratorModule = await import('./flux-plugins-generator') - const fluxPluginsGenerator = pluginsGeneratorModule.fluxPluginsGenerator - const discoveredPlugins = await fluxPluginsGenerator.preBuild() - + // Run pre-build steps + liveComponentsGenerator = await this.runPreBuildSteps() + // Ensure output directory exists - if (!existsSync(this.config.outDir)) { - mkdirSync(this.config.outDir, { recursive: true }) - } + this.ensureOutputDirectory() - const external = [ - "@tailwindcss/vite", - "tailwindcss", - "lightningcss", - "vite", - "@vitejs/plugin-react", - ...(this.config.external || []), - ...(options.external || []) - ] + // Get external dependencies + const external = this.getExternalDependencies(options) const buildArgs = [ - "bun", "build", - entryPoint, + "bun", "build", + entryPoint, "--outdir", this.config.outDir, "--target", this.config.target, ...external.flatMap(ext => ["--external", ext]) @@ -132,20 +116,12 @@ export class Bundler { const exitCode = await buildProcess.exited const duration = Date.now() - startTime - // ๐Ÿงน POST-BUILD: Handle auto-generated registration file - // (liveComponentsGenerator already available from above) - if (exitCode === 0) { buildLogger.success(`Server bundle completed in ${buildLogger.formatDuration(duration)}`) - - // Keep generated files for production (they're now baked into bundle) - await liveComponentsGenerator.postBuild(false) - - // Cleanup plugins registry - const pluginsGeneratorModule = await import('./flux-plugins-generator') - const fluxPluginsGenerator = pluginsGeneratorModule.fluxPluginsGenerator - await fluxPluginsGenerator.postBuild(false) - + + // Run post-build cleanup + await this.runPostBuildCleanup(liveComponentsGenerator) + return { success: true, duration, @@ -154,15 +130,10 @@ export class Bundler { } } else { buildLogger.error("Server bundle failed") - - // Restore original files since build failed - await liveComponentsGenerator.postBuild(false) - - // Restore plugins registry - const pluginsGeneratorModule = await import('./flux-plugins-generator') - const fluxPluginsGenerator = pluginsGeneratorModule.fluxPluginsGenerator - await fluxPluginsGenerator.postBuild(false) - + + // Run post-build cleanup + await this.runPostBuildCleanup(liveComponentsGenerator) + const stderr = await new Response(buildProcess.stderr).text() return { success: false, @@ -172,21 +143,139 @@ export class Bundler { } } catch (error) { const duration = Date.now() - startTime - + + // ๐Ÿงน CLEANUP: Restore original files on error + try { + await this.runPostBuildCleanup(liveComponentsGenerator) + } catch (cleanupError) { + buildLogger.warn(`Failed to cleanup generated files: ${cleanupError}`) + } + + return { + success: false, + duration, + error: error instanceof Error ? error.message : "Unknown error" + } + } + } + + async compileToExecutable(entryPoint: string, outputName: string = "app", options: BundleOptions = {}): Promise { + buildLogger.section('Executable Build', '๐Ÿ“ฆ') + + const startTime = Date.now() + let liveComponentsGenerator: any = null + + try { + // Run pre-build steps + liveComponentsGenerator = await this.runPreBuildSteps() + + // Ensure output directory exists + this.ensureOutputDirectory() + + const outputPath = join(this.config.outDir, outputName) + + // Get external dependencies + const external = this.getExternalDependencies(options) + + const buildArgs = [ + "bun", "build", + entryPoint, + "--compile", + "--outfile", outputPath, + "--target", this.config.target, + ...external.flatMap(ext => ["--external", ext]) + ] + + if (this.config.sourceMaps) { + buildArgs.push("--sourcemap") + } + + if (this.config.minify) { + buildArgs.push("--minify") + } + + // Add Windows-specific options if provided + if (options.executable?.windows) { + const winOpts = options.executable.windows + if (winOpts.hideConsole) { + buildArgs.push("--windows-hide-console") + } + if (winOpts.icon) { + buildArgs.push("--windows-icon", winOpts.icon) + } + if (winOpts.title) { + buildArgs.push("--windows-title", winOpts.title) + } + if (winOpts.publisher) { + buildArgs.push("--windows-publisher", winOpts.publisher) + } + if (winOpts.version) { + buildArgs.push("--windows-version", winOpts.version) + } + if (winOpts.description) { + buildArgs.push("--windows-description", winOpts.description) + } + if (winOpts.copyright) { + buildArgs.push("--windows-copyright", winOpts.copyright) + } + } + + // Add custom build arguments if provided + if (options.executable?.customArgs) { + buildArgs.push(...options.executable.customArgs) + } + + buildLogger.step(`Compiling ${entryPoint} to ${outputPath}...`) + + const buildProcess = spawn({ + cmd: buildArgs, + stdout: "pipe", + stderr: "pipe", + env: { + ...process.env, + NODE_ENV: 'production', + ...options.env + } + }) + + const exitCode = await buildProcess.exited + const duration = Date.now() - startTime + + if (exitCode === 0) { + buildLogger.success(`Executable compiled in ${buildLogger.formatDuration(duration)}`) + + // Run post-build cleanup + await this.runPostBuildCleanup(liveComponentsGenerator) + + return { + success: true, + duration, + outputPath, + entryPoint: outputPath + } + } else { + buildLogger.error("Executable compilation failed") + + // Run post-build cleanup + await this.runPostBuildCleanup(liveComponentsGenerator) + + const stderr = await new Response(buildProcess.stderr).text() + return { + success: false, + duration, + error: stderr || "Executable compilation failed" + } + } + } catch (error) { + const duration = Date.now() - startTime + // ๐Ÿงน CLEANUP: Restore original files on error try { - if (liveComponentsGenerator) { - await liveComponentsGenerator.postBuild(false) - } - - // Cleanup plugins registry - const pluginsGeneratorModule = await import('./flux-plugins-generator') - const fluxPluginsGenerator = pluginsGeneratorModule.fluxPluginsGenerator - await fluxPluginsGenerator.postBuild(false) + await this.runPostBuildCleanup(liveComponentsGenerator) } catch (cleanupError) { buildLogger.warn(`Failed to cleanup generated files: ${cleanupError}`) } - + return { success: false, duration, @@ -195,6 +284,61 @@ export class Bundler { } } + /** + * Get list of external dependencies that should not be bundled + */ + private getExternalDependencies(options: BundleOptions = {}): string[] { + return [ + "@tailwindcss/vite", + "tailwindcss", + "lightningcss", + "vite", + "@vitejs/plugin-react", + "rollup", + ...(this.config.external || []), + ...(options.external || []) + ] + } + + /** + * Ensure output directory exists + */ + private ensureOutputDirectory(): void { + if (!existsSync(this.config.outDir)) { + mkdirSync(this.config.outDir, { recursive: true }) + } + } + + /** + * Run pre-build steps (Live Components and Plugins generation) + */ + private async runPreBuildSteps(): Promise { + // ๐Ÿš€ PRE-BUILD: Auto-generate Live Components registration + const generatorModule = await import('./live-components-generator') + const liveComponentsGenerator = generatorModule.liveComponentsGenerator + await liveComponentsGenerator.preBuild() + + // ๐Ÿ”Œ PRE-BUILD: Auto-generate FluxStack Plugins registration + const pluginsGeneratorModule = await import('./flux-plugins-generator') + const fluxPluginsGenerator = pluginsGeneratorModule.fluxPluginsGenerator + await fluxPluginsGenerator.preBuild() + + return liveComponentsGenerator + } + + /** + * Run post-build cleanup + */ + private async runPostBuildCleanup(liveComponentsGenerator: any): Promise { + if (liveComponentsGenerator) { + await liveComponentsGenerator.postBuild(false) + } + + const pluginsGeneratorModule = await import('./flux-plugins-generator') + const fluxPluginsGenerator = pluginsGeneratorModule.fluxPluginsGenerator + await fluxPluginsGenerator.postBuild(false) + } + private async getClientAssets(): Promise { // This would analyze the build output to get asset information // For now, return empty array - can be enhanced later diff --git a/core/build/index.ts b/core/build/index.ts index 87d0a9a1..4cb9aa20 100644 --- a/core/build/index.ts +++ b/core/build/index.ts @@ -47,6 +47,10 @@ export class FluxStackBuilder { return await this.bundler.bundleServer("app/server/index.ts") } + async buildExecutable(outputName: string = "CLauncher", options?: import("../types/build").BundleOptions) { + return await this.bundler.compileToExecutable("app/server/index.ts", outputName, options) + } + async createDockerFiles() { buildLogger.section('Docker Configuration', '๐Ÿณ') diff --git a/core/cli/generators/plugin.ts b/core/cli/generators/plugin.ts index 8d53e0fc..d31b7fbd 100644 --- a/core/cli/generators/plugin.ts +++ b/core/cli/generators/plugin.ts @@ -131,7 +131,7 @@ export default {{camelName}}Config }, { path: 'plugins/{{name}}/index.ts', - content: `import type { FluxStackPlugin, PluginContext } from '@/core/types/plugin' + content: `import type { ErrorContext, FluxStack, PluginContext, RequestContext, ResponseContext } from "@/core/plugins/types" // โœ… Plugin imports its own configuration import { {{camelName}}Config } from './config' @@ -139,7 +139,7 @@ import { {{camelName}}Config } from './config' * {{pascalName}} Plugin * {{description}} */ -export class {{pascalName}}Plugin implements FluxStackPlugin { +export class {{pascalName}}Plugin implements FluxStack.Plugin { name = '{{name}}' version = '1.0.0' @@ -173,7 +173,7 @@ export class {{pascalName}}Plugin implements FluxStackPlugin { /** * Request hook - called on each request */ - async onRequest?(context: PluginContext, request: Request): Promise { + async onRequest?(context: RequestContext): Promise { if (!{{camelName}}Config.enabled) return // Add request processing logic @@ -182,7 +182,7 @@ export class {{pascalName}}Plugin implements FluxStackPlugin { /** * Response hook - called on each response */ - async onResponse?(context: PluginContext, response: Response): Promise { + async onResponse?(context: ResponseContext): Promise { if (!{{camelName}}Config.enabled) return // Add response processing logic @@ -191,8 +191,8 @@ export class {{pascalName}}Plugin implements FluxStackPlugin { /** * Error hook - called when errors occur */ - async onError?(context: PluginContext, error: Error): Promise { - console.error(\`[{{name}}] Error:\`, error) + async onError?(context: ErrorContext): Promise { + console.error(\`[{{name}}] Error:\`, context.error) // Add error handling logic } diff --git a/core/cli/index.ts b/core/cli/index.ts index 97d119b3..2eda6d66 100644 --- a/core/cli/index.ts +++ b/core/cli/index.ts @@ -498,6 +498,106 @@ Examples: await builder.buildServer() } }) + + // Build:exe command (compile to executable) + cliRegistry.register({ + name: 'build:exe', + description: 'Compile application to standalone executable', + category: 'Build', + usage: 'flux build:exe [options]', + examples: [ + 'flux build:exe # Compile to CLauncher executable', + 'flux build:exe --name MyApp # Compile with custom name', + 'flux build:exe --windows-title "My Launcher" # Set Windows executable title', + 'flux build:exe --windows-hide-console # Hide console window on Windows' + ], + options: [ + { + name: 'name', + short: 'n', + description: 'Name for the executable file', + type: 'string', + default: 'CLauncher' + }, + { + name: 'windows-hide-console', + description: 'Hide console window on Windows', + type: 'boolean', + default: false + }, + { + name: 'windows-icon', + description: 'Path to .ico file for Windows executable', + type: 'string' + }, + { + name: 'windows-title', + description: 'Product name for Windows executable', + type: 'string' + }, + { + name: 'windows-publisher', + description: 'Company name for Windows executable', + type: 'string' + }, + { + name: 'windows-version', + description: 'Version string for Windows executable (e.g. 1.2.3.4)', + type: 'string' + }, + { + name: 'windows-description', + description: 'Description for Windows executable', + type: 'string' + }, + { + name: 'windows-copyright', + description: 'Copyright string for Windows executable', + type: 'string' + } + ], + handler: async (args, options, context) => { + const config = getConfigSync() + const builder = new FluxStackBuilder(config) + + // Build executable options from CLI args + const executableOptions: import("../types/build").BundleOptions = { + executable: { + windows: { + hideConsole: options['windows-hide-console'], + icon: options['windows-icon'], + title: options['windows-title'], + publisher: options['windows-publisher'], + version: options['windows-version'], + description: options['windows-description'], + copyright: options['windows-copyright'] + } + } + } + + const result = await builder.buildExecutable(options.name, executableOptions) + + if (result.success) { + console.log(`\nโœ… Executable created successfully: ${result.outputPath}`) + + // Show applied Windows options + if (process.platform === 'win32' && Object.values(executableOptions.executable?.windows || {}).some(v => v)) { + console.log('\n๐Ÿ“ฆ Windows executable options applied:') + const winOpts = executableOptions.executable?.windows + if (winOpts?.hideConsole) console.log(' โ€ข Console window hidden') + if (winOpts?.icon) console.log(` โ€ข Icon: ${winOpts.icon}`) + if (winOpts?.title) console.log(` โ€ข Title: ${winOpts.title}`) + if (winOpts?.publisher) console.log(` โ€ข Publisher: ${winOpts.publisher}`) + if (winOpts?.version) console.log(` โ€ข Version: ${winOpts.version}`) + if (winOpts?.description) console.log(` โ€ข Description: ${winOpts.description}`) + if (winOpts?.copyright) console.log(` โ€ข Copyright: ${winOpts.copyright}`) + } + } else { + console.error(`\nโŒ Compilation failed: ${result.error}`) + process.exit(1) + } + } + }) } // Main CLI logic diff --git a/core/server/live/auto-generated-components.ts b/core/server/live/auto-generated-components.ts index 9fbefd32..f28aec1c 100644 --- a/core/server/live/auto-generated-components.ts +++ b/core/server/live/auto-generated-components.ts @@ -1,6 +1,6 @@ // ๐Ÿ”ฅ Auto-generated Live Components Registration // This file is automatically generated during build time - DO NOT EDIT MANUALLY -// Generated at: 2025-11-12T22:57:08.521Z +// Generated at: 2025-11-18T01:48:36.705Z import { LiveClockComponent } from "@/app/server/live/LiveClockComponent" import { componentRegistry } from "@/core/server/live/ComponentRegistry" diff --git a/core/types/build.ts b/core/types/build.ts index 8bbb61d1..023ee72a 100644 --- a/core/types/build.ts +++ b/core/types/build.ts @@ -83,6 +83,27 @@ export interface BundleOptions { external?: string[] minify?: boolean sourceMaps?: boolean + executable?: ExecutableOptions +} + +/** + * Options for compiling standalone executables + * Note: Cross-platform compilation is limited - executables are built for the current platform. + * To build for different platforms, run the build on that platform. + */ +export interface ExecutableOptions { + // Windows-specific options + windows?: { + hideConsole?: boolean + icon?: string + title?: string + publisher?: string + version?: string + description?: string + copyright?: string + } + // Additional custom build arguments + customArgs?: string[] } export interface OptimizationOptions { diff --git a/create-fluxstack.ts b/create-fluxstack.ts index afaa4208..c243c600 100644 --- a/create-fluxstack.ts +++ b/create-fluxstack.ts @@ -165,20 +165,20 @@ Plugins can intercept and modify requests using hooks: \`\`\`typescript // plugins/my-plugin/index.ts -import type { FluxStackPlugin, PluginContext } from '@/core/types/plugin' +import type { FluxStack, PluginContext, RequestContext, ResponseContext } from "@/core/plugins/types" -export class MyPlugin implements FluxStackPlugin { +export class MyPlugin implements FluxStack.Plugin { name = 'my-plugin' version = FLUXSTACK_VERSION // Intercept every request - async onRequest(context: PluginContext, request: Request): Promise { + async onRequest(context: PluginContext): Promise { // Example: Add custom headers const url = (() => { try { - return new URL(request.url) + return new URL(PluginContext.request.url) } catch { - const host = request.headers.get('host') || 'localhost' + const host = PluginContext.request.headers.get('host') || 'localhost' return new URL(request.url, \`http://\${host}\`) } })() @@ -192,8 +192,8 @@ export class MyPlugin implements FluxStackPlugin { } // Intercept every response - async onResponse(context: PluginContext, response: Response): Promise { - console.log(\`[\${this.name}] Response status: \${response.status}\`) + async onResponse(context: PluginContext): Promise { + console.log(\`[\${this.name}] Response status: \${PluginContext.response.status}\`) } // Handle errors diff --git a/package.json b/package.json index 6eb24cb4..3ac99163 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "build": "cross-env NODE_ENV=production bun run core/cli/index.ts build", "build:frontend": "bun run core/cli/index.ts build:frontend", "build:backend": "bun run core/cli/index.ts build:backend", + "build:exe": "cross-env NODE_ENV=production && bun run core/cli/index.ts build && bun run core/cli/index.ts build:exe", "start": "bun run core/cli/index.ts start", "start:frontend": "bun run app/client/frontend-only.ts", "start:backend": "bun run app/server/backend-only.ts", diff --git a/tsconfig.json b/tsconfig.json index a694c38d..280a8f8c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -46,6 +46,7 @@ "examples/**/*", "**/*.test.ts", "**/__tests__/**/*", - "run-env-tests.ts" + "run-env-tests.ts", + "plugins/**/scripts/**/*" ] }