create-fluxstack 1.0.22 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/app/server/backend-only.ts +5 -5
  2. package/app/server/index.ts +63 -54
  3. package/app/server/live/FluxStackConfig.ts +43 -39
  4. package/app/server/live/SystemMonitorIntegration.ts +2 -2
  5. package/app/server/live/register-components.ts +6 -26
  6. package/app/server/middleware/errorHandling.ts +6 -4
  7. package/app/server/routes/config.ts +145 -0
  8. package/app/server/routes/index.ts +5 -3
  9. package/config/app.config.ts +113 -0
  10. package/config/build.config.ts +24 -0
  11. package/config/database.config.ts +99 -0
  12. package/config/index.ts +68 -0
  13. package/config/logger.config.ts +27 -0
  14. package/config/runtime.config.ts +92 -0
  15. package/config/server.config.ts +46 -0
  16. package/config/services.config.ts +130 -0
  17. package/config/system.config.ts +105 -0
  18. package/core/build/bundler.ts +53 -5
  19. package/core/build/flux-plugins-generator.ts +315 -0
  20. package/core/build/index.ts +11 -7
  21. package/core/build/live-components-generator.ts +231 -0
  22. package/core/build/optimizer.ts +2 -54
  23. package/core/cli/index.ts +31 -13
  24. package/core/config/env.ts +38 -94
  25. package/core/config/runtime-config.ts +61 -58
  26. package/core/config/schema.ts +1 -0
  27. package/core/framework/server.ts +55 -11
  28. package/core/plugins/built-in/index.ts +7 -17
  29. package/core/plugins/built-in/static/index.ts +24 -10
  30. package/core/plugins/built-in/swagger/index.ts +228 -228
  31. package/core/plugins/built-in/vite/index.ts +374 -358
  32. package/core/plugins/dependency-manager.ts +5 -5
  33. package/core/plugins/manager.ts +57 -14
  34. package/core/plugins/registry.ts +3 -3
  35. package/core/server/index.ts +0 -1
  36. package/core/server/live/ComponentRegistry.ts +34 -8
  37. package/core/server/live/LiveComponentPerformanceMonitor.ts +1 -1
  38. package/core/server/live/websocket-plugin.ts +434 -434
  39. package/core/server/middleware/README.md +488 -0
  40. package/core/server/middleware/elysia-helpers.ts +227 -0
  41. package/core/server/middleware/index.ts +25 -9
  42. package/core/server/plugins/static-files-plugin.ts +231 -231
  43. package/core/utils/config-schema.ts +484 -0
  44. package/core/utils/env.ts +306 -0
  45. package/core/utils/helpers.ts +9 -3
  46. package/core/utils/logger/colors.ts +114 -0
  47. package/core/utils/logger/config.ts +35 -0
  48. package/core/utils/logger/formatter.ts +82 -0
  49. package/core/utils/logger/group-logger.ts +101 -0
  50. package/core/utils/logger/index.ts +199 -250
  51. package/core/utils/logger/stack-trace.ts +92 -0
  52. package/core/utils/logger/startup-banner.ts +92 -0
  53. package/core/utils/logger/winston-logger.ts +152 -0
  54. package/core/utils/version.ts +5 -0
  55. package/create-fluxstack.ts +1 -0
  56. package/fluxstack.config.ts +6 -12
  57. package/package.json +117 -114
  58. package/plugins/crypto-auth/README.md +238 -0
  59. package/plugins/crypto-auth/client/CryptoAuthClient.ts +325 -0
  60. package/plugins/crypto-auth/client/components/AuthProvider.tsx +190 -0
  61. package/plugins/crypto-auth/client/components/LoginButton.tsx +155 -0
  62. package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +109 -0
  63. package/plugins/crypto-auth/client/components/SessionInfo.tsx +242 -0
  64. package/plugins/crypto-auth/client/components/index.ts +15 -0
  65. package/plugins/crypto-auth/client/index.ts +12 -0
  66. package/plugins/crypto-auth/index.ts +230 -0
  67. package/plugins/crypto-auth/package.json +65 -0
  68. package/plugins/crypto-auth/plugin.json +29 -0
  69. package/plugins/crypto-auth/server/AuthMiddleware.ts +237 -0
  70. package/plugins/crypto-auth/server/CryptoAuthService.ts +293 -0
  71. package/plugins/crypto-auth/server/index.ts +9 -0
  72. package/vite.config.ts +16 -0
  73. package/core/config/env-dynamic.ts +0 -326
  74. package/core/plugins/built-in/logger/index.ts +0 -180
  75. package/core/server/plugins/logger.ts +0 -47
  76. package/core/utils/env-runtime-v2.ts +0 -232
  77. package/core/utils/env-runtime.ts +0 -259
  78. package/core/utils/logger/formatters.ts +0 -222
  79. package/core/utils/logger/middleware.ts +0 -253
  80. package/core/utils/logger/performance.ts +0 -384
  81. package/core/utils/logger/transports.ts +0 -365
  82. package/core/utils/logger.ts +0 -106
@@ -1,18 +1,14 @@
1
1
  /**
2
2
  * Runtime Configuration System for FluxStack
3
- * Uses dynamic environment loading to solve Bun build issues
4
- * Drop-in replacement for process.env based configuration
3
+ * Uses declarative configuration system
5
4
  */
6
5
 
7
- import { env, createEnvNamespace, envValidation } from '../utils/env-runtime'
8
- import {
9
- dynamicEnvironmentProcessor,
10
- createDynamicConfig,
11
- validateProductionEnv,
12
- getDynamicEnvironmentInfo
13
- } from './env-dynamic'
6
+ import { env, createNamespace } from '../utils/env'
14
7
  import type { FluxStackConfig } from './schema'
15
8
  import { defaultFluxStackConfig } from './schema'
9
+ import { loggerConfig } from '../../config/logger.config'
10
+ import { buildConfig } from '../../config/build.config'
11
+ import { appConfig } from '../../config/app.config'
16
12
 
17
13
  /**
18
14
  * Runtime Configuration Builder
@@ -35,11 +31,21 @@ export class RuntimeConfigBuilder {
35
31
  }
36
32
 
37
33
  /**
38
- * Load from dynamic environment variables
34
+ * Load from environment variables
39
35
  */
40
36
  private loadFromDynamicEnv(): this {
41
- const envConfig = createDynamicConfig()
42
- this.config = this.deepMerge(this.config, envConfig)
37
+ // Environment vars are loaded automatically by env loader
38
+ // Just merge common overrides here
39
+ const envOverrides: Partial<FluxStackConfig> = {}
40
+
41
+ if (env.has('PORT')) {
42
+ envOverrides.server = { ...this.config.server, port: env.PORT }
43
+ }
44
+ if (env.has('LOG_LEVEL')) {
45
+ envOverrides.logging = { ...this.config.logging, level: env.LOG_LEVEL }
46
+ }
47
+
48
+ this.config = this.deepMerge(this.config, envOverrides)
43
49
  return this
44
50
  }
45
51
 
@@ -64,8 +70,8 @@ export class RuntimeConfigBuilder {
64
70
  */
65
71
  build(): FluxStackConfig {
66
72
  // Validate production environment if needed
67
- if (env.get('NODE_ENV') === 'production') {
68
- validateProductionEnv()
73
+ if (env.NODE_ENV === 'production') {
74
+ env.require(['NODE_ENV'])
69
75
  }
70
76
 
71
77
  return this.config as FluxStackConfig
@@ -140,8 +146,8 @@ export const runtimeConfig = {
140
146
  */
141
147
  development(): FluxStackConfig {
142
148
  return new RuntimeConfigBuilder()
143
- .override('logging.level', env.get('LOG_LEVEL', 'debug'))
144
- .override('logging.format', env.get('LOG_FORMAT', 'pretty'))
149
+ .override('logging.level', loggerConfig.level)
150
+ .override('logging.format', 'pretty')
145
151
  .override('build.optimization.minify', false)
146
152
  .override('build.sourceMaps', true)
147
153
  .override('monitoring.enabled', false)
@@ -153,11 +159,11 @@ export const runtimeConfig = {
153
159
  */
154
160
  production(): FluxStackConfig {
155
161
  return new RuntimeConfigBuilder()
156
- .override('logging.level', env.get('LOG_LEVEL', 'warn'))
157
- .override('logging.format', env.get('LOG_FORMAT', 'json'))
162
+ .override('logging.level', loggerConfig.level)
163
+ .override('logging.format', 'json')
158
164
  .override('build.optimization.minify', true)
159
165
  .override('build.sourceMaps', false)
160
- .override('monitoring.enabled', env.bool('MONITORING_ENABLED', true))
166
+ .override('monitoring.enabled', buildConfig.monitoringEnabled)
161
167
  .build()
162
168
  },
163
169
 
@@ -166,7 +172,7 @@ export const runtimeConfig = {
166
172
  */
167
173
  test(): FluxStackConfig {
168
174
  return new RuntimeConfigBuilder()
169
- .override('logging.level', env.get('LOG_LEVEL', 'error'))
175
+ .override('logging.level', loggerConfig.level)
170
176
  .override('server.port', 0) // Random port for tests
171
177
  .override('client.port', 0)
172
178
  .override('monitoring.enabled', false)
@@ -177,7 +183,7 @@ export const runtimeConfig = {
177
183
  * Auto-detect environment and create appropriate config
178
184
  */
179
185
  auto(overrides?: Partial<FluxStackConfig>): FluxStackConfig {
180
- const environment = env.get('NODE_ENV', 'development') as 'development' | 'production' | 'test'
186
+ const environment = appConfig.env
181
187
 
182
188
  let config: FluxStackConfig
183
189
 
@@ -214,27 +220,27 @@ export const envLoaders = {
214
220
  /**
215
221
  * Database environment loader
216
222
  */
217
- database: createEnvNamespace('DATABASE_'),
218
-
223
+ database: createNamespace('DATABASE_'),
224
+
219
225
  /**
220
226
  * JWT environment loader
221
227
  */
222
- jwt: createEnvNamespace('JWT_'),
223
-
228
+ jwt: createNamespace('JWT_'),
229
+
224
230
  /**
225
231
  * SMTP environment loader
226
232
  */
227
- smtp: createEnvNamespace('SMTP_'),
228
-
233
+ smtp: createNamespace('SMTP_'),
234
+
229
235
  /**
230
236
  * CORS environment loader
231
237
  */
232
- cors: createEnvNamespace('CORS_'),
233
-
238
+ cors: createNamespace('CORS_'),
239
+
234
240
  /**
235
241
  * FluxStack specific environment loader
236
242
  */
237
- fluxstack: createEnvNamespace('FLUXSTACK_')
243
+ fluxstack: createNamespace('FLUXSTACK_')
238
244
  }
239
245
 
240
246
  /**
@@ -245,15 +251,12 @@ export const configHelpers = {
245
251
  * Get database URL with validation
246
252
  */
247
253
  getDatabaseUrl(): string | null {
248
- const url = env.get('DATABASE_URL') as string | undefined
249
-
250
- if (url) {
251
- envValidation.validate('DATABASE_URL',
252
- (value) => value.includes('://'),
253
- 'Must be a valid URL'
254
- )
254
+ const url = env.DATABASE_URL
255
+
256
+ if (url && !url.includes('://')) {
257
+ throw new Error('DATABASE_URL must be a valid URL')
255
258
  }
256
-
259
+
257
260
  return url || null
258
261
  },
259
262
 
@@ -261,18 +264,18 @@ export const configHelpers = {
261
264
  * Get CORS origins with proper defaults
262
265
  */
263
266
  getCorsOrigins(): string[] {
264
- const origins = env.array('CORS_ORIGINS')
265
-
266
- if (origins.length === 0) {
267
- const environment = env.get('NODE_ENV', 'development') as 'development' | 'production' | 'test'
268
-
267
+ const origins = env.CORS_ORIGINS
268
+
269
+ if (origins.length === 0 || (origins.length === 1 && origins[0] === '*')) {
270
+ const environment = env.NODE_ENV
271
+
269
272
  if (environment === 'development') {
270
273
  return ['http://localhost:3000', 'http://localhost:5173']
271
274
  } else if (environment === 'production') {
272
275
  return [] // Must be explicitly configured in production
273
276
  }
274
277
  }
275
-
278
+
276
279
  return origins
277
280
  },
278
281
 
@@ -281,15 +284,15 @@ export const configHelpers = {
281
284
  */
282
285
  getServerConfig() {
283
286
  return {
284
- port: env.num('PORT', 3000),
285
- host: env.get('HOST', 'localhost'),
286
- apiPrefix: env.get('API_PREFIX', '/api'),
287
+ port: env.PORT,
288
+ host: env.HOST,
289
+ apiPrefix: env.API_PREFIX,
287
290
  cors: {
288
291
  origins: this.getCorsOrigins(),
289
- methods: env.array('CORS_METHODS', ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']),
290
- headers: env.array('CORS_HEADERS', ['Content-Type', 'Authorization']),
291
- credentials: env.bool('CORS_CREDENTIALS', false),
292
- maxAge: env.num('CORS_MAX_AGE', 86400)
292
+ methods: env.CORS_METHODS,
293
+ headers: env.CORS_HEADERS,
294
+ credentials: env.CORS_CREDENTIALS,
295
+ maxAge: env.CORS_MAX_AGE
293
296
  }
294
297
  }
295
298
  },
@@ -299,16 +302,16 @@ export const configHelpers = {
299
302
  */
300
303
  getClientConfig() {
301
304
  return {
302
- port: env.num('VITE_PORT', 5173),
305
+ port: env.VITE_PORT,
303
306
  proxy: {
304
- target: env.get('API_URL', 'http://localhost:3000'),
305
- changeOrigin: env.bool('PROXY_CHANGE_ORIGIN', true)
307
+ target: buildConfig.apiUrl,
308
+ changeOrigin: buildConfig.proxyChangeOrigin
306
309
  },
307
310
  build: {
308
- outDir: env.get('CLIENT_BUILD_DIR', 'dist/client'),
309
- sourceMaps: env.bool('CLIENT_SOURCEMAPS', env.get('NODE_ENV') === 'development'),
310
- minify: env.bool('CLIENT_MINIFY', env.get('NODE_ENV') === 'production'),
311
- target: env.get('CLIENT_TARGET', 'es2020')
311
+ outDir: buildConfig.clientBuildDir,
312
+ sourceMaps: buildConfig.clientSourceMaps,
313
+ minify: buildConfig.clientMinify,
314
+ target: buildConfig.clientTarget
312
315
  }
313
316
  }
314
317
  }
@@ -35,6 +35,7 @@ export interface ServerConfig {
35
35
  apiPrefix: string
36
36
  cors: CorsConfig
37
37
  middleware: MiddlewareConfig[]
38
+ showBanner?: boolean // Show startup banner (default: true)
38
39
  }
39
40
 
40
41
  export interface ProxyConfig {
@@ -5,6 +5,7 @@ import { PluginRegistry } from "../plugins/registry"
5
5
  import { PluginManager } from "../plugins/manager"
6
6
  import { getConfigSync, getEnvironmentInfo } from "../config"
7
7
  import { logger } from "../utils/logger"
8
+ import { displayStartupBanner, type StartupInfo } from "../utils/logger/startup-banner"
8
9
  import { createErrorHandler } from "../utils/errors/handlers"
9
10
  import { createTimer, formatBytes, isProduction, isDevelopment } from "../utils/helpers"
10
11
 
@@ -73,7 +74,7 @@ export class FluxStackFramework {
73
74
  info: (message: string, meta?: any) => logger.info(message, meta),
74
75
  warn: (message: string, meta?: any) => logger.warn(message, meta),
75
76
  error: (message: string, meta?: any) => logger.error(message, meta),
76
- child: (context: any) => (logger as any).child(context),
77
+ child: (context: any) => (logger as any).child ? (logger as any).child(context) : logger,
77
78
  time: (label: string) => (logger as any).time(label),
78
79
  timeEnd: (label: string) => (logger as any).timeEnd(label),
79
80
  request: (method: string, path: string, status?: number, duration?: number) =>
@@ -95,10 +96,11 @@ export class FluxStackFramework {
95
96
  })
96
97
 
97
98
  this.setupCors()
99
+ this.setupHeadHandler()
98
100
  this.setupHooks()
99
101
  this.setupErrorHandling()
100
102
 
101
- logger.framework('FluxStack framework initialized', {
103
+ logger.debug('FluxStack framework initialized', {
102
104
  environment: envInfo.name,
103
105
  port: fullConfig.server.port
104
106
  })
@@ -113,7 +115,7 @@ export class FluxStackFramework {
113
115
  try {
114
116
  await this.pluginManager.initialize()
115
117
  const stats = this.pluginManager.getRegistry().getStats()
116
- logger.framework('Automatic plugins loaded successfully', {
118
+ logger.debug('Automatic plugins loaded successfully', {
117
119
  pluginCount: stats.totalPlugins,
118
120
  enabledPlugins: stats.enabledPlugins,
119
121
  disabledPlugins: stats.disabledPlugins
@@ -141,6 +143,37 @@ export class FluxStackFramework {
141
143
  })
142
144
  }
143
145
 
146
+ private setupHeadHandler() {
147
+ // Global HEAD handler to prevent Elysia's automatic HEAD conversion bug
148
+ this.app.head("*", ({ request, set }) => {
149
+ const url = new URL(request.url)
150
+
151
+ // Handle API routes
152
+ if (url.pathname.startsWith(this.context.config.server.apiPrefix)) {
153
+ set.status = 200
154
+ set.headers['Content-Type'] = 'application/json'
155
+ set.headers['Content-Length'] = '0'
156
+ return ""
157
+ }
158
+
159
+ // Handle static files (assume they're HTML if no extension)
160
+ const isStatic = url.pathname === '/' || !url.pathname.includes('.')
161
+ if (isStatic) {
162
+ set.status = 200
163
+ set.headers['Content-Type'] = 'text/html'
164
+ set.headers['Content-Length'] = '478' // approximate size of index.html
165
+ set.headers['Cache-Control'] = 'no-cache'
166
+ return ""
167
+ }
168
+
169
+ // Handle other file types
170
+ set.status = 200
171
+ set.headers['Content-Type'] = 'application/octet-stream'
172
+ set.headers['Content-Length'] = '0'
173
+ return ""
174
+ })
175
+ }
176
+
144
177
  private setupHooks() {
145
178
  // Setup onRequest hook and onBeforeRoute hook
146
179
  this.app.onRequest(async ({ request, set }) => {
@@ -388,7 +421,7 @@ export class FluxStackFramework {
388
421
  ;(this.pluginRegistry as any).loadOrder = loadOrder
389
422
  }
390
423
 
391
- logger.framework(`Plugin '${plugin.name}' registered`, {
424
+ logger.debug(`Plugin '${plugin.name}' registered`, {
392
425
  version: plugin.version,
393
426
  dependencies: plugin.dependencies
394
427
  })
@@ -446,7 +479,7 @@ export class FluxStackFramework {
446
479
  }
447
480
 
448
481
  this.isStarted = true
449
- logger.framework('All plugins loaded successfully', {
482
+ logger.debug('All plugins loaded successfully', {
450
483
  pluginCount: loadOrder.length
451
484
  })
452
485
 
@@ -503,15 +536,26 @@ export class FluxStackFramework {
503
536
  const apiPrefix = this.context.config.server.apiPrefix
504
537
 
505
538
  this.app.listen(port, () => {
506
- logger.framework(`Server started on port ${port}`, {
539
+ const showBanner = this.context.config.server.showBanner !== false // default: true
540
+ const vitePluginActive = this.pluginRegistry.has('vite')
541
+
542
+ // Prepare startup info for banner or callback
543
+ const startupInfo: StartupInfo = {
544
+ port,
507
545
  apiPrefix,
508
546
  environment: this.context.environment,
509
- pluginCount: this.pluginRegistry.getAll().length
510
- })
547
+ pluginCount: this.pluginRegistry.getAll().length,
548
+ vitePort: this.context.config.client?.port,
549
+ viteEmbedded: vitePluginActive, // Vite is embedded when plugin is active
550
+ swaggerPath: '/swagger' // TODO: Get from swagger plugin config
551
+ }
552
+
553
+ // Display banner if enabled
554
+ if (showBanner) {
555
+ displayStartupBanner(startupInfo)
556
+ }
511
557
 
512
- console.log(`🚀 API ready at http://localhost:${port}${apiPrefix}`)
513
- console.log(`📋 Health check: http://localhost:${port}${apiPrefix}/health`)
514
- console.log()
558
+ // Call user callback with startup info
515
559
  callback?.()
516
560
  })
517
561
 
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Built-in Plugins for FluxStack
3
3
  * Core plugins that provide essential functionality
4
+ *
5
+ * Note: Logger is NOT a plugin - it's core infrastructure used by plugins
4
6
  */
5
7
 
6
8
  // Import all built-in plugins
7
- import { loggerPlugin } from './logger'
8
9
  import { swaggerPlugin } from './swagger'
9
10
  import { vitePlugin } from './vite'
10
11
  import { staticPlugin } from './static'
11
12
  import { monitoringPlugin } from './monitoring'
12
13
 
13
14
  // Export individual plugins
14
- export { loggerPlugin } from './logger'
15
15
  export { swaggerPlugin } from './swagger'
16
16
  export { vitePlugin } from './vite'
17
17
  export { staticPlugin } from './static'
@@ -19,7 +19,6 @@ export { monitoringPlugin } from './monitoring'
19
19
 
20
20
  // Export as a collection
21
21
  export const builtInPlugins = {
22
- logger: loggerPlugin,
23
22
  swagger: swaggerPlugin,
24
23
  vite: vitePlugin,
25
24
  static: staticPlugin,
@@ -28,7 +27,6 @@ export const builtInPlugins = {
28
27
 
29
28
  // Export as an array for easy registration
30
29
  export const builtInPluginsList = [
31
- loggerPlugin,
32
30
  swaggerPlugin,
33
31
  vitePlugin,
34
32
  staticPlugin,
@@ -37,22 +35,14 @@ export const builtInPluginsList = [
37
35
 
38
36
  // Plugin categories
39
37
  export const pluginCategories = {
40
- core: [loggerPlugin, staticPlugin],
38
+ core: [staticPlugin],
41
39
  development: [vitePlugin],
42
40
  documentation: [swaggerPlugin],
43
- monitoring: [loggerPlugin, monitoringPlugin]
41
+ monitoring: [monitoringPlugin]
44
42
  } as const
45
43
 
46
44
  // Default plugin configuration
47
45
  export const defaultPluginConfig = {
48
- logger: {
49
- logRequests: true,
50
- logResponses: true,
51
- logErrors: true,
52
- includeHeaders: false,
53
- includeBody: false,
54
- slowRequestThreshold: 1000
55
- },
56
46
  swagger: {
57
47
  enabled: true,
58
48
  path: '/swagger',
@@ -104,15 +94,15 @@ export const defaultPluginConfig = {
104
94
  * Get default plugins for a specific environment
105
95
  */
106
96
  export function getDefaultPlugins(environment: 'development' | 'production' | 'test' = 'development') {
107
- const basePlugins = [loggerPlugin, staticPlugin]
108
-
97
+ const basePlugins = [staticPlugin]
98
+
109
99
  switch (environment) {
110
100
  case 'development':
111
101
  return [...basePlugins, vitePlugin, swaggerPlugin, monitoringPlugin]
112
102
  case 'production':
113
103
  return [...basePlugins, monitoringPlugin]
114
104
  case 'test':
115
- return [loggerPlugin] // Minimal plugins for testing
105
+ return [] // Minimal plugins for testing
116
106
  default:
117
107
  return basePlugins
118
108
  }
@@ -104,8 +104,8 @@ export const staticPlugin: Plugin = {
104
104
  compression: config.compression.enabled
105
105
  })
106
106
 
107
- // Setup static file handling in Elysia
108
- context.app.get("/*", async ({ request, set }: { request: Request, set: any }) => {
107
+ // Helper function for handling both GET and HEAD requests
108
+ const handleStaticRequest = async ({ request, set }: { request: Request, set: any }) => {
109
109
  const url = new URL(request.url)
110
110
 
111
111
  // Skip API routes
@@ -123,7 +123,7 @@ export const staticPlugin: Plugin = {
123
123
  // This plugin only handles static files serving in production or fallback
124
124
 
125
125
  // Serve static files
126
- return await serveStaticFile(url.pathname, config, context, set)
126
+ return await serveStaticFile(url.pathname, config, context, set, request.method === 'HEAD')
127
127
 
128
128
  } catch (error) {
129
129
  context.logger.error("Error serving static file", {
@@ -134,7 +134,11 @@ export const staticPlugin: Plugin = {
134
134
  set.status = 500
135
135
  return "Internal Server Error"
136
136
  }
137
- })
137
+ }
138
+
139
+ // Setup static file handling in Elysia - handle both GET and HEAD
140
+ context.app.get("/*", handleStaticRequest)
141
+ context.app.head("/*", handleStaticRequest)
138
142
  },
139
143
 
140
144
  onServerStart: async (context: PluginContext) => {
@@ -162,7 +166,8 @@ async function serveStaticFile(
162
166
  pathname: string,
163
167
  config: any,
164
168
  context: PluginContext,
165
- set: any
169
+ set: any,
170
+ isHead: boolean = false
166
171
  ): Promise<any> {
167
172
  const isDev = context.utils.isDevelopment()
168
173
 
@@ -209,7 +214,7 @@ async function serveStaticFile(
209
214
  if (config.spa.enabled && !pathname.includes('.')) {
210
215
  const indexPath = join(process.cwd(), baseDir, config.spa.fallback)
211
216
  if (existsSync(indexPath)) {
212
- return serveFile(indexPath, config, set, context)
217
+ return serveFile(indexPath, config, set, context, isHead)
213
218
  }
214
219
  }
215
220
 
@@ -222,18 +227,18 @@ async function serveStaticFile(
222
227
  if (stats.isDirectory()) {
223
228
  const indexPath = join(filePath, config.indexFile)
224
229
  if (existsSync(indexPath)) {
225
- return serveFile(indexPath, config, set, context)
230
+ return serveFile(indexPath, config, set, context, isHead)
226
231
  }
227
232
 
228
233
  set.status = 404
229
234
  return "Not Found"
230
235
  }
231
236
 
232
- return serveFile(filePath, config, set, context)
237
+ return serveFile(filePath, config, set, context, isHead)
233
238
  }
234
239
 
235
240
  // Serve individual file
236
- function serveFile(filePath: string, config: any, set: any, context: PluginContext) {
241
+ function serveFile(filePath: string, config: any, set: any, context: PluginContext, isHead: boolean = false) {
237
242
  const ext = extname(filePath)
238
243
  const file = Bun.file(filePath)
239
244
 
@@ -258,6 +263,9 @@ function serveFile(filePath: string, config: any, set: any, context: PluginConte
258
263
  const contentType = mimeTypes[ext] || 'application/octet-stream'
259
264
  set.headers['Content-Type'] = contentType
260
265
 
266
+ // Set content-length for both GET and HEAD requests
267
+ set.headers['Content-Length'] = file.size.toString()
268
+
261
269
  // Set cache headers
262
270
  if (config.cacheControl.enabled) {
263
271
  if (ext === '.html') {
@@ -280,9 +288,15 @@ function serveFile(filePath: string, config: any, set: any, context: PluginConte
280
288
 
281
289
  context.logger.debug(`Serving static file: ${filePath}`, {
282
290
  contentType,
283
- size: file.size
291
+ size: file.size,
292
+ method: isHead ? 'HEAD' : 'GET'
284
293
  })
285
294
 
295
+ // For HEAD requests, return empty body but keep all headers
296
+ if (isHead) {
297
+ return ""
298
+ }
299
+
286
300
  return file
287
301
  }
288
302