create-fluxstack 1.7.5 → 1.8.1

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 (39) hide show
  1. package/.dockerignore +82 -0
  2. package/Dockerfile +70 -0
  3. package/app/server/app.ts +20 -5
  4. package/app/server/backend-only.ts +15 -12
  5. package/app/server/index.ts +83 -96
  6. package/app/server/live/FluxStackConfig.ts +5 -5
  7. package/app/server/routes/env-test.ts +59 -0
  8. package/config/app.config.ts +2 -54
  9. package/config/client.config.ts +95 -0
  10. package/config/index.ts +57 -22
  11. package/config/monitoring.config.ts +114 -0
  12. package/config/plugins.config.ts +59 -0
  13. package/config/runtime.config.ts +0 -17
  14. package/config/server.config.ts +50 -30
  15. package/core/build/bundler.ts +17 -16
  16. package/core/build/flux-plugins-generator.ts +29 -18
  17. package/core/build/index.ts +32 -31
  18. package/core/build/live-components-generator.ts +29 -18
  19. package/core/build/optimizer.ts +37 -17
  20. package/core/cli/index.ts +6 -2
  21. package/core/config/env.ts +4 -0
  22. package/core/config/runtime-config.ts +10 -8
  23. package/core/config/schema.ts +24 -2
  24. package/core/framework/server.ts +1 -0
  25. package/core/index.ts +31 -23
  26. package/core/plugins/built-in/static/index.ts +73 -246
  27. package/core/plugins/built-in/vite/index.ts +377 -377
  28. package/core/plugins/registry.ts +22 -18
  29. package/core/server/backend-entry.ts +51 -0
  30. package/core/types/plugin.ts +6 -0
  31. package/core/utils/build-logger.ts +324 -0
  32. package/core/utils/config-schema.ts +2 -6
  33. package/core/utils/helpers.ts +14 -9
  34. package/core/utils/regenerate-files.ts +69 -0
  35. package/core/utils/version.ts +1 -1
  36. package/fluxstack.config.ts +138 -252
  37. package/package.json +2 -17
  38. package/vitest.config.ts +8 -26
  39. package/config/build.config.ts +0 -24
@@ -1,19 +1,17 @@
1
- import { join, extname } from "path"
2
- import { existsSync, statSync } from "fs"
3
- import type { FluxStack, PluginContext } from "../../types"
4
-
5
- type Plugin = FluxStack.Plugin
1
+ import { join } from "path"
2
+ import { statSync, existsSync } from "fs"
3
+ import type { Plugin, PluginContext } from "../.."
6
4
 
7
5
  export const staticPlugin: Plugin = {
8
6
  name: "static",
9
- version: "1.0.0",
10
- description: "Enhanced static file serving plugin for FluxStack with caching and compression",
7
+ version: "2.0.0",
8
+ description: "Simple and efficient static file serving plugin for FluxStack",
11
9
  author: "FluxStack Team",
12
- priority: 100, // Should run after other plugins
10
+ priority: 200, // Run after all other plugins
13
11
  category: "core",
14
12
  tags: ["static", "files", "spa"],
15
- dependencies: [], // No hard dependencies, but works with vite plugin
16
-
13
+ dependencies: [],
14
+
17
15
  configSchema: {
18
16
  type: "object",
19
17
  properties: {
@@ -23,135 +21,102 @@ export const staticPlugin: Plugin = {
23
21
  },
24
22
  publicDir: {
25
23
  type: "string",
26
- description: "Public directory for static files"
27
- },
28
- distDir: {
29
- type: "string",
30
- description: "Distribution directory for built files"
24
+ description: "Directory for static files"
31
25
  },
32
26
  indexFile: {
33
27
  type: "string",
34
28
  description: "Index file for SPA routing"
35
- },
36
- cacheControl: {
37
- type: "object",
38
- properties: {
39
- enabled: { type: "boolean" },
40
- maxAge: { type: "number" },
41
- immutable: { type: "boolean" }
42
- },
43
- description: "Cache control settings"
44
- },
45
- compression: {
46
- type: "object",
47
- properties: {
48
- enabled: { type: "boolean" },
49
- types: {
50
- type: "array",
51
- items: { type: "string" }
52
- }
53
- },
54
- description: "Compression settings"
55
- },
56
- spa: {
57
- type: "object",
58
- properties: {
59
- enabled: { type: "boolean" },
60
- fallback: { type: "string" }
61
- },
62
- description: "Single Page Application settings"
63
- },
64
- excludePaths: {
65
- type: "array",
66
- items: { type: "string" },
67
- description: "Paths to exclude from static serving"
68
29
  }
69
30
  },
70
31
  additionalProperties: false
71
32
  },
72
-
33
+
73
34
  defaultConfig: {
74
35
  enabled: true,
75
- publicDir: "public",
76
- distDir: "dist/client",
77
- indexFile: "index.html",
78
- cacheControl: {
79
- enabled: true,
80
- maxAge: 31536000, // 1 year for assets
81
- immutable: true
82
- },
83
- compression: {
84
- enabled: true,
85
- types: [".js", ".css", ".html", ".json", ".svg"]
86
- },
87
- spa: {
88
- enabled: true,
89
- fallback: "index.html"
90
- },
91
- excludePaths: []
36
+ publicDir: "./dist/client",
37
+ indexFile: "index.html"
92
38
  },
93
39
 
94
40
  setup: async (context: PluginContext) => {
95
41
  const config = getPluginConfig(context)
96
-
42
+
97
43
  if (!config.enabled) {
98
- context.logger.info('Static files plugin disabled by configuration')
44
+ context.logger.info('Static files plugin disabled')
99
45
  return
100
46
  }
101
47
 
102
- context.logger.info("Enhanced static files plugin activated", {
103
- publicDir: config.publicDir,
104
- distDir: config.distDir,
105
- spa: config.spa.enabled,
106
- compression: config.compression.enabled
48
+ context.logger.info("Static files plugin activated", {
49
+ publicDir: config.publicDir
107
50
  })
108
-
109
- // Helper function for handling both GET and HEAD requests
110
- const handleStaticRequest = async ({ request, set }: { request: Request, set: any }) => {
111
- const url = new URL(request.url)
112
-
113
- // Skip API routes
114
- if (url.pathname.startsWith(context.config.server.apiPrefix)) {
115
- return
51
+
52
+ // Static fallback handler (runs last)
53
+ const staticFallback = (c: any) => {
54
+ const req = c.request
55
+ if (!req) return
56
+
57
+ const url = new URL(req.url)
58
+ let pathname = decodeURIComponent(url.pathname)
59
+
60
+ // Determine base directory using path discovery
61
+ const isDev = context.utils.isDevelopment()
62
+ let baseDir: string
63
+
64
+ if (isDev && existsSync(config.publicDir)) {
65
+ // Development: use public directory
66
+ baseDir = config.publicDir
67
+ } else {
68
+ // Production: try paths in order of preference
69
+ if (existsSync('client')) {
70
+ // Found client/ in current directory (running from dist/)
71
+ baseDir = 'client'
72
+ } else if (existsSync('dist/client')) {
73
+ // Found dist/client/ (running from project root)
74
+ baseDir = 'dist/client'
75
+ } else {
76
+ // Fallback to configured path
77
+ baseDir = config.publicDir
78
+ }
116
79
  }
117
-
118
- // Skip excluded paths
119
- if (config.excludePaths.some((path: string) => url.pathname.startsWith(path))) {
120
- return
80
+
81
+ // Root or empty path → index.html
82
+ if (pathname === '/' || pathname === '') {
83
+ pathname = `/${config.indexFile}`
121
84
  }
122
-
85
+
86
+ const filePath = join(baseDir, pathname)
87
+
123
88
  try {
124
- // Note: Vite proxy is now handled by the Vite plugin via onBeforeRoute hook
125
- // This plugin only handles static files serving in production or fallback
126
-
127
- // Serve static files
128
- return await serveStaticFile(url.pathname, config, context, set, request.method === 'HEAD')
129
-
130
- } catch (error) {
131
- context.logger.error("Error serving static file", {
132
- path: url.pathname,
133
- error: error instanceof Error ? error.message : String(error)
134
- })
135
-
136
- set.status = 500
137
- return "Internal Server Error"
89
+ const info = statSync(filePath)
90
+
91
+ // File exists → serve it
92
+ if (info.isFile()) {
93
+ return new Response(Bun.file(filePath))
94
+ }
95
+ } catch (_) {
96
+ // File not found continue
97
+ }
98
+
99
+ // SPA fallback: serve index.html for non-file routes
100
+ const indexPath = join(baseDir, config.indexFile)
101
+ try {
102
+ statSync(indexPath) // Ensure index exists
103
+ return new Response(Bun.file(indexPath))
104
+ } catch (_) {
105
+ // Index not found → let request continue (404)
138
106
  }
139
107
  }
140
108
 
141
- // Setup static file handling in Elysia - handle both GET and HEAD
142
- context.app.get("/*", handleStaticRequest)
143
- context.app.head("/*", handleStaticRequest)
109
+ // Register as catch-all fallback (runs after all other routes)
110
+ context.app.all('*', staticFallback)
144
111
  },
145
112
 
146
113
  onServerStart: async (context: PluginContext) => {
147
114
  const config = getPluginConfig(context)
148
-
115
+
149
116
  if (config.enabled) {
150
- const mode = context.utils.isDevelopment() ? 'development' : 'production'
151
- context.logger.info(`Static files plugin ready in ${mode} mode`, {
117
+ context.logger.info(`Static files plugin ready`, {
152
118
  publicDir: config.publicDir,
153
- distDir: config.distDir,
154
- spa: config.spa.enabled
119
+ indexFile: config.indexFile
155
120
  })
156
121
  }
157
122
  }
@@ -163,143 +128,5 @@ function getPluginConfig(context: PluginContext) {
163
128
  return { ...staticPlugin.defaultConfig, ...pluginConfig }
164
129
  }
165
130
 
166
- // Serve static file
167
- async function serveStaticFile(
168
- pathname: string,
169
- config: any,
170
- context: PluginContext,
171
- set: any,
172
- isHead: boolean = false
173
- ): Promise<any> {
174
- const isDev = context.utils.isDevelopment()
175
-
176
- // Determine base directory using path discovery (no hardcoded detection)
177
- let baseDir: string
178
-
179
- if (isDev && existsSync(config.publicDir)) {
180
- // Development: use public directory
181
- baseDir = config.publicDir
182
- } else {
183
- // Production: try paths in order of preference
184
- if (existsSync('client')) {
185
- // Found client/ in current directory (running from dist/)
186
- baseDir = 'client'
187
- } else if (existsSync('dist/client')) {
188
- // Found dist/client/ (running from project root)
189
- baseDir = 'dist/client'
190
- } else {
191
- // Fallback to configured path
192
- baseDir = config.distDir
193
- }
194
- }
195
-
196
- if (!existsSync(baseDir)) {
197
- context.logger.warn(`Static directory not found: ${baseDir}`)
198
- set.status = 404
199
- return "Not Found"
200
- }
201
-
202
- // Clean pathname
203
- const cleanPath = pathname === '/' ? `/${config.indexFile}` : pathname
204
- const filePath = join(process.cwd(), baseDir, cleanPath)
205
-
206
- // Security check - prevent directory traversal
207
- const resolvedPath = join(process.cwd(), baseDir)
208
- if (!filePath.startsWith(resolvedPath)) {
209
- set.status = 403
210
- return "Forbidden"
211
- }
212
-
213
- // Check if file exists
214
- if (!existsSync(filePath)) {
215
- // For SPA, serve index.html for non-file routes
216
- if (config.spa.enabled && !pathname.includes('.')) {
217
- const indexPath = join(process.cwd(), baseDir, config.spa.fallback)
218
- if (existsSync(indexPath)) {
219
- return serveFile(indexPath, config, set, context, isHead)
220
- }
221
- }
222
-
223
- set.status = 404
224
- return "Not Found"
225
- }
226
-
227
- // Check if it's a directory
228
- const stats = statSync(filePath)
229
- if (stats.isDirectory()) {
230
- const indexPath = join(filePath, config.indexFile)
231
- if (existsSync(indexPath)) {
232
- return serveFile(indexPath, config, set, context, isHead)
233
- }
234
-
235
- set.status = 404
236
- return "Not Found"
237
- }
238
-
239
- return serveFile(filePath, config, set, context, isHead)
240
- }
241
-
242
- // Serve individual file
243
- function serveFile(filePath: string, config: any, set: any, context: PluginContext, isHead: boolean = false) {
244
- const ext = extname(filePath)
245
- const file = Bun.file(filePath)
246
-
247
- // Set content type
248
- const mimeTypes: Record<string, string> = {
249
- '.html': 'text/html',
250
- '.css': 'text/css',
251
- '.js': 'application/javascript',
252
- '.json': 'application/json',
253
- '.png': 'image/png',
254
- '.jpg': 'image/jpeg',
255
- '.jpeg': 'image/jpeg',
256
- '.gif': 'image/gif',
257
- '.svg': 'image/svg+xml',
258
- '.ico': 'image/x-icon',
259
- '.woff': 'font/woff',
260
- '.woff2': 'font/woff2',
261
- '.ttf': 'font/ttf',
262
- '.eot': 'application/vnd.ms-fontobject'
263
- }
264
-
265
- const contentType = mimeTypes[ext] || 'application/octet-stream'
266
- set.headers['Content-Type'] = contentType
267
-
268
- // Set content-length for both GET and HEAD requests
269
- set.headers['Content-Length'] = file.size.toString()
270
-
271
- // Set cache headers
272
- if (config.cacheControl.enabled) {
273
- if (ext === '.html') {
274
- // Don't cache HTML files aggressively
275
- set.headers['Cache-Control'] = 'no-cache'
276
- } else {
277
- // Cache assets aggressively
278
- const maxAge = config.cacheControl.maxAge
279
- const cacheControl = config.cacheControl.immutable
280
- ? `public, max-age=${maxAge}, immutable`
281
- : `public, max-age=${maxAge}`
282
- set.headers['Cache-Control'] = cacheControl
283
- }
284
- }
285
-
286
- // Add compression hint if enabled
287
- if (config.compression.enabled && config.compression.types.includes(ext)) {
288
- set.headers['Vary'] = 'Accept-Encoding'
289
- }
290
-
291
- context.logger.debug(`Serving static file: ${filePath}`, {
292
- contentType,
293
- size: file.size,
294
- method: isHead ? 'HEAD' : 'GET'
295
- })
296
-
297
- // For HEAD requests, return empty body but keep all headers
298
- if (isHead) {
299
- return ""
300
- }
301
-
302
- return file
303
- }
304
131
 
305
132
  export default staticPlugin