create-fluxstack 1.18.0 → 1.19.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/CHANGELOG.md +132 -0
- package/app/client/src/App.tsx +7 -7
- package/app/client/src/components/AppLayout.tsx +60 -23
- package/app/client/src/components/ColorWheel.tsx +195 -0
- package/app/client/src/components/DemoPage.tsx +5 -3
- package/app/client/src/components/LiveUploadWidget.tsx +1 -1
- package/app/client/src/components/ThemePicker.tsx +307 -0
- package/app/client/src/config/theme.config.ts +127 -0
- package/app/client/src/hooks/useThemeClock.ts +66 -0
- package/app/client/src/index.css +193 -0
- package/app/client/src/lib/theme-clock.ts +201 -0
- package/app/client/src/live/AuthDemo.tsx +9 -9
- package/app/client/src/live/CounterDemo.tsx +10 -10
- package/app/client/src/live/FormDemo.tsx +8 -8
- package/app/client/src/live/PingPongDemo.tsx +10 -10
- package/app/client/src/live/RoomChatDemo.tsx +10 -10
- package/app/client/src/live/SharedCounterDemo.tsx +5 -5
- package/app/client/src/pages/ApiTestPage.tsx +5 -5
- package/app/client/src/pages/HomePage.tsx +12 -12
- package/app/server/index.ts +8 -0
- package/app/server/live/auto-generated-components.ts +1 -1
- package/app/server/live/rooms/ChatRoom.ts +13 -8
- package/app/server/routes/index.ts +20 -10
- package/core/build/index.ts +1 -1
- package/core/cli/command-registry.ts +1 -1
- package/core/cli/commands/build.ts +25 -6
- package/core/cli/commands/plugin-deps.ts +1 -2
- package/core/cli/generators/plugin.ts +433 -581
- package/core/framework/server.ts +34 -8
- package/core/index.ts +6 -5
- package/core/plugins/index.ts +71 -199
- package/core/plugins/types.ts +76 -461
- package/core/server/index.ts +1 -1
- package/core/utils/logger/startup-banner.ts +26 -4
- package/core/utils/version.ts +6 -6
- package/create-fluxstack.ts +216 -107
- package/package.json +108 -107
- package/tsconfig.json +2 -1
- package/app/client/.live-stubs/LiveAdminPanel.js +0 -15
- package/app/client/.live-stubs/LiveCounter.js +0 -9
- package/app/client/.live-stubs/LiveForm.js +0 -11
- package/app/client/.live-stubs/LiveLocalCounter.js +0 -8
- package/app/client/.live-stubs/LivePingPong.js +0 -10
- package/app/client/.live-stubs/LiveRoomChat.js +0 -11
- package/app/client/.live-stubs/LiveSharedCounter.js +0 -10
- package/app/client/.live-stubs/LiveUpload.js +0 -15
- package/core/plugins/config.ts +0 -356
- package/core/plugins/dependency-manager.ts +0 -481
- package/core/plugins/discovery.ts +0 -379
- package/core/plugins/executor.ts +0 -353
- package/core/plugins/manager.ts +0 -645
- package/core/plugins/module-resolver.ts +0 -227
- package/core/plugins/registry.ts +0 -913
- package/vitest.config.live.ts +0 -69
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plugin Discovery System
|
|
3
|
-
* Handles automatic discovery and loading of plugins from various sources
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { FluxStack, PluginManifest, PluginLoadResult, PluginDiscoveryOptions } from "./types"
|
|
7
|
-
import type { Logger } from "@core/utils/logger/index"
|
|
8
|
-
import { readdir, readFile } from "fs/promises"
|
|
9
|
-
import { join, resolve } from "path"
|
|
10
|
-
import { existsSync } from "fs"
|
|
11
|
-
|
|
12
|
-
type Plugin = FluxStack.Plugin
|
|
13
|
-
|
|
14
|
-
export interface PluginDiscoveryConfig {
|
|
15
|
-
logger?: Logger
|
|
16
|
-
baseDir?: string
|
|
17
|
-
builtInDir?: string
|
|
18
|
-
externalDir?: string
|
|
19
|
-
nodeModulesDir?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class PluginDiscovery {
|
|
23
|
-
private logger?: Logger
|
|
24
|
-
private baseDir: string
|
|
25
|
-
private builtInDir: string
|
|
26
|
-
private externalDir: string
|
|
27
|
-
private nodeModulesDir: string
|
|
28
|
-
|
|
29
|
-
constructor(config: PluginDiscoveryConfig = {}) {
|
|
30
|
-
this.logger = config.logger
|
|
31
|
-
this.baseDir = config.baseDir || process.cwd()
|
|
32
|
-
this.builtInDir = config.builtInDir || join(this.baseDir, 'core/plugins/built-in')
|
|
33
|
-
this.externalDir = config.externalDir || join(this.baseDir, 'plugins')
|
|
34
|
-
this.nodeModulesDir = config.nodeModulesDir || join(this.baseDir, 'node_modules')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Discover all available plugins
|
|
39
|
-
*/
|
|
40
|
-
async discoverAll(options: PluginDiscoveryOptions = {}): Promise<PluginLoadResult[]> {
|
|
41
|
-
const results: PluginLoadResult[] = []
|
|
42
|
-
const {
|
|
43
|
-
includeBuiltIn = true,
|
|
44
|
-
includeExternal = true
|
|
45
|
-
} = options
|
|
46
|
-
|
|
47
|
-
// Discover built-in plugins
|
|
48
|
-
if (includeBuiltIn) {
|
|
49
|
-
const builtInResults = await this.discoverBuiltInPlugins()
|
|
50
|
-
results.push(...builtInResults)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Discover external plugins
|
|
54
|
-
if (includeExternal) {
|
|
55
|
-
const externalResults = await this.discoverExternalPlugins()
|
|
56
|
-
results.push(...externalResults)
|
|
57
|
-
|
|
58
|
-
const npmResults = await this.discoverNpmPlugins()
|
|
59
|
-
results.push(...npmResults)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return results
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Discover built-in plugins
|
|
67
|
-
*/
|
|
68
|
-
async discoverBuiltInPlugins(): Promise<PluginLoadResult[]> {
|
|
69
|
-
if (!existsSync(this.builtInDir)) {
|
|
70
|
-
this.logger?.debug('Built-in plugins directory not found', { dir: this.builtInDir })
|
|
71
|
-
return []
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return this.discoverPluginsInDirectory(this.builtInDir, 'built-in')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Discover external plugins in the plugins directory
|
|
79
|
-
*/
|
|
80
|
-
async discoverExternalPlugins(): Promise<PluginLoadResult[]> {
|
|
81
|
-
if (!existsSync(this.externalDir)) {
|
|
82
|
-
this.logger?.debug('External plugins directory not found', { dir: this.externalDir })
|
|
83
|
-
return []
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return this.discoverPluginsInDirectory(this.externalDir, 'external')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Discover npm-installed plugins
|
|
91
|
-
*/
|
|
92
|
-
async discoverNpmPlugins(): Promise<PluginLoadResult[]> {
|
|
93
|
-
if (!existsSync(this.nodeModulesDir)) {
|
|
94
|
-
this.logger?.debug('Node modules directory not found', { dir: this.nodeModulesDir })
|
|
95
|
-
return []
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const results: PluginLoadResult[] = []
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const entries = await readdir(this.nodeModulesDir, { withFileTypes: true })
|
|
102
|
-
|
|
103
|
-
for (const entry of entries) {
|
|
104
|
-
if (entry.isDirectory() && entry.name.startsWith('fluxstack-plugin-')) {
|
|
105
|
-
const pluginDir = join(this.nodeModulesDir, entry.name)
|
|
106
|
-
const result = await this.loadPluginFromDirectory(pluginDir, 'npm')
|
|
107
|
-
results.push(result)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
} catch (error) {
|
|
111
|
-
this.logger?.error('Failed to discover npm plugins', { error })
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return results
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Load a specific plugin by name
|
|
119
|
-
*/
|
|
120
|
-
async loadPlugin(name: string): Promise<PluginLoadResult> {
|
|
121
|
-
// Try built-in first
|
|
122
|
-
const builtInPath = join(this.builtInDir, name)
|
|
123
|
-
if (existsSync(builtInPath)) {
|
|
124
|
-
return this.loadPluginFromDirectory(builtInPath, 'built-in')
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Try external plugins
|
|
128
|
-
const externalPath = join(this.externalDir, name)
|
|
129
|
-
if (existsSync(externalPath)) {
|
|
130
|
-
return this.loadPluginFromDirectory(externalPath, 'external')
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Try npm plugins
|
|
134
|
-
const npmPath = join(this.nodeModulesDir, `fluxstack-plugin-${name}`)
|
|
135
|
-
if (existsSync(npmPath)) {
|
|
136
|
-
return this.loadPluginFromDirectory(npmPath, 'npm')
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
success: false,
|
|
141
|
-
error: `Plugin '${name}' not found in any plugin directory`
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Discover plugins in a specific directory
|
|
147
|
-
*/
|
|
148
|
-
private async discoverPluginsInDirectory(
|
|
149
|
-
directory: string,
|
|
150
|
-
source: 'built-in' | 'external' | 'npm'
|
|
151
|
-
): Promise<PluginLoadResult[]> {
|
|
152
|
-
const results: PluginLoadResult[] = []
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
const entries = await readdir(directory, { withFileTypes: true })
|
|
156
|
-
|
|
157
|
-
for (const entry of entries) {
|
|
158
|
-
if (entry.isDirectory()) {
|
|
159
|
-
const pluginDir = join(directory, entry.name)
|
|
160
|
-
const result = await this.loadPluginFromDirectory(pluginDir, source)
|
|
161
|
-
results.push(result)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
} catch (error) {
|
|
165
|
-
this.logger?.error(`Failed to discover plugins in directory '${directory}'`, { error })
|
|
166
|
-
results.push({
|
|
167
|
-
success: false,
|
|
168
|
-
error: `Failed to scan directory: ${error instanceof Error ? error.message : String(error)}`
|
|
169
|
-
})
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return results
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Load a plugin from a specific directory
|
|
177
|
-
*/
|
|
178
|
-
private async loadPluginFromDirectory(
|
|
179
|
-
pluginDir: string,
|
|
180
|
-
source: 'built-in' | 'external' | 'npm'
|
|
181
|
-
): Promise<PluginLoadResult> {
|
|
182
|
-
try {
|
|
183
|
-
// Load manifest if it exists
|
|
184
|
-
const manifest = await this.loadPluginManifest(pluginDir)
|
|
185
|
-
|
|
186
|
-
// Find the main plugin file
|
|
187
|
-
const pluginFile = await this.findPluginFile(pluginDir)
|
|
188
|
-
if (!pluginFile) {
|
|
189
|
-
return {
|
|
190
|
-
success: false,
|
|
191
|
-
error: 'No plugin entry point found (index.ts, index.js, plugin.ts, or plugin.js)'
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Import the plugin
|
|
196
|
-
const pluginModule = await import(resolve(pluginFile))
|
|
197
|
-
const plugin: Plugin = pluginModule.default || pluginModule
|
|
198
|
-
|
|
199
|
-
if (!this.isValidPlugin(plugin)) {
|
|
200
|
-
return {
|
|
201
|
-
success: false,
|
|
202
|
-
error: 'Invalid plugin: must export a plugin object with a name property'
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Validate manifest compatibility
|
|
207
|
-
const warnings: string[] = []
|
|
208
|
-
if (manifest) {
|
|
209
|
-
const manifestWarnings = this.validateManifestCompatibility(plugin, manifest)
|
|
210
|
-
warnings.push(...manifestWarnings)
|
|
211
|
-
} else {
|
|
212
|
-
warnings.push('No plugin manifest found')
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
this.logger?.debug(`Loaded plugin '${plugin.name}' from ${source}`, {
|
|
216
|
-
plugin: plugin.name,
|
|
217
|
-
version: plugin.version,
|
|
218
|
-
source,
|
|
219
|
-
path: pluginDir
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
success: true,
|
|
224
|
-
plugin,
|
|
225
|
-
warnings
|
|
226
|
-
}
|
|
227
|
-
} catch (error) {
|
|
228
|
-
this.logger?.error(`Failed to load plugin from '${pluginDir}'`, { error })
|
|
229
|
-
return {
|
|
230
|
-
success: false,
|
|
231
|
-
error: error instanceof Error ? error.message : String(error)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Load plugin manifest from directory
|
|
238
|
-
*/
|
|
239
|
-
private async loadPluginManifest(pluginDir: string): Promise<PluginManifest | undefined> {
|
|
240
|
-
const manifestPath = join(pluginDir, 'plugin.json')
|
|
241
|
-
|
|
242
|
-
if (!existsSync(manifestPath)) {
|
|
243
|
-
// Try package.json for npm plugins
|
|
244
|
-
const packagePath = join(pluginDir, 'package.json')
|
|
245
|
-
if (existsSync(packagePath)) {
|
|
246
|
-
try {
|
|
247
|
-
const packageContent = await readFile(packagePath, 'utf-8')
|
|
248
|
-
const packageJson = JSON.parse(packageContent)
|
|
249
|
-
|
|
250
|
-
if (packageJson.fluxstack) {
|
|
251
|
-
return {
|
|
252
|
-
name: packageJson.name,
|
|
253
|
-
version: packageJson.version,
|
|
254
|
-
description: packageJson.description || '',
|
|
255
|
-
author: packageJson.author || '',
|
|
256
|
-
license: packageJson.license || '',
|
|
257
|
-
homepage: packageJson.homepage,
|
|
258
|
-
repository: packageJson.repository,
|
|
259
|
-
keywords: packageJson.keywords || [],
|
|
260
|
-
dependencies: packageJson.dependencies || {},
|
|
261
|
-
peerDependencies: packageJson.peerDependencies,
|
|
262
|
-
fluxstack: packageJson.fluxstack
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
} catch (error) {
|
|
266
|
-
this.logger?.warn(`Failed to parse package.json in '${pluginDir}'`, { error })
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return undefined
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
try {
|
|
273
|
-
const manifestContent = await readFile(manifestPath, 'utf-8')
|
|
274
|
-
return JSON.parse(manifestContent)
|
|
275
|
-
} catch (error) {
|
|
276
|
-
this.logger?.warn(`Failed to parse plugin manifest in '${pluginDir}'`, { error })
|
|
277
|
-
return undefined
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Find the main plugin file in a directory
|
|
283
|
-
*/
|
|
284
|
-
private async findPluginFile(pluginDir: string): Promise<string | null> {
|
|
285
|
-
const possibleFiles = [
|
|
286
|
-
'index.ts',
|
|
287
|
-
'index.js',
|
|
288
|
-
'plugin.ts',
|
|
289
|
-
'plugin.js',
|
|
290
|
-
'src/index.ts',
|
|
291
|
-
'src/index.js',
|
|
292
|
-
'dist/index.js'
|
|
293
|
-
]
|
|
294
|
-
|
|
295
|
-
for (const file of possibleFiles) {
|
|
296
|
-
const filePath = join(pluginDir, file)
|
|
297
|
-
if (existsSync(filePath)) {
|
|
298
|
-
return filePath
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return null
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Validate if an object is a valid plugin
|
|
307
|
-
*/
|
|
308
|
-
private isValidPlugin(plugin: unknown): plugin is Plugin {
|
|
309
|
-
if (!plugin || typeof plugin !== 'object') {
|
|
310
|
-
return false
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const pluginObj = plugin as Record<string, unknown>
|
|
314
|
-
|
|
315
|
-
if (typeof pluginObj.name !== 'string' || pluginObj.name.length === 0) {
|
|
316
|
-
return false
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const hookNames = [
|
|
320
|
-
'setup', 'onConfigLoad', 'onBeforeServerStart', 'onServerStart',
|
|
321
|
-
'onAfterServerStart', 'onBeforeServerStop', 'onServerStop',
|
|
322
|
-
'onRequest', 'onResponse', 'onError'
|
|
323
|
-
]
|
|
324
|
-
|
|
325
|
-
for (const hook of hookNames) {
|
|
326
|
-
if (hook in pluginObj && typeof pluginObj[hook] !== 'function') {
|
|
327
|
-
this.logger?.warn(`Plugin "${pluginObj.name}" has invalid hook "${hook}" (expected function, got ${typeof pluginObj[hook]})`)
|
|
328
|
-
return false
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return true
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Validate manifest compatibility with plugin
|
|
337
|
-
*/
|
|
338
|
-
private validateManifestCompatibility(plugin: Plugin, manifest: PluginManifest): string[] {
|
|
339
|
-
const warnings: string[] = []
|
|
340
|
-
|
|
341
|
-
if (plugin.name !== manifest.name) {
|
|
342
|
-
warnings.push(`Plugin name mismatch: plugin exports '${plugin.name}' but manifest declares '${manifest.name}'`)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (plugin.version && plugin.version !== manifest.version) {
|
|
346
|
-
warnings.push(`Plugin version mismatch: plugin exports '${plugin.version}' but manifest declares '${manifest.version}'`)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (plugin.dependencies && manifest.fluxstack.hooks) {
|
|
350
|
-
// Check if plugin implements the hooks declared in manifest
|
|
351
|
-
const declaredHooks = manifest.fluxstack.hooks
|
|
352
|
-
const implementedHooks = Object.keys(plugin).filter(key =>
|
|
353
|
-
key.startsWith('on') || key === 'setup'
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
for (const hook of declaredHooks) {
|
|
357
|
-
if (!implementedHooks.includes(hook)) {
|
|
358
|
-
warnings.push(`Plugin declares hook '${hook}' in manifest but doesn't implement it`)
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return warnings
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* @deprecated Unused — PluginRegistry handles discovery directly.
|
|
369
|
-
* Instantiation deferred to first access to avoid side effects at module load.
|
|
370
|
-
* Remove this export in the next major version.
|
|
371
|
-
*/
|
|
372
|
-
let _pluginDiscovery: PluginDiscovery | undefined
|
|
373
|
-
export function getPluginDiscovery(): PluginDiscovery {
|
|
374
|
-
_pluginDiscovery ??= new PluginDiscovery()
|
|
375
|
-
return _pluginDiscovery
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/** @deprecated Use getPluginDiscovery() instead */
|
|
379
|
-
export const pluginDiscovery = {} as PluginDiscovery
|