arckode-framework 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.
Files changed (52) hide show
  1. package/README.md +546 -0
  2. package/adapters/__tests__/mysql.test.ts +283 -0
  3. package/adapters/jwt.ts +18 -0
  4. package/adapters/mysql.ts +98 -0
  5. package/adapters/postgres.ts +52 -0
  6. package/adapters/redis-cache.ts +64 -0
  7. package/adapters/sqlite.ts +73 -0
  8. package/adapters/vendor.d.ts +48 -0
  9. package/bin/arckode.js +7 -0
  10. package/cli/analyze.ts +506 -0
  11. package/cli/commands/db-migrate.ts +121 -0
  12. package/cli/commands/db-seed.ts +54 -0
  13. package/cli/commands/generate-api-client.ts +106 -0
  14. package/cli/commands/make-adapter.ts +132 -0
  15. package/cli/commands/make-auth.ts +297 -0
  16. package/cli/commands/make-frontend-module.ts +271 -0
  17. package/cli/commands/make-helper.ts +65 -0
  18. package/cli/commands/make-migration.ts +30 -0
  19. package/cli/commands/make-seed.ts +29 -0
  20. package/cli/generate.ts +132 -0
  21. package/cli/index.ts +604 -0
  22. package/cli/stubs/frontend-stub.ts +294 -0
  23. package/cli/stubs/fullstack-stub.ts +46 -0
  24. package/cli/stubs/module-stub.ts +469 -0
  25. package/kernel/__tests__/adapters.test.ts +101 -0
  26. package/kernel/__tests__/analyzer.test.ts +282 -0
  27. package/kernel/__tests__/framework.test.ts +617 -0
  28. package/kernel/__tests__/middlewares.test.ts +174 -0
  29. package/kernel/__tests__/static.test.ts +94 -0
  30. package/kernel/framework.ts +1851 -0
  31. package/kernel/middlewares.ts +179 -0
  32. package/kernel/static.ts +76 -0
  33. package/kernel/testing.ts +237 -0
  34. package/modules/events/index.ts +99 -0
  35. package/modules/mail/index.ts +51 -0
  36. package/modules/mail/smtp-adapter.ts +42 -0
  37. package/modules/queue/index.ts +78 -0
  38. package/modules/storage/index.ts +40 -0
  39. package/modules/storage/local-adapter.ts +41 -0
  40. package/modules/ws/__tests__/ws.test.ts +114 -0
  41. package/modules/ws/index.ts +136 -0
  42. package/package.json +99 -0
  43. package/skills/auth/SKILL.md +243 -0
  44. package/skills/cli/SKILL.md +258 -0
  45. package/skills/config/SKILL.md +253 -0
  46. package/skills/connectors/SKILL.md +259 -0
  47. package/skills/helpers/SKILL.md +206 -0
  48. package/skills/middlewares/SKILL.md +282 -0
  49. package/skills/orm/SKILL.md +260 -0
  50. package/skills/realtime/SKILL.md +307 -0
  51. package/skills/services/SKILL.md +206 -0
  52. package/skills/testing/SKILL.md +257 -0
@@ -0,0 +1,294 @@
1
+ // cli/stubs/frontend-stub.ts — Stubs para módulos frontend (Vue 3 + Composition API + TypeScript)
2
+
3
+ export interface FrontendStubParams {
4
+ name: string // PascalCase: Producto
5
+ module: string // kebab-case: productos
6
+ }
7
+
8
+ // ─── API Client ─────────────────────────────
9
+ export function apiStub(p: FrontendStubParams): string {
10
+ return `// ${p.module}/api/${p.module}.api.ts
11
+ // SOLO acá se llama al backend. Los composables usan esto, nunca fetch directo.
12
+
13
+ const BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:3000'
14
+
15
+ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
16
+ const token = localStorage.getItem('token')
17
+ const res = await fetch(\`\${BASE_URL}\${path}\`, {
18
+ ...options,
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ ...(token ? { Authorization: \`Bearer \${token}\` } : {}),
22
+ ...options.headers,
23
+ },
24
+ })
25
+ if (!res.ok) {
26
+ const err = await res.json().catch(() => ({ error: res.statusText }))
27
+ throw new Error(err.error ?? 'Error de red')
28
+ }
29
+ return res.json()
30
+ }
31
+
32
+ export interface ${p.name}DTO {
33
+ id: string
34
+ nombre: string
35
+ activo: boolean
36
+ createdAt: string
37
+ }
38
+
39
+ export interface Create${p.name}DTO {
40
+ nombre: string
41
+ }
42
+
43
+ export const ${p.module}Api = {
44
+ list: (params?: Record<string, string>) =>
45
+ request<{ data: ${p.name}DTO[]; pagination: any }>(\`/${p.module}?\${new URLSearchParams(params)}\`),
46
+
47
+ getById: (id: string) =>
48
+ request<${p.name}DTO>(\`/${p.module}/\${id}\`),
49
+
50
+ create: (data: Create${p.name}DTO) =>
51
+ request<${p.name}DTO>('/${p.module}', { method: 'POST', body: JSON.stringify(data) }),
52
+
53
+ update: (id: string, data: Partial<Create${p.name}DTO>) =>
54
+ request<${p.name}DTO>(\`/${p.module}/\${id}\`, { method: 'PUT', body: JSON.stringify(data) }),
55
+
56
+ delete: (id: string) =>
57
+ request<void>(\`/${p.module}/\${id}\`, { method: 'DELETE' }),
58
+ }
59
+ `
60
+ }
61
+
62
+ // ─── Composable (hook) ─────────────────────
63
+ export function composableStub(p: FrontendStubParams): string {
64
+ return `// ${p.module}/composables/use${p.name}.ts
65
+ // Responsabilidad ÚNICA: conectar la UI con la API del módulo.
66
+
67
+ import { ref, computed } from 'vue'
68
+ import { ${p.module}Api, type ${p.name}DTO, type Create${p.name}DTO } from '../api/${p.module}.api'
69
+
70
+ export function use${p.name}() {
71
+ const items = ref<${p.name}DTO[]>([])
72
+ const loading = ref(false)
73
+ const error = ref<string | null>(null)
74
+
75
+ const hasItems = computed(() => items.value.length > 0)
76
+
77
+ async function list(params?: Record<string, string>) {
78
+ loading.value = true
79
+ error.value = null
80
+ try {
81
+ const result = await ${p.module}Api.list(params)
82
+ items.value = result.data
83
+ return result
84
+ } catch (e) {
85
+ error.value = (e as Error).message
86
+ throw e
87
+ } finally {
88
+ loading.value = false
89
+ }
90
+ }
91
+
92
+ async function create(data: Create${p.name}DTO) {
93
+ loading.value = true
94
+ try {
95
+ const item = await ${p.module}Api.create(data)
96
+ items.value.push(item)
97
+ return item
98
+ } catch (e) {
99
+ error.value = (e as Error).message
100
+ throw e
101
+ } finally {
102
+ loading.value = false
103
+ }
104
+ }
105
+
106
+ async function remove(id: string) {
107
+ loading.value = true
108
+ try {
109
+ await ${p.module}Api.delete(id)
110
+ items.value = items.value.filter(i => i.id !== id)
111
+ } catch (e) {
112
+ error.value = (e as Error).message
113
+ throw e
114
+ } finally {
115
+ loading.value = false
116
+ }
117
+ }
118
+
119
+ return { items, loading, error, hasItems, list, create, remove }
120
+ }
121
+ `
122
+ }
123
+
124
+ // ─── Componente Lista ──────────────────────
125
+ export function listComponentStub(p: FrontendStubParams): string {
126
+ return `<script setup lang="ts">
127
+ // ${p.module}/components/${p.name}List.vue
128
+ // Lista paginada con carga desde API
129
+
130
+ import { onMounted } from 'vue'
131
+ import { use${p.name} } from '../composables/use${p.name}'
132
+
133
+ const { items, loading, error, list, remove } = use${p.name}()
134
+
135
+ onMounted(() => list())
136
+
137
+ function confirmarEliminacion(id: string, nombre: string) {
138
+ if (confirm(\`¿Eliminar \${nombre}?\`)) {
139
+ remove(id)
140
+ }
141
+ }
142
+ </script>
143
+
144
+ <template>
145
+ <div class="${p.module}-list">
146
+ <div v-if="loading" class="loading">Cargando...</div>
147
+ <div v-else-if="error" class="error">{{ error }}</div>
148
+ <div v-else-if="!items.length" class="empty">No hay ${p.module} registrados</div>
149
+ <table v-else class="table">
150
+ <thead>
151
+ <tr>
152
+ <th>Nombre</th>
153
+ <th>Estado</th>
154
+ <th>Acciones</th>
155
+ </tr>
156
+ </thead>
157
+ <tbody>
158
+ <tr v-for="item in items" :key="item.id">
159
+ <td>{{ item.nombre }}</td>
160
+ <td>{{ item.activo ? 'Activo' : 'Inactivo' }}</td>
161
+ <td>
162
+ <button @click="confirmarEliminacion(item.id, item.nombre)" class="btn-danger">
163
+ Eliminar
164
+ </button>
165
+ </td>
166
+ </tr>
167
+ </tbody>
168
+ </table>
169
+ </div>
170
+ </template>
171
+
172
+ <style scoped>
173
+ .table { width: 100%; border-collapse: collapse; }
174
+ .table th, .table td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #ddd; }
175
+ .loading { padding: 20px; text-align: center; color: #666; }
176
+ .error { padding: 20px; color: #e74c3c; }
177
+ .empty { padding: 20px; color: #999; text-align: center; }
178
+ .btn-danger { background: #e74c3c; color: white; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; }
179
+ </style>
180
+ `
181
+ }
182
+
183
+ // ─── Componente Formulario ─────────────────
184
+ export function formComponentStub(p: FrontendStubParams): string {
185
+ return `<script setup lang="ts">
186
+ // ${p.module}/components/${p.name}Form.vue
187
+ // Formulario de creación/edición
188
+
189
+ import { ref } from 'vue'
190
+ import { use${p.name} } from '../composables/use${p.name}'
191
+
192
+ const props = defineProps<{ item?: { id: string; nombre: string } }>()
193
+ const emit = defineEmits<{ saved: []; canceled: [] }>()
194
+
195
+ const { create } = use${p.name}()
196
+ const nombre = ref(props.item?.nombre ?? '')
197
+ const submitting = ref(false)
198
+ const formError = ref<string | null>(null)
199
+
200
+ async function handleSubmit() {
201
+ if (!nombre.value.trim()) { formError.value = 'El nombre es requerido'; return }
202
+ submitting.value = true
203
+ formError.value = null
204
+ try {
205
+ await create({ nombre: nombre.value })
206
+ emit('saved')
207
+ } catch (e) {
208
+ formError.value = (e as Error).message
209
+ } finally {
210
+ submitting.value = false
211
+ }
212
+ }
213
+ </script>
214
+
215
+ <template>
216
+ <form @submit.prevent="handleSubmit" class="${p.module}-form">
217
+ <div v-if="formError" class="error">{{ formError }}</div>
218
+ <div class="field">
219
+ <label>Nombre</label>
220
+ <input v-model="nombre" type="text" required placeholder="Nombre del ${p.module}" />
221
+ </div>
222
+ <div class="actions">
223
+ <button type="submit" :disabled="submitting" class="btn-primary">
224
+ {{ submitting ? 'Guardando...' : 'Guardar' }}
225
+ </button>
226
+ <button type="button" @click="emit('canceled')" class="btn-secondary">Cancelar</button>
227
+ </div>
228
+ </form>
229
+ </template>
230
+
231
+ <style scoped>
232
+ .field { margin-bottom: 16px; }
233
+ .field label { display: block; margin-bottom: 4px; font-weight: 600; }
234
+ .field input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; }
235
+ .actions { display: flex; gap: 8px; }
236
+ .btn-primary { background: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
237
+ .btn-secondary { background: #95a5a6; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
238
+ .error { color: #e74c3c; margin-bottom: 12px; padding: 8px; background: #fdf0ef; border-radius: 4px; }
239
+ </style>
240
+ `
241
+ }
242
+
243
+ // ─── Página ─────────────────────────────────
244
+ export function pageStub(p: FrontendStubParams): string {
245
+ return `<script setup lang="ts">
246
+ // ${p.module}/pages/${p.name}Page.vue
247
+ // Página completa con lista + formulario
248
+
249
+ import { ref } from 'vue'
250
+ import ${p.name}List from '../components/${p.name}List.vue'
251
+ import ${p.name}Form from '../components/${p.name}Form.vue'
252
+
253
+ const showForm = ref(false)
254
+ </script>
255
+
256
+ <template>
257
+ <div class="${p.module}-page">
258
+ <div class="header">
259
+ <h1>${p.name}s</h1>
260
+ <button @click="showForm = !showForm" class="btn-primary">
261
+ {{ showForm ? 'Cancelar' : 'Nuevo ${p.name}' }}
262
+ </button>
263
+ </div>
264
+
265
+ <${p.name}Form v-if="showForm" @saved="showForm = false" @canceled="showForm = false" />
266
+
267
+ <${p.name}List />
268
+ </div>
269
+ </template>
270
+
271
+ <style scoped>
272
+ .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
273
+ .btn-primary { background: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
274
+ </style>
275
+ `
276
+ }
277
+
278
+ // ─── Router ─────────────────────────────────
279
+ export function routerStub(p: FrontendStubParams): string {
280
+ return `// ${p.module}/router/index.ts
281
+ // Rutas del módulo
282
+
283
+ import type { RouteRecordRaw } from 'vue-router'
284
+
285
+ export const ${p.module}Routes: RouteRecordRaw[] = [
286
+ {
287
+ path: '/${p.module}',
288
+ name: '${p.module}',
289
+ component: () => import('../pages/${p.name}Page.vue'),
290
+ meta: { requiresAuth: true },
291
+ },
292
+ ]
293
+ `
294
+ }
@@ -0,0 +1,46 @@
1
+ // cli/stubs/fullstack-stub.ts — Stub de módulo full-stack (backend + frontend juntos)
2
+ // Para la opción de monolito completo.
3
+
4
+ export interface FullstackStubParams {
5
+ name: string
6
+ module: string
7
+ fields: { name: string; type: string; required: boolean }[]
8
+ }
9
+
10
+ export function fullstackModuleStub(p: FullstackStubParams): string {
11
+ return `// ${p.module}/index.ts — Módulo full-stack
12
+ // Exporta backend + frontend. Mismas types compartidas.
13
+
14
+ export type { ${p.name}DTO, Create${p.name}DTO } from './types'
15
+ export type { ${p.name}Sockets } from './sockets'
16
+
17
+ // Backend
18
+ export { ${p.name}Service } from './backend/service'
19
+ export { ${p.name}Controller } from './backend/controller'
20
+
21
+ // Frontend
22
+ export { ${p.name}Api } from './frontend/api/${p.module}.api'
23
+ export { use${p.name} } from './frontend/composables/use${p.name}'
24
+ export { default as ${p.name}List } from './frontend/components/${p.name}List.vue'
25
+ export { default as ${p.name}Form } from './frontend/components/${p.name}Form.vue'
26
+ export { default as ${p.name}Page } from './frontend/pages/${p.name}Page.vue'
27
+ export { ${p.module}Routes } from './frontend/router'
28
+ `
29
+ }
30
+
31
+ export function fullstackTypesStub(p: FullstackStubParams): string {
32
+ return `// ${p.module}/types.ts — DTOs COMPARTIDOS entre backend y frontend
33
+ // Misma interfaz, mismo contrato. Un solo lugar para definirlos.
34
+
35
+ export interface ${p.name}DTO {
36
+ id: string
37
+ ${p.fields.map(f => ` ${f.name}${f.required ? '' : '?'}: ${f.type === 'number' ? 'number' : f.type === 'boolean' ? 'boolean' : 'string'}`).join('\n')}
38
+ createdAt: string
39
+ updatedAt: string
40
+ }
41
+
42
+ export interface Create${p.name}DTO {
43
+ ${p.fields.filter(f => f.name !== 'id').map(f => ` ${f.name}${f.required ? '' : '?'}: ${f.type === 'number' ? 'number' : f.type === 'boolean' ? 'boolean' : 'string'}`).join('\n')}
44
+ }
45
+ `
46
+ }