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.
- package/.dockerignore +82 -0
- package/Dockerfile +70 -0
- package/app/server/app.ts +20 -5
- package/app/server/backend-only.ts +15 -12
- package/app/server/index.ts +83 -96
- package/app/server/live/FluxStackConfig.ts +5 -5
- package/app/server/routes/env-test.ts +59 -0
- package/config/app.config.ts +2 -54
- package/config/client.config.ts +95 -0
- package/config/index.ts +57 -22
- package/config/monitoring.config.ts +114 -0
- package/config/plugins.config.ts +59 -0
- package/config/runtime.config.ts +0 -17
- package/config/server.config.ts +50 -30
- package/core/build/bundler.ts +17 -16
- package/core/build/flux-plugins-generator.ts +29 -18
- package/core/build/index.ts +32 -31
- package/core/build/live-components-generator.ts +29 -18
- package/core/build/optimizer.ts +37 -17
- package/core/cli/index.ts +6 -2
- package/core/config/env.ts +4 -0
- package/core/config/runtime-config.ts +10 -8
- package/core/config/schema.ts +24 -2
- package/core/framework/server.ts +1 -0
- package/core/index.ts +31 -23
- package/core/plugins/built-in/static/index.ts +73 -246
- package/core/plugins/built-in/vite/index.ts +377 -377
- package/core/plugins/registry.ts +22 -18
- package/core/server/backend-entry.ts +51 -0
- package/core/types/plugin.ts +6 -0
- package/core/utils/build-logger.ts +324 -0
- package/core/utils/config-schema.ts +2 -6
- package/core/utils/helpers.ts +14 -9
- package/core/utils/regenerate-files.ts +69 -0
- package/core/utils/version.ts +1 -1
- package/fluxstack.config.ts +138 -252
- package/package.json +2 -17
- package/vitest.config.ts +8 -26
- package/config/build.config.ts +0 -24
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { join
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
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: "
|
|
10
|
-
description: "
|
|
7
|
+
version: "2.0.0",
|
|
8
|
+
description: "Simple and efficient static file serving plugin for FluxStack",
|
|
11
9
|
author: "FluxStack Team",
|
|
12
|
-
priority:
|
|
10
|
+
priority: 200, // Run after all other plugins
|
|
13
11
|
category: "core",
|
|
14
12
|
tags: ["static", "files", "spa"],
|
|
15
|
-
dependencies: [],
|
|
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: "
|
|
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: "
|
|
76
|
-
|
|
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
|
|
44
|
+
context.logger.info('Static files plugin disabled')
|
|
99
45
|
return
|
|
100
46
|
}
|
|
101
47
|
|
|
102
|
-
context.logger.info("
|
|
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
|
-
//
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
//
|
|
119
|
-
if (
|
|
120
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
} catch (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
//
|
|
142
|
-
context.app.
|
|
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
|
-
|
|
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
|
-
|
|
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
|