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.
- package/README.md +546 -0
- package/adapters/__tests__/mysql.test.ts +283 -0
- package/adapters/jwt.ts +18 -0
- package/adapters/mysql.ts +98 -0
- package/adapters/postgres.ts +52 -0
- package/adapters/redis-cache.ts +64 -0
- package/adapters/sqlite.ts +73 -0
- package/adapters/vendor.d.ts +48 -0
- package/bin/arckode.js +7 -0
- package/cli/analyze.ts +506 -0
- package/cli/commands/db-migrate.ts +121 -0
- package/cli/commands/db-seed.ts +54 -0
- package/cli/commands/generate-api-client.ts +106 -0
- package/cli/commands/make-adapter.ts +132 -0
- package/cli/commands/make-auth.ts +297 -0
- package/cli/commands/make-frontend-module.ts +271 -0
- package/cli/commands/make-helper.ts +65 -0
- package/cli/commands/make-migration.ts +30 -0
- package/cli/commands/make-seed.ts +29 -0
- package/cli/generate.ts +132 -0
- package/cli/index.ts +604 -0
- package/cli/stubs/frontend-stub.ts +294 -0
- package/cli/stubs/fullstack-stub.ts +46 -0
- package/cli/stubs/module-stub.ts +469 -0
- package/kernel/__tests__/adapters.test.ts +101 -0
- package/kernel/__tests__/analyzer.test.ts +282 -0
- package/kernel/__tests__/framework.test.ts +617 -0
- package/kernel/__tests__/middlewares.test.ts +174 -0
- package/kernel/__tests__/static.test.ts +94 -0
- package/kernel/framework.ts +1851 -0
- package/kernel/middlewares.ts +179 -0
- package/kernel/static.ts +76 -0
- package/kernel/testing.ts +237 -0
- package/modules/events/index.ts +99 -0
- package/modules/mail/index.ts +51 -0
- package/modules/mail/smtp-adapter.ts +42 -0
- package/modules/queue/index.ts +78 -0
- package/modules/storage/index.ts +40 -0
- package/modules/storage/local-adapter.ts +41 -0
- package/modules/ws/__tests__/ws.test.ts +114 -0
- package/modules/ws/index.ts +136 -0
- package/package.json +99 -0
- package/skills/auth/SKILL.md +243 -0
- package/skills/cli/SKILL.md +258 -0
- package/skills/config/SKILL.md +253 -0
- package/skills/connectors/SKILL.md +259 -0
- package/skills/helpers/SKILL.md +206 -0
- package/skills/middlewares/SKILL.md +282 -0
- package/skills/orm/SKILL.md +260 -0
- package/skills/realtime/SKILL.md +307 -0
- package/skills/services/SKILL.md +206 -0
- 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
|
+
}
|