create-fluxstack 1.0.13 → 1.0.15
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.example +29 -29
- package/app/client/README.md +69 -69
- package/app/client/index.html +14 -13
- package/app/client/src/App.tsx +157 -524
- package/app/client/src/components/ErrorBoundary.tsx +107 -0
- package/app/client/src/components/ErrorDisplay.css +365 -0
- package/app/client/src/components/ErrorDisplay.tsx +258 -0
- package/app/client/src/components/FluxStackConfig.tsx +1321 -0
- package/app/client/src/components/HybridLiveCounter.tsx +140 -0
- package/app/client/src/components/LiveClock.tsx +286 -0
- package/app/client/src/components/MainLayout.tsx +390 -0
- package/app/client/src/components/SidebarNavigation.tsx +391 -0
- package/app/client/src/components/StateDemo.tsx +178 -0
- package/app/client/src/components/SystemMonitor.tsx +1038 -0
- package/app/client/src/components/Teste.tsx +104 -0
- package/app/client/src/components/UserProfile.tsx +809 -0
- package/app/client/src/hooks/useAuth.ts +39 -0
- package/app/client/src/hooks/useNotifications.ts +56 -0
- package/app/client/src/lib/eden-api.ts +189 -53
- package/app/client/src/lib/errors.ts +340 -0
- package/app/client/src/lib/hooks/useErrorHandler.ts +258 -0
- package/app/client/src/lib/index.ts +45 -0
- package/app/client/src/main.tsx +3 -2
- package/app/client/src/pages/ApiDocs.tsx +182 -0
- package/app/client/src/pages/Demo.tsx +174 -0
- package/app/client/src/pages/HybridLive.tsx +263 -0
- package/app/client/src/pages/Overview.tsx +155 -0
- package/app/client/src/store/README.md +43 -0
- package/app/client/src/store/index.ts +16 -0
- package/app/client/src/store/slices/uiSlice.ts +151 -0
- package/app/client/src/store/slices/userSlice.ts +161 -0
- package/app/client/src/test/README.md +257 -0
- package/app/client/src/test/setup.ts +70 -0
- package/app/client/src/test/types.ts +12 -0
- package/app/client/src/vite-env.d.ts +1 -1
- package/app/client/tsconfig.app.json +44 -43
- package/app/client/tsconfig.json +7 -7
- package/app/client/tsconfig.node.json +25 -25
- package/app/client/zustand-setup.md +65 -0
- package/app/server/controllers/users.controller.ts +68 -68
- package/app/server/index.ts +9 -1
- package/app/server/live/CounterComponent.ts +191 -0
- package/app/server/live/FluxStackConfig.ts +529 -0
- package/app/server/live/LiveClockComponent.ts +214 -0
- package/app/server/live/SidebarNavigation.ts +156 -0
- package/app/server/live/SystemMonitor.ts +594 -0
- package/app/server/live/SystemMonitorIntegration.ts +151 -0
- package/app/server/live/TesteComponent.ts +87 -0
- package/app/server/live/UserProfileComponent.ts +135 -0
- package/app/server/live/register-components.ts +28 -0
- package/app/server/middleware/auth.ts +136 -0
- package/app/server/middleware/errorHandling.ts +250 -0
- package/app/server/middleware/index.ts +10 -0
- package/app/server/middleware/rateLimit.ts +193 -0
- package/app/server/middleware/requestLogging.ts +215 -0
- package/app/server/middleware/validation.ts +270 -0
- package/app/server/routes/index.ts +14 -2
- package/app/server/routes/upload.ts +92 -0
- package/app/server/routes/users.routes.ts +2 -9
- package/app/server/services/NotificationService.ts +302 -0
- package/app/server/services/UserService.ts +222 -0
- package/app/server/services/index.ts +46 -0
- package/core/cli/commands/plugin-deps.ts +263 -0
- package/core/cli/generators/README.md +339 -0
- package/core/cli/generators/component.ts +770 -0
- package/core/cli/generators/controller.ts +299 -0
- package/core/cli/generators/index.ts +144 -0
- package/core/cli/generators/interactive.ts +228 -0
- package/core/cli/generators/prompts.ts +83 -0
- package/core/cli/generators/route.ts +513 -0
- package/core/cli/generators/service.ts +465 -0
- package/core/cli/generators/template-engine.ts +154 -0
- package/core/cli/generators/types.ts +71 -0
- package/core/cli/generators/utils.ts +192 -0
- package/core/cli/index.ts +69 -0
- package/core/cli/plugin-discovery.ts +16 -85
- package/core/client/fluxstack.ts +17 -0
- package/core/client/hooks/index.ts +7 -0
- package/core/client/hooks/state-validator.ts +130 -0
- package/core/client/hooks/useAuth.ts +49 -0
- package/core/client/hooks/useChunkedUpload.ts +258 -0
- package/core/client/hooks/useHybridLiveComponent.ts +967 -0
- package/core/client/hooks/useWebSocket.ts +373 -0
- package/core/client/index.ts +47 -0
- package/core/client/state/createStore.ts +193 -0
- package/core/client/state/index.ts +15 -0
- package/core/config/env-dynamic.ts +1 -1
- package/core/config/env.ts +2 -1
- package/core/config/runtime-config.ts +3 -3
- package/core/config/schema.ts +84 -49
- package/core/framework/server.ts +30 -0
- package/core/index.ts +25 -0
- package/core/live/ComponentRegistry.ts +399 -0
- package/core/live/types.ts +164 -0
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +1201 -0
- package/core/plugins/built-in/live-components/index.ts +27 -0
- package/core/plugins/built-in/logger/index.ts +1 -1
- package/core/plugins/built-in/monitoring/index.ts +1 -1
- package/core/plugins/built-in/static/index.ts +1 -1
- package/core/plugins/built-in/swagger/index.ts +1 -1
- package/core/plugins/built-in/vite/index.ts +1 -1
- package/core/plugins/dependency-manager.ts +384 -0
- package/core/plugins/index.ts +5 -1
- package/core/plugins/manager.ts +7 -3
- package/core/plugins/registry.ts +88 -10
- package/core/plugins/types.ts +11 -11
- package/core/server/framework.ts +43 -0
- package/core/server/index.ts +11 -1
- package/core/server/live/ComponentRegistry.ts +1017 -0
- package/core/server/live/FileUploadManager.ts +272 -0
- package/core/server/live/LiveComponentPerformanceMonitor.ts +930 -0
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +644 -0
- package/core/server/live/WebSocketConnectionManager.ts +688 -0
- package/core/server/live/websocket-plugin.ts +435 -0
- package/core/server/middleware/errorHandling.ts +141 -0
- package/core/server/middleware/index.ts +16 -0
- package/core/server/plugins/static-files-plugin.ts +232 -0
- package/core/server/services/BaseService.ts +95 -0
- package/core/server/services/ServiceContainer.ts +144 -0
- package/core/server/services/index.ts +9 -0
- package/core/templates/create-project.ts +196 -33
- package/core/testing/index.ts +10 -0
- package/core/testing/setup.ts +74 -0
- package/core/types/build.ts +38 -14
- package/core/types/types.ts +319 -0
- package/core/utils/env-runtime.ts +7 -0
- package/core/utils/errors/handlers.ts +264 -39
- package/core/utils/errors/index.ts +528 -18
- package/core/utils/errors/middleware.ts +114 -0
- package/core/utils/logger/formatters.ts +222 -0
- package/core/utils/logger/index.ts +167 -48
- package/core/utils/logger/middleware.ts +253 -0
- package/core/utils/logger/performance.ts +384 -0
- package/core/utils/logger/transports.ts +365 -0
- package/create-fluxstack.ts +296 -296
- package/fluxstack.config.ts +17 -1
- package/package-template.json +66 -66
- package/package.json +31 -6
- package/public/README.md +16 -0
- package/vite.config.ts +29 -14
- package/.claude/settings.local.json +0 -74
- package/.github/workflows/ci-build-tests.yml +0 -480
- package/.github/workflows/dependency-management.yml +0 -324
- package/.github/workflows/release-validation.yml +0 -355
- package/.kiro/specs/fluxstack-architecture-optimization/design.md +0 -700
- package/.kiro/specs/fluxstack-architecture-optimization/requirements.md +0 -127
- package/.kiro/specs/fluxstack-architecture-optimization/tasks.md +0 -330
- package/CLAUDE.md +0 -200
- package/Dockerfile +0 -58
- package/Dockerfile.backend +0 -52
- package/Dockerfile.frontend +0 -54
- package/README-Docker.md +0 -85
- package/ai-context/00-QUICK-START.md +0 -86
- package/ai-context/README.md +0 -88
- package/ai-context/development/eden-treaty-guide.md +0 -362
- package/ai-context/development/patterns.md +0 -382
- package/ai-context/development/plugins-guide.md +0 -572
- package/ai-context/examples/crud-complete.md +0 -626
- package/ai-context/project/architecture.md +0 -399
- package/ai-context/project/overview.md +0 -213
- package/ai-context/recent-changes/eden-treaty-refactor.md +0 -281
- package/ai-context/recent-changes/type-inference-fix.md +0 -223
- package/ai-context/reference/environment-vars.md +0 -384
- package/ai-context/reference/troubleshooting.md +0 -407
- package/app/client/src/components/TestPage.tsx +0 -453
- package/bun.lock +0 -1063
- package/bunfig.toml +0 -16
- package/core/__tests__/integration.test.ts +0 -227
- package/core/build/index.ts +0 -186
- package/core/config/__tests__/config-loader.test.ts +0 -554
- package/core/config/__tests__/config-merger.test.ts +0 -657
- package/core/config/__tests__/env-converter.test.ts +0 -372
- package/core/config/__tests__/env-processor.test.ts +0 -431
- package/core/config/__tests__/env.test.ts +0 -452
- package/core/config/__tests__/integration.test.ts +0 -418
- package/core/config/__tests__/loader.test.ts +0 -331
- package/core/config/__tests__/schema.test.ts +0 -129
- package/core/config/__tests__/validator.test.ts +0 -318
- package/core/framework/__tests__/server.test.ts +0 -233
- package/core/plugins/__tests__/built-in.test.ts.disabled +0 -366
- package/core/plugins/__tests__/manager.test.ts +0 -398
- package/core/plugins/__tests__/monitoring.test.ts +0 -401
- package/core/plugins/__tests__/registry.test.ts +0 -335
- package/core/utils/__tests__/errors.test.ts +0 -139
- package/core/utils/__tests__/helpers.test.ts +0 -297
- package/core/utils/__tests__/logger.test.ts +0 -141
- package/create-test-app.ts +0 -156
- package/docker-compose.microservices.yml +0 -75
- package/docker-compose.simple.yml +0 -57
- package/docker-compose.yml +0 -71
- package/eslint.config.js +0 -23
- package/flux-cli.ts +0 -214
- package/nginx-lb.conf +0 -37
- package/publish.sh +0 -63
- package/run-clean.ts +0 -26
- package/run-env-tests.ts +0 -313
- package/tailwind.config.js +0 -34
- package/tests/__mocks__/api.ts +0 -56
- package/tests/fixtures/users.ts +0 -69
- package/tests/integration/api/users.routes.test.ts +0 -221
- package/tests/setup.ts +0 -29
- package/tests/unit/app/client/App-simple.test.tsx +0 -56
- package/tests/unit/app/client/App.test.tsx.skip +0 -237
- package/tests/unit/app/client/eden-api.test.ts +0 -186
- package/tests/unit/app/client/simple.test.tsx +0 -23
- package/tests/unit/app/controllers/users.controller.test.ts +0 -150
- package/tests/unit/core/create-project.test.ts.skip +0 -95
- package/tests/unit/core/framework.test.ts +0 -144
- package/tests/unit/core/plugins/logger.test.ts.skip +0 -268
- package/tests/unit/core/plugins/vite.test.ts.disabled +0 -188
- package/tests/utils/test-helpers.ts +0 -61
- package/vitest.config.ts +0 -50
- package/workspace.json +0 -6
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import type { Generator } from "./index.js"
|
|
2
|
+
import type { GeneratorContext, GeneratorOptions, Template } from "./types.js"
|
|
3
|
+
import { templateEngine } from "./template-engine.js"
|
|
4
|
+
|
|
5
|
+
export class ServiceGenerator implements Generator {
|
|
6
|
+
name = 'service'
|
|
7
|
+
description = 'Generate a new service with business logic'
|
|
8
|
+
|
|
9
|
+
async generate(context: GeneratorContext, options: GeneratorOptions): Promise<void> {
|
|
10
|
+
const template = this.getTemplate(options.template)
|
|
11
|
+
|
|
12
|
+
if (template.hooks?.beforeGenerate) {
|
|
13
|
+
await template.hooks.beforeGenerate(context, options)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const files = await templateEngine.processTemplate(template, context, options)
|
|
17
|
+
|
|
18
|
+
if (options.dryRun) {
|
|
19
|
+
console.log(`\n📋 Would generate service '${options.name}':\n`)
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
console.log(`${file.action === 'create' ? '📄' : '✏️'} ${file.path}`)
|
|
22
|
+
}
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await templateEngine.generateFiles(files, options.dryRun)
|
|
27
|
+
|
|
28
|
+
if (template.hooks?.afterGenerate) {
|
|
29
|
+
const filePaths = files.map(f => f.path)
|
|
30
|
+
await template.hooks.afterGenerate(context, options, filePaths)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`\n✅ Generated service '${options.name}' with ${files.length} files`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private getTemplate(templateName?: string): Template {
|
|
37
|
+
switch (templateName) {
|
|
38
|
+
case 'minimal':
|
|
39
|
+
return this.getMinimalTemplate()
|
|
40
|
+
case 'repository':
|
|
41
|
+
return this.getRepositoryTemplate()
|
|
42
|
+
case 'crud':
|
|
43
|
+
default:
|
|
44
|
+
return this.getCrudTemplate()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private getCrudTemplate(): Template {
|
|
49
|
+
return {
|
|
50
|
+
name: 'crud-service',
|
|
51
|
+
description: 'Full CRUD service with validation and error handling',
|
|
52
|
+
files: [
|
|
53
|
+
{
|
|
54
|
+
path: 'app/server/services/{{kebabName}}.service.ts',
|
|
55
|
+
content: `import { {{pascalName}}, Create{{pascalName}}, Update{{pascalName}} } from '../schemas/{{kebabName}}.schema'
|
|
56
|
+
import { {{pascalName}}Repository } from '../repositories/{{kebabName}}.repository'
|
|
57
|
+
import { ValidationError, NotFoundError } from '../../../core/utils/errors'
|
|
58
|
+
import { logger } from '../../../core/utils/logger'
|
|
59
|
+
|
|
60
|
+
export class {{pascalName}}Service {
|
|
61
|
+
private repository: {{pascalName}}Repository
|
|
62
|
+
|
|
63
|
+
constructor() {
|
|
64
|
+
this.repository = new {{pascalName}}Repository()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async findAll(): Promise<{{pascalName}}[]> {
|
|
68
|
+
try {
|
|
69
|
+
logger.debug('Fetching all {{camelName}}s')
|
|
70
|
+
const {{camelName}}s = await this.repository.findAll()
|
|
71
|
+
logger.info(\`Found \${{{camelName}}s.length} {{camelName}}s\`)
|
|
72
|
+
return {{camelName}}s
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger.error('Failed to fetch {{camelName}}s', { error })
|
|
75
|
+
throw new Error('Failed to fetch {{camelName}}s')
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async findById(id: string): Promise<{{pascalName}} | null> {
|
|
80
|
+
try {
|
|
81
|
+
this.validateId(id)
|
|
82
|
+
|
|
83
|
+
logger.debug(\`Fetching {{camelName}} with id: \${id}\`)
|
|
84
|
+
const {{camelName}} = await this.repository.findById(id)
|
|
85
|
+
|
|
86
|
+
if (!{{camelName}}) {
|
|
87
|
+
logger.warn(\`{{pascalName}} not found with id: \${id}\`)
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
logger.info(\`Found {{camelName}}: \${{{camelName}}.name}\`)
|
|
92
|
+
return {{camelName}}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof ValidationError) {
|
|
95
|
+
throw error
|
|
96
|
+
}
|
|
97
|
+
logger.error(\`Failed to fetch {{camelName}} with id: \${id}\`, { error })
|
|
98
|
+
throw new Error('Failed to fetch {{camelName}}')
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async create(data: Create{{pascalName}}): Promise<{{pascalName}}> {
|
|
103
|
+
try {
|
|
104
|
+
this.validateCreateData(data)
|
|
105
|
+
|
|
106
|
+
logger.debug('Creating new {{camelName}}', { data })
|
|
107
|
+
|
|
108
|
+
// Check for duplicates
|
|
109
|
+
const existing = await this.repository.findByName(data.name)
|
|
110
|
+
if (existing) {
|
|
111
|
+
throw new ValidationError('{{pascalName}} with this name already exists')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const {{camelName}} = await this.repository.create({
|
|
115
|
+
...data,
|
|
116
|
+
id: this.generateId(),
|
|
117
|
+
createdAt: new Date(),
|
|
118
|
+
updatedAt: new Date()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
logger.info(\`Created {{camelName}}: \${{{camelName}}.name}\`, { id: {{camelName}}.id })
|
|
122
|
+
return {{camelName}}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof ValidationError) {
|
|
125
|
+
throw error
|
|
126
|
+
}
|
|
127
|
+
logger.error('Failed to create {{camelName}}', { error, data })
|
|
128
|
+
throw new Error('Failed to create {{camelName}}')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async update(id: string, data: Update{{pascalName}}): Promise<{{pascalName}} | null> {
|
|
133
|
+
try {
|
|
134
|
+
this.validateId(id)
|
|
135
|
+
this.validateUpdateData(data)
|
|
136
|
+
|
|
137
|
+
logger.debug(\`Updating {{camelName}} with id: \${id}\`, { data })
|
|
138
|
+
|
|
139
|
+
const existing = await this.repository.findById(id)
|
|
140
|
+
if (!existing) {
|
|
141
|
+
logger.warn(\`{{pascalName}} not found for update with id: \${id}\`)
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check for name conflicts if name is being updated
|
|
146
|
+
if (data.name && data.name !== existing.name) {
|
|
147
|
+
const nameConflict = await this.repository.findByName(data.name)
|
|
148
|
+
if (nameConflict && nameConflict.id !== id) {
|
|
149
|
+
throw new ValidationError('{{pascalName}} with this name already exists')
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const updated{{pascalName}} = await this.repository.update(id, {
|
|
154
|
+
...data,
|
|
155
|
+
updatedAt: new Date()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
logger.info(\`Updated {{camelName}}: \${updated{{pascalName}}?.name}\`, { id })
|
|
159
|
+
return updated{{pascalName}}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof ValidationError) {
|
|
162
|
+
throw error
|
|
163
|
+
}
|
|
164
|
+
logger.error(\`Failed to update {{camelName}} with id: \${id}\`, { error, data })
|
|
165
|
+
throw new Error('Failed to update {{camelName}}')
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async delete(id: string): Promise<boolean> {
|
|
170
|
+
try {
|
|
171
|
+
this.validateId(id)
|
|
172
|
+
|
|
173
|
+
logger.debug(\`Deleting {{camelName}} with id: \${id}\`)
|
|
174
|
+
|
|
175
|
+
const existing = await this.repository.findById(id)
|
|
176
|
+
if (!existing) {
|
|
177
|
+
logger.warn(\`{{pascalName}} not found for deletion with id: \${id}\`)
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const deleted = await this.repository.delete(id)
|
|
182
|
+
|
|
183
|
+
if (deleted) {
|
|
184
|
+
logger.info(\`Deleted {{camelName}}: \${existing.name}\`, { id })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return deleted
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof ValidationError) {
|
|
190
|
+
throw error
|
|
191
|
+
}
|
|
192
|
+
logger.error(\`Failed to delete {{camelName}} with id: \${id}\`, { error })
|
|
193
|
+
throw new Error('Failed to delete {{camelName}}')
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async search(query: string): Promise<{{pascalName}}[]> {
|
|
198
|
+
try {
|
|
199
|
+
if (!query || query.trim().length === 0) {
|
|
200
|
+
throw new ValidationError('Search query cannot be empty')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
logger.debug(\`Searching {{camelName}}s with query: \${query}\`)
|
|
204
|
+
const results = await this.repository.search(query.trim())
|
|
205
|
+
logger.info(\`Found \${results.length} {{camelName}}s matching query: \${query}\`)
|
|
206
|
+
|
|
207
|
+
return results
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error instanceof ValidationError) {
|
|
210
|
+
throw error
|
|
211
|
+
}
|
|
212
|
+
logger.error(\`Failed to search {{camelName}}s with query: \${query}\`, { error })
|
|
213
|
+
throw new Error('Failed to search {{camelName}}s')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private validateId(id: string): void {
|
|
218
|
+
if (!id || typeof id !== 'string' || id.trim().length === 0) {
|
|
219
|
+
throw new ValidationError('Invalid ID provided')
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private validateCreateData(data: Create{{pascalName}}): void {
|
|
224
|
+
if (!data) {
|
|
225
|
+
throw new ValidationError('{{pascalName}} data is required')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!data.name || typeof data.name !== 'string' || data.name.trim().length === 0) {
|
|
229
|
+
throw new ValidationError('{{pascalName}} name is required')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (data.name.length > 100) {
|
|
233
|
+
throw new ValidationError('{{pascalName}} name must be less than 100 characters')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (data.description && data.description.length > 500) {
|
|
237
|
+
throw new ValidationError('{{pascalName}} description must be less than 500 characters')
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private validateUpdateData(data: Update{{pascalName}}): void {
|
|
242
|
+
if (!data || Object.keys(data).length === 0) {
|
|
243
|
+
throw new ValidationError('Update data is required')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (data.name !== undefined) {
|
|
247
|
+
if (!data.name || typeof data.name !== 'string' || data.name.trim().length === 0) {
|
|
248
|
+
throw new ValidationError('{{pascalName}} name cannot be empty')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (data.name.length > 100) {
|
|
252
|
+
throw new ValidationError('{{pascalName}} name must be less than 100 characters')
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (data.description !== undefined && data.description && data.description.length > 500) {
|
|
257
|
+
throw new ValidationError('{{pascalName}} description must be less than 500 characters')
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private generateId(): string {
|
|
262
|
+
return \`{{kebabName}}_\${Date.now()}_\${Math.random().toString(36).substr(2, 9)}\`
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
`
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
path: 'app/server/repositories/{{kebabName}}.repository.ts',
|
|
269
|
+
content: `import { {{pascalName}} } from '../schemas/{{kebabName}}.schema'
|
|
270
|
+
|
|
271
|
+
// In-memory storage for demo purposes
|
|
272
|
+
// Replace with your preferred database implementation
|
|
273
|
+
let {{camelName}}Store: {{pascalName}}[] = []
|
|
274
|
+
|
|
275
|
+
export class {{pascalName}}Repository {
|
|
276
|
+
async findAll(): Promise<{{pascalName}}[]> {
|
|
277
|
+
return [...{{camelName}}Store]
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async findById(id: string): Promise<{{pascalName}} | null> {
|
|
281
|
+
const {{camelName}} = {{camelName}}Store.find(item => item.id === id)
|
|
282
|
+
return {{camelName}} || null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async findByName(name: string): Promise<{{pascalName}} | null> {
|
|
286
|
+
const {{camelName}} = {{camelName}}Store.find(item =>
|
|
287
|
+
item.name.toLowerCase() === name.toLowerCase()
|
|
288
|
+
)
|
|
289
|
+
return {{camelName}} || null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async create(data: {{pascalName}}): Promise<{{pascalName}}> {
|
|
293
|
+
{{camelName}}Store.push(data)
|
|
294
|
+
return data
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async update(id: string, data: Partial<{{pascalName}}>): Promise<{{pascalName}} | null> {
|
|
298
|
+
const index = {{camelName}}Store.findIndex(item => item.id === id)
|
|
299
|
+
|
|
300
|
+
if (index === -1) {
|
|
301
|
+
return null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
{{camelName}}Store[index] = { ...{{camelName}}Store[index], ...data }
|
|
305
|
+
return {{camelName}}Store[index]
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async delete(id: string): Promise<boolean> {
|
|
309
|
+
const index = {{camelName}}Store.findIndex(item => item.id === id)
|
|
310
|
+
|
|
311
|
+
if (index === -1) {
|
|
312
|
+
return false
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
{{camelName}}Store.splice(index, 1)
|
|
316
|
+
return true
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async search(query: string): Promise<{{pascalName}}[]> {
|
|
320
|
+
const lowerQuery = query.toLowerCase()
|
|
321
|
+
return {{camelName}}Store.filter(item =>
|
|
322
|
+
item.name.toLowerCase().includes(lowerQuery) ||
|
|
323
|
+
(item.description && item.description.toLowerCase().includes(lowerQuery))
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async count(): Promise<number> {
|
|
328
|
+
return {{camelName}}Store.length
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async clear(): Promise<void> {
|
|
332
|
+
{{camelName}}Store = []
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
`
|
|
336
|
+
}
|
|
337
|
+
],
|
|
338
|
+
hooks: {
|
|
339
|
+
afterGenerate: async (context, options, files) => {
|
|
340
|
+
context.logger.info(`Generated service files:`)
|
|
341
|
+
files.forEach(file => {
|
|
342
|
+
context.logger.info(` - ${file}`)
|
|
343
|
+
})
|
|
344
|
+
context.logger.info(`\nNext steps:`)
|
|
345
|
+
context.logger.info(`1. Replace the in-memory repository with your database implementation`)
|
|
346
|
+
context.logger.info(`2. Add any additional business logic methods`)
|
|
347
|
+
context.logger.info(`3. Configure proper error handling and logging`)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private getMinimalTemplate(): Template {
|
|
354
|
+
return {
|
|
355
|
+
name: 'minimal-service',
|
|
356
|
+
description: 'Minimal service with basic structure',
|
|
357
|
+
files: [
|
|
358
|
+
{
|
|
359
|
+
path: 'app/server/services/{{kebabName}}.service.ts',
|
|
360
|
+
content: `export class {{pascalName}}Service {
|
|
361
|
+
async findAll() {
|
|
362
|
+
// TODO: Implement find all logic
|
|
363
|
+
return []
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async findById(id: string) {
|
|
367
|
+
// TODO: Implement find by id logic
|
|
368
|
+
return null
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async create(data: any) {
|
|
372
|
+
// TODO: Implement create logic
|
|
373
|
+
return data
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async update(id: string, data: any) {
|
|
377
|
+
// TODO: Implement update logic
|
|
378
|
+
return { id, ...data }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async delete(id: string) {
|
|
382
|
+
// TODO: Implement delete logic
|
|
383
|
+
return true
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
`
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private getRepositoryTemplate(): Template {
|
|
393
|
+
return {
|
|
394
|
+
name: 'repository-service',
|
|
395
|
+
description: 'Service with repository pattern',
|
|
396
|
+
files: [
|
|
397
|
+
{
|
|
398
|
+
path: 'app/server/services/{{kebabName}}.service.ts',
|
|
399
|
+
content: `import { {{pascalName}}Repository } from '../repositories/{{kebabName}}.repository'
|
|
400
|
+
|
|
401
|
+
export class {{pascalName}}Service {
|
|
402
|
+
private repository: {{pascalName}}Repository
|
|
403
|
+
|
|
404
|
+
constructor() {
|
|
405
|
+
this.repository = new {{pascalName}}Repository()
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async findAll() {
|
|
409
|
+
return await this.repository.findAll()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async findById(id: string) {
|
|
413
|
+
return await this.repository.findById(id)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async create(data: any) {
|
|
417
|
+
// Add business logic here
|
|
418
|
+
return await this.repository.create(data)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async update(id: string, data: any) {
|
|
422
|
+
// Add business logic here
|
|
423
|
+
return await this.repository.update(id, data)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async delete(id: string) {
|
|
427
|
+
return await this.repository.delete(id)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
`
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
path: 'app/server/repositories/{{kebabName}}.repository.ts',
|
|
434
|
+
content: `export class {{pascalName}}Repository {
|
|
435
|
+
async findAll() {
|
|
436
|
+
// TODO: Implement database query
|
|
437
|
+
return []
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async findById(id: string) {
|
|
441
|
+
// TODO: Implement database query
|
|
442
|
+
return null
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async create(data: any) {
|
|
446
|
+
// TODO: Implement database insert
|
|
447
|
+
return data
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async update(id: string, data: any) {
|
|
451
|
+
// TODO: Implement database update
|
|
452
|
+
return { id, ...data }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async delete(id: string) {
|
|
456
|
+
// TODO: Implement database delete
|
|
457
|
+
return true
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
`
|
|
461
|
+
}
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { GeneratorContext, GeneratorOptions, Template, TemplateFile, GeneratedFile, TemplateProcessor } from "./types.js"
|
|
2
|
+
import { join, dirname } from "path"
|
|
3
|
+
import { mkdir, writeFile, readFile, stat } from "fs/promises"
|
|
4
|
+
import { existsSync } from "fs"
|
|
5
|
+
|
|
6
|
+
export class TemplateEngine {
|
|
7
|
+
private processor: TemplateProcessor
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.processor = this.createTemplateProcessor()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async processTemplate(
|
|
14
|
+
template: Template,
|
|
15
|
+
context: GeneratorContext,
|
|
16
|
+
options: GeneratorOptions
|
|
17
|
+
): Promise<GeneratedFile[]> {
|
|
18
|
+
const variables = await this.collectVariables(template, context, options)
|
|
19
|
+
const files: GeneratedFile[] = []
|
|
20
|
+
|
|
21
|
+
for (const templateFile of template.files) {
|
|
22
|
+
// Check condition if specified
|
|
23
|
+
if (templateFile.condition && !templateFile.condition(variables)) {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const processedPath = this.processor(templateFile.path, variables)
|
|
28
|
+
const processedContent = this.processor(templateFile.content, variables)
|
|
29
|
+
const fullPath = join(context.workingDir, processedPath)
|
|
30
|
+
|
|
31
|
+
const exists = existsSync(fullPath)
|
|
32
|
+
let action: 'create' | 'overwrite' | 'skip' = 'create'
|
|
33
|
+
|
|
34
|
+
if (exists) {
|
|
35
|
+
if (options.force) {
|
|
36
|
+
action = 'overwrite'
|
|
37
|
+
} else {
|
|
38
|
+
action = 'skip'
|
|
39
|
+
context.logger.warn(`File already exists: ${processedPath}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
files.push({
|
|
44
|
+
path: fullPath,
|
|
45
|
+
content: processedContent,
|
|
46
|
+
exists,
|
|
47
|
+
action
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return files
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async generateFiles(files: GeneratedFile[], dryRun: boolean = false): Promise<void> {
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
if (file.action === 'skip') {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (dryRun) {
|
|
61
|
+
console.log(`${file.action === 'create' ? '📄' : '✏️'} ${file.path}`)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Ensure directory exists
|
|
66
|
+
const dir = dirname(file.path)
|
|
67
|
+
await mkdir(dir, { recursive: true })
|
|
68
|
+
|
|
69
|
+
// Write file
|
|
70
|
+
await writeFile(file.path, file.content, 'utf-8')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async collectVariables(
|
|
75
|
+
template: Template,
|
|
76
|
+
context: GeneratorContext,
|
|
77
|
+
options: GeneratorOptions
|
|
78
|
+
): Promise<Record<string, any>> {
|
|
79
|
+
const variables: Record<string, any> = {
|
|
80
|
+
// Built-in variables
|
|
81
|
+
name: options.name,
|
|
82
|
+
Name: this.capitalize(options.name),
|
|
83
|
+
NAME: options.name.toUpperCase(),
|
|
84
|
+
kebabName: this.toKebabCase(options.name),
|
|
85
|
+
camelName: this.toCamelCase(options.name),
|
|
86
|
+
pascalName: this.toPascalCase(options.name),
|
|
87
|
+
snakeName: this.toSnakeCase(options.name),
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
date: new Date().toLocaleDateString(),
|
|
90
|
+
year: new Date().getFullYear(),
|
|
91
|
+
author: 'FluxStack Developer',
|
|
92
|
+
projectName: context.config.app?.name || 'fluxstack-app',
|
|
93
|
+
...options
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Process template-specific variables
|
|
97
|
+
if (template.variables) {
|
|
98
|
+
for (const variable of template.variables) {
|
|
99
|
+
if (!(variable.name in variables)) {
|
|
100
|
+
if (variable.required && !variable.default) {
|
|
101
|
+
// In a real implementation, you'd use a proper prompt library
|
|
102
|
+
// For now, we'll use the default or throw an error
|
|
103
|
+
if (variable.default !== undefined) {
|
|
104
|
+
variables[variable.name] = variable.default
|
|
105
|
+
} else {
|
|
106
|
+
throw new Error(`Required variable '${variable.name}' not provided`)
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
variables[variable.name] = variable.default
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return variables
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private createTemplateProcessor(): TemplateProcessor {
|
|
119
|
+
return (template: string, variables: Record<string, any>) => {
|
|
120
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
121
|
+
return variables[key] !== undefined ? String(variables[key]) : match
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// String transformation utilities
|
|
127
|
+
private capitalize(str: string): string {
|
|
128
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private toCamelCase(str: string): string {
|
|
132
|
+
return str.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : '')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private toPascalCase(str: string): string {
|
|
136
|
+
return this.capitalize(this.toCamelCase(str))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private toKebabCase(str: string): string {
|
|
140
|
+
return str
|
|
141
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
142
|
+
.replace(/[\s_]+/g, '-')
|
|
143
|
+
.toLowerCase()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private toSnakeCase(str: string): string {
|
|
147
|
+
return str
|
|
148
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
149
|
+
.replace(/[\s-]+/g, '_')
|
|
150
|
+
.toLowerCase()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const templateEngine = new TemplateEngine()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { FluxStackConfig } from "../../config/schema"
|
|
2
|
+
import type { Logger } from "../../utils/logger/index"
|
|
3
|
+
import type { PluginUtils } from "../../plugins/types"
|
|
4
|
+
|
|
5
|
+
export interface GeneratorContext {
|
|
6
|
+
workingDir: string
|
|
7
|
+
config: FluxStackConfig
|
|
8
|
+
logger: Logger
|
|
9
|
+
utils: PluginUtils
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GeneratorOptions {
|
|
13
|
+
name: string
|
|
14
|
+
path?: string
|
|
15
|
+
template?: string
|
|
16
|
+
force?: boolean
|
|
17
|
+
dryRun?: boolean
|
|
18
|
+
[key: string]: any
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TemplateVariable {
|
|
22
|
+
name: string
|
|
23
|
+
description: string
|
|
24
|
+
type: 'string' | 'boolean' | 'choice'
|
|
25
|
+
required?: boolean
|
|
26
|
+
default?: any
|
|
27
|
+
choices?: string[]
|
|
28
|
+
prompt?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Template {
|
|
32
|
+
name: string
|
|
33
|
+
description: string
|
|
34
|
+
files: TemplateFile[]
|
|
35
|
+
variables?: TemplateVariable[]
|
|
36
|
+
hooks?: {
|
|
37
|
+
beforeGenerate?: (context: GeneratorContext, options: GeneratorOptions) => Promise<void>
|
|
38
|
+
afterGenerate?: (context: GeneratorContext, options: GeneratorOptions, files: string[]) => Promise<void>
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TemplateFile {
|
|
43
|
+
path: string
|
|
44
|
+
content: string
|
|
45
|
+
condition?: (variables: Record<string, any>) => boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface GeneratedFile {
|
|
49
|
+
path: string
|
|
50
|
+
content: string
|
|
51
|
+
exists: boolean
|
|
52
|
+
action: 'create' | 'overwrite' | 'skip'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface GenerationResult {
|
|
56
|
+
success: boolean
|
|
57
|
+
files: GeneratedFile[]
|
|
58
|
+
errors?: string[]
|
|
59
|
+
warnings?: string[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Utility types for template processing
|
|
63
|
+
export type TemplateProcessor = (template: string, variables: Record<string, any>) => string
|
|
64
|
+
|
|
65
|
+
export interface PromptConfig {
|
|
66
|
+
type: 'input' | 'confirm' | 'select' | 'multiselect'
|
|
67
|
+
message: string
|
|
68
|
+
default?: any
|
|
69
|
+
choices?: Array<{ name: string; value: any; description?: string }>
|
|
70
|
+
validate?: (value: any) => boolean | string
|
|
71
|
+
}
|