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.
- package/AIREF.md +479 -0
- package/package.json +28 -0
- package/skills/SKILL.md +69 -0
- package/src/cli.js +432 -0
- package/src/commands/generate.js +191 -0
- package/src/commands/new.js +188 -0
- package/src/templates/index.js +521 -0
|
@@ -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>© {{ 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
|
+
`
|