create-fluxstack 1.0.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.
- package/.env +30 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/app/client/README.md +69 -0
- package/app/client/frontend-only.ts +12 -0
- package/app/client/index.html +13 -0
- package/app/client/public/vite.svg +1 -0
- package/app/client/src/App.css +883 -0
- package/app/client/src/App.tsx +669 -0
- package/app/client/src/assets/react.svg +1 -0
- package/app/client/src/components/TestPage.tsx +453 -0
- package/app/client/src/index.css +51 -0
- package/app/client/src/lib/eden-api.ts +110 -0
- package/app/client/src/main.tsx +10 -0
- package/app/client/src/vite-env.d.ts +1 -0
- package/app/client/tsconfig.app.json +43 -0
- package/app/client/tsconfig.json +7 -0
- package/app/client/tsconfig.node.json +25 -0
- package/app/server/app.ts +10 -0
- package/app/server/backend-only.ts +15 -0
- package/app/server/controllers/users.controller.ts +69 -0
- package/app/server/index.ts +104 -0
- package/app/server/routes/index.ts +25 -0
- package/app/server/routes/users.routes.ts +121 -0
- package/app/server/types/index.ts +1 -0
- package/app/shared/types/index.ts +18 -0
- package/bun.lock +1053 -0
- package/core/__tests__/integration.test.ts +227 -0
- package/core/build/index.ts +186 -0
- package/core/cli/command-registry.ts +334 -0
- package/core/cli/index.ts +394 -0
- package/core/cli/plugin-discovery.ts +200 -0
- package/core/client/standalone.ts +57 -0
- package/core/config/__tests__/config-loader.test.ts +591 -0
- package/core/config/__tests__/config-merger.test.ts +657 -0
- package/core/config/__tests__/env-converter.test.ts +372 -0
- package/core/config/__tests__/env-processor.test.ts +431 -0
- package/core/config/__tests__/env.test.ts +452 -0
- package/core/config/__tests__/integration.test.ts +418 -0
- package/core/config/__tests__/loader.test.ts +331 -0
- package/core/config/__tests__/schema.test.ts +129 -0
- package/core/config/__tests__/validator.test.ts +318 -0
- package/core/config/env-dynamic.ts +326 -0
- package/core/config/env.ts +597 -0
- package/core/config/index.ts +317 -0
- package/core/config/loader.ts +546 -0
- package/core/config/runtime-config.ts +322 -0
- package/core/config/schema.ts +694 -0
- package/core/config/validator.ts +540 -0
- package/core/framework/__tests__/server.test.ts +233 -0
- package/core/framework/client.ts +132 -0
- package/core/framework/index.ts +8 -0
- package/core/framework/server.ts +501 -0
- package/core/framework/types.ts +63 -0
- package/core/plugins/__tests__/built-in.test.ts.disabled +366 -0
- package/core/plugins/__tests__/manager.test.ts +398 -0
- package/core/plugins/__tests__/monitoring.test.ts +401 -0
- package/core/plugins/__tests__/registry.test.ts +335 -0
- package/core/plugins/built-in/index.ts +142 -0
- package/core/plugins/built-in/logger/index.ts +180 -0
- package/core/plugins/built-in/monitoring/README.md +193 -0
- package/core/plugins/built-in/monitoring/index.ts +912 -0
- package/core/plugins/built-in/static/index.ts +289 -0
- package/core/plugins/built-in/swagger/index.ts +229 -0
- package/core/plugins/built-in/vite/index.ts +316 -0
- package/core/plugins/config.ts +348 -0
- package/core/plugins/discovery.ts +350 -0
- package/core/plugins/executor.ts +351 -0
- package/core/plugins/index.ts +195 -0
- package/core/plugins/manager.ts +583 -0
- package/core/plugins/registry.ts +424 -0
- package/core/plugins/types.ts +254 -0
- package/core/server/framework.ts +123 -0
- package/core/server/index.ts +8 -0
- package/core/server/plugins/database.ts +182 -0
- package/core/server/plugins/logger.ts +47 -0
- package/core/server/plugins/swagger.ts +34 -0
- package/core/server/standalone.ts +91 -0
- package/core/templates/create-project.ts +455 -0
- package/core/types/api.ts +169 -0
- package/core/types/build.ts +174 -0
- package/core/types/config.ts +68 -0
- package/core/types/index.ts +127 -0
- package/core/types/plugin.ts +94 -0
- package/core/utils/__tests__/errors.test.ts +139 -0
- package/core/utils/__tests__/helpers.test.ts +297 -0
- package/core/utils/__tests__/logger.test.ts +141 -0
- package/core/utils/env-runtime-v2.ts +232 -0
- package/core/utils/env-runtime.ts +252 -0
- package/core/utils/errors/codes.ts +115 -0
- package/core/utils/errors/handlers.ts +63 -0
- package/core/utils/errors/index.ts +81 -0
- package/core/utils/helpers.ts +180 -0
- package/core/utils/index.ts +18 -0
- package/core/utils/logger/index.ts +161 -0
- package/core/utils/logger.ts +106 -0
- package/core/utils/monitoring/index.ts +212 -0
- package/create-fluxstack.ts +231 -0
- package/package.json +43 -0
- package/tsconfig.json +51 -0
- package/vite.config.ts +42 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import type { Plugin, PluginManifest, PluginLoadResult, PluginDiscoveryOptions } from "./types"
|
|
2
|
+
import type { FluxStackConfig } from "../config/schema"
|
|
3
|
+
import type { Logger } from "../utils/logger/index"
|
|
4
|
+
import { FluxStackError } from "../utils/errors"
|
|
5
|
+
import { readdir, readFile } from "fs/promises"
|
|
6
|
+
import { join, resolve } from "path"
|
|
7
|
+
import { existsSync } from "fs"
|
|
8
|
+
|
|
9
|
+
export interface PluginRegistryConfig {
|
|
10
|
+
logger?: Logger
|
|
11
|
+
config?: FluxStackConfig
|
|
12
|
+
discoveryOptions?: PluginDiscoveryOptions
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class PluginRegistry {
|
|
16
|
+
private plugins: Map<string, Plugin> = new Map()
|
|
17
|
+
private manifests: Map<string, PluginManifest> = new Map()
|
|
18
|
+
private loadOrder: string[] = []
|
|
19
|
+
private dependencies: Map<string, string[]> = new Map()
|
|
20
|
+
private conflicts: string[] = []
|
|
21
|
+
private logger?: Logger
|
|
22
|
+
private config?: FluxStackConfig
|
|
23
|
+
|
|
24
|
+
constructor(options: PluginRegistryConfig = {}) {
|
|
25
|
+
this.logger = options.logger
|
|
26
|
+
this.config = options.config
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register a plugin with the registry
|
|
31
|
+
*/
|
|
32
|
+
async register(plugin: Plugin, manifest?: PluginManifest): Promise<void> {
|
|
33
|
+
if (this.plugins.has(plugin.name)) {
|
|
34
|
+
throw new FluxStackError(
|
|
35
|
+
`Plugin '${plugin.name}' is already registered`,
|
|
36
|
+
'PLUGIN_ALREADY_REGISTERED',
|
|
37
|
+
400
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate plugin structure
|
|
42
|
+
this.validatePlugin(plugin)
|
|
43
|
+
|
|
44
|
+
// Validate plugin configuration if schema is provided
|
|
45
|
+
if (plugin.configSchema && this.config?.plugins.config[plugin.name]) {
|
|
46
|
+
this.validatePluginConfig(plugin, this.config.plugins.config[plugin.name])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.plugins.set(plugin.name, plugin)
|
|
50
|
+
|
|
51
|
+
if (manifest) {
|
|
52
|
+
this.manifests.set(plugin.name, manifest)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Update dependency tracking
|
|
56
|
+
if (plugin.dependencies) {
|
|
57
|
+
this.dependencies.set(plugin.name, plugin.dependencies)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Update load order
|
|
61
|
+
this.updateLoadOrder()
|
|
62
|
+
|
|
63
|
+
this.logger?.debug(`Plugin '${plugin.name}' registered successfully`, {
|
|
64
|
+
plugin: plugin.name,
|
|
65
|
+
version: plugin.version,
|
|
66
|
+
dependencies: plugin.dependencies
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unregister a plugin from the registry
|
|
72
|
+
*/
|
|
73
|
+
unregister(name: string): void {
|
|
74
|
+
if (!this.plugins.has(name)) {
|
|
75
|
+
throw new FluxStackError(
|
|
76
|
+
`Plugin '${name}' is not registered`,
|
|
77
|
+
'PLUGIN_NOT_FOUND',
|
|
78
|
+
404
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if other plugins depend on this one
|
|
83
|
+
const dependents = this.getDependents(name)
|
|
84
|
+
if (dependents.length > 0) {
|
|
85
|
+
throw new FluxStackError(
|
|
86
|
+
`Cannot unregister plugin '${name}' because it is required by: ${dependents.join(', ')}`,
|
|
87
|
+
'PLUGIN_HAS_DEPENDENTS',
|
|
88
|
+
400
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.plugins.delete(name)
|
|
93
|
+
this.manifests.delete(name)
|
|
94
|
+
this.dependencies.delete(name)
|
|
95
|
+
this.loadOrder = this.loadOrder.filter(pluginName => pluginName !== name)
|
|
96
|
+
|
|
97
|
+
this.logger?.debug(`Plugin '${name}' unregistered successfully`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get a plugin by name
|
|
102
|
+
*/
|
|
103
|
+
get(name: string): Plugin | undefined {
|
|
104
|
+
return this.plugins.get(name)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get plugin manifest by name
|
|
109
|
+
*/
|
|
110
|
+
getManifest(name: string): PluginManifest | undefined {
|
|
111
|
+
return this.manifests.get(name)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get all registered plugins
|
|
116
|
+
*/
|
|
117
|
+
getAll(): Plugin[] {
|
|
118
|
+
return Array.from(this.plugins.values())
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get all plugin manifests
|
|
123
|
+
*/
|
|
124
|
+
getAllManifests(): PluginManifest[] {
|
|
125
|
+
return Array.from(this.manifests.values())
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get plugins in load order
|
|
130
|
+
*/
|
|
131
|
+
getLoadOrder(): string[] {
|
|
132
|
+
return [...this.loadOrder]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get plugins that depend on the specified plugin
|
|
137
|
+
*/
|
|
138
|
+
getDependents(pluginName: string): string[] {
|
|
139
|
+
const dependents: string[] = []
|
|
140
|
+
|
|
141
|
+
for (const [name, deps] of this.dependencies.entries()) {
|
|
142
|
+
if (deps.includes(pluginName)) {
|
|
143
|
+
dependents.push(name)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return dependents
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get plugin dependencies
|
|
152
|
+
*/
|
|
153
|
+
getDependencies(pluginName: string): string[] {
|
|
154
|
+
return this.dependencies.get(pluginName) || []
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if a plugin is registered
|
|
159
|
+
*/
|
|
160
|
+
has(name: string): boolean {
|
|
161
|
+
return this.plugins.has(name)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get registry statistics
|
|
166
|
+
*/
|
|
167
|
+
getStats() {
|
|
168
|
+
return {
|
|
169
|
+
totalPlugins: this.plugins.size,
|
|
170
|
+
enabledPlugins: this.config?.plugins.enabled.length || 0,
|
|
171
|
+
disabledPlugins: this.config?.plugins.disabled.length || 0,
|
|
172
|
+
conflicts: this.conflicts.length,
|
|
173
|
+
loadOrder: this.loadOrder.length
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Validate all plugin dependencies
|
|
179
|
+
*/
|
|
180
|
+
validateDependencies(): void {
|
|
181
|
+
this.conflicts = []
|
|
182
|
+
|
|
183
|
+
for (const plugin of this.plugins.values()) {
|
|
184
|
+
if (plugin.dependencies) {
|
|
185
|
+
for (const dependency of plugin.dependencies) {
|
|
186
|
+
if (!this.plugins.has(dependency)) {
|
|
187
|
+
const error = `Plugin '${plugin.name}' depends on '${dependency}' which is not registered`
|
|
188
|
+
this.conflicts.push(error)
|
|
189
|
+
this.logger?.error(error, { plugin: plugin.name, dependency })
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (this.conflicts.length > 0) {
|
|
196
|
+
throw new FluxStackError(
|
|
197
|
+
`Plugin dependency validation failed: ${this.conflicts.join('; ')}`,
|
|
198
|
+
'PLUGIN_DEPENDENCY_ERROR',
|
|
199
|
+
400,
|
|
200
|
+
{ conflicts: this.conflicts }
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Discover plugins from filesystem
|
|
207
|
+
*/
|
|
208
|
+
async discoverPlugins(options: PluginDiscoveryOptions = {}): Promise<PluginLoadResult[]> {
|
|
209
|
+
const results: PluginLoadResult[] = []
|
|
210
|
+
const {
|
|
211
|
+
directories = ['core/plugins/built-in', 'plugins', 'node_modules'],
|
|
212
|
+
patterns: _patterns = ['**/plugin.{js,ts}', '**/index.{js,ts}'],
|
|
213
|
+
includeBuiltIn: _includeBuiltIn = true,
|
|
214
|
+
includeExternal: _includeExternal = true
|
|
215
|
+
} = options
|
|
216
|
+
|
|
217
|
+
for (const directory of directories) {
|
|
218
|
+
if (!existsSync(directory)) {
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const pluginResults = await this.discoverPluginsInDirectory(directory, _patterns)
|
|
224
|
+
results.push(...pluginResults)
|
|
225
|
+
} catch (error) {
|
|
226
|
+
this.logger?.warn(`Failed to discover plugins in directory '${directory}'`, { error })
|
|
227
|
+
results.push({
|
|
228
|
+
success: false,
|
|
229
|
+
error: `Failed to scan directory: ${error instanceof Error ? error.message : String(error)}`
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return results
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Load a plugin from file path
|
|
239
|
+
*/
|
|
240
|
+
async loadPlugin(pluginPath: string): Promise<PluginLoadResult> {
|
|
241
|
+
try {
|
|
242
|
+
// Check if manifest exists
|
|
243
|
+
const manifestPath = join(pluginPath, 'plugin.json')
|
|
244
|
+
let manifest: PluginManifest | undefined
|
|
245
|
+
|
|
246
|
+
if (existsSync(manifestPath)) {
|
|
247
|
+
const manifestContent = await readFile(manifestPath, 'utf-8')
|
|
248
|
+
manifest = JSON.parse(manifestContent)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Try to import the plugin
|
|
252
|
+
const pluginModule = await import(resolve(pluginPath))
|
|
253
|
+
const plugin: Plugin = pluginModule.default || pluginModule
|
|
254
|
+
|
|
255
|
+
if (!plugin || typeof plugin !== 'object' || !plugin.name) {
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
error: 'Invalid plugin: must export a plugin object with a name property'
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Register the plugin
|
|
263
|
+
await this.register(plugin, manifest)
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
plugin,
|
|
268
|
+
warnings: manifest ? [] : ['No plugin manifest found']
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return {
|
|
272
|
+
success: false,
|
|
273
|
+
error: error instanceof Error ? error.message : String(error)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Validate plugin structure
|
|
280
|
+
*/
|
|
281
|
+
private validatePlugin(plugin: Plugin): void {
|
|
282
|
+
if (!plugin.name || typeof plugin.name !== 'string') {
|
|
283
|
+
throw new FluxStackError(
|
|
284
|
+
'Plugin must have a valid name property',
|
|
285
|
+
'INVALID_PLUGIN_STRUCTURE',
|
|
286
|
+
400
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (plugin.version && typeof plugin.version !== 'string') {
|
|
291
|
+
throw new FluxStackError(
|
|
292
|
+
'Plugin version must be a string',
|
|
293
|
+
'INVALID_PLUGIN_STRUCTURE',
|
|
294
|
+
400
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (plugin.dependencies && !Array.isArray(plugin.dependencies)) {
|
|
299
|
+
throw new FluxStackError(
|
|
300
|
+
'Plugin dependencies must be an array',
|
|
301
|
+
'INVALID_PLUGIN_STRUCTURE',
|
|
302
|
+
400
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (plugin.priority && typeof plugin.priority !== 'number') {
|
|
307
|
+
throw new FluxStackError(
|
|
308
|
+
'Plugin priority must be a number',
|
|
309
|
+
'INVALID_PLUGIN_STRUCTURE',
|
|
310
|
+
400
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Validate plugin configuration against schema
|
|
317
|
+
*/
|
|
318
|
+
private validatePluginConfig(plugin: Plugin, config: any): void {
|
|
319
|
+
if (!plugin.configSchema) {
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Basic validation - in a real implementation, you'd use a proper JSON schema validator
|
|
324
|
+
if (plugin.configSchema.required) {
|
|
325
|
+
for (const requiredField of plugin.configSchema.required) {
|
|
326
|
+
if (!(requiredField in config)) {
|
|
327
|
+
throw new FluxStackError(
|
|
328
|
+
`Plugin '${plugin.name}' configuration missing required field: ${requiredField}`,
|
|
329
|
+
'INVALID_PLUGIN_CONFIG',
|
|
330
|
+
400
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Update the load order based on dependencies and priorities
|
|
339
|
+
*/
|
|
340
|
+
private updateLoadOrder(): void {
|
|
341
|
+
const visited = new Set<string>()
|
|
342
|
+
const visiting = new Set<string>()
|
|
343
|
+
const order: string[] = []
|
|
344
|
+
|
|
345
|
+
const visit = (pluginName: string) => {
|
|
346
|
+
if (visiting.has(pluginName)) {
|
|
347
|
+
throw new FluxStackError(
|
|
348
|
+
`Circular dependency detected involving plugin '${pluginName}'`,
|
|
349
|
+
'CIRCULAR_DEPENDENCY',
|
|
350
|
+
400
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (visited.has(pluginName)) {
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
visiting.add(pluginName)
|
|
359
|
+
|
|
360
|
+
const plugin = this.plugins.get(pluginName)
|
|
361
|
+
if (plugin?.dependencies) {
|
|
362
|
+
for (const dependency of plugin.dependencies) {
|
|
363
|
+
if (this.plugins.has(dependency)) {
|
|
364
|
+
visit(dependency)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
visiting.delete(pluginName)
|
|
370
|
+
visited.add(pluginName)
|
|
371
|
+
order.push(pluginName)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Visit all plugins to build dependency order
|
|
375
|
+
for (const pluginName of this.plugins.keys()) {
|
|
376
|
+
visit(pluginName)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Sort by priority within dependency groups
|
|
380
|
+
this.loadOrder = order.sort((a, b) => {
|
|
381
|
+
const pluginA = this.plugins.get(a)
|
|
382
|
+
const pluginB = this.plugins.get(b)
|
|
383
|
+
if (!pluginA || !pluginB) return 0
|
|
384
|
+
const priorityA = typeof pluginA.priority === 'number' ? pluginA.priority : 0
|
|
385
|
+
const priorityB = typeof pluginB.priority === 'number' ? pluginB.priority : 0
|
|
386
|
+
return priorityB - priorityA
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Discover plugins in a specific directory
|
|
392
|
+
*/
|
|
393
|
+
private async discoverPluginsInDirectory(
|
|
394
|
+
directory: string,
|
|
395
|
+
_patterns: string[]
|
|
396
|
+
): Promise<PluginLoadResult[]> {
|
|
397
|
+
const results: PluginLoadResult[] = []
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const entries = await readdir(directory, { withFileTypes: true })
|
|
401
|
+
|
|
402
|
+
for (const entry of entries) {
|
|
403
|
+
if (entry.isDirectory()) {
|
|
404
|
+
const pluginDir = join(directory, entry.name)
|
|
405
|
+
|
|
406
|
+
// Check if this looks like a plugin directory
|
|
407
|
+
const hasPluginFile = existsSync(join(pluginDir, 'index.ts')) ||
|
|
408
|
+
existsSync(join(pluginDir, 'index.js')) ||
|
|
409
|
+
existsSync(join(pluginDir, 'plugin.ts')) ||
|
|
410
|
+
existsSync(join(pluginDir, 'plugin.js'))
|
|
411
|
+
|
|
412
|
+
if (hasPluginFile) {
|
|
413
|
+
const result = await this.loadPlugin(pluginDir)
|
|
414
|
+
results.push(result)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} catch (error) {
|
|
419
|
+
this.logger?.error(`Failed to read directory '${directory}'`, { error })
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return results
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { FluxStackConfig } from "../config/schema"
|
|
2
|
+
import type { Logger } from "../utils/logger/index"
|
|
3
|
+
|
|
4
|
+
export type PluginHook =
|
|
5
|
+
| 'setup'
|
|
6
|
+
| 'onServerStart'
|
|
7
|
+
| 'onServerStop'
|
|
8
|
+
| 'onRequest'
|
|
9
|
+
| 'onBeforeRoute'
|
|
10
|
+
| 'onResponse'
|
|
11
|
+
| 'onError'
|
|
12
|
+
| 'onBuild'
|
|
13
|
+
| 'onBuildComplete'
|
|
14
|
+
|
|
15
|
+
export type PluginPriority = 'highest' | 'high' | 'normal' | 'low' | 'lowest' | number
|
|
16
|
+
|
|
17
|
+
export interface PluginContext {
|
|
18
|
+
config: FluxStackConfig
|
|
19
|
+
logger: Logger
|
|
20
|
+
app: any // Elysia app
|
|
21
|
+
utils: PluginUtils
|
|
22
|
+
registry?: any // Plugin registry reference
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PluginUtils {
|
|
26
|
+
// Utility functions that plugins can use
|
|
27
|
+
createTimer: (label: string) => { end: () => number }
|
|
28
|
+
formatBytes: (bytes: number) => string
|
|
29
|
+
isProduction: () => boolean
|
|
30
|
+
isDevelopment: () => boolean
|
|
31
|
+
getEnvironment: () => string
|
|
32
|
+
createHash: (data: string) => string
|
|
33
|
+
deepMerge: (target: any, source: any) => any
|
|
34
|
+
validateSchema: (data: any, schema: any) => { valid: boolean; errors: string[] }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RequestContext {
|
|
38
|
+
request: Request
|
|
39
|
+
path: string
|
|
40
|
+
method: string
|
|
41
|
+
headers: Record<string, string>
|
|
42
|
+
query: Record<string, string>
|
|
43
|
+
params: Record<string, string>
|
|
44
|
+
body?: any
|
|
45
|
+
user?: any
|
|
46
|
+
startTime: number
|
|
47
|
+
handled?: boolean
|
|
48
|
+
response?: Response
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ResponseContext extends RequestContext {
|
|
52
|
+
response: Response
|
|
53
|
+
statusCode: number
|
|
54
|
+
duration: number
|
|
55
|
+
size?: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ErrorContext extends RequestContext {
|
|
59
|
+
error: Error
|
|
60
|
+
duration: number
|
|
61
|
+
handled: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BuildContext {
|
|
65
|
+
target: string
|
|
66
|
+
outDir: string
|
|
67
|
+
mode: 'development' | 'production'
|
|
68
|
+
config: FluxStackConfig
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PluginConfigSchema {
|
|
72
|
+
type: 'object'
|
|
73
|
+
properties: Record<string, any>
|
|
74
|
+
required?: string[]
|
|
75
|
+
additionalProperties?: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface Plugin {
|
|
79
|
+
name: string
|
|
80
|
+
version?: string
|
|
81
|
+
description?: string
|
|
82
|
+
author?: string
|
|
83
|
+
dependencies?: string[]
|
|
84
|
+
priority?: number | PluginPriority
|
|
85
|
+
|
|
86
|
+
// Lifecycle hooks
|
|
87
|
+
setup?: (context: PluginContext) => void | Promise<void>
|
|
88
|
+
onServerStart?: (context: PluginContext) => void | Promise<void>
|
|
89
|
+
onServerStop?: (context: PluginContext) => void | Promise<void>
|
|
90
|
+
onRequest?: (context: RequestContext) => void | Promise<void>
|
|
91
|
+
onBeforeRoute?: (context: RequestContext) => void | Promise<void>
|
|
92
|
+
onResponse?: (context: ResponseContext) => void | Promise<void>
|
|
93
|
+
onError?: (context: ErrorContext) => void | Promise<void>
|
|
94
|
+
onBuild?: (context: BuildContext) => void | Promise<void>
|
|
95
|
+
onBuildComplete?: (context: BuildContext) => void | Promise<void>
|
|
96
|
+
|
|
97
|
+
// CLI commands
|
|
98
|
+
commands?: CliCommand[]
|
|
99
|
+
|
|
100
|
+
// Configuration
|
|
101
|
+
configSchema?: PluginConfigSchema
|
|
102
|
+
defaultConfig?: any
|
|
103
|
+
|
|
104
|
+
// Plugin metadata
|
|
105
|
+
enabled?: boolean
|
|
106
|
+
tags?: string[]
|
|
107
|
+
category?: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface PluginManifest {
|
|
111
|
+
name: string
|
|
112
|
+
version: string
|
|
113
|
+
description: string
|
|
114
|
+
author: string
|
|
115
|
+
license: string
|
|
116
|
+
homepage?: string
|
|
117
|
+
repository?: string
|
|
118
|
+
keywords: string[]
|
|
119
|
+
dependencies: Record<string, string>
|
|
120
|
+
peerDependencies?: Record<string, string>
|
|
121
|
+
fluxstack: {
|
|
122
|
+
version: string
|
|
123
|
+
hooks: PluginHook[]
|
|
124
|
+
config?: PluginConfigSchema
|
|
125
|
+
category?: string
|
|
126
|
+
tags?: string[]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface PluginLoadResult {
|
|
131
|
+
success: boolean
|
|
132
|
+
plugin?: Plugin
|
|
133
|
+
error?: string
|
|
134
|
+
warnings?: string[]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface PluginRegistryState {
|
|
138
|
+
plugins: Map<string, Plugin>
|
|
139
|
+
manifests: Map<string, PluginManifest>
|
|
140
|
+
loadOrder: string[]
|
|
141
|
+
dependencies: Map<string, string[]>
|
|
142
|
+
conflicts: string[]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface PluginHookResult {
|
|
146
|
+
success: boolean
|
|
147
|
+
error?: Error
|
|
148
|
+
duration: number
|
|
149
|
+
plugin: string
|
|
150
|
+
hook: PluginHook
|
|
151
|
+
context?: any
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface PluginMetrics {
|
|
155
|
+
loadTime: number
|
|
156
|
+
setupTime: number
|
|
157
|
+
hookExecutions: Map<PluginHook, number>
|
|
158
|
+
errors: number
|
|
159
|
+
warnings: number
|
|
160
|
+
lastExecution?: Date
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface PluginDiscoveryOptions {
|
|
164
|
+
directories?: string[]
|
|
165
|
+
patterns?: string[]
|
|
166
|
+
includeBuiltIn?: boolean
|
|
167
|
+
includeExternal?: boolean
|
|
168
|
+
includeNpm?: boolean
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface PluginInstallOptions {
|
|
172
|
+
version?: string
|
|
173
|
+
registry?: string
|
|
174
|
+
force?: boolean
|
|
175
|
+
dev?: boolean
|
|
176
|
+
source?: 'npm' | 'git' | 'local'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface PluginExecutionContext {
|
|
180
|
+
plugin: Plugin
|
|
181
|
+
hook: PluginHook
|
|
182
|
+
startTime: number
|
|
183
|
+
timeout?: number
|
|
184
|
+
retries?: number
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface PluginValidationResult {
|
|
188
|
+
valid: boolean
|
|
189
|
+
errors: string[]
|
|
190
|
+
warnings: string[]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Plugin hook execution options
|
|
194
|
+
export interface HookExecutionOptions {
|
|
195
|
+
timeout?: number
|
|
196
|
+
parallel?: boolean
|
|
197
|
+
stopOnError?: boolean
|
|
198
|
+
retries?: number
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Plugin lifecycle events
|
|
202
|
+
export type PluginLifecycleEvent =
|
|
203
|
+
| 'plugin:registered'
|
|
204
|
+
| 'plugin:unregistered'
|
|
205
|
+
| 'plugin:enabled'
|
|
206
|
+
| 'plugin:disabled'
|
|
207
|
+
| 'plugin:error'
|
|
208
|
+
| 'hook:before'
|
|
209
|
+
| 'hook:after'
|
|
210
|
+
| 'hook:error'
|
|
211
|
+
|
|
212
|
+
// CLI Command interfaces
|
|
213
|
+
export interface CliArgument {
|
|
214
|
+
name: string
|
|
215
|
+
description: string
|
|
216
|
+
required?: boolean
|
|
217
|
+
type?: 'string' | 'number' | 'boolean'
|
|
218
|
+
default?: any
|
|
219
|
+
choices?: string[]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface CliOption {
|
|
223
|
+
name: string
|
|
224
|
+
short?: string
|
|
225
|
+
description: string
|
|
226
|
+
type?: 'string' | 'number' | 'boolean' | 'array'
|
|
227
|
+
default?: any
|
|
228
|
+
required?: boolean
|
|
229
|
+
choices?: string[]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface CliCommand {
|
|
233
|
+
name: string
|
|
234
|
+
description: string
|
|
235
|
+
usage?: string
|
|
236
|
+
examples?: string[]
|
|
237
|
+
arguments?: CliArgument[]
|
|
238
|
+
options?: CliOption[]
|
|
239
|
+
aliases?: string[]
|
|
240
|
+
category?: string
|
|
241
|
+
hidden?: boolean
|
|
242
|
+
handler: (args: any[], options: any, context: CliContext) => Promise<void> | void
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface CliContext {
|
|
246
|
+
config: FluxStackConfig
|
|
247
|
+
logger: Logger
|
|
248
|
+
utils: PluginUtils
|
|
249
|
+
workingDir: string
|
|
250
|
+
packageInfo: {
|
|
251
|
+
name: string
|
|
252
|
+
version: string
|
|
253
|
+
}
|
|
254
|
+
}
|