vstruct-cli 0.1.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.
@@ -0,0 +1,188 @@
1
+ // ---------------------------------------------------------------------------
2
+ // vstruct new — scaffold completo de proyecto Vue 3 predecible
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import * as fs from 'node:fs'
6
+ import * as path from 'node:path'
7
+ import {
8
+ VSTRUCT_MANIFEST,
9
+ PACKAGE_JSON,
10
+ VITE_CONFIG,
11
+ TSCONFIG,
12
+ INDEX_HTML,
13
+ MAIN_TS,
14
+ APP_VUE,
15
+ MAIN_CSS,
16
+ ROUTER_INDEX,
17
+ GITIGNORE,
18
+ AGENTS_MD,
19
+ CLAUDE_MD,
20
+ README_MD,
21
+ AIREF_MD,
22
+ } from '../templates/index.js'
23
+
24
+ export function newProject(projectName, options = {}) {
25
+ const cwd = options.cwd ?? process.cwd()
26
+ const projectDir = path.join(cwd, projectName)
27
+ const dryRun = options.dryRun ?? false
28
+
29
+ const files = [
30
+ // Root config files
31
+ { rel: 'vstruct.json', content: VSTRUCT_MANIFEST(projectName) },
32
+ { rel: 'env.d.ts', content: `/// <reference types="vite/client" />
33
+
34
+ declare module '*.vue' {
35
+ import type { DefineComponent } from 'vue'
36
+ const component: DefineComponent<{}, {}, any>
37
+ export default component
38
+ }
39
+ ` },
40
+ { rel: 'package.json', content: PACKAGE_JSON(projectName) },
41
+ { rel: 'vite.config.ts', content: VITE_CONFIG },
42
+ { rel: 'tsconfig.json', content: TSCONFIG },
43
+ { rel: 'index.html', content: INDEX_HTML(projectName) },
44
+ { rel: '.gitignore', content: GITIGNORE },
45
+ { rel: 'AGENTS.md', content: AGENTS_MD(projectName) },
46
+ { rel: 'CLAUDE.md', content: CLAUDE_MD(projectName) },
47
+ { rel: 'AIREF.md', content: AIREF_MD(projectName) },
48
+ { rel: 'README.md', content: README_MD(projectName) },
49
+
50
+ // src/
51
+ { rel: 'src/main.ts', content: MAIN_TS },
52
+ { rel: 'src/App.vue', content: APP_VUE },
53
+
54
+ // styles
55
+ { rel: 'src/styles/main.css', content: MAIN_CSS },
56
+
57
+ // router
58
+ { rel: 'src/router/index.ts', content: ROUTER_INDEX },
59
+
60
+ // pages — starter pages
61
+ { rel: 'src/pages/index.vue', content: `<template>
62
+ <div class="page-home">
63
+ <h1>Bienvenido a ${projectName}</h1>
64
+ <p>Editá <code>src/pages/index.vue</code> para empezar.</p>
65
+ </div>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ // Home page
70
+ </script>
71
+ ` },
72
+
73
+ // layouts — default layout
74
+ { rel: 'src/layouts/DefaultLayout.vue', content: `<template>
75
+ <div class="layout-default">
76
+ <header class="layout-header">
77
+ <nav>
78
+ <router-link to="/">${projectName}</router-link>
79
+ </nav>
80
+ </header>
81
+ <main class="layout-main">
82
+ <slot />
83
+ </main>
84
+ </div>
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ // DefaultLayout
89
+ </script>
90
+
91
+ <style scoped>
92
+ .layout-default {
93
+ min-height: 100dvh;
94
+ display: flex;
95
+ flex-direction: column;
96
+ }
97
+ .layout-header {
98
+ padding: 1rem 2rem;
99
+ border-bottom: 1px solid #e5e5e5;
100
+ }
101
+ .layout-header a {
102
+ text-decoration: none;
103
+ font-weight: 600;
104
+ color: #1a1a1a;
105
+ }
106
+ .layout-main {
107
+ flex: 1;
108
+ padding: 2rem;
109
+ }
110
+ </style>
111
+ ` },
112
+
113
+ // components — starter
114
+ { rel: 'src/components/features/.gitkeep', content: '' },
115
+ { rel: 'src/components/ui/.gitkeep', content: '' },
116
+
117
+ // stores — starter
118
+ { rel: 'src/stores/app.store.ts', content: `import { defineStore } from 'pinia'
119
+ import { ref } from 'vue'
120
+
121
+ export const useAppStore = defineStore('app', () => {
122
+ const ready = ref(false)
123
+
124
+ function init() {
125
+ ready.value = true
126
+ }
127
+
128
+ return { ready, init }
129
+ })
130
+ ` },
131
+
132
+ // services — starter
133
+ { rel: 'src/services/.gitkeep', content: '' },
134
+
135
+ // composables — starter
136
+ { rel: 'src/composables/.gitkeep', content: '' },
137
+
138
+ // types
139
+ { rel: 'src/types/index.ts', content: `// Tipos compartidos del proyecto
140
+
141
+ export interface User {
142
+ id: string
143
+ email: string
144
+ nombre: string
145
+ rol?: string
146
+ }
147
+
148
+ export interface ApiEnvelope<T> {
149
+ success: boolean
150
+ data: T
151
+ message?: string
152
+ }
153
+ ` },
154
+
155
+ // shared
156
+ { rel: 'src/shared/.gitkeep', content: '' },
157
+
158
+ // public
159
+ { rel: 'public/.gitkeep', content: '' },
160
+ ]
161
+
162
+ if (!dryRun) {
163
+ if (fs.existsSync(projectDir)) {
164
+ throw new NewProjectError(`El directorio "${projectDir}" ya existe.`)
165
+ }
166
+
167
+ for (const f of files) {
168
+ const fullPath = path.join(projectDir, f.rel)
169
+ const dir = path.dirname(fullPath)
170
+ if (!fs.existsSync(dir)) {
171
+ fs.mkdirSync(dir, { recursive: true })
172
+ }
173
+ fs.writeFileSync(fullPath, f.content, 'utf-8')
174
+ }
175
+ }
176
+
177
+ return {
178
+ projectDir,
179
+ files: files.map((f) => ({ path: path.join(projectDir, f.rel), content: f.content })),
180
+ }
181
+ }
182
+
183
+ export class NewProjectError extends Error {
184
+ constructor(message) {
185
+ super(message)
186
+ this.name = 'NewProjectError'
187
+ }
188
+ }
@@ -0,0 +1,521 @@
1
+ // ---------------------------------------------------------------------------
2
+ // vstruct Templates — archivos generados por `vstruct new` y `vstruct g`
3
+ // ---------------------------------------------------------------------------
4
+
5
+ // ===========================================================================
6
+ // HELPERS
7
+ // ===========================================================================
8
+
9
+ function capitalize(str) {
10
+ if (!str) return str
11
+ return str.charAt(0).toUpperCase() + str.slice(1)
12
+ }
13
+
14
+ function toPascalCase(str) {
15
+ return str.split(/[-_\s]+/).map(capitalize).join('')
16
+ }
17
+
18
+ function pascalToKebab(str) {
19
+ return str
20
+ .replace(/([A-Z])/g, (c, i) => (i === 0 ? c.toLowerCase() : '-' + c.toLowerCase()))
21
+ .replace(/^-/, '')
22
+ }
23
+
24
+ // ===========================================================================
25
+ // GENERATE TEMPLATES
26
+ // ===========================================================================
27
+
28
+ export function componentTemplate(name) {
29
+ const cssClass = pascalToKebab(name)
30
+ return `<template>
31
+ <div class="${cssClass}">
32
+ {{ msg }}
33
+ </div>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import { ref } from 'vue'
38
+
39
+ interface Props {
40
+ // props here
41
+ }
42
+
43
+ const props = defineProps<Props>()
44
+ const emit = defineEmits<{
45
+ // events here
46
+ }>()
47
+
48
+ const msg = ref('${name}')
49
+ </script>
50
+
51
+ <style scoped>
52
+ .${cssClass} {
53
+ /* styles */
54
+ }
55
+ </style>
56
+ `
57
+ }
58
+
59
+ export function pageTemplate(name) {
60
+ const componentName = toPascalCase(name) + 'Page'
61
+ return `<template>
62
+ <div class="page-${name}">
63
+ <h1>${componentName}</h1>
64
+ </div>
65
+ </template>
66
+
67
+ <script setup lang="ts">
68
+ // ${componentName} — page component
69
+ </script>
70
+ `
71
+ }
72
+
73
+ export function storeTemplate(name) {
74
+ const storeName = 'use' + capitalize(name) + 'Store'
75
+ return `import { defineStore } from 'pinia'
76
+ import { ref, computed } from 'vue'
77
+
78
+ export const ${storeName} = defineStore('${name}', () => {
79
+ // state
80
+ const loading = ref(false)
81
+
82
+ // getters
83
+ const isLoading = computed(() => loading.value)
84
+
85
+ // actions
86
+ function init() {
87
+ loading.value = true
88
+ // ...
89
+ loading.value = false
90
+ }
91
+
92
+ return { loading, isLoading, init }
93
+ })
94
+ `
95
+ }
96
+
97
+ export function serviceTemplate(name) {
98
+ const serviceName = capitalize(name) + 'Service'
99
+ const urlBase = pascalToKebab(name) + 's'
100
+ return `const BASE_URL = '/api/${urlBase}'
101
+
102
+ export const ${serviceName} = {
103
+ async getAll() {
104
+ const res = await fetch(BASE_URL)
105
+ if (!res.ok) throw new Error('Failed to fetch')
106
+ return res.json()
107
+ },
108
+
109
+ async getById(id: string) {
110
+ const res = await fetch(\`\${BASE_URL}/\${id}\`)
111
+ if (!res.ok) throw new Error('Failed to fetch')
112
+ return res.json()
113
+ },
114
+
115
+ async create(data: unknown) {
116
+ const res = await fetch(BASE_URL, {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/json' },
119
+ body: JSON.stringify(data),
120
+ })
121
+ if (!res.ok) throw new Error('Failed to create')
122
+ return res.json()
123
+ },
124
+ }
125
+ `
126
+ }
127
+
128
+ export function composableTemplate(name) {
129
+ const fnName = name.startsWith('use') ? name : 'use' + capitalize(name)
130
+ return `import { ref } from 'vue'
131
+
132
+ export function ${fnName}() {
133
+ const loading = ref(false)
134
+ const error = ref<string | null>(null)
135
+
136
+ function reset() {
137
+ loading.value = false
138
+ error.value = null
139
+ }
140
+
141
+ return { loading, error, reset }
142
+ }
143
+ `
144
+ }
145
+
146
+ export function layoutTemplate(name) {
147
+ const componentName = toPascalCase(name) + 'Layout'
148
+ return `<template>
149
+ <div class="layout-${name}">
150
+ <header class="layout-header">
151
+ <nav>
152
+ <router-link to="/">Home</router-link>
153
+ </nav>
154
+ </header>
155
+ <main class="layout-main">
156
+ <slot />
157
+ </main>
158
+ <footer class="layout-footer">
159
+ <p>&copy; {{ new Date().getFullYear() }}</p>
160
+ </footer>
161
+ </div>
162
+ </template>
163
+
164
+ <script setup lang="ts">
165
+ // ${componentName}
166
+ </script>
167
+ `
168
+ }
169
+
170
+ // ===========================================================================
171
+ // PROJECT SCAFFOLD TEMPLATES
172
+ // ===========================================================================
173
+
174
+ export const VSTRUCT_MANIFEST = (projectName) => `{
175
+ "version": "1",
176
+ "project": {
177
+ "name": "${projectName}",
178
+ "description": ""
179
+ },
180
+ "layers": {
181
+ "ui": "src/components/ui/*.vue",
182
+ "features": "src/components/features/*.vue",
183
+ "pages": "src/pages/**/*.vue",
184
+ "layouts": "src/layouts/*.vue",
185
+ "stores": "src/stores/*.store.ts",
186
+ "services": "src/services/*.service.ts",
187
+ "composables": "src/composables/*.ts"
188
+ },
189
+ "conventions": {
190
+ "naming": {
191
+ "components": "PascalCase.vue",
192
+ "pages": "kebab-case.vue",
193
+ "stores": "camelCase.store.ts",
194
+ "services": "PascalCase.service.ts",
195
+ "composables": "useXxx.ts",
196
+ "layouts": "PascalCase.vue"
197
+ },
198
+ "maxComponentLines": 300,
199
+ "maxProps": 12,
200
+ "maxNesting": 6
201
+ },
202
+ "forbidden": [],
203
+ "required": {
204
+ "asyncButtons": false,
205
+ "errorHandling": true,
206
+ "imgAlt": true
207
+ }
208
+ }
209
+ `
210
+
211
+ export const PACKAGE_JSON = (name) => `{
212
+ "name": "${name}",
213
+ "version": "0.0.1",
214
+ "type": "module",
215
+ "scripts": {
216
+ "dev": "vite",
217
+ "build": "vue-tsc && vite build",
218
+ "preview": "vite preview"
219
+ },
220
+ "dependencies": {
221
+ "pinia": "^2.1.0",
222
+ "vue": "^3.4.0",
223
+ "vue-router": "^4.3.0"
224
+ },
225
+ "devDependencies": {
226
+ "@vitejs/plugin-vue": "^5.0.0",
227
+ "typescript": "^5.4.0",
228
+ "vite": "^5.4.0",
229
+ "vue-tsc": "^2.0.0"
230
+ }
231
+ }
232
+ `
233
+
234
+ export const VITE_CONFIG = `import { defineConfig } from 'vite'
235
+ import vue from '@vitejs/plugin-vue'
236
+ import { resolve } from 'path'
237
+
238
+ export default defineConfig({
239
+ plugins: [vue()],
240
+ resolve: {
241
+ alias: {
242
+ '@': resolve(__dirname, 'src'),
243
+ },
244
+ },
245
+ })
246
+ `
247
+
248
+ export const TSCONFIG = `{
249
+ "compilerOptions": {
250
+ "target": "ESNext",
251
+ "module": "ESNext",
252
+ "moduleResolution": "bundler",
253
+ "strict": true,
254
+ "jsx": "preserve",
255
+ "lib": ["ESNext", "DOM"],
256
+ "skipLibCheck": true,
257
+ "baseUrl": ".",
258
+ "paths": {
259
+ "@/*": ["src/*"]
260
+ }
261
+ },
262
+ "include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts"],
263
+ "exclude": ["node_modules", "dist"]
264
+ }
265
+ `
266
+
267
+ export const INDEX_HTML = (name) => `<!DOCTYPE html>
268
+ <html lang="es">
269
+ <head>
270
+ <meta charset="UTF-8" />
271
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
272
+ <title>${name}</title>
273
+ </head>
274
+ <body>
275
+ <div id="app"></div>
276
+ <script type="module" src="/src/main.ts"></script>
277
+ </body>
278
+ </html>
279
+ `
280
+
281
+ export const MAIN_TS = `import { createApp } from 'vue'
282
+ import { createPinia } from 'pinia'
283
+ import App from './App.vue'
284
+ import router from './router'
285
+ import './styles/main.css'
286
+
287
+ const app = createApp(App)
288
+ app.use(createPinia())
289
+ app.use(router)
290
+ app.mount('#app')
291
+ `
292
+
293
+ export const APP_VUE = `<template>
294
+ <router-view />
295
+ </template>
296
+
297
+ <script setup lang="ts">
298
+ // App root component
299
+ </script>
300
+ `
301
+
302
+ export const MAIN_CSS = `*,
303
+ *::before,
304
+ *::after {
305
+ box-sizing: border-box;
306
+ margin: 0;
307
+ padding: 0;
308
+ }
309
+
310
+ html {
311
+ font-family: system-ui, -apple-system, sans-serif;
312
+ -webkit-font-smoothing: antialiased;
313
+ }
314
+
315
+ body {
316
+ min-height: 100dvh;
317
+ color: #1a1a1a;
318
+ background: #ffffff;
319
+ }
320
+ `
321
+
322
+ export const ROUTER_INDEX = `import { createRouter, createWebHistory } from 'vue-router'
323
+
324
+ const router = createRouter({
325
+ history: createWebHistory(),
326
+ routes: [
327
+ {
328
+ path: '/',
329
+ name: 'home',
330
+ component: () => import('@/pages/index.vue'),
331
+ },
332
+ ],
333
+ })
334
+
335
+ export default router
336
+ `
337
+
338
+ export const GITIGNORE = `node_modules/
339
+ dist/
340
+ .env
341
+ .env.local
342
+ *.log
343
+ `
344
+
345
+ export const AGENTS_MD = (name) => `# ${name}
346
+
347
+ Framework: **Vue 3** + **Vite** + **TypeScript** + **Pinia** + **Vue Router**.
348
+
349
+ ## ⚠️ LECTURA OBLIGATORIA — antes de generar código
350
+
351
+ 1. **Leer \`vstruct.json\`** — reglas exactas del proyecto (naming, layers, límites). OBLIGATORIO.
352
+ 2. **Leer \`AIREF.md\`** — tabla canónica, patrones, buenas/malas prácticas.
353
+
354
+ ## TABLA CANÓNICA — dónde va cada cosa
355
+
356
+ | Tipo | Ubicación | Naming |
357
+ |------|-----------|--------|
358
+ | Componente UI | \`src/components/ui/\` | \`PascalCase.vue\` |
359
+ | Componente Feature | \`src/components/features/\` | \`PascalCase.vue\` |
360
+ | Página | \`src/pages/\` | \`kebab-case.vue\` |
361
+ | Layout | \`src/layouts/\` | \`PascalCase.vue\` |
362
+ | Store (Pinia) | \`src/stores/\` | \`camelCase.store.ts\` |
363
+ | Service (API) | \`src/services/\` | \`PascalCase.service.ts\` |
364
+ | Composable | \`src/composables/\` | \`useXxx.ts\` |
365
+ | Tipos compartidos | \`src/types/index.ts\` | — |
366
+
367
+ ## REGLAS INMUTABLES
368
+
369
+ | # | Regla | Por qué |
370
+ |---|-------|---------|
371
+ | 1 | Componentes en \`components/\`, NO en \`pages/\` ni \`layouts/\` | Separación de concerns |
372
+ | 2 | NUNCA \`fetch()\` directo en componentes → \`services/\` | Testabilidad, caching |
373
+ | 3 | NUNCA \`<a href>\` para rutas → \`<router-link>\` o \`router.push()\` | Recarga la SPA |
374
+ | 4 | NUNCA \`<script>\` sin \`lang="ts"\` | TypeScript obligatorio |
375
+ | 5 | NUNCA \`<style>\` sin \`scoped\` en componentes | Contamina otros componentes |
376
+ | 6 | Store NO llama a router | Dependencia circular |
377
+ | 7 | Service NO importa stores | Services son puros HTTP |
378
+ | 8 | Tipos en \`types/index.ts\`, NO duplicados | Fuente única de verdad |
379
+
380
+ ## ANTIPATRONES — NUNCA hacer
381
+
382
+ \`\`\`
383
+ ❌ fetch() en <script setup> de .vue → Crear X.service.ts
384
+ ❌ <a href="/ruta"> → <router-link to="/ruta">
385
+ ❌ <style> sin scoped → <style scoped>
386
+ ❌ import { useRouter } en store → Componente hace router.push()
387
+ ❌ interface User en 3 archivos → types/index.ts
388
+ ❌ pages/UserList.vue → pages/user-list.vue (kebab-case)
389
+ ❌ services/product.ts → Product.service.ts
390
+ ❌ composables/fetch.ts → useFetch.ts
391
+ \`\`\`
392
+
393
+ ## VALIDACIÓN
394
+
395
+ \`\`\`bash
396
+ vstruct analyze # 0 errores = listo. NUNCA declarar "terminado" sin 0 errores.
397
+ \`\`\`
398
+
399
+ ## CLI rápida
400
+
401
+ \`\`\`bash
402
+ vstruct g c UserCard # componente
403
+ vstruct g p user-list # página
404
+ vstruct g s auth # store
405
+ vstruct g sv Product # service
406
+ vstruct g co useFetch # composable
407
+ vstruct g l Admin # layout
408
+ \`\`\`
409
+ `
410
+
411
+ export const README_MD = (name) => `# ${name}
412
+
413
+ Creado con [vstruct](https://www.npmjs.com/package/vstruct) — estructura Vue 3 predecible.
414
+
415
+ \`\`\`bash
416
+ npm install
417
+ npm run dev
418
+ \`\`\`
419
+
420
+ Abre [http://localhost:5173](http://localhost:5173).
421
+
422
+ ## Estructura
423
+
424
+ \`\`\`
425
+ src/
426
+ components/
427
+ ui/ ← componentes base (Button, Input, Card...)
428
+ features/ ← componentes de dominio
429
+ pages/ ← rutas (file-based por convención)
430
+ layouts/ ← layouts con <slot>
431
+ stores/ ← estado global (Pinia)
432
+ services/ ← llamadas API
433
+ composables/ ← lógica reutilizable (useXxx)
434
+ types/ ← tipos TypeScript compartidos
435
+ router/
436
+ index.ts ← configuración de Vue Router
437
+ styles/
438
+ main.css ← estilos globales
439
+ App.vue
440
+ main.ts
441
+ \`\`\`
442
+
443
+ ## CLI
444
+
445
+ | Comando | Descripción |
446
+ |---------|-------------|
447
+ | \`npx vstruct g c <Name>\` | Generar componente |
448
+ | \`npx vstruct g p <name>\` | Generar página |
449
+ | \`npx vstruct g s <name>\` | Generar store (Pinia) |
450
+ | \`npx vstruct g sv <Name>\` | Generar service (API) |
451
+ | \`npx vstruct g co <name>\` | Generar composable |
452
+ | \`npx vstruct g l <name>\` | Generar layout |
453
+ | \`npx vstruct analyze\` | Validar estructura del proyecto |
454
+ `
455
+
456
+ export const CLAUDE_MD = AGENTS_MD
457
+
458
+ export const AIREF_MD = () => `# AIREF — Quick Reference
459
+
460
+ Estructura Vue 3 predecible. Todo tiene UN lugar. Cero ambigüedad.
461
+
462
+ ## Tabla canónica
463
+
464
+ | Tipo | Ubicación | Naming | Ejemplo |
465
+ |------|-----------|--------|---------|
466
+ | Componente UI | \`src/components/ui/\` | \`PascalCase.vue\` | \`Button.vue\` |
467
+ | Componente Feature | \`src/components/features/\` | \`PascalCase.vue\` | \`UserCard.vue\` |
468
+ | Página | \`src/pages/\` | \`kebab-case.vue\` | \`user-list.vue\` |
469
+ | Layout | \`src/layouts/\` | \`PascalCase.vue\` | \`AdminLayout.vue\` |
470
+ | Store | \`src/stores/\` | \`camelCase.store.ts\` | \`cart.store.ts\` |
471
+ | Service | \`src/services/\` | \`PascalCase.service.ts\` | \`Product.service.ts\` |
472
+ | Composable | \`src/composables/\` | \`useXxx.ts\` | \`useFetch.ts\` |
473
+ | Tipos | \`src/types/\` | \`index.ts\` | — |
474
+
475
+ ## Reglas inmutables
476
+
477
+ 1. NUNCA inventar ubicaciones — usar la tabla
478
+ 2. NUNCA \`fetch()\` directo en componentes → \`services/\`
479
+ 3. NUNCA \`<a href>\` para rutas → \`<router-link>\` o \`router.push()\`
480
+ 4. NUNCA \`<script>\` sin \`lang="ts"\`
481
+ 5. NUNCA \`<style>\` sin \`scoped\` en componentes
482
+ 6. Store NO llama a router
483
+ 7. Service NO importa stores
484
+
485
+ ## Buenas vs Malas
486
+
487
+ | ✅ BIEN | ❌ MAL |
488
+ |---------|--------|
489
+ | \`<router-link to="/">\` | \`<a href="/">\` |
490
+ | \`<script setup lang="ts">\` | \`<script>\` |
491
+ | \`<style scoped>\` | \`<style>\` |
492
+ | \`defineStore('x', () => {})\` | \`defineStore('x', { state })\` |
493
+ | \`if (!res.ok) throw Error\` | \`return res.json()\` sin check |
494
+ | \`types/index.ts\` centralizado | Tipos duplicados |
495
+ | \`useFetch\` (con use) | \`fetchData\` (sin use) |
496
+ | \`stores/auth.store.ts\` | \`store/auth.ts\` |
497
+ | \`services/Product.service.ts\` | \`services/product.ts\` |
498
+ | \`pages/user-list.vue\` | \`pages/UserList.vue\` |
499
+
500
+ ## Antipatrones que rompen la app
501
+
502
+ \`\`\`
503
+ ❌ fetch() en .vue → crear X.service.ts
504
+ ❌ <a href="/ruta"> → <router-link to="/ruta">
505
+ ❌ import { useRouter } en store → componente hace router.push()
506
+ ❌ interface User en 3 archivos → types/index.ts
507
+ ❌ <style> sin scoped → <style scoped>
508
+ \`\`\`
509
+
510
+ ## CLI
511
+
512
+ \`\`\`bash
513
+ vstruct g c UserCard # src/components/features/UserCard.vue
514
+ vstruct g p user-list # src/pages/user-list.vue
515
+ vstruct g s cart # src/stores/cart.store.ts
516
+ vstruct g sv Product # src/services/Product.service.ts
517
+ vstruct g co useFetch # src/composables/useFetch.ts
518
+ vstruct g l Admin # src/layouts/Admin.vue
519
+ vstruct analyze # 0 errores = listo
520
+ \`\`\`
521
+ `